diff --git a/codex-rs/codex-client/src/outbound_proxy.rs b/codex-rs/codex-client/src/outbound_proxy.rs index abfda68aea..3f97aa6f7a 100644 --- a/codex-rs/codex-client/src/outbound_proxy.rs +++ b/codex-rs/codex-client/src/outbound_proxy.rs @@ -593,22 +593,53 @@ enum ParsedProxyListDecision { fn parse_proxy_list(input: &str, target_scheme: &str) -> ParsedProxyListDecision { let mut saw_unsupported = false; let mut http_fallback = None; - for token in input - .split(';') - .map(str::trim) - .filter(|token| !token.is_empty()) + { - if target_scheme == "https" - && http_fallback.is_none() - && let Some(ParsedProxyListDecision::Proxy(url)) = parse_proxy_key_token(token, "http") + let mut process_token = |token: &str| { + if target_scheme == "https" + && http_fallback.is_none() + && let Some(ParsedProxyListDecision::Proxy(url)) = + parse_proxy_key_token(token, "http") + { + http_fallback = Some(url); + } + match parse_proxy_token(token, target_scheme) { + ParsedProxyListDecision::Direct => Some(ParsedProxyListDecision::Direct), + ParsedProxyListDecision::Proxy(url) => Some(ParsedProxyListDecision::Proxy(url)), + ParsedProxyListDecision::UnsupportedScheme => { + saw_unsupported = true; + None + } + ParsedProxyListDecision::Unavailable => None, + } + }; + + for segment in input + .split(';') + .map(str::trim) + .filter(|segment| !segment.is_empty()) { - http_fallback = Some(url); - } - match parse_proxy_token(token, target_scheme) { - ParsedProxyListDecision::Direct => return ParsedProxyListDecision::Direct, - ParsedProxyListDecision::Proxy(url) => return ParsedProxyListDecision::Proxy(url), - ParsedProxyListDecision::UnsupportedScheme => saw_unsupported = true, - ParsedProxyListDecision::Unavailable => {} + let mut parts = segment.split_whitespace(); + let directive = parts.next(); + let hostport = parts.next(); + let extra = parts.next(); + let is_proxy_directive = matches!( + directive.map(str::to_ascii_lowercase).as_deref(), + Some("proxy" | "http" | "https" | "socks" | "socks4" | "socks5") + ) && hostport.is_some() + && extra.is_none(); + + if is_proxy_directive { + if let Some(decision) = process_token(segment) { + return decision; + } + } else { + for token in segment.split_whitespace() { + if let Some(decision) = process_token(token) { + return decision; + } + } + } } } @@ -634,10 +665,11 @@ fn parse_proxy_token(token: &str, target_scheme: &str) -> ParsedProxyListDecisio return ParsedProxyListDecision::Unavailable; } - if let Some((scheme, hostport)) = token.split_once(' ') { - let scheme = scheme.trim().to_ascii_lowercase(); - let hostport = hostport.trim(); - return match scheme.as_str() { + let mut parts = token.split_whitespace(); + let directive = parts.next(); + let hostport = parts.next(); + if let (Some(directive), Some(hostport), None) = (directive, hostport, parts.next()) { + return match directive.to_ascii_lowercase().as_str() { "proxy" | "http" => proxy_url_from_hostport("http", hostport), "https" => proxy_url_from_hostport("https", hostport), "socks" | "socks4" | "socks5" => ParsedProxyListDecision::UnsupportedScheme, @@ -692,6 +724,10 @@ mod tests { parse_proxy_list("http=web-proxy:8080;https=secure-proxy:8443", "https"), ParsedProxyListDecision::Proxy("http://secure-proxy:8443".to_string()) ); + assert_eq!( + parse_proxy_list("http=web-proxy:8080 https=secure-proxy:8443", "https"), + ParsedProxyListDecision::Proxy("http://secure-proxy:8443".to_string()) + ); assert_eq!( parse_proxy_list("proxy.internal:8080", "https"), ParsedProxyListDecision::Proxy("http://proxy.internal:8080".to_string()) diff --git a/codex-rs/codex-client/src/outbound_proxy/windows.rs b/codex-rs/codex-client/src/outbound_proxy/windows.rs index c073e4cdaa..aa73ee4d3c 100644 --- a/codex-rs/codex-client/src/outbound_proxy/windows.rs +++ b/codex-rs/codex-client/src/outbound_proxy/windows.rs @@ -7,6 +7,7 @@ use super::RouteFailureClass; use super::SystemProxyDecision; use super::no_proxy_matches_origin; use super::parse_proxy_list; +use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND; use windows_sys::Win32::Foundation::FALSE; use windows_sys::Win32::Foundation::GetLastError; use windows_sys::Win32::Foundation::GlobalFree; @@ -192,7 +193,11 @@ fn current_user_ie_proxy_config() -> Result { }; let ok = unsafe { WinHttpGetIEProxyConfigForCurrentUser(&mut raw) }; if ok == FALSE { - return Err(classify_winhttp_error(last_error())); + let error = last_error(); + if error == ERROR_FILE_NOT_FOUND { + return Ok(IeProxyConfig::default()); + } + return Err(classify_winhttp_error(error)); } let auto_config_url = GlobalWideString::from_raw(raw.lpszAutoConfigUrl).into_string(); @@ -207,7 +212,7 @@ fn current_user_ie_proxy_config() -> Result { }) } -#[derive(Debug)] +#[derive(Debug, Default)] struct IeProxyConfig { auto_detect: bool, auto_config_url: Option, @@ -294,15 +299,23 @@ impl Drop for WinHttpSession { } fn proxy_bypass_matches_origin(proxy_bypass: &str, origin: &RequestOrigin) -> bool { - proxy_bypass.split([';', ',']).map(str::trim).any(|entry| { - if entry.eq_ignore_ascii_case("") { - !origin.host.contains('.') - } else { - no_proxy_matches_origin(entry, origin) - } - }) + proxy_bypass + .split(|ch: char| ch == ';' || ch == ',' || ch.is_whitespace()) + .map(str::trim) + .filter(|entry| !entry.is_empty()) + .any(|entry| { + if entry.eq_ignore_ascii_case("") { + !origin.host.contains('.') + } else { + no_proxy_matches_origin(entry, origin) + } + }) } +#[cfg(test)] +#[path = "windows_tests.rs"] +mod tests; + fn wide_null(value: &str) -> Vec { value.encode_utf16().chain(std::iter::once(0)).collect() } diff --git a/codex-rs/codex-client/src/outbound_proxy/windows_tests.rs b/codex-rs/codex-client/src/outbound_proxy/windows_tests.rs new file mode 100644 index 0000000000..3772c057db --- /dev/null +++ b/codex-rs/codex-client/src/outbound_proxy/windows_tests.rs @@ -0,0 +1,18 @@ +use super::*; + +#[test] +fn proxy_bypass_matches_whitespace_separated_winhttp_entries() { + let local_origin = RequestOrigin { + scheme: "https".to_string(), + host: "intranet".to_string(), + port: 443, + }; + assert!(proxy_bypass_matches_origin(" *.corp", &local_origin)); + + let corp_origin = RequestOrigin { + scheme: "https".to_string(), + host: "service.corp".to_string(), + port: 443, + }; + assert!(proxy_bypass_matches_origin(" *.corp", &corp_origin)); +}