Skip to content

Commit 522761e

Browse files
committed
feat(client): add macOS system proxy support for Matcher
1 parent 6c29abb commit 522761e

File tree

2 files changed

+116
-1
lines changed

2 files changed

+116
-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

+111
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,93 @@ 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,
568+
kSCPropNetProxiesHTTPPort,
569+
kSCPropNetProxiesHTTPProxy,
570+
kSCPropNetProxiesHTTPSEnable,
571+
kSCPropNetProxiesHTTPSPort,
572+
kSCPropNetProxiesHTTPSProxy,
573+
};
574+
575+
pub(super) fn with_system(builder: &mut super::Builder) {
576+
let store = SCDynamicStoreBuilder::new("").build();
577+
578+
let proxies_map = if let Some(proxies_map) = store.get_proxies() {
579+
proxies_map
580+
} else {
581+
return;
582+
};
583+
584+
if builder.http.is_empty() {
585+
let http_proxy_config = parse_setting_from_dynamic_store(
586+
&proxies_map,
587+
unsafe { kSCPropNetProxiesHTTPEnable },
588+
unsafe { kSCPropNetProxiesHTTPProxy },
589+
unsafe { kSCPropNetProxiesHTTPPort },
590+
);
591+
if let Some(http) = http_proxy_config {
592+
builder.http = http;
593+
}
594+
}
595+
596+
if builder.https.is_empty() {
597+
let https_proxy_config = parse_setting_from_dynamic_store(
598+
&proxies_map,
599+
unsafe { kSCPropNetProxiesHTTPSEnable },
600+
unsafe { kSCPropNetProxiesHTTPSProxy },
601+
unsafe { kSCPropNetProxiesHTTPSPort },
602+
);
603+
604+
if let Some(https) = https_proxy_config {
605+
builder.https = https;
606+
}
607+
}
608+
}
609+
610+
fn parse_setting_from_dynamic_store(
611+
proxies_map: &CFDictionary<CFString, CFType>,
612+
enabled_key: CFStringRef,
613+
host_key: CFStringRef,
614+
port_key: CFStringRef,
615+
) -> Option<String> {
616+
let proxy_enabled = proxies_map
617+
.find(enabled_key)
618+
.and_then(|flag| flag.downcast::<CFNumber>())
619+
.and_then(|flag| flag.to_i32())
620+
.unwrap_or(0)
621+
== 1;
622+
623+
if proxy_enabled {
624+
let proxy_host = proxies_map
625+
.find(host_key)
626+
.and_then(|host| host.downcast::<CFString>())
627+
.map(|host| host.to_string());
628+
let proxy_port = proxies_map
629+
.find(port_key)
630+
.and_then(|port| port.downcast::<CFNumber>())
631+
.and_then(|port| port.to_i32());
632+
633+
return match (proxy_host, proxy_port) {
634+
(Some(proxy_host), Some(proxy_port)) => Some(format!("{proxy_host}:{proxy_port}")),
635+
(Some(proxy_host), None) => Some(proxy_host),
636+
(None, Some(_)) => None,
637+
(None, None) => None,
638+
};
639+
}
640+
641+
None
642+
}
643+
}
644+
534645
#[cfg(test)]
535646
mod tests {
536647
use super::*;

0 commit comments

Comments
 (0)