use std::path::PathBuf;
use std::sync::Arc;
use rustls_acme::caches::DirCache;
use rustls_acme::{AcmeConfig, is_tls_alpn_challenge};
use tokio_rustls::LazyConfigAcceptor;
use tokio_rustls::rustls::ServerConfig;
pub struct AcmeOptions {
pub domains: Vec<String>,
pub contact: Option<String>,
pub cache_dir: PathBuf,
pub staging: bool,
}
pub struct AcmeAcceptors {
pub challenge: Arc<ServerConfig>,
pub default: Arc<ServerConfig>,
}
pub fn bootstrap(opts: AcmeOptions) -> AcmeAcceptors {
if let Err(e) = std::fs::create_dir_all(&opts.cache_dir) {
tracing::warn!(
"could not create ACME cache directory {}: {e}",
opts.cache_dir.display()
);
}
let mut state = AcmeConfig::new(opts.domains.clone())
.contact(
opts.contact
.iter()
.map(|e| format!("mailto:{e}"))
.collect::<Vec<_>>(),
)
.cache(DirCache::new(opts.cache_dir.clone()))
.directory_lets_encrypt(!opts.staging)
.state();
let challenge = state.challenge_rustls_config();
let default = state.default_rustls_config();
let domains = opts.domains.join(",");
tokio::spawn(async move {
use futures::StreamExt;
loop {
match state.next().await {
Some(Ok(ok)) => {
tracing::info!(target: "s4_acme", domains = %domains, "ACME event: {ok:?}");
crate::metrics::record_acme_renewal(true);
}
Some(Err(err)) => {
tracing::warn!(target: "s4_acme", domains = %domains, "ACME error: {err:?}");
crate::metrics::record_acme_renewal(false);
}
None => {
tracing::warn!(target: "s4_acme", "ACME state stream ended unexpectedly");
break;
}
}
}
});
AcmeAcceptors { challenge, default }
}
pub async fn accept_one<IO>(
sock: IO,
acceptors: &AcmeAcceptors,
) -> Result<Option<tokio_rustls::server::TlsStream<IO>>, Box<dyn std::error::Error + Send + Sync>>
where
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
let start = LazyConfigAcceptor::new(Default::default(), sock).await?;
if is_tls_alpn_challenge(&start.client_hello()) {
let mut tls = start.into_stream(acceptors.challenge.clone()).await?;
use tokio::io::AsyncWriteExt;
let _ = tls.shutdown().await;
Ok(None)
} else {
let tls = start.into_stream(acceptors.default.clone()).await?;
Ok(Some(tls))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn bootstrap_returns_challenge_and_default_configs() {
crate::tls::install_default_crypto_provider();
let dir = tempfile::tempdir().unwrap();
let acceptors = bootstrap(AcmeOptions {
domains: vec!["example.test".into()],
contact: Some("ops@example.test".into()),
cache_dir: dir.path().to_path_buf(),
staging: true,
});
assert!(!Arc::ptr_eq(&acceptors.challenge, &acceptors.default));
}
}