#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct SystemProxySettings {
pub(crate) proxy: Option<String>,
pub(crate) bypass: Option<String>,
pub(crate) auto_config: bool,
pub(crate) auto_detect: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum SystemProxyChoice {
None,
Single {
url: String,
no_proxy: Vec<String>,
},
Unsupported(String),
}
pub(crate) fn endpoints_by_scheme(proxy: &str) -> (Option<String>, Option<String>) {
let mut any = None;
let mut http = None;
let mut https = None;
for entry in proxy
.split(|c: char| c == ';' || c.is_whitespace())
.map(str::trim)
.filter(|e| !e.is_empty())
{
match entry.split_once('=') {
Some((protocol, value)) => match protocol.trim().to_ascii_lowercase().as_str() {
"http" => http = http.or(Some(value.trim())),
"https" => https = https.or(Some(value.trim())),
_ => {}
},
None => any = any.or(Some(entry)),
}
}
let url = |chosen: Option<&str>| {
let chosen = chosen.filter(|v| !v.is_empty())?;
let url = if chosen.contains("://") {
chosen.to_string()
} else {
format!("http://{chosen}")
};
let scheme = url
.split("://")
.next()
.unwrap_or_default()
.to_ascii_lowercase();
(scheme == "http" || scheme == "https").then_some(url)
};
let http = if http.is_some() { url(http) } else { url(any) };
let https = if https.is_some() {
url(https)
} else {
url(any)
};
(http, https)
}
pub(crate) fn bypass_hosts(bypass: &str) -> Result<Vec<String>, String> {
let mut no_proxy = Vec::new();
for entry in bypass
.split(|c: char| c == ';' || c.is_whitespace())
.map(str::trim)
.filter(|e| !e.is_empty())
{
let host = entry.strip_prefix('*').unwrap_or(entry);
let expressible = !entry.contains("://")
&& !entry.contains('<')
&& !host.contains('*')
&& !host.is_empty()
&& host != "."
&& (host.len() == entry.len() || host.starts_with('.'));
if !expressible {
return Err(entry.to_string());
}
if !no_proxy.iter().any(|known: &String| known == host) {
no_proxy.push(host.to_string());
}
}
Ok(no_proxy)
}
pub(crate) fn proxy_choice(settings: &SystemProxySettings) -> SystemProxyChoice {
if settings.auto_detect {
return SystemProxyChoice::Unsupported(
"\"Automatically detect settings\" (proxy auto-detect) is on".into(),
);
}
if settings.auto_config {
return SystemProxyChoice::Unsupported("a proxy auto-config script is set".into());
}
let raw = settings.proxy.as_deref().unwrap_or_default().trim();
let (http, https) = endpoints_by_scheme(raw);
match (http, https) {
(None, None) => SystemProxyChoice::None,
(Some(http), Some(https)) if http.eq_ignore_ascii_case(&https) => {
match bypass_hosts(settings.bypass.as_deref().unwrap_or_default()) {
Ok(no_proxy) => SystemProxyChoice::Single {
url: http,
no_proxy,
},
Err(entry) => SystemProxyChoice::Unsupported(format!(
"the bypass entry `{entry}` cannot be followed exactly"
)),
}
}
_ => {
SystemProxyChoice::Unsupported("HTTP and HTTPS are given different proxy routes".into())
}
}
}
#[cfg(not(windows))]
pub(crate) fn system_proxy_settings() -> SystemProxySettings {
SystemProxySettings::default()
}
#[cfg(windows)]
pub(crate) fn system_proxy_settings() -> SystemProxySettings {
use std::ffi::c_void;
#[repr(C)]
struct IeProxyConfig {
auto_detect: i32,
auto_config_url: *mut u16,
proxy: *mut u16,
proxy_bypass: *mut u16,
}
#[link(name = "winhttp")]
unsafe extern "system" {
fn WinHttpGetIEProxyConfigForCurrentUser(config: *mut IeProxyConfig) -> i32;
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GlobalFree(memory: *mut c_void) -> *mut c_void;
}
unsafe fn take(text: *mut u16) -> Option<String> {
if text.is_null() {
return None;
}
let mut len = 0;
while unsafe { *text.add(len) } != 0 {
len += 1;
}
let value = String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(text, len) });
unsafe { GlobalFree(text.cast()) };
Some(value)
}
let mut config = IeProxyConfig {
auto_detect: 0,
auto_config_url: std::ptr::null_mut(),
proxy: std::ptr::null_mut(),
proxy_bypass: std::ptr::null_mut(),
};
if unsafe { WinHttpGetIEProxyConfigForCurrentUser(&mut config) } == 0 {
return SystemProxySettings::default();
}
let (pac, proxy, bypass) = unsafe {
(
take(config.auto_config_url),
take(config.proxy),
take(config.proxy_bypass),
)
};
SystemProxySettings {
proxy,
bypass,
auto_config: pac.is_some_and(|url| !url.trim().is_empty()),
auto_detect: config.auto_detect != 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn settings(proxy: &str, bypass: &str) -> SystemProxySettings {
SystemProxySettings {
proxy: Some(proxy.into()).filter(|p: &String| !p.is_empty()),
bypass: Some(bypass.into()).filter(|b: &String| !b.is_empty()),
..SystemProxySettings::default()
}
}
fn single(url: &str, no_proxy: &[&str]) -> SystemProxyChoice {
SystemProxyChoice::Single {
url: url.into(),
no_proxy: no_proxy.iter().map(|s| s.to_string()).collect(),
}
}
fn unsupported(element: &str) -> SystemProxyChoice {
SystemProxyChoice::Unsupported(element.into())
}
const DIFFERENT_ROUTES: &str = "HTTP and HTTPS are given different proxy routes";
#[test]
fn a_bare_host_and_port_is_one_http_proxy_for_both_schemes() {
assert_eq!(
endpoints_by_scheme("proxy.corp:3128"),
(
Some("http://proxy.corp:3128".into()),
Some("http://proxy.corp:3128".into())
)
);
assert_eq!(
proxy_choice(&settings("proxy.corp:3128", "")),
single("http://proxy.corp:3128", &[])
);
}
#[test]
fn each_scheme_gets_its_own_entry_and_never_the_other_one() {
let rows: &[(&str, Option<&str>, Option<&str>)] = &[
(
"http=h:80;https=s:443",
Some("http://h:80"),
Some("http://s:443"),
),
(
"https=s:443;http=h:80",
Some("http://h:80"),
Some("http://s:443"),
),
("https=s:443", None, Some("http://s:443")),
("http=h:80", Some("http://h:80"), None),
(
"https=s:443;any:8080",
Some("http://any:8080"),
Some("http://s:443"),
),
(
"any:8080 http=h:80",
Some("http://h:80"),
Some("http://any:8080"),
),
("ftp=f:21;socks=k:1080", None, None),
("ftp=f:21;socks=k:1080;http=h:80", Some("http://h:80"), None),
("http=first:1;http=second:2", Some("http://first:1"), None),
(
"https=https://tls.corp:8443",
None,
Some("https://tls.corp:8443"),
),
(
"https=socks5://k:1080;any:8080",
Some("http://any:8080"),
None,
),
];
for (proxy, http, https) in rows {
assert_eq!(
endpoints_by_scheme(proxy),
(http.map(str::to_string), https.map(str::to_string)),
"{proxy}"
);
}
}
#[test]
fn the_same_endpoint_for_both_schemes_is_one_proxy() {
assert_eq!(
proxy_choice(&settings("http=p.corp:8080;https=P.CORP:8080", "")),
single("http://p.corp:8080", &[])
);
}
#[test]
fn different_endpoints_or_one_direct_scheme_are_reported_not_approximated() {
for proxy in [
"http=web.corp:8080;https=secure.corp:8443",
"https=secure.corp:8443",
"http=web.corp:8080",
] {
assert_eq!(
proxy_choice(&settings(proxy, "")),
unsupported(DIFFERENT_ROUTES),
"{proxy}"
);
}
}
#[test]
fn nothing_usable_is_no_proxy() {
assert_eq!(
proxy_choice(&SystemProxySettings::default()),
SystemProxyChoice::None
);
assert_eq!(
proxy_choice(&settings(" ", "localhost")),
SystemProxyChoice::None
);
assert_eq!(
proxy_choice(&settings("socks=k:1080", "")),
SystemProxyChoice::None
);
assert_eq!(
proxy_choice(&settings("socks5://k:1080", "")),
SystemProxyChoice::None
);
}
#[test]
fn a_bypass_list_of_plain_hosts_is_kept_whole() {
assert_eq!(
bypass_hosts("localhost; *.corp.example ;intranet 10.0.0.1;localhost;.suffix.example"),
Ok(vec![
"localhost".to_string(),
".corp.example".to_string(),
"intranet".to_string(),
"10.0.0.1".to_string(),
".suffix.example".to_string(),
])
);
assert_eq!(bypass_hosts(""), Ok(vec![]));
assert_eq!(
proxy_choice(&settings("p:1", "localhost;*.corp.example")),
single("http://p:1", &["localhost", ".corp.example"])
);
}
#[test]
fn a_bypass_entry_this_client_cannot_follow_is_reported_never_dropped() {
let rows = [
("https://only.example;both.example", "https://only.example"),
("both.example;http://plain.example", "http://plain.example"),
("HTTP://plain.example", "HTTP://plain.example"),
("localhost;<local>", "<local>"),
("<LOCAL>", "<LOCAL>"),
("10.*", "10.*"),
("*.corp.example;192.168.*.1", "192.168.*.1"),
("*", "*"),
("*corp.example", "*corp.example"),
(".", "."),
];
for (bypass, entry) in rows {
assert_eq!(bypass_hosts(bypass), Err(entry.to_string()), "{bypass}");
assert_eq!(
proxy_choice(&settings("p:1", bypass)),
unsupported(&format!(
"the bypass entry `{entry}` cannot be followed exactly"
)),
"{bypass}"
);
}
assert_eq!(
proxy_choice(&settings("", "https://only.example;<local>")),
SystemProxyChoice::None
);
assert_eq!(
proxy_choice(&settings("http=h:80;https=s:443", "<local>")),
unsupported(DIFFERENT_ROUTES)
);
}
#[test]
fn a_pac_script_or_automatic_detection_is_reported_with_or_without_a_static_proxy() {
for proxy in ["", "p:1"] {
let detect = SystemProxySettings {
auto_detect: true,
..settings(proxy, "")
};
assert_eq!(
proxy_choice(&detect),
unsupported("\"Automatically detect settings\" (proxy auto-detect) is on"),
"{proxy:?}"
);
let pac = SystemProxySettings {
auto_config: true,
..settings(proxy, "")
};
assert_eq!(
proxy_choice(&pac),
unsupported("a proxy auto-config script is set"),
"{proxy:?}"
);
}
}
}