mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
🛠 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
This commit is contained in:
parent
e928a8eee4
commit
cbba914290
20 changed files with 337 additions and 63 deletions
2
api/cache/keyvRedis.js
vendored
2
api/cache/keyvRedis.js
vendored
|
@ -9,7 +9,7 @@ const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, RED
|
||||||
|
|
||||||
let keyvRedis;
|
let keyvRedis;
|
||||||
const redis_prefix = REDIS_KEY_PREFIX || '';
|
const redis_prefix = REDIS_KEY_PREFIX || '';
|
||||||
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 10;
|
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 40;
|
||||||
|
|
||||||
function mapURI(uri) {
|
function mapURI(uri) {
|
||||||
const regex =
|
const regex =
|
||||||
|
|
|
@ -103,6 +103,7 @@
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"rate-limit-redis": "^4.2.0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"tiktoken": "^1.0.15",
|
"tiktoken": "^1.0.15",
|
||||||
"traverse": "^0.6.7",
|
"traverse": "^0.6.7",
|
||||||
|
|
|
@ -41,7 +41,7 @@ const banResponse = async (req, res) => {
|
||||||
* @function
|
* @function
|
||||||
* @param {Object} req - Express request object.
|
* @param {Object} req - Express request object.
|
||||||
* @param {Object} res - Express response object.
|
* @param {Object} res - Express response object.
|
||||||
* @param {Function} next - Next middleware function.
|
* @param {import('express').NextFunction} next - Next middleware function.
|
||||||
*
|
*
|
||||||
* @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`.
|
* @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`.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,7 +21,7 @@ const {
|
||||||
* @function
|
* @function
|
||||||
* @param {Object} req - Express request object containing user information.
|
* @param {Object} req - Express request object containing user information.
|
||||||
* @param {Object} res - Express response object.
|
* @param {Object} res - Express response object.
|
||||||
* @param {function} next - Express next middleware function.
|
* @param {import('express').NextFunction} next - Next middleware function.
|
||||||
* @throws {Error} Throws an error if the user exceeds the concurrent request limit.
|
* @throws {Error} Throws an error if the user exceeds the concurrent request limit.
|
||||||
*/
|
*/
|
||||||
const concurrentLimiter = async (req, res, next) => {
|
const concurrentLimiter = async (req, res, next) => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ const checkInviteUser = require('./checkInviteUser');
|
||||||
const requireJwtAuth = require('./requireJwtAuth');
|
const requireJwtAuth = require('./requireJwtAuth');
|
||||||
const validateModel = require('./validateModel');
|
const validateModel = require('./validateModel');
|
||||||
const moderateText = require('./moderateText');
|
const moderateText = require('./moderateText');
|
||||||
|
const logHeaders = require('./logHeaders');
|
||||||
const setHeaders = require('./setHeaders');
|
const setHeaders = require('./setHeaders');
|
||||||
const validate = require('./validate');
|
const validate = require('./validate');
|
||||||
const limiters = require('./limiters');
|
const limiters = require('./limiters');
|
||||||
|
@ -31,6 +32,7 @@ module.exports = {
|
||||||
checkBan,
|
checkBan,
|
||||||
uaParser,
|
uaParser,
|
||||||
setHeaders,
|
setHeaders,
|
||||||
|
logHeaders,
|
||||||
moderateText,
|
moderateText,
|
||||||
validateModel,
|
validateModel,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const logViolation = require('~/cache/logViolation');
|
const logViolation = require('~/cache/logViolation');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const getEnvironmentVariables = () => {
|
const getEnvironmentVariables = () => {
|
||||||
const IMPORT_IP_MAX = parseInt(process.env.IMPORT_IP_MAX) || 100;
|
const IMPORT_IP_MAX = parseInt(process.env.IMPORT_IP_MAX) || 100;
|
||||||
|
@ -48,21 +53,39 @@ const createImportLimiters = () => {
|
||||||
const { importIpWindowMs, importIpMax, importUserWindowMs, importUserMax } =
|
const { importIpWindowMs, importIpMax, importUserWindowMs, importUserMax } =
|
||||||
getEnvironmentVariables();
|
getEnvironmentVariables();
|
||||||
|
|
||||||
const importIpLimiter = rateLimit({
|
const ipLimiterOptions = {
|
||||||
windowMs: importIpWindowMs,
|
windowMs: importIpWindowMs,
|
||||||
max: importIpMax,
|
max: importIpMax,
|
||||||
handler: createImportHandler(),
|
handler: createImportHandler(),
|
||||||
});
|
};
|
||||||
|
const userLimiterOptions = {
|
||||||
const importUserLimiter = rateLimit({
|
|
||||||
windowMs: importUserWindowMs,
|
windowMs: importUserWindowMs,
|
||||||
max: importUserMax,
|
max: importUserMax,
|
||||||
handler: createImportHandler(false),
|
handler: createImportHandler(false),
|
||||||
keyGenerator: function (req) {
|
keyGenerator: function (req) {
|
||||||
return req.user?.id; // Use the user ID or NULL if not available
|
return req.user?.id; // Use the user ID or NULL if not available
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for import rate limiters.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const ipStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'import_ip_limiter:',
|
||||||
|
});
|
||||||
|
const userStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'import_user_limiter:',
|
||||||
|
});
|
||||||
|
ipLimiterOptions.store = ipStore;
|
||||||
|
userLimiterOptions.store = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const importIpLimiter = rateLimit(ipLimiterOptions);
|
||||||
|
const importUserLimiter = rateLimit(userLimiterOptions);
|
||||||
return { importIpLimiter, importUserLimiter };
|
return { importIpLimiter, importUserLimiter };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const { removePorts } = require('~/server/utils');
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
|
const { removePorts, isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
const { logViolation } = require('~/cache');
|
const { logViolation } = require('~/cache');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;
|
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;
|
||||||
const windowMs = LOGIN_WINDOW * 60 * 1000;
|
const windowMs = LOGIN_WINDOW * 60 * 1000;
|
||||||
|
@ -20,11 +24,25 @@ const handler = async (req, res) => {
|
||||||
return res.status(429).json({ message });
|
return res.status(429).json({ message });
|
||||||
};
|
};
|
||||||
|
|
||||||
const loginLimiter = rateLimit({
|
const limiterOptions = {
|
||||||
windowMs,
|
windowMs,
|
||||||
max,
|
max,
|
||||||
handler,
|
handler,
|
||||||
keyGenerator: removePorts,
|
keyGenerator: removePorts,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for login rate limiter.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const store = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'login_limiter:',
|
||||||
|
});
|
||||||
|
limiterOptions.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginLimiter = rateLimit(limiterOptions);
|
||||||
|
|
||||||
module.exports = loginLimiter;
|
module.exports = loginLimiter;
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const denyRequest = require('~/server/middleware/denyRequest');
|
const denyRequest = require('~/server/middleware/denyRequest');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
const { logViolation } = require('~/cache');
|
const { logViolation } = require('~/cache');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
MESSAGE_IP_MAX = 40,
|
MESSAGE_IP_MAX = 40,
|
||||||
|
@ -41,25 +46,49 @@ const createHandler = (ip = true) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message request rate limiter by IP
|
* Message request rate limiters
|
||||||
*/
|
*/
|
||||||
const messageIpLimiter = rateLimit({
|
const ipLimiterOptions = {
|
||||||
windowMs: ipWindowMs,
|
windowMs: ipWindowMs,
|
||||||
max: ipMax,
|
max: ipMax,
|
||||||
handler: createHandler(),
|
handler: createHandler(),
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
const userLimiterOptions = {
|
||||||
* Message request rate limiter by userId
|
|
||||||
*/
|
|
||||||
const messageUserLimiter = rateLimit({
|
|
||||||
windowMs: userWindowMs,
|
windowMs: userWindowMs,
|
||||||
max: userMax,
|
max: userMax,
|
||||||
handler: createHandler(false),
|
handler: createHandler(false),
|
||||||
keyGenerator: function (req) {
|
keyGenerator: function (req) {
|
||||||
return req.user?.id; // Use the user ID or NULL if not available
|
return req.user?.id; // Use the user ID or NULL if not available
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for message rate limiters.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const ipStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'message_ip_limiter:',
|
||||||
|
});
|
||||||
|
const userStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'message_user_limiter:',
|
||||||
|
});
|
||||||
|
ipLimiterOptions.store = ipStore;
|
||||||
|
userLimiterOptions.store = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message request rate limiter by IP
|
||||||
|
*/
|
||||||
|
const messageIpLimiter = rateLimit(ipLimiterOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message request rate limiter by userId
|
||||||
|
*/
|
||||||
|
const messageUserLimiter = rateLimit(userLimiterOptions);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
messageIpLimiter,
|
messageIpLimiter,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
const { removePorts } = require('~/server/utils');
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
|
const { removePorts, isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
const { logViolation } = require('~/cache');
|
const { logViolation } = require('~/cache');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;
|
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;
|
||||||
const windowMs = REGISTER_WINDOW * 60 * 1000;
|
const windowMs = REGISTER_WINDOW * 60 * 1000;
|
||||||
|
@ -20,11 +24,25 @@ const handler = async (req, res) => {
|
||||||
return res.status(429).json({ message });
|
return res.status(429).json({ message });
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerLimiter = rateLimit({
|
const limiterOptions = {
|
||||||
windowMs,
|
windowMs,
|
||||||
max,
|
max,
|
||||||
handler,
|
handler,
|
||||||
keyGenerator: removePorts,
|
keyGenerator: removePorts,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for register rate limiter.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const store = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'register_limiter:',
|
||||||
|
});
|
||||||
|
limiterOptions.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerLimiter = rateLimit(limiterOptions);
|
||||||
|
|
||||||
module.exports = registerLimiter;
|
module.exports = registerLimiter;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const { removePorts } = require('~/server/utils');
|
const { removePorts, isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
const { logViolation } = require('~/cache');
|
const { logViolation } = require('~/cache');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
RESET_PASSWORD_WINDOW = 2,
|
RESET_PASSWORD_WINDOW = 2,
|
||||||
|
@ -25,11 +29,25 @@ const handler = async (req, res) => {
|
||||||
return res.status(429).json({ message });
|
return res.status(429).json({ message });
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetPasswordLimiter = rateLimit({
|
const limiterOptions = {
|
||||||
windowMs,
|
windowMs,
|
||||||
max,
|
max,
|
||||||
handler,
|
handler,
|
||||||
keyGenerator: removePorts,
|
keyGenerator: removePorts,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for reset password rate limiter.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const store = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'reset_password_limiter:',
|
||||||
|
});
|
||||||
|
limiterOptions.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPasswordLimiter = rateLimit(limiterOptions);
|
||||||
|
|
||||||
module.exports = resetPasswordLimiter;
|
module.exports = resetPasswordLimiter;
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const logViolation = require('~/cache/logViolation');
|
const logViolation = require('~/cache/logViolation');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const getEnvironmentVariables = () => {
|
const getEnvironmentVariables = () => {
|
||||||
const STT_IP_MAX = parseInt(process.env.STT_IP_MAX) || 100;
|
const STT_IP_MAX = parseInt(process.env.STT_IP_MAX) || 100;
|
||||||
|
@ -47,20 +52,40 @@ const createSTTHandler = (ip = true) => {
|
||||||
const createSTTLimiters = () => {
|
const createSTTLimiters = () => {
|
||||||
const { sttIpWindowMs, sttIpMax, sttUserWindowMs, sttUserMax } = getEnvironmentVariables();
|
const { sttIpWindowMs, sttIpMax, sttUserWindowMs, sttUserMax } = getEnvironmentVariables();
|
||||||
|
|
||||||
const sttIpLimiter = rateLimit({
|
const ipLimiterOptions = {
|
||||||
windowMs: sttIpWindowMs,
|
windowMs: sttIpWindowMs,
|
||||||
max: sttIpMax,
|
max: sttIpMax,
|
||||||
handler: createSTTHandler(),
|
handler: createSTTHandler(),
|
||||||
});
|
};
|
||||||
|
|
||||||
const sttUserLimiter = rateLimit({
|
const userLimiterOptions = {
|
||||||
windowMs: sttUserWindowMs,
|
windowMs: sttUserWindowMs,
|
||||||
max: sttUserMax,
|
max: sttUserMax,
|
||||||
handler: createSTTHandler(false),
|
handler: createSTTHandler(false),
|
||||||
keyGenerator: function (req) {
|
keyGenerator: function (req) {
|
||||||
return req.user?.id; // Use the user ID or NULL if not available
|
return req.user?.id; // Use the user ID or NULL if not available
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for STT rate limiters.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const ipStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'stt_ip_limiter:',
|
||||||
|
});
|
||||||
|
const userStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'stt_user_limiter:',
|
||||||
|
});
|
||||||
|
ipLimiterOptions.store = ipStore;
|
||||||
|
userLimiterOptions.store = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sttIpLimiter = rateLimit(ipLimiterOptions);
|
||||||
|
const sttUserLimiter = rateLimit(userLimiterOptions);
|
||||||
|
|
||||||
return { sttIpLimiter, sttUserLimiter };
|
return { sttIpLimiter, sttUserLimiter };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,25 +1,46 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const logViolation = require('~/cache/logViolation');
|
const logViolation = require('~/cache/logViolation');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const toolCallLimiter = rateLimit({
|
const handler = async (req, res) => {
|
||||||
|
const type = ViolationTypes.TOOL_CALL_LIMIT;
|
||||||
|
const errorMessage = {
|
||||||
|
type,
|
||||||
|
max: 1,
|
||||||
|
limiter: 'user',
|
||||||
|
windowInMinutes: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
await logViolation(req, res, type, errorMessage, 0);
|
||||||
|
res.status(429).json({ message: 'Too many tool call requests. Try again later' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const limiterOptions = {
|
||||||
windowMs: 1000,
|
windowMs: 1000,
|
||||||
max: 1,
|
max: 1,
|
||||||
handler: async (req, res) => {
|
handler,
|
||||||
const type = ViolationTypes.TOOL_CALL_LIMIT;
|
|
||||||
const errorMessage = {
|
|
||||||
type,
|
|
||||||
max: 1,
|
|
||||||
limiter: 'user',
|
|
||||||
windowInMinutes: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
await logViolation(req, res, type, errorMessage, 0);
|
|
||||||
res.status(429).json({ message: 'Too many tool call requests. Try again later' });
|
|
||||||
},
|
|
||||||
keyGenerator: function (req) {
|
keyGenerator: function (req) {
|
||||||
return req.user?.id;
|
return req.user?.id;
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for tool call rate limiter.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const store = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'tool_call_limiter:',
|
||||||
|
});
|
||||||
|
limiterOptions.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCallLimiter = rateLimit(limiterOptions);
|
||||||
|
|
||||||
module.exports = toolCallLimiter;
|
module.exports = toolCallLimiter;
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const logViolation = require('~/cache/logViolation');
|
const logViolation = require('~/cache/logViolation');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const getEnvironmentVariables = () => {
|
const getEnvironmentVariables = () => {
|
||||||
const TTS_IP_MAX = parseInt(process.env.TTS_IP_MAX) || 100;
|
const TTS_IP_MAX = parseInt(process.env.TTS_IP_MAX) || 100;
|
||||||
|
@ -47,20 +52,40 @@ const createTTSHandler = (ip = true) => {
|
||||||
const createTTSLimiters = () => {
|
const createTTSLimiters = () => {
|
||||||
const { ttsIpWindowMs, ttsIpMax, ttsUserWindowMs, ttsUserMax } = getEnvironmentVariables();
|
const { ttsIpWindowMs, ttsIpMax, ttsUserWindowMs, ttsUserMax } = getEnvironmentVariables();
|
||||||
|
|
||||||
const ttsIpLimiter = rateLimit({
|
const ipLimiterOptions = {
|
||||||
windowMs: ttsIpWindowMs,
|
windowMs: ttsIpWindowMs,
|
||||||
max: ttsIpMax,
|
max: ttsIpMax,
|
||||||
handler: createTTSHandler(),
|
handler: createTTSHandler(),
|
||||||
});
|
};
|
||||||
|
|
||||||
const ttsUserLimiter = rateLimit({
|
const userLimiterOptions = {
|
||||||
windowMs: ttsUserWindowMs,
|
windowMs: ttsUserWindowMs,
|
||||||
max: ttsUserMax,
|
max: ttsUserMax,
|
||||||
handler: createTTSHandler(false),
|
handler: createTTSHandler(false),
|
||||||
keyGenerator: function (req) {
|
keyGenerator: function (req) {
|
||||||
return req.user?.id; // Use the user ID or NULL if not available
|
return req.user?.id; // Use the user ID or NULL if not available
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for TTS rate limiters.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const ipStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'tts_ip_limiter:',
|
||||||
|
});
|
||||||
|
const userStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'tts_user_limiter:',
|
||||||
|
});
|
||||||
|
ipLimiterOptions.store = ipStore;
|
||||||
|
userLimiterOptions.store = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttsIpLimiter = rateLimit(ipLimiterOptions);
|
||||||
|
const ttsUserLimiter = rateLimit(userLimiterOptions);
|
||||||
|
|
||||||
return { ttsIpLimiter, ttsUserLimiter };
|
return { ttsIpLimiter, ttsUserLimiter };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const logViolation = require('~/cache/logViolation');
|
const logViolation = require('~/cache/logViolation');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const getEnvironmentVariables = () => {
|
const getEnvironmentVariables = () => {
|
||||||
const FILE_UPLOAD_IP_MAX = parseInt(process.env.FILE_UPLOAD_IP_MAX) || 100;
|
const FILE_UPLOAD_IP_MAX = parseInt(process.env.FILE_UPLOAD_IP_MAX) || 100;
|
||||||
|
@ -52,20 +57,40 @@ const createFileLimiters = () => {
|
||||||
const { fileUploadIpWindowMs, fileUploadIpMax, fileUploadUserWindowMs, fileUploadUserMax } =
|
const { fileUploadIpWindowMs, fileUploadIpMax, fileUploadUserWindowMs, fileUploadUserMax } =
|
||||||
getEnvironmentVariables();
|
getEnvironmentVariables();
|
||||||
|
|
||||||
const fileUploadIpLimiter = rateLimit({
|
const ipLimiterOptions = {
|
||||||
windowMs: fileUploadIpWindowMs,
|
windowMs: fileUploadIpWindowMs,
|
||||||
max: fileUploadIpMax,
|
max: fileUploadIpMax,
|
||||||
handler: createFileUploadHandler(),
|
handler: createFileUploadHandler(),
|
||||||
});
|
};
|
||||||
|
|
||||||
const fileUploadUserLimiter = rateLimit({
|
const userLimiterOptions = {
|
||||||
windowMs: fileUploadUserWindowMs,
|
windowMs: fileUploadUserWindowMs,
|
||||||
max: fileUploadUserMax,
|
max: fileUploadUserMax,
|
||||||
handler: createFileUploadHandler(false),
|
handler: createFileUploadHandler(false),
|
||||||
keyGenerator: function (req) {
|
keyGenerator: function (req) {
|
||||||
return req.user?.id; // Use the user ID or NULL if not available
|
return req.user?.id; // Use the user ID or NULL if not available
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for file upload rate limiters.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const ipStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'file_upload_ip_limiter:',
|
||||||
|
});
|
||||||
|
const userStore = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'file_upload_user_limiter:',
|
||||||
|
});
|
||||||
|
ipLimiterOptions.store = ipStore;
|
||||||
|
userLimiterOptions.store = userStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUploadIpLimiter = rateLimit(ipLimiterOptions);
|
||||||
|
const fileUploadUserLimiter = rateLimit(userLimiterOptions);
|
||||||
|
|
||||||
return { fileUploadIpLimiter, fileUploadUserLimiter };
|
return { fileUploadIpLimiter, fileUploadUserLimiter };
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
const Keyv = require('keyv');
|
||||||
const rateLimit = require('express-rate-limit');
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const { RedisStore } = require('rate-limit-redis');
|
||||||
const { ViolationTypes } = require('librechat-data-provider');
|
const { ViolationTypes } = require('librechat-data-provider');
|
||||||
const { removePorts } = require('~/server/utils');
|
const { removePorts, isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
const { logViolation } = require('~/cache');
|
const { logViolation } = require('~/cache');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
VERIFY_EMAIL_WINDOW = 2,
|
VERIFY_EMAIL_WINDOW = 2,
|
||||||
|
@ -25,11 +29,25 @@ const handler = async (req, res) => {
|
||||||
return res.status(429).json({ message });
|
return res.status(429).json({ message });
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyEmailLimiter = rateLimit({
|
const limiterOptions = {
|
||||||
windowMs,
|
windowMs,
|
||||||
max,
|
max,
|
||||||
handler,
|
handler,
|
||||||
keyGenerator: removePorts,
|
keyGenerator: removePorts,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
|
logger.debug('Using Redis for verify email rate limiter.');
|
||||||
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
|
const client = keyv.opts.store.redis;
|
||||||
|
const sendCommand = (...args) => client.call(...args);
|
||||||
|
const store = new RedisStore({
|
||||||
|
sendCommand,
|
||||||
|
prefix: 'verify_email_limiter:',
|
||||||
|
});
|
||||||
|
limiterOptions.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyEmailLimiter = rateLimit(limiterOptions);
|
||||||
|
|
||||||
module.exports = verifyEmailLimiter;
|
module.exports = verifyEmailLimiter;
|
||||||
|
|
32
api/server/middleware/logHeaders.js
Normal file
32
api/server/middleware/logHeaders.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to log Forwarded Headers
|
||||||
|
* @function
|
||||||
|
* @param {ServerRequest} req - Express request object containing user information.
|
||||||
|
* @param {ServerResponse} res - Express response object.
|
||||||
|
* @param {import('express').NextFunction} next - Next middleware function.
|
||||||
|
* @throws {Error} Throws an error if the user exceeds the concurrent request limit.
|
||||||
|
*/
|
||||||
|
const logHeaders = (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const forwardedHeaders = {};
|
||||||
|
if (req.headers['x-forwarded-for']) {
|
||||||
|
forwardedHeaders['x-forwarded-for'] = req.headers['x-forwarded-for'];
|
||||||
|
}
|
||||||
|
if (req.headers['x-forwarded-host']) {
|
||||||
|
forwardedHeaders['x-forwarded-host'] = req.headers['x-forwarded-host'];
|
||||||
|
}
|
||||||
|
if (req.headers['x-forwarded-proto']) {
|
||||||
|
forwardedHeaders['x-forwarded-proto'] = req.headers['x-forwarded-proto'];
|
||||||
|
}
|
||||||
|
if (Object.keys(forwardedHeaders).length > 0) {
|
||||||
|
logger.debug('X-Forwarded headers detected in OAuth request:', forwardedHeaders);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error logging X-Forwarded headers:', error);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = logHeaders;
|
|
@ -17,6 +17,7 @@ const {
|
||||||
} = require('~/server/controllers/TwoFactorController');
|
} = require('~/server/controllers/TwoFactorController');
|
||||||
const {
|
const {
|
||||||
checkBan,
|
checkBan,
|
||||||
|
logHeaders,
|
||||||
loginLimiter,
|
loginLimiter,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
checkInviteUser,
|
checkInviteUser,
|
||||||
|
@ -35,6 +36,7 @@ const ldapAuth = !!process.env.LDAP_URL && !!process.env.LDAP_USER_SEARCH_BASE;
|
||||||
router.post('/logout', requireJwtAuth, logoutController);
|
router.post('/logout', requireJwtAuth, logoutController);
|
||||||
router.post(
|
router.post(
|
||||||
'/login',
|
'/login',
|
||||||
|
logHeaders,
|
||||||
loginLimiter,
|
loginLimiter,
|
||||||
checkBan,
|
checkBan,
|
||||||
ldapAuth ? requireLdapAuth : requireLocalAuth,
|
ldapAuth ? requireLdapAuth : requireLocalAuth,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
// file deepcode ignore NoRateLimitingForLogin: Rate limiting is handled by the `loginLimiter` middleware
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const { loginLimiter, checkBan, checkDomainAllowed } = require('~/server/middleware');
|
const { loginLimiter, logHeaders, checkBan, checkDomainAllowed } = require('~/server/middleware');
|
||||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ const domains = {
|
||||||
server: process.env.DOMAIN_SERVER,
|
server: process.env.DOMAIN_SERVER,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
router.use(logHeaders);
|
||||||
router.use(loginLimiter);
|
router.use(loginLimiter);
|
||||||
|
|
||||||
const oauthHandler = async (req, res) => {
|
const oauthHandler = async (req, res) => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const Redis = require('ioredis');
|
const Keyv = require('keyv');
|
||||||
const passport = require('passport');
|
const passport = require('passport');
|
||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
const MemoryStore = require('memorystore')(session);
|
const MemoryStore = require('memorystore')(session);
|
||||||
|
@ -12,6 +12,7 @@ const {
|
||||||
appleLogin,
|
appleLogin,
|
||||||
} = require('~/strategies');
|
} = require('~/strategies');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
const keyvRedis = require('~/cache/keyvRedis');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,6 +20,8 @@ const { logger } = require('~/config');
|
||||||
* @param {Express.Application} app
|
* @param {Express.Application} app
|
||||||
*/
|
*/
|
||||||
const configureSocialLogins = (app) => {
|
const configureSocialLogins = (app) => {
|
||||||
|
logger.info('Configuring social logins...');
|
||||||
|
|
||||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||||
passport.use(googleLogin());
|
passport.use(googleLogin());
|
||||||
}
|
}
|
||||||
|
@ -41,18 +44,17 @@ const configureSocialLogins = (app) => {
|
||||||
process.env.OPENID_SCOPE &&
|
process.env.OPENID_SCOPE &&
|
||||||
process.env.OPENID_SESSION_SECRET
|
process.env.OPENID_SESSION_SECRET
|
||||||
) {
|
) {
|
||||||
|
logger.info('Configuring OpenID Connect...');
|
||||||
const sessionOptions = {
|
const sessionOptions = {
|
||||||
secret: process.env.OPENID_SESSION_SECRET,
|
secret: process.env.OPENID_SESSION_SECRET,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
};
|
};
|
||||||
if (isEnabled(process.env.USE_REDIS)) {
|
if (isEnabled(process.env.USE_REDIS)) {
|
||||||
const client = new Redis(process.env.REDIS_URI);
|
logger.debug('Using Redis for session storage in OpenID...');
|
||||||
client
|
const keyv = new Keyv({ store: keyvRedis });
|
||||||
.on('error', (err) => logger.error('ioredis error:', err))
|
const client = keyv.opts.store.redis;
|
||||||
.on('ready', () => logger.info('ioredis successfully initialized.'))
|
sessionOptions.store = new RedisStore({ client, prefix: 'openid_session' });
|
||||||
.on('reconnecting', () => logger.info('ioredis reconnecting...'));
|
|
||||||
sessionOptions.store = new RedisStore({ client, prefix: 'librechat' });
|
|
||||||
} else {
|
} else {
|
||||||
sessionOptions.store = new MemoryStore({
|
sessionOptions.store = new MemoryStore({
|
||||||
checkPeriod: 86400000, // prune expired entries every 24h
|
checkPeriod: 86400000, // prune expired entries every 24h
|
||||||
|
@ -61,6 +63,8 @@ const configureSocialLogins = (app) => {
|
||||||
app.use(session(sessionOptions));
|
app.use(session(sessionOptions));
|
||||||
app.use(passport.session());
|
app.use(passport.session());
|
||||||
setupOpenId();
|
setupOpenId();
|
||||||
|
|
||||||
|
logger.info('OpenID Connect configured.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -119,6 +119,7 @@
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"rate-limit-redis": "^4.2.0",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"tiktoken": "^1.0.15",
|
"tiktoken": "^1.0.15",
|
||||||
"traverse": "^0.6.7",
|
"traverse": "^0.6.7",
|
||||||
|
@ -37941,6 +37942,17 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rate-limit-redis": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-wV450NQyKC24NmPosJb2131RoczLdfIJdKCReNwtVpm5998U8SgKrAZrIHaN/NfQgqOHaan8Uq++B4sa5REwjA==",
|
||||||
|
"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",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue