From 5dd323856e434e3a42b86ee2c5d5b5abfd189d40 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 21 Feb 2025 09:21:04 -0800 Subject: [PATCH] add ws rate limiter --- package-lock.json | 7 +++++++ package.json | 1 + src/client/index.html | 2 +- src/server/GameServer.ts | 14 +++++++++++++- src/server/Server.ts | 19 ++++++++++++++++++- 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1ded4a32..774a3d850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,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", @@ -13674,6 +13675,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 04c758f88..60e110238 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,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/client/index.html b/src/client/index.html index 77073fe1a..3e68fae83 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -144,7 +144,7 @@
- v0.15.2 + v0.15.3
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 1b77e274b..94884a517 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 ed94ad9cb..ce36d0711 100644 --- a/src/server/Server.ts +++ b/src/server/Server.ts @@ -23,6 +23,7 @@ import { } from "../core/validations/username"; import { Request, Response } from "express"; import rateLimit from "express-rate-limit"; +import { RateLimiterMemory } from "rate-limiter-flexible"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -43,6 +44,11 @@ app.use( }), ); +const rateLimiter = new RateLimiterMemory({ + points: 20, // 20 messages + duration: 1, // per 1 second +}); + const gm = new GameManager(getServerConfig()); const bot = new DiscordBot(); @@ -153,7 +159,18 @@ app.get("*", function (req, res) { }); wss.on("connection", (ws, req) => { - ws.on("message", (message: string) => { + 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),