Spaces:
Sleeping
Sleeping
File size: 2,606 Bytes
ed57015 | 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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | import type { Request, Response, NextFunction } from 'express';
// Per-IP fixed-window rate limiter for the public /v1 proxy (#35, item #6).
//
// The /v1 surface authenticates with the unified API key but has no password
// login like the dashboard does, so without this an attacker who can reach the
// server could brute-force the key or flood upstream providers. This caps how
// many requests a single client IP can make per minute and returns a standard
// OpenAI-shaped 429 once the cap is exceeded.
//
// FreeLLMAPI is a single-user tool, so the default ceiling is generous. Tune it
// with PROXY_RATE_LIMIT_RPM (requests per minute per IP); set it to 0 to turn
// rate limiting off entirely.
const WINDOW_MS = 60_000;
const DEFAULT_RPM = 120;
// Bound the IP map so a flood of distinct (e.g. spoofed) source addresses can't
// grow it without limit; expired entries are pruned opportunistically.
const MAX_TRACKED_IPS = 10_000;
interface WindowState {
count: number;
resetAt: number;
}
function parseLimit(): number {
const raw = process.env.PROXY_RATE_LIMIT_RPM;
if (raw === undefined || raw.trim() === '') return DEFAULT_RPM;
const n = Number(raw);
if (!Number.isFinite(n) || n < 0) return DEFAULT_RPM;
return Math.floor(n);
}
export function createProxyRateLimiter() {
const limit = parseLimit();
const windows = new Map<string, WindowState>();
return function proxyRateLimit(req: Request, res: Response, next: NextFunction): void {
if (limit === 0) {
next();
return;
}
const now = Date.now();
const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';
let state = windows.get(ip);
if (!state || now >= state.resetAt) {
state = { count: 0, resetAt: now + WINDOW_MS };
windows.set(ip, state);
}
state.count += 1;
if (windows.size > MAX_TRACKED_IPS) {
for (const [key, value] of windows) {
if (now >= value.resetAt) windows.delete(key);
}
}
res.setHeader('X-RateLimit-Limit', String(limit));
res.setHeader('X-RateLimit-Remaining', String(Math.max(0, limit - state.count)));
res.setHeader('X-RateLimit-Reset', String(Math.ceil(state.resetAt / 1000)));
if (state.count > limit) {
const retryAfter = Math.max(1, Math.ceil((state.resetAt - now) / 1000));
res.setHeader('Retry-After', String(retryAfter));
res.status(429).json({
error: {
message: `Rate limit exceeded: more than ${limit} requests per minute. Retry in ${retryAfter}s.`,
type: 'rate_limit_error',
},
});
return;
}
next();
};
}
|