2023-07-09 12:25:01 +00:00
|
|
|
use crate::domain::{self, acme, checker, store};
|
2023-06-17 16:13:02 +00:00
|
|
|
use crate::error::unexpected::{self, Mappable};
|
2023-06-14 18:22:10 +00:00
|
|
|
use crate::origin;
|
2023-06-21 11:15:42 +00:00
|
|
|
use crate::util;
|
2023-05-17 10:34:24 +00:00
|
|
|
|
2023-07-08 13:19:31 +00:00
|
|
|
use std::sync;
|
2023-06-17 16:13:02 +00:00
|
|
|
use tokio_util::sync::CancellationToken;
|
2023-05-10 11:34:33 +00:00
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum GetConfigError {
|
|
|
|
#[error("not found")]
|
|
|
|
NotFound,
|
|
|
|
|
|
|
|
#[error(transparent)]
|
2023-06-14 18:22:10 +00:00
|
|
|
Unexpected(#[from] unexpected::Error),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-09 12:25:01 +00:00
|
|
|
impl From<store::GetError> for GetConfigError {
|
|
|
|
fn from(e: store::GetError) -> GetConfigError {
|
2023-05-10 11:34:33 +00:00
|
|
|
match e {
|
2023-07-09 12:25:01 +00:00
|
|
|
store::GetError::NotFound => GetConfigError::NotFound,
|
|
|
|
store::GetError::Unexpected(e) => GetConfigError::Unexpected(e),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
2023-07-06 17:19:51 +00:00
|
|
|
pub enum GetFileError {
|
2023-07-05 17:03:51 +00:00
|
|
|
#[error("domain not found")]
|
|
|
|
DomainNotFound,
|
|
|
|
|
|
|
|
#[error("file not found")]
|
|
|
|
FileNotFound,
|
2023-05-10 11:34:33 +00:00
|
|
|
|
|
|
|
#[error(transparent)]
|
2023-06-14 18:22:10 +00:00
|
|
|
Unexpected(#[from] unexpected::Error),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-09 12:25:01 +00:00
|
|
|
impl From<store::GetError> for GetFileError {
|
|
|
|
fn from(e: store::GetError) -> Self {
|
2023-05-10 11:34:33 +00:00
|
|
|
match e {
|
2023-07-09 12:25:01 +00:00
|
|
|
store::GetError::NotFound => Self::DomainNotFound,
|
|
|
|
store::GetError::Unexpected(e) => Self::Unexpected(e),
|
2023-07-05 17:03:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-06 17:19:51 +00:00
|
|
|
impl From<origin::GetFileError> for GetFileError {
|
|
|
|
fn from(e: origin::GetFileError) -> Self {
|
2023-07-05 17:03:51 +00:00
|
|
|
match e {
|
2023-07-06 17:19:51 +00:00
|
|
|
origin::GetFileError::DescrNotSynced => {
|
2023-07-05 17:03:51 +00:00
|
|
|
Self::Unexpected(unexpected::Error::from("origin descr not synced"))
|
|
|
|
}
|
2023-07-06 17:19:51 +00:00
|
|
|
origin::GetFileError::FileNotFound => Self::FileNotFound,
|
|
|
|
origin::GetFileError::Unexpected(e) => Self::Unexpected(e),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum SyncError {
|
|
|
|
#[error("not found")]
|
|
|
|
NotFound,
|
|
|
|
|
2023-05-10 13:12:34 +00:00
|
|
|
#[error("already in progress")]
|
|
|
|
AlreadyInProgress,
|
|
|
|
|
2023-05-10 11:34:33 +00:00
|
|
|
#[error(transparent)]
|
2023-06-14 18:22:10 +00:00
|
|
|
Unexpected(#[from] unexpected::Error),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-09 12:25:01 +00:00
|
|
|
impl From<store::GetError> for SyncError {
|
|
|
|
fn from(e: store::GetError) -> SyncError {
|
2023-05-10 11:34:33 +00:00
|
|
|
match e {
|
2023-07-09 12:25:01 +00:00
|
|
|
store::GetError::NotFound => SyncError::NotFound,
|
|
|
|
store::GetError::Unexpected(e) => SyncError::Unexpected(e),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum SyncWithConfigError {
|
|
|
|
#[error("invalid url")]
|
|
|
|
InvalidURL,
|
|
|
|
|
|
|
|
#[error("invalid branch name")]
|
|
|
|
InvalidBranchName,
|
|
|
|
|
2023-05-10 13:12:34 +00:00
|
|
|
#[error("already in progress")]
|
|
|
|
AlreadyInProgress,
|
|
|
|
|
2023-05-15 20:16:29 +00:00
|
|
|
#[error("target A/AAAA not set")]
|
|
|
|
TargetANotSet,
|
2023-05-10 11:34:33 +00:00
|
|
|
|
|
|
|
#[error("challenge token not set")]
|
|
|
|
ChallengeTokenNotSet,
|
|
|
|
|
|
|
|
#[error(transparent)]
|
2023-06-14 18:22:10 +00:00
|
|
|
Unexpected(#[from] unexpected::Error),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-05 17:03:51 +00:00
|
|
|
impl From<origin::SyncError> for SyncWithConfigError {
|
|
|
|
fn from(e: origin::SyncError) -> SyncWithConfigError {
|
2023-05-10 11:34:33 +00:00
|
|
|
match e {
|
2023-07-05 17:03:51 +00:00
|
|
|
origin::SyncError::InvalidURL => SyncWithConfigError::InvalidURL,
|
|
|
|
origin::SyncError::InvalidBranchName => SyncWithConfigError::InvalidBranchName,
|
|
|
|
origin::SyncError::AlreadyInProgress => SyncWithConfigError::AlreadyInProgress,
|
|
|
|
origin::SyncError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<checker::CheckDomainError> for SyncWithConfigError {
|
|
|
|
fn from(e: checker::CheckDomainError) -> SyncWithConfigError {
|
|
|
|
match e {
|
2023-05-15 20:16:29 +00:00
|
|
|
checker::CheckDomainError::TargetANotSet => SyncWithConfigError::TargetANotSet,
|
2023-05-10 11:34:33 +00:00
|
|
|
checker::CheckDomainError::ChallengeTokenNotSet => {
|
|
|
|
SyncWithConfigError::ChallengeTokenNotSet
|
|
|
|
}
|
|
|
|
checker::CheckDomainError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-09 12:25:01 +00:00
|
|
|
impl From<store::SetError> for SyncWithConfigError {
|
|
|
|
fn from(e: store::SetError) -> SyncWithConfigError {
|
2023-05-10 11:34:33 +00:00
|
|
|
match e {
|
2023-07-09 12:25:01 +00:00
|
|
|
store::SetError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-18 20:02:57 +00:00
|
|
|
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
|
|
|
|
2023-06-21 11:47:04 +00:00
|
|
|
//#[mockall::automock]
|
|
|
|
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
2023-07-09 12:25:01 +00:00
|
|
|
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError>;
|
2023-05-17 12:37:23 +00:00
|
|
|
|
2023-07-06 17:19:51 +00:00
|
|
|
fn get_file<'store>(
|
|
|
|
&'store self,
|
|
|
|
domain: &domain::Name,
|
|
|
|
path: &str,
|
2023-07-08 13:19:31 +00:00
|
|
|
) -> Result<util::BoxByteStream, GetFileError>;
|
2023-07-06 17:19:51 +00:00
|
|
|
|
2023-06-18 12:46:52 +00:00
|
|
|
fn sync_cert<'mgr>(
|
|
|
|
&'mgr self,
|
|
|
|
domain: domain::Name,
|
2023-07-08 13:19:31 +00:00
|
|
|
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>;
|
2023-06-18 12:46:52 +00:00
|
|
|
|
2023-06-17 16:13:02 +00:00
|
|
|
fn sync_with_config<'mgr>(
|
|
|
|
&'mgr self,
|
2023-05-15 15:42:32 +00:00
|
|
|
domain: domain::Name,
|
2023-07-09 12:25:01 +00:00
|
|
|
config: domain::Domain,
|
2023-07-08 13:19:31 +00:00
|
|
|
) -> util::BoxFuture<'mgr, Result<(), SyncWithConfigError>>;
|
2023-05-18 20:02:57 +00:00
|
|
|
|
|
|
|
fn get_acme_http01_challenge_key(
|
|
|
|
&self,
|
|
|
|
token: &str,
|
|
|
|
) -> Result<String, GetAcmeHttp01ChallengeKeyError>;
|
2023-05-19 11:26:27 +00:00
|
|
|
|
2023-06-18 11:53:02 +00:00
|
|
|
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-03 11:39:44 +00:00
|
|
|
pub struct ManagerImpl {
|
2023-07-05 17:03:51 +00:00
|
|
|
origin_store: Box<dyn origin::Store + Send + Sync>,
|
2023-07-09 13:08:33 +00:00
|
|
|
domain_store: Box<dyn store::Store + Send + Sync>,
|
2023-05-16 15:17:47 +00:00
|
|
|
domain_checker: checker::DNSChecker,
|
2023-07-03 11:39:44 +00:00
|
|
|
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
|
2023-05-16 15:17:47 +00:00
|
|
|
}
|
|
|
|
|
2023-07-03 11:39:44 +00:00
|
|
|
impl ManagerImpl {
|
|
|
|
pub fn new<
|
2023-07-05 17:03:51 +00:00
|
|
|
OriginStore: origin::Store + Send + Sync + 'static,
|
2023-07-09 13:08:33 +00:00
|
|
|
DomainStore: store::Store + Send + Sync + 'static,
|
2023-07-03 11:39:44 +00:00
|
|
|
AcmeManager: acme::manager::Manager + Send + Sync + 'static,
|
|
|
|
>(
|
|
|
|
task_stack: &mut util::TaskStack<unexpected::Error>,
|
|
|
|
origin_store: OriginStore,
|
2023-07-09 13:08:33 +00:00
|
|
|
domain_store: DomainStore,
|
2023-07-03 11:39:44 +00:00
|
|
|
domain_checker: checker::DNSChecker,
|
|
|
|
acme_manager: Option<AcmeManager>,
|
2023-07-04 17:42:12 +00:00
|
|
|
) -> sync::Arc<Self> {
|
2023-07-03 11:39:44 +00:00
|
|
|
let manager = sync::Arc::new(ManagerImpl {
|
|
|
|
origin_store: Box::from(origin_store),
|
2023-07-09 13:08:33 +00:00
|
|
|
domain_store: Box::from(domain_store),
|
2023-07-04 17:42:12 +00:00
|
|
|
domain_checker,
|
2023-07-03 11:39:44 +00:00
|
|
|
acme_manager: acme_manager
|
|
|
|
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
|
|
|
|
});
|
|
|
|
|
|
|
|
task_stack.push_spawn(|canceller| {
|
|
|
|
let manager = manager.clone();
|
2023-07-04 17:42:12 +00:00
|
|
|
async move {
|
|
|
|
manager.sync_origins(canceller).await;
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-07-03 11:39:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
manager
|
|
|
|
}
|
|
|
|
|
|
|
|
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() => {
|
|
|
|
match self.origin_store.all_descrs() {
|
|
|
|
Ok(iter) => iter.into_iter(),
|
|
|
|
Err(err) => {
|
|
|
|
log::error!("Error fetching origin descriptors: {err}");
|
|
|
|
return;
|
|
|
|
}
|
2023-06-21 12:05:28 +00:00
|
|
|
}
|
2023-07-03 11:39:44 +00:00
|
|
|
.for_each(|descr| {
|
2023-07-04 17:33:03 +00:00
|
|
|
if let Err(err) = self.origin_store.sync(&descr) {
|
2023-07-04 17:42:12 +00:00
|
|
|
log::error!("Failed to sync store for {:?}: {err}", descr)
|
2023-07-03 11:39:44 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
_ = canceller.cancelled() => return,
|
2023-06-21 11:15:42 +00:00
|
|
|
}
|
2023-06-17 16:13:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-18 11:53:02 +00:00
|
|
|
impl Manager for ManagerImpl {
|
2023-07-09 12:25:01 +00:00
|
|
|
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError> {
|
2023-07-09 13:08:33 +00:00
|
|
|
Ok(self.domain_store.get(domain)?)
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-06 17:19:51 +00:00
|
|
|
fn get_file<'store>(
|
|
|
|
&'store self,
|
|
|
|
domain: &domain::Name,
|
|
|
|
path: &str,
|
2023-07-08 13:19:31 +00:00
|
|
|
) -> Result<util::BoxByteStream, GetFileError> {
|
2023-07-09 13:08:33 +00:00
|
|
|
let config = self.domain_store.get(domain)?;
|
2023-07-06 17:19:51 +00:00
|
|
|
let f = self.origin_store.get_file(&config.origin_descr, path)?;
|
|
|
|
Ok(f)
|
|
|
|
}
|
|
|
|
|
2023-06-18 12:46:52 +00:00
|
|
|
fn sync_cert<'mgr>(
|
|
|
|
&'mgr self,
|
|
|
|
domain: domain::Name,
|
2023-07-08 13:19:31 +00:00
|
|
|
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>> {
|
2023-06-18 12:46:52 +00:00
|
|
|
Box::pin(async move {
|
|
|
|
if let Some(ref acme_manager) = self.acme_manager {
|
|
|
|
acme_manager.sync_domain(domain.clone()).await?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-17 16:13:02 +00:00
|
|
|
fn sync_with_config<'mgr>(
|
|
|
|
&'mgr self,
|
2023-05-15 15:42:32 +00:00
|
|
|
domain: domain::Name,
|
2023-07-09 12:25:01 +00:00
|
|
|
config: domain::Domain,
|
2023-07-08 13:19:31 +00:00
|
|
|
) -> util::BoxFuture<'mgr, Result<(), SyncWithConfigError>> {
|
2023-05-15 15:42:32 +00:00
|
|
|
Box::pin(async move {
|
2023-06-14 18:22:10 +00:00
|
|
|
let config_hash = config
|
|
|
|
.hash()
|
|
|
|
.or_unexpected_while("calculating config hash")?;
|
2023-05-10 11:34:33 +00:00
|
|
|
|
2023-05-15 15:42:32 +00:00
|
|
|
self.domain_checker
|
|
|
|
.check_domain(&domain, &config_hash)
|
|
|
|
.await?;
|
2023-05-10 11:34:33 +00:00
|
|
|
|
2023-07-04 17:33:03 +00:00
|
|
|
self.origin_store.sync(&config.origin_descr)?;
|
2023-05-10 11:34:33 +00:00
|
|
|
|
2023-07-09 13:08:33 +00:00
|
|
|
self.domain_store.set(&domain, &config)?;
|
2023-05-10 11:34:33 +00:00
|
|
|
|
2023-06-18 12:46:52 +00:00
|
|
|
self.sync_cert(domain).await?;
|
2023-05-18 20:02:57 +00:00
|
|
|
|
2023-05-15 15:42:32 +00:00
|
|
|
Ok(())
|
|
|
|
})
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
2023-05-17 12:37:23 +00:00
|
|
|
|
2023-05-18 20:02:57 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-05-19 11:26:27 +00:00
|
|
|
|
2023-06-18 11:53:02 +00:00
|
|
|
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> {
|
2023-07-09 13:08:33 +00:00
|
|
|
self.domain_store.all_domains()
|
2023-05-19 11:26:27 +00:00
|
|
|
}
|
2023-05-10 11:34:33 +00:00
|
|
|
}
|
2023-06-21 11:47:04 +00:00
|
|
|
|
|
|
|
impl rustls::server::ResolvesServerCert for ManagerImpl {
|
|
|
|
fn resolve(
|
|
|
|
&self,
|
|
|
|
client_hello: rustls::server::ClientHello<'_>,
|
|
|
|
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
|
|
|
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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|