Domani connects your domain to whatever you want to host on it, all with no account needed
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
domani/src/domain/manager.rs

621 lines
19 KiB

use crate::domain::{self, acme, checker, gemini, store, tls};
use crate::error::unexpected::{self, Mappable};
use crate::{origin, task_stack, util};
use std::sync;
fn collect_into<I, V>(into: &mut Vec<V>, iter: I)
where
I: std::iter::Iterator<Item = V>,
{
for v in iter {
into.push(v)
}
}
pub enum GetSettingsResult {
Stored(domain::Settings),
Builtin(domain::config::ConfigBuiltinDomain),
Proxied(domain::config::ConfigProxiedDomain),
Interface,
External(domain::config::ConfigExternalDomain),
}
#[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),
}
}
}
pub type GetFileError = origin::GetFileError;
#[derive(thiserror::Error, Debug)]
pub enum SyncWithSettingsError {
#[error("domain's settings cannot be modified")]
NotModifiable,
#[error("invalid url")]
InvalidURL,
#[error("unavailable due to server-side issue")]
Unavailable,
#[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::Unavailable => SyncWithSettingsError::Unavailable,
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),
}
}
}
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
pub struct ManagedDomain {
pub domain: domain::Name,
pub public: bool,
}
#[mockall::automock]
pub trait Manager: Sync + Send {
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError>;
fn get_file(
&self,
settings: &domain::Settings,
path: &str,
) -> util::BoxFuture<'_, Result<util::BoxByteStream, GetFileError>>;
fn sync_with_settings(
&self,
domain: domain::Name,
settings: domain::Settings,
) -> util::BoxFuture<'_, 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<ManagedDomain>, 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>>,
config: domain::Config,
}
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>,
config: domain::Config,
) -> 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>),
config,
});
const SYNC_PERIOD_SECS: u64 = 20 * 60; // 20 minutes
task_stack.push_spawn_periodically(
manager.clone(),
SYNC_PERIOD_SECS,
|_canceller, manager| async move {
manager
.sync_origins()
.await
.or_unexpected_while("syncing origins")
},
);
if manager.can_sync_gemini_cert() {
task_stack.push_spawn_periodically(
manager.clone(),
SYNC_PERIOD_SECS,
|_canceller, manager| async move {
manager
.sync_gemini_certs()
.await
.or_unexpected_while("syncing gemini certs")
},
);
}
if manager.can_sync_https_cert() {
task_stack.push_spawn_periodically(
manager.clone(),
SYNC_PERIOD_SECS,
|_canceller, manager| async move {
manager
.sync_https_certs()
.await
.or_unexpected_while("syncing https certs")
},
);
}
if manager.can_sync_external_cert() {
task_stack.push_spawn_periodically(
manager.clone(),
SYNC_PERIOD_SECS,
|_canceller, manager| async move {
manager
.sync_external_certs()
.await
.or_unexpected_while("syncing external certs")
},
);
}
manager
}
async fn sync_domain_origin(
&self,
domain: &domain::Name,
origin_descr: &origin::Descr,
) -> Result<(), origin::SyncError> {
log::info!("Syncing origin {:?} for domain {domain}", origin_descr,);
self.origin_store.sync(origin_descr).await
}
async fn sync_origins(&self) -> unexpected::Result<()> {
let domains = self
.all_domains()
.or_unexpected_while("fetching all domains")?
.into_iter();
for ManagedDomain { domain, .. } in domains {
let settings = match self
.get_settings(&domain)
.map_unexpected_while(|| format!("fetching settings for {domain}"))?
{
GetSettingsResult::Stored(settings) => settings,
GetSettingsResult::Builtin(config) => config.settings,
_ => continue,
};
self.sync_domain_origin(&domain, &settings.origin_descr)
.await
.map_unexpected_while(|| {
format!(
"syncing origin {:?} for domain {domain}",
&settings.origin_descr,
)
})?;
}
Ok(())
}
fn can_sync_gemini_cert(&self) -> bool {
self.gemini_store.is_some()
}
fn sync_domain_gemini_cert(&self, domain: &domain::Name) -> unexpected::Result<()> {
let gemini_store = self.gemini_store.as_ref().unwrap();
log::info!("Syncing gemini certificate for domain {domain}");
if gemini_store
.get_certificate(domain)
.or_unexpected_while("checking if cert is already stored")?
.is_some()
{
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)
}
async fn sync_gemini_certs(&self) -> unexpected::Result<()> {
let domains = self
.all_domains()
.or_unexpected_while("fetching all domains")?
.into_iter();
for ManagedDomain { domain, .. } in domains {
match self
.get_settings(&domain)
.map_unexpected_while(|| format!("fetching settings for {domain}"))?
{
GetSettingsResult::Stored(_) => (),
GetSettingsResult::Builtin(_) => (),
_ => continue,
};
self.sync_domain_gemini_cert(&domain)
.map_unexpected_while(|| format!("syncing domain {domain}"))?;
}
Ok(())
}
fn can_sync_https_cert(&self) -> bool {
self.acme_manager.is_some()
}
async fn sync_domain_https_cert(&self, domain: &domain::Name) -> unexpected::Result<()> {
log::info!("Syncing HTTPS certificate for domain {domain}");
self.acme_manager
.as_ref()
.unwrap()
.sync_domain(domain.clone(), None)
.await
}
async fn sync_https_certs(&self) -> unexpected::Result<()> {
let domains = self
.all_domains()
.or_unexpected_while("fetching all domains")?
.into_iter();
for ManagedDomain { domain, .. } in domains {
match self
.get_settings(&domain)
.map_unexpected_while(|| format!("fetching settings for {domain}"))?
{
GetSettingsResult::Stored(_) => (),
GetSettingsResult::Builtin(_) => (),
GetSettingsResult::Proxied(config) => {
if config.https_disabled {
continue;
}
}
GetSettingsResult::Interface => (),
_ => continue,
};
self.sync_domain_https_cert(&domain)
.await
.map_unexpected_while(|| format!("syncing domain {domain}",))?;
}
Ok(())
}
fn can_sync_external_cert(&self) -> bool {
self.acme_manager.is_some()
}
async fn sync_external_domain(
&self,
domain: &domain::Name,
config: &domain::ConfigExternalDomain,
) -> unexpected::Result<()> {
log::info!("Syncing HTTPS certificate for external domain {domain}");
self.acme_manager
.as_ref()
.unwrap()
.sync_domain(domain.clone(), Some(config.clone()))
.await
}
async fn sync_external_certs(&self) -> unexpected::Result<()> {
let domains = self
.all_domains()
.or_unexpected_while("fetching all domains")?
.into_iter();
for ManagedDomain { domain, .. } in domains {
if let GetSettingsResult::External(config) = self
.get_settings(&domain)
.map_unexpected_while(|| format!("fetching settings for {domain}"))?
{
self.sync_external_domain(&domain, &config)
.await
.map_unexpected_while(|| format!("syncing external {domain}"))?;
}
}
Ok(())
}
}
impl Manager for ManagerImpl {
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError> {
if Some(domain) == self.config.interface_domain.as_ref() {
return Ok(GetSettingsResult::Interface);
}
if let Some(config) = self.config.builtin_domains.get(domain) {
return Ok(GetSettingsResult::Builtin(config.clone()));
}
if let Some(config) = self.config.proxied_domains.get(domain) {
return Ok(GetSettingsResult::Proxied(config.clone()));
}
if let Some(config) = self.config.external_domains.get(domain) {
return Ok(GetSettingsResult::External(config.clone()));
}
Ok(GetSettingsResult::Stored(self.domain_store.get(domain)?))
}
fn get_file(
&self,
settings: &domain::Settings,
path: &str,
) -> util::BoxFuture<'_, Result<util::BoxByteStream, GetFileError>> {
let path = settings.process_path(path);
self.origin_store
.get_file(&settings.origin_descr, path.as_ref())
}
fn sync_with_settings(
&self,
domain: domain::Name,
settings: domain::Settings,
) -> util::BoxFuture<'_, Result<(), SyncWithSettingsError>> {
Box::pin(async move {
let is_interface = Some(&domain) == self.config.interface_domain.as_ref();
let is_builtin = self.config.builtin_domains.contains_key(&domain);
let is_proxied = self.config.proxied_domains.contains_key(&domain);
let is_external = self.config.external_domains.contains_key(&domain);
if is_interface || is_builtin || is_proxied || is_external {
return Err(SyncWithSettingsError::NotModifiable);
}
let hash = settings
.hash()
.or_unexpected_while("calculating config hash")?;
self.domain_checker.check_domain(&domain, &hash).await?;
self.sync_domain_origin(&domain, &settings.origin_descr)
.await?;
if self.can_sync_gemini_cert() {
self.sync_domain_gemini_cert(&domain)
.or_unexpected_while("syncing domain gemini cert")?;
}
if self.can_sync_https_cert() {
self.sync_domain_https_cert(&domain)
.await
.or_unexpected_while("syncing domain https cert")?;
}
self.domain_store.set(&domain, &settings)?;
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<ManagedDomain>, unexpected::Error> {
let mut res: Vec<ManagedDomain> = self
.domain_store
.all_domains()?
.into_iter()
.map(|domain| ManagedDomain {
domain,
public: true,
})
.collect();
collect_into(
&mut res,
self.config
.builtin_domains
.iter()
.map(|(domain, config)| ManagedDomain {
domain: domain.clone(),
public: config.public,
}),
);
collect_into(
&mut res,
self.config
.proxied_domains
.keys()
.map(|domain| ManagedDomain {
domain: domain.clone(),
public: false,
}),
);
if let Some(ref interface_domain) = self.config.interface_domain {
res.push(ManagedDomain {
domain: interface_domain.clone(),
public: false,
})
}
collect_into(
&mut res,
self.config
.external_domains
.keys()
.map(|domain| ManagedDomain {
domain: domain.clone(),
public: false,
}),
);
Ok(res)
}
}
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(err) => Err(err),
Ok(None) => {
log::warn!("No cert found for domain {domain}");
Ok(None)
}
Ok(Some((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
})
}
}