1use std::ops::Not;
22
23use url::{Host, Url};
24
25pub trait NoProxy {
27 fn no_proxy_for(&self, url: &Url) -> bool;
32
33 fn proxy_allowed_for(&self, url: &Url) -> bool {
38 self.no_proxy_for(url).not()
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum NoProxyRule {
45 MatchExact(String),
47 MatchSubdomain(String),
49}
50
51static_assertions::assert_impl_all!(NoProxyRule: Send, Sync);
52
53impl NoProxy for NoProxyRule {
54 fn no_proxy_for(&self, url: &Url) -> bool {
55 match self {
56 Self::MatchExact(host) => match url.host() {
57 Some(Host::Domain(domain)) => domain == host,
58 Some(Host::Ipv4(ipv4)) => &ipv4.to_string() == host,
59 Some(Host::Ipv6(ipv6)) => &ipv6.to_string() == host,
60 None => false,
61 },
62 Self::MatchSubdomain(subdomain) => match url.host() {
63 Some(Host::Domain(domain)) => {
64 domain.ends_with(subdomain) || domain == &subdomain[1..]
65 }
66 _ => false,
67 },
68 }
69 }
70}
71
72#[derive(Debug, Clone, Eq, PartialEq)]
74pub enum NoProxyRules {
75 All,
77 Rules(Vec<NoProxyRule>),
81}
82
83static_assertions::assert_impl_all!(NoProxyRules: Send, Sync);
84
85fn lookup(var: &str) -> Option<String> {
86 std::env::var_os(var).and_then(|v| {
87 v.to_str().map(ToOwned::to_owned).or_else(|| {
88 log::warn!("Variable ${} does not contain valid unicode, skipping", var);
89 None
90 })
91 })
92}
93
94impl NoProxyRules {
95 pub fn new(rules: Vec<NoProxyRule>) -> Self {
97 Self::Rules(rules)
98 }
99
100 pub fn none() -> Self {
102 NoProxyRules::Rules(Vec::new())
103 }
104
105 pub fn all() -> Self {
107 Self::All
108 }
109
110 pub fn parse_curl_env<S: AsRef<str>>(value: S) -> Self {
114 let value = value.as_ref().trim();
115 if value == "*" {
116 Self::all()
117 } else {
118 let rules = value
119 .split(',')
120 .map(|r| r.trim())
121 .filter(|r| !r.is_empty())
122 .map(|rule| {
123 if rule.starts_with('.') {
124 NoProxyRule::MatchSubdomain(rule.to_string())
125 } else {
126 NoProxyRule::MatchExact(rule.to_string())
127 }
128 })
129 .collect::<Vec<_>>();
130 Self::new(rules)
131 }
132 }
133
134 pub fn from_curl_env() -> Option<Self> {
152 lookup("no_proxy")
153 .or_else(|| lookup("NO_PROXY"))
154 .map(Self::parse_curl_env)
155 }
156}
157
158impl NoProxy for NoProxyRules {
159 fn no_proxy_for(&self, url: &Url) -> bool {
160 match self {
161 NoProxyRules::All => true,
162 NoProxyRules::Rules(ref rules) => rules.iter().any(|rule| rule.no_proxy_for(url)),
163 }
164 }
165}
166
167impl From<Vec<NoProxyRule>> for NoProxyRules {
168 fn from(rules: Vec<NoProxyRule>) -> Self {
169 Self::new(rules)
170 }
171}
172
173impl From<NoProxyRule> for NoProxyRules {
174 fn from(rule: NoProxyRule) -> Self {
175 Self::new(vec![rule])
176 }
177}
178
179impl Default for NoProxyRules {
180 fn default() -> Self {
182 NoProxyRules::none()
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
188pub struct EnvProxies {
189 pub http: Option<Url>,
193 pub https: Option<Url>,
197 pub no_proxy_rules: Option<NoProxyRules>,
201}
202
203fn lookup_url(var: &str) -> Option<Url> {
204 lookup(var).as_ref().and_then(|s| match Url::parse(s) {
205 Ok(url) => Some(url),
206 Err(error) => {
207 log::warn!(
208 "Failed to parse value of ${} as URL, skipping: {}",
209 var,
210 error
211 );
212 None
213 }
214 })
215}
216
217impl EnvProxies {
218 pub fn unset() -> Self {
220 Self {
221 http: None,
222 https: None,
223 no_proxy_rules: None,
224 }
225 }
226
227 pub fn from_curl_env() -> Self {
243 Self {
244 http: lookup_url("http_proxy").or_else(|| lookup_url("HTTP_PROXY")),
245 https: lookup_url("https_proxy").or_else(|| lookup_url("HTTPS_PROXY")),
246 no_proxy_rules: NoProxyRules::from_curl_env(),
247 }
248 }
249
250 pub fn is_unset(&self) -> bool {
255 self.http.is_none() && self.https.is_none()
256 }
257
258 pub fn lookup(&self, url: &Url) -> Option<&Url> {
260 let rules = self.no_proxy_rules.as_ref();
261 let proxy = match url.scheme() {
262 "http" => self.http.as_ref(),
263 "https" => self.https.as_ref(),
264 _ => None,
265 };
266 if proxy.is_some() && rules.map_or(true, |r| r.proxy_allowed_for(url)) {
267 proxy
268 } else {
269 None
270 }
271 }
272}
273
274pub fn from_curl_env() -> EnvProxies {
278 EnvProxies::from_curl_env()
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use pretty_assertions::assert_eq;
285
286 #[test]
287 fn noproxy_rule_subdomain() {
288 let rule = NoProxyRule::MatchSubdomain(".example.com".to_string());
289 assert!(rule.no_proxy_for(&Url::parse("http://example.com/foo").unwrap()));
290 assert!(rule.no_proxy_for(&Url::parse("http://example.com/bar").unwrap()));
291 assert!(rule.no_proxy_for(&Url::parse("http://foo.example.com/foo").unwrap()));
292 assert!(!rule.no_proxy_for(&Url::parse("http://barexample.com/foo").unwrap()));
293 }
294
295 #[test]
296 fn noproxy_rule_exact_hostname() {
297 let rule = NoProxyRule::MatchExact("example.com".to_string());
298 assert!(rule.no_proxy_for(&Url::parse("http://example.com/foo").unwrap()));
299 assert!(rule.no_proxy_for(&Url::parse("http://example.com/bar").unwrap()));
300 assert!(!rule.no_proxy_for(&Url::parse("http://foo.example.com/foo").unwrap()));
301 assert!(!rule.no_proxy_for(&Url::parse("http://barexample.com/foo").unwrap()));
302 }
303
304 #[test]
305 fn noproxy_rule_exact_ipv4() {
306 let rule = NoProxyRule::MatchExact("192.168.100.12".to_string());
307 assert!(rule.no_proxy_for(&Url::parse("http://192.168.100.12/foo").unwrap()));
308 assert!(!rule.no_proxy_for(&Url::parse("http://192.168.100.122/foo").unwrap()));
309 }
310
311 #[test]
312 fn noproxy_rule_exact_ipv6() {
313 let rule = NoProxyRule::MatchExact("fe80::2ead:fea3:1423:6637".to_string());
314 assert!(rule.no_proxy_for(&Url::parse("http://[fe80::2ead:fea3:1423:6637]/foo").unwrap()));
315 assert!(!rule.no_proxy_for(&Url::parse("http://[fe80::2ead:fea3:1423:6638]/foo").unwrap()));
316 }
317
318 #[test]
319 fn noproxy_rules_all_matches() {
320 let samples = vec![
321 "http://[fe80::2ead:fea3:1423:6637]/foo",
322 "http://192.168.100.12/foo",
323 "http://foo.example.com/foo",
324 "http:///foo",
325 ];
326 for url in samples {
327 assert!(
328 NoProxyRules::All.no_proxy_for(&Url::parse(url).unwrap()),
329 "URL: {}",
330 url
331 );
332 }
333 }
334
335 #[test]
336 fn noproxy_rules_none_matches() {
337 let samples = vec![
338 "http://[fe80::2ead:fea3:1423:6637]/foo",
339 "http://192.168.100.12/foo",
340 "http://foo.example.com/foo",
341 "http:///foo",
342 ];
343 for url in samples {
344 assert!(
345 !NoProxyRules::Rules(Vec::new()).no_proxy_for(&Url::parse(url).unwrap()),
346 "URL: {}",
347 url
348 );
349 }
350 }
351
352 #[test]
353 fn noproxy_rules_matches() {
354 let rules = NoProxyRules::Rules(vec![
355 NoProxyRule::MatchSubdomain(".example.com".to_string()),
356 NoProxyRule::MatchExact("192.168.12.100".to_string()),
357 ]);
358
359 assert!(rules.no_proxy_for(&Url::parse("http://example.com").unwrap()));
360 assert!(rules.no_proxy_for(&Url::parse("http://foo.example.com").unwrap()));
361 assert!(rules.no_proxy_for(&Url::parse("http://192.168.12.100/foo").unwrap()));
362
363 assert!(!rules.no_proxy_for(&Url::parse("http://192.168.12.101/foo").unwrap()));
364 assert!(!rules.no_proxy_for(&Url::parse("http://192.168.12/foo").unwrap()));
365 assert!(!rules.no_proxy_for(&Url::parse("http://fooexample.com/foo").unwrap()));
366 assert!(!rules.no_proxy_for(&Url::parse("http://github.com/swsnr").unwrap()));
367 }
368
369 #[test]
370 fn from_curl_env_no_env() {
371 temp_env::with_vars_unset(
372 vec![
373 "http_proxy",
374 "https_proxy",
375 "no_proxy",
376 "HTTP_PROXY",
377 "HTTPS_PROXY",
378 "NO_PROXY",
379 ],
380 || {
381 assert_eq!(
382 EnvProxies::from_curl_env(),
383 EnvProxies {
384 http: None,
385 https: None,
386 no_proxy_rules: None
387 }
388 )
389 },
390 )
391 }
392
393 #[test]
394 fn from_curl_env_lowercase() {
395 temp_env::with_vars(
396 vec![
397 ("http_proxy", Some("http://thehttpproxy:1234")),
398 ("https_proxy", Some("http://thehttpsproxy:1234")),
399 ("no_proxy", Some("example.com")),
400 ],
401 || {
402 assert_eq!(
403 EnvProxies::from_curl_env(),
404 EnvProxies {
405 http: Some(Url::parse("http://thehttpproxy:1234").unwrap()),
406 https: Some(Url::parse("http://thehttpsproxy:1234").unwrap()),
407 no_proxy_rules: Some(
408 NoProxyRule::MatchExact("example.com".to_string()).into()
409 )
410 }
411 )
412 },
413 )
414 }
415
416 #[test]
417 fn from_curl_env_uppercase() {
418 temp_env::with_vars(
419 vec![
420 ("http_proxy", None),
421 ("https_proxy", None),
422 ("no_proxy", None),
423 ("HTTP_PROXY", Some("http://thehttpproxy:1234")),
424 ("HTTPS_PROXY", Some("http://thehttpsproxy:1234")),
425 ("NO_PROXY", Some("example.com")),
426 ],
427 || {
428 assert_eq!(
429 EnvProxies::from_curl_env(),
430 EnvProxies {
431 http: Some(Url::parse("http://thehttpproxy:1234").unwrap()),
432 https: Some(Url::parse("http://thehttpsproxy:1234").unwrap()),
433 no_proxy_rules: Some(
434 NoProxyRule::MatchExact("example.com".to_string()).into()
435 )
436 }
437 )
438 },
439 )
440 }
441
442 #[test]
443 fn from_curl_env_both() {
444 temp_env::with_vars(
445 vec![
446 ("HTTP_PROXY", Some("http://up.thehttpproxy:1234")),
447 ("HTTPS_PROXY", Some("http://up.thehttpsproxy:1234")),
448 ("NO_PROXY", Some("up.example.com")),
449 ("http_proxy", Some("http://low.thehttpproxy:1234")),
450 ("https_proxy", Some("http://low.thehttpsproxy:1234")),
451 ("no_proxy", Some("low.example.com")),
452 ],
453 || {
454 assert_eq!(
455 EnvProxies::from_curl_env(),
456 EnvProxies {
457 http: Some(Url::parse("http://low.thehttpproxy:1234").unwrap()),
458 https: Some(Url::parse("http://low.thehttpsproxy:1234").unwrap()),
459 no_proxy_rules: Some(
460 NoProxyRule::MatchExact("low.example.com".to_string()).into()
461 )
462 }
463 )
464 },
465 )
466 }
467
468 #[test]
469 fn parse_no_proxy_rules_many_rules() {
470 let rules = NoProxyRules::parse_curl_env("example.com ,.example.com , foo.bar,192.122.100.10, fe80::2ead:fea3:1423:6637,[fe80::2ead:fea3:1423:6637]");
471 assert_eq!(
472 rules,
473 NoProxyRules::Rules(vec![
474 NoProxyRule::MatchExact("example.com".into()),
475 NoProxyRule::MatchSubdomain(".example.com".into()),
476 NoProxyRule::MatchExact("foo.bar".into()),
477 NoProxyRule::MatchExact("192.122.100.10".into()),
478 NoProxyRule::MatchExact("fe80::2ead:fea3:1423:6637".into()),
479 NoProxyRule::MatchExact("[fe80::2ead:fea3:1423:6637]".into()),
480 ])
481 );
482 }
483
484 #[test]
485 fn parse_no_proxy_rules_wildcard() {
486 assert_eq!(NoProxyRules::parse_curl_env("*"), NoProxyRules::all());
487 assert_eq!(NoProxyRules::parse_curl_env(" * "), NoProxyRules::all());
488 assert_eq!(
489 NoProxyRules::parse_curl_env("*,foo.example.com"),
490 NoProxyRules::Rules(vec![
491 NoProxyRule::MatchExact("*".into()),
492 NoProxyRule::MatchExact("foo.example.com".into())
493 ])
494 );
495 }
496
497 #[test]
498 fn parse_no_proxy_rules_empty() {
499 assert_eq!(NoProxyRules::parse_curl_env(""), NoProxyRules::default());
500 assert_eq!(NoProxyRules::parse_curl_env(" "), NoProxyRules::default());
501 assert_eq!(
502 NoProxyRules::parse_curl_env("\t "),
503 NoProxyRules::default()
504 );
505 }
506
507 #[test]
508 fn lookup_http_proxy() {
509 let proxies = EnvProxies {
510 http: Some(Url::parse("http://httproxy.example.com:1284").unwrap()),
511 https: None,
512 no_proxy_rules: Some(NoProxyRules::default()),
513 };
514 assert_eq!(
515 proxies.lookup(&Url::parse("http://github.com").unwrap()),
516 Some(&Url::parse("http://httproxy.example.com:1284").unwrap())
517 );
518 assert_eq!(
519 proxies.lookup(&Url::parse("https://github.com").unwrap()),
520 None
521 );
522 }
523
524 #[test]
525 fn lookup_https_proxy() {
526 let proxies = EnvProxies {
527 http: None,
528 https: Some(Url::parse("http://httpsproxy.example.com:1284").unwrap()),
529 no_proxy_rules: Some(NoProxyRules::default()),
530 };
531 assert_eq!(
532 proxies.lookup(&Url::parse("https://github.com").unwrap()),
533 Some(&Url::parse("http://httpsproxy.example.com:1284").unwrap())
534 );
535 assert_eq!(
536 proxies.lookup(&Url::parse("http://github.com").unwrap()),
537 None
538 );
539 }
540
541 #[test]
542 fn lookup_rule_matches() {
543 let proxies = EnvProxies {
544 http: Some(Url::parse("http://httproxy.example.com:1284").unwrap()),
545 https: Some(Url::parse("http://httpsproxy.example.com:1284").unwrap()),
546 no_proxy_rules: Some(NoProxyRules::All),
547 };
548 assert_eq!(
549 proxies.lookup(&Url::parse("https://github.com").unwrap()),
550 None
551 );
552 assert_eq!(
553 proxies.lookup(&Url::parse("http://github.com").unwrap()),
554 None
555 );
556
557 let proxies = EnvProxies {
558 http: Some(Url::parse("http://httproxy.example.com:1284").unwrap()),
559 https: Some(Url::parse("http://httpsproxy.example.com:1284").unwrap()),
560 no_proxy_rules: Some(NoProxyRules::parse_curl_env("github.com")),
561 };
562 assert_eq!(
563 proxies.lookup(&Url::parse("https://github.com").unwrap()),
564 None
565 );
566 assert_eq!(
567 proxies.lookup(&Url::parse("http://github.com").unwrap()),
568 None
569 );
570 }
571
572 #[test]
573 fn lookup_rule_does_not_match() {
574 let resolver = EnvProxies {
575 http: Some(Url::parse("http://httproxy.example.com:1284").unwrap()),
576 https: Some(Url::parse("http://httpsproxy.example.com:1284").unwrap()),
577 no_proxy_rules: Some(NoProxyRules::default()),
578 };
579 assert_eq!(
580 resolver.lookup(&Url::parse("https://github.com").unwrap()),
581 Some(&Url::parse("http://httpsproxy.example.com:1284").unwrap())
582 );
583 assert_eq!(
584 resolver.lookup(&Url::parse("http://github.com").unwrap()),
585 Some(&Url::parse("http://httproxy.example.com:1284").unwrap())
586 );
587
588 let proxies = EnvProxies {
589 http: Some(Url::parse("http://httproxy.example.com:1284").unwrap()),
590 https: Some(Url::parse("http://httpsproxy.example.com:1284").unwrap()),
591 no_proxy_rules: Some(NoProxyRules::parse_curl_env("github.net")),
592 };
593 assert_eq!(
594 proxies.lookup(&Url::parse("https://github.com").unwrap()),
595 Some(&Url::parse("http://httpsproxy.example.com:1284").unwrap())
596 );
597 assert_eq!(
598 proxies.lookup(&Url::parse("http://github.com").unwrap()),
599 Some(&Url::parse("http://httproxy.example.com:1284").unwrap())
600 );
601 }
602}