mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
PAC 4 - Add macOS system proxy resolver (#26709)
## Summary Stacked on #26708. Adds the macOS implementation of the shared system-proxy contract. This allows Codex-owned auth clients to use the route macOS selects for each auth URL through SystemConfiguration and CFNetwork, including PAC and WPAD results. The `respect_system_proxy` feature is disabled by default, so existing client behavior remains unchanged unless explicitly enabled. ## Implementation - Adds the macOS-only `system-configuration` dependency to `codex-client`. - Dispatches system-proxy resolution to `outbound_proxy/macos.rs` on macOS. - Reads system proxy settings from `SCDynamicStore` and resolves the target URL with `CFNetworkCopyProxiesForURL`. - Executes PAC URLs and inline PAC JavaScript through a bounded run loop with a five-second timeout. - Handles `DIRECT`, HTTP proxies, and CFNetwork HTTPS entries using HTTP CONNECT; unsupported SOCKS entries map to `UnsupportedProxyScheme`. - Builds concrete proxy URLs from host and port entries, including IPv6 host bracketing. - Maps results into the shared `SystemProxyDecision::{Direct, Proxy, Unavailable}` contract. - Hashes URL-specific cache keys so PAC decisions remain distinct without retaining raw request URLs or query strings. ## End-user behavior - Disabled/default: existing client behavior is unchanged. - Enabled with `[features.respect_system_proxy]`: - macOS auth clients honor system proxy configuration, PAC, and WPAD; - valid OS/PAC `DIRECT` decisions use a direct connection; - unavailable system resolution falls back to explicit environment proxy variables, then `DIRECT`, through the shared contract from #26707. - Unsupported proxy schemes are not silently translated into another route. - Custom CA handling remains separate from proxy selection. - Known limitation: only the first supported system/PAC candidate is used. Subsequent proxy or `DIRECT` candidates are not attempted after a connection failure. This matches the current Windows behavior and leaves room for future ordered-fallback support. ## Tests - `just test -p codex-client` — 34 tests passed. - `just clippy -p codex-client` - `just fmt` - `just bazel-lock-check`
This commit is contained in:
committed by
GitHub
Unverified
parent
ff31ba8d0a
commit
b16d2858f5
Generated
+1
@@ -2405,6 +2405,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"system-configuration",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
||||
@@ -409,6 +409,7 @@ strum_macros = "0.28.0"
|
||||
supports-color = "3.0.2"
|
||||
syntect = "5"
|
||||
sys-locale = "0.3.2"
|
||||
system-configuration = "0.7"
|
||||
tar = { version = "=0.4.45", default-features = false }
|
||||
tempfile = "3.23.0"
|
||||
test-log = "0.2.19"
|
||||
|
||||
@@ -24,8 +24,13 @@ tracing-opentelemetry = { workspace = true }
|
||||
codex-utils-rustls-provider = { workspace = true }
|
||||
zstd = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
sha2 = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
system-configuration = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-sys = { version = "0.52", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Networking_WinHttp",
|
||||
|
||||
@@ -14,9 +14,9 @@ use std::time::Instant;
|
||||
|
||||
use crate::custom_ca::BuildCustomCaTransportError;
|
||||
use crate::custom_ca::build_reqwest_client_with_custom_ca;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use sha2::Digest;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -24,6 +24,8 @@ const SYSTEM_PROXY_SUCCESS_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
const SYSTEM_PROXY_UNAVAILABLE_CACHE_TTL: Duration = Duration::from_secs(5);
|
||||
const SYSTEM_PROXY_CACHE_MAX_ENTRIES: usize = 256;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
@@ -115,15 +117,23 @@ impl From<BuildRouteAwareHttpClientError> for io::Error {
|
||||
/// Builds a reqwest client with conservative route selection and shared CA handling.
|
||||
///
|
||||
/// Unavailable platform resolution falls back to environment proxies and then direct. Errors after
|
||||
/// a route is selected are returned without trying another route.
|
||||
/// a route is selected are returned without trying another route. Ordered PAC candidates are
|
||||
/// currently collapsed to one route on both Windows and macOS; later proxy or `DIRECT` candidates
|
||||
/// are not retried after a connection failure.
|
||||
pub fn build_reqwest_client_for_route(
|
||||
builder: reqwest::ClientBuilder,
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
config: Option<&OutboundProxyConfig>,
|
||||
) -> Result<reqwest::Client, BuildRouteAwareHttpClientError> {
|
||||
let builder =
|
||||
configure_proxy_for_route(&ProcessEnv, builder, request_url, route_class, config)?;
|
||||
let builder = configure_proxy_for_route(
|
||||
&ProcessEnv,
|
||||
builder,
|
||||
request_url,
|
||||
route_class,
|
||||
config,
|
||||
resolve_system_proxy,
|
||||
)?;
|
||||
build_reqwest_client_with_custom_ca(builder).map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -133,6 +143,7 @@ fn configure_proxy_for_route(
|
||||
request_url: &str,
|
||||
route_class: ClientRouteClass,
|
||||
config: Option<&OutboundProxyConfig>,
|
||||
resolve_system_proxy: impl FnOnce(&str, &RequestOrigin) -> SystemProxyDecision,
|
||||
) -> Result<reqwest::ClientBuilder, BuildRouteAwareHttpClientError> {
|
||||
if config.is_none() {
|
||||
return Ok(builder);
|
||||
@@ -217,7 +228,7 @@ impl RequestOrigin {
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
not(target_os = "windows"),
|
||||
not(any(target_os = "windows", target_os = "macos")),
|
||||
allow(
|
||||
dead_code,
|
||||
reason = "Direct and Proxy are constructed only by platform-specific resolvers"
|
||||
@@ -240,12 +251,17 @@ fn resolve_system_proxy(request_url: &str, origin: &RequestOrigin) -> SystemProx
|
||||
decision
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn resolve_platform_system_proxy(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
|
||||
macos::resolve(request_url, origin)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve_platform_system_proxy(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
|
||||
windows::resolve(request_url, origin)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
fn resolve_platform_system_proxy(
|
||||
_request_url: &str,
|
||||
_origin: &RequestOrigin,
|
||||
@@ -317,7 +333,7 @@ fn insert_system_proxy_cache_entry(
|
||||
}
|
||||
|
||||
fn system_proxy_cache_key(request_url: &str) -> String {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
{
|
||||
// Keep URL-specific PAC decisions without retaining the raw routed URL.
|
||||
let mut hasher = Sha256::new();
|
||||
@@ -326,7 +342,7 @@ fn system_proxy_cache_key(request_url: &str) -> String {
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
request_url.to_string()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::RequestOrigin;
|
||||
use super::RouteFailureClass;
|
||||
use super::SystemProxyDecision;
|
||||
use system_configuration::core_foundation::array::CFArray;
|
||||
use system_configuration::core_foundation::array::CFArrayRef;
|
||||
use system_configuration::core_foundation::base::CFEqual;
|
||||
use system_configuration::core_foundation::base::CFGetTypeID;
|
||||
use system_configuration::core_foundation::base::CFIndex;
|
||||
use system_configuration::core_foundation::base::CFType;
|
||||
use system_configuration::core_foundation::base::CFTypeRef;
|
||||
use system_configuration::core_foundation::base::TCFType;
|
||||
use system_configuration::core_foundation::base::kCFAllocatorDefault;
|
||||
use system_configuration::core_foundation::dictionary::CFDictionary;
|
||||
use system_configuration::core_foundation::dictionary::CFDictionaryRef;
|
||||
use system_configuration::core_foundation::error::CFErrorRef;
|
||||
use system_configuration::core_foundation::number::CFNumber;
|
||||
use system_configuration::core_foundation::runloop::CFRunLoop;
|
||||
use system_configuration::core_foundation::runloop::CFRunLoopSource;
|
||||
use system_configuration::core_foundation::runloop::CFRunLoopSourceInvalidate;
|
||||
use system_configuration::core_foundation::runloop::CFRunLoopSourceRef;
|
||||
use system_configuration::core_foundation::runloop::kCFRunLoopDefaultMode;
|
||||
use system_configuration::core_foundation::string::CFString;
|
||||
use system_configuration::core_foundation::string::CFStringRef;
|
||||
use system_configuration::core_foundation::url::CFURL;
|
||||
use system_configuration::core_foundation::url::CFURLCreateWithString;
|
||||
use system_configuration::core_foundation::url::CFURLGetTypeID;
|
||||
use system_configuration::core_foundation::url::CFURLRef;
|
||||
use system_configuration::dynamic_store::SCDynamicStoreBuilder;
|
||||
|
||||
const PAC_EXECUTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
type ProxyDictionary = CFDictionary<CFString, CFType>;
|
||||
type ProxyArray = CFArray<ProxyDictionary>;
|
||||
|
||||
#[repr(C)]
|
||||
struct CFStreamClientContext {
|
||||
version: CFIndex,
|
||||
info: *mut c_void,
|
||||
retain: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>,
|
||||
release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
copy_description: Option<unsafe extern "C" fn(*mut c_void) -> CFStringRef>,
|
||||
}
|
||||
|
||||
type CFProxyAutoConfigurationResultCallback =
|
||||
unsafe extern "C" fn(*mut c_void, CFArrayRef, CFErrorRef);
|
||||
|
||||
#[link(name = "CFNetwork", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
static kCFProxyTypeKey: CFStringRef;
|
||||
static kCFProxyHostNameKey: CFStringRef;
|
||||
static kCFProxyPortNumberKey: CFStringRef;
|
||||
static kCFProxyAutoConfigurationURLKey: CFStringRef;
|
||||
static kCFProxyAutoConfigurationJavaScriptKey: CFStringRef;
|
||||
static kCFProxyTypeNone: CFStringRef;
|
||||
static kCFProxyTypeHTTP: CFStringRef;
|
||||
static kCFProxyTypeHTTPS: CFStringRef;
|
||||
static kCFProxyTypeSOCKS: CFStringRef;
|
||||
static kCFProxyTypeAutoConfigurationURL: CFStringRef;
|
||||
static kCFProxyTypeAutoConfigurationJavaScript: CFStringRef;
|
||||
|
||||
fn CFNetworkCopyProxiesForURL(url: CFURLRef, proxy_settings: CFDictionaryRef) -> CFArrayRef;
|
||||
fn CFNetworkExecuteProxyAutoConfigurationURL(
|
||||
proxy_auto_config_url: CFURLRef,
|
||||
target_url: CFURLRef,
|
||||
callback: CFProxyAutoConfigurationResultCallback,
|
||||
client_context: *mut CFStreamClientContext,
|
||||
) -> CFRunLoopSourceRef;
|
||||
fn CFNetworkExecuteProxyAutoConfigurationScript(
|
||||
proxy_auto_config_script: CFStringRef,
|
||||
target_url: CFURLRef,
|
||||
callback: CFProxyAutoConfigurationResultCallback,
|
||||
client_context: *mut CFStreamClientContext,
|
||||
) -> CFRunLoopSourceRef;
|
||||
}
|
||||
|
||||
pub(super) fn resolve(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
|
||||
let Some(target_url) = cf_url(request_url) else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::InvalidProxyConfig,
|
||||
};
|
||||
};
|
||||
|
||||
let Some(settings) = system_proxy_settings() else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
};
|
||||
|
||||
let Some(proxies) = copy_proxies_for_url(&target_url, &settings) else {
|
||||
return SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
};
|
||||
};
|
||||
|
||||
proxy_array_decision(&proxies, &target_url, origin)
|
||||
}
|
||||
|
||||
fn system_proxy_settings() -> Option<CFDictionary<CFString, CFType>> {
|
||||
let store = SCDynamicStoreBuilder::new("Codex").build()?;
|
||||
store.get_proxies()
|
||||
}
|
||||
|
||||
fn copy_proxies_for_url(
|
||||
target_url: &CFURL,
|
||||
settings: &CFDictionary<CFString, CFType>,
|
||||
) -> Option<ProxyArray> {
|
||||
let proxies = unsafe {
|
||||
CFNetworkCopyProxiesForURL(
|
||||
target_url.as_concrete_TypeRef(),
|
||||
settings.as_concrete_TypeRef(),
|
||||
)
|
||||
};
|
||||
if proxies.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { ProxyArray::wrap_under_create_rule(proxies) })
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_array_decision(
|
||||
proxies: &ProxyArray,
|
||||
target_url: &CFURL,
|
||||
origin: &RequestOrigin,
|
||||
) -> SystemProxyDecision {
|
||||
let mut saw_unsupported = false;
|
||||
let mut saw_unavailable = false;
|
||||
|
||||
// CFNetwork returns candidates in failover order, but the shared resolver currently carries
|
||||
// only one route. This matches the Windows limitation; cross-platform retry requires request
|
||||
// replay semantics and is intentionally deferred.
|
||||
for proxy in proxies {
|
||||
match proxy_entry_decision(&proxy, target_url, origin) {
|
||||
ProxyEntryDecision::Direct => return SystemProxyDecision::Direct,
|
||||
ProxyEntryDecision::Proxy { url } => return SystemProxyDecision::Proxy { url },
|
||||
ProxyEntryDecision::UnsupportedScheme => saw_unsupported = true,
|
||||
ProxyEntryDecision::Unavailable => saw_unavailable = true,
|
||||
}
|
||||
}
|
||||
|
||||
if saw_unsupported {
|
||||
SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::UnsupportedProxyScheme,
|
||||
}
|
||||
} else if saw_unavailable {
|
||||
SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
}
|
||||
} else {
|
||||
SystemProxyDecision::Direct
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_entry_decision(
|
||||
proxy: &ProxyDictionary,
|
||||
target_url: &CFURL,
|
||||
origin: &RequestOrigin,
|
||||
) -> ProxyEntryDecision {
|
||||
let Some(proxy_type) = cf_string_value(proxy, unsafe { kCFProxyTypeKey }) else {
|
||||
return ProxyEntryDecision::Unavailable;
|
||||
};
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeNone }) {
|
||||
return ProxyEntryDecision::Direct;
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTP }) {
|
||||
return concrete_proxy_entry(proxy, "http");
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeHTTPS }) {
|
||||
// CFNetwork's HTTPS proxy type is a tunneling proxy for HTTPS destinations; it does not
|
||||
// preserve an explicit TLS-to-proxy transport. See https://developer.apple.com/documentation/cfnetwork/kcfproxytypehttps.
|
||||
return concrete_proxy_entry(proxy, "http");
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeSOCKS }) {
|
||||
return ProxyEntryDecision::UnsupportedScheme;
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe { kCFProxyTypeAutoConfigurationURL }) {
|
||||
let Some(pac_url) = cf_url_value(proxy, unsafe { kCFProxyAutoConfigurationURLKey }) else {
|
||||
return ProxyEntryDecision::Unavailable;
|
||||
};
|
||||
return pac_decision(execute_pac_url(&pac_url, target_url), target_url, origin);
|
||||
}
|
||||
|
||||
if cf_string_equals(&proxy_type, unsafe {
|
||||
kCFProxyTypeAutoConfigurationJavaScript
|
||||
}) {
|
||||
let Some(script) =
|
||||
cf_string_value(proxy, unsafe { kCFProxyAutoConfigurationJavaScriptKey })
|
||||
else {
|
||||
return ProxyEntryDecision::Unavailable;
|
||||
};
|
||||
return pac_decision(
|
||||
execute_pac(|callback, context| unsafe {
|
||||
CFNetworkExecuteProxyAutoConfigurationScript(
|
||||
script.as_concrete_TypeRef(),
|
||||
target_url.as_concrete_TypeRef(),
|
||||
callback,
|
||||
context,
|
||||
)
|
||||
}),
|
||||
target_url,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
ProxyEntryDecision::Unavailable
|
||||
}
|
||||
|
||||
fn pac_decision(
|
||||
result: Result<ProxyArray, RouteFailureClass>,
|
||||
target_url: &CFURL,
|
||||
origin: &RequestOrigin,
|
||||
) -> ProxyEntryDecision {
|
||||
let proxies = match result {
|
||||
Ok(proxies) => proxies,
|
||||
Err(RouteFailureClass::UnsupportedProxyScheme) => {
|
||||
return ProxyEntryDecision::UnsupportedScheme;
|
||||
}
|
||||
Err(_) => return ProxyEntryDecision::Unavailable,
|
||||
};
|
||||
|
||||
match proxy_array_decision(&proxies, target_url, origin) {
|
||||
SystemProxyDecision::Direct => ProxyEntryDecision::Direct,
|
||||
SystemProxyDecision::Proxy { url } => ProxyEntryDecision::Proxy { url },
|
||||
SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::UnsupportedProxyScheme,
|
||||
} => ProxyEntryDecision::UnsupportedScheme,
|
||||
SystemProxyDecision::Unavailable { failure: _ } => ProxyEntryDecision::Unavailable,
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_pac_url(pac_url: &CFURL, target_url: &CFURL) -> Result<ProxyArray, RouteFailureClass> {
|
||||
execute_pac(|callback, context| unsafe {
|
||||
CFNetworkExecuteProxyAutoConfigurationURL(
|
||||
pac_url.as_concrete_TypeRef(),
|
||||
target_url.as_concrete_TypeRef(),
|
||||
callback,
|
||||
context,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn execute_pac(
|
||||
create_source: impl FnOnce(
|
||||
CFProxyAutoConfigurationResultCallback,
|
||||
*mut CFStreamClientContext,
|
||||
) -> CFRunLoopSourceRef,
|
||||
) -> Result<ProxyArray, RouteFailureClass> {
|
||||
let mut state = PacRunLoopState { result: None };
|
||||
let mut context = CFStreamClientContext {
|
||||
version: 0,
|
||||
info: (&mut state as *mut PacRunLoopState).cast::<c_void>(),
|
||||
retain: None,
|
||||
release: None,
|
||||
copy_description: None,
|
||||
};
|
||||
|
||||
let source = create_source(pac_result_callback, &mut context);
|
||||
if source.is_null() {
|
||||
return Err(RouteFailureClass::ProxyResolutionUnavailable);
|
||||
}
|
||||
|
||||
let source = unsafe { CFRunLoopSource::wrap_under_create_rule(source) };
|
||||
let run_loop = CFRunLoop::get_current();
|
||||
let mode = unsafe { kCFRunLoopDefaultMode };
|
||||
run_loop.add_source(&source, mode);
|
||||
|
||||
let started_at = Instant::now();
|
||||
while state.result.is_none() && started_at.elapsed() < PAC_EXECUTION_TIMEOUT {
|
||||
CFRunLoop::run_in_mode(mode, Duration::from_millis(50), true);
|
||||
}
|
||||
|
||||
if state.result.is_none() {
|
||||
unsafe { CFRunLoopSourceInvalidate(source.as_concrete_TypeRef()) };
|
||||
}
|
||||
run_loop.remove_source(&source, mode);
|
||||
state
|
||||
.result
|
||||
.unwrap_or(Err(RouteFailureClass::ConnectTimeout))
|
||||
}
|
||||
|
||||
unsafe extern "C" fn pac_result_callback(
|
||||
client: *mut c_void,
|
||||
proxies: CFArrayRef,
|
||||
error: CFErrorRef,
|
||||
) {
|
||||
let state = unsafe { &mut *client.cast::<PacRunLoopState>() };
|
||||
state.result = if !error.is_null() || proxies.is_null() {
|
||||
Some(Err(RouteFailureClass::ProxyResolutionUnavailable))
|
||||
} else {
|
||||
Some(Ok(unsafe { ProxyArray::wrap_under_get_rule(proxies) }))
|
||||
};
|
||||
CFRunLoop::get_current().stop();
|
||||
}
|
||||
|
||||
struct PacRunLoopState {
|
||||
result: Option<Result<ProxyArray, RouteFailureClass>>,
|
||||
}
|
||||
|
||||
fn concrete_proxy_entry(proxy: &ProxyDictionary, proxy_scheme: &str) -> ProxyEntryDecision {
|
||||
let Some(host) = cf_string_value(proxy, unsafe { kCFProxyHostNameKey })
|
||||
.map(|host| host.to_string())
|
||||
.filter(|host| !host.is_empty())
|
||||
else {
|
||||
return ProxyEntryDecision::Unavailable;
|
||||
};
|
||||
|
||||
let host = bracket_ipv6_host(&host);
|
||||
let url = match cf_i32_value(proxy, unsafe { kCFProxyPortNumberKey }) {
|
||||
Some(port) if port > 0 => format!("{proxy_scheme}://{host}:{port}"),
|
||||
_ => format!("{proxy_scheme}://{host}"),
|
||||
};
|
||||
ProxyEntryDecision::Proxy { url }
|
||||
}
|
||||
|
||||
fn bracket_ipv6_host(host: &str) -> String {
|
||||
if host.contains(':') && !host.starts_with('[') {
|
||||
format!("[{host}]")
|
||||
} else {
|
||||
host.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn cf_string_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<CFString> {
|
||||
proxy
|
||||
.find(key)
|
||||
.and_then(|value| value.downcast::<CFString>())
|
||||
}
|
||||
|
||||
fn cf_i32_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<i32> {
|
||||
proxy
|
||||
.find(key)
|
||||
.and_then(|value| value.downcast::<CFNumber>())
|
||||
.and_then(|value| value.to_i32())
|
||||
}
|
||||
|
||||
fn cf_url_value(proxy: &ProxyDictionary, key: CFStringRef) -> Option<CFURL> {
|
||||
proxy.find(key).and_then(|value| {
|
||||
if unsafe { CFGetTypeID(value.as_CFTypeRef()) == CFURLGetTypeID() } {
|
||||
Some(unsafe { CFURL::wrap_under_get_rule(value.as_CFTypeRef() as CFURLRef) })
|
||||
} else {
|
||||
value
|
||||
.downcast::<CFString>()
|
||||
.and_then(|value| cf_url(value.to_string().as_str()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn cf_string_equals(value: &CFString, expected: CFStringRef) -> bool {
|
||||
unsafe { CFEqual(value.as_CFTypeRef(), expected as CFTypeRef) != 0 }
|
||||
}
|
||||
|
||||
fn cf_url(value: &str) -> Option<CFURL> {
|
||||
let value = CFString::new(value);
|
||||
let url = unsafe {
|
||||
CFURLCreateWithString(
|
||||
kCFAllocatorDefault,
|
||||
value.as_concrete_TypeRef(),
|
||||
ptr::null(),
|
||||
)
|
||||
};
|
||||
if url.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { CFURL::wrap_under_create_rule(url) })
|
||||
}
|
||||
}
|
||||
|
||||
enum ProxyEntryDecision {
|
||||
Direct,
|
||||
Proxy { url: String },
|
||||
UnsupportedScheme,
|
||||
Unavailable,
|
||||
}
|
||||
@@ -84,6 +84,9 @@ async fn enabled_environment_proxy_routes_request_through_proxy() {
|
||||
request_url,
|
||||
ClientRouteClass::Auth,
|
||||
Some(&config),
|
||||
|_, _| SystemProxyDecision::Unavailable {
|
||||
failure: RouteFailureClass::ProxyResolutionUnavailable,
|
||||
},
|
||||
)
|
||||
.expect("enabled proxy route should configure");
|
||||
|
||||
@@ -204,6 +207,6 @@ fn system_proxy_cache_key_preserves_url_specific_pac_decisions() {
|
||||
cache_key,
|
||||
system_proxy_cache_key("https://auth.openai.com/oauth/revoke")
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
assert!(!cache_key.contains(request_url));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user