use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Context;
use reqwest::{Certificate, ClientBuilder};
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TlsConfig {
#[serde(default)]
pub ca_certs: Vec<PathBuf>,
#[serde(default)]
pub insecure: bool,
}
impl TlsConfig {
pub fn apply(&self, builder: ClientBuilder) -> anyhow::Result<ClientBuilder> {
if self.insecure {
warn_insecure_once();
return Ok(builder.danger_accept_invalid_certs(true));
}
let mut builder = builder;
let native = rustls_native_certs::load_native_certs();
for err in &native.errors {
eprintln!("webtools: warning: reading a system certificate failed: {err}");
}
let mut native_roots = 0usize;
for cert in native.certs {
if let Ok(c) = Certificate::from_der(&cert) {
builder = builder.add_root_certificate(c);
native_roots += 1;
}
}
builder = builder.tls_built_in_root_certs(native_roots == 0);
if let Some(path) = std::env::var_os("SSL_CERT_FILE") {
let path = PathBuf::from(path);
match std::fs::read(&path) {
Ok(pem) => builder = add_pem_bundle(builder, &pem, &path)?,
Err(e) => eprintln!(
"webtools: warning: SSL_CERT_FILE ({}) is set but unreadable: {e}",
path.display()
),
}
}
for path in &self.ca_certs {
let pem = std::fs::read(path)
.with_context(|| format!("reading --ca-cert {}", path.display()))?;
builder = add_pem_bundle(builder, &pem, path)?;
}
Ok(builder)
}
}
fn add_pem_bundle(
mut builder: ClientBuilder,
pem: &[u8],
path: &Path,
) -> anyhow::Result<ClientBuilder> {
let certs = Certificate::from_pem_bundle(pem)
.with_context(|| format!("parsing PEM certificates from {}", path.display()))?;
if certs.is_empty() {
eprintln!(
"webtools: warning: no certificates found in {}",
path.display()
);
}
for cert in certs {
builder = builder.add_root_certificate(cert);
}
Ok(builder)
}
fn warn_insecure_once() {
static WARNED: AtomicBool = AtomicBool::new(false);
if !WARNED.swap(true, Ordering::Relaxed) {
eprintln!(
"webtools: WARNING: --insecure disables TLS certificate verification; \
the connection can be intercepted. Use only as a last resort — \
prefer the OS trust store, SSL_CERT_FILE, or --ca-cert."
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_secure_with_no_extra_roots() {
let cfg = TlsConfig::default();
assert!(!cfg.insecure);
assert!(cfg.ca_certs.is_empty());
assert!(cfg.apply(reqwest::Client::builder()).is_ok());
}
#[test]
fn insecure_config_applies() {
let cfg = TlsConfig {
insecure: true,
..Default::default()
};
assert!(cfg.apply(reqwest::Client::builder()).is_ok());
}
#[test]
fn missing_ca_cert_is_an_error() {
let cfg = TlsConfig {
ca_certs: vec![PathBuf::from("/no/such/ca-cert.pem")],
..Default::default()
};
assert!(cfg.apply(reqwest::Client::builder()).is_err());
}
}