Skip to content

Commit

Permalink
Respect MacOS proxy settings
Browse files Browse the repository at this point in the history
  • Loading branch information
jefflloyd committed Aug 26, 2023
1 parent a4e8ab6 commit b498cda
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 31 deletions.
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
133 changes: 102 additions & 31 deletions src/proxy.rs
Expand Up @@ -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")]
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -703,7 +708,7 @@ impl fmt::Debug for ProxyScheme {
}

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

#[derive(Clone, Debug)]
enum Intercept {
Expand Down Expand Up @@ -788,34 +793,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 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<PlatformProxyValues>,
) -> 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,7 +880,7 @@ fn is_cgi() -> bool {
}

#[cfg(target_os = "windows")]
fn get_from_registry_impl() -> Result<RegistryProxyValues, Box<dyn Error>> {
fn get_from_platform_impl() -> Result<PlatformProxyValues, Box<dyn Error>> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let internet_setting: RegKey =
hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?;
Expand All @@ -884,21 +891,85 @@ fn get_from_registry_impl() -> Result<RegistryProxyValues, Box<dyn Error>> {
Ok((proxy_enable, 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>,
scheme: &str,
) -> Option<String> {
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::<CFNumber>())
.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::<CFString>())
.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::<CFNumber>())
.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<RegistryProxyValues> {
#[cfg(target_os = "macos")]
fn get_from_platform_impl() -> Result<PlatformProxyValues, Box<dyn Error>> {
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<PlatformProxyValues> {
get_from_platform_impl().ok()
}

#[cfg(not(any(target_os = "windows", target_os = "macos")))]
fn get_from_platform() -> Option<PlatformProxyValues> {
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<SystemProxyMap, Box<dyn Error>> {
let (proxy_enable, proxy_server) = registry_values;
let (proxy_enable, proxy_server) = platform_values;

if proxy_enable == 0 {
return Ok(HashMap::new());
Expand Down Expand Up @@ -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 {
Expand All @@ -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)]
Expand Down

0 comments on commit b498cda

Please sign in to comment.