/**
* Simple Rate Limiting Middleware
* In-memory rate limiter for protecting against DoS
*/
import http from 'http';
interface RateLimitEntry {
count: number;
resetTime: number;
}
export class RateLimiter {
private requests: Map<string, RateLimitEntry>;
private cleanupInterval: NodeJS.Timeout | null;
constructor(
private readonly maxRequests: number = 100,
private readonly windowMs: number = 15 * 60 * 1000 // 15 minutes
) {
this.requests = new Map();
this.cleanupInterval = null;
this.startCleanup();
}
private startCleanup(): void {
// Clean up expired entries every 5 minutes
this.cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.requests.entries()) {
if (now > entry.resetTime) {
this.requests.delete(key);
}
}
}, 5 * 60 * 1000);
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
}
/**
* Check if request should be rate limited
* Returns true if request should be blocked
*/
public check(identifier: string): boolean {
const now = Date.now();
let entry = this.requests.get(identifier);
if (!entry || now > entry.resetTime) {
// New window
entry = {
count: 1,
resetTime: now + this.windowMs,
};
this.requests.set(identifier, entry);
return false;
}
entry.count++;
if (entry.count > this.maxRequests) {
return true; // Rate limited
}
return false;
}
/**
* Get remaining requests for an identifier
*/
public getRemaining(identifier: string): number {
const entry = this.requests.get(identifier);
if (!entry || Date.now() > entry.resetTime) {
return this.maxRequests;
}
return Math.max(0, this.maxRequests - entry.count);
}
/**
* Get reset time for an identifier
*/
public getResetTime(identifier: string): number | null {
const entry = this.requests.get(identifier);
if (!entry || Date.now() > entry.resetTime) {
return null;
}
return entry.resetTime;
}
public destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.requests.clear();
}
}
/**
* Get client identifier from request
*/
export function getClientIdentifier(req: http.IncomingMessage): string {
// Try to get real IP from headers (for proxies)
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
const ips = typeof forwarded === 'string' ? forwarded.split(',') : forwarded;
return ips[0].trim();
}
return req.socket.remoteAddress || 'unknown';
}
/**
* Send rate limit error response
*/
export function sendRateLimitError(
res: http.ServerResponse,
resetTime: number | null
): void {
const retryAfter = resetTime ? Math.ceil((resetTime - Date.now()) / 1000) : 900;
res.setHeader('Retry-After', retryAfter.toString());
res.setHeader('X-RateLimit-Limit', '100');
res.setHeader('X-RateLimit-Remaining', '0');
res.writeHead(429, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
error: 'Too many requests',
message: 'You have exceeded the rate limit. Please try again later.',
retryAfter,
})
);
}