# `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.