summaryrefslogtreecommitdiffstats
path: root/vendor/reqwest/src/proxy.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 03:59:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 03:59:24 +0000
commit023939b627b7dc93b01471f7d41fb8553ddb4ffa (patch)
tree60fc59477c605c72b0a1051409062ddecc43f877 /vendor/reqwest/src/proxy.rs
parentAdding debian version 1.72.1+dfsg1-1. (diff)
downloadrustc-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.rs1857
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);
+ }
+ }
+ }
+ }
+}