diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 03:59:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-30 03:59:24 +0000 |
commit | 023939b627b7dc93b01471f7d41fb8553ddb4ffa (patch) | |
tree | 60fc59477c605c72b0a1051409062ddecc43f877 /vendor/reqwest/src/proxy.rs | |
parent | Adding debian version 1.72.1+dfsg1-1. (diff) | |
download | rustc-023939b627b7dc93b01471f7d41fb8553ddb4ffa.tar.xz rustc-023939b627b7dc93b01471f7d41fb8553ddb4ffa.zip |
Merging upstream version 1.73.0+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/reqwest/src/proxy.rs')
-rw-r--r-- | vendor/reqwest/src/proxy.rs | 1857 |
1 files changed, 1857 insertions, 0 deletions
diff --git a/vendor/reqwest/src/proxy.rs b/vendor/reqwest/src/proxy.rs new file mode 100644 index 000000000..cfb4f047c --- /dev/null +++ b/vendor/reqwest/src/proxy.rs @@ -0,0 +1,1857 @@ +use std::fmt; +#[cfg(feature = "socks")] +use std::net::SocketAddr; +use std::sync::Arc; + +use crate::into_url::{IntoUrl, IntoUrlSealed}; +use crate::Url; +use http::{header::HeaderValue, Uri}; +use ipnet::IpNet; +use once_cell::sync::Lazy; +use percent_encoding::percent_decode; +use std::collections::HashMap; +use std::env; +use std::error::Error; +use std::net::IpAddr; +#[cfg(target_os = "windows")] +use winreg::enums::HKEY_CURRENT_USER; +#[cfg(target_os = "windows")] +use winreg::RegKey; + +/// Configuration of a proxy that a `Client` should pass requests to. +/// +/// A `Proxy` has a couple pieces to it: +/// +/// - a URL of how to talk to the proxy +/// - rules on what `Client` requests should be directed to the proxy +/// +/// For instance, let's look at `Proxy::http`: +/// +/// ```rust +/// # fn run() -> Result<(), Box<std::error::Error>> { +/// let proxy = reqwest::Proxy::http("https://secure.example")?; +/// # Ok(()) +/// # } +/// ``` +/// +/// This proxy will intercept all HTTP requests, and make use of the proxy +/// at `https://secure.example`. A request to `http://hyper.rs` will talk +/// to your proxy. A request to `https://hyper.rs` will not. +/// +/// Multiple `Proxy` rules can be configured for a `Client`. The `Client` will +/// check each `Proxy` in the order it was added. This could mean that a +/// `Proxy` added first with eager intercept rules, such as `Proxy::all`, +/// would prevent a `Proxy` later in the list from ever working, so take care. +/// +/// By enabling the `"socks"` feature it is possible to use a socks proxy: +/// ```rust +/// # fn run() -> Result<(), Box<std::error::Error>> { +/// let proxy = reqwest::Proxy::http("socks5://192.168.1.1:9000")?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Clone)] +pub struct Proxy { + intercept: Intercept, + no_proxy: Option<NoProxy>, +} + +/// Represents a possible matching entry for an IP address +#[derive(Clone, Debug)] +enum Ip { + Address(IpAddr), + Network(IpNet), +} + +/// A wrapper around a list of IP cidr blocks or addresses with a [IpMatcher::contains] method for +/// checking if an IP address is contained within the matcher +#[derive(Clone, Debug, Default)] +struct IpMatcher(Vec<Ip>); + +/// A wrapper around a list of domains with a [DomainMatcher::contains] method for checking if a +/// domain is contained within the matcher +#[derive(Clone, Debug, Default)] +struct DomainMatcher(Vec<String>); + +/// A configuration for filtering out requests that shouldn't be proxied +#[derive(Clone, Debug, Default)] +pub struct NoProxy { + ips: IpMatcher, + domains: DomainMatcher, +} + +/// A particular scheme used for proxying requests. +/// +/// For example, HTTP vs SOCKS5 +#[derive(Clone)] +pub enum ProxyScheme { + Http { + auth: Option<HeaderValue>, + host: http::uri::Authority, + }, + Https { + auth: Option<HeaderValue>, + host: http::uri::Authority, + }, + #[cfg(feature = "socks")] + Socks5 { + addr: SocketAddr, + auth: Option<(String, String)>, + remote_dns: bool, + }, +} + +impl ProxyScheme { + fn maybe_http_auth(&self) -> Option<&HeaderValue> { + match self { + ProxyScheme::Http { auth, .. } | ProxyScheme::Https { auth, .. } => auth.as_ref(), + #[cfg(feature = "socks")] + _ => None, + } + } +} + +/// Trait used for converting into a proxy scheme. This trait supports +/// parsing from a URL-like type, whilst also supporting proxy schemes +/// built directly using the factory methods. +pub trait IntoProxyScheme { + fn into_proxy_scheme(self) -> crate::Result<ProxyScheme>; +} + +impl<S: IntoUrl> IntoProxyScheme for S { + fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> { + // validate the URL + let url = match self.as_str().into_url() { + Ok(ok) => ok, + Err(e) => { + let mut presumed_to_have_scheme = true; + let mut source = e.source(); + while let Some(err) = source { + if let Some(parse_error) = err.downcast_ref::<url::ParseError>() { + match parse_error { + url::ParseError::RelativeUrlWithoutBase => { + presumed_to_have_scheme = false; + break; + } + _ => {} + } + } else if let Some(_) = err.downcast_ref::<crate::error::BadScheme>() { + presumed_to_have_scheme = false; + break; + } + source = err.source(); + } + if !presumed_to_have_scheme { + // the issue could have been caused by a missing scheme, so we try adding http:// + let try_this = format!("http://{}", self.as_str()); + try_this.into_url().map_err(|_| { + // return the original error + crate::error::builder(e) + })? + } else { + return Err(crate::error::builder(e)); + } + } + }; + ProxyScheme::parse(url) + } +} + +// These bounds are accidentally leaked by the blanket impl of IntoProxyScheme +// for all types that implement IntoUrl. So, this function exists to detect +// if we were to break those bounds for a user. +fn _implied_bounds() { + fn prox<T: IntoProxyScheme>(_t: T) {} + + fn url<T: IntoUrl>(t: T) { + prox(t); + } +} + +impl IntoProxyScheme for ProxyScheme { + fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> { + Ok(self) + } +} + +impl Proxy { + /// Proxy all HTTP traffic to the passed URL. + /// + /// # Example + /// + /// ``` + /// # extern crate reqwest; + /// # fn run() -> Result<(), Box<std::error::Error>> { + /// let client = reqwest::Client::builder() + /// .proxy(reqwest::Proxy::http("https://my.prox")?) + /// .build()?; + /// # Ok(()) + /// # } + /// # fn main() {} + /// ``` + pub fn http<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> { + Ok(Proxy::new(Intercept::Http( + proxy_scheme.into_proxy_scheme()?, + ))) + } + + /// Proxy all HTTPS traffic to the passed URL. + /// + /// # Example + /// + /// ``` + /// # extern crate reqwest; + /// # fn run() -> Result<(), Box<std::error::Error>> { + /// let client = reqwest::Client::builder() + /// .proxy(reqwest::Proxy::https("https://example.prox:4545")?) + /// .build()?; + /// # Ok(()) + /// # } + /// # fn main() {} + /// ``` + pub fn https<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> { + Ok(Proxy::new(Intercept::Https( + proxy_scheme.into_proxy_scheme()?, + ))) + } + + /// Proxy **all** traffic to the passed URL. + /// + /// # Example + /// + /// ``` + /// # extern crate reqwest; + /// # fn run() -> Result<(), Box<std::error::Error>> { + /// let client = reqwest::Client::builder() + /// .proxy(reqwest::Proxy::all("http://pro.xy")?) + /// .build()?; + /// # Ok(()) + /// # } + /// # fn main() {} + /// ``` + pub fn all<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> { + Ok(Proxy::new(Intercept::All( + proxy_scheme.into_proxy_scheme()?, + ))) + } + + /// Provide a custom function to determine what traffic to proxy to where. + /// + /// # Example + /// + /// ``` + /// # extern crate reqwest; + /// # fn run() -> Result<(), Box<std::error::Error>> { + /// let target = reqwest::Url::parse("https://my.prox")?; + /// let client = reqwest::Client::builder() + /// .proxy(reqwest::Proxy::custom(move |url| { + /// if url.host_str() == Some("hyper.rs") { + /// Some(target.clone()) + /// } else { + /// None + /// } + /// })) + /// .build()?; + /// # Ok(()) + /// # } + /// # fn main() {} + /// ``` + pub fn custom<F, U: IntoProxyScheme>(fun: F) -> Proxy + where + F: Fn(&Url) -> Option<U> + Send + Sync + 'static, + { + Proxy::new(Intercept::Custom(Custom { + auth: None, + func: Arc::new(move |url| fun(url).map(IntoProxyScheme::into_proxy_scheme)), + })) + } + + pub(crate) fn system() -> Proxy { + let mut proxy = if cfg!(feature = "__internal_proxy_sys_no_cache") { + Proxy::new(Intercept::System(Arc::new(get_sys_proxies( + get_from_registry(), + )))) + } else { + Proxy::new(Intercept::System(SYS_PROXIES.clone())) + }; + proxy.no_proxy = NoProxy::from_env(); + proxy + } + + fn new(intercept: Intercept) -> Proxy { + Proxy { + intercept, + no_proxy: None, + } + } + + /// Set the `Proxy-Authorization` header using Basic auth. + /// + /// # Example + /// + /// ``` + /// # extern crate reqwest; + /// # fn run() -> Result<(), Box<std::error::Error>> { + /// let proxy = reqwest::Proxy::https("http://localhost:1234")? + /// .basic_auth("Aladdin", "open sesame"); + /// # Ok(()) + /// # } + /// # fn main() {} + /// ``` + pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy { + self.intercept.set_basic_auth(username, password); + self + } + + /// Adds a `No Proxy` exclusion list to this Proxy + /// + /// # Example + /// + /// ``` + /// # extern crate reqwest; + /// # fn run() -> Result<(), Box<std::error::Error>> { + /// let proxy = reqwest::Proxy::https("http://localhost:1234")? + /// .no_proxy(reqwest::NoProxy::from_string("direct.tld, sub.direct2.tld")); + /// # Ok(()) + /// # } + /// # fn main() {} + /// ``` + pub fn no_proxy(mut self, no_proxy: Option<NoProxy>) -> Proxy { + self.no_proxy = no_proxy; + self + } + + pub(crate) fn maybe_has_http_auth(&self) -> bool { + match &self.intercept { + Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().is_some(), + // Custom *may* match 'http', so assume so. + Intercept::Custom(_) => true, + Intercept::System(system) => system + .get("http") + .and_then(|s| s.maybe_http_auth()) + .is_some(), + _ => false, + } + } + + pub(crate) fn http_basic_auth<D: Dst>(&self, uri: &D) -> Option<HeaderValue> { + match &self.intercept { + Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().cloned(), + Intercept::System(system) => system + .get("http") + .and_then(|s| s.maybe_http_auth().cloned()), + Intercept::Custom(custom) => { + custom.call(uri).and_then(|s| s.maybe_http_auth().cloned()) + } + _ => None, + } + } + + pub(crate) fn intercept<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> { + let in_no_proxy = self + .no_proxy + .as_ref() + .map_or(false, |np| np.contains(uri.host())); + match self.intercept { + Intercept::All(ref u) => { + if !in_no_proxy { + Some(u.clone()) + } else { + None + } + } + Intercept::Http(ref u) => { + if !in_no_proxy && uri.scheme() == "http" { + Some(u.clone()) + } else { + None + } + } + Intercept::Https(ref u) => { + if !in_no_proxy && uri.scheme() == "https" { + Some(u.clone()) + } else { + None + } + } + Intercept::System(ref map) => { + if in_no_proxy { + None + } else { + map.get(uri.scheme()).cloned() + } + } + Intercept::Custom(ref custom) => { + if !in_no_proxy { + custom.call(uri) + } else { + None + } + } + } + } + + pub(crate) fn is_match<D: Dst>(&self, uri: &D) -> bool { + match self.intercept { + Intercept::All(_) => true, + Intercept::Http(_) => uri.scheme() == "http", + Intercept::Https(_) => uri.scheme() == "https", + Intercept::System(ref map) => map.contains_key(uri.scheme()), + Intercept::Custom(ref custom) => custom.call(uri).is_some(), + } + } +} + +impl fmt::Debug for Proxy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Proxy") + .field(&self.intercept) + .field(&self.no_proxy) + .finish() + } +} + +impl NoProxy { + /// Returns a new no-proxy configuration based on environment variables (or `None` if no variables are set) + /// see [self::NoProxy::from_string()] for the string format + pub fn from_env() -> Option<NoProxy> { + let raw = env::var("NO_PROXY") + .or_else(|_| env::var("no_proxy")) + .unwrap_or_default(); + + Self::from_string(&raw) + } + + /// Returns a new no-proxy configuration based on a no_proxy string (or `None` if no variables + /// are set) + /// The rules are as follows: + /// * The environment variable `NO_PROXY` is checked, if it is not set, `no_proxy` is checked + /// * If neither environment variable is set, `None` is returned + /// * Entries are expected to be comma-separated (whitespace between entries is ignored) + /// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size, + /// for example "`192.168.1.0/24`"). + /// * An entry "`*`" matches all hostnames (this is the only wildcard allowed) + /// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com` + /// and `.google.com` are equivalent) and would match both that domain AND all subdomains. + /// + /// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all of the following would match + /// (and therefore would bypass the proxy): + /// * `http://google.com/` + /// * `http://www.google.com/` + /// * `http://192.168.1.42/` + /// + /// The URL `http://notgoogle.com/` would not match. + pub fn from_string(no_proxy_list: &str) -> Option<Self> { + if no_proxy_list.is_empty() { + return None; + } + let mut ips = Vec::new(); + let mut domains = Vec::new(); + let parts = no_proxy_list.split(',').map(str::trim); + for part in parts { + match part.parse::<IpNet>() { + // If we can parse an IP net or address, then use it, otherwise, assume it is a domain + Ok(ip) => ips.push(Ip::Network(ip)), + Err(_) => match part.parse::<IpAddr>() { + Ok(addr) => ips.push(Ip::Address(addr)), + Err(_) => domains.push(part.to_owned()), + }, + } + } + Some(NoProxy { + ips: IpMatcher(ips), + domains: DomainMatcher(domains), + }) + } + + fn contains(&self, host: &str) -> bool { + // According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off + // the end in order to parse correctly + let host = if host.starts_with('[') { + let x: &[_] = &['[', ']']; + host.trim_matches(x) + } else { + host + }; + match host.parse::<IpAddr>() { + // If we can parse an IP addr, then use it, otherwise, assume it is a domain + Ok(ip) => self.ips.contains(ip), + Err(_) => self.domains.contains(host), + } + } +} + +impl IpMatcher { + fn contains(&self, addr: IpAddr) -> bool { + for ip in self.0.iter() { + match ip { + Ip::Address(address) => { + if &addr == address { + return true; + } + } + Ip::Network(net) => { + if net.contains(&addr) { + return true; + } + } + } + } + false + } +} + +impl DomainMatcher { + // The following links may be useful to understand the origin of these rules: + // * https://curl.se/libcurl/c/CURLOPT_NOPROXY.html + // * https://github.com/curl/curl/issues/1208 + fn contains(&self, domain: &str) -> bool { + let domain_len = domain.len(); + for d in self.0.iter() { + if d == domain || d.strip_prefix('.') == Some(domain) { + return true; + } else if domain.ends_with(d) { + if d.starts_with('.') { + // If the first character of d is a dot, that means the first character of domain + // must also be a dot, so we are looking at a subdomain of d and that matches + return true; + } else if domain.as_bytes().get(domain_len - d.len() - 1) == Some(&b'.') { + // Given that d is a prefix of domain, if the prior character in domain is a dot + // then that means we must be matching a subdomain of d, and that matches + return true; + } + } else if d == "*" { + return true; + } + } + false + } +} + +impl ProxyScheme { + // To start conservative, keep builders private for now. + + /// Proxy traffic via the specified URL over HTTP + fn http(host: &str) -> crate::Result<Self> { + Ok(ProxyScheme::Http { + auth: None, + host: host.parse().map_err(crate::error::builder)?, + }) + } + + /// Proxy traffic via the specified URL over HTTPS + fn https(host: &str) -> crate::Result<Self> { + Ok(ProxyScheme::Https { + auth: None, + host: host.parse().map_err(crate::error::builder)?, + }) + } + + /// Proxy traffic via the specified socket address over SOCKS5 + /// + /// # Note + /// + /// Current SOCKS5 support is provided via blocking IO. + #[cfg(feature = "socks")] + fn socks5(addr: SocketAddr) -> crate::Result<Self> { + Ok(ProxyScheme::Socks5 { + addr, + auth: None, + remote_dns: false, + }) + } + + /// Proxy traffic via the specified socket address over SOCKS5H + /// + /// This differs from SOCKS5 in that DNS resolution is also performed via the proxy. + /// + /// # Note + /// + /// Current SOCKS5 support is provided via blocking IO. + #[cfg(feature = "socks")] + fn socks5h(addr: SocketAddr) -> crate::Result<Self> { + Ok(ProxyScheme::Socks5 { + addr, + auth: None, + remote_dns: true, + }) + } + + /// Use a username and password when connecting to the proxy server + fn with_basic_auth<T: Into<String>, U: Into<String>>( + mut self, + username: T, + password: U, + ) -> Self { + self.set_basic_auth(username, password); + self + } + + fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) { + match *self { + ProxyScheme::Http { ref mut auth, .. } => { + let header = encode_basic_auth(&username.into(), &password.into()); + *auth = Some(header); + } + ProxyScheme::Https { ref mut auth, .. } => { + let header = encode_basic_auth(&username.into(), &password.into()); + *auth = Some(header); + } + #[cfg(feature = "socks")] + ProxyScheme::Socks5 { ref mut auth, .. } => { + *auth = Some((username.into(), password.into())); + } + } + } + + fn if_no_auth(mut self, update: &Option<HeaderValue>) -> Self { + match self { + ProxyScheme::Http { ref mut auth, .. } => { + if auth.is_none() { + *auth = update.clone(); + } + } + ProxyScheme::Https { ref mut auth, .. } => { + if auth.is_none() { + *auth = update.clone(); + } + } + #[cfg(feature = "socks")] + ProxyScheme::Socks5 { .. } => {} + } + + self + } + + /// Convert a URL into a proxy scheme + /// + /// Supported schemes: HTTP, HTTPS, (SOCKS5, SOCKS5H if `socks` feature is enabled). + // Private for now... + fn parse(url: Url) -> crate::Result<Self> { + use url::Position; + + // Resolve URL to a host and port + #[cfg(feature = "socks")] + let to_addr = || { + let addrs = url + .socket_addrs(|| match url.scheme() { + "socks5" | "socks5h" => Some(1080), + _ => None, + }) + .map_err(crate::error::builder)?; + addrs + .into_iter() + .next() + .ok_or_else(|| crate::error::builder("unknown proxy scheme")) + }; + + let mut scheme = match url.scheme() { + "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?, + "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?, + #[cfg(feature = "socks")] + "socks5" => Self::socks5(to_addr()?)?, + #[cfg(feature = "socks")] + "socks5h" => Self::socks5h(to_addr()?)?, + _ => return Err(crate::error::builder("unknown proxy scheme")), + }; + + if let Some(pwd) = url.password() { + let decoded_username = percent_decode(url.username().as_bytes()).decode_utf8_lossy(); + let decoded_password = percent_decode(pwd.as_bytes()).decode_utf8_lossy(); + scheme = scheme.with_basic_auth(decoded_username, decoded_password); + } + + Ok(scheme) + } + + #[cfg(test)] + fn scheme(&self) -> &str { + match self { + ProxyScheme::Http { .. } => "http", + ProxyScheme::Https { .. } => "https", + #[cfg(feature = "socks")] + ProxyScheme::Socks5 { .. } => "socks5", + } + } + + #[cfg(test)] + fn host(&self) -> &str { + match self { + ProxyScheme::Http { host, .. } => host.as_str(), + ProxyScheme::Https { host, .. } => host.as_str(), + #[cfg(feature = "socks")] + ProxyScheme::Socks5 { .. } => panic!("socks5"), + } + } +} + +impl fmt::Debug for ProxyScheme { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ProxyScheme::Http { auth: _auth, host } => write!(f, "http://{}", host), + ProxyScheme::Https { auth: _auth, host } => write!(f, "https://{}", host), + #[cfg(feature = "socks")] + ProxyScheme::Socks5 { + addr, + auth: _auth, + remote_dns, + } => { + let h = if *remote_dns { "h" } else { "" }; + write!(f, "socks5{}://{}", h, addr) + } + } + } +} + +type SystemProxyMap = HashMap<String, ProxyScheme>; +type RegistryProxyValues = (u32, String); + +#[derive(Clone, Debug)] +enum Intercept { + All(ProxyScheme), + Http(ProxyScheme), + Https(ProxyScheme), + System(Arc<SystemProxyMap>), + Custom(Custom), +} + +impl Intercept { + fn set_basic_auth(&mut self, username: &str, password: &str) { + match self { + Intercept::All(ref mut s) + | Intercept::Http(ref mut s) + | Intercept::Https(ref mut s) => s.set_basic_auth(username, password), + Intercept::System(_) => unimplemented!(), + Intercept::Custom(ref mut custom) => { + let header = encode_basic_auth(username, password); + custom.auth = Some(header); + } + } + } +} + +#[derive(Clone)] +struct Custom { + // This auth only applies if the returned ProxyScheme doesn't have an auth... + auth: Option<HeaderValue>, + func: Arc<dyn Fn(&Url) -> Option<crate::Result<ProxyScheme>> + Send + Sync + 'static>, +} + +impl Custom { + fn call<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> { + let url = format!( + "{}://{}{}{}", + uri.scheme(), + uri.host(), + uri.port().map(|_| ":").unwrap_or(""), + uri.port().map(|p| p.to_string()).unwrap_or_default() + ) + .parse() + .expect("should be valid Url"); + + (self.func)(&url) + .and_then(|result| result.ok()) + .map(|scheme| scheme.if_no_auth(&self.auth)) + } +} + +impl fmt::Debug for Custom { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("_") + } +} + +pub(crate) fn encode_basic_auth(username: &str, password: &str) -> HeaderValue { + crate::util::basic_auth(username, Some(password)) +} + +/// A helper trait to allow testing `Proxy::intercept` without having to +/// construct `hyper::client::connect::Destination`s. +pub(crate) trait Dst { + fn scheme(&self) -> &str; + fn host(&self) -> &str; + fn port(&self) -> Option<u16>; +} + +#[doc(hidden)] +impl Dst for Uri { + fn scheme(&self) -> &str { + self.scheme().expect("Uri should have a scheme").as_str() + } + + fn host(&self) -> &str { + Uri::host(self).expect("<Uri as Dst>::host should have a str") + } + + fn port(&self) -> Option<u16> { + self.port().map(|p| p.as_u16()) + } +} + +static SYS_PROXIES: Lazy<Arc<SystemProxyMap>> = + Lazy::new(|| Arc::new(get_sys_proxies(get_from_registry()))); + +/// Get system proxies information. +/// +/// It can only support Linux, Unix like, and windows system. Note that it will always +/// return a HashMap, even if something runs into error when find registry information in +/// Windows system. Note that invalid proxy url in the system setting will be ignored. +/// +/// Returns: +/// System proxies information as a hashmap like +/// {"http": Url::parse("http://127.0.0.1:80"), "https": Url::parse("https://127.0.0.1:80")} +fn get_sys_proxies( + #[cfg_attr(not(target_os = "windows"), allow(unused_variables))] registry_values: Option< + RegistryProxyValues, + >, +) -> SystemProxyMap { + let proxies = get_from_environment(); + + // TODO: move the following #[cfg] to `if expression` when attributes on `if` expressions allowed + #[cfg(target_os = "windows")] + { + if proxies.is_empty() { + // don't care errors if can't get proxies from registry, just return an empty HashMap. + if let Some(registry_values) = registry_values { + return parse_registry_values(registry_values); + } + } + } + proxies +} + +fn insert_proxy(proxies: &mut SystemProxyMap, scheme: impl Into<String>, addr: String) -> bool { + if addr.trim().is_empty() { + // do not accept empty or whitespace proxy address + false + } else if let Ok(valid_addr) = addr.into_proxy_scheme() { + proxies.insert(scheme.into(), valid_addr); + true + } else { + false + } +} + +fn get_from_environment() -> SystemProxyMap { + let mut proxies = HashMap::new(); + + if is_cgi() { + if log::log_enabled!(log::Level::Warn) && env::var_os("HTTP_PROXY").is_some() { + log::warn!("HTTP_PROXY environment variable ignored in CGI"); + } + } else if !insert_from_env(&mut proxies, "http", "HTTP_PROXY") { + insert_from_env(&mut proxies, "http", "http_proxy"); + } + + if !insert_from_env(&mut proxies, "https", "HTTPS_PROXY") { + insert_from_env(&mut proxies, "https", "https_proxy"); + } + + proxies +} + +fn insert_from_env(proxies: &mut SystemProxyMap, scheme: &str, var: &str) -> bool { + if let Ok(val) = env::var(var) { + insert_proxy(proxies, scheme, val) + } else { + false + } +} + +/// Check if we are being executed in a CGI context. +/// +/// If so, a malicious client can send the `Proxy:` header, and it will +/// be in the `HTTP_PROXY` env var. So we don't use it :) +fn is_cgi() -> bool { + env::var_os("REQUEST_METHOD").is_some() +} + +#[cfg(target_os = "windows")] +fn get_from_registry_impl() -> Result<RegistryProxyValues, Box<dyn Error>> { + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let internet_setting: RegKey = + hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?; + // ensure the proxy is enable, if the value doesn't exist, an error will returned. + let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?; + let proxy_server: String = internet_setting.get_value("ProxyServer")?; + + Ok((proxy_enable, proxy_server)) +} + +#[cfg(target_os = "windows")] +fn get_from_registry() -> Option<RegistryProxyValues> { + get_from_registry_impl().ok() +} + +#[cfg(not(target_os = "windows"))] +fn get_from_registry() -> Option<RegistryProxyValues> { + None +} + +#[cfg(target_os = "windows")] +fn parse_registry_values_impl( + registry_values: RegistryProxyValues, +) -> Result<SystemProxyMap, Box<dyn Error>> { + let (proxy_enable, proxy_server) = registry_values; + + if proxy_enable == 0 { + return Ok(HashMap::new()); + } + + let mut proxies = HashMap::new(); + if proxy_server.contains("=") { + // per-protocol settings. + for p in proxy_server.split(";") { + let protocol_parts: Vec<&str> = p.split("=").collect(); + match protocol_parts.as_slice() { + [protocol, address] => { + // If address doesn't specify an explicit protocol as protocol://address + // then default to HTTP + let address = if extract_type_prefix(*address).is_some() { + String::from(*address) + } else { + format!("http://{}", address) + }; + + insert_proxy(&mut proxies, *protocol, address); + } + _ => { + // Contains invalid protocol setting, just break the loop + // And make proxies to be empty. + proxies.clear(); + break; + } + } + } + } else { + if let Some(scheme) = extract_type_prefix(&proxy_server) { + // Explicit protocol has been specified + insert_proxy(&mut proxies, scheme, proxy_server.to_owned()); + } else { + // No explicit protocol has been specified, default to HTTP + insert_proxy(&mut proxies, "http", format!("http://{}", proxy_server)); + insert_proxy(&mut proxies, "https", format!("http://{}", proxy_server)); + } + } + Ok(proxies) +} + +/// Extract the protocol from the given address, if present +/// For example, "https://example.com" will return Some("https") +#[cfg(target_os = "windows")] +fn extract_type_prefix(address: &str) -> Option<&str> { + if let Some(indice) = address.find("://") { + if indice == 0 { + None + } else { + let prefix = &address[..indice]; + let contains_banned = prefix.contains(|c| c == ':' || c == '/'); + + if !contains_banned { + Some(prefix) + } else { + None + } + } + } else { + None + } +} + +#[cfg(target_os = "windows")] +fn parse_registry_values(registry_values: RegistryProxyValues) -> SystemProxyMap { + parse_registry_values_impl(registry_values).unwrap_or(HashMap::new()) +} + +#[cfg(test)] +mod tests { + use super::*; + use once_cell::sync::Lazy; + use std::sync::Mutex; + + impl Dst for Url { + fn scheme(&self) -> &str { + Url::scheme(self) + } + + fn host(&self) -> &str { + Url::host_str(self).expect("<Url as Dst>::host should have a str") + } + + fn port(&self) -> Option<u16> { + Url::port(self) + } + } + + fn url(s: &str) -> Url { + s.parse().unwrap() + } + + fn intercepted_uri(p: &Proxy, s: &str) -> Uri { + let (scheme, host) = match p.intercept(&url(s)).unwrap() { + ProxyScheme::Http { host, .. } => ("http", host), + ProxyScheme::Https { host, .. } => ("https", host), + #[cfg(feature = "socks")] + _ => panic!("intercepted as socks"), + }; + http::Uri::builder() + .scheme(scheme) + .authority(host) + .path_and_query("/") + .build() + .expect("intercepted_uri") + } + + #[test] + fn test_http() { + let target = "http://example.domain/"; + let p = Proxy::http(target).unwrap(); + + let http = "http://hyper.rs"; + let other = "https://hyper.rs"; + + assert_eq!(intercepted_uri(&p, http), target); + assert!(p.intercept(&url(other)).is_none()); + } + + #[test] + fn test_https() { + let target = "http://example.domain/"; + let p = Proxy::https(target).unwrap(); + + let http = "http://hyper.rs"; + let other = "https://hyper.rs"; + + assert!(p.intercept(&url(http)).is_none()); + assert_eq!(intercepted_uri(&p, other), target); + } + + #[test] + fn test_all() { + let target = "http://example.domain/"; + let p = Proxy::all(target).unwrap(); + + let http = "http://hyper.rs"; + let https = "https://hyper.rs"; + let other = "x-youve-never-heard-of-me-mr-proxy://hyper.rs"; + + assert_eq!(intercepted_uri(&p, http), target); + assert_eq!(intercepted_uri(&p, https), target); + assert_eq!(intercepted_uri(&p, other), target); + } + + #[test] + fn test_custom() { + let target1 = "http://example.domain/"; + let target2 = "https://example.domain/"; + let p = Proxy::custom(move |url| { + if url.host_str() == Some("hyper.rs") { + target1.parse().ok() + } else if url.scheme() == "http" { + target2.parse().ok() + } else { + None::<Url> + } + }); + + let http = "http://seanmonstar.com"; + let https = "https://hyper.rs"; + let other = "x-youve-never-heard-of-me-mr-proxy://seanmonstar.com"; + + assert_eq!(intercepted_uri(&p, http), target2); + assert_eq!(intercepted_uri(&p, https), target1); + assert!(p.intercept(&url(other)).is_none()); + } + + #[test] + fn test_proxy_scheme_parse() { + let ps = "http://foo:bar@localhost:1239".into_proxy_scheme().unwrap(); + + match ps { + ProxyScheme::Http { auth, host } => { + assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar")); + assert_eq!(host, "localhost:1239"); + } + other => panic!("unexpected: {:?}", other), + } + } + + #[test] + fn test_proxy_scheme_ip_address_default_http() { + let ps = "192.168.1.1:8888".into_proxy_scheme().unwrap(); + + match ps { + ProxyScheme::Http { auth, host } => { + assert!(auth.is_none()); + assert_eq!(host, "192.168.1.1:8888"); + } + other => panic!("unexpected: {:?}", other), + } + } + + #[test] + fn test_proxy_scheme_parse_default_http_with_auth() { + // this should fail because `foo` is interpreted as the scheme and no host can be found + let ps = "foo:bar@localhost:1239".into_proxy_scheme().unwrap(); + + match ps { + ProxyScheme::Http { auth, host } => { + assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar")); + assert_eq!(host, "localhost:1239"); + } + other => panic!("unexpected: {:?}", other), + } + } + + #[test] + fn test_domain_matcher() { + let domains = vec![".foo.bar".into(), "bar.foo".into()]; + let matcher = DomainMatcher(domains); + + // domains match with leading `.` + assert!(matcher.contains("foo.bar")); + // subdomains match with leading `.` + assert!(matcher.contains("www.foo.bar")); + + // domains match with no leading `.` + assert!(matcher.contains("bar.foo")); + // subdomains match with no leading `.` + assert!(matcher.contains("www.bar.foo")); + + // non-subdomain string prefixes don't match + assert!(!matcher.contains("notfoo.bar")); + assert!(!matcher.contains("notbar.foo")); + } + + // Smallest possible content for a mutex + struct MutexInner; + + static ENVLOCK: Lazy<Mutex<MutexInner>> = Lazy::new(|| Mutex::new(MutexInner)); + + #[test] + fn test_get_sys_proxies_parsing() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + // save system setting first. + let _g1 = env_guard("HTTP_PROXY"); + let _g2 = env_guard("http_proxy"); + + // Mock ENV, get the results, before doing assertions + // to avoid assert! -> panic! -> Mutex Poisoned. + let baseline_proxies = get_sys_proxies(None); + // the system proxy setting url is invalid. + env::set_var("http_proxy", "file://123465"); + let invalid_proxies = get_sys_proxies(None); + // set valid proxy + env::set_var("http_proxy", "127.0.0.1/"); + let valid_proxies = get_sys_proxies(None); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + // Let other threads run now + drop(_lock); + + assert!(!baseline_proxies.contains_key("http")); + assert!(!invalid_proxies.contains_key("http")); + + let p = &valid_proxies["http"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.1"); + } + + #[cfg(target_os = "windows")] + #[test] + fn test_get_sys_proxies_registry_parsing() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + // save system setting first. + let _g1 = env_guard("HTTP_PROXY"); + let _g2 = env_guard("http_proxy"); + + // Mock ENV, get the results, before doing assertions + // to avoid assert! -> panic! -> Mutex Poisoned. + let baseline_proxies = get_sys_proxies(None); + // the system proxy in the registry has been disabled + let disabled_proxies = get_sys_proxies(Some((0, String::from("http://127.0.0.1/")))); + // set valid proxy + let valid_proxies = get_sys_proxies(Some((1, String::from("http://127.0.0.1/")))); + let valid_proxies_no_scheme = get_sys_proxies(Some((1, String::from("127.0.0.1")))); + let valid_proxies_explicit_https = + get_sys_proxies(Some((1, String::from("https://127.0.0.1/")))); + let multiple_proxies = get_sys_proxies(Some(( + 1, + String::from("http=127.0.0.1:8888;https=127.0.0.2:8888"), + ))); + let multiple_proxies_explicit_scheme = get_sys_proxies(Some(( + 1, + String::from("http=http://127.0.0.1:8888;https=https://127.0.0.2:8888"), + ))); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + // Let other threads run now + drop(_lock); + + assert_eq!(baseline_proxies.contains_key("http"), false); + assert_eq!(disabled_proxies.contains_key("http"), false); + + let p = &valid_proxies["http"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.1"); + + let p = &valid_proxies_no_scheme["http"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.1"); + + let p = &valid_proxies_no_scheme["https"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.1"); + + let p = &valid_proxies_explicit_https["https"]; + assert_eq!(p.scheme(), "https"); + assert_eq!(p.host(), "127.0.0.1"); + + let p = &multiple_proxies["http"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.1:8888"); + + let p = &multiple_proxies["https"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.2:8888"); + + let p = &multiple_proxies_explicit_scheme["http"]; + assert_eq!(p.scheme(), "http"); + assert_eq!(p.host(), "127.0.0.1:8888"); + + let p = &multiple_proxies_explicit_scheme["https"]; + assert_eq!(p.scheme(), "https"); + assert_eq!(p.host(), "127.0.0.2:8888"); + } + + #[test] + fn test_get_sys_proxies_in_cgi() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + // save system setting first. + let _g1 = env_guard("REQUEST_METHOD"); + let _g2 = env_guard("HTTP_PROXY"); + + // Mock ENV, get the results, before doing assertions + // to avoid assert! -> panic! -> Mutex Poisoned. + env::set_var("HTTP_PROXY", "http://evil/"); + + let baseline_proxies = get_sys_proxies(None); + // set like we're in CGI + env::set_var("REQUEST_METHOD", "GET"); + + let cgi_proxies = get_sys_proxies(None); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + // Let other threads run now + drop(_lock); + + // not in CGI yet + assert_eq!(baseline_proxies["http"].host(), "evil"); + // In CGI + assert!(!cgi_proxies.contains_key("http")); + } + + #[test] + fn test_sys_no_proxy() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + // save system setting first. + let _g1 = env_guard("HTTP_PROXY"); + let _g2 = env_guard("NO_PROXY"); + + let target = "http://example.domain/"; + env::set_var("HTTP_PROXY", target); + + env::set_var( + "NO_PROXY", + ".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17", + ); + + // Manually construct this so we aren't use the cache + let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None)))); + p.no_proxy = NoProxy::from_env(); + + // random url, not in no_proxy + assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target); + // make sure that random non-subdomain string prefixes don't match + assert_eq!(intercepted_uri(&p, "http://notfoo.bar"), target); + // make sure that random non-subdomain string prefixes don't match + assert_eq!(intercepted_uri(&p, "http://notbar.baz"), target); + // ipv4 address out of range + assert_eq!(intercepted_uri(&p, "http://10.43.1.1"), target); + // ipv4 address out of range + assert_eq!(intercepted_uri(&p, "http://10.124.7.7"), target); + // ipv6 address out of range + assert_eq!(intercepted_uri(&p, "http://[ffff:db8:a0b:12f0::1]"), target); + // ipv6 address out of range + assert_eq!(intercepted_uri(&p, "http://[2005:db8:a0b:12f0::1]"), target); + + // make sure subdomains (with leading .) match + assert!(p.intercept(&url("http://hello.foo.bar")).is_none()); + // make sure exact matches (without leading .) match (also makes sure spaces between entries work) + assert!(p.intercept(&url("http://bar.baz")).is_none()); + // check case sensitivity + assert!(p.intercept(&url("http://BAR.baz")).is_none()); + // make sure subdomains (without leading . in no_proxy) match + assert!(p.intercept(&url("http://foo.bar.baz")).is_none()); + // make sure subdomains (without leading . in no_proxy) match - this differs from cURL + assert!(p.intercept(&url("http://foo.bar")).is_none()); + // ipv4 address match within range + assert!(p.intercept(&url("http://10.42.1.100")).is_none()); + // ipv6 address exact match + assert!(p.intercept(&url("http://[::1]")).is_none()); + // ipv6 address match within range + assert!(p.intercept(&url("http://[2001:db8:a0b:12f0::1]")).is_none()); + // ipv4 address exact match + assert!(p.intercept(&url("http://10.124.7.8")).is_none()); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + // Let other threads run now + drop(_lock); + } + + #[test] + fn test_proxy_no_proxy_interception_for_proxy_types() { + let proxy_url = "http://example.domain/"; + let no_proxy = ".no.proxy.tld"; + + // test all proxy interception + let p = Proxy::all(proxy_url) + .unwrap() + .no_proxy(NoProxy::from_string(no_proxy)); + + // random url, not in no_proxy + assert_eq!(intercepted_uri(&p, "http://hyper.rs"), proxy_url); + + // positive match for no proxy + assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none()); + + // test http proxy interception + let p = Proxy::http(proxy_url) + .unwrap() + .no_proxy(NoProxy::from_string(no_proxy)); + + // random url, not in no_proxy + assert_eq!(intercepted_uri(&p, "http://hyper.rs"), proxy_url); + + // positive match for no proxy + assert!(p.intercept(&url("http://hello.no.proxy.tld")).is_none()); + + // should not be intercepted due to scheme + assert!(p.intercept(&url("https://hyper.rs")).is_none()); + + // test https proxy interception + let p = Proxy::https(proxy_url) + .unwrap() + .no_proxy(NoProxy::from_string(no_proxy)); + + // random url, not in no_proxy + assert_eq!(intercepted_uri(&p, "https://hyper.rs"), proxy_url); + + // positive match for no proxy + assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none()); + + // should not be intercepted due to scheme + assert!(p.intercept(&url("http://hyper.rs")).is_none()); + + // test custom proxy interception + let p = Proxy::custom(move |_url| Some(proxy_url)).no_proxy(NoProxy::from_string(no_proxy)); + + // random url, not in no_proxy + assert_eq!(intercepted_uri(&p, "https://hyper.rs"), proxy_url); + + // positive match for no proxy + assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none()); + assert!(p.intercept(&url("http://hello.no.proxy.tld")).is_none()); + } + + #[test] + fn test_wildcard_sys_no_proxy() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + // save system setting first. + let _g1 = env_guard("HTTP_PROXY"); + let _g2 = env_guard("NO_PROXY"); + + let target = "http://example.domain/"; + env::set_var("HTTP_PROXY", target); + + env::set_var("NO_PROXY", "*"); + + // Manually construct this so we aren't use the cache + let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None)))); + p.no_proxy = NoProxy::from_env(); + + assert!(p.intercept(&url("http://foo.bar")).is_none()); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + // Let other threads run now + drop(_lock); + } + + #[test] + fn test_empty_sys_no_proxy() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + // save system setting first. + let _g1 = env_guard("HTTP_PROXY"); + let _g2 = env_guard("NO_PROXY"); + + let target = "http://example.domain/"; + env::set_var("HTTP_PROXY", target); + + env::set_var("NO_PROXY", ","); + + // Manually construct this so we aren't use the cache + let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None)))); + p.no_proxy = NoProxy::from_env(); + + // everything should go through proxy, "effectively" nothing is in no_proxy + assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + // Let other threads run now + drop(_lock); + } + + #[test] + fn test_no_proxy_load() { + // Stop other threads from modifying process-global ENV while we are. + let _lock = ENVLOCK.lock(); + + let _g1 = env_guard("no_proxy"); + let domain = "lower.case"; + env::set_var("no_proxy", domain); + // Manually construct this so we aren't use the cache + let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None)))); + p.no_proxy = NoProxy::from_env(); + assert_eq!( + p.no_proxy.expect("should have a no proxy set").domains.0[0], + domain + ); + + env::remove_var("no_proxy"); + let _g2 = env_guard("NO_PROXY"); + let domain = "upper.case"; + env::set_var("NO_PROXY", domain); + // Manually construct this so we aren't use the cache + let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None)))); + p.no_proxy = NoProxy::from_env(); + assert_eq!( + p.no_proxy.expect("should have a no proxy set").domains.0[0], + domain + ); + + let _g3 = env_guard("HTTP_PROXY"); + env::remove_var("NO_PROXY"); + env::remove_var("no_proxy"); + let target = "http://example.domain/"; + env::set_var("HTTP_PROXY", target); + + // Manually construct this so we aren't use the cache + let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None)))); + p.no_proxy = NoProxy::from_env(); + assert!(p.no_proxy.is_none(), "NoProxy shouldn't have been created"); + + assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target); + + // reset user setting when guards drop + drop(_g1); + drop(_g2); + drop(_g3); + // Let other threads run now + drop(_lock); + } + + #[cfg(target_os = "windows")] + #[test] + fn test_type_prefix_extraction() { + assert!(extract_type_prefix("test").is_none()); + assert!(extract_type_prefix("://test").is_none()); + assert!(extract_type_prefix("some:prefix://test").is_none()); + assert!(extract_type_prefix("some/prefix://test").is_none()); + + assert_eq!(extract_type_prefix("http://test").unwrap(), "http"); + assert_eq!(extract_type_prefix("a://test").unwrap(), "a"); + } + + /// Guard an environment variable, resetting it to the original value + /// when dropped. + fn env_guard(name: impl Into<String>) -> EnvGuard { + let name = name.into(); + let orig_val = env::var(&name).ok(); + env::remove_var(&name); + EnvGuard { name, orig_val } + } + + struct EnvGuard { + name: String, + orig_val: Option<String>, + } + + impl Drop for EnvGuard { + fn drop(&mut self) { + if let Some(val) = self.orig_val.take() { + env::set_var(&self.name, val); + } else { + env::remove_var(&self.name); + } + } + } + + #[test] + fn test_has_http_auth() { + let http_proxy_with_auth = Proxy { + intercept: Intercept::Http(ProxyScheme::Http { + auth: Some(HeaderValue::from_static("auth1")), + host: http::uri::Authority::from_static("authority"), + }), + no_proxy: None, + }; + assert!(http_proxy_with_auth.maybe_has_http_auth()); + assert_eq!( + http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")), + Some(HeaderValue::from_static("auth1")) + ); + + let http_proxy_without_auth = Proxy { + intercept: Intercept::Http(ProxyScheme::Http { + auth: None, + host: http::uri::Authority::from_static("authority"), + }), + no_proxy: None, + }; + assert!(!http_proxy_without_auth.maybe_has_http_auth()); + assert_eq!( + http_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")), + None + ); + + let https_proxy_with_auth = Proxy { + intercept: Intercept::Http(ProxyScheme::Https { + auth: Some(HeaderValue::from_static("auth2")), + host: http::uri::Authority::from_static("authority"), + }), + no_proxy: None, + }; + assert!(https_proxy_with_auth.maybe_has_http_auth()); + assert_eq!( + https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")), + Some(HeaderValue::from_static("auth2")) + ); + + let all_http_proxy_with_auth = Proxy { + intercept: Intercept::All(ProxyScheme::Http { + auth: Some(HeaderValue::from_static("auth3")), + host: http::uri::Authority::from_static("authority"), + }), + no_proxy: None, + }; + assert!(all_http_proxy_with_auth.maybe_has_http_auth()); + assert_eq!( + all_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")), + Some(HeaderValue::from_static("auth3")) + ); + + let all_https_proxy_with_auth = Proxy { + intercept: Intercept::All(ProxyScheme::Https { + auth: Some(HeaderValue::from_static("auth4")), + host: http::uri::Authority::from_static("authority"), + }), + no_proxy: None, + }; + assert!(all_https_proxy_with_auth.maybe_has_http_auth()); + assert_eq!( + all_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")), + Some(HeaderValue::from_static("auth4")) + ); + + let all_https_proxy_without_auth = Proxy { + intercept: Intercept::All(ProxyScheme::Https { + auth: None, + host: http::uri::Authority::from_static("authority"), + }), + no_proxy: None, + }; + assert!(!all_https_proxy_without_auth.maybe_has_http_auth()); + assert_eq!( + all_https_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")), + None + ); + + let system_http_proxy_with_auth = Proxy { + intercept: Intercept::System(Arc::new({ + let mut m = HashMap::new(); + m.insert( + "http".into(), + ProxyScheme::Http { + auth: Some(HeaderValue::from_static("auth5")), + host: http::uri::Authority::from_static("authority"), + }, + ); + m + })), + no_proxy: None, + }; + assert!(system_http_proxy_with_auth.maybe_has_http_auth()); + assert_eq!( + system_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")), + Some(HeaderValue::from_static("auth5")) + ); + + let system_https_proxy_with_auth = Proxy { + intercept: Intercept::System(Arc::new({ + let mut m = HashMap::new(); + m.insert( + "https".into(), + ProxyScheme::Https { + auth: Some(HeaderValue::from_static("auth6")), + host: http::uri::Authority::from_static("authority"), + }, + ); + m + })), + no_proxy: None, + }; + assert!(!system_https_proxy_with_auth.maybe_has_http_auth()); + assert_eq!( + system_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")), + None + ); + } +} + +#[cfg(test)] +mod test { + mod into_proxy_scheme { + use crate::Proxy; + use std::error::Error; + use std::mem::discriminant; + + fn includes(haystack: &crate::error::Error, needle: url::ParseError) -> bool { + let mut source = haystack.source(); + while let Some(error) = source { + if let Some(parse_error) = error.downcast_ref::<url::ParseError>() { + if discriminant(parse_error) == discriminant(&needle) { + return true; + } + } + source = error.source(); + } + false + } + + fn check_parse_error(url: &str, needle: url::ParseError) { + let error = Proxy::http(url).unwrap_err(); + if !includes(&error, needle) { + panic!("{:?} expected; {:?}, {} found", needle, error, error); + } + } + + mod when_scheme_missing { + mod and_url_is_valid { + use crate::Proxy; + + #[test] + fn lookback_works() { + let _ = Proxy::http("127.0.0.1").unwrap(); + } + + #[test] + fn loopback_port_works() { + let _ = Proxy::http("127.0.0.1:8080").unwrap(); + } + + #[test] + fn loopback_username_works() { + let _ = Proxy::http("username@127.0.0.1").unwrap(); + } + + #[test] + fn loopback_username_password_works() { + let _ = Proxy::http("username:password@127.0.0.1").unwrap(); + } + + #[test] + fn loopback_username_password_port_works() { + let _ = Proxy::http("ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap(); + } + + #[test] + fn domain_works() { + let _ = Proxy::http("proxy.example.com").unwrap(); + } + + #[test] + fn domain_port_works() { + let _ = Proxy::http("proxy.example.com:8080").unwrap(); + } + + #[test] + fn domain_username_works() { + let _ = Proxy::http("username@proxy.example.com").unwrap(); + } + + #[test] + fn domain_username_password_works() { + let _ = Proxy::http("username:password@proxy.example.com").unwrap(); + } + + #[test] + fn domain_username_password_port_works() { + let _ = + Proxy::http("ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080").unwrap(); + } + } + mod and_url_has_bad { + use super::super::check_parse_error; + + #[test] + fn host() { + check_parse_error("username@", url::ParseError::RelativeUrlWithoutBase); + } + + #[test] + fn idna_encoding() { + check_parse_error("xn---", url::ParseError::RelativeUrlWithoutBase); + } + + #[test] + fn port() { + check_parse_error("127.0.0.1:808080", url::ParseError::RelativeUrlWithoutBase); + } + + #[test] + fn ip_v4_address() { + check_parse_error("421.627.718.469", url::ParseError::RelativeUrlWithoutBase); + } + + #[test] + fn ip_v6_address() { + check_parse_error( + "[56FE::2159:5BBC::6594]", + url::ParseError::RelativeUrlWithoutBase, + ); + } + + #[test] + fn invalid_domain_character() { + check_parse_error("abc 123", url::ParseError::RelativeUrlWithoutBase); + } + } + } + + mod when_scheme_present { + mod and_url_is_valid { + use crate::Proxy; + + #[test] + fn loopback_works() { + let _ = Proxy::http("http://127.0.0.1").unwrap(); + } + + #[test] + fn loopback_port_works() { + let _ = Proxy::http("https://127.0.0.1:8080").unwrap(); + } + + #[test] + fn loopback_username_works() { + let _ = Proxy::http("http://username@127.0.0.1").unwrap(); + } + + #[test] + fn loopback_username_password_works() { + let _ = Proxy::http("https://username:password@127.0.0.1").unwrap(); + } + + #[test] + fn loopback_username_password_port_works() { + let _ = + Proxy::http("http://ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap(); + } + + #[test] + fn domain_works() { + let _ = Proxy::http("https://proxy.example.com").unwrap(); + } + + #[test] + fn domain_port_works() { + let _ = Proxy::http("http://proxy.example.com:8080").unwrap(); + } + + #[test] + fn domain_username_works() { + let _ = Proxy::http("https://username@proxy.example.com").unwrap(); + } + + #[test] + fn domain_username_password_works() { + let _ = Proxy::http("http://username:password@proxy.example.com").unwrap(); + } + + #[test] + fn domain_username_password_port_works() { + let _ = + Proxy::http("https://ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080") + .unwrap(); + } + } + mod and_url_has_bad { + use super::super::check_parse_error; + + #[test] + fn host() { + check_parse_error("http://username@", url::ParseError::EmptyHost); + } + + #[test] + fn idna_encoding() { + check_parse_error("http://xn---", url::ParseError::IdnaError); + } + + #[test] + fn port() { + check_parse_error("http://127.0.0.1:808080", url::ParseError::InvalidPort); + } + + #[test] + fn ip_v4_address() { + check_parse_error( + "http://421.627.718.469", + url::ParseError::InvalidIpv4Address, + ); + } + + #[test] + fn ip_v6_address() { + check_parse_error( + "http://[56FE::2159:5BBC::6594]", + url::ParseError::InvalidIpv6Address, + ); + } + + #[test] + fn invalid_domain_character() { + check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter); + } + } + } + } +} |