From 058432130628d00764663ecf819a9adc6793768d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 8 Nov 2026 03:29:35 +0100 Subject: 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. --- webd/README.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 webd/README.md (limited to 'webd/README.md') diff --git a/webd/README.md b/webd/README.md new file mode 100644 index 0000000..ac95fdb --- /dev/null +++ b/webd/README.md @@ -0,0 +1,118 @@ +# `webd` — browser gateway + +The browser cannot open the game's TCP/TLS stream, and the rules must not be +reimplemented in JavaScript. `webd` is a server-side frontend: one blocking +thread per browser session, one `ClientCore`, and one ordinary client TLS +connection to the game server. nginx terminates TLS and proxies `/ws` here. + +It is a workspace member like `gui/`. Its dependencies never enter the TUI, +GUI or server build graph. + +## Build + +```sh +cargo build -p zagan-bet-webd --release +``` + +## Configuration + +| Variable | Default | Meaning | +| --- | --- | --- | +| `ZAGAN_WEB_BIND` | `127.0.0.1:8890` | Loopback address nginx proxies to | +| `ZAGAN_WEB_ORIGIN` | `https://zagan.2137697.xyz` | Exact `Origin` allowed | +| `ZAGAN_UPSTREAM` | `127.0.0.1:11525` | Game server `host:port` | +| `ZAGAN_UPSTREAM_FINGERPRINT` | **required** | SPKI SHA-256, 64 lowercase hex | +| `ZAGAN_GRACE_SECONDS` | `60` | Session survives this long without a socket | +| `ZAGAN_WEB_COOKIE_SECURE` | on for `https://` origin | `Secure` on the cookie | + +The fingerprint is the same one the desktop client shows: + +```sh +openssl x509 -in cert.pem -pubkey -noout \ + | openssl pkey -pubin -outform DER \ + | sha256sum +``` + +There is no trust-on-first-use and no unsafe mode. A mismatch refuses to +connect and logs both fingerprints. Run the gateway on the same host as the +game server (or pin the remote key and accept the network hop). + +## systemd + +Install `zagan-bet-webd` under `/usr/local/bin/`, then: + +```ini +# /etc/systemd/system/zagan-bet-webd.service +[Unit] +Description=zagan-bet browser gateway +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=zagan +Group=zagan +EnvironmentFile=/etc/zagan-bet-webd.env +ExecStart=/usr/local/bin/zagan-bet-webd +Restart=on-failure +RestartSec=2 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true + +[Install] +WantedBy=multi-user.target +``` + +```sh +sudo install -m 0755 target/release/zagan-bet-webd /usr/local/bin/ +sudo install -d -m 0755 /etc +sudo tee /etc/zagan-bet-webd.env >/dev/null <<'ENV' +ZAGAN_UPSTREAM=127.0.0.1:11525 +ZAGAN_UPSTREAM_FINGERPRINT=<64 hex> +ZAGAN_WEB_ORIGIN=https://zagan.2137697.xyz +ZAGAN_WEB_BIND=127.0.0.1:8890 +ENV +sudo systemctl daemon-reload +sudo systemctl enable --now zagan-bet-webd +``` + +nginx needs: + +```nginx +location = /ws { + proxy_pass http://127.0.0.1:8890; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} +``` + +## Behaviour + +- The WebSocket handshake sets an HttpOnly, SameSite=Strict session cookie. + There is no credential storage in the browser or gateway. +- A browser close starts the grace window. Upstream stays connected, crash + ticks are still applied to the core, and a reconnect with the same cookie + gets the current `View`. Expiry closes upstream; the server forfeits a live + hand, exactly like a desktop disconnect. +- A second socket with the same cookie takes over: the old tab receives + `{"type":"replaced"}` and is closed. +- A gateway restart loses live sessions, so their rounds are forfeited. There + is no protocol-level resume. +- The browser sends `{"type":"command","command":...}` and receives + `view`/`notice`/`error`/`replaced`/`hello`/`pong` JSON. It never sees the + game wire or a certificate. + +## Security notes + +- Bind loopback. Do not expose the gateway directly. +- Keep `ZAGAN_WEB_ORIGIN` exact. The cookie is SameSite=Strict, and the + `Origin` check is a second lock on cross-site WebSocket attempts. +- The gateway is play-money only in v1. No payments, KYC or real-money copy. +- `ZAGAN_UPSTREAM_FINGERPRINT` is the browser's credential path: a wrong or + missing pin must fail closed. -- cgit v1.2.3