feat: HTTP/WebSocket REST API for external monitoring clients#3941
feat: HTTP/WebSocket REST API for external monitoring clients#3941beats-dh wants to merge 10 commits into
Conversation
Replace placeholder login (beats:1234) with real authentication against the accounts table using the existing argon2/SHA1 path. JWT tokens now embed the account id (sub) and account type (acc_type claim) and are signed with a configurable secret. The verify path no longer does the tautological with_subject(decoded.get_subject()) check; non-GET endpoints additionally require accountType >= apiMinAdminType. CSRF middleware was removed — Bearer-token auth is not subject to cookie-based CSRF and the previous check accepted any non-empty header. Rate limits are now real values (5/min on /login for anti-bruteforce, 10/min on destructive admin endpoints, 30-60/min on read-only) instead of the 100M placeholder. All g_game() and Database access from Crow worker threads now goes through the dispatcher via runOnDispatcher (api/utils/dispatcher_sync.hpp), so the game loop's invariants are not racing with API handlers. API server is opt-in via config: - apiEnabled (default false) - apiPort (default 8081) - apiJwtSecret (refused to start if empty) - apiMinAdminType (default 5 = CommunityManager) - apiCorsOrigin (default empty = no CORS header) Removed the test endpoint POST /api/v1/players, the obsolete /api/v1/check-update with hardcoded GitHub URLs, and the leftover VersionInfo/availableVersions/isNewerVersion machinery.
…middleware The XSS sanitizer was rebuilding std::regex objects on every request — once per pattern, per body and per URL param. Out of the 5 patterns, 4 were plain substrings that don't need a regex at all. Compile the one real pattern (<script>...</script>) once into a static std::regex with regex::optimize, and match the rest with a case-insensitive substring search. Behavior unchanged; per-request cost drops from O(patterns × inputs) regex constructions to one cheap pass.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The previous hardening commit removed the endpoint along with the hardcoded GitHub release URL it used. The Beats Monitor app actually consumes it (lib/services/update_service.dart:64), so removing it broke that flow. Restore the endpoint with the same request/response contract, but read the catalog from data/api_versions.json (loaded once at API startup) instead of baking URLs into the binary. Operators ship their own JSON; the engine defaults to "no updates available" when the file is missing. A data/api_versions.json.dist template is included documenting the schema.
|
|
To fix the compilation errors in VS Solution, I had to do this diff --git a/src/api/api.cpp b/src/api/api.cpp
index bf0f5572a..0d2dcf901 100644
--- a/src/api/api.cpp
+++ b/src/api/api.cpp
@@ -56,7 +56,7 @@ void APIServer::initialize(uint16_t port) {
VersionStore::getInstance().loadFromFile("data/api_versions.json");
app.loglevel(crow::LogLevel::Warning);
- app.port(port).use_compression(crow::compression::DEFLATE).multithreaded();
+ app.port(port).multithreaded();
setupRoutes();
setupWebSocket();
diff --git a/src/api/middleware/auth.hpp b/src/api/middleware/auth.hpp
index d8fd222c1..0ba42f3b6 100644
--- a/src/api/middleware/auth.hpp
+++ b/src/api/middleware/auth.hpp
@@ -36,7 +36,7 @@ public:
ctx.accountId = static_cast<uint32_t>(std::stoul(decoded.get_subject()));
if (decoded.has_payload_claim("acc_type")) {
- ctx.accountType = static_cast<uint8_t>(decoded.get_payload_claim("acc_type").as_int());
+ ctx.accountType = static_cast<uint8_t>(decoded.get_payload_claim("acc_type").as_integer());
}
} catch (const std::exception &) {
res = APIResponse::unauthorized("Token inválido ou expirado");
diff --git a/src/api/utils/system_info.cpp b/src/api/utils/system_info.cpp
index 4c3162f6f..4f839ea51 100644
--- a/src/api/utils/system_info.cpp
+++ b/src/api/utils/system_info.cpp
@@ -10,6 +10,7 @@ bool SystemInfo::firstCall = true;
#include <psapi.h>
#include <winreg.h>
#pragma comment(lib, "pdh.lib")
+ #pragma comment(lib, "advapi32.lib")
ULARGE_INTEGER SystemInfo::lastSystemKernel { 0 };
ULARGE_INTEGER SystemInfo::lastSystemUser { 0 };
diff --git a/vcproj/canary.vcxproj b/vcproj/canary.vcxproj
index bc8e08e47..e89f9df01 100644
--- a/vcproj/canary.vcxproj
+++ b/vcproj/canary.vcxproj
@@ -236,6 +236,23 @@
<ClInclude Include="..\src\utils\vectorset.hpp" />
<ClInclude Include="..\src\utils\vectorsort.hpp" />
<ClInclude Include="..\src\utils\wildcardtree.hpp" />
+ <ClInclude Include="..\src\api\api.hpp" />
+ <ClInclude Include="..\src\api\endpoints\player.hpp" />
+ <ClInclude Include="..\src\api\endpoints\server.hpp" />
+ <ClInclude Include="..\src\api\endpoints\websocket.hpp" />
+ <ClInclude Include="..\src\api\middleware\auth.hpp" />
+ <ClInclude Include="..\src\api\middleware\logging.hpp" />
+ <ClInclude Include="..\src\api\middleware\rate_limit.hpp" />
+ <ClInclude Include="..\src\api\middleware\security.hpp" />
+ <ClInclude Include="..\src\api\middleware\validation.hpp" />
+ <ClInclude Include="..\src\api\models\responses.hpp" />
+ <ClInclude Include="..\src\api\utils\broadcast_manager.hpp" />
+ <ClInclude Include="..\src\api\utils\chat_history.hpp" />
+ <ClInclude Include="..\src\api\utils\dispatcher_sync.hpp" />
+ <ClInclude Include="..\src\api\utils\system_info.hpp" />
+ <ClInclude Include="..\src\api\utils\validators.hpp" />
+ <ClInclude Include="..\src\api\utils\version_store.hpp" />
+ <ClInclude Include="..\src\api\utils\websocket_events.hpp" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\src\account\account_repository.cpp" />
@@ -426,6 +443,17 @@
<ClCompile Include="..\src\utils\pugicast.cpp" />
<ClCompile Include="..\src\utils\tools.cpp" />
<ClCompile Include="..\src\utils\wildcardtree.cpp" />
+ <ClCompile Include="..\src\api\api.cpp" />
+ <ClCompile Include="..\src\api\endpoints\player.cpp">
+ <ObjectFileName>$(IntDir)api_player.obj</ObjectFileName>
+ </ClCompile>
+ <ClCompile Include="..\src\api\endpoints\server.cpp">
+ <ObjectFileName>$(IntDir)api_server.obj</ObjectFileName>
+ </ClCompile>
+ <ClCompile Include="..\src\api\utils\broadcast_manager.cpp" />
+ <ClCompile Include="..\src\api\utils\chat_history.cpp" />
+ <ClCompile Include="..\src\api\utils\system_info.cpp" />
+ <ClCompile Include="..\src\api\utils\version_store.cpp" />
</ItemGroup>
As for the APK, I compiled it using this name: Build Android APK
on:
push:
branches:
- main
jobs:
build:
name: Build Release APK
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.29.2'
channel: stable
cache: true
- name: Install dependencies
run: flutter pub get
- name: Build release APK
run: flutter build apk --release
- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: beats-monitor-release-apk
path: build/app/outputs/flutter-apk/app-release.apk
retention-days: 30 |
This comment has been minimized.
This comment has been minimized.
Can you open a PR adding the workflow to the app repository's GHA? |
|
This PR is stale because it has been open 45 days with no activity. |




Summary
Adds an opt-in HTTP + WebSocket REST API embedded in canary, exposing server status, system metrics, online/banned player lists, and a small set of admin actions (kick, ban, broadcast, state). Intended for companion tooling — the Beats Monitor Flutter client is the reference consumer, but the API is generic.
Why
There's no first-class way for external dashboards or admin apps to read live server state without scraping the status protocol or querying the database directly. This adds a structured JSON API with proper auth, plus a WebSocket channel for real-time events (chat bridge, player join/leave, system metrics).
What's added
New module:
src/api/api.{cpp,hpp}— Crow application setup, route registration, lifecycleendpoints/— server, player, websocket handlersmiddleware/— auth (JWT), rate limit, validation, security headers, loggingutils/— broadcast manager (1Hz status push), system info (CPU/mem via/procor PDH), chat history, validators, dispatcher sync helper, version store (for client update catalog)Dependencies (vcpkg.json):
crow,simdjson,jwt-cpp.Game integration (minimal touchpoints):
canary_server.{cpp,hpp}— start/stop the API gated onapiEnabledgame/game.{cpp,hpp}— broadcast hooks for chat / player eventsgame/scheduling/dispatcher.{cpp,hpp}—asyncEventgains ataskNameparameter (used by the API helper to label posted events)lua/functions/core/game/game_functions.{cpp,hpp}—Game.banPlayer/Game.unbanPlayerLua bindings (so the API and/bantalkaction can share a code path)Configuration
Two surfaces:
config.lua.dist— runtime knobs (API is disabled by default):```lua
apiEnabled = true
apiPort = 8081
apiJwtSecret = ""
apiMinAdminType = 5 -- minimum AccountType for destructive endpoints
apiCorsOrigin = "" -- exact origin string; empty disables CORS header
```
The server refuses to start the API if `apiJwtSecret` is empty (logs an error and continues without the API). Silent fallback to a default secret is a foot-gun, so we just refuse.
data/api_versions.json(optional) — catalog consumed by/api/v1/check-update. Operators publish their own client release URLs here; the engine ships a.disttemplate and defaults to "no updates available" when the file is missing. No third-party download URLs are baked into the binary.Endpoints
```
POST /api/v1/login — argon2 auth → JWT
POST /api/v1/check-update — client version check (uses data/api_versions.json)
GET /api/v1/server/status — uptime, version, player counts, world state
GET /api/v1/server/resources — CPU/memory of the canary process
GET /api/v1/server/motd
POST /api/v1/server/state — admin: change game state
POST /api/v1/server/broadcast — admin: broadcast message
GET /api/v1/playersOnline — live list with details
GET /api/v1/players/ — player drill-down
POST /api/v1/players/kick — admin
POST /api/v1/players/ban — admin (action: ban|unban)
GET /api/v1/players/banned — list of bans
GET /api/v1/players/ban/history?name=
WS /ws — subscribe/unsubscribe + push events
```
WebSocket events: `server_status`, `system_resources`, `chat_global`, `chat_trade`, `chat_help`, `chat_history`, plus player join/leave.
Security
Threading
API request handlers run on Crow worker threads, which would race the game loop if they touched `g_game()` or `Database` directly. All such access is marshalled via `runOnDispatcher()` (see `src/api/utils/dispatcher_sync.hpp`), which posts to the dispatcher and waits on a `std::future` with a timeout. The 1Hz broadcast loop does the same for status snapshots; `SystemInfo` reads `/proc` (Linux) or PDH counters (Windows) and stays out of the dispatcher.
This keeps the game's invariants intact and means an unresponsive API request can't deadlock — it times out (default 2s, 5s for DB queries) and returns 500 to the client while the dispatcher keeps running.
Test plan
Notes