aws_smithy_http_client/client/
proxy.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Proxy configuration for HTTP clients
7//!
8//! This module provides types and utilities for configuring HTTP and HTTPS proxies,
9//! including support for environment variable detection, authentication, and bypass rules.
10
11use http_1x::Uri;
12use hyper_util::client::proxy::matcher::Matcher;
13use std::fmt;
14
15/// Proxy configuration for HTTP clients
16///
17/// Supports HTTP and HTTPS proxy configuration with authentication and bypass rules.
18/// Can be configured programmatically or automatically detected from environment variables.
19///
20/// # Examples
21///
22/// ```rust
23/// use aws_smithy_http_client::proxy::ProxyConfig;
24///
25/// // HTTP proxy for all traffic
26/// let config = ProxyConfig::http("http://proxy.example.com:8080")?;
27///
28/// // HTTPS traffic through HTTP proxy (common case - no TLS needed for proxy connection)
29/// let config = ProxyConfig::https("http://proxy.example.com:8080")?
30///     .with_basic_auth("username", "password")
31///     .no_proxy("localhost,*.internal");
32///
33/// // Detect from environment variables
34/// let config = ProxyConfig::from_env();
35/// # Ok::<(), Box<dyn std::error::Error>>(())
36/// ```
37#[derive(Debug, Clone)]
38pub struct ProxyConfig {
39    inner: ProxyConfigInner,
40}
41
42/// Internal configuration representation
43#[derive(Debug, Clone)]
44enum ProxyConfigInner {
45    /// Use environment variable detection
46    FromEnvironment,
47    /// Explicit HTTP proxy
48    Http {
49        uri: Uri,
50        auth: Option<ProxyAuth>,
51        no_proxy: Option<String>,
52    },
53    /// Explicit HTTPS proxy
54    Https {
55        uri: Uri,
56        auth: Option<ProxyAuth>,
57        no_proxy: Option<String>,
58    },
59    /// Proxy for all traffic
60    All {
61        uri: Uri,
62        auth: Option<ProxyAuth>,
63        no_proxy: Option<String>,
64    },
65    /// Explicitly disabled
66    Disabled,
67}
68
69/// Proxy authentication configuration
70///
71/// Stored for later conversion to hyper-util format.
72#[derive(Debug, Clone)]
73struct ProxyAuth {
74    /// Username for authentication
75    username: String,
76    /// Password for authentication
77    password: String,
78}
79
80/// Errors that can occur during proxy configuration
81#[derive(Debug)]
82pub struct ProxyError {
83    kind: ErrorKind,
84}
85
86#[derive(Debug)]
87enum ErrorKind {
88    InvalidUrl(String),
89}
90
91impl From<ErrorKind> for ProxyError {
92    fn from(value: ErrorKind) -> Self {
93        Self { kind: value }
94    }
95}
96
97impl fmt::Display for ProxyError {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match &self.kind {
100            ErrorKind::InvalidUrl(url) => write!(f, "invalid proxy URL: {}", url),
101        }
102    }
103}
104
105impl std::error::Error for ProxyError {}
106
107impl ProxyConfig {
108    /// Create a new proxy configuration for HTTP traffic only
109    ///
110    /// # Arguments
111    /// * `proxy_url` - The HTTP proxy URL
112    ///
113    /// # Examples
114    /// ```rust
115    /// use aws_smithy_http_client::proxy::ProxyConfig;
116    ///
117    /// let config = ProxyConfig::http("http://proxy.example.com:8080")?;
118    /// # Ok::<(), Box<dyn std::error::Error>>(())
119    /// ```
120    pub fn http<U>(proxy_url: U) -> Result<Self, ProxyError>
121    where
122        U: TryInto<Uri>,
123        U::Error: fmt::Display,
124    {
125        let uri = proxy_url
126            .try_into()
127            .map_err(|e| ErrorKind::InvalidUrl(e.to_string()))?;
128
129        Self::validate_proxy_uri(&uri)?;
130
131        Ok(ProxyConfig {
132            inner: ProxyConfigInner::Http {
133                uri,
134                auth: None,
135                no_proxy: None,
136            },
137        })
138    }
139
140    /// Create a new proxy configuration for HTTPS traffic only
141    ///
142    /// This proxy will only be used for `https://` requests. HTTP requests
143    /// will connect directly unless a separate HTTP proxy is configured.
144    ///
145    /// The proxy URL itself can use either HTTP or HTTPS scheme:
146    /// - `http://proxy.example.com:8080` - Connect to proxy using HTTP (no TLS needed)
147    /// - `https://proxy.example.com:8080` - Connect to proxy using HTTPS (TLS required)
148    ///
149    /// **Note**: If the proxy URL itself uses HTTPS scheme, TLS support must be
150    /// available when building the connector, otherwise connections will fail.
151    ///
152    /// # Arguments
153    /// * `proxy_url` - The proxy URL
154    ///
155    /// # Examples
156    /// ```rust
157    /// use aws_smithy_http_client::proxy::ProxyConfig;
158    ///
159    /// // HTTPS traffic through HTTP proxy (no TLS needed for proxy connection)
160    /// let config = ProxyConfig::https("http://proxy.example.com:8080")?;
161    ///
162    /// // HTTPS traffic through HTTPS proxy (TLS needed for proxy connection)
163    /// let config = ProxyConfig::https("https://secure-proxy.example.com:8080")?;
164    /// # Ok::<(), Box<dyn std::error::Error>>(())
165    /// ```
166    pub fn https<U>(proxy_url: U) -> Result<Self, ProxyError>
167    where
168        U: TryInto<Uri>,
169        U::Error: fmt::Display,
170    {
171        let uri = proxy_url
172            .try_into()
173            .map_err(|e| ErrorKind::InvalidUrl(e.to_string()))?;
174
175        Self::validate_proxy_uri(&uri)?;
176
177        Ok(ProxyConfig {
178            inner: ProxyConfigInner::Https {
179                uri,
180                auth: None,
181                no_proxy: None,
182            },
183        })
184    }
185
186    /// Create a new proxy configuration for all HTTP and HTTPS traffic
187    ///
188    /// This proxy will be used for both `http://` and `https://` requests.
189    /// This is equivalent to setting both HTTP and HTTPS proxies to the same URL.
190    ///
191    /// **Note**: If the proxy URL itself uses HTTPS scheme, TLS support must be
192    /// available when building the connector, otherwise connections will fail.
193    ///
194    /// # Arguments
195    /// * `proxy_url` - The proxy URL
196    ///
197    /// # Examples
198    /// ```rust
199    /// use aws_smithy_http_client::proxy::ProxyConfig;
200    ///
201    /// let config = ProxyConfig::all("http://proxy.example.com:8080")?;
202    /// # Ok::<(), Box<dyn std::error::Error>>(())
203    /// ```
204    pub fn all<U>(proxy_url: U) -> Result<Self, ProxyError>
205    where
206        U: TryInto<Uri>,
207        U::Error: fmt::Display,
208    {
209        let uri = proxy_url
210            .try_into()
211            .map_err(|e| ErrorKind::InvalidUrl(e.to_string()))?;
212
213        Self::validate_proxy_uri(&uri)?;
214
215        Ok(ProxyConfig {
216            inner: ProxyConfigInner::All {
217                uri,
218                auth: None,
219                no_proxy: None,
220            },
221        })
222    }
223
224    /// Create a proxy configuration that disables all proxy usage
225    ///
226    /// This is useful for explicitly disabling proxy support even when
227    /// environment variables are set.
228    ///
229    /// # Examples
230    /// ```rust
231    /// use aws_smithy_http_client::proxy::ProxyConfig;
232    ///
233    /// let config = ProxyConfig::disabled();
234    /// ```
235    pub fn disabled() -> Self {
236        ProxyConfig {
237            inner: ProxyConfigInner::Disabled,
238        }
239    }
240
241    /// Add basic authentication to this proxy configuration
242    ///
243    /// # Arguments
244    /// * `username` - Username for proxy authentication
245    /// * `password` - Password for proxy authentication
246    ///
247    /// # Examples
248    /// ```rust
249    /// use aws_smithy_http_client::proxy::ProxyConfig;
250    ///
251    /// let config = ProxyConfig::http("http://proxy.example.com:8080")?
252    ///     .with_basic_auth("username", "password");
253    /// # Ok::<(), Box<dyn std::error::Error>>(())
254    /// ```
255    pub fn with_basic_auth<U, P>(mut self, username: U, password: P) -> Self
256    where
257        U: Into<String>,
258        P: Into<String>,
259    {
260        let auth = ProxyAuth {
261            username: username.into(),
262            password: password.into(),
263        };
264
265        match &mut self.inner {
266            ProxyConfigInner::Http {
267                auth: ref mut a, ..
268            } => *a = Some(auth),
269            ProxyConfigInner::Https {
270                auth: ref mut a, ..
271            } => *a = Some(auth),
272            ProxyConfigInner::All {
273                auth: ref mut a, ..
274            } => *a = Some(auth),
275            ProxyConfigInner::FromEnvironment | ProxyConfigInner::Disabled => {
276                // Cannot add auth to environment or disabled configs
277            }
278        }
279
280        self
281    }
282
283    /// Add NO_PROXY rules to this configuration
284    ///
285    /// NO_PROXY rules specify hosts that should bypass the proxy and connect directly.
286    ///
287    /// # Arguments
288    /// * `rules` - Comma-separated list of bypass rules
289    ///
290    /// # Examples
291    /// ```rust
292    /// use aws_smithy_http_client::proxy::ProxyConfig;
293    ///
294    /// let config = ProxyConfig::http("http://proxy.example.com:8080")?
295    ///     .no_proxy("localhost,127.0.0.1,*.internal,10.0.0.0/8");
296    /// # Ok::<(), Box<dyn std::error::Error>>(())
297    /// ```
298    pub fn no_proxy<S: AsRef<str>>(mut self, rules: S) -> Self {
299        let rules_str = rules.as_ref().to_string();
300
301        match &mut self.inner {
302            ProxyConfigInner::Http {
303                no_proxy: ref mut n,
304                ..
305            } => *n = Some(rules_str),
306            ProxyConfigInner::Https {
307                no_proxy: ref mut n,
308                ..
309            } => *n = Some(rules_str),
310            ProxyConfigInner::All {
311                no_proxy: ref mut n,
312                ..
313            } => *n = Some(rules_str),
314            ProxyConfigInner::FromEnvironment | ProxyConfigInner::Disabled => {
315                // Cannot add no_proxy to environment or disabled configs
316                // Environment configs will use NO_PROXY env var
317                // FIXME - is this what we want?
318            }
319        }
320
321        self
322    }
323
324    /// Create proxy configuration from environment variables
325    ///
326    /// Reads standard proxy environment variables:
327    /// - `HTTP_PROXY` / `http_proxy`: HTTP proxy URL
328    /// - `HTTPS_PROXY` / `https_proxy`: HTTPS proxy URL
329    /// - `ALL_PROXY` / `all_proxy`: Proxy for all protocols (fallback)
330    /// - `NO_PROXY` / `no_proxy`: Comma-separated bypass rules
331    ///
332    /// If no proxy environment variables are set, this returns a configuration
333    /// that won't intercept any requests (equivalent to no proxy).
334    ///
335    /// # Examples
336    /// ```rust
337    /// use aws_smithy_http_client::proxy::ProxyConfig;
338    ///
339    /// // Always succeeds, even if no environment variables are set
340    /// let config = ProxyConfig::from_env();
341    /// ```
342    pub fn from_env() -> Self {
343        // Delegate to environment variable parsing
344        // If no env vars are set, creates a matcher that doesn't intercept anything
345        ProxyConfig {
346            inner: ProxyConfigInner::FromEnvironment,
347        }
348    }
349
350    /// Check if proxy is disabled (no proxy configuration)
351    pub fn is_disabled(&self) -> bool {
352        matches!(self.inner, ProxyConfigInner::Disabled)
353    }
354
355    /// Check if this configuration uses environment variables
356    pub fn is_from_env(&self) -> bool {
357        matches!(self.inner, ProxyConfigInner::FromEnvironment)
358    }
359
360    /// Convert this configuration to internal proxy matcher
361    ///
362    /// This method converts the user-friendly configuration to the internal
363    /// proxy matching implementation used by the HTTP client.
364    pub(crate) fn into_hyper_util_matcher(self) -> Matcher {
365        match self.inner {
366            ProxyConfigInner::FromEnvironment => Matcher::from_env(),
367            ProxyConfigInner::Http {
368                uri,
369                auth,
370                no_proxy,
371            } => {
372                let mut builder = Matcher::builder();
373
374                // Set HTTP proxy with authentication embedded in URL if present
375                let proxy_url = Self::build_proxy_url(uri, auth);
376                builder = builder.http(proxy_url);
377
378                // Add NO_PROXY rules if present
379                if let Some(no_proxy_rules) = no_proxy {
380                    builder = builder.no(no_proxy_rules);
381                }
382
383                builder.build()
384            }
385            ProxyConfigInner::Https {
386                uri,
387                auth,
388                no_proxy,
389            } => {
390                let mut builder = Matcher::builder();
391
392                // Set HTTPS proxy with authentication embedded in URL if present
393                let proxy_url = Self::build_proxy_url(uri, auth);
394                builder = builder.https(proxy_url);
395
396                // Add NO_PROXY rules if present
397                if let Some(no_proxy_rules) = no_proxy {
398                    builder = builder.no(no_proxy_rules);
399                }
400
401                builder.build()
402            }
403            ProxyConfigInner::All {
404                uri,
405                auth,
406                no_proxy,
407            } => {
408                let mut builder = Matcher::builder();
409
410                // Set proxy for all traffic with authentication embedded in URL if present
411                let proxy_url = Self::build_proxy_url(uri, auth);
412                builder = builder.all(proxy_url);
413
414                // Add NO_PROXY rules if present
415                if let Some(no_proxy_rules) = no_proxy {
416                    builder = builder.no(no_proxy_rules);
417                }
418
419                builder.build()
420            }
421            ProxyConfigInner::Disabled => {
422                // Create an empty matcher that won't intercept anything
423                Matcher::builder().build()
424            }
425        }
426    }
427
428    /// Check if this proxy configuration requires TLS support
429    ///
430    /// Returns true if any of the configured proxy URLs use HTTPS scheme,
431    /// which requires TLS to establish the connection to the proxy server.
432    pub(crate) fn requires_tls(&self) -> bool {
433        match &self.inner {
434            ProxyConfigInner::Http { uri, .. } => uri.scheme_str() == Some("https"),
435            ProxyConfigInner::Https { uri, .. } => uri.scheme_str() == Some("https"),
436            ProxyConfigInner::All { uri, .. } => uri.scheme_str() == Some("https"),
437            ProxyConfigInner::FromEnvironment => {
438                // Check environment variables for HTTPS proxy URLs
439                Self::env_vars_require_tls()
440            }
441            ProxyConfigInner::Disabled => false,
442        }
443    }
444
445    /// Check if any environment proxy variables contain HTTPS URLs
446    fn env_vars_require_tls() -> bool {
447        let proxy_vars = [
448            "HTTP_PROXY",
449            "http_proxy",
450            "HTTPS_PROXY",
451            "https_proxy",
452            "ALL_PROXY",
453            "all_proxy",
454        ];
455
456        for var in &proxy_vars {
457            if let Ok(proxy_url) = std::env::var(var) {
458                if !proxy_url.is_empty() {
459                    // Simple check for https:// scheme
460                    if proxy_url.starts_with("https://") {
461                        return true;
462                    }
463                }
464            }
465        }
466        false
467    }
468
469    fn validate_proxy_uri(uri: &Uri) -> Result<(), ProxyError> {
470        // Validate scheme
471        match uri.scheme_str() {
472            Some("http") | Some("https") => {}
473            Some(scheme) => {
474                return Err(
475                    ErrorKind::InvalidUrl(format!("unsupported proxy scheme: {}", scheme)).into(),
476                );
477            }
478            None => {
479                return Err(ErrorKind::InvalidUrl(
480                    "proxy URL must include scheme (http:// or https://)".to_string(),
481                )
482                .into());
483            }
484        }
485
486        // Validate host
487        if uri.host().is_none() {
488            return Err(ErrorKind::InvalidUrl("proxy URL must include host".to_string()).into());
489        }
490
491        Ok(())
492    }
493
494    fn build_proxy_url(uri: Uri, auth: Option<ProxyAuth>) -> String {
495        let uri_str = uri.to_string();
496
497        if let Some(auth) = auth {
498            // Embed authentication in the URL: scheme://username:password@host:port/path
499            if let Some(scheme_end) = uri_str.find("://") {
500                let scheme = &uri_str[..scheme_end + 3];
501                let rest = &uri_str[scheme_end + 3..];
502
503                // Check if auth is already present in the URI
504                if rest.contains('@') {
505                    // Auth already present, return as-is
506                    uri_str
507                } else {
508                    // Add auth to the URI
509                    format!("{}{}:{}@{}", scheme, auth.username, auth.password, rest)
510                }
511            } else {
512                // Invalid URI format, return as-is
513                uri_str
514            }
515        } else {
516            // No authentication, return URI as-is
517            uri_str
518        }
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use std::env;
526
527    #[test]
528    fn test_proxy_config_http() {
529        let config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
530        assert!(!config.is_disabled());
531        assert!(!config.is_from_env());
532    }
533
534    #[test]
535    fn test_proxy_config_https() {
536        let config = ProxyConfig::https("http://proxy.example.com:8080").unwrap();
537        assert!(!config.is_disabled());
538        assert!(!config.is_from_env());
539    }
540
541    #[test]
542    fn test_proxy_config_all() {
543        let config = ProxyConfig::all("http://proxy.example.com:8080").unwrap();
544        assert!(!config.is_disabled());
545        assert!(!config.is_from_env());
546    }
547
548    #[test]
549    fn test_proxy_config_disabled() {
550        let config = ProxyConfig::disabled();
551        assert!(config.is_disabled());
552        assert!(!config.is_from_env());
553    }
554
555    #[test]
556    fn test_proxy_config_with_auth() {
557        let config = ProxyConfig::http("http://proxy.example.com:8080")
558            .unwrap()
559            .with_basic_auth("user", "pass");
560
561        // Auth is stored internally
562        assert!(!config.is_disabled());
563    }
564
565    #[test]
566    fn test_proxy_config_with_no_proxy() {
567        let config = ProxyConfig::http("http://proxy.example.com:8080")
568            .unwrap()
569            .no_proxy("localhost,*.internal");
570
571        // NO_PROXY rules are stored internally
572        assert!(!config.is_disabled());
573    }
574
575    #[test]
576    fn test_proxy_config_invalid_url() {
577        let result = ProxyConfig::http("not-a-url");
578        assert!(result.is_err());
579    }
580
581    #[test]
582    fn test_proxy_config_invalid_scheme() {
583        let result = ProxyConfig::http("ftp://proxy.example.com:8080");
584        assert!(result.is_err());
585    }
586
587    #[test]
588    #[serial_test::serial]
589    fn test_proxy_config_from_env_with_vars() {
590        // Save original environment
591        let original_http = env::var("HTTP_PROXY");
592
593        // Set test environment
594        env::set_var("HTTP_PROXY", "http://test-proxy:8080");
595
596        let config = ProxyConfig::from_env();
597        assert!(config.is_from_env());
598
599        // Restore original environment
600        match original_http {
601            Ok(val) => env::set_var("HTTP_PROXY", val),
602            Err(_) => env::remove_var("HTTP_PROXY"),
603        }
604    }
605
606    #[test]
607    #[serial_test::serial]
608    fn test_proxy_config_from_env_without_vars() {
609        // Save original environment
610        let original_vars: Vec<_> = [
611            "HTTP_PROXY",
612            "http_proxy",
613            "HTTPS_PROXY",
614            "https_proxy",
615            "ALL_PROXY",
616            "all_proxy",
617        ]
618        .iter()
619        .map(|var| (*var, env::var(var)))
620        .collect();
621
622        // Clear all proxy environment variables
623        for (var, _) in &original_vars {
624            env::remove_var(var);
625        }
626
627        let config = ProxyConfig::from_env();
628        assert!(config.is_from_env());
629
630        // Restore original environment
631        for (var, original_value) in original_vars {
632            match original_value {
633                Ok(val) => env::set_var(var, val),
634                Err(_) => env::remove_var(var),
635            }
636        }
637    }
638
639    #[test]
640    #[serial_test::serial]
641    fn test_auth_cannot_be_added_to_env_config() {
642        // Save original environment
643        let original_http = env::var("HTTP_PROXY");
644        env::set_var("HTTP_PROXY", "http://test-proxy:8080");
645
646        let config = ProxyConfig::from_env().with_basic_auth("user", "pass"); // This should be ignored
647
648        assert!(config.is_from_env());
649
650        // Restore original environment
651        match original_http {
652            Ok(val) => env::set_var("HTTP_PROXY", val),
653            Err(_) => env::remove_var("HTTP_PROXY"),
654        }
655    }
656
657    #[test]
658    #[serial_test::serial]
659    fn test_no_proxy_cannot_be_added_to_env_config() {
660        // Save original environment
661        let original_http = env::var("HTTP_PROXY");
662        env::set_var("HTTP_PROXY", "http://test-proxy:8080");
663
664        let config = ProxyConfig::from_env().no_proxy("localhost"); // This should be ignored
665
666        assert!(config.is_from_env());
667
668        // Restore original environment
669        match original_http {
670            Ok(val) => env::set_var("HTTP_PROXY", val),
671            Err(_) => env::remove_var("HTTP_PROXY"),
672        }
673    }
674
675    #[test]
676    fn test_build_proxy_url_without_auth() {
677        let uri = "http://proxy.example.com:8080".parse().unwrap();
678        let url = ProxyConfig::build_proxy_url(uri, None);
679        assert_eq!(url, "http://proxy.example.com:8080/");
680    }
681
682    #[test]
683    fn test_build_proxy_url_with_auth() {
684        let uri = "http://proxy.example.com:8080".parse().unwrap();
685        let auth = ProxyAuth {
686            username: "user".to_string(),
687            password: "pass".to_string(),
688        };
689        let url = ProxyConfig::build_proxy_url(uri, Some(auth));
690        assert_eq!(url, "http://user:pass@proxy.example.com:8080/");
691    }
692
693    #[test]
694    fn test_build_proxy_url_with_existing_auth() {
695        let uri = "http://existing:creds@proxy.example.com:8080"
696            .parse()
697            .unwrap();
698        let auth = ProxyAuth {
699            username: "user".to_string(),
700            password: "pass".to_string(),
701        };
702        let url = ProxyConfig::build_proxy_url(uri, Some(auth));
703        // Should not override existing auth
704        assert_eq!(url, "http://existing:creds@proxy.example.com:8080/");
705    }
706
707    #[test]
708    #[serial_test::serial]
709    fn test_into_hyper_util_matcher_from_env() {
710        // Save original environment
711        let original_http = env::var("HTTP_PROXY");
712        env::set_var("HTTP_PROXY", "http://test-proxy:8080");
713
714        let config = ProxyConfig::from_env();
715        let matcher = config.into_hyper_util_matcher();
716
717        // Test that the matcher intercepts HTTP requests
718        let test_uri = "http://example.com".parse().unwrap();
719        let intercept = matcher.intercept(&test_uri);
720        assert!(intercept.is_some());
721
722        // Restore original environment
723        match original_http {
724            Ok(val) => env::set_var("HTTP_PROXY", val),
725            Err(_) => env::remove_var("HTTP_PROXY"),
726        }
727    }
728
729    #[test]
730    fn test_into_hyper_util_matcher_http() {
731        let config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
732        let matcher = config.into_hyper_util_matcher();
733
734        // Test that the matcher intercepts HTTP requests
735        let test_uri = "http://example.com".parse().unwrap();
736        let intercept = matcher.intercept(&test_uri);
737        assert!(intercept.is_some());
738        // The intercept URI might be normalized
739        assert!(intercept
740            .unwrap()
741            .uri()
742            .to_string()
743            .starts_with("http://proxy.example.com:8080"));
744
745        // Test that it doesn't intercept HTTPS requests
746        let https_uri = "https://example.com".parse().unwrap();
747        let https_intercept = matcher.intercept(&https_uri);
748        assert!(https_intercept.is_none());
749    }
750
751    #[test]
752    fn test_into_hyper_util_matcher_with_auth() {
753        let config = ProxyConfig::http("http://proxy.example.com:8080")
754            .unwrap()
755            .with_basic_auth("user", "pass");
756        let matcher = config.into_hyper_util_matcher();
757
758        // Test that the matcher intercepts HTTP requests
759        let test_uri = "http://example.com".parse().unwrap();
760        let intercept = matcher.intercept(&test_uri);
761        assert!(intercept.is_some());
762
763        let intercept = intercept.unwrap();
764        // The proxy URI should contain the host (auth is handled separately)
765        assert!(intercept
766            .uri()
767            .to_string()
768            .contains("proxy.example.com:8080"));
769
770        // Test that basic auth is available
771        assert!(intercept.basic_auth().is_some());
772    }
773
774    #[test]
775    fn test_into_hyper_util_matcher_disabled() {
776        let config = ProxyConfig::disabled();
777        let matcher = config.into_hyper_util_matcher();
778
779        // Test that the matcher doesn't intercept any requests
780        let test_uri = "http://example.com".parse().unwrap();
781        let intercept = matcher.intercept(&test_uri);
782        assert!(intercept.is_none());
783    }
784
785    #[test]
786    #[serial_test::serial]
787    fn test_requires_tls_detection() {
788        // HTTP proxy should not require TLS
789        let http_config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
790        assert!(!http_config.requires_tls());
791
792        // HTTPS proxy URL should require TLS
793        let https_config = ProxyConfig::http("https://proxy.example.com:8080").unwrap();
794        assert!(https_config.requires_tls());
795
796        // All proxy with HTTP URL should not require TLS
797        let all_http_config = ProxyConfig::all("http://proxy.example.com:8080").unwrap();
798        assert!(!all_http_config.requires_tls());
799
800        // Environment config with HTTPS proxy should require TLS
801        env::set_var("HTTP_PROXY", "https://proxy.example.com:8080");
802        let env_config = ProxyConfig::from_env();
803        assert!(env_config.requires_tls()); // Now detects HTTPS in env vars
804        env::remove_var("HTTP_PROXY");
805
806        // Environment config with HTTP proxy should not require TLS
807        env::set_var("HTTP_PROXY", "http://proxy.example.com:8080");
808        let env_config = ProxyConfig::from_env();
809        assert!(!env_config.requires_tls());
810        env::remove_var("HTTP_PROXY");
811
812        // Disabled config should not require TLS
813        let disabled_config = ProxyConfig::disabled();
814        assert!(!disabled_config.requires_tls());
815    }
816}