use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::watch;
use tracing::{info, warn};
use crate::config::server::TlsSettings;
use crate::control::security::audit::AuditEvent;
use crate::control::state::SharedState;
pub type TlsAcceptorWatch = watch::Receiver<Option<Arc<tokio_rustls::rustls::ServerConfig>>>;
fn load_server_config(tls: &TlsSettings) -> crate::Result<tokio_rustls::rustls::ServerConfig> {
use std::fs::File;
use std::io::BufReader;
let cert_file = File::open(&tls.cert_path)?;
let key_file = File::open(&tls.key_path)?;
let certs: Vec<_> =
rustls_pemfile::certs(&mut BufReader::new(cert_file)).collect::<Result<Vec<_>, _>>()?;
let key = rustls_pemfile::private_key(&mut BufReader::new(key_file))?.ok_or_else(|| {
crate::Error::Config {
detail: format!("no private key in {}", tls.key_path.display()),
}
})?;
tokio_rustls::rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| crate::Error::Config {
detail: format!("TLS config error: {e}"),
})
}
fn file_mtime(path: &PathBuf) -> Option<SystemTime> {
std::fs::metadata(path).ok().and_then(|m| m.modified().ok())
}
pub fn start_tls_reloader(
tls: &TlsSettings,
check_interval: Duration,
state: Arc<SharedState>,
) -> crate::Result<(
TlsAcceptorWatch,
watch::Sender<Option<Arc<tokio_rustls::rustls::ServerConfig>>>,
)> {
let initial_config = load_server_config(tls)?;
let (tx, rx) = watch::channel(Some(Arc::new(initial_config)));
let cert_path = tls.cert_path.clone();
let key_path = tls.key_path.clone();
let tls_settings = tls.clone();
let mut last_cert_mtime = file_mtime(&cert_path);
let mut last_key_mtime = file_mtime(&key_path);
let tx_clone = tx.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(check_interval).await;
let cert_mtime = file_mtime(&cert_path);
let key_mtime = file_mtime(&key_path);
let changed = cert_mtime != last_cert_mtime || key_mtime != last_key_mtime;
if !changed {
continue;
}
info!("TLS cert/key file change detected, reloading");
match load_server_config(&tls_settings) {
Ok(new_config) => {
last_cert_mtime = cert_mtime;
last_key_mtime = key_mtime;
let _ = tx_clone.send(Some(Arc::new(new_config)));
state.audit_record(
AuditEvent::CertRotation,
None,
"tls_reloader",
"TLS certificate reloaded successfully",
);
info!("TLS certificate reloaded successfully");
}
Err(e) => {
state.audit_record(
AuditEvent::CertRotationFailed,
None,
"tls_reloader",
&format!("TLS reload failed: {e}"),
);
warn!(error = %e, "TLS certificate reload failed, keeping current config");
}
}
}
});
Ok((rx, tx))
}
pub fn acceptor_from_watch(rx: &TlsAcceptorWatch) -> Option<tokio_rustls::TlsAcceptor> {
rx.borrow()
.as_ref()
.map(|config| tokio_rustls::TlsAcceptor::from(Arc::clone(config)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_nonexistent_cert_fails() {
let tls = TlsSettings {
cert_path: "/nonexistent/cert.pem".into(),
key_path: "/nonexistent/key.pem".into(),
cert_reload_interval_secs: None,
};
assert!(load_server_config(&tls).is_err());
}
}