From 57b56934a9b94fb1b9d342c10983dbee96181717 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 9 Jul 2023 16:09:00 +0200 Subject: [PATCH] Switch to using a config file --- .dev-config.yml | 10 ++++ .env.dev | 7 +-- Cargo.lock | 46 +++++++++++++++++- Cargo.toml | 1 + src/config.rs | 8 ++++ src/domain/config.rs | 8 +++- src/lib.rs | 1 + src/main.rs | 96 ++++++++++---------------------------- src/origin/git.rs | 2 +- src/service.rs | 17 +++++++ src/service/http.rs | 19 ++++++-- src/service/http/config.rs | 20 ++++++-- src/service/http/tasks.rs | 4 +- 13 files changed, 146 insertions(+), 93 deletions(-) create mode 100644 .dev-config.yml create mode 100644 src/config.rs diff --git a/.dev-config.yml b/.dev-config.yml new file mode 100644 index 0000000..280ee7b --- /dev/null +++ b/.dev-config.yml @@ -0,0 +1,10 @@ +origin: + store_dir_path: /tmp/domani_dev_env/origin +domain: + store_dir_path: /tmp/domani_dev_env/domain + dns: + target_records: + - type: A + addr: 127.0.0.1 +service: + passphrase: foobar diff --git a/.env.dev b/.env.dev index a524b5e..6bc77b3 100644 --- a/.env.dev +++ b/.env.dev @@ -1,6 +1 @@ -export DOMANI_HTTP_DOMAIN=localhost -export DOMANI_PASSPHRASE=foobar -export DOMANI_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domani_dev_env/origin/git -export DOMANI_DOMAIN_CHECKER_TARGET_A=127.0.0.1 -export DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domani_dev_env/domain/config -export DOMANI_DOMAIN_ACME_STORE_DIR_PATH=/tmp/domani_dev_env/domain/acme +export DOMANI_CONFIG_PATH=./.dev-config.yml diff --git a/Cargo.lock b/Cargo.lock index 8bea9eb..1694236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "serde_with", + "serde_yaml", "sha2", "signal-hook", "signal-hook-tokio", @@ -537,6 +538,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -1362,7 +1369,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1396,6 +1403,12 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "heck" version = "0.4.1" @@ -1597,6 +1610,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "instant" version = "0.1.12" @@ -2552,7 +2575,7 @@ dependencies = [ "base64 0.21.0", "chrono", "hex", - "indexmap", + "indexmap 1.9.3", "serde", "serde_json", "serde_with_macros", @@ -2571,6 +2594,19 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "serde_yaml" +version = "0.9.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "452e67b9c20c37fa79df53201dc03839651086ed9bbe92b3ca585ca9fdaa7d85" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1_smol" version = "1.0.0" @@ -3041,6 +3077,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 0251505..4d286aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,3 +41,4 @@ tls-listener = { version = "0.7.0", features = [ "rustls", "hyper-h1" ]} tokio-rustls = "0.24.0" log = "0.4.19" env_logger = "0.10.0" +serde_yaml = "0.9.22" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a30db94 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct Config { + pub origin: crate::origin::Config, + pub domain: crate::domain::Config, + pub service: crate::service::Config, +} diff --git a/src/domain/config.rs b/src/domain/config.rs index e8da538..a89afa5 100644 --- a/src/domain/config.rs +++ b/src/domain/config.rs @@ -1,14 +1,20 @@ -use std::{net, path}; +use std::{net, path, str::FromStr}; use serde::Deserialize; #[derive(Deserialize)] +#[serde(tag = "type")] pub enum ConfigDNSTargetRecord { A { addr: net::Ipv4Addr }, } +fn default_resolver_addr() -> net::SocketAddr { + net::SocketAddr::from_str("1.1.1.1:53").unwrap() +} + #[derive(Deserialize)] pub struct ConfigDNS { + #[serde(default = "default_resolver_addr")] pub resolver_addr: net::SocketAddr, pub target_records: Vec, } diff --git a/src/lib.rs b/src/lib.rs index f3bb63f..8a40578 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ #![feature(iterator_try_collect)] #![feature(iter_collect_into)] +pub mod config; pub mod domain; pub mod error; pub mod origin; diff --git a/src/main.rs b/src/main.rs index cb2498c..bf2690b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,7 @@ use clap::Parser; use futures::stream::StreamExt; use signal_hook_tokio::Signals; -use std::net::SocketAddr; use std::path; -use std::str::FromStr; #[derive(Parser, Debug)] #[command(version)] @@ -23,101 +21,54 @@ struct Cli { #[arg(long, default_value_t = false, env = "DOMANI_LOG_TIMESTAMP")] log_timestamp: bool, - #[arg(long, required = true, env = "DOMANI_HTTP_DOMAIN")] - http_domain: domani::domain::Name, - - #[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMANI_HTTP_LISTEN_ADDR")] - http_listen_addr: SocketAddr, - #[arg( long, - help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt", - env = "DOMANI_HTTPS_LISTEN_ADDR", - requires = "domain_acme_contact_email" + help = "Path to config file", + required = true, + env = "DOMANI_CONFIG_PATH" )] - https_listen_addr: Option, - - #[arg(long, required = true, env = "DOMANI_PASSPHRASE")] - passphrase: String, - - #[arg(long, required = true, env = "DOMANI_ORIGIN_STORE_GIT_DIR_PATH")] - origin_store_git_dir_path: path::PathBuf, - - #[arg(long, required = true, env = "DOMANI_DOMAIN_CHECKER_TARGET_A")] - domain_checker_target_a: std::net::Ipv4Addr, - - #[arg(long, default_value_t = String::from("1.1.1.1:53"), env = "DOMANI_DOMAIN_CHECKER_RESOLVER_ADDR")] - domain_checker_resolver_addr: String, - - #[arg(long, required = true, env = "DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH")] - domain_config_store_dir_path: path::PathBuf, - - #[arg(long, env = "DOMANI_DOMAIN_ACME_CONTACT_EMAIL")] - domain_acme_contact_email: Option, + config_path: path::PathBuf, } #[tokio::main] async fn main() { - let config = Cli::parse(); + let cli = Cli::parse(); + + let config: domani::config::Config = { + let path = &cli.config_path; + let f = std::fs::File::open(path) + .unwrap_or_else(|e| panic!("failed to open config file at {}: {e}", path.display())); + serde_yaml::from_reader(f) + .unwrap_or_else(|e| panic!("failed to parse config file at {}: {e}", path.display())) + }; env_logger::Builder::new() - .filter_level(config.log_level) + .filter_level(cli.log_level) .format_timestamp( - config - .log_timestamp + cli.log_timestamp .then_some(env_logger::TimestampPrecision::Micros), ) .init(); - let origin_config = domani::origin::Config { - 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(&config.origin) .expect("git origin store initialization failed"); - let domain_checker = domani::domain::checker::DNSChecker::new(&domain_config.dns) + let domain_checker = domani::domain::checker::DNSChecker::new(&config.domain.dns) .await .expect("domain checker initialization failed"); let domain_config_store = - domani::domain::store::FSStore::new(&domain_config.store_dir_path.join("domains")) + domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains")) .expect("domain config store initialization failed"); - let domain_acme_manager = if service_http_config.https_addr.is_some() { - let acme_config = domain_config + let domain_acme_manager = if config.service.http.https_addr.is_some() { + let acme_config = config + .domain .acme .expect("acme configuration must be set if https is enabled"); let domain_acme_store = - domani::domain::acme::store::FSStore::new(&domain_config.store_dir_path.join("acme")) + domani::domain::acme::store::FSStore::new(&config.domain.store_dir_path.join("acme")) .expect("domain acme store initialization failed"); Some( @@ -143,7 +94,8 @@ async fn main() { &mut task_stack, domain_manager.clone(), domain_manager.clone(), - service_http_config, + config.service, + config.domain.dns.target_records, ); let mut signals = diff --git a/src/origin/git.rs b/src/origin/git.rs index 5000dbb..3f4b6d9 100644 --- a/src/origin/git.rs +++ b/src/origin/git.rs @@ -35,7 +35,7 @@ pub struct FSStore { impl FSStore { pub fn new(config: &origin::Config) -> io::Result { - let dir_path = config.store_dir_path.clone(); + let dir_path = config.store_dir_path.join("git"); fs::create_dir_all(&dir_path)?; Ok(Self { dir_path, diff --git a/src/service.rs b/src/service.rs index 9a4a545..dc06589 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,2 +1,19 @@ pub mod http; mod util; + +use crate::domain; +use serde::Deserialize; +use std::str::FromStr; + +fn default_primary_domain() -> domain::Name { + domain::Name::from_str("localhost").unwrap() +} + +#[derive(Deserialize)] +pub struct Config { + #[serde(default = "default_primary_domain")] + pub primary_domain: domain::Name, + pub passphrase: String, + #[serde(default)] + pub http: self::http::Config, +} diff --git a/src/service/http.rs b/src/service/http.rs index c3836e2..0a545eb 100644 --- a/src/service/http.rs +++ b/src/service/http.rs @@ -17,21 +17,24 @@ pub struct Service { domain_manager: sync::Arc, cert_resolver: sync::Arc, handlebars: handlebars::Handlebars<'static>, - config: Config, + config: service::Config, + dns_target_records: Vec, } pub fn new( task_stack: &mut util::TaskStack, domain_manager: sync::Arc, cert_resolver: sync::Arc, - config: Config, + config: service::Config, + dns_target_records: Vec, ) -> sync::Arc { - let https_enabled = config.https_addr.is_some(); + let https_enabled = config.http.https_addr.is_some(); let service = sync::Arc::new(Service { domain_manager: domain_manager.clone(), cert_resolver, handlebars: tpl::get(), config, + dns_target_records, }); task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller)); @@ -248,12 +251,20 @@ impl<'svc> Service { } }; + let target_a = match self + .dns_target_records + .get(0) + .expect("at least one target record expected") + { + domain::ConfigDNSTargetRecord::A { addr } => addr.clone(), + }; + self.render_page( "/domain_init.html", Response { domain: args.domain, flat_config: config.into(), - target_a: self.config.target_a, + target_a: target_a, challenge_token: config_hash, }, ) diff --git a/src/service/http/config.rs b/src/service/http/config.rs index cc480b1..ba571db 100644 --- a/src/service/http/config.rs +++ b/src/service/http/config.rs @@ -1,12 +1,22 @@ -use crate::domain; use serde::Deserialize; -use std::net; +use std::{net, str::FromStr}; + +fn default_http_addr() -> net::SocketAddr { + net::SocketAddr::from_str("[::]:3030").unwrap() +} #[derive(Deserialize)] pub struct Config { + #[serde(default = "default_http_addr")] pub http_addr: net::SocketAddr, pub https_addr: Option, - pub primary_domain: domain::Name, - pub target_a: net::Ipv4Addr, - pub passphrase: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + http_addr: default_http_addr(), + https_addr: None, + } + } } diff --git a/src/service/http/tasks.rs b/src/service/http/tasks.rs index 7bef7e7..8598e43 100644 --- a/src/service/http/tasks.rs +++ b/src/service/http/tasks.rs @@ -10,7 +10,7 @@ pub async fn listen_http( service: sync::Arc, canceller: CancellationToken, ) -> Result<(), unexpected::Error> { - let addr = service.config.http_addr.clone(); + let addr = service.config.http.http_addr.clone(); let primary_domain = service.config.primary_domain.clone(); let make_service = hyper::service::make_service_fn(move |_| { @@ -45,7 +45,7 @@ pub async fn listen_https( canceller: CancellationToken, ) -> Result<(), unexpected::Error> { let cert_resolver = service.cert_resolver.clone(); - let addr = service.config.https_addr.unwrap().clone(); + let addr = service.config.http.https_addr.unwrap().clone(); let primary_domain = service.config.primary_domain.clone(); let make_service = hyper::service::make_service_fn(move |_| {