Skip to content

Commit 7b6fbfd

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

File tree

2 files changed

+110
-1
lines changed

2 files changed

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

+105
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,87 @@ mod builder {
531555
}
532556
}
533557

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

0 commit comments

Comments
 (0)