/** * Rate limiting utility to prevent abuse */ class RateLimiter { constructor() { // Map of peer ID to request timestamps this.requests = new Map(); // Configuration — sized for interactive UI (auto-refresh, dashboard, // multi-tab inspect) without tripping under normal operator use. this.config = { maxRequests: 3000, // Max non-stream RPCs per window (~50/s avg) windowMs: 60000, // 1 minute window commandLimits: { deployContainer: { max: 30, windowMs: 60000 }, // deploys per minute dockerCommand: { max: 200, windowMs: 10000 }, // host docker CLI / 10s startContainer: { max: 120, windowMs: 60000 }, // starts per minute stopContainer: { max: 120, windowMs: 60000 }, // stops per minute }, }; /** * High-frequency stream methods (keystrokes, resizes, chunks). * Exempt from the general cap; optional soft ceiling. */ this.streamMethods = new Set([ 'terminalInput', 'terminalResize', 'execInput', 'attachInput', 'loadImageChunk', 'binaryStreamChunk', 'dockerTerminalResize', ]); this.streamLimit = { max: 18000, windowMs: 60000 }; // ~300/s sustained } /** * Get peer identifier * @param {Object} peer - Peer object * @returns {string} - Peer identifier */ getPeerId(peer) { if (peer?.id && typeof peer.id === 'string') return peer.id if (peer?.remotePublicKey) { return typeof peer.remotePublicKey === 'string' ? peer.remotePublicKey : Buffer.from(peer.remotePublicKey).toString('hex') } return 'unknown' } /** * Check if request is within rate limit * @param {Object} peer - Peer object * @param {string} command - Command name * @returns {boolean} - True if allowed */ isAllowed(peer, command) { const peerId = this.getPeerId(peer); const now = Date.now(); // Clean up old entries this.cleanup(now); // Initialize peer entry if needed if (!this.requests.has(peerId)) { this.requests.set(peerId, { general: [], stream: [], commands: {} }); } const peerData = this.requests.get(peerId); if (!peerData.stream) peerData.stream = []; // Stream methods: separate high ceiling, never consume general budget if (this.streamMethods.has(command)) { const streamWindow = now - this.streamLimit.windowMs; peerData.stream = peerData.stream.filter((t) => t > streamWindow); if (peerData.stream.length >= this.streamLimit.max) { return false; } peerData.stream.push(now); return true; } // Check general rate limit const generalWindow = now - this.config.windowMs; peerData.general = peerData.general.filter(timestamp => timestamp > generalWindow); if (peerData.general.length >= this.config.maxRequests) { return false; } // Check command-specific rate limit if (this.config.commandLimits[command]) { const limit = this.config.commandLimits[command]; const commandWindow = now - limit.windowMs; if (!peerData.commands[command]) { peerData.commands[command] = []; } peerData.commands[command] = peerData.commands[command].filter( timestamp => timestamp > commandWindow ); if (peerData.commands[command].length >= limit.max) { return false; } // Record command request peerData.commands[command].push(now); } // Record general request peerData.general.push(now); return true; } /** @param {string} method */ isStreamMethod(method) { return this.streamMethods.has(method); } /** * Clean up old entries * @param {number} now - Current timestamp */ cleanup(now) { const maxAge = Math.max( this.config.windowMs, ...Object.values(this.config.commandLimits).map(l => l.windowMs) ); for (const [peerId, peerData] of this.requests.entries()) { // Clean general requests peerData.general = peerData.general.filter(timestamp => timestamp > now - maxAge); // Clean command requests for (const [command, timestamps] of Object.entries(peerData.commands)) { const limit = this.config.commandLimits[command]; if (limit) { peerData.commands[command] = timestamps.filter( timestamp => timestamp > now - limit.windowMs ); } } // Remove peer if no active requests if (peerData.general.length === 0 && Object.values(peerData.commands).every(arr => arr.length === 0)) { this.requests.delete(peerId); } } } /** * Reset rate limit for a peer (useful for testing or manual override) * @param {Object} peer - Peer object */ reset(peer) { const peerId = this.getPeerId(peer); this.requests.delete(peerId); } } // Singleton instance const rateLimiter = new RateLimiter(); export default rateLimiter;