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}