-- porch/middleware/limiter.wo — rate limiter middleware backed by @table. -- Fixed-window counter with hybrid key (net.peer default, client_ip when verified proxy). -- Iteration 1 of the porch track. use time use http use net -- Limiter counts requests per key per window. -- On limit exceeded: returns 429 with Retry-After and X-RateLimit headers. -- Key selection: if req.ctx["verified_proxy"] == "true" → client_ip(req) (XFF left-most), -- else → net.peer(req.conn) (unforgeable "ip:port" or "unix"). -- Falls back to "principal:" when authenticated. pub class Limiter { max: Int window: Int -- window size in µs (e.g., 60_000_000 = 60s) fn before(mut req: Req) -> ?Resp { let key = limiter_key(req); let now = time.ticks(); -- Read existing counter let hits = from c in RateLimitCounter where c.key == key take 1 select c; let counter = RateLimitCounter { key: key, count: 0, window: now }; let is_new_window = false; if len(hits) == 0 { is_new_window = true; } else { counter = hits[0]; -- Check if window has elapsed if now - counter.window > self.window { counter.count = 0; counter.window = now; is_new_window = true; } } -- Increment and check limit counter.count = counter.count + 1; -- Headers for both allowed and limited responses let limit_hdr = "${self.max}"; let remaining = self.max - counter.count; if remaining < 0 { remaining = 0; } let remaining_hdr = "${remaining}"; let reset_sec = (counter.window + self.window) / 1_000_000; let reset_hdr = "${reset_sec}"; -- Store the counter (insert new or delete+insert for update) if is_new_window { try insert RateLimitCounter { key: counter.key, count: counter.count, window: counter.window } catch (e) nil; } else { -- Update existing: delete old, insert new let to_delete = from c in RateLimitCounter where c.key == key take 1 select c; if len(to_delete) > 0 { delete to_delete[0]; } try insert RateLimitCounter { key: counter.key, count: counter.count, window: counter.window } catch (e) nil; } -- Check limit if counter.count > self.max { let r = Resp { status: 429, headers: {}, body: "{\"error\":\"rate limit exceeded\"}" }; set_header(r, "content-type", "application/json"); set_header(r, "x-ratelimit-limit", limit_hdr); set_header(r, "x-ratelimit-remaining", "0"); set_header(r, "x-ratelimit-reset", reset_hdr); set_header(r, "retry-after", "${((counter.window + self.window - now) / 1_000_000) + 1}"); return r; } -- Allowed: attach headers to request for after-chain to stamp on response req.ctx["ratelimit_limit"] = limit_hdr; req.ctx["ratelimit_remaining"] = remaining_hdr; req.ctx["ratelimit_reset"] = reset_hdr; return nil; } } -- Default hybrid key function: net.peer unless verified proxy pub fn limiter_key(req: Req) -> Text { if req.ctx["verified_proxy"] == "true" { let ip = client_ip(req); if ip != "" { return "ip:${ip}"; } } -- net.Conn is a scalar (fd), so req.conn should work directly as the fd argument let peer = net.peer(req.conn); if peer != "" { return "ip:${peer}"; } if req.principal != "" { return "principal:${req.principal}"; } return "unknown"; } -- Helper to build a Limiter with defaults pub fn make_limiter(max: Int, window_sec: Int) -> Limiter { return Limiter { max: max, window: window_sec * 1_000_000 }; }