domani/src/domain/manager.rs
2023-08-01 16:44:16 +02:00

444 lines
14 KiB
Rust

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<store::GetError> 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<store::GetError> for GetFileError {
fn from(e: store::GetError) -> Self {
match e {
store::GetError::NotFound => Self::DomainNotFound,
store::GetError::Unexpected(e) => Self::Unexpected(e),
}
}
}
impl From<origin::GetFileError> 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<store::GetError> 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<origin::SyncError> 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<checker::CheckDomainError> 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<store::SetError> 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<GetSettingsResult, GetSettingsError>;
fn get_file<'store>(
&'store self,
domain: &domain::Name,
path: &str,
) -> Result<util::BoxByteStream, GetFileError>;
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<String, GetAcmeHttp01ChallengeKeyError>;
fn get_domain_checker_challenge_token(
&self,
domain: &domain::Name,
) -> unexpected::Result<Option<String>>;
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
}
pub struct ManagerImpl {
origin_store: Box<dyn origin::Store + Send + Sync>,
domain_store: Box<dyn store::Store + Send + Sync>,
domain_checker: checker::DNSChecker,
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
gemini_store: Option<Box<dyn gemini::Store + Send + Sync>>,
}
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<unexpected::Error>,
origin_store: OriginStore,
domain_store: DomainStore,
domain_checker: checker::DNSChecker,
acme_manager: Option<AcmeManager>,
gemini_store: Option<GeminiStore>,
) -> sync::Arc<Self> {
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<dyn acme::manager::Manager + Send + Sync>),
gemini_store: gemini_store.map(|m| Box::new(m) as Box<dyn gemini::Store + Send + Sync>),
});
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<GetSettingsResult, GetSettingsError> {
Ok(self.domain_store.get(domain)?)
}
fn get_file<'store>(
&'store self,
domain: &domain::Name,
path: &str,
) -> Result<util::BoxByteStream, GetFileError> {
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<String, GetAcmeHttp01ChallengeKeyError> {
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<Option<String>> {
self.domain_checker.get_challenge_token(domain)
}
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> {
self.domain_store.all_domains()
}
}
pub struct HttpsCertResolver(sync::Arc<ManagerImpl>);
impl From<sync::Arc<ManagerImpl>> for HttpsCertResolver {
fn from(mgr: sync::Arc<ManagerImpl>) -> Self {
Self(mgr)
}
}
impl rustls::server::ResolvesServerCert for HttpsCertResolver {
fn resolve(
&self,
client_hello: rustls::server::ClientHello<'_>,
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
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<ManagerImpl>);
impl From<sync::Arc<ManagerImpl>> for GeminiCertResolver {
fn from(mgr: sync::Arc<ManagerImpl>) -> Self {
Self(mgr)
}
}
impl rustls::server::ResolvesServerCert for GeminiCertResolver {
fn resolve(
&self,
client_hello: rustls::server::ClientHello<'_>,
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
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<Option<sync::Arc<rustls::sign::CertifiedKey>>> = (|| {
(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
})
}
}