1use std::fmt;
2#[cfg(feature = "socks")]
3use std::net::SocketAddr;
4use std::sync::Arc;
5
6use crate::into_url::{IntoUrl, IntoUrlSealed};
7use crate::Url;
8use http::{header::HeaderValue, Uri};
9use ipnet::IpNet;
10use once_cell::sync::Lazy;
11use percent_encoding::percent_decode;
12use std::collections::HashMap;
13use std::env;
14use std::error::Error;
15use std::net::IpAddr;
16#[cfg(target_os = "macos")]
17use system_configuration::{
18 core_foundation::{
19 base::CFType,
20 dictionary::CFDictionary,
21 number::CFNumber,
22 string::{CFString, CFStringRef},
23 },
24 dynamic_store::SCDynamicStoreBuilder,
25 sys::schema_definitions::kSCPropNetProxiesHTTPEnable,
26 sys::schema_definitions::kSCPropNetProxiesHTTPPort,
27 sys::schema_definitions::kSCPropNetProxiesHTTPProxy,
28 sys::schema_definitions::kSCPropNetProxiesHTTPSEnable,
29 sys::schema_definitions::kSCPropNetProxiesHTTPSPort,
30 sys::schema_definitions::kSCPropNetProxiesHTTPSProxy,
31};
32#[cfg(target_os = "windows")]
33use winreg::enums::HKEY_CURRENT_USER;
34#[cfg(target_os = "windows")]
35use winreg::RegKey;
36
37/// Configuration of a proxy that a `Client` should pass requests to.
38///
39/// A `Proxy` has a couple pieces to it:
40///
41/// - a URL of how to talk to the proxy
42/// - rules on what `Client` requests should be directed to the proxy
43///
44/// For instance, let's look at `Proxy::http`:
45///
46/// ```rust
47/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
48/// let proxy = reqwest::Proxy::http("https://secure.example")?;
49/// # Ok(())
50/// # }
51/// ```
52///
53/// This proxy will intercept all HTTP requests, and make use of the proxy
54/// at `https://secure.example`. A request to `http://hyper.rs` will talk
55/// to your proxy. A request to `https://hyper.rs` will not.
56///
57/// Multiple `Proxy` rules can be configured for a `Client`. The `Client` will
58/// check each `Proxy` in the order it was added. This could mean that a
59/// `Proxy` added first with eager intercept rules, such as `Proxy::all`,
60/// would prevent a `Proxy` later in the list from ever working, so take care.
61///
62/// By enabling the `"socks"` feature it is possible to use a socks proxy:
63/// ```rust
64/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
65/// let proxy = reqwest::Proxy::http("socks5://192.168.1.1:9000")?;
66/// # Ok(())
67/// # }
68/// ```
69#[derive(Clone)]
70pub struct Proxy {
71 intercept: Intercept,
72 no_proxy: Option<NoProxy>,
73}
74
75/// Represents a possible matching entry for an IP address
76#[derive(Clone, Debug)]
77enum Ip {
78 Address(IpAddr),
79 Network(IpNet),
80}
81
82/// A wrapper around a list of IP cidr blocks or addresses with a [IpMatcher::contains] method for
83/// checking if an IP address is contained within the matcher
84#[derive(Clone, Debug, Default)]
85struct IpMatcher(Vec<Ip>);
86
87/// A wrapper around a list of domains with a [DomainMatcher::contains] method for checking if a
88/// domain is contained within the matcher
89#[derive(Clone, Debug, Default)]
90struct DomainMatcher(Vec<String>);
91
92/// A configuration for filtering out requests that shouldn't be proxied
93#[derive(Clone, Debug, Default)]
94pub struct NoProxy {
95 ips: IpMatcher,
96 domains: DomainMatcher,
97}
98
99/// A particular scheme used for proxying requests.
100///
101/// For example, HTTP vs SOCKS5
102#[derive(Clone)]
103pub enum ProxyScheme {
104 Http {
105 auth: Option<HeaderValue>,
106 host: http::uri::Authority,
107 },
108 Https {
109 auth: Option<HeaderValue>,
110 host: http::uri::Authority,
111 },
112 #[cfg(feature = "socks")]
113 Socks5 {
114 addr: SocketAddr,
115 auth: Option<(String, String)>,
116 remote_dns: bool,
117 },
118}
119
120impl ProxyScheme {
121 fn maybe_http_auth(&self) -> Option<&HeaderValue> {
122 match self {
123 ProxyScheme::Http { auth: &Option, .. } | ProxyScheme::Https { auth: &Option, .. } => auth.as_ref(),
124 #[cfg(feature = "socks")]
125 _ => None,
126 }
127 }
128}
129
130/// Trait used for converting into a proxy scheme. This trait supports
131/// parsing from a URL-like type, whilst also supporting proxy schemes
132/// built directly using the factory methods.
133pub trait IntoProxyScheme {
134 fn into_proxy_scheme(self) -> crate::Result<ProxyScheme>;
135}
136
137impl<S: IntoUrl> IntoProxyScheme for S {
138 fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> {
139 // validate the URL
140 let url = match self.as_str().into_url() {
141 Ok(ok) => ok,
142 Err(e) => {
143 let mut presumed_to_have_scheme = true;
144 let mut source = e.source();
145 while let Some(err) = source {
146 if let Some(parse_error) = err.downcast_ref::<url::ParseError>() {
147 match parse_error {
148 url::ParseError::RelativeUrlWithoutBase => {
149 presumed_to_have_scheme = false;
150 break;
151 }
152 _ => {}
153 }
154 } else if let Some(_) = err.downcast_ref::<crate::error::BadScheme>() {
155 presumed_to_have_scheme = false;
156 break;
157 }
158 source = err.source();
159 }
160 if presumed_to_have_scheme {
161 return Err(crate::error::builder(e));
162 }
163 // the issue could have been caused by a missing scheme, so we try adding http://
164 let try_this = format!("http://{}", self.as_str());
165 try_this.into_url().map_err(|_| {
166 // return the original error
167 crate::error::builder(e)
168 })?
169 }
170 };
171 ProxyScheme::parse(url)
172 }
173}
174
175// These bounds are accidentally leaked by the blanket impl of IntoProxyScheme
176// for all types that implement IntoUrl. So, this function exists to detect
177// if we were to break those bounds for a user.
178fn _implied_bounds() {
179 fn prox<T: IntoProxyScheme>(_t: T) {}
180
181 fn url<T: IntoUrl>(t: T) {
182 prox(t);
183 }
184}
185
186impl IntoProxyScheme for ProxyScheme {
187 fn into_proxy_scheme(self) -> crate::Result<ProxyScheme> {
188 Ok(self)
189 }
190}
191
192impl Proxy {
193 /// Proxy all HTTP traffic to the passed URL.
194 ///
195 /// # Example
196 ///
197 /// ```
198 /// # extern crate reqwest;
199 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
200 /// let client = reqwest::Client::builder()
201 /// .proxy(reqwest::Proxy::http("https://my.prox")?)
202 /// .build()?;
203 /// # Ok(())
204 /// # }
205 /// # fn main() {}
206 /// ```
207 pub fn http<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
208 Ok(Proxy::new(Intercept::Http(
209 proxy_scheme.into_proxy_scheme()?,
210 )))
211 }
212
213 /// Proxy all HTTPS traffic to the passed URL.
214 ///
215 /// # Example
216 ///
217 /// ```
218 /// # extern crate reqwest;
219 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
220 /// let client = reqwest::Client::builder()
221 /// .proxy(reqwest::Proxy::https("https://example.prox:4545")?)
222 /// .build()?;
223 /// # Ok(())
224 /// # }
225 /// # fn main() {}
226 /// ```
227 pub fn https<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
228 Ok(Proxy::new(Intercept::Https(
229 proxy_scheme.into_proxy_scheme()?,
230 )))
231 }
232
233 /// Proxy **all** traffic to the passed URL.
234 ///
235 /// # Example
236 ///
237 /// ```
238 /// # extern crate reqwest;
239 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
240 /// let client = reqwest::Client::builder()
241 /// .proxy(reqwest::Proxy::all("http://pro.xy")?)
242 /// .build()?;
243 /// # Ok(())
244 /// # }
245 /// # fn main() {}
246 /// ```
247 pub fn all<U: IntoProxyScheme>(proxy_scheme: U) -> crate::Result<Proxy> {
248 Ok(Proxy::new(Intercept::All(
249 proxy_scheme.into_proxy_scheme()?,
250 )))
251 }
252
253 /// Provide a custom function to determine what traffic to proxy to where.
254 ///
255 /// # Example
256 ///
257 /// ```
258 /// # extern crate reqwest;
259 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
260 /// let target = reqwest::Url::parse("https://my.prox")?;
261 /// let client = reqwest::Client::builder()
262 /// .proxy(reqwest::Proxy::custom(move |url| {
263 /// if url.host_str() == Some("hyper.rs") {
264 /// Some(target.clone())
265 /// } else {
266 /// None
267 /// }
268 /// }))
269 /// .build()?;
270 /// # Ok(())
271 /// # }
272 /// # fn main() {}
273 /// ```
274 pub fn custom<F, U: IntoProxyScheme>(fun: F) -> Proxy
275 where
276 F: Fn(&Url) -> Option<U> + Send + Sync + 'static,
277 {
278 Proxy::new(Intercept::Custom(Custom {
279 auth: None,
280 func: Arc::new(move |url| fun(url).map(IntoProxyScheme::into_proxy_scheme)),
281 }))
282 }
283
284 pub(crate) fn system() -> Proxy {
285 let mut proxy = if cfg!(feature = "__internal_proxy_sys_no_cache") {
286 Proxy::new(Intercept::System(Arc::new(get_sys_proxies(
287 get_from_platform(),
288 ))))
289 } else {
290 Proxy::new(Intercept::System(SYS_PROXIES.clone()))
291 };
292 proxy.no_proxy = NoProxy::from_env();
293 proxy
294 }
295
296 fn new(intercept: Intercept) -> Proxy {
297 Proxy {
298 intercept,
299 no_proxy: None,
300 }
301 }
302
303 /// Set the `Proxy-Authorization` header using Basic auth.
304 ///
305 /// # Example
306 ///
307 /// ```
308 /// # extern crate reqwest;
309 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
310 /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
311 /// .basic_auth("Aladdin", "open sesame");
312 /// # Ok(())
313 /// # }
314 /// # fn main() {}
315 /// ```
316 pub fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
317 self.intercept.set_basic_auth(username, password);
318 self
319 }
320
321 /// Set the `Proxy-Authorization` header to a specified value.
322 ///
323 /// # Example
324 ///
325 /// ```
326 /// # extern crate reqwest;
327 /// # use reqwest::header::*;
328 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
329 /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
330 /// .custom_http_auth(HeaderValue::from_static("justletmeinalreadyplease"));
331 /// # Ok(())
332 /// # }
333 /// # fn main() {}
334 /// ```
335 pub fn custom_http_auth(mut self, header_value: HeaderValue) -> Proxy {
336 self.intercept.set_custom_http_auth(header_value);
337 self
338 }
339
340 /// Adds a `No Proxy` exclusion list to this Proxy
341 ///
342 /// # Example
343 ///
344 /// ```
345 /// # extern crate reqwest;
346 /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
347 /// let proxy = reqwest::Proxy::https("http://localhost:1234")?
348 /// .no_proxy(reqwest::NoProxy::from_string("direct.tld, sub.direct2.tld"));
349 /// # Ok(())
350 /// # }
351 /// # fn main() {}
352 /// ```
353 pub fn no_proxy(mut self, no_proxy: Option<NoProxy>) -> Proxy {
354 self.no_proxy = no_proxy;
355 self
356 }
357
358 pub(crate) fn maybe_has_http_auth(&self) -> bool {
359 match &self.intercept {
360 Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().is_some(),
361 // Custom *may* match 'http', so assume so.
362 Intercept::Custom(_) => true,
363 Intercept::System(system) => system
364 .get("http")
365 .and_then(|s| s.maybe_http_auth())
366 .is_some(),
367 Intercept::Https(_) => false,
368 }
369 }
370
371 pub(crate) fn http_basic_auth<D: Dst>(&self, uri: &D) -> Option<HeaderValue> {
372 match &self.intercept {
373 Intercept::All(p) | Intercept::Http(p) => p.maybe_http_auth().cloned(),
374 Intercept::System(system) => system
375 .get("http")
376 .and_then(|s| s.maybe_http_auth().cloned()),
377 Intercept::Custom(custom) => {
378 custom.call(uri).and_then(|s| s.maybe_http_auth().cloned())
379 }
380 Intercept::Https(_) => None,
381 }
382 }
383
384 pub(crate) fn intercept<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> {
385 let in_no_proxy = self
386 .no_proxy
387 .as_ref()
388 .map_or(false, |np| np.contains(uri.host()));
389 match self.intercept {
390 Intercept::All(ref u) => {
391 if !in_no_proxy {
392 Some(u.clone())
393 } else {
394 None
395 }
396 }
397 Intercept::Http(ref u) => {
398 if !in_no_proxy && uri.scheme() == "http" {
399 Some(u.clone())
400 } else {
401 None
402 }
403 }
404 Intercept::Https(ref u) => {
405 if !in_no_proxy && uri.scheme() == "https" {
406 Some(u.clone())
407 } else {
408 None
409 }
410 }
411 Intercept::System(ref map) => {
412 if in_no_proxy {
413 None
414 } else {
415 map.get(uri.scheme()).cloned()
416 }
417 }
418 Intercept::Custom(ref custom) => {
419 if !in_no_proxy {
420 custom.call(uri)
421 } else {
422 None
423 }
424 }
425 }
426 }
427
428 pub(crate) fn is_match<D: Dst>(&self, uri: &D) -> bool {
429 match self.intercept {
430 Intercept::All(_) => true,
431 Intercept::Http(_) => uri.scheme() == "http",
432 Intercept::Https(_) => uri.scheme() == "https",
433 Intercept::System(ref map) => map.contains_key(uri.scheme()),
434 Intercept::Custom(ref custom) => custom.call(uri).is_some(),
435 }
436 }
437}
438
439impl fmt::Debug for Proxy {
440 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
441 f&mut DebugTuple<'_, '_>.debug_tuple(name:"Proxy")
442 .field(&self.intercept)
443 .field(&self.no_proxy)
444 .finish()
445 }
446}
447
448impl NoProxy {
449 /// Returns a new no-proxy configuration based on environment variables (or `None` if no variables are set)
450 /// see [self::NoProxy::from_string()] for the string format
451 pub fn from_env() -> Option<NoProxy> {
452 let raw = env::var("NO_PROXY")
453 .or_else(|_| env::var("no_proxy"))
454 .unwrap_or_default();
455
456 Self::from_string(&raw)
457 }
458
459 /// Returns a new no-proxy configuration based on a `no_proxy` string (or `None` if no variables
460 /// are set)
461 /// The rules are as follows:
462 /// * The environment variable `NO_PROXY` is checked, if it is not set, `no_proxy` is checked
463 /// * If neither environment variable is set, `None` is returned
464 /// * Entries are expected to be comma-separated (whitespace between entries is ignored)
465 /// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size,
466 /// for example "`192.168.1.0/24`").
467 /// * An entry "`*`" matches all hostnames (this is the only wildcard allowed)
468 /// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com`
469 /// and `.google.com` are equivalent) and would match both that domain AND all subdomains.
470 ///
471 /// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all of the following would match
472 /// (and therefore would bypass the proxy):
473 /// * `http://google.com/`
474 /// * `http://www.google.com/`
475 /// * `http://192.168.1.42/`
476 ///
477 /// The URL `http://notgoogle.com/` would not match.
478 pub fn from_string(no_proxy_list: &str) -> Option<Self> {
479 if no_proxy_list.is_empty() {
480 return None;
481 }
482 let mut ips = Vec::new();
483 let mut domains = Vec::new();
484 let parts = no_proxy_list.split(',').map(str::trim);
485 for part in parts {
486 match part.parse::<IpNet>() {
487 // If we can parse an IP net or address, then use it, otherwise, assume it is a domain
488 Ok(ip) => ips.push(Ip::Network(ip)),
489 Err(_) => match part.parse::<IpAddr>() {
490 Ok(addr) => ips.push(Ip::Address(addr)),
491 Err(_) => domains.push(part.to_owned()),
492 },
493 }
494 }
495 Some(NoProxy {
496 ips: IpMatcher(ips),
497 domains: DomainMatcher(domains),
498 })
499 }
500
501 fn contains(&self, host: &str) -> bool {
502 // According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off
503 // the end in order to parse correctly
504 let host = if host.starts_with('[') {
505 let x: &[_] = &['[', ']'];
506 host.trim_matches(x)
507 } else {
508 host
509 };
510 match host.parse::<IpAddr>() {
511 // If we can parse an IP addr, then use it, otherwise, assume it is a domain
512 Ok(ip) => self.ips.contains(ip),
513 Err(_) => self.domains.contains(host),
514 }
515 }
516}
517
518impl IpMatcher {
519 fn contains(&self, addr: IpAddr) -> bool {
520 for ip: &Ip in &self.0 {
521 match ip {
522 Ip::Address(address: &IpAddr) => {
523 if &addr == address {
524 return true;
525 }
526 }
527 Ip::Network(net: &IpNet) => {
528 if net.contains(&addr) {
529 return true;
530 }
531 }
532 }
533 }
534 false
535 }
536}
537
538impl DomainMatcher {
539 // The following links may be useful to understand the origin of these rules:
540 // * https://curl.se/libcurl/c/CURLOPT_NOPROXY.html
541 // * https://github.com/curl/curl/issues/1208
542 fn contains(&self, domain: &str) -> bool {
543 let domain_len = domain.len();
544 for d in &self.0 {
545 if d == domain || d.strip_prefix('.') == Some(domain) {
546 return true;
547 } else if domain.ends_with(d) {
548 if d.starts_with('.') {
549 // If the first character of d is a dot, that means the first character of domain
550 // must also be a dot, so we are looking at a subdomain of d and that matches
551 return true;
552 } else if domain.as_bytes().get(domain_len - d.len() - 1) == Some(&b'.') {
553 // Given that d is a prefix of domain, if the prior character in domain is a dot
554 // then that means we must be matching a subdomain of d, and that matches
555 return true;
556 }
557 } else if d == "*" {
558 return true;
559 }
560 }
561 false
562 }
563}
564
565impl ProxyScheme {
566 // To start conservative, keep builders private for now.
567
568 /// Proxy traffic via the specified URL over HTTP
569 fn http(host: &str) -> crate::Result<Self> {
570 Ok(ProxyScheme::Http {
571 auth: None,
572 host: host.parse().map_err(crate::error::builder)?,
573 })
574 }
575
576 /// Proxy traffic via the specified URL over HTTPS
577 fn https(host: &str) -> crate::Result<Self> {
578 Ok(ProxyScheme::Https {
579 auth: None,
580 host: host.parse().map_err(crate::error::builder)?,
581 })
582 }
583
584 /// Proxy traffic via the specified socket address over SOCKS5
585 ///
586 /// # Note
587 ///
588 /// Current SOCKS5 support is provided via blocking IO.
589 #[cfg(feature = "socks")]
590 fn socks5(addr: SocketAddr) -> crate::Result<Self> {
591 Ok(ProxyScheme::Socks5 {
592 addr,
593 auth: None,
594 remote_dns: false,
595 })
596 }
597
598 /// Proxy traffic via the specified socket address over SOCKS5H
599 ///
600 /// This differs from SOCKS5 in that DNS resolution is also performed via the proxy.
601 ///
602 /// # Note
603 ///
604 /// Current SOCKS5 support is provided via blocking IO.
605 #[cfg(feature = "socks")]
606 fn socks5h(addr: SocketAddr) -> crate::Result<Self> {
607 Ok(ProxyScheme::Socks5 {
608 addr,
609 auth: None,
610 remote_dns: true,
611 })
612 }
613
614 /// Use a username and password when connecting to the proxy server
615 fn with_basic_auth<T: Into<String>, U: Into<String>>(
616 mut self,
617 username: T,
618 password: U,
619 ) -> Self {
620 self.set_basic_auth(username, password);
621 self
622 }
623
624 fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
625 match *self {
626 ProxyScheme::Http { ref mut auth, .. } => {
627 let header = encode_basic_auth(&username.into(), &password.into());
628 *auth = Some(header);
629 }
630 ProxyScheme::Https { ref mut auth, .. } => {
631 let header = encode_basic_auth(&username.into(), &password.into());
632 *auth = Some(header);
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::Socks5 { .. } => {
651 panic!("Socks is not supported for this method")
652 }
653 }
654 }
655
656 fn if_no_auth(mut self, update: &Option<HeaderValue>) -> Self {
657 match self {
658 ProxyScheme::Http { ref mut auth, .. } => {
659 if auth.is_none() {
660 *auth = update.clone();
661 }
662 }
663 ProxyScheme::Https { ref mut auth, .. } => {
664 if auth.is_none() {
665 *auth = update.clone();
666 }
667 }
668 #[cfg(feature = "socks")]
669 ProxyScheme::Socks5 { .. } => {}
670 }
671
672 self
673 }
674
675 /// Convert a URL into a proxy scheme
676 ///
677 /// Supported schemes: HTTP, HTTPS, (SOCKS5, SOCKS5H if `socks` feature is enabled).
678 // Private for now...
679 fn parse(url: Url) -> crate::Result<Self> {
680 use url::Position;
681
682 // Resolve URL to a host and port
683 #[cfg(feature = "socks")]
684 let to_addr = || {
685 let addrs = url
686 .socket_addrs(|| match url.scheme() {
687 "socks5" | "socks5h" => Some(1080),
688 _ => None,
689 })
690 .map_err(crate::error::builder)?;
691 addrs
692 .into_iter()
693 .next()
694 .ok_or_else(|| crate::error::builder("unknown proxy scheme"))
695 };
696
697 let mut scheme = match url.scheme() {
698 "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
699 "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
700 #[cfg(feature = "socks")]
701 "socks5" => Self::socks5(to_addr()?)?,
702 #[cfg(feature = "socks")]
703 "socks5h" => Self::socks5h(to_addr()?)?,
704 _ => return Err(crate::error::builder("unknown proxy scheme")),
705 };
706
707 if let Some(pwd) = url.password() {
708 let decoded_username = percent_decode(url.username().as_bytes()).decode_utf8_lossy();
709 let decoded_password = percent_decode(pwd.as_bytes()).decode_utf8_lossy();
710 scheme = scheme.with_basic_auth(decoded_username, decoded_password);
711 }
712
713 Ok(scheme)
714 }
715
716 #[cfg(test)]
717 fn scheme(&self) -> &str {
718 match self {
719 ProxyScheme::Http { .. } => "http",
720 ProxyScheme::Https { .. } => "https",
721 #[cfg(feature = "socks")]
722 ProxyScheme::Socks5 { .. } => "socks5",
723 }
724 }
725
726 #[cfg(test)]
727 fn host(&self) -> &str {
728 match self {
729 ProxyScheme::Http { host, .. } => host.as_str(),
730 ProxyScheme::Https { host, .. } => host.as_str(),
731 #[cfg(feature = "socks")]
732 ProxyScheme::Socks5 { .. } => panic!("socks5"),
733 }
734 }
735}
736
737impl fmt::Debug for ProxyScheme {
738 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
739 match self {
740 ProxyScheme::Http { auth: _auth: &Option, host: &Authority } => write!(f, "http://{host}"),
741 ProxyScheme::Https { auth: _auth: &Option, host: &Authority } => write!(f, "https://{host}"),
742 #[cfg(feature = "socks")]
743 ProxyScheme::Socks5 {
744 addr,
745 auth: _auth,
746 remote_dns,
747 } => {
748 let h = if *remote_dns { "h" } else { "" };
749 write!(f, "socks5{h}://{addr}")
750 }
751 }
752 }
753}
754
755type SystemProxyMap = HashMap<String, ProxyScheme>;
756
757#[derive(Clone, Debug)]
758enum Intercept {
759 All(ProxyScheme),
760 Http(ProxyScheme),
761 Https(ProxyScheme),
762 System(Arc<SystemProxyMap>),
763 Custom(Custom),
764}
765
766impl Intercept {
767 fn set_basic_auth(&mut self, username: &str, password: &str) {
768 match self {
769 Intercept::All(ref mut s)
770 | Intercept::Http(ref mut s)
771 | Intercept::Https(ref mut s) => s.set_basic_auth(username, password),
772 Intercept::System(_) => unimplemented!(),
773 Intercept::Custom(ref mut custom) => {
774 let header = encode_basic_auth(username, password);
775 custom.auth = Some(header);
776 }
777 }
778 }
779
780 fn set_custom_http_auth(&mut self, header_value: HeaderValue) {
781 match self {
782 Intercept::All(ref mut s)
783 | Intercept::Http(ref mut s)
784 | Intercept::Https(ref mut s) => s.set_custom_http_auth(header_value),
785 Intercept::System(_) => unimplemented!(),
786 Intercept::Custom(ref mut custom) => {
787 custom.auth = Some(header_value);
788 }
789 }
790 }
791}
792
793#[derive(Clone)]
794struct Custom {
795 // This auth only applies if the returned ProxyScheme doesn't have an auth...
796 auth: Option<HeaderValue>,
797 func: Arc<dyn Fn(&Url) -> Option<crate::Result<ProxyScheme>> + Send + Sync + 'static>,
798}
799
800impl Custom {
801 fn call<D: Dst>(&self, uri: &D) -> Option<ProxyScheme> {
802 let url: Url = format!(
803 "{}://{}{}{}",
804 uri.scheme(),
805 uri.host(),
806 uri.port().map_or("", |_| ":"),
807 uri.port().map_or(String::new(), |p| p.to_string())
808 )
809 .parse()
810 .expect(msg:"should be valid Url");
811
812 (self.func)(&url)
813 .and_then(|result: Result| result.ok())
814 .map(|scheme: ProxyScheme| scheme.if_no_auth(&self.auth))
815 }
816}
817
818impl fmt::Debug for Custom {
819 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
820 f.write_str(data:"_")
821 }
822}
823
824pub(crate) fn encode_basic_auth(username: &str, password: &str) -> HeaderValue {
825 crate::util::basic_auth(username, password:Some(password))
826}
827
828/// A helper trait to allow testing `Proxy::intercept` without having to
829/// construct `hyper::client::connect::Destination`s.
830pub(crate) trait Dst {
831 fn scheme(&self) -> &str;
832 fn host(&self) -> &str;
833 fn port(&self) -> Option<u16>;
834}
835
836#[doc(hidden)]
837impl Dst for Uri {
838 fn scheme(&self) -> &str {
839 self.scheme().expect(msg:"Uri should have a scheme").as_str()
840 }
841
842 fn host(&self) -> &str {
843 Uri::host(self).expect(msg:"<Uri as Dst>::host should have a str")
844 }
845
846 fn port(&self) -> Option<u16> {
847 self.port().map(|p: Port<&str>| p.as_u16())
848 }
849}
850
851static SYS_PROXIES: Lazy<Arc<SystemProxyMap>> =
852 Lazy::new(|| Arc::new(data:get_sys_proxies(platform_proxies:get_from_platform())));
853
854/// Get system proxies information.
855///
856/// All platforms will check for proxy settings via environment variables.
857/// If those aren't set, platform-wide proxy settings will be looked up on
858/// Windows and MacOS platforms instead. Errors encountered while discovering
859/// these settings are ignored.
860///
861/// Returns:
862/// System proxies information as a hashmap like
863/// {"http": Url::parse("http://127.0.0.1:80"), "https": Url::parse("https://127.0.0.1:80")}
864fn get_sys_proxies(
865 #[cfg_attr(
866 not(any(target_os = "windows", target_os = "macos")),
867 allow(unused_variables)
868 )]
869 platform_proxies: Option<String>,
870) -> SystemProxyMap {
871 let proxies: HashMap = get_from_environment();
872
873 #[cfg(any(target_os = "windows", target_os = "macos"))]
874 if proxies.is_empty() {
875 // if there are errors in acquiring the platform proxies,
876 // we'll just return an empty HashMap
877 if let Some(platform_proxies) = platform_proxies {
878 return parse_platform_values(platform_proxies);
879 }
880 }
881
882 proxies
883}
884
885fn insert_proxy(proxies: &mut SystemProxyMap, scheme: impl Into<String>, addr: String) -> bool {
886 if addr.trim().is_empty() {
887 // do not accept empty or whitespace proxy address
888 false
889 } else if let Ok(valid_addr: ProxyScheme) = addr.into_proxy_scheme() {
890 proxies.insert(k:scheme.into(), v:valid_addr);
891 true
892 } else {
893 false
894 }
895}
896
897fn get_from_environment() -> SystemProxyMap {
898 let mut proxies: HashMap = HashMap::new();
899
900 if is_cgi() {
901 if log::log_enabled!(log::Level::Warn) && env::var_os(key:"HTTP_PROXY").is_some() {
902 log::warn!("HTTP_PROXY environment variable ignored in CGI");
903 }
904 } else if !insert_from_env(&mut proxies, scheme:"http", var:"HTTP_PROXY") {
905 insert_from_env(&mut proxies, scheme:"http", var:"http_proxy");
906 }
907
908 if !insert_from_env(&mut proxies, scheme:"https", var:"HTTPS_PROXY") {
909 insert_from_env(&mut proxies, scheme:"https", var:"https_proxy");
910 }
911
912 if !(insert_from_env(&mut proxies, scheme:"http", var:"ALL_PROXY")
913 && insert_from_env(&mut proxies, scheme:"https", var:"ALL_PROXY"))
914 {
915 insert_from_env(&mut proxies, scheme:"http", var:"all_proxy");
916 insert_from_env(&mut proxies, scheme:"https", var:"all_proxy");
917 }
918
919 proxies
920}
921
922fn insert_from_env(proxies: &mut SystemProxyMap, scheme: &str, var: &str) -> bool {
923 if let Ok(val: String) = env::var(key:var) {
924 insert_proxy(proxies, scheme, addr:val)
925 } else {
926 false
927 }
928}
929
930/// Check if we are being executed in a CGI context.
931///
932/// If so, a malicious client can send the `Proxy:` header, and it will
933/// be in the `HTTP_PROXY` env var. So we don't use it :)
934fn is_cgi() -> bool {
935 env::var_os(key:"REQUEST_METHOD").is_some()
936}
937
938#[cfg(target_os = "windows")]
939fn get_from_platform_impl() -> Result<Option<String>, Box<dyn Error>> {
940 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
941 let internet_setting: RegKey =
942 hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")?;
943 // ensure the proxy is enable, if the value doesn't exist, an error will returned.
944 let proxy_enable: u32 = internet_setting.get_value("ProxyEnable")?;
945 let proxy_server: String = internet_setting.get_value("ProxyServer")?;
946
947 Ok((proxy_enable == 1).then_some(proxy_server))
948}
949
950#[cfg(target_os = "macos")]
951fn parse_setting_from_dynamic_store(
952 proxies_map: &CFDictionary<CFString, CFType>,
953 enabled_key: CFStringRef,
954 host_key: CFStringRef,
955 port_key: CFStringRef,
956 scheme: &str,
957) -> Option<String> {
958 let proxy_enabled = proxies_map
959 .find(enabled_key)
960 .and_then(|flag| flag.downcast::<CFNumber>())
961 .and_then(|flag| flag.to_i32())
962 .unwrap_or(0)
963 == 1;
964
965 if proxy_enabled {
966 let proxy_host = proxies_map
967 .find(host_key)
968 .and_then(|host| host.downcast::<CFString>())
969 .map(|host| host.to_string());
970 let proxy_port = proxies_map
971 .find(port_key)
972 .and_then(|port| port.downcast::<CFNumber>())
973 .and_then(|port| port.to_i32());
974
975 return match (proxy_host, proxy_port) {
976 (Some(proxy_host), Some(proxy_port)) => {
977 Some(format!("{scheme}={proxy_host}:{proxy_port}"))
978 }
979 (Some(proxy_host), None) => Some(format!("{scheme}={proxy_host}")),
980 (None, Some(_)) => None,
981 (None, None) => None,
982 };
983 }
984
985 None
986}
987
988#[cfg(target_os = "macos")]
989fn get_from_platform_impl() -> Result<Option<String>, Box<dyn Error>> {
990 let store = SCDynamicStoreBuilder::new("reqwest").build();
991
992 let proxies_map = if let Some(proxies_map) = store.get_proxies() {
993 proxies_map
994 } else {
995 return Ok(None);
996 };
997
998 let http_proxy_config = parse_setting_from_dynamic_store(
999 &proxies_map,
1000 unsafe { kSCPropNetProxiesHTTPEnable },
1001 unsafe { kSCPropNetProxiesHTTPProxy },
1002 unsafe { kSCPropNetProxiesHTTPPort },
1003 "http",
1004 );
1005 let https_proxy_config = parse_setting_from_dynamic_store(
1006 &proxies_map,
1007 unsafe { kSCPropNetProxiesHTTPSEnable },
1008 unsafe { kSCPropNetProxiesHTTPSProxy },
1009 unsafe { kSCPropNetProxiesHTTPSPort },
1010 "https",
1011 );
1012
1013 match http_proxy_config.as_ref().zip(https_proxy_config.as_ref()) {
1014 Some((http_config, https_config)) => Ok(Some(format!("{http_config};{https_config}"))),
1015 None => Ok(http_proxy_config.or(https_proxy_config)),
1016 }
1017}
1018
1019#[cfg(any(target_os = "windows", target_os = "macos"))]
1020fn get_from_platform() -> Option<String> {
1021 get_from_platform_impl().ok().flatten()
1022}
1023
1024#[cfg(not(any(target_os = "windows", target_os = "macos")))]
1025fn get_from_platform() -> Option<String> {
1026 None
1027}
1028
1029#[cfg(any(target_os = "windows", target_os = "macos"))]
1030fn parse_platform_values_impl(platform_values: String) -> SystemProxyMap {
1031 let mut proxies = HashMap::new();
1032 if platform_values.contains("=") {
1033 // per-protocol settings.
1034 for p in platform_values.split(";") {
1035 let protocol_parts: Vec<&str> = p.split("=").collect();
1036 match protocol_parts.as_slice() {
1037 [protocol, address] => {
1038 // If address doesn't specify an explicit protocol as protocol://address
1039 // then default to HTTP
1040 let address = if extract_type_prefix(*address).is_some() {
1041 String::from(*address)
1042 } else {
1043 format!("http://{address}")
1044 };
1045
1046 insert_proxy(&mut proxies, *protocol, address);
1047 }
1048 _ => {
1049 // Contains invalid protocol setting, just break the loop
1050 // And make proxies to be empty.
1051 proxies.clear();
1052 break;
1053 }
1054 }
1055 }
1056 } else {
1057 if let Some(scheme) = extract_type_prefix(&platform_values) {
1058 // Explicit protocol has been specified
1059 insert_proxy(&mut proxies, scheme, platform_values.to_owned());
1060 } else {
1061 // No explicit protocol has been specified, default to HTTP
1062 insert_proxy(&mut proxies, "http", format!("http://{platform_values}"));
1063 insert_proxy(&mut proxies, "https", format!("http://{platform_values}"));
1064 }
1065 }
1066 proxies
1067}
1068
1069/// Extract the protocol from the given address, if present
1070/// For example, "https://example.com" will return Some("https")
1071#[cfg(any(target_os = "windows", target_os = "macos"))]
1072fn extract_type_prefix(address: &str) -> Option<&str> {
1073 if let Some(indice) = address.find("://") {
1074 if indice == 0 {
1075 None
1076 } else {
1077 let prefix = &address[..indice];
1078 let contains_banned = prefix.contains(|c| c == ':' || c == '/');
1079
1080 if !contains_banned {
1081 Some(prefix)
1082 } else {
1083 None
1084 }
1085 }
1086 } else {
1087 None
1088 }
1089}
1090
1091#[cfg(any(target_os = "windows", target_os = "macos"))]
1092fn parse_platform_values(platform_values: String) -> SystemProxyMap {
1093 parse_platform_values_impl(platform_values)
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098 use super::*;
1099 use once_cell::sync::Lazy;
1100 use std::sync::Mutex;
1101
1102 impl Dst for Url {
1103 fn scheme(&self) -> &str {
1104 Url::scheme(self)
1105 }
1106
1107 fn host(&self) -> &str {
1108 Url::host_str(self).expect("<Url as Dst>::host should have a str")
1109 }
1110
1111 fn port(&self) -> Option<u16> {
1112 Url::port(self)
1113 }
1114 }
1115
1116 fn url(s: &str) -> Url {
1117 s.parse().unwrap()
1118 }
1119
1120 fn intercepted_uri(p: &Proxy, s: &str) -> Uri {
1121 let (scheme, host) = match p.intercept(&url(s)).unwrap() {
1122 ProxyScheme::Http { host, .. } => ("http", host),
1123 ProxyScheme::Https { host, .. } => ("https", host),
1124 #[cfg(feature = "socks")]
1125 _ => panic!("intercepted as socks"),
1126 };
1127 http::Uri::builder()
1128 .scheme(scheme)
1129 .authority(host)
1130 .path_and_query("/")
1131 .build()
1132 .expect("intercepted_uri")
1133 }
1134
1135 #[test]
1136 fn test_http() {
1137 let target = "http://example.domain/";
1138 let p = Proxy::http(target).unwrap();
1139
1140 let http = "http://hyper.rs";
1141 let other = "https://hyper.rs";
1142
1143 assert_eq!(intercepted_uri(&p, http), target);
1144 assert!(p.intercept(&url(other)).is_none());
1145 }
1146
1147 #[test]
1148 fn test_https() {
1149 let target = "http://example.domain/";
1150 let p = Proxy::https(target).unwrap();
1151
1152 let http = "http://hyper.rs";
1153 let other = "https://hyper.rs";
1154
1155 assert!(p.intercept(&url(http)).is_none());
1156 assert_eq!(intercepted_uri(&p, other), target);
1157 }
1158
1159 #[test]
1160 fn test_all() {
1161 let target = "http://example.domain/";
1162 let p = Proxy::all(target).unwrap();
1163
1164 let http = "http://hyper.rs";
1165 let https = "https://hyper.rs";
1166 let other = "x-youve-never-heard-of-me-mr-proxy://hyper.rs";
1167
1168 assert_eq!(intercepted_uri(&p, http), target);
1169 assert_eq!(intercepted_uri(&p, https), target);
1170 assert_eq!(intercepted_uri(&p, other), target);
1171 }
1172
1173 #[test]
1174 fn test_custom() {
1175 let target1 = "http://example.domain/";
1176 let target2 = "https://example.domain/";
1177 let p = Proxy::custom(move |url| {
1178 if url.host_str() == Some("hyper.rs") {
1179 target1.parse().ok()
1180 } else if url.scheme() == "http" {
1181 target2.parse().ok()
1182 } else {
1183 None::<Url>
1184 }
1185 });
1186
1187 let http = "http://seanmonstar.com";
1188 let https = "https://hyper.rs";
1189 let other = "x-youve-never-heard-of-me-mr-proxy://seanmonstar.com";
1190
1191 assert_eq!(intercepted_uri(&p, http), target2);
1192 assert_eq!(intercepted_uri(&p, https), target1);
1193 assert!(p.intercept(&url(other)).is_none());
1194 }
1195
1196 #[test]
1197 fn test_proxy_scheme_parse() {
1198 let ps = "http://foo:bar@localhost:1239".into_proxy_scheme().unwrap();
1199
1200 match ps {
1201 ProxyScheme::Http { auth, host } => {
1202 assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar"));
1203 assert_eq!(host, "localhost:1239");
1204 }
1205 other => panic!("unexpected: {other:?}"),
1206 }
1207 }
1208
1209 #[test]
1210 fn test_proxy_scheme_ip_address_default_http() {
1211 let ps = "192.168.1.1:8888".into_proxy_scheme().unwrap();
1212
1213 match ps {
1214 ProxyScheme::Http { auth, host } => {
1215 assert!(auth.is_none());
1216 assert_eq!(host, "192.168.1.1:8888");
1217 }
1218 other => panic!("unexpected: {other:?}"),
1219 }
1220 }
1221
1222 #[test]
1223 fn test_proxy_scheme_parse_default_http_with_auth() {
1224 // this should fail because `foo` is interpreted as the scheme and no host can be found
1225 let ps = "foo:bar@localhost:1239".into_proxy_scheme().unwrap();
1226
1227 match ps {
1228 ProxyScheme::Http { auth, host } => {
1229 assert_eq!(auth.unwrap(), encode_basic_auth("foo", "bar"));
1230 assert_eq!(host, "localhost:1239");
1231 }
1232 other => panic!("unexpected: {other:?}"),
1233 }
1234 }
1235
1236 #[test]
1237 fn test_domain_matcher() {
1238 let domains = vec![".foo.bar".into(), "bar.foo".into()];
1239 let matcher = DomainMatcher(domains);
1240
1241 // domains match with leading `.`
1242 assert!(matcher.contains("foo.bar"));
1243 // subdomains match with leading `.`
1244 assert!(matcher.contains("www.foo.bar"));
1245
1246 // domains match with no leading `.`
1247 assert!(matcher.contains("bar.foo"));
1248 // subdomains match with no leading `.`
1249 assert!(matcher.contains("www.bar.foo"));
1250
1251 // non-subdomain string prefixes don't match
1252 assert!(!matcher.contains("notfoo.bar"));
1253 assert!(!matcher.contains("notbar.foo"));
1254 }
1255
1256 // Smallest possible content for a mutex
1257 struct MutexInner;
1258
1259 static ENVLOCK: Lazy<Mutex<MutexInner>> = Lazy::new(|| Mutex::new(MutexInner));
1260
1261 #[test]
1262 fn test_get_sys_proxies_parsing() {
1263 // Stop other threads from modifying process-global ENV while we are.
1264 let _lock = ENVLOCK.lock();
1265 // save system setting first.
1266 let _g1 = env_guard("HTTP_PROXY");
1267 let _g2 = env_guard("http_proxy");
1268 let _g3 = env_guard("ALL_PROXY");
1269
1270 // Mock ENV, get the results, before doing assertions
1271 // to avoid assert! -> panic! -> Mutex Poisoned.
1272 let baseline_proxies = get_sys_proxies(None);
1273 // the system proxy setting url is invalid.
1274 env::set_var("http_proxy", "file://123465");
1275 let invalid_proxies = get_sys_proxies(None);
1276 // set valid proxy
1277 env::set_var("http_proxy", "127.0.0.1/");
1278 let valid_proxies = get_sys_proxies(None);
1279 // set valid ALL_PROXY
1280 env::set_var("ALL_PROXY", "127.0.0.2/");
1281 let all_proxies = get_sys_proxies(None);
1282
1283 // reset user setting when guards drop
1284 drop(_g1);
1285 drop(_g2);
1286 // Let other threads run now
1287 drop(_lock);
1288
1289 assert!(!baseline_proxies.contains_key("http"));
1290 assert!(!invalid_proxies.contains_key("http"));
1291
1292 let p = &valid_proxies["http"];
1293 assert_eq!(p.scheme(), "http");
1294 assert_eq!(p.host(), "127.0.0.1");
1295
1296 assert_eq!(all_proxies.len(), 2);
1297 assert!(all_proxies.values().all(|p| p.host() == "127.0.0.2"));
1298 }
1299
1300 #[cfg(any(target_os = "windows", target_os = "macos"))]
1301 #[test]
1302 fn test_get_sys_proxies_registry_parsing() {
1303 // Stop other threads from modifying process-global ENV while we are.
1304 let _lock = ENVLOCK.lock();
1305 // save system setting first.
1306 let _g1 = env_guard("HTTP_PROXY");
1307 let _g2 = env_guard("http_proxy");
1308
1309 // Mock ENV, get the results, before doing assertions
1310 // to avoid assert! -> panic! -> Mutex Poisoned.
1311 let baseline_proxies = get_sys_proxies(None);
1312 // set valid proxy
1313 let valid_proxies = get_sys_proxies(Some(String::from("http://127.0.0.1/")));
1314 let valid_proxies_no_scheme = get_sys_proxies(Some(String::from("127.0.0.1")));
1315 let valid_proxies_explicit_https =
1316 get_sys_proxies(Some(String::from("https://127.0.0.1/")));
1317 let multiple_proxies = get_sys_proxies(Some(String::from(
1318 "http=127.0.0.1:8888;https=127.0.0.2:8888",
1319 )));
1320 let multiple_proxies_explicit_scheme = get_sys_proxies(Some(String::from(
1321 "http=http://127.0.0.1:8888;https=https://127.0.0.2:8888",
1322 )));
1323
1324 // reset user setting when guards drop
1325 drop(_g1);
1326 drop(_g2);
1327 // Let other threads run now
1328 drop(_lock);
1329
1330 assert_eq!(baseline_proxies.contains_key("http"), false);
1331
1332 let p = &valid_proxies["http"];
1333 assert_eq!(p.scheme(), "http");
1334 assert_eq!(p.host(), "127.0.0.1");
1335
1336 let p = &valid_proxies_no_scheme["http"];
1337 assert_eq!(p.scheme(), "http");
1338 assert_eq!(p.host(), "127.0.0.1");
1339
1340 let p = &valid_proxies_no_scheme["https"];
1341 assert_eq!(p.scheme(), "http");
1342 assert_eq!(p.host(), "127.0.0.1");
1343
1344 let p = &valid_proxies_explicit_https["https"];
1345 assert_eq!(p.scheme(), "https");
1346 assert_eq!(p.host(), "127.0.0.1");
1347
1348 let p = &multiple_proxies["http"];
1349 assert_eq!(p.scheme(), "http");
1350 assert_eq!(p.host(), "127.0.0.1:8888");
1351
1352 let p = &multiple_proxies["https"];
1353 assert_eq!(p.scheme(), "http");
1354 assert_eq!(p.host(), "127.0.0.2:8888");
1355
1356 let p = &multiple_proxies_explicit_scheme["http"];
1357 assert_eq!(p.scheme(), "http");
1358 assert_eq!(p.host(), "127.0.0.1:8888");
1359
1360 let p = &multiple_proxies_explicit_scheme["https"];
1361 assert_eq!(p.scheme(), "https");
1362 assert_eq!(p.host(), "127.0.0.2:8888");
1363 }
1364
1365 #[test]
1366 fn test_get_sys_proxies_in_cgi() {
1367 // Stop other threads from modifying process-global ENV while we are.
1368 let _lock = ENVLOCK.lock();
1369 // save system setting first.
1370 let _g1 = env_guard("REQUEST_METHOD");
1371 let _g2 = env_guard("HTTP_PROXY");
1372
1373 // Mock ENV, get the results, before doing assertions
1374 // to avoid assert! -> panic! -> Mutex Poisoned.
1375 env::set_var("HTTP_PROXY", "http://evil/");
1376
1377 let baseline_proxies = get_sys_proxies(None);
1378 // set like we're in CGI
1379 env::set_var("REQUEST_METHOD", "GET");
1380
1381 let cgi_proxies = get_sys_proxies(None);
1382
1383 // reset user setting when guards drop
1384 drop(_g1);
1385 drop(_g2);
1386 // Let other threads run now
1387 drop(_lock);
1388
1389 // not in CGI yet
1390 assert_eq!(baseline_proxies["http"].host(), "evil");
1391 // In CGI
1392 assert!(!cgi_proxies.contains_key("http"));
1393 }
1394
1395 #[test]
1396 fn test_sys_no_proxy() {
1397 // Stop other threads from modifying process-global ENV while we are.
1398 let _lock = ENVLOCK.lock();
1399 // save system setting first.
1400 let _g1 = env_guard("HTTP_PROXY");
1401 let _g2 = env_guard("NO_PROXY");
1402
1403 let target = "http://example.domain/";
1404 env::set_var("HTTP_PROXY", target);
1405
1406 env::set_var(
1407 "NO_PROXY",
1408 ".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17",
1409 );
1410
1411 // Manually construct this so we aren't use the cache
1412 let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1413 p.no_proxy = NoProxy::from_env();
1414
1415 // random url, not in no_proxy
1416 assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
1417 // make sure that random non-subdomain string prefixes don't match
1418 assert_eq!(intercepted_uri(&p, "http://notfoo.bar"), target);
1419 // make sure that random non-subdomain string prefixes don't match
1420 assert_eq!(intercepted_uri(&p, "http://notbar.baz"), target);
1421 // ipv4 address out of range
1422 assert_eq!(intercepted_uri(&p, "http://10.43.1.1"), target);
1423 // ipv4 address out of range
1424 assert_eq!(intercepted_uri(&p, "http://10.124.7.7"), target);
1425 // ipv6 address out of range
1426 assert_eq!(intercepted_uri(&p, "http://[ffff:db8:a0b:12f0::1]"), target);
1427 // ipv6 address out of range
1428 assert_eq!(intercepted_uri(&p, "http://[2005:db8:a0b:12f0::1]"), target);
1429
1430 // make sure subdomains (with leading .) match
1431 assert!(p.intercept(&url("http://hello.foo.bar")).is_none());
1432 // make sure exact matches (without leading .) match (also makes sure spaces between entries work)
1433 assert!(p.intercept(&url("http://bar.baz")).is_none());
1434 // check case sensitivity
1435 assert!(p.intercept(&url("http://BAR.baz")).is_none());
1436 // make sure subdomains (without leading . in no_proxy) match
1437 assert!(p.intercept(&url("http://foo.bar.baz")).is_none());
1438 // make sure subdomains (without leading . in no_proxy) match - this differs from cURL
1439 assert!(p.intercept(&url("http://foo.bar")).is_none());
1440 // ipv4 address match within range
1441 assert!(p.intercept(&url("http://10.42.1.100")).is_none());
1442 // ipv6 address exact match
1443 assert!(p.intercept(&url("http://[::1]")).is_none());
1444 // ipv6 address match within range
1445 assert!(p.intercept(&url("http://[2001:db8:a0b:12f0::1]")).is_none());
1446 // ipv4 address exact match
1447 assert!(p.intercept(&url("http://10.124.7.8")).is_none());
1448
1449 // reset user setting when guards drop
1450 drop(_g1);
1451 drop(_g2);
1452 // Let other threads run now
1453 drop(_lock);
1454 }
1455
1456 #[test]
1457 fn test_proxy_no_proxy_interception_for_proxy_types() {
1458 let proxy_url = "http://example.domain/";
1459 let no_proxy = ".no.proxy.tld";
1460
1461 // test all proxy interception
1462 let p = Proxy::all(proxy_url)
1463 .unwrap()
1464 .no_proxy(NoProxy::from_string(no_proxy));
1465
1466 // random url, not in no_proxy
1467 assert_eq!(intercepted_uri(&p, "http://hyper.rs"), proxy_url);
1468
1469 // positive match for no proxy
1470 assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none());
1471
1472 // test http proxy interception
1473 let p = Proxy::http(proxy_url)
1474 .unwrap()
1475 .no_proxy(NoProxy::from_string(no_proxy));
1476
1477 // random url, not in no_proxy
1478 assert_eq!(intercepted_uri(&p, "http://hyper.rs"), proxy_url);
1479
1480 // positive match for no proxy
1481 assert!(p.intercept(&url("http://hello.no.proxy.tld")).is_none());
1482
1483 // should not be intercepted due to scheme
1484 assert!(p.intercept(&url("https://hyper.rs")).is_none());
1485
1486 // test https proxy interception
1487 let p = Proxy::https(proxy_url)
1488 .unwrap()
1489 .no_proxy(NoProxy::from_string(no_proxy));
1490
1491 // random url, not in no_proxy
1492 assert_eq!(intercepted_uri(&p, "https://hyper.rs"), proxy_url);
1493
1494 // positive match for no proxy
1495 assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none());
1496
1497 // should not be intercepted due to scheme
1498 assert!(p.intercept(&url("http://hyper.rs")).is_none());
1499
1500 // test custom proxy interception
1501 let p = Proxy::custom(move |_url| Some(proxy_url)).no_proxy(NoProxy::from_string(no_proxy));
1502
1503 // random url, not in no_proxy
1504 assert_eq!(intercepted_uri(&p, "https://hyper.rs"), proxy_url);
1505
1506 // positive match for no proxy
1507 assert!(p.intercept(&url("https://hello.no.proxy.tld")).is_none());
1508 assert!(p.intercept(&url("http://hello.no.proxy.tld")).is_none());
1509 }
1510
1511 #[test]
1512 fn test_wildcard_sys_no_proxy() {
1513 // Stop other threads from modifying process-global ENV while we are.
1514 let _lock = ENVLOCK.lock();
1515 // save system setting first.
1516 let _g1 = env_guard("HTTP_PROXY");
1517 let _g2 = env_guard("NO_PROXY");
1518
1519 let target = "http://example.domain/";
1520 env::set_var("HTTP_PROXY", target);
1521
1522 env::set_var("NO_PROXY", "*");
1523
1524 // Manually construct this so we aren't use the cache
1525 let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1526 p.no_proxy = NoProxy::from_env();
1527
1528 assert!(p.intercept(&url("http://foo.bar")).is_none());
1529
1530 // reset user setting when guards drop
1531 drop(_g1);
1532 drop(_g2);
1533 // Let other threads run now
1534 drop(_lock);
1535 }
1536
1537 #[test]
1538 fn test_empty_sys_no_proxy() {
1539 // Stop other threads from modifying process-global ENV while we are.
1540 let _lock = ENVLOCK.lock();
1541 // save system setting first.
1542 let _g1 = env_guard("HTTP_PROXY");
1543 let _g2 = env_guard("NO_PROXY");
1544
1545 let target = "http://example.domain/";
1546 env::set_var("HTTP_PROXY", target);
1547
1548 env::set_var("NO_PROXY", ",");
1549
1550 // Manually construct this so we aren't use the cache
1551 let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1552 p.no_proxy = NoProxy::from_env();
1553
1554 // everything should go through proxy, "effectively" nothing is in no_proxy
1555 assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
1556
1557 // reset user setting when guards drop
1558 drop(_g1);
1559 drop(_g2);
1560 // Let other threads run now
1561 drop(_lock);
1562 }
1563
1564 #[test]
1565 fn test_no_proxy_load() {
1566 // Stop other threads from modifying process-global ENV while we are.
1567 let _lock = ENVLOCK.lock();
1568
1569 let _g1 = env_guard("no_proxy");
1570 let domain = "lower.case";
1571 env::set_var("no_proxy", domain);
1572 // Manually construct this so we aren't use the cache
1573 let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1574 p.no_proxy = NoProxy::from_env();
1575 assert_eq!(
1576 p.no_proxy.expect("should have a no proxy set").domains.0[0],
1577 domain
1578 );
1579
1580 env::remove_var("no_proxy");
1581 let _g2 = env_guard("NO_PROXY");
1582 let domain = "upper.case";
1583 env::set_var("NO_PROXY", domain);
1584 // Manually construct this so we aren't use the cache
1585 let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1586 p.no_proxy = NoProxy::from_env();
1587 assert_eq!(
1588 p.no_proxy.expect("should have a no proxy set").domains.0[0],
1589 domain
1590 );
1591
1592 let _g3 = env_guard("HTTP_PROXY");
1593 env::remove_var("NO_PROXY");
1594 env::remove_var("no_proxy");
1595 let target = "http://example.domain/";
1596 env::set_var("HTTP_PROXY", target);
1597
1598 // Manually construct this so we aren't use the cache
1599 let mut p = Proxy::new(Intercept::System(Arc::new(get_sys_proxies(None))));
1600 p.no_proxy = NoProxy::from_env();
1601 assert!(p.no_proxy.is_none(), "NoProxy shouldn't have been created");
1602
1603 assert_eq!(intercepted_uri(&p, "http://hyper.rs"), target);
1604
1605 // reset user setting when guards drop
1606 drop(_g1);
1607 drop(_g2);
1608 drop(_g3);
1609 // Let other threads run now
1610 drop(_lock);
1611 }
1612
1613 #[cfg(any(target_os = "windows", target_os = "macos"))]
1614 #[test]
1615 fn test_type_prefix_extraction() {
1616 assert!(extract_type_prefix("test").is_none());
1617 assert!(extract_type_prefix("://test").is_none());
1618 assert!(extract_type_prefix("some:prefix://test").is_none());
1619 assert!(extract_type_prefix("some/prefix://test").is_none());
1620
1621 assert_eq!(extract_type_prefix("http://test").unwrap(), "http");
1622 assert_eq!(extract_type_prefix("a://test").unwrap(), "a");
1623 }
1624
1625 /// Guard an environment variable, resetting it to the original value
1626 /// when dropped.
1627 fn env_guard(name: impl Into<String>) -> EnvGuard {
1628 let name = name.into();
1629 let orig_val = env::var(&name).ok();
1630 env::remove_var(&name);
1631 EnvGuard { name, orig_val }
1632 }
1633
1634 struct EnvGuard {
1635 name: String,
1636 orig_val: Option<String>,
1637 }
1638
1639 impl Drop for EnvGuard {
1640 fn drop(&mut self) {
1641 if let Some(val) = self.orig_val.take() {
1642 env::set_var(&self.name, val);
1643 } else {
1644 env::remove_var(&self.name);
1645 }
1646 }
1647 }
1648
1649 #[test]
1650 fn test_has_http_auth() {
1651 let http_proxy_with_auth = Proxy {
1652 intercept: Intercept::Http(ProxyScheme::Http {
1653 auth: Some(HeaderValue::from_static("auth1")),
1654 host: http::uri::Authority::from_static("authority"),
1655 }),
1656 no_proxy: None,
1657 };
1658 assert!(http_proxy_with_auth.maybe_has_http_auth());
1659 assert_eq!(
1660 http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1661 Some(HeaderValue::from_static("auth1"))
1662 );
1663
1664 let http_proxy_without_auth = Proxy {
1665 intercept: Intercept::Http(ProxyScheme::Http {
1666 auth: None,
1667 host: http::uri::Authority::from_static("authority"),
1668 }),
1669 no_proxy: None,
1670 };
1671 assert!(!http_proxy_without_auth.maybe_has_http_auth());
1672 assert_eq!(
1673 http_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1674 None
1675 );
1676
1677 let https_proxy_with_auth = Proxy {
1678 intercept: Intercept::Http(ProxyScheme::Https {
1679 auth: Some(HeaderValue::from_static("auth2")),
1680 host: http::uri::Authority::from_static("authority"),
1681 }),
1682 no_proxy: None,
1683 };
1684 assert!(https_proxy_with_auth.maybe_has_http_auth());
1685 assert_eq!(
1686 https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1687 Some(HeaderValue::from_static("auth2"))
1688 );
1689
1690 let all_http_proxy_with_auth = Proxy {
1691 intercept: Intercept::All(ProxyScheme::Http {
1692 auth: Some(HeaderValue::from_static("auth3")),
1693 host: http::uri::Authority::from_static("authority"),
1694 }),
1695 no_proxy: None,
1696 };
1697 assert!(all_http_proxy_with_auth.maybe_has_http_auth());
1698 assert_eq!(
1699 all_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1700 Some(HeaderValue::from_static("auth3"))
1701 );
1702
1703 let all_https_proxy_with_auth = Proxy {
1704 intercept: Intercept::All(ProxyScheme::Https {
1705 auth: Some(HeaderValue::from_static("auth4")),
1706 host: http::uri::Authority::from_static("authority"),
1707 }),
1708 no_proxy: None,
1709 };
1710 assert!(all_https_proxy_with_auth.maybe_has_http_auth());
1711 assert_eq!(
1712 all_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1713 Some(HeaderValue::from_static("auth4"))
1714 );
1715
1716 let all_https_proxy_without_auth = Proxy {
1717 intercept: Intercept::All(ProxyScheme::Https {
1718 auth: None,
1719 host: http::uri::Authority::from_static("authority"),
1720 }),
1721 no_proxy: None,
1722 };
1723 assert!(!all_https_proxy_without_auth.maybe_has_http_auth());
1724 assert_eq!(
1725 all_https_proxy_without_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1726 None
1727 );
1728
1729 let system_http_proxy_with_auth = Proxy {
1730 intercept: Intercept::System(Arc::new({
1731 let mut m = HashMap::new();
1732 m.insert(
1733 "http".into(),
1734 ProxyScheme::Http {
1735 auth: Some(HeaderValue::from_static("auth5")),
1736 host: http::uri::Authority::from_static("authority"),
1737 },
1738 );
1739 m
1740 })),
1741 no_proxy: None,
1742 };
1743 assert!(system_http_proxy_with_auth.maybe_has_http_auth());
1744 assert_eq!(
1745 system_http_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1746 Some(HeaderValue::from_static("auth5"))
1747 );
1748
1749 let system_https_proxy_with_auth = Proxy {
1750 intercept: Intercept::System(Arc::new({
1751 let mut m = HashMap::new();
1752 m.insert(
1753 "https".into(),
1754 ProxyScheme::Https {
1755 auth: Some(HeaderValue::from_static("auth6")),
1756 host: http::uri::Authority::from_static("authority"),
1757 },
1758 );
1759 m
1760 })),
1761 no_proxy: None,
1762 };
1763 assert!(!system_https_proxy_with_auth.maybe_has_http_auth());
1764 assert_eq!(
1765 system_https_proxy_with_auth.http_basic_auth(&Uri::from_static("http://example.com")),
1766 None
1767 );
1768 }
1769}
1770
1771#[cfg(test)]
1772mod test {
1773 mod into_proxy_scheme {
1774 use crate::Proxy;
1775 use std::error::Error;
1776 use std::mem::discriminant;
1777
1778 fn includes(haystack: &crate::error::Error, needle: url::ParseError) -> bool {
1779 let mut source = haystack.source();
1780 while let Some(error) = source {
1781 if let Some(parse_error) = error.downcast_ref::<url::ParseError>() {
1782 if discriminant(parse_error) == discriminant(&needle) {
1783 return true;
1784 }
1785 }
1786 source = error.source();
1787 }
1788 false
1789 }
1790
1791 fn check_parse_error(url: &str, needle: url::ParseError) {
1792 let error = Proxy::http(url).unwrap_err();
1793 if !includes(&error, needle) {
1794 panic!("{needle:?} expected; {error:?}, {error} found");
1795 }
1796 }
1797
1798 mod when_scheme_missing {
1799 mod and_url_is_valid {
1800 use crate::Proxy;
1801
1802 #[test]
1803 fn lookback_works() {
1804 let _ = Proxy::http("127.0.0.1").unwrap();
1805 }
1806
1807 #[test]
1808 fn loopback_port_works() {
1809 let _ = Proxy::http("127.0.0.1:8080").unwrap();
1810 }
1811
1812 #[test]
1813 fn loopback_username_works() {
1814 let _ = Proxy::http("username@127.0.0.1").unwrap();
1815 }
1816
1817 #[test]
1818 fn loopback_username_password_works() {
1819 let _ = Proxy::http("username:password@127.0.0.1").unwrap();
1820 }
1821
1822 #[test]
1823 fn loopback_username_password_port_works() {
1824 let _ = Proxy::http("ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
1825 }
1826
1827 #[test]
1828 fn domain_works() {
1829 let _ = Proxy::http("proxy.example.com").unwrap();
1830 }
1831
1832 #[test]
1833 fn domain_port_works() {
1834 let _ = Proxy::http("proxy.example.com:8080").unwrap();
1835 }
1836
1837 #[test]
1838 fn domain_username_works() {
1839 let _ = Proxy::http("username@proxy.example.com").unwrap();
1840 }
1841
1842 #[test]
1843 fn domain_username_password_works() {
1844 let _ = Proxy::http("username:password@proxy.example.com").unwrap();
1845 }
1846
1847 #[test]
1848 fn domain_username_password_port_works() {
1849 let _ =
1850 Proxy::http("ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080").unwrap();
1851 }
1852 }
1853 mod and_url_has_bad {
1854 use super::super::check_parse_error;
1855
1856 #[test]
1857 fn host() {
1858 check_parse_error("username@", url::ParseError::RelativeUrlWithoutBase);
1859 }
1860
1861 #[test]
1862 fn idna_encoding() {
1863 check_parse_error("xn---", url::ParseError::RelativeUrlWithoutBase);
1864 }
1865
1866 #[test]
1867 fn port() {
1868 check_parse_error("127.0.0.1:808080", url::ParseError::RelativeUrlWithoutBase);
1869 }
1870
1871 #[test]
1872 fn ip_v4_address() {
1873 check_parse_error("421.627.718.469", url::ParseError::RelativeUrlWithoutBase);
1874 }
1875
1876 #[test]
1877 fn ip_v6_address() {
1878 check_parse_error(
1879 "[56FE::2159:5BBC::6594]",
1880 url::ParseError::RelativeUrlWithoutBase,
1881 );
1882 }
1883
1884 #[test]
1885 fn invalid_domain_character() {
1886 check_parse_error("abc 123", url::ParseError::RelativeUrlWithoutBase);
1887 }
1888 }
1889 }
1890
1891 mod when_scheme_present {
1892 mod and_url_is_valid {
1893 use crate::Proxy;
1894
1895 #[test]
1896 fn loopback_works() {
1897 let _ = Proxy::http("http://127.0.0.1").unwrap();
1898 }
1899
1900 #[test]
1901 fn loopback_port_works() {
1902 let _ = Proxy::http("https://127.0.0.1:8080").unwrap();
1903 }
1904
1905 #[test]
1906 fn loopback_username_works() {
1907 let _ = Proxy::http("http://username@127.0.0.1").unwrap();
1908 }
1909
1910 #[test]
1911 fn loopback_username_password_works() {
1912 let _ = Proxy::http("https://username:password@127.0.0.1").unwrap();
1913 }
1914
1915 #[test]
1916 fn loopback_username_password_port_works() {
1917 let _ =
1918 Proxy::http("http://ldap%5Cgremlin:pass%3Bword@127.0.0.1:8080").unwrap();
1919 }
1920
1921 #[test]
1922 fn domain_works() {
1923 let _ = Proxy::http("https://proxy.example.com").unwrap();
1924 }
1925
1926 #[test]
1927 fn domain_port_works() {
1928 let _ = Proxy::http("http://proxy.example.com:8080").unwrap();
1929 }
1930
1931 #[test]
1932 fn domain_username_works() {
1933 let _ = Proxy::http("https://username@proxy.example.com").unwrap();
1934 }
1935
1936 #[test]
1937 fn domain_username_password_works() {
1938 let _ = Proxy::http("http://username:password@proxy.example.com").unwrap();
1939 }
1940
1941 #[test]
1942 fn domain_username_password_port_works() {
1943 let _ =
1944 Proxy::http("https://ldap%5Cgremlin:pass%3Bword@proxy.example.com:8080")
1945 .unwrap();
1946 }
1947 }
1948 mod and_url_has_bad {
1949 use super::super::check_parse_error;
1950
1951 #[test]
1952 fn host() {
1953 check_parse_error("http://username@", url::ParseError::EmptyHost);
1954 }
1955
1956 #[test]
1957 fn idna_encoding() {
1958 check_parse_error("http://xn---", url::ParseError::IdnaError);
1959 }
1960
1961 #[test]
1962 fn port() {
1963 check_parse_error("http://127.0.0.1:808080", url::ParseError::InvalidPort);
1964 }
1965
1966 #[test]
1967 fn ip_v4_address() {
1968 check_parse_error(
1969 "http://421.627.718.469",
1970 url::ParseError::InvalidIpv4Address,
1971 );
1972 }
1973
1974 #[test]
1975 fn ip_v6_address() {
1976 check_parse_error(
1977 "http://[56FE::2159:5BBC::6594]",
1978 url::ParseError::InvalidIpv6Address,
1979 );
1980 }
1981
1982 #[test]
1983 fn invalid_domain_character() {
1984 check_parse_error("http://abc 123/", url::ParseError::InvalidDomainCharacter);
1985 }
1986 }
1987 }
1988 }
1989}
1990