Skip to main content

hyper_util/client/proxy/
matcher.rs

1//! Proxy matchers
2//!
3//! This module contains different matchers to configure rules for when a proxy
4//! should be used, and if so, with what arguments.
5//!
6//! A [`Matcher`] can be constructed either using environment variables, or
7//! a [`Matcher::builder()`].
8//!
9//! Once constructed, the `Matcher` can be asked if it intercepts a `Uri` by
10//! calling [`Matcher::intercept()`].
11//!
12//! An [`Intercept`] includes the destination for the proxy, and any parsed
13//! authentication to be used.
14
15use 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
27/// A proxy matcher, usually built from environment variables.
28pub struct Matcher {
29    http: Option<Intercept>,
30    https: Option<Intercept>,
31    no: NoProxy,
32}
33
34/// A matched proxy,
35///
36/// This is returned by a matcher if a proxy should be used.
37#[derive(Clone)]
38pub struct Intercept {
39    uri: http::Uri,
40    auth: Auth,
41}
42
43/// A builder to create a [`Matcher`].
44///
45/// Construct with [`Matcher::builder()`].
46#[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/// A filter for proxy matchers.
63///
64/// This type is based off the `NO_PROXY` rules used by curl.
65#[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
83// ===== impl Matcher =====
84
85impl Matcher {
86    /// Create a matcher reading the current environment variables.
87    ///
88    /// This checks for values in the following variables, treating them the
89    /// same as curl does:
90    ///
91    /// - `ALL_PROXY`/`all_proxy`
92    /// - `HTTPS_PROXY`/`https_proxy`
93    /// - `HTTP_PROXY`/`http_proxy`
94    /// - `NO_PROXY`/`no_proxy`
95    pub fn from_env() -> Self {
96        Builder::from_env().build()
97    }
98
99    /// Create a matcher from the environment or system.
100    ///
101    /// This checks the same environment variables as `from_env()`, and if not
102    /// set, checks the system configuration for values for the OS.
103    ///
104    /// This constructor is always available, but if the `client-proxy-system`
105    /// feature is enabled, it will check more configuration. Use this
106    /// constructor if you want to allow users to optionally enable more, or
107    /// use `from_env` if you do not want the values to change based on an
108    /// enabled feature.
109    pub fn from_system() -> Self {
110        Builder::from_system().build()
111    }
112
113    /// Start a builder to configure a matcher.
114    pub fn builder() -> Builder {
115        Builder::default()
116    }
117
118    /// Check if the destination should be intercepted by a proxy.
119    ///
120    /// If the proxy rules match the destination, a new `Uri` will be returned
121    /// to connect to.
122    pub fn intercept(&self, dst: &http::Uri) -> Option<Intercept> {
123        // TODO(perf): don't need to check `no` if below doesn't match...
124        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
155// ===== impl Intercept =====
156
157impl Intercept {
158    /// Get the `http::Uri` for the target proxy.
159    pub fn uri(&self) -> &http::Uri {
160        &self.uri
161    }
162
163    /// Get any configured basic authorization.
164    ///
165    /// This should usually be used with a `Proxy-Authorization` header, to
166    /// send in Basic format.
167    ///
168    /// # Example
169    ///
170    /// ```rust
171    /// # use hyper_util::client::proxy::matcher::Matcher;
172    /// # let uri = http::Uri::from_static("https://hyper.rs");
173    /// let m = Matcher::builder()
174    ///     .all("https://Aladdin:opensesame@localhost:8887")
175    ///     .build();
176    ///
177    /// let proxy = m.intercept(&uri).expect("example");
178    /// let auth = proxy.basic_auth().expect("example");
179    /// assert_eq!(auth, "Basic QWxhZGRpbjpvcGVuc2VzYW1l");
180    /// ```
181    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    /// Get any configured raw authorization.
190    ///
191    /// If not detected as another scheme, this is the username and password
192    /// that should be sent with whatever protocol the proxy handshake uses.
193    ///
194    /// # Example
195    ///
196    /// ```rust
197    /// # use hyper_util::client::proxy::matcher::Matcher;
198    /// # let uri = http::Uri::from_static("https://hyper.rs");
199    /// let m = Matcher::builder()
200    ///     .all("socks5h://Aladdin:opensesame@localhost:8887")
201    ///     .build();
202    ///
203    /// let proxy = m.intercept(&uri).expect("example");
204    /// let auth = proxy.raw_auth().expect("example");
205    /// assert_eq!(auth, ("Aladdin", "opensesame"));
206    /// ```
207    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            // dont output auth, its sensitive
221            .finish()
222    }
223}
224
225// ===== impl Builder =====
226
227impl 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    /// Set the target proxy for all destinations.
252    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    /// Set the target proxy for HTTP destinations.
261    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    /// Set the target proxy for HTTPS destinations.
270    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    /// Set the "no" proxy filter.
279    ///
280    /// The rules are as follows:
281    /// * Entries are expected to be comma-separated (whitespace between entries is ignored)
282    /// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size,
283    ///   for example "`192.168.1.0/24`").
284    /// * An entry "`*`" matches all hostnames (this is the only wildcard allowed)
285    /// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com`
286    ///   and `.google.com` are equivalent) and would match both that domain AND all subdomains.
287    ///
288    /// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all of the following would match
289    /// (and therefore would bypass the proxy):
290    /// * `http://google.com/`
291    /// * `http://www.google.com/`
292    /// * `http://192.168.1.42/`
293    ///
294    /// The URL `http://notgoogle.com/` would not match.
295    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    /// Construct a [`Matcher`] using the configured values.
304    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                // can't use this proxy scheme
350                return None;
351            }
352        }
353        // if no scheme provided, assume they meant 'http'
354        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    // removing any path, but we MUST specify one or the builder errors
383    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    /*
410    fn from_env() -> NoProxy {
411        let raw = std::env::var("NO_PROXY")
412            .or_else(|_| std::env::var("no_proxy"))
413            .unwrap_or_default();
414
415        Self::from_string(&raw)
416    }
417    */
418
419    fn empty() -> NoProxy {
420        NoProxy {
421            ips: IpMatcher(Vec::new()),
422            domains: DomainMatcher(Vec::new()),
423        }
424    }
425
426    /// Returns a new no-proxy configuration based on a `no_proxy` string (or `None` if no variables
427    /// are set)
428    /// The rules are as follows:
429    /// * The environment variable `NO_PROXY` is checked, if it is not set, `no_proxy` is checked
430    /// * If neither environment variable is set, `None` is returned
431    /// * Entries are expected to be comma-separated (whitespace between entries is ignored)
432    /// * IP addresses (both IPv4 and IPv6) are allowed, as are optional subnet masks (by adding /size,
433    ///   for example "`192.168.1.0/24`").
434    /// * An entry "`*`" matches all hostnames (this is the only wildcard allowed)
435    /// * Any other entry is considered a domain name (and may contain a leading dot, for example `google.com`
436    ///   and `.google.com` are equivalent) and would match both that domain AND all subdomains.
437    ///
438    /// For example, if `"NO_PROXY=google.com, 192.168.1.0/24"` was set, all of the following would match
439    /// (and therefore would bypass the proxy):
440    /// * `http://google.com/`
441    /// * `http://www.google.com/`
442    /// * `http://192.168.1.42/`
443    ///
444    /// The URL `http://notgoogle.com/` would not match.
445    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                // If we can parse an IP net or address, then use it, otherwise, assume it is a domain
452                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    /// Return true if this matches the host (domain or IP).
470    pub fn contains(&self, host: &str) -> bool {
471        // According to RFC3986, raw IPv6 hosts will be wrapped in []. So we need to strip those off
472        // the end in order to parse correctly
473        let host = crate::client::strip_ipv6_brackets(host);
474        match host.parse::<IpAddr>() {
475            // If we can parse an IP addr, then use it, otherwise, assume it is a domain
476            Ok(ip) => self.ips.contains(ip),
477            Err(_) => self.domains.contains(host),
478        }
479    }
480
481    fn is_empty(&self) -> bool {
482        self.ips.0.is_empty() && self.domains.0.is_empty()
483    }
484}
485
486impl IpMatcher {
487    fn contains(&self, addr: IpAddr) -> bool {
488        for ip in &self.0 {
489            match ip {
490                Ip::Address(address) => {
491                    if &addr == address {
492                        return true;
493                    }
494                }
495                Ip::Network(net) => {
496                    if net.contains(&addr) {
497                        return true;
498                    }
499                }
500            }
501        }
502        false
503    }
504}
505
506impl DomainMatcher {
507    // The following links may be useful to understand the origin of these rules:
508    // * https://curl.se/libcurl/c/CURLOPT_NOPROXY.html
509    // * https://github.com/curl/curl/issues/1208
510    fn contains(&self, domain: &str) -> bool {
511        let domain_len = domain.len();
512        for d in &self.0 {
513            if d.eq_ignore_ascii_case(domain)
514                || d.strip_prefix('.')
515                    .map_or(false, |s| s.eq_ignore_ascii_case(domain))
516            {
517                return true;
518            } else if domain
519                .get(domain_len.saturating_sub(d.len())..)
520                .map_or(false, |s| s.eq_ignore_ascii_case(d))
521            {
522                if d.starts_with('.') {
523                    // If the first character of d is a dot, that means the first character of domain
524                    // must also be a dot, so we are looking at a subdomain of d and that matches
525                    return true;
526                } else if domain.as_bytes().get(domain_len - d.len() - 1) == Some(&b'.') {
527                    // Given that d is a prefix of domain, if the prior character in domain is a dot
528                    // then that means we must be matching a subdomain of d, and that matches
529                    return true;
530                }
531            } else if d == "*" {
532                return true;
533            }
534        }
535        false
536    }
537}
538
539mod builder {
540    /// A type that can used as a `Builder` value.
541    ///
542    /// Private and sealed, only visible in docs.
543    pub trait IntoValue {
544        #[doc(hidden)]
545        fn into_value(self) -> String;
546    }
547
548    impl IntoValue for String {
549        #[doc(hidden)]
550        fn into_value(self) -> String {
551            self
552        }
553    }
554
555    impl IntoValue for &String {
556        #[doc(hidden)]
557        fn into_value(self) -> String {
558            self.into()
559        }
560    }
561
562    impl IntoValue for &str {
563        #[doc(hidden)]
564        fn into_value(self) -> String {
565            self.into()
566        }
567    }
568}
569
570#[cfg(feature = "client-proxy-system")]
571#[cfg(target_os = "macos")]
572mod mac {
573    use system_configuration::core_foundation::base::CFType;
574    use system_configuration::core_foundation::dictionary::CFDictionary;
575    use system_configuration::core_foundation::number::CFNumber;
576    use system_configuration::core_foundation::string::{CFString, CFStringRef};
577    use system_configuration::dynamic_store::SCDynamicStoreBuilder;
578    use system_configuration::sys::schema_definitions::{
579        kSCPropNetProxiesHTTPEnable, kSCPropNetProxiesHTTPPort, kSCPropNetProxiesHTTPProxy,
580        kSCPropNetProxiesHTTPSEnable, kSCPropNetProxiesHTTPSPort, kSCPropNetProxiesHTTPSProxy,
581    };
582
583    pub(super) fn with_system(builder: &mut super::Builder) {
584        let store = if let Some(store) = SCDynamicStoreBuilder::new("hyper-util").build() {
585            store
586        } else {
587            return;
588        };
589
590        let proxies_map = if let Some(proxies_map) = store.get_proxies() {
591            proxies_map
592        } else {
593            return;
594        };
595
596        if builder.http.is_empty() {
597            let http_proxy_config = parse_setting_from_dynamic_store(
598                &proxies_map,
599                unsafe { kSCPropNetProxiesHTTPEnable },
600                unsafe { kSCPropNetProxiesHTTPProxy },
601                unsafe { kSCPropNetProxiesHTTPPort },
602            );
603            if let Some(http) = http_proxy_config {
604                builder.http = http;
605            }
606        }
607
608        if builder.https.is_empty() {
609            let https_proxy_config = parse_setting_from_dynamic_store(
610                &proxies_map,
611                unsafe { kSCPropNetProxiesHTTPSEnable },
612                unsafe { kSCPropNetProxiesHTTPSProxy },
613                unsafe { kSCPropNetProxiesHTTPSPort },
614            );
615
616            if let Some(https) = https_proxy_config {
617                builder.https = https;
618            }
619        }
620    }
621
622    fn parse_setting_from_dynamic_store(
623        proxies_map: &CFDictionary<CFString, CFType>,
624        enabled_key: CFStringRef,
625        host_key: CFStringRef,
626        port_key: CFStringRef,
627    ) -> Option<String> {
628        let proxy_enabled = proxies_map
629            .find(enabled_key)
630            .and_then(|flag| flag.downcast::<CFNumber>())
631            .and_then(|flag| flag.to_i32())
632            .unwrap_or(0)
633            == 1;
634
635        if proxy_enabled {
636            let proxy_host = proxies_map
637                .find(host_key)
638                .and_then(|host| host.downcast::<CFString>())
639                .map(|host| host.to_string());
640            let proxy_port = proxies_map
641                .find(port_key)
642                .and_then(|port| port.downcast::<CFNumber>())
643                .and_then(|port| port.to_i32());
644
645            return match (proxy_host, proxy_port) {
646                (Some(proxy_host), Some(proxy_port)) => Some(format!("{proxy_host}:{proxy_port}")),
647                (Some(proxy_host), None) => Some(proxy_host),
648                (None, Some(_)) => None,
649                (None, None) => None,
650            };
651        }
652
653        None
654    }
655}
656
657#[cfg(feature = "client-proxy-system")]
658#[cfg(windows)]
659mod win {
660    fn ipv4_wildcard_to_cidr(value: &str) -> Option<String> {
661        let parts = value.split('.').collect::<Vec<_>>();
662        let wildcard = parts.iter().position(|part| *part == "*")?;
663
664        if wildcard == 0 || wildcard > 3 || parts[wildcard..].iter().any(|part| *part != "*") {
665            return None;
666        }
667
668        let mut octets = [0; 4];
669        for (index, part) in parts[..wildcard].iter().enumerate() {
670            octets[index] = part.parse().ok()?;
671        }
672
673        Some(format!(
674            "{}.{}.{}.{}/{}",
675            octets[0],
676            octets[1],
677            octets[2],
678            octets[3],
679            wildcard * 8
680        ))
681    }
682
683    pub(super) fn normalize_proxy_override(value: &str) -> String {
684        value
685            .split(';')
686            .map(|entry| {
687                let entry = entry.trim();
688                ipv4_wildcard_to_cidr(entry).unwrap_or_else(|| entry.to_string())
689            })
690            .collect::<Vec<_>>()
691            .join(",")
692            .replace("*.", "")
693    }
694
695    pub(super) fn with_system(builder: &mut super::Builder) {
696        let settings = if let Ok(settings) = windows_registry::CURRENT_USER
697            .open("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
698        {
699            settings
700        } else {
701            return;
702        };
703
704        if settings.get_u32("ProxyEnable").unwrap_or(0) == 0 {
705            return;
706        }
707
708        if let Ok(val) = settings.get_string("ProxyServer") {
709            if builder.http.is_empty() {
710                builder.http = val.clone();
711            }
712            if builder.https.is_empty() {
713                builder.https = val;
714            }
715        }
716
717        if builder.no.is_empty() {
718            if let Ok(val) = settings.get_string("ProxyOverride") {
719                builder.no = normalize_proxy_override(&val);
720            }
721        }
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    #[test]
730    fn test_domain_matcher() {
731        let domains = vec![".foo.bar".into(), "bar.foo".into()];
732        let matcher = DomainMatcher(domains);
733
734        // domains match with leading `.`
735        assert!(matcher.contains("foo.bar"));
736        assert!(matcher.contains("FOO.BAR"));
737
738        // subdomains match with leading `.`
739        assert!(matcher.contains("www.foo.bar"));
740        assert!(matcher.contains("WWW.FOO.BAR"));
741
742        // domains match with no leading `.`
743        assert!(matcher.contains("bar.foo"));
744        assert!(matcher.contains("Bar.foo"));
745
746        // subdomains match with no leading `.`
747        assert!(matcher.contains("www.bar.foo"));
748        assert!(matcher.contains("WWW.BAR.FOO"));
749
750        // non-subdomain string prefixes don't match
751        assert!(!matcher.contains("notfoo.bar"));
752        assert!(!matcher.contains("notbar.foo"));
753    }
754
755    #[test]
756    fn test_no_proxy_wildcard() {
757        let no_proxy = NoProxy::from_string("*");
758        assert!(no_proxy.contains("any.where"));
759    }
760
761    #[test]
762    fn test_no_proxy_ip_ranges() {
763        let no_proxy =
764            NoProxy::from_string(".foo.bar, bar.baz,10.42.1.1/24,::1,10.124.7.8,2001::/17");
765
766        let should_not_match = [
767            // random url, not in no_proxy
768            "hyper.rs",
769            // make sure that random non-subdomain string prefixes don't match
770            "notfoo.bar",
771            // make sure that random non-subdomain string prefixes don't match
772            "notbar.baz",
773            // ipv4 address out of range
774            "10.43.1.1",
775            // ipv4 address out of range
776            "10.124.7.7",
777            // ipv6 address out of range
778            "[ffff:db8:a0b:12f0::1]",
779            // ipv6 address out of range
780            "[2005:db8:a0b:12f0::1]",
781        ];
782
783        for host in &should_not_match {
784            assert!(!no_proxy.contains(host), "should not contain {host:?}");
785        }
786
787        let should_match = [
788            // make sure subdomains (with leading .) match
789            "hello.foo.bar",
790            // make sure exact matches (without leading .) match (also makes sure spaces between entries work)
791            "bar.baz",
792            // make sure subdomains (without leading . in no_proxy) match
793            "foo.bar.baz",
794            // make sure subdomains (without leading . in no_proxy) match - this differs from cURL
795            "foo.bar",
796            // ipv4 address match within range
797            "10.42.1.100",
798            // ipv6 address exact match
799            "[::1]",
800            // ipv6 address match within range
801            "[2001:db8:a0b:12f0::1]",
802            // ipv4 address exact match
803            "10.124.7.8",
804        ];
805
806        for host in &should_match {
807            assert!(no_proxy.contains(host), "should contain {host:?}");
808        }
809    }
810
811    #[cfg(all(feature = "client-proxy-system", windows))]
812    #[test]
813    fn test_windows_proxy_override_ip_wildcard() {
814        let normalized =
815            win::normalize_proxy_override("127.*; 10.*.*.*; 192.168.*; 192.168.1.*; *.example.com");
816        let no_proxy = NoProxy::from_string(&normalized);
817
818        assert!(no_proxy.contains("127.0.0.1"));
819        assert!(no_proxy.contains("10.12.34.56"));
820        assert!(no_proxy.contains("192.168.42.1"));
821        assert!(no_proxy.contains("192.168.1.42"));
822        assert!(no_proxy.contains("www.example.com"));
823        assert!(!no_proxy.contains("128.0.0.1"));
824        assert!(!no_proxy.contains("192.169.42.1"));
825
826        let subnet = NoProxy::from_string(&win::normalize_proxy_override("192.168.1.*"));
827        assert!(!subnet.contains("192.168.2.42"));
828    }
829
830    macro_rules! p {
831        ($($n:ident = $v:expr,)*) => ({Builder {
832            $($n: $v.into(),)*
833            ..Builder::default()
834        }.build()});
835    }
836
837    fn intercept(p: &Matcher, u: &str) -> Intercept {
838        p.intercept(&u.parse().unwrap()).unwrap()
839    }
840
841    #[test]
842    fn test_all_proxy() {
843        let p = p! {
844            all = "http://om.nom",
845        };
846
847        assert_eq!("http://om.nom", intercept(&p, "http://example.com").uri());
848
849        assert_eq!("http://om.nom", intercept(&p, "https://example.com").uri());
850    }
851
852    #[test]
853    fn test_specific_overrides_all() {
854        let p = p! {
855            all = "http://no.pe",
856            http = "http://y.ep",
857        };
858
859        assert_eq!("http://no.pe", intercept(&p, "https://example.com").uri());
860
861        // the http rule is "more specific" than the all rule
862        assert_eq!("http://y.ep", intercept(&p, "http://example.com").uri());
863    }
864
865    #[test]
866    fn test_parse_no_scheme_defaults_to_http() {
867        let p = p! {
868            https = "y.ep",
869            http = "127.0.0.1:8887",
870        };
871
872        assert_eq!(intercept(&p, "https://example.local").uri(), "http://y.ep");
873        assert_eq!(
874            intercept(&p, "http://example.local").uri(),
875            "http://127.0.0.1:8887"
876        );
877    }
878
879    #[test]
880    fn test_parse_http_auth() {
881        let p = p! {
882            all = "http://Aladdin:opensesame@y.ep",
883        };
884
885        let proxy = intercept(&p, "https://example.local");
886        assert_eq!(proxy.uri(), "http://y.ep");
887        assert_eq!(
888            proxy.basic_auth().expect("basic_auth"),
889            "Basic QWxhZGRpbjpvcGVuc2VzYW1l"
890        );
891    }
892
893    #[test]
894    fn test_parse_http_auth_without_password() {
895        let p = p! {
896            all = "http://Aladdin@y.ep",
897        };
898        let proxy = intercept(&p, "https://example.local");
899        assert_eq!(proxy.uri(), "http://y.ep");
900        assert_eq!(
901            proxy.basic_auth().expect("basic_auth"),
902            "Basic QWxhZGRpbjo="
903        );
904    }
905
906    #[test]
907    fn test_parse_http_auth_without_scheme() {
908        let p = p! {
909            all = "Aladdin:opensesame@y.ep",
910        };
911
912        let proxy = intercept(&p, "https://example.local");
913        assert_eq!(proxy.uri(), "http://y.ep");
914        assert_eq!(
915            proxy.basic_auth().expect("basic_auth"),
916            "Basic QWxhZGRpbjpvcGVuc2VzYW1l"
917        );
918    }
919
920    #[test]
921    fn test_dont_parse_http_when_is_cgi() {
922        let mut builder = Matcher::builder();
923        builder.is_cgi = true;
924        builder.http = "http://never.gonna.let.you.go".into();
925        let m = builder.build();
926
927        assert!(m.intercept(&"http://rick.roll".parse().unwrap()).is_none());
928    }
929
930    #[test]
931    fn test_domain_matcher_case_insensitive() {
932        let domains = vec![".foo.bar".into()];
933        let matcher = DomainMatcher(domains);
934
935        assert!(matcher.contains("foo.bar"));
936        assert!(matcher.contains("FOO.BAR"));
937        assert!(matcher.contains("Foo.Bar"));
938
939        assert!(matcher.contains("www.foo.bar"));
940        assert!(matcher.contains("WWW.FOO.BAR"));
941        assert!(matcher.contains("Www.Foo.Bar"));
942    }
943
944    #[test]
945    fn test_no_proxy_case_insensitive() {
946        let p = p! {
947            all = "http://proxy.local",
948            no = ".example.com",
949        };
950
951        // should bypass proxy (case insensitive match)
952        assert!(
953            p.intercept(&"http://example.com".parse().unwrap())
954                .is_none()
955        );
956        assert!(
957            p.intercept(&"http://EXAMPLE.COM".parse().unwrap())
958                .is_none()
959        );
960        assert!(
961            p.intercept(&"http://Example.com".parse().unwrap())
962                .is_none()
963        );
964
965        // subdomain should bypass proxy (case insensitive match)
966        assert!(
967            p.intercept(&"http://www.example.com".parse().unwrap())
968                .is_none()
969        );
970        assert!(
971            p.intercept(&"http://WWW.EXAMPLE.COM".parse().unwrap())
972                .is_none()
973        );
974        assert!(
975            p.intercept(&"http://Www.Example.Com".parse().unwrap())
976                .is_none()
977        );
978    }
979}