use std::env;
use std::net::IpAddr;
use std::str::FromStr;
use http::uri::{Authority, Uri};
use ipnet::IpNet;
use lazy_static::lazy_static;
use self::matchers::{DomainMatcher, IpMatcher};
lazy_static! {
pub static ref PROXY_CONFIG: ProxyConfig = load_system_config();
}
fn load_system_config() -> ProxyConfig {
fn get_any_env(names: &[&str]) -> Option<String> {
let val = names
.iter()
.map(|n| env::var(n))
.find(|v| *v != Err(env::VarError::NotPresent));
match val {
Some(Ok(val)) => Some(val),
Some(Err(e)) => {
log::warn!("ignoring invalid configuration for {}: {}", names[0], e);
None
}
None => None,
}
}
fn parse_env_uri(names: &[&str]) -> Option<Uri> {
match get_any_env(names)?.parse() {
Ok(uri) => Some(uri),
Err(e) => {
log::warn!("ignoring invalid configuration for {}: {}", names[0], e);
None
}
}
}
let http_proxy = parse_env_uri(&["http_proxy"]);
let https_proxy = parse_env_uri(&["https_proxy", "HTTPS_PROXY"]);
let all_proxy = parse_env_uri(&["all_proxy", "ALL_PROXY"]);
let no_proxy = get_any_env(&["no_proxy", "NO_PROXY"])
.map(|s| NoProxy::parse(&s))
.unwrap_or(NoProxy::None);
ProxyConfig {
http_proxy,
https_proxy,
all_proxy,
no_proxy,
}
}
#[derive(Debug, Clone)]
pub struct ProxyConfig {
http_proxy: Option<Uri>,
https_proxy: Option<Uri>,
all_proxy: Option<Uri>,
no_proxy: NoProxy,
}
impl ProxyConfig {
pub fn http_proxy(&self) -> Option<&Uri> {
self.http_proxy.as_ref()
}
pub fn https_proxy(&self) -> Option<&Uri> {
self.https_proxy.as_ref()
}
pub fn all_proxy(&self) -> Option<&Uri> {
self.all_proxy.as_ref()
}
pub fn exclude(&self, scheme: Option<&str>, host: Option<&str>, port: Option<u16>) -> bool {
self.no_proxy.matches(scheme, host, port)
}
}
#[derive(Debug, Clone)]
enum NoProxy {
None,
Some {
ips: Vec<matchers::IpMatcher>,
hosts: Vec<matchers::DomainMatcher>,
},
All,
}
impl NoProxy {
fn parse(s: &str) -> NoProxy {
match s.trim() {
"" => NoProxy::None,
"*" => NoProxy::All,
_ => {
let mut ips = vec![];
let mut hosts = vec![];
for host in s.split(',') {
let host = host.trim();
if let Ok(net) = IpNet::from_str(host) {
ips.push(IpMatcher::from_net(net));
continue;
}
let authority = Authority::from_str(host).ok();
let (mut host, port) = match &authority {
Some(authority) => (authority.host(), authority.port_u16()),
None => (host, None),
};
if let Some(h) = host.strip_prefix('[') {
if let Some(h) = h.strip_suffix(']') {
host = h;
}
}
if host.is_empty() {
continue;
}
if let Ok(addr) = IpAddr::from_str(host) {
ips.push(IpMatcher::from_addr(addr, port))
} else {
hosts.push(DomainMatcher::new(host, port))
}
}
NoProxy::Some { ips, hosts }
}
}
}
fn matches(&self, scheme: Option<&str>, host: Option<&str>, port: Option<u16>) -> bool {
match self {
NoProxy::None => false,
NoProxy::All => true,
NoProxy::Some { ips, hosts } => {
let host = match host {
Some(host) => host.to_lowercase(),
None => return false,
};
let mut host = host.as_str();
if let Some(h) = host.strip_prefix('[') {
if let Some(h) = h.strip_suffix(']') {
host = h;
}
}
let port = match (scheme, port) {
(_, Some(port)) => port,
(Some("https"), None) => 443,
(Some("http"), None) => 80,
_ => return false,
};
if let Ok(addr) = IpAddr::from_str(host) {
ips.iter().any(|m| m.matches(addr, port))
} else {
hosts.iter().any(|m| m.matches(host, port))
}
}
}
}
}
mod matchers {
use std::net::IpAddr;
use ipnet::IpNet;
#[derive(Debug, Clone)]
pub struct IpMatcher {
net: IpNet,
port: Option<u16>,
}
impl IpMatcher {
pub fn from_net(net: IpNet) -> IpMatcher {
IpMatcher { net, port: None }
}
pub fn from_addr(mut addr: IpAddr, port: Option<u16>) -> IpMatcher {
Self::normalize_addr(&mut addr);
IpMatcher {
net: addr.into(),
port,
}
}
pub fn matches(&self, mut addr: IpAddr, port: u16) -> bool {
Self::normalize_addr(&mut addr);
self.net.contains(&addr) && (self.port.is_none() || self.port == Some(port))
}
fn normalize_addr(addr: &mut IpAddr) {
if let IpAddr::V6(v6_addr) = addr {
if let Some(v4_addr) = v6_addr.to_ipv4() {
*addr = IpAddr::V4(v4_addr);
}
}
}
}
#[derive(Debug, Clone)]
pub struct DomainMatcher {
domain: String,
port: Option<u16>,
}
impl DomainMatcher {
pub fn new(host: &str, port: Option<u16>) -> DomainMatcher {
let mut domain = host.to_lowercase();
if !domain.starts_with('.') {
domain.insert(0, '.')
}
DomainMatcher { domain, port }
}
pub fn matches(&self, host: &str, port: u16) -> bool {
debug_assert_eq!(&self.domain[0..1], ".");
(host == &self.domain[1..] || host.ends_with(&self.domain))
&& (self.port.is_none() || self.port == Some(port))
}
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use http::Uri;
use super::NoProxy;
#[test]
fn test_no_proxy() {
struct TestCase {
no_proxy: &'static str,
matches: &'static [&'static str],
nonmatches: &'static [&'static str],
}
let test_cases = &[
TestCase {
no_proxy: "localhost, anotherdomain.com, newdomain.com:1234, .d.o.t",
matches: &[
"http://localhost",
"http://LocalHost",
"http://LOCALHOST",
"http://newdomain.com:1234",
"http://foo.d.o.t",
"http://d.o.t",
"http://anotherdomain.com:8888",
"http://www.newdomain.com:1234",
],
nonmatches: &[
"http://prelocalhost",
"http://newdomain.com",
"http://newdomain.com:1235",
],
},
TestCase {
no_proxy: "foobar.com, .barbaz.net, \
192.168.1.1, 192.168.1.2:81, 192.168.1.3:80, 10.0.0.0/30, \
2001:db8::52:0:1, [2001:db8::52:0:2]:443, [2001:db8::52:0:3]:80, \
2002:db8:a::45/64",
matches: &[
"http://192.168.1.1",
"http://192.168.1.3",
"http://10.0.0.2",
"http://[2001:db8::52:0:1]",
"http://[2001:db8::52:0:3]",
"http://[2002:db8:a::123]",
"http://www.barbaz.net",
"http://barbaz.net",
"http://foobar.com",
"http://www.foobar.com",
],
nonmatches: &[
"http://192.168.1.2",
"http://192.168.1.4",
"http://[2001:db8::52:0:2]",
"http://[fe80::424b:c8be:1643:a1b6]",
"http://foofoobar.com",
"http://baz.com",
"http://localhost.net",
"http://local.localhost",
"http://barbarbaz.net",
],
},
TestCase {
no_proxy: "example.com:443",
matches: &["https://example.com"],
nonmatches: &["http://example.com"],
},
TestCase {
no_proxy: "*",
matches: &["http://newdomain.com", "http://newdomain.com:1234"],
nonmatches: &[],
},
TestCase {
no_proxy: "*, anotherdomain.com",
matches: &["http://anotherdomain.com"],
nonmatches: &["http://newdomain.com", "http://newdomain.com:1234"],
},
TestCase {
no_proxy: ", , []",
matches: &[],
nonmatches: &["http://anydomain.com"],
},
TestCase {
no_proxy: "::ffff:192.168.1.1",
matches: &["http://192.168.1.1", "http://[::ffff:192.168.1.1]"],
nonmatches: &["http://192.168.1.2", "http://[::ffff:192.168.1.2]"],
},
TestCase {
no_proxy: "192.168.1.1",
matches: &["http://192.168.1.1", "http://[::ffff:192.168.1.1]"],
nonmatches: &["http://192.168.1.2", "http://[::ffff:192.168.1.2]"],
},
];
for test_case in test_cases {
let no_proxy = NoProxy::parse(test_case.no_proxy);
for uri in test_case.matches {
let uri = Uri::from_str(uri).unwrap();
assert!(
no_proxy.matches(uri.scheme_str(), uri.host(), uri.port_u16()),
"no_proxy '{}' did not match '{}' as expected",
test_case.no_proxy,
uri,
);
}
for uri in test_case.nonmatches {
let uri = Uri::from_str(uri).unwrap();
assert!(
!no_proxy.matches(uri.scheme_str(), uri.host(), uri.port_u16()),
"no_proxy '{}' unexpectedly matched '{}'",
test_case.no_proxy,
uri,
);
}
}
}
}