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 use std::borrow::Cow;
335
336 let uri = val.parse::<http::Uri>().ok()?;
337 let mut builder = http::Uri::builder();
338 let mut is_httpish = false;
339 let mut auth = Auth::Empty;
340
341 builder = builder.scheme(match uri.scheme() {
342 Some(s) => {
343 if s == &http::uri::Scheme::HTTP || s == &http::uri::Scheme::HTTPS {
344 is_httpish = true;
345 s.clone()
346 } else if matches!(s.as_str(), "socks4" | "socks4a" | "socks5" | "socks5h") {
347 s.clone()
348 } else {
349 return None;
351 }
352 }
353 None => {
355 is_httpish = true;
356 http::uri::Scheme::HTTP
357 }
358 });
359
360 let authority = uri.authority()?;
361
362 if let Some((userinfo, host_port)) = authority.as_str().split_once('@') {
363 let (user, pass) = match userinfo.split_once(':') {
364 Some((user, pass)) => (user, Some(pass)),
365 None => (userinfo, None),
366 };
367 let user = percent_decode_str(user).decode_utf8_lossy();
368 let pass = pass.map(|pass| percent_decode_str(pass).decode_utf8_lossy());
369 if is_httpish {
370 auth = Auth::Basic(encode_basic_auth(&user, pass.as_deref()));
371 } else {
372 auth = Auth::Raw(
373 user.into_owned(),
374 pass.map_or_else(String::new, Cow::into_owned),
375 );
376 }
377 builder = builder.authority(host_port);
378 } else {
379 builder = builder.authority(authority.clone());
380 }
381
382 builder = builder.path_and_query("/");
384
385 let dst = builder.build().ok()?;
386
387 Some(Intercept { uri: dst, auth })
388}
389
390fn encode_basic_auth(user: &str, pass: Option<&str>) -> HeaderValue {
391 use base64::prelude::BASE64_STANDARD;
392 use base64::write::EncoderWriter;
393 use std::io::Write;
394
395 let mut buf = b"Basic ".to_vec();
396 {
397 let mut encoder = EncoderWriter::new(&mut buf, &BASE64_STANDARD);
398 let _ = write!(encoder, "{user}:");
399 if let Some(password) = pass {
400 let _ = write!(encoder, "{password}");
401 }
402 }
403 let mut header = HeaderValue::from_bytes(&buf).expect("base64 is always valid HeaderValue");
404 header.set_sensitive(true);
405 header
406}
407
408impl NoProxy {
409 fn empty() -> NoProxy {
420 NoProxy {
421 ips: IpMatcher(Vec::new()),
422 domains: DomainMatcher(Vec::new()),
423 }
424 }
425
426 pub fn from_string(no_proxy_list: &str) -> Self {
446 let mut ips = Vec::new();
447 let mut domains = Vec::new();
448 let parts = no_proxy_list.split(',').map(str::trim);
449 for part in parts {
450 match part.parse::<IpNet>() {
451 Ok(ip) => ips.push(Ip::Network(ip)),
453 Err(_) => match part.parse::<IpAddr>() {
454 Ok(addr) => ips.push(Ip::Address(addr)),
455 Err(_) => {
456 if !part.trim().is_empty() {
457 domains.push(part.to_owned())
458 }
459 }
460 },
461 }
462 }
463 NoProxy {
464 ips: IpMatcher(ips),
465 domains: DomainMatcher(domains),
466 }
467 }
468
469 pub fn contains(&self, host: &str) -> bool {
471 let host = if host.starts_with('[') {
474 let x: &[_] = &['[', ']'];
475 host.trim_matches(x)
476 } else {
477 host
478 };
479 match host.parse::<IpAddr>() {
480 Ok(ip) => self.ips.contains(ip),
482 Err(_) => self.domains.contains(host),
483 }
484 }
485
486 fn is_empty(&self) -> bool {
487 self.ips.0.is_empty() && self.domains.0.is_empty()
488 }
489}
490
491impl IpMatcher {
492 fn contains(&self, addr: IpAddr) -> bool {
493 for ip in &self.0 {
494 match ip {
495 Ip::Address(address) => {
496 if &addr == address {
497 return true;
498 }
499 }
500 Ip::Network(net) => {
501 if net.contains(&addr) {
502 return true;
503 }
504 }
505 }
506 }
507 false
508 }
509}
510
511impl DomainMatcher {
512 fn contains(&self, domain: &str) -> bool {
516 let domain_len = domain.len();
517 for d in &self.0 {
518 if d.eq_ignore_ascii_case(domain)
519 || d.strip_prefix('.')
520 .map_or(false, |s| s.eq_ignore_ascii_case(domain))
521 {
522 return true;
523 } else if domain
524 .get(domain_len.saturating_sub(d.len())..)
525 .map_or(false, |s| s.eq_ignore_ascii_case(d))
526 {
527 if d.starts_with('.') {
528 return true;
531 } else if domain.as_bytes().get(domain_len - d.len() - 1) == Some(&b'.') {
532 return true;
535 }
536 } else if d == "*" {
537 return true;
538 }
539 }
540 false
541 }
542}
543
544mod builder {
545 pub trait IntoValue {
549 #[doc(hidden)]
550 fn into_value(self) -> String;
551 }
552
553 impl IntoValue for String {
554 #[doc(hidden)]
555 fn into_value(self) -> String {
556 self
557 }
558 }
559
560 impl IntoValue for &String {
561 #[doc(hidden)]
562 fn into_value(self) -> String {
563 self.into()
564 }
565 }
566
567 impl IntoValue for &str {
568 #[doc(hidden)]
569 fn into_value(self) -> String {
570 self.into()
571 }
572 }
573}
574
575#[cfg(feature = "client-proxy-system")]
576#[cfg(target_os = "macos")]
577mod mac {
578 use system_configuration::core_foundation::base::CFType;
579 use system_configuration::core_foundation::dictionary::CFDictionary;
580 use system_configuration::core_foundation::number::CFNumber;
581 use system_configuration::core_foundation::string::{CFString, CFStringRef};
582 use system_configuration::dynamic_store::SCDynamicStoreBuilder;
583 use system_configuration::sys::schema_definitions::{
584 kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPProxy,
585 kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesHTTPSProxy,
586 };
587
588 pub(super) fn with_system(builder: &mut super::Builder) {
589 let store = if let Some(store) = SCDynamicStoreBuilder::new("hyper-util").build() {
590 store
591 } else {
592 return;
593 };
594
595 let proxies_map = if let Some(proxies_map) = store.get_proxies() {
596 proxies_map
597 } else {
598 return;
599 };
600
601 if builder.http.is_empty() {
602 let http_proxy_config = parse_setting_from_dynamic_store(
603 &proxies_map,
604 unsafe { kSCPropNetProxiesHTTPEnable },
605 unsafe { kSCPropNetProxiesHTTPProxy },
606 unsafe { kSCPropNetProxiesHTTPPort },
607 );
608 if let Some(http) = http_proxy_config {
609 builder.http = http;
610 }
611 }
612
613 if builder.https.is_empty() {
614 let https_proxy_config = parse_setting_from_dynamic_store(
615 &proxies_map,
616 unsafe { kSCPropNetProxiesHTTPSEnable },
617 unsafe { kSCPropNetProxiesHTTPSProxy },
618 unsafe { kSCPropNetProxiesHTTPSPort },
619 );
620
621 if let Some(https) = https_proxy_config {
622 builder.https = https;
623 }
624 }
625 }
626
627 fn parse_setting_from_dynamic_store(
628 proxies_map: &CFDictionary<CFString, CFType>,
629 enabled_key: CFStringRef,
630 host_key: CFStringRef,
631 port_key: CFStringRef,
632 ) -> Option<String> {
633 let proxy_enabled = proxies_map
634 .find(enabled_key)
635 .and_then(|flag| flag.downcast::<CFNumber>())
636 .and_then(|flag| flag.to_i32())
637 .unwrap_or(0)
638 == 1;
639
640 if proxy_enabled {
641 let proxy_host = proxies_map
642 .find(host_key)
643 .and_then(|host| host.downcast::<CFString>())
644 .map(|host| host.to_string());
645 let proxy_port = proxies_map
646 .find(port_key)
647 .and_then(|port| port.downcast::<CFNumber>())
648 .and_then(|port| port.to_i32());
649
650 return match (proxy_host, proxy_port) {
651 (Some(proxy_host), Some(proxy_port)) => Some(format!("{proxy_host}:{proxy_port}")),
652 (Some(proxy_host), None) => Some(proxy_host),
653 (None, Some(_)) => None,
654 (None, None) => None,
655 };
656 }
657
658 None
659 }
660}
661
662#[cfg(feature = "client-proxy-system")]
663#[cfg(windows)]
664mod win {
665 pub(super) fn with_system(builder: &mut super::Builder) {
666 let settings = if let Ok(settings) = windows_registry::CURRENT_USER
667 .open("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
668 {
669 settings
670 } else {
671 return;
672 };
673
674 if settings.get_u32("ProxyEnable").unwrap_or(0) == 0 {
675 return;
676 }
677
678 if let Ok(val) = settings.get_string("ProxyServer") {
679 if builder.http.is_empty() {
680 builder.http = val.clone();
681 }
682 if builder.https.is_empty() {
683 builder.https = val;
684 }
685 }
686
687 if builder.no.is_empty() {
688 if let Ok(val) = settings.get_string("ProxyOverride") {
689 builder.no = val
690 .split(';')
691 .map(|s| s.trim())
692 .collect::<Vec<&str>>()
693 .join(",")
694 .replace("*.", "");
695 }
696 }
697 }
698}
699
700#[cfg(test)]
701mod tests {
702 use super::*;
703
704 #[test]
705 fn test_domain_matcher() {
706 let domains = vec![".foo.bar".into(), "bar.foo".into()];
707 let matcher = DomainMatcher(domains);
708
709 assert!(matcher.contains("foo.bar"));
711 assert!(matcher.contains("FOO.BAR"));
712
713 assert!(matcher.contains("www.foo.bar"));
715 assert!(matcher.contains("WWW.FOO.BAR"));
716
717 assert!(matcher.contains("bar.foo"));
719 assert!(matcher.contains("Bar.foo"));
720
721 assert!(matcher.contains("www.bar.foo"));
723 assert!(matcher.contains("WWW.BAR.FOO"));
724
725 assert!(!matcher.contains("notfoo.bar"));
727 assert!(!matcher.contains("notbar.foo"));
728 }
729
730 #[test]
731 fn test_no_proxy_wildcard() {
732 let no_proxy = NoProxy::from_string("*");
733 assert!(no_proxy.contains("any.where"));
734 }
735
736 #[test]
737 fn test_no_proxy_ip_ranges() {
738 let no_proxy =
739 NoProxy::from_string(".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17");
740
741 let should_not_match = [
742 "hyper.rs",
744 "notfoo.bar",
746 "notbar.baz",
748 "10.43.1.1",
750 "10.124.7.7",
752 "[ffff:db8:a0b:12f0::1]",
754 "[2005:db8:a0b:12f0::1]",
756 ];
757
758 for host in &should_not_match {
759 assert!(!no_proxy.contains(host), "should not contain {host:?}");
760 }
761
762 let should_match = [
763 "hello.foo.bar",
765 "bar.baz",
767 "foo.bar.baz",
769 "foo.bar",
771 "10.42.1.100",
773 "[::1]",
775 "[2001:db8:a0b:12f0::1]",
777 "10.124.7.8",
779 ];
780
781 for host in &should_match {
782 assert!(no_proxy.contains(host), "should contain {host:?}");
783 }
784 }
785
786 macro_rules! p {
787 ($($n:ident = $v:expr,)*) => ({Builder {
788 $($n: $v.into(),)*
789 ..Builder::default()
790 }.build()});
791 }
792
793 fn intercept(p: &Matcher, u: &str) -> Intercept {
794 p.intercept(&u.parse().unwrap()).unwrap()
795 }
796
797 #[test]
798 fn test_all_proxy() {
799 let p = p! {
800 all = "http://om.nom",
801 };
802
803 assert_eq!("http://om.nom", intercept(&p, "http://example.com").uri());
804
805 assert_eq!("http://om.nom", intercept(&p, "https://example.com").uri());
806 }
807
808 #[test]
809 fn test_specific_overrides_all() {
810 let p = p! {
811 all = "http://no.pe",
812 http = "http://y.ep",
813 };
814
815 assert_eq!("http://no.pe", intercept(&p, "https://example.com").uri());
816
817 assert_eq!("http://y.ep", intercept(&p, "http://example.com").uri());
819 }
820
821 #[test]
822 fn test_parse_no_scheme_defaults_to_http() {
823 let p = p! {
824 https = "y.ep",
825 http = "127.0.0.1:8887",
826 };
827
828 assert_eq!(intercept(&p, "https://example.local").uri(), "http://y.ep");
829 assert_eq!(
830 intercept(&p, "http://example.local").uri(),
831 "http://127.0.0.1:8887"
832 );
833 }
834
835 #[test]
836 fn test_parse_http_auth() {
837 let p = p! {
838 all = "http://Aladdin:opensesame@y.ep",
839 };
840
841 let proxy = intercept(&p, "https://example.local");
842 assert_eq!(proxy.uri(), "http://y.ep");
843 assert_eq!(
844 proxy.basic_auth().expect("basic_auth"),
845 "Basic QWxhZGRpbjpvcGVuc2VzYW1l"
846 );
847 }
848
849 #[test]
850 fn test_parse_http_auth_without_password() {
851 let p = p! {
852 all = "http://Aladdin@y.ep",
853 };
854 let proxy = intercept(&p, "https://example.local");
855 assert_eq!(proxy.uri(), "http://y.ep");
856 assert_eq!(
857 proxy.basic_auth().expect("basic_auth"),
858 "Basic QWxhZGRpbjo="
859 );
860 }
861
862 #[test]
863 fn test_parse_http_auth_without_scheme() {
864 let p = p! {
865 all = "Aladdin:opensesame@y.ep",
866 };
867
868 let proxy = intercept(&p, "https://example.local");
869 assert_eq!(proxy.uri(), "http://y.ep");
870 assert_eq!(
871 proxy.basic_auth().expect("basic_auth"),
872 "Basic QWxhZGRpbjpvcGVuc2VzYW1l"
873 );
874 }
875
876 #[test]
877 fn test_dont_parse_http_when_is_cgi() {
878 let mut builder = Matcher::builder();
879 builder.is_cgi = true;
880 builder.http = "http://never.gonna.let.you.go".into();
881 let m = builder.build();
882
883 assert!(m.intercept(&"http://rick.roll".parse().unwrap()).is_none());
884 }
885
886 #[test]
887 fn test_domain_matcher_case_insensitive() {
888 let domains = vec![".foo.bar".into()];
889 let matcher = DomainMatcher(domains);
890
891 assert!(matcher.contains("foo.bar"));
892 assert!(matcher.contains("FOO.BAR"));
893 assert!(matcher.contains("Foo.Bar"));
894
895 assert!(matcher.contains("www.foo.bar"));
896 assert!(matcher.contains("WWW.FOO.BAR"));
897 assert!(matcher.contains("Www.Foo.Bar"));
898 }
899
900 #[test]
901 fn test_no_proxy_case_insensitive() {
902 let p = p! {
903 all = "http://proxy.local",
904 no = ".example.com",
905 };
906
907 assert!(p
909 .intercept(&"http://example.com".parse().unwrap())
910 .is_none());
911 assert!(p
912 .intercept(&"http://EXAMPLE.COM".parse().unwrap())
913 .is_none());
914 assert!(p
915 .intercept(&"http://Example.com".parse().unwrap())
916 .is_none());
917
918 assert!(p
920 .intercept(&"http://www.example.com".parse().unwrap())
921 .is_none());
922 assert!(p
923 .intercept(&"http://WWW.EXAMPLE.COM".parse().unwrap())
924 .is_none());
925 assert!(p
926 .intercept(&"http://Www.Example.Com".parse().unwrap())
927 .is_none());
928 }
929}