[go: up one dir, main page]

ureq/
proxy.rs

1use std::convert::{TryFrom, TryInto};
2use std::fmt;
3use std::sync::Arc;
4use ureq_proto::http::uri::{PathAndQuery, Scheme};
5
6use http::Uri;
7
8use crate::http;
9use crate::util::{AuthorityExt, DebugUri};
10use crate::Error;
11
12/// Proxy protocol
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
14#[non_exhaustive]
15pub enum ProxyProtocol {
16    /// CONNECT proxy over HTTP
17    Http,
18    /// CONNECT proxy over HTTPS
19    Https,
20    /// A SOCKS4 proxy
21    Socks4,
22    /// A SOCKS4a proxy (proxy can resolve domain name)
23    Socks4A,
24    /// SOCKS5 proxy
25    Socks5,
26}
27
28impl ProxyProtocol {
29    pub(crate) fn default_port(&self) -> u16 {
30        match self {
31            ProxyProtocol::Http => 80,
32            ProxyProtocol::Https => 443,
33            ProxyProtocol::Socks4 | ProxyProtocol::Socks4A | ProxyProtocol::Socks5 => 1080,
34        }
35    }
36
37    pub(crate) fn is_socks(&self) -> bool {
38        matches!(self, Self::Socks4 | Self::Socks4A | Self::Socks5)
39    }
40
41    pub(crate) fn is_connect(&self) -> bool {
42        matches!(self, Self::Http | Self::Https)
43    }
44
45    fn default_resolve_target(&self) -> bool {
46        match self {
47            ProxyProtocol::Http => false,
48            ProxyProtocol::Https => false,
49            ProxyProtocol::Socks4 => true, // we must locally resolve before using proxy
50            ProxyProtocol::Socks4A => false,
51            ProxyProtocol::Socks5 => false,
52        }
53    }
54}
55
56/// Proxy server settings
57///
58/// This struct represents a proxy server configuration that can be used to route HTTP/HTTPS
59/// requests through a proxy server. It supports various proxy protocols including HTTP CONNECT,
60/// HTTPS CONNECT, SOCKS4, SOCKS4A, and SOCKS5.
61///
62/// # Protocol Support
63///
64/// * `HTTP`: HTTP CONNECT proxy
65/// * `HTTPS`: HTTPS CONNECT proxy (requires a TLS provider)
66/// * `SOCKS4`: SOCKS4 proxy (requires **socks-proxy** feature)
67/// * `SOCKS4A`: SOCKS4A proxy (requires **socks-proxy** feature)
68/// * `SOCKS5`: SOCKS5 proxy (requires **socks-proxy** feature)
69///
70/// # DNS Resolution
71///
72/// The `resolve_target` setting controls where DNS resolution happens:
73///
74/// * When `true`: DNS resolution happens locally before connecting to the proxy.
75///   The resolved IP address is sent to the proxy.
76/// * When `false`: The hostname is sent to the proxy, which performs DNS resolution.
77///
78/// Default behavior:
79/// * For SOCKS4: `true` (local resolution required)
80/// * For all other protocols: `false` (proxy performs resolution)
81///
82/// # Examples
83///
84/// ```rust
85/// use ureq::{Proxy, ProxyProtocol};
86///
87/// // Create a proxy from a URI string
88/// let proxy = Proxy::new("http://localhost:8080").unwrap();
89///
90/// // Create a proxy using the builder pattern
91/// let proxy = Proxy::builder(ProxyProtocol::Socks5)
92///     .host("proxy.example.com")
93///     .port(1080)
94///     .username("user")
95///     .password("pass")
96///     .resolve_target(true)  // Force local DNS resolution
97///     .build()
98///     .unwrap();
99///
100/// // Read proxy settings from environment variables
101/// if let Some(proxy) = Proxy::try_from_env() {
102///     // Use proxy from environment
103/// }
104/// ```
105#[derive(Clone, Eq, Hash, PartialEq)]
106pub struct Proxy {
107    inner: Arc<ProxyInner>,
108}
109
110#[derive(Eq, Hash, PartialEq)]
111struct ProxyInner {
112    proto: ProxyProtocol,
113    uri: Uri,
114    from_env: bool,
115    resolve_target: bool,
116    no_proxy: Option<NoProxy>,
117}
118
119impl Proxy {
120    /// Create a proxy from a uri.
121    ///
122    /// # Arguments:
123    ///
124    /// * `proxy` - a str of format `<protocol>://<user>:<password>@<host>:port` . All parts
125    ///   except host are optional.
126    ///
127    /// ###  Protocols
128    ///
129    /// * `http`: HTTP CONNECT proxy
130    /// * `https`: HTTPS CONNECT proxy (requires a TLS provider)
131    /// * `socks4`: SOCKS4 (requires **socks-proxy** feature)
132    /// * `socks4a`: SOCKS4A (requires **socks-proxy** feature)
133    /// * `socks5` and `socks`: SOCKS5 (requires **socks-proxy** feature)
134    ///
135    /// # Examples proxy formats
136    ///
137    /// * `http://127.0.0.1:8080`
138    /// * `socks5://john:smith@socks.google.com`
139    /// * `john:smith@socks.google.com:8000`
140    /// * `localhost`
141    pub fn new(proxy: &str) -> Result<Self, Error> {
142        Self::new_with_flag(proxy, None, false, None)
143    }
144
145    /// Creates a proxy config using a builder.
146    pub fn builder(p: ProxyProtocol) -> ProxyBuilder {
147        ProxyBuilder {
148            protocol: p,
149            host: None,
150            port: None,
151            username: None,
152            password: None,
153            resolve_target: p.default_resolve_target(),
154            no_proxy: None,
155        }
156    }
157
158    fn new_with_flag(
159        proxy: &str,
160        no_proxy: Option<NoProxy>,
161        from_env: bool,
162        resolve_target: Option<bool>,
163    ) -> Result<Self, Error> {
164        let mut uri = proxy.parse::<Uri>().or(Err(Error::InvalidProxyUrl))?;
165
166        // The uri must have an authority part (with the host), or
167        // it is invalid.
168        let _ = uri.authority().ok_or(Error::InvalidProxyUrl)?;
169
170        let scheme = match uri.scheme_str() {
171            Some(v) => v,
172            None => {
173                // The default protocol is Proto::HTTP, and it is missing in
174                // the uri. Let's put it in place.
175                uri = insert_default_scheme(uri);
176                "http"
177            }
178        };
179
180        let proto: ProxyProtocol = scheme.try_into()?;
181        let resolve_target = resolve_target.unwrap_or(proto.default_resolve_target());
182
183        let inner = ProxyInner {
184            proto,
185            uri,
186            from_env,
187            resolve_target,
188            no_proxy,
189        };
190
191        Ok(Self {
192            inner: Arc::new(inner),
193        })
194    }
195
196    /// Read proxy settings from environment variables.
197    ///
198    /// The environment variable is expected to contain a proxy URI. The following
199    /// environment variables are attempted:
200    ///
201    /// * `ALL_PROXY`
202    /// * `HTTPS_PROXY`
203    /// * `HTTP_PROXY`
204    ///
205    /// Additionally, the `NO_PROXY` environment variable is automatically read to determine
206    /// which hosts should bypass the proxy. This supports various pattern types including
207    /// exact hostnames, wildcard suffixes, and dot suffixes.
208    ///
209    /// Returns `None` if no environment variable is set or the URI is invalid.
210    pub fn try_from_env() -> Option<Self> {
211        const TRY_ENV: &[&str] = &[
212            "ALL_PROXY",
213            "all_proxy",
214            "HTTPS_PROXY",
215            "https_proxy",
216            "HTTP_PROXY",
217            "http_proxy",
218        ];
219
220        for attempt in TRY_ENV {
221            if let Ok(env) = std::env::var(attempt) {
222                let no_proxy = NoProxy::try_from_env();
223                if let Ok(proxy) = Self::new_with_flag(&env, no_proxy, true, None) {
224                    return Some(proxy);
225                }
226            }
227        }
228
229        None
230    }
231
232    /// The configured protocol.
233    pub fn protocol(&self) -> ProxyProtocol {
234        self.inner.proto
235    }
236
237    /// The proxy uri
238    pub fn uri(&self) -> &Uri {
239        &self.inner.uri
240    }
241
242    /// The host part of the proxy uri
243    pub fn host(&self) -> &str {
244        self.inner
245            .uri
246            .authority()
247            .map(|a| a.host())
248            .expect("constructor to ensure there is an authority")
249    }
250
251    /// The port of the proxy uri
252    pub fn port(&self) -> u16 {
253        self.inner
254            .uri
255            .authority()
256            .and_then(|a| a.port_u16())
257            .unwrap_or_else(|| self.inner.proto.default_port())
258    }
259
260    /// The username of the proxy uri
261    pub fn username(&self) -> Option<&str> {
262        self.inner.uri.authority().and_then(|a| a.username())
263    }
264
265    /// The password of the proxy uri
266    pub fn password(&self) -> Option<&str> {
267        self.inner.uri.authority().and_then(|a| a.password())
268    }
269
270    /// Whether this proxy setting was created manually or from
271    /// environment variables.
272    pub fn is_from_env(&self) -> bool {
273        self.inner.from_env
274    }
275
276    /// Whether to resolve target locally before calling the proxy.
277    ///
278    /// * `true` - resolve the DNS before calling proxy.
279    /// * `false` - send the target host to the proxy and let it resolve.
280    ///
281    /// Defaults to `false` for all proxies protocols except `SOCKS4`. I.e. the normal
282    /// case is to let the proxy resolve the target host.
283    pub fn resolve_target(&self) -> bool {
284        self.inner.resolve_target
285    }
286
287    /// Tells if this entry matches anything on the NO_PROXY list.
288    ///
289    /// This method is used by Proxy Connectors to decide if a connection to the given host
290    /// should be routed through the proxy or established directly.
291    ///
292    /// * `false` - The connection should be routed through the proxy connector
293    /// * `true` - The connection should bypass the proxy and connect directly to the host
294    pub fn is_no_proxy(&self, uri: &Uri) -> bool {
295        if let (Some(no_proxy), Some(host)) = (&self.inner.no_proxy, uri.host()) {
296            return no_proxy.is_no_proxy(host);
297        }
298        false
299    }
300}
301
302fn insert_default_scheme(uri: Uri) -> Uri {
303    let mut parts = uri.into_parts();
304
305    parts.scheme = Some(Scheme::HTTP);
306
307    // For some reason uri.into_parts can produce None for
308    // the path, but Uri::from_parts does not accept that.
309    parts.path_and_query = parts
310        .path_and_query
311        .or_else(|| Some(PathAndQuery::from_static("/")));
312
313    Uri::from_parts(parts).unwrap()
314}
315
316/// Builder for configuring a proxy.
317///
318/// Obtained via [`Proxy::builder()`].
319pub struct ProxyBuilder {
320    protocol: ProxyProtocol,
321    host: Option<String>,
322    port: Option<u16>,
323    username: Option<String>,
324    password: Option<String>,
325    resolve_target: bool,
326    no_proxy: Option<NoProxy>,
327}
328
329impl ProxyBuilder {
330    /// Set the proxy hostname
331    ///
332    /// Defaults to `localhost`. Invalid hostnames surface in [`ProxyBuilder::build()`].
333    pub fn host(mut self, host: &str) -> Self {
334        self.host = Some(host.to_string());
335        self
336    }
337
338    /// Set the proxy port
339    ///
340    /// Defaults to whatever is default for the chosen [`ProxyProtocol`].
341    pub fn port(mut self, port: u16) -> Self {
342        self.port = Some(port);
343        self
344    }
345
346    /// Set the username
347    ///
348    /// Defaults to none. Invalid usernames surface in [`ProxyBuilder::build()`].
349    pub fn username(mut self, v: &str) -> Self {
350        self.username = Some(v.to_string());
351        self
352    }
353
354    /// Set the password
355    ///
356    /// If you want to set only a password, no username, i.e. `https://secret@foo.com`,
357    /// you need to set it as [`ProxyBuilder::username()`].
358    ///
359    /// Defaults to none.  Invalid passwords surface in [`ProxyBuilder::build()`].
360    pub fn password(mut self, v: &str) -> Self {
361        self.password = Some(v.to_string());
362        self
363    }
364
365    /// Whether to resolve the target host locally before calling the proxy.
366    ///
367    /// * `true` - resolve target host locally before calling proxy.
368    /// * `false` - let proxy resolve the host.
369    ///
370    /// For SOCKS4, this defaults to `true`, for all other protocols `false`. I.e.
371    /// in the "normal" case, we let the proxy itself resolve host names.
372    pub fn resolve_target(mut self, do_resolve: bool) -> Self {
373        self.resolve_target = do_resolve;
374        self
375    }
376
377    /// Add a NO_PROXY expression to not route proxy through.
378    ///
379    /// Correct expressions are:
380    ///
381    /// * `example.com` -> Literally match `example.com`, but not `sub.example.com`
382    /// * `.example.com` -> Match `sub.example.com` and `foo.sub.example.com`, but not `example.com`.
383    /// * `*.example.com` -> Exactly like `.example.com`
384    /// * `*` -> Match everything
385    ///
386    /// Silently ignores expressions that are not on the above form.
387    pub fn no_proxy(mut self, expr: &str) -> Self {
388        if let Some(entry) = NoProxyEntry::try_parse(expr) {
389            if self.no_proxy.is_none() {
390                self.no_proxy = Some(NoProxy::default());
391            }
392            self.no_proxy.as_mut().unwrap().inner.push(entry);
393        }
394
395        self
396    }
397
398    /// Construct the [`Proxy`]
399    pub fn build(self) -> Result<Proxy, Error> {
400        let host = self.host.as_deref().unwrap_or("localhost");
401        let port = self.port.unwrap_or(self.protocol.default_port());
402
403        let mut userpass = String::new();
404        if let Some(username) = self.username {
405            userpass.push_str(&username);
406            if let Some(password) = self.password {
407                userpass.push(':');
408                userpass.push_str(&password);
409            }
410            userpass.push('@');
411        }
412
413        // TODO(martin): This incurs as a somewhat unnecessary allocation, but we get some
414        // validation and normalization in new_with_flag. This could be refactored
415        // in the future.
416        let proxy = format!("{}://{}{}:{}", self.protocol, userpass, host, port);
417        Proxy::new_with_flag(&proxy, self.no_proxy, false, Some(self.resolve_target))
418    }
419}
420
421impl TryFrom<&str> for ProxyProtocol {
422    type Error = Error;
423
424    fn try_from(scheme: &str) -> Result<Self, Self::Error> {
425        match scheme.to_ascii_lowercase().as_str() {
426            "http" => Ok(ProxyProtocol::Http),
427            "https" => Ok(ProxyProtocol::Https),
428            "socks4" => Ok(ProxyProtocol::Socks4),
429            "socks4a" => Ok(ProxyProtocol::Socks4A),
430            "socks" => Ok(ProxyProtocol::Socks5),
431            "socks5" => Ok(ProxyProtocol::Socks5),
432            _ => Err(Error::InvalidProxyUrl),
433        }
434    }
435}
436
437impl fmt::Debug for Proxy {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        f.debug_struct("Proxy")
440            .field("proto", &self.inner.proto)
441            .field("uri", &DebugUri(&self.inner.uri))
442            .field("from_env", &self.inner.from_env)
443            .finish()
444    }
445}
446
447impl fmt::Display for ProxyProtocol {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        match self {
450            ProxyProtocol::Http => write!(f, "HTTP"),
451            ProxyProtocol::Https => write!(f, "HTTPS"),
452            ProxyProtocol::Socks4 => write!(f, "SOCKS4"),
453            ProxyProtocol::Socks4A => write!(f, "SOCKS4a"),
454            ProxyProtocol::Socks5 => write!(f, "SOCKS5"),
455        }
456    }
457}
458
459#[derive(Debug, Clone, Eq, PartialEq, Hash)]
460enum NoProxyEntry {
461    ExactHost(String),
462    HostSuffix(String),
463    MatchAll,
464}
465
466#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
467struct NoProxy {
468    inner: Vec<NoProxyEntry>,
469}
470
471impl NoProxy {
472    /// Read no proxy settings from environment variables.
473    ///
474    /// The environment variable is expected to contain values separated by comma. The following
475    /// environment variables are attempted:
476    ///
477    /// * `NO_PROXY`
478    /// * `no_proxy`
479    ///
480    /// ## Supported Pattern Types
481    ///
482    /// * **Exact match**: `localhost`, `127.0.0.1` - matches the exact hostname (case-insensitive)
483    /// * **Wildcard suffix**: `*.example.com` - matches any subdomain of example.com
484    /// * **Dot suffix**: `.example.com` - matches any subdomain of example.com (but not example.com itself)
485    /// * **Match all**: `*` - bypasses proxy for all requests
486    ///
487    /// ## Examples
488    ///
489    /// ```bash
490    /// # Bypass proxy for localhost and internal domains
491    /// export NO_PROXY=localhost,127.0.0.1,*.internal.com
492    ///
493    /// # Bypass proxy for staging subdomains but not staging itself
494    /// export NO_PROXY=.staging
495    ///
496    /// # Bypass proxy for everything
497    /// export NO_PROXY=*
498    /// ```
499    ///
500    /// Returns `None` if no environment variable is set
501    pub fn try_from_env() -> Option<Self> {
502        const TRY_ENV: &[&str] = &["NO_PROXY", "no_proxy"];
503
504        for attempt in TRY_ENV {
505            if let Ok(env) = std::env::var(attempt) {
506                let inner = env.split(',').filter_map(NoProxyEntry::try_parse).collect();
507                return Some(Self { inner });
508            }
509        }
510
511        None
512    }
513
514    pub fn is_no_proxy(&self, host: &str) -> bool {
515        self.inner.iter().any(|entry| entry.matches(host))
516    }
517}
518
519impl NoProxyEntry {
520    fn try_parse(u: &str) -> Option<Self> {
521        let entry = match u {
522            "*" => Self::MatchAll,
523            u if u.starts_with("*") => {
524                Self::HostSuffix(u.chars().skip(1).collect::<String>().to_ascii_lowercase())
525            }
526            u if u.starts_with(".") => Self::HostSuffix(u.to_ascii_lowercase()),
527            _ => Self::ExactHost(u.to_ascii_lowercase()),
528        };
529        Some(entry)
530    }
531
532    fn matches(&self, host: &str) -> bool {
533        match self {
534            NoProxyEntry::MatchAll => true,
535            NoProxyEntry::ExactHost(pattern) => {
536                // Fast path: if host is already lowercase, do direct comparison
537                if host.chars().all(|c| !c.is_ascii_uppercase()) {
538                    pattern == host
539                } else {
540                    // Slow path: convert host to lowercase and compare
541                    pattern == &host.to_ascii_lowercase()
542                }
543            }
544            NoProxyEntry::HostSuffix(suffix) => {
545                if host.len() < suffix.len() {
546                    return false;
547                }
548                let host_suffix = &host[host.len() - suffix.len()..];
549                // Fast path: if host suffix is already lowercase, do direct comparison
550                if host_suffix.chars().all(|c| !c.is_ascii_uppercase()) {
551                    suffix == host_suffix
552                } else {
553                    // Slow path: convert host suffix to lowercase and compare
554                    suffix == &host_suffix.to_ascii_lowercase()
555                }
556            }
557        }
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use assert_no_alloc::*;
564    use std::str::FromStr;
565
566    use super::*;
567
568    #[test]
569    fn parse_proxy_fakeproto() {
570        assert!(Proxy::new("fakeproto://localhost").is_err());
571    }
572
573    #[test]
574    fn parse_proxy_http_user_pass_server_port() {
575        let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999").unwrap();
576        assert_eq!(proxy.username(), Some("user"));
577        assert_eq!(proxy.password(), Some("p@ssw0rd"));
578        assert_eq!(proxy.host(), "localhost");
579        assert_eq!(proxy.port(), 9999);
580        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
581    }
582
583    #[test]
584    fn parse_proxy_http_user_pass_server_port_trailing_slash() {
585        let proxy = Proxy::new("http://user:p@ssw0rd@localhost:9999/").unwrap();
586        assert_eq!(proxy.username(), Some("user"));
587        assert_eq!(proxy.password(), Some("p@ssw0rd"));
588        assert_eq!(proxy.host(), "localhost");
589        assert_eq!(proxy.port(), 9999);
590        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
591    }
592
593    #[test]
594    fn parse_proxy_socks4_user_pass_server_port() {
595        let proxy = Proxy::new("socks4://user:p@ssw0rd@localhost:9999").unwrap();
596        assert_eq!(proxy.username(), Some("user"));
597        assert_eq!(proxy.password(), Some("p@ssw0rd"));
598        assert_eq!(proxy.host(), "localhost");
599        assert_eq!(proxy.port(), 9999);
600        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4);
601    }
602
603    #[test]
604    fn parse_proxy_socks4a_user_pass_server_port() {
605        let proxy = Proxy::new("socks4a://user:p@ssw0rd@localhost:9999").unwrap();
606        assert_eq!(proxy.username(), Some("user"));
607        assert_eq!(proxy.password(), Some("p@ssw0rd"));
608        assert_eq!(proxy.host(), "localhost");
609        assert_eq!(proxy.port(), 9999);
610        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks4A);
611    }
612
613    #[test]
614    fn parse_proxy_socks_user_pass_server_port() {
615        let proxy = Proxy::new("socks://user:p@ssw0rd@localhost:9999").unwrap();
616        assert_eq!(proxy.username(), Some("user"));
617        assert_eq!(proxy.password(), Some("p@ssw0rd"));
618        assert_eq!(proxy.host(), "localhost");
619        assert_eq!(proxy.port(), 9999);
620        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5);
621    }
622
623    #[test]
624    fn parse_proxy_socks5_user_pass_server_port() {
625        let proxy = Proxy::new("socks5://user:p@ssw0rd@localhost:9999").unwrap();
626        assert_eq!(proxy.username(), Some("user"));
627        assert_eq!(proxy.password(), Some("p@ssw0rd"));
628        assert_eq!(proxy.host(), "localhost");
629        assert_eq!(proxy.port(), 9999);
630        assert_eq!(proxy.inner.proto, ProxyProtocol::Socks5);
631    }
632
633    #[test]
634    fn parse_proxy_user_pass_server_port() {
635        let proxy = Proxy::new("user:p@ssw0rd@localhost:9999").unwrap();
636        assert_eq!(proxy.username(), Some("user"));
637        assert_eq!(proxy.password(), Some("p@ssw0rd"));
638        assert_eq!(proxy.host(), "localhost");
639        assert_eq!(proxy.port(), 9999);
640        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
641    }
642
643    #[test]
644    fn parse_proxy_server_port() {
645        let proxy = Proxy::new("localhost:9999").unwrap();
646        assert_eq!(proxy.username(), None);
647        assert_eq!(proxy.password(), None);
648        assert_eq!(proxy.host(), "localhost");
649        assert_eq!(proxy.port(), 9999);
650        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
651    }
652
653    #[test]
654    fn parse_proxy_server() {
655        let proxy = Proxy::new("localhost").unwrap();
656        assert_eq!(proxy.username(), None);
657        assert_eq!(proxy.password(), None);
658        assert_eq!(proxy.host(), "localhost");
659        assert_eq!(proxy.port(), 80);
660        assert_eq!(proxy.inner.proto, ProxyProtocol::Http);
661    }
662
663    #[test]
664    fn no_proxy_exact_host_matching() {
665        let p = Proxy::builder(ProxyProtocol::Http)
666            .host("proxy.example.com")
667            .port(8080)
668            .no_proxy("localhost")
669            .no_proxy("127.0.0.1")
670            .no_proxy("api.internal.com")
671            .build()
672            .unwrap();
673
674        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
675            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
676            p.is_no_proxy(&uri)
677        }
678
679        // Should match exact hosts
680        assert!(is_no_proxy(&p, "localhost"));
681        assert!(is_no_proxy(&p, "127.0.0.1"));
682        assert!(is_no_proxy(&p, "api.internal.com"));
683
684        // Should not match partial or different hosts
685        assert!(!is_no_proxy(&p, "mylocalhost"));
686        assert!(!is_no_proxy(&p, "localhost.example.com"));
687        assert!(!is_no_proxy(&p, "127.0.0.2"));
688        assert!(!is_no_proxy(&p, "api.internal.com.evil.com"));
689        assert!(!is_no_proxy(&p, "docs.rs"));
690    }
691
692    #[test]
693    fn no_proxy_wildcard_suffix_matching() {
694        let p = Proxy::builder(ProxyProtocol::Http)
695            .host("proxy.example.com")
696            .port(8080)
697            .no_proxy("*.internal.com")
698            .no_proxy("*.dev")
699            .build()
700            .unwrap();
701
702        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
703            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
704            p.is_no_proxy(&uri)
705        }
706
707        // Should match wildcard suffixes
708        assert!(is_no_proxy(&p, "api.internal.com"));
709        assert!(is_no_proxy(&p, "auth.internal.com"));
710        assert!(is_no_proxy(&p, "db.internal.com"));
711        assert!(is_no_proxy(&p, "app.dev"));
712        assert!(is_no_proxy(&p, "test.dev"));
713
714        // Should not match the bare suffix or unrelated hosts
715        assert!(!is_no_proxy(&p, "internal.com"));
716        assert!(!is_no_proxy(&p, "dev"));
717        assert!(!is_no_proxy(&p, "api.external.com"));
718        assert!(!is_no_proxy(&p, "app.prod"));
719        assert!(!is_no_proxy(&p, "docs.rs"));
720    }
721
722    #[test]
723    fn no_proxy_dot_suffix_matching() {
724        let p = Proxy::builder(ProxyProtocol::Http)
725            .host("proxy.example.com")
726            .port(8080)
727            .no_proxy(".internal.com")
728            .no_proxy(".staging")
729            .build()
730            .unwrap();
731
732        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
733            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
734            p.is_no_proxy(&uri)
735        }
736
737        // Should match dot suffix patterns (only subdomains, not the domain itself)
738        assert!(is_no_proxy(&p, "api.internal.com"));
739        assert!(is_no_proxy(&p, "auth.internal.com"));
740        assert!(is_no_proxy(&p, "db.sub.internal.com"));
741        assert!(is_no_proxy(&p, "app.staging"));
742        assert!(is_no_proxy(&p, "test.staging"));
743
744        // Should NOT match the bare domain (key difference from wildcard)
745        assert!(!is_no_proxy(&p, "internal.com"));
746        assert!(!is_no_proxy(&p, "staging"));
747
748        // Should not match unrelated hosts
749        assert!(!is_no_proxy(&p, "api.external.com"));
750        assert!(!is_no_proxy(&p, "prod"));
751        assert!(!is_no_proxy(&p, "docs.rs"));
752    }
753
754    #[test]
755    fn no_proxy_match_all_wildcard() {
756        let p = Proxy::builder(ProxyProtocol::Http)
757            .host("proxy.example.com")
758            .port(8080)
759            .no_proxy("*")
760            .build()
761            .unwrap();
762
763        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
764            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
765            p.is_no_proxy(&uri)
766        }
767
768        // Should match everything when using "*"
769        assert!(is_no_proxy(&p, "localhost"));
770        assert!(is_no_proxy(&p, "127.0.0.1"));
771        assert!(is_no_proxy(&p, "api.example.com"));
772        assert!(is_no_proxy(&p, "docs.rs"));
773        assert!(is_no_proxy(&p, "github.com"));
774        assert!(is_no_proxy(&p, "any.random.domain"));
775    }
776
777    #[test]
778    fn no_proxy_mixed_patterns() {
779        let p = Proxy::builder(ProxyProtocol::Http)
780            .host("proxy.example.com")
781            .port(8080)
782            .no_proxy("localhost") // exact host
783            .no_proxy("*.dev") // wildcard suffix
784            .no_proxy(".staging") // dot suffix
785            .no_proxy("127.0.0.1") // exact IP
786            .build()
787            .unwrap();
788
789        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
790            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
791            p.is_no_proxy(&uri)
792        }
793
794        // Should match exact hosts
795        assert!(is_no_proxy(&p, "localhost"));
796        assert!(is_no_proxy(&p, "127.0.0.1"));
797
798        // Should match wildcard suffixes
799        assert!(is_no_proxy(&p, "api.dev"));
800        assert!(is_no_proxy(&p, "test.dev"));
801
802        // Should match dot suffixes (only subdomains, not the domain itself)
803        assert!(is_no_proxy(&p, "app.staging"));
804        assert!(!is_no_proxy(&p, "staging"));
805
806        // Should not match unrelated hosts
807        assert!(!is_no_proxy(&p, "dev")); // bare wildcard suffix
808        assert!(!is_no_proxy(&p, "api.prod")); // different suffix
809        assert!(!is_no_proxy(&p, "docs.rs")); // unrelated
810        assert!(!is_no_proxy(&p, "127.0.0.2")); // different IP
811    }
812
813    #[test]
814    fn no_proxy_case_insensitive_matching() {
815        let p = Proxy::builder(ProxyProtocol::Http)
816            .host("proxy.example.com")
817            .port(8080)
818            .no_proxy("localhost")
819            .no_proxy("*.Example.Com")
820            .no_proxy(".INTERNAL")
821            .build()
822            .unwrap();
823
824        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
825            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
826            p.is_no_proxy(&uri)
827        }
828
829        // Test exact host matching - should be case insensitive
830        // These patterns are stored as lowercase: "localhost"
831        assert!(is_no_proxy(&p, "localhost")); // fast path: already lowercase
832        assert!(is_no_proxy(&p, "LOCALHOST")); // slow path: needs conversion
833        assert!(is_no_proxy(&p, "LocalHost")); // slow path: needs conversion
834
835        // Test wildcard suffix case insensitive matching
836        // These patterns are stored as lowercase: ".example.com"
837        assert!(is_no_proxy(&p, "api.example.com")); // fast path: already lowercase
838        assert!(is_no_proxy(&p, "api.EXAMPLE.COM")); // slow path: needs conversion
839        assert!(is_no_proxy(&p, "API.example.COM")); // slow path: needs conversion
840        assert!(is_no_proxy(&p, "api.Example.Com")); // slow path: needs conversion
841
842        // Test dot suffix case insensitive matching (only matches subdomains)
843        // These patterns are stored as lowercase: ".internal"
844        assert!(is_no_proxy(&p, "app.internal")); // fast path: already lowercase
845        assert!(is_no_proxy(&p, "app.INTERNAL")); // slow path: needs conversion
846        assert!(is_no_proxy(&p, "APP.Internal")); // slow path: needs conversion
847        assert!(!is_no_proxy(&p, "INTERNAL")); // bare domain doesn't match dot suffix
848        assert!(!is_no_proxy(&p, "internal")); // bare domain doesn't match dot suffix
849    }
850
851    #[test]
852    fn no_proxy_edge_cases() {
853        let p = Proxy::builder(ProxyProtocol::Http)
854            .host("proxy.example.com")
855            .port(8080)
856            .no_proxy("") // empty string
857            .no_proxy("single") // single word
858            .no_proxy("*..") // malformed wildcard
859            .no_proxy("..") // malformed dot suffix
860            .no_proxy("192.168.1.1") // IP address
861            .no_proxy("*.local") // local domain
862            .build()
863            .unwrap();
864
865        fn is_no_proxy(p: &Proxy, host: &str) -> bool {
866            let uri = Uri::from_str(&format!("http://{}", host)).unwrap();
867            p.is_no_proxy(&uri)
868        }
869
870        // Test exact matching of various formats
871        assert!(is_no_proxy(&p, "single"));
872        assert!(is_no_proxy(&p, "192.168.1.1"));
873        assert!(!is_no_proxy(&p, "192.168.1.2"));
874
875        // Test wildcard with local domains
876        assert!(is_no_proxy(&p, "printer.local"));
877        assert!(is_no_proxy(&p, "router.local"));
878        assert!(!is_no_proxy(&p, "local")); // bare domain
879
880        // Test that malformed patterns don't break things
881        assert!(is_no_proxy(&p, "something..")); // matches exactly
882        assert!(!is_no_proxy(&p, "something.else"));
883
884        // Test empty string exact match
885        // Note: This is likely an edge case that shouldn't happen in practice
886        // but we want to ensure it doesn't crash
887    }
888
889    #[test]
890    fn proxy_clone_does_not_allocate() {
891        let c = Proxy::new("socks://1.2.3.4").unwrap();
892        assert_no_alloc(|| c.clone());
893    }
894
895    #[test]
896    fn proxy_new_default_scheme() {
897        let c = Proxy::new("localhost:1234").unwrap();
898        assert_eq!(c.protocol(), ProxyProtocol::Http);
899        assert_eq!(c.uri(), "http://localhost:1234");
900    }
901
902    #[test]
903    fn proxy_empty_env_url() {
904        let result = Proxy::new_with_flag("", None, false, None);
905        assert!(result.is_err());
906    }
907
908    #[test]
909    fn proxy_invalid_env_url() {
910        let result = Proxy::new_with_flag("r32/?//52:**", None, false, None);
911        assert!(result.is_err());
912    }
913
914    #[test]
915    fn proxy_builder() {
916        let proxy = Proxy::builder(ProxyProtocol::Socks4)
917            .host("my-proxy.com")
918            .port(5551)
919            .resolve_target(false)
920            .build()
921            .unwrap();
922
923        assert_eq!(proxy.protocol(), ProxyProtocol::Socks4);
924        assert_eq!(proxy.uri(), "SOCKS4://my-proxy.com:5551/");
925        assert_eq!(proxy.host(), "my-proxy.com");
926        assert_eq!(proxy.port(), 5551);
927        assert_eq!(proxy.username(), None);
928        assert_eq!(proxy.password(), None);
929        assert_eq!(proxy.is_from_env(), false);
930        assert_eq!(proxy.resolve_target(), false);
931    }
932
933    #[test]
934    fn proxy_builder_username() {
935        let proxy = Proxy::builder(ProxyProtocol::Https)
936            .username("hemligearne")
937            .build()
938            .unwrap();
939
940        assert_eq!(proxy.protocol(), ProxyProtocol::Https);
941        assert_eq!(proxy.uri(), "https://hemligearne@localhost:443/");
942        assert_eq!(proxy.host(), "localhost");
943        assert_eq!(proxy.port(), 443);
944        assert_eq!(proxy.username(), Some("hemligearne"));
945        assert_eq!(proxy.password(), None);
946        assert_eq!(proxy.is_from_env(), false);
947        assert_eq!(proxy.resolve_target(), false);
948    }
949
950    #[test]
951    fn proxy_builder_username_password() {
952        let proxy = Proxy::builder(ProxyProtocol::Https)
953            .username("hemligearne")
954            .password("kulgrej")
955            .build()
956            .unwrap();
957
958        assert_eq!(proxy.protocol(), ProxyProtocol::Https);
959        assert_eq!(proxy.uri(), "https://hemligearne:kulgrej@localhost:443/");
960        assert_eq!(proxy.host(), "localhost");
961        assert_eq!(proxy.port(), 443);
962        assert_eq!(proxy.username(), Some("hemligearne"));
963        assert_eq!(proxy.password(), Some("kulgrej"));
964        assert_eq!(proxy.is_from_env(), false);
965        assert_eq!(proxy.resolve_target(), false);
966    }
967}