use crate::url::Url;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyChoice {
Direct,
Proxy(String),
}
pub trait ProxyResolver: Send + Sync + std::fmt::Debug {
fn resolve(&self, url: &Url) -> ProxyChoice;
}
#[derive(Debug, Clone, Default)]
pub struct EnvProxyResolver;
fn env_any<S: AsRef<str>>(names: &[S]) -> Option<String> {
names
.iter()
.find_map(|n| std::env::var(n.as_ref()).ok().filter(|v| !v.is_empty()))
}
fn scheme_proxy_vars(scheme: &str) -> Vec<String> {
let scheme = scheme.to_ascii_lowercase();
let scheme = match scheme.as_str() {
"ws" => "http",
"wss" => "https",
other => other,
};
let lower = format!("{scheme}_proxy");
if scheme == "http" {
vec![lower]
} else {
let upper = lower.to_ascii_uppercase();
vec![lower, upper]
}
}
impl ProxyResolver for EnvProxyResolver {
fn resolve(&self, url: &Url) -> ProxyChoice {
if let Some(no) = env_any(&["no_proxy", "NO_PROXY"]) {
if no_proxy_matches(no.split(','), &url.host) {
return ProxyChoice::Direct;
}
}
match env_any(&scheme_proxy_vars(&url.scheme))
.or_else(|| env_any(&["all_proxy", "ALL_PROXY"]))
{
Some(spec) => ProxyChoice::Proxy(spec),
None => ProxyChoice::Direct,
}
}
}
pub fn from_env() -> EnvProxyResolver {
EnvProxyResolver
}
pub(crate) fn no_proxy_matches<'a, I>(entries: I, host: &str) -> bool
where
I: IntoIterator<Item = &'a str>,
{
let host_ip = crate::url::ip_literal_addr(host, 0).map(|a| a.ip());
let host = crate::url::unbracket(host).trim_end_matches('.');
entries.into_iter().any(|entry| {
let entry = entry.trim();
if entry == "*" {
return true;
}
let entry = crate::url::unbracket(entry);
if entry.is_empty() {
return false;
}
match host_ip {
Some(ip) => match entry.split_once('/') {
Some((net, bits)) => cidr_contains(net, bits, ip),
None => entry.parse::<std::net::IpAddr>().is_ok_and(|e| e == ip),
},
None => {
let e = entry.trim_start_matches('.').trim_end_matches('.');
let (h, e) = (host.as_bytes(), e.as_bytes());
!e.is_empty()
&& (h.eq_ignore_ascii_case(e)
|| (h.len() > e.len()
&& h[h.len() - e.len()..].eq_ignore_ascii_case(e)
&& h[h.len() - e.len() - 1] == b'.'))
}
}
})
}
fn cidr_contains(net: &str, bits: &str, ip: std::net::IpAddr) -> bool {
use std::net::IpAddr;
let Ok(bits) = bits.parse::<u32>() else {
return false;
};
match (crate::url::unbracket(net).parse::<IpAddr>(), ip) {
(Ok(IpAddr::V4(n)), IpAddr::V4(h)) if bits <= 32 => {
let mask = u32::MAX.checked_shl(32 - bits).unwrap_or(0);
u32::from(n) & mask == u32::from(h) & mask
}
(Ok(IpAddr::V6(n)), IpAddr::V6(h)) if bits <= 128 => {
let mask = u128::MAX.checked_shl(128 - bits).unwrap_or(0);
u128::from(n) & mask == u128::from(h) & mask
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct Static(ProxyChoice);
impl ProxyResolver for Static {
fn resolve(&self, _url: &Url) -> ProxyChoice {
self.0.clone()
}
}
#[test]
fn scheme_vars_skip_uppercase_http_proxy() {
assert_eq!(scheme_proxy_vars("http"), vec!["http_proxy"]);
assert_eq!(scheme_proxy_vars("ws"), vec!["http_proxy"]);
assert_eq!(
scheme_proxy_vars("HTTPS"),
vec!["https_proxy", "HTTPS_PROXY"]
);
assert_eq!(scheme_proxy_vars("wss"), vec!["https_proxy", "HTTPS_PROXY"]);
assert_eq!(scheme_proxy_vars("ftp"), vec!["ftp_proxy", "FTP_PROXY"]);
}
#[test]
fn no_proxy_host_suffix_rules() {
let m = |list: &str, host: &str| no_proxy_matches(list.split(','), host);
assert!(m("example.com", "example.com"));
assert!(m("example.com", "a.b.example.com"));
assert!(m(".example.com", "example.com"));
assert!(m("EXAMPLE.com.", "www.Example.COM"));
assert!(!m("example.com", "notexample.com"));
assert!(!m("example.com", "example.com.evil.net"));
assert!(m(" foo , *", "anything"));
assert!(!m("", "anything"));
assert!(!m(" , ", "anything"));
}
#[test]
fn no_proxy_ip_hosts_match_exactly_or_by_cidr() {
let m = |list: &str, host: &str| no_proxy_matches(list.split(','), host);
assert!(!m("2.3.4", "1.2.3.4"));
assert!(!m("3.4", "1.2.3.4"));
assert!(m("1.2.3.4", "1.2.3.4"));
assert!(m("10.0.0.0/8", "10.200.3.4"));
assert!(!m("10.0.0.0/8", "11.0.0.1"));
assert!(m("192.168.1.0/24", "192.168.1.77"));
assert!(m("0.0.0.0/0", "8.8.8.8"));
assert!(m("fc00::/7", "[fd12::1]"));
assert!(!m("fc00::/7", "[2001:db8::1]"));
assert!(!m("10.0.0.0/8", "[::1]"));
assert!(!m("10.0.0.0/99", "10.0.0.1"));
assert!(m("::1", "[::1]"));
assert!(m("[::1]", "[::1]"));
assert!(m("[::1]", "::1"));
assert!(!m("10.0.0.0/8", "10.example"));
}
#[test]
fn static_resolver_returns_choice() {
let u = Url::parse("http://example.com/").unwrap();
assert_eq!(Static(ProxyChoice::Direct).resolve(&u), ProxyChoice::Direct);
assert_eq!(
Static(ProxyChoice::Proxy("http://p:8080".into())).resolve(&u),
ProxyChoice::Proxy("http://p:8080".into())
);
}
}