use std::path::PathBuf; use anyhow::{Error, bail}; use clap::Parser; /// Runtime configuration, from command line flags or the corresponding /// environment variables. #[derive(Parser, Debug, Clone)] #[command(name = "mugit", version, about)] pub struct Config { /// Directory holding the bare repositories, one .git each #[arg(long, env = "MUGIT_REPOS", default_value = "/srv/git")] pub repos: PathBuf, /// Address for plain HTTP. When a TLS domain is configured, this /// listener only redirects to HTTPS. #[arg(long, env = "MUGIT_LISTEN", default_value = "0.0.0.0:8000")] pub listen: String, /// Domain to obtain a Let's Encrypt certificate for. Setting it enables /// HTTPS on the TLS listener. #[arg(long, env = "MUGIT_TLS_DOMAIN")] pub tls_domain: Option, /// Address for HTTPS #[arg(long, env = "MUGIT_TLS_LISTEN", default_value = "0.0.0.0:443")] pub tls_listen: String, /// Directory where the ACME account key and certificates are kept #[arg(long, env = "MUGIT_ACME_CACHE", default_value = "/var/lib/mugit/acme")] pub acme_cache: PathBuf, /// Contact email registered with the certificate authority #[arg(long, env = "MUGIT_ACME_CONTACT")] pub acme_contact: Option, /// Use the Let's Encrypt staging environment, for testing #[arg(long, env = "MUGIT_ACME_STAGING")] pub acme_staging: bool, /// Site name shown in page headers and titles #[arg(long, env = "MUGIT_SITE_NAME", default_value = "mugit")] pub site_name: String, /// Number of repositories per page of the index #[arg(long, env = "MUGIT_PAGE_SIZE", default_value_t = 50, value_parser = clap::value_parser!(u32).range(1..))] pub page_size: u32, /// Base URL that clone URLs are built from. Defaults to /// https://, or without TLS to wherever each request /// arrived. #[arg(long, env = "MUGIT_PUBLIC_URL")] pub public_url: Option, /// Number of git processes serving clones, fetches and raw files at /// once; further requests wait for a free one, then are refused #[arg(long, env = "MUGIT_GIT_PROCESSES", default_value_t = 8, value_parser = clap::value_parser!(u32).range(1..))] pub git_processes: u32, /// Seconds a clone, fetch or raw download may stall before it is /// dropped, and the longest a request waits for a free git process /// before it is refused #[arg(long, env = "MUGIT_GIT_IDLE_TIMEOUT", default_value_t = 60, value_parser = clap::value_parser!(u64).range(1..))] pub git_idle_timeout: u64, /// Seconds a connection may carry no traffic at all before it is /// closed, bounding how many idle clients can hold file descriptors. /// A request may legitimately be silent while it waits for a git /// process and again while that process stalls, so this must leave /// room for both: it has to exceed twice the git idle timeout. #[arg(long, env = "MUGIT_HTTP_IDLE_TIMEOUT", default_value_t = 300, value_parser = clap::value_parser!(u64).range(1..))] pub http_idle_timeout: u64, } impl Config { /// Checks the invariants that span more than one option, which clap /// cannot express on its own. pub fn validate(&self) -> Result<(), Error> { let floor = 2 * self.git_idle_timeout; if self.http_idle_timeout <= floor { bail!( "--http-idle-timeout is {}s, which does not exceed twice \ --git-idle-timeout ({}s); a clone that waits {}s for a git \ process and then stalls another {}s is still healthy, and \ closing the connection first would cut it off", self.http_idle_timeout, self.git_idle_timeout, self.git_idle_timeout, self.git_idle_timeout, ); } Ok(()) } /// The externally visible base URL of the site, without a trailing /// slash, when the configuration knows it. pub fn public_url(&self) -> Option { let url = match (&self.public_url, &self.tls_domain) { (Some(url), _) => url.clone(), (None, Some(domain)) => format!("https://{domain}"), (None, None) => return None, }; Some(url.trim_end_matches('/').to_owned()) } } #[cfg(test)] mod tests { use assert2::check; use clap::Parser; use super::Config; fn config(args: &[&str]) -> Config { Config::parse_from(std::iter::once("mugit").chain(args.iter().copied())) } #[test] fn the_default_http_idle_timeout_outlasts_a_slow_clone() { check!(config(&[]).validate().is_ok()); } #[test] fn an_http_idle_timeout_that_could_cut_off_a_healthy_clone_is_refused() { // A request may wait a whole git idle timeout for a process and // then stall for another, so 2n is the shortest silence that is // still healthy and the connection has to outlast it. let args = ["--git-idle-timeout", "60", "--http-idle-timeout", "120"]; check!(config(&args).validate().is_err()); let args = ["--git-idle-timeout", "60", "--http-idle-timeout", "121"]; check!(config(&args).validate().is_ok()); } }