1use std::{
8 env::{var, var_os},
9 fmt::Write as _,
10 net::IpAddr,
11 sync::Arc,
12};
13
14use http::Uri;
15use ipnet::IpNet;
16use reqwest::{ClientBuilder, NoProxy as ReqwestNoProxy, Proxy, Url};
17use serde::Deserialize;
18use smallstr::SmallString;
19use smallvec::SmallVec;
20use url::Host;
21
22use crate::{Err, Result, implement, utils::url::hostname_matches_domain};
23
24type Domain = SmallString<[u8; 32]>;
25type Domains = Box<[Domain]>;
26type Networks = Box<[Network]>;
27type Proxies = SmallVec<[Proxy; 1]>;
28type ProxyHostBuffer = SmallVec<[ProxyHost; 2]>;
29
30pub type ProxyHost = SmallString<[u8; 32]>;
34
35pub type ProxyHosts = Arc<[ProxyHost]>;
40type ProxyUrlString = SmallString<[u8; 128]>;
41
42#[derive(Clone, Copy, Eq, PartialEq)]
43enum ProxyScheme {
44 Http,
45 Https,
46 Socks4,
47 Socks4a,
48 Socks5,
49 Socks5h,
50}
51
52pub struct ProxySnapshot {
57 configured: Option<Arc<ProxyConfig>>,
58 environment: Option<EnvironmentProxy>,
59 proxies: Proxies,
60 hosts: ProxyHosts,
61}
62
63#[derive(Default)]
64struct EnvironmentProxy {
65 http: Option<ProxyScheme>,
66 https: Option<ProxyScheme>,
67 bypass: NoProxyRules,
68}
69
70#[derive(Clone, PartialEq)]
71struct EnvironmentEndpoint {
72 scheme: ProxyScheme,
73 url: Url,
74 host: Option<ProxyHost>,
75}
76
77#[derive(Default)]
78struct EnvironmentProxyBuild {
79 policy: EnvironmentProxy,
80 proxies: [Option<Proxy>; 2],
81 hosts: [Option<ProxyHost>; 2],
82}
83
84#[derive(Default)]
85struct NoProxyRules {
86 all_domains: bool,
87 ips: Networks,
88 domains: Domains,
89}
90
91enum Network {
92 Address(IpAddr),
93 Range(IpNet),
94}
95
96#[derive(Clone, Default, Debug, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum ProxyConfig {
124 #[default]
125 None,
130
131 Global {
136 #[serde(deserialize_with = "crate::utils::deserialize_from_str")]
141 url: Url,
142 },
143
144 ByDomain(Vec<PartialProxyConfig>),
149}
150
151#[implement(ProxyConfig)]
157pub fn to_proxy(&self) -> Result<Option<Proxy>> {
158 self.validate_proxy_schemes()?;
159
160 let proxy = match self {
161 | Self::None => None,
162 | Self::Global { url } => Some(Proxy::all(url.clone())?),
163 | Self::ByDomain(_) => {
164 let config = self.clone();
165
166 Some(Proxy::custom(move |url| config.proxy_for(url).cloned()))
167 },
168 };
169
170 Ok(proxy)
171}
172
173#[implement(ProxyConfig)]
174fn validate_proxy_schemes(&self) -> Result {
175 if let Some(url) = self
176 .proxy_urls()
177 .find(|url| ProxyScheme::parse(url.scheme()).is_none())
178 {
179 let scheme = url.scheme();
180
181 return Err!(Config("proxy", "Unsupported proxy scheme: {scheme}"));
182 }
183
184 Ok(())
185}
186
187#[implement(ProxyScheme)]
188fn parse(scheme: &str) -> Option<Self> {
189 match scheme {
190 | "http" => Some(Self::Http),
191 | "https" => Some(Self::Https),
192 | "socks4" => Some(Self::Socks4),
193 | "socks4a" => Some(Self::Socks4a),
194 | "socks5" => Some(Self::Socks5),
195 | "socks5h" => Some(Self::Socks5h),
196 | _ => None,
197 }
198}
199
200#[implement(ProxyScheme)]
201const fn as_str(self) -> &'static str {
202 match self {
203 | Self::Http => "http",
204 | Self::Https => "https",
205 | Self::Socks4 => "socks4",
206 | Self::Socks4a => "socks4a",
207 | Self::Socks5 => "socks5",
208 | Self::Socks5h => "socks5h",
209 }
210}
211
212#[implement(ProxyConfig)]
217#[inline]
218pub fn hosts(&self) -> impl Iterator<Item = &str> { self.proxy_urls().filter_map(proxy_hostname) }
219
220fn proxy_hostname(url: &Url) -> Option<&str> {
221 match url.host()? {
222 | Host::Domain(host) => Some(host),
223 | Host::Ipv4(_) | Host::Ipv6(_) => None,
224 }
225}
226
227#[implement(ProxyConfig)]
228fn proxy_urls(&self) -> impl Iterator<Item = &Url> {
229 let global = match self {
230 | Self::Global { url } => Some(url),
231 | _ => None,
232 };
233
234 let domains = match self {
235 | Self::ByDomain(proxies) => Some(proxies.as_slice()),
236 | _ => None,
237 };
238
239 global.into_iter().chain(
240 domains
241 .into_iter()
242 .flatten()
243 .map(|proxy| &proxy.url),
244 )
245}
246
247#[implement(ProxyConfig)]
252#[inline]
253#[must_use]
254pub fn intercepts(&self, url: &Url) -> bool { self.proxy_for(url).is_some() }
255
256#[implement(ProxyConfig)]
257fn proxy_for(&self, url: &Url) -> Option<&Url> {
258 matches!(url.scheme(), "http" | "https")
259 .then(|| match self {
260 | Self::None => None,
261 | Self::Global { url } => Some(url),
262 | Self::ByDomain(proxies) => proxies
263 .iter()
264 .find_map(|proxy| proxy.for_url(url)),
265 })
266 .flatten()
267 .filter(|proxy| ProxyScheme::parse(proxy.scheme()).is_some())
268}
269
270#[implement(ProxySnapshot)]
276#[must_use]
277pub fn new(config: &ProxyConfig) -> Result<Self> {
278 Self::with_vars(config, var_os("REQUEST_METHOD").is_some(), |name| var(name).ok())
279}
280
281#[implement(ProxySnapshot)]
282pub(super) fn with_vars<F>(config: &ProxyConfig, is_cgi: bool, var: F) -> Result<Self>
283where
284 F: Fn(&str) -> Option<String>,
285{
286 let configured = (!matches!(config, ProxyConfig::None)).then(|| Arc::new(config.clone()));
287 let environment = matches!(config, ProxyConfig::None)
288 .then(|| EnvironmentProxy::with_vars(is_cgi, &var))
289 .transpose()?;
290
291 let (environment, proxies, environment_hosts) = match environment {
292 | Some(EnvironmentProxyBuild { policy, proxies, hosts }) =>
293 (Some(policy), proxies.into_iter().flatten().collect(), hosts),
294 | None => {
295 let proxy = configured
296 .as_ref()
297 .map(ProxyConfig::to_proxy_shared)
298 .transpose()?
299 .flatten();
300
301 (None, proxy.into_iter().collect(), [None, None])
302 },
303 };
304
305 let hosts = config
306 .hosts()
307 .map(ProxyHost::from)
308 .chain(environment_hosts.into_iter().flatten())
309 .fold(ProxyHostBuffer::new(), |mut hosts, host| {
310 if !hosts
311 .iter()
312 .any(|known| known.eq_ignore_ascii_case(&host))
313 {
314 hosts.push(host);
315 }
316
317 hosts
318 });
319
320 let spills = hosts.spilled() || hosts.iter().any(ProxyHost::spilled);
321 let hosts = match hosts {
322 | hosts if hosts.is_empty() => ProxyHosts::default(),
323 | hosts if spills => ProxyHosts::from(hosts.into_vec()),
324 | hosts => ProxyHosts::from(hosts.as_slice()),
325 };
326
327 Ok(Self { configured, environment, proxies, hosts })
328}
329
330#[implement(EnvironmentProxy)]
331fn with_vars<F>(is_cgi: bool, var: &F) -> Result<EnvironmentProxyBuild>
332where
333 F: Fn(&str) -> Option<String>,
334{
335 if is_cgi {
336 return Ok(EnvironmentProxyBuild::default());
337 }
338
339 let http = first_var(var, ["HTTP_PROXY", "http_proxy"])
340 .as_deref()
341 .and_then(parse_environment_url);
342
343 let https = first_var(var, ["HTTPS_PROXY", "https_proxy"])
344 .as_deref()
345 .and_then(parse_environment_url);
346
347 let all = (http.is_none() || https.is_none())
348 .then(|| {
349 first_var(var, ["ALL_PROXY", "all_proxy"])
350 .as_deref()
351 .and_then(parse_environment_url)
352 })
353 .flatten();
354
355 if http.is_none() && https.is_none() && all.is_none() {
356 return Ok(EnvironmentProxyBuild::default());
357 }
358
359 let no_proxy = first_var(var, ["NO_PROXY", "no_proxy"]).unwrap_or_default();
360 let bypass = NoProxyRules::new(&no_proxy);
361 let route = |proxy: &Option<EnvironmentEndpoint>| {
362 proxy
363 .as_ref()
364 .or(all.as_ref())
365 .map(|proxy| proxy.scheme)
366 };
367
368 let policy = Self {
369 http: route(&http),
370 https: route(&https),
371 bypass,
372 };
373
374 let mut http = http;
375 let mut https = https;
376 let mut all = all;
377 let take_host = |proxy: &mut Option<EnvironmentEndpoint>,
378 all: &mut Option<EnvironmentEndpoint>| {
379 proxy
380 .as_mut()
381 .and_then(|proxy| proxy.host.take())
382 .or_else(|| all.as_mut().and_then(|proxy| proxy.host.take()))
383 };
384
385 let hosts = [take_host(&mut http, &mut all), take_host(&mut https, &mut all)];
386
387 let proxies = environment_proxies(http, https, all, &no_proxy)?;
388
389 Ok(EnvironmentProxyBuild { policy, proxies, hosts })
390}
391
392fn first_var<F>(var: &F, names: [&str; 2]) -> Option<String>
393where
394 F: Fn(&str) -> Option<String>,
395{
396 names.into_iter().find_map(var)
397}
398
399#[cfg(test)]
400pub(super) fn parse_environment_proxy_url(raw: &str) -> Option<Url> {
401 parse_environment_url(raw).map(|proxy| proxy.url)
402}
403
404fn parse_environment_url(raw: &str) -> Option<EnvironmentEndpoint> {
405 let uri = raw.parse::<Uri>().ok()?;
406 let scheme = uri
407 .scheme_str()
408 .map_or(Some(ProxyScheme::Http), ProxyScheme::parse)?;
409
410 let authority = uri.authority()?;
411 let (userinfo, host_port) = authority
412 .as_str()
413 .split_once('@')
414 .map_or((None, authority.as_str()), |(userinfo, host_port)| (Some(userinfo), host_port));
415
416 let uri = Uri::builder()
417 .scheme(scheme.as_str())
418 .authority(host_port)
419 .path_and_query("/")
420 .build()
421 .ok()?;
422
423 let host = proxy_uri_hostname(&uri).map(ProxyHost::from);
424 let url = effective_environment_url(&uri, userinfo)?;
425
426 Some(EnvironmentEndpoint { scheme, url, host })
427}
428
429fn proxy_uri_hostname(uri: &Uri) -> Option<&str> {
430 let host = proxy_uri_host(uri.host()?);
431
432 host.parse::<IpAddr>().is_err().then_some(host)
433}
434
435fn proxy_uri_host(host: &str) -> &str {
436 host.strip_prefix('[')
437 .and_then(|host| host.strip_suffix(']'))
438 .unwrap_or(host)
439}
440
441fn effective_environment_url(uri: &Uri, userinfo: Option<&str>) -> Option<Url> {
442 let scheme = uri.scheme_str()?;
443 let authority = uri.authority()?;
444 let host = proxy_uri_host(authority.host());
445
446 let port = authority
447 .port_u16()
448 .unwrap_or(if scheme == "https" { 443 } else { 80 });
449
450 let mut normalized = ProxyUrlString::new();
451
452 normalized.push_str(scheme);
453 normalized.push_str("://");
454 if let Some(userinfo) = userinfo {
455 normalized.push_str(userinfo);
456 normalized.push('@');
457 }
458
459 if matches!(host.parse::<IpAddr>(), Ok(IpAddr::V6(_))) {
460 normalized.push('[');
461 normalized.push_str(host);
462 normalized.push(']');
463 } else {
464 normalized.push_str(host);
465 }
466
467 write!(&mut normalized, ":{port}/").ok()?;
468 Url::parse(&normalized).ok()
469}
470
471fn environment_proxies(
472 http: Option<EnvironmentEndpoint>,
473 https: Option<EnvironmentEndpoint>,
474 all: Option<EnvironmentEndpoint>,
475 no_proxy: &str,
476) -> Result<[Option<Proxy>; 2]> {
477 if http.is_none() && https.is_none() {
478 let proxy = all
479 .map(|proxy| {
480 let bypass = ReqwestNoProxy::from_string(no_proxy);
481
482 Proxy::all(proxy.url).map(|proxy| proxy.no_proxy(bypass))
483 })
484 .transpose()?;
485
486 return Ok([proxy, None]);
487 }
488
489 let (http, https) = match (http, https, all) {
490 | (Some(http), Some(https), _) => (Some(http), Some(https)),
491 | (Some(http), None, all) => (Some(http), all),
492 | (None, Some(https), all) => (all, Some(https)),
493 | (None, None, _) => (None, None),
494 };
495
496 let bypass = ReqwestNoProxy::from_string(no_proxy);
497
498 match (http, https) {
499 | (None, None) => Ok([None, None]),
500 | (Some(http), None) => {
501 let proxy = Proxy::http(http.url)?.no_proxy(bypass);
502
503 Ok([Some(proxy), None])
504 },
505 | (None, Some(https)) => {
506 let proxy = Proxy::https(https.url)?.no_proxy(bypass);
507
508 Ok([None, Some(proxy)])
509 },
510 | (Some(http), Some(https)) if http == https => {
511 let proxy = Proxy::all(http.url)?.no_proxy(bypass);
512
513 Ok([Some(proxy), None])
514 },
515 | (Some(http), Some(https)) => {
516 let http = Proxy::http(http.url)?.no_proxy(bypass.clone());
517 let https = Proxy::https(https.url)?.no_proxy(bypass);
518
519 Ok([Some(http), Some(https)])
520 },
521 }
522}
523
524#[implement(ProxyConfig)]
525fn to_proxy_shared(config: &Arc<Self>) -> Result<Option<Proxy>> {
526 config.validate_proxy_schemes()?;
527
528 let proxy = match config.as_ref() {
529 | Self::None => None,
530 | Self::Global { url } => Some(Proxy::all(url.clone())?),
531 | Self::ByDomain(_) => {
532 let config = Arc::clone(config);
533
534 Some(Proxy::custom(move |url| config.proxy_for(url).cloned()))
535 },
536 };
537
538 Ok(proxy)
539}
540
541#[implement(ProxySnapshot)]
546#[must_use]
547pub fn configure(&self, builder: ClientBuilder) -> ClientBuilder {
548 let builder = if self.environment.is_some() {
549 builder.no_proxy()
550 } else {
551 builder
552 };
553
554 self.proxies
555 .iter()
556 .cloned()
557 .fold(builder, ClientBuilder::proxy)
558}
559
560#[implement(ProxySnapshot)]
565#[inline]
566pub fn hosts(&self) -> impl Iterator<Item = &str> { self.hosts.iter().map(ProxyHost::as_str) }
567
568#[implement(ProxySnapshot)]
573#[inline]
574#[must_use]
575pub fn shared_hosts(&self) -> ProxyHosts { Arc::clone(&self.hosts) }
576
577#[implement(ProxySnapshot)]
582#[inline]
583#[must_use]
584pub fn intercepts(&self, url: &Url) -> bool { self.proxy_scheme(url).is_some() }
585
586#[implement(ProxySnapshot)]
591#[must_use]
592pub fn resolver_alias(&self, url: &Url) -> bool {
593 let is_proxy_host = url.host_str().is_some_and(|host| {
594 self.hosts
595 .iter()
596 .any(|proxy| proxy.eq_ignore_ascii_case(host))
597 });
598
599 is_proxy_host
600 && self
601 .proxy_scheme(url)
602 .is_none_or(|scheme| !scheme.resolves_remotely())
603}
604
605#[implement(ProxySnapshot)]
606fn proxy_scheme(&self, url: &Url) -> Option<ProxyScheme> {
607 self.configured
608 .as_deref()
609 .and_then(|proxy| proxy.proxy_for(url))
610 .and_then(|proxy| ProxyScheme::parse(proxy.scheme()))
611 .or_else(|| {
612 self.environment
613 .as_ref()
614 .and_then(|proxy| proxy.proxy_for(url))
615 })
616}
617
618#[implement(EnvironmentProxy)]
619fn proxy_for(&self, url: &Url) -> Option<ProxyScheme> {
620 let proxy = match url.scheme() {
621 | "http" => self.http,
622 | "https" => self.https,
623 | _ => None,
624 }?;
625
626 let host = url.host()?;
627
628 (!self.bypass.contains(&host)).then_some(proxy)
629}
630
631#[implement(ProxyScheme)]
632const fn resolves_remotely(self) -> bool {
633 matches!(self, Self::Http | Self::Https | Self::Socks4a | Self::Socks5h)
634}
635
636#[implement(NoProxyRules)]
637fn new(raw: &str) -> Self {
638 let (all_domains, ips, domains) = raw
639 .split(',')
640 .map(str::trim)
641 .filter(|part| !part.is_empty())
642 .fold(
643 (false, Vec::new(), Vec::new()),
644 |(mut all_domains, mut ips, mut domains), part| {
645 if part == "*" {
646 all_domains = true;
647 domains.clear();
648 } else if let Ok(network) = part.parse::<IpNet>() {
649 ips.push(Network::Range(network));
650 } else if let Ok(address) = part.parse::<IpAddr>() {
651 ips.push(Network::Address(address));
652 } else if !all_domains {
653 domains.push(part.into());
654 }
655
656 (all_domains, ips, domains)
657 },
658 );
659
660 Self {
661 all_domains,
662 ips: ips.into_boxed_slice(),
663 domains: domains.into_boxed_slice(),
664 }
665}
666
667#[implement(NoProxyRules)]
668fn contains(&self, host: &Host<&str>) -> bool {
669 match host {
670 | Host::Ipv4(ip) => self.contains_ip(IpAddr::V4(*ip)),
671 | Host::Ipv6(ip) => self.contains_ip(IpAddr::V6(*ip)),
672 | Host::Domain(host) =>
673 self.all_domains
674 || self
675 .domains
676 .iter()
677 .any(|domain| hostname_matches_domain(host, domain)),
678 }
679}
680
681#[implement(NoProxyRules)]
682fn contains_ip(&self, ip: IpAddr) -> bool {
683 self.ips.iter().any(|network| match network {
684 | Network::Address(address) => *address == ip,
685 | Network::Range(range) => range.contains(&ip),
686 })
687}
688
689#[derive(Clone, Debug, Deserialize)]
694pub struct PartialProxyConfig {
695 #[serde(deserialize_with = "crate::utils::deserialize_from_str")]
696 url: Url,
697 #[serde(default)]
698 include: Vec<WildCardedDomain>,
699 #[serde(default)]
700 exclude: Vec<WildCardedDomain>,
701}
702impl PartialProxyConfig {
703 #[must_use]
704 pub fn for_url(&self, url: &Url) -> Option<&Url> {
710 let domain = url.domain()?;
711 let mut included_because = None; let mut excluded_because = None; if self.include.is_empty() {
714 included_because = Some(&WildCardedDomain::WildCard);
716 }
717 for wc_domain in &self.include {
718 if wc_domain.matches(domain) {
719 match included_because {
720 | Some(prev) if !wc_domain.more_specific_than(prev) => (),
721 | _ => included_because = Some(wc_domain),
722 }
723 }
724 }
725 for wc_domain in &self.exclude {
726 if wc_domain.matches(domain) {
727 match excluded_because {
728 | Some(prev) if !wc_domain.more_specific_than(prev) => (),
729 | _ => excluded_because = Some(wc_domain),
730 }
731 }
732 }
733 match (included_because, excluded_because) {
734 | (Some(a), Some(b)) if a.more_specific_than(b) => Some(&self.url),
735 | (Some(_), None) => Some(&self.url),
736 | _ => None,
737 }
738 }
739}
740
741#[derive(Clone, Debug)]
743enum WildCardedDomain {
744 WildCard,
745 WildCarded(String),
746 Exact(String),
747}
748impl WildCardedDomain {
749 fn matches(&self, domain: &str) -> bool {
750 match self {
751 | Self::WildCard => true,
752 | Self::WildCarded(d) => domain.ends_with(d),
753 | Self::Exact(d) => domain == d,
754 }
755 }
756
757 fn more_specific_than(&self, other: &Self) -> bool {
758 match (self, other) {
759 | (Self::WildCard, Self::WildCard) => false,
760 | (_, Self::WildCard) => true,
761 | (Self::Exact(a), Self::WildCarded(_)) => other.matches(a),
762 | (Self::WildCarded(a), Self::WildCarded(b)) => a != b && a.ends_with(b),
763 | _ => false,
764 }
765 }
766}
767impl std::str::FromStr for WildCardedDomain {
768 type Err = std::convert::Infallible;
769
770 #[expect(clippy::string_slice)]
771 fn from_str(s: &str) -> Result<Self, Self::Err> {
772 Ok(if s.starts_with("*.") {
774 Self::WildCarded(s[1..].to_owned())
775 } else if s == "*" {
776 Self::WildCarded(String::new())
777 } else {
778 Self::Exact(s.to_owned())
779 })
780 }
781}
782impl<'de> Deserialize<'de> for WildCardedDomain {
783 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
784 where
785 D: serde::de::Deserializer<'de>,
786 {
787 crate::utils::deserialize_from_str(deserializer)
788 }
789}