diff --git a/Cargo.toml b/Cargo.toml index 4d6bbd3..7063488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,13 +26,13 @@ http = "1.0" http-body = "1.0.0" hyper = "1.6.0" ipnet = { version = "2.9", optional = true } +libc = { version = "0.2", optional = true } percent-encoding = { version = "2.3", optional = true } pin-project-lite = "0.2.4" socket2 = { version = "0.5.9", optional = true, features = ["all"] } tracing = { version = "0.1", default-features = false, features = ["std"], optional = true } tokio = { version = "1", optional = true, default-features = false } tower-service = { version = "0.3", optional = true } -libc = { version = "0.2", optional = true } [dev-dependencies] hyper = { version = "1.4.0", features = ["full"] } @@ -45,6 +45,9 @@ pretty_env_logger = "0.5" [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dev-dependencies] pnet_datalink = "0.35.0" +[target.'cfg(target_os = "macos")'.dependencies] +system-configuration = { version = "0.6.1", optional = true } + [features] default = [] @@ -65,6 +68,7 @@ full = [ client = ["hyper/client", "dep:tracing", "dep:futures-channel", "dep:tower-service"] client-legacy = ["client", "dep:socket2", "tokio/sync", "dep:libc"] client-proxy = ["client", "dep:base64", "dep:ipnet", "dep:percent-encoding"] +client-proxy-system = ["dep:system-configuration"] server = ["hyper/server"] server-auto = ["server", "http1", "http2"] diff --git a/src/client/proxy/matcher.rs b/src/client/proxy/matcher.rs index a9b5bd5..c2a1cc2 100644 --- a/src/client/proxy/matcher.rs +++ b/src/client/proxy/matcher.rs @@ -96,6 +96,20 @@ impl Matcher { Builder::from_env().build() } + /// Create a matcher from the environment or system. + /// + /// This checks the same environment variables as `from_env()`, and if not + /// set, checks the system configuration for values for the OS. + /// + /// This constructor is always available, but if the `client-proxy-system` + /// feature is enabled, it will check more configuration. Use this + /// constructor if you want to allow users to optionally enable more, or + /// use `from_env` if you do not want the values to change based on an + /// enabled feature. + pub fn from_system() -> Self { + Builder::from_system().build() + } + /// Start a builder to configure a matcher. pub fn builder() -> Builder { Builder::default() @@ -221,6 +235,16 @@ impl Builder { } } + fn from_system() -> Self { + #[allow(unused_mut)] + let mut builder = Self::from_env(); + + #[cfg(all(feature = "client-proxy-system", target_os = "macos"))] + mac::with_system(&mut builder); + + builder + } + /// Set the target proxy for all destinations. pub fn all(mut self, val: S) -> Self where @@ -531,6 +555,89 @@ mod builder { } } +#[cfg(feature = "client-proxy-system")] +#[cfg(target_os = "macos")] +mod mac { + use system_configuration::core_foundation::base::{CFType, TCFType, TCFTypeRef}; + use system_configuration::core_foundation::dictionary::CFDictionary; + use system_configuration::core_foundation::number::CFNumber; + use system_configuration::core_foundation::string::{CFString, CFStringRef}; + use system_configuration::dynamic_store::SCDynamicStoreBuilder; + use system_configuration::sys::schema_definitions::{ + kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPProxy, + kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesHTTPSProxy, + }; + + pub(super) fn with_system(builder: &mut super::Builder) { + let store = SCDynamicStoreBuilder::new("").build(); + + let proxies_map = if let Some(proxies_map) = store.get_proxies() { + proxies_map + } else { + return; + }; + + if builder.http.is_empty() { + let http_proxy_config = parse_setting_from_dynamic_store( + &proxies_map, + unsafe { kSCPropNetProxiesHTTPEnable }, + unsafe { kSCPropNetProxiesHTTPProxy }, + unsafe { kSCPropNetProxiesHTTPPort }, + ); + if let Some(http) = http_proxy_config { + builder.http = http; + } + } + + if builder.https.is_empty() { + let https_proxy_config = parse_setting_from_dynamic_store( + &proxies_map, + unsafe { kSCPropNetProxiesHTTPSEnable }, + unsafe { kSCPropNetProxiesHTTPSProxy }, + unsafe { kSCPropNetProxiesHTTPSPort }, + ); + + if let Some(https) = https_proxy_config { + builder.https = https; + } + } + } + + fn parse_setting_from_dynamic_store( + proxies_map: &CFDictionary, + enabled_key: CFStringRef, + host_key: CFStringRef, + port_key: CFStringRef, + ) -> 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!("{proxy_host}:{proxy_port}")), + (Some(proxy_host), None) => Some(proxy_host), + (None, Some(_)) => None, + (None, None) => None, + }; + } + + None + } +} + #[cfg(test)] mod tests { use super::*;