diff options
Diffstat (limited to '')
| -rw-r--r-- | webd/src/config.rs | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/webd/src/config.rs b/webd/src/config.rs new file mode 100644 index 0000000..d3ada8f --- /dev/null +++ b/webd/src/config.rs | |||
| @@ -0,0 +1,95 @@ | |||
| 1 | //! Gateway configuration from the environment. | ||
| 2 | //! | ||
| 3 | //! The upstream key is mandatory. There is no trust-on-first-use and no unsafe | ||
| 4 | //! mode: the browser's credentials ride on this connection. | ||
| 5 | |||
| 6 | use std::env; | ||
| 7 | use std::time::Duration; | ||
| 8 | |||
| 9 | pub const DEFAULT_BIND: &str = "127.0.0.1:8890"; | ||
| 10 | pub const DEFAULT_UPSTREAM: &str = "127.0.0.1:11525"; | ||
| 11 | pub const DEFAULT_GRACE_SECONDS: u64 = 60; | ||
| 12 | pub const DEFAULT_ORIGIN: &str = "https://zagan.2137697.xyz"; | ||
| 13 | |||
| 14 | #[derive(Debug, Clone)] | ||
| 15 | pub struct Config { | ||
| 16 | /// Loopback address nginx proxies `/ws` to. | ||
| 17 | pub bind: String, | ||
| 18 | /// Exact browser origin allowed on the WebSocket handshake. | ||
| 19 | pub origin: String, | ||
| 20 | /// Game server address, `host:port`. | ||
| 21 | pub upstream: String, | ||
| 22 | /// Lowercase hex SHA-256 of the upstream certificate's SPKI. | ||
| 23 | pub fingerprint: String, | ||
| 24 | /// How long a session survives without a browser socket. | ||
| 25 | pub grace: Duration, | ||
| 26 | /// `Secure` on the session cookie. Off only for local http testing. | ||
| 27 | pub cookie_secure: bool, | ||
| 28 | } | ||
| 29 | |||
| 30 | impl Config { | ||
| 31 | pub fn from_env() -> Result<Self, String> { | ||
| 32 | let bind = value("ZAGAN_WEB_BIND", DEFAULT_BIND); | ||
| 33 | let origin = value("ZAGAN_WEB_ORIGIN", DEFAULT_ORIGIN); | ||
| 34 | if !origin.starts_with("http://") && !origin.starts_with("https://") { | ||
| 35 | return Err("ZAGAN_WEB_ORIGIN must start with http:// or https://".to_string()); | ||
| 36 | } | ||
| 37 | let origin = origin.trim_end_matches('/').to_string(); | ||
| 38 | |||
| 39 | let upstream = value("ZAGAN_UPSTREAM", DEFAULT_UPSTREAM); | ||
| 40 | let fingerprint = required("ZAGAN_UPSTREAM_FINGERPRINT")?; | ||
| 41 | let fingerprint = fingerprint.trim().to_ascii_lowercase(); | ||
| 42 | if fingerprint.len() != 64 || !fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit()) { | ||
| 43 | return Err( | ||
| 44 | "ZAGAN_UPSTREAM_FINGERPRINT must be the 64-character SHA-256 of the upstream SPKI" | ||
| 45 | .to_string(), | ||
| 46 | ); | ||
| 47 | } | ||
| 48 | |||
| 49 | let grace_seconds = match env::var("ZAGAN_GRACE_SECONDS") { | ||
| 50 | Ok(raw) => raw | ||
| 51 | .trim() | ||
| 52 | .parse::<u64>() | ||
| 53 | .map_err(|_| "ZAGAN_GRACE_SECONDS must be a whole number of seconds".to_string())?, | ||
| 54 | Err(_) => DEFAULT_GRACE_SECONDS, | ||
| 55 | }; | ||
| 56 | |||
| 57 | let cookie_secure = match env::var("ZAGAN_WEB_COOKIE_SECURE") { | ||
| 58 | Ok(raw) => matches!( | ||
| 59 | raw.trim().to_ascii_lowercase().as_str(), | ||
| 60 | "1" | "true" | "yes" | ||
| 61 | ), | ||
| 62 | Err(_) => origin.starts_with("https://"), | ||
| 63 | }; | ||
| 64 | |||
| 65 | Ok(Self { | ||
| 66 | bind, | ||
| 67 | origin, | ||
| 68 | upstream, | ||
| 69 | fingerprint, | ||
| 70 | grace: Duration::from_secs(grace_seconds), | ||
| 71 | cookie_secure, | ||
| 72 | }) | ||
| 73 | } | ||
| 74 | } | ||
| 75 | |||
| 76 | fn value(name: &str, default: &str) -> String { | ||
| 77 | env::var(name) | ||
| 78 | .ok() | ||
| 79 | .map(|value| value.trim().to_string()) | ||
| 80 | .filter(|value| !value.is_empty()) | ||
| 81 | .unwrap_or_else(|| default.to_string()) | ||
| 82 | } | ||
| 83 | |||
| 84 | fn required(name: &str) -> Result<String, String> { | ||
| 85 | env::var(name) | ||
| 86 | .ok() | ||
| 87 | .map(|value| value.trim().to_string()) | ||
| 88 | .filter(|value| !value.is_empty()) | ||
| 89 | .ok_or_else(|| { | ||
| 90 | format!( | ||
| 91 | "{name} is required: the gateway pins the upstream key. \ | ||
| 92 | Use the SPKI fingerprint the desktop client shows." | ||
| 93 | ) | ||
| 94 | }) | ||
| 95 | } | ||
