use crate::domain::{self, acme, checker, config}; use crate::error::unexpected::{self, Mappable}; use crate::origin; use crate::util; use std::{future, pin, sync}; use tokio_util::sync::CancellationToken; #[derive(thiserror::Error, Debug)] pub enum GetConfigError { #[error("not found")] NotFound, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for GetConfigError { fn from(e: config::GetError) -> GetConfigError { match e { config::GetError::NotFound => GetConfigError::NotFound, config::GetError::Unexpected(e) => GetConfigError::Unexpected(e), } } } #[derive(thiserror::Error, Debug)] pub enum GetOriginError { #[error("not found")] NotFound, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for GetOriginError { fn from(e: config::GetError) -> GetOriginError { match e { config::GetError::NotFound => GetOriginError::NotFound, config::GetError::Unexpected(e) => GetOriginError::Unexpected(e), } } } #[derive(thiserror::Error, Debug)] pub enum SyncError { #[error("not found")] NotFound, #[error("already in progress")] AlreadyInProgress, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for SyncError { fn from(e: config::GetError) -> SyncError { match e { config::GetError::NotFound => SyncError::NotFound, config::GetError::Unexpected(e) => SyncError::Unexpected(e), } } } #[derive(thiserror::Error, Debug)] pub enum SyncWithConfigError { #[error("invalid url")] InvalidURL, #[error("invalid branch name")] InvalidBranchName, #[error("already in progress")] AlreadyInProgress, #[error("target A/AAAA not set")] TargetANotSet, #[error("challenge token not set")] ChallengeTokenNotSet, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for SyncWithConfigError { fn from(e: origin::store::SyncError) -> SyncWithConfigError { match e { origin::store::SyncError::InvalidURL => SyncWithConfigError::InvalidURL, origin::store::SyncError::InvalidBranchName => SyncWithConfigError::InvalidBranchName, origin::store::SyncError::AlreadyInProgress => SyncWithConfigError::AlreadyInProgress, origin::store::SyncError::Unexpected(e) => SyncWithConfigError::Unexpected(e), } } } impl From for SyncWithConfigError { fn from(e: checker::CheckDomainError) -> SyncWithConfigError { match e { checker::CheckDomainError::TargetANotSet => SyncWithConfigError::TargetANotSet, checker::CheckDomainError::ChallengeTokenNotSet => { SyncWithConfigError::ChallengeTokenNotSet } checker::CheckDomainError::Unexpected(e) => SyncWithConfigError::Unexpected(e), } } } impl From for SyncWithConfigError { fn from(e: config::SetError) -> SyncWithConfigError { match e { config::SetError::Unexpected(e) => SyncWithConfigError::Unexpected(e), } } } pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError; //#[mockall::automock] pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert { fn get_config(&self, domain: &domain::Name) -> Result; fn get_origin( &self, domain: &domain::Name, ) -> Result, GetOriginError>; fn sync_cert<'mgr>( &'mgr self, domain: domain::Name, ) -> pin::Pin> + Send + 'mgr>>; fn sync_with_config<'mgr>( &'mgr self, domain: domain::Name, config: config::Config, ) -> pin::Pin> + Send + 'mgr>>; fn get_acme_http01_challenge_key( &self, token: &str, ) -> Result; fn all_domains(&self) -> Result, unexpected::Error>; } struct ManagerImpl { origin_store: Box, domain_config_store: Box, domain_checker: checker::DNSChecker, acme_manager: Option>, } async fn sync_origins(origin_store: &dyn origin::store::Store, canceller: CancellationToken) { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(20 * 60)); loop { tokio::select! { _ = interval.tick() => { match origin_store.all_descrs() { Ok(iter) => iter.into_iter(), Err(err) => { log::error!("Error fetching origin descriptors: {err}"); return; } } .for_each(|descr| { if let Err(err) = origin_store.sync(descr.clone(), origin::store::Limits {}) { log::error!("Failed to sync store for {:?}: {err}", descr); return; } }); }, _ = canceller.cancelled() => return, } } } pub fn new< OriginStore: origin::store::Store + 'static, DomainConfigStore: config::Store + 'static, AcmeManager: acme::manager::Manager + 'static, >( task_stack: &mut util::TaskStack, origin_store: OriginStore, domain_config_store: DomainConfigStore, domain_checker: checker::DNSChecker, acme_manager: Option, ) -> sync::Arc { let manager = sync::Arc::new(ManagerImpl { origin_store: Box::from(origin_store), domain_config_store: Box::from(domain_config_store), domain_checker: domain_checker, acme_manager: acme_manager.map(|m| Box::new(m) as Box), }); task_stack.push_spawn(|canceller| { let manager = manager.clone(); async move { Ok(sync_origins(manager.origin_store.as_ref(), canceller).await) } }); manager } impl Manager for ManagerImpl { fn get_config(&self, domain: &domain::Name) -> Result { Ok(self.domain_config_store.get(domain)?) } fn get_origin( &self, domain: &domain::Name, ) -> Result, GetOriginError> { let config = self.domain_config_store.get(domain)?; let origin = self .origin_store .get(config.origin_descr) // if there's a config there should be an origin, any error here is unexpected .or_unexpected()?; Ok(origin) } fn sync_cert<'mgr>( &'mgr self, domain: domain::Name, ) -> pin::Pin> + Send + 'mgr>> { Box::pin(async move { if let Some(ref acme_manager) = self.acme_manager { acme_manager.sync_domain(domain.clone()).await?; } Ok(()) }) } fn sync_with_config<'mgr>( &'mgr self, domain: domain::Name, config: config::Config, ) -> pin::Pin> + Send + 'mgr>> { Box::pin(async move { let config_hash = config .hash() .or_unexpected_while("calculating config hash")?; self.domain_checker .check_domain(&domain, &config_hash) .await?; self.origin_store .sync(config.origin_descr.clone(), origin::store::Limits {})?; self.domain_config_store.set(&domain, &config)?; self.sync_cert(domain).await?; Ok(()) }) } fn get_acme_http01_challenge_key( &self, token: &str, ) -> Result { if let Some(ref acme_manager) = self.acme_manager { return acme_manager.get_http01_challenge_key(token); } Err(GetAcmeHttp01ChallengeKeyError::NotFound) } fn all_domains(&self) -> Result, unexpected::Error> { self.domain_config_store.all_domains() } } impl rustls::server::ResolvesServerCert for ManagerImpl { fn resolve( &self, client_hello: rustls::server::ClientHello<'_>, ) -> Option> { let domain = client_hello.server_name()?; match self.acme_manager.as_ref()?.get_certificate(domain) { Err(acme::manager::GetCertificateError::NotFound) => { log::warn!("No cert found for domain {domain}"); Ok(None) } Err(acme::manager::GetCertificateError::Unexpected(err)) => Err(err), Ok((key, cert)) => { match rustls::sign::any_supported_type(&key.into()).or_unexpected() { Err(err) => Err(err), Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey { cert: cert.into_iter().map(|cert| cert.into()).collect(), key, ocsp: None, sct_list: None, }))), } } } .unwrap_or_else(|err| { log::error!("Unexpected error getting cert for domain {domain}: {err}"); None }) } }