diff options
| author | tv <tv@krebsco.de> | 2026-10-26 20:16:41 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-10-26 20:16:41 +0100 |
| commit | f3eb7ee93ee2544cb5afa6a7ffcf88d580214fb9 (patch) | |
| tree | 541862582923c56a95e8f13d7a04beb8b8d97282 | |
| parent | a3b7b4dd5bbee6bedaf0880732e26464cc1c8dec (diff) | |
detect broken htpasswd files during startup
| -rw-r--r-- | src/api/auth.rs | 33 | ||||
| -rw-r--r-- | src/main.rs | 3 |
2 files changed, 24 insertions, 12 deletions
diff --git a/src/api/auth.rs b/src/api/auth.rs index de4220c..c07db74 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use argon2::{Argon2, password_hash::PasswordVerifier}; use axum::extract::State; use axum::{ @@ -21,18 +22,28 @@ use std::{ use tokio::sync::Semaphore; use tokio::time::sleep; -fn load_users(path: &str) -> std::io::Result<HashMap<String, String>> { - let file = File::open(path)?; +/// The users of an htpasswd file (`user:hash` lines; empty lines and `#` comments are +/// skipped). A line that is neither, or a file without users, is an error: every request +/// would be refused, which is better told at startup. +fn load_users(path: &str) -> anyhow::Result<HashMap<String, String>> { + let file = File::open(path).with_context(|| format!("Opening the htpasswd file {path}"))?; let reader = BufReader::new(file); let mut users = HashMap::new(); - for line in reader.lines() { - let line = line?; - - if let Some((user, hash)) = line.split_once(':') { - users.insert(user.to_owned(), hash.to_owned()); + for (number, line) in reader.lines().enumerate() { + let line = line.with_context(|| format!("Reading the htpasswd file {path}"))?; + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; } + let Some((user, hash)) = line.split_once(':') else { + anyhow::bail!("{path}, line {}: not `user:hash`", number + 1); + }; + users.insert(user.to_owned(), hash.to_owned()); + } + if users.is_empty() { + anyhow::bail!("The htpasswd file {path} has no users"); } Ok(users) @@ -49,15 +60,15 @@ pub struct AuthState { } impl AuthState { - pub fn new(path: &str) -> Self { - let users = load_users(path).unwrap_or_default(); + pub fn new(path: &str) -> anyhow::Result<Self> { + let users = load_users(path)?; - Self { + Ok(Self { idle_timeout: Duration::from_secs(300), cache: Arc::new(DashMap::new()), users: Arc::new(users), verify_slots: Arc::new(Semaphore::new(2)), - } + }) } } diff --git a/src/main.rs b/src/main.rs index 99ee1b9..dee0a32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,8 @@ async fn main() -> Result<()> { // First thing: until a handler is installed, SIGINT and SIGTERM kill the process on // the spot, and startup (spawning Mycelium, bootstrap) can take minutes. let stop = watch_for_shutdown().context("Installing signal handlers")?; + // A broken htpasswd will abort startup. + let auth_state = AuthState::new(&args.htpasswd)?; let mycelium_setup = { let spawn_config = MyceliumSpawnConfig { @@ -223,7 +225,6 @@ async fn main() -> Result<()> { ); swarm::state_file::update(&state, true).await; - let auth_state = AuthState::new(&args.htpasswd); let app = api::router(state.clone(), auth_state, args.debug_api); let listener = tokio::net::TcpListener::bind(args.bind_addr) |
