from fastapi import Request def client_ip(request: Request) -> str: """The caller's real IP, from Caddy's X-Forwarded-For (see Caddyfile) — request.client.host would otherwise be the reverse proxy's own address, not the caller's. Falls back to request.client.host only if the header is somehow missing (e.g. the app container hit directly, bypassing Caddy). Shared by the login/registration throttles (B-33) and the SSE per-IP subscriber cap (B-38) so the two can't drift into different notions of "the client's IP". B-54: the *last* element, not the first. A proxy appends the address it saw to any X-Forwarded-For the client already sent, so the first element is attacker-controlled — with the header read from the front, rotating a fake value per request gave every request a fresh identity and turned all three IP-keyed controls above into decoration. The last element is the one written by the hop closest to us, i.e. by our own proxy. Exactly one trusted proxy sits in front of this app (Caddy, see docker-compose.yml, where `app` is only `expose`d on the compose network and never published to the host), so the last element is the real peer. The Caddyfile now also overwrites the header with `header_up X-Forwarded-For {remote_host}`, which collapses it to a single value — belt and braces: either fix alone closes B-54. """ forwarded = request.headers.get("x-forwarded-for") if forwarded: hops = [hop.strip() for hop in forwarded.split(",") if hop.strip()] if hops: return hops[-1] return request.client.host if request.client else "unknown"