summaryrefslogtreecommitdiffstats
path: root/webd/src/config.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
commit058432130628d00764663ecf819a9adc6793768d (patch)
treed3f49f111fbfaa48647cca2600adcf9017af2d30 /webd/src/config.rs
parentweb: serve latest.php through PHP and refuse stray PHP files (diff)
downloadzagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz
zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip
v5.0.0: protocol v5 and the browser frontend
Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO.
Diffstat (limited to 'webd/src/config.rs')
-rw-r--r--webd/src/config.rs95
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
6use std::env;
7use std::time::Duration;
8
9pub const DEFAULT_BIND: &str = "127.0.0.1:8890";
10pub const DEFAULT_UPSTREAM: &str = "127.0.0.1:11525";
11pub const DEFAULT_GRACE_SECONDS: u64 = 60;
12pub const DEFAULT_ORIGIN: &str = "https://zagan.2137697.xyz";
13
14#[derive(Debug, Clone)]
15pub 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
30impl 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
76fn 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
84fn 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}