use std::env;
use std::sync::Arc;
use http::Uri;
use hyper_http_proxy::{Intercept, Proxy, ProxyConnector};
use hyper_rustls::HttpsConnectorBuilder;
use hyper_util::client::legacy::connect::HttpConnector;
use super::{HttpStackError, ProxyConfig, ProxySettings};
pub type ProxyAwareConnector = hyper_rustls::HttpsConnector<ProxyConnector<HttpConnector>>;
pub fn build_proxy_connector(config: &ProxyConfig) -> Result<ProxyAwareConnector, HttpStackError> {
let entries = resolve_proxy(config)?;
let mut http_connector = HttpConnector::new();
http_connector.enforce_http(false);
let mut proxy_connector = ProxyConnector::unsecured(http_connector);
for entry in entries {
proxy_connector.add_proxy(Proxy::new(entry.intercept, entry.uri));
}
let https = HttpsConnectorBuilder::new()
.with_native_roots()
.map_err(|e| HttpStackError::Config {
hint: format!("load native TLS roots failed: {e}"),
})?
.https_or_http()
.enable_all_versions()
.wrap_connector(proxy_connector);
Ok(https)
}
struct ResolvedProxy {
intercept: Intercept,
uri: Uri,
}
fn resolve_proxy(config: &ProxyConfig) -> Result<Vec<ResolvedProxy>, HttpStackError> {
match config {
ProxyConfig::Disabled => Ok(Vec::new()),
ProxyConfig::FromEnv => resolve_from_env(),
ProxyConfig::Explicit(settings) => resolve_explicit(settings),
}
}
fn resolve_from_env() -> Result<Vec<ResolvedProxy>, HttpStackError> {
let http_proxy = env_proxy("http_proxy", "HTTP_PROXY")?;
let https_proxy = env_proxy("https_proxy", "HTTPS_PROXY")?;
let no_proxy = parse_no_proxy(env_first("no_proxy", "NO_PROXY").as_deref().unwrap_or(""));
let settings = ProxySettings {
http_proxy,
https_proxy,
no_proxy,
};
resolve_explicit(&settings)
}
fn resolve_explicit(settings: &ProxySettings) -> Result<Vec<ResolvedProxy>, HttpStackError> {
if no_proxy_disables_all(&settings.no_proxy) {
return Ok(Vec::new());
}
let no_proxy = Arc::<[String]>::from(settings.no_proxy.clone());
let mut entries = Vec::with_capacity(2);
if let Some(uri) = settings.http_proxy.clone() {
entries.push(ResolvedProxy {
intercept: scheme_intercept_with_no_proxy("http", no_proxy.clone()),
uri,
});
}
if let Some(uri) = settings.https_proxy.clone() {
entries.push(ResolvedProxy {
intercept: scheme_intercept_with_no_proxy("https", no_proxy.clone()),
uri,
});
}
Ok(entries)
}
fn env_proxy(lower: &str, upper: &str) -> Result<Option<Uri>, HttpStackError> {
let raw = match env_first(lower, upper) {
Some(v) => v,
None => return Ok(None),
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return Ok(None);
}
let uri = trimmed.parse::<Uri>().map_err(|e| HttpStackError::Config {
hint: format!("invalid proxy URL `{trimmed}` from env: {e}"),
})?;
Ok(Some(uri))
}
fn env_first(lower: &str, upper: &str) -> Option<String> {
if let Ok(v) = env::var(lower)
&& !v.trim().is_empty()
{
return Some(v);
}
if let Ok(v) = env::var(upper)
&& !v.trim().is_empty()
{
return Some(v);
}
None
}
fn scheme_intercept_with_no_proxy(scheme: &'static str, no_proxy: Arc<[String]>) -> Intercept {
Intercept::Custom(
(move |s: Option<&str>, h: Option<&str>, _p: Option<u16>| -> bool {
if s != Some(scheme) {
return false;
}
let host = match h {
Some(h) => h,
None => return true,
};
!matches_no_proxy(host, &no_proxy)
})
.into(),
)
}
fn parse_no_proxy(raw: &str) -> Vec<String> {
raw.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn no_proxy_disables_all(patterns: &[String]) -> bool {
patterns.iter().any(|p| p == "*")
}
pub(crate) fn matches_no_proxy(host: &str, patterns: &[String]) -> bool {
let host = host.trim_end_matches('.').to_ascii_lowercase();
if host.is_empty() {
return false;
}
for raw in patterns {
let pat = raw
.trim_start_matches('.')
.trim_end_matches('.')
.to_ascii_lowercase();
if pat.is_empty() {
continue;
}
if host == pat {
return true;
}
if host.ends_with(&format!(".{pat}")) {
return true;
}
}
false
}
#[cfg(test)]
mod tests;