diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 281f943..d1782e2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -19,15 +19,6 @@ dependencies = [ "cpufeatures 0.3.1", ] -[[package]] -name = "aho-corasick" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" -dependencies = [ - "memchr", -] - [[package]] name = "aho-corasick" version = "1.1.5" @@ -66,7 +57,7 @@ checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" dependencies = [ "android_log-sys", "env_filter", - "log 0.4.34", + "log", ] [[package]] @@ -91,7 +82,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a" dependencies = [ "keyring-core", - "log 0.4.34", + "log", "security-framework", ] @@ -103,7 +94,7 @@ checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" dependencies = [ "clipboard-win", "image", - "log 0.4.34", + "log", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -1232,7 +1223,7 @@ dependencies = [ "rustc_version", "toml 1.1.5+spec-1.1.0", "vswhom", - "winreg 0.55.0", + "winreg", ] [[package]] @@ -1283,8 +1274,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ - "log 0.4.34", - "regex 1.13.1", + "log", + "regex", ] [[package]] @@ -1379,7 +1370,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" dependencies = [ - "log 0.4.34", + "log", ] [[package]] @@ -1388,7 +1379,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset 0.9.1", + "memoffset", "rustc_version", ] @@ -1411,9 +1402,9 @@ dependencies = [ "hmac", "keepawake", "keyring-core", - "log 0.4.34", + "log", "objc", - "regex 1.13.1", + "regex", "reqwest", "rusqlite", "rustls", @@ -1423,7 +1414,6 @@ dependencies = [ "sha1", "sha2 0.11.0", "sysinfo", - "sysproxy", "system_shutdown", "tauri", "tauri-build", @@ -1918,21 +1908,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "handlebars" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb04af2006ea09d985fef82b81e0eb25337e51b691c76403332378a53d521edc" -dependencies = [ - "lazy_static 0.2.11", - "log 0.3.9", - "pest", - "quick-error 1.2.3", - "regex 0.2.11", - "serde", - "serde_json", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -2029,7 +2004,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ - "log 0.4.34", + "log", "markup5ever", ] @@ -2156,7 +2131,7 @@ dependencies = [ "core-foundation-sys", "iana-time-zone-haiku", "js-sys", - "log 0.4.34", + "log", "wasm-bindgen", "windows-core 0.62.2", ] @@ -2346,22 +2321,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "interfaces" -version = "0.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec8f50a973916cac3da5057c986db05cd3346f38c78e9bc24f64cc9f6a3978f" -dependencies = [ - "bitflags 1.3.2", - "cc", - "handlebars", - "lazy_static 1.5.0", - "libc", - "nix", - "serde", - "serde_derive", -] - [[package]] name = "ipnet" version = "2.12.1" @@ -2426,7 +2385,7 @@ dependencies = [ "jiff-core", "jiff-static", "jiff-tzdb-platform", - "log 0.4.34", + "log", "portable-atomic", "portable-atomic-util", "serde_core", @@ -2479,7 +2438,7 @@ dependencies = [ "cfg-if", "combine", "jni-sys 0.3.1", - "log 0.4.34", + "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", @@ -2495,7 +2454,7 @@ dependencies = [ "combine", "jni-macros", "jni-sys 0.4.1", - "log 0.4.34", + "log", "simd_cesu8", "thiserror 2.0.20", "walkdir", @@ -2608,21 +2567,9 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" dependencies = [ - "log 0.4.34", + "log", ] -[[package]] -name = "lazy_static" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libappindicator" version = "0.9.0" @@ -2633,7 +2580,7 @@ dependencies = [ "gtk", "gtk-sys", "libappindicator-sys", - "log 0.4.34", + "log", ] [[package]] @@ -2713,15 +2660,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" -dependencies = [ - "log 0.4.34", -] - [[package]] name = "log" version = "0.4.34" @@ -2735,7 +2673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" dependencies = [ "cc", - "log 0.4.34", + "log", "objc2", "objc2-foundation", "time", @@ -2757,7 +2695,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ - "log 0.4.34", + "log", "tendril", "web_atoms", ] @@ -2774,15 +2712,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -2868,7 +2797,7 @@ checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ "bitflags 2.13.1", "jni-sys 0.3.1", - "log 0.4.34", + "log", "ndk-sys", "num_enum", "raw-window-handle", @@ -2890,19 +2819,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nom" version = "8.0.0" @@ -2919,7 +2835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" dependencies = [ "futures-lite", - "log 0.4.34", + "log", "mac-notification-sys", "serde", "tauri-winrt-notification", @@ -3396,12 +3312,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pest" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6dda33d67c26f0aac90d324ab2eb7239c819fc7b2552fe9faa4fe88441edc8" - [[package]] name = "petgraph" version = "0.8.3" @@ -3655,12 +3565,6 @@ version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-error" version = "2.0.1" @@ -3789,29 +3693,16 @@ dependencies = [ "syn 3.0.4", ] -[[package]] -name = "regex" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" -dependencies = [ - "aho-corasick 0.6.10", - "memchr", - "regex-syntax 0.5.6", - "thread_local", - "utf8-ranges", -] - [[package]] name = "regex" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ - "aho-corasick 1.1.5", + "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.11", + "regex-syntax", ] [[package]] @@ -3820,18 +3711,9 @@ version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ - "aho-corasick 1.1.5", + "aho-corasick", "memchr", - "regex-syntax 0.8.11", -] - -[[package]] -name = "regex-syntax" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" -dependencies = [ - "ucd-util", + "regex-syntax", ] [[package]] @@ -3857,7 +3739,7 @@ dependencies = [ "hyper-rustls", "hyper-util", "js-sys", - "log 0.4.34", + "log", "percent-encoding", "pin-project-lite", "rustls", @@ -3891,7 +3773,7 @@ dependencies = [ "gobject-sys", "gtk-sys", "js-sys", - "log 0.4.34", + "log", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -4024,7 +3906,7 @@ dependencies = [ "core-foundation", "core-foundation-sys", "jni 0.22.4", - "log 0.4.34", + "log", "once_cell", "rustls", "rustls-native-certs", @@ -4191,7 +4073,7 @@ dependencies = [ "bitflags 2.13.1", "cssparser", "derive_more", - "log 0.4.34", + "log", "new_debug_unreachable", "phf", "phf_codegen", @@ -4704,18 +4586,6 @@ dependencies = [ "windows 0.62.2", ] -[[package]] -name = "sysproxy" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9707a79d3b95683aa5a9521e698ffd878b8fb289727c25a69157fb85d529ffff" -dependencies = [ - "interfaces", - "thiserror 1.0.69", - "winapi", - "winreg 0.10.1", -] - [[package]] name = "system-deps" version = "6.2.2" @@ -4759,7 +4629,7 @@ dependencies = [ "gtk", "jni 0.21.1", "libc", - "log 0.4.34", + "log", "ndk", "ndk-sys", "objc2", @@ -4816,7 +4686,7 @@ dependencies = [ "image", "jni 0.21.1", "libc", - "log 0.4.34", + "log", "mime", "muda", "objc2", @@ -4933,7 +4803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4136fb69d967753d000423d7e5f863f89bf949efbdfbecb43a580426a01a0194" dependencies = [ "arboard", - "log 0.4.34", + "log", "serde", "serde_json", "tauri", @@ -4968,7 +4838,7 @@ version = "2.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61854a36651aa48381e5e209f69a01273b77f3f9f91f0c430b1b98d33bd47229" dependencies = [ - "log 0.4.34", + "log", "raw-window-handle", "rfd", "serde", @@ -4989,7 +4859,7 @@ dependencies = [ "anyhow", "dunce", "glob", - "log 0.4.34", + "log", "objc2-foundation", "percent-encoding", "schemars 0.8.22", @@ -5012,7 +4882,7 @@ checksum = "b4e8861142c21636b03ff6eb9682a073814112e35220435670738a8be7b49896" dependencies = [ "android_logger", "fern", - "log 0.4.34", + "log", "objc2", "objc2-foundation", "serde", @@ -5031,7 +4901,7 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad2fd40946aef810c4be9fd33a2d1b9b397cb79042b2d21c81a0a8f204354fd1" dependencies = [ - "log 0.4.34", + "log", "notify-rust", "rand 0.9.5", "serde", @@ -5073,10 +4943,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8548af174c5516e4f71f142acea4d02e00316296ea9aafa58798851481003e3c" dependencies = [ "encoding_rs", - "log 0.4.34", + "log", "open", "os_pipe", - "regex 1.13.1", + "regex", "schemars 0.8.22", "serde", "serde_json", @@ -5138,7 +5008,7 @@ dependencies = [ "gtk", "http", "jni 0.21.1", - "log 0.4.34", + "log", "objc2", "objc2-app-kit", "once_cell", @@ -5171,13 +5041,13 @@ dependencies = [ "http", "infer", "json-patch", - "log 0.4.34", + "log", "memchr", "phf", "plist", "proc-macro2", "quote", - "regex 1.13.1", + "regex", "schemars 0.8.22", "semver", "serde", @@ -5286,15 +5156,6 @@ dependencies = [ "syn 3.0.4", ] -[[package]] -name = "thread_local" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" -dependencies = [ - "lazy_static 1.5.0", -] - [[package]] name = "tiff" version = "0.11.3" @@ -5304,7 +5165,7 @@ dependencies = [ "fax", "flate2", "half", - "quick-error 2.0.1", + "quick-error", "weezl", "zune-jpeg", ] @@ -5419,7 +5280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" dependencies = [ "futures-util", - "log 0.4.34", + "log", "tokio", "tungstenite", ] @@ -5626,7 +5487,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log 0.4.34", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5660,7 +5521,7 @@ checksum = "08676401d031c4a43b7022d90e1748ef4624fabb68ac2abf0cb5812f8c02d78e" dependencies = [ "chrono", "libc", - "log 0.4.34", + "log", "objc2", "objc2-foundation", "once_cell", @@ -5742,7 +5603,7 @@ dependencies = [ "data-encoding", "http", "httparse", - "log 0.4.34", + "log", "rand 0.10.2", "sha1", "thiserror 2.0.20", @@ -5760,19 +5621,13 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "ucd-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" - [[package]] name = "uds_windows" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ - "memoffset 0.9.1", + "memoffset", "tempfile", "windows-sys 0.61.2", ] @@ -5870,18 +5725,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" dependencies = [ - "regex 1.13.1", + "regex", "serde", "unic-ucd-ident", "url", ] -[[package]] -name = "utf8-ranges" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -6450,7 +6299,7 @@ checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8" dependencies = [ "byteorder", "keyring-core", - "regex 1.13.1", + "regex", "windows-sys 0.61.2", "zeroize", ] @@ -6813,15 +6662,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.55.0" @@ -6845,7 +6685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" dependencies = [ "libc", - "log 0.4.34", + "log", "os_pipe", "rustix", "thiserror 2.0.20", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5524243..81fc13f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,7 +48,6 @@ tempfile = "3" thiserror = "2.0.20" axum = "0.8.9" tower-http = { version = "0.7", features = ["cors", "limit"] } -sysproxy = "0.3.0" semver = "1.0.28" keepawake = "0.6.1" system_shutdown = "4.1.0" diff --git a/src-tauri/src/parity.rs b/src-tauri/src/parity.rs index 4c29d3f..183df88 100644 --- a/src-tauri/src/parity.rs +++ b/src-tauri/src/parity.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[cfg(any(target_os = "macos", target_os = "linux"))] -use std::process::Command; +use std::{process::Stdio, time::Duration}; +use tokio::io::AsyncReadExt; use ts_rs::TS; use crate::ipc::DownloadCategory; @@ -8,66 +8,114 @@ use crate::ipc::DownloadCategory; #[tauri::command] pub async fn get_system_proxy(caller: tauri::WebviewWindow) -> Result, String> { crate::properties_window::ensure_main_window(&caller)?; - match native_system_proxy() { + match native_system_proxy(&SystemProxyCommandRunner).await { Ok(Some(proxy)) => Ok(Some(proxy)), Ok(None) => Ok(proxy_from_environment()), - Err(native_error) => match sysproxy::Sysproxy::get_system_proxy() { - Ok(proxy) if proxy.enable => { - if proxy.host.contains('=') { - Ok(parse_windows_proxy_server(&proxy.host).or_else(proxy_from_environment)) - } else { - Ok(normalize_sysproxy_address(&proxy.host, proxy.port) - .or_else(proxy_from_environment)) - } + Err(native_error) => proxy_from_environment() + .map(Some) + .ok_or_else(|| format!("failed to read system proxy settings: {native_error}")), + } +} + +const PROXY_COMMAND_TIMEOUT: Duration = Duration::from_secs(5); +const PROXY_COMMAND_OUTPUT_LIMIT: u64 = 64 * 1024; + +#[async_trait::async_trait] +trait ProxyCommandRunner: Sync { + async fn stdout(&self, program: &str, args: &[String]) -> Result; +} + +struct SystemProxyCommandRunner; + +#[async_trait::async_trait] +impl ProxyCommandRunner for SystemProxyCommandRunner { + async fn stdout(&self, program: &str, args: &[String]) -> Result { + let mut command = tokio::process::Command::new(program); + command + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .kill_on_drop(true); + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + command.as_std_mut().creation_flags(CREATE_NO_WINDOW); + } + + let mut child = command + .spawn() + .map_err(|error| format!("{program} is unavailable: {error}"))?; + let stdout = child + .stdout + .take() + .ok_or_else(|| format!("failed to capture {program} output"))?; + let operation = async { + let mut bytes = Vec::new(); + stdout + .take(PROXY_COMMAND_OUTPUT_LIMIT + 1) + .read_to_end(&mut bytes) + .await + .map_err(|error| format!("failed to read {program} output: {error}"))?; + if bytes.len() as u64 > PROXY_COMMAND_OUTPUT_LIMIT { + return Err(format!("{program} output exceeded the safety limit")); } - Ok(_) => Ok(proxy_from_environment()), - Err(error) => proxy_from_environment().map(Some).ok_or_else(|| { - format!( - "failed to read system proxy settings: {native_error}; sysproxy fallback: {error}" - ) - }), - }, + let status = child + .wait() + .await + .map_err(|error| format!("failed to wait for {program}: {error}"))?; + if !status.success() { + return Err(format!("{program} exited unsuccessfully")); + } + String::from_utf8(bytes).map_err(|_| format!("{program} returned non-UTF-8 output")) + }; + + tokio::time::timeout(PROXY_COMMAND_TIMEOUT, operation) + .await + .map_err(|_| format!("{program} timed out"))? } } #[cfg(target_os = "windows")] -fn native_system_proxy() -> Result, String> { - fallback_windows_proxy().map_err(|_| "failed to read Windows proxy registry".to_string()) +async fn native_system_proxy(runner: &dyn ProxyCommandRunner) -> Result, String> { + windows_system_proxy(runner).await } #[cfg(target_os = "macos")] -fn native_system_proxy() -> Result, String> { - let proxy = sysproxy::Sysproxy::get_system_proxy().map_err(|error| error.to_string())?; - if !proxy.enable { - return Ok(None); - } - Ok(macos_proxy_for_host_port(&proxy.host, proxy.port) - .unwrap_or_else(|| { - normalize_sysproxy_address(&proxy.host, proxy.port) - .unwrap_or_else(|| format!("http://{}:{}", proxy.host, proxy.port)) - }) - .into()) +async fn native_system_proxy(runner: &dyn ProxyCommandRunner) -> Result, String> { + macos_system_proxy(runner).await } #[cfg(target_os = "linux")] -fn native_system_proxy() -> Result, String> { - let mode = - command_stdout(Command::new("gsettings").args(["get", "org.gnome.system.proxy", "mode"])) - .map_err(|error| error.to_string())?; +async fn native_system_proxy(runner: &dyn ProxyCommandRunner) -> Result, String> { + let mode = runner + .stdout( + "gsettings", + &string_args(&["get", "org.gnome.system.proxy", "mode"]), + ) + .await?; if strip_gsettings_string(&mode) != "manual" { return Ok(None); } - Ok(linux_gsettings_proxy("https", "http") - .or_else(|| linux_gsettings_proxy("http", "http")) - .or_else(|| linux_gsettings_proxy("socks", "socks5"))) + for (service, scheme) in [("https", "http"), ("http", "http"), ("socks", "socks5")] { + if let Some(proxy) = linux_gsettings_proxy(runner, service, scheme).await { + return Ok(Some(proxy)); + } + } + Ok(None) } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] -fn native_system_proxy() -> Result, String> { +async fn native_system_proxy(_runner: &dyn ProxyCommandRunner) -> Result, String> { Ok(None) } +fn string_args(args: &[&str]) -> Vec { + args.iter().map(|value| (*value).to_string()).collect() +} + fn proxy_from_environment() -> Option { [ "HTTPS_PROXY", @@ -85,18 +133,6 @@ fn proxy_from_environment() -> Option { }) } -#[cfg(any(target_os = "macos", target_os = "linux"))] -fn command_stdout(command: &mut Command) -> std::io::Result { - let output = command.output()?; - if !output.status.success() { - return Err(std::io::Error::other(format!( - "command exited with {}", - output.status - ))); - } - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - fn normalize_proxy_address(raw: &str, default_scheme: &str) -> Option { let trimmed = raw.trim().trim_matches('"').trim_end_matches('/'); if trimmed.is_empty() { @@ -117,27 +153,7 @@ fn normalize_proxy_address(raw: &str, default_scheme: &str) -> Option { Some(candidate) } -fn normalize_sysproxy_address(host: &str, port: u16) -> Option { - let host = host.trim(); - if host.is_empty() { - return None; - } - - if host.contains("://") { - let mut parsed = url::Url::parse(host).ok()?; - if parsed.port().is_none() && port != 0 { - parsed.set_port(Some(port)).ok()?; - } - return normalize_proxy_address(parsed.as_str(), "http"); - } - - if port == 0 { - normalize_proxy_address(host, "http") - } else { - normalize_proxy_address(&format!("{host}:{port}"), "http") - } -} - +#[cfg_attr(not(target_os = "windows"), allow(dead_code))] fn parse_windows_proxy_server(value: &str) -> Option { let value = value.trim().trim_matches('"'); if value.is_empty() { @@ -169,27 +185,32 @@ fn parse_windows_proxy_server(value: &str) -> Option { } #[cfg(target_os = "macos")] -fn macos_proxy_for_host_port(host: &str, port: u16) -> Option { - let services_output = - command_stdout(Command::new("networksetup").arg("-listallnetworkservices")).ok()?; - for service in parse_macos_network_services(&services_output) { - for (target, scheme) in [ - ("securewebproxy", "http"), - ("webproxy", "http"), - ("socksfirewallproxy", "socks5"), - ] { - let output = command_stdout( - Command::new("networksetup").args([format!("-get{target}"), service.clone()]), - ) - .ok()?; - if let Some(proxy) = parse_macos_networksetup_proxy(&output, scheme) - .filter(|proxy| proxy_matches_host_port(proxy, host, port)) - { - return Some(proxy); +async fn macos_system_proxy(runner: &dyn ProxyCommandRunner) -> Result, String> { + let services_output = runner + .stdout("networksetup", &string_args(&["-listallnetworkservices"])) + .await?; + let services = parse_macos_network_services(&services_output); + let mut successful_probe = false; + for (target, scheme) in [ + ("securewebproxy", "http"), + ("webproxy", "http"), + ("socksfirewallproxy", "socks5"), + ] { + for service in &services { + let args = vec![format!("-get{target}"), service.clone()]; + if let Ok(output) = runner.stdout("networksetup", &args).await { + successful_probe = true; + if let Some(proxy) = parse_macos_networksetup_proxy(&output, scheme) { + return Ok(Some(proxy)); + } } } } - None + if !services.is_empty() && !successful_probe { + Err("failed to query enabled macOS proxy settings".to_string()) + } else { + Ok(None) + } } #[cfg_attr(not(target_os = "macos"), allow(dead_code))] @@ -217,13 +238,6 @@ fn parse_macos_networksetup_proxy(output: &str, scheme: &str) -> Option } #[cfg_attr(not(target_os = "macos"), allow(dead_code))] -fn proxy_matches_host_port(proxy: &str, host: &str, port: u16) -> bool { - let Ok(parsed) = url::Url::parse(proxy) else { - return false; - }; - parsed.host_str() == Some(host) && parsed.port() == Some(port) -} - #[cfg_attr(not(target_os = "macos"), allow(dead_code))] fn macos_networksetup_value<'a>(output: &'a str, key: &str) -> Option<&'a str> { output @@ -234,14 +248,24 @@ fn macos_networksetup_value<'a>(output: &'a str, key: &str) -> Option<&'a str> { } #[cfg(target_os = "linux")] -fn linux_gsettings_proxy(service: &str, scheme: &str) -> Option { +async fn linux_gsettings_proxy( + runner: &dyn ProxyCommandRunner, + service: &str, + scheme: &str, +) -> Option { let schema = format!("org.gnome.system.proxy.{service}"); - let host = command_stdout(Command::new("gsettings").args(["get", &schema, "host"])).ok()?; + let host = runner + .stdout("gsettings", &string_args(&["get", &schema, "host"])) + .await + .ok()?; let host = strip_gsettings_string(&host); if host.is_empty() { return None; } - let port = command_stdout(Command::new("gsettings").args(["get", &schema, "port"])).ok()?; + let port = runner + .stdout("gsettings", &string_args(&["get", &schema, "port"])) + .await + .ok()?; let port = port.trim(); normalize_proxy_address(&format!("{scheme}://{host}:{port}"), scheme) } @@ -256,51 +280,37 @@ fn strip_gsettings_string(value: &str) -> String { } #[cfg(target_os = "windows")] -fn fallback_windows_proxy() -> Result, ()> { - use std::os::windows::process::CommandExt; - use std::process::Command; - const CREATE_NO_WINDOW: u32 = 0x08000000; - - let output = Command::new("reg") - .args(&[ - "query", - "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", - "/v", - "ProxyEnable", - ]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .map_err(|_| ())?; - - if !output.status.success() { - return Err(()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - let enabled = registry_value(&stdout, "ProxyEnable") +async fn windows_system_proxy(runner: &dyn ProxyCommandRunner) -> Result, String> { + let output = runner + .stdout( + "reg", + &string_args(&[ + "query", + "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", + "/v", + "ProxyEnable", + ]), + ) + .await?; + let enabled = registry_value(&output, "ProxyEnable") .as_deref() .is_some_and(windows_proxy_enabled); if !enabled { return Ok(None); } - let output = Command::new("reg") - .args(&[ - "query", - "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", - "/v", - "ProxyServer", - ]) - .creation_flags(CREATE_NO_WINDOW) - .output() - .map_err(|_| ())?; - - if !output.status.success() { - return Err(()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - Ok(registry_value(&stdout, "ProxyServer").and_then(|value| parse_windows_proxy_server(&value))) + let output = runner + .stdout( + "reg", + &string_args(&[ + "query", + "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", + "/v", + "ProxyServer", + ]), + ) + .await?; + Ok(registry_value(&output, "ProxyServer").and_then(|value| parse_windows_proxy_server(&value))) } #[cfg_attr(not(target_os = "windows"), allow(dead_code))] @@ -341,11 +351,40 @@ fn registry_value(output: &str, name: &str) -> Option { #[cfg(test)] mod proxy_tests { use super::{ - normalize_proxy_address, normalize_sysproxy_address, parse_macos_network_services, - parse_macos_networksetup_proxy, parse_windows_proxy_server, proxy_matches_host_port, - registry_value, strip_gsettings_string, windows_proxy_enabled, + normalize_proxy_address, parse_macos_network_services, parse_macos_networksetup_proxy, + parse_windows_proxy_server, registry_value, strip_gsettings_string, windows_proxy_enabled, + ProxyCommandRunner, SystemProxyCommandRunner, }; + #[cfg(target_os = "macos")] + struct MockProxyCommandRunner; + + #[cfg(target_os = "macos")] + #[async_trait::async_trait] + impl ProxyCommandRunner for MockProxyCommandRunner { + async fn stdout(&self, program: &str, args: &[String]) -> Result { + assert_eq!(program, "networksetup"); + match args + .iter() + .map(String::as_str) + .collect::>() + .as_slice() + { + ["-listallnetworkservices"] => Ok("Wi-Fi\nEthernet\n".to_string()), + ["-getsecurewebproxy", "Wi-Fi"] => { + Ok("Enabled: No\nServer: ignored.example\nPort: 443\n".to_string()) + } + ["-getsecurewebproxy", "Ethernet"] => { + Ok("Enabled: Yes\nServer: secure.example\nPort: 8443\n".to_string()) + } + ["-getwebproxy", _] | ["-getsocksfirewallproxy", _] => { + panic!("lower-priority proxy was queried after HTTPS succeeded") + } + _ => Err("unexpected command".to_string()), + } + } + } + #[test] fn normalizes_bare_proxy_addresses() { assert_eq!( @@ -375,22 +414,6 @@ mod proxy_tests { ); } - #[test] - fn normalizes_sysproxy_host_without_duplicating_ports() { - assert_eq!( - normalize_sysproxy_address("http://proxy.local", 8080).as_deref(), - Some("http://proxy.local:8080") - ); - assert_eq!( - normalize_sysproxy_address("http://proxy.local:9000", 8080).as_deref(), - Some("http://proxy.local:9000") - ); - assert_eq!( - normalize_sysproxy_address("proxy.local", 8080).as_deref(), - Some("http://proxy.local:8080") - ); - } - #[test] fn parses_macos_proxy_outputs_with_scheme() { let services = r#" @@ -414,21 +437,34 @@ Authenticated Proxy Enabled: 0 parse_macos_networksetup_proxy(proxy, "socks5").as_deref(), Some("socks5://127.0.0.1:1080") ); - assert!(proxy_matches_host_port( - "socks5://127.0.0.1:1080", - "127.0.0.1", - 1080 - )); - assert!(!proxy_matches_host_port( - "socks5://127.0.0.1:1080", - "127.0.0.1", - 1081 - )); - let disabled = proxy.replace("Enabled: Yes", "Enabled: No"); assert_eq!(parse_macos_networksetup_proxy(&disabled, "socks5"), None); } + #[cfg(any(target_os = "macos", target_os = "linux"))] + #[tokio::test] + async fn bounds_native_command_runtime_and_output() { + let runner = SystemProxyCommandRunner; + let oversized = format!("print('x' * {})", super::PROXY_COMMAND_OUTPUT_LIMIT + 1); + let error = runner + .stdout("python3", &["-c".to_string(), oversized]) + .await + .expect_err("oversized output must fail closed"); + assert!(error.contains("safety limit")); + } + + #[cfg(target_os = "macos")] + #[tokio::test] + async fn selects_https_across_services_before_lower_priority_proxies() { + assert_eq!( + super::macos_system_proxy(&MockProxyCommandRunner) + .await + .unwrap() + .as_deref(), + Some("http://secure.example:8443") + ); + } + #[test] fn strips_gsettings_string_quotes() { assert_eq!(strip_gsettings_string("'manual'\n"), "manual");