mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 16:24:36 +00:00
67f7d09fe5
## What A trusted, server-side HTTP API so a bot authenticated with a shared secret can **create private games, change their settings, start them, kick players, and pause/resume** — without opening a WebSocket or joining as a player. Two endpoints under `/api/adminbot/`, reaching the owning worker via the existing `/wN/` nginx routing. They reuse the existing Zod schemas and `GameServer` methods, mirroring the WebSocket intent flow rather than inventing a new wire protocol. | Endpoint | Purpose | | --- | --- | | `POST /api/adminbot/create_game` | Create a private game; the worker mints a self-owned id and returns it (body: `GameConfigSchema.partial()`) | | `POST /api/adminbot/game/:id/intent` | Send a lobby-management intent (body: base `IntentSchema`) | ## How it works - **Auth:** `ADMIN_BOT_API_KEY` env var via the `x-admin-bot-key` header (timing-safe compare). The whole API is **disabled — 404 — when the var is unset**, so non-configured environments expose nothing. It's distinct from the per-instance `ADMIN_TOKEN`, which an external bot can't know. - **`GameServer.handleIntent`** is the unified intent dispatch for both the WebSocket `case "intent"` path and the admin-bot HTTP API. An `IntentActor` carries identity + authority (per-connection lobby-creator/role checks for the WS path; admin authority for the bot). It honors `update_game_config`, `toggle_game_start_timer`, `kick_player`, and `toggle_pause` — **on private games only** (`isPublic()` → 403). Gameplay intents and `mark_disconnected` are rejected (400). - **Private games only.** `create_game` rejects any `gameType` other than `Private` (Public *and* Singleplayer → 400); an omitted `gameType` defaults to `Private`. - **The bot is never a player.** It sends no `clientID`; the server stamps a placeholder `ADMIN_BOT_CLIENT_ID = "ADMINBOT"` (collision-proof — contains `I`/`O`, which `generateID()` never emits). A gameplay intent stamped with it would resolve to no player, so puppeteering is structurally impossible on top of the explicit 400. - **Determinism unchanged:** the only intent that reaches the sim is `toggle_pause`, via the same `addIntent` → turn queue → `ServerTurnMessage` path the WS uses. ## Notable details for review - **`hostCheats` is assigned unconditionally — on purpose.** `updateGameConfig` sets `this.gameConfig.hostCheats = gameConfig.hostCheats` unconditionally, unlike its sibling fields (which are guarded on `!== undefined`). The WS host clears cheats by re-sending the *full* config with `hostCheats: undefined`, so here `undefined` must mean "clear", not "leave unchanged". **Caveat for the admin bot**, which is a *partial*-update client: a partial `update_game_config` that omits `hostCheats` will clear it — the bot should send `hostCheats` explicitly (or a full config) when it wants to keep a previously-set value. - **Deploy wiring:** `ADMIN_BOT_API_KEY` is piped through the deploy steps' `env:` in `deploy.yml`/`release.yml` → `deploy.sh` heredoc → container via `update.sh`'s `--env-file`. The remaining manual step is creating the GitHub secret itself. ## Tests 19 new tests: - `GameServer.handleIntent` admin-bot behavior (per-intent, private-only, post-start guards, placeholder clientID, rejected gameplay/`mark_disconnected` intents). - `create_game` gameType guard (Public and Singleplayer both rejected). - `requireAdminBotKey` middleware (404 disabled / 401 missing / 401 wrong / pass). tsc + eslint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
285 lines
11 KiB
Nginx Configuration File
285 lines
11 KiB
Nginx Configuration File
# WebSocket settings
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' close;
|
|
}
|
|
|
|
# Cache configuration
|
|
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;
|
|
|
|
# API cache for frequently requested endpoints
|
|
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=API_CACHE:10m inactive=60m max_size=100m;
|
|
|
|
server {
|
|
listen 80 default_server;
|
|
|
|
# Large cookie support
|
|
large_client_header_buffers 4 32k;
|
|
proxy_buffer_size 32k;
|
|
proxy_busy_buffers_size 48k;
|
|
proxy_buffers 4 32k;
|
|
|
|
# Logging
|
|
access_log /var/log/nginx/access.log;
|
|
error_log /var/log/nginx/error.log;
|
|
|
|
location ^~ /assets/ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
location ^~ /_assets/ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
location ~* ^/w(\d+)(/(?:assets|_assets)/.*)$ {
|
|
set $worker $1;
|
|
proxy_pass http://127.0.0.1:$worker_port$2$is_args$args;
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
|
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Worker locations - Processing this first so worker-specific requests are handled by workers
|
|
# This prevents static file regexes from capturing worker requests
|
|
location ~* ^/w(\d+)(/.*)?$ {
|
|
set $worker $1;
|
|
# Preserve query string by appending $is_args$args
|
|
proxy_pass http://127.0.0.1:$worker_port$2$is_args$args;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Randomly distribute new-game creation across the live workers; the worker
|
|
# mints a self-owned id and returns it. The openfront_workers upstream is
|
|
# generated at container start from NUM_WORKERS (generate-nginx-upstream.sh)
|
|
# and can be reused by any future endpoint that wants the same balancing.
|
|
location = /api/create_game {
|
|
proxy_pass http://openfront_workers;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Admin bot create-game: same random-worker balancing as /api/create_game.
|
|
# The worker checks the admin bot key, mints a self-owned id, and returns it.
|
|
location = /api/adminbot/create_game {
|
|
proxy_pass http://openfront_workers;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Static file handling with proper MIME types and consistent caching
|
|
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
|
|
# Include MIME types
|
|
include /etc/nginx/mime.types;
|
|
|
|
# Cache configuration for static files
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h; # Cache successful responses for 24 hours
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
|
|
# Show cache status in response headers
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
# Standard proxy headers
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Default cache policy for static files
|
|
add_header Cache-Control "public, max-age=86400"; # 24 hours
|
|
}
|
|
|
|
# /api/health endpoint - No caching, always hit the backend
|
|
location = /api/health {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_http_version 1.1;
|
|
|
|
# Cache configuration - No caching for health checks
|
|
proxy_cache off;
|
|
add_header X-Cache-Status "BYPASS";
|
|
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
add_header Pragma "no-cache";
|
|
add_header Expires "0";
|
|
|
|
# Standard proxy headers
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# /commit.txt endpoint - Cache for 5 seconds
|
|
location = /commit.txt {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_http_version 1.1;
|
|
|
|
# Cache configuration
|
|
proxy_cache API_CACHE;
|
|
proxy_cache_valid 200 5s; # Cache successful responses for 5 seconds
|
|
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
# Standard proxy headers
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Binary files caching
|
|
location ~* \.(bin|dat|exe|dll|so|dylib)$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
add_header Cache-Control "public, max-age=31536000, immutable"; # 1 year for binary files
|
|
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Specific file type caching rules (outside the /static/ location)
|
|
location ~* \.js$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
add_header Content-Type application/javascript;
|
|
add_header Cache-Control "public, max-age=31536000, immutable"; # 1 year for JS files
|
|
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
location ~* \.css$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
add_header Content-Type text/css;
|
|
add_header Cache-Control "public, max-age=31536000, immutable"; # 1 year for CSS files
|
|
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 24h;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Updated HTML file caching - 1 second cache
|
|
location ~* \.html$ {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
add_header Content-Type text/html;
|
|
add_header Cache-Control "public, max-age=1"; # 1 second for HTML files
|
|
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 1s; # Cache successful responses for 1 second
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
|
|
# Root location with short shared cache for the app shell
|
|
location = / {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
|
|
# Cache the shared app shell briefly at the proxy; browser/CDN policy
|
|
# comes from the upstream Cache-Control header.
|
|
proxy_cache STATIC;
|
|
proxy_cache_valid 200 302 300s;
|
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
|
proxy_cache_lock on;
|
|
|
|
# Show cache status in response headers
|
|
add_header X-Cache-Status $upstream_cache_status;
|
|
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
# Default location for all other requests
|
|
location / {
|
|
proxy_pass http://127.0.0.1:3000;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
}
|