241 lines
16 KiB
JavaScript
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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
|