Handle Windows proxy configuration edge cases

This commit is contained in:
canvrno-oai
2026-06-10 13:39:16 -07:00
parent 4a9f012433
commit 8a2b6461a5
3 changed files with 94 additions and 27 deletions

View File

@@ -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())

View File

@@ -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<IeProxyConfig, RouteFailureClass> {
};
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<IeProxyConfig, RouteFailureClass> {
})
}
#[derive(Debug)]
#[derive(Debug, Default)]
struct IeProxyConfig {
auto_detect: bool,
auto_config_url: Option<String>,
@@ -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("<local>") {
!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("<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()
}

View File

@@ -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));
}