use crate::config::AcmeConfig;
use crate::error::{ProxyError, ProxyResult};
use crate::tls::CertificateStore;
use rustls_acme::caches::DirCache;
use rustls_acme::AcmeConfig as RustlsAcmeConfig;
use std::path::Path;
use std::sync::Arc;
use tokio_rustls::rustls::ServerConfig;
use tokio_util::sync::CancellationToken;
use tracing::{error, info};
pub struct AcmeManager {
config: AcmeConfig,
cert_store: Arc<CertificateStore>,
cache_dir: std::path::PathBuf,
}
impl AcmeManager {
pub fn new(
config: AcmeConfig,
cert_store: Arc<CertificateStore>,
cache_dir: impl AsRef<Path>,
) -> Self {
Self {
config,
cert_store,
cache_dir: cache_dir.as_ref().to_path_buf(),
}
}
pub fn create_resolver(&self, domains: Vec<String>) -> ProxyResult<Arc<ServerConfig>> {
if !self.config.enabled {
return Err(ProxyError::Tls("ACME is not enabled".into()));
}
let directory = if self.config.staging {
"https://acme-staging-v02.api.letsencrypt.org/directory"
} else {
"https://acme-v02.api.letsencrypt.org/directory"
};
let contacts: Vec<String> = self
.config
.email
.as_ref()
.map(|email| vec![format!("mailto:{}", email)])
.unwrap_or_default();
info!("Setting up ACME for domains: {:?}", domains);
let cache_dir = self.cache_dir.clone();
let cache = DirCache::new(cache_dir);
let state = RustlsAcmeConfig::new(domains)
.contact(contacts)
.cache(cache)
.directory(directory)
.state();
let resolver = state.resolver();
let server_config = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(resolver);
Ok(Arc::new(server_config))
}
pub fn build_server_config_from_pem(
cert_pem: &[u8],
key_pem: &[u8],
) -> ProxyResult<Arc<ServerConfig>> {
use rustls_pemfile::{certs, private_key};
use std::io::BufReader;
let certs: Vec<_> = certs(&mut BufReader::new(cert_pem))
.filter_map(|r| r.ok())
.collect();
if certs.is_empty() {
return Err(ProxyError::Tls("No certificates found in PEM".into()));
}
let key = private_key(&mut BufReader::new(key_pem))
.map_err(|e| ProxyError::Tls(format!("Failed to parse private key: {}", e)))?
.ok_or_else(|| ProxyError::Tls("No private key found in PEM".into()))?;
let config = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| ProxyError::Tls(format!("Failed to build TLS config: {}", e)))?;
Ok(Arc::new(config))
}
pub async fn start_renewal_task(self: Arc<Self>, cancel_token: CancellationToken) {
let check_interval = std::time::Duration::from_secs(86400);
info!("ACME certificate renewal task started");
loop {
tokio::select! {
_ = cancel_token.cancelled() => {
info!("ACME renewal task stopped");
break;
}
_ = tokio::time::sleep(check_interval) => {
if let Err(e) = self.check_renewals().await {
error!("Certificate renewal check failed: {}", e);
}
}
}
}
}
async fn check_renewals(&self) -> ProxyResult<()> {
let domains = self.cert_store.list_domains().await?;
let now = chrono::Utc::now();
let renewal_threshold = chrono::Duration::days(30);
for domain in domains {
if let Some(cert) = self.cert_store.get(&domain).await {
if let Some(expires_at) = cert.expires_at {
if expires_at < now + renewal_threshold {
info!(
"Certificate for {} needs renewal (expires {})",
domain, expires_at
);
}
}
}
}
Ok(())
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn domains(&self) -> &[String] {
&self.config.domains
}
}