diff --git a/package-lock.json b/package-lock.json index c1e3dfec..e43c0c28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "oslo": "1.2.1", "pg": "^8.16.0", "qrcode.react": "4.2.0", + "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -11506,6 +11507,18 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", + "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, "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 58eeef2d..d4b70836 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "oslo": "1.2.1", "pg": "^8.16.0", "qrcode.react": "4.2.0", + "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -107,9 +108,9 @@ "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.2", + "yargs": "18.0.0", "zod": "3.25.56", - "zod-validation-error": "3.4.1", - "yargs": "18.0.0" + "zod-validation-error": "3.4.1" }, "devDependencies": { "@dotenvx/dotenvx": "1.44.1", diff --git a/server/db/redis.ts b/server/db/redis.ts index 7b09fcb9..e693c205 100644 --- a/server/db/redis.ts +++ b/server/db/redis.ts @@ -4,7 +4,7 @@ import config from "@server/lib/config"; class RedisManager { private static instance: RedisManager; - private client: Redis | null = null; + public client: Redis | null = null; private subscriber: Redis | null = null; private publisher: Redis | null = null; private isEnabled: boolean = false; diff --git a/server/middlewares/rateLimit.ts b/server/middlewares/rateLimit.ts index 2098288f..9b40e242 100644 --- a/server/middlewares/rateLimit.ts +++ b/server/middlewares/rateLimit.ts @@ -3,20 +3,27 @@ import createHttpError from "http-errors"; import { NextFunction, Request, Response } from "express"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; +import config from "@server/lib/config"; +import { RedisStore } from "rate-limit-redis"; +import redisManager from "@server/db/redis"; +import { Command as RedisCommand } from "ioredis"; export function rateLimitMiddleware({ windowMin, max, type, - skipCondition, + skipCondition }: { windowMin: number; max: number; type: "IP_ONLY" | "IP_AND_PATH"; skipCondition?: (req: Request, res: Response) => boolean; }) { + const enableRedis = config.getRawConfig().flags?.enable_redis; + + let opts; if (type === "IP_AND_PATH") { - return rateLimit({ + opts = { windowMs: windowMin * 60 * 1000, max, skip: skipCondition, @@ -26,24 +33,37 @@ export function rateLimitMiddleware({ handler: (req: Request, res: Response, next: NextFunction) => { const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; logger.warn( - `Rate limit exceeded for IP ${req.ip} on path ${req.path}`, + `Rate limit exceeded for IP ${req.ip} on path ${req.path}` ); return next( - createHttpError(HttpCode.TOO_MANY_REQUESTS, message), + createHttpError(HttpCode.TOO_MANY_REQUESTS, message) ); - }, + } + } as any; + } else { + opts = { + windowMs: windowMin * 60 * 1000, + max, + skip: skipCondition, + handler: (req: Request, res: Response, next: NextFunction) => { + const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; + logger.warn(`Rate limit exceeded for IP ${req.ip}`); + return next( + createHttpError(HttpCode.TOO_MANY_REQUESTS, message) + ); + } + }; + } + + if (enableRedis) { + const client = redisManager.client!; + opts.store = new RedisStore({ + sendCommand: async (command: string, ...args: string[]) => + (await client.call(command, args)) as any }); } - return rateLimit({ - windowMs: windowMin * 60 * 1000, - max, - skip: skipCondition, - handler: (req: Request, res: Response, next: NextFunction) => { - const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`; - logger.warn(`Rate limit exceeded for IP ${req.ip}`); - return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message)); - }, - }); + + return rateLimit(opts); } export default rateLimitMiddleware; diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index e06f810c..941f7638 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -21,13 +21,13 @@ export const startOfflineChecker = (): void => { offlineCheckerInterval = setInterval(async () => { try { const twoMinutesAgo = new Date(Date.now() - OFFLINE_THRESHOLD_MS); - + // Find clients that haven't pinged in the last 2 minutes and mark them as offline await db .update(clients) .set({ online: false }) .where( - eq(clients.online, true) && + eq(clients.online, true) && (lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing)) ); @@ -90,4 +90,4 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { broadcast: false, excludeSender: false }; -}; \ No newline at end of file +}; diff --git a/server/routers/ws.ts b/server/routers/ws.ts index f6b5b99d..c925ac5c 100644 --- a/server/routers/ws.ts +++ b/server/routers/ws.ts @@ -398,7 +398,7 @@ if (redisManager.isRedisEnabled()) { }); logger.info(`WebSocket handler initialized with Redis support - Node ID: ${NODE_ID}`); } else { - logger.info('WebSocket handler initialized in local mode (Redis disabled)'); + logger.debug('WebSocket handler initialized in local mode (Redis disabled)'); } // Cleanup function for graceful shutdown