mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-18 07:13:13 +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 serde::{Deserialize, Serialize};
|
||||||
|
use std::process::Command;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
use crate::ipc::DownloadCategory;
|
use crate::ipc::DownloadCategory;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_system_proxy() -> Result<Option<String>, String> {
|
pub async fn get_system_proxy() -> Result<Option<String>, String> {
|
||||||
match sysproxy::Sysproxy::get_system_proxy() {
|
match native_system_proxy() {
|
||||||
Ok(proxy) => {
|
Ok(Some(proxy)) => Ok(Some(proxy)),
|
||||||
if proxy.enable {
|
Ok(None) => Ok(proxy_from_environment()),
|
||||||
|
Err(native_error) => match sysproxy::Sysproxy::get_system_proxy() {
|
||||||
|
Ok(proxy) if proxy.enable => {
|
||||||
if proxy.host.contains('=') {
|
if proxy.host.contains('=') {
|
||||||
Ok(parse_windows_proxy_server(&proxy.host))
|
Ok(parse_windows_proxy_server(&proxy.host).or_else(proxy_from_environment))
|
||||||
} else {
|
} 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)
|
|
||||||
}
|
}
|
||||||
}
|
Ok(_) => Ok(proxy_from_environment()),
|
||||||
Err(error) => {
|
Err(error) => proxy_from_environment().map(Some).ok_or_else(|| {
|
||||||
#[cfg(target_os = "windows")]
|
format!(
|
||||||
if let Ok(Some(proxy)) = fallback_windows_proxy() {
|
"failed to read system proxy settings: {native_error}; sysproxy fallback: {error}"
|
||||||
return Ok(Some(proxy));
|
)
|
||||||
}
|
}),
|
||||||
|
},
|
||||||
if let Some(proxy) = proxy_from_environment() {
|
|
||||||
return Ok(Some(proxy));
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("failed to read system proxy settings: {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> {
|
fn proxy_from_environment() -> Option<String> {
|
||||||
[
|
[
|
||||||
"HTTPS_PROXY",
|
"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> {
|
fn normalize_proxy_address(raw: &str, default_scheme: &str) -> Option<String> {
|
||||||
let trimmed = raw.trim().trim_matches('"').trim_end_matches('/');
|
let trimmed = raw.trim().trim_matches('"').trim_end_matches('/');
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -120,6 +165,93 @@ fn parse_windows_proxy_server(value: &str) -> Option<String> {
|
|||||||
https.or(http).or(socks)
|
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")]
|
#[cfg(target_os = "windows")]
|
||||||
fn fallback_windows_proxy() -> Result<Option<String>, ()> {
|
fn fallback_windows_proxy() -> Result<Option<String>, ()> {
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
@@ -206,8 +338,9 @@ fn registry_value(output: &str, name: &str) -> Option<String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod proxy_tests {
|
mod proxy_tests {
|
||||||
use super::{
|
use super::{
|
||||||
normalize_proxy_address, normalize_sysproxy_address, parse_windows_proxy_server,
|
normalize_proxy_address, normalize_sysproxy_address, parse_macos_network_services,
|
||||||
registry_value, windows_proxy_enabled,
|
parse_macos_networksetup_proxy, parse_windows_proxy_server, proxy_matches_host_port,
|
||||||
|
registry_value, strip_gsettings_string, windows_proxy_enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[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]
|
#[test]
|
||||||
fn parses_reg_query_output_values() {
|
fn parses_reg_query_output_values() {
|
||||||
let output = r#"
|
let output = r#"
|
||||||
|
|||||||
+44
-11
@@ -998,6 +998,28 @@ fn uri_host_for_log(uri: &str) -> String {
|
|||||||
.unwrap_or_else(|| "<unknown host>".to_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(
|
async fn probe_bounded_range_support(
|
||||||
uri: &str,
|
uri: &str,
|
||||||
payload: &SpawnPayload,
|
payload: &SpawnPayload,
|
||||||
@@ -1159,6 +1181,12 @@ impl SidecarSpawner for ProductionSpawner {
|
|||||||
if !crate::is_safe_path(&resolved_dest, &self.app_handle) {
|
if !crate::is_safe_path(&resolved_dest, &self.app_handle) {
|
||||||
return Err("Path traversal blocked".to_string());
|
return Err("Path traversal blocked".to_string());
|
||||||
}
|
}
|
||||||
|
let proxy_value = payload
|
||||||
|
.proxy
|
||||||
|
.as_deref()
|
||||||
|
.map(aria2_all_proxy_value)
|
||||||
|
.transpose()?
|
||||||
|
.flatten();
|
||||||
options.insert(
|
options.insert(
|
||||||
"dir".to_string(),
|
"dir".to_string(),
|
||||||
serde_json::json!(resolved_dest.to_string_lossy().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() {
|
if !header_list.is_empty() {
|
||||||
options.insert("header".to_string(), serde_json::json!(header_list));
|
options.insert("header".to_string(), serde_json::json!(header_list));
|
||||||
}
|
}
|
||||||
if let Some(prox) = payload
|
if let Some(prox) = proxy_value {
|
||||||
.proxy
|
options.insert("all-proxy".to_string(), serde_json::json!(prox));
|
||||||
.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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let uris = crate::collect_download_uris(&payload.url, payload.mirrors.as_deref());
|
let uris = crate::collect_download_uris(&payload.url, payload.mirrors.as_deref());
|
||||||
let params = serde_json::json!([uris, options]);
|
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]
|
#[test]
|
||||||
fn bounded_range_probe_rejects_server_that_expands_to_end() {
|
fn bounded_range_probe_rejects_server_that_expands_to_end() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -838,7 +838,7 @@ runEngineChecks(false);
|
|||||||
)}
|
)}
|
||||||
<p className="settings-group-footer">
|
<p className="settings-group-footer">
|
||||||
{settings.proxyMode === 'none' && 'Downloads ignore configured proxies.'}
|
{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
|
{settings.proxyMode === 'custom' && (settings.proxyHost
|
||||||
? 'Downloads use the configured proxy URL for HTTP, HTTPS, and media engines.'
|
? 'Downloads use the configured proxy URL for HTTP, HTTPS, and media engines.'
|
||||||
: 'Enter a proxy host and port to enable the custom proxy.')}
|
: 'Enter a proxy host and port to enable the custom proxy.')}
|
||||||
|
|||||||
Reference in New Issue
Block a user