1053 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			1053 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import * as plugins from '../../plugins.ts'; | ||
|  | import { EventEmitter } from 'node:events'; | ||
|  | import { logger } from '../../logger.ts'; | ||
|  | import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.ts'; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Interface for rate limit configuration | ||
|  |  */ | ||
|  | export interface IRateLimitConfig { | ||
|  |   maxMessagesPerMinute?: number; | ||
|  |   maxRecipientsPerMessage?: number; | ||
|  |   maxConnectionsPerIP?: number; | ||
|  |   maxErrorsPerIP?: number; | ||
|  |   maxAuthFailuresPerIP?: number; | ||
|  |   blockDuration?: number; // in milliseconds
 | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Interface for hierarchical rate limits | ||
|  |  */ | ||
|  | export interface IHierarchicalRateLimits { | ||
|  |   // Global rate limits (applied to all traffic)
 | ||
|  |   global: IRateLimitConfig; | ||
|  |    | ||
|  |   // Pattern-specific rate limits (applied to matching patterns)
 | ||
|  |   patterns?: Record<string, IRateLimitConfig>; | ||
|  |    | ||
|  |   // IP-specific rate limits (applied to specific IPs)
 | ||
|  |   ips?: Record<string, IRateLimitConfig>; | ||
|  |    | ||
|  |   // Domain-specific rate limits (applied to specific email domains)
 | ||
|  |   domains?: Record<string, IRateLimitConfig>; | ||
|  |    | ||
|  |   // Temporary blocks list and their expiry times
 | ||
|  |   blocks?: Record<string, number>; // IP to expiry timestamp
 | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Counter interface for rate limiting | ||
|  |  */ | ||
|  | interface ILimitCounter { | ||
|  |   count: number; | ||
|  |   lastReset: number; | ||
|  |   recipients: number; | ||
|  |   errors: number; | ||
|  |   authFailures: number; | ||
|  |   connections: number; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Rate limiter statistics | ||
|  |  */ | ||
|  | export interface IRateLimiterStats { | ||
|  |   activeCounters: number; | ||
|  |   totalBlocked: number; | ||
|  |   currentlyBlocked: number; | ||
|  |   byPattern: Record<string, { | ||
|  |     messagesPerMinute: number; | ||
|  |     totalMessages: number; | ||
|  |     totalBlocked: number; | ||
|  |   }>; | ||
|  |   byIp: Record<string, { | ||
|  |     messagesPerMinute: number; | ||
|  |     totalMessages: number; | ||
|  |     totalBlocked: number; | ||
|  |     connections: number; | ||
|  |     errors: number; | ||
|  |     authFailures: number; | ||
|  |     blocked: boolean; | ||
|  |   }>; | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Result of a rate limit check | ||
|  |  */ | ||
|  | export interface IRateLimitResult { | ||
|  |   allowed: boolean; | ||
|  |   reason?: string; | ||
|  |   limit?: number; | ||
|  |   current?: number; | ||
|  |   resetIn?: number; // milliseconds until reset
 | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Unified rate limiter for all email processing modes | ||
|  |  */ | ||
|  | export class UnifiedRateLimiter extends EventEmitter { | ||
|  |   private config: IHierarchicalRateLimits; | ||
|  |   private counters: Map<string, ILimitCounter> = new Map(); | ||
|  |   private patternCounters: Map<string, ILimitCounter> = new Map(); | ||
|  |   private ipCounters: Map<string, ILimitCounter> = new Map(); | ||
|  |   private domainCounters: Map<string, ILimitCounter> = new Map(); | ||
|  |   private cleanupInterval?: NodeJS.Timeout; | ||
|  |   private stats: IRateLimiterStats; | ||
|  |    | ||
|  |   /** | ||
|  |    * Create a new unified rate limiter | ||
|  |    * @param config Rate limit configuration | ||
|  |    */ | ||
|  |   constructor(config: IHierarchicalRateLimits) { | ||
|  |     super(); | ||
|  |      | ||
|  |     // Set default configuration
 | ||
|  |     this.config = { | ||
|  |       global: { | ||
|  |         maxMessagesPerMinute: config.global.maxMessagesPerMinute || 100, | ||
|  |         maxRecipientsPerMessage: config.global.maxRecipientsPerMessage || 100, | ||
|  |         maxConnectionsPerIP: config.global.maxConnectionsPerIP || 20, | ||
|  |         maxErrorsPerIP: config.global.maxErrorsPerIP || 10, | ||
|  |         maxAuthFailuresPerIP: config.global.maxAuthFailuresPerIP || 5, | ||
|  |         blockDuration: config.global.blockDuration || 3600000 // 1 hour
 | ||
|  |       }, | ||
|  |       patterns: config.patterns || {}, | ||
|  |       ips: config.ips || {}, | ||
|  |       blocks: config.blocks || {} | ||
|  |     }; | ||
|  |      | ||
|  |     // Initialize statistics
 | ||
|  |     this.stats = { | ||
|  |       activeCounters: 0, | ||
|  |       totalBlocked: 0, | ||
|  |       currentlyBlocked: 0, | ||
|  |       byPattern: {}, | ||
|  |       byIp: {} | ||
|  |     }; | ||
|  |      | ||
|  |     // Start cleanup interval
 | ||
|  |     this.startCleanupInterval(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Start the cleanup interval | ||
|  |    */ | ||
|  |   private startCleanupInterval(): void { | ||
|  |     if (this.cleanupInterval) { | ||
|  |       clearInterval(this.cleanupInterval); | ||
|  |     } | ||
|  |      | ||
|  |     // Run cleanup every minute
 | ||
|  |     this.cleanupInterval = setInterval(() => this.cleanup(), 60000); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Stop the cleanup interval | ||
|  |    */ | ||
|  |   public stop(): void { | ||
|  |     if (this.cleanupInterval) { | ||
|  |       clearInterval(this.cleanupInterval); | ||
|  |       this.cleanupInterval = undefined; | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Destroy the rate limiter and clean up all resources | ||
|  |    */ | ||
|  |   public destroy(): void { | ||
|  |     // Stop the cleanup interval
 | ||
|  |     this.stop(); | ||
|  |      | ||
|  |     // Clear all maps to free memory
 | ||
|  |     this.counters.clear(); | ||
|  |     this.ipCounters.clear(); | ||
|  |     this.patternCounters.clear(); | ||
|  |      | ||
|  |     // Clear blocks
 | ||
|  |     if (this.config.blocks) { | ||
|  |       this.config.blocks = {}; | ||
|  |     } | ||
|  |      | ||
|  |     // Clear statistics
 | ||
|  |     this.stats = { | ||
|  |       activeCounters: 0, | ||
|  |       totalBlocked: 0, | ||
|  |       currentlyBlocked: 0, | ||
|  |       byPattern: {}, | ||
|  |       byIp: {} | ||
|  |     }; | ||
|  |      | ||
|  |     logger.log('info', 'UnifiedRateLimiter destroyed'); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Clean up expired counters and blocks | ||
|  |    */ | ||
|  |   private cleanup(): void { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Clean up expired blocks
 | ||
|  |     if (this.config.blocks) { | ||
|  |       for (const [ip, expiry] of Object.entries(this.config.blocks)) { | ||
|  |         if (expiry <= now) { | ||
|  |           delete this.config.blocks[ip]; | ||
|  |           logger.log('info', `Rate limit block expired for IP ${ip}`); | ||
|  |            | ||
|  |           // Update statistics
 | ||
|  |           if (this.stats.byIp[ip]) { | ||
|  |             this.stats.byIp[ip].blocked = false; | ||
|  |           } | ||
|  |           this.stats.currentlyBlocked--; | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Clean up old counters (older than 10 minutes)
 | ||
|  |     const cutoff = now - 600000; | ||
|  |      | ||
|  |     // Clean global counters
 | ||
|  |     for (const [key, counter] of this.counters.entries()) { | ||
|  |       if (counter.lastReset < cutoff) { | ||
|  |         this.counters.delete(key); | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Clean pattern counters
 | ||
|  |     for (const [key, counter] of this.patternCounters.entries()) { | ||
|  |       if (counter.lastReset < cutoff) { | ||
|  |         this.patternCounters.delete(key); | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Clean IP counters
 | ||
|  |     for (const [key, counter] of this.ipCounters.entries()) { | ||
|  |       if (counter.lastReset < cutoff) { | ||
|  |         this.ipCounters.delete(key); | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Clean domain counters
 | ||
|  |     for (const [key, counter] of this.domainCounters.entries()) { | ||
|  |       if (counter.lastReset < cutoff) { | ||
|  |         this.domainCounters.delete(key); | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.updateStats(); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check if a message is allowed by rate limits | ||
|  |    * @param email Email address | ||
|  |    * @param ip IP address | ||
|  |    * @param recipients Number of recipients | ||
|  |    * @param pattern Matched pattern | ||
|  |    * @param domain Domain name for domain-specific limits | ||
|  |    * @returns Result of rate limit check | ||
|  |    */ | ||
|  |   public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: string, domain?: string): IRateLimitResult { | ||
|  |     // Check if IP is blocked
 | ||
|  |     if (this.isIpBlocked(ip)) { | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: 'IP is blocked', | ||
|  |         resetIn: this.getBlockReleaseTime(ip) | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Check global message rate limit
 | ||
|  |     const globalResult = this.checkGlobalMessageLimit(email); | ||
|  |     if (!globalResult.allowed) { | ||
|  |       return globalResult; | ||
|  |     } | ||
|  |      | ||
|  |     // Check pattern-specific limit if pattern is provided
 | ||
|  |     if (pattern) { | ||
|  |       const patternResult = this.checkPatternMessageLimit(pattern); | ||
|  |       if (!patternResult.allowed) { | ||
|  |         return patternResult; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check domain-specific limit if domain is provided
 | ||
|  |     if (domain) { | ||
|  |       const domainResult = this.checkDomainMessageLimit(domain); | ||
|  |       if (!domainResult.allowed) { | ||
|  |         return domainResult; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check IP-specific limit
 | ||
|  |     const ipResult = this.checkIpMessageLimit(ip); | ||
|  |     if (!ipResult.allowed) { | ||
|  |       return ipResult; | ||
|  |     } | ||
|  |      | ||
|  |     // Check recipient limit
 | ||
|  |     const recipientResult = this.checkRecipientLimit(email, recipients, pattern, domain); | ||
|  |     if (!recipientResult.allowed) { | ||
|  |       return recipientResult; | ||
|  |     } | ||
|  |      | ||
|  |     // All checks passed
 | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check global message rate limit | ||
|  |    * @param email Email address | ||
|  |    */ | ||
|  |   private checkGlobalMessageLimit(email: string): IRateLimitResult { | ||
|  |     const now = Date.now(); | ||
|  |     const limit = this.config.global.maxMessagesPerMinute!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return { allowed: true }; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     const key = 'global'; | ||
|  |     let counter = this.counters.get(key); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.counters.set(key, counter); | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.count = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.count >= limit) { | ||
|  |       // Calculate reset time
 | ||
|  |       const resetIn = 60000 - (now - counter.lastReset); | ||
|  |        | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: 'Global message rate limit exceeded', | ||
|  |         limit, | ||
|  |         current: counter.count, | ||
|  |         resetIn | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.count++; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.updateStats(); | ||
|  |      | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check pattern-specific message rate limit | ||
|  |    * @param pattern Pattern to check | ||
|  |    */ | ||
|  |   private checkPatternMessageLimit(pattern: string): IRateLimitResult { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Get pattern-specific limit or use global
 | ||
|  |     const patternConfig = this.config.patterns?.[pattern]; | ||
|  |     const limit = patternConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return { allowed: true }; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     let counter = this.patternCounters.get(pattern); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.patternCounters.set(pattern, counter); | ||
|  |        | ||
|  |       // Initialize pattern stats if needed
 | ||
|  |       if (!this.stats.byPattern[pattern]) { | ||
|  |         this.stats.byPattern[pattern] = { | ||
|  |           messagesPerMinute: 0, | ||
|  |           totalMessages: 0, | ||
|  |           totalBlocked: 0 | ||
|  |         }; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.count = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.count >= limit) { | ||
|  |       // Calculate reset time
 | ||
|  |       const resetIn = 60000 - (now - counter.lastReset); | ||
|  |        | ||
|  |       // Update statistics
 | ||
|  |       this.stats.byPattern[pattern].totalBlocked++; | ||
|  |       this.stats.totalBlocked++; | ||
|  |        | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: `Pattern "${pattern}" message rate limit exceeded`, | ||
|  |         limit, | ||
|  |         current: counter.count, | ||
|  |         resetIn | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.count++; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.stats.byPattern[pattern].messagesPerMinute = counter.count; | ||
|  |     this.stats.byPattern[pattern].totalMessages++; | ||
|  |      | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check domain-specific message rate limit | ||
|  |    * @param domain Domain to check | ||
|  |    */ | ||
|  |   private checkDomainMessageLimit(domain: string): IRateLimitResult { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Get domain-specific limit or use global
 | ||
|  |     const domainConfig = this.config.domains?.[domain]; | ||
|  |     const limit = domainConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return { allowed: true }; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     let counter = this.domainCounters.get(domain); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.domainCounters.set(domain, counter); | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.count = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.count >= limit) { | ||
|  |       // Calculate reset time
 | ||
|  |       const resetIn = 60000 - (now - counter.lastReset); | ||
|  |        | ||
|  |       logger.log('warn', `Domain ${domain} rate limit exceeded: ${counter.count}/${limit} messages per minute`); | ||
|  |        | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: `Domain "${domain}" message rate limit exceeded`, | ||
|  |         limit, | ||
|  |         current: counter.count, | ||
|  |         resetIn | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.count++; | ||
|  |      | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check IP-specific message rate limit | ||
|  |    * @param ip IP address | ||
|  |    */ | ||
|  |   private checkIpMessageLimit(ip: string): IRateLimitResult { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Get IP-specific limit or use global
 | ||
|  |     const ipConfig = this.config.ips?.[ip]; | ||
|  |     const limit = ipConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return { allowed: true }; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     let counter = this.ipCounters.get(ip); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.ipCounters.set(ip, counter); | ||
|  |        | ||
|  |       // Initialize IP stats if needed
 | ||
|  |       if (!this.stats.byIp[ip]) { | ||
|  |         this.stats.byIp[ip] = { | ||
|  |           messagesPerMinute: 0, | ||
|  |           totalMessages: 0, | ||
|  |           totalBlocked: 0, | ||
|  |           connections: 0, | ||
|  |           errors: 0, | ||
|  |           authFailures: 0, | ||
|  |           blocked: false | ||
|  |         }; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.count = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.count >= limit) { | ||
|  |       // Calculate reset time
 | ||
|  |       const resetIn = 60000 - (now - counter.lastReset); | ||
|  |        | ||
|  |       // Update statistics
 | ||
|  |       this.stats.byIp[ip].totalBlocked++; | ||
|  |       this.stats.totalBlocked++; | ||
|  |        | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: `IP ${ip} message rate limit exceeded`, | ||
|  |         limit, | ||
|  |         current: counter.count, | ||
|  |         resetIn | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.count++; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.stats.byIp[ip].messagesPerMinute = counter.count; | ||
|  |     this.stats.byIp[ip].totalMessages++; | ||
|  |      | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check recipient limit | ||
|  |    * @param email Email address | ||
|  |    * @param recipients Number of recipients | ||
|  |    * @param pattern Matched pattern | ||
|  |    * @param domain Domain name | ||
|  |    */ | ||
|  |   private checkRecipientLimit(email: string, recipients: number, pattern?: string, domain?: string): IRateLimitResult { | ||
|  |     // Get the most specific limit available
 | ||
|  |     let limit = this.config.global.maxRecipientsPerMessage!; | ||
|  |      | ||
|  |     // Check pattern-specific limit
 | ||
|  |     if (pattern && this.config.patterns?.[pattern]?.maxRecipientsPerMessage) { | ||
|  |       limit = this.config.patterns[pattern].maxRecipientsPerMessage!; | ||
|  |     } | ||
|  |      | ||
|  |     // Check domain-specific limit (overrides pattern if present)
 | ||
|  |     if (domain && this.config.domains?.[domain]?.maxRecipientsPerMessage) { | ||
|  |       limit = this.config.domains[domain].maxRecipientsPerMessage!; | ||
|  |     } | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return { allowed: true }; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (recipients > limit) { | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: 'Recipient limit exceeded', | ||
|  |         limit, | ||
|  |         current: recipients | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Record a connection from an IP | ||
|  |    * @param ip IP address | ||
|  |    * @returns Result of rate limit check | ||
|  |    */ | ||
|  |   public recordConnection(ip: string): IRateLimitResult { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Check if IP is blocked
 | ||
|  |     if (this.isIpBlocked(ip)) { | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: 'IP is blocked', | ||
|  |         resetIn: this.getBlockReleaseTime(ip) | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Get IP-specific limit or use global
 | ||
|  |     const ipConfig = this.config.ips?.[ip]; | ||
|  |     const limit = ipConfig?.maxConnectionsPerIP || this.config.global.maxConnectionsPerIP!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return { allowed: true }; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     let counter = this.ipCounters.get(ip); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.ipCounters.set(ip, counter); | ||
|  |        | ||
|  |       // Initialize IP stats if needed
 | ||
|  |       if (!this.stats.byIp[ip]) { | ||
|  |         this.stats.byIp[ip] = { | ||
|  |           messagesPerMinute: 0, | ||
|  |           totalMessages: 0, | ||
|  |           totalBlocked: 0, | ||
|  |           connections: 0, | ||
|  |           errors: 0, | ||
|  |           authFailures: 0, | ||
|  |           blocked: false | ||
|  |         }; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.connections = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.connections >= limit) { | ||
|  |       // Calculate reset time
 | ||
|  |       const resetIn = 60000 - (now - counter.lastReset); | ||
|  |        | ||
|  |       // Update statistics
 | ||
|  |       this.stats.byIp[ip].totalBlocked++; | ||
|  |       this.stats.totalBlocked++; | ||
|  |        | ||
|  |       return { | ||
|  |         allowed: false, | ||
|  |         reason: `IP ${ip} connection rate limit exceeded`, | ||
|  |         limit, | ||
|  |         current: counter.connections, | ||
|  |         resetIn | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.connections++; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.stats.byIp[ip].connections = counter.connections; | ||
|  |      | ||
|  |     return { allowed: true }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Record an error from an IP | ||
|  |    * @param ip IP address | ||
|  |    * @returns True if IP should be blocked | ||
|  |    */ | ||
|  |   public recordError(ip: string): boolean { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Get IP-specific limit or use global
 | ||
|  |     const ipConfig = this.config.ips?.[ip]; | ||
|  |     const limit = ipConfig?.maxErrorsPerIP || this.config.global.maxErrorsPerIP!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     let counter = this.ipCounters.get(ip); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.ipCounters.set(ip, counter); | ||
|  |        | ||
|  |       // Initialize IP stats if needed
 | ||
|  |       if (!this.stats.byIp[ip]) { | ||
|  |         this.stats.byIp[ip] = { | ||
|  |           messagesPerMinute: 0, | ||
|  |           totalMessages: 0, | ||
|  |           totalBlocked: 0, | ||
|  |           connections: 0, | ||
|  |           errors: 0, | ||
|  |           authFailures: 0, | ||
|  |           blocked: false | ||
|  |         }; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.errors = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.errors++; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.stats.byIp[ip].errors = counter.errors; | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.errors >= limit) { | ||
|  |       // Block the IP
 | ||
|  |       this.blockIp(ip); | ||
|  |        | ||
|  |       logger.log('warn', `IP ${ip} blocked due to excessive errors (${counter.errors}/${limit})`); | ||
|  |        | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.WARN, | ||
|  |         type: SecurityEventType.RATE_LIMITING, | ||
|  |         message: 'IP blocked due to excessive errors', | ||
|  |         ipAddress: ip, | ||
|  |         details: { | ||
|  |           errors: counter.errors, | ||
|  |           limit | ||
|  |         }, | ||
|  |         success: false | ||
|  |       }); | ||
|  |        | ||
|  |       return true; | ||
|  |     } | ||
|  |      | ||
|  |     return false; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Record an authentication failure from an IP | ||
|  |    * @param ip IP address | ||
|  |    * @returns True if IP should be blocked | ||
|  |    */ | ||
|  |   public recordAuthFailure(ip: string): boolean { | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     // Get IP-specific limit or use global
 | ||
|  |     const ipConfig = this.config.ips?.[ip]; | ||
|  |     const limit = ipConfig?.maxAuthFailuresPerIP || this.config.global.maxAuthFailuresPerIP!; | ||
|  |      | ||
|  |     if (!limit) { | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Get or create counter
 | ||
|  |     let counter = this.ipCounters.get(ip); | ||
|  |      | ||
|  |     if (!counter) { | ||
|  |       counter = { | ||
|  |         count: 0, | ||
|  |         lastReset: now, | ||
|  |         recipients: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         connections: 0 | ||
|  |       }; | ||
|  |       this.ipCounters.set(ip, counter); | ||
|  |        | ||
|  |       // Initialize IP stats if needed
 | ||
|  |       if (!this.stats.byIp[ip]) { | ||
|  |         this.stats.byIp[ip] = { | ||
|  |           messagesPerMinute: 0, | ||
|  |           totalMessages: 0, | ||
|  |           totalBlocked: 0, | ||
|  |           connections: 0, | ||
|  |           errors: 0, | ||
|  |           authFailures: 0, | ||
|  |           blocked: false | ||
|  |         }; | ||
|  |       } | ||
|  |     } | ||
|  |      | ||
|  |     // Check if counter needs to be reset
 | ||
|  |     if (now - counter.lastReset >= 60000) { | ||
|  |       counter.authFailures = 0; | ||
|  |       counter.lastReset = now; | ||
|  |     } | ||
|  |      | ||
|  |     // Increment counter
 | ||
|  |     counter.authFailures++; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     this.stats.byIp[ip].authFailures = counter.authFailures; | ||
|  |      | ||
|  |     // Check if limit is exceeded
 | ||
|  |     if (counter.authFailures >= limit) { | ||
|  |       // Block the IP
 | ||
|  |       this.blockIp(ip); | ||
|  |        | ||
|  |       logger.log('warn', `IP ${ip} blocked due to excessive authentication failures (${counter.authFailures}/${limit})`); | ||
|  |        | ||
|  |       SecurityLogger.getInstance().logEvent({ | ||
|  |         level: SecurityLogLevel.WARN, | ||
|  |         type: SecurityEventType.AUTHENTICATION, | ||
|  |         message: 'IP blocked due to excessive authentication failures', | ||
|  |         ipAddress: ip, | ||
|  |         details: { | ||
|  |           authFailures: counter.authFailures, | ||
|  |           limit | ||
|  |         }, | ||
|  |         success: false | ||
|  |       }); | ||
|  |        | ||
|  |       return true; | ||
|  |     } | ||
|  |      | ||
|  |     return false; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Block an IP address | ||
|  |    * @param ip IP address to block | ||
|  |    * @param duration Override the default block duration (milliseconds) | ||
|  |    */ | ||
|  |   public blockIp(ip: string, duration?: number): void { | ||
|  |     if (!this.config.blocks) { | ||
|  |       this.config.blocks = {}; | ||
|  |     } | ||
|  |      | ||
|  |     // Set block expiry time
 | ||
|  |     const expiry = Date.now() + (duration || this.config.global.blockDuration || 3600000); | ||
|  |     this.config.blocks[ip] = expiry; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     if (!this.stats.byIp[ip]) { | ||
|  |       this.stats.byIp[ip] = { | ||
|  |         messagesPerMinute: 0, | ||
|  |         totalMessages: 0, | ||
|  |         totalBlocked: 0, | ||
|  |         connections: 0, | ||
|  |         errors: 0, | ||
|  |         authFailures: 0, | ||
|  |         blocked: false | ||
|  |       }; | ||
|  |     } | ||
|  |     this.stats.byIp[ip].blocked = true; | ||
|  |     this.stats.currentlyBlocked++; | ||
|  |      | ||
|  |     // Emit event
 | ||
|  |     this.emit('ipBlocked', { | ||
|  |       ip, | ||
|  |       expiry, | ||
|  |       duration: duration || this.config.global.blockDuration | ||
|  |     }); | ||
|  |      | ||
|  |     logger.log('warn', `IP ${ip} blocked until ${new Date(expiry).toISOString()}`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Unblock an IP address | ||
|  |    * @param ip IP address to unblock | ||
|  |    */ | ||
|  |   public unblockIp(ip: string): void { | ||
|  |     if (!this.config.blocks) { | ||
|  |       return; | ||
|  |     } | ||
|  |      | ||
|  |     // Remove block
 | ||
|  |     delete this.config.blocks[ip]; | ||
|  |      | ||
|  |     // Update statistics
 | ||
|  |     if (this.stats.byIp[ip]) { | ||
|  |       this.stats.byIp[ip].blocked = false; | ||
|  |       this.stats.currentlyBlocked--; | ||
|  |     } | ||
|  |      | ||
|  |     // Emit event
 | ||
|  |     this.emit('ipUnblocked', { ip }); | ||
|  |      | ||
|  |     logger.log('info', `IP ${ip} unblocked`); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Check if an IP is blocked | ||
|  |    * @param ip IP address to check | ||
|  |    */ | ||
|  |   public isIpBlocked(ip: string): boolean { | ||
|  |     if (!this.config.blocks) { | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if IP is in blocks
 | ||
|  |     if (!(ip in this.config.blocks)) { | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     // Check if block has expired
 | ||
|  |     const expiry = this.config.blocks[ip]; | ||
|  |     if (expiry <= Date.now()) { | ||
|  |       // Remove expired block
 | ||
|  |       delete this.config.blocks[ip]; | ||
|  |        | ||
|  |       // Update statistics
 | ||
|  |       if (this.stats.byIp[ip]) { | ||
|  |         this.stats.byIp[ip].blocked = false; | ||
|  |         this.stats.currentlyBlocked--; | ||
|  |       } | ||
|  |        | ||
|  |       return false; | ||
|  |     } | ||
|  |      | ||
|  |     return true; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get the time until a block is released | ||
|  |    * @param ip IP address | ||
|  |    * @returns Milliseconds until release or 0 if not blocked | ||
|  |    */ | ||
|  |   public getBlockReleaseTime(ip: string): number { | ||
|  |     if (!this.config.blocks || !(ip in this.config.blocks)) { | ||
|  |       return 0; | ||
|  |     } | ||
|  |      | ||
|  |     const expiry = this.config.blocks[ip]; | ||
|  |     const now = Date.now(); | ||
|  |      | ||
|  |     return expiry > now ? expiry - now : 0; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Update rate limiter statistics | ||
|  |    */ | ||
|  |   private updateStats(): void { | ||
|  |     // Update active counters count
 | ||
|  |     this.stats.activeCounters = this.counters.size + this.patternCounters.size + this.ipCounters.size; | ||
|  |      | ||
|  |     // Emit statistics update
 | ||
|  |     this.emit('statsUpdated', this.stats); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get rate limiter statistics | ||
|  |    */ | ||
|  |   public getStats(): IRateLimiterStats { | ||
|  |     return { ...this.stats }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Update rate limiter configuration | ||
|  |    * @param config New configuration | ||
|  |    */ | ||
|  |   public updateConfig(config: Partial<IHierarchicalRateLimits>): void { | ||
|  |     if (config.global) { | ||
|  |       this.config.global = { | ||
|  |         ...this.config.global, | ||
|  |         ...config.global | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     if (config.patterns) { | ||
|  |       this.config.patterns = { | ||
|  |         ...this.config.patterns, | ||
|  |         ...config.patterns | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     if (config.ips) { | ||
|  |       this.config.ips = { | ||
|  |         ...this.config.ips, | ||
|  |         ...config.ips | ||
|  |       }; | ||
|  |     } | ||
|  |      | ||
|  |     logger.log('info', 'Rate limiter configuration updated'); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get configuration for debugging | ||
|  |    */ | ||
|  |   public getConfig(): IHierarchicalRateLimits { | ||
|  |     return { ...this.config }; | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Apply domain-specific rate limits | ||
|  |    * Merges domain limits with existing configuration | ||
|  |    * @param domain Domain name | ||
|  |    * @param limits Rate limit configuration for the domain | ||
|  |    */ | ||
|  |   public applyDomainLimits(domain: string, limits: IRateLimitConfig): void { | ||
|  |     if (!this.config.domains) { | ||
|  |       this.config.domains = {}; | ||
|  |     } | ||
|  |      | ||
|  |     // Merge the limits with any existing domain config
 | ||
|  |     this.config.domains[domain] = { | ||
|  |       ...this.config.domains[domain], | ||
|  |       ...limits | ||
|  |     }; | ||
|  |      | ||
|  |     logger.log('info', `Applied rate limits for domain ${domain}:`, limits); | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Remove domain-specific rate limits | ||
|  |    * @param domain Domain name | ||
|  |    */ | ||
|  |   public removeDomainLimits(domain: string): void { | ||
|  |     if (this.config.domains && this.config.domains[domain]) { | ||
|  |       delete this.config.domains[domain]; | ||
|  |       // Also remove the counter
 | ||
|  |       this.domainCounters.delete(domain); | ||
|  |       logger.log('info', `Removed rate limits for domain ${domain}`); | ||
|  |     } | ||
|  |   } | ||
|  |    | ||
|  |   /** | ||
|  |    * Get domain-specific rate limits | ||
|  |    * @param domain Domain name | ||
|  |    * @returns Domain rate limit config or undefined | ||
|  |    */ | ||
|  |   public getDomainLimits(domain: string): IRateLimitConfig | undefined { | ||
|  |     return this.config.domains?.[domain]; | ||
|  |   } | ||
|  | } |