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