diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f80436ab5..133ab69fd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -122,7 +122,10 @@ second with the Engines page's `TransferRate` and units, rewritten four times a private, carrier-grade NAT, link-local, unique local, multicast, `localhost` and `.local` names, IPv4 inside IPv6) is refused (`error.web_local_address`), on every redirect and after the name is looked up: the handler's `ConnectCallback` connects only to the checked addresses, so a name that changes its answer (DNS rebinding) gets - nowhere. A connection to the user's proxy isn't checked; the names and numbers in the address still are. + nowhere. A connection to the user's proxy isn't checked, including the CONNECT tunnel the handler opens to an + https site through a proxy on this computer (`127.0.0.1:7890`); the names and numbers in the address still are. + The proxy is Settings → General → Network (`ProxySetting`, which never sends local addresses through it) or the + system's. "Allow links to this computer and the local network" (Settings → General, `BrowserDropSettings.AllowLocalNetwork`, off by default, read for each drop) lifts this for an intranet or a NAS. Those downloads go through an `HttpClient` of their own, so a pooled connection to a local address never serves a drop that may not go there. diff --git a/docs/ENGINES.md b/docs/ENGINES.md index b945a9b7e..bf1537ce8 100644 --- a/docs/ENGINES.md +++ b/docs/ENGINES.md @@ -107,6 +107,12 @@ they can be installed or removed later in Settings → *Engines*. A conversion t SHA-256 before the first one is unpacked, so a missing or wrong file leaves the package as it was. Only the downloads are taken, never an installed engine folder: an unpacked and trimmed folder has nothing pinned to check it against. The other computer needs the same Filee version (the same engines.json). +- Downloads (engines, OCR languages, update checks, links dragged from a browser) go through the proxy in + Settings → *General* → *Network* when one is set (`AppSettings.Proxy`, `ProxySetting`): `http`, `https`, `socks4`, + `socks4a` or `socks5` with a host and port (`http://127.0.0.1:7890`), optionally with `user:password@`. It is for + Filee alone (#879), so the system can stay without a proxy, and the CLI reads the same setting. Empty: the system + proxy and `HTTPS_PROXY` / `NO_PROXY`, as before. Addresses on this computer and the local network (Ollama, an + intranet) never go through it, and logs show only its scheme, host and port. - Engines go to `%LOCALAPPDATA%\Filee\engines`: outside the app folder, so updates keep them, and inside Filee's install root, so uninstalling removes them. - LibreOffice and calibre come as MSIs and are unpacked with an administrative install (`msiexec /a`): files only, diff --git a/docs/PORTABLE.md b/docs/PORTABLE.md index f1489ecc9..49395d84d 100644 --- a/docs/PORTABLE.md +++ b/docs/PORTABLE.md @@ -8,6 +8,8 @@ are disabled, leaving an installed copy's entries untouched. The data folder mus an error instead of falling back to the user profile. Conversion work folders, temporary files and engine caches keep their existing behavior and locations. +A proxy set in Settings → General → Network is kept in `data/settings.json` as well, so the copy downloads engines +and updates through it on any computer without touching that computer's proxy settings. An explicit `FILEE_DATA_DIR` still takes precedence. Installer packages keep using the normal user profile. Update a marked copy by replacing its application files while keeping `filee.portable` and `data`. diff --git a/src/Filee.App/App.axaml.cs b/src/Filee.App/App.axaml.cs index 9b4ef5680..9be6549bf 100644 --- a/src/Filee.App/App.axaml.cs +++ b/src/Filee.App/App.axaml.cs @@ -12,6 +12,7 @@ using Filee.Core.Localization; using Filee.Core.Platform; using Filee.Core.Settings; +using Filee.Core.Web; using Filee.Engines.Infrastructure; using Microsoft.Extensions.Logging; @@ -74,6 +75,15 @@ private void Start(IClassicDesktopStyleApplicationLifetime desktop) var store = AppHost.Get(); if (Program.PreloadedStore is null) store.Load(); + // Before anything downloads: the proxy from Settings, read for each request so a change there applies at once. + ProxySetting.Use(() => store.Settings.Proxy); + if (!string.IsNullOrWhiteSpace(store.Settings.Proxy)) + { + if (ProxySetting.TryParse(store.Settings.Proxy, out var proxy) && proxy is not null) + log.LogInformation("Downloads go through the proxy {Proxy}", ProxySetting.Describe(proxy)); + else + log.LogWarning("The proxy in the settings isn't a proxy address; downloads use the system's"); + } var loc = AppHost.Get(); loc.Attach(this); diff --git a/src/Filee.App/Assets/i18n/ar.json b/src/Filee.App/Assets/i18n/ar.json index 58f678a03..2b76106f2 100644 --- a/src/Filee.App/Assets/i18n/ar.json +++ b/src/Filee.App/Assets/i18n/ar.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "يبقى الأصل في مجلد مؤقت ويُحذف بعد اكتمال التحويل.", "general.browser_drops_local_network": "السماح بالروابط إلى هذا الكمبيوتر والشبكة المحلية", "general.browser_drops_local_network_hint": "يتيح لـ Filee تنزيل الروابط من هذا الكمبيوتر ومن الشبكة المحلية، مثل الشبكة الداخلية (الإنترانت) أو جهاز NAS. لا تفعّله إلا إذا كنت تثق بالصفحات التي تسحب منها.", + "general.network": "الشبكة", + "general.proxy": "وكيل لتنزيلات Filee", + "general.proxy_hint": "تمر تنزيلات المحركات والتحقق من التحديثات والروابط المسحوبة من المتصفح عبر هذا الوكيل، ولا تتأثر البرامج الأخرى. اتركه فارغًا لاستخدام وكيل النظام. يتم الوصول دائمًا إلى العناوين الموجودة على هذا الكمبيوتر وفي الشبكة المحلية مباشرةً.", + "general.proxy_invalid": "هذا ليس عنوان وكيل. اكتبه بالشكل http://127.0.0.1:7890 (http أو https أو socks4 أو socks5).", "general.zip_geo": "حفظ كل نتيجة جغرافية بصيغة ZIP", "general.zip_geo_hint": "تُجمَّع نتائج Shapefile وGeoJSON وKML وGPX وغيرها من البيانات الجغرافية في ZIP واحد لكل ملف، مع كل إعداد مسبق.\nعند التعطيل: يُطبَّق ذلك فقط على الإعدادات المسبقة التي تفعّل «حفظ النتيجة بصيغة ZIP».", "general.check_updates": "التحقق من التحديثات تلقائيًا", diff --git a/src/Filee.App/Assets/i18n/bn-BD.json b/src/Filee.App/Assets/i18n/bn-BD.json index 4edb7bb39..c24601ded 100644 --- a/src/Filee.App/Assets/i18n/bn-BD.json +++ b/src/Filee.App/Assets/i18n/bn-BD.json @@ -621,6 +621,10 @@ "general.browser_drops_only_converted_hint": "মূল ফাইলটি একটি অস্থায়ী ফোল্ডারে অপেক্ষা করে এবং রূপান্তর সম্পন্ন হলে সেটি মুছে ফেলা হয়।", "general.browser_drops_local_network": "এই কম্পিউটার ও স্থানীয় নেটওয়ার্কের লিঙ্ক অনুমতি দিন", "general.browser_drops_local_network_hint": "ফাইলিকে এই কম্পিউটার ও স্থানীয় নেটওয়ার্কের লিঙ্ক, যেমন ইন্ট্রানেট বা NAS, থেকে ডাউনলোড করতে দেয়। শুধু তখনই চালু করুন যখন যে পেজ থেকে টেনে আনছেন সেগুলোকে আপনি বিশ্বাস করেন।", + "general.network": "নেটওয়ার্ক", + "general.proxy": "Filee-এর ডাউনলোডের জন্য প্রক্সি", + "general.proxy_hint": "ইঞ্জিন ডাউনলোড, আপডেট পরীক্ষা এবং ব্রাউজার থেকে টেনে আনা লিংক এই প্রক্সির মাধ্যমে যায়; অন্য প্রোগ্রামে এর প্রভাব পড়ে না। সিস্টেমের প্রক্সি ব্যবহার করতে এটি ফাঁকা রাখুন। এই কম্পিউটার ও স্থানীয় নেটওয়ার্কের ঠিকানায় সবসময় সরাসরি সংযোগ করা হয়।", + "general.proxy_invalid": "এটি প্রক্সির ঠিকানা নয়। http://127.0.0.1:7890-এর মতো করে লিখুন (http, https, socks4 বা socks5)।", "general.zip_geo": "প্রতিটি ভৌগোলিক ফলাফল ZIP হিসেবে সংরক্ষণ করুন", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX এবং অন্যান্য ভৌগোলিক ফলাফল প্রতিটি প্রিসেট দিয়ে প্রতি ফাইলের জন্য একটি ZIP-এ প্যাক করা হয়।\nবন্ধ থাকলে: কেবল \"ফলাফল ZIP হিসেবে সংরক্ষণ করুন\" সক্রিয় থাকা প্রিসেটগুলো এটি করে।", "general.check_updates": "স্বয়ংক্রিয়ভাবে আপডেটের জন্য পরীক্ষা করুন", diff --git a/src/Filee.App/Assets/i18n/de.json b/src/Filee.App/Assets/i18n/de.json index 74f990d1c..0fdef1fed 100644 --- a/src/Filee.App/Assets/i18n/de.json +++ b/src/Filee.App/Assets/i18n/de.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "Das Original bleibt in einem temporären Ordner und wird nach der Konvertierung gelöscht.", "general.browser_drops_local_network": "Links zu diesem Computer und ins lokale Netzwerk erlauben", "general.browser_drops_local_network_hint": "Filee lädt dann auch Links von diesem Computer und aus dem lokalen Netzwerk herunter, etwa aus einem Intranet oder von einem NAS. Schalte das nur ein, wenn du den Seiten vertraust, aus denen du ziehst.", + "general.network": "Netzwerk", + "general.proxy": "Proxy für die Downloads von Filee", + "general.proxy_hint": "Engine-Downloads, die Suche nach Updates und aus dem Browser gezogene Links laufen über diesen Proxy; andere Programme sind nicht betroffen. Leer lassen, um den Proxy des Systems zu verwenden. Adressen auf diesem Computer und im lokalen Netzwerk werden immer direkt erreicht.", + "general.proxy_invalid": "Das ist keine Proxy-Adresse. Schreibe sie wie http://127.0.0.1:7890 (http, https, socks4 oder socks5).", "general.zip_geo": "Alle Geodaten-Ergebnisse als ZIP speichern", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX und andere Geodaten-Ergebnisse werden bei jeder Vorlage in eine ZIP-Datei pro Datei gepackt.\nAusgeschaltet: Gilt nur für Vorlagen mit „Ergebnis als ZIP speichern“.", "general.check_updates": "Automatisch nach Updates suchen", diff --git a/src/Filee.App/Assets/i18n/en.json b/src/Filee.App/Assets/i18n/en.json index 8c2d96d45..91b29375c 100644 --- a/src/Filee.App/Assets/i18n/en.json +++ b/src/Filee.App/Assets/i18n/en.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "The original waits in a temporary folder and is deleted once it's converted.", "general.browser_drops_local_network": "Allow links to this computer and the local network", "general.browser_drops_local_network_hint": "Lets Filee download links from this computer and the local network, such as an intranet or a NAS. Turn it on only if you trust the pages you drag from.", + "general.network": "Network", + "general.proxy": "Proxy for Filee's downloads", + "general.proxy_hint": "Engine downloads, update checks and links dragged from a browser go through this proxy; other programs aren't affected. Leave it empty to use the system proxy. Addresses on this computer and the local network are always reached directly.", + "general.proxy_invalid": "This isn't a proxy address. Write it like http://127.0.0.1:7890 (http, https, socks4 or socks5).", "general.zip_geo": "Save every geospatial result as a ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX and the other geospatial results are packed into one ZIP per file, with every preset.\nOff: only presets with \"Save the result as a ZIP\" do it.", "general.check_updates": "Check for updates automatically", diff --git a/src/Filee.App/Assets/i18n/es.json b/src/Filee.App/Assets/i18n/es.json index c0514ff66..18cc545b6 100644 --- a/src/Filee.App/Assets/i18n/es.json +++ b/src/Filee.App/Assets/i18n/es.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "El original espera en una carpeta temporal y se elimina en cuanto se convierte.", "general.browser_drops_local_network": "Permitir enlaces a este equipo y a la red local", "general.browser_drops_local_network_hint": "Permite que Filee descargue enlaces de este equipo y de la red local, como una intranet o un NAS. Actívalo solo si confías en las páginas desde las que arrastras.", + "general.network": "Red", + "general.proxy": "Proxy para las descargas de Filee", + "general.proxy_hint": "Las descargas de motores, la búsqueda de actualizaciones y los enlaces arrastrados desde un navegador pasan por este proxy; los demás programas no se ven afectados. Déjalo vacío para usar el proxy del sistema. Las direcciones de este equipo y de la red local siempre se alcanzan directamente.", + "general.proxy_invalid": "Esta no es una dirección de proxy. Escríbela como http://127.0.0.1:7890 (http, https, socks4 o socks5).", "general.zip_geo": "Guardar todo resultado geoespacial como ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX y los demás resultados geoespaciales se empaquetan en un ZIP por archivo, con cualquier preajuste.\nDesactivado: solo lo hacen los preajustes con «Guardar el resultado como ZIP».", "general.check_updates": "Buscar actualizaciones automáticamente", diff --git a/src/Filee.App/Assets/i18n/fa.json b/src/Filee.App/Assets/i18n/fa.json index 1bd8836ca..2df0cd58f 100644 --- a/src/Filee.App/Assets/i18n/fa.json +++ b/src/Filee.App/Assets/i18n/fa.json @@ -621,6 +621,10 @@ "general.browser_drops_only_converted_hint": "فایل اصلی در یک پوشه موقت منتظر می‌ماند و پس از تبدیل حذف می‌شود.", "general.browser_drops_local_network": "مجاز بودن پیوندها به این رایانه و شبکه محلی", "general.browser_drops_local_network_hint": "به Filee اجازه می‌دهد پیوندهای این رایانه و شبکه محلی، مانند اینترانت یا NAS، را دانلود کند. این گزینه را تنها در صورتی فعال کنید که به صفحاتی که از آن‌ها فایل می‌کشید اعتماد دارید.", + "general.network": "شبکه", + "general.proxy": "پراکسی برای دانلودهای Filee", + "general.proxy_hint": "دانلود موتورها، بررسی به‌روزرسانی‌ها و پیوندهایی که از مرورگر کشیده می‌شوند از این پراکسی عبور می‌کنند؛ برنامه‌های دیگر تحت تأثیر قرار نمی‌گیرند. برای استفاده از پراکسی سیستم آن را خالی بگذارید. نشانی‌های این رایانه و شبکه محلی همیشه به‌طور مستقیم در دسترس قرار می‌گیرند.", + "general.proxy_invalid": "این یک نشانی پراکسی نیست. آن را به شکل http://127.0.0.1:7890 بنویسید (http، https، socks4 یا socks5).", "general.zip_geo": "ذخیره هر نتیجه جغرافیایی به صورت ZIP", "general.zip_geo_hint": "فایل‌های Shapefile، GeoJSON، KML، GPX و سایر نتایج جغرافیایی در تمام پیش‌تنظیم‌ها درون یک فایل ZIP به ازای هر فایل بسته‌بندی می‌شوند.\nخاموش: فقط پیش‌تنظیم‌هایی که گزینه «ذخیره نتیجه به صورت ZIP» دارند این کار را انجام می‌دهند.", "general.check_updates": "بررسی خودکار برای به‌روزرسانی‌ها", diff --git a/src/Filee.App/Assets/i18n/fr.json b/src/Filee.App/Assets/i18n/fr.json index c27675e61..b2ad0b918 100644 --- a/src/Filee.App/Assets/i18n/fr.json +++ b/src/Filee.App/Assets/i18n/fr.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "L'original attend dans un dossier temporaire et est supprimé une fois converti.", "general.browser_drops_local_network": "Autoriser les liens vers cet ordinateur et le réseau local", "general.browser_drops_local_network_hint": "Permet à Filee de télécharger des liens de cet ordinateur et du réseau local, comme un intranet ou un NAS. Ne l'activez que si vous faites confiance aux pages depuis lesquelles vous glissez.", + "general.network": "Réseau", + "general.proxy": "Proxy pour les téléchargements de Filee", + "general.proxy_hint": "Les téléchargements de moteurs, la recherche de mises à jour et les liens glissés depuis un navigateur passent par ce proxy ; les autres programmes ne sont pas concernés. Laissez vide pour utiliser le proxy du système. Les adresses de cet ordinateur et du réseau local sont toujours jointes directement.", + "general.proxy_invalid": "Ce n'est pas une adresse de proxy. Écrivez-la comme http://127.0.0.1:7890 (http, https, socks4 ou socks5).", "general.zip_geo": "Enregistrer chaque résultat géospatial en ZIP", "general.zip_geo_hint": "Les résultats Shapefile, GeoJSON, KML, GPX et autres formats géospatiaux sont regroupés dans un ZIP par fichier, quel que soit le préréglage.\nDésactivé : seuls les préréglages avec « Enregistrer le résultat en ZIP » le font.", "general.check_updates": "Rechercher automatiquement les mises à jour", diff --git a/src/Filee.App/Assets/i18n/hi.json b/src/Filee.App/Assets/i18n/hi.json index 1c9e0b53c..7364d06ab 100644 --- a/src/Filee.App/Assets/i18n/hi.json +++ b/src/Filee.App/Assets/i18n/hi.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "मूल फ़ाइल एक अस्थायी फ़ोल्डर में रहती है और कन्वर्ट होने के बाद हटा दी जाती है।", "general.browser_drops_local_network": "इस कंप्यूटर और लोकल नेटवर्क के लिंक की अनुमति दें", "general.browser_drops_local_network_hint": "इससे Filee इस कंप्यूटर और लोकल नेटवर्क, जैसे इंट्रानेट या NAS, के लिंक भी डाउनलोड कर सकता है। इसे तभी चालू करें जब आप उन पेजों पर भरोसा करते हों जिनसे आप खींचते हैं।", + "general.network": "नेटवर्क", + "general.proxy": "Filee के डाउनलोड के लिए प्रॉक्सी", + "general.proxy_hint": "इंजन डाउनलोड, अपडेट की जाँच और ब्राउज़र से खींचे गए लिंक इस प्रॉक्सी से होकर जाते हैं; दूसरे प्रोग्राम पर इसका असर नहीं पड़ता। सिस्टम प्रॉक्सी इस्तेमाल करने के लिए इसे खाली छोड़ें। इस कंप्यूटर और लोकल नेटवर्क के पतों से हमेशा सीधे जुड़ा जाता है।", + "general.proxy_invalid": "यह प्रॉक्सी पता नहीं है। इसे http://127.0.0.1:7890 की तरह लिखें (http, https, socks4 या socks5)।", "general.zip_geo": "हर भू-स्थानिक नतीजे को ZIP के रूप में सहेजें", "general.zip_geo_hint": "हर प्रीसेट के साथ, Shapefile, GeoJSON, KML, GPX और दूसरे भू-स्थानिक नतीजे हर फ़ाइल के लिए एक ZIP में पैक किए जाते हैं।\nबंद होने पर: केवल वही प्रीसेट ऐसा करते हैं जिनमें \"नतीजे को ZIP के रूप में सहेजें\" चालू है।", "general.check_updates": "अपडेट अपने-आप जाँचें", diff --git a/src/Filee.App/Assets/i18n/id.json b/src/Filee.App/Assets/i18n/id.json index 4c02a912f..76861b9bf 100644 --- a/src/Filee.App/Assets/i18n/id.json +++ b/src/Filee.App/Assets/i18n/id.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "File asli disimpan di folder sementara dan dihapus otomatis setelah konversi selesai.", "general.browser_drops_local_network": "Izinkan tautan ke komputer ini dan jaringan lokal", "general.browser_drops_local_network_hint": "Memungkinkan Filee mengunduh tautan dari komputer ini dan jaringan lokal, seperti intranet atau NAS. Aktifkan hanya jika Anda memercayai halaman tempat Anda menyeret.", + "general.network": "Jaringan", + "general.proxy": "Proxy untuk unduhan Filee", + "general.proxy_hint": "Unduhan mesin, pemeriksaan pembaruan, dan tautan yang diseret dari browser melewati proxy ini; program lain tidak terpengaruh. Biarkan kosong untuk memakai proxy sistem. Alamat di komputer ini dan jaringan lokal selalu dihubungi secara langsung.", + "general.proxy_invalid": "Ini bukan alamat proxy. Tulis seperti http://127.0.0.1:7890 (http, https, socks4, atau socks5).", "general.zip_geo": "Simpan setiap hasil geospasial sebagai ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX, dan hasil geospasial lainnya dikemas menjadi satu ZIP per file di semua preset.\nNonaktif: hanya preset dengan \"Simpan hasil sebagai ZIP\" yang menerapkannya.", "general.check_updates": "Periksa pembaruan secara otomatis", diff --git a/src/Filee.App/Assets/i18n/it.json b/src/Filee.App/Assets/i18n/it.json index 292f914b9..f47e559ad 100644 --- a/src/Filee.App/Assets/i18n/it.json +++ b/src/Filee.App/Assets/i18n/it.json @@ -621,6 +621,10 @@ "general.browser_drops_only_converted_hint": "L'originale resta in una cartella temporanea e viene eliminato dopo la conversione.", "general.browser_drops_local_network": "Consenti i link a questo computer e alla rete locale", "general.browser_drops_local_network_hint": "Permette a Filee di scaricare link da questo computer e dalla rete locale, come una intranet o un NAS. Attivalo solo se ti fidi delle pagine da cui trascini.", + "general.network": "Rete", + "general.proxy": "Proxy per i download di Filee", + "general.proxy_hint": "I download dei motori, la ricerca degli aggiornamenti e i link trascinati da un browser passano da questo proxy; gli altri programmi non ne sono toccati. Lascia vuoto per usare il proxy di sistema. Gli indirizzi di questo computer e della rete locale vengono sempre raggiunti direttamente.", + "general.proxy_invalid": "Questo non è un indirizzo proxy. Scrivilo come http://127.0.0.1:7890 (http, https, socks4 o socks5).", "general.zip_geo": "Salva ogni risultato geospaziale come ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX e gli altri risultati geospaziali vengono raccolti in un ZIP per file, con ogni preset.\nDisattivato: lo fanno solo i preset con “Salva il risultato come ZIP”.", "general.check_updates": "Controlla automaticamente gli aggiornamenti", diff --git a/src/Filee.App/Assets/i18n/ja.json b/src/Filee.App/Assets/i18n/ja.json index a35cbc281..bf7ce7d25 100644 --- a/src/Filee.App/Assets/i18n/ja.json +++ b/src/Filee.App/Assets/i18n/ja.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "元ファイルは一時フォルダーに置かれ、変換が終わると削除されます。", "general.browser_drops_local_network": "このコンピューターとローカルネットワークへのリンクを許可する", "general.browser_drops_local_network_hint": "イントラネットや NAS など、このコンピューターとローカルネットワーク内のリンクも Filee でダウンロードできるようにします。ドラッグ元のページを信頼できる場合にだけオンにしてください。", + "general.network": "ネットワーク", + "general.proxy": "Filee のダウンロードに使うプロキシ", + "general.proxy_hint": "エンジンのダウンロード、更新の確認、ブラウザーからドラッグしたリンクがこのプロキシを経由します。ほかのプログラムには影響しません。空欄にするとシステムのプロキシを使います。このコンピューターとローカルネットワーク内のアドレスには常に直接接続します。", + "general.proxy_invalid": "プロキシのアドレスではありません。http://127.0.0.1:7890 のように入力してください (http、https、socks4、socks5)。", "general.zip_geo": "地理空間データの変換結果はすべてZIPで保存する", "general.zip_geo_hint": "どのプリセットでも、Shapefile、GeoJSON、KML、GPXなどの地理空間データの変換結果をファイルごとに1つのZIPにまとめます。\nオフの場合は、「変換結果をZIPで保存」を有効にしたプリセットだけに適用されます。", "general.check_updates": "更新を自動で確認", diff --git a/src/Filee.App/Assets/i18n/ko.json b/src/Filee.App/Assets/i18n/ko.json index e2e06bada..a4c60ffdc 100644 --- a/src/Filee.App/Assets/i18n/ko.json +++ b/src/Filee.App/Assets/i18n/ko.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "원본은 임시 폴더에 두었다가 변환이 끝나면 지워요.", "general.browser_drops_local_network": "이 컴퓨터와 내부 네트워크의 링크 허용", "general.browser_drops_local_network_hint": "인트라넷이나 NAS처럼 이 컴퓨터와 내부 네트워크에 있는 링크도 Filee가 내려받을 수 있어요. 드래그하는 페이지를 믿을 수 있을 때만 켜 주세요.", + "general.network": "네트워크", + "general.proxy": "Filee 다운로드용 프록시", + "general.proxy_hint": "엔진 다운로드, 업데이트 확인, 브라우저에서 드래그한 링크가 이 프록시를 거쳐요. 다른 프로그램에는 영향이 없어요. 비워 두면 시스템 프록시를 써요. 이 컴퓨터와 내부 네트워크의 주소는 항상 직접 연결해요.", + "general.proxy_invalid": "프록시 주소가 아니에요. http://127.0.0.1:7890 처럼 적어 주세요 (http, https, socks4, socks5).", "general.zip_geo": "지리 정보 결과를 모두 ZIP으로 저장", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX 등 지리 정보 결과를 모든 프리셋에서 파일마다 ZIP 하나로 묶어요.\n꺼 두면 \"결과를 ZIP으로 저장\"을 켠 프리셋만 묶어요.", "general.check_updates": "자동으로 업데이트 확인", diff --git a/src/Filee.App/Assets/i18n/ne.json b/src/Filee.App/Assets/i18n/ne.json index 320f12e7a..ede9fca94 100644 --- a/src/Filee.App/Assets/i18n/ne.json +++ b/src/Filee.App/Assets/i18n/ne.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "मूल फाइल अस्थायी फोल्डरमा रहन्छ र रूपान्तरण भएपछि मेटाइन्छ।", "general.browser_drops_local_network": "यो कम्प्युटर र स्थानीय नेटवर्कका लिङ्कहरूलाई अनुमति दिनुहोस्", "general.browser_drops_local_network_hint": "यसले Filee लाई यो कम्प्युटर र स्थानीय नेटवर्क, जस्तै इन्ट्रानेट वा NAS, बाट लिङ्कहरू डाउनलोड गर्न दिन्छ। तपाईंले तान्ने पृष्ठहरूमा भरोसा छ भने मात्र यसलाई खोल्नुहोस्।", + "general.network": "नेटवर्क", + "general.proxy": "Filee का डाउनलोडका लागि प्रोक्सी", + "general.proxy_hint": "इन्जिन डाउनलोड, अपडेट जाँच र ब्राउजरबाट तानिएका लिंकहरू यो प्रोक्सी हुँदै जान्छन्; अरू प्रोग्रामहरूमा असर पर्दैन। प्रणालीको प्रोक्सी प्रयोग गर्न खाली छोड्नुहोस्। यो कम्प्युटर र स्थानीय नेटवर्कका ठेगानाहरूमा सधैँ सिधै जोडिन्छ।", + "general.proxy_invalid": "यो प्रोक्सी ठेगाना होइन। यसलाई http://127.0.0.1:7890 जस्तै लेख्नुहोस् (http, https, socks4 वा socks5)।", "general.zip_geo": "प्रत्येक भौगोलिक परिणामलाई ZIP को रूपमा सुरक्षित गर्नुहोस्", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX र अन्य भौगोलिक परिणामहरू हरेक प्रिसेटसँग प्रति फाइल एउटा ZIP मा प्याक गरिन्छ।\nबन्द हुँदा: केवल \"परिणामलाई ZIP को रूपमा सुरक्षित गर्नुहोस्\" सक्षम भएका प्रिसेटहरूले मात्र यसो गर्छन्।", "general.check_updates": "स्वचालित रूपमा अपडेटहरू जाँच गर्नुहोस्", diff --git a/src/Filee.App/Assets/i18n/pl.json b/src/Filee.App/Assets/i18n/pl.json index 016fb9607..bebbb0310 100644 --- a/src/Filee.App/Assets/i18n/pl.json +++ b/src/Filee.App/Assets/i18n/pl.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "Oryginał czeka w folderze tymczasowym i jest usuwany po konwersji.", "general.browser_drops_local_network": "Zezwalaj na linki do tego komputera i sieci lokalnej", "general.browser_drops_local_network_hint": "Pozwala Filee pobierać linki z tego komputera i z sieci lokalnej, np. z intranetu lub z serwera NAS. Włącz to tylko wtedy, gdy ufasz stronom, z których przeciągasz.", + "general.network": "Sieć", + "general.proxy": "Proxy dla pobierania w Filee", + "general.proxy_hint": "Pobieranie silników, sprawdzanie aktualizacji i linki przeciągnięte z przeglądarki przechodzą przez to proxy; inne programy nie są objęte. Zostaw puste, aby używać proxy systemowego. Adresy na tym komputerze i w sieci lokalnej są zawsze osiągane bezpośrednio.", + "general.proxy_invalid": "To nie jest adres proxy. Wpisz go w postaci http://127.0.0.1:7890 (http, https, socks4 lub socks5).", "general.zip_geo": "Zapisuj każdy wynik geoprzestrzenny jako ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX i inne wyniki geoprzestrzenne są pakowane do jednego ZIP na plik, w każdym ustawieniu.\nWyłączone: robią to tylko ustawienia z opcją „Zapisz wynik jako ZIP”.", "general.check_updates": "Automatycznie sprawdzaj aktualizacje", diff --git a/src/Filee.App/Assets/i18n/pt-BR.json b/src/Filee.App/Assets/i18n/pt-BR.json index 044461a63..41b3b1d15 100644 --- a/src/Filee.App/Assets/i18n/pt-BR.json +++ b/src/Filee.App/Assets/i18n/pt-BR.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "O original fica numa pasta temporária e é apagado depois da conversão.", "general.browser_drops_local_network": "Permitir links para este computador e para a rede local", "general.browser_drops_local_network_hint": "Permite que o Filee baixe links deste computador e da rede local, como uma intranet ou um NAS. Ative só se você confia nas páginas de onde arrasta.", + "general.network": "Rede", + "general.proxy": "Proxy para os downloads do Filee", + "general.proxy_hint": "Downloads de mecanismos, verificação de atualizações e links arrastados de um navegador passam por este proxy; outros programas não são afetados. Deixe vazio para usar o proxy do sistema. Endereços deste computador e da rede local são sempre acessados diretamente.", + "general.proxy_invalid": "Este não é um endereço de proxy. Escreva como http://127.0.0.1:7890 (http, https, socks4 ou socks5).", "general.zip_geo": "Salvar todo resultado geoespacial como ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX e os outros resultados geoespaciais são compactados num ZIP por arquivo, em toda predefinição.\nDesligado: só as predefinições com “Salvar o resultado como ZIP” fazem isso.", "general.check_updates": "Procurar atualizações automaticamente", diff --git a/src/Filee.App/Assets/i18n/pt-PT.json b/src/Filee.App/Assets/i18n/pt-PT.json index 3b598acc3..7fb458670 100644 --- a/src/Filee.App/Assets/i18n/pt-PT.json +++ b/src/Filee.App/Assets/i18n/pt-PT.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "O original fica numa pasta temporária e é eliminado após a conversão.", "general.browser_drops_local_network": "Permitir links para este computador e para a rede local", "general.browser_drops_local_network_hint": "Permite que o Filee descarregue links deste computador e da rede local, como uma intranet ou um NAS. Ativa apenas se confias nas páginas de onde arrastas.", + "general.network": "Rede", + "general.proxy": "Proxy para as transferências do Filee", + "general.proxy_hint": "As transferências de motores, a procura de atualizações e as ligações arrastadas de um navegador passam por este proxy; os outros programas não são afetados. Deixe vazio para usar o proxy do sistema. Os endereços deste computador e da rede local são sempre contactados diretamente.", + "general.proxy_invalid": "Este não é um endereço de proxy. Escreva-o como http://127.0.0.1:7890 (http, https, socks4 ou socks5).", "general.zip_geo": "Guardar todos os resultados geoespaciais em ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX e os restantes resultados geoespaciais são comprimidos num ZIP por ficheiro, em todas as predefinições.\nDesativado: só o fazem as predefinições com «Guardar o resultado em ZIP».", "general.check_updates": "Procurar atualizações automaticamente", diff --git a/src/Filee.App/Assets/i18n/ru.json b/src/Filee.App/Assets/i18n/ru.json index 4ebb6fab9..aa96e4a8f 100644 --- a/src/Filee.App/Assets/i18n/ru.json +++ b/src/Filee.App/Assets/i18n/ru.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "Оригинал ждёт во временной папке и удаляется после конвертации.", "general.browser_drops_local_network": "Разрешить ссылки на этот компьютер и в локальную сеть", "general.browser_drops_local_network_hint": "Filee сможет скачивать по ссылкам с этого компьютера и из локальной сети, например из интранета или с NAS. Включайте, только если доверяете страницам, из которых перетаскиваете.", + "general.network": "Сеть", + "general.proxy": "Прокси для загрузок Filee", + "general.proxy_hint": "Загрузка движков, проверка обновлений и ссылки, перетащенные из браузера, идут через этот прокси; другие программы это не затрагивает. Оставьте поле пустым, чтобы использовать системный прокси. К адресам на этом компьютере и в локальной сети Filee всегда подключается напрямую.", + "general.proxy_invalid": "Это не адрес прокси. Укажите его в виде http://127.0.0.1:7890 (http, https, socks4 или socks5).", "general.zip_geo": "Сохранять любой результат с геоданными в ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX и другие результаты с геоданными упаковываются в один ZIP на файл, с любым пресетом.\nВыключено: так делают только пресеты с включённым «Сохранять результат в ZIP».", "general.check_updates": "Проверять обновления автоматически", diff --git a/src/Filee.App/Assets/i18n/th.json b/src/Filee.App/Assets/i18n/th.json index 774623b30..1b204e7a1 100644 --- a/src/Filee.App/Assets/i18n/th.json +++ b/src/Filee.App/Assets/i18n/th.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "ไฟล์ต้นฉบับจะถูกพักไว้ในโฟลเดอร์ชั่วคราวและจะถูกลบหลังจากแปลงไฟล์เสร็จสิ้น", "general.browser_drops_local_network": "อนุญาตลิงก์ไปยังคอมพิวเตอร์เครื่องนี้และเครือข่ายภายใน", "general.browser_drops_local_network_hint": "ให้ Filee ดาวน์โหลดลิงก์จากคอมพิวเตอร์เครื่องนี้และเครือข่ายภายใน เช่น อินทราเน็ตหรือ NAS ได้ด้วย เปิดใช้เฉพาะเมื่อคุณเชื่อถือหน้าเว็บที่คุณลากมา", + "general.network": "เครือข่าย", + "general.proxy": "พร็อกซีสำหรับการดาวน์โหลดของ Filee", + "general.proxy_hint": "การดาวน์โหลดเอนจิน การตรวจหาอัปเดต และลิงก์ที่ลากมาจากเบราว์เซอร์จะผ่านพร็อกซีนี้ โปรแกรมอื่นไม่ได้รับผลกระทบ เว้นว่างไว้เพื่อใช้พร็อกซีของระบบ ที่อยู่บนคอมพิวเตอร์เครื่องนี้และเครือข่ายภายในจะเชื่อมต่อโดยตรงเสมอ", + "general.proxy_invalid": "นี่ไม่ใช่ที่อยู่พร็อกซี โปรดเขียนในรูปแบบ http://127.0.0.1:7890 (http, https, socks4 หรือ socks5)", "general.zip_geo": "บันทึกผลลัพธ์ข้อมูลเชิงพื้นที่ทั้งหมดเป็น ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX และผลลัพธ์เชิงพื้นที่อื่นๆ จะถูกรวมเป็นไฟล์ ZIP หนึ่งไฟล์ต่อหนึ่งรายการในทุกพรีเซ็ต\nหากปิด: จะทำเฉพาะพรีเซ็ตที่เลือก \"บันทึกผลลัพธ์เป็น ZIP\"", "general.check_updates": "ตรวจสอบการอัปเดตอัตโนมัติ", diff --git a/src/Filee.App/Assets/i18n/tr.json b/src/Filee.App/Assets/i18n/tr.json index e4195232f..3554f93c9 100644 --- a/src/Filee.App/Assets/i18n/tr.json +++ b/src/Filee.App/Assets/i18n/tr.json @@ -622,6 +622,10 @@ "general.browser_drops_only_converted_hint": "Özgün dosya geçici klasörde bekler ve dönüştürülünce silinir.", "general.browser_drops_local_network": "Bu bilgisayara ve yerel ağa giden bağlantılara izin ver", "general.browser_drops_local_network_hint": "Filee'nin bu bilgisayardaki ve yerel ağdaki (ör. intranet veya NAS) bağlantıları da indirmesini sağlar. Yalnızca sürüklediğiniz sayfalara güveniyorsanız açın.", + "general.network": "Ağ", + "general.proxy": "Filee indirmeleri için proxy", + "general.proxy_hint": "Motor indirmeleri, güncelleme denetimleri ve tarayıcıdan sürüklenen bağlantılar bu proxy üzerinden geçer; diğer programlar etkilenmez. Sistem proxy'sini kullanmak için boş bırakın. Bu bilgisayardaki ve yerel ağdaki adreslere her zaman doğrudan bağlanılır.", + "general.proxy_invalid": "Bu bir proxy adresi değil. http://127.0.0.1:7890 biçiminde yazın (http, https, socks4 veya socks5).", "general.zip_geo": "Her coğrafi veri sonucunu ZIP olarak kaydet", "general.zip_geo_hint": "Tüm ön ayarlarda Shapefile, GeoJSON, KML, GPX ve diğer coğrafi veri sonuçları dosya başına tek bir ZIP'e paketlenir.\nKapalıysa yalnızca “Sonucu ZIP olarak kaydet” seçili ön ayarlar bunu yapar.", "general.check_updates": "Güncellemeleri otomatik denetle", diff --git a/src/Filee.App/Assets/i18n/vi.json b/src/Filee.App/Assets/i18n/vi.json index 4a4dbaa5f..06f2a5296 100644 --- a/src/Filee.App/Assets/i18n/vi.json +++ b/src/Filee.App/Assets/i18n/vi.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "Bản gốc chờ trong một thư mục tạm và bị xóa khi đã chuyển đổi xong.", "general.browser_drops_local_network": "Cho phép liên kết tới máy tính này và mạng nội bộ", "general.browser_drops_local_network_hint": "Cho phép Filee tải xuống liên kết từ máy tính này và mạng nội bộ, chẳng hạn như mạng intranet hoặc NAS. Chỉ bật khi bạn tin tưởng các trang mà bạn kéo từ đó.", + "general.network": "Mạng", + "general.proxy": "Proxy cho các lượt tải của Filee", + "general.proxy_hint": "Việc tải engine, kiểm tra cập nhật và các liên kết kéo từ trình duyệt sẽ đi qua proxy này; các chương trình khác không bị ảnh hưởng. Để trống để dùng proxy của hệ thống. Các địa chỉ trên máy tính này và mạng nội bộ luôn được kết nối trực tiếp.", + "general.proxy_invalid": "Đây không phải là địa chỉ proxy. Hãy nhập theo dạng http://127.0.0.1:7890 (http, https, socks4 hoặc socks5).", "general.zip_geo": "Lưu mọi kết quả không gian địa lý thành tệp ZIP", "general.zip_geo_hint": "Shapefile, GeoJSON, KML, GPX và các kết quả không gian địa lý khác được đóng gói thành một tệp ZIP cho mỗi tệp, với mọi cấu hình sẵn.\nTắt: chỉ các cấu hình sẵn có bật “Lưu kết quả thành tệp ZIP” mới làm vậy.", "general.check_updates": "Tự động kiểm tra cập nhật", diff --git a/src/Filee.App/Assets/i18n/zh-CN.json b/src/Filee.App/Assets/i18n/zh-CN.json index 8fefa9659..3c51591c0 100644 --- a/src/Filee.App/Assets/i18n/zh-CN.json +++ b/src/Filee.App/Assets/i18n/zh-CN.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "原文件先放在临时文件夹中,转换完成后删除。", "general.browser_drops_local_network": "允许指向本机和局域网的链接", "general.browser_drops_local_network_hint": "允许 Filee 下载本机和局域网中的链接,例如内网或 NAS。仅在您信任拖拽来源的网页时开启。", + "general.network": "网络", + "general.proxy": "Filee 下载使用的代理", + "general.proxy_hint": "引擎下载、检查更新以及从浏览器拖入的链接都会经过此代理,其他程序不受影响。留空则使用系统代理。本机和局域网中的地址始终直接连接。", + "general.proxy_invalid": "这不是有效的代理地址。请按 http://127.0.0.1:7890 的格式填写(支持 http、https、socks4 或 socks5)。", "general.zip_geo": "将所有地理信息结果保存为 ZIP", "general.zip_geo_hint": "无论使用哪个预设,Shapefile、GeoJSON、KML、GPX 等地理信息结果都会按每个文件打包成一个 ZIP。\n关闭后,只有开启了“将结果保存为 ZIP”的预设会这样做。", "general.check_updates": "自动检查更新", diff --git a/src/Filee.App/Assets/i18n/zh-TW.json b/src/Filee.App/Assets/i18n/zh-TW.json index 4956c7684..ff5fd4d43 100644 --- a/src/Filee.App/Assets/i18n/zh-TW.json +++ b/src/Filee.App/Assets/i18n/zh-TW.json @@ -634,6 +634,10 @@ "general.browser_drops_only_converted_hint": "原始檔案先放在臨時資料夾中,轉換完成後刪除。", "general.browser_drops_local_network": "允許指向這台電腦和區域網路的連結", "general.browser_drops_local_network_hint": "允許 Filee 下載這台電腦和區域網路中的連結,例如內部網路或 NAS。僅在您信任拖曳來源的網頁時開啟。", + "general.network": "網路", + "general.proxy": "Filee 下載使用的 Proxy", + "general.proxy_hint": "引擎下載、檢查更新以及從瀏覽器拖入的連結都會經過此 Proxy,其他程式不受影響。留空則使用系統 Proxy。本機和區域網路中的位址一律直接連線。", + "general.proxy_invalid": "這不是有效的 Proxy 位址。請依 http://127.0.0.1:7890 的格式填寫(支援 http、https、socks4 或 socks5)。", "general.zip_geo": "將所有地理資訊結果儲存為 ZIP", "general.zip_geo_hint": "無論使用哪個預設,Shapefile、GeoJSON、KML、GPX 等地理資訊結果都會按每個檔案打包成一個 ZIP。\n關閉後,只有開啟了「將結果儲存為 ZIP」的預設會這樣做。", "general.check_updates": "自動檢查更新", diff --git a/src/Filee.App/ViewModels/Pages/GeneralPageViewModel.cs b/src/Filee.App/ViewModels/Pages/GeneralPageViewModel.cs index 6d1e99117..59b5eaab5 100644 --- a/src/Filee.App/ViewModels/Pages/GeneralPageViewModel.cs +++ b/src/Filee.App/ViewModels/Pages/GeneralPageViewModel.cs @@ -7,6 +7,7 @@ using Filee.Core.Platform; using Filee.Core.Presets; using Filee.Core.Settings; +using Filee.Core.Web; using Microsoft.Extensions.Logging; namespace Filee.App.ViewModels.Pages; @@ -56,6 +57,7 @@ public GeneralPageViewModel(UserDataStore store, ILocalizer loc, IPlatformServic _browserFolder = store.Settings.BrowserDrops.Folder; _saveOnlyConverted = store.Settings.BrowserDrops.SaveOnlyConverted; _allowLocalNetwork = store.Settings.BrowserDrops.AllowLocalNetwork; + _proxy = store.Settings.Proxy ?? ""; _zipGeoResults = store.Settings.ZipGeoResults; CloseChoices = Enum.GetValues().Select(v => new Choice(v, loc[$"general.close.{v}"])).ToList(); _closeChoice = CloseChoices.First(c => c.Value == store.Settings.CloseAction); @@ -125,6 +127,19 @@ partial void OnDefaultSubfolderChanged(string value) => partial void OnAllowLocalNetworkChanged(bool value) => Save(s => s.BrowserDrops.AllowLocalNetwork = value); + /// The proxy for Filee's downloads as typed. Saved whenever it is empty or an address. + [ObservableProperty, NotifyPropertyChangedFor(nameof(ProxyInvalid))] + private string _proxy; + + /// What is typed isn't a proxy address; the last one that was stays in use. + public bool ProxyInvalid => !ProxySetting.TryParse(Proxy, out _); + + partial void OnProxyChanged(string value) + { + if (ProxySetting.TryParse(value, out _)) + Save(s => s.Proxy = string.IsNullOrWhiteSpace(value) ? null : value.Trim()); + } + /// What closing the main window does; its "Don't ask again" also sets it. public IReadOnlyList> CloseChoices { get; } diff --git a/src/Filee.App/Views/Pages/GeneralPage.axaml b/src/Filee.App/Views/Pages/GeneralPage.axaml index 0b4303b2e..37e9ee06e 100644 --- a/src/Filee.App/Views/Pages/GeneralPage.axaml +++ b/src/Filee.App/Views/Pages/GeneralPage.axaml @@ -91,6 +91,21 @@ + + + + + + + + + + + + + diff --git a/src/Filee.Cli/CliHost.cs b/src/Filee.Cli/CliHost.cs index 064973f53..a5609d1d2 100644 --- a/src/Filee.Cli/CliHost.cs +++ b/src/Filee.Cli/CliHost.cs @@ -5,6 +5,7 @@ using Filee.Core.Localization; using Filee.Core.Platform; using Filee.Core.Settings; +using Filee.Core.Web; using Filee.Engines; using Filee.Engines.Infrastructure; @@ -40,6 +41,8 @@ public static CliHost Create(string? dataDirectory = null, EngineInstaller? engi { var store = new UserDataStore(dataDirectory ?? UserDataStore.DefaultDirectory); store.Load(); + // 'filee engines install' and OCR languages download through the app's proxy setting too. + ProxySetting.Use(() => store.Settings.Proxy); EngineEnvironment.OwnCopies = new Dictionary(store.Settings.EngineOwnCopies); EngineEnvironment.LibreOfficeForSlides = store.Settings.LibreOfficeForSlides; EngineEnvironment.LibreOfficeForHwp = store.Settings.LibreOfficeForHwp; diff --git a/src/Filee.Core/Settings/AppSettings.cs b/src/Filee.Core/Settings/AppSettings.cs index 23566ecb5..638a794c0 100644 --- a/src/Filee.Core/Settings/AppSettings.cs +++ b/src/Filee.Core/Settings/AppSettings.cs @@ -184,6 +184,13 @@ public sealed class AppSettings public bool CheckForUpdates { get; set; } = true; + /// + /// The proxy Filee's downloads go through (Settings → General → Network), such as http://127.0.0.1:7890 or + /// socks5://host:1080; see . Null or empty: the system proxy and HTTPS_PROXY. + /// Missing in older settings, which reads as empty, so it needs no schema step. + /// + public string? Proxy { get; set; } + /// What closing the main window does. A value from a newer Filee reads as . [JsonConverter(typeof(LenientEnumJsonConverter))] public CloseAction CloseAction { get; set; } = CloseAction.Ask; diff --git a/src/Filee.Core/Web/ProxySetting.cs b/src/Filee.Core/Web/ProxySetting.cs new file mode 100644 index 000000000..fab8c4f6d --- /dev/null +++ b/src/Filee.Core/Web/ProxySetting.cs @@ -0,0 +1,161 @@ +// The proxy Filee's own downloads go through (Settings → General → Network, AppSettings.Proxy): engines, update +// checks, OCR languages and links dragged from a browser. It is for users who reach GitHub only through a proxy but +// don't want every program to use it, as the system proxy would (#879). Empty: the system proxy or HTTPS_PROXY, as +// before. Addresses on this computer and the local network never go through it (Ollama, an intranet link). + +using System.Net; + +namespace Filee.Core.Web; + +public static class ProxySetting +{ + /// + /// What .NET can talk to a proxy with. curl's socks5h ("the proxy looks up names") is read as socks5, which in + /// .NET already leaves the lookup to the proxy. + /// + private static readonly string[] Schemes = ["http", "https", "socks4", "socks4a", "socks5"]; + + private static readonly Lock Gate = new(); + + /// + /// Reads a proxy address as typed in Settings: http://127.0.0.1:7890, socks5://user:pass@host:1080, + /// or just 127.0.0.1:7890 (taken as http). True with a null for an empty value. + /// + public static bool TryParse(string? text, out Uri? proxy) + { + proxy = null; + if (string.IsNullOrWhiteSpace(text)) + return true; + var value = text.Trim(); + if (!value.Contains("://", StringComparison.Ordinal)) + value = "http://" + value; + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || uri.HostNameType == UriHostNameType.Unknown) + return false; + if (uri.Scheme == "socks5h") + uri = new UriBuilder(uri) { Scheme = "socks5" }.Uri; + // A proxy is a host and a port; a path or query means something else was pasted (a download link). + if (!Schemes.Contains(uri.Scheme) || uri.Port <= 0 || uri.AbsolutePath != "/" || uri.Query.Length > 0 + || uri.Fragment.Length > 0) + return false; + proxy = uri; + return true; + } + + /// The address without its user name and password, which only hands on. + public static Uri WithoutCredentials(Uri proxy) => + proxy.UserInfo.Length == 0 ? proxy : new UriBuilder(proxy) { UserName = "", Password = "" }.Uri; + + /// The user name and password in the address; null without them. + public static NetworkCredential? CredentialsOf(Uri proxy) + { + if (proxy.UserInfo.Length == 0) + return null; + var colon = proxy.UserInfo.IndexOf(':', StringComparison.Ordinal); + return colon < 0 + ? new NetworkCredential(Uri.UnescapeDataString(proxy.UserInfo), "") + : new NetworkCredential(Uri.UnescapeDataString(proxy.UserInfo[..colon]), Uri.UnescapeDataString(proxy.UserInfo[(colon + 1)..])); + } + + /// The address for logs: scheme, host and port, never the password. + public static string Describe(Uri proxy) => $"{proxy.Scheme}://{proxy.Authority}"; + + /// + /// Makes every that uses the default proxy (all of Filee's downloads; the clients for + /// Ollama turn proxies off) read for each request, so a change in Settings applies at + /// once. Called once the settings are loaded and before anything downloads; a second call replaces the setting. + /// + public static void Use(Func setting) + { + lock (Gate) + { + if (HttpClient.DefaultProxy is SettingsWebProxy proxy) + proxy.Setting = setting; + else + HttpClient.DefaultProxy = new SettingsWebProxy(setting, HttpClient.DefaultProxy); + } + } +} + +/// +/// The proxy in Settings when one is set, for every address but those on this computer and the local network; +/// otherwise the system's (Windows' proxy settings, HTTPS_PROXY and the other variables). +/// +public sealed class SettingsWebProxy : IWebProxy +{ + private readonly IWebProxy? _system; + private Parsed _parsed = new(null, null, null); + + /// Read for each request: the proxy address as typed in Settings. + /// What is used while the setting is empty or not an address. + public SettingsWebProxy(Func setting, IWebProxy? system) + { + Setting = setting; + _system = system; + Credentials = new CurrentCredentials(this); + } + + internal Func Setting { get; set; } + + /// + /// Asks for the current proxy's sign-in each time, since the handler may keep this object from its first request. + /// + public ICredentials? Credentials { get; set; } + + /// The proxy from Settings, without its password; null when the setting is empty or not an address. + public Uri? Configured => Current().Address; + + public Uri? GetProxy(Uri destination) + { + var current = Current(); + if (current.Address is null) + return _system?.GetProxy(destination); + return IsLocal(destination) ? null : current.Address; + } + + public bool IsBypassed(Uri host) + { + if (Current().Address is null) + return _system?.IsBypassed(host) ?? true; + return IsLocal(host); + } + + /// + /// This computer and the local network: loopback, private and link-local numbers, localhost, .local and other + /// local names, and single-label names (an intranet server). A proxy elsewhere couldn't reach them. + /// + internal static bool IsLocal(Uri address) + { + if (address.IsLoopback) + return true; + var host = address.IdnHost.Trim('[', ']'); + if (IPAddress.TryParse(host, out var ip)) + return LocalNetwork.IsLocal(ip); + return LocalNetwork.IsLocalName(host) || !host.TrimEnd('.').Contains('.', StringComparison.Ordinal); + } + + private Parsed Current() + { + var text = Setting(); + var parsed = _parsed; + if (string.Equals(parsed.Text, text, StringComparison.Ordinal)) + return parsed; + parsed = ProxySetting.TryParse(text, out var uri) && uri is not null + ? new Parsed(text, ProxySetting.WithoutCredentials(uri), ProxySetting.CredentialsOf(uri)) + : new Parsed(text, null, null); + _parsed = parsed; + return parsed; + } + + private sealed record Parsed(string? Text, Uri? Address, NetworkCredential? Credential); + + private sealed class CurrentCredentials(SettingsWebProxy owner) : ICredentials + { + public NetworkCredential? GetCredential(Uri uri, string authType) + { + var current = owner.Current(); + return current.Address is null + ? owner._system?.Credentials?.GetCredential(uri, authType) + : current.Credential; + } + } +} diff --git a/src/Filee.Core/Web/WebDownloader.cs b/src/Filee.Core/Web/WebDownloader.cs index 9fe846a48..3ea38badc 100644 --- a/src/Filee.Core/Web/WebDownloader.cs +++ b/src/Filee.Core/Web/WebDownloader.cs @@ -32,16 +32,23 @@ public sealed class WebDownloader : IDisposable /// /// "Filee/1.13.0"; some servers refuse requests without one. public WebDownloader(HttpMessageHandler? handler = null, string? userAgent = null) + : this(handler, userAgent, proxy: null) { - _http = Client(handler ?? Handler(publicOnly: true), userAgent); - _localHttp = handler is null ? Client(Handler(publicOnly: false), userAgent) : _http; } - private SocketsHttpHandler Handler(bool publicOnly) => new() + /// For tests; by default the one every download uses (Settings → General, else the system's). + internal WebDownloader(HttpMessageHandler? handler, string? userAgent, IWebProxy? proxy) + { + _http = Client(handler ?? Handler(publicOnly: true, proxy), userAgent); + _localHttp = handler is null ? Client(Handler(publicOnly: false, proxy), userAgent) : _http; + } + + private SocketsHttpHandler Handler(bool publicOnly, IWebProxy? proxy) => new() { AllowAutoRedirect = false, UseCookies = false, UseProxy = true, + Proxy = proxy, Credentials = null, PreAuthenticate = false, AutomaticDecompression = DecompressionMethods.All, @@ -255,7 +262,10 @@ private void EnsurePublic(Uri url) /// /// Opens the connection to an address the name resolved to that isn't local. The check is made on the very /// addresses connected to, so a name that resolves to a public address once and a local one the next time (DNS - /// rebinding) gets nowhere. A connection to the user's proxy is left alone: the proxy is their own choice. + /// rebinding) gets nowhere. A connection to the user's proxy is left alone: the proxy is their own choice. That is + /// one to another address than the link's, or the handler's own CONNECT, which opens a tunnel to an https site + /// through an http proxy: it names the proxy as its address, and a proxy on this computer (127.0.0.1:7890) would + /// otherwise be refused. Downloads only ever send GET, so a CONNECT can only come from the proxy settings. /// private async ValueTask ConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken) { @@ -263,8 +273,9 @@ private async ValueTask ConnectAsync(SocketsHttpConnectionContext contex var host = endpoint.Host.Trim('[', ']'); var addresses = IPAddress.TryParse(host, out var literal) ? [literal] : await ResolveAsync(host, cancellationToken); var target = context.InitialRequestMessage.RequestUri; - var direct = target is null - || (string.Equals(target.IdnHost.Trim('[', ']'), host, StringComparison.OrdinalIgnoreCase) && target.Port == endpoint.Port); + var direct = context.InitialRequestMessage.Method != HttpMethod.Connect + && (target is null + || (string.Equals(target.IdnHost.Trim('[', ']'), host, StringComparison.OrdinalIgnoreCase) && target.Port == endpoint.Port)); if (direct) { addresses = [.. addresses.Where(address => !IsRefused(address))]; diff --git a/tests/Filee.App.Tests/ProxyPageTests.cs b/tests/Filee.App.Tests/ProxyPageTests.cs new file mode 100644 index 000000000..f592cb194 --- /dev/null +++ b/tests/Filee.App.Tests/ProxyPageTests.cs @@ -0,0 +1,43 @@ +// Settings → General → Network (#879): the proxy box saves an address or nothing, never what isn't one. + +using Avalonia.Headless.XUnit; +using Filee.App.Services; +using Filee.App.ViewModels.Pages; +using Filee.Core.Localization; +using Filee.Core.Platform; +using Filee.Core.Settings; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Filee.App.Tests; + +[Collection("MainWindow")] +public class ProxyPageTests +{ + [AvaloniaFact] + public void The_proxy_box_saves_addresses_and_keeps_the_last_one_while_the_text_isnt_one() + { + TestServices.EnsureInitialized("en"); + var store = AppHost.Get(); + var before = store.Settings.Proxy; + using var page = new GeneralPageViewModel(store, AppHost.Get(), AppHost.Get(), + NullLogger.Instance); + try + { + page.Proxy = " http://127.0.0.1:7890 "; + Assert.False(page.ProxyInvalid); + Assert.Equal("http://127.0.0.1:7890", store.Settings.Proxy); + + page.Proxy = "http://127.0.0.1:7890/files/setup.zip"; + Assert.True(page.ProxyInvalid); + Assert.Equal("http://127.0.0.1:7890", store.Settings.Proxy); + + page.Proxy = ""; + Assert.False(page.ProxyInvalid); + Assert.Null(store.Settings.Proxy); + } + finally + { + store.Settings.Proxy = before; + } + } +} diff --git a/tests/Filee.Core.Tests/ProxySettingTests.cs b/tests/Filee.Core.Tests/ProxySettingTests.cs new file mode 100644 index 000000000..45c670d90 --- /dev/null +++ b/tests/Filee.Core.Tests/ProxySettingTests.cs @@ -0,0 +1,191 @@ +// The proxy in Settings (#879): which addresses are taken, which requests go through it, and that its password stays +// out of the address the handler (and its error messages) get. No network: only what the proxy object answers. + +using System.Net; +using Filee.Core.Web; + +namespace Filee.Core.Tests; + +public sealed class ProxySettingTests +{ + [Theory] + [InlineData("http://127.0.0.1:7890", "http://127.0.0.1:7890/")] + [InlineData(" http://127.0.0.1:7890/ ", "http://127.0.0.1:7890/")] + [InlineData("127.0.0.1:7890", "http://127.0.0.1:7890/")] + [InlineData("https://proxy.example:8443", "https://proxy.example:8443/")] + [InlineData("http://proxy.example", "http://proxy.example/")] + [InlineData("proxy.example", "http://proxy.example/")] + [InlineData("socks5://127.0.0.1:1080", "socks5://127.0.0.1:1080/")] + [InlineData("socks5h://127.0.0.1:1080", "socks5://127.0.0.1:1080/")] + [InlineData("socks4a://[::1]:1080", "socks4a://[::1]:1080/")] + [InlineData("http://user:pass@127.0.0.1:7890", "http://user:pass@127.0.0.1:7890/")] + public void Takes_a_proxy_address(string text, string expected) + { + Assert.True(ProxySetting.TryParse(text, out var proxy)); + Assert.Equal(expected, proxy!.AbsoluteUri); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Empty_means_no_proxy_of_its_own(string? text) + { + Assert.True(ProxySetting.TryParse(text, out var proxy)); + Assert.Null(proxy); + } + + [Theory] + [InlineData("ftp://127.0.0.1:21")] + [InlineData("socks5://127.0.0.1")] // no port, and socks has no default one + [InlineData("http://127.0.0.1:7890/download/file.zip")] + [InlineData("http://127.0.0.1:7890/?a=1")] + [InlineData("http://127.0.0.1:99999")] + [InlineData("http://")] + [InlineData("not a proxy")] + public void Refuses_what_isnt_a_proxy_address(string text) + { + Assert.False(ProxySetting.TryParse(text, out var proxy)); + Assert.Null(proxy); + } + + [Fact] + public void The_password_is_handed_on_apart_and_never_described() + { + Assert.True(ProxySetting.TryParse("http://us%40er:p%3Ass@proxy.example:7890", out var proxy)); + + var credential = ProxySetting.CredentialsOf(proxy!); + Assert.Equal("us@er", credential!.UserName); + Assert.Equal("p:ss", credential.Password); + Assert.Equal("http://proxy.example:7890/", ProxySetting.WithoutCredentials(proxy!).AbsoluteUri); + Assert.Equal("http://proxy.example:7890", ProxySetting.Describe(proxy!)); + Assert.Null(ProxySetting.CredentialsOf(new Uri("http://proxy.example:7890"))); + } + + [Fact] + public void A_set_proxy_takes_internet_addresses_and_the_password_stays_out_of_its_address() + { + var system = new FixedProxy(new Uri("http://system.example:8080")); + var proxy = new SettingsWebProxy(() => "http://user:secret@127.0.0.1:7890", system); + var github = new Uri("https://github.com/KnifeLemon/Filee/releases"); + + Assert.False(proxy.IsBypassed(github)); + Assert.Equal(new Uri("http://127.0.0.1:7890/"), proxy.GetProxy(github)); + Assert.DoesNotContain("secret", proxy.GetProxy(github)!.AbsoluteUri, StringComparison.Ordinal); + var credential = proxy.Credentials!.GetCredential(new Uri("http://127.0.0.1:7890/"), "Basic"); + Assert.Equal(("user", "secret"), (credential!.UserName, credential.Password)); + Assert.Equal(0, system.Asked); + } + + [Theory] + [InlineData("http://localhost:11434/api/tags")] // Ollama + [InlineData("http://127.0.0.1:5000/")] + [InlineData("http://[::1]:5000/")] + [InlineData("http://192.168.1.20/share/photo.png")] // a NAS + [InlineData("http://10.0.0.7/")] + [InlineData("http://printer.local/")] + [InlineData("http://nas/photo.png")] // an intranet name + public void Addresses_on_this_computer_and_the_local_network_go_direct(string address) + { + var proxy = new SettingsWebProxy(() => "http://127.0.0.1:7890", new FixedProxy(new Uri("http://system.example:8080"))); + var uri = new Uri(address); + + Assert.True(proxy.IsBypassed(uri)); + Assert.Null(proxy.GetProxy(uri)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("not a proxy")] + public void Without_a_usable_setting_the_system_proxy_decides(string? setting) + { + var system = new FixedProxy(new Uri("http://system.example:8080")) { Bypassed = false }; + system.Credentials = new NetworkCredential("system-user", "x"); + var proxy = new SettingsWebProxy(() => setting, system); + var github = new Uri("https://github.com/"); + + Assert.False(proxy.IsBypassed(github)); + Assert.Equal(new Uri("http://system.example:8080"), proxy.GetProxy(github)); + Assert.Equal("system-user", proxy.Credentials!.GetCredential(github, "Basic")!.UserName); + Assert.Equal(2, system.Asked); + } + + [Fact] + public void Without_a_setting_or_a_system_proxy_everything_goes_direct() + { + var proxy = new SettingsWebProxy(() => null, null); + + Assert.True(proxy.IsBypassed(new Uri("https://github.com/"))); + Assert.Null(proxy.GetProxy(new Uri("https://github.com/"))); + Assert.Null(proxy.Credentials!.GetCredential(new Uri("https://github.com/"), "Basic")); + } + + [Fact] + public void A_change_in_Settings_applies_to_the_next_request() + { + string? setting = null; + var proxy = new SettingsWebProxy(() => setting, null); + var github = new Uri("https://github.com/"); + + Assert.Null(proxy.GetProxy(github)); + setting = "socks5://127.0.0.1:1080"; + Assert.Equal(new Uri("socks5://127.0.0.1:1080/"), proxy.GetProxy(github)); + Assert.Equal(new Uri("socks5://127.0.0.1:1080/"), proxy.Configured); + setting = ""; + Assert.Null(proxy.GetProxy(github)); + Assert.Null(proxy.Configured); + } + + /// The system's proxy, counting how often it was asked. + private sealed class FixedProxy(Uri address) : IWebProxy + { + public int Asked { get; private set; } + + public bool Bypassed { get; init; } = true; + + public ICredentials? Credentials { get; set; } + + public Uri? GetProxy(Uri destination) + { + Asked++; + return address; + } + + public bool IsBypassed(Uri host) + { + Asked++; + return Bypassed; + } + } +} + +/// +/// Replaces HttpClient.DefaultProxy for a moment, so nothing else may run meanwhile: a handler created then would keep +/// the test's proxy. +/// +[CollectionDefinition("Default proxy", DisableParallelization = true)] +public sealed class DefaultProxyCollection; + +[Collection("Default proxy")] +public sealed class ProxySettingUseTests +{ + [Fact] + public void Use_wraps_the_default_proxy_once_and_keeps_the_latest_setting() + { + var original = HttpClient.DefaultProxy; + try + { + ProxySetting.Use(() => "http://127.0.0.1:1"); + var installed = Assert.IsType(HttpClient.DefaultProxy); + ProxySetting.Use(() => "http://127.0.0.1:2"); + + Assert.Same(installed, HttpClient.DefaultProxy); + Assert.Equal(new Uri("http://127.0.0.1:2/"), installed.GetProxy(new Uri("https://github.com/"))); + } + finally + { + HttpClient.DefaultProxy = original; + } + } +} diff --git a/tests/Filee.Core.Tests/WebDownloaderTests.cs b/tests/Filee.Core.Tests/WebDownloaderTests.cs index 4cd6f15ec..331bd6291 100644 --- a/tests/Filee.Core.Tests/WebDownloaderTests.cs +++ b/tests/Filee.Core.Tests/WebDownloaderTests.cs @@ -458,6 +458,94 @@ public async Task Bytes_a_browser_handed_over_are_saved_like_a_download() program.SaveAsync(_dir.Path, downloader, null, TestContext.Current.CancellationToken))).ErrorKey); } + [Fact] + public async Task An_https_link_tunnels_through_a_proxy_on_this_computer() + { + // Clash and the like listen on 127.0.0.1. For an https site the handler first sends CONNECT to the proxy, + // naming the proxy as its address: that connection must not be taken for a link to this computer. + await using var proxy = new FakeProxy("HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n"u8.ToArray()); + using var downloader = new WebDownloader(null, null, new SettingsWebProxy(() => proxy.Address, null)); + var ct = TestContext.Current.CancellationToken; + + var error = await Assert.ThrowsAsync(() => + downloader.DownloadAsync(new Uri("https://files.example/photo.png"), _dir.Path, null, null, ct)); + + Assert.Equal("error.web_download_failed", error.ErrorKey); + Assert.StartsWith("CONNECT files.example:443 ", await proxy.Request); + } + + [Fact] + public async Task An_http_link_is_fetched_through_a_proxy_on_this_computer() + { + var head = "HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: 16\r\nConnection: close\r\n\r\n"u8.ToArray(); + await using var proxy = new FakeProxy([.. head, .. Png]); + using var downloader = new WebDownloader(null, null, new SettingsWebProxy(() => proxy.Address, null)); + var ct = TestContext.Current.CancellationToken; + + var path = await downloader.DownloadAsync(new Uri("http://files.example/photo.png"), _dir.Path, null, null, ct); + + Assert.Equal(Png, await File.ReadAllBytesAsync(path, ct)); + Assert.StartsWith("GET http://files.example/photo.png ", await proxy.Request); + } + + [Fact] + public async Task With_a_proxy_a_link_to_this_computer_is_still_refused_and_never_sent_to_it() + { + await using var proxy = new FakeProxy([]); + using var downloader = new WebDownloader(null, null, new SettingsWebProxy(() => proxy.Address, null)); + var ct = TestContext.Current.CancellationToken; + + var error = await Assert.ThrowsAsync(() => downloader.DownloadAsync(Url("/photo.png"), _dir.Path, null, null, ct)); + + Assert.Equal("error.web_local_address", error.ErrorKey); + Assert.False(proxy.Request.IsCompleted); + } + + /// A proxy on 127.0.0.1 that reads one request's head and answers with fixed bytes. + private sealed class FakeProxy : IAsyncDisposable + { + private readonly TcpListener _server = new(IPAddress.Loopback, 0); + private readonly CancellationTokenSource _stop = new(); + + public FakeProxy(byte[] answer) + { + _server.Start(); + Address = $"http://127.0.0.1:{((IPEndPoint)_server.LocalEndpoint).Port}"; + Request = Task.Run(async () => + { + using var client = await _server.AcceptTcpClientAsync(_stop.Token); + await using var stream = client.GetStream(); + var head = new List(); + var one = new byte[1]; + while (!head.TakeLast(4).SequenceEqual("\r\n\r\n"u8.ToArray()) && await stream.ReadAsync(one, _stop.Token) == 1) + head.Add(one[0]); + await stream.WriteAsync(answer, _stop.Token); + return System.Text.Encoding.ASCII.GetString([.. head]); + }); + } + + public string Address { get; } + + /// The request the proxy got, from its first line on. + public Task Request { get; } + + public async ValueTask DisposeAsync() + { + await _stop.CancelAsync(); + _server.Stop(); + try + { + await Request; + } + catch (Exception ex) when (ex is OperationCanceledException or SocketException or IOException or ObjectDisposedException + or InvalidOperationException) + { + // Never connected to, or the client hung up; or stopped before the accept even began. + } + _stop.Dispose(); + } + } + private sealed class ProgressCallback(Action report) : IProgress { public void Report(TransferProgress value) => report(value);