diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 1c504fc29..812ed9e8a 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -64,11 +64,11 @@ export class GameManager { hasActiveGame(gameID: GameID): boolean { const game = this.games + .filter((g) => g.id == gameID) .filter( (g) => g.phase() == GamePhase.Lobby || g.phase() == GamePhase.Active - ) - .find((g) => g.id == gameID); - return game != null; + ); + return game.length > 0; } // TODO: stop private games to prevent memory leak. @@ -135,7 +135,13 @@ export class GameManager { .forEach((g) => { g.start(); }); - finished.map((g) => g.endGame()); // Fire and forget + finished.forEach((g) => { + try { + g.endGame(); + } catch (error) { + console.log(`error ending game ${g.id}: `, error); + } + }); this.games = [...lobbies, ...active]; } } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index cc7731aa5..32e418c45 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -32,7 +32,7 @@ export class GameServer { duration: 1, // per 1 second }); - private maxGameDuration = 5 * 60 * 60 * 1000; // 5 hours + private maxGameDuration = 3 * 60 * 60 * 1000; // 3 hours private turns: Turn[] = []; private intents: Intent[] = []; @@ -237,8 +237,8 @@ export class GameServer { async endGame() { // Close all WebSocket connections clearInterval(this.endTurnIntervalID); - this.activeClients.forEach((client) => { - client.ws.removeAllListeners("message"); // TODO: remove this? + this.allClients.forEach((client) => { + client.ws.removeAllListeners("message"); if (client.ws.readyState === WebSocket.OPEN) { client.ws.close(1000, "game has ended"); } @@ -311,10 +311,7 @@ export class GameServer { } } this.activeClients = alive; - if ( - now > - this.createdAt + this.config.lobbyLifetime() + this.maxGameDuration - ) { + if (now > this.createdAt + this.maxGameDuration) { console.warn(`${this.id}: game past max duration ${this.id}`); return GamePhase.Finished; } diff --git a/src/server/Server.ts b/src/server/Server.ts index aedf4a93f..fc5104eac 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -67,6 +67,11 @@ const rateLimiter = new RateLimiterMemory({ duration: 1, // per 1 second }); +const updateRateLimiter = new RateLimiterMemory({ + points: 10, + duration: 240, // 4 minutes +}); + const gm = new GameManager(getServerConfig()); const bot = new DiscordBot(); @@ -150,15 +155,32 @@ app.get("/lobbies", (req: Request, res: Response) => { res.send(lobbiesString); }); -app.post("/private_lobby", (req, res) => { +app.post("/private_lobby", async (req, res) => { + let clientIP = ""; + try { + clientIP = req.ip || req.socket.remoteAddress || "unknown"; + await updateRateLimiter.consume(clientIP); + } catch (error) { + console.warn(`create private lobby rate limited for IP ${clientIP}`); + return; + } const id = gm.createPrivateGame(); - console.log("creating private lobby with id ${id}"); + console.log(`ip ${clientIP} creating private lobby with id ${id}`); res.json({ id: id, }); }); -app.post("/archive_singleplayer_game", (req, res) => { +app.post("/archive_singleplayer_game", async (req, res) => { + let clientIP = ""; + try { + clientIP = req.ip || req.socket.remoteAddress || "unknown"; + await updateRateLimiter.consume(clientIP); + } catch (error) { + console.warn(`archive singleplayer game limited for IP ${clientIP}`); + return; + } + try { const gameRecord: GameRecord = req.body; const clientIP = req.ip || req.socket.remoteAddress || "unknown"; @@ -184,12 +206,20 @@ app.post("/archive_singleplayer_game", (req, res) => { } }); -app.post("/start_private_lobby/:id", (req, res) => { +app.post("/start_private_lobby/:id", async (req, res) => { + let clientIP = ""; + try { + clientIP = req.ip || req.socket.remoteAddress || "unknown"; + await updateRateLimiter.consume(clientIP); + } catch (error) { + console.warn(`start private lobby rate limited for IP ${clientIP}`); + return; + } console.log(`starting private lobby with id ${req.params.id}`); gm.startPrivateGame(req.params.id); }); -app.put("/private_lobby/:id", (req, res) => { +app.put("/private_lobby/:id", async (req, res) => { const lobbyID = req.params.id; gm.updateGameConfig(lobbyID, { gameMap: req.body.gameMap,