reqwest/
proxy.rs

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