use crate::config::{FetchPolicy, NpmConfig};
use std::path::Path;
fn apply_extra_root_certs(
mut builder: reqwest::ClientBuilder,
ca: &[String],
cafile: Option<&Path>,
scope: &str,
) -> reqwest::ClientBuilder {
for pem in ca {
match reqwest::Certificate::from_pem(pem.as_bytes()) {
Ok(cert) => builder = builder.add_root_certificate(cert),
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_CA,
"ignoring invalid {scope} ca: {e}"
),
}
}
if let Some(cafile) = cafile {
match std::fs::read(cafile) {
Ok(bytes) => match reqwest::Certificate::from_pem_bundle(&bytes) {
Ok(certs) => {
for cert in certs {
builder = builder.add_root_certificate(cert);
}
}
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_CAFILE,
"ignoring invalid {scope} cafile {}: {e}",
cafile.display()
),
},
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_UNREADABLE_CAFILE,
"ignoring unreadable {scope} cafile {}: {e}",
cafile.display()
),
}
}
builder
}
pub(super) fn build_http_client(
config: &NpmConfig,
registry_config: Option<&crate::config::AuthConfig>,
fetch_policy: &FetchPolicy,
extra_ca_certs: &[reqwest::Certificate],
) -> reqwest::Client {
build_http_client_inner(config, registry_config, fetch_policy, extra_ca_certs, false)
}
pub(super) fn load_node_extra_ca_certs() -> Vec<reqwest::Certificate> {
let Some(path) = std::env::var_os("NODE_EXTRA_CA_CERTS").filter(|value| !value.is_empty())
else {
return Vec::new();
};
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(e) => {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_UNREADABLE_CAFILE,
"ignoring unreadable NODE_EXTRA_CA_CERTS {}: {e}",
Path::new(&path).display()
);
return Vec::new();
}
};
match reqwest::Certificate::from_pem_bundle(&bytes) {
Ok(certs) => certs,
Err(e) => {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_CAFILE,
"ignoring invalid NODE_EXTRA_CA_CERTS {}: {e}",
Path::new(&path).display()
);
Vec::new()
}
}
}
pub(super) fn build_http_tarball_client(
config: &NpmConfig,
registry_config: Option<&crate::config::AuthConfig>,
fetch_policy: &FetchPolicy,
extra_ca_certs: &[reqwest::Certificate],
) -> reqwest::Client {
build_http_client_inner(config, registry_config, fetch_policy, extra_ca_certs, true)
}
fn build_http_client_inner(
config: &NpmConfig,
registry_config: Option<&crate::config::AuthConfig>,
fetch_policy: &FetchPolicy,
extra_ca_certs: &[reqwest::Certificate],
for_tarball: bool,
) -> reqwest::Client {
let pool_max_idle = config.max_sockets.unwrap_or(64);
static UA: std::sync::OnceLock<String> = std::sync::OnceLock::new();
let user_agent = UA.get_or_init(|| {
format!(
"{} ({} {})",
aube_util::embedder().user_agent,
std::env::consts::OS,
std::env::consts::ARCH
)
});
let mut builder = aube_util::http::with_webpki_root_fallback(reqwest::Client::builder())
.use_rustls_tls()
.user_agent(user_agent)
.gzip(true)
.brotli(true)
.zstd(true)
.timeout(std::time::Duration::from_millis(fetch_policy.timeout_ms))
.pool_max_idle_per_host(pool_max_idle)
.pool_idle_timeout(std::time::Duration::from_secs(90))
.tcp_nodelay(true);
if !for_tarball {
builder = builder
.http2_keep_alive_interval(std::time::Duration::from_secs(30))
.http2_keep_alive_timeout(std::time::Duration::from_secs(20))
.http2_keep_alive_while_idle(true)
.http2_adaptive_window(true)
.http2_initial_stream_window_size(Some(16 * 1024 * 1024))
.http2_initial_connection_window_size(Some(16 * 1024 * 1024))
.http2_max_frame_size(Some(16 * 1024 * 1024 - 1));
} else {
builder = builder.http1_only();
}
#[cfg(feature = "hickory-dns")]
{
builder = builder.hickory_dns(true);
}
builder = builder
.tcp_keepalive(std::time::Duration::from_secs(60))
.danger_accept_invalid_certs(!config.strict_ssl)
.min_tls_version(reqwest::tls::Version::TLS_1_2)
.redirect(reqwest::redirect::Policy::custom(|attempt| {
if attempt.previous().len() >= 10 {
return attempt.error("too many redirects");
}
if let Some(prev) = attempt.previous().last()
&& prev.scheme() == "https"
&& attempt.url().scheme() != "https"
{
return attempt.stop();
}
attempt.follow()
}))
.no_proxy();
if let Some(ip) = config.local_address {
builder = builder.local_address(Some(ip));
}
let no_proxy = config
.no_proxy
.as_deref()
.and_then(reqwest::NoProxy::from_string);
if let Some(ref url) = config.https_proxy {
match reqwest::Proxy::https(url) {
Ok(mut p) => {
if let Some(ref np) = no_proxy {
p = p.no_proxy(Some(np.clone()));
}
builder = builder.proxy(p);
}
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_HTTPS_PROXY,
"ignoring https-proxy {url:?}: {e}"
),
}
}
if let Some(ref url) = config.http_proxy {
match reqwest::Proxy::http(url) {
Ok(mut p) => {
if let Some(ref np) = no_proxy {
p = p.no_proxy(Some(np.clone()));
}
builder = builder.proxy(p);
}
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_HTTP_PROXY,
"ignoring http-proxy {url:?}: {e}"
),
}
}
for cert in extra_ca_certs {
builder = builder.add_root_certificate(cert.clone());
}
builder = apply_extra_root_certs(builder, &config.ca, config.cafile.as_deref(), "top-level");
if let Some(registry_config) = registry_config {
builder = apply_extra_root_certs(
builder,
®istry_config.tls.ca,
registry_config.tls.cafile.as_deref(),
"per-registry",
);
if let (Some(cert), Some(key)) = (®istry_config.tls.cert, ®istry_config.tls.key) {
match client_identity(cert, key) {
Ok(identity) => builder = builder.identity(identity),
Err(e) => tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_INVALID_CLIENT_CERT,
"ignoring invalid per-registry client cert/key: {e}"
),
}
}
}
builder.build().expect("failed to build HTTP client")
}
fn client_identity(cert: &str, key: &str) -> reqwest::Result<reqwest::Identity> {
let mut pem = Vec::with_capacity(cert.len() + key.len() + 1);
pem.extend_from_slice(cert.as_bytes());
if !cert.ends_with('\n') {
pem.push(b'\n');
}
pem.extend_from_slice(key.as_bytes());
reqwest::Identity::from_pem(&pem)
}
pub(super) fn force_full_packument() -> bool {
aube_util::env::embedder_env("INTERNAL_FORCE_FULL_PACKUMENT")
.as_deref()
.is_some_and(|v| v == "1")
}
#[cfg(test)]
mod tests {
use super::{build_http_client, load_node_extra_ca_certs};
use crate::config::{FetchPolicy, NpmConfig};
use std::ffi::{OsStr, OsString};
const TEST_CA_FIXTURE: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/test-ca.pem");
struct EnvVarGuard {
prior: Option<OsString>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl EnvVarGuard {
fn acquire() -> Self {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let lock = LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
EnvVarGuard {
prior: std::env::var_os("NODE_EXTRA_CA_CERTS"),
_lock: lock,
}
}
fn set(&self, value: impl AsRef<OsStr>) {
unsafe { std::env::set_var("NODE_EXTRA_CA_CERTS", value) };
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe {
match &self.prior {
Some(value) => std::env::set_var("NODE_EXTRA_CA_CERTS", value),
None => std::env::remove_var("NODE_EXTRA_CA_CERTS"),
}
}
}
}
#[test]
fn node_extra_ca_certs_is_loaded_and_failures_are_non_fatal() {
let env = EnvVarGuard::acquire();
env.set("");
assert!(load_node_extra_ca_certs().is_empty());
env.set(TEST_CA_FIXTURE);
let certs = load_node_extra_ca_certs();
assert_eq!(certs.len(), 1);
drop(build_http_client(
&NpmConfig::default(),
None,
&FetchPolicy::default(),
&certs,
));
let bad =
std::env::temp_dir().join(format!("aube-node-extra-ca-{}.pem", std::process::id()));
std::fs::write(&bad, b"not a certificate").expect("write temp ca bundle");
env.set(&bad);
assert!(load_node_extra_ca_certs().is_empty());
let _ = std::fs::remove_file(&bad);
env.set("/aube/does-not-exist.pem");
assert!(load_node_extra_ca_certs().is_empty());
}
}