aws_smithy_http_client/client/
proxy.rs1use http_1x::Uri;
12use hyper_util::client::proxy::matcher::Matcher;
13use std::fmt;
14
15#[derive(Debug, Clone)]
38pub struct ProxyConfig {
39 inner: ProxyConfigInner,
40}
41
42#[derive(Debug, Clone)]
44enum ProxyConfigInner {
45 FromEnvironment,
47 Http {
49 uri: Uri,
50 auth: Option<ProxyAuth>,
51 no_proxy: Option<String>,
52 },
53 Https {
55 uri: Uri,
56 auth: Option<ProxyAuth>,
57 no_proxy: Option<String>,
58 },
59 All {
61 uri: Uri,
62 auth: Option<ProxyAuth>,
63 no_proxy: Option<String>,
64 },
65 Disabled,
67}
68
69#[derive(Debug, Clone)]
73struct ProxyAuth {
74 username: String,
76 password: String,
78}
79
80#[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 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 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 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 pub fn disabled() -> Self {
236 ProxyConfig {
237 inner: ProxyConfigInner::Disabled,
238 }
239 }
240
241 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 }
278 }
279
280 self
281 }
282
283 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 }
319 }
320
321 self
322 }
323
324 pub fn from_env() -> Self {
343 ProxyConfig {
346 inner: ProxyConfigInner::FromEnvironment,
347 }
348 }
349
350 pub fn is_disabled(&self) -> bool {
352 matches!(self.inner, ProxyConfigInner::Disabled)
353 }
354
355 pub fn is_from_env(&self) -> bool {
357 matches!(self.inner, ProxyConfigInner::FromEnvironment)
358 }
359
360 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 let proxy_url = Self::build_proxy_url(uri, auth);
376 builder = builder.http(proxy_url);
377
378 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 let proxy_url = Self::build_proxy_url(uri, auth);
394 builder = builder.https(proxy_url);
395
396 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 let proxy_url = Self::build_proxy_url(uri, auth);
412 builder = builder.all(proxy_url);
413
414 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 Matcher::builder().build()
424 }
425 }
426 }
427
428 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 Self::env_vars_require_tls()
440 }
441 ProxyConfigInner::Disabled => false,
442 }
443 }
444
445 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 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 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 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 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 if rest.contains('@') {
505 uri_str
507 } else {
508 format!("{}{}:{}@{}", scheme, auth.username, auth.password, rest)
510 }
511 } else {
512 uri_str
514 }
515 } else {
516 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 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 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 let original_http = env::var("HTTP_PROXY");
592
593 env::set_var("HTTP_PROXY", "http://test-proxy:8080");
595
596 let config = ProxyConfig::from_env();
597 assert!(config.is_from_env());
598
599 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 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 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 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 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"); assert!(config.is_from_env());
649
650 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 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"); assert!(config.is_from_env());
667
668 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 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 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 let test_uri = "http://example.com".parse().unwrap();
719 let intercept = matcher.intercept(&test_uri);
720 assert!(intercept.is_some());
721
722 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 let test_uri = "http://example.com".parse().unwrap();
736 let intercept = matcher.intercept(&test_uri);
737 assert!(intercept.is_some());
738 assert!(intercept
740 .unwrap()
741 .uri()
742 .to_string()
743 .starts_with("http://proxy.example.com:8080"));
744
745 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 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 assert!(intercept
766 .uri()
767 .to_string()
768 .contains("proxy.example.com:8080"));
769
770 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 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 let http_config = ProxyConfig::http("http://proxy.example.com:8080").unwrap();
790 assert!(!http_config.requires_tls());
791
792 let https_config = ProxyConfig::http("https://proxy.example.com:8080").unwrap();
794 assert!(https_config.requires_tls());
795
796 let all_http_config = ProxyConfig::all("http://proxy.example.com:8080").unwrap();
798 assert!(!all_http_config.requires_tls());
799
800 env::set_var("HTTP_PROXY", "https://proxy.example.com:8080");
802 let env_config = ProxyConfig::from_env();
803 assert!(env_config.requires_tls()); env::remove_var("HTTP_PROXY");
805
806 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 let disabled_config = ProxyConfig::disabled();
814 assert!(!disabled_config.requires_tls());
815 }
816}