diff options
Diffstat (limited to '')
| -rw-r--r-- | webd/README.md | 118 |
1 files changed, 118 insertions, 0 deletions
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 @@ | |||
| 1 | # `webd` — browser gateway | ||
| 2 | |||
| 3 | The browser cannot open the game's TCP/TLS stream, and the rules must not be | ||
| 4 | reimplemented in JavaScript. `webd` is a server-side frontend: one blocking | ||
| 5 | thread per browser session, one `ClientCore`, and one ordinary client TLS | ||
| 6 | connection to the game server. nginx terminates TLS and proxies `/ws` here. | ||
| 7 | |||
| 8 | It is a workspace member like `gui/`. Its dependencies never enter the TUI, | ||
| 9 | GUI or server build graph. | ||
| 10 | |||
| 11 | ## Build | ||
| 12 | |||
| 13 | ```sh | ||
| 14 | cargo build -p zagan-bet-webd --release | ||
| 15 | ``` | ||
| 16 | |||
| 17 | ## Configuration | ||
| 18 | |||
| 19 | | Variable | Default | Meaning | | ||
| 20 | | --- | --- | --- | | ||
| 21 | | `ZAGAN_WEB_BIND` | `127.0.0.1:8890` | Loopback address nginx proxies to | | ||
| 22 | | `ZAGAN_WEB_ORIGIN` | `https://zagan.2137697.xyz` | Exact `Origin` allowed | | ||
| 23 | | `ZAGAN_UPSTREAM` | `127.0.0.1:11525` | Game server `host:port` | | ||
| 24 | | `ZAGAN_UPSTREAM_FINGERPRINT` | **required** | SPKI SHA-256, 64 lowercase hex | | ||
| 25 | | `ZAGAN_GRACE_SECONDS` | `60` | Session survives this long without a socket | | ||
| 26 | | `ZAGAN_WEB_COOKIE_SECURE` | on for `https://` origin | `Secure` on the cookie | | ||
| 27 | |||
| 28 | The fingerprint is the same one the desktop client shows: | ||
| 29 | |||
| 30 | ```sh | ||
| 31 | openssl x509 -in cert.pem -pubkey -noout \ | ||
| 32 | | openssl pkey -pubin -outform DER \ | ||
| 33 | | sha256sum | ||
| 34 | ``` | ||
| 35 | |||
| 36 | There is no trust-on-first-use and no unsafe mode. A mismatch refuses to | ||
| 37 | connect and logs both fingerprints. Run the gateway on the same host as the | ||
| 38 | game server (or pin the remote key and accept the network hop). | ||
| 39 | |||
| 40 | ## systemd | ||
| 41 | |||
| 42 | Install `zagan-bet-webd` under `/usr/local/bin/`, then: | ||
| 43 | |||
| 44 | ```ini | ||
| 45 | # /etc/systemd/system/zagan-bet-webd.service | ||
| 46 | [Unit] | ||
| 47 | Description=zagan-bet browser gateway | ||
| 48 | After=network-online.target | ||
| 49 | Wants=network-online.target | ||
| 50 | |||
| 51 | [Service] | ||
| 52 | Type=simple | ||
| 53 | User=zagan | ||
| 54 | Group=zagan | ||
| 55 | EnvironmentFile=/etc/zagan-bet-webd.env | ||
| 56 | ExecStart=/usr/local/bin/zagan-bet-webd | ||
| 57 | Restart=on-failure | ||
| 58 | RestartSec=2 | ||
| 59 | NoNewPrivileges=true | ||
| 60 | PrivateTmp=true | ||
| 61 | ProtectSystem=strict | ||
| 62 | ProtectHome=true | ||
| 63 | |||
| 64 | [Install] | ||
| 65 | WantedBy=multi-user.target | ||
| 66 | ``` | ||
| 67 | |||
| 68 | ```sh | ||
| 69 | sudo install -m 0755 target/release/zagan-bet-webd /usr/local/bin/ | ||
| 70 | sudo install -d -m 0755 /etc | ||
| 71 | sudo tee /etc/zagan-bet-webd.env >/dev/null <<'ENV' | ||
| 72 | ZAGAN_UPSTREAM=127.0.0.1:11525 | ||
| 73 | ZAGAN_UPSTREAM_FINGERPRINT=<64 hex> | ||
| 74 | ZAGAN_WEB_ORIGIN=https://zagan.2137697.xyz | ||
| 75 | ZAGAN_WEB_BIND=127.0.0.1:8890 | ||
| 76 | ENV | ||
| 77 | sudo systemctl daemon-reload | ||
| 78 | sudo systemctl enable --now zagan-bet-webd | ||
| 79 | ``` | ||
| 80 | |||
| 81 | nginx needs: | ||
| 82 | |||
| 83 | ```nginx | ||
| 84 | location = /ws { | ||
| 85 | proxy_pass http://127.0.0.1:8890; | ||
| 86 | proxy_http_version 1.1; | ||
| 87 | proxy_set_header Upgrade $http_upgrade; | ||
| 88 | proxy_set_header Connection "upgrade"; | ||
| 89 | proxy_set_header Host $host; | ||
| 90 | proxy_read_timeout 3600s; | ||
| 91 | proxy_send_timeout 3600s; | ||
| 92 | } | ||
| 93 | ``` | ||
| 94 | |||
| 95 | ## Behaviour | ||
| 96 | |||
| 97 | - The WebSocket handshake sets an HttpOnly, SameSite=Strict session cookie. | ||
| 98 | There is no credential storage in the browser or gateway. | ||
| 99 | - A browser close starts the grace window. Upstream stays connected, crash | ||
| 100 | ticks are still applied to the core, and a reconnect with the same cookie | ||
| 101 | gets the current `View`. Expiry closes upstream; the server forfeits a live | ||
| 102 | hand, exactly like a desktop disconnect. | ||
| 103 | - A second socket with the same cookie takes over: the old tab receives | ||
| 104 | `{"type":"replaced"}` and is closed. | ||
| 105 | - A gateway restart loses live sessions, so their rounds are forfeited. There | ||
| 106 | is no protocol-level resume. | ||
| 107 | - The browser sends `{"type":"command","command":...}` and receives | ||
| 108 | `view`/`notice`/`error`/`replaced`/`hello`/`pong` JSON. It never sees the | ||
| 109 | game wire or a certificate. | ||
| 110 | |||
| 111 | ## Security notes | ||
| 112 | |||
| 113 | - Bind loopback. Do not expose the gateway directly. | ||
| 114 | - Keep `ZAGAN_WEB_ORIGIN` exact. The cookie is SameSite=Strict, and the | ||
| 115 | `Origin` check is a second lock on cross-site WebSocket attempts. | ||
| 116 | - The gateway is play-money only in v1. No payments, KYC or real-money copy. | ||
| 117 | - `ZAGAN_UPSTREAM_FINGERPRINT` is the browser's credential path: a wrong or | ||
| 118 | missing pin must fail closed. | ||
