Skip to content

Commit 956200b

Browse files
committed
🛠 feat: Enhance Redis Integration, Rate Limiters & Log Headers (#6462)
* feat: Implement Redis-based rate limiting, initially import limits * feat: Enhance rate limiters with Redis support and custom prefixes * chore: import orders * chore: update JSDoc for next middleware parameter type in ban and limiter middleware * feat: add logHeaders middleware to log forwarded headers in requests * refactor: change log level from info to debug for Redis rate limiters * feat: increase Redis max listeners and refactor session storage to use Keyv
1 parent 492ecf4 commit 956200b

20 files changed

+337
-63
lines changed

api/cache/keyvRedis.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, RED
99

1010
let keyvRedis;
1111
const redis_prefix = REDIS_KEY_PREFIX || '';
12-
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 10;
12+
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 40;
1313

1414
function mapURI(uri) {
1515
const regex =

api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"passport-jwt": "^4.0.1",
106106
"passport-ldapauth": "^3.0.1",
107107
"passport-local": "^1.0.0",
108+
"rate-limit-redis": "^4.2.0",
108109
"sharp": "^0.32.6",
109110
"snaptrade-typescript-sdk": "^9.0.57",
110111
"tiktoken": "^1.0.15",

api/server/middleware/checkBan.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const banResponse = async (req, res) => {
4141
* @function
4242
* @param {Object} req - Express request object.
4343
* @param {Object} res - Express response object.
44-
* @param {Function} next - Next middleware function.
44+
* @param {import('express').NextFunction} next - Next middleware function.
4545
*
4646
* @returns {Promise<function|Object>} - Returns a Promise which when resolved calls next middleware if user or source IP is not banned. Otherwise calls `banResponse()` and sets ban details in `banCache`.
4747
*/

api/server/middleware/concurrentLimiter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const {
2121
* @function
2222
* @param {Object} req - Express request object containing user information.
2323
* @param {Object} res - Express response object.
24-
* @param {function} next - Express next middleware function.
24+
* @param {import('express').NextFunction} next - Next middleware function.
2525
* @throws {Error} Throws an error if the user exceeds the concurrent request limit.
2626
*/
2727
const concurrentLimiter = async (req, res, next) => {

api/server/middleware/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const checkInviteUser = require('./checkInviteUser');
1414
const requireJwtAuth = require('./requireJwtAuth');
1515
const validateModel = require('./validateModel');
1616
const moderateText = require('./moderateText');
17+
const logHeaders = require('./logHeaders');
1718
const setHeaders = require('./setHeaders');
1819
const validate = require('./validate');
1920
const limiters = require('./limiters');
@@ -31,6 +32,7 @@ module.exports = {
3132
checkBan,
3233
uaParser,
3334
setHeaders,
35+
logHeaders,
3436
moderateText,
3537
validateModel,
3638
requireJwtAuth,

api/server/middleware/limiters/importLimiters.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
const Keyv = require('keyv');
12
const rateLimit = require('express-rate-limit');
3+
const { RedisStore } = require('rate-limit-redis');
24
const { ViolationTypes } = require('librechat-data-provider');
35
const logViolation = require('~/cache/logViolation');
6+
const { isEnabled } = require('~/server/utils');
7+
const keyvRedis = require('~/cache/keyvRedis');
8+
const { logger } = require('~/config');
49

510
const getEnvironmentVariables = () => {
611
const IMPORT_IP_MAX = parseInt(process.env.IMPORT_IP_MAX) || 100;
@@ -48,21 +53,39 @@ const createImportLimiters = () => {
4853
const { importIpWindowMs, importIpMax, importUserWindowMs, importUserMax } =
4954
getEnvironmentVariables();
5055

51-
const importIpLimiter = rateLimit({
56+
const ipLimiterOptions = {
5257
windowMs: importIpWindowMs,
5358
max: importIpMax,
5459
handler: createImportHandler(),
55-
});
56-
57-
const importUserLimiter = rateLimit({
60+
};
61+
const userLimiterOptions = {
5862
windowMs: importUserWindowMs,
5963
max: importUserMax,
6064
handler: createImportHandler(false),
6165
keyGenerator: function (req) {
6266
return req.user?.id; // Use the user ID or NULL if not available
6367
},
64-
});
68+
};
69+
70+
if (isEnabled(process.env.USE_REDIS)) {
71+
logger.debug('Using Redis for import rate limiters.');
72+
const keyv = new Keyv({ store: keyvRedis });
73+
const client = keyv.opts.store.redis;
74+
const sendCommand = (...args) => client.call(...args);
75+
const ipStore = new RedisStore({
76+
sendCommand,
77+
prefix: 'import_ip_limiter:',
78+
});
79+
const userStore = new RedisStore({
80+
sendCommand,
81+
prefix: 'import_user_limiter:',
82+
});
83+
ipLimiterOptions.store = ipStore;
84+
userLimiterOptions.store = userStore;
85+
}
6586

87+
const importIpLimiter = rateLimit(ipLimiterOptions);
88+
const importUserLimiter = rateLimit(userLimiterOptions);
6689
return { importIpLimiter, importUserLimiter };
6790
};
6891

api/server/middleware/limiters/loginLimiter.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
const Keyv = require('keyv');
12
const rateLimit = require('express-rate-limit');
2-
const { removePorts } = require('~/server/utils');
3+
const { RedisStore } = require('rate-limit-redis');
4+
const { removePorts, isEnabled } = require('~/server/utils');
5+
const keyvRedis = require('~/cache/keyvRedis');
36
const { logViolation } = require('~/cache');
7+
const { logger } = require('~/config');
48

59
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;
610
const windowMs = LOGIN_WINDOW * 60 * 1000;
@@ -20,11 +24,25 @@ const handler = async (req, res) => {
2024
return res.status(429).json({ message });
2125
};
2226

23-
const loginLimiter = rateLimit({
27+
const limiterOptions = {
2428
windowMs,
2529
max,
2630
handler,
2731
keyGenerator: removePorts,
28-
});
32+
};
33+
34+
if (isEnabled(process.env.USE_REDIS)) {
35+
logger.debug('Using Redis for login rate limiter.');
36+
const keyv = new Keyv({ store: keyvRedis });
37+
const client = keyv.opts.store.redis;
38+
const sendCommand = (...args) => client.call(...args);
39+
const store = new RedisStore({
40+
sendCommand,
41+
prefix: 'login_limiter:',
42+
});
43+
limiterOptions.store = store;
44+
}
45+
46+
const loginLimiter = rateLimit(limiterOptions);
2947

3048
module.exports = loginLimiter;

api/server/middleware/limiters/messageLimiters.js

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
const Keyv = require('keyv');
12
const rateLimit = require('express-rate-limit');
3+
const { RedisStore } = require('rate-limit-redis');
24
const denyRequest = require('~/server/middleware/denyRequest');
5+
const { isEnabled } = require('~/server/utils');
6+
const keyvRedis = require('~/cache/keyvRedis');
37
const { logViolation } = require('~/cache');
8+
const { logger } = require('~/config');
49

510
const {
611
MESSAGE_IP_MAX = 40,
@@ -41,25 +46,49 @@ const createHandler = (ip = true) => {
4146
};
4247

4348
/**
44-
* Message request rate limiter by IP
49+
* Message request rate limiters
4550
*/
46-
const messageIpLimiter = rateLimit({
51+
const ipLimiterOptions = {
4752
windowMs: ipWindowMs,
4853
max: ipMax,
4954
handler: createHandler(),
50-
});
55+
};
5156

52-
/**
53-
* Message request rate limiter by userId
54-
*/
55-
const messageUserLimiter = rateLimit({
57+
const userLimiterOptions = {
5658
windowMs: userWindowMs,
5759
max: userMax,
5860
handler: createHandler(false),
5961
keyGenerator: function (req) {
6062
return req.user?.id; // Use the user ID or NULL if not available
6163
},
62-
});
64+
};
65+
66+
if (isEnabled(process.env.USE_REDIS)) {
67+
logger.debug('Using Redis for message rate limiters.');
68+
const keyv = new Keyv({ store: keyvRedis });
69+
const client = keyv.opts.store.redis;
70+
const sendCommand = (...args) => client.call(...args);
71+
const ipStore = new RedisStore({
72+
sendCommand,
73+
prefix: 'message_ip_limiter:',
74+
});
75+
const userStore = new RedisStore({
76+
sendCommand,
77+
prefix: 'message_user_limiter:',
78+
});
79+
ipLimiterOptions.store = ipStore;
80+
userLimiterOptions.store = userStore;
81+
}
82+
83+
/**
84+
* Message request rate limiter by IP
85+
*/
86+
const messageIpLimiter = rateLimit(ipLimiterOptions);
87+
88+
/**
89+
* Message request rate limiter by userId
90+
*/
91+
const messageUserLimiter = rateLimit(userLimiterOptions);
6392

6493
module.exports = {
6594
messageIpLimiter,

api/server/middleware/limiters/registerLimiter.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
const Keyv = require('keyv');
12
const rateLimit = require('express-rate-limit');
2-
const { removePorts } = require('~/server/utils');
3+
const { RedisStore } = require('rate-limit-redis');
4+
const { removePorts, isEnabled } = require('~/server/utils');
5+
const keyvRedis = require('~/cache/keyvRedis');
36
const { logViolation } = require('~/cache');
7+
const { logger } = require('~/config');
48

59
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;
610
const windowMs = REGISTER_WINDOW * 60 * 1000;
@@ -20,11 +24,25 @@ const handler = async (req, res) => {
2024
return res.status(429).json({ message });
2125
};
2226

23-
const registerLimiter = rateLimit({
27+
const limiterOptions = {
2428
windowMs,
2529
max,
2630
handler,
2731
keyGenerator: removePorts,
28-
});
32+
};
33+
34+
if (isEnabled(process.env.USE_REDIS)) {
35+
logger.debug('Using Redis for register rate limiter.');
36+
const keyv = new Keyv({ store: keyvRedis });
37+
const client = keyv.opts.store.redis;
38+
const sendCommand = (...args) => client.call(...args);
39+
const store = new RedisStore({
40+
sendCommand,
41+
prefix: 'register_limiter:',
42+
});
43+
limiterOptions.store = store;
44+
}
45+
46+
const registerLimiter = rateLimit(limiterOptions);
2947

3048
module.exports = registerLimiter;

api/server/middleware/limiters/resetPasswordLimiter.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
const Keyv = require('keyv');
12
const rateLimit = require('express-rate-limit');
3+
const { RedisStore } = require('rate-limit-redis');
24
const { ViolationTypes } = require('librechat-data-provider');
3-
const { removePorts } = require('~/server/utils');
5+
const { removePorts, isEnabled } = require('~/server/utils');
6+
const keyvRedis = require('~/cache/keyvRedis');
47
const { logViolation } = require('~/cache');
8+
const { logger } = require('~/config');
59

610
const {
711
RESET_PASSWORD_WINDOW = 2,
@@ -25,11 +29,25 @@ const handler = async (req, res) => {
2529
return res.status(429).json({ message });
2630
};
2731

28-
const resetPasswordLimiter = rateLimit({
32+
const limiterOptions = {
2933
windowMs,
3034
max,
3135
handler,
3236
keyGenerator: removePorts,
33-
});
37+
};
38+
39+
if (isEnabled(process.env.USE_REDIS)) {
40+
logger.debug('Using Redis for reset password rate limiter.');
41+
const keyv = new Keyv({ store: keyvRedis });
42+
const client = keyv.opts.store.redis;
43+
const sendCommand = (...args) => client.call(...args);
44+
const store = new RedisStore({
45+
sendCommand,
46+
prefix: 'reset_password_limiter:',
47+
});
48+
limiterOptions.store = store;
49+
}
50+
51+
const resetPasswordLimiter = rateLimit(limiterOptions);
3452

3553
module.exports = resetPasswordLimiter;

0 commit comments

Comments
 (0)