use std::path::PathBuf;
use std::sync::Arc;
use futures::StreamExt;
use rustls_acme::{caches::DirCache, AcmeConfig};
use tokio_rustls::rustls::server::ResolvesServerCert;
use tracing::{error, info};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ChallengeStrategy {
#[default]
TlsAlpn01,
Dns01 { provider: Option<String> },
}
#[derive(Debug, Clone)]
pub struct AcmeParams {
pub domains: Vec<String>,
pub email: Option<String>,
pub cache_dir: PathBuf,
pub staging: bool,
pub directory_url: Option<String>,
pub strategy: ChallengeStrategy,
}
#[derive(Debug)]
pub enum AcmeError {
NoDomains,
Dns01NotImplemented,
}
impl std::fmt::Display for AcmeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoDomains => write!(f, "[tls.acme] requires at least one domain"),
Self::Dns01NotImplemented => write!(
f,
"[tls.acme] strategy = \"dns-01\" is not implemented yet (use \"tls-alpn-01\")"
),
}
}
}
impl std::error::Error for AcmeError {}
pub fn start_resolver(params: &AcmeParams) -> Result<Arc<dyn ResolvesServerCert>, AcmeError> {
if params.domains.is_empty() {
return Err(AcmeError::NoDomains);
}
if let ChallengeStrategy::Dns01 { .. } = params.strategy {
return Err(AcmeError::Dns01NotImplemented);
}
let mut config = AcmeConfig::new(params.domains.clone()).cache(DirCache::new(params.cache_dir.clone()));
if let Some(email) = ¶ms.email {
config = config.contact_push(format!("mailto:{email}"));
}
let config = match ¶ms.directory_url {
Some(url) => config.directory(url.clone()),
None => config.directory_lets_encrypt(!params.staging), };
let mut state = config.state();
let resolver = state.resolver();
info!(domains = ?params.domains, "ACME (TLS-ALPN-01) resolver started; ordering/renewal in background");
tokio::spawn(async move {
loop {
match state.next().await {
Some(Ok(ok)) => info!(event = ?ok, "ACME"),
Some(Err(e)) => error!(error = %e, "ACME error"),
None => break,
}
}
});
Ok(resolver)
}