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];
 | |
|   }
 | |
| } |