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 maybe_has_http_custom_headers(&self) -> bool {
554 self.maybe_has_http_custom_headers
555 }
556}
557
558impl fmt::Debug for Matcher {
559 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560 match self.inner {
561 Matcher_::Util(ref m) => m.fmt(f),
562 Matcher_::Custom(ref m) => m.fmt(f),
563 }
564 }
565}
566
567impl Intercepted {
568 pub(crate) fn http_non_tunnel_basic_auth(&self) -> Option<HeaderValue> {
569 let scheme = self.uri().scheme();
570 if scheme == Some(&Scheme::HTTP) || scheme == Some(&Scheme::HTTPS) {
571 return self.basic_auth().cloned();
572 }
573
574 None
575 }
576
577 pub(crate) fn http_non_tunnel_custom_headers(&self) -> Option<HeaderMap> {
578 let scheme = self.uri().scheme();
579 if scheme == Some(&Scheme::HTTP) || scheme == Some(&Scheme::HTTPS) {
580 return self.custom_headers().cloned();
581 }
582
583 None
584 }
585
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 forwards");
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 let m = Proxy::all("http://:in@yo.local").unwrap().into_matcher();
922 assert!(m.maybe_has_http_auth(), "http forwards with empty username");
923
924 let m = Proxy::all("http://letme:@yo.local").unwrap().into_matcher();
925 assert!(m.maybe_has_http_auth(), "http forwards with empty password");
926 }
927
928 #[test]
929 fn test_socks_proxy_default_port() {
930 {
931 let m = Proxy::all("socks5://example.com").unwrap().into_matcher();
932
933 let http = "http://hyper.rs";
934 let https = "https://hyper.rs";
935
936 assert_eq!(intercepted_uri(&m, http).port_u16(), Some(1080));
937 assert_eq!(intercepted_uri(&m, https).port_u16(), Some(1080));
938
939 // custom port
940 let m = Proxy::all("socks5://example.com:1234")
941 .unwrap()
942 .into_matcher();
943
944 assert_eq!(intercepted_uri(&m, http).port_u16(), Some(1234));
945 assert_eq!(intercepted_uri(&m, https).port_u16(), Some(1234));
946 }
947 }
948}