dusks_reqwest/
proxy.rs

1use std::fmt;
2#[cfg(feature = "socks")]
3use std::net::SocketAddr;
4use std::sync::Arc;
5
6use crate::into_url::{IntoUrl, IntoUrlSealed};
7use crate::Url;
8use http::{header::HeaderValue, Uri};
9use ipnet::IpNet;
10use once_cell::sync::Lazy;
11use percent_encoding::percent_decode;
12use std::collections::HashMap;
13use std::env;
14use std::error::Error;
15use std::net::IpAddr;
16#[cfg(all(target_os = "macos", feature = "macos-system-configuration"))]
17use system_configuration::{
18    core_foundation::{
19        base::CFType,
20        dictionary::CFDictionary,
21        number::CFNumber,
22        string::{CFString, CFStringRef},
23    },
24    dynamic_store::SCDynamicStoreBuilder,
25    sys::schema_definitions::kSCPropNetProxiesHTTPEnable,
26    sys::schema_definitions::kSCPropNetProxiesHTTPPort,
27    sys::schema_definitions::kSCPropNetProxiesHTTPProxy,
28    sys::schema_definitions::kSCPropNetProxiesHTTPSEnable,
29    sys::schema_definitions::kSCPropNetProxiesHTTPSPort,
30    sys::schema_definitions::kSCPropNetProxiesHTTPSProxy,
31};
32#[cfg(target_os = "windows")]
33use winreg::enums::HKEY_CURRENT_USER;
34#[cfg(target_os = "windows")]
35use winreg::RegKey;
36
37/// Configuration of a proxy that a `Client` should pass requests to.
38///
39/// A `Proxy` has a couple pieces to it:
40///
41/// - a URL of how to talk to the proxy
42/// - rules on what `Client` requests should be directed to the proxy
43///
44/// For instance, let's look at `Proxy::http`:
45///
46/// ```rust
47/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
48/// let proxy = reqwest::Proxy::http("https://secure.example")?;
49/// # Ok(())
50/// # }
51/// ```
52///
53/// This proxy will intercept all HTTP requests, and make use of the proxy
54/// at `https://secure.example`. A request to `http://hyper.rs` will talk
55/// to your proxy. A request to `https://hyper.rs` will not.
56///
57/// Multiple `Proxy` rules can be configured for a `Client`. The `Client` will
58/// check each `Proxy` in the order it was added. This could mean that a
59/// `Proxy` added first with eager intercept rules, such as `Proxy::all`,
60/// would prevent a `Proxy` later in the list from ever working, so take care.
61///
62/// By enabling the `"socks"` feature it is possible to use a socks proxy:
63/// ```rust
64/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
65/// let proxy = reqwest::Proxy::http("socks5://192.168.1.1:9000")?;
66/// # Ok(())
67/// # }
68/// ```
69#[derive(Clone)]
70pub struct Proxy {
71    intercept: Intercept,
72    no_proxy: Option<NoProxy>,
73}
74
75/// Represents a possible matching entry for an IP address
76#[derive(Clone, Debug)]
77enum Ip {
78    Address(IpAddr),
79    Network(IpNet),
80}
81
82/// A wrapper around a list of IP cidr blocks or addresses with a [IpMatcher::contains] method for
83/// checking if an IP address is contained within the matcher
84#[derive(Clone, Debug, Default)]
85struct IpMatcher(Vec<Ip>);
86
87/// A wrapper around a list of domains with a [DomainMatcher::contains] method for checking if a
88/// domain is contained within the matcher
89#[derive(Clone, Debug, Default)]
90struct DomainMatcher(Vec<String>);
91
92/// A configuration for filtering out requests that shouldn't be proxied
93#[derive(Clone, Debug, Default)]
94pub struct NoProxy {
95    ips: IpMatcher,
96    domains: DomainMatcher,
97}
98
99/// A particular scheme used for proxying requests.
100///
101/// For example, HTTP vs SOCKS5
102#[derive(Clone)]
103pub enum ProxyScheme {
104    Http {
105        auth: Option<HeaderValue>,
106        host: http::uri::Authority,
107    },
108    Https {
109        auth: Option<HeaderValue>,
110        host: http::uri::Authority,
111    },
112    #[cfg(feature = "socks")]
113    Socks5 {
114        addr: SocketAddr,
115        auth: Option<(String, String)>,
116        remote_dns: bool,
117    },
118}
119
120impl ProxyScheme {
121    fn maybe_http_auth(&self) -> Option<&HeaderValue> {
122        match self {
123            ProxyScheme::Http { auth, .. } | ProxyScheme::Https { auth, .. } => auth.as_ref(),
124            #[cfg(feature = "socks")]
125            _ => None,
126        }
127    }
128}
129
130/// Trait used for converting into a proxy scheme. This trait supports
131/// parsing from a URL-like type, whilst also supporting proxy schemes
132/// built directly using the factory methods.
133pub trait IntoProxyScheme {
134    fn into_proxy_scheme(self) -> crate::Result<ProxyScheme>;
135}
136
137impl<S: IntoUrl> IntoProxyScheme for S {
138    fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> {
139        // validate the URL
140        let url = match self.as_str().into_url() {
141            Ok(ok) => ok,
142            Err(e) => {
143                let mut presumed_to_have_scheme = true;
144                let mut source = e.source();
145                while let Some(err) = source {
146                    if let Some(parse_error) = err.downcast_ref::<url::ParseError>() {
147                        match parse_error {
148                            url::ParseError::RelativeUrlWithoutBase => {
149                                presumed_to_have_scheme = false;
150                                break;
151                            }
152                            _ => {}
153                        }
154                    } else if let Some(_) = err.downcast_ref::<crate::error::BadScheme>() {
155                        presumed_to_have_scheme = false;
156                        break;
157                    }
158                    source = err.source();
159                }
160                if presumed_to_have_scheme {
161                    return Err(crate::error::builder(e));
162                }
163                // the issue could have been caused by a missing scheme, so we try adding http://
164                let try_this = format!("http://{}", self.as_str());
165                try_this.into_url().map_err(|_| {
166                    // return the original error
167                    crate::error::builder(e)
168                })?
169            }
170        };
171        ProxyScheme::parse(url)
172    }
173}
174
175// These bounds are accidentally leaked by the blanket impl of IntoProxyScheme
176// for all types that implement IntoUrl. So, this function exists to detect
177// if we were to break those bounds for a user.
178fn _implied_bounds() {
179    fn prox<T: IntoProxyScheme>(_t: T) {}
180
181    fn url<T: IntoUrl>(t: T) {
182        prox(t);
183    }
184}
185
186impl IntoProxyScheme for ProxyScheme {
187    fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> {
188        Ok(self)
189    }
190}
191
192impl Proxy {
193    /// Proxy all HTTP traffic to the passed URL.
194    ///
195    /// # Example
196    ///
197    /// ```
198    /// # extern crate reqwest;
199    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
200    /// let client = reqwest::Client::builder()
201    ///     .proxy(reqwest::Proxy::http("https://my.prox")?)
202    ///     .build()?;
203    /// # Ok(())
204    /// # }
205    /// # fn main() {}
206    /// ```
207    pub fn http<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
208        Ok(Proxy::new(Intercept::Http(
209            proxy_scheme.into_proxy_scheme()?,
210        )))
211    }
212
213    /// Proxy all HTTPS traffic to the passed URL.
214    ///
215    /// # Example
216    ///
217    /// ```
218    /// # extern crate reqwest;
219    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
220    /// let client = reqwest::Client::builder()
221    ///     .proxy(reqwest::Proxy::https("https://example.prox:4545")?)
222    ///     .build()?;
223    /// # Ok(())
224    /// # }
225    /// # fn main() {}
226    /// ```
227    pub fn https<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
228        Ok(Proxy::new(Intercept::Https(
229            proxy_scheme.into_proxy_scheme()?,
230        )))
231    }
232
233    /// Proxy **all** traffic to the passed URL.
234    ///
235    /// # Example
236    ///
237    /// ```
238    /// # extern crate reqwest;
239    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
240    /// let client = reqwest::Client::builder()
241    ///     .proxy(reqwest::Proxy::all("http://pro.xy")?)
242    ///     .build()?;
243    /// # Ok(())
244    /// # }
245    /// # fn main() {}
246    /// ```
247    pub fn all<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
248        Ok(Proxy::new(Intercept::All(
249            proxy_scheme.into_proxy_scheme()?,
250        )))
251    }
252
253    /// Provide a custom function to determine what traffic to proxy to where.
254    ///
255    /// # Example
256    ///
257    /// ```
258    /// # extern crate reqwest;
259    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
260    /// let target = reqwest::Url::parse("https://my.prox")?;
261    /// let client = reqwest::Client::builder()
262    ///     .proxy(reqwest::Proxy::custom(move |url| {
263    ///         if url.host_str() == Some("hyper.rs") {
264    ///             Some(target.clone())
265    ///         } else {
266    ///             None
267    ///         }
268    ///     }))
269    ///     .build()?;
270    /// # Ok(())
271    /// # }
272    /// # fn main() {}
273    /// ```
274    pub fn custom<F, U: IntoProxyScheme>(fun: F) -> Proxy
275    where
276        F: Fn(&Url) -> Option<U> + Send + Sync + 'static,
277    {
278        Proxy::new(Intercept::Custom(Custom {
279            auth: None,
280            func: Arc::new(move |url| fun(url).map(IntoProxyScheme::into_proxy_scheme)),
281        }))
282    }
283
284    pub(crate) fn system() -> Proxy {
285        let mut proxy = if cfg!(feature = "__internal_proxy_sys_no_cache") {
286            Proxy::new(Intercept::System(Arc::new(get_sys_proxies(
287                get_from_platform(),
288            ))))
289        } else {
290            Proxy::new(Intercept::System(SYS_PROXIES.clone()))
291        };
292        proxy.no_proxy = NoProxy::from_env();
293        proxy
294    }
295
296    fn new(intercept: Intercept) -> Proxy {
297        Proxy {
298            intercept,
299            no_proxy: None,
300        }
301    }
302
303    /// Set the `Proxy-Authorization` header using Basic auth.
304    ///
305    /// # Example
306    ///
307    /// ```
308    /// # extern crate reqwest;
309    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
310    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
311    ///     .basic_auth("Aladdin", "open sesame");
312    /// # Ok(())
313    /// # }
314    /// # fn main() {}
315    /// ```
316    pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
317        self.intercept.set_basic_auth(username, password);
318        self
319    }
320
321    /// Set the `Proxy-Authorization` header to a specified value.
322    ///
323    /// # Example
324    ///
325    /// ```
326    /// # extern crate reqwest;
327    /// # use reqwest::header::*;
328    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
329    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
330    ///     .custom_http_auth(HeaderValue::from_static("justletmeinalreadyplease"));
331    /// # Ok(())
332    /// # }
333    /// # fn main() {}
334    /// ```
335    pub fn custom_http_auth(mut self, header_value: HeaderValue) -> Proxy {
336        self.intercept.set_custom_http_auth(header_value);
337        self
338    }
339
340    /// Adds a `No Proxy` exclusion list to this Proxy
341    ///
342    /// # Example
343    ///
344    /// ```
345    /// # extern crate reqwest;
346    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
347    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
348    ///     .no_proxy(reqwest::NoProxy::from_string("direct.tld, sub.direct2.tld"));
349    /// # Ok(())
350    /// # }
351    /// # fn main() {}
352    /// ```
353    pub fn no_proxy(mut self, no_proxy: Option<NoProxy>) -> Proxy {
354        self.no_proxy = no_proxy;
355        self
356    }
357
358    pub(crate) fn maybe_has_http_auth(&self) -> bool {
359        match &self.intercept {
360            Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().is_some(),
361            // Custom *may* match 'http', so assume so.
362            Intercept::Custom(_) => true,
363            Intercept::System(system) => system
364                .get("http")
365                .and_then(|s| s.maybe_http_auth())
366                .is_some(),
367            Intercept::Https(_) => false,
368        }
369    }
370
371    pub(crate) fn http_basic_auth<D: Dst>(&self, uri: &D) -> Option<HeaderValue> {
372        match &self.intercept {
373            Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().cloned(),
374            Intercept::System(system) => system
375                .get("http")
376                .and_then(|s| s.maybe_http_auth().cloned()),
377            Intercept::Custom(custom) => {
378                custom.call(uri).and_then(|s| s.maybe_http_auth().cloned())
379            }
380            Intercept::Https(_) => None,
381        }
382    }
383
384    pub(crate) fn intercept<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> {
385        let in_no_proxy = self
386            .no_proxy
387            .as_ref()
388            .map_or(false, |np| np.contains(uri.host()));
389        match self.intercept {
390            Intercept::All(ref u) => {
391                if !in_no_proxy {
392                    Some(u.clone())
393                } else {
394                    None
395                }
396            }
397            Intercept::Http(ref u) => {
398                if !in_no_proxy && uri.scheme() == "http" {
399                    Some(u.clone())
400                } else {
401                    None
402                }
403            }
404            Intercept::Https(ref u) => {
405                if !in_no_proxy && uri.scheme() == "https" {
406                    Some(u.clone())
407                } else {
408                    None
409                }
410            }
411            Intercept::System(ref map) => {
412                if in_no_proxy {
413                    None
414                } else {
415                    map.get(uri.scheme()).cloned()
416                }
417            }
418            Intercept::Custom(ref custom) => {
419                if !in_no_proxy {
420                    custom.call(uri)
421                } else {
422                    None
423                }
424            }
425        }
426    }
427
428    pub(crate) fn is_match<D: Dst>(&self, uri: &D) -> bool {
429        match self.intercept {
430            Intercept::All(_) => true,
431            Intercept::Http(_) => uri.scheme() == "http",
432            Intercept::Https(_) => uri.scheme() == "https",
433            Intercept::System(ref map) => map.contains_key(uri.scheme()),
434            Intercept::Custom(ref custom) => custom.call(uri).is_some(),
435        }
436    }
437}
438
439impl fmt::Debug for Proxy {
440    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
441        f.debug_tuple("Proxy")
442            .field(&self.intercept)
443            .field(&self.no_proxy)
444            .finish()
445    }
446}
447
448impl NoProxy {
449    /// Returns a new no-proxy configuration based on environment variables (or `None` if no variables are set)
450    /// see [self::NoProxy::from_string()] for the string format
451    pub fn from_env() -> Option<NoProxy> {
452        let raw = env::var("NO_PROXY")
453            .or_else(|_| env::var("no_proxy"))
454            .unwrap_or_default();
455
456        Self::from_string(&raw)
457    }
458
459    /// Returns a new no-proxy configuration based on a `no_proxy` string (or `None` if no variables
460    /// are set)
461    /// The rules are as follows:
462    /// * The environment variable `NO_PROXY` is checked, if it is not set, `no_proxy` is checked
463    /// * If neither environment variable is set, `None` is returned
464    /// * Entries are expected to be comma-separated (whitespace between entries is ignored)
465    /// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size,
466    /// for example "`192.168.1.0/24`").
467    /// * An entry "`*`" matches all hostnames (this is the only wildcard allowed)
468    /// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com`
469    /// and `.google.com` are equivalent) and would match both that domain AND all subdomains.
470    ///
471    /// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all of the following would match
472    /// (and therefore would bypass the proxy):
473    /// * `http://google.com/`
474    /// * `http://www.google.com/`
475    /// * `http://192.168.1.42/`
476    ///
477    /// The URL `http://notgoogle.com/` would not match.
478    pub fn from_string(no_proxy_list: &str) -> Option<Self> {
479        if no_proxy_list.is_empty() {
480            return None;
481        }
482        let mut ips = Vec::new();
483        let mut domains = Vec::new();
484        let parts = no_proxy_list.split(',').map(str::trim);
485        for part in parts {
486            match part.parse::<IpNet>() {
487                // If we can parse an IP net or address, then use it, otherwise, assume it is a domain
488                Ok(ip) => ips.push(Ip::Network(ip)),
489                Err(_) => match part.parse::<IpAddr>() {
490                    Ok(addr) => ips.push(Ip::Address(addr)),
491                    Err(_) => domains.push(part.to_owned()),
492                },
493            }
494        }
495        Some(NoProxy {
496            ips: IpMatcher(ips),
497            domains: DomainMatcher(domains),
498        })
499    }
500
501    fn contains(&self, host: &str) -> bool {
502        // According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off
503        // the end in order to parse correctly
504        let host = if host.starts_with('[') {
505            let x: &[_] = &['[', ']'];
506            host.trim_matches(x)
507        } else {
508            host
509        };
510        match host.parse::<IpAddr>() {
511            // If we can parse an IP addr, then use it, otherwise, assume it is a domain
512            Ok(ip) => self.ips.contains(ip),
513            Err(_) => self.domains.contains(host),
514        }
515    }
516}
517
518impl IpMatcher {
519    fn contains(&self, addr: IpAddr) -> bool {
520        for ip in &self.0 {
521            match ip {
522                Ip::Address(address) => {
523                    if &addr == address {
524                        return true;
525                    }
526                }
527                Ip::Network(net) => {
528                    if net.contains(&addr) {
529                        return true;
530                    }
531                }
532            }
533        }
534        false
535    }
536}
537
538impl DomainMatcher {
539    // The following links may be useful to understand the origin of these rules:
540    // * https://curl.se/libcurl/c/CURLOPT_NOPROXY.html
541    // * https://github.com/curl/curl/issues/1208
542    fn contains(&self, domain: &str) -> bool {
543        let domain_len = domain.len();
544        for d in &self.0 {
545            if d == domain || d.strip_prefix('.') == Some(domain) {
546                return true;
547            } else if domain.ends_with(d) {
548                if d.starts_with('.') {
549                    // If the first character of d is a dot, that means the first character of domain
550                    // must also be a dot, so we are looking at a subdomain of d and that matches
551                    return true;
552                } else if domain.as_bytes().get(domain_len - d.len() - 1) == Some(&b'.') {
553                    // Given that d is a prefix of domain, if the prior character in domain is a dot
554                    // then that means we must be matching a subdomain of d, and that matches
555                    return true;
556                }
557            } else if d == "*" {
558                return true;
559            }
560        }
561        false
562    }
563}
564
565impl ProxyScheme {
566    // To start conservative, keep builders private for now.
567
568    /// Proxy traffic via the specified URL over HTTP
569    fn http(host: &str) -> crate::Result<Self> {
570        Ok(ProxyScheme::Http {
571            auth: None,
572            host: host.parse().map_err(crate::error::builder)?,
573        })
574    }
575
576    /// Proxy traffic via the specified URL over HTTPS
577    fn https(host: &str) -> crate::Result<Self> {
578        Ok(ProxyScheme::Https {
579            auth: None,
580            host: host.parse().map_err(crate::error::builder)?,
581        })
582    }
583
584    /// Proxy traffic via the specified socket address over SOCKS5
585    ///
586    /// # Note
587    ///
588    /// Current SOCKS5 support is provided via blocking IO.
589    #[cfg(feature = "socks")]
590    fn socks5(addr: SocketAddr) -> crate::Result<Self> {
591        Ok(ProxyScheme::Socks5 {
592            addr,
593            auth: None,
594            remote_dns: false,
595        })
596    }
597
598    /// Proxy traffic via the specified socket address over SOCKS5H
599    ///
600    /// This differs from SOCKS5 in that DNS resolution is also performed via the proxy.
601    ///
602    /// # Note
603    ///
604    /// Current SOCKS5 support is provided via blocking IO.
605    #[cfg(feature = "socks")]
606    fn socks5h(addr: SocketAddr) -> crate::Result<Self> {
607        Ok(ProxyScheme::Socks5 {
608            addr,
609            auth: None,
610            remote_dns: true,
611        })
612    }
613
614    /// Use a username and password when connecting to the proxy server
615    fn with_basic_auth<T: Into<String>, U: Into<String>>(
616        mut self,
617        username: T,
618        password: U,
619    ) -> Self {
620        self.set_basic_auth(username, password);
621        self
622    }
623
624    fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
625        match *self {
626            ProxyScheme::Http { ref mut auth, .. } => {
627                let header = encode_basic_auth(&username.into(), &password.into());
628                *auth = Some(header);
629            }
630            ProxyScheme::Https { ref mut auth, .. } => {
631                let header = encode_basic_auth(&username.into(), &password.into());
632                *auth = Some(header);
633            }
634            #[cfg(feature = "socks")]
635            ProxyScheme::Socks5 { ref mut auth, .. } => {
636                *auth = Some((username.into(), password.into()));
637            }
638        }
639    }
640
641    fn set_custom_http_auth(&mut self, header_value: HeaderValue) {
642        match *self {
643            ProxyScheme::Http { ref mut auth, .. } => {
644                *auth = Some(header_value);
645            }
646            ProxyScheme::Https { ref mut auth, .. } => {
647                *auth = Some(header_value);
648            }
649            #[cfg(feature = "socks")]
650            ProxyScheme::Socks5 { .. } => {
651                panic!("Socks is not supported for this method")
652            }
653        }
654    }
655
656    fn if_no_auth(mut self, update: &Option<HeaderValue>) -> Self {
657        match self {
658            ProxyScheme::Http { ref mut auth, .. } => {
659                if auth.is_none() {
660                    *auth = update.clone();
661                }
662            }
663            ProxyScheme::Https { ref mut auth, .. } => {
664                if auth.is_none() {
665                    *auth = update.clone();
666                }
667            }
668            #[cfg(feature = "socks")]
669            ProxyScheme::Socks5 { .. } => {}
670        }
671
672        self
673    }
674
675    /// Convert a URL into a proxy scheme
676    ///
677    /// Supported schemes: HTTP, HTTPS, (SOCKS5, SOCKS5H if `socks` feature is enabled).
678    // Private for now...
679    fn parse(url: Url) -> crate::Result<Self> {
680        use url::Position;
681
682        // Resolve URL to a host and port
683        #[cfg(feature = "socks")]
684        let to_addr = || {
685            let addrs = url
686                .socket_addrs(|| match url.scheme() {
687                    "socks5" | "socks5h" => Some(1080),
688                    _ => None,
689                })
690                .map_err(crate::error::builder)?;
691            addrs
692                .into_iter()
693                .next()
694                .ok_or_else(|| crate::error::builder("unknown proxy scheme"))
695        };
696
697        let mut scheme = match url.scheme() {
698            "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
699            "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
700            #[cfg(feature = "socks")]
701            "socks5" => Self::socks5(to_addr()?)?,
702            #[cfg(feature = "socks")]
703            "socks5h" => Self::socks5h(to_addr()?)?,
704            _ => return Err(crate::error::builder("unknown proxy scheme")),
705        };
706
707        if let Some(pwd) = url.password() {
708            let decoded_username = percent_decode(url.username().as_bytes()).decode_utf8_lossy();
709            let decoded_password = percent_decode(pwd.as_bytes()).decode_utf8_lossy();
710            scheme = scheme.with_basic_auth(decoded_username, decoded_password);
711        }
712
713        Ok(scheme)
714    }
715
716    #[cfg(test)]
717    fn scheme(&self) -> &str {
718        match self {
719            ProxyScheme::Http { .. } => "http",
720            ProxyScheme::Https { .. } => "https",
721            #[cfg(feature = "socks")]
722            ProxyScheme::Socks5 { .. } => "socks5",
723        }
724    }
725
726    #[cfg(test)]
727    fn host(&self) -> &str {
728        match self {
729            ProxyScheme::Http { host, .. } => host.as_str(),
730            ProxyScheme::Https { host, .. } => host.as_str(),
731            #[cfg(feature = "socks")]
732            ProxyScheme::Socks5 { .. } => panic!("socks5"),
733        }
734    }
735}
736
737impl fmt::Debug for ProxyScheme {
738    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
739        match self {
740            ProxyScheme::Http { auth: _auth, host } => write!(f, "http://{host}"),
741            ProxyScheme::Https { auth: _auth, host } => write!(f, "https://{host}"),
742            #[cfg(feature = "socks")]
743            ProxyScheme::Socks5 {
744                addr,
745                auth: _auth,
746                remote_dns,
747            } => {
748                let h = if *remote_dns { "h" } else { "" };
749                write!(f, "socks5{h}://{addr}")
750            }
751        }
752    }
753}
754
755type SystemProxyMap = HashMap<String, ProxyScheme>;
756
757#[derive(Clone, Debug)]
758enum Intercept {
759    All(ProxyScheme),
760    Http(ProxyScheme),
761    Https(ProxyScheme),
762    System(Arc<SystemProxyMap>),
763    Custom(Custom),
764}
765
766impl Intercept {
767    fn set_basic_auth(&mut self, username: &str, password: &str) {
768        match self {
769            Intercept::All(ref mut s)
770            | Intercept::Http(ref mut s)
771            | Intercept::Https(ref mut s) => s.set_basic_auth(username, password),
772            Intercept::System(_) => unimplemented!(),
773            Intercept::Custom(ref mut custom) => {
774                let header = encode_basic_auth(username, password);
775                custom.auth = Some(header);
776            }
777        }
778    }
779
780    fn set_custom_http_auth(&mut self, header_value: HeaderValue) {
781        match self {
782            Intercept::All(ref mut s)
783            | Intercept::Http(ref mut s)
784            | Intercept::Https(ref mut s) => s.set_custom_http_auth(header_value),
785            Intercept::System(_) => unimplemented!(),
786            Intercept::Custom(ref mut custom) => {
787                custom.auth = Some(header_value);
788            }
789        }
790    }
791}
792
793#[derive(Clone)]
794struct Custom {
795    // This auth only applies if the returned ProxyScheme doesn't have an auth...
796    auth: Option<HeaderValue>,
797    func: Arc<dyn Fn(&Url) -> Option<crate::Result<ProxyScheme>> + Send + Sync + 'static>,
798}
799
800impl Custom {
801    fn call<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> {
802        let url = format!(
803            "{}://{}{}{}",
804            uri.scheme(),
805            uri.host(),
806            uri.port().map_or("", |_| ":"),
807            uri.port().map_or(String::new(), |p| p.to_string())
808        )
809        .parse()
810        .expect("should be valid Url");
811
812        (self.func)(&url)
813            .and_then(|result| result.ok())
814            .map(|scheme| scheme.if_no_auth(&self.auth))
815    }
816}
817
818impl fmt::Debug for Custom {
819    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
820        f.write_str("_")
821    }
822}
823
824pub(crate) fn encode_basic_auth(username: &str, password: &str) -> HeaderValue {
825    crate::util::basic_auth(username, Some(password))
826}
827
828/// A helper trait to allow testing `Proxy::intercept` without having to
829/// construct `hyper::client::connect::Destination`s.
830pub(crate) trait Dst {
831    fn scheme(&self) -> &str;
832    fn host(&self) -> &str;
833    fn port(&self) -> Option<u16>;
834}
835
836#[doc(hidden)]
837impl Dst for Uri {
838    fn scheme(&self) -> &str {
839        self.scheme().expect("Uri should have a scheme").as_str()
840    }
841
842    fn host(&self) -> &str {
843        Uri::host(self).expect("<Uri as Dst>::host should have a str")
844    }
845
846    fn port(&self) -> Option<u16> {
847        self.port().map(|p| p.as_u16())
848    }
849}
850
851static SYS_PROXIES: Lazy<Arc<SystemProxyMap>> =
852    Lazy::new(|| Arc::new(get_sys_proxies(get_from_platform())));
853
854/// Get system proxies information.
855///
856/// All platforms will check for proxy settings via environment variables.
857/// If those aren't set, platform-wide proxy settings will be looked up on
858/// Windows and MacOS platforms instead. Errors encountered while discovering
859/// these settings are ignored.
860///
861/// Returns:
862///     System proxies information as a hashmap like
863///     {"http": Url::parse("http://127.0.0.1:80"), "https": Url::parse("https://127.0.0.1:80")}
864fn get_sys_proxies(
865    #[cfg_attr(
866        not(any(target_os = "windows", target_os = "macos")),
867        allow(unused_variables)
868    )]
869    platform_proxies: Option<String>,
870) -> SystemProxyMap {
871    let proxies = get_from_environment();
872
873    #[cfg(any(target_os = "windows", target_os = "macos"))]
874    if proxies.is_empty() {
875        // if there are errors in acquiring the platform proxies,
876        // we'll just return an empty HashMap
877        if let Some(platform_proxies) = platform_proxies {
878            return parse_platform_values(platform_proxies);
879        }
880    }
881
882    proxies
883}
884
885fn insert_proxy(proxies: &mut SystemProxyMap, scheme: impl Into<String>, addr: String) -> bool {
886    if addr.trim().is_empty() {
887        // do not accept empty or whitespace proxy address
888        false
889    } else if let Ok(valid_addr) = addr.into_proxy_scheme() {
890        proxies.insert(scheme.into(), valid_addr);
891        true
892    } else {
893        false
894    }
895}
896
897fn get_from_environment() -> SystemProxyMap {
898    let mut proxies = HashMap::new();
899
900    if is_cgi() {
901        if log::log_enabled!(log::Level::Warn) && env::var_os("HTTP_PROXY").is_some() {
902            log::warn!("HTTP_PROXY environment variable ignored in CGI");
903        }
904    } else if !insert_from_env(&mut proxies, "http", "HTTP_PROXY") {
905        insert_from_env(&mut proxies, "http", "http_proxy");
906    }
907
908    if !insert_from_env(&mut proxies, "https", "HTTPS_PROXY") {
909        insert_from_env(&mut proxies, "https", "https_proxy");
910    }
911
912    if !(insert_from_env(&mut proxies, "http", "ALL_PROXY")
913        && insert_from_env(&mut proxies, "https", "ALL_PROXY"))
914    {
915        insert_from_env(&mut proxies, "http", "all_proxy");
916        insert_from_env(&mut proxies, "https", "all_proxy");
917    }
918
919    proxies
920}
921
922fn insert_from_env(proxies: &mut SystemProxyMap, scheme: &str, var: &str) -> bool {
923    if let Ok(val) = env::var(var) {
924        insert_proxy(proxies, scheme, val)
925    } else {
926        false
927    }
928}
929
930/// Check if we are being executed in a CGI context.
931///
932/// If so, a malicious client can send the `Proxy:` header, and it will
933/// be in the `HTTP_PROXY` env var. So we don't use it :)
934fn is_cgi() -> bool {
935    env::var_os("REQUEST_METHOD").is_some()
936}
937
938#[cfg(target_os = "windows")]
939fn get_from_platform_impl() -> Result<Option<String>, Box<dyn Error>> {
940    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
941    let internet_setting: RegKey =
942        hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?;
943    // ensure the proxy is enable, if the value doesn't exist, an error will returned.
944    let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?;
945    let proxy_server: String = internet_setting.get_value("ProxyServer")?;
946
947    Ok((proxy_enable == 1).then_some(proxy_server))
948}
949
950#[cfg(all(target_os = "macos", feature = "macos-system-configuration"))]
951fn parse_setting_from_dynamic_store(
952    proxies_map: &CFDictionary<CFString, CFType>,
953    enabled_key: CFStringRef,
954    host_key: CFStringRef,
955    port_key: CFStringRef,
956    scheme: &str,
957) -> Option<String> {
958    let proxy_enabled = proxies_map
959        .find(enabled_key)
960        .and_then(|flag| flag.downcast::<CFNumber>())
961        .and_then(|flag| flag.to_i32())
962        .unwrap_or(0)
963        == 1;
964
965    if proxy_enabled {
966        let proxy_host = proxies_map
967            .find(host_key)
968            .and_then(|host| host.downcast::<CFString>())
969            .map(|host| host.to_string());
970        let proxy_port = proxies_map
971            .find(port_key)
972            .and_then(|port| port.downcast::<CFNumber>())
973            .and_then(|port| port.to_i32());
974
975        return match (proxy_host, proxy_port) {
976            (Some(proxy_host), Some(proxy_port)) => {
977                Some(format!("{scheme}={proxy_host}:{proxy_port}"))
978            }
979            (Some(proxy_host), None) => Some(format!("{scheme}={proxy_host}")),
980            (None, Some(_)) => None,
981            (None, None) => None,
982        };
983    }
984
985    None
986}
987
988#[cfg(all(target_os = "macos", feature = "macos-system-configuration"))]
989fn get_from_platform_impl() -> Result<Option<String>, Box<dyn Error>> {
990    let store = SCDynamicStoreBuilder::new("reqwest").build();
991
992    let proxies_map = if let Some(proxies_map) = store.get_proxies() {
993        proxies_map
994    } else {
995        return Ok(None);
996    };
997
998    let http_proxy_config = parse_setting_from_dynamic_store(
999        &proxies_map,
1000        unsafe { kSCPropNetProxiesHTTPEnable },
1001        unsafe { kSCPropNetProxiesHTTPProxy },
1002        unsafe { kSCPropNetProxiesHTTPPort },
1003        "http",
1004    );
1005    let https_proxy_config = parse_setting_from_dynamic_store(
1006        &proxies_map,
1007        unsafe { kSCPropNetProxiesHTTPSEnable },
1008        unsafe { kSCPropNetProxiesHTTPSProxy },
1009        unsafe { kSCPropNetProxiesHTTPSPort },
1010        "https",
1011    );
1012
1013    match http_proxy_config.as_ref().zip(https_proxy_config.as_ref()) {
1014        Some((http_config, https_config)) => Ok(Some(format!("{http_config};{https_config}"))),
1015        None => Ok(http_proxy_config.or(https_proxy_config)),
1016    }
1017}
1018
1019#[cfg(any(
1020    target_os = "windows",
1021    all(target_os = "macos", feature = "macos-system-configuration")
1022))]
1023fn get_from_platform() -> Option<String> {
1024    get_from_platform_impl().ok().flatten()
1025}
1026
1027#[cfg(not(any(
1028    target_os = "windows",
1029    all(target_os = "macos", feature = "macos-system-configuration")
1030)))]
1031fn get_from_platform() -> Option<String> {
1032    None
1033}
1034
1035#[cfg(any(target_os = "windows", target_os = "macos"))]
1036fn parse_platform_values_impl(platform_values: String) -> SystemProxyMap {
1037    let mut proxies = HashMap::new();
1038    if platform_values.contains("=") {
1039        // per-protocol settings.
1040        for p in platform_values.split(";") {
1041            let protocol_parts: Vec<&str> = p.split("=").collect();
1042            match protocol_parts.as_slice() {
1043                [protocol, address] => {
1044                    // If address doesn't specify an explicit protocol as protocol://address
1045                    // then default to HTTP
1046                    let address = if extract_type_prefix(*address).is_some() {
1047                        String::from(*address)
1048                    } else {
1049                        format!("http://{address}")
1050                    };
1051
1052                    insert_proxy(&mut proxies, *protocol, address);
1053                }
1054                _ => {
1055                    // Contains invalid protocol setting, just break the loop
1056                    // And make proxies to be empty.
1057                    proxies.clear();
1058                    break;
1059                }
1060            }
1061        }
1062    } else {
1063        if let Some(scheme) = extract_type_prefix(&platform_values) {
1064            // Explicit protocol has been specified
1065            insert_proxy(&mut proxies, scheme, platform_values.to_owned());
1066        } else {
1067            // No explicit protocol has been specified, default to HTTP
1068            insert_proxy(&mut proxies, "http", format!("http://{platform_values}"));
1069            insert_proxy(&mut proxies, "https", format!("http://{platform_values}"));
1070        }
1071    }
1072    proxies
1073}
1074
1075/// Extract the protocol from the given address, if present
1076/// For example, "https://example.com" will return Some("https")
1077#[cfg(any(target_os = "windows", target_os = "macos"))]
1078fn extract_type_prefix(address: &str) -> Option<&str> {
1079    if let Some(indice) = address.find("://") {
1080        if indice == 0 {
1081            None
1082        } else {
1083            let prefix = &address[..indice];
1084            let contains_banned = prefix.contains(|c| c == ':' || c == '/');
1085
1086            if !contains_banned {
1087                Some(prefix)
1088            } else {
1089                None
1090            }
1091        }
1092    } else {
1093        None
1094    }
1095}
1096
1097#[cfg(any(target_os = "windows", target_os = "macos"))]
1098fn parse_platform_values(platform_values: String) -> SystemProxyMap {
1099    parse_platform_values_impl(platform_values)
1100}
1101
1102#[cfg(test)]
1103mod tests {
1104    use super::*;
1105    use once_cell::sync::Lazy;
1106    use std::sync::Mutex;
1107
1108    impl Dst for Url {
1109        fn scheme(&self) -> &str {
1110            Url::scheme(self)
1111        }
1112
1113        fn host(&self) -> &str {
1114            Url::host_str(self).expect("<Url as Dst>::host should have a str")
1115        }
1116
1117        fn port(&self) -> Option<u16> {
1118            Url::port(self)
1119        }
1120    }
1121
1122    fn url(s: &str) -> Url {
1123        s.parse().unwrap()
1124    }
1125
1126    fn intercepted_uri(p: &Proxy, s: &str) -> Uri {
1127        let (scheme, host) = match p.intercept(&url(s)).unwrap() {
1128            ProxyScheme::Http { host, .. } => ("http", host),
1129            ProxyScheme::Https { host, .. } => ("https", host),
1130            #[cfg(feature = "socks")]
1131            _ => panic!("intercepted as socks"),
1132        };
1133        http::Uri::builder()
1134            .scheme(scheme)
1135            .authority(host)
1136            .path_and_query("/")
1137            .build()
1138            .expect("intercepted_uri")
1139    }
1140
1141    #[test]
1142    fn test_http() {
1143        let target = "http://example.domain/";
1144        let p = Proxy::http(target).unwrap();
1145
1146        let http = "http://hyper.rs";
1147        let other = "https://hyper.rs";
1148
1149        assert_eq!(intercepted_uri(&p, http), target);
1150        assert!(p.intercept(&url(other)).is_none());
1151    }
1152
1153    #[test]
1154    fn test_https() {
1155        let target = "http://example.domain/";
1156        let p = Proxy::https(target).unwrap();
1157
1158        let http = "http://hyper.rs";
1159        let other = "https://hyper.rs";
1160
1161        assert!(p.intercept(&url(http)).is_none());
1162        assert_eq!(intercepted_uri(&p, other), target);
1163    }
1164
1165    #[test]
1166    fn test_all() {
1167        let target = "http://example.domain/";
1168        let p = Proxy::all(target).unwrap();
1169
1170        let http = "http://hyper.rs";
1171        let https = "https://hyper.rs";
1172        let other = "x-youve-never-heard-of-me-mr-proxy://hyper.rs";
1173
1174        assert_eq!(intercepted_uri(&p, http), target);
1175        assert_eq!(intercepted_uri(&p, https), target);
1176        assert_eq!(intercepted_uri(&p, other), target);
1177    }
1178
1179    #[test]
1180    fn test_custom() {
1181        let target1 = "http://example.domain/";
1182        let target2 = "https://example.domain/";
1183        let p = Proxy::custom(move |url| {
1184            if url.host_str() == Some("hyper.rs") {
1185                target1.parse().ok()
1186            } else if url.scheme() == "http" {
1187                target2.parse().ok()
1188            } else {
1189                None::<Url>
1190            }
1191        });
1192
1193        let http = "http://seanmonstar.com";
1194        let https = "https://hyper.rs";
1195        let other = "x-youve-never-heard-of-me-mr-proxy://seanmonstar.com";
1196
1197        assert_eq!(intercepted_uri(&p, http), target2);
1198        assert_eq!(intercepted_uri(&p, https), target1);
1199        assert!(p.intercept(&url(other)).is_none());
1200    }
1201
1202    #[test]
1203    fn test_proxy_scheme_parse() {
1204        let ps = "http://foo:bar@localhost:1239".into_proxy_scheme().unwrap();
1205
1206        match ps {
1207            ProxyScheme::Http { auth, host } => {
1208                assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar"));
1209                assert_eq!(host, "localhost:1239");
1210            }
1211            other => panic!("unexpected: {other:?}"),
1212        }
1213    }
1214
1215    #[test]
1216    fn test_proxy_scheme_ip_address_default_http() {
1217        let ps = "192.168.1.1:8888".into_proxy_scheme().unwrap();
1218
1219        match ps {
1220            ProxyScheme::Http { auth, host } => {
1221                assert!(auth.is_none());
1222                assert_eq!(host, "192.168.1.1:8888");
1223            }
1224            other => panic!("unexpected: {other:?}"),
1225        }
1226    }
1227
1228    #[test]
1229    fn test_proxy_scheme_parse_default_http_with_auth() {
1230        // this should fail because `foo` is interpreted as the scheme and no host can be found
1231        let ps = "foo:bar@localhost:1239".into_proxy_scheme().unwrap();
1232
1233        match ps {
1234            ProxyScheme::Http { auth, host } => {
1235                assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar"));
1236                assert_eq!(host, "localhost:1239");
1237            }
1238            other => panic!("unexpected: {other:?}"),
1239        }
1240    }
1241
1242    #[test]
1243    fn test_domain_matcher() {
1244        let domains = vec![".foo.bar".into(), "bar.foo".into()];
1245        let matcher = DomainMatcher(domains);
1246
1247        // domains match with leading `.`
1248        assert!(matcher.contains("foo.bar"));
1249        // subdomains match with leading `.`
1250        assert!(matcher.contains("www.foo.bar"));
1251
1252        // domains match with no leading `.`
1253        assert!(matcher.contains("bar.foo"));
1254        // subdomains match with no leading `.`
1255        assert!(matcher.contains("www.bar.foo"));
1256
1257        // non-subdomain string prefixes don't match
1258        assert!(!matcher.contains("notfoo.bar"));
1259        assert!(!matcher.contains("notbar.foo"));
1260    }
1261
1262    // Smallest possible content for a mutex
1263    struct MutexInner;
1264
1265    static ENVLOCK: Lazy<Mutex<MutexInner>> = Lazy::new(|| Mutex::new(MutexInner));
1266
1267    #[test]
1268    fn test_get_sys_proxies_parsing() {
1269        // Stop other threads from modifying process-global ENV while we are.
1270        let _lock = ENVLOCK.lock();
1271        // save system setting first.
1272        let _g1 = env_guard("HTTP_PROXY");
1273        let _g2 = env_guard("http_proxy");
1274        let _g3 = env_guard("ALL_PROXY");
1275
1276        // Mock ENV, get the results, before doing assertions
1277        // to avoid assert! -> panic! -> Mutex Poisoned.
1278        let baseline_proxies = get_sys_proxies(None);
1279        // the system proxy setting url is invalid.
1280        env::set_var("http_proxy", "file://123465");
1281        let invalid_proxies = get_sys_proxies(None);
1282        // set valid proxy
1283        env::set_var("http_proxy", "127.0.0.1/");
1284        let valid_proxies = get_sys_proxies(None);
1285        // set valid ALL_PROXY
1286        env::set_var("ALL_PROXY", "127.0.0.2/");
1287        let all_proxies = get_sys_proxies(None);
1288
1289        // reset user setting when guards drop
1290        drop(_g1);
1291        drop(_g2);
1292        // Let other threads run now
1293        drop(_lock);
1294
1295        assert!(!baseline_proxies.contains_key("http"));
1296        assert!(!invalid_proxies.contains_key("http"));
1297
1298        let p = &valid_proxies["http"];
1299        assert_eq!(p.scheme(), "http");
1300        assert_eq!(p.host(), "127.0.0.1");
1301
1302        assert_eq!(all_proxies.len(), 2);
1303        assert!(all_proxies.values().all(|p| p.host() == "127.0.0.2"));
1304    }
1305
1306    #[cfg(any(target_os = "windows", target_os = "macos"))]
1307    #[test]
1308    fn test_get_sys_proxies_registry_parsing() {
1309        // Stop other threads from modifying process-global ENV while we are.
1310        let _lock = ENVLOCK.lock();
1311        // save system setting first.
1312        let _g1 = env_guard("HTTP_PROXY");
1313        let _g2 = env_guard("http_proxy");
1314
1315        // Mock ENV, get the results, before doing assertions
1316        // to avoid assert! -> panic! -> Mutex Poisoned.
1317        let baseline_proxies = get_sys_proxies(None);
1318        // set valid proxy
1319        let valid_proxies = get_sys_proxies(Some(String::from("http://127.0.0.1/")));
1320        let valid_proxies_no_scheme = get_sys_proxies(Some(String::from("127.0.0.1")));
1321        let valid_proxies_explicit_https =
1322            get_sys_proxies(Some(String::from("https://127.0.0.1/")));
1323        let multiple_proxies = get_sys_proxies(Some(String::from(
1324            "http=127.0.0.1:8888;https=127.0.0.2:8888",
1325        )));
1326        let multiple_proxies_explicit_scheme = get_sys_proxies(Some(String::from(
1327            "http=http://127.0.0.1:8888;https=https://127.0.0.2:8888",
1328        )));
1329
1330        // reset user setting when guards drop
1331        drop(_g1);
1332        drop(_g2);
1333        // Let other threads run now
1334        drop(_lock);
1335
1336        assert_eq!(baseline_proxies.contains_key("http"), false);
1337
1338        let p = &valid_proxies["http"];
1339        assert_eq!(p.scheme(), "http");
1340        assert_eq!(p.host(), "127.0.0.1");
1341
1342        let p = &valid_proxies_no_scheme["http"];
1343        assert_eq!(p.scheme(), "http");
1344        assert_eq!(p.host(), "127.0.0.1");
1345
1346        let p = &valid_proxies_no_scheme["https"];
1347        assert_eq!(p.scheme(), "http");
1348        assert_eq!(p.host(), "127.0.0.1");
1349
1350        let p = &valid_proxies_explicit_https["https"];
1351        assert_eq!(p.scheme(), "https");
1352        assert_eq!(p.host(), "127.0.0.1");
1353
1354        let p = &multiple_proxies["http"];
1355        assert_eq!(p.scheme(), "http");
1356        assert_eq!(p.host(), "127.0.0.1:8888");
1357
1358        let p = &multiple_proxies["https"];
1359        assert_eq!(p.scheme(), "http");
1360        assert_eq!(p.host(), "127.0.0.2:8888");
1361
1362        let p = &multiple_proxies_explicit_scheme["http"];
1363        assert_eq!(p.scheme(), "http");
1364        assert_eq!(p.host(), "127.0.0.1:8888");
1365
1366        let p = &multiple_proxies_explicit_scheme["https"];
1367        assert_eq!(p.scheme(), "https");
1368        assert_eq!(p.host(), "127.0.0.2:8888");
1369    }
1370
1371    #[test]
1372    fn test_get_sys_proxies_in_cgi() {
1373        // Stop other threads from modifying process-global ENV while we are.
1374        let _lock = ENVLOCK.lock();
1375        // save system setting first.
1376        let _g1 = env_guard("REQUEST_METHOD");
1377        let _g2 = env_guard("HTTP_PROXY");
1378
1379        // Mock ENV, get the results, before doing assertions
1380        // to avoid assert! -> panic! -> Mutex Poisoned.
1381        env::set_var("HTTP_PROXY", "http://evil/");
1382
1383        let baseline_proxies = get_sys_proxies(None);
1384        // set like we're in CGI
1385        env::set_var("REQUEST_METHOD", "GET");
1386
1387        let cgi_proxies = get_sys_proxies(None);
1388
1389        // reset user setting when guards drop
1390        drop(_g1);
1391        drop(_g2);
1392        // Let other threads run now
1393        drop(_lock);
1394
1395        // not in CGI yet
1396        assert_eq!(baseline_proxies["http"].host(), "evil");
1397        // In CGI
1398        assert!(!cgi_proxies.contains_key("http"));
1399    }
1400
1401    #[test]
1402    fn test_sys_no_proxy() {
1403        // Stop other threads from modifying process-global ENV while we are.
1404        let _lock = ENVLOCK.lock();
1405        // save system setting first.
1406        let _g1 = env_guard("HTTP_PROXY");
1407        let _g2 = env_guard("NO_PROXY");
1408
1409        let target = "http://example.domain/";
1410        env::set_var("HTTP_PROXY", target);
1411
1412        env::set_var(
1413            "NO_PROXY",
1414            ".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17",
1415        );
1416
1417        // Manually construct this so we aren't use the cache
1418        let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1419        p.no_proxy = NoProxy::from_env();
1420
1421        // random url, not in no_proxy
1422        assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
1423        // make sure that random non-subdomain string prefixes don't match
1424        assert_eq!(intercepted_uri(&p, "http://notfoo.bar"), target);
1425        // make sure that random non-subdomain string prefixes don't match
1426        assert_eq!(intercepted_uri(&p, "http://notbar.baz"), target);
1427        // ipv4 address out of range
1428        assert_eq!(intercepted_uri(&p, "http://10.43.1.1"), target);
1429        // ipv4 address out of range
1430        assert_eq!(intercepted_uri(&p, "http://10.124.7.7"), target);
1431        // ipv6 address out of range
1432        assert_eq!(intercepted_uri(&p, "http://[ffff:db8:a0b:12f0::1]"), target);
1433        // ipv6 address out of range
1434        assert_eq!(intercepted_uri(&p, "http://[2005:db8:a0b:12f0::1]"), target);
1435
1436        // make sure subdomains (with leading .) match
1437        assert!(p.intercept(&url("http://hello.foo.bar")).is_none());
1438        // make sure exact matches (without leading .) match (also makes sure spaces between entries work)
1439        assert!(p.intercept(&url("http://bar.baz")).is_none());
1440        // check case sensitivity
1441        assert!(p.intercept(&url("http://BAR.baz")).is_none());
1442        // make sure subdomains (without leading . in no_proxy) match
1443        assert!(p.intercept(&url("http://foo.bar.baz")).is_none());
1444        // make sure subdomains (without leading . in no_proxy) match - this differs from cURL
1445        assert!(p.intercept(&url("http://foo.bar")).is_none());
1446        // ipv4 address match within range
1447        assert!(p.intercept(&url("http://10.42.1.100")).is_none());
1448        // ipv6 address exact match
1449        assert!(p.intercept(&url("http://[::1]")).is_none());
1450        // ipv6 address match within range
1451        assert!(p.intercept(&url("http://[2001:db8:a0b:12f0::1]")).is_none());
1452        // ipv4 address exact match
1453        assert!(p.intercept(&url("http://10.124.7.8")).is_none());
1454
1455        // reset user setting when guards drop
1456        drop(_g1);
1457        drop(_g2);
1458        // Let other threads run now
1459        drop(_lock);
1460    }
1461
1462    #[test]
1463    fn test_proxy_no_proxy_interception_for_proxy_types() {
1464        let proxy_url = "http://example.domain/";
1465        let no_proxy = ".no.proxy.tld";
1466
1467        // test all proxy interception
1468        let p = Proxy::all(proxy_url)
1469            .unwrap()
1470            .no_proxy(NoProxy::from_string(no_proxy));
1471
1472        // random url, not in no_proxy
1473        assert_eq!(intercepted_uri(&p, "http://hyper.rs"), proxy_url);
1474
1475        // positive match for no proxy
1476        assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none());
1477
1478        // test http proxy interception
1479        let p = Proxy::http(proxy_url)
1480            .unwrap()
1481            .no_proxy(NoProxy::from_string(no_proxy));
1482
1483        // random url, not in no_proxy
1484        assert_eq!(intercepted_uri(&p, "http://hyper.rs"), proxy_url);
1485
1486        // positive match for no proxy
1487        assert!(p.intercept(&url("http://hello.no.proxy.tld")).is_none());
1488
1489        // should not be intercepted due to scheme
1490        assert!(p.intercept(&url("https://hyper.rs")).is_none());
1491
1492        // test https proxy interception
1493        let p = Proxy::https(proxy_url)
1494            .unwrap()
1495            .no_proxy(NoProxy::from_string(no_proxy));
1496
1497        // random url, not in no_proxy
1498        assert_eq!(intercepted_uri(&p, "https://hyper.rs"), proxy_url);
1499
1500        // positive match for no proxy
1501        assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none());
1502
1503        // should not be intercepted due to scheme
1504        assert!(p.intercept(&url("http://hyper.rs")).is_none());
1505
1506        // test custom proxy interception
1507        let p = Proxy::custom(move |_url| Some(proxy_url)).no_proxy(NoProxy::from_string(no_proxy));
1508
1509        // random url, not in no_proxy
1510        assert_eq!(intercepted_uri(&p, "https://hyper.rs"), proxy_url);
1511
1512        // positive match for no proxy
1513        assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none());
1514        assert!(p.intercept(&url("http://hello.no.proxy.tld")).is_none());
1515    }
1516
1517    #[test]
1518    fn test_wildcard_sys_no_proxy() {
1519        // Stop other threads from modifying process-global ENV while we are.
1520        let _lock = ENVLOCK.lock();
1521        // save system setting first.
1522        let _g1 = env_guard("HTTP_PROXY");
1523        let _g2 = env_guard("NO_PROXY");
1524
1525        let target = "http://example.domain/";
1526        env::set_var("HTTP_PROXY", target);
1527
1528        env::set_var("NO_PROXY", "*");
1529
1530        // Manually construct this so we aren't use the cache
1531        let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1532        p.no_proxy = NoProxy::from_env();
1533
1534        assert!(p.intercept(&url("http://foo.bar")).is_none());
1535
1536        // reset user setting when guards drop
1537        drop(_g1);
1538        drop(_g2);
1539        // Let other threads run now
1540        drop(_lock);
1541    }
1542
1543    #[test]
1544    fn test_empty_sys_no_proxy() {
1545        // Stop other threads from modifying process-global ENV while we are.
1546        let _lock = ENVLOCK.lock();
1547        // save system setting first.
1548        let _g1 = env_guard("HTTP_PROXY");
1549        let _g2 = env_guard("NO_PROXY");
1550
1551        let target = "http://example.domain/";
1552        env::set_var("HTTP_PROXY", target);
1553
1554        env::set_var("NO_PROXY", ",");
1555
1556        // Manually construct this so we aren't use the cache
1557        let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1558        p.no_proxy = NoProxy::from_env();
1559
1560        // everything should go through proxy, "effectively" nothing is in no_proxy
1561        assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
1562
1563        // reset user setting when guards drop
1564        drop(_g1);
1565        drop(_g2);
1566        // Let other threads run now
1567        drop(_lock);
1568    }
1569
1570    #[test]
1571    fn test_no_proxy_load() {
1572        // Stop other threads from modifying process-global ENV while we are.
1573        let _lock = ENVLOCK.lock();
1574
1575        let _g1 = env_guard("no_proxy");
1576        let domain = "lower.case";
1577        env::set_var("no_proxy", domain);
1578        // Manually construct this so we aren't use the cache
1579        let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1580        p.no_proxy = NoProxy::from_env();
1581        assert_eq!(
1582            p.no_proxy.expect("should have a no proxy set").domains.0[0],
1583            domain
1584        );
1585
1586        env::remove_var("no_proxy");
1587        let _g2 = env_guard("NO_PROXY");
1588        let domain = "upper.case";
1589        env::set_var("NO_PROXY", domain);
1590        // Manually construct this so we aren't use the cache
1591        let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1592        p.no_proxy = NoProxy::from_env();
1593        assert_eq!(
1594            p.no_proxy.expect("should have a no proxy set").domains.0[0],
1595            domain
1596        );
1597
1598        let _g3 = env_guard("HTTP_PROXY");
1599        env::remove_var("NO_PROXY");
1600        env::remove_var("no_proxy");
1601        let target = "http://example.domain/";
1602        env::set_var("HTTP_PROXY", target);
1603
1604        // Manually construct this so we aren't use the cache
1605        let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1606        p.no_proxy = NoProxy::from_env();
1607        assert!(p.no_proxy.is_none(), "NoProxy shouldn't have been created");
1608
1609        assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
1610
1611        // reset user setting when guards drop
1612        drop(_g1);
1613        drop(_g2);
1614        drop(_g3);
1615        // Let other threads run now
1616        drop(_lock);
1617    }
1618
1619    #[cfg(any(target_os = "windows", target_os = "macos"))]
1620    #[test]
1621    fn test_type_prefix_extraction() {
1622        assert!(extract_type_prefix("test").is_none());
1623        assert!(extract_type_prefix("://test").is_none());
1624        assert!(extract_type_prefix("some:prefix://test").is_none());
1625        assert!(extract_type_prefix("some/prefix://test").is_none());
1626
1627        assert_eq!(extract_type_prefix("http://test").unwrap(), "http");
1628        assert_eq!(extract_type_prefix("a://test").unwrap(), "a");
1629    }
1630
1631    /// Guard an environment variable, resetting it to the original value
1632    /// when dropped.
1633    fn env_guard(name: impl Into<String>) -> EnvGuard {
1634        let name = name.into();
1635        let orig_val = env::var(&name).ok();
1636        env::remove_var(&name);
1637        EnvGuard { name, orig_val }
1638    }
1639
1640    struct EnvGuard {
1641        name: String,
1642        orig_val: Option<String>,
1643    }
1644
1645    impl Drop for EnvGuard {
1646        fn drop(&mut self) {
1647            if let Some(val) = self.orig_val.take() {
1648                env::set_var(&self.name, val);
1649            } else {
1650                env::remove_var(&self.name);
1651            }
1652        }
1653    }
1654
1655    #[test]
1656    fn test_has_http_auth() {
1657        let http_proxy_with_auth = Proxy {
1658            intercept: Intercept::Http(ProxyScheme::Http {
1659                auth: Some(HeaderValue::from_static("auth1")),
1660                host: http::uri::Authority::from_static("authority"),
1661            }),
1662            no_proxy: None,
1663        };
1664        assert!(http_proxy_with_auth.maybe_has_http_auth());
1665        assert_eq!(
1666            http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1667            Some(HeaderValue::from_static("auth1"))
1668        );
1669
1670        let http_proxy_without_auth = Proxy {
1671            intercept: Intercept::Http(ProxyScheme::Http {
1672                auth: None,
1673                host: http::uri::Authority::from_static("authority"),
1674            }),
1675            no_proxy: None,
1676        };
1677        assert!(!http_proxy_without_auth.maybe_has_http_auth());
1678        assert_eq!(
1679            http_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1680            None
1681        );
1682
1683        let https_proxy_with_auth = Proxy {
1684            intercept: Intercept::Http(ProxyScheme::Https {
1685                auth: Some(HeaderValue::from_static("auth2")),
1686                host: http::uri::Authority::from_static("authority"),
1687            }),
1688            no_proxy: None,
1689        };
1690        assert!(https_proxy_with_auth.maybe_has_http_auth());
1691        assert_eq!(
1692            https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1693            Some(HeaderValue::from_static("auth2"))
1694        );
1695
1696        let all_http_proxy_with_auth = Proxy {
1697            intercept: Intercept::All(ProxyScheme::Http {
1698                auth: Some(HeaderValue::from_static("auth3")),
1699                host: http::uri::Authority::from_static("authority"),
1700            }),
1701            no_proxy: None,
1702        };
1703        assert!(all_http_proxy_with_auth.maybe_has_http_auth());
1704        assert_eq!(
1705            all_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1706            Some(HeaderValue::from_static("auth3"))
1707        );
1708
1709        let all_https_proxy_with_auth = Proxy {
1710            intercept: Intercept::All(ProxyScheme::Https {
1711                auth: Some(HeaderValue::from_static("auth4")),
1712                host: http::uri::Authority::from_static("authority"),
1713            }),
1714            no_proxy: None,
1715        };
1716        assert!(all_https_proxy_with_auth.maybe_has_http_auth());
1717        assert_eq!(
1718            all_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1719            Some(HeaderValue::from_static("auth4"))
1720        );
1721
1722        let all_https_proxy_without_auth = Proxy {
1723            intercept: Intercept::All(ProxyScheme::Https {
1724                auth: None,
1725                host: http::uri::Authority::from_static("authority"),
1726            }),
1727            no_proxy: None,
1728        };
1729        assert!(!all_https_proxy_without_auth.maybe_has_http_auth());
1730        assert_eq!(
1731            all_https_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1732            None
1733        );
1734
1735        let system_http_proxy_with_auth = Proxy {
1736            intercept: Intercept::System(Arc::new({
1737                let mut m = HashMap::new();
1738                m.insert(
1739                    "http".into(),
1740                    ProxyScheme::Http {
1741                        auth: Some(HeaderValue::from_static("auth5")),
1742                        host: http::uri::Authority::from_static("authority"),
1743                    },
1744                );
1745                m
1746            })),
1747            no_proxy: None,
1748        };
1749        assert!(system_http_proxy_with_auth.maybe_has_http_auth());
1750        assert_eq!(
1751            system_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1752            Some(HeaderValue::from_static("auth5"))
1753        );
1754
1755        let system_https_proxy_with_auth = Proxy {
1756            intercept: Intercept::System(Arc::new({
1757                let mut m = HashMap::new();
1758                m.insert(
1759                    "https".into(),
1760                    ProxyScheme::Https {
1761                        auth: Some(HeaderValue::from_static("auth6")),
1762                        host: http::uri::Authority::from_static("authority"),
1763                    },
1764                );
1765                m
1766            })),
1767            no_proxy: None,
1768        };
1769        assert!(!system_https_proxy_with_auth.maybe_has_http_auth());
1770        assert_eq!(
1771            system_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1772            None
1773        );
1774    }
1775}
1776
1777#[cfg(test)]
1778mod test {
1779    mod into_proxy_scheme {
1780        use crate::Proxy;
1781        use std::error::Error;
1782        use std::mem::discriminant;
1783
1784        fn includes(haystack: &crate::error::Error, needle: url::ParseError) -> bool {
1785            let mut source = haystack.source();
1786            while let Some(error) = source {
1787                if let Some(parse_error) = error.downcast_ref::<url::ParseError>() {
1788                    if discriminant(parse_error) == discriminant(&needle) {
1789                        return true;
1790                    }
1791                }
1792                source = error.source();
1793            }
1794            false
1795        }
1796
1797        fn check_parse_error(url: &str, needle: url::ParseError) {
1798            let error = Proxy::http(url).unwrap_err();
1799            if !includes(&error, needle) {
1800                panic!("{needle:?} expected; {error:?}, {error} found");
1801            }
1802        }
1803
1804        mod when_scheme_missing {
1805            mod and_url_is_valid {
1806                use crate::Proxy;
1807
1808                #[test]
1809                fn lookback_works() {
1810                    let _ = Proxy::http("127.0.0.1").unwrap();
1811                }
1812
1813                #[test]
1814                fn loopback_port_works() {
1815                    let _ = Proxy::http("127.0.0.1:8080").unwrap();
1816                }
1817
1818                #[test]
1819                fn loopback_username_works() {
1820                    let _ = Proxy::http("username@127.0.0.1").unwrap();
1821                }
1822
1823                #[test]
1824                fn loopback_username_password_works() {
1825                    let _ = Proxy::http("username:password@127.0.0.1").unwrap();
1826                }
1827
1828                #[test]
1829                fn loopback_username_password_port_works() {
1830                    let _ = Proxy::http("ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
1831                }
1832
1833                #[test]
1834                fn domain_works() {
1835                    let _ = Proxy::http("proxy.example.com").unwrap();
1836                }
1837
1838                #[test]
1839                fn domain_port_works() {
1840                    let _ = Proxy::http("proxy.example.com:8080").unwrap();
1841                }
1842
1843                #[test]
1844                fn domain_username_works() {
1845                    let _ = Proxy::http("username@proxy.example.com").unwrap();
1846                }
1847
1848                #[test]
1849                fn domain_username_password_works() {
1850                    let _ = Proxy::http("username:password@proxy.example.com").unwrap();
1851                }
1852
1853                #[test]
1854                fn domain_username_password_port_works() {
1855                    let _ =
1856                        Proxy::http("ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080").unwrap();
1857                }
1858            }
1859            mod and_url_has_bad {
1860                use super::super::check_parse_error;
1861
1862                #[test]
1863                fn host() {
1864                    check_parse_error("username@", url::ParseError::RelativeUrlWithoutBase);
1865                }
1866
1867                #[test]
1868                fn idna_encoding() {
1869                    check_parse_error("xn---", url::ParseError::RelativeUrlWithoutBase);
1870                }
1871
1872                #[test]
1873                fn port() {
1874                    check_parse_error("127.0.0.1:808080", url::ParseError::RelativeUrlWithoutBase);
1875                }
1876
1877                #[test]
1878                fn ip_v4_address() {
1879                    check_parse_error("421.627.718.469", url::ParseError::RelativeUrlWithoutBase);
1880                }
1881
1882                #[test]
1883                fn ip_v6_address() {
1884                    check_parse_error(
1885                        "[56FE::2159:5BBC::6594]",
1886                        url::ParseError::RelativeUrlWithoutBase,
1887                    );
1888                }
1889
1890                #[test]
1891                fn invalid_domain_character() {
1892                    check_parse_error("abc 123", url::ParseError::RelativeUrlWithoutBase);
1893                }
1894            }
1895        }
1896
1897        mod when_scheme_present {
1898            mod and_url_is_valid {
1899                use crate::Proxy;
1900
1901                #[test]
1902                fn loopback_works() {
1903                    let _ = Proxy::http("http://127.0.0.1").unwrap();
1904                }
1905
1906                #[test]
1907                fn loopback_port_works() {
1908                    let _ = Proxy::http("https://127.0.0.1:8080").unwrap();
1909                }
1910
1911                #[test]
1912                fn loopback_username_works() {
1913                    let _ = Proxy::http("http://username@127.0.0.1").unwrap();
1914                }
1915
1916                #[test]
1917                fn loopback_username_password_works() {
1918                    let _ = Proxy::http("https://username:password@127.0.0.1").unwrap();
1919                }
1920
1921                #[test]
1922                fn loopback_username_password_port_works() {
1923                    let _ =
1924                        Proxy::http("http://ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
1925                }
1926
1927                #[test]
1928                fn domain_works() {
1929                    let _ = Proxy::http("https://proxy.example.com").unwrap();
1930                }
1931
1932                #[test]
1933                fn domain_port_works() {
1934                    let _ = Proxy::http("http://proxy.example.com:8080").unwrap();
1935                }
1936
1937                #[test]
1938                fn domain_username_works() {
1939                    let _ = Proxy::http("https://username@proxy.example.com").unwrap();
1940                }
1941
1942                #[test]
1943                fn domain_username_password_works() {
1944                    let _ = Proxy::http("http://username:password@proxy.example.com").unwrap();
1945                }
1946
1947                #[test]
1948                fn domain_username_password_port_works() {
1949                    let _ =
1950                        Proxy::http("https://ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080")
1951                            .unwrap();
1952                }
1953            }
1954            mod and_url_has_bad {
1955                use super::super::check_parse_error;
1956
1957                #[test]
1958                fn host() {
1959                    check_parse_error("http://username@", url::ParseError::EmptyHost);
1960                }
1961
1962                #[test]
1963                fn idna_encoding() {
1964                    check_parse_error("http://xn---", url::ParseError::IdnaError);
1965                }
1966
1967                #[test]
1968                fn port() {
1969                    check_parse_error("http://127.0.0.1:808080", url::ParseError::InvalidPort);
1970                }
1971
1972                #[test]
1973                fn ip_v4_address() {
1974                    check_parse_error(
1975                        "http://421.627.718.469",
1976                        url::ParseError::InvalidIpv4Address,
1977                    );
1978                }
1979
1980                #[test]
1981                fn ip_v6_address() {
1982                    check_parse_error(
1983                        "http://[56FE::2159:5BBC::6594]",
1984                        url::ParseError::InvalidIpv6Address,
1985                    );
1986                }
1987
1988                #[test]
1989                fn invalid_domain_character() {
1990                    check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter);
1991                }
1992            }
1993        }
1994    }
1995}