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