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