use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use camel_api::CamelError;
use rustls::ServerConfig;
#[derive(Debug, Clone)]
pub struct ServerTlsSource {
pub cert_path: PathBuf,
pub key_path: PathBuf,
pub client_ca_path: Option<PathBuf>,
}
impl ServerTlsSource {
pub fn build_server_config(&self) -> Result<ServerConfig, CamelError> {
let cert_pem = std::fs::read(&self.cert_path).map_err(|e| {
CamelError::EndpointCreationFailed(format!(
"TLS cert file '{}': {e}",
self.cert_path.display()
))
})?;
let key_pem = std::fs::read(&self.key_path).map_err(|e| {
CamelError::EndpointCreationFailed(format!(
"TLS key file '{}': {e}",
self.key_path.display()
))
})?;
let certs: Vec<_> = rustls_pemfile::certs(&mut cert_pem.as_slice())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
CamelError::EndpointCreationFailed(format!("TLS cert parse error: {e}"))
})?;
let key = rustls_pemfile::private_key(&mut key_pem.as_slice())
.map_err(|e| CamelError::EndpointCreationFailed(format!("TLS key parse error: {e}")))?
.ok_or_else(|| {
CamelError::EndpointCreationFailed("TLS: no private key found in key PEM".into())
})?;
let provider = Arc::new(rustls::crypto::ring::default_provider());
let builder = rustls::ServerConfig::builder_with_provider(Arc::clone(&provider))
.with_safe_default_protocol_versions()
.map_err(|e| {
CamelError::EndpointCreationFailed(format!("rustls protocol versions: {e}"))
})?;
let config = match &self.client_ca_path {
Some(ca_path) => {
let ca_pem = std::fs::read(ca_path).map_err(|e| {
CamelError::EndpointCreationFailed(format!(
"TLS client CA file '{}': {e}",
ca_path.display()
))
})?;
let ca_certs: Vec<_> = rustls_pemfile::certs(&mut ca_pem.as_slice())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| {
CamelError::EndpointCreationFailed(format!("invalid client CA PEM: {e}"))
})?;
let mut roots = rustls::RootCertStore::empty();
for cert in ca_certs {
roots.add(cert).map_err(|e| {
CamelError::EndpointCreationFailed(format!("client CA root add: {e}"))
})?;
}
let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
Arc::new(roots),
Arc::clone(&provider),
)
.build()
.map_err(|e| {
CamelError::EndpointCreationFailed(format!("mTLS client cert verifier: {e}"))
})?;
builder
.with_client_cert_verifier(verifier)
.with_single_cert(certs, key)
.map_err(|e| {
CamelError::EndpointCreationFailed(format!("rustls ServerConfig: {e}"))
})?
}
None => builder
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| {
CamelError::EndpointCreationFailed(format!("rustls ServerConfig: {e}"))
})?,
};
Ok(config)
}
}
#[async_trait]
pub trait TlsReloadHandler: Send + Sync {
fn matches(&self, scheme: &str, host: &str, port: u16) -> bool;
async fn reload(&self) -> Result<(), CamelError>;
}
#[derive(Default)]
pub struct TlsReloadRegistry {
handlers: Mutex<Vec<Arc<dyn TlsReloadHandler>>>,
}
impl TlsReloadRegistry {
pub fn global() -> &'static TlsReloadRegistry {
static INSTANCE: std::sync::OnceLock<TlsReloadRegistry> = std::sync::OnceLock::new();
INSTANCE.get_or_init(TlsReloadRegistry::default)
}
pub fn register(&self, handler: Arc<dyn TlsReloadHandler>) {
let mut guard = self
.handlers
.lock()
.expect("TlsReloadRegistry lock poisoned"); guard.push(handler);
}
pub fn find(&self, scheme: &str, host: &str, port: u16) -> Option<Arc<dyn TlsReloadHandler>> {
let guard = self
.handlers
.lock()
.expect("TlsReloadRegistry lock poisoned"); guard
.iter()
.find(|h| h.matches(scheme, host, port))
.cloned()
}
pub fn unregister(&self, scheme: &str, host: &str, port: u16) {
let mut guard = self
.handlers
.lock()
.expect("TlsReloadRegistry lock poisoned"); guard.retain(|h| !h.matches(scheme, host, port));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn write_pem(pem: &str, name: &str) -> PathBuf {
crate::test_support::tls::write_pem_tmp(name, pem)
}
#[test]
fn build_server_config_valid_cert() {
let (ca_pem, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
let _ca = write_pem(&ca_pem, "ca.pem");
let cert = write_pem(&cert_pem, "cert.pem");
let key = write_pem(&key_pem, "key.pem");
let source = ServerTlsSource {
cert_path: cert,
key_path: key,
client_ca_path: None,
};
let config = source.build_server_config().expect("valid cert+key");
assert!(config.alpn_protocols.is_empty(), "ALPN should be empty");
}
#[test]
fn build_server_config_mtls() {
let (ca_pem, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
let ca = write_pem(&ca_pem, "mtls-ca.pem");
let cert = write_pem(&cert_pem, "cert.pem");
let key = write_pem(&key_pem, "key.pem");
let source = ServerTlsSource {
cert_path: cert,
key_path: key,
client_ca_path: Some(ca),
};
let config = source.build_server_config().expect("mTLS should work");
assert!(config.alpn_protocols.is_empty());
}
#[test]
fn build_server_config_mtls_bad_ca_rejected() {
let (_, cert_pem, key_pem) = crate::test_support::tls::gen_server_cert();
let cert = write_pem(&cert_pem, "cert.pem");
let key = write_pem(&key_pem, "key.pem");
let bad_ca = write_pem("not-a-valid-ca-certificate\n", "bad-ca.pem");
let source = ServerTlsSource {
cert_path: cert,
key_path: key,
client_ca_path: Some(bad_ca),
};
let err = source.build_server_config().unwrap_err();
assert!(
matches!(&err, CamelError::EndpointCreationFailed(_)),
"expected EndpointCreationFailed, got {err:?}"
);
}
#[test]
fn build_server_config_missing_file() {
let source = ServerTlsSource {
cert_path: PathBuf::from("/nonexistent/cert.pem"),
key_path: PathBuf::from("/nonexistent/key.pem"),
client_ca_path: None,
};
let err = source.build_server_config().unwrap_err();
assert!(
matches!(&err, CamelError::EndpointCreationFailed(_)),
"expected EndpointCreationFailed, got {err:?}"
);
}
#[test]
fn build_server_config_malformed_pem() {
let cert = write_pem("garbage-content\n", "bad-cert.pem");
let key = write_pem("also-garbage\n", "bad-key.pem");
let source = ServerTlsSource {
cert_path: cert,
key_path: key,
client_ca_path: None,
};
let err = source.build_server_config().unwrap_err();
assert!(
matches!(&err, CamelError::EndpointCreationFailed(_)),
"expected EndpointCreationFailed, got {err:?}"
);
}
}
#[cfg(test)]
mod registry_tests {
use super::*;
struct FakeHandler {
scheme: String,
host: String,
port: u16,
}
#[async_trait::async_trait]
impl TlsReloadHandler for FakeHandler {
fn matches(&self, scheme: &str, host: &str, port: u16) -> bool {
self.scheme == scheme && self.host == host && self.port == port
}
async fn reload(&self) -> Result<(), CamelError> {
Ok(())
}
}
#[test]
fn registry_find_returns_matching_handler() {
let reg = TlsReloadRegistry::default();
reg.register(Arc::new(FakeHandler {
scheme: "grpcs".into(),
host: "0.0.0.0".into(),
port: 9090,
}));
assert!(reg.find("grpcs", "0.0.0.0", 9090).is_some());
assert!(reg.find("https", "0.0.0.0", 9090).is_none());
}
#[test]
fn registry_find_returns_none_when_empty() {
let reg = TlsReloadRegistry::default();
assert!(reg.find("grpcs", "0.0.0.0", 9090).is_none());
}
#[test]
fn registry_unregister_removes_handler() {
let reg = TlsReloadRegistry::default();
reg.register(Arc::new(FakeHandler {
scheme: "grpcs".into(),
host: "0.0.0.0".into(),
port: 9090,
}));
assert!(reg.find("grpcs", "0.0.0.0", 9090).is_some());
reg.unregister("grpcs", "0.0.0.0", 9090);
assert!(reg.find("grpcs", "0.0.0.0", 9090).is_none());
}
}