mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-15 23:17:55 +02:00
add redis rate limiter
This commit is contained in:
parent
377eb2b851
commit
494b54ac32
6 changed files with 56 additions and 22 deletions
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -74,6 +74,7 @@
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
|
"rate-limit-redis": "^4.2.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-easy-sort": "^1.6.0",
|
"react-easy-sort": "^1.6.0",
|
||||||
|
@ -11506,6 +11507,18 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||||
|
|
|
@ -92,6 +92,7 @@
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
|
"rate-limit-redis": "^4.2.1",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-easy-sort": "^1.6.0",
|
"react-easy-sort": "^1.6.0",
|
||||||
|
@ -107,9 +108,9 @@
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.2",
|
"ws": "8.18.2",
|
||||||
|
"yargs": "18.0.0",
|
||||||
"zod": "3.25.56",
|
"zod": "3.25.56",
|
||||||
"zod-validation-error": "3.4.1",
|
"zod-validation-error": "3.4.1"
|
||||||
"yargs": "18.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.44.1",
|
"@dotenvx/dotenvx": "1.44.1",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import config from "@server/lib/config";
|
||||||
|
|
||||||
class RedisManager {
|
class RedisManager {
|
||||||
private static instance: RedisManager;
|
private static instance: RedisManager;
|
||||||
private client: Redis | null = null;
|
public client: Redis | null = null;
|
||||||
private subscriber: Redis | null = null;
|
private subscriber: Redis | null = null;
|
||||||
private publisher: Redis | null = null;
|
private publisher: Redis | null = null;
|
||||||
private isEnabled: boolean = false;
|
private isEnabled: boolean = false;
|
||||||
|
|
|
@ -3,20 +3,27 @@ import createHttpError from "http-errors";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
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({
|
export function rateLimitMiddleware({
|
||||||
windowMin,
|
windowMin,
|
||||||
max,
|
max,
|
||||||
type,
|
type,
|
||||||
skipCondition,
|
skipCondition
|
||||||
}: {
|
}: {
|
||||||
windowMin: number;
|
windowMin: number;
|
||||||
max: number;
|
max: number;
|
||||||
type: "IP_ONLY" | "IP_AND_PATH";
|
type: "IP_ONLY" | "IP_AND_PATH";
|
||||||
skipCondition?: (req: Request, res: Response) => boolean;
|
skipCondition?: (req: Request, res: Response) => boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const enableRedis = config.getRawConfig().flags?.enable_redis;
|
||||||
|
|
||||||
|
let opts;
|
||||||
if (type === "IP_AND_PATH") {
|
if (type === "IP_AND_PATH") {
|
||||||
return rateLimit({
|
opts = {
|
||||||
windowMs: windowMin * 60 * 1000,
|
windowMs: windowMin * 60 * 1000,
|
||||||
max,
|
max,
|
||||||
skip: skipCondition,
|
skip: skipCondition,
|
||||||
|
@ -26,24 +33,37 @@ export function rateLimitMiddleware({
|
||||||
handler: (req: Request, res: Response, next: NextFunction) => {
|
handler: (req: Request, res: Response, next: NextFunction) => {
|
||||||
const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`;
|
const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`;
|
||||||
logger.warn(
|
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(
|
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,
|
return rateLimit(opts);
|
||||||
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));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default rateLimitMiddleware;
|
export default rateLimitMiddleware;
|
||||||
|
|
|
@ -21,13 +21,13 @@ export const startOfflineChecker = (): void => {
|
||||||
offlineCheckerInterval = setInterval(async () => {
|
offlineCheckerInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const twoMinutesAgo = new Date(Date.now() - OFFLINE_THRESHOLD_MS);
|
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
|
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
|
||||||
await db
|
await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({ online: false })
|
.set({ online: false })
|
||||||
.where(
|
.where(
|
||||||
eq(clients.online, true) &&
|
eq(clients.online, true) &&
|
||||||
(lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing))
|
(lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -90,4 +90,4 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
||||||
broadcast: false,
|
broadcast: false,
|
||||||
excludeSender: false
|
excludeSender: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -398,7 +398,7 @@ if (redisManager.isRedisEnabled()) {
|
||||||
});
|
});
|
||||||
logger.info(`WebSocket handler initialized with Redis support - Node ID: ${NODE_ID}`);
|
logger.info(`WebSocket handler initialized with Redis support - Node ID: ${NODE_ID}`);
|
||||||
} else {
|
} 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
|
// Cleanup function for graceful shutdown
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue