diff --git a/Cargo.toml b/Cargo.toml index 036024c30..5b6ff0d47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,9 @@ tokio = { version = "1.0", default-features = false, features = ["macros", "rt-m [target.'cfg(windows)'.dependencies] winreg = "0.50.0" +[target.'cfg(target_os = "macos")'.dependencies] +system-configuration = "0.5.1" + # wasm [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/src/proxy.rs b/src/proxy.rs index 359332b51..93ad00c39 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -13,6 +13,22 @@ use std::collections::HashMap; use std::env; use std::error::Error; use std::net::IpAddr; +#[cfg(target_os = "macos")] +use system_configuration::{ + core_foundation::{ + base::CFType, + dictionary::CFDictionary, + number::CFNumber, + string::{CFString, CFStringRef}, + }, + dynamic_store::SCDynamicStoreBuilder, + sys::schema_definitions::kSCPropNetProxiesHTTPEnable, + sys::schema_definitions::kSCPropNetProxiesHTTPPort, + sys::schema_definitions::kSCPropNetProxiesHTTPProxy, + sys::schema_definitions::kSCPropNetProxiesHTTPSEnable, + sys::schema_definitions::kSCPropNetProxiesHTTPSPort, + sys::schema_definitions::kSCPropNetProxiesHTTPSProxy, +}; #[cfg(target_os = "windows")] use winreg::enums::HKEY_CURRENT_USER; #[cfg(target_os = "windows")] @@ -268,7 +284,7 @@ impl Proxy { pub(crate) fn system() -> Proxy { let mut proxy = if cfg!(feature = "__internal_proxy_sys_no_cache") { Proxy::new(Intercept::System(Arc::new(get_sys_proxies( - get_from_registry(), + get_from_platform(), )))) } else { Proxy::new(Intercept::System(SYS_PROXIES.clone())) @@ -703,7 +719,6 @@ impl fmt::Debug for ProxyScheme { } type SystemProxyMap = HashMap; -type RegistryProxyValues = (u32, String); #[derive(Clone, Debug)] enum Intercept { @@ -788,34 +803,36 @@ impl Dst for Uri { } static SYS_PROXIES: Lazy> = - Lazy::new(|| Arc::new(get_sys_proxies(get_from_registry()))); + Lazy::new(|| Arc::new(get_sys_proxies(get_from_platform()))); /// Get system proxies information. /// -/// It can only support Linux, Unix like, and windows system. Note that it will always -/// return a HashMap, even if something runs into error when find registry information in -/// Windows system. Note that invalid proxy url in the system setting will be ignored. +/// All platforms will check for proxy settings via environment variables. +/// If those aren't set, platform-wide proxy settings will be looked up on +/// Windows and MacOS platforms instead. Errors encountered while discovering +/// these settings are ignored. /// /// Returns: /// System proxies information as a hashmap like /// {"http": Url::parse("http://127.0.0.1:80"), "https": Url::parse("https://127.0.0.1:80")} fn get_sys_proxies( - #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] registry_values: Option< - RegistryProxyValues, - >, + #[cfg_attr( + not(any(target_os = "windows", target_os = "macos")), + allow(unused_variables) + )] + platform_proxies: Option, ) -> SystemProxyMap { let proxies = get_from_environment(); - // TODO: move the following #[cfg] to `if expression` when attributes on `if` expressions allowed - #[cfg(target_os = "windows")] - { - if proxies.is_empty() { - // don't care errors if can't get proxies from registry, just return an empty HashMap. - if let Some(registry_values) = registry_values { - return parse_registry_values(registry_values); - } + #[cfg(any(target_os = "windows", target_os = "macos"))] + if proxies.is_empty() { + // if there are errors in acquiring the platform proxies, + // we'll just return an empty HashMap + if let Some(platform_proxies) = platform_proxies { + return parse_platform_values(platform_proxies); } } + proxies } @@ -873,7 +890,7 @@ fn is_cgi() -> bool { } #[cfg(target_os = "windows")] -fn get_from_registry_impl() -> Result> { +fn get_from_platform_impl() -> Result, Box> { let hkcu = RegKey::predef(HKEY_CURRENT_USER); let internet_setting: RegKey = hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?; @@ -881,33 +898,92 @@ fn get_from_registry_impl() -> Result> { let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?; let proxy_server: String = internet_setting.get_value("ProxyServer")?; - Ok((proxy_enable, proxy_server)) + Ok((proxy_enable == 1).then_some(proxy_server)) } -#[cfg(target_os = "windows")] -fn get_from_registry() -> Option { - get_from_registry_impl().ok() -} +#[cfg(target_os = "macos")] +fn parse_setting_from_dynamic_store( + proxies_map: &CFDictionary, + enabled_key: CFStringRef, + host_key: CFStringRef, + port_key: CFStringRef, + scheme: &str, +) -> Option { + let proxy_enabled = proxies_map + .find(enabled_key) + .and_then(|flag| flag.downcast::()) + .and_then(|flag| flag.to_i32()) + .unwrap_or(0) + == 1; + + if proxy_enabled { + let proxy_host = proxies_map + .find(host_key) + .and_then(|host| host.downcast::()) + .map(|host| host.to_string()); + let proxy_port = proxies_map + .find(port_key) + .and_then(|port| port.downcast::()) + .and_then(|port| port.to_i32()); + + return match (proxy_host, proxy_port) { + (Some(proxy_host), Some(proxy_port)) => { + Some(format!("{scheme}={proxy_host}:{proxy_port}")) + } + (Some(proxy_host), None) => Some(format!("{scheme}={proxy_host}")), + (None, Some(_)) => None, + (None, None) => None, + }; + } -#[cfg(not(target_os = "windows"))] -fn get_from_registry() -> Option { None } -#[cfg(target_os = "windows")] -fn parse_registry_values_impl( - registry_values: RegistryProxyValues, -) -> Result> { - let (proxy_enable, proxy_server) = registry_values; - - if proxy_enable == 0 { - return Ok(HashMap::new()); +#[cfg(target_os = "macos")] +fn get_from_platform_impl() -> Result, Box> { + let store = SCDynamicStoreBuilder::new("reqwest").build(); + + let Some(proxies_map) = store.get_proxies() else { + return Ok(None); + }; + + let http_proxy_config = parse_setting_from_dynamic_store( + &proxies_map, + unsafe { kSCPropNetProxiesHTTPEnable }, + unsafe { kSCPropNetProxiesHTTPProxy }, + unsafe { kSCPropNetProxiesHTTPPort }, + "http", + ); + let https_proxy_config = parse_setting_from_dynamic_store( + &proxies_map, + unsafe { kSCPropNetProxiesHTTPSEnable }, + unsafe { kSCPropNetProxiesHTTPSProxy }, + unsafe { kSCPropNetProxiesHTTPSPort }, + "https", + ); + + match http_proxy_config.as_ref().zip(https_proxy_config.as_ref()) { + Some((http_config, https_config)) => Ok(Some(format!("{http_config};{https_config}"))), + None => Ok(http_proxy_config.or(https_proxy_config)), } +} + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn get_from_platform() -> Option { + get_from_platform_impl().ok().flatten() +} + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +fn get_from_platform() -> Option { + None +} +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn parse_platform_values_impl(platform_values: String) -> SystemProxyMap { let mut proxies = HashMap::new(); - if proxy_server.contains("=") { + if platform_values.contains("=") { // per-protocol settings. - for p in proxy_server.split(";") { + for p in platform_values.split(";") { let protocol_parts: Vec<&str> = p.split("=").collect(); match protocol_parts.as_slice() { [protocol, address] => { @@ -930,21 +1006,21 @@ fn parse_registry_values_impl( } } } else { - if let Some(scheme) = extract_type_prefix(&proxy_server) { + if let Some(scheme) = extract_type_prefix(&platform_values) { // Explicit protocol has been specified - insert_proxy(&mut proxies, scheme, proxy_server.to_owned()); + insert_proxy(&mut proxies, scheme, platform_values.to_owned()); } else { // No explicit protocol has been specified, default to HTTP - insert_proxy(&mut proxies, "http", format!("http://{}", proxy_server)); - insert_proxy(&mut proxies, "https", format!("http://{}", proxy_server)); + insert_proxy(&mut proxies, "http", format!("http://{}", platform_values)); + insert_proxy(&mut proxies, "https", format!("http://{}", platform_values)); } } - Ok(proxies) + proxies } /// Extract the protocol from the given address, if present /// For example, "https://example.com" will return Some("https") -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "windows", target_os = "macos"))] fn extract_type_prefix(address: &str) -> Option<&str> { if let Some(indice) = address.find("://") { if indice == 0 { @@ -964,9 +1040,9 @@ fn extract_type_prefix(address: &str) -> Option<&str> { } } -#[cfg(target_os = "windows")] -fn parse_registry_values(registry_values: RegistryProxyValues) -> SystemProxyMap { - parse_registry_values_impl(registry_values).unwrap_or(HashMap::new()) +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn parse_platform_values(platform_values: String) -> SystemProxyMap { + parse_platform_values_impl(platform_values) } #[cfg(test)] @@ -1173,7 +1249,7 @@ mod tests { assert!(all_proxies.values().all(|p| p.host() == "127.0.0.2")); } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] #[test] fn test_get_sys_proxies_registry_parsing() { // Stop other threads from modifying process-global ENV while we are. @@ -1185,20 +1261,16 @@ mod tests { // Mock ENV, get the results, before doing assertions // to avoid assert! -> panic! -> Mutex Poisoned. let baseline_proxies = get_sys_proxies(None); - // the system proxy in the registry has been disabled - let disabled_proxies = get_sys_proxies(Some((0, String::from("http://127.0.0.1/")))); // set valid proxy - let valid_proxies = get_sys_proxies(Some((1, String::from("http://127.0.0.1/")))); - let valid_proxies_no_scheme = get_sys_proxies(Some((1, String::from("127.0.0.1")))); + let valid_proxies = get_sys_proxies(Some(String::from("http://127.0.0.1/"))); + let valid_proxies_no_scheme = get_sys_proxies(Some(String::from("127.0.0.1"))); let valid_proxies_explicit_https = - get_sys_proxies(Some((1, String::from("https://127.0.0.1/")))); - let multiple_proxies = get_sys_proxies(Some(( - 1, - String::from("http=127.0.0.1:8888;https=127.0.0.2:8888"), + get_sys_proxies(Some(String::from("https://127.0.0.1/"))); + let multiple_proxies = get_sys_proxies(Some(String::from( + "http=127.0.0.1:8888;https=127.0.0.2:8888", ))); - let multiple_proxies_explicit_scheme = get_sys_proxies(Some(( - 1, - String::from("http=http://127.0.0.1:8888;https=https://127.0.0.2:8888"), + let multiple_proxies_explicit_scheme = get_sys_proxies(Some(String::from( + "http=http://127.0.0.1:8888;https=https://127.0.0.2:8888", ))); // reset user setting when guards drop @@ -1208,7 +1280,6 @@ mod tests { drop(_lock); assert_eq!(baseline_proxies.contains_key("http"), false); - assert_eq!(disabled_proxies.contains_key("http"), false); let p = &valid_proxies["http"]; assert_eq!(p.scheme(), "http"); @@ -1491,7 +1562,7 @@ mod tests { drop(_lock); } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", target_os = "macos"))] #[test] fn test_type_prefix_extraction() { assert!(extract_type_prefix("test").is_none());