Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect MacOS proxy settings #1955

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
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
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);
jefflloyd marked this conversation as resolved.
Show resolved Hide resolved

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