Skip to content

Commit acdda1a

Browse files
authored
feat(client): add macOS system proxy support for Matcher (#189)
1 parent 6c29abb commit acdda1a

File tree

2 files changed

+112
-1
lines changed

2 files changed

+112
-1
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ http = "1.0"
2626
http-body = "1.0.0"
2727
hyper = "1.6.0"
2828
ipnet = { version = "2.9", optional = true }
29+
libc = { version = "0.2", optional = true }
2930
percent-encoding = { version = "2.3", optional = true }
3031
pin-project-lite = "0.2.4"
3132
socket2 = { version = "0.5.9", optional = true, features = ["all"] }
3233
tracing = { version = "0.1", default-features = false, features = ["std"], optional = true }
3334
tokio = { version = "1", optional = true, default-features = false }
3435
tower-service = { version = "0.3", optional = true }
35-
libc = { version = "0.2", optional = true }
3636

3737
[dev-dependencies]
3838
hyper = { version = "1.4.0", features = ["full"] }
@@ -45,6 +45,9 @@ pretty_env_logger = "0.5"
4545
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dev-dependencies]
4646
pnet_datalink = "0.35.0"
4747

48+
[target.'cfg(target_os = "macos")'.dependencies]
49+
system-configuration = { version = "0.6.1", optional = true }
50+
4851
[features]
4952
default = []
5053

@@ -65,6 +68,7 @@ full = [
6568
client = ["hyper/client", "dep:tracing", "dep:futures-channel", "dep:tower-service"]
6669
client-legacy = ["client", "dep:socket2", "tokio/sync", "dep:libc"]
6770
client-proxy = ["client", "dep:base64", "dep:ipnet", "dep:percent-encoding"]
71+
client-proxy-system = ["dep:system-configuration"]
6872

6973
server = ["hyper/server"]
7074
server-auto = ["server", "http1", "http2"]

src/client/proxy/matcher.rs

+107
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,20 @@ impl Matcher {
9696
Builder::from_env().build()
9797
}
9898

99+
/// Create a matcher from the environment or system.
100+
///
101+
/// This checks the same environment variables as `from_env()`, and if not
102+
/// set, checks the system configuration for values for the OS.
103+
///
104+
/// This constructor is always available, but if the `client-proxy-system`
105+
/// feature is enabled, it will check more configuration. Use this
106+
/// constructor if you want to allow users to optionally enable more, or
107+
/// use `from_env` if you do not want the values to change based on an
108+
/// enabled feature.
109+
pub fn from_system() -> Self {
110+
Builder::from_system().build()
111+
}
112+
99113
/// Start a builder to configure a matcher.
100114
pub fn builder() -> Builder {
101115
Builder::default()
@@ -221,6 +235,16 @@ impl Builder {
221235
}
222236
}
223237

238+
fn from_system() -> Self {
239+
#[allow(unused_mut)]
240+
let mut builder = Self::from_env();
241+
242+
#[cfg(all(feature = "client-proxy-system", target_os = "macos"))]
243+
mac::with_system(&mut builder);
244+
245+
builder
246+
}
247+
224248
/// Set the target proxy for all destinations.
225249
pub fn all<S>(mut self, val: S) -> Self
226250
where
@@ -531,6 +555,89 @@ mod builder {
531555
}
532556
}
533557

558+
#[cfg(feature = "client-proxy-system")]
559+
#[cfg(target_os = "macos")]
560+
mod mac {
561+
use system_configuration::core_foundation::base::{CFType, TCFType, TCFTypeRef};
562+
use system_configuration::core_foundation::dictionary::CFDictionary;
563+
use system_configuration::core_foundation::number::CFNumber;
564+
use system_configuration::core_foundation::string::{CFString, CFStringRef};
565+
use system_configuration::dynamic_store::SCDynamicStoreBuilder;
566+
use system_configuration::sys::schema_definitions::{
567+
kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPProxy,
568+
kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesHTTPSProxy,
569+
};
570+
571+
pub(super) fn with_system(builder: &mut super::Builder) {
572+
let store = SCDynamicStoreBuilder::new("").build();
573+
574+
let proxies_map = if let Some(proxies_map) = store.get_proxies() {
575+
proxies_map
576+
} else {
577+
return;
578+
};
579+
580+
if builder.http.is_empty() {
581+
let http_proxy_config = parse_setting_from_dynamic_store(
582+
&proxies_map,
583+
unsafe { kSCPropNetProxiesHTTPEnable },
584+
unsafe { kSCPropNetProxiesHTTPProxy },
585+
unsafe { kSCPropNetProxiesHTTPPort },
586+
);
587+
if let Some(http) = http_proxy_config {
588+
builder.http = http;
589+
}
590+
}
591+
592+
if builder.https.is_empty() {
593+
let https_proxy_config = parse_setting_from_dynamic_store(
594+
&proxies_map,
595+
unsafe { kSCPropNetProxiesHTTPSEnable },
596+
unsafe { kSCPropNetProxiesHTTPSProxy },
597+
unsafe { kSCPropNetProxiesHTTPSPort },
598+
);
599+
600+
if let Some(https) = https_proxy_config {
601+
builder.https = https;
602+
}
603+
}
604+
}
605+
606+
fn parse_setting_from_dynamic_store(
607+
proxies_map: &CFDictionary<CFString, CFType>,
608+
enabled_key: CFStringRef,
609+
host_key: CFStringRef,
610+
port_key: CFStringRef,
611+
) -> Option<String> {
612+
let proxy_enabled = proxies_map
613+
.find(enabled_key)
614+
.and_then(|flag| flag.downcast::<CFNumber>())
615+
.and_then(|flag| flag.to_i32())
616+
.unwrap_or(0)
617+
== 1;
618+
619+
if proxy_enabled {
620+
let proxy_host = proxies_map
621+
.find(host_key)
622+
.and_then(|host| host.downcast::<CFString>())
623+
.map(|host| host.to_string());
624+
let proxy_port = proxies_map
625+
.find(port_key)
626+
.and_then(|port| port.downcast::<CFNumber>())
627+
.and_then(|port| port.to_i32());
628+
629+
return match (proxy_host, proxy_port) {
630+
(Some(proxy_host), Some(proxy_port)) => Some(format!("{proxy_host}:{proxy_port}")),
631+
(Some(proxy_host), None) => Some(proxy_host),
632+
(None, Some(_)) => None,
633+
(None, None) => None,
634+
};
635+
}
636+
637+
None
638+
}
639+
}
640+
534641
#[cfg(test)]
535642
mod tests {
536643
use super::*;

0 commit comments

Comments
 (0)