use std::time::Duration;
use camel_component_api::CamelError;
use crate::HttpConfig;
#[cfg(test)]
thread_local! {
static BUILD_CLIENT_CALLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static BUILD_CLIENT_FALLBACKS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
pub(crate) static FORCE_WEBPKI_FALLBACK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
pub(crate) static FORCE_FALLBACK_REBUILD_FAIL: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
}
fn client_builder(
config: &HttpConfig,
resolve_override: Option<(&str, &[std::net::SocketAddr])>,
) -> reqwest::ClientBuilder {
let mut builder = reqwest::Client::builder()
.no_proxy() .connect_timeout(Duration::from_millis(config.connect_timeout_ms))
.pool_max_idle_per_host(config.pool_max_idle_per_host)
.pool_idle_timeout(Duration::from_millis(config.pool_idle_timeout_ms));
builder = builder.redirect(reqwest::redirect::Policy::none());
if let Some((host, addrs)) = resolve_override {
builder = builder.resolve_to_addrs(host, addrs);
}
if let Some(tls) = &config.tls
&& tls.enabled
{
if tls.insecure || !tls.verify_peer {
tracing::warn!("HTTP TLS verification disabled — connections are vulnerable to MitM");
builder = builder.danger_accept_invalid_certs(true);
}
if let Some(ca_path) = &tls.ca_cert_path {
match std::fs::read(ca_path) {
Ok(ca_bytes) => {
let pem_sections = rustls_pemfile::certs(&mut std::io::Cursor::new(&ca_bytes))
.filter(|r| r.is_ok())
.count();
if pem_sections == 0 {
tracing::warn!(
"configured CA certificate contains no parseable PEM CERTIFICATE section — falling back to system roots"
);
}
match reqwest::Certificate::from_pem(&ca_bytes)
.or_else(|_| reqwest::Certificate::from_der(&ca_bytes))
{
Ok(ca_cert) => {
builder = builder.add_root_certificate(ca_cert);
}
Err(e) => {
tracing::warn!(
error = %e,
"configured CA certificate failed to parse — falling back to system roots"
);
}
}
}
Err(e) => {
tracing::warn!(
error = %e,
"configured CA certificate file unreadable — falling back to system roots"
);
}
}
}
if let (Some(cert_path), Some(key_path)) = (&tls.client_cert_path, &tls.client_key_path) {
match (std::fs::read(cert_path), std::fs::read(key_path)) {
(Ok(cert_bytes), Ok(key_bytes)) => {
let mut identity_pem = cert_bytes;
identity_pem.extend_from_slice(&key_bytes);
match reqwest::Identity::from_pem(&identity_pem) {
Ok(identity) => {
builder = builder.identity(identity);
}
Err(e) => {
tracing::warn!(
error = %e,
"configured mTLS identity failed to parse — client certificate NOT used"
);
}
}
}
(cert_r, key_r) => {
tracing::warn!(
cert_ok = cert_r.is_ok(),
key_ok = key_r.is_ok(),
"configured mTLS cert/key file unreadable — client certificate NOT used"
);
}
}
}
}
builder
}
fn fallback_client_config_builder()
-> rustls::ConfigBuilder<rustls::ClientConfig, rustls::WantsVerifier> {
let provider = rustls::crypto::CryptoProvider::get_default()
.cloned()
.unwrap_or_else(|| std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider()));
rustls::ClientConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.expect("stock rustls provider supports the safe default protocol versions") }
fn webpki_root_client_config() -> rustls::ClientConfig {
fallback_client_config_builder()
.with_root_certificates(mozilla_only())
.with_no_client_auth()
}
fn fallback_root_store(
custom_ca: Option<&str>,
strict: bool,
) -> Result<rustls::RootCertStore, CamelError> {
let mut store = rustls::RootCertStore::empty();
let Some(ca_path) = custom_ca else {
return Ok(mozilla_only());
};
let ca_bytes = match std::fs::read(ca_path) {
Ok(bytes) => bytes,
Err(e) => {
if strict {
return Err(CamelError::EndpointCreationFailed(format!(
"tls.strict/webpki-fallback: configured CA certificate '{ca_path}' is unreadable: {e}"
)));
}
tracing::warn!(
error = %e,
"configured CA certificate file unreadable — falling back to bundled Mozilla roots"
);
return Ok(mozilla_only());
}
};
let certs: Vec<_> = rustls_pemfile::certs(&mut std::io::Cursor::new(&ca_bytes))
.filter_map(|r| r.ok())
.collect();
if certs.is_empty() {
if strict {
return Err(CamelError::EndpointCreationFailed(format!(
"tls.strict/webpki-fallback: configured CA certificate '{ca_path}' has no \
parseable PEM CERTIFICATE section"
)));
}
tracing::warn!(
"configured CA certificate contains no parseable PEM CERTIFICATE section — \
falling back to bundled Mozilla roots"
);
return Ok(mozilla_only());
}
let certs_len = certs.len();
let (added, _ignored) = store.add_parsable_certificates(certs);
if added == 0 {
if strict {
return Err(CamelError::EndpointCreationFailed(format!(
"tls.strict/webpki-fallback: configured CA certificate '{ca_path}' was \
rejected by the TLS root store (0 of {certs_len} accepted)"
)));
}
tracing::warn!(
"configured CA certificate was rejected by the TLS root store — \
falling back to bundled Mozilla roots"
);
return Ok(mozilla_only());
}
store
.roots
.extend_from_slice(webpki_roots::TLS_SERVER_ROOTS);
Ok(store)
}
fn mozilla_only() -> rustls::RootCertStore {
rustls::RootCertStore {
roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
}
}
#[derive(Debug)]
struct NoVerifyServerCertVerifier;
impl rustls::client::danger::ServerCertVerifier for NoVerifyServerCertVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA1,
rustls::SignatureScheme::ECDSA_SHA1_Legacy,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ED448,
]
}
}
fn fallback_client_config(
tls: &crate::config::TlsConfig,
) -> Result<Option<rustls::ClientConfig>, CamelError> {
if !tls.enabled {
return Ok(None);
}
let material = tls.ca_cert_path.is_some()
|| tls.client_cert_path.is_some()
|| tls.client_key_path.is_some();
let verification_disabled = tls.insecure || !tls.verify_peer;
if !material && !verification_disabled {
return Ok(None);
}
let builder = fallback_client_config_builder();
let store = fallback_root_store(tls.ca_cert_path.as_deref(), tls.strict)?;
let builder = if verification_disabled {
builder
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(NoVerifyServerCertVerifier))
} else {
builder.with_root_certificates(store)
};
let config = match (&tls.client_cert_path, &tls.client_key_path) {
(Some(cert_path), Some(key_path)) => {
match (std::fs::read(cert_path), std::fs::read(key_path)) {
(Ok(cert_bytes), Ok(key_bytes)) => {
let chain: Vec<_> =
rustls_pemfile::certs(&mut std::io::Cursor::new(&cert_bytes))
.filter_map(|r| r.ok())
.collect();
if chain.is_empty() {
if tls.strict {
return Err(CamelError::EndpointCreationFailed(format!(
"tls.strict/webpki-fallback: configured client certificate \
chain '{cert_path}' has no parseable PEM CERTIFICATE section"
)));
}
tracing::warn!(
"configured client certificate chain has no parseable PEM \
CERTIFICATE section — client certificate NOT used"
);
builder.with_no_client_auth()
} else {
match rustls_pemfile::private_key(&mut std::io::Cursor::new(&key_bytes)) {
Ok(Some(key)) => {
match builder.clone().with_client_auth_cert(chain, key) {
Ok(config) => config,
Err(e) => {
if tls.strict {
return Err(CamelError::EndpointCreationFailed(
format!(
"tls.strict/webpki-fallback: configured mTLS \
identity was rejected by the TLS backend: {e}"
),
));
}
tracing::warn!(
error = %e,
"configured mTLS identity was rejected by the TLS \
backend — client certificate NOT used"
);
builder.with_no_client_auth()
}
}
}
Ok(None) | Err(_) => {
if tls.strict {
return Err(CamelError::EndpointCreationFailed(format!(
"tls.strict/webpki-fallback: configured client key \
'{key_path}' has no parseable private key section"
)));
}
tracing::warn!(
"configured client key has no parseable private key \
section — client certificate NOT used"
);
builder.with_no_client_auth()
}
}
}
}
(cert_r, key_r) => {
if tls.strict {
return Err(CamelError::EndpointCreationFailed(
"tls.strict/webpki-fallback: configured mTLS cert/key files are \
unreadable"
.to_string(),
));
}
tracing::warn!(
cert_ok = cert_r.is_ok(),
key_ok = key_r.is_ok(),
"configured mTLS cert/key file unreadable — client certificate NOT used"
);
builder.with_no_client_auth()
}
}
}
(Some(_), None) | (None, Some(_)) => {
if tls.strict {
return Err(CamelError::EndpointCreationFailed(
"tls.strict/webpki-fallback: mTLS requires BOTH client_cert_path and \
client_key_path"
.to_string(),
));
}
tracing::warn!(
"mTLS requires BOTH client_cert_path and client_key_path — \
client certificate NOT used"
);
builder.with_no_client_auth()
}
(None, None) => builder.with_no_client_auth(),
};
Ok(Some(config))
}
fn emergency_webpki_client() -> reqwest::Client {
reqwest::Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.tls_backend_preconfigured(webpki_root_client_config())
.build()
.expect("preconfigured webpki-rooted client build has no fallible stage") }
pub(crate) enum FallbackTrigger {
Platform(reqwest::Error),
#[cfg(test)]
Forced,
}
impl std::fmt::Display for FallbackTrigger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FallbackTrigger::Platform(e) => std::fmt::Display::fmt(e, f),
#[cfg(test)]
FallbackTrigger::Forced => f.write_str("forced webpki fallback entry (test)"),
}
}
}
fn fallback_rebuild_failed(
config: &HttpConfig,
failure: &dyn std::fmt::Display,
) -> Result<reqwest::Client, CamelError> {
tracing::error!(
error = %failure,
"webpki fallback client build failed — TLS stack broken process-wide"
);
if config.tls.as_ref().is_some_and(|tls| tls.strict) {
Err(CamelError::EndpointCreationFailed(format!(
"tls.strict/webpki-fallback: webpki fallback client rebuild failed — \
refusing material-free emergency client under tls.strict: {failure}"
)))
} else {
Ok(emergency_webpki_client())
}
}
pub(crate) fn webpki_fallback_client(
config: &HttpConfig,
resolve_override: Option<(&str, &[std::net::SocketAddr])>,
trigger: FallbackTrigger,
) -> Result<reqwest::Client, CamelError> {
#[cfg(test)]
BUILD_CLIENT_FALLBACKS.with(|c| c.set(c.get() + 1));
tracing::warn!(
error = %trigger,
"HTTP client build failed on platform TLS roots — the platform CA store \
is missing or unreadable (typical on Android/Termux). Retrying with \
bundled Mozilla root certificates; platform trust settings do not \
apply to this client"
);
let build_with_backend =
|backend: rustls::ClientConfig| -> Result<reqwest::Client, CamelError> {
match client_builder(config, resolve_override)
.tls_backend_preconfigured(backend)
.build()
{
Ok(client) => {
#[cfg(test)]
if FORCE_FALLBACK_REBUILD_FAIL.with(std::cell::Cell::get) {
return fallback_rebuild_failed(
config,
&"forced webpki fallback rebuild failure (test)",
);
}
Ok(client)
}
Err(second_error) => fallback_rebuild_failed(config, &second_error),
}
};
let material_configured = config.tls.as_ref().is_some_and(|tls| {
tls.ca_cert_path.is_some()
|| tls.client_cert_path.is_some()
|| tls.client_key_path.is_some()
});
match config.tls.as_ref().map(fallback_client_config) {
None | Some(Ok(None)) => build_with_backend(webpki_root_client_config()),
Some(Ok(Some(strict_or_parity_config))) => {
if material_configured && config.tls.as_ref().is_some_and(|tls| tls.strict) {
tracing::info!(
"platform CA store unavailable — webpki fallback carries \
configured TLS material"
);
}
build_with_backend(strict_or_parity_config)
}
Some(Err(e)) => Err(e),
}
}
pub(crate) fn build_client(
config: &HttpConfig,
resolve_override: Option<(&str, &[std::net::SocketAddr])>,
) -> Result<reqwest::Client, CamelError> {
#[cfg(test)]
BUILD_CLIENT_CALLS.with(|c| c.set(c.get() + 1));
#[cfg(test)]
if FORCE_WEBPKI_FALLBACK.with(std::cell::Cell::get) {
return webpki_fallback_client(config, resolve_override, FallbackTrigger::Forced);
}
match client_builder(config, resolve_override).build() {
Ok(client) => Ok(client),
Err(first_error) => webpki_fallback_client(
config,
resolve_override,
FallbackTrigger::Platform(first_error),
),
}
}
pub(crate) fn strict_tls_error(config: &HttpConfig) -> Option<CamelError> {
let tls = config.tls.as_ref()?;
if !tls.enabled || !tls.strict {
return None;
}
if let Some(ca_path) = &tls.ca_cert_path {
match std::fs::read(ca_path) {
Ok(ca_bytes) => {
let pem_sections = rustls_pemfile::certs(&mut std::io::Cursor::new(&ca_bytes))
.filter(|r| r.is_ok())
.count();
if pem_sections == 0 {
return Some(CamelError::EndpointCreationFailed(format!(
"tls.strict: configured CA certificate '{ca_path}' has no \
parseable PEM CERTIFICATE section (DER bundles are not \
enforced by the TLS backend — convert to PEM)"
)));
}
}
Err(e) => {
return Some(CamelError::EndpointCreationFailed(format!(
"tls.strict: configured CA certificate '{ca_path}' is unreadable: {e}"
)));
}
}
}
if tls.client_cert_path.is_some() != tls.client_key_path.is_some() {
return Some(CamelError::EndpointCreationFailed(
"tls.strict: mTLS requires BOTH client_cert_path and client_key_path".to_string(),
));
}
if let (Some(cert_path), Some(key_path)) = (&tls.client_cert_path, &tls.client_key_path) {
match (std::fs::read(cert_path), std::fs::read(key_path)) {
(Ok(mut cert_bytes), Ok(key_bytes)) => {
cert_bytes.extend_from_slice(&key_bytes);
if reqwest::Identity::from_pem(&cert_bytes).is_err() {
return Some(CamelError::EndpointCreationFailed(
"tls.strict: configured mTLS identity failed to parse".to_string(),
));
}
}
_ => {
return Some(CamelError::EndpointCreationFailed(
"tls.strict: configured mTLS cert/key files are unreadable".to_string(),
));
}
}
}
None
}
#[cfg(test)]
pub(crate) fn build_client_call_count() -> u64 {
BUILD_CLIENT_CALLS.with(|c| c.get())
}
#[cfg(test)]
pub(crate) fn build_client_fallback_count() -> u64 {
BUILD_CLIENT_FALLBACKS.with(|c| c.get())
}
pub(crate) fn client_or_emergency(config: &HttpConfig) -> (reqwest::Client, Option<CamelError>) {
match build_client(config, None) {
Ok(c) => (c, None),
Err(e) => (emergency_webpki_client(), Some(e)),
}
}
#[cfg(test)]
#[path = "tls_tests.rs"]
mod tests;