/** * Rate Limiting Middleware * Implements comprehensive rate limiting with Redis backend and flexible configuration */ const rateLimit = require('express-rate-limit'); const { getRedisClient } = require('../config/redis'); const logger = require('../utils/logger'); // Rate limiting configuration const RATE_LIMIT_CONFIG = { // Global API rate limits global: { windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 1000, // 1000 requests per window standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, skipFailedRequests: false, }, // Authentication endpoints (more restrictive) auth: { windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // 10 attempts per window standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: true, // Don't count successful logins skipFailedRequests: false, }, // Player API endpoints player: { windowMs: 1 * 60 * 1000, // 1 minute max: 120, // 120 requests per minute standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, skipFailedRequests: false, }, // Admin API endpoints (more lenient for legitimate admin users) admin: { windowMs: 1 * 60 * 1000, // 1 minute max: 300, // 300 requests per minute standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, skipFailedRequests: false, }, // Game action endpoints (prevent spam) gameAction: { windowMs: 30 * 1000, // 30 seconds max: 30, // 30 actions per 30 seconds standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, skipFailedRequests: true, }, // Message sending (prevent spam) messaging: { windowMs: 5 * 60 * 1000, // 5 minutes max: 10, // 10 messages per 5 minutes standardHeaders: true, legacyHeaders: false, skipSuccessfulRequests: false, skipFailedRequests: true, }, }; /** * Create Redis store for rate limiting if Redis is available * @returns {Object|null} Redis store or null if Redis unavailable */ function createRedisStore() { // Check if Redis is disabled first if (process.env.DISABLE_REDIS === 'true') { logger.info('Redis disabled for rate limiting, using memory store'); return null; } try { const redis = getRedisClient(); if (!redis) { logger.warn('Redis not available for rate limiting, using memory store'); return null; } // Create Redis store for express-rate-limit try { const { RedisStore } = require('rate-limit-redis'); return new RedisStore({ sendCommand: (...args) => redis.sendCommand(args), prefix: 'rl:', // Rate limit prefix }); } catch (error) { logger.warn('Failed to create RedisStore, falling back to memory store', { error: error.message, }); return null; } } catch (error) { logger.warn('Failed to create Redis store for rate limiting', { error: error.message, }); return null; } } /** * Create key generator for rate limiting * @param {string} prefix - Key prefix * @returns {Function} Key generator function */ function createKeyGenerator(prefix = 'global') { return (req) => { const ip = req.ip || req.connection.remoteAddress || 'unknown'; const userId = req.user?.playerId || req.user?.adminId || 'anonymous'; return `${prefix}:${userId}:${ip}`; }; } /** * Create rate limit handler * @param {string} type - Rate limit type for logging * @returns {Function} Rate limit handler function */ function createRateLimitHandler(type) { return (req, res) => { const correlationId = req.correlationId; const ip = req.ip || req.connection.remoteAddress; const userId = req.user?.playerId || req.user?.adminId; const userType = req.user?.type || 'anonymous'; logger.warn('Rate limit exceeded', { correlationId, type, ip, userId, userType, path: req.path, method: req.method, userAgent: req.get('User-Agent'), retryAfter: res.get('Retry-After'), }); return res.status(429).json({ error: 'Too Many Requests', message: 'Rate limit exceeded. Please try again later.', type, retryAfter: res.get('Retry-After'), correlationId, }); }; } /** * Create skip function for rate limiting * @param {Array} skipPaths - Paths to skip rate limiting * @param {Array} skipIPs - IPs to skip rate limiting * @returns {Function} Skip function */ function createSkipFunction(skipPaths = [], skipIPs = []) { return (req) => { const ip = req.ip || req.connection.remoteAddress; // Skip health checks if (req.path === '/health' || req.path === '/api/health') { return true; } // Skip specified paths if (skipPaths.some(path => req.path.startsWith(path))) { return true; } // Skip specified IPs (for development/testing) if (skipIPs.includes(ip)) { return true; } // Skip if rate limiting is disabled if (process.env.DISABLE_RATE_LIMITING === 'true') { return true; } return false; }; } /** * Create rate limiter middleware * @param {string} type - Type of rate limiter * @param {Object} customConfig - Custom configuration * @returns {Function} Rate limiter middleware */ function createRateLimiter(type, customConfig = {}) { const config = { ...RATE_LIMIT_CONFIG[type], ...customConfig }; const store = createRedisStore(); const rateLimiter = rateLimit({ ...config, store, keyGenerator: createKeyGenerator(type), handler: createRateLimitHandler(type), skip: createSkipFunction(), // Note: onLimitReached is deprecated in express-rate-limit v7 // Removed for compatibility }); // Log rate limiter creation logger.info('Rate limiter created', { type, windowMs: config.windowMs, max: config.max, useRedis: !!store, }); return rateLimiter; } /** * Pre-configured rate limiters */ const rateLimiters = { global: createRateLimiter('global'), auth: createRateLimiter('auth'), player: createRateLimiter('player'), admin: createRateLimiter('admin'), gameAction: createRateLimiter('gameAction'), messaging: createRateLimiter('messaging'), }; /** * Middleware to add rate limit headers even when not limiting * @param {Object} req - Express request object * @param {Object} res - Express response object * @param {Function} next - Express next function */ function addRateLimitHeaders(req, res, next) { // Add custom headers for client information res.set({ 'X-RateLimit-Policy': 'See API documentation for rate limiting details', }); next(); } /** * Custom rate limiter for WebSocket connections * @param {number} maxConnections - Maximum connections per IP * @param {number} windowMs - Time window in milliseconds * @returns {Function} WebSocket rate limiter function */ function createWebSocketRateLimiter(maxConnections = 10, windowMs = 60000) { const connections = new Map(); return (socket, next) => { const ip = socket.handshake.address; const now = Date.now(); // Clean up old connections if (connections.has(ip)) { const connectionTimes = connections.get(ip).filter(time => now - time < windowMs); connections.set(ip, connectionTimes); } // Check rate limit const currentConnections = connections.get(ip) || []; if (currentConnections.length >= maxConnections) { logger.warn('WebSocket connection rate limit exceeded', { ip, currentConnections: currentConnections.length, maxConnections, }); return next(new Error('Connection rate limit exceeded')); } // Add current connection currentConnections.push(now); connections.set(ip, currentConnections); logger.debug('WebSocket connection allowed', { ip, connections: currentConnections.length, maxConnections, }); next(); }; } /** * Middleware to apply different rate limits based on user type * @param {Object} req - Express request object * @param {Object} res - Express response object * @param {Function} next - Express next function */ function dynamicRateLimit(req, res, next) { const userType = req.user?.type; let limiter; if (userType === 'admin') { limiter = rateLimiters.admin; } else if (userType === 'player') { limiter = rateLimiters.player; } else { limiter = rateLimiters.global; } return limiter(req, res, next); } module.exports = { rateLimiters, createRateLimiter, createWebSocketRateLimiter, addRateLimitHeaders, dynamicRateLimit, RATE_LIMIT_CONFIG, };