config.rs
raw
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 <name>.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<String>,
/// 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<String>,
/// 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://<tls-domain>, or without TLS to wherever each request
/// arrived.
#[arg(long, env = "MUGIT_PUBLIC_URL")]
pub public_url: Option<String>,
/// 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<String> {
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());
}
}