Introduce domain::Config

This commit is contained in:
Brian Picciano 2023-07-09 15:08:33 +02:00
parent 80e96c47fb
commit 7c68702ab8
7 changed files with 94 additions and 66 deletions

View File

@ -1,9 +1,11 @@
pub mod acme; pub mod acme;
pub mod checker; pub mod checker;
mod config;
pub mod manager; pub mod manager;
mod name; mod name;
pub mod store; pub mod store;
pub use config::*;
pub use name::*; pub use name::*;
use crate::error::unexpected::{self, Mappable}; use crate::error::unexpected::{self, Mappable};

View File

@ -33,7 +33,7 @@ pub struct ManagerImpl {
impl ManagerImpl { impl ManagerImpl {
pub async fn new<Store: acme::store::Store + Send + Sync + 'static>( pub async fn new<Store: acme::store::Store + Send + Sync + 'static>(
store: Store, store: Store,
contact_email: &str, config: &domain::ConfigACME,
) -> Result<Self, unexpected::Error> { ) -> Result<Self, unexpected::Error> {
let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string()) let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string())
.build() .build()
@ -41,7 +41,7 @@ impl ManagerImpl {
.or_unexpected_while("creating acme2 directory builder")?; .or_unexpected_while("creating acme2 directory builder")?;
let mut contact = String::from("mailto:"); let mut contact = String::from("mailto:");
contact.push_str(contact_email); contact.push_str(config.contact_email.as_str());
let mut builder = acme2::AccountBuilder::new(dir); let mut builder = acme2::AccountBuilder::new(dir);
builder.contact(vec![contact]); builder.contact(vec![contact]);

View File

@ -8,15 +8,6 @@ use trust_dns_client::client::{AsyncClient, ClientHandle};
use trust_dns_client::rr::{DNSClass, Name, RData, RecordType}; use trust_dns_client::rr::{DNSClass, Name, RData, RecordType};
use trust_dns_client::udp; use trust_dns_client::udp;
#[derive(thiserror::Error, Debug)]
pub enum NewDNSCheckerError {
#[error("invalid resolver address")]
InvalidResolverAddress,
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
}
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum CheckDomainError { pub enum CheckDomainError {
#[error("target A not set")] #[error("target A not set")]
@ -37,19 +28,19 @@ pub struct DNSChecker {
} }
impl DNSChecker { impl DNSChecker {
pub async fn new( pub async fn new(config: &domain::ConfigDNS) -> Result<Self, unexpected::Error> {
target_a: net::Ipv4Addr, let target_a = match config
resolver_addr: &str, .target_records
) -> Result<Self, NewDNSCheckerError> { .get(0)
let resolver_addr = resolver_addr .expect("at least one target record expected")
.parse() {
.map_err(|_| NewDNSCheckerError::InvalidResolverAddress)?; domain::ConfigDNSTargetRecord::A { addr } => addr.clone(),
};
let stream = udp::UdpClientStream::<tokio::net::UdpSocket>::new(resolver_addr);
let stream = udp::UdpClientStream::<tokio::net::UdpSocket>::new(config.resolver_addr);
let (client, bg) = AsyncClient::connect(stream).await.or_unexpected()?; let (client, bg) = AsyncClient::connect(stream).await.or_unexpected()?;
tokio::spawn(bg); tokio::spawn(bg);
// TODO there should be a mechanism to clean this up
Ok(Self { Ok(Self {
target_a, target_a,

26
src/domain/config.rs Normal file
View File

@ -0,0 +1,26 @@
use std::{net, path};
use serde::Deserialize;
#[derive(Deserialize)]
pub enum ConfigDNSTargetRecord {
A { addr: net::Ipv4Addr },
}
#[derive(Deserialize)]
pub struct ConfigDNS {
pub resolver_addr: net::SocketAddr,
pub target_records: Vec<ConfigDNSTargetRecord>,
}
#[derive(Deserialize)]
pub struct ConfigACME {
pub contact_email: String,
}
#[derive(Deserialize)]
pub struct Config {
pub store_dir_path: path::PathBuf,
pub dns: ConfigDNS,
pub acme: Option<ConfigACME>,
}

View File

@ -163,7 +163,7 @@ pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
pub struct ManagerImpl { pub struct ManagerImpl {
origin_store: Box<dyn origin::Store + Send + Sync>, origin_store: Box<dyn origin::Store + Send + Sync>,
domain_config_store: Box<dyn store::Store + Send + Sync>, domain_store: Box<dyn store::Store + Send + Sync>,
domain_checker: checker::DNSChecker, domain_checker: checker::DNSChecker,
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>, acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
} }
@ -171,18 +171,18 @@ pub struct ManagerImpl {
impl ManagerImpl { impl ManagerImpl {
pub fn new< pub fn new<
OriginStore: origin::Store + Send + Sync + 'static, OriginStore: origin::Store + Send + Sync + 'static,
DomainConfigStore: store::Store + Send + Sync + 'static, DomainStore: store::Store + Send + Sync + 'static,
AcmeManager: acme::manager::Manager + Send + Sync + 'static, AcmeManager: acme::manager::Manager + Send + Sync + 'static,
>( >(
task_stack: &mut util::TaskStack<unexpected::Error>, task_stack: &mut util::TaskStack<unexpected::Error>,
origin_store: OriginStore, origin_store: OriginStore,
domain_config_store: DomainConfigStore, domain_store: DomainStore,
domain_checker: checker::DNSChecker, domain_checker: checker::DNSChecker,
acme_manager: Option<AcmeManager>, acme_manager: Option<AcmeManager>,
) -> sync::Arc<Self> { ) -> sync::Arc<Self> {
let manager = sync::Arc::new(ManagerImpl { let manager = sync::Arc::new(ManagerImpl {
origin_store: Box::from(origin_store), origin_store: Box::from(origin_store),
domain_config_store: Box::from(domain_config_store), domain_store: Box::from(domain_store),
domain_checker, domain_checker,
acme_manager: acme_manager acme_manager: acme_manager
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>), .map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
@ -225,7 +225,7 @@ impl ManagerImpl {
impl Manager for ManagerImpl { impl Manager for ManagerImpl {
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError> { fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError> {
Ok(self.domain_config_store.get(domain)?) Ok(self.domain_store.get(domain)?)
} }
fn get_file<'store>( fn get_file<'store>(
@ -233,7 +233,7 @@ impl Manager for ManagerImpl {
domain: &domain::Name, domain: &domain::Name,
path: &str, path: &str,
) -> Result<util::BoxByteStream, GetFileError> { ) -> Result<util::BoxByteStream, GetFileError> {
let config = self.domain_config_store.get(domain)?; let config = self.domain_store.get(domain)?;
let f = self.origin_store.get_file(&config.origin_descr, path)?; let f = self.origin_store.get_file(&config.origin_descr, path)?;
Ok(f) Ok(f)
} }
@ -267,7 +267,7 @@ impl Manager for ManagerImpl {
self.origin_store.sync(&config.origin_descr)?; self.origin_store.sync(&config.origin_descr)?;
self.domain_config_store.set(&domain, &config)?; self.domain_store.set(&domain, &config)?;
self.sync_cert(domain).await?; self.sync_cert(domain).await?;
@ -287,7 +287,7 @@ impl Manager for ManagerImpl {
} }
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> { fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> {
self.domain_config_store.all_domains() self.domain_store.all_domains()
} }
} }

View File

@ -1,4 +1,4 @@
use std::path::{Path, PathBuf}; use std::path;
use std::str::FromStr; use std::str::FromStr;
use std::{fs, io}; use std::{fs, io};
@ -28,22 +28,22 @@ pub trait Store {
} }
pub struct FSStore { pub struct FSStore {
dir_path: PathBuf, dir_path: path::PathBuf,
} }
impl FSStore { impl FSStore {
pub fn new(dir_path: &Path) -> io::Result<Self> { pub fn new(dir_path: &path::Path) -> io::Result<Self> {
fs::create_dir_all(dir_path)?; fs::create_dir_all(dir_path)?;
Ok(Self { Ok(Self {
dir_path: dir_path.into(), dir_path: dir_path.into(),
}) })
} }
fn config_dir_path(&self, domain: &domain::Name) -> PathBuf { fn config_dir_path(&self, domain: &domain::Name) -> path::PathBuf {
self.dir_path.join(domain.as_str()) self.dir_path.join(domain.as_str())
} }
fn config_file_path(&self, domain: &domain::Name) -> PathBuf { fn config_file_path(&self, domain: &domain::Name) -> path::PathBuf {
self.config_dir_path(domain).join("config.json") self.config_dir_path(domain).join("config.json")
} }
} }

View File

@ -33,8 +33,7 @@ struct Cli {
long, long,
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt", help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt",
env = "DOMANI_HTTPS_LISTEN_ADDR", env = "DOMANI_HTTPS_LISTEN_ADDR",
requires = "domain_acme_contact_email", requires = "domain_acme_contact_email"
requires = "domain_acme_store_dir_path"
)] )]
https_listen_addr: Option<SocketAddr>, https_listen_addr: Option<SocketAddr>,
@ -53,9 +52,6 @@ struct Cli {
#[arg(long, required = true, env = "DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH")] #[arg(long, required = true, env = "DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH")]
domain_config_store_dir_path: path::PathBuf, domain_config_store_dir_path: path::PathBuf,
#[arg(long, env = "DOMANI_DOMAIN_ACME_STORE_DIR_PATH")]
domain_acme_store_dir_path: Option<path::PathBuf>,
#[arg(long, env = "DOMANI_DOMAIN_ACME_CONTACT_EMAIL")] #[arg(long, env = "DOMANI_DOMAIN_ACME_CONTACT_EMAIL")]
domain_acme_contact_email: Option<String>, domain_acme_contact_email: Option<String>,
} }
@ -77,38 +73,57 @@ async fn main() {
store_dir_path: config.origin_store_git_dir_path, store_dir_path: config.origin_store_git_dir_path,
}; };
let domain_config = domani::domain::Config {
store_dir_path: config.domain_config_store_dir_path,
dns: domani::domain::ConfigDNS {
resolver_addr: std::net::SocketAddr::from_str(
config.domain_checker_resolver_addr.as_str(),
)
.expect("parsing resolver addr"),
target_records: vec![domani::domain::ConfigDNSTargetRecord::A {
addr: config.domain_checker_target_a,
}],
},
acme: config
.https_listen_addr
.and(Some(domani::domain::ConfigACME {
contact_email: config.domain_acme_contact_email.unwrap(),
})),
};
let service_http_config = domani::service::http::Config {
http_addr: config.http_listen_addr,
https_addr: config.https_listen_addr,
primary_domain: config.http_domain,
target_a: config.domain_checker_target_a,
passphrase: config.passphrase,
};
let origin_store = domani::origin::git::FSStore::new(&origin_config) let origin_store = domani::origin::git::FSStore::new(&origin_config)
.expect("git origin store initialization failed"); .expect("git origin store initialization failed");
let domain_checker = domani::domain::checker::DNSChecker::new( let domain_checker = domani::domain::checker::DNSChecker::new(&domain_config.dns)
config.domain_checker_target_a, .await
&config.domain_checker_resolver_addr, .expect("domain checker initialization failed");
)
.await
.expect("domain checker initialization failed");
let domain_config_store = let domain_config_store =
domani::domain::store::FSStore::new(&config.domain_config_store_dir_path) domani::domain::store::FSStore::new(&domain_config.store_dir_path.join("domains"))
.expect("domain config store initialization failed"); .expect("domain config store initialization failed");
let domain_acme_manager = if config.https_listen_addr.is_some() { let domain_acme_manager = if service_http_config.https_addr.is_some() {
let domain_acme_store_dir_path = config.domain_acme_store_dir_path.unwrap(); let acme_config = domain_config
.acme
.expect("acme configuration must be set if https is enabled");
let domain_acme_store = let domain_acme_store =
domani::domain::acme::store::FSStore::new(&domain_acme_store_dir_path) domani::domain::acme::store::FSStore::new(&domain_config.store_dir_path.join("acme"))
.expect("domain acme store initialization failed"); .expect("domain acme store initialization failed");
// if https_listen_addr is set then domain_acme_contact_email is required, see the Cli/clap
// settings.
let domain_acme_contact_email = config.domain_acme_contact_email.unwrap();
Some( Some(
domani::domain::acme::manager::ManagerImpl::new( domani::domain::acme::manager::ManagerImpl::new(domain_acme_store, &acme_config)
domain_acme_store, .await
&domain_acme_contact_email, .expect("domain acme manager initialization failed"),
)
.await
.expect("domain acme manager initialization failed"),
) )
} else { } else {
None None
@ -128,13 +143,7 @@ async fn main() {
&mut task_stack, &mut task_stack,
domain_manager.clone(), domain_manager.clone(),
domain_manager.clone(), domain_manager.clone(),
domani::service::http::Config { service_http_config,
http_addr: config.http_listen_addr,
https_addr: config.https_listen_addr,
primary_domain: config.http_domain,
target_a: config.domain_checker_target_a,
passphrase: config.passphrase,
},
); );
let mut signals = let mut signals =