From b498cda586d409fd0dd75e2c1ae7d90a1d4552c3 Mon Sep 17 00:00:00 2001 From: Jeff Lloyd Date: Fri, 25 Aug 2023 21:34:14 -0400 Subject: [PATCH] Respect MacOS proxy settings --- Cargo.toml | 3 ++ src/proxy.rs | 133 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 105 insertions(+), 31 deletions(-) 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..7a27d88ee 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -13,6 +13,11 @@ 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}, + dynamic_store::SCDynamicStoreBuilder, +}; #[cfg(target_os = "windows")] use winreg::enums::HKEY_CURRENT_USER; #[cfg(target_os = "windows")] @@ -268,7 +273,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 +708,7 @@ impl fmt::Debug for ProxyScheme { } type SystemProxyMap = HashMap; -type RegistryProxyValues = (u32, String); +type PlatformProxyValues = (u32, String); #[derive(Clone, Debug)] enum Intercept { @@ -788,34 +793,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 the `http_proxy`, `https_proxy`, +/// and `all_proxy` 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 +880,7 @@ fn is_cgi() -> bool { } #[cfg(target_os = "windows")] -fn get_from_registry_impl() -> Result> { +fn get_from_platform_impl() -> Result> { let hkcu = RegKey::predef(HKEY_CURRENT_USER); let internet_setting: RegKey = hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?; @@ -884,21 +891,85 @@ fn get_from_registry_impl() -> Result> { Ok((proxy_enable, 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, + scheme: &str, +) -> Option { + let uppercase_scheme = scheme.to_ascii_uppercase(); + let enabled_key = uppercase_scheme.clone() + "Enable"; + let host_key = uppercase_scheme.clone() + "Proxy"; + let port_key = uppercase_scheme.clone() + "Port"; + + let proxy_enabled = proxies_map + .find(CFString::new(enabled_key.as_str())) + .and_then(|flag| flag.downcast::()) + .and_then(|flag| flag.to_i32()) + .unwrap_or(0); + + if proxy_enabled == 1 { + let proxy_host = proxies_map + .find(CFString::new(host_key.as_str())) + .and_then(|host| host.downcast::()) + .and_then(|host| Some(host.to_string())) + .unwrap_or_default(); + let proxy_port = proxies_map + .find(CFString::new(port_key.as_str())) + .and_then(|port| port.downcast::()) + .and_then(|port| port.to_i32()); + + if let Some(proxy_port) = proxy_port { + return Some(format_args!("{scheme}={proxy_host}:{proxy_port}").to_string()); + } else { + return Some(format_args!("{scheme}={proxy_host}").to_string()); + } + } + + None } -#[cfg(not(target_os = "windows"))] -fn get_from_registry() -> Option { +#[cfg(target_os = "macos")] +fn get_from_platform_impl() -> Result> { + let store = SCDynamicStoreBuilder::new("reqwest").build(); + + if let Some(proxies_map) = store.get_proxies() { + let http_proxy_config = parse_setting_from_dynamic_store(&proxies_map, "http"); + let https_proxy_config = parse_setting_from_dynamic_store(&proxies_map, "https"); + + match (http_proxy_config, https_proxy_config) { + (Some(http_proxy_config), Some(https_proxy_config)) => { + return Ok(( + 1, + format_args!("{http_proxy_config};{https_proxy_config}").to_string(), + )); + } + (Some(config), None) | (None, Some(config)) => { + return Ok((1, config)); + } + (None, None) => { + return Ok((0, Default::default())); + } + } + } + + Ok((0, Default::default())) +} + +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn get_from_platform() -> Option { + get_from_platform_impl().ok() +} + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +fn get_from_platform() -> Option { None } -#[cfg(target_os = "windows")] -fn parse_registry_values_impl( - registry_values: RegistryProxyValues, +#[cfg(any(target_os = "windows", target_os = "macos"))] +fn parse_platform_values_impl( + platform_values: PlatformProxyValues, ) -> Result> { - let (proxy_enable, proxy_server) = registry_values; + let (proxy_enable, proxy_server) = platform_values; if proxy_enable == 0 { return Ok(HashMap::new()); @@ -944,7 +1015,7 @@ fn parse_registry_values_impl( /// 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 +1035,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: PlatformProxyValues) -> SystemProxyMap { + parse_platform_values_impl(platform_values).unwrap_or(HashMap::new()) } #[cfg(test)]