File size: 1,384 Bytes
87cb242 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | import { IncomingMessage } from 'http';
/**
* Simple in-process token bucket rate limiter for WS upgrade requests.
* Prevents a single IP from opening thousands of connections on HF free tier.
*
* Limits: 20 connections per IP per 60 seconds.
*/
interface Bucket {
tokens: number;
lastRefill: number;
}
const CAPACITY = 20;
const REFILL_RATE = 20; // tokens per window
const WINDOW_MS = 60_000; // 60 seconds
const buckets = new Map<string, Bucket>();
// Prune old entries every 5 minutes to prevent memory leak
setInterval(() => {
const cutoff = Date.now() - WINDOW_MS * 2;
for (const [ip, bucket] of buckets) {
if (bucket.lastRefill < cutoff) buckets.delete(ip);
}
}, 300_000).unref();
export function isRateLimited(req: IncomingMessage): boolean {
const ip =
(req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() ??
req.socket.remoteAddress ??
'unknown';
const now = Date.now();
let bucket = buckets.get(ip);
if (!bucket) {
bucket = { tokens: CAPACITY - 1, lastRefill: now };
buckets.set(ip, bucket);
return false;
}
// Refill based on elapsed time
const elapsed = now - bucket.lastRefill;
if (elapsed >= WINDOW_MS) {
bucket.tokens = CAPACITY - 1;
bucket.lastRefill = now;
return false;
}
if (bucket.tokens <= 0) return true;
bucket.tokens--;
return false;
}
|