1use std::fmt;
16use std::net::IpAddr;
17
18use http::header::HeaderValue;
19use ipnet::IpNet;
20use percent_encoding::percent_decode_str;
21
22#[cfg(docsrs)]
23pub use self::builder::IntoValue;
24#[cfg(not(docsrs))]
25use self::builder::IntoValue;
26
27pub struct Matcher {
29 http: Option<Intercept>,
30 https: Option<Intercept>,
31 no: NoProxy,
32}
33
34#[derive(Clone)]
38pub struct Intercept {
39 uri: http::Uri,
40 auth: Auth,
41}
42
43#[derive(Default)]
47pub struct Builder {
48 is_cgi: bool,
49 all: String,
50 http: String,
51 https: String,
52 no: String,
53}
54
55#[derive(Clone)]
56enum Auth {
57 Empty,
58 Basic(http::header::HeaderValue),
59 Raw(String, String),
60}
61
62#[derive(Clone, Debug, Default)]
66struct NoProxy {
67 ips: IpMatcher,
68 domains: DomainMatcher,
69}
70
71#[derive(Clone, Debug, Default)]
72struct DomainMatcher(Vec<String>);
73
74#[derive(Clone, Debug, Default)]
75struct IpMatcher(Vec<Ip>);
76
77#[derive(Clone, Debug)]
78enum Ip {
79 Address(IpAddr),
80 Network(IpNet),
81}
82
83impl Matcher {
86 pub fn from_env() -> Self {
96 Builder::from_env().build()
97 }
98
99 pub fn from_system() -> Self {
110 Builder::from_system().build()
111 }
112
113 pub fn builder() -> Builder {
115 Builder::default()
116 }
117
118 pub fn intercept(&self, dst: &http::Uri) -> Option<Intercept> {
123 if self.no.contains(dst.host()?) {
125 return None;
126 }
127
128 match dst.scheme_str() {
129 Some("http") => self.http.clone(),
130 Some("https") => self.https.clone(),
131 _ => None,
132 }
133 }
134}
135
136impl fmt::Debug for Matcher {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 let mut b = f.debug_struct("Matcher");
139
140 if let Some(ref http) = self.http {
141 b.field("http", http);
142 }
143
144 if let Some(ref https) = self.https {
145 b.field("https", https);
146 }
147
148 if !self.no.is_empty() {
149 b.field("no", &self.no);
150 }
151 b.finish()
152 }
153}
154
155impl Intercept {
158 pub fn uri(&self) -> &http::Uri {
160 &self.uri
161 }
162
163 pub fn basic_auth(&self) -> Option<&HeaderValue> {
182 if let Auth::Basic(ref val) = self.auth {
183 Some(val)
184 } else {
185 None
186 }
187 }
188
189 pub fn raw_auth(&self) -> Option<(&str, &str)> {
208 if let Auth::Raw(ref u, ref p) = self.auth {
209 Some((u.as_str(), p.as_str()))
210 } else {
211 None
212 }
213 }
214}
215
216impl fmt::Debug for Intercept {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 f.debug_struct("Intercept")
219 .field("uri", &self.uri)
220 .finish()
222 }
223}
224
225impl Builder {
228 fn from_env() -> Self {
229 Builder {
230 is_cgi: std::env::var_os("REQUEST_METHOD").is_some(),
231 all: get_first_env(&["ALL_PROXY", "all_proxy"]),
232 http: get_first_env(&["HTTP_PROXY", "http_proxy"]),
233 https: get_first_env(&["HTTPS_PROXY", "https_proxy"]),
234 no: get_first_env(&["NO_PROXY", "no_proxy"]),
235 }
236 }
237
238 fn from_system() -> Self {
239 #[allow(unused_mut)]
240 let mut builder = Self::from_env();
241
242 #[cfg(all(feature = "client-proxy-system", target_os = "macos"))]
243 mac::with_system(&mut builder);
244
245 #[cfg(all(feature = "client-proxy-system", windows))]
246 win::with_system(&mut builder);
247
248 builder
249 }
250
251 pub fn all<S>(mut self, val: S) -> Self
253 where
254 S: IntoValue,
255 {
256 self.all = val.into_value();
257 self
258 }
259
260 pub fn http<S>(mut self, val: S) -> Self
262 where
263 S: IntoValue,
264 {
265 self.http = val.into_value();
266 self
267 }
268
269 pub fn https<S>(mut self, val: S) -> Self
271 where
272 S: IntoValue,
273 {
274 self.https = val.into_value();
275 self
276 }
277
278 pub fn no<S>(mut self, val: S) -> Self
296 where
297 S: IntoValue,
298 {
299 self.no = val.into_value();
300 self
301 }
302
303 pub fn build(self) -> Matcher {
305 if self.is_cgi {
306 return Matcher {
307 http: None,
308 https: None,
309 no: NoProxy::empty(),
310 };
311 }
312
313 let all = parse_env_uri(&self.all);
314
315 Matcher {
316 http: parse_env_uri(&self.http).or_else(|| all.clone()),
317 https: parse_env_uri(&self.https).or(all),
318 no: NoProxy::from_string(&self.no),
319 }
320 }
321}
322
323fn get_first_env(names: &[&str]) -> String {
324 for name in names {
325 if let Ok(val) = std::env::var(name) {
326 return val;
327 }
328 }
329
330 String::new()
331}
332
333fn parse_env_uri(val: &str) -> Option<Intercept> {
334 let uri = val.parse::<http::Uri>().ok()?;
335 let mut builder = http::Uri::builder();
336 let mut is_httpish = false;
337 let mut auth = Auth::Empty;
338
339 builder = builder.scheme(match uri.scheme() {
340 Some(s) => {
341 if s == &http::uri::Scheme::HTTP || s == &http::uri::Scheme::HTTPS {
342 is_httpish = true;
343 s.clone()
344 } else if s.as_str() == "socks5" || s.as_str() == "socks5h" {
345 s.clone()
346 } else {
347 return None;
349 }
350 }
351 None => {
353 is_httpish = true;
354 http::uri::Scheme::HTTP
355 }
356 });
357
358 let authority = uri.authority()?;
359
360 if let Some((userinfo, host_port)) = authority.as_str().split_once('@') {
361 let (user, pass) = userinfo.split_once(':')?;
362 let user = percent_decode_str(user).decode_utf8_lossy();
363 let pass = percent_decode_str(pass).decode_utf8_lossy();
364 if is_httpish {
365 auth = Auth::Basic(encode_basic_auth(&user, Some(&pass)));
366 } else {
367 auth = Auth::Raw(user.into(), pass.into());
368 }
369 builder = builder.authority(host_port);
370 } else {
371 builder = builder.authority(authority.clone());
372 }
373
374 builder = builder.path_and_query("/");
376
377 let dst = builder.build().ok()?;
378
379 Some(Intercept { uri: dst, auth })
380}
381
382fn encode_basic_auth(user: &str, pass: Option<&str>) -> HeaderValue {
383 use base64::prelude::BASE64_STANDARD;
384 use base64::write::EncoderWriter;
385 use std::io::Write;
386
387 let mut buf = b"Basic ".to_vec();
388 {
389 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
390 let _ = write!(encoder, "{user}:");
391 if let Some(password) = pass {
392 let _ = write!(encoder, "{password}");
393 }
394 }
395 let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
396 header.set_sensitive(true);
397 header
398}
399
400impl NoProxy {
401 fn empty() -> NoProxy {
412 NoProxy {
413 ips: IpMatcher(Vec::new()),
414 domains: DomainMatcher(Vec::new()),
415 }
416 }
417
418 pub fn from_string(no_proxy_list: &str) -> Self {
438 let mut ips = Vec::new();
439 let mut domains = Vec::new();
440 let parts = no_proxy_list.split(',').map(str::trim);
441 for part in parts {
442 match part.parse::<IpNet>() {
443 Ok(ip) => ips.push(Ip::Network(ip)),
445 Err(_) => match part.parse::<IpAddr>() {
446 Ok(addr) => ips.push(Ip::Address(addr)),
447 Err(_) => {
448 if !part.trim().is_empty() {
449 domains.push(part.to_owned())
450 }
451 }
452 },
453 }
454 }
455 NoProxy {
456 ips: IpMatcher(ips),
457 domains: DomainMatcher(domains),
458 }
459 }
460
461 pub fn contains(&self, host: &str) -> bool {
463 let host = if host.starts_with('[') {
466 let x: &[_] = &['[', ']'];
467 host.trim_matches(x)
468 } else {
469 host
470 };
471 match host.parse::<IpAddr>() {
472 Ok(ip) => self.ips.contains(ip),
474 Err(_) => self.domains.contains(host),
475 }
476 }
477
478 fn is_empty(&self) -> bool {
479 self.ips.0.is_empty() && self.domains.0.is_empty()
480 }
481}
482
483impl IpMatcher {
484 fn contains(&self, addr: IpAddr) -> bool {
485 for ip in &self.0 {
486 match ip {
487 Ip::Address(address) => {
488 if &addr == address {
489 return true;
490 }
491 }
492 Ip::Network(net) => {
493 if net.contains(&addr) {
494 return true;
495 }
496 }
497 }
498 }
499 false
500 }
501}
502
503impl DomainMatcher {
504 fn contains(&self, domain: &str) -> bool {
508 let domain_len = domain.len();
509 for d in &self.0 {
510 if d == domain || d.strip_prefix('.') == Some(domain) {
511 return true;
512 } else if domain.ends_with(d) {
513 if d.starts_with('.') {
514 return true;
517 } else if domain.as_bytes().get(domain_len - d.len() - 1) == Some(&b'.') {
518 return true;
521 }
522 } else if d == "*" {
523 return true;
524 }
525 }
526 false
527 }
528}
529
530mod builder {
531 pub trait IntoValue {
535 #[doc(hidden)]
536 fn into_value(self) -> String;
537 }
538
539 impl IntoValue for String {
540 #[doc(hidden)]
541 fn into_value(self) -> String {
542 self
543 }
544 }
545
546 impl IntoValue for &String {
547 #[doc(hidden)]
548 fn into_value(self) -> String {
549 self.into()
550 }
551 }
552
553 impl IntoValue for &str {
554 #[doc(hidden)]
555 fn into_value(self) -> String {
556 self.into()
557 }
558 }
559}
560
561#[cfg(feature = "client-proxy-system")]
562#[cfg(target_os = "macos")]
563mod mac {
564 use system_configuration::core_foundation::base::{CFType, TCFType, TCFTypeRef};
565 use system_configuration::core_foundation::dictionary::CFDictionary;
566 use system_configuration::core_foundation::number::CFNumber;
567 use system_configuration::core_foundation::string::{CFString, CFStringRef};
568 use system_configuration::dynamic_store::SCDynamicStoreBuilder;
569 use system_configuration::sys::schema_definitions::{
570 kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPProxy,
571 kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesHTTPSProxy,
572 };
573
574 pub(super) fn with_system(builder: &mut super::Builder) {
575 let store = SCDynamicStoreBuilder::new("").build();
576
577 let proxies_map = if let Some(proxies_map) = store.get_proxies() {
578 proxies_map
579 } else {
580 return;
581 };
582
583 if builder.http.is_empty() {
584 let http_proxy_config = parse_setting_from_dynamic_store(
585 &proxies_map,
586 unsafe { kSCPropNetProxiesHTTPEnable },
587 unsafe { kSCPropNetProxiesHTTPProxy },
588 unsafe { kSCPropNetProxiesHTTPPort },
589 );
590 if let Some(http) = http_proxy_config {
591 builder.http = http;
592 }
593 }
594
595 if builder.https.is_empty() {
596 let https_proxy_config = parse_setting_from_dynamic_store(
597 &proxies_map,
598 unsafe { kSCPropNetProxiesHTTPSEnable },
599 unsafe { kSCPropNetProxiesHTTPSProxy },
600 unsafe { kSCPropNetProxiesHTTPSPort },
601 );
602
603 if let Some(https) = https_proxy_config {
604 builder.https = https;
605 }
606 }
607 }
608
609 fn parse_setting_from_dynamic_store(
610 proxies_map: &CFDictionary<CFString, CFType>,
611 enabled_key: CFStringRef,
612 host_key: CFStringRef,
613 port_key: CFStringRef,
614 ) -> Option<String> {
615 let proxy_enabled = proxies_map
616 .find(enabled_key)
617 .and_then(|flag| flag.downcast::<CFNumber>())
618 .and_then(|flag| flag.to_i32())
619 .unwrap_or(0)
620 == 1;
621
622 if proxy_enabled {
623 let proxy_host = proxies_map
624 .find(host_key)
625 .and_then(|host| host.downcast::<CFString>())
626 .map(|host| host.to_string());
627 let proxy_port = proxies_map
628 .find(port_key)
629 .and_then(|port| port.downcast::<CFNumber>())
630 .and_then(|port| port.to_i32());
631
632 return match (proxy_host, proxy_port) {
633 (Some(proxy_host), Some(proxy_port)) => Some(format!("{proxy_host}:{proxy_port}")),
634 (Some(proxy_host), None) => Some(proxy_host),
635 (None, Some(_)) => None,
636 (None, None) => None,
637 };
638 }
639
640 None
641 }
642}
643
644#[cfg(feature = "client-proxy-system")]
645#[cfg(windows)]
646mod win {
647 pub(super) fn with_system(builder: &mut super::Builder) {
648 let settings = if let Ok(settings) = windows_registry::CURRENT_USER
649 .open("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
650 {
651 settings
652 } else {
653 return;
654 };
655
656 if settings.get_u32("ProxyEnable").unwrap_or(0) == 0 {
657 return;
658 }
659
660 if let Ok(val) = settings.get_string("ProxyServer") {
661 if builder.http.is_empty() {
662 builder.http = val.clone();
663 }
664 if builder.https.is_empty() {
665 builder.https = val;
666 }
667 }
668
669 if builder.no.is_empty() {
670 if let Ok(val) = settings.get_string("ProxyOverride") {
671 builder.no = val
672 .split(';')
673 .map(|s| s.trim())
674 .collect::<Vec<&str>>()
675 .join(",")
676 .replace("*.", "");
677 }
678 }
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn test_domain_matcher() {
688 let domains = vec![".foo.bar".into(), "bar.foo".into()];
689 let matcher = DomainMatcher(domains);
690
691 assert!(matcher.contains("foo.bar"));
693 assert!(matcher.contains("www.foo.bar"));
695
696 assert!(matcher.contains("bar.foo"));
698 assert!(matcher.contains("www.bar.foo"));
700
701 assert!(!matcher.contains("notfoo.bar"));
703 assert!(!matcher.contains("notbar.foo"));
704 }
705
706 #[test]
707 fn test_no_proxy_wildcard() {
708 let no_proxy = NoProxy::from_string("*");
709 assert!(no_proxy.contains("any.where"));
710 }
711
712 #[test]
713 fn test_no_proxy_ip_ranges() {
714 let no_proxy =
715 NoProxy::from_string(".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17");
716
717 let should_not_match = [
718 "hyper.rs",
720 "notfoo.bar",
722 "notbar.baz",
724 "10.43.1.1",
726 "10.124.7.7",
728 "[ffff:db8:a0b:12f0::1]",
730 "[2005:db8:a0b:12f0::1]",
732 ];
733
734 for host in &should_not_match {
735 assert!(!no_proxy.contains(host), "should not contain {host:?}");
736 }
737
738 let should_match = [
739 "hello.foo.bar",
741 "bar.baz",
743 "foo.bar.baz",
745 "foo.bar",
747 "10.42.1.100",
749 "[::1]",
751 "[2001:db8:a0b:12f0::1]",
753 "10.124.7.8",
755 ];
756
757 for host in &should_match {
758 assert!(no_proxy.contains(host), "should contain {host:?}");
759 }
760 }
761
762 macro_rules! p {
763 ($($n:ident = $v:expr,)*) => ({Builder {
764 $($n: $v.into(),)*
765 ..Builder::default()
766 }.build()});
767 }
768
769 fn intercept(p: &Matcher, u: &str) -> Intercept {
770 p.intercept(&u.parse().unwrap()).unwrap()
771 }
772
773 #[test]
774 fn test_all_proxy() {
775 let p = p! {
776 all = "http://om.nom",
777 };
778
779 assert_eq!("http://om.nom", intercept(&p, "http://example.com").uri());
780
781 assert_eq!("http://om.nom", intercept(&p, "https://example.com").uri());
782 }
783
784 #[test]
785 fn test_specific_overrides_all() {
786 let p = p! {
787 all = "http://no.pe",
788 http = "http://y.ep",
789 };
790
791 assert_eq!("http://no.pe", intercept(&p, "https://example.com").uri());
792
793 assert_eq!("http://y.ep", intercept(&p, "http://example.com").uri());
795 }
796
797 #[test]
798 fn test_parse_no_scheme_defaults_to_http() {
799 let p = p! {
800 https = "y.ep",
801 http = "127.0.0.1:8887",
802 };
803
804 assert_eq!(intercept(&p, "https://example.local").uri(), "http://y.ep");
805 assert_eq!(
806 intercept(&p, "http://example.local").uri(),
807 "http://127.0.0.1:8887"
808 );
809 }
810
811 #[test]
812 fn test_parse_http_auth() {
813 let p = p! {
814 all = "http://Aladdin:opensesame@y.ep",
815 };
816
817 let proxy = intercept(&p, "https://example.local");
818 assert_eq!(proxy.uri(), "http://y.ep");
819 assert_eq!(
820 proxy.basic_auth().expect("basic_auth"),
821 "Basic QWxhZGRpbjpvcGVuc2VzYW1l"
822 );
823 }
824
825 #[test]
826 fn test_parse_http_auth_without_scheme() {
827 let p = p! {
828 all = "Aladdin:opensesame@y.ep",
829 };
830
831 let proxy = intercept(&p, "https://example.local");
832 assert_eq!(proxy.uri(), "http://y.ep");
833 assert_eq!(
834 proxy.basic_auth().expect("basic_auth"),
835 "Basic QWxhZGRpbjpvcGVuc2VzYW1l"
836 );
837 }
838
839 #[test]
840 fn test_dont_parse_http_when_is_cgi() {
841 let mut builder = Matcher::builder();
842 builder.is_cgi = true;
843 builder.http = "http://never.gonna.let.you.go".into();
844 let m = builder.build();
845
846 assert!(m.intercept(&"http://rick.roll".parse().unwrap()).is_none());
847 }
848}