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),