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.
606 lines
19 KiB
606 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;
|
|
|
|
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();
|
|
|
|
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,
|
|
})
|
|
.collect_into(&mut res);
|
|
|
|
if let Some(ref interface_domain) = self.config.interface_domain {
|
|
res.push(ManagedDomain {
|
|
domain: interface_domain.clone(),
|
|
public: false,
|
|
})
|
|
}
|
|
|
|
self.config
|
|
.external_domains
|
|
.keys()
|
|
.map(|domain| ManagedDomain {
|
|
domain: domain.clone(),
|
|
public: false,
|
|
})
|
|
.collect_into(&mut res);
|
|
|
|
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
|
|
})
|
|
}
|
|
}
|
|
|