Switch to using a config file

This commit is contained in:
Brian Picciano 2023-07-09 16:09:00 +02:00
parent 1bc3420930
commit 57b56934a9
13 changed files with 146 additions and 93 deletions

10
.dev-config.yml Normal file
View File

@ -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

View File

@ -1,6 +1 @@
export DOMANI_HTTP_DOMAIN=localhost export DOMANI_CONFIG_PATH=./.dev-config.yml
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

46
Cargo.lock generated
View File

@ -467,6 +467,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"serde_with", "serde_with",
"serde_yaml",
"sha2", "sha2",
"signal-hook", "signal-hook",
"signal-hook-tokio", "signal-hook-tokio",
@ -537,6 +538,12 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "equivalent"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.1" version = "0.3.1"
@ -1362,7 +1369,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap", "indexmap 1.9.3",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -1396,6 +1403,12 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@ -1597,6 +1610,16 @@ dependencies = [
"serde", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -2552,7 +2575,7 @@ dependencies = [
"base64 0.21.0", "base64 0.21.0",
"chrono", "chrono",
"hex", "hex",
"indexmap", "indexmap 1.9.3",
"serde", "serde",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@ -2571,6 +2594,19 @@ dependencies = [
"syn 2.0.15", "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]] [[package]]
name = "sha1_smol" name = "sha1_smol"
version = "1.0.0" version = "1.0.0"
@ -3041,6 +3077,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"

View File

@ -41,3 +41,4 @@ tls-listener = { version = "0.7.0", features = [ "rustls", "hyper-h1" ]}
tokio-rustls = "0.24.0" tokio-rustls = "0.24.0"
log = "0.4.19" log = "0.4.19"
env_logger = "0.10.0" env_logger = "0.10.0"
serde_yaml = "0.9.22"

8
src/config.rs Normal file
View File

@ -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,
}

View File

@ -1,14 +1,20 @@
use std::{net, path}; use std::{net, path, str::FromStr};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(tag = "type")]
pub enum ConfigDNSTargetRecord { pub enum ConfigDNSTargetRecord {
A { addr: net::Ipv4Addr }, A { addr: net::Ipv4Addr },
} }
fn default_resolver_addr() -> net::SocketAddr {
net::SocketAddr::from_str("1.1.1.1:53").unwrap()
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ConfigDNS { pub struct ConfigDNS {
#[serde(default = "default_resolver_addr")]
pub resolver_addr: net::SocketAddr, pub resolver_addr: net::SocketAddr,
pub target_records: Vec<ConfigDNSTargetRecord>, pub target_records: Vec<ConfigDNSTargetRecord>,
} }

View File

@ -2,6 +2,7 @@
#![feature(iterator_try_collect)] #![feature(iterator_try_collect)]
#![feature(iter_collect_into)] #![feature(iter_collect_into)]
pub mod config;
pub mod domain; pub mod domain;
pub mod error; pub mod error;
pub mod origin; pub mod origin;

View File

@ -4,9 +4,7 @@ use clap::Parser;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use signal_hook_tokio::Signals; use signal_hook_tokio::Signals;
use std::net::SocketAddr;
use std::path; use std::path;
use std::str::FromStr;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version)] #[command(version)]
@ -23,101 +21,54 @@ struct Cli {
#[arg(long, default_value_t = false, env = "DOMANI_LOG_TIMESTAMP")] #[arg(long, default_value_t = false, env = "DOMANI_LOG_TIMESTAMP")]
log_timestamp: bool, 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( #[arg(
long, long,
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt", help = "Path to config file",
env = "DOMANI_HTTPS_LISTEN_ADDR", required = true,
requires = "domain_acme_contact_email" env = "DOMANI_CONFIG_PATH"
)] )]
https_listen_addr: Option<SocketAddr>, config_path: path::PathBuf,
#[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<String>,
} }
#[tokio::main] #[tokio::main]
async fn 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() env_logger::Builder::new()
.filter_level(config.log_level) .filter_level(cli.log_level)
.format_timestamp( .format_timestamp(
config cli.log_timestamp
.log_timestamp
.then_some(env_logger::TimestampPrecision::Micros), .then_some(env_logger::TimestampPrecision::Micros),
) )
.init(); .init();
let origin_config = domani::origin::Config { let origin_store = domani::origin::git::FSStore::new(&config.origin)
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)
.expect("git origin store initialization failed"); .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 .await
.expect("domain checker initialization failed"); .expect("domain checker initialization failed");
let domain_config_store = 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"); .expect("domain config store initialization failed");
let domain_acme_manager = if service_http_config.https_addr.is_some() { let domain_acme_manager = if config.service.http.https_addr.is_some() {
let acme_config = domain_config let acme_config = config
.domain
.acme .acme
.expect("acme configuration must be set if https is enabled"); .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_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"); .expect("domain acme store initialization failed");
Some( Some(
@ -143,7 +94,8 @@ async fn main() {
&mut task_stack, &mut task_stack,
domain_manager.clone(), domain_manager.clone(),
domain_manager.clone(), domain_manager.clone(),
service_http_config, config.service,
config.domain.dns.target_records,
); );
let mut signals = let mut signals =

View File

@ -35,7 +35,7 @@ pub struct FSStore {
impl FSStore { impl FSStore {
pub fn new(config: &origin::Config) -> io::Result<Self> { pub fn new(config: &origin::Config) -> io::Result<Self> {
let dir_path = config.store_dir_path.clone(); let dir_path = config.store_dir_path.join("git");
fs::create_dir_all(&dir_path)?; fs::create_dir_all(&dir_path)?;
Ok(Self { Ok(Self {
dir_path, dir_path,

View File

@ -1,2 +1,19 @@
pub mod http; pub mod http;
mod util; 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,
}

View File

@ -17,21 +17,24 @@ pub struct Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>, domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>, cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
handlebars: handlebars::Handlebars<'static>, handlebars: handlebars::Handlebars<'static>,
config: Config, config: service::Config,
dns_target_records: Vec<domain::ConfigDNSTargetRecord>,
} }
pub fn new( pub fn new(
task_stack: &mut util::TaskStack<unexpected::Error>, task_stack: &mut util::TaskStack<unexpected::Error>,
domain_manager: sync::Arc<dyn domain::manager::Manager>, domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>, cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
config: Config, config: service::Config,
dns_target_records: Vec<domain::ConfigDNSTargetRecord>,
) -> sync::Arc<Service> { ) -> sync::Arc<Service> {
let https_enabled = config.https_addr.is_some(); let https_enabled = config.http.https_addr.is_some();
let service = sync::Arc::new(Service { let service = sync::Arc::new(Service {
domain_manager: domain_manager.clone(), domain_manager: domain_manager.clone(),
cert_resolver, cert_resolver,
handlebars: tpl::get(), handlebars: tpl::get(),
config, config,
dns_target_records,
}); });
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller)); 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( self.render_page(
"/domain_init.html", "/domain_init.html",
Response { Response {
domain: args.domain, domain: args.domain,
flat_config: config.into(), flat_config: config.into(),
target_a: self.config.target_a, target_a: target_a,
challenge_token: config_hash, challenge_token: config_hash,
}, },
) )

View File

@ -1,12 +1,22 @@
use crate::domain;
use serde::Deserialize; 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)] #[derive(Deserialize)]
pub struct Config { pub struct Config {
#[serde(default = "default_http_addr")]
pub http_addr: net::SocketAddr, pub http_addr: net::SocketAddr,
pub https_addr: Option<net::SocketAddr>, pub https_addr: Option<net::SocketAddr>,
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,
}
}
} }

View File

@ -10,7 +10,7 @@ pub async fn listen_http(
service: sync::Arc<service::http::Service>, service: sync::Arc<service::http::Service>,
canceller: CancellationToken, canceller: CancellationToken,
) -> Result<(), unexpected::Error> { ) -> 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 primary_domain = service.config.primary_domain.clone();
let make_service = hyper::service::make_service_fn(move |_| { let make_service = hyper::service::make_service_fn(move |_| {
@ -45,7 +45,7 @@ pub async fn listen_https(
canceller: CancellationToken, canceller: CancellationToken,
) -> Result<(), unexpected::Error> { ) -> Result<(), unexpected::Error> {
let cert_resolver = service.cert_resolver.clone(); 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 primary_domain = service.config.primary_domain.clone();
let make_service = hyper::service::make_service_fn(move |_| { let make_service = hyper::service::make_service_fn(move |_| {