diff --git a/package-lock.json b/package-lock.json index 284d6c73e..fa9fee6d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "protobufjs": "^7.3.2", "pureimage": "^0.4.13", "raphael": "^2.3.0", + "rate-limiter-flexible": "^5.0.5", "twemoji": "^14.0.2", "uuid": "^10.0.0", "wheelnav": "^1.7.1", @@ -13895,6 +13896,12 @@ "eve-raphael": "0.5.0" } }, + "node_modules/rate-limiter-flexible": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.5.tgz", + "integrity": "sha512-+/dSQfo+3FYwYygUs/V2BBdwGa9nFtakDwKt4l0bnvNB53TNT++QSFewwHX9qXrZJuMe9j+TUaU21lm5ARgqdQ==", + "license": "ISC" + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", diff --git a/package.json b/package.json index 7bbd15342..779439e07 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "protobufjs": "^7.3.2", "pureimage": "^0.4.13", "raphael": "^2.3.0", + "rate-limiter-flexible": "^5.0.5", "twemoji": "^14.0.2", "uuid": "^10.0.0", "wheelnav": "^1.7.1", diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index e7b2acafc..111cecbc5 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -18,6 +18,7 @@ import WebSocket from "ws"; import { slog } from "./StructuredLog"; import { CreateGameRecord } from "../core/Util"; import { archive } from "./Archive"; +import { RateLimiterMemory } from "rate-limiter-flexible"; export enum GamePhase { Lobby = "LOBBY", @@ -26,6 +27,11 @@ export enum GamePhase { } export class GameServer { + private rateLimiter = new RateLimiterMemory({ + points: 20, // 20 messages + duration: 1, // per 1 second + }); + private maxGameDuration = 5 * 60 * 60 * 1000; // 5 hours private turns: Turn[] = []; @@ -98,7 +104,13 @@ export class GameServer { this.allClients.set(client.clientID, client); - client.ws.on("message", (message: string) => { + client.ws.on("message", async (message: string) => { + try { + await this.rateLimiter.consume(client.ip); + } catch (error) { + console.warn(`Rate limit exceeded for ${client.ip}`); + return; + } try { const clientMsg: ClientMessage = ClientMessageSchema.parse( JSON.parse(message), diff --git a/src/server/Server.ts b/src/server/Server.ts index 219a856ca..5214466fe 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -32,6 +32,7 @@ import dotenv from "dotenv"; import crypto from "crypto"; dotenv.config(); import rateLimit from "express-rate-limit"; +import { RateLimiterMemory } from "rate-limiter-flexible"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -61,15 +62,19 @@ app.use( }), ); -app.set("trust proxy", 2); -app.use( - rateLimit({ - windowMs: 1000, // 1 second - max: 20, // 20 requests per IP per second - }), -); +const rateLimiter = new RateLimiterMemory({ + points: 20, // 20 messages + duration: 1, // per 1 second +}); -const gm = new GameManager(serverConfig); +const gm = new GameManager(getServerConfig()); + +const bot = new DiscordBot(); +try { + await bot.start(); +} catch (error) { + console.error("Failed to start bot:", error); +} let lobbiesString = ""; @@ -241,6 +246,17 @@ app.get("*", function (req, res) { wss.on("connection", (ws, req) => { ws.on("message", async (message: string) => { + let ip = ""; + try { + const forwarded = req.headers["x-forwarded-for"]; + ip = Array.isArray(forwarded) + ? forwarded[0] + : forwarded || req.socket.remoteAddress; + await rateLimiter.consume(ip); + } catch (error) { + console.warn(`rate limit exceede for ${ip}`); + return; + } try { const clientMsg: ClientMessage = ClientMessageSchema.parse( JSON.parse(message),