Skip to main content

reqwest/
proxy.rs

1use std::error::Error;
2use std::fmt;
3use std::sync::Arc;
4
5use http::uri::Scheme;
6use http::{header::HeaderValue, HeaderMap, Uri};
7use hyper_util::client::proxy::matcher;
8
9use crate::into_url::{IntoUrl, IntoUrlSealed};
10use crate::Url;
11
12// # Internals
13//
14// This module is a couple pieces:
15//
16// - The public builder API
17// - The internal built types that our Connector knows how to use.
18//
19// The user creates a builder (`reqwest::Proxy`), and configures any extras.
20// Once that type is passed to the `ClientBuilder`, we convert it into the
21// built matcher types, making use of `hyper-util`'s matchers.
22
23/// Configuration of a proxy that a `Client` should pass requests to.
24///
25/// A `Proxy` has a couple pieces to it:
26///
27/// - a URL of how to talk to the proxy
28/// - rules on what `Client` requests should be directed to the proxy
29///
30/// For instance, let's look at `Proxy::http`:
31///
32/// ```rust
33/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
34/// let proxy = reqwest::Proxy::http("https://secure.example")?;
35/// # Ok(())
36/// # }
37/// ```
38///
39/// This proxy will intercept all HTTP requests, and make use of the proxy
40/// at `https://secure.example`. A request to `http://hyper.rs` will talk
41/// to your proxy. A request to `https://hyper.rs` will not.
42///
43/// Multiple `Proxy` rules can be configured for a `Client`. The `Client` will
44/// check each `Proxy` in the order it was added. This could mean that a
45/// `Proxy` added first with eager intercept rules, such as `Proxy::all`,
46/// would prevent a `Proxy` later in the list from ever working, so take care.
47///
48/// By enabling the `"socks"` feature it is possible to use a socks proxy:
49/// ```rust
50/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
51/// let proxy = reqwest::Proxy::http("socks5://192.168.1.1:9000")?;
52/// # Ok(())
53/// # }
54/// ```
55#[derive(Clone)]
56pub struct Proxy {
57    extra: Extra,
58    intercept: Intercept,
59    no_proxy: Option<NoProxy>,
60}
61
62/// A configuration for filtering out requests that shouldn't be proxied
63#[derive(Clone, Debug, Default)]
64pub struct NoProxy {
65    inner: String,
66}
67
68#[derive(Clone)]
69struct Extra {
70    auth: Option<HeaderValue>,
71    misc: Option<HeaderMap>,
72}
73
74// ===== Internal =====
75
76pub(crate) struct Matcher {
77    inner: Matcher_,
78    extra: Extra,
79    maybe_has_http_auth: bool,
80    maybe_has_http_custom_headers: bool,
81}
82
83#[allow(clippy::large_enum_variant)]
84enum Matcher_ {
85    Util(matcher::Matcher),
86    Custom(Custom),
87}
88
89/// Our own type, wrapping an `Intercept`, since we may have a few additional
90/// pieces attached thanks to `reqwest`s extra proxy configuration.
91pub(crate) struct Intercepted {
92    inner: matcher::Intercept,
93    /// This is because of `reqwest::Proxy`'s design which allows configuring
94    /// an explicit auth, besides what might have been in the URL (or Custom).
95    extra: Extra,
96}
97
98/*
99impl ProxyScheme {
100    fn maybe_http_auth(&self) -> Option<&HeaderValue> {
101        match self {
102            ProxyScheme::Http { auth, .. } | ProxyScheme::Https { auth, .. } => auth.as_ref(),
103            #[cfg(feature = "socks")]
104            _ => None,
105        }
106    }
107
108    fn maybe_http_custom_headers(&self) -> Option<&HeaderMap> {
109        match self {
110            ProxyScheme::Http { misc, .. } | ProxyScheme::Https { misc, .. } => misc.as_ref(),
111            #[cfg(feature = "socks")]
112            _ => None,
113        }
114    }
115}
116*/
117
118/// Trait used for converting into a proxy scheme. This trait supports
119/// parsing from a URL-like type, whilst also supporting proxy schemes
120/// built directly using the factory methods.
121pub trait IntoProxy {
122    fn into_proxy(self) -> crate::Result<Url>;
123}
124
125impl<S: IntoUrl> IntoProxy for S {
126    fn into_proxy(self) -> crate::Result<Url> {
127        match self.as_str().into_url() {
128            Ok(mut url) => {
129                // If the scheme is a SOCKS protocol and no port is specified, set the default
130                if url.port().is_none()
131                    && matches!(url.scheme(), "socks4" | "socks4a" | "socks5" | "socks5h")
132                {
133                    let _ = url.set_port(Some(1080));
134                }
135                Ok(url)
136            }
137            Err(e) => {
138                let mut presumed_to_have_scheme = true;
139                let mut source = e.source();
140                while let Some(err) = source {
141                    if let Some(parse_error) = err.downcast_ref::<url::ParseError>() {
142                        if *parse_error == url::ParseError::RelativeUrlWithoutBase {
143                            presumed_to_have_scheme = false;
144                            break;
145                        }
146                    } else if err.downcast_ref::<crate::error::BadScheme>().is_some() {
147                        presumed_to_have_scheme = false;
148                        break;
149                    }
150                    source = err.source();
151                }
152                if presumed_to_have_scheme {
153                    return Err(crate::error::builder(e));
154                }
155                // the issue could have been caused by a missing scheme, so we try adding http://
156                let try_this = format!("http://{}", self.as_str());
157                try_this.into_url().map_err(|_| {
158                    // return the original error
159                    crate::error::builder(e)
160                })
161            }
162        }
163    }
164}
165
166// These bounds are accidentally leaked by the blanket impl of IntoProxy
167// for all types that implement IntoUrl. So, this function exists to detect
168// if we were to break those bounds for a user.
169fn _implied_bounds() {
170    fn prox<T: IntoProxy>(_t: T) {}
171
172    fn url<T: IntoUrl>(t: T) {
173        prox(t);
174    }
175}
176
177impl Proxy {
178    /// Proxy all HTTP traffic to the passed URL.
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// # extern crate reqwest;
184    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
185    /// let client = reqwest::Client::builder()
186    ///     .proxy(reqwest::Proxy::http("https://my.prox")?)
187    ///     .build()?;
188    /// # Ok(())
189    /// # }
190    /// # fn main() {}
191    /// ```
192    pub fn http<U: IntoProxy>(proxy_scheme: U) -> crate::Result<Proxy> {
193        Ok(Proxy::new(Intercept::Http(proxy_scheme.into_proxy()?)))
194    }
195
196    /// Proxy all HTTPS traffic to the passed URL.
197    ///
198    /// # Example
199    ///
200    /// ```
201    /// # extern crate reqwest;
202    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
203    /// let client = reqwest::Client::builder()
204    ///     .proxy(reqwest::Proxy::https("https://example.prox:4545")?)
205    ///     .build()?;
206    /// # Ok(())
207    /// # }
208    /// # fn main() {}
209    /// ```
210    pub fn https<U: IntoProxy>(proxy_scheme: U) -> crate::Result<Proxy> {
211        Ok(Proxy::new(Intercept::Https(proxy_scheme.into_proxy()?)))
212    }
213
214    /// Proxy **all** traffic to the passed URL.
215    ///
216    /// "All" refers to `https` and `http` URLs. Other schemes are not
217    /// recognized by reqwest.
218    ///
219    /// # Example
220    ///
221    /// ```
222    /// # extern crate reqwest;
223    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
224    /// let client = reqwest::Client::builder()
225    ///     .proxy(reqwest::Proxy::all("http://pro.xy")?)
226    ///     .build()?;
227    /// # Ok(())
228    /// # }
229    /// # fn main() {}
230    /// ```
231    pub fn all<U: IntoProxy>(proxy_scheme: U) -> crate::Result<Proxy> {
232        Ok(Proxy::new(Intercept::All(proxy_scheme.into_proxy()?)))
233    }
234
235    /// Provide a custom function to determine what traffic to proxy to where.
236    ///
237    /// # Example
238    ///
239    /// ```
240    /// # extern crate reqwest;
241    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
242    /// let target = reqwest::Url::parse("https://my.prox")?;
243    /// let client = reqwest::Client::builder()
244    ///     .proxy(reqwest::Proxy::custom(move |url| {
245    ///         if url.host_str() == Some("hyper.rs") {
246    ///             Some(target.clone())
247    ///         } else {
248    ///             None
249    ///         }
250    ///     }))
251    ///     .build()?;
252    /// # Ok(())
253    /// # }
254    /// # fn main() {}
255    /// ```
256    pub fn custom<F, U: IntoProxy>(fun: F) -> Proxy
257    where
258        F: Fn(&Url) -> Option<U> + Send + Sync + 'static,
259    {
260        Proxy::new(Intercept::Custom(Custom {
261            func: Arc::new(move |url| fun(url).map(IntoProxy::into_proxy)),
262            no_proxy: None,
263        }))
264    }
265
266    fn new(intercept: Intercept) -> Proxy {
267        Proxy {
268            extra: Extra {
269                auth: None,
270                misc: None,
271            },
272            intercept,
273            no_proxy: None,
274        }
275    }
276
277    /// Set the `Proxy-Authorization` header using Basic auth.
278    ///
279    /// # Example
280    ///
281    /// ```
282    /// # extern crate reqwest;
283    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
284    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
285    ///     .basic_auth("Aladdin", "open sesame");
286    /// # Ok(())
287    /// # }
288    /// # fn main() {}
289    /// ```
290    pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
291        match self.intercept {
292            Intercept::All(ref mut s)
293            | Intercept::Http(ref mut s)
294            | Intercept::Https(ref mut s) => url_auth(s, username, password),
295            Intercept::Custom(_) => {
296                let header = encode_basic_auth(username, password);
297                self.extra.auth = Some(header);
298            }
299        }
300
301        self
302    }
303
304    /// Set the `Proxy-Authorization` header to a specified value.
305    ///
306    /// # Example
307    ///
308    /// ```
309    /// # extern crate reqwest;
310    /// # use reqwest::header::*;
311    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
312    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
313    ///     .custom_http_auth(HeaderValue::from_static("justletmeinalreadyplease"));
314    /// # Ok(())
315    /// # }
316    /// # fn main() {}
317    /// ```
318    pub fn custom_http_auth(mut self, header_value: HeaderValue) -> Proxy {
319        self.extra.auth = Some(header_value);
320        self
321    }
322
323    /// Adds a Custom Headers to Proxy
324    /// Adds custom headers to this Proxy
325    ///
326    /// # Example
327    /// ```
328    /// # extern crate reqwest;
329    /// # use reqwest::header::*;
330    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
331    /// let mut headers = HeaderMap::new();
332    /// headers.insert(USER_AGENT, "reqwest".parse().unwrap());
333    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
334    ///     .headers(headers);
335    /// # Ok(())
336    /// # }
337    /// # fn main() {}
338    /// ```
339    pub fn headers(mut self, headers: HeaderMap) -> Proxy {
340        match self.intercept {
341            Intercept::All(_) | Intercept::Http(_) | Intercept::Https(_) | Intercept::Custom(_) => {
342                self.extra.misc = Some(headers);
343            }
344        }
345
346        self
347    }
348
349    /// Adds a `No Proxy` exclusion list to this Proxy
350    ///
351    /// # Example
352    ///
353    /// ```
354    /// # extern crate reqwest;
355    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
356    /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
357    ///     .no_proxy(reqwest::NoProxy::from_string("direct.tld, sub.direct2.tld"));
358    /// # Ok(())
359    /// # }
360    /// # fn main() {}
361    /// ```
362    pub fn no_proxy(mut self, no_proxy: Option<NoProxy>) -> Proxy {
363        self.no_proxy = no_proxy;
364        self
365    }
366
367    pub(crate) fn into_matcher(self) -> Matcher {
368        let Proxy {
369            intercept,
370            extra,
371            no_proxy,
372        } = self;
373
374        let maybe_has_http_auth;
375        let maybe_has_http_custom_headers;
376
377        let inner = match intercept {
378            Intercept::All(url) => {
379                maybe_has_http_auth = cache_maybe_has_http_auth(&url, &extra.auth);
380                maybe_has_http_custom_headers =
381                    cache_maybe_has_http_custom_headers(&url, &extra.misc);
382                Matcher_::Util(
383                    matcher::Matcher::builder()
384                        .all(String::from(url))
385                        .no(no_proxy.as_ref().map(|n| n.inner.as_ref()).unwrap_or(""))
386                        .build(),
387                )
388            }
389            Intercept::Http(url) => {
390                maybe_has_http_auth = cache_maybe_has_http_auth(&url, &extra.auth);
391                maybe_has_http_custom_headers =
392                    cache_maybe_has_http_custom_headers(&url, &extra.misc);
393                Matcher_::Util(
394                    matcher::Matcher::builder()
395                        .http(String::from(url))
396                        .no(no_proxy.as_ref().map(|n| n.inner.as_ref()).unwrap_or(""))
397                        .build(),
398                )
399            }
400            Intercept::Https(url) => {
401                maybe_has_http_auth = cache_maybe_has_http_auth(&url, &extra.auth);
402                maybe_has_http_custom_headers =
403                    cache_maybe_has_http_custom_headers(&url, &extra.misc);
404                Matcher_::Util(
405                    matcher::Matcher::builder()
406                        .https(String::from(url))
407                        .no(no_proxy.as_ref().map(|n| n.inner.as_ref()).unwrap_or(""))
408                        .build(),
409                )
410            }
411            Intercept::Custom(mut custom) => {
412                maybe_has_http_auth = true; // never know
413                maybe_has_http_custom_headers = true;
414                custom.no_proxy = no_proxy;
415                Matcher_::Custom(custom)
416            }
417        };
418
419        Matcher {
420            inner,
421            extra,
422            maybe_has_http_auth,
423            maybe_has_http_custom_headers,
424        }
425    }
426
427    /*
428    pub(crate) fn maybe_has_http_auth(&self) -> bool {
429        match &self.intercept {
430            Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().is_some(),
431            // Custom *may* match 'http', so assume so.
432            Intercept::Custom(_) => true,
433            Intercept::System(system) => system
434                .get("http")
435                .and_then(|s| s.maybe_http_auth())
436                .is_some(),
437            Intercept::Https(_) => false,
438        }
439    }
440
441    pub(crate) fn http_basic_auth<D: Dst>(&self, uri: &D) -> Option<HeaderValue> {
442        match &self.intercept {
443            Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().cloned(),
444            Intercept::System(system) => system
445                .get("http")
446                .and_then(|s| s.maybe_http_auth().cloned()),
447            Intercept::Custom(custom) => {
448                custom.call(uri).and_then(|s| s.maybe_http_auth().cloned())
449            }
450            Intercept::Https(_) => None,
451        }
452    }
453    */
454}
455
456fn cache_maybe_has_http_auth(url: &Url, extra: &Option<HeaderValue>) -> bool {
457    (url.scheme() == "http" || url.scheme() == "https")
458        && (!url.username().is_empty() || url.password().is_some() || extra.is_some())
459}
460
461fn cache_maybe_has_http_custom_headers(url: &Url, extra: &Option<HeaderMap>) -> bool {
462    (url.scheme() == "http" || url.scheme() == "https") && extra.is_some()
463}
464
465impl fmt::Debug for Proxy {
466    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
467        f.debug_tuple("Proxy")
468            .field(&self.intercept)
469            .field(&self.no_proxy)
470            .finish()
471    }
472}
473
474impl NoProxy {
475    /// Returns a new no-proxy configuration based on environment variables (or `None` if no variables are set)
476    /// see [self::NoProxy::from_string()] for the string format
477    pub fn from_env() -> Option<NoProxy> {
478        let raw = std::env::var("NO_PROXY")
479            .or_else(|_| std::env::var("no_proxy"))
480            .ok()?;
481
482        // Per the docs, this returns `None` if no environment variable is set. We can only reach
483        // here if an env var is set, so we return `Some(NoProxy::default)` if `from_string`
484        // returns None, which occurs with an empty string.
485        Some(Self::from_string(&raw).unwrap_or_default())
486    }
487
488    /// Returns a new no-proxy configuration based on a `no_proxy` string (or `None` if no variables
489    /// are set)
490    /// The rules are as follows:
491    /// * The environment variable `NO_PROXY` is checked, if it is not set, `no_proxy` is checked
492    /// * If neither environment variable is set, `None` is returned
493    /// * Entries are expected to be comma-separated (whitespace between entries is ignored)
494    /// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size,
495    ///   for example "`192.168.1.0/24`").
496    /// * An entry "`*`" matches all hostnames (this is the only wildcard allowed)
497    /// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com`
498    ///   and `.google.com` are equivalent) and would match both that domain AND all subdomains.
499    ///
500    /// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all the following would match
501    /// (and therefore would bypass the proxy):
502    /// * `http://google.com/`
503    /// * `http://www.google.com/`
504    /// * `http://192.168.1.42/`
505    ///
506    /// The URL `http://notgoogle.com/` would not match.
507    pub fn from_string(no_proxy_list: &str) -> Option<Self> {
508        // lazy parsed, to not make the type public in hyper-util
509        Some(NoProxy {
510            inner: no_proxy_list.into(),
511        })
512    }
513}
514
515impl Matcher {
516    pub(crate) fn system() -> Self {
517        Self {
518            inner: Matcher_::Util(matcher::Matcher::from_system()),
519            extra: Extra {
520                auth: None,
521                misc: None,
522            },
523            // maybe env vars have auth!
524            maybe_has_http_auth: true,
525            maybe_has_http_custom_headers: true,
526        }
527    }
528
529    pub(crate) fn intercept(&self, dst: &Uri) -> Option<Intercepted> {
530        let inner = match self.inner {
531            Matcher_::Util(ref m) => m.intercept(dst),
532            Matcher_::Custom(ref c) => c.call(dst),
533        };
534
535        inner.map(|inner| Intercepted {
536            inner,
537            extra: self.extra.clone(),
538        })
539    }
540
541    /// Return whether this matcher might provide HTTP (not s) auth.
542    ///
543    /// This is very specific. If this proxy needs auth to be part of a Forward
544    /// request (instead of a tunnel), this should return true.
545    ///
546    /// If it's not sure, this should return true.
547    ///
548    /// This is meant as a hint to allow skipping a more expensive check
549    /// (calling `intercept()`) if it will never need auth when Forwarding.
550    pub(crate) fn maybe_has_http_auth(&self) -> bool {
551        self.maybe_has_http_auth
552    }
553
554    pub(crate) fn http_non_tunnel_basic_auth(&self, dst: &Uri) -> Option<HeaderValue> {
555        if let Some(proxy) = self.intercept(dst) {
556            let scheme = proxy.uri().scheme();
557            if scheme == Some(&Scheme::HTTP) || scheme == Some(&Scheme::HTTPS) {
558                return proxy.basic_auth().cloned();
559            }
560        }
561
562        None
563    }
564
565    pub(crate) fn maybe_has_http_custom_headers(&self) -> bool {
566        self.maybe_has_http_custom_headers
567    }
568
569    pub(crate) fn http_non_tunnel_custom_headers(&self, dst: &Uri) -> Option<HeaderMap> {
570        if let Some(proxy) = self.intercept(dst) {
571            let scheme = proxy.uri().scheme();
572            if scheme == Some(&Scheme::HTTP) || scheme == Some(&Scheme::HTTPS) {
573                return proxy.custom_headers().cloned();
574            }
575        }
576
577        None
578    }
579}
580
581impl fmt::Debug for Matcher {
582    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
583        match self.inner {
584            Matcher_::Util(ref m) => m.fmt(f),
585            Matcher_::Custom(ref m) => m.fmt(f),
586        }
587    }
588}
589
590impl Intercepted {
591    pub(crate) fn uri(&self) -> &http::Uri {
592        self.inner.uri()
593    }
594
595    pub(crate) fn basic_auth(&self) -> Option<&HeaderValue> {
596        if let Some(ref val) = self.extra.auth {
597            return Some(val);
598        }
599        self.inner.basic_auth()
600    }
601
602    pub(crate) fn custom_headers(&self) -> Option<&HeaderMap> {
603        if let Some(ref val) = self.extra.misc {
604            return Some(val);
605        }
606        None
607    }
608
609    #[cfg(feature = "socks")]
610    pub(crate) fn raw_auth(&self) -> Option<(&str, &str)> {
611        self.inner.raw_auth()
612    }
613}
614
615impl fmt::Debug for Intercepted {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        self.inner.uri().fmt(f)
618    }
619}
620
621/*
622impl ProxyScheme {
623    /// Use a username and password when connecting to the proxy server
624    fn with_basic_auth<T: Into<String>, U: Into<String>>(
625        mut self,
626        username: T,
627        password: U,
628    ) -> Self {
629        self.set_basic_auth(username, password);
630        self
631    }
632
633    fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
634        match *self {
635            ProxyScheme::Http { ref mut auth, .. } => {
636                let header = encode_basic_auth(&username.into(), &password.into());
637                *auth = Some(header);
638            }
639            ProxyScheme::Https { ref mut auth, .. } => {
640                let header = encode_basic_auth(&username.into(), &password.into());
641                *auth = Some(header);
642            }
643            #[cfg(feature = "socks")]
644            ProxyScheme::Socks4 { .. } => {
645                panic!("Socks4 is not supported for this method")
646            }
647            #[cfg(feature = "socks")]
648            ProxyScheme::Socks5 { ref mut auth, .. } => {
649                *auth = Some((username.into(), password.into()));
650            }
651        }
652    }
653
654    fn set_custom_http_auth(&mut self, header_value: HeaderValue) {
655        match *self {
656            ProxyScheme::Http { ref mut auth, .. } => {
657                *auth = Some(header_value);
658            }
659            ProxyScheme::Https { ref mut auth, .. } => {
660                *auth = Some(header_value);
661            }
662            #[cfg(feature = "socks")]
663            ProxyScheme::Socks4 { .. } => {
664                panic!("Socks4 is not supported for this method")
665            }
666            #[cfg(feature = "socks")]
667            ProxyScheme::Socks5 { .. } => {
668                panic!("Socks5 is not supported for this method")
669            }
670        }
671    }
672
673    fn set_custom_headers(&mut self, headers: HeaderMap) {
674        match *self {
675            ProxyScheme::Http { ref mut misc, .. } => {
676                misc.get_or_insert_with(HeaderMap::new).extend(headers)
677            }
678            ProxyScheme::Https { ref mut misc, .. } => {
679                misc.get_or_insert_with(HeaderMap::new).extend(headers)
680            }
681            #[cfg(feature = "socks")]
682            ProxyScheme::Socks4 { .. } => {
683                panic!("Socks4 is not supported for this method")
684            }
685            #[cfg(feature = "socks")]
686            ProxyScheme::Socks5 { .. } => {
687                panic!("Socks5 is not supported for this method")
688            }
689        }
690    }
691
692    fn if_no_auth(mut self, update: &Option<HeaderValue>) -> Self {
693        match self {
694            ProxyScheme::Http { ref mut auth, .. } => {
695                if auth.is_none() {
696                    *auth = update.clone();
697                }
698            }
699            ProxyScheme::Https { ref mut auth, .. } => {
700                if auth.is_none() {
701                    *auth = update.clone();
702                }
703            }
704            #[cfg(feature = "socks")]
705            ProxyScheme::Socks4 { .. } => {}
706            #[cfg(feature = "socks")]
707            ProxyScheme::Socks5 { .. } => {}
708        }
709
710        self
711    }
712
713    /// Convert a URL into a proxy scheme
714    ///
715    /// Supported schemes: HTTP, HTTPS, (SOCKS4, SOCKS5, SOCKS5H if `socks` feature is enabled).
716    // Private for now...
717    fn parse(url: Url) -> crate::Result<Self> {
718        use url::Position;
719
720        // Resolve URL to a host and port
721        #[cfg(feature = "socks")]
722        let to_addr = || {
723            let addrs = url
724                .socket_addrs(|| match url.scheme() {
725                    "socks4" | "socks4a" | "socks5" | "socks5h" => Some(1080),
726                    _ => None,
727                })
728                .map_err(crate::error::builder)?;
729            addrs
730                .into_iter()
731                .next()
732                .ok_or_else(|| crate::error::builder("unknown proxy scheme"))
733        };
734
735        let mut scheme = match url.scheme() {
736            "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
737            "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
738            #[cfg(feature = "socks")]
739            "socks4" => Self::socks4(to_addr()?)?,
740            #[cfg(feature = "socks")]
741            "socks4a" => Self::socks4a(to_addr()?)?,
742            #[cfg(feature = "socks")]
743            "socks5" => Self::socks5(to_addr()?)?,
744            #[cfg(feature = "socks")]
745            "socks5h" => Self::socks5h(to_addr()?)?,
746            _ => return Err(crate::error::builder("unknown proxy scheme")),
747        };
748
749        if let Some(pwd) = url.password() {
750            let decoded_username = percent_decode(url.username().as_bytes()).decode_utf8_lossy();
751            let decoded_password = percent_decode(pwd.as_bytes()).decode_utf8_lossy();
752            scheme = scheme.with_basic_auth(decoded_username, decoded_password);
753        }
754
755        Ok(scheme)
756    }
757}
758*/
759
760#[derive(Clone, Debug)]
761enum Intercept {
762    All(Url),
763    Http(Url),
764    Https(Url),
765    Custom(Custom),
766}
767
768fn url_auth(url: &mut Url, username: &str, password: &str) {
769    url.set_username(username).expect("is a base");
770    url.set_password(Some(password)).expect("is a base");
771}
772
773type CustomProxyFn = dyn Fn(&Url) -> Option<crate::Result<Url>> + Send + Sync + 'static;
774
775#[derive(Clone)]
776struct Custom {
777    func: Arc<CustomProxyFn>,
778    no_proxy: Option<NoProxy>,
779}
780
781impl Custom {
782    fn call(&self, uri: &http::Uri) -> Option<matcher::Intercept> {
783        let url = format!(
784            "{}://{}{}{}",
785            uri.scheme()?,
786            uri.host()?,
787            uri.port().map_or("", |_| ":"),
788            uri.port().map_or(String::new(), |p| p.to_string())
789        )
790        .parse()
791        .expect("should be valid Url");
792
793        (self.func)(&url)
794            .and_then(|result| result.ok())
795            .and_then(|target| {
796                let m = matcher::Matcher::builder()
797                    .all(String::from(target))
798                    .build();
799
800                m.intercept(uri)
801            })
802        //.map(|scheme| scheme.if_no_auth(&self.auth))
803    }
804}
805
806impl fmt::Debug for Custom {
807    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
808        f.write_str("_")
809    }
810}
811
812pub(crate) fn encode_basic_auth(username: &str, password: &str) -> HeaderValue {
813    crate::util::basic_auth(username, Some(password))
814}
815
816#[cfg(test)]
817mod tests {
818    use super::*;
819
820    fn url(s: &str) -> http::Uri {
821        s.parse().unwrap()
822    }
823
824    fn intercepted_uri(p: &Matcher, s: &str) -> Uri {
825        p.intercept(&s.parse().unwrap()).unwrap().uri().clone()
826    }
827
828    #[test]
829    fn test_http() {
830        let target = "http://example.domain/";
831        let p = Proxy::http(target).unwrap().into_matcher();
832
833        let http = "http://hyper.rs";
834        let other = "https://hyper.rs";
835
836        assert_eq!(intercepted_uri(&p, http), target);
837        assert!(p.intercept(&url(other)).is_none());
838    }
839
840    #[test]
841    fn test_https() {
842        let target = "http://example.domain/";
843        let p = Proxy::https(target).unwrap().into_matcher();
844
845        let http = "http://hyper.rs";
846        let other = "https://hyper.rs";
847
848        assert!(p.intercept(&url(http)).is_none());
849        assert_eq!(intercepted_uri(&p, other), target);
850    }
851
852    #[test]
853    fn test_all() {
854        let target = "http://example.domain/";
855        let p = Proxy::all(target).unwrap().into_matcher();
856
857        let http = "http://hyper.rs";
858        let https = "https://hyper.rs";
859        // no longer supported
860        //let other = "x-youve-never-heard-of-me-mr-proxy://hyper.rs";
861
862        assert_eq!(intercepted_uri(&p, http), target);
863        assert_eq!(intercepted_uri(&p, https), target);
864        //assert_eq!(intercepted_uri(&p, other), target);
865    }
866
867    #[test]
868    fn test_custom() {
869        let target1 = "http://example.domain/";
870        let target2 = "https://example.domain/";
871        let p = Proxy::custom(move |url| {
872            if url.host_str() == Some("hyper.rs") {
873                target1.parse().ok()
874            } else if url.scheme() == "http" {
875                target2.parse().ok()
876            } else {
877                None::<Url>
878            }
879        })
880        .into_matcher();
881
882        let http = "http://seanmonstar.com";
883        let https = "https://hyper.rs";
884        let other = "x-youve-never-heard-of-me-mr-proxy://seanmonstar.com";
885
886        assert_eq!(intercepted_uri(&p, http), target2);
887        assert_eq!(intercepted_uri(&p, https), target1);
888        assert!(p.intercept(&url(other)).is_none());
889    }
890
891    #[test]
892    fn test_standard_with_custom_auth_header() {
893        let target = "http://example.domain/";
894        let p = Proxy::all(target)
895            .unwrap()
896            .custom_http_auth(http::HeaderValue::from_static("testme"))
897            .into_matcher();
898
899        let got = p.intercept(&url("http://anywhere.local")).unwrap();
900        let auth = got.basic_auth().unwrap();
901        assert_eq!(auth, "testme");
902    }
903
904    #[test]
905    fn test_custom_with_custom_auth_header() {
906        let target = "http://example.domain/";
907        let p = Proxy::custom(move |_| target.parse::<Url>().ok())
908            .custom_http_auth(http::HeaderValue::from_static("testme"))
909            .into_matcher();
910
911        let got = p.intercept(&url("http://anywhere.local")).unwrap();
912        let auth = got.basic_auth().unwrap();
913        assert_eq!(auth, "testme");
914    }
915
916    #[test]
917    fn test_maybe_has_http_auth() {
918        let m = Proxy::all("https://letme:in@yo.local")
919            .unwrap()
920            .into_matcher();
921        assert!(m.maybe_has_http_auth(), "https forwards");
922
923        let m = Proxy::all("http://letme:in@yo.local")
924            .unwrap()
925            .into_matcher();
926        assert!(m.maybe_has_http_auth(), "http forwards");
927
928        let m = Proxy::all("http://:in@yo.local").unwrap().into_matcher();
929        assert!(m.maybe_has_http_auth(), "http forwards with empty username");
930
931        let m = Proxy::all("http://letme:@yo.local").unwrap().into_matcher();
932        assert!(m.maybe_has_http_auth(), "http forwards with empty password");
933    }
934
935    #[test]
936    fn test_socks_proxy_default_port() {
937        {
938            let m = Proxy::all("socks5://example.com").unwrap().into_matcher();
939
940            let http = "http://hyper.rs";
941            let https = "https://hyper.rs";
942
943            assert_eq!(intercepted_uri(&m, http).port_u16(), Some(1080));
944            assert_eq!(intercepted_uri(&m, https).port_u16(), Some(1080));
945
946            // custom port
947            let m = Proxy::all("socks5://example.com:1234")
948                .unwrap()
949                .into_matcher();
950
951            assert_eq!(intercepted_uri(&m, http).port_u16(), Some(1234));
952            assert_eq!(intercepted_uri(&m, https).port_u16(), Some(1234));
953        }
954    }
955}