mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
🏦 refactor: Centralize Caching & Redis Key Prefixing (#8457)
* 🔧 Overhauled caching feature:
- Refactored caching logic.
- Fixed redis prefix, namespace, tls, ttl, and cluster.
- Added REDIS_KEY_PREFIX_VAR
* # refactor: Rename redisCache to standardCache
* # Add Redis pinging mechanism to maintain connection.
* # docs: Add warning about Keyv Redis client prefix support
This commit is contained in:
parent
418b5e9070
commit
01b012a8fa
39 changed files with 1407 additions and 526 deletions
26
.env.example
26
.env.example
|
@ -601,11 +601,31 @@ HELP_AND_FAQ_URL=https://librechat.ai
|
|||
# REDIS Options #
|
||||
#===============#
|
||||
|
||||
# REDIS_URI=10.10.10.10:6379
|
||||
# Enable Redis for caching and session storage
|
||||
# USE_REDIS=true
|
||||
|
||||
# USE_REDIS_CLUSTER=true
|
||||
# REDIS_CA=/path/to/ca.crt
|
||||
# Single Redis instance
|
||||
# REDIS_URI=redis://127.0.0.1:6379
|
||||
|
||||
# Redis cluster (multiple nodes)
|
||||
# REDIS_URI=redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
|
||||
|
||||
# Redis with TLS/SSL encryption and CA certificate
|
||||
# REDIS_URI=rediss://127.0.0.1:6380
|
||||
# REDIS_CA=/path/to/ca-cert.pem
|
||||
|
||||
# Redis authentication (if required)
|
||||
# REDIS_USERNAME=your_redis_username
|
||||
# REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# Redis key prefix configuration
|
||||
# Use environment variable name for dynamic prefix (recommended for cloud deployments)
|
||||
# REDIS_KEY_PREFIX_VAR=K_REVISION
|
||||
# Or use static prefix directly
|
||||
# REDIS_KEY_PREFIX=librechat
|
||||
|
||||
# Redis connection limits
|
||||
# REDIS_MAX_LISTENERS=40
|
||||
|
||||
#==================================================#
|
||||
# Others #
|
||||
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -125,3 +125,12 @@ helm/**/.values.yaml
|
|||
|
||||
# SAML Idp cert
|
||||
*.cert
|
||||
|
||||
# AI Assistants
|
||||
/.claude/
|
||||
/.cursor/
|
||||
/.copilot/
|
||||
/.aider/
|
||||
/.openai/
|
||||
/.tabnine/
|
||||
/.codeium
|
||||
|
|
33
api/cache/cacheConfig.js
vendored
Normal file
33
api/cache/cacheConfig.js
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
const fs = require('fs');
|
||||
const { math, isEnabled } = require('@librechat/api');
|
||||
|
||||
// To ensure that different deployments do not interfere with each other's cache, we use a prefix for the Redis keys.
|
||||
// This prefix is usually the deployment ID, which is often passed to the container or pod as an env var.
|
||||
// Set REDIS_KEY_PREFIX_VAR to the env var that contains the deployment ID.
|
||||
const REDIS_KEY_PREFIX_VAR = process.env.REDIS_KEY_PREFIX_VAR;
|
||||
const REDIS_KEY_PREFIX = process.env.REDIS_KEY_PREFIX;
|
||||
if (REDIS_KEY_PREFIX_VAR && REDIS_KEY_PREFIX) {
|
||||
throw new Error('Only either REDIS_KEY_PREFIX_VAR or REDIS_KEY_PREFIX can be set.');
|
||||
}
|
||||
|
||||
const USE_REDIS = isEnabled(process.env.USE_REDIS);
|
||||
if (USE_REDIS && !process.env.REDIS_URI) {
|
||||
throw new Error('USE_REDIS is enabled but REDIS_URI is not set.');
|
||||
}
|
||||
|
||||
const cacheConfig = {
|
||||
USE_REDIS,
|
||||
REDIS_URI: process.env.REDIS_URI,
|
||||
REDIS_USERNAME: process.env.REDIS_USERNAME,
|
||||
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
|
||||
REDIS_CA: process.env.REDIS_CA ? fs.readFileSync(process.env.REDIS_CA, 'utf8') : null,
|
||||
REDIS_KEY_PREFIX: process.env[REDIS_KEY_PREFIX_VAR] || REDIS_KEY_PREFIX || '',
|
||||
REDIS_MAX_LISTENERS: math(process.env.REDIS_MAX_LISTENERS, 40),
|
||||
|
||||
CI: isEnabled(process.env.CI),
|
||||
DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE),
|
||||
|
||||
BAN_DURATION: math(process.env.BAN_DURATION, 7200000), // 2 hours
|
||||
};
|
||||
|
||||
module.exports = { cacheConfig };
|
108
api/cache/cacheConfig.spec.js
vendored
Normal file
108
api/cache/cacheConfig.spec.js
vendored
Normal file
|
@ -0,0 +1,108 @@
|
|||
const fs = require('fs');
|
||||
|
||||
describe('cacheConfig', () => {
|
||||
let originalEnv;
|
||||
let originalReadFileSync;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
originalReadFileSync = fs.readFileSync;
|
||||
|
||||
// Clear all related env vars first
|
||||
delete process.env.REDIS_URI;
|
||||
delete process.env.REDIS_CA;
|
||||
delete process.env.REDIS_KEY_PREFIX_VAR;
|
||||
delete process.env.REDIS_KEY_PREFIX;
|
||||
delete process.env.USE_REDIS;
|
||||
|
||||
// Clear require cache
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
fs.readFileSync = originalReadFileSync;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('REDIS_KEY_PREFIX validation and resolution', () => {
|
||||
test('should throw error when both REDIS_KEY_PREFIX_VAR and REDIS_KEY_PREFIX are set', () => {
|
||||
process.env.REDIS_KEY_PREFIX_VAR = 'DEPLOYMENT_ID';
|
||||
process.env.REDIS_KEY_PREFIX = 'manual-prefix';
|
||||
|
||||
expect(() => {
|
||||
require('./cacheConfig');
|
||||
}).toThrow('Only either REDIS_KEY_PREFIX_VAR or REDIS_KEY_PREFIX can be set.');
|
||||
});
|
||||
|
||||
test('should resolve REDIS_KEY_PREFIX from variable reference', () => {
|
||||
process.env.REDIS_KEY_PREFIX_VAR = 'DEPLOYMENT_ID';
|
||||
process.env.DEPLOYMENT_ID = 'test-deployment-123';
|
||||
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('test-deployment-123');
|
||||
});
|
||||
|
||||
test('should use direct REDIS_KEY_PREFIX value', () => {
|
||||
process.env.REDIS_KEY_PREFIX = 'direct-prefix';
|
||||
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('direct-prefix');
|
||||
});
|
||||
|
||||
test('should default to empty string when no prefix is configured', () => {
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('');
|
||||
});
|
||||
|
||||
test('should handle empty variable reference', () => {
|
||||
process.env.REDIS_KEY_PREFIX_VAR = 'EMPTY_VAR';
|
||||
process.env.EMPTY_VAR = '';
|
||||
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('');
|
||||
});
|
||||
|
||||
test('should handle undefined variable reference', () => {
|
||||
process.env.REDIS_KEY_PREFIX_VAR = 'UNDEFINED_VAR';
|
||||
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.REDIS_KEY_PREFIX).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('USE_REDIS and REDIS_URI validation', () => {
|
||||
test('should throw error when USE_REDIS is enabled but REDIS_URI is not set', () => {
|
||||
process.env.USE_REDIS = 'true';
|
||||
|
||||
expect(() => {
|
||||
require('./cacheConfig');
|
||||
}).toThrow('USE_REDIS is enabled but REDIS_URI is not set.');
|
||||
});
|
||||
|
||||
test('should not throw error when USE_REDIS is enabled and REDIS_URI is set', () => {
|
||||
process.env.USE_REDIS = 'true';
|
||||
process.env.REDIS_URI = 'redis://localhost:6379';
|
||||
|
||||
expect(() => {
|
||||
require('./cacheConfig');
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should handle empty REDIS_URI when USE_REDIS is enabled', () => {
|
||||
process.env.USE_REDIS = 'true';
|
||||
process.env.REDIS_URI = '';
|
||||
|
||||
expect(() => {
|
||||
require('./cacheConfig');
|
||||
}).toThrow('USE_REDIS is enabled but REDIS_URI is not set.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('REDIS_CA file reading', () => {
|
||||
test('should be null when REDIS_CA is not set', () => {
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
expect(cacheConfig.REDIS_CA).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
66
api/cache/cacheFactory.js
vendored
Normal file
66
api/cache/cacheFactory.js
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
const KeyvRedis = require('@keyv/redis').default;
|
||||
const { Keyv } = require('keyv');
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
const { keyvRedisClient, ioredisClient, GLOBAL_PREFIX_SEPARATOR } = require('./redisClients');
|
||||
const { Time } = require('librechat-data-provider');
|
||||
const ConnectRedis = require('connect-redis').default;
|
||||
const MemoryStore = require('memorystore')(require('express-session'));
|
||||
const { violationFile } = require('./keyvFiles');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
|
||||
/**
|
||||
* Creates a cache instance using Redis or a fallback store. Suitable for general caching needs.
|
||||
* @param {string} namespace - The cache namespace.
|
||||
* @param {number} [ttl] - Time to live for cache entries.
|
||||
* @param {object} [fallbackStore] - Optional fallback store if Redis is not used.
|
||||
* @returns {Keyv} Cache instance.
|
||||
*/
|
||||
const standardCache = (namespace, ttl = undefined, fallbackStore = undefined) => {
|
||||
if (cacheConfig.USE_REDIS) {
|
||||
const keyvRedis = new KeyvRedis(keyvRedisClient);
|
||||
const cache = new Keyv(keyvRedis, { namespace, ttl });
|
||||
keyvRedis.namespace = cacheConfig.REDIS_KEY_PREFIX;
|
||||
keyvRedis.keyPrefixSeparator = GLOBAL_PREFIX_SEPARATOR;
|
||||
return cache;
|
||||
}
|
||||
if (fallbackStore) return new Keyv({ store: fallbackStore, namespace, ttl });
|
||||
return new Keyv({ namespace, ttl });
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a cache instance for storing violation data.
|
||||
* Uses a file-based fallback store if Redis is not enabled.
|
||||
* @param {string} namespace - The cache namespace for violations.
|
||||
* @param {number} [ttl] - Time to live for cache entries.
|
||||
* @returns {Keyv} Cache instance for violations.
|
||||
*/
|
||||
const violationCache = (namespace, ttl = undefined) => {
|
||||
return standardCache(`violations:${namespace}`, ttl, violationFile);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a session cache instance using Redis or in-memory store.
|
||||
* @param {string} namespace - The session namespace.
|
||||
* @param {number} [ttl] - Time to live for session entries.
|
||||
* @returns {MemoryStore | ConnectRedis} Session store instance.
|
||||
*/
|
||||
const sessionCache = (namespace, ttl = undefined) => {
|
||||
namespace = namespace.endsWith(':') ? namespace : `${namespace}:`;
|
||||
if (!cacheConfig.USE_REDIS) return new MemoryStore({ ttl, checkPeriod: Time.ONE_DAY });
|
||||
return new ConnectRedis({ client: ioredisClient, ttl, prefix: namespace });
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a rate limiter cache using Redis.
|
||||
* @param {string} prefix - The key prefix for rate limiting.
|
||||
* @returns {RedisStore|undefined} RedisStore instance or undefined if Redis is not used.
|
||||
*/
|
||||
const limiterCache = (prefix) => {
|
||||
if (!prefix) throw new Error('prefix is required');
|
||||
if (!cacheConfig.USE_REDIS) return undefined;
|
||||
prefix = prefix.endsWith(':') ? prefix : `${prefix}:`;
|
||||
return new RedisStore({ sendCommand, prefix });
|
||||
};
|
||||
const sendCommand = (...args) => ioredisClient?.call(...args);
|
||||
|
||||
module.exports = { standardCache, sessionCache, violationCache, limiterCache };
|
272
api/cache/cacheFactory.spec.js
vendored
Normal file
272
api/cache/cacheFactory.spec.js
vendored
Normal file
|
@ -0,0 +1,272 @@
|
|||
const { Time } = require('librechat-data-provider');
|
||||
|
||||
// Mock dependencies first
|
||||
const mockKeyvRedis = {
|
||||
namespace: '',
|
||||
keyPrefixSeparator: '',
|
||||
};
|
||||
|
||||
const mockKeyv = jest.fn().mockReturnValue({ mock: 'keyv' });
|
||||
const mockConnectRedis = jest.fn().mockReturnValue({ mock: 'connectRedis' });
|
||||
const mockMemoryStore = jest.fn().mockReturnValue({ mock: 'memoryStore' });
|
||||
const mockRedisStore = jest.fn().mockReturnValue({ mock: 'redisStore' });
|
||||
|
||||
const mockIoredisClient = {
|
||||
call: jest.fn(),
|
||||
};
|
||||
|
||||
const mockKeyvRedisClient = {};
|
||||
const mockViolationFile = {};
|
||||
|
||||
// Mock modules before requiring the main module
|
||||
jest.mock('@keyv/redis', () => ({
|
||||
default: jest.fn().mockImplementation(() => mockKeyvRedis),
|
||||
}));
|
||||
|
||||
jest.mock('keyv', () => ({
|
||||
Keyv: mockKeyv,
|
||||
}));
|
||||
|
||||
jest.mock('./cacheConfig', () => ({
|
||||
cacheConfig: {
|
||||
USE_REDIS: false,
|
||||
REDIS_KEY_PREFIX: 'test',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./redisClients', () => ({
|
||||
keyvRedisClient: mockKeyvRedisClient,
|
||||
ioredisClient: mockIoredisClient,
|
||||
GLOBAL_PREFIX_SEPARATOR: '::',
|
||||
}));
|
||||
|
||||
jest.mock('./keyvFiles', () => ({
|
||||
violationFile: mockViolationFile,
|
||||
}));
|
||||
|
||||
jest.mock('connect-redis', () => ({
|
||||
default: mockConnectRedis,
|
||||
}));
|
||||
|
||||
jest.mock('memorystore', () => jest.fn(() => mockMemoryStore));
|
||||
|
||||
jest.mock('rate-limit-redis', () => ({
|
||||
RedisStore: mockRedisStore,
|
||||
}));
|
||||
|
||||
// Import after mocking
|
||||
const { standardCache, sessionCache, violationCache, limiterCache } = require('./cacheFactory');
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
|
||||
describe('cacheFactory', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset cache config mock
|
||||
cacheConfig.USE_REDIS = false;
|
||||
cacheConfig.REDIS_KEY_PREFIX = 'test';
|
||||
});
|
||||
|
||||
describe('redisCache', () => {
|
||||
it('should create Redis cache when USE_REDIS is true', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
const namespace = 'test-namespace';
|
||||
const ttl = 3600;
|
||||
|
||||
standardCache(namespace, ttl);
|
||||
|
||||
expect(require('@keyv/redis').default).toHaveBeenCalledWith(mockKeyvRedisClient);
|
||||
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl });
|
||||
expect(mockKeyvRedis.namespace).toBe(cacheConfig.REDIS_KEY_PREFIX);
|
||||
expect(mockKeyvRedis.keyPrefixSeparator).toBe('::');
|
||||
});
|
||||
|
||||
it('should create Redis cache with undefined ttl when not provided', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
const namespace = 'test-namespace';
|
||||
|
||||
standardCache(namespace);
|
||||
|
||||
expect(mockKeyv).toHaveBeenCalledWith(mockKeyvRedis, { namespace, ttl: undefined });
|
||||
});
|
||||
|
||||
it('should use fallback store when USE_REDIS is false and fallbackStore is provided', () => {
|
||||
cacheConfig.USE_REDIS = false;
|
||||
const namespace = 'test-namespace';
|
||||
const ttl = 3600;
|
||||
const fallbackStore = { some: 'store' };
|
||||
|
||||
standardCache(namespace, ttl, fallbackStore);
|
||||
|
||||
expect(mockKeyv).toHaveBeenCalledWith({ store: fallbackStore, namespace, ttl });
|
||||
});
|
||||
|
||||
it('should create default Keyv instance when USE_REDIS is false and no fallbackStore', () => {
|
||||
cacheConfig.USE_REDIS = false;
|
||||
const namespace = 'test-namespace';
|
||||
const ttl = 3600;
|
||||
|
||||
standardCache(namespace, ttl);
|
||||
|
||||
expect(mockKeyv).toHaveBeenCalledWith({ namespace, ttl });
|
||||
});
|
||||
|
||||
it('should handle namespace and ttl as undefined', () => {
|
||||
cacheConfig.USE_REDIS = false;
|
||||
|
||||
standardCache();
|
||||
|
||||
expect(mockKeyv).toHaveBeenCalledWith({ namespace: undefined, ttl: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
describe('violationCache', () => {
|
||||
it('should create violation cache with prefixed namespace', () => {
|
||||
const namespace = 'test-violations';
|
||||
const ttl = 7200;
|
||||
|
||||
// We can't easily mock the internal redisCache call since it's in the same module
|
||||
// But we can test that the function executes without throwing
|
||||
expect(() => violationCache(namespace, ttl)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should create violation cache with undefined ttl', () => {
|
||||
const namespace = 'test-violations';
|
||||
|
||||
violationCache(namespace);
|
||||
|
||||
// The function should call redisCache with violations: prefixed namespace
|
||||
// Since we can't easily mock the internal redisCache call, we test the behavior
|
||||
expect(() => violationCache(namespace)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle undefined namespace', () => {
|
||||
expect(() => violationCache(undefined)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionCache', () => {
|
||||
it('should return MemoryStore when USE_REDIS is false', () => {
|
||||
cacheConfig.USE_REDIS = false;
|
||||
const namespace = 'sessions';
|
||||
const ttl = 86400;
|
||||
|
||||
const result = sessionCache(namespace, ttl);
|
||||
|
||||
expect(mockMemoryStore).toHaveBeenCalledWith({ ttl, checkPeriod: Time.ONE_DAY });
|
||||
expect(result).toBe(mockMemoryStore());
|
||||
});
|
||||
|
||||
it('should return ConnectRedis when USE_REDIS is true', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
const namespace = 'sessions';
|
||||
const ttl = 86400;
|
||||
|
||||
const result = sessionCache(namespace, ttl);
|
||||
|
||||
expect(mockConnectRedis).toHaveBeenCalledWith({
|
||||
client: mockIoredisClient,
|
||||
ttl,
|
||||
prefix: `${namespace}:`,
|
||||
});
|
||||
expect(result).toBe(mockConnectRedis());
|
||||
});
|
||||
|
||||
it('should add colon to namespace if not present', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
const namespace = 'sessions';
|
||||
|
||||
sessionCache(namespace);
|
||||
|
||||
expect(mockConnectRedis).toHaveBeenCalledWith({
|
||||
client: mockIoredisClient,
|
||||
ttl: undefined,
|
||||
prefix: 'sessions:',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add colon to namespace if already present', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
const namespace = 'sessions:';
|
||||
|
||||
sessionCache(namespace);
|
||||
|
||||
expect(mockConnectRedis).toHaveBeenCalledWith({
|
||||
client: mockIoredisClient,
|
||||
ttl: undefined,
|
||||
prefix: 'sessions:',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined ttl', () => {
|
||||
cacheConfig.USE_REDIS = false;
|
||||
const namespace = 'sessions';
|
||||
|
||||
sessionCache(namespace);
|
||||
|
||||
expect(mockMemoryStore).toHaveBeenCalledWith({
|
||||
ttl: undefined,
|
||||
checkPeriod: Time.ONE_DAY,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('limiterCache', () => {
|
||||
it('should return undefined when USE_REDIS is false', () => {
|
||||
cacheConfig.USE_REDIS = false;
|
||||
const result = limiterCache('prefix');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return RedisStore when USE_REDIS is true', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
const result = limiterCache('rate-limit');
|
||||
|
||||
expect(mockRedisStore).toHaveBeenCalledWith({
|
||||
sendCommand: expect.any(Function),
|
||||
prefix: `rate-limit:`,
|
||||
});
|
||||
expect(result).toBe(mockRedisStore());
|
||||
});
|
||||
|
||||
it('should add colon to prefix if not present', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
limiterCache('rate-limit');
|
||||
|
||||
expect(mockRedisStore).toHaveBeenCalledWith({
|
||||
sendCommand: expect.any(Function),
|
||||
prefix: 'rate-limit:',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add colon to prefix if already present', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
limiterCache('rate-limit:');
|
||||
|
||||
expect(mockRedisStore).toHaveBeenCalledWith({
|
||||
sendCommand: expect.any(Function),
|
||||
prefix: 'rate-limit:',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass sendCommand function that calls ioredisClient.call', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
limiterCache('rate-limit');
|
||||
|
||||
const sendCommandCall = mockRedisStore.mock.calls[0][0];
|
||||
const sendCommand = sendCommandCall.sendCommand;
|
||||
|
||||
// Test that sendCommand properly delegates to ioredisClient.call
|
||||
const args = ['GET', 'test-key'];
|
||||
sendCommand(...args);
|
||||
|
||||
expect(mockIoredisClient.call).toHaveBeenCalledWith(...args);
|
||||
});
|
||||
|
||||
it('should handle undefined prefix', () => {
|
||||
cacheConfig.USE_REDIS = true;
|
||||
expect(() => limiterCache()).toThrow('prefix is required');
|
||||
});
|
||||
});
|
||||
});
|
164
api/cache/getLogStores.js
vendored
164
api/cache/getLogStores.js
vendored
|
@ -1,113 +1,52 @@
|
|||
const { cacheConfig } = require('./cacheConfig');
|
||||
const { Keyv } = require('keyv');
|
||||
const { isEnabled, math } = require('@librechat/api');
|
||||
const { CacheKeys, ViolationTypes, Time } = require('librechat-data-provider');
|
||||
const { logFile, violationFile } = require('./keyvFiles');
|
||||
const keyvRedis = require('./keyvRedis');
|
||||
const { logFile } = require('./keyvFiles');
|
||||
const keyvMongo = require('./keyvMongo');
|
||||
|
||||
const { BAN_DURATION, USE_REDIS, DEBUG_MEMORY_CACHE, CI } = process.env ?? {};
|
||||
|
||||
const duration = math(BAN_DURATION, 7200000);
|
||||
const isRedisEnabled = isEnabled(USE_REDIS);
|
||||
const debugMemoryCache = isEnabled(DEBUG_MEMORY_CACHE);
|
||||
|
||||
const createViolationInstance = (namespace) => {
|
||||
const config = isRedisEnabled ? { store: keyvRedis } : { store: violationFile, namespace };
|
||||
return new Keyv(config);
|
||||
};
|
||||
|
||||
// Serve cache from memory so no need to clear it on startup/exit
|
||||
const pending_req = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.PENDING_REQ });
|
||||
|
||||
const config = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.CONFIG_STORE });
|
||||
|
||||
const roles = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.ROLES });
|
||||
|
||||
const mcpTools = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.MCP_TOOLS });
|
||||
|
||||
const audioRuns = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.TEN_MINUTES })
|
||||
: new Keyv({ namespace: CacheKeys.AUDIO_RUNS, ttl: Time.TEN_MINUTES });
|
||||
|
||||
const messages = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.ONE_MINUTE })
|
||||
: new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.ONE_MINUTE });
|
||||
|
||||
const flows = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.TWO_MINUTES })
|
||||
: new Keyv({ namespace: CacheKeys.FLOWS, ttl: Time.ONE_MINUTE * 3 });
|
||||
|
||||
const tokenConfig = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
|
||||
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES });
|
||||
|
||||
const genTitle = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.TWO_MINUTES })
|
||||
: new Keyv({ namespace: CacheKeys.GEN_TITLE, ttl: Time.TWO_MINUTES });
|
||||
|
||||
const s3ExpiryInterval = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
|
||||
: new Keyv({ namespace: CacheKeys.S3_EXPIRY_INTERVAL, ttl: Time.THIRTY_MINUTES });
|
||||
|
||||
const modelQueries = isEnabled(process.env.USE_REDIS)
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.MODEL_QUERIES });
|
||||
|
||||
const abortKeys = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis })
|
||||
: new Keyv({ namespace: CacheKeys.ABORT_KEYS, ttl: Time.TEN_MINUTES });
|
||||
|
||||
const openIdExchangedTokensCache = isRedisEnabled
|
||||
? new Keyv({ store: keyvRedis, ttl: Time.TEN_MINUTES })
|
||||
: new Keyv({ namespace: CacheKeys.OPENID_EXCHANGED_TOKENS, ttl: Time.TEN_MINUTES });
|
||||
const { standardCache, sessionCache, violationCache } = require('./cacheFactory');
|
||||
|
||||
const namespaces = {
|
||||
[CacheKeys.ROLES]: roles,
|
||||
[CacheKeys.MCP_TOOLS]: mcpTools,
|
||||
[CacheKeys.CONFIG_STORE]: config,
|
||||
[CacheKeys.PENDING_REQ]: pending_req,
|
||||
[ViolationTypes.BAN]: new Keyv({ store: keyvMongo, namespace: CacheKeys.BANS, ttl: duration }),
|
||||
[CacheKeys.ENCODED_DOMAINS]: new Keyv({
|
||||
[ViolationTypes.GENERAL]: new Keyv({ store: logFile, namespace: 'violations' }),
|
||||
[ViolationTypes.LOGINS]: violationCache(ViolationTypes.LOGINS),
|
||||
[ViolationTypes.CONCURRENT]: violationCache(ViolationTypes.CONCURRENT),
|
||||
[ViolationTypes.NON_BROWSER]: violationCache(ViolationTypes.NON_BROWSER),
|
||||
[ViolationTypes.MESSAGE_LIMIT]: violationCache(ViolationTypes.MESSAGE_LIMIT),
|
||||
[ViolationTypes.REGISTRATIONS]: violationCache(ViolationTypes.REGISTRATIONS),
|
||||
[ViolationTypes.TOKEN_BALANCE]: violationCache(ViolationTypes.TOKEN_BALANCE),
|
||||
[ViolationTypes.TTS_LIMIT]: violationCache(ViolationTypes.TTS_LIMIT),
|
||||
[ViolationTypes.STT_LIMIT]: violationCache(ViolationTypes.STT_LIMIT),
|
||||
[ViolationTypes.CONVO_ACCESS]: violationCache(ViolationTypes.CONVO_ACCESS),
|
||||
[ViolationTypes.TOOL_CALL_LIMIT]: violationCache(ViolationTypes.TOOL_CALL_LIMIT),
|
||||
[ViolationTypes.FILE_UPLOAD_LIMIT]: violationCache(ViolationTypes.FILE_UPLOAD_LIMIT),
|
||||
[ViolationTypes.VERIFY_EMAIL_LIMIT]: violationCache(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
||||
[ViolationTypes.RESET_PASSWORD_LIMIT]: violationCache(ViolationTypes.RESET_PASSWORD_LIMIT),
|
||||
[ViolationTypes.ILLEGAL_MODEL_REQUEST]: violationCache(ViolationTypes.ILLEGAL_MODEL_REQUEST),
|
||||
[ViolationTypes.BAN]: new Keyv({
|
||||
store: keyvMongo,
|
||||
namespace: CacheKeys.ENCODED_DOMAINS,
|
||||
ttl: 0,
|
||||
namespace: CacheKeys.BANS,
|
||||
ttl: cacheConfig.BAN_DURATION,
|
||||
}),
|
||||
general: new Keyv({ store: logFile, namespace: 'violations' }),
|
||||
concurrent: createViolationInstance('concurrent'),
|
||||
non_browser: createViolationInstance('non_browser'),
|
||||
message_limit: createViolationInstance('message_limit'),
|
||||
token_balance: createViolationInstance(ViolationTypes.TOKEN_BALANCE),
|
||||
registrations: createViolationInstance('registrations'),
|
||||
[ViolationTypes.TTS_LIMIT]: createViolationInstance(ViolationTypes.TTS_LIMIT),
|
||||
[ViolationTypes.STT_LIMIT]: createViolationInstance(ViolationTypes.STT_LIMIT),
|
||||
[ViolationTypes.CONVO_ACCESS]: createViolationInstance(ViolationTypes.CONVO_ACCESS),
|
||||
[ViolationTypes.TOOL_CALL_LIMIT]: createViolationInstance(ViolationTypes.TOOL_CALL_LIMIT),
|
||||
[ViolationTypes.FILE_UPLOAD_LIMIT]: createViolationInstance(ViolationTypes.FILE_UPLOAD_LIMIT),
|
||||
[ViolationTypes.VERIFY_EMAIL_LIMIT]: createViolationInstance(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
||||
[ViolationTypes.RESET_PASSWORD_LIMIT]: createViolationInstance(
|
||||
ViolationTypes.RESET_PASSWORD_LIMIT,
|
||||
|
||||
[CacheKeys.OPENID_SESSION]: sessionCache(CacheKeys.OPENID_SESSION),
|
||||
[CacheKeys.SAML_SESSION]: sessionCache(CacheKeys.SAML_SESSION),
|
||||
|
||||
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
|
||||
[CacheKeys.MCP_TOOLS]: standardCache(CacheKeys.MCP_TOOLS),
|
||||
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
|
||||
[CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ),
|
||||
[CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }),
|
||||
[CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES),
|
||||
[CacheKeys.TOKEN_CONFIG]: standardCache(CacheKeys.TOKEN_CONFIG, Time.THIRTY_MINUTES),
|
||||
[CacheKeys.GEN_TITLE]: standardCache(CacheKeys.GEN_TITLE, Time.TWO_MINUTES),
|
||||
[CacheKeys.S3_EXPIRY_INTERVAL]: standardCache(CacheKeys.S3_EXPIRY_INTERVAL, Time.THIRTY_MINUTES),
|
||||
[CacheKeys.MODEL_QUERIES]: standardCache(CacheKeys.MODEL_QUERIES),
|
||||
[CacheKeys.AUDIO_RUNS]: standardCache(CacheKeys.AUDIO_RUNS, Time.TEN_MINUTES),
|
||||
[CacheKeys.MESSAGES]: standardCache(CacheKeys.MESSAGES, Time.ONE_MINUTE),
|
||||
[CacheKeys.FLOWS]: standardCache(CacheKeys.FLOWS, Time.ONE_MINUTE * 3),
|
||||
[CacheKeys.OPENID_EXCHANGED_TOKENS]: standardCache(
|
||||
CacheKeys.OPENID_EXCHANGED_TOKENS,
|
||||
Time.TEN_MINUTES,
|
||||
),
|
||||
[ViolationTypes.ILLEGAL_MODEL_REQUEST]: createViolationInstance(
|
||||
ViolationTypes.ILLEGAL_MODEL_REQUEST,
|
||||
),
|
||||
logins: createViolationInstance('logins'),
|
||||
[CacheKeys.ABORT_KEYS]: abortKeys,
|
||||
[CacheKeys.TOKEN_CONFIG]: tokenConfig,
|
||||
[CacheKeys.GEN_TITLE]: genTitle,
|
||||
[CacheKeys.S3_EXPIRY_INTERVAL]: s3ExpiryInterval,
|
||||
[CacheKeys.MODEL_QUERIES]: modelQueries,
|
||||
[CacheKeys.AUDIO_RUNS]: audioRuns,
|
||||
[CacheKeys.MESSAGES]: messages,
|
||||
[CacheKeys.FLOWS]: flows,
|
||||
[CacheKeys.OPENID_EXCHANGED_TOKENS]: openIdExchangedTokensCache,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -116,7 +55,10 @@ const namespaces = {
|
|||
*/
|
||||
function getTTLStores() {
|
||||
return Object.values(namespaces).filter(
|
||||
(store) => store instanceof Keyv && typeof store.opts?.ttl === 'number' && store.opts.ttl > 0,
|
||||
(store) =>
|
||||
store instanceof Keyv &&
|
||||
parseInt(store.opts?.ttl ?? '0') > 0 &&
|
||||
!store.opts?.store?.constructor?.name?.includes('Redis'), // Only include non-Redis stores
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -152,18 +94,18 @@ async function clearExpiredFromCache(cache) {
|
|||
if (data?.expires && data.expires <= expiryTime) {
|
||||
const deleted = await cache.opts.store.delete(key);
|
||||
if (!deleted) {
|
||||
debugMemoryCache &&
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`);
|
||||
continue;
|
||||
}
|
||||
cleared++;
|
||||
}
|
||||
} catch (error) {
|
||||
debugMemoryCache &&
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.log(`[Cache] Error processing entry from ${cache.opts.namespace}:`, error);
|
||||
const deleted = await cache.opts.store.delete(key);
|
||||
if (!deleted) {
|
||||
debugMemoryCache &&
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`);
|
||||
continue;
|
||||
}
|
||||
|
@ -172,7 +114,7 @@ async function clearExpiredFromCache(cache) {
|
|||
}
|
||||
|
||||
if (cleared > 0) {
|
||||
debugMemoryCache &&
|
||||
cacheConfig.DEBUG_MEMORY_CACHE &&
|
||||
console.log(
|
||||
`[Cache] Cleared ${cleared} entries older than ${ttl}ms from ${cache.opts.namespace}`,
|
||||
);
|
||||
|
@ -213,7 +155,7 @@ async function clearAllExpiredFromCache() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!isRedisEnabled && !isEnabled(CI)) {
|
||||
if (!cacheConfig.USE_REDIS && !cacheConfig.CI) {
|
||||
/** @type {Set<NodeJS.Timeout>} */
|
||||
const cleanupIntervals = new Set();
|
||||
|
||||
|
@ -224,7 +166,7 @@ if (!isRedisEnabled && !isEnabled(CI)) {
|
|||
|
||||
cleanupIntervals.add(cleanup);
|
||||
|
||||
if (debugMemoryCache) {
|
||||
if (cacheConfig.DEBUG_MEMORY_CACHE) {
|
||||
const monitor = setInterval(() => {
|
||||
const ttlStores = getTTLStores();
|
||||
const memory = process.memoryUsage();
|
||||
|
@ -245,13 +187,13 @@ if (!isRedisEnabled && !isEnabled(CI)) {
|
|||
}
|
||||
|
||||
const dispose = () => {
|
||||
debugMemoryCache && console.log('[Cache] Cleaning up and shutting down...');
|
||||
cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Cleaning up and shutting down...');
|
||||
cleanupIntervals.forEach((interval) => clearInterval(interval));
|
||||
cleanupIntervals.clear();
|
||||
|
||||
// One final cleanup before exit
|
||||
clearAllExpiredFromCache().then(() => {
|
||||
debugMemoryCache && console.log('[Cache] Final cleanup completed');
|
||||
cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Final cleanup completed');
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
|
92
api/cache/ioredisClient.js
vendored
92
api/cache/ioredisClient.js
vendored
|
@ -1,92 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const Redis = require('ioredis');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const logger = require('~/config/winston');
|
||||
|
||||
const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_MAX_LISTENERS } = process.env;
|
||||
|
||||
/** @type {import('ioredis').Redis | import('ioredis').Cluster} */
|
||||
let ioredisClient;
|
||||
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 40;
|
||||
|
||||
function mapURI(uri) {
|
||||
const regex =
|
||||
/^(?:(?<scheme>\w+):\/\/)?(?:(?<user>[^:@]+)(?::(?<password>[^@]+))?@)?(?<host>[\w.-]+)(?::(?<port>\d{1,5}))?$/;
|
||||
const match = uri.match(regex);
|
||||
|
||||
if (match) {
|
||||
const { scheme, user, password, host, port } = match.groups;
|
||||
|
||||
return {
|
||||
scheme: scheme || 'none',
|
||||
user: user || null,
|
||||
password: password || null,
|
||||
host: host || null,
|
||||
port: port || null,
|
||||
};
|
||||
} else {
|
||||
const parts = uri.split(':');
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
scheme: 'none',
|
||||
user: null,
|
||||
password: null,
|
||||
host: parts[0],
|
||||
port: parts[1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scheme: 'none',
|
||||
user: null,
|
||||
password: null,
|
||||
host: uri,
|
||||
port: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (REDIS_URI && isEnabled(USE_REDIS)) {
|
||||
let redisOptions = null;
|
||||
|
||||
if (REDIS_CA) {
|
||||
const ca = fs.readFileSync(REDIS_CA);
|
||||
redisOptions = { tls: { ca } };
|
||||
}
|
||||
|
||||
if (isEnabled(USE_REDIS_CLUSTER)) {
|
||||
const hosts = REDIS_URI.split(',').map((item) => {
|
||||
var value = mapURI(item);
|
||||
|
||||
return {
|
||||
host: value.host,
|
||||
port: value.port,
|
||||
};
|
||||
});
|
||||
ioredisClient = new Redis.Cluster(hosts, { redisOptions });
|
||||
} else {
|
||||
ioredisClient = new Redis(REDIS_URI, redisOptions);
|
||||
}
|
||||
|
||||
ioredisClient.on('ready', () => {
|
||||
logger.info('IoRedis connection ready');
|
||||
});
|
||||
ioredisClient.on('reconnecting', () => {
|
||||
logger.info('IoRedis connection reconnecting');
|
||||
});
|
||||
ioredisClient.on('end', () => {
|
||||
logger.info('IoRedis connection ended');
|
||||
});
|
||||
ioredisClient.on('close', () => {
|
||||
logger.info('IoRedis connection closed');
|
||||
});
|
||||
ioredisClient.on('error', (err) => logger.error('IoRedis connection error:', err));
|
||||
ioredisClient.setMaxListeners(redis_max_listeners);
|
||||
logger.info(
|
||||
'[Optional] IoRedis initialized for rate limiters. If you have issues, disable Redis or restart the server.',
|
||||
);
|
||||
} else {
|
||||
logger.info('[Optional] IoRedis not initialized for rate limiters.');
|
||||
}
|
||||
|
||||
module.exports = ioredisClient;
|
109
api/cache/keyvRedis.js
vendored
109
api/cache/keyvRedis.js
vendored
|
@ -1,109 +0,0 @@
|
|||
const fs = require('fs');
|
||||
const ioredis = require('ioredis');
|
||||
const KeyvRedis = require('@keyv/redis').default;
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const logger = require('~/config/winston');
|
||||
|
||||
const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, REDIS_MAX_LISTENERS } =
|
||||
process.env;
|
||||
|
||||
let keyvRedis;
|
||||
const redis_prefix = REDIS_KEY_PREFIX || '';
|
||||
const redis_max_listeners = Number(REDIS_MAX_LISTENERS) || 40;
|
||||
|
||||
function mapURI(uri) {
|
||||
const regex =
|
||||
/^(?:(?<scheme>\w+):\/\/)?(?:(?<user>[^:@]+)(?::(?<password>[^@]+))?@)?(?<host>[\w.-]+)(?::(?<port>\d{1,5}))?$/;
|
||||
const match = uri.match(regex);
|
||||
|
||||
if (match) {
|
||||
const { scheme, user, password, host, port } = match.groups;
|
||||
|
||||
return {
|
||||
scheme: scheme || 'none',
|
||||
user: user || null,
|
||||
password: password || null,
|
||||
host: host || null,
|
||||
port: port || null,
|
||||
};
|
||||
} else {
|
||||
const parts = uri.split(':');
|
||||
if (parts.length === 2) {
|
||||
return {
|
||||
scheme: 'none',
|
||||
user: null,
|
||||
password: null,
|
||||
host: parts[0],
|
||||
port: parts[1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scheme: 'none',
|
||||
user: null,
|
||||
password: null,
|
||||
host: uri,
|
||||
port: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (REDIS_URI && isEnabled(USE_REDIS)) {
|
||||
let redisOptions = null;
|
||||
/** @type {import('@keyv/redis').KeyvRedisOptions} */
|
||||
let keyvOpts = {
|
||||
useRedisSets: false,
|
||||
keyPrefix: redis_prefix,
|
||||
};
|
||||
|
||||
if (REDIS_CA) {
|
||||
const ca = fs.readFileSync(REDIS_CA);
|
||||
redisOptions = { tls: { ca } };
|
||||
}
|
||||
|
||||
if (isEnabled(USE_REDIS_CLUSTER)) {
|
||||
const hosts = REDIS_URI.split(',').map((item) => {
|
||||
var value = mapURI(item);
|
||||
|
||||
return {
|
||||
host: value.host,
|
||||
port: value.port,
|
||||
};
|
||||
});
|
||||
const cluster = new ioredis.Cluster(hosts, { redisOptions });
|
||||
keyvRedis = new KeyvRedis(cluster, keyvOpts);
|
||||
} else {
|
||||
keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts);
|
||||
}
|
||||
|
||||
const pingInterval = setInterval(
|
||||
() => {
|
||||
logger.debug('KeyvRedis ping');
|
||||
keyvRedis.client.ping().catch((err) => logger.error('Redis keep-alive ping failed:', err));
|
||||
},
|
||||
5 * 60 * 1000,
|
||||
);
|
||||
|
||||
keyvRedis.on('ready', () => {
|
||||
logger.info('KeyvRedis connection ready');
|
||||
});
|
||||
keyvRedis.on('reconnecting', () => {
|
||||
logger.info('KeyvRedis connection reconnecting');
|
||||
});
|
||||
keyvRedis.on('end', () => {
|
||||
logger.info('KeyvRedis connection ended');
|
||||
});
|
||||
keyvRedis.on('close', () => {
|
||||
clearInterval(pingInterval);
|
||||
logger.info('KeyvRedis connection closed');
|
||||
});
|
||||
keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err));
|
||||
keyvRedis.setMaxListeners(redis_max_listeners);
|
||||
logger.info(
|
||||
'[Optional] Redis initialized. If you have issues, or seeing older values, disable it or flush cache to refresh values.',
|
||||
);
|
||||
} else {
|
||||
logger.info('[Optional] Redis not initialized.');
|
||||
}
|
||||
|
||||
module.exports = keyvRedis;
|
3
api/cache/logViolation.js
vendored
3
api/cache/logViolation.js
vendored
|
@ -1,4 +1,5 @@
|
|||
const { isEnabled } = require('~/server/utils');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const getLogStores = require('./getLogStores');
|
||||
const banViolation = require('./banViolation');
|
||||
|
||||
|
@ -16,7 +17,7 @@ const logViolation = async (req, res, type, errorMessage, score = 1) => {
|
|||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
const logs = getLogStores('general');
|
||||
const logs = getLogStores(ViolationTypes.GENERAL);
|
||||
const violationLogs = getLogStores(type);
|
||||
const key = isEnabled(process.env.USE_REDIS) ? `${type}:${userId}` : userId;
|
||||
|
||||
|
|
57
api/cache/redisClients.js
vendored
Normal file
57
api/cache/redisClients.js
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
const IoRedis = require('ioredis');
|
||||
const { cacheConfig } = require('./cacheConfig');
|
||||
const { createClient, createCluster } = require('@keyv/redis');
|
||||
|
||||
const GLOBAL_PREFIX_SEPARATOR = '::';
|
||||
|
||||
const urls = cacheConfig.REDIS_URI?.split(',').map((uri) => new URL(uri));
|
||||
const username = urls?.[0].username || cacheConfig.REDIS_USERNAME;
|
||||
const password = urls?.[0].password || cacheConfig.REDIS_PASSWORD;
|
||||
const ca = cacheConfig.REDIS_CA;
|
||||
|
||||
/** @type {import('ioredis').Redis | import('ioredis').Cluster | null} */
|
||||
let ioredisClient = null;
|
||||
if (cacheConfig.USE_REDIS) {
|
||||
const redisOptions = {
|
||||
username: username,
|
||||
password: password,
|
||||
tls: ca ? { ca } : undefined,
|
||||
keyPrefix: `${cacheConfig.REDIS_KEY_PREFIX}${GLOBAL_PREFIX_SEPARATOR}`,
|
||||
maxListeners: cacheConfig.REDIS_MAX_LISTENERS,
|
||||
};
|
||||
|
||||
ioredisClient =
|
||||
urls.length === 1
|
||||
? new IoRedis(cacheConfig.REDIS_URI, redisOptions)
|
||||
: new IoRedis.Cluster(cacheConfig.REDIS_URI, { redisOptions });
|
||||
|
||||
// Pinging the Redis server every 5 minutes to keep the connection alive
|
||||
const pingInterval = setInterval(() => ioredisClient.ping(), 5 * 60 * 1000);
|
||||
ioredisClient.on('close', () => clearInterval(pingInterval));
|
||||
ioredisClient.on('end', () => clearInterval(pingInterval));
|
||||
}
|
||||
|
||||
/** @type {import('@keyv/redis').RedisClient | import('@keyv/redis').RedisCluster | null} */
|
||||
let keyvRedisClient = null;
|
||||
if (cacheConfig.USE_REDIS) {
|
||||
// ** WARNING ** Keyv Redis client does not support Prefix like ioredis above.
|
||||
// The prefix feature will be handled by the Keyv-Redis store in cacheFactory.js
|
||||
const redisOptions = { username, password, socket: { tls: ca != null, ca } };
|
||||
|
||||
keyvRedisClient =
|
||||
urls.length === 1
|
||||
? createClient({ url: cacheConfig.REDIS_URI, ...redisOptions })
|
||||
: createCluster({
|
||||
rootNodes: cacheConfig.REDIS_URI.split(',').map((url) => ({ url })),
|
||||
defaults: redisOptions,
|
||||
});
|
||||
|
||||
keyvRedisClient.setMaxListeners(cacheConfig.REDIS_MAX_LISTENERS);
|
||||
|
||||
// Pinging the Redis server every 5 minutes to keep the connection alive
|
||||
const keyvPingInterval = setInterval(() => keyvRedisClient.ping(), 5 * 60 * 1000);
|
||||
keyvRedisClient.on('disconnect', () => clearInterval(keyvPingInterval));
|
||||
keyvRedisClient.on('end', () => clearInterval(keyvPingInterval));
|
||||
}
|
||||
|
||||
module.exports = { ioredisClient, keyvRedisClient, GLOBAL_PREFIX_SEPARATOR };
|
|
@ -1,4 +1,4 @@
|
|||
const { Time, CacheKeys } = require('librechat-data-provider');
|
||||
const { Time, CacheKeys, ViolationTypes } = require('librechat-data-provider');
|
||||
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||
const { logViolation, getLogStores } = require('~/cache');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
|
@ -37,7 +37,7 @@ const concurrentLimiter = async (req, res, next) => {
|
|||
|
||||
const userId = req.user?.id ?? req.user?._id ?? '';
|
||||
const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1);
|
||||
const type = 'concurrent';
|
||||
const type = ViolationTypes.CONCURRENT;
|
||||
|
||||
const key = `${isEnabled(USE_REDIS) ? namespace : ''}:${userId}`;
|
||||
const pendingRequests = +((await cache.get(key)) ?? 0);
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
|
||||
const getEnvironmentVariables = () => {
|
||||
|
@ -62,6 +59,7 @@ const createForkLimiters = () => {
|
|||
windowMs: forkIpWindowMs,
|
||||
max: forkIpMax,
|
||||
handler: createForkHandler(),
|
||||
store: limiterCache('fork_ip_limiter'),
|
||||
};
|
||||
const userLimiterOptions = {
|
||||
windowMs: forkUserWindowMs,
|
||||
|
@ -70,23 +68,9 @@ const createForkLimiters = () => {
|
|||
keyGenerator: function (req) {
|
||||
return req.user?.id;
|
||||
},
|
||||
store: limiterCache('fork_user_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for fork rate limiters.');
|
||||
const sendCommand = (...args) => ioredisClient.call(...args);
|
||||
const ipStore = new RedisStore({
|
||||
sendCommand,
|
||||
prefix: 'fork_ip_limiter:',
|
||||
});
|
||||
const userStore = new RedisStore({
|
||||
sendCommand,
|
||||
prefix: 'fork_user_limiter:',
|
||||
});
|
||||
ipLimiterOptions.store = ipStore;
|
||||
userLimiterOptions.store = userStore;
|
||||
}
|
||||
|
||||
const forkIpLimiter = rateLimit(ipLimiterOptions);
|
||||
const forkUserLimiter = rateLimit(userLimiterOptions);
|
||||
return { forkIpLimiter, forkUserLimiter };
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
|
||||
const getEnvironmentVariables = () => {
|
||||
|
@ -63,6 +60,7 @@ const createImportLimiters = () => {
|
|||
windowMs: importIpWindowMs,
|
||||
max: importIpMax,
|
||||
handler: createImportHandler(),
|
||||
store: limiterCache('import_ip_limiter'),
|
||||
};
|
||||
const userLimiterOptions = {
|
||||
windowMs: importUserWindowMs,
|
||||
|
@ -71,23 +69,9 @@ const createImportLimiters = () => {
|
|||
keyGenerator: function (req) {
|
||||
return req.user?.id; // Use the user ID or NULL if not available
|
||||
},
|
||||
store: limiterCache('import_user_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for import rate limiters.');
|
||||
const sendCommand = (...args) => ioredisClient.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 };
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { removePorts, isEnabled } = require('~/server/utils');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const { logViolation } = require('~/cache');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { LOGIN_WINDOW = 5, LOGIN_MAX = 7, LOGIN_VIOLATION_SCORE: score } = process.env;
|
||||
const windowMs = LOGIN_WINDOW * 60 * 1000;
|
||||
|
@ -12,7 +11,7 @@ const windowInMinutes = windowMs / 60000;
|
|||
const message = `Too many login attempts, please try again after ${windowInMinutes} minutes.`;
|
||||
|
||||
const handler = async (req, res) => {
|
||||
const type = 'logins';
|
||||
const type = ViolationTypes.LOGINS;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max,
|
||||
|
@ -28,17 +27,9 @@ const limiterOptions = {
|
|||
max,
|
||||
handler,
|
||||
keyGenerator: removePorts,
|
||||
store: limiterCache('login_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for login rate limiter.');
|
||||
const store = new RedisStore({
|
||||
sendCommand: (...args) => ioredisClient.call(...args),
|
||||
prefix: 'login_limiter:',
|
||||
});
|
||||
limiterOptions.store = store;
|
||||
}
|
||||
|
||||
const loginLimiter = rateLimit(limiterOptions);
|
||||
|
||||
module.exports = loginLimiter;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const denyRequest = require('~/server/middleware/denyRequest');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const { logViolation } = require('~/cache');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const {
|
||||
MESSAGE_IP_MAX = 40,
|
||||
|
@ -32,7 +30,7 @@ const userWindowInMinutes = userWindowMs / 60000;
|
|||
*/
|
||||
const createHandler = (ip = true) => {
|
||||
return async (req, res) => {
|
||||
const type = 'message_limit';
|
||||
const type = ViolationTypes.MESSAGE_LIMIT;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max: ip ? ipMax : userMax,
|
||||
|
@ -52,6 +50,7 @@ const ipLimiterOptions = {
|
|||
windowMs: ipWindowMs,
|
||||
max: ipMax,
|
||||
handler: createHandler(),
|
||||
store: limiterCache('message_ip_limiter'),
|
||||
};
|
||||
|
||||
const userLimiterOptions = {
|
||||
|
@ -61,23 +60,9 @@ const userLimiterOptions = {
|
|||
keyGenerator: function (req) {
|
||||
return req.user?.id; // Use the user ID or NULL if not available
|
||||
},
|
||||
store: limiterCache('message_user_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for message rate limiters.');
|
||||
const sendCommand = (...args) => ioredisClient.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
|
||||
*/
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { removePorts, isEnabled } = require('~/server/utils');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const { logViolation } = require('~/cache');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { REGISTER_WINDOW = 60, REGISTER_MAX = 5, REGISTRATION_VIOLATION_SCORE: score } = process.env;
|
||||
const windowMs = REGISTER_WINDOW * 60 * 1000;
|
||||
|
@ -12,7 +11,7 @@ const windowInMinutes = windowMs / 60000;
|
|||
const message = `Too many accounts created, please try again after ${windowInMinutes} minutes`;
|
||||
|
||||
const handler = async (req, res) => {
|
||||
const type = 'registrations';
|
||||
const type = ViolationTypes.REGISTRATIONS;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max,
|
||||
|
@ -28,17 +27,9 @@ const limiterOptions = {
|
|||
max,
|
||||
handler,
|
||||
keyGenerator: removePorts,
|
||||
store: limiterCache('register_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for register rate limiter.');
|
||||
const store = new RedisStore({
|
||||
sendCommand: (...args) => ioredisClient.call(...args),
|
||||
prefix: 'register_limiter:',
|
||||
});
|
||||
limiterOptions.store = store;
|
||||
}
|
||||
|
||||
const registerLimiter = rateLimit(limiterOptions);
|
||||
|
||||
module.exports = registerLimiter;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { removePorts, isEnabled } = require('~/server/utils');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const { logViolation } = require('~/cache');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const {
|
||||
RESET_PASSWORD_WINDOW = 2,
|
||||
|
@ -33,17 +31,9 @@ const limiterOptions = {
|
|||
max,
|
||||
handler,
|
||||
keyGenerator: removePorts,
|
||||
store: limiterCache('reset_password_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for reset password rate limiter.');
|
||||
const store = new RedisStore({
|
||||
sendCommand: (...args) => ioredisClient.call(...args),
|
||||
prefix: 'reset_password_limiter:',
|
||||
});
|
||||
limiterOptions.store = store;
|
||||
}
|
||||
|
||||
const resetPasswordLimiter = rateLimit(limiterOptions);
|
||||
|
||||
module.exports = resetPasswordLimiter;
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const getEnvironmentVariables = () => {
|
||||
const STT_IP_MAX = parseInt(process.env.STT_IP_MAX) || 100;
|
||||
|
@ -57,6 +54,7 @@ const createSTTLimiters = () => {
|
|||
windowMs: sttIpWindowMs,
|
||||
max: sttIpMax,
|
||||
handler: createSTTHandler(),
|
||||
store: limiterCache('stt_ip_limiter'),
|
||||
};
|
||||
|
||||
const userLimiterOptions = {
|
||||
|
@ -66,23 +64,9 @@ const createSTTLimiters = () => {
|
|||
keyGenerator: function (req) {
|
||||
return req.user?.id; // Use the user ID or NULL if not available
|
||||
},
|
||||
store: limiterCache('stt_user_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for STT rate limiters.');
|
||||
const sendCommand = (...args) => ioredisClient.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);
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const { TOOL_CALL_VIOLATION_SCORE: score } = process.env;
|
||||
|
||||
|
@ -28,17 +25,9 @@ const limiterOptions = {
|
|||
keyGenerator: function (req) {
|
||||
return req.user?.id;
|
||||
},
|
||||
store: limiterCache('tool_call_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for tool call rate limiter.');
|
||||
const store = new RedisStore({
|
||||
sendCommand: (...args) => ioredisClient.call(...args),
|
||||
prefix: 'tool_call_limiter:',
|
||||
});
|
||||
limiterOptions.store = store;
|
||||
}
|
||||
|
||||
const toolCallLimiter = rateLimit(limiterOptions);
|
||||
|
||||
module.exports = toolCallLimiter;
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
|
||||
const getEnvironmentVariables = () => {
|
||||
const TTS_IP_MAX = parseInt(process.env.TTS_IP_MAX) || 100;
|
||||
|
@ -57,32 +54,19 @@ const createTTSLimiters = () => {
|
|||
windowMs: ttsIpWindowMs,
|
||||
max: ttsIpMax,
|
||||
handler: createTTSHandler(),
|
||||
store: limiterCache('tts_ip_limiter'),
|
||||
};
|
||||
|
||||
const userLimiterOptions = {
|
||||
windowMs: ttsUserWindowMs,
|
||||
max: ttsUserMax,
|
||||
handler: createTTSHandler(false),
|
||||
store: limiterCache('tts_user_limiter'),
|
||||
keyGenerator: function (req) {
|
||||
return req.user?.id; // Use the user ID or NULL if not available
|
||||
},
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for TTS rate limiters.');
|
||||
const sendCommand = (...args) => ioredisClient.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);
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const logViolation = require('~/cache/logViolation');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const getEnvironmentVariables = () => {
|
||||
const FILE_UPLOAD_IP_MAX = parseInt(process.env.FILE_UPLOAD_IP_MAX) || 100;
|
||||
|
@ -63,6 +60,7 @@ const createFileLimiters = () => {
|
|||
windowMs: fileUploadIpWindowMs,
|
||||
max: fileUploadIpMax,
|
||||
handler: createFileUploadHandler(),
|
||||
store: limiterCache('file_upload_ip_limiter'),
|
||||
};
|
||||
|
||||
const userLimiterOptions = {
|
||||
|
@ -72,23 +70,9 @@ const createFileLimiters = () => {
|
|||
keyGenerator: function (req) {
|
||||
return req.user?.id; // Use the user ID or NULL if not available
|
||||
},
|
||||
store: limiterCache('file_upload_user_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for file upload rate limiters.');
|
||||
const sendCommand = (...args) => ioredisClient.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);
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
const rateLimit = require('express-rate-limit');
|
||||
const { RedisStore } = require('rate-limit-redis');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { removePorts, isEnabled } = require('~/server/utils');
|
||||
const ioredisClient = require('~/cache/ioredisClient');
|
||||
const { removePorts } = require('~/server/utils');
|
||||
const { limiterCache } = require('~/cache/cacheFactory');
|
||||
const { logViolation } = require('~/cache');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
const {
|
||||
VERIFY_EMAIL_WINDOW = 2,
|
||||
|
@ -33,17 +31,9 @@ const limiterOptions = {
|
|||
max,
|
||||
handler,
|
||||
keyGenerator: removePorts,
|
||||
store: limiterCache('verify_email_limiter'),
|
||||
};
|
||||
|
||||
if (isEnabled(process.env.USE_REDIS) && ioredisClient) {
|
||||
logger.debug('Using Redis for verify email rate limiter.');
|
||||
const store = new RedisStore({
|
||||
sendCommand: (...args) => ioredisClient.call(...args),
|
||||
prefix: 'verify_email_limiter:',
|
||||
});
|
||||
limiterOptions.store = store;
|
||||
}
|
||||
|
||||
const verifyEmailLimiter = rateLimit(limiterOptions);
|
||||
|
||||
module.exports = verifyEmailLimiter;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const uap = require('ua-parser-js');
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
const { handleError } = require('@librechat/api');
|
||||
const { logViolation } = require('../../cache');
|
||||
|
||||
|
@ -21,7 +22,7 @@ async function uaParser(req, res, next) {
|
|||
const ua = uap(req.headers['user-agent']);
|
||||
|
||||
if (!ua.browser.name) {
|
||||
const type = 'non_browser';
|
||||
const type = ViolationTypes.NON_BROWSER;
|
||||
await logViolation(req, res, type, { type }, score);
|
||||
return handleError(res, { message: 'Illegal request' });
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
const { Keyv } = require('keyv');
|
||||
const passport = require('passport');
|
||||
const session = require('express-session');
|
||||
const MemoryStore = require('memorystore')(session);
|
||||
const RedisStore = require('connect-redis').default;
|
||||
const {
|
||||
setupOpenId,
|
||||
googleLogin,
|
||||
|
@ -14,8 +11,9 @@ const {
|
|||
openIdJwtLogin,
|
||||
} = require('~/strategies');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const keyvRedis = require('~/cache/keyvRedis');
|
||||
const { logger } = require('~/config');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const { CacheKeys } = require('librechat-data-provider');
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -51,17 +49,8 @@ const configureSocialLogins = async (app) => {
|
|||
secret: process.env.OPENID_SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: getLogStores(CacheKeys.OPENID_SESSION),
|
||||
};
|
||||
if (isEnabled(process.env.USE_REDIS)) {
|
||||
logger.debug('Using Redis for session storage in OpenID...');
|
||||
const keyv = new Keyv({ store: keyvRedis });
|
||||
const client = keyv.opts.store.client;
|
||||
sessionOptions.store = new RedisStore({ client, prefix: 'openid_session' });
|
||||
} else {
|
||||
sessionOptions.store = new MemoryStore({
|
||||
checkPeriod: 86400000, // prune expired entries every 24h
|
||||
});
|
||||
}
|
||||
app.use(session(sessionOptions));
|
||||
app.use(passport.session());
|
||||
const config = await setupOpenId();
|
||||
|
@ -82,17 +71,8 @@ const configureSocialLogins = async (app) => {
|
|||
secret: process.env.SAML_SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: getLogStores(CacheKeys.SAML_SESSION),
|
||||
};
|
||||
if (isEnabled(process.env.USE_REDIS)) {
|
||||
logger.debug('Using Redis for session storage in SAML...');
|
||||
const keyv = new Keyv({ store: keyvRedis });
|
||||
const client = keyv.opts.store.client;
|
||||
sessionOptions.store = new RedisStore({ client, prefix: 'saml_session' });
|
||||
} else {
|
||||
sessionOptions.store = new MemoryStore({
|
||||
checkPeriod: 86400000, // prune expired entries every 24h
|
||||
});
|
||||
}
|
||||
app.use(session(sessionOptions));
|
||||
app.use(passport.session());
|
||||
setupSaml();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
const path = require('path');
|
||||
const mongoose = require(path.resolve(__dirname, '..', 'api', 'node_modules', 'mongoose'));
|
||||
const { User } = require('@librechat/data-schemas').createModels(mongoose);
|
||||
const { ViolationTypes } = require('librechat-data-provider');
|
||||
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
|
||||
const { askQuestion, silentExit } = require('./helpers');
|
||||
const banViolation = require('~/cache/banViolation');
|
||||
|
@ -65,7 +66,7 @@ const connect = require('./connect');
|
|||
};
|
||||
|
||||
const errorMessage = {
|
||||
type: 'concurrent',
|
||||
type: ViolationTypes.CONCURRENT,
|
||||
violation_count: 20,
|
||||
user_id: user._id,
|
||||
prev_count: 0,
|
||||
|
|
|
@ -1064,10 +1064,12 @@ export enum InfiniteCollections {
|
|||
* Enum for time intervals
|
||||
*/
|
||||
export enum Time {
|
||||
ONE_DAY = 86400000,
|
||||
ONE_HOUR = 3600000,
|
||||
THIRTY_MINUTES = 1800000,
|
||||
TEN_MINUTES = 600000,
|
||||
FIVE_MINUTES = 300000,
|
||||
THREE_MINUTES = 180000,
|
||||
TWO_MINUTES = 120000,
|
||||
ONE_MINUTE = 60000,
|
||||
THIRTY_SECONDS = 30000,
|
||||
|
@ -1167,6 +1169,14 @@ export enum CacheKeys {
|
|||
* key for open id exchanged tokens
|
||||
*/
|
||||
OPENID_EXCHANGED_TOKENS = 'OPENID_EXCHANGED_TOKENS',
|
||||
/**
|
||||
* Key for OpenID session.
|
||||
*/
|
||||
OPENID_SESSION = 'openid_session',
|
||||
/**
|
||||
* Key for SAML session.
|
||||
*/
|
||||
SAML_SESSION = 'saml_session',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1213,6 +1223,30 @@ export enum ViolationTypes {
|
|||
* Tool Call Limit Violation.
|
||||
*/
|
||||
TOOL_CALL_LIMIT = 'tool_call_limit',
|
||||
/**
|
||||
* General violation (catch-all).
|
||||
*/
|
||||
GENERAL = 'general',
|
||||
/**
|
||||
* Login attempt violations.
|
||||
*/
|
||||
LOGINS = 'logins',
|
||||
/**
|
||||
* Concurrent request violations.
|
||||
*/
|
||||
CONCURRENT = 'concurrent',
|
||||
/**
|
||||
* Non-browser access violations.
|
||||
*/
|
||||
NON_BROWSER = 'non_browser',
|
||||
/**
|
||||
* Message limit violations.
|
||||
*/
|
||||
MESSAGE_LIMIT = 'message_limit',
|
||||
/**
|
||||
* Registration violations.
|
||||
*/
|
||||
REGISTRATIONS = 'registrations',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
399
redis-config/README.md
Normal file
399
redis-config/README.md
Normal file
|
@ -0,0 +1,399 @@
|
|||
# Redis Configuration and Setup
|
||||
|
||||
This directory contains comprehensive Redis configuration files and scripts for LibreChat development and testing, supporting both cluster and single-node setups with optional TLS encryption.
|
||||
|
||||
## Supported Configurations
|
||||
|
||||
### 1. Redis Cluster (3 Nodes)
|
||||
- **3 Redis nodes** running on ports 7001, 7002, and 7003
|
||||
- **No replicas** (each node is a master)
|
||||
- **Automatic hash slot distribution** across all nodes
|
||||
|
||||
### 2. Single Redis with TLS Encryption
|
||||
- **Single Redis instance** on port 6380 with TLS encryption
|
||||
- **CA certificate validation** for secure connections
|
||||
- **Self-signed certificates** with proper Subject Alternative Names
|
||||
|
||||
### 3. Standard Single Redis
|
||||
- **Basic Redis instance** on port 6379 (default)
|
||||
- **No encryption** - suitable for local development
|
||||
|
||||
All configurations are designed for **local development and testing**.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Redis** must be installed on your system:
|
||||
```bash
|
||||
# macOS
|
||||
brew install redis
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install redis-server
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install redis
|
||||
```
|
||||
|
||||
2. **Redis CLI** should be available (usually included with Redis)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Redis Cluster (3 Nodes)
|
||||
|
||||
```bash
|
||||
# Navigate to the redis-config directory
|
||||
cd redis-config
|
||||
|
||||
# Start and initialize the cluster
|
||||
./start-cluster.sh
|
||||
```
|
||||
|
||||
### Option 2: Single Redis with TLS
|
||||
|
||||
```bash
|
||||
# Start Redis with TLS encryption on port 6380
|
||||
./start-redis-tls.sh
|
||||
```
|
||||
|
||||
### Option 3: Standard Redis
|
||||
|
||||
```bash
|
||||
# Use system Redis on default port 6379
|
||||
redis-server
|
||||
```
|
||||
|
||||
## Testing Your Setup
|
||||
|
||||
### Test Cluster
|
||||
```bash
|
||||
# Connect to the cluster
|
||||
redis-cli -c -p 7001
|
||||
|
||||
# Test basic operations
|
||||
SET test_key "Hello World"
|
||||
GET test_key
|
||||
```
|
||||
|
||||
### Test TLS Redis
|
||||
```bash
|
||||
# Test with CA certificate validation
|
||||
redis-cli --tls --cacert certs/ca-cert.pem -p 6380 ping
|
||||
```
|
||||
|
||||
### Test Standard Redis
|
||||
```bash
|
||||
# Connect to default Redis
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
## Stopping Services
|
||||
|
||||
### Stop Cluster
|
||||
```bash
|
||||
./stop-cluster.sh
|
||||
```
|
||||
|
||||
### Stop TLS Redis
|
||||
```bash
|
||||
# Find and stop TLS Redis process
|
||||
ps aux | grep "redis-server.*6380"
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- `redis-7001.conf` - Configuration for node 1 (port 7001)
|
||||
- `redis-7002.conf` - Configuration for node 2 (port 7002)
|
||||
- `redis-7003.conf` - Configuration for node 3 (port 7003)
|
||||
|
||||
## Scripts
|
||||
|
||||
- `start-cluster.sh` - Starts and initializes the Redis cluster
|
||||
- `stop-cluster.sh` - Stops all Redis nodes and cleans up
|
||||
- `start-redis-tls.sh` - Starts Redis with TLS encryption and CA certificate validation
|
||||
- `redis-tls.conf` - TLS Redis configuration file
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
redis-config/
|
||||
├── README.md
|
||||
├── redis-7001.conf # Cluster node 1 configuration
|
||||
├── redis-7002.conf # Cluster node 2 configuration
|
||||
├── redis-7003.conf # Cluster node 3 configuration
|
||||
├── redis-tls.conf # TLS Redis configuration
|
||||
├── start-cluster.sh # Start cluster script
|
||||
├── stop-cluster.sh # Stop cluster script
|
||||
├── start-redis-tls.sh # Start TLS Redis script
|
||||
├── certs/ # TLS certificates (created automatically)
|
||||
│ ├── ca-cert.pem # Certificate Authority certificate
|
||||
│ ├── ca-key.pem # CA private key
|
||||
│ ├── server-cert.pem # Server certificate with SAN
|
||||
│ ├── server-key.pem # Server private key
|
||||
│ ├── redis.dh # Diffie-Hellman parameters
|
||||
│ └── server.conf # OpenSSL certificate configuration
|
||||
├── data/ # Data files (created automatically)
|
||||
│ ├── 7001/ # Cluster node 1 data
|
||||
│ ├── 7002/ # Cluster node 2 data
|
||||
│ └── 7003/ # Cluster node 3 data
|
||||
└── logs/ # Log directory (created automatically)
|
||||
# Note: By default, Redis logs to stdout/stderr
|
||||
# Log files would be created here if enabled in config
|
||||
```
|
||||
|
||||
## Using with LibreChat
|
||||
|
||||
Update your `.env` file based on your chosen Redis configuration:
|
||||
|
||||
### For Redis Cluster
|
||||
```bash
|
||||
USE_REDIS=true
|
||||
REDIS_URI=redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
|
||||
```
|
||||
|
||||
### For TLS Redis
|
||||
```bash
|
||||
USE_REDIS=true
|
||||
REDIS_URI=rediss://127.0.0.1:6380
|
||||
REDIS_CA=/path/to/LibreChat/redis-config/certs/ca-cert.pem
|
||||
```
|
||||
|
||||
### For Standard Redis
|
||||
```bash
|
||||
USE_REDIS=true
|
||||
REDIS_URI=redis://127.0.0.1:6379
|
||||
```
|
||||
|
||||
### Optional Configuration
|
||||
```bash
|
||||
# Use environment variable for dynamic key prefixing
|
||||
REDIS_KEY_PREFIX_VAR=K_REVISION
|
||||
|
||||
# Or set static prefix
|
||||
REDIS_KEY_PREFIX=librechat
|
||||
|
||||
# Connection limits
|
||||
REDIS_MAX_LISTENERS=40
|
||||
```
|
||||
|
||||
## TLS/SSL Redis Setup
|
||||
|
||||
For secure Redis connections using TLS encryption with CA certificate validation:
|
||||
|
||||
### 1. Start Redis with TLS
|
||||
|
||||
```bash
|
||||
# Start Redis with TLS on port 6380
|
||||
./start-redis-tls.sh
|
||||
```
|
||||
|
||||
### 2. Configure LibreChat for TLS
|
||||
|
||||
Update your `.env` file:
|
||||
|
||||
```bash
|
||||
# .env file - TLS Redis with CA certificate validation
|
||||
USE_REDIS=true
|
||||
REDIS_URI=rediss://127.0.0.1:6380
|
||||
REDIS_CA=/path/to/LibreChat/redis-config/certs/ca-cert.pem
|
||||
```
|
||||
|
||||
### 3. Test TLS Connection
|
||||
|
||||
```bash
|
||||
# Test Redis TLS connection with CA certificate
|
||||
redis-cli --tls --cacert certs/ca-cert.pem -p 6380 ping
|
||||
|
||||
# Should return: PONG
|
||||
|
||||
# Test basic operations
|
||||
redis-cli --tls --cacert certs/ca-cert.pem -p 6380 set test_tls "TLS Working"
|
||||
redis-cli --tls --cacert certs/ca-cert.pem -p 6380 get test_tls
|
||||
```
|
||||
|
||||
### 4. Test Backend Integration
|
||||
|
||||
```bash
|
||||
# Start LibreChat backend
|
||||
npm run backend
|
||||
|
||||
# Look for these success indicators in logs:
|
||||
# ✅ "No changes needed for 'USER' role permissions"
|
||||
# ✅ "No changes needed for 'ADMIN' role permissions"
|
||||
# ✅ "Server listening at http://localhost:3080"
|
||||
# ✅ No "IoRedis connection error" messages
|
||||
```
|
||||
|
||||
### TLS Certificate Details
|
||||
|
||||
The TLS setup includes:
|
||||
|
||||
- **CA Certificate**: Self-signed Certificate Authority for validation
|
||||
- **Server Certificate**: Contains Subject Alternative Names (SAN) for:
|
||||
- `DNS: localhost`
|
||||
- `IP: 127.0.0.1`
|
||||
- **TLS Configuration**:
|
||||
- TLS v1.2 and v1.3 support
|
||||
- No client certificate authentication required
|
||||
- Strong cipher suites (AES-256-GCM, ChaCha20-Poly1305)
|
||||
|
||||
### Troubleshooting TLS
|
||||
|
||||
#### Certificate Validation Errors
|
||||
|
||||
```bash
|
||||
# If you see "Hostname/IP does not match certificate's altnames"
|
||||
# Check certificate SAN entries:
|
||||
openssl x509 -in certs/server-cert.pem -text -noout | grep -A3 "Subject Alternative Name"
|
||||
|
||||
# Should show: DNS:localhost, IP Address:127.0.0.1
|
||||
```
|
||||
|
||||
#### Connection Refused
|
||||
|
||||
```bash
|
||||
# Check if Redis TLS is running
|
||||
lsof -i :6380
|
||||
|
||||
# Check Redis TLS server logs
|
||||
ps aux | grep redis-server
|
||||
```
|
||||
|
||||
#### Backend Connection Issues
|
||||
|
||||
```bash
|
||||
# Verify CA certificate path in .env
|
||||
ls -la /path/to/LibreChat/redis-config/certs/ca-cert.pem
|
||||
|
||||
# Test LibreChat Redis configuration
|
||||
cd /path/to/LibreChat
|
||||
npm run backend
|
||||
# Look for Redis connection errors in output
|
||||
```
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Check Cluster Status
|
||||
|
||||
```bash
|
||||
# Cluster information
|
||||
redis-cli -p 7001 cluster info
|
||||
|
||||
# Node information
|
||||
redis-cli -p 7001 cluster nodes
|
||||
|
||||
# Check specific node
|
||||
redis-cli -p 7002 info replication
|
||||
```
|
||||
|
||||
### Monitor Cluster
|
||||
|
||||
```bash
|
||||
# Monitor all operations
|
||||
redis-cli -p 7001 monitor
|
||||
|
||||
# Check memory usage
|
||||
redis-cli -p 7001 info memory
|
||||
redis-cli -p 7002 info memory
|
||||
redis-cli -p 7003 info memory
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### Cluster Won't Start
|
||||
|
||||
1. Check if Redis is installed: `redis-server --version`
|
||||
2. Check for port conflicts: `netstat -tlnp | grep :700`
|
||||
3. Check Redis processes: `ps aux | grep redis-server`
|
||||
4. Check if nodes are responding: `redis-cli -p 7001 ping`
|
||||
|
||||
#### Cluster Initialization Fails
|
||||
|
||||
1. Ensure all nodes are running: `./start-cluster.sh`
|
||||
2. Check cluster configuration: `redis-cli -p 7001 cluster nodes`
|
||||
3. Reset if needed: `redis-cli -p 7001 CLUSTER RESET`
|
||||
|
||||
#### Performance Issues
|
||||
|
||||
1. Monitor memory usage: `redis-cli -p 7001 info memory`
|
||||
2. Check slow queries: `redis-cli -p 7001 slowlog get 10`
|
||||
3. Adjust `maxmemory` settings in configuration files
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### Node Configuration
|
||||
|
||||
Each node is configured with:
|
||||
- **Memory limit**: 256MB with LRU eviction
|
||||
- **Persistence**: AOF + RDB snapshots
|
||||
- **Clustering**: Enabled with 15-second timeout
|
||||
- **Logging**: Notice level (logs to stdout/stderr by default)
|
||||
|
||||
### Hash Slot Distribution
|
||||
|
||||
With 3 nodes and no replicas:
|
||||
- Node 1 (7001): Hash slots 0-5460
|
||||
- Node 2 (7002): Hash slots 5461-10922
|
||||
- Node 3 (7003): Hash slots 10923-16383
|
||||
|
||||
## Security Note
|
||||
|
||||
### Development Setup
|
||||
The basic Redis cluster setup is designed for **local development only**.
|
||||
|
||||
### TLS Setup
|
||||
The TLS Redis configuration provides:
|
||||
- ✅ **TLS encryption** with CA certificate validation
|
||||
- ✅ **Server certificate** with proper Subject Alternative Names
|
||||
- ✅ **Strong cipher suites** (AES-256-GCM, ChaCha20-Poly1305)
|
||||
- ✅ **Certificate validation** via self-signed CA
|
||||
|
||||
### Production Considerations
|
||||
For production use, consider:
|
||||
- Authentication (`requirepass` or `AUTH` commands)
|
||||
- Client certificate authentication (`tls-auth-clients yes`)
|
||||
- Firewall configuration
|
||||
- Replica nodes for high availability
|
||||
- Proper certificate management (not self-signed)
|
||||
- Key rotation policies
|
||||
|
||||
## Backup and Recovery
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
# Backup all nodes
|
||||
mkdir -p backup
|
||||
redis-cli -p 7001 BGSAVE
|
||||
redis-cli -p 7002 BGSAVE
|
||||
redis-cli -p 7003 BGSAVE
|
||||
|
||||
# Copy backup files
|
||||
cp data/7001/dump.rdb backup/dump-7001.rdb
|
||||
cp data/7002/dump.rdb backup/dump-7002.rdb
|
||||
cp data/7003/dump.rdb backup/dump-7003.rdb
|
||||
```
|
||||
|
||||
### Recovery
|
||||
|
||||
```bash
|
||||
# Stop cluster
|
||||
./stop-cluster.sh
|
||||
|
||||
# Restore backup files
|
||||
cp backup/dump-7001.rdb data/7001/dump.rdb
|
||||
cp backup/dump-7002.rdb data/7002/dump.rdb
|
||||
cp backup/dump-7003.rdb data/7003/dump.rdb
|
||||
|
||||
# Start cluster
|
||||
./start-cluster.sh
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For Redis-specific issues:
|
||||
- [Redis Documentation](https://redis.io/docs/)
|
||||
- [Redis Cluster Tutorial](https://redis.io/docs/manual/scaling/)
|
||||
|
||||
For LibreChat integration:
|
||||
- [LibreChat Documentation](https://github.com/danny-avila/LibreChat)
|
1
redis-config/certs/ca-cert.srl
Normal file
1
redis-config/certs/ca-cert.srl
Normal file
|
@ -0,0 +1 @@
|
|||
54E48A77C8FF4781A554C80BF6EFC0401A6ACE8A
|
BIN
redis-config/certs/dump.rdb
Normal file
BIN
redis-config/certs/dump.rdb
Normal file
Binary file not shown.
8
redis-config/certs/redis.dh
Normal file
8
redis-config/certs/redis.dh
Normal file
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN DH PARAMETERS-----
|
||||
MIIBDAKCAQEAmPCtZIwB9l9LqyFewdGKxxk3HEcQdHswM3IHbhE+GqZOaD8KwxB+
|
||||
rfPH54pDSW42WLWM+y7/eC2ufPdw6ZDBjCYFC8rGkWUPwguDl90INuzCCCAgwBVw
|
||||
tpKfcZ92T8ek1qR6UgZa4zPq4FjQm09ZcVmMzaUeIkiRGv0/t2GjswZDVuLhKRp5
|
||||
eSH7pByYCNYj3X9HyMqcCDfGhTkg8azcWJiEOCTCpsYgXcW1tz2PQsaJZpERnffk
|
||||
px8mLuDPMVxTRWpXcIBmzs/Nwv8bGigyI4ADocM3jmQ8c6b9ZYUmyvled/1LEBzC
|
||||
10g2R67op+dGOxk40lwrLmN6bzYFt/YKbwIBAgICAOE=
|
||||
-----END DH PARAMETERS-----
|
16
redis-config/certs/server.conf
Normal file
16
redis-config/certs/server.conf
Normal file
|
@ -0,0 +1,16 @@
|
|||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
CN = localhost
|
||||
|
||||
[v3_req]
|
||||
keyUsage = keyEncipherment, dataEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
27
redis-config/redis-7001.conf
Normal file
27
redis-config/redis-7001.conf
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Redis Cluster Node 1 Configuration
|
||||
port 7001
|
||||
cluster-enabled yes
|
||||
cluster-config-file nodes-7001.conf
|
||||
cluster-node-timeout 15000
|
||||
appendonly yes
|
||||
appendfilename "appendonly-7001.aof"
|
||||
|
||||
# Data directory
|
||||
dir ./data/7001
|
||||
|
||||
# Logging
|
||||
# logfile ./logs/redis-7001.log
|
||||
loglevel notice
|
||||
|
||||
# Network
|
||||
bind 127.0.0.1
|
||||
protected-mode no
|
||||
|
||||
# Memory management
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
|
||||
# Persistence
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
27
redis-config/redis-7002.conf
Normal file
27
redis-config/redis-7002.conf
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Redis Cluster Node 2 Configuration
|
||||
port 7002
|
||||
cluster-enabled yes
|
||||
cluster-config-file nodes-7002.conf
|
||||
cluster-node-timeout 15000
|
||||
appendonly yes
|
||||
appendfilename "appendonly-7002.aof"
|
||||
|
||||
# Data directory
|
||||
dir ./data/7002
|
||||
|
||||
# Logging
|
||||
# logfile ./logs/redis-7002.log
|
||||
loglevel notice
|
||||
|
||||
# Network
|
||||
bind 127.0.0.1
|
||||
protected-mode no
|
||||
|
||||
# Memory management
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
|
||||
# Persistence
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
27
redis-config/redis-7003.conf
Normal file
27
redis-config/redis-7003.conf
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Redis Cluster Node 3 Configuration
|
||||
port 7003
|
||||
cluster-enabled yes
|
||||
cluster-config-file nodes-7003.conf
|
||||
cluster-node-timeout 15000
|
||||
appendonly yes
|
||||
appendfilename "appendonly-7003.aof"
|
||||
|
||||
# Data directory
|
||||
dir ./data/7003
|
||||
|
||||
# Logging
|
||||
# logfile ./logs/redis-7003.log
|
||||
loglevel notice
|
||||
|
||||
# Network
|
||||
bind 127.0.0.1
|
||||
protected-mode no
|
||||
|
||||
# Memory management
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
|
||||
# Persistence
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
31
redis-config/redis-tls.conf
Normal file
31
redis-config/redis-tls.conf
Normal file
|
@ -0,0 +1,31 @@
|
|||
port 0
|
||||
tls-port 6380
|
||||
tls-cert-file /Users/theotr/WebstormProjects/LibreChat/redis-cluster/certs/server-cert.pem
|
||||
tls-key-file /Users/theotr/WebstormProjects/LibreChat/redis-cluster/certs/server-key.pem
|
||||
tls-ca-cert-file /Users/theotr/WebstormProjects/LibreChat/redis-cluster/certs/ca-cert.pem
|
||||
tls-auth-clients no
|
||||
tls-dh-params-file /Users/theotr/WebstormProjects/LibreChat/redis-cluster/certs/redis.dh
|
||||
tls-protocols "TLSv1.2 TLSv1.3"
|
||||
tls-ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
|
||||
tls-prefer-server-ciphers yes
|
||||
tls-session-caching no
|
||||
tls-session-cache-size 5000
|
||||
tls-session-cache-timeout 60
|
||||
bind 127.0.0.1
|
||||
protected-mode yes
|
||||
timeout 0
|
||||
tcp-keepalive 300
|
||||
daemonize no
|
||||
pidfile /var/run/redis_6379.pid
|
||||
loglevel notice
|
||||
logfile ""
|
||||
databases 16
|
||||
always-show-logo no
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
stop-writes-on-bgsave-error yes
|
||||
rdbcompression yes
|
||||
rdbchecksum yes
|
||||
dbfilename dump.rdb
|
||||
dir ./
|
84
redis-config/start-cluster.sh
Executable file
84
redis-config/start-cluster.sh
Executable file
|
@ -0,0 +1,84 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Redis Cluster Startup Script
|
||||
# This script starts and initializes a 3-node Redis cluster with no replicas
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "🚀 Starting Redis Cluster..."
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p data/7001 data/7002 data/7003
|
||||
mkdir -p logs
|
||||
|
||||
# Check if Redis is installed
|
||||
if ! command -v redis-server &> /dev/null; then
|
||||
echo "❌ Redis is not installed. Please install Redis first:"
|
||||
echo " macOS: brew install redis"
|
||||
echo " Ubuntu: sudo apt-get install redis-server"
|
||||
echo " CentOS: sudo yum install redis"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Redis CLI is available
|
||||
if ! command -v redis-cli &> /dev/null; then
|
||||
echo "❌ Redis CLI is not available. Please install Redis CLI."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start Redis instances
|
||||
redis-server redis-7001.conf --daemonize yes
|
||||
redis-server redis-7002.conf --daemonize yes
|
||||
redis-server redis-7003.conf --daemonize yes
|
||||
|
||||
# Wait for nodes to start
|
||||
sleep 3
|
||||
|
||||
# Check if all nodes are running
|
||||
NODES_RUNNING=0
|
||||
for port in 7001 7002 7003; do
|
||||
if redis-cli -p $port ping &> /dev/null; then
|
||||
NODES_RUNNING=$((NODES_RUNNING + 1))
|
||||
else
|
||||
echo "❌ Node on port $port failed to start"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $NODES_RUNNING -ne 3 ]; then
|
||||
echo "❌ Not all Redis nodes started successfully."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ All Redis nodes started"
|
||||
|
||||
# Check if cluster is already initialized
|
||||
if redis-cli -p 7001 cluster info 2>/dev/null | grep -q "cluster_state:ok"; then
|
||||
echo "✅ Cluster already initialized"
|
||||
echo ""
|
||||
echo "📋 Usage:"
|
||||
echo " Connect: redis-cli -c -p 7001"
|
||||
echo " Stop: ./stop-cluster.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Initialize the cluster
|
||||
echo "🔧 Initializing cluster..."
|
||||
echo "yes" | redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 --cluster-replicas 0 > /dev/null
|
||||
|
||||
# Wait for cluster to stabilize
|
||||
sleep 3
|
||||
|
||||
# Verify cluster status
|
||||
if redis-cli -p 7001 cluster info | grep -q "cluster_state:ok"; then
|
||||
echo "✅ Redis cluster ready!"
|
||||
echo ""
|
||||
echo "📋 Usage:"
|
||||
echo " Connect: redis-cli -c -p 7001"
|
||||
echo " Stop: ./stop-cluster.sh"
|
||||
else
|
||||
echo "❌ Cluster initialization failed!"
|
||||
exit 1
|
||||
fi
|
13
redis-config/start-redis-tls.sh
Executable file
13
redis-config/start-redis-tls.sh
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Start Redis with TLS configuration
|
||||
echo "Starting Redis with TLS on port 6379..."
|
||||
|
||||
# Check if Redis is already running
|
||||
if pgrep -f "redis-server.*tls" > /dev/null; then
|
||||
echo "Redis with TLS is already running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start Redis with TLS config
|
||||
redis-server /Users/theotr/WebstormProjects/LibreChat/redis-cluster/redis-tls.conf
|
69
redis-config/stop-cluster.sh
Executable file
69
redis-config/stop-cluster.sh
Executable file
|
@ -0,0 +1,69 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Redis Cluster Shutdown Script
|
||||
# This script stops all Redis cluster nodes
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "🛑 Stopping Redis Cluster..."
|
||||
|
||||
# Function to stop a Redis node
|
||||
stop_node() {
|
||||
local port=$1
|
||||
|
||||
if redis-cli -p $port ping &> /dev/null; then
|
||||
# Try graceful shutdown first
|
||||
redis-cli -p $port SHUTDOWN NOSAVE 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Check if still running and force kill if needed
|
||||
if redis-cli -p $port ping &> /dev/null; then
|
||||
PID=$(ps aux | grep "[r]edis-server.*:$port" | awk '{print $2}')
|
||||
if [ -n "$PID" ]; then
|
||||
kill -TERM $PID 2>/dev/null || true
|
||||
sleep 2
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
kill -KILL $PID 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Final check
|
||||
if redis-cli -p $port ping &> /dev/null; then
|
||||
echo "❌ Failed to stop Redis node on port $port"
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop all nodes
|
||||
NODES_STOPPED=0
|
||||
for port in 7001 7002 7003; do
|
||||
if stop_node $port; then
|
||||
NODES_STOPPED=$((NODES_STOPPED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up cluster configuration files
|
||||
rm -f nodes-7001.conf nodes-7002.conf nodes-7003.conf
|
||||
|
||||
if [ $NODES_STOPPED -eq 3 ]; then
|
||||
echo "✅ Redis cluster stopped"
|
||||
else
|
||||
echo "⚠️ Some nodes may not have stopped properly"
|
||||
echo "Check running processes: ps aux | grep redis-server"
|
||||
fi
|
||||
|
||||
# Check for remaining processes
|
||||
REMAINING_PROCESSES=$(ps aux | grep "[r]edis-server" | grep -E ":(7001|7002|7003)" | wc -l)
|
||||
if [ $REMAINING_PROCESSES -gt 0 ]; then
|
||||
echo "⚠️ Found $REMAINING_PROCESSES remaining Redis processes"
|
||||
echo "Kill with: pkill -f 'redis-server.*:700[1-3]'"
|
||||
fi
|
Loading…
Add table
Add a link
Reference in a new issue