Coverage Report

Created: 2026-05-16 06:09

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