//! Server configuration from the environment (SPEC ยง10.4). The variable list is exhaustive. use std::net::SocketAddr; use std::path::PathBuf; use crate::error::AppError; /// Default of `KVITT_DATA_DIR`. pub const DEFAULT_DATA_DIR: &str = "/data"; /// Default of `KVITT_BIND`. pub const DEFAULT_BIND: &str = "0.0.0.0:8080"; /// Default of `KVITT_PUBLIC_URL`. pub const DEFAULT_PUBLIC_URL: &str = "https://icehack.asuscomm.com:8443"; /// Default of `KVITT_FCM_CREDENTIALS`. pub const DEFAULT_FCM_CREDENTIALS: &str = "/secrets/fcm-service-account.json"; /// Default of `KVITT_FCM_PROJECT_ID`. pub const DEFAULT_FCM_PROJECT_ID: &str = "kvitt-1337"; /// Default of `KVITT_TRUSTED_PROXY`. pub const DEFAULT_TRUSTED_PROXY: &str = "caddy"; /// Default of `RUST_LOG`. pub const DEFAULT_LOG_FILTER: &str = "info"; /// File name of the database inside the data directory. pub const DATABASE_FILE: &str = "kvitt.sqlite"; /// The server's configuration. `RUST_LOG` is read by the tracing set-up in `main`, not stored here. /// /// Nothing here touches the network or the file system: a trusted proxy host that does not resolve and a missing /// FCM key file are both normal (no proxy: the peer address is the client; no key: push disabled). #[derive(Debug, Clone, PartialEq, Eq)] pub struct Config { /// `KVITT_DATA_DIR`: holds `kvitt.sqlite`, `pictures/`, `apk/`. pub data_dir: PathBuf, /// `KVITT_BIND`: listen address. pub bind: SocketAddr, /// `KVITT_PUBLIC_URL`: used in invite links and the landing page. Stored without a trailing slash. pub public_url: String, /// `KVITT_FCM_CREDENTIALS`: service-account key; push is disabled if the file is missing. pub fcm_credentials: PathBuf, /// `KVITT_FCM_PROJECT_ID`. pub fcm_project_id: String, /// `KVITT_TRUSTED_PROXY`: host name whose resolved address may set `X-Forwarded-For`. Resolved lazily by the /// rate limiter; an unresolvable name means "no proxy". pub trusted_proxy: String, } impl Config { /// Reads the process environment. pub fn from_env() -> Result { Self::from_lookup(|key| std::env::var(key).ok()) } /// Reads the variables through `lookup` (testable without touching the process environment). Empty values count /// as unset. pub fn from_lookup(lookup: impl Fn(&str) -> Option) -> Result { let get = |key: &str, default: &str| { lookup(key) .filter(|v| !v.trim().is_empty()) .unwrap_or_else(|| default.to_owned()) }; let bind_text = get("KVITT_BIND", DEFAULT_BIND); let bind: SocketAddr = bind_text.parse().map_err(|e| { AppError::new( crate::error::ErrorCode::Internal, format!("KVITT_BIND {bind_text:?} is not ip:port: {e}"), ) })?; Ok(Self { data_dir: PathBuf::from(get("KVITT_DATA_DIR", DEFAULT_DATA_DIR)), bind, public_url: get("KVITT_PUBLIC_URL", DEFAULT_PUBLIC_URL) .trim_end_matches('/') .to_owned(), fcm_credentials: PathBuf::from(get("KVITT_FCM_CREDENTIALS", DEFAULT_FCM_CREDENTIALS)), fcm_project_id: get("KVITT_FCM_PROJECT_ID", DEFAULT_FCM_PROJECT_ID), trusted_proxy: get("KVITT_TRUSTED_PROXY", DEFAULT_TRUSTED_PROXY), }) } /// `/kvitt.sqlite`. pub fn database_path(&self) -> PathBuf { self.data_dir.join(DATABASE_FILE) } /// `/pictures`. pub fn pictures_dir(&self) -> PathBuf { self.data_dir.join("pictures") } /// `/apk`. pub fn apk_dir(&self) -> PathBuf { self.data_dir.join("apk") } } #[cfg(test)] mod tests { use super::*; #[test] fn defaults_when_unset_or_empty() { let config = Config::from_lookup(|k| (k == "KVITT_BIND").then(String::new)).unwrap(); assert_eq!(config.data_dir, PathBuf::from("/data")); assert_eq!(config.bind, "0.0.0.0:8080".parse().unwrap()); assert_eq!(config.public_url, "https://icehack.asuscomm.com:8443"); assert_eq!( config.fcm_credentials, PathBuf::from("/secrets/fcm-service-account.json") ); assert_eq!(config.fcm_project_id, "kvitt-1337"); assert_eq!(config.trusted_proxy, "caddy"); assert_eq!(config.database_path(), PathBuf::from("/data/kvitt.sqlite")); } #[test] fn overrides() { let config = Config::from_lookup(|k| match k { "KVITT_DATA_DIR" => Some("/tmp/kvitt".into()), "KVITT_BIND" => Some("127.0.0.1:9000".into()), "KVITT_PUBLIC_URL" => Some("http://192.168.1.10:8080/".into()), _ => None, }) .unwrap(); assert_eq!( config.database_path(), PathBuf::from("/tmp/kvitt/kvitt.sqlite") ); assert_eq!(config.bind, "127.0.0.1:9000".parse().unwrap()); assert_eq!(config.public_url, "http://192.168.1.10:8080"); } #[test] fn bad_bind_is_an_error() { assert!(Config::from_lookup(|k| (k == "KVITT_BIND").then(|| "nope".into())).is_err()); } }