//! Gateway configuration from the environment. //! //! The upstream key is mandatory. There is no trust-on-first-use and no unsafe //! mode: the browser's credentials ride on this connection. use std::env; use std::time::Duration; pub const DEFAULT_BIND: &str = "127.0.0.1:8890"; pub const DEFAULT_UPSTREAM: &str = "127.0.0.1:11525"; pub const DEFAULT_GRACE_SECONDS: u64 = 60; pub const DEFAULT_ORIGIN: &str = "https://zagan.2137697.xyz"; #[derive(Debug, Clone)] pub struct Config { /// Loopback address nginx proxies `/ws` to. pub bind: String, /// Exact browser origin allowed on the WebSocket handshake. pub origin: String, /// Game server address, `host:port`. pub upstream: String, /// Lowercase hex SHA-256 of the upstream certificate's SPKI. pub fingerprint: String, /// How long a session survives without a browser socket. pub grace: Duration, /// `Secure` on the session cookie. Off only for local http testing. pub cookie_secure: bool, } impl Config { pub fn from_env() -> Result { let bind = value("ZAGAN_WEB_BIND", DEFAULT_BIND); let origin = value("ZAGAN_WEB_ORIGIN", DEFAULT_ORIGIN); if !origin.starts_with("http://") && !origin.starts_with("https://") { return Err("ZAGAN_WEB_ORIGIN must start with http:// or https://".to_string()); } let origin = origin.trim_end_matches('/').to_string(); let upstream = value("ZAGAN_UPSTREAM", DEFAULT_UPSTREAM); let fingerprint = required("ZAGAN_UPSTREAM_FINGERPRINT")?; let fingerprint = fingerprint.trim().to_ascii_lowercase(); if fingerprint.len() != 64 || !fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit()) { return Err( "ZAGAN_UPSTREAM_FINGERPRINT must be the 64-character SHA-256 of the upstream SPKI" .to_string(), ); } let grace_seconds = match env::var("ZAGAN_GRACE_SECONDS") { Ok(raw) => raw .trim() .parse::() .map_err(|_| "ZAGAN_GRACE_SECONDS must be a whole number of seconds".to_string())?, Err(_) => DEFAULT_GRACE_SECONDS, }; let cookie_secure = match env::var("ZAGAN_WEB_COOKIE_SECURE") { Ok(raw) => matches!( raw.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" ), Err(_) => origin.starts_with("https://"), }; Ok(Self { bind, origin, upstream, fingerprint, grace: Duration::from_secs(grace_seconds), cookie_secure, }) } } fn value(name: &str, default: &str) -> String { env::var(name) .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| default.to_string()) } fn required(name: &str) -> Result { env::var(name) .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| { format!( "{name} is required: the gateway pins the upstream key. \ Use the SPKI fingerprint the desktop client shows." ) }) }