use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::tls::{TlsConfig, TlsError};
#[derive(Debug)]
pub struct DynamicServerConfig {
inner: ArcSwap<rustls::ServerConfig>,
}
impl DynamicServerConfig {
pub fn from_tls_config(cfg: &TlsConfig) -> Result<Arc<Self>, TlsError> {
let server_config = cfg.build_server_config()?;
Ok(Arc::new(Self {
inner: ArcSwap::new(server_config),
}))
}
#[must_use]
pub fn current(&self) -> Arc<rustls::ServerConfig> {
self.inner.load_full()
}
pub fn reload_from(&self, cfg: &TlsConfig) -> Result<(), TlsError> {
let new = cfg.build_server_config()?;
self.inner.store(new);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert2::assert;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use crate::tls::ClientAuthMode;
fn install_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
fn write_pair(dir: &std::path::Path, cert_pem: &str, key_pem: &str) -> (PathBuf, PathBuf) {
let cp = dir.join("cert.pem");
let kp = dir.join("key.pem");
File::create(&cp)
.unwrap()
.write_all(cert_pem.as_bytes())
.unwrap();
File::create(&kp)
.unwrap()
.write_all(key_pem.as_bytes())
.unwrap();
(cp, kp)
}
#[test]
fn snapshot_is_stable_across_reload() {
install_provider();
let dir = tempfile::tempdir().unwrap();
let (cp, kp) = write_pair(
dir.path(),
include_str!("../tests/fixtures/dev_cert.pem"),
include_str!("../tests/fixtures/dev_key.pem"),
);
let cfg = TlsConfig {
cert_chain_path: cp.clone(),
private_key_path: kp.clone(),
trust_roots_path: None,
client_ca_path: None,
client_auth: ClientAuthMode::Disabled,
};
let dynamic = DynamicServerConfig::from_tls_config(&cfg).unwrap();
let snap_before = dynamic.current();
std::fs::write(&cp, include_str!("../tests/fixtures/dev_cert_alt.pem")).unwrap();
std::fs::write(&kp, include_str!("../tests/fixtures/dev_key_alt.pem")).unwrap();
dynamic.reload_from(&cfg).expect("reload must succeed");
let snap_after = dynamic.current();
assert!(
!Arc::ptr_eq(&snap_before, &snap_after),
"after-reload snapshot must point at a fresh ServerConfig"
);
}
#[test]
fn reload_error_does_not_swap() {
install_provider();
let dir = tempfile::tempdir().unwrap();
let (cp, kp) = write_pair(
dir.path(),
include_str!("../tests/fixtures/dev_cert.pem"),
include_str!("../tests/fixtures/dev_key.pem"),
);
let cfg = TlsConfig {
cert_chain_path: cp,
private_key_path: kp,
trust_roots_path: None,
client_ca_path: None,
client_auth: ClientAuthMode::Disabled,
};
let dynamic = DynamicServerConfig::from_tls_config(&cfg).unwrap();
let snap_before = dynamic.current();
let bogus = TlsConfig {
cert_chain_path: dir.path().join("missing.pem"),
private_key_path: dir.path().join("missing.key"),
trust_roots_path: None,
client_ca_path: None,
client_auth: ClientAuthMode::Disabled,
};
let err = dynamic.reload_from(&bogus).unwrap_err();
let _ = err;
let snap_after = dynamic.current();
assert!(
Arc::ptr_eq(&snap_before, &snap_after),
"failed reload must leave previous ServerConfig in place"
);
}
}