#![feature(trait_upcasting)] use clap::Parser; use std::path; #[derive(Parser, Debug)] #[command(version)] #[command(about = "A domani to another dimension")] struct Cli { #[arg( long, help = "OFF, ERROR, WARN, INFO, DEBUG, or TRACE", default_value_t = log::LevelFilter::Info, env = "DOMANI_LOG_LEVEL" )] log_level: log::LevelFilter, #[arg(long, default_value_t = false, env = "DOMANI_LOG_TIMESTAMP")] log_timestamp: bool, #[arg( long, help = "Path to config file", required = true, env = "DOMANI_CONFIG_PATH" )] config_path: path::PathBuf, #[arg(long, help = "Dump the full process configuration to stdout and exit")] dump_config: bool, } #[tokio::main] async fn main() { let cli = Cli::parse(); env_logger::Builder::new() .filter_level(cli.log_level) .format_timestamp( cli.log_timestamp .then_some(env_logger::TimestampPrecision::Micros), ) .init(); let config = { let mut 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()) }) }; // interface_cname is a CNAME record which points to the interface domain of the service. // Since the interface domain _must_ point to the service (otherwise it wouldn't work) it's // reasonable to assume that a CNAME on any domain would suffice to point that domain to // the service. if let Some(ref interface_domain) = config.domain.interface_domain { let interface_cname = domani::service::ConfigDNSRecord::CNAME { name: interface_domain.clone(), }; let dns_records_have_interface_cname = config .service .dns_records .iter() .any(|r| r == &interface_cname); if !dns_records_have_interface_cname { log::info!("Adding 'CNAME {interface_domain}' to service.dns_records"); config.service.dns_records.push(interface_cname); } } config }; if cli.dump_config { let stdout = std::io::stdout().lock(); serde_yaml::to_writer(stdout, &config).expect("writing config to stdout"); return; }; let https_enabled = config.service.http.https_addr.is_some(); let gemini_enabled = config.service.gemini.gemini_addr.is_some(); let external_domains_enabled = !config.domain.external_domains.is_empty(); let origin_store = domani::origin::git::FSStore::new(&config.origin) .expect("git origin store initialization failed"); let domain_checker = domani::domain::checker::DNSChecker::new( domani::token::MemStore::new(), &config.domain.dns, ) .await .expect("domain checker initialization failed"); let domain_store = domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains")) .expect("domain config store initialization failed"); let domain_acme_manager = if https_enabled || external_domains_enabled { let acme_config = config .domain .acme .clone() .expect("acme configuration must be set if https is enabled"); let acme_dir_path = config.domain.store_dir_path.join("acme"); let domain_acme_store = { let dir_path = acme_dir_path.join("certificates"); domani::domain::acme::store::JSONFSStore::new(&dir_path).unwrap_or_else(|e| { panic!( "failed to initialize acme cert store at {}: {e}", dir_path.display() ) }) }; let domain_acme_account_key_store = domani::domain::acme::account_key_store::FSStore::new( &acme_dir_path, ) .unwrap_or_else(|e| { panic!( "failed to initialize account key store at {}: {e}", acme_dir_path.display() ) }); Some( domani::domain::acme::manager::ManagerImpl::new( domain_acme_store, domani::token::MemStore::new(), domain_acme_account_key_store, |config| { Ok(Box::from(domani::domain::acme::store::DirectFSStore::new( &config.tls_key_path, &config.tls_cert_path, ))) }, &acme_config, ) .await .expect("domain acme manager initialization failed"), ) } else { None }; let domain_gemini_store = if gemini_enabled { Some( domani::domain::gemini::FSStore::new(&config.domain.store_dir_path.join("gemini")) .unwrap_or_else(|e| panic!("domain gemini store initialization failed: {e}")), ) } else { None }; let mut task_stack = domani::task_stack::TaskStack::new(); let domain_manager = domani::domain::manager::ManagerImpl::new( &mut task_stack, origin_store, domain_store, domain_checker, domain_acme_manager, domain_gemini_store, config.domain.clone(), ); let _ = domani::service::http::Service::new( &mut task_stack, domain_manager.clone(), domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()), config.service.clone(), ); if gemini_enabled { let _ = domani::service::gemini::Service::new( &mut task_stack, domain_manager.clone(), domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()), config.service.gemini.clone(), ); } tokio::signal::ctrl_c() .await .expect("failed to listen for kill signal"); log::info!("Gracefully shutting down..."); tokio::spawn(async move { tokio::signal::ctrl_c() .await .expect("failed to listen for kill signal"); log::warn!("Forcefully shutting down"); std::process::exit(1); }); task_stack .stop() .await .expect("failed to stop all background tasks"); log::info!("Graceful shutdown complete"); }