2025-05-08 01:13:54 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
2025-05-08 00:39:43 +00:00
|
|
|
import { EventEmitter } from 'node:events';
|
2025-05-08 01:13:54 +00:00
|
|
|
import { logger } from '../../logger.js';
|
|
|
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
2025-05-08 00:39:43 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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>;
|
|
|
|
|
|
|
|
// 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 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
* @returns Result of rate limit check
|
|
|
|
*/
|
|
|
|
public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: 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 IP-specific limit
|
|
|
|
const ipResult = this.checkIpMessageLimit(ip);
|
|
|
|
if (!ipResult.allowed) {
|
|
|
|
return ipResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check recipient limit
|
|
|
|
const recipientResult = this.checkRecipientLimit(email, recipients, pattern);
|
|
|
|
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 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
|
|
|
|
*/
|
|
|
|
private checkRecipientLimit(email: string, recipients: number, pattern?: string): IRateLimitResult {
|
|
|
|
// Get pattern-specific limit if available
|
|
|
|
let limit = this.config.global.maxRecipientsPerMessage!;
|
|
|
|
|
|
|
|
if (pattern && this.config.patterns?.[pattern]?.maxRecipientsPerMessage) {
|
|
|
|
limit = this.config.patterns[pattern].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 };
|
|
|
|
}
|
|
|
|
}
|