PAC 3 - Add Windows system proxy resolver (#26708)

## Summary

Stacked on #26707.

Adds the Windows implementation of the shared system-proxy contract.
This allows Codex-owned auth clients to use the route Windows selects
for each auth URL, including explicit PAC configuration, WPAD
auto-detection, static proxies, and bypass rules.

The `respect_system_proxy` feature is disabled by default, so existing
client behavior remains unchanged unless explicitly enabled.

## Implementation

- Adds Windows-only `codex-client` dependencies:
- `windows-sys` with `Win32_Foundation` and `Win32_Networking_WinHttp`;
  - `sha2` for redacted cache keys.
- Dispatches system-proxy resolution to `outbound_proxy/windows.rs` on
Windows.
- Reads the current-user WinHTTP/IE proxy configuration via
`WinHttpGetIEProxyConfigForCurrentUser`.
- Resolves explicit PAC URLs first, then OS-enabled WPAD auto-detection,
then static proxy and bypass settings.
- Uses `WinHttpGetProxyForUrl` for PAC/WPAD and maps results into the
shared `SystemProxyDecision::{Direct, Proxy, Unavailable}` contract.
- Parses `DIRECT`, `PROXY`, `HTTPS`, and keyed static proxy entries.
- Treats unsupported schemes such as SOCKS as unavailable so the shared
resolver can apply its environment-proxy fallback.
- Handles Windows bypass entries, including `<local>` and host, suffix,
wildcard, and port matching.
- Releases WinHTTP-owned strings with `GlobalFree` and closes sessions
with `WinHttpCloseHandle`.
- Hashes URL-specific cache keys with SHA-256 so PAC decisions remain
URL-specific without retaining raw request URLs or query strings.

## End-user behavior

- Disabled/default: existing client behavior is unchanged.
- Enabled with `[features.respect_system_proxy]`:
- Windows auth clients honor explicit PAC configuration, OS-enabled
WPAD, static proxies, and bypass rules;
  - 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 a different
route.
- Custom CA handling remains separate from proxy selection.

## Tests

Adds coverage for:

- PAC-style proxy tokens such as `PROXY proxy.internal:8080` and `HTTPS
proxy.internal:8443`;
- static WinHTTP proxy entries keyed by target scheme;
- `DIRECT` and unsupported proxy-token behavior;
- Windows bypass matching, including `<local>`, wildcard, suffix, and
port-qualified entries;
- preserving URL-specific PAC cache decisions without retaining the raw
URL on Windows.
This commit is contained in:
canvrno-oai
2026-06-22 14:38:33 -07:00
committed by GitHub
Unverified
parent a72433d560
commit 5f129a4703
6 changed files with 696 additions and 13 deletions
+2
View File
@@ -2404,12 +2404,14 @@ dependencies = [
"rustls-pki-types",
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
"thiserror 2.0.18",
"tokio",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
"windows-sys 0.52.0",
"zstd 0.13.3",
]
+7
View File
@@ -24,6 +24,13 @@ tracing-opentelemetry = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
zstd = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
sha2 = { workspace = true }
windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_Networking_WinHttp",
] }
[lints]
workspace = true
+231 -13
View File
@@ -14,12 +14,19 @@ use std::time::Instant;
use crate::custom_ca::BuildCustomCaTransportError;
use crate::custom_ca::build_reqwest_client_with_custom_ca;
#[cfg(target_os = "windows")]
use sha2::Digest;
#[cfg(target_os = "windows")]
use sha2::Sha256;
use thiserror::Error;
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 = "windows")]
mod windows;
/// Coarse semantic bucket for the HTTP or WebSocket client being constructed.
///
/// This is not the selected proxy route or a concrete endpoint. It labels the
@@ -209,11 +216,14 @@ impl RequestOrigin {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(
dead_code,
reason = "Direct and Proxy are constructed by platform resolvers added in later PRs"
#[cfg_attr(
not(target_os = "windows"),
allow(
dead_code,
reason = "Direct and Proxy are constructed only by platform-specific resolvers"
)
)]
#[derive(Debug, Clone, PartialEq, Eq)]
enum SystemProxyDecision {
Direct,
Proxy { url: String },
@@ -230,6 +240,12 @@ fn resolve_system_proxy(request_url: &str, origin: &RequestOrigin) -> SystemProx
decision
}
#[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"))]
fn resolve_platform_system_proxy(
_request_url: &str,
_origin: &RequestOrigin,
@@ -251,24 +267,26 @@ static SYSTEM_PROXY_CACHE: OnceLock<Mutex<HashMap<String, CachedSystemProxyDecis
fn cached_system_proxy_decision(request_url: &str) -> Option<SystemProxyDecision> {
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut cache = cache.lock().ok()?;
let cached = cache.get(request_url)?;
let key = system_proxy_cache_key(request_url);
let cached = cache.get(&key)?;
if cached.expires_at > Instant::now() {
return Some(cached.decision.clone());
}
cache.remove(request_url);
cache.remove(&key);
None
}
fn cache_system_proxy_decision(request_url: &str, decision: SystemProxyDecision) {
let cache = SYSTEM_PROXY_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(mut cache) = cache.lock() {
insert_system_proxy_cache_entry(&mut cache, request_url, decision, Instant::now());
let cache_key = system_proxy_cache_key(request_url);
insert_system_proxy_cache_entry(&mut cache, &cache_key, decision, Instant::now());
}
}
fn insert_system_proxy_cache_entry(
cache: &mut HashMap<String, CachedSystemProxyDecision>,
request_url: &str,
cache_key: &str,
decision: SystemProxyDecision,
now: Instant,
) {
@@ -281,16 +299,16 @@ fn insert_system_proxy_cache_entry(
cache.retain(|_, cached| cached.expires_at > now);
if cache.len() >= SYSTEM_PROXY_CACHE_MAX_ENTRIES
&& !cache.contains_key(request_url)
&& let Some(request_url_to_evict) = cache
&& !cache.contains_key(cache_key)
&& let Some(cache_key_to_evict) = cache
.iter()
.min_by_key(|(_, cached)| cached.expires_at)
.map(|(request_url, _)| request_url.clone())
.map(|(cache_key, _)| cache_key.clone())
{
cache.remove(&request_url_to_evict);
cache.remove(&cache_key_to_evict);
}
cache.insert(
request_url.to_string(),
cache_key.to_string(),
CachedSystemProxyDecision {
decision,
expires_at: now + ttl,
@@ -298,6 +316,206 @@ fn insert_system_proxy_cache_entry(
);
}
fn system_proxy_cache_key(request_url: &str) -> String {
#[cfg(target_os = "windows")]
{
// Keep URL-specific PAC decisions without retaining the raw routed URL.
let mut hasher = Sha256::new();
hasher.update(b"system-proxy-cache-v1\0");
hasher.update(request_url.as_bytes());
format!("{:x}", hasher.finalize())
}
#[cfg(not(target_os = "windows"))]
request_url.to_string()
}
#[cfg(any(test, target_os = "windows"))]
fn no_proxy_matches_origin(no_proxy: &str, origin: &RequestOrigin) -> bool {
no_proxy
.split(',')
.map(str::trim)
.filter(|entry| !entry.is_empty())
.any(|entry| no_proxy_entry_matches_origin(entry, origin))
}
#[cfg(any(test, target_os = "windows"))]
fn no_proxy_entry_matches_origin(entry: &str, origin: &RequestOrigin) -> bool {
if entry == "*" {
return true;
}
let mut entry = entry
.strip_prefix("http://")
.or_else(|| entry.strip_prefix("https://"))
.unwrap_or(entry)
.trim_matches(['[', ']'])
.to_ascii_lowercase();
let mut port = None;
let parsed_host_port = entry.rsplit_once(':').and_then(|(host, candidate_port)| {
if host.contains(':') {
return None;
}
candidate_port
.parse::<u16>()
.ok()
.map(|parsed_port| (host.to_string(), parsed_port))
});
if let Some((host, parsed_port)) = parsed_host_port {
entry = host;
port = Some(parsed_port);
}
if port.is_some_and(|port| port != origin.port) {
return false;
}
if let Some(suffix) = entry.strip_prefix('.') {
return origin.host == suffix || origin.host.ends_with(&format!(".{suffix}"));
}
if entry.contains('*') {
return wildcard_host_match(&entry, &origin.host);
}
origin.host == entry
}
#[cfg(any(test, target_os = "windows"))]
fn wildcard_host_match(pattern: &str, host: &str) -> bool {
let mut remaining = host;
let mut first = true;
for part in pattern.split('*') {
if part.is_empty() {
continue;
}
if first && !pattern.starts_with('*') {
let Some(stripped) = remaining.strip_prefix(part) else {
return false;
};
remaining = stripped;
} else {
let Some(index) = remaining.find(part) else {
return false;
};
remaining = &remaining[index + part.len()..];
}
first = false;
}
pattern.ends_with('*') || remaining.is_empty()
}
#[cfg(any(test, target_os = "windows"))]
#[derive(Debug, Clone, PartialEq, Eq)]
enum ParsedProxyListDecision {
Direct,
Proxy(String),
UnsupportedScheme,
Unavailable,
}
#[cfg(any(test, target_os = "windows"))]
fn parse_proxy_list(input: &str, target_scheme: &str) -> ParsedProxyListDecision {
let mut saw_unsupported = false;
{
let mut process_token = |token: &str| {
let decision = parse_proxy_token(token, target_scheme);
match decision {
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())
{
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;
}
}
}
}
}
if saw_unsupported {
ParsedProxyListDecision::UnsupportedScheme
} else {
ParsedProxyListDecision::Unavailable
}
}
#[cfg(any(test, target_os = "windows"))]
fn parse_proxy_token(token: &str, target_scheme: &str) -> ParsedProxyListDecision {
if token.eq_ignore_ascii_case("DIRECT") {
return ParsedProxyListDecision::Direct;
}
if let Some(decision) = parse_proxy_key_token(token, target_scheme) {
return decision;
}
if token.contains('=') {
return ParsedProxyListDecision::Unavailable;
}
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,
_ => ParsedProxyListDecision::Unavailable,
};
}
proxy_url_from_hostport("http", token)
}
#[cfg(any(test, target_os = "windows"))]
fn parse_proxy_key_token(token: &str, target_scheme: &str) -> Option<ParsedProxyListDecision> {
let (key, value) = token.split_once('=')?;
if key.trim().eq_ignore_ascii_case(target_scheme) {
Some(proxy_url_from_hostport("http", value.trim()))
} else {
Some(ParsedProxyListDecision::Unavailable)
}
}
#[cfg(any(test, target_os = "windows"))]
fn proxy_url_from_hostport(proxy_scheme: &str, hostport: &str) -> ParsedProxyListDecision {
if hostport.is_empty() {
return ParsedProxyListDecision::Unavailable;
}
if hostport.contains("://") {
return ParsedProxyListDecision::Proxy(hostport.to_string());
}
ParsedProxyListDecision::Proxy(format!("{proxy_scheme}://{hostport}"))
}
trait EnvSource {
fn var(&self, key: &str) -> Option<String>;
}
@@ -0,0 +1,362 @@
use std::ffi::c_void;
use std::ptr;
use super::ParsedProxyListDecision;
use super::RequestOrigin;
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;
use windows_sys::Win32::Foundation::TRUE;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_AUTODETECTION_FAILED;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_CANNOT_CONNECT;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_CONNECTION_ERROR;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_INVALID_URL;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_LOGIN_FAILURE;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_NAME_NOT_RESOLVED;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SCRIPT_EXECUTION_ERROR;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_CERT_CN_INVALID;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_CERT_DATE_INVALID;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_CERT_REV_FAILED;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_CERT_REVOKED;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_CERT_WRONG_USAGE;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_CHANNEL_ERROR;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_FAILURE;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_INVALID_CA;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_SECURE_INVALID_CERT;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_TIMEOUT;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_UNHANDLED_SCRIPT_TYPE;
use windows_sys::Win32::Networking::WinHttp::ERROR_WINHTTP_UNRECOGNIZED_SCHEME;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_ACCESS_TYPE_NAMED_PROXY;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_ACCESS_TYPE_NO_PROXY;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_AUTO_DETECT_TYPE_DHCP;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_AUTO_DETECT_TYPE_DNS_A;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_AUTOPROXY_AUTO_DETECT;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_AUTOPROXY_CONFIG_URL;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_AUTOPROXY_OPTIONS;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_CURRENT_USER_IE_PROXY_CONFIG;
use windows_sys::Win32::Networking::WinHttp::WINHTTP_PROXY_INFO;
use windows_sys::Win32::Networking::WinHttp::WinHttpCloseHandle;
use windows_sys::Win32::Networking::WinHttp::WinHttpGetIEProxyConfigForCurrentUser;
use windows_sys::Win32::Networking::WinHttp::WinHttpGetProxyForUrl;
use windows_sys::Win32::Networking::WinHttp::WinHttpOpen;
use windows_sys::core::PWSTR;
pub(super) fn resolve(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
let ie_config = match current_user_ie_proxy_config() {
Ok(config) => config,
Err(failure) => {
return SystemProxyDecision::Unavailable { failure };
}
};
if let Some(pac_url) = ie_config.auto_config_url.as_deref() {
let decision = resolve_with_pac_url(request_url, origin, pac_url);
if !matches!(decision, SystemProxyDecision::Unavailable { .. }) {
return decision;
}
}
if ie_config.auto_detect {
let decision = resolve_with_auto_detect(request_url, origin);
if !matches!(decision, SystemProxyDecision::Unavailable { .. }) {
return decision;
}
}
if let Some(proxy) = ie_config.static_proxy.as_deref() {
if ie_config
.proxy_bypass
.as_deref()
.is_some_and(|bypass| proxy_bypass_matches_origin(bypass, origin))
{
return SystemProxyDecision::Direct;
}
return proxy_list_decision(proxy, origin);
}
if ie_config.auto_config_url.is_some() || ie_config.auto_detect {
SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
}
} else {
SystemProxyDecision::Direct
}
}
fn resolve_with_pac_url(
request_url: &str,
origin: &RequestOrigin,
pac_url: &str,
) -> SystemProxyDecision {
let pac_url = wide_null(pac_url);
let options = WINHTTP_AUTOPROXY_OPTIONS {
dwFlags: WINHTTP_AUTOPROXY_CONFIG_URL,
dwAutoDetectFlags: 0,
lpszAutoConfigUrl: pac_url.as_ptr(),
lpvReserved: ptr::null_mut(),
dwReserved: 0,
fAutoLogonIfChallenged: TRUE,
};
resolve_with_winhttp_options(request_url, origin, options)
}
fn resolve_with_auto_detect(request_url: &str, origin: &RequestOrigin) -> SystemProxyDecision {
let options = WINHTTP_AUTOPROXY_OPTIONS {
dwFlags: WINHTTP_AUTOPROXY_AUTO_DETECT,
dwAutoDetectFlags: WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A,
lpszAutoConfigUrl: ptr::null(),
lpvReserved: ptr::null_mut(),
dwReserved: 0,
fAutoLogonIfChallenged: FALSE,
};
resolve_with_winhttp_options(request_url, origin, options)
}
fn resolve_with_winhttp_options(
request_url: &str,
origin: &RequestOrigin,
mut options: WINHTTP_AUTOPROXY_OPTIONS,
) -> SystemProxyDecision {
let Some(session) = WinHttpSession::open() else {
return SystemProxyDecision::Unavailable {
failure: classify_winhttp_error(last_error()),
};
};
let request_url = wide_null(request_url);
let mut proxy_info = WINHTTP_PROXY_INFO {
dwAccessType: WINHTTP_ACCESS_TYPE_NO_PROXY,
lpszProxy: ptr::null_mut(),
lpszProxyBypass: ptr::null_mut(),
};
let ok = unsafe {
WinHttpGetProxyForUrl(
session.0,
request_url.as_ptr(),
&mut options,
&mut proxy_info,
)
};
if ok == FALSE {
return SystemProxyDecision::Unavailable {
failure: classify_winhttp_error(last_error()),
};
}
let proxy_info = ProxyInfo::from_raw(proxy_info);
if proxy_info.access_type == WINHTTP_ACCESS_TYPE_NO_PROXY {
return SystemProxyDecision::Direct;
}
if proxy_info.access_type != WINHTTP_ACCESS_TYPE_NAMED_PROXY {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
}
let Some(proxy) = proxy_info.proxy.as_deref() else {
return SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
};
};
proxy_list_decision(proxy, origin)
}
fn proxy_list_decision(proxy_list: &str, origin: &RequestOrigin) -> SystemProxyDecision {
match parse_proxy_list(proxy_list, &origin.scheme) {
ParsedProxyListDecision::Direct => SystemProxyDecision::Direct,
ParsedProxyListDecision::Proxy(url) => SystemProxyDecision::Proxy { url },
ParsedProxyListDecision::UnsupportedScheme => SystemProxyDecision::Unavailable {
failure: RouteFailureClass::UnsupportedProxyScheme,
},
ParsedProxyListDecision::Unavailable => SystemProxyDecision::Unavailable {
failure: RouteFailureClass::ProxyResolutionUnavailable,
},
}
}
fn current_user_ie_proxy_config() -> Result<IeProxyConfig, RouteFailureClass> {
let mut raw = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG {
fAutoDetect: FALSE,
lpszAutoConfigUrl: ptr::null_mut(),
lpszProxy: ptr::null_mut(),
lpszProxyBypass: ptr::null_mut(),
};
let ok = unsafe { WinHttpGetIEProxyConfigForCurrentUser(&mut raw) };
if ok == FALSE {
let error = last_error();
if error == ERROR_FILE_NOT_FOUND {
// Match WinHTTP's fallback by attempting WPAD when no IE proxy settings exist.
return Ok(IeProxyConfig {
auto_detect: true,
..Default::default()
});
}
return Err(classify_winhttp_error(error));
}
let auto_config_url = GlobalWideString::from_raw(raw.lpszAutoConfigUrl).into_string();
let static_proxy = GlobalWideString::from_raw(raw.lpszProxy).into_string();
let proxy_bypass = GlobalWideString::from_raw(raw.lpszProxyBypass).into_string();
Ok(IeProxyConfig {
auto_detect: raw.fAutoDetect != FALSE,
auto_config_url,
static_proxy,
proxy_bypass,
})
}
#[derive(Debug, Default)]
struct IeProxyConfig {
auto_detect: bool,
auto_config_url: Option<String>,
static_proxy: Option<String>,
proxy_bypass: Option<String>,
}
struct ProxyInfo {
access_type: u32,
proxy: Option<String>,
_proxy_bypass: Option<String>,
}
impl ProxyInfo {
fn from_raw(raw: WINHTTP_PROXY_INFO) -> Self {
Self {
access_type: raw.dwAccessType,
proxy: GlobalWideString::from_raw(raw.lpszProxy).into_string(),
_proxy_bypass: GlobalWideString::from_raw(raw.lpszProxyBypass).into_string(),
}
}
}
struct GlobalWideString(PWSTR);
impl GlobalWideString {
fn from_raw(ptr: PWSTR) -> Self {
Self(ptr)
}
fn into_string(self) -> Option<String> {
if self.0.is_null() {
return None;
}
let string = unsafe { wide_ptr_to_string(self.0) };
if string.is_empty() {
None
} else {
Some(string)
}
}
}
impl Drop for GlobalWideString {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
GlobalFree(self.0.cast::<c_void>());
}
}
}
}
struct WinHttpSession(*mut c_void);
impl WinHttpSession {
fn open() -> Option<Self> {
let agent = wide_null("Codex");
let handle = unsafe {
WinHttpOpen(
agent.as_ptr(),
WINHTTP_ACCESS_TYPE_NO_PROXY,
ptr::null(),
ptr::null(),
0,
)
};
if handle.is_null() {
None
} else {
Some(Self(handle))
}
}
}
impl Drop for WinHttpSession {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe {
WinHttpCloseHandle(self.0);
}
}
}
}
fn proxy_bypass_matches_origin(proxy_bypass: &str, origin: &RequestOrigin) -> bool {
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("<local>") {
!origin.host.contains('.')
} else {
no_proxy_matches_origin(entry, origin)
}
})
}
#[cfg(test)]
#[path = "windows_tests.rs"]
mod tests;
fn wide_null(value: &str) -> Vec<u16> {
value.encode_utf16().chain(std::iter::once(0)).collect()
}
unsafe fn wide_ptr_to_string(ptr: PWSTR) -> String {
let mut len = 0;
while unsafe { *ptr.add(len) } != 0 {
len += 1;
}
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
String::from_utf16_lossy(slice)
}
fn last_error() -> u32 {
unsafe { GetLastError() }
}
fn classify_winhttp_error(code: u32) -> RouteFailureClass {
match code {
ERROR_WINHTTP_TIMEOUT => RouteFailureClass::ConnectTimeout,
ERROR_WINHTTP_LOGIN_FAILURE => RouteFailureClass::ProxyAuthenticationRequired,
ERROR_WINHTTP_AUTODETECTION_FAILED
| ERROR_WINHTTP_BAD_AUTO_PROXY_SCRIPT
| ERROR_WINHTTP_SCRIPT_EXECUTION_ERROR
| ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT
| ERROR_WINHTTP_UNHANDLED_SCRIPT_TYPE => RouteFailureClass::ProxyResolutionUnavailable,
ERROR_WINHTTP_SECURE_CERT_CN_INVALID
| ERROR_WINHTTP_SECURE_CERT_DATE_INVALID
| ERROR_WINHTTP_SECURE_CERT_REVOKED
| ERROR_WINHTTP_SECURE_CERT_REV_FAILED
| ERROR_WINHTTP_SECURE_CERT_WRONG_USAGE
| ERROR_WINHTTP_SECURE_CHANNEL_ERROR
| ERROR_WINHTTP_SECURE_FAILURE
| ERROR_WINHTTP_SECURE_INVALID_CA
| ERROR_WINHTTP_SECURE_INVALID_CERT => RouteFailureClass::TlsError,
ERROR_WINHTTP_INVALID_URL | ERROR_WINHTTP_UNRECOGNIZED_SCHEME => {
RouteFailureClass::InvalidProxyConfig
}
ERROR_WINHTTP_CANNOT_CONNECT
| ERROR_WINHTTP_CONNECTION_ERROR
| ERROR_WINHTTP_NAME_NOT_RESOLVED => RouteFailureClass::ResolverError,
_ => RouteFailureClass::ResolverError,
}
}
@@ -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("<local> *.corp", &local_origin));
let corp_origin = RequestOrigin {
scheme: "https".to_string(),
host: "service.corp".to_string(),
port: 443,
};
assert!(proxy_bypass_matches_origin("<local> *.corp", &corp_origin));
}
@@ -103,6 +103,18 @@ async fn enabled_environment_proxy_routes_request_through_proxy() {
);
}
#[test]
fn parses_pac_proxy_tokens() {
assert_eq!(
parse_proxy_list("PROXY proxy.internal:8080; DIRECT", "https"),
ParsedProxyListDecision::Proxy("http://proxy.internal:8080".to_string())
);
assert_eq!(
parse_proxy_list("HTTPS proxy.internal:8443", "https"),
ParsedProxyListDecision::Proxy("https://proxy.internal:8443".to_string())
);
}
#[test]
fn unavailable_system_proxy_decision_is_cached() {
let request_url = "https://unavailable-cache.test/oauth/token";
@@ -131,3 +143,67 @@ fn system_proxy_cache_is_bounded() {
assert_eq!(cache.len(), SYSTEM_PROXY_CACHE_MAX_ENTRIES);
}
#[test]
fn parses_static_winhttp_proxy_entries_for_target_scheme() {
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("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"),
ParsedProxyListDecision::Unavailable
);
assert_eq!(
parse_proxy_list("proxy.internal:8080", "https"),
ParsedProxyListDecision::Proxy("http://proxy.internal:8080".to_string())
);
}
#[test]
fn reports_direct_and_unsupported_proxy_tokens() {
assert_eq!(
parse_proxy_list("DIRECT; PROXY proxy.internal:8080", "https"),
ParsedProxyListDecision::Direct
);
assert_eq!(
parse_proxy_list("DIRECT", "https"),
ParsedProxyListDecision::Direct
);
assert_eq!(
parse_proxy_list("SOCKS proxy.internal:1080", "https"),
ParsedProxyListDecision::UnsupportedScheme
);
}
#[test]
fn no_proxy_matches_exact_suffix_wildcard_and_port() {
let origin = RequestOrigin {
scheme: "https".to_string(),
host: "auth.openai.com".to_string(),
port: 443,
};
assert!(no_proxy_matches_origin("auth.openai.com", &origin));
assert!(!no_proxy_matches_origin("openai.com", &origin));
assert!(no_proxy_matches_origin(".openai.com", &origin));
assert!(no_proxy_matches_origin("*.openai.com", &origin));
assert!(no_proxy_matches_origin("auth.openai.com:443", &origin));
assert!(!no_proxy_matches_origin("auth.openai.com:8443", &origin));
}
#[test]
fn system_proxy_cache_key_preserves_url_specific_pac_decisions() {
let request_url = "https://auth.openai.com/oauth/token?access_token=secret";
let cache_key = system_proxy_cache_key(request_url);
assert_ne!(
cache_key,
system_proxy_cache_key("https://auth.openai.com/oauth/revoke")
);
#[cfg(target_os = "windows")]
assert!(!cache_key.contains(request_url));
}