fix(network): replace legacy system proxy fallback

- replace sysproxy with bounded asynchronous native platform probes
- preserve deterministic proxy precedence and environment fallback
- cover native parsing, output bounds, and macOS service selection
This commit is contained in:
NimBold
2026-09-05 07:36:16 +03:30
parent 69a818d621
commit ddde763133
3 changed files with 255 additions and 380 deletions
+50 -210
View File
@@ -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",
-1
View File
@@ -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"
+205 -169
View File
@@ -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<Option<String>, 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<String, String>;
}
struct SystemProxyCommandRunner;
#[async_trait::async_trait]
impl ProxyCommandRunner for SystemProxyCommandRunner {
async fn stdout(&self, program: &str, args: &[String]) -> Result<String, String> {
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<Option<String>, String> {
fallback_windows_proxy().map_err(|_| "failed to read Windows proxy registry".to_string())
async fn native_system_proxy(runner: &dyn ProxyCommandRunner) -> Result<Option<String>, String> {
windows_system_proxy(runner).await
}
#[cfg(target_os = "macos")]
fn native_system_proxy() -> Result<Option<String>, 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<Option<String>, String> {
macos_system_proxy(runner).await
}
#[cfg(target_os = "linux")]
fn native_system_proxy() -> Result<Option<String>, 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<Option<String>, 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<Option<String>, String> {
async fn native_system_proxy(_runner: &dyn ProxyCommandRunner) -> Result<Option<String>, String> {
Ok(None)
}
fn string_args(args: &[&str]) -> Vec<String> {
args.iter().map(|value| (*value).to_string()).collect()
}
fn proxy_from_environment() -> Option<String> {
[
"HTTPS_PROXY",
@@ -85,18 +133,6 @@ fn proxy_from_environment() -> Option<String> {
})
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn command_stdout(command: &mut Command) -> std::io::Result<String> {
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<String> {
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<String> {
Some(candidate)
}
fn normalize_sysproxy_address(host: &str, port: u16) -> Option<String> {
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<String> {
let value = value.trim().trim_matches('"');
if value.is_empty() {
@@ -169,27 +185,32 @@ fn parse_windows_proxy_server(value: &str) -> Option<String> {
}
#[cfg(target_os = "macos")]
fn macos_proxy_for_host_port(host: &str, port: u16) -> Option<String> {
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<Option<String>, 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<String>
}
#[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<String> {
async fn linux_gsettings_proxy(
runner: &dyn ProxyCommandRunner,
service: &str,
scheme: &str,
) -> Option<String> {
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<Option<String>, ()> {
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<Option<String>, 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<String> {
#[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<String, String> {
assert_eq!(program, "networksetup");
match args
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.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");