"""Reading what the proxies in front of this app say about a request. Two headers carry information the app cannot see for itself — who the client is (`X-Forwarded-For`) and whether they arrived over TLS (`X-Forwarded-Proto`) — and both are trusted by the same rule, so the rule lives in one place. Writing it twice is precisely how issue 2183 happened: two places holding one decision, and only one of them updated. ## The rule A forwarding header grows LEFT to RIGHT. Each hop appends what IT saw, so the rightmost entries are the ones our own infrastructure wrote, and anything a caller sent arrives to the LEFT of those. That inverts the intuitive reading. The leftmost entry is nominally "the original client" — and is exactly the one a caller can forge, by sending the header themselves. So we count in from the right by the number of proxies we actually run (the **Trusted proxy hops** setting, default 1), and a forged prefix can never be selected no matter how much of it there is. Too HIGH a hop count is the dangerous direction: it starts believing entries no proxy of ours wrote. Too low just means several callers share a bucket. So when the header is shorter than configured — fewer proxies than expected — we fall back to the socket address rather than reaching further left. """ from __future__ import annotations from quart import has_request_context, request from .settings import live def trusted_entry(header: str, hops: int) -> str | None: """The nth-from-the-right entry of a forwarding header, or None if there isn't one. Pure, so the trust boundary is testable without a request context. """ if hops <= 0: return None entries = [part.strip() for part in header.split(",") if part.strip()] if len(entries) < hops: return None return entries[-hops] def forwarded_for(header: str, remote_addr: str | None, hops: int) -> str: """The client address a proxy chain vouches for, else this connection's peer.""" entry = trusted_entry(header, hops) return (entry or remote_addr or "unknown")[:64] # bounded: becomes a dict key def client_address() -> str: """The caller's address, as far as the deployment's own proxies vouch for it.""" return forwarded_for( request.headers.get("X-Forwarded-For", ""), request.remote_addr, live("trusted_proxy_hops"), ) def is_https() -> bool: """Whether this request reached us over TLS — directly, or via a trusted proxy. Shared by the session cookie's `Secure` flag and by HSTS, because they are the same question. Read with the same hop count as the address: a caller who sets `X-Forwarded-Proto: https` on a plain-HTTP request puts it to the left of whatever our proxy appended, so it is not what gets read. """ if not has_request_context(): return False if request.is_secure: return True entry = trusted_entry(request.headers.get("X-Forwarded-Proto", ""), live("trusted_proxy_hops")) return (entry or "").lower() == "https"