820 lines
54 KiB
JavaScript
820 lines
54 KiB
JavaScript
import * as plugins from '../../plugins.js';
|
|
import { EventEmitter } from 'node:events';
|
|
import { logger } from '../../logger.js';
|
|
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
|
/**
|
|
* Unified rate limiter for all email processing modes
|
|
*/
|
|
export class UnifiedRateLimiter extends EventEmitter {
|
|
config;
|
|
counters = new Map();
|
|
patternCounters = new Map();
|
|
ipCounters = new Map();
|
|
domainCounters = new Map();
|
|
cleanupInterval;
|
|
stats;
|
|
/**
|
|
* Create a new unified rate limiter
|
|
* @param config Rate limit configuration
|
|
*/
|
|
constructor(config) {
|
|
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
|
|
*/
|
|
startCleanupInterval() {
|
|
if (this.cleanupInterval) {
|
|
clearInterval(this.cleanupInterval);
|
|
}
|
|
// Run cleanup every minute
|
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
|
}
|
|
/**
|
|
* Stop the cleanup interval
|
|
*/
|
|
stop() {
|
|
if (this.cleanupInterval) {
|
|
clearInterval(this.cleanupInterval);
|
|
this.cleanupInterval = undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Destroy the rate limiter and clean up all resources
|
|
*/
|
|
destroy() {
|
|
// 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
|
|
*/
|
|
cleanup() {
|
|
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
|
|
*/
|
|
checkMessageLimit(email, ip, recipients, pattern, domain) {
|
|
// 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
|
|
*/
|
|
checkGlobalMessageLimit(email) {
|
|
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
|
|
*/
|
|
checkPatternMessageLimit(pattern) {
|
|
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
|
|
*/
|
|
checkDomainMessageLimit(domain) {
|
|
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
|
|
*/
|
|
checkIpMessageLimit(ip) {
|
|
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
|
|
*/
|
|
checkRecipientLimit(email, recipients, pattern, domain) {
|
|
// 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
|
|
*/
|
|
recordConnection(ip) {
|
|
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
|
|
*/
|
|
recordError(ip) {
|
|
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
|
|
*/
|
|
recordAuthFailure(ip) {
|
|
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)
|
|
*/
|
|
blockIp(ip, duration) {
|
|
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
|
|
*/
|
|
unblockIp(ip) {
|
|
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
|
|
*/
|
|
isIpBlocked(ip) {
|
|
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
|
|
*/
|
|
getBlockReleaseTime(ip) {
|
|
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
|
|
*/
|
|
updateStats() {
|
|
// 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
|
|
*/
|
|
getStats() {
|
|
return { ...this.stats };
|
|
}
|
|
/**
|
|
* Update rate limiter configuration
|
|
* @param config New configuration
|
|
*/
|
|
updateConfig(config) {
|
|
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
|
|
*/
|
|
getConfig() {
|
|
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
|
|
*/
|
|
applyDomainLimits(domain, limits) {
|
|
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
|
|
*/
|
|
removeDomainLimits(domain) {
|
|
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
|
|
*/
|
|
getDomainLimits(domain) {
|
|
return this.config.domains?.[domain];
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy51bmlmaWVkLnJhdGUubGltaXRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvY2xhc3Nlcy51bmlmaWVkLnJhdGUubGltaXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDM0MsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3pDLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQWdGOUY7O0dBRUc7QUFDSCxNQUFNLE9BQU8sa0JBQW1CLFNBQVEsWUFBWTtJQUMxQyxNQUFNLENBQTBCO0lBQ2hDLFFBQVEsR0FBK0IsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUNqRCxlQUFlLEdBQStCLElBQUksR0FBRyxFQUFFLENBQUM7SUFDeEQsVUFBVSxHQUErQixJQUFJLEdBQUcsRUFBRSxDQUFDO0lBQ25ELGNBQWMsR0FBK0IsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUN2RCxlQUFlLENBQWtCO0lBQ2pDLEtBQUssQ0FBb0I7SUFFakM7OztPQUdHO0lBQ0gsWUFBWSxNQUErQjtRQUN6QyxLQUFLLEVBQUUsQ0FBQztRQUVSLDRCQUE0QjtRQUM1QixJQUFJLENBQUMsTUFBTSxHQUFHO1lBQ1osTUFBTSxFQUFFO2dCQUNOLG9CQUFvQixFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsb0JBQW9CLElBQUksR0FBRztnQkFDL0QsdUJBQXVCLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyx1QkFBdUIsSUFBSSxHQUFHO2dCQUNyRSxtQkFBbUIsRUFBRSxNQUFNLENBQUMsTUFBTSxDQUFDLG1CQUFtQixJQUFJLEVBQUU7Z0JBQzVELGNBQWMsRUFBRSxNQUFNLENBQUMsTUFBTSxD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
|