use crate::domain::{self, acme, checker, gemini, store, tls}; use crate::error::unexpected::{self, Mappable}; use crate::{origin, task_stack, util}; use std::sync; use tokio_util::sync::CancellationToken; pub type GetSettingsResult = domain::store::GetResult; #[derive(thiserror::Error, Debug)] pub enum GetSettingsError { #[error("not found")] NotFound, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for GetSettingsError { fn from(e: store::GetError) -> GetSettingsError { match e { store::GetError::NotFound => GetSettingsError::NotFound, store::GetError::Unexpected(e) => GetSettingsError::Unexpected(e), } } } #[derive(thiserror::Error, Debug)] pub enum GetFileError { #[error("domain not found")] DomainNotFound, #[error("file not found")] FileNotFound, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for GetFileError { fn from(e: store::GetError) -> Self { match e { store::GetError::NotFound => Self::DomainNotFound, store::GetError::Unexpected(e) => Self::Unexpected(e), } } } impl From for GetFileError { fn from(e: origin::GetFileError) -> Self { match e { origin::GetFileError::DescrNotSynced => { Self::Unexpected(unexpected::Error::from("origin descr not synced")) } origin::GetFileError::FileNotFound => Self::FileNotFound, origin::GetFileError::Unexpected(e) => Self::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: store::GetError) -> SyncError { match e { store::GetError::NotFound => SyncError::NotFound, store::GetError::Unexpected(e) => SyncError::Unexpected(e), } } } #[derive(thiserror::Error, Debug)] pub enum SyncWithSettingsError { #[error("cannot call SyncWithSettings on builtin domain")] BuiltinDomain, #[error("invalid url")] InvalidURL, #[error("invalid branch name")] InvalidBranchName, #[error("already in progress")] AlreadyInProgress, #[error("no service dns records set")] ServiceDNSRecordsNotSet, #[error("challenge token not set")] ChallengeTokenNotSet, #[error(transparent)] Unexpected(#[from] unexpected::Error), } impl From for SyncWithSettingsError { fn from(e: origin::SyncError) -> SyncWithSettingsError { match e { origin::SyncError::InvalidURL => SyncWithSettingsError::InvalidURL, origin::SyncError::InvalidBranchName => SyncWithSettingsError::InvalidBranchName, origin::SyncError::AlreadyInProgress => SyncWithSettingsError::AlreadyInProgress, origin::SyncError::Unexpected(e) => SyncWithSettingsError::Unexpected(e), } } } impl From for SyncWithSettingsError { fn from(e: checker::CheckDomainError) -> SyncWithSettingsError { match e { checker::CheckDomainError::ServiceDNSRecordsNotSet => { SyncWithSettingsError::ServiceDNSRecordsNotSet } checker::CheckDomainError::ChallengeTokenNotSet => { SyncWithSettingsError::ChallengeTokenNotSet } checker::CheckDomainError::Unexpected(e) => SyncWithSettingsError::Unexpected(e), } } } impl From for SyncWithSettingsError { fn from(e: store::SetError) -> SyncWithSettingsError { match e { store::SetError::BuiltinDomain => SyncWithSettingsError::BuiltinDomain, store::SetError::Unexpected(e) => SyncWithSettingsError::Unexpected(e), } } } pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError; //#[mockall::automock] pub trait Manager: Sync + Send { fn get_settings(&self, domain: &domain::Name) -> Result; fn get_file<'store>( &'store self, domain: &domain::Name, path: &str, ) -> Result; fn sync_https_cert<'mgr>( &'mgr self, domain: domain::Name, ) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>; fn sync_with_settings<'mgr>( &'mgr self, domain: domain::Name, settings: domain::Settings, ) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>>; fn get_acme_http01_challenge_key( &self, token: &str, ) -> Result; fn get_domain_checker_challenge_token( &self, domain: &domain::Name, ) -> unexpected::Result>; fn all_domains(&self) -> Result, unexpected::Error>; } pub struct ManagerImpl { origin_store: Box, domain_store: Box, domain_checker: checker::DNSChecker, acme_manager: Option>, gemini_store: Option>, } impl ManagerImpl { pub fn new< OriginStore: origin::Store + Send + Sync + 'static, DomainStore: store::Store + Send + Sync + 'static, AcmeManager: acme::manager::Manager + Send + Sync + 'static, GeminiStore: gemini::Store + Send + Sync + 'static, >( task_stack: &mut task_stack::TaskStack, origin_store: OriginStore, domain_store: DomainStore, domain_checker: checker::DNSChecker, acme_manager: Option, gemini_store: Option, ) -> sync::Arc { let manager = sync::Arc::new(ManagerImpl { origin_store: Box::from(origin_store), domain_store: Box::from(domain_store), domain_checker, acme_manager: acme_manager .map(|m| Box::new(m) as Box), gemini_store: gemini_store.map(|m| Box::new(m) as Box), }); task_stack.push_spawn(|canceller| { let manager = manager.clone(); async move { manager.sync_origins(canceller).await; Ok(()) } }); manager } async fn sync_origins_once(&self) { match self.domain_store.all_domains() { Ok(domains) => domains, Err(err) => { log::error!("Error fetching all domains: {err}"); return; } } .into_iter() .for_each(|domain| { log::info!("Syncing domain {}", &domain); let settings = match self.domain_store.get(&domain) { Ok(settings) => settings, Err(err) => { log::error!("Error syncing {domain}: {err}"); return; } }; let descr = &settings.settings.origin_descr; if let Err(err) = self.origin_store.sync(descr) { log::error!("Failed to sync origin for {domain}, origin:{descr:?}: {err}") } }); } async fn sync_origins(&self, canceller: CancellationToken) { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(20 * 60)); loop { tokio::select! { _ = interval.tick() => self.sync_origins_once().await, _ = canceller.cancelled() => return, } } } fn sync_gemini_cert(&self, domain: &domain::Name) -> unexpected::Result<()> { if let Some(ref gemini_store) = self.gemini_store { if let Some(_) = gemini_store.get_certificate(domain).or_unexpected()? { return Ok(()); } // no cert/key stored for the domain, generate and store it let pkey = tls::PrivateKey::new(); let cert = tls::Certificate::new_self_signed(&pkey, domain) .or_unexpected_while("creating self-signed cert")?; gemini_store.set_certificate(domain, pkey, cert)?; } Ok(()) } } impl Manager for ManagerImpl { fn get_settings(&self, domain: &domain::Name) -> Result { Ok(self.domain_store.get(domain)?) } fn get_file<'store>( &'store self, domain: &domain::Name, path: &str, ) -> Result { let settings = self.domain_store.get(domain)?.settings; let path = settings.process_path(path); let f = self .origin_store .get_file(&settings.origin_descr, path.as_ref())?; Ok(f) } fn sync_https_cert<'mgr>( &'mgr self, domain: domain::Name, ) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>> { Box::pin(async move { if let Some(ref acme_manager) = self.acme_manager { acme_manager.sync_domain(domain.clone()).await?; } Ok(()) }) } fn sync_with_settings<'mgr>( &'mgr self, domain: domain::Name, settings: domain::Settings, ) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> { Box::pin(async move { let hash = settings .hash() .or_unexpected_while("calculating config hash")?; self.domain_checker.check_domain(&domain, &hash).await?; self.origin_store.sync(&settings.origin_descr)?; self.domain_store.set(&domain, &settings)?; self.sync_gemini_cert(&domain)?; self.sync_https_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 get_domain_checker_challenge_token( &self, domain: &domain::Name, ) -> unexpected::Result> { self.domain_checker.get_challenge_token(domain) } fn all_domains(&self) -> Result, unexpected::Error> { self.domain_store.all_domains() } } pub struct HttpsCertResolver(sync::Arc); impl From> for HttpsCertResolver { fn from(mgr: sync::Arc) -> Self { Self(mgr) } } impl rustls::server::ResolvesServerCert for HttpsCertResolver { fn resolve( &self, client_hello: rustls::server::ClientHello<'_>, ) -> Option> { let domain = client_hello.server_name()?; match (self.0).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 }) } } pub struct GeminiCertResolver(sync::Arc); impl From> for GeminiCertResolver { fn from(mgr: sync::Arc) -> Self { Self(mgr) } } impl rustls::server::ResolvesServerCert for GeminiCertResolver { fn resolve( &self, client_hello: rustls::server::ClientHello<'_>, ) -> Option> { let domain = client_hello.server_name()?; let domain: domain::Name = match domain.parse() { Ok(domain) => domain, Err(e) => { log::warn!("failed to parse domain name {domain}: {e}"); return None; } }; let res: unexpected::Result>> = (|| { (self.0) .gemini_store .as_ref() .or_unexpected_while("gemini store is not enabled")? .get_certificate(&domain) .or_unexpected_while("fetching pkey/cert")? .map(|(pkey, cert)| { let pkey = rustls::sign::any_supported_type(&pkey.into()).or_unexpected()?; Ok(sync::Arc::new(rustls::sign::CertifiedKey { cert: vec![cert.into()], key: pkey, ocsp: None, sct_list: None, })) }) .transpose() })(); res.unwrap_or_else(|err| { log::error!("Unexpected error getting cert for domain {domain}: {err}"); None }) } }