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