use crate::core::error::{OlError, ERR_PROXY_CONFIG_INVALID};
use super::config::EgressConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaSource {
Native,
Custom,
}
impl CaSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Native => "native",
Self::Custom => "custom",
}
}
}
pub fn ca_source(cfg: &EgressConfig) -> CaSource {
if cfg.ca_bundle.is_some() {
CaSource::Custom
} else {
CaSource::Native
}
}
pub fn interception_verdict(ca_source: CaSource) -> Option<bool> {
match ca_source {
CaSource::Custom => Some(true),
CaSource::Native => None,
}
}
pub fn apply(
builder: reqwest::ClientBuilder,
cfg: &EgressConfig,
) -> Result<reqwest::ClientBuilder, OlError> {
let Some(path) = cfg.ca_bundle.as_ref() else {
return Ok(builder);
};
let certs = read_bundle(path)?;
Ok(builder.tls_certs_merge(certs))
}
pub fn apply_blocking(
builder: reqwest::blocking::ClientBuilder,
cfg: &EgressConfig,
) -> Result<reqwest::blocking::ClientBuilder, OlError> {
let Some(path) = cfg.ca_bundle.as_ref() else {
return Ok(builder);
};
let certs = read_bundle(path)?;
Ok(builder.tls_certs_merge(certs))
}
fn read_bundle(path: &std::path::Path) -> Result<Vec<reqwest::Certificate>, OlError> {
let pem = std::fs::read(path).map_err(|e| {
OlError::new(
ERR_PROXY_CONFIG_INVALID,
format!("[proxy] ca_bundle {} cannot be read: {e}", path.display()),
)
.with_suggestion("Point ca_bundle at a readable PEM file, or remove the key.")
})?;
let certs = reqwest::Certificate::from_pem_bundle(&pem).map_err(|e| {
OlError::new(
ERR_PROXY_CONFIG_INVALID,
format!(
"[proxy] ca_bundle {} is not a valid PEM bundle: {e}",
path.display()
),
)
.with_suggestion(
"The file must be PEM, not DER. Convert with: \
openssl x509 -inform der -in cert.der -out cert.pem",
)
})?;
if certs.is_empty() {
return Err(OlError::new(
ERR_PROXY_CONFIG_INVALID,
format!(
"[proxy] ca_bundle {} contains no certificates",
path.display()
),
)
.with_suggestion("Check the file is the CA bundle you meant to point at."));
}
Ok(certs)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg_with_bundle(path: Option<&std::path::Path>) -> EgressConfig {
let mut cfg = EgressConfig::direct();
cfg.ca_bundle = path.map(|p| p.to_path_buf());
cfg
}
#[test]
fn no_bundle_is_native_trust() {
assert_eq!(ca_source(&cfg_with_bundle(None)), CaSource::Native);
assert_eq!(CaSource::Native.as_str(), "native");
}
#[test]
fn interception_is_claimed_only_when_our_own_bundle_verified_it() {
assert_eq!(interception_verdict(CaSource::Custom), Some(true));
assert_eq!(
interception_verdict(CaSource::Native),
None,
"a leaf issuer cannot tell an OS-installed private CA from a public one"
);
}
#[test]
fn a_bundle_makes_the_source_custom() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("ca.pem");
std::fs::write(&path, b"placeholder").expect("write");
assert_eq!(ca_source(&cfg_with_bundle(Some(&path))), CaSource::Custom);
assert_eq!(CaSource::Custom.as_str(), "custom");
}
#[test]
fn a_real_pem_merges() {
let issued =
rcgen::generate_simple_self_signed(vec!["ca.test".to_string()]).expect("generate cert");
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("ca.pem");
std::fs::write(&path, issued.cert.pem()).expect("write");
let cfg = cfg_with_bundle(Some(&path));
let builder = apply(super::super::client_builder(), &cfg).expect("merge");
builder.build().expect("client builds with a merged bundle");
}
#[test]
fn a_garbage_bundle_is_rejected_not_ignored() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("bad.pem");
std::fs::write(&path, b"-----BEGIN CERTIFICATE-----\nnot base64\n").expect("write");
let cfg = cfg_with_bundle(Some(&path));
let err = apply(super::super::client_builder(), &cfg).expect_err("must reject");
assert_eq!(err.code, ERR_PROXY_CONFIG_INVALID);
}
#[test]
fn an_empty_bundle_is_rejected() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("empty.pem");
std::fs::write(&path, b"").expect("write");
let cfg = cfg_with_bundle(Some(&path));
let err = apply(super::super::client_builder(), &cfg).expect_err("must reject");
assert_eq!(err.code, ERR_PROXY_CONFIG_INVALID);
}
}