1// Limits for the forge's Node servers (lib/private-server.ts,2// lib/events-server.ts, lib/pages-server.ts): who a request comes from, how3// often someone may do something, and how long a request may take to arrive.4import type http from 'node:http';56// The address a request comes from. The servers listen on 127.0.0.1 only,7// behind Caddy, which sets X-Forge-Client-IP to the client it saw8// ({client_ip}: behind the edge, Fly-Client-IP; bin/forge server9// caddyfile, server/container/start.sh), replacing any the client sent.10// Without Caddy (demo/serve.ts, scenarios), the socket's.11export function clientIp(req: http.IncomingMessage): string {12 const h = req.headers['x-forge-client-ip'];13 const v = (Array.isArray(h) ? h[0] : h)?.trim();14 return v && v.length <= 64 ? v : req.socket.remoteAddress ?? '';15}1617// At most `max` hits per key in any `ms`. At most `keys` keys are kept,18// the least recently counted going first, so a flood of new keys costs19// memory only up to there.20export class Window {21 private hits = new Map<string, number[]>();22 private max: number;23 private ms: number;24 private keys: number;25 constructor(max: number, ms: number, keys = 10000) {26 this.max = max;27 this.ms = ms;28 this.keys = keys;29 }3031 // Whether key may have one more now; if so, it is counted.32 take(key: string, t = Date.now()): boolean {33 const mine = (this.hits.get(key) ?? []).filter((x) => t - x < this.ms);34 if (mine.length >= this.max) return false;35 mine.push(t);36 this.hits.delete(key);37 this.hits.set(key, mine);38 while (this.hits.size > this.keys) this.hits.delete(this.hits.keys().next().value!);39 return true;40 }41}4243// How long a request's headers, and the whole request, may take to arrive:44// a client that trickles them in holds a socket no longer than this. (A45// response, such as the event stream, may go on as long as it likes.)46export function timeouts(server: http.Server): http.Server {47 server.headersTimeout = 10_000;48 server.requestTimeout = 30_000;49 return server;50}