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