Skip to content
This repository was archived by the owner on Oct 5, 2024. It is now read-only.

Commit e6fd804

Browse files
Merge pull request #28 from TheRedScreen64/backend-rate-limits
Backend rate limits
2 parents c5b9ee4 + 122b785 commit e6fd804

File tree

4 files changed

+89
-6
lines changed

4 files changed

+89
-6
lines changed

backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"cors": "^2.8.5",
3232
"dotenv": "^16.4.4",
3333
"express": "^4.18.2",
34+
"express-rate-limit": "^7.2.0",
3435
"lucia": "^3.0.1",
3536
"node-cron": "^3.0.3",
3637
"oslo": "^1.1.1",

backend/src/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cookieParser from "cookie-parser";
22
import * as dotenv from "dotenv";
33
import express from "express";
4+
import rateLimit from "express-rate-limit";
45
import { createServer } from "http";
56
import { WebSocketServer } from "ws";
67
import { lucia } from "./lib/auth.js";
@@ -38,6 +39,15 @@ const errorHandler = (err: any, req: any, res: any, next: any) => {
3839
}
3940
};
4041

42+
const authLimiter = rateLimit({
43+
windowMs: 60 * 1000,
44+
max: 5,
45+
});
46+
const limiter = rateLimit({
47+
windowMs: 60 * 1000,
48+
max: 100,
49+
});
50+
4151
const app = express();
4252
const server = createServer(app);
4353
const wss = new WebSocketServer({ server });
@@ -70,6 +80,9 @@ app.use(async (req, res, next) => {
7080
return next();
7181
});
7282

83+
app.use(limiter);
84+
app.use("/auth/*", authLimiter);
85+
7386
app.use(
7487
loginRouter,
7588
logoutRouter,

backend/src/lib/websocket.ts

+72-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,36 @@ const messageSchema = z.object({
1313

1414
// TODO: Overhaul
1515

16+
let connections = new Map<string, any>();
17+
18+
type RateLimit = {
19+
count: number;
20+
};
21+
22+
const generalRateLimit = {
23+
timeWindowMs: 10000,
24+
count: 10,
25+
};
26+
const globalChatRateLimit: RateLimit = {
27+
count: 1,
28+
};
29+
1630
function initWebsocket(wss: WebSocketServer) {
1731
wss.on("connection", async (ws, req) => {
32+
if (!req.socket.remoteAddress) {
33+
ws.close();
34+
return;
35+
}
36+
const ip = req.socket.remoteAddress;
37+
let connection = connections.get(ip);
38+
if (!connection) {
39+
connection = {
40+
count: 0,
41+
lastMessageTime: Date.now(),
42+
};
43+
connections.set(ip, connection);
44+
}
45+
1846
let sessionId = url.parse(req.url!, true).query.session!;
1947
if (!sessionId || Array.isArray(sessionId)) {
2048
ws.close();
@@ -31,7 +59,15 @@ function initWebsocket(wss: WebSocketServer) {
3159

3260
ws.on("message", async (reqData: string) => {
3361
try {
34-
await validateSession(ws, sessionId);
62+
const exceededRateLimit = await handleRateLimit(connection!);
63+
if (exceededRateLimit) {
64+
return ws.send(JSON.stringify({ error: { message: "Rate limit exceeded, slow down" } }));
65+
}
66+
67+
const sessionValid = await validateSession(sessionId);
68+
if (!sessionValid) {
69+
return ws.send(JSON.stringify({ error: { message: "Invalid session" } }));
70+
}
3571

3672
let reqJson = JSON.parse(reqData);
3773
let parsed = messageSchema.safeParse(reqJson);
@@ -43,6 +79,10 @@ function initWebsocket(wss: WebSocketServer) {
4379

4480
switch (type) {
4581
case "globalMessage": {
82+
const exceededRateLimit = await checkRateLimit(connection!, globalChatRateLimit);
83+
if (exceededRateLimit) {
84+
return ws.send(JSON.stringify({ error: { message: "Rate limit exceeded, slow down" } }));
85+
}
4686
handleGlobalMessage(ws, wss, data, user);
4787
break;
4888
}
@@ -123,19 +163,45 @@ async function handleChatMessage(ws: WebSocket, data: any, user: User) {
123163
}
124164
}
125165

126-
async function validateSession(ws: WebSocket, sessionId: string | string[]) {
166+
async function validateSession(sessionId: string | string[]): Promise<boolean> {
127167
if (Array.isArray(sessionId)) {
128-
ws.close();
129-
return;
168+
return false;
130169
}
131170
const { session, user } = await lucia.validateSession(sessionId);
132171
if (!session || !user) {
133-
ws.close();
134-
return;
172+
return false;
173+
} else {
174+
return true;
175+
}
176+
}
177+
178+
async function handleRateLimit(connection: any): Promise<boolean> {
179+
const now = Date.now();
180+
const elapsedTime = now - connection.lastMessageTime;
181+
182+
if (elapsedTime < generalRateLimit.timeWindowMs) {
183+
connection.count++;
184+
if (connection.count > generalRateLimit.count) {
185+
return true;
186+
}
187+
} else {
188+
connection.count = 1;
189+
connection.lastMessageTime = now;
190+
}
191+
return false;
192+
}
193+
194+
async function checkRateLimit(connection: any, rateLimit: RateLimit): Promise<boolean> {
195+
console.log(connection.count > rateLimit.count);
196+
if (connection.count > rateLimit.count) {
197+
return true;
198+
} else {
199+
return false;
135200
}
136201
}
137202

138203
function broadcast(wss: WebSocketServer, data: any, skip: WebSocket | null) {
204+
wss.clients;
139205
wss.clients.forEach((client) => {
140206
if (skip && client == skip) return;
141207
if (client.readyState === WebSocket.OPEN) {

backend/src/routes/user/exists.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { formatPrismaError } from "../../lib/utils.js";
66
export const existsRouter = express.Router();
77

88
existsRouter.post("/user/exists", async (req, res, next) => {
9+
if (!res.locals.user) {
10+
return next({ msg: "Not authorized", status: 401 });
11+
}
912
if (!req.body) {
1013
return next({ msg: "No input provided", status: 400 });
1114
}

0 commit comments

Comments
 (0)