reqwest/
proxy.rs

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