Skip to content

Commit

Permalink
Feature: auto detect MacOS proxy settings (#1955)
Browse files Browse the repository at this point in the history
  • Loading branch information
jefflloyd authored Aug 31, 2023
1 parent 34f6c70 commit 70d100c
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 58 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
187 changes: 129 additions & 58 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -703,7 +719,6 @@ impl fmt::Debug for ProxyScheme {
}

type SystemProxyMap = HashMap<String, ProxyScheme>;
type RegistryProxyValues = (u32, String);

#[derive(Clone, Debug)]
enum Intercept {
Expand Down Expand Up @@ -788,34 +803,36 @@ impl Dst for Uri {
}

static SYS_PROXIES: Lazy<Arc<SystemProxyMap>> =
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<String>,
) -> 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
}

Expand Down Expand Up @@ -873,41 +890,100 @@ fn is_cgi() -> bool {
}

#[cfg(target_os = "windows")]
fn get_from_registry_impl() -> Result<RegistryProxyValues, Box<dyn Error>> {
fn get_from_platform_impl() -> Result<Option<String>, Box<dyn Error>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let internet_setting: RegKey =
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?;
// ensure the proxy is enable, if the value doesn't exist, an error will returned.
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<RegistryProxyValues> {
get_from_registry_impl().ok()
}
#[cfg(target_os = "macos")]
fn parse_setting_from_dynamic_store(
proxies_map: &CFDictionary<CFString, CFType>,
enabled_key: CFStringRef,
host_key: CFStringRef,
port_key: CFStringRef,
scheme: &str,
) -> Option<String> {
let proxy_enabled = proxies_map
.find(enabled_key)
.and_then(|flag| flag.downcast::<CFNumber>())
.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::<CFString>())
.map(|host| host.to_string());
let proxy_port = proxies_map
.find(port_key)
.and_then(|port| port.downcast::<CFNumber>())
.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<RegistryProxyValues> {
None
}

#[cfg(target_os = "windows")]
fn parse_registry_values_impl(
registry_values: RegistryProxyValues,
) -> Result<SystemProxyMap, Box<dyn Error>> {
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<Option<String>, Box<dyn Error>> {
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<String> {
get_from_platform_impl().ok().flatten()
}

#[cfg(not(any(target_os = "windows", target_os = "macos")))]
fn get_from_platform() -> Option<String> {
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] => {
Expand All @@ -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 {
Expand All @@ -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)]
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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");
Expand Down Expand Up @@ -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());
Expand Down

0 comments on commit 70d100c

Please sign in to comment.