From a2eae98d6be5a42e077c1e89dca18f0442f8c75e Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 3 May 2026 22:34:00 -0600 Subject: [PATCH] revert --- src/client/Main.ts | 12 +----------- src/server/GameServer.ts | 31 ++++++++++++------------------- src/server/Worker.ts | 32 +++++++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index cb009ba9d..767026d33 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -259,7 +259,6 @@ class Client { private storeModal: StoreModal; private tokenLoginModal: TokenLoginModal; private matchmakingModal: MatchmakingModal; - private mostRecentJoinEvent: number; private turnstileTokenPromise: Promise<{ token: string; @@ -756,7 +755,6 @@ class Client { private async handleJoinLobby(event: CustomEvent) { const lobby = event.detail; - this.mostRecentJoinEvent = event.timeStamp; if (this.usernameInput && !this.usernameInput.validateOrShowError()) { return; } @@ -777,7 +775,7 @@ class Client { } const auth = await userAuth(); const playerRole = auth !== false ? (auth.claims.role ?? null) : null; - const newLobbyHandle = joinLobby(this.eventBus, { + this.lobbyHandle = joinLobby(this.eventBus, { gameID: lobby.gameID, serverConfig: config, cosmetics: await getPlayerCosmeticsRefs(), @@ -789,14 +787,6 @@ class Client { gameRecord: lobby.gameRecord, }); - if (this.mostRecentJoinEvent !== event.timeStamp) { - newLobbyHandle.stop(true); - console.warn("Join requested, but was superseded"); - return; - } - - this.lobbyHandle = newLobbyHandle; - this.lobbyHandle.prestart.then(() => { console.log("Closing modals"); document.getElementById("settings-button")?.classList.add("hidden"); diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 2f6175776..93c4557fe 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -593,22 +593,19 @@ export class GameServer { (c) => c.clientID !== client.clientID, ); - if (!this._hasStarted) { - // Remove persistentId if the game has not started to prevent going over max players - this.persistentIdToClientId.delete(client.persistentID); - // Close lobby when host leaves before game starts - if ( - !this.isPublic() && - client.persistentID === this.creatorPersistentID - ) { - this.log.info("Host left, closing lobby", { - gameID: this.id, - }); - for (const c of [...this.activeClients]) { - this.kickClient(c.clientID, KICK_REASON_HOST_LEFT); - } - this._hasEnded = true; + // Close lobby when host leaves before game starts + if ( + !this._hasStarted && + !this.isPublic() && + client.persistentID === this.creatorPersistentID + ) { + this.log.info("Host left, closing lobby", { + gameID: this.id, + }); + for (const c of [...this.activeClients]) { + this.kickClient(c.clientID, KICK_REASON_HOST_LEFT); } + this._hasEnded = true; } }); client.ws.on("error", (error: Error) => { @@ -626,10 +623,6 @@ export class GameServer { this.activeClients = this.activeClients.filter( (c) => c.clientID !== client.clientID, ); - // Remove persistentId if the game has not started to prevent going over max players - if (!this._hasStarted) { - this.persistentIdToClientId.delete(client.persistentID); - } } } diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 2905e1706..f86f926f3 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -3,6 +3,7 @@ import express, { NextFunction, Request, Response } from "express"; import rateLimit from "express-rate-limit"; import http from "http"; import ipAnonymize from "ip-anonymize"; +import { RateLimiter } from "limiter"; import path from "path"; import { fileURLToPath } from "url"; import { WebSocket, WebSocketServer } from "ws"; @@ -286,7 +287,16 @@ export async function startWorker() { // WebSocket handling wss.on("connection", (ws: WebSocket, req) => { ws.on("message", async (message: string) => { - const ip = getClientIp(req); + const forwarded = req.headers["x-forwarded-for"]; + const ip = Array.isArray(forwarded) + ? forwarded[0] + : // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + forwarded || req.socket.remoteAddress || "unknown"; + + if (!getWsIpLimiter(ip).tryRemoveTokens(1)) { + ws.close(1008, "Rate limit exceeded"); + return; + } try { // Parse and handle client messages @@ -619,8 +629,20 @@ function generateGameIdForWorker(): GameID | null { return null; } -function getClientIp(req: http.IncomingMessage): string { - const cfIp = req.headers["cf-connecting-ip"]; - if (typeof cfIp === "string" && cfIp) return cfIp; - return req.socket.remoteAddress ?? "unknown"; +// Per-IP rate limiter for pre-join WebSocket messages. +// Prevents unauthenticated connections from spamming messages +// (e.g. pings) before joining a game. +const wsIpLimiters = new Map(); +function getWsIpLimiter(ip: string): RateLimiter { + let limiter = wsIpLimiters.get(ip); + if (!limiter) { + limiter = new RateLimiter({ + tokensPerInterval: 5, + interval: "second", + }); + wsIpLimiters.set(ip, limiter); + } + return limiter; } +// Clean up stale IP limiters every 10 minutes +setInterval(() => wsIpLimiters.clear(), 10 * 60 * 1000);