feat(mail/delivery): add error-count based blocking to rate limiter; improve test SMTP server port selection; add tsbuild scripts and devDependency; remove stale backup file
This commit is contained in:
@@ -29,15 +29,21 @@ export interface IRateLimitConfig {
|
||||
interface TokenBucket {
|
||||
/** Current number of tokens */
|
||||
tokens: number;
|
||||
|
||||
|
||||
/** Last time tokens were refilled */
|
||||
lastRefill: number;
|
||||
|
||||
|
||||
/** Total allowed requests */
|
||||
allowed: number;
|
||||
|
||||
|
||||
/** Total denied requests */
|
||||
denied: number;
|
||||
|
||||
/** Error count for blocking decisions */
|
||||
errors: number;
|
||||
|
||||
/** Timestamp of first error in current window */
|
||||
firstErrorTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +80,9 @@ export class RateLimiter {
|
||||
tokens: this.config.initialTokens,
|
||||
lastRefill: Date.now(),
|
||||
allowed: 0,
|
||||
denied: 0
|
||||
denied: 0,
|
||||
errors: 0,
|
||||
firstErrorTime: 0
|
||||
};
|
||||
|
||||
// Log initialization
|
||||
@@ -196,7 +204,9 @@ export class RateLimiter {
|
||||
tokens: this.config.initialTokens,
|
||||
lastRefill: Date.now(),
|
||||
allowed: 0,
|
||||
denied: 0
|
||||
denied: 0,
|
||||
errors: 0,
|
||||
firstErrorTime: 0
|
||||
});
|
||||
}
|
||||
|
||||
@@ -266,16 +276,50 @@ export class RateLimiter {
|
||||
public cleanup(maxAge: number = 24 * 60 * 60 * 1000): void {
|
||||
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
|
||||
*/
|
||||
public recordError(key: string, errorWindow: number = 5 * 60 * 1000, errorThreshold: number = 10): boolean {
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user