Files
smartmta/dist_ts/mail/delivery/classes.ratelimiter.js
2026-02-10 15:54:09 +00:00

241 lines
16 KiB
JavaScript

import { logger } from '../../logger.js';
/**
* Rate limiter using token bucket algorithm
* Provides more sophisticated rate limiting with burst handling
*/
export class RateLimiter {
/** Rate limit configuration */
config;
/** Token buckets per key */
buckets = new Map();
/** Global bucket for non-keyed rate limiting */
globalBucket;
/**
* Create a new rate limiter
* @param config Rate limiter configuration
*/
constructor(config) {
// Set defaults
this.config = {
maxPerPeriod: config.maxPerPeriod,
periodMs: config.periodMs,
perKey: config.perKey ?? true,
initialTokens: config.initialTokens ?? config.maxPerPeriod,
burstTokens: config.burstTokens ?? 0,
useGlobalLimit: config.useGlobalLimit ?? false
};
// Initialize global bucket
this.globalBucket = {
tokens: this.config.initialTokens,
lastRefill: Date.now(),
allowed: 0,
denied: 0,
errors: 0,
firstErrorTime: 0
};
// Log initialization
logger.log('info', `Rate limiter initialized: ${this.config.maxPerPeriod} per ${this.config.periodMs}ms${this.config.perKey ? ' per key' : ''}`);
}
/**
* Check if a request is allowed under rate limits
* @param key Key to check rate limit for (e.g. domain, user, IP)
* @param cost Token cost (defaults to 1)
* @returns Whether the request is allowed
*/
isAllowed(key = 'global', cost = 1) {
// If using global bucket directly, just check that
if (key === 'global' || !this.config.perKey) {
return this.checkBucket(this.globalBucket, cost);
}
// Get the key-specific bucket
const bucket = this.getBucket(key);
// If we also need to check global limit
if (this.config.useGlobalLimit) {
// Both key bucket and global bucket must have tokens
return this.checkBucket(bucket, cost) && this.checkBucket(this.globalBucket, cost);
}
else {
// Only need to check the key-specific bucket
return this.checkBucket(bucket, cost);
}
}
/**
* Check if a bucket has enough tokens and consume them
* @param bucket The token bucket to check
* @param cost Token cost
* @returns Whether tokens were consumed
*/
checkBucket(bucket, cost) {
// Refill tokens based on elapsed time
this.refillBucket(bucket);
// Check if we have enough tokens
if (bucket.tokens >= cost) {
// Use tokens
bucket.tokens -= cost;
bucket.allowed++;
return true;
}
else {
// Rate limit exceeded
bucket.denied++;
return false;
}
}
/**
* Consume tokens for a request (if available)
* @param key Key to consume tokens for
* @param cost Token cost (defaults to 1)
* @returns Whether tokens were consumed
*/
consume(key = 'global', cost = 1) {
const isAllowed = this.isAllowed(key, cost);
return isAllowed;
}
/**
* Get the remaining tokens for a key
* @param key Key to check
* @returns Number of remaining tokens
*/
getRemainingTokens(key = 'global') {
const bucket = this.getBucket(key);
this.refillBucket(bucket);
return bucket.tokens;
}
/**
* Get stats for a specific key
* @param key Key to get stats for
* @returns Rate limit statistics
*/
getStats(key = 'global') {
const bucket = this.getBucket(key);
this.refillBucket(bucket);
// Calculate time until next token
const resetIn = bucket.tokens < this.config.maxPerPeriod ?
Math.ceil(this.config.periodMs / this.config.maxPerPeriod) :
0;
return {
remaining: bucket.tokens,
limit: this.config.maxPerPeriod,
resetIn,
allowed: bucket.allowed,
denied: bucket.denied
};
}
/**
* Get or create a token bucket for a key
* @param key The rate limit key
* @returns Token bucket
*/
getBucket(key) {
if (!this.config.perKey || key === 'global') {
return this.globalBucket;
}
if (!this.buckets.has(key)) {
// Create new bucket
this.buckets.set(key, {
tokens: this.config.initialTokens,
lastRefill: Date.now(),
allowed: 0,
denied: 0,
errors: 0,
firstErrorTime: 0
});
}
return this.buckets.get(key);
}
/**
* Refill tokens in a bucket based on elapsed time
* @param bucket Token bucket to refill
*/
refillBucket(bucket) {
const now = Date.now();
const elapsedMs = now - bucket.lastRefill;
// Calculate how many tokens to add
const rate = this.config.maxPerPeriod / this.config.periodMs;
const tokensToAdd = elapsedMs * rate;
if (tokensToAdd >= 0.1) { // Allow for partial token refills
// Add tokens, but don't exceed the normal maximum (without burst)
// This ensures burst tokens are only used for bursts and don't refill
const normalMax = this.config.maxPerPeriod;
bucket.tokens = Math.min(
// Don't exceed max + burst
this.config.maxPerPeriod + (this.config.burstTokens || 0),
// Don't exceed normal max when refilling
Math.min(normalMax, bucket.tokens + tokensToAdd));
// Update last refill time
bucket.lastRefill = now;
}
}
/**
* Reset rate limits for a specific key
* @param key Key to reset
*/
reset(key = 'global') {
if (key === 'global' || !this.config.perKey) {
this.globalBucket.tokens = this.config.initialTokens;
this.globalBucket.lastRefill = Date.now();
}
else if (this.buckets.has(key)) {
const bucket = this.buckets.get(key);
bucket.tokens = this.config.initialTokens;
bucket.lastRefill = Date.now();
}
}
/**
* Reset all rate limiters
*/
resetAll() {
this.globalBucket.tokens = this.config.initialTokens;
this.globalBucket.lastRefill = Date.now();
for (const bucket of this.buckets.values()) {
bucket.tokens = this.config.initialTokens;
bucket.lastRefill = Date.now();
}
}
/**
* Cleanup old buckets to prevent memory leaks
* @param maxAge Maximum age in milliseconds
*/
cleanup(maxAge = 24 * 60 * 60 * 1000) {
const now = Date.now();
let removed = 0;
for (const [key, bucket] of this.buckets.entries()) {
if (now - bucket.lastRefill > maxAge) {
this.buckets.delete(key);
removed++;
}
}
if (removed > 0) {
logger.log('debug', `Cleaned up ${removed} stale rate limit buckets`);
}
}
/**
* Record an error for a key (e.g., IP address) and determine if blocking is needed
* RFC 5321 Section 4.5.4.1 suggests limiting errors to prevent abuse
*
* @param key Key to record error for (typically an IP address)
* @param errorWindow Time window for error tracking in ms (default: 5 minutes)
* @param errorThreshold Maximum errors before blocking (default: 10)
* @returns true if the key should be blocked due to excessive errors
*/
recordError(key, errorWindow = 5 * 60 * 1000, errorThreshold = 10) {
const bucket = this.getBucket(key);
const now = Date.now();
// Reset error count if the time window has expired
if (bucket.firstErrorTime === 0 || now - bucket.firstErrorTime > errorWindow) {
bucket.errors = 0;
bucket.firstErrorTime = now;
}
// Increment error count
bucket.errors++;
// Log error tracking
logger.log('debug', `Error recorded for ${key}: ${bucket.errors}/${errorThreshold} in window`);
// Check if threshold exceeded
if (bucket.errors >= errorThreshold) {
logger.log('warn', `Error threshold exceeded for ${key}: ${bucket.errors} errors`);
return true; // Should block
}
return false; // Continue allowing
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5yYXRlbGltaXRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvZGVsaXZlcnkvY2xhc3Nlcy5yYXRlbGltaXRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFnRHpDOzs7R0FHRztBQUNILE1BQU0sT0FBTyxXQUFXO0lBQ3RCLCtCQUErQjtJQUN2QixNQUFNLENBQW1CO0lBRWpDLDRCQUE0QjtJQUNwQixPQUFPLEdBQTZCLElBQUksR0FBRyxFQUFFLENBQUM7SUFFdEQsZ0RBQWdEO0lBQ3hDLFlBQVksQ0FBYztJQUVsQzs7O09BR0c7SUFDSCxZQUFZLE1BQXdCO1FBQ2xDLGVBQWU7UUFDZixJQUFJLENBQUMsTUFBTSxHQUFHO1lBQ1osWUFBWSxFQUFFLE1BQU0sQ0FBQyxZQUFZO1lBQ2pDLFFBQVEsRUFBRSxNQUFNLENBQUMsUUFBUTtZQUN6QixNQUFNLEVBQUUsTUFBTSxDQUFDLE1BQU0sSUFBSSxJQUFJO1lBQzdCLGFBQWEsRUFBRSxNQUFNLENBQUMsYUFBYSxJQUFJLE1BQU0sQ0FBQyxZQUFZO1lBQzFELFdBQVcsRUFBRSxNQUFNLENBQUMsV0FBVyxJQUFJLENBQUM7WUFDcEMsY0FBYyxFQUFFLE1BQU0sQ0FBQyxjQUFjLElBQUksS0FBSztTQUMvQyxDQUFDO1FBRUYsMkJBQTJCO1FBQzNCLElBQUksQ0FBQyxZQUFZLEdBQUc7WUFDbEIsTUFBTSxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsYUFBYTtZQUNqQyxVQUFVLEVBQUUsSUFBSSxDQUFDLEdBQUcsRUFBRTtZQUN0QixPQUFPLEVBQUUsQ0FBQztZQUNWLE1BQU0sRUFBRSxDQUFDO1lBQ1QsTUFBTSxFQUFFLENBQUM7WUFDVCxjQUFjLEVBQUUsQ0FBQztTQUNsQixDQUFDO1FBRUYscUJBQXFCO1FBQ3JCLE1BQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxFQUFFLDZC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