#![cfg(all(not(target_arch = "wasm32"), feature = "proxy"))]
use std::{collections::HashSet, env, fmt, str::FromStr, sync::Arc};
use base64::Engine;
use http::{HeaderValue, Uri};
#[derive(Clone, Debug)]
pub struct Proxy {
matcher: Arc<Matcher>,
}
impl Proxy {
#[must_use]
pub fn from_env() -> Self {
Self::new(Matcher::from_env())
}
#[must_use]
pub fn from_system() -> Self {
Self::from_env()
}
#[must_use]
pub fn builder() -> ProxyBuilder {
ProxyBuilder {
http: None,
https: None,
all: None,
no_proxy: HashSet::new(),
}
}
fn new(matcher: Matcher) -> Self {
Self {
matcher: Arc::new(matcher),
}
}
#[allow(dead_code)]
pub(crate) fn into_matcher(self) -> Arc<Matcher> {
self.matcher
}
#[cfg(any(feature = "curl-backend", test))]
pub(crate) fn intercept(&self, uri: &Uri) -> Option<Intercept> {
self.matcher.intercept(uri)
}
}
pub struct ProxyBuilder {
http: Option<String>,
https: Option<String>,
all: Option<String>,
no_proxy: HashSet<String>,
}
impl fmt::Debug for ProxyBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProxyBuilder").finish_non_exhaustive()
}
}
impl ProxyBuilder {
#[must_use]
pub fn all(mut self, value: impl Into<String>) -> Self {
self.all = Some(value.into());
self
}
#[must_use]
pub fn http(mut self, value: impl Into<String>) -> Self {
self.http = Some(value.into());
self
}
#[must_use]
pub fn https(mut self, value: impl Into<String>) -> Self {
self.https = Some(value.into());
self
}
#[must_use]
pub fn no_proxy(mut self, value: impl Into<String>) -> Self {
let raw = value.into();
let entries = raw
.split(',')
.filter(|s| !s.is_empty())
.map(str::to_lowercase)
.collect::<Vec<_>>();
self.no_proxy.extend(entries);
self
}
#[must_use]
pub fn build(self) -> Proxy {
let matcher = Matcher {
http: self.http.as_deref().and_then(ProxyConfig::parse),
https: self.https.as_deref().and_then(ProxyConfig::parse),
all: self.all.as_deref().and_then(ProxyConfig::parse),
no_proxy: self.no_proxy,
};
Proxy::new(matcher)
}
}
#[derive(Clone, Debug)]
struct ProxyConfig {
uri: Uri,
basic_auth: Option<HeaderValue>,
raw_auth: Option<(String, String)>,
}
impl ProxyConfig {
fn parse(value: &str) -> Option<Self> {
let parsed = Uri::from_str(value).ok()?;
let auth = parsed.authority()?;
let (userinfo, _) = auth
.as_str()
.rsplit_once('@')
.unwrap_or(("", auth.as_str()));
let basic_auth = (!userinfo.is_empty())
.then(|| {
let encoded = base64::engine::general_purpose::STANDARD.encode(userinfo.as_bytes());
HeaderValue::from_str(&format!("Basic {encoded}")).ok()
})
.flatten();
let raw_auth = userinfo
.split_once(':')
.map(|(user, pass)| (user.to_string(), pass.to_string()));
Some(Self {
uri: parsed,
basic_auth,
raw_auth,
})
}
}
#[derive(Clone, Debug)]
pub(crate) struct Intercept {
uri: Uri,
basic_auth: Option<HeaderValue>,
raw_auth: Option<(String, String)>,
}
impl Intercept {
pub(crate) const fn uri(&self) -> &Uri {
&self.uri
}
pub(crate) const fn basic_auth(&self) -> Option<&HeaderValue> {
self.basic_auth.as_ref()
}
pub(crate) fn raw_auth(&self) -> Option<(&str, &str)> {
self.raw_auth
.as_ref()
.map(|(user, pass)| (user.as_str(), pass.as_str()))
}
}
#[derive(Clone, Debug)]
pub(crate) struct Matcher {
http: Option<ProxyConfig>,
https: Option<ProxyConfig>,
all: Option<ProxyConfig>,
no_proxy: HashSet<String>,
}
impl Matcher {
fn from_env() -> Self {
let http = env::var("HTTP_PROXY").ok();
let https = env::var("HTTPS_PROXY").ok();
let all = env::var("ALL_PROXY").ok();
let no_proxy = env::var("NO_PROXY")
.ok()
.map(|v| {
v.split(',')
.filter(|s| !s.is_empty())
.map(str::to_lowercase)
.collect()
})
.unwrap_or_default();
Self {
http: http.as_deref().and_then(ProxyConfig::parse),
https: https.as_deref().and_then(ProxyConfig::parse),
all: all.as_deref().and_then(ProxyConfig::parse),
no_proxy,
}
}
fn intercept(&self, uri: &Uri) -> Option<Intercept> {
let host = uri.host()?.to_lowercase();
if self.no_proxy.iter().any(|entry| host.ends_with(entry)) {
return None;
}
let scheme = uri.scheme_str().unwrap_or("http");
let config = match scheme {
"http" => self.http.as_ref().or(self.all.as_ref())?,
"https" => self.https.as_ref().or(self.all.as_ref())?,
_ => return None,
};
Some(Intercept {
uri: config.uri.clone(),
basic_auth: config.basic_auth.clone(),
raw_auth: config.raw_auth.clone(),
})
}
}