use std::time::Duration;
const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 10;
const DEFAULT_POOL_IDLE_TIMEOUT: Duration = Duration::from_secs(90);
#[derive(Debug, Clone, Default)]
pub struct ProxyConfig {
pub url: Option<String>,
pub no_proxy: Option<String>,
}
impl ProxyConfig {
fn normalize(&mut self) {
if self.url.as_ref().is_some_and(|s| s.trim().is_empty()) {
self.url = None;
}
if self.no_proxy.as_ref().is_some_and(|s| s.trim().is_empty()) {
self.no_proxy = None;
}
}
}
#[derive(Debug)]
pub struct HttpClient {
inner: reqwest::Client,
max_idle_per_host: usize,
idle_timeout: Duration,
proxy_config: ProxyConfig,
}
impl HttpClient {
pub fn new() -> Self {
HttpClientBuilder::new()
.build()
.expect("HttpClient construction should not fail with valid defaults")
}
pub fn client(&self) -> &reqwest::Client {
&self.inner
}
pub fn pool_config(&self) -> (usize, Duration) {
(self.max_idle_per_host, self.idle_timeout)
}
pub fn proxy_config(&self) -> &ProxyConfig {
&self.proxy_config
}
}
impl Default for HttpClient {
fn default() -> Self {
Self::new()
}
}
pub struct HttpClientBuilder {
max_idle_per_host: usize,
idle_timeout: Duration,
proxy_config: ProxyConfig,
}
impl HttpClientBuilder {
pub fn new() -> Self {
Self {
max_idle_per_host: DEFAULT_POOL_MAX_IDLE_PER_HOST,
idle_timeout: DEFAULT_POOL_IDLE_TIMEOUT,
proxy_config: ProxyConfig::default(),
}
}
pub fn max_idle_per_host(mut self, n: usize) -> Self {
self.max_idle_per_host = n;
self
}
pub fn idle_timeout(mut self, d: Duration) -> Self {
self.idle_timeout = d;
self
}
pub fn proxy(mut self, config: ProxyConfig) -> Self {
self.proxy_config = config;
self.proxy_config.normalize();
self
}
pub fn build(self) -> Result<HttpClient, reqwest::Error> {
let mut builder = reqwest::Client::builder()
.pool_max_idle_per_host(self.max_idle_per_host)
.pool_idle_timeout(Some(self.idle_timeout));
if let Some(ref url) = self.proxy_config.url {
let mut proxy = reqwest::Proxy::all(url)?;
if let Some(ref np) = self.proxy_config.no_proxy {
proxy = proxy.no_proxy(reqwest::NoProxy::from_string(np));
}
builder = builder.proxy(proxy);
}
let inner = builder.build()?;
Ok(HttpClient {
inner,
max_idle_per_host: self.max_idle_per_host,
idle_timeout: self.idle_timeout,
proxy_config: self.proxy_config,
})
}
}
impl Default for HttpClientBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn resolve_proxy(
http_proxy: Option<&str>,
https_proxy: Option<&str>,
no_proxy: Option<&str>,
) -> ProxyConfig {
let url = https_proxy
.and_then(|s| {
if s.trim().is_empty() {
None
} else {
Some(s.to_string())
}
})
.or_else(|| {
http_proxy.and_then(|s| {
if s.trim().is_empty() {
None
} else {
Some(s.to_string())
}
})
});
let np = no_proxy.and_then(|s| {
if s.trim().is_empty() {
None
} else {
Some(s.to_string())
}
});
ProxyConfig { url, no_proxy: np }
}
fn env_var_case_insensitive(upper: &str, lower: &str) -> Option<String> {
std::env::var(upper)
.ok()
.or_else(|| std::env::var(lower).ok())
}
pub fn proxy_from_env() -> ProxyConfig {
let https_proxy = env_var_case_insensitive("HTTPS_PROXY", "https_proxy");
let http_proxy = env_var_case_insensitive("HTTP_PROXY", "http_proxy");
let no_proxy = env_var_case_insensitive("NO_PROXY", "no_proxy");
resolve_proxy(
http_proxy.as_deref(),
https_proxy.as_deref(),
no_proxy.as_deref(),
)
}
pub fn redact_proxy_credentials(url: &str) -> String {
if let Some(scheme_end) = url.find("://") {
let after_scheme = &url[scheme_end + 3..];
if let Some(at_pos) = after_scheme.find('@') {
let credentials = &after_scheme[..at_pos];
let host_part = &after_scheme[at_pos + 1..];
if credentials.contains(':') {
return format!("{}***:***@{}", &url[..scheme_end + 3], host_part);
}
return format!("{}***@{}", &url[..scheme_end + 3], host_part);
}
}
url.to_string()
}