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