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;
}