mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(proxy): preserve system proxy schemes
Detect platform system proxy schemes before normalizing proxy values. Fail normal aria2 downloads clearly when the selected proxy is SOCKS, since aria2 only supports HTTP-style all-proxy URLs.
This commit is contained in:
+199
-22
@@ -1,37 +1,71 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::ipc::DownloadCategory;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_system_proxy() -> Result<Option<String>, String> {
|
||||
match sysproxy::Sysproxy::get_system_proxy() {
|
||||
Ok(proxy) => {
|
||||
if proxy.enable {
|
||||
match native_system_proxy() {
|
||||
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))
|
||||
Ok(parse_windows_proxy_server(&proxy.host).or_else(proxy_from_environment))
|
||||
} else {
|
||||
Ok(normalize_sysproxy_address(&proxy.host, proxy.port))
|
||||
Ok(normalize_sysproxy_address(&proxy.host, proxy.port)
|
||||
.or_else(proxy_from_environment))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Ok(Some(proxy)) = fallback_windows_proxy() {
|
||||
return Ok(Some(proxy));
|
||||
}
|
||||
|
||||
if let Some(proxy) = proxy_from_environment() {
|
||||
return Ok(Some(proxy));
|
||||
}
|
||||
|
||||
Err(format!("failed to read system proxy settings: {error}"))
|
||||
}
|
||||
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}"
|
||||
)
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
||||
#[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())?;
|
||||
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")))
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
fn native_system_proxy() -> Result<Option<String>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn proxy_from_environment() -> Option<String> {
|
||||
[
|
||||
"HTTPS_PROXY",
|
||||
@@ -49,6 +83,17 @@ fn proxy_from_environment() -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn command_stdout(command: &mut Command) -> std::io::Result<String> {
|
||||
let output = command.output()?;
|
||||
if !output.status.success() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::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() {
|
||||
@@ -120,6 +165,93 @@ fn parse_windows_proxy_server(value: &str) -> Option<String> {
|
||||
https.or(http).or(socks)
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
fn parse_macos_network_services(output: &str) -> Vec<String> {
|
||||
output
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.filter(|line| !line.starts_with("An asterisk"))
|
||||
.filter(|line| !line.starts_with('*'))
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
fn parse_macos_networksetup_proxy(output: &str, scheme: &str) -> Option<String> {
|
||||
let enabled = macos_networksetup_value(output, "Enabled:")
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("yes"));
|
||||
if !enabled {
|
||||
return None;
|
||||
}
|
||||
let server = macos_networksetup_value(output, "Server:")?;
|
||||
let port = macos_networksetup_value(output, "Port:")?;
|
||||
normalize_proxy_address(&format!("{scheme}://{server}:{port}"), scheme)
|
||||
}
|
||||
|
||||
#[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
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find_map(|line| line.strip_prefix(key).map(str::trim))
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_gsettings_proxy(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 = strip_gsettings_string(&host);
|
||||
if host.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let port = command_stdout(Command::new("gsettings").args(["get", &schema, "port"])).ok()?;
|
||||
let port = port.trim();
|
||||
normalize_proxy_address(&format!("{scheme}://{host}:{port}"), scheme)
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
fn strip_gsettings_string(value: &str) -> String {
|
||||
value
|
||||
.trim()
|
||||
.trim_matches('\'')
|
||||
.trim_matches('"')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn fallback_windows_proxy() -> Result<Option<String>, ()> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -206,8 +338,9 @@ fn registry_value(output: &str, name: &str) -> Option<String> {
|
||||
#[cfg(test)]
|
||||
mod proxy_tests {
|
||||
use super::{
|
||||
normalize_proxy_address, normalize_sysproxy_address, parse_windows_proxy_server,
|
||||
registry_value, windows_proxy_enabled,
|
||||
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,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -255,6 +388,50 @@ mod proxy_tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_macos_proxy_outputs_with_scheme() {
|
||||
let services = r#"
|
||||
An asterisk (*) denotes that a network service is disabled.
|
||||
Wi-Fi
|
||||
*USB 10/100/1000 LAN
|
||||
Thunderbolt Bridge
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_macos_network_services(services),
|
||||
vec!["Wi-Fi".to_string(), "Thunderbolt Bridge".to_string()]
|
||||
);
|
||||
|
||||
let proxy = r#"
|
||||
Enabled: Yes
|
||||
Server: 127.0.0.1
|
||||
Port: 1080
|
||||
Authenticated Proxy Enabled: 0
|
||||
"#;
|
||||
assert_eq!(
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_gsettings_string_quotes() {
|
||||
assert_eq!(strip_gsettings_string("'manual'\n"), "manual");
|
||||
assert_eq!(strip_gsettings_string("\"127.0.0.1\"\n"), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_reg_query_output_values() {
|
||||
let output = r#"
|
||||
|
||||
+44
-11
@@ -998,6 +998,28 @@ fn uri_host_for_log(uri: &str) -> String {
|
||||
.unwrap_or_else(|| "<unknown host>".to_string())
|
||||
}
|
||||
|
||||
fn proxy_scheme(proxy: &str) -> Option<String> {
|
||||
proxy
|
||||
.split_once("://")
|
||||
.map(|(scheme, _)| scheme.trim().to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn aria2_all_proxy_value(proxy: &str) -> Result<Option<String>, String> {
|
||||
let proxy = proxy.trim();
|
||||
if proxy.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if proxy.eq_ignore_ascii_case("none") {
|
||||
return Ok(Some(String::new()));
|
||||
}
|
||||
if proxy_scheme(proxy).is_some_and(|scheme| scheme.starts_with("socks")) {
|
||||
return Err(
|
||||
"SOCKS system proxies are not supported for normal file downloads because aria2 only accepts HTTP/HTTPS/FTP proxy URLs. Use an HTTP proxy endpoint for normal downloads, or use media downloads where yt-dlp supports SOCKS.".to_string(),
|
||||
);
|
||||
}
|
||||
Ok(Some(proxy.to_string()))
|
||||
}
|
||||
|
||||
async fn probe_bounded_range_support(
|
||||
uri: &str,
|
||||
payload: &SpawnPayload,
|
||||
@@ -1159,6 +1181,12 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
if !crate::is_safe_path(&resolved_dest, &self.app_handle) {
|
||||
return Err("Path traversal blocked".to_string());
|
||||
}
|
||||
let proxy_value = payload
|
||||
.proxy
|
||||
.as_deref()
|
||||
.map(aria2_all_proxy_value)
|
||||
.transpose()?
|
||||
.flatten();
|
||||
options.insert(
|
||||
"dir".to_string(),
|
||||
serde_json::json!(resolved_dest.to_string_lossy().to_string()),
|
||||
@@ -1210,17 +1238,8 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
if !header_list.is_empty() {
|
||||
options.insert("header".to_string(), serde_json::json!(header_list));
|
||||
}
|
||||
if let Some(prox) = payload
|
||||
.proxy
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
if prox.eq_ignore_ascii_case("none") {
|
||||
options.insert("all-proxy".to_string(), serde_json::json!(""));
|
||||
} else {
|
||||
options.insert("all-proxy".to_string(), serde_json::json!(prox));
|
||||
}
|
||||
if let Some(prox) = proxy_value {
|
||||
options.insert("all-proxy".to_string(), serde_json::json!(prox));
|
||||
}
|
||||
let uris = crate::collect_download_uris(&payload.url, payload.mirrors.as_deref());
|
||||
let params = serde_json::json!([uris, options]);
|
||||
@@ -1389,6 +1408,20 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aria2_proxy_value_rejects_socks_proxies() {
|
||||
assert_eq!(aria2_all_proxy_value("none").unwrap().as_deref(), Some(""));
|
||||
assert_eq!(
|
||||
aria2_all_proxy_value("http://127.0.0.1:8080")
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("http://127.0.0.1:8080")
|
||||
);
|
||||
assert!(aria2_all_proxy_value("socks5://127.0.0.1:1080")
|
||||
.unwrap_err()
|
||||
.contains("SOCKS system proxies are not supported"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_range_probe_rejects_server_that_expands_to_end() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -838,7 +838,7 @@ runEngineChecks(false);
|
||||
)}
|
||||
<p className="settings-group-footer">
|
||||
{settings.proxyMode === 'none' && 'Downloads ignore configured proxies.'}
|
||||
{settings.proxyMode === 'system' && `Downloads use the detected ${platform.os === 'macos' ? 'macOS' : platform.os === 'windows' ? 'Windows' : 'desktop'} system proxy when available, otherwise no proxy.`}
|
||||
{settings.proxyMode === 'system' && `Downloads use the detected ${platform.os === 'macos' ? 'macOS' : platform.os === 'windows' ? 'Windows' : 'desktop'} system proxy. Normal file downloads require an HTTP or HTTPS proxy endpoint; media downloads can use SOCKS.`}
|
||||
{settings.proxyMode === 'custom' && (settings.proxyHost
|
||||
? 'Downloads use the configured proxy URL for HTTP, HTTPS, and media engines.'
|
||||
: 'Enter a proxy host and port to enable the custom proxy.')}
|
||||
|
||||
Reference in New Issue
Block a user