use std::{
net::{IpAddr, SocketAddr},
time::Duration,
};
#[cfg(any(feature = "cookies", feature = "http3"))]
use std::sync::Arc;
use http::header::HeaderMap;
#[cfg(feature = "cache")]
mod http_cache;
#[cfg(feature = "cache")]
pub(crate) use http_cache::{HttpCacheRecipe, HttpCacheStore};
#[cfg(feature = "cache")]
use http_cache_reqwest::{Cache, HttpCache};
use reqwest::{Client, Identity, redirect::Policy, tls::Certificate};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
#[cfg(feature = "cookies")]
use web_faith_cookies::FaithJar;
#[cfg(feature = "dns")]
use web_faith_dns::FaithResolver;
#[cfg(feature = "dns")]
use crate::retry::StaleAddressRetry;
#[cfg(feature = "http3")]
use web_faith_alt_svc::{AltSvcCache, AltSvcMiddleware, H3Prober};
use crate::{
error::{FaithError, FaithErrorKind},
options::RedirectPolicy,
retry::DeadConnectionRetry,
};
pub(crate) const DEFAULT_STREAM_WINDOW: u32 = 6 * 1024 * 1024;
pub(crate) const DEFAULT_CONNECTION_WINDOW: u32 = 15 * 1024 * 1024;
const _: () = assert!(DEFAULT_CONNECTION_WINDOW > DEFAULT_STREAM_WINDOW);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ResolvedWindows {
pub(crate) stream: u32,
pub(crate) connection: u32,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct NodeEnvRecipe {
extra_ca_certs: Vec<Certificate>,
accept_invalid_certs: bool,
no_proxy: bool,
}
impl NodeEnvRecipe {
pub fn read() -> Self {
let mut recipe = Self::default();
if let Ok(path) = std::env::var("NODE_EXTRA_CA_CERTS")
&& !path.is_empty()
&& let Ok(bytes) = std::fs::read(&path)
&& let Ok(certs) = Certificate::from_pem_bundle(&bytes)
{
recipe.extra_ca_certs = certs;
}
recipe.accept_invalid_certs =
std::env::var("NODE_TLS_REJECT_UNAUTHORIZED").as_deref() == Ok("0");
recipe.no_proxy = std::env::var("NODE_USE_ENV_PROXY").as_deref() == Ok("0");
recipe
}
fn apply(&self, mut client: reqwest::ClientBuilder) -> reqwest::ClientBuilder {
if !self.extra_ca_certs.is_empty() {
client = client.tls_certs_merge(self.extra_ca_certs.iter().cloned());
}
if self.accept_invalid_certs {
client = client.danger_accept_invalid_certs(true);
}
if self.no_proxy {
client = client.no_proxy();
}
client
}
}
#[cfg(feature = "http3")]
#[derive(Debug, Clone)]
pub(crate) struct H3UpgradeRecipe {
pub(crate) enabled: bool,
pub(crate) attempt_timeout: Option<Duration>,
pub(crate) probe: bool,
pub(crate) probe_timeout: Option<Duration>,
}
#[derive(Debug, Clone)]
pub(crate) struct ClientRecipe {
pub(crate) user_agent: String,
pub(crate) local_address: Option<IpAddr>,
pub(crate) default_headers: Option<HeaderMap>,
#[cfg(feature = "dns")]
pub(crate) dns_system: bool,
pub(crate) dns_overrides: Vec<(String, Vec<SocketAddr>)>,
pub(crate) http2_adaptive_window: bool,
pub(crate) http2_windows: Option<ResolvedWindows>,
#[cfg(feature = "http3")]
pub(crate) http3_max_idle_timeout: Duration,
#[cfg(feature = "http3")]
pub(crate) http3_windows: ResolvedWindows,
#[cfg(feature = "http3")]
pub(crate) http3_congestion_bbr: bool,
#[cfg(feature = "http3")]
pub(crate) http3_send_window: Option<u32>,
pub(crate) pool_idle_timeout: Option<Duration>,
pub(crate) pool_max_idle_per_host: Option<usize>,
pub(crate) redirect: Option<RedirectPolicy>,
pub(crate) connect_timeout: Option<Duration>,
pub(crate) read_timeout: Option<Duration>,
pub(crate) total_timeout: Option<Duration>,
#[cfg(feature = "http3")]
pub(crate) tls_early_data: Option<bool>,
pub(crate) tls_identity: Option<Identity>,
pub(crate) tls_required: Option<bool>,
pub(crate) tls_extra_roots: Vec<Certificate>,
pub(crate) node_env: NodeEnvRecipe,
#[cfg(feature = "cache")]
pub(crate) http_cache: Option<HttpCacheRecipe>,
#[cfg(feature = "http3")]
pub(crate) h3_upgrade: H3UpgradeRecipe,
}
#[cfg(all(feature = "http3", feature = "dns"))]
pub fn install_https_sink(
dns_resolver: Option<&FaithResolver>,
alt_svc_cache: Option<&Arc<AltSvcCache>>,
prober: Option<&Arc<H3Prober>>,
upgrade_enabled: bool,
) {
if !upgrade_enabled {
return;
}
let (Some(resolver), Some(cache)) = (dns_resolver, alt_svc_cache) else {
return;
};
resolver.set_https_sink(Arc::new(web_faith_alt_svc::H3HttpsSink::new(
Arc::clone(cache),
prober,
)));
}
pub(crate) struct BuiltClients {
pub(crate) client: ClientWithMiddleware,
pub(crate) raw_client: Client,
#[cfg(feature = "http3")]
pub(crate) prober: Option<Arc<H3Prober>>,
}
#[cfg(all(feature = "tls-ring", not(feature = "tls-aws-lc-rs")))]
fn install_crypto_provider() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
}
impl ClientRecipe {
pub fn conn_timeout(&self) -> Duration {
self.pool_idle_timeout.unwrap_or(Duration::from_secs(90))
}
pub fn build(
&self,
#[cfg(feature = "cookies")] cookie_jar: Option<&Arc<FaithJar>>,
#[cfg(feature = "dns")] dns_resolver: Option<&FaithResolver>,
#[cfg(feature = "http3")] alt_svc_cache: Option<&Arc<AltSvcCache>>,
) -> Result<BuiltClients, FaithError> {
#[cfg(all(feature = "tls-ring", not(feature = "tls-aws-lc-rs")))]
install_crypto_provider();
let mut client = Client::builder()
.tls_info(true)
.tls_sslkeylogfile(true)
.user_agent(self.user_agent.clone());
if let Some(ip) = self.local_address {
client = client.local_address(ip);
}
#[cfg(feature = "cookies")]
if let Some(jar) = cookie_jar {
client = client.cookie_provider(jar.clone());
}
for (domain, addresses) in &self.dns_overrides {
client = client.resolve_to_addrs(domain, addresses);
}
#[cfg(feature = "dns")]
if self.dns_system {
client = client.no_hickory_dns();
} else if let Some(resolver) = dns_resolver {
client = client.dns_resolver(resolver.clone());
}
if let Some(headers) = &self.default_headers {
client = client.default_headers(headers.clone());
}
if self.http2_adaptive_window {
client = client.http2_adaptive_window(true);
} else if let Some(windows) = self.http2_windows {
client = client
.http2_initial_stream_window_size(windows.stream)
.http2_initial_connection_window_size(windows.connection);
}
#[cfg(feature = "http3")]
{
client = client
.http3_max_idle_timeout(self.http3_max_idle_timeout)
.http3_stream_receive_window(self.http3_windows.stream.into())
.http3_conn_receive_window(self.http3_windows.connection.into());
if self.http3_congestion_bbr {
client = client.http3_congestion_bbr();
}
if let Some(send_window) = self.http3_send_window {
client = client.http3_send_window(send_window.into());
}
}
if let Some(timeout) = self.pool_idle_timeout {
client = client.pool_idle_timeout(Some(timeout));
}
if let Some(max_idle) = self.pool_max_idle_per_host {
client = client.pool_max_idle_per_host(max_idle);
}
match self.redirect {
None | Some(RedirectPolicy::Follow) => {}
Some(RedirectPolicy::Error) => {
client = client.redirect(Policy::custom(|attempt| {
attempt.error(FaithError::from(FaithErrorKind::Redirect))
}));
}
Some(RedirectPolicy::Stop) => {
client = client.redirect(Policy::none());
}
}
if let Some(timeout) = self.connect_timeout {
client = client.connect_timeout(timeout);
}
if let Some(timeout) = self.read_timeout {
client = client.read_timeout(timeout);
}
if let Some(timeout) = self.total_timeout {
client = client.timeout(timeout);
}
#[cfg(feature = "http3")]
if let Some(early_data) = self.tls_early_data {
client = client.tls_early_data(early_data);
}
if let Some(identity) = &self.tls_identity {
client = client.identity(identity.clone());
}
if let Some(https_only) = self.tls_required {
client = client.https_only(https_only);
}
if !self.tls_extra_roots.is_empty() {
client = client.tls_certs_merge(self.tls_extra_roots.iter().cloned());
}
client = self.node_env.apply(client);
let raw_client = client
.build()
.map_err(|e| FaithError::new(FaithErrorKind::Config, format!("{e:?}")))?;
let mut client = ClientBuilder::new(raw_client.clone());
#[cfg(feature = "http3")]
let prober = {
alt_svc_cache
.filter(|_| self.h3_upgrade.enabled && self.h3_upgrade.probe)
.map(|cache| {
Arc::new(H3Prober::new(
raw_client.clone(),
cache.clone(),
self.h3_upgrade.probe_timeout,
))
})
};
#[cfg(feature = "cache")]
if let Some(cache) = &self.http_cache {
client = match &cache.store {
HttpCacheStore::Disk(manager) => client.with(Cache(HttpCache {
mode: cache.mode,
manager: manager.clone(),
options: cache.options.clone(),
})),
HttpCacheStore::Memory(manager) => client.with(Cache(HttpCache {
mode: cache.mode,
manager: manager.clone(),
options: cache.options.clone(),
})),
};
}
#[cfg(feature = "http3")]
if let Some(alt_svc_cache) = alt_svc_cache {
client = client.with(AltSvcMiddleware::new(
alt_svc_cache.clone(),
self.h3_upgrade.enabled,
self.h3_upgrade.attempt_timeout,
prober.clone(),
));
}
#[cfg(feature = "dns")]
{
client = client.with(StaleAddressRetry::new(dns_resolver.cloned()));
}
client = client.with(DeadConnectionRetry);
Ok(BuiltClients {
client: client.build(),
raw_client,
#[cfg(feature = "http3")]
prober,
})
}
}