mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
Gateway: don't let one malformed WS frame kill the process
A single unauthenticated frame could take down the whole gateway:
- `parsed.type?.startsWith('sdk_')` guarded an undefined/null *type value*,
but not `parsed` itself being null, and not a non-string type. So `null`,
`{"type":1}` and `{"type":{}}` all parsed fine then threw TypeError in the
router.
- The throw landed in async handleMessage(), whose promise the websocket
message callback neither awaited nor caught -> unhandled rejection, which
Bun treats as fatal (exit 1).
Result: any frame above dropped every bot and SDK session and left /status
unreachable. The engine runs as PID 1 and the gateway was backgrounded
unsupervised, so the container stayed "healthy" with SDK connectivity dead
until a redeploy.
Fixes:
- Validate the envelope before dispatch (non-null object, string `type`);
reject and close the offending socket.
- Catch the handleMessage promise so a throw can only affect its own socket.
- Supervise the gateway in the entrypoint: respawn with a 2s backoff.
- Add process-level rejection/exception guards as a last-resort net.
Verified against an isolated local gateway: all malformed frames now leave
the gateway up and /status reachable, only the sending socket is closed, and
a concurrent bot+SDK pair keeps routing state.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
6013a96218
commit
180b4001d8
2 files changed
+62
-5
No files matched your search
+11
-1
@@ -101,8 +101,18 @@ if [ ! -L /opt/server/engine/data/players ]; then
|
||||
fi
|
||||
|
||||
# === START GATEWAY SERVICE ===
|
||||
# Supervised: the engine runs as PID 1, so if the gateway dies the container stays
|
||||
# "healthy" while SDK connectivity is silently dead until a redeploy. Respawn it
|
||||
# instead, with a short backoff so a crash-on-boot can't spin the CPU.
|
||||
echo "Starting gateway service on port 7780..."
|
||||
cd /opt/server/gateway && bun run gateway.ts &
|
||||
(
|
||||
cd /opt/server/gateway
|
||||
while true; do
|
||||
bun run gateway.ts
|
||||
echo "[entrypoint] Gateway exited (code $?), restarting in 2s..." >&2
|
||||
sleep 2
|
||||
done
|
||||
) &
|
||||
|
||||
# === START GAME SERVER ===
|
||||
cd /opt/server/engine
|
||||
|
||||
@@ -598,12 +598,37 @@ const SyncModule = {
|
||||
|
||||
// ============ Message Router ============
|
||||
|
||||
/**
|
||||
* Reject a frame we can't route. The socket is closed rather than ignored: every
|
||||
* legitimate client sends well-formed envelopes, so a malformed one is either a
|
||||
* broken client or a probe, and neither should keep an unauthenticated socket open.
|
||||
*/
|
||||
function rejectFrame(ws: any, reason: string) {
|
||||
console.error(`[Gateway] Rejecting frame: ${reason}`);
|
||||
try {
|
||||
ws.send(JSON.stringify({ type: 'error', error: `Invalid message: ${reason}` }));
|
||||
} catch {}
|
||||
try { ws.close(); } catch {}
|
||||
}
|
||||
|
||||
async function handleMessage(ws: any, data: string) {
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(data);
|
||||
} catch {
|
||||
console.error('[Gateway] Invalid JSON');
|
||||
rejectFrame(ws, 'malformed JSON');
|
||||
return;
|
||||
}
|
||||
|
||||
// Shape-check before dispatch. `null` and `[]` are valid JSON, and a non-string
|
||||
// `type` survives JSON.parse — both used to reach the router and throw on
|
||||
// `parsed.type.startsWith(...)`, taking the whole gateway down with them.
|
||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
rejectFrame(ws, 'expected a JSON object');
|
||||
return;
|
||||
}
|
||||
if (typeof parsed.type !== 'string') {
|
||||
rejectFrame(ws, 'missing or non-string "type"');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -619,7 +644,7 @@ async function handleMessage(ws: any, data: string) {
|
||||
}
|
||||
|
||||
// Route based on message type for new connections
|
||||
if (parsed.type?.startsWith('sdk_')) {
|
||||
if (parsed.type.startsWith('sdk_')) {
|
||||
await SyncModule.handleSDKMessage(ws, parsed);
|
||||
} else if (parsed.type === 'connected' || parsed.type === 'state' || parsed.type === 'actionResult') {
|
||||
await SyncModule.handleBotMessage(ws, parsed);
|
||||
@@ -632,6 +657,17 @@ function handleClose(ws: any) {
|
||||
|
||||
// ============ Server Setup ============
|
||||
|
||||
// Last-resort net. Bun exits(1) on an unhandled rejection by default, so a single
|
||||
// missed `.catch()` anywhere would drop every bot and SDK session. Individual call
|
||||
// sites still catch their own errors — this only keeps the router alive if one is
|
||||
// ever added without one.
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
console.error('[Gateway] Unhandled rejection (ignored, gateway stays up):', reason);
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Gateway] Uncaught exception (ignored, gateway stays up):', error);
|
||||
});
|
||||
|
||||
console.log(`[Gateway] Starting Gateway Service on port ${GATEWAY_PORT}...`);
|
||||
|
||||
const server = Bun.serve({
|
||||
@@ -743,11 +779,22 @@ Bots: ${botSessions.size} | SDKs: ${sdkSessions.size}
|
||||
},
|
||||
|
||||
message(ws: any, message: string | Buffer) {
|
||||
handleMessage(ws, message.toString());
|
||||
// handleMessage is async: without this catch a throw anywhere in a handler
|
||||
// becomes an unhandled rejection, which Bun treats as fatal (exit 1) and
|
||||
// takes down every other session with it. One bad frame must only ever
|
||||
// affect its own socket.
|
||||
handleMessage(ws, message.toString()).catch((error) => {
|
||||
console.error('[Gateway] Unhandled error while handling message:', error);
|
||||
try { ws.close(); } catch {}
|
||||
});
|
||||
},
|
||||
|
||||
close(ws: any) {
|
||||
handleClose(ws);
|
||||
try {
|
||||
handleClose(ws);
|
||||
} catch (error) {
|
||||
console.error('[Gateway] Unhandled error while closing socket:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user