diff --git a/apps/desktop/src/composables/useAppUpdater.ts b/apps/desktop/src/composables/useAppUpdater.ts index fefa48ba8..b3a079cdf 100644 --- a/apps/desktop/src/composables/useAppUpdater.ts +++ b/apps/desktop/src/composables/useAppUpdater.ts @@ -8,6 +8,16 @@ export function shouldOpenUpdateDialog(options: { silent?: boolean }) { return options.silent !== true; } +export async function resolveUpdaterProxy(): Promise { + if (!isTauriRuntime()) return undefined; + try { + const proxy = await api.getSystemProxyUrl(); + return proxy || undefined; + } catch { + return undefined; + } +} + export function useAppUpdater() { const { t } = useI18n(); const { toast } = useToast(); @@ -74,7 +84,8 @@ export function useAppUpdater() { downloadProgress.value = 0; try { const { check } = await import("@tauri-apps/plugin-updater"); - const update = await check(); + const proxy = await resolveUpdaterProxy(); + const update = await check(proxy ? { proxy } : undefined); if (!update) return; let totalBytes = 0; let downloadedBytes = 0; diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 1efeec9da..4613afc12 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -213,6 +213,7 @@ export const deleteHistoryEntry = forward("deleteHistoryEntry"); // Updates export const checkForUpdates = forward("checkForUpdates"); +export const getSystemProxyUrl = forward("getSystemProxyUrl"); export const getAppVersion = forward("getAppVersion"); // Layout diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index f86f97883..249588675 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -1258,6 +1258,10 @@ export async function checkForUpdates(): Promise { return get("/api/update/check"); } +export async function getSystemProxyUrl(): Promise { + return null; +} + export async function getAppVersion(): Promise { const res: { version: string } = await get("/api/version"); return res.version; diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 1c5bffd34..acf55aa10 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -830,6 +830,10 @@ export async function checkForUpdates(): Promise { return invoke("check_for_updates"); } +export async function getSystemProxyUrl(): Promise { + return invoke("get_system_proxy_url"); +} + export async function getAppVersion(): Promise { const { getVersion } = await import("@tauri-apps/api/app"); return getVersion(); diff --git a/crates/dbx-core/src/update.rs b/crates/dbx-core/src/update.rs index 8f530157e..c46a91622 100644 --- a/crates/dbx-core/src/update.rs +++ b/crates/dbx-core/src/update.rs @@ -41,10 +41,7 @@ pub struct UpdateInfo { } pub async fn fetch_latest_release() -> Result { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .build() - .map_err(|e| format!("Failed to create HTTP client: {e}"))?; + let client = build_update_http_client()?; let resp = crate::race_download(&client, LATEST_JSON_PATH, LATEST_JSON_R2_PATH, "dbx-update-checker") .await @@ -57,6 +54,132 @@ pub async fn fetch_latest_release() -> Result { Ok(release) } +fn build_update_http_client() -> Result { + let mut builder = + reqwest::Client::builder().timeout(std::time::Duration::from_secs(10)).user_agent("dbx-update-checker"); + + if let Some(proxy_url) = system_proxy_url() { + let proxy = reqwest::Proxy::all(&proxy_url).map_err(|e| format!("Invalid system proxy URL: {e}"))?; + builder = builder.proxy(proxy); + } + + builder.build().map_err(|e| format!("Failed to create HTTP client: {e}")) +} + +pub fn system_proxy_url() -> Option { + system_proxy_url_from_platform() +} + +#[cfg(target_os = "macos")] +fn system_proxy_url_from_platform() -> Option { + let output = std::process::Command::new("scutil").arg("--proxy").output().ok()?; + if !output.status.success() { + return None; + } + let stdout = String::from_utf8(output.stdout).ok()?; + system_proxy_url_from_scutil_output(&stdout) +} + +#[cfg(target_os = "windows")] +fn system_proxy_url_from_platform() -> Option { + let key = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings"; + let proxy_enable = std::process::Command::new("reg").args(["query", key, "/v", "ProxyEnable"]).output().ok()?; + let proxy_server = std::process::Command::new("reg").args(["query", key, "/v", "ProxyServer"]).output().ok()?; + if !proxy_enable.status.success() || !proxy_server.status.success() { + return None; + } + let proxy_enable = String::from_utf8(proxy_enable.stdout).ok()?; + let proxy_server = String::from_utf8(proxy_server.stdout).ok()?; + system_proxy_url_from_windows_registry_output(&proxy_enable, &proxy_server) +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +fn system_proxy_url_from_platform() -> Option { + None +} + +#[cfg_attr(not(test), allow(dead_code))] +fn system_proxy_url_from_scutil_output(output: &str) -> Option { + let value = |key: &str| { + output.lines().find_map(|line| { + let (line_key, line_value) = line.split_once(':')?; + (line_key.trim() == key).then(|| line_value.trim()) + }) + }; + + if value("HTTPSEnable") == Some("1") { + if let Some(url) = proxy_url(value("HTTPSProxy")?, value("HTTPSPort")?) { + return Some(url); + } + } + + if value("HTTPEnable") == Some("1") { + if let Some(url) = proxy_url(value("HTTPProxy")?, value("HTTPPort")?) { + return Some(url); + } + } + + None +} + +#[cfg_attr(not(test), allow(dead_code))] +fn system_proxy_url_from_windows_registry_output(proxy_enable: &str, proxy_server: &str) -> Option { + let enabled = proxy_enable + .lines() + .find(|line| line.contains("ProxyEnable"))? + .split_whitespace() + .last() + .is_some_and(|value| value == "0x1" || value == "1"); + if !enabled { + return None; + } + + let server = proxy_server.lines().find(|line| line.contains("ProxyServer"))?.split_whitespace().last()?; + + proxy_url_from_windows_proxy_server(server) +} + +fn proxy_url_from_windows_proxy_server(server: &str) -> Option { + let entries = server.split(';').map(str::trim).filter(|entry| !entry.is_empty()).collect::>(); + + for key in ["https=", "http="] { + if let Some(entry) = entries.iter().find_map(|entry| entry.strip_prefix(key)) { + if let Some(url) = proxy_url_from_host_port(entry) { + return Some(url); + } + } + } + + entries.iter().find(|entry| !entry.contains('=')).and_then(|entry| proxy_url_from_host_port(entry)) +} + +fn proxy_url_from_host_port(value: &str) -> Option { + let value = value.trim(); + if value.starts_with("http://") || value.starts_with("https://") { + return Some(value.to_string()); + } + if value.starts_with("socks://") || value.starts_with("socks5://") || value.starts_with("socks5h://") { + return None; + } + + let (host, port) = if let Some(rest) = value.strip_prefix('[') { + let (host, rest) = rest.split_once(']')?; + let port = rest.strip_prefix(':')?; + (host, port) + } else { + value.rsplit_once(':')? + }; + proxy_url(host, port) +} + +fn proxy_url(host: &str, port: &str) -> Option { + if host.is_empty() || port.parse::().is_err() { + return None; + } + let host = if host.contains(':') && !host.starts_with('[') { format!("[{host}]") } else { host.to_string() }; + Some(format!("http://{host}:{port}")) +} + async fn fetch_github_release_metadata( client: &reqwest::Client, version: &str, @@ -141,7 +264,10 @@ pub fn is_newer_version(latest: &str, current: &str) -> bool { #[cfg(test)] mod tests { - use super::{build_update_info, is_newer_version, normalize_version, GithubReleaseMetadata, TauriRelease}; + use super::{ + build_update_info, is_newer_version, normalize_version, system_proxy_url_from_scutil_output, + system_proxy_url_from_windows_registry_output, GithubReleaseMetadata, TauriRelease, + }; #[test] fn normalizes_tag_versions() { @@ -157,6 +283,63 @@ mod tests { assert!(!is_newer_version("0.1.9", "0.2.0")); } + #[test] + fn parses_macos_https_system_proxy() { + let output = r#" { + HTTPEnable : 1 + HTTPPort : 7890 + HTTPProxy : 127.0.0.1 + HTTPSEnable : 1 + HTTPSPort : 7891 + HTTPSProxy : 127.0.0.1 +}"#; + + assert_eq!(system_proxy_url_from_scutil_output(output), Some("http://127.0.0.1:7891".to_string())); + } + + #[test] + fn falls_back_to_macos_http_system_proxy() { + let output = r#" { + HTTPEnable : 1 + HTTPPort : 7890 + HTTPProxy : 127.0.0.1 + HTTPSEnable : 0 +}"#; + + assert_eq!(system_proxy_url_from_scutil_output(output), Some("http://127.0.0.1:7890".to_string())); + } + + #[test] + fn ignores_disabled_or_incomplete_macos_system_proxy() { + assert_eq!(system_proxy_url_from_scutil_output("HTTPEnable : 0\nHTTPProxy : 127.0.0.1\nHTTPPort : 7890"), None); + assert_eq!(system_proxy_url_from_scutil_output("HTTPEnable : 1\nHTTPProxy : 127.0.0.1"), None); + } + + #[test] + fn parses_windows_system_proxy() { + let enabled = r#" +HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings + ProxyEnable REG_DWORD 0x1 +"#; + let server = r#" +HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings + ProxyServer REG_SZ http=127.0.0.1:7890;https=127.0.0.1:7891 +"#; + + assert_eq!( + system_proxy_url_from_windows_registry_output(enabled, server), + Some("http://127.0.0.1:7891".to_string()) + ); + } + + #[test] + fn ignores_disabled_windows_system_proxy() { + let disabled = "ProxyEnable REG_DWORD 0x0"; + let server = "ProxyServer REG_SZ 127.0.0.1:7890"; + + assert_eq!(system_proxy_url_from_windows_registry_output(disabled, server), None); + } + #[test] fn parses_jdbc_plugin_metadata_from_latest_json() { let release: TauriRelease = serde_json::from_str( diff --git a/packages/app-tests/appUpdateBadge.test.ts b/packages/app-tests/appUpdateBadge.test.ts index 82551895c..c1517605b 100644 --- a/packages/app-tests/appUpdateBadge.test.ts +++ b/packages/app-tests/appUpdateBadge.test.ts @@ -34,6 +34,13 @@ test("app passes update availability to the toolbar badge", () => { assert.match(source, /:has-update-available="hasUpdateAvailable"/); }); +test("updater download passes system proxy to tauri updater check", () => { + const source = readFileSync("apps/desktop/src/composables/useAppUpdater.ts", "utf8"); + + assert.match(source, /getSystemProxyUrl/); + assert.match(source, /check\(proxy \? \{ proxy } : undefined\)/); +}); + test("driver manager entry can show an update count badge", () => { const toolbarSource = readFileSync("apps/desktop/src/components/layout/AppToolbar.vue", "utf8"); const tabSource = readFileSync("apps/desktop/src/components/layout/AppTabBar.vue", "utf8"); diff --git a/src-tauri/src/commands/update.rs b/src-tauri/src/commands/update.rs index b001118c5..40d900074 100644 --- a/src-tauri/src/commands/update.rs +++ b/src-tauri/src/commands/update.rs @@ -6,3 +6,8 @@ pub async fn check_for_updates() -> Result { let current_version = env!("CARGO_PKG_VERSION"); Ok(dbx_core::update::build_update_info(release, current_version)) } + +#[tauri::command] +pub fn get_system_proxy_url() -> Option { + dbx_core::update::system_proxy_url() +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3944506ab..da83f73f2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -369,6 +369,7 @@ pub fn run() { commands::history::clear_history, commands::history::delete_history_entry, commands::update::check_for_updates, + commands::update::get_system_proxy_url, commands::transfer::start_transfer, commands::transfer::cancel_transfer, commands::database_export::export_database_sql,