"""What the request looked like before the reverse proxy. Steward serves plain HTTP; TLS ends at the operator's proxy (Traefik, often with Cloudflare in front). The proxy reports the original scheme and client in X-Forwarded-Proto / X-Forwarded-For, but anyone can send those headers, so they are believed only as far as the configured number of trusted proxy hops (Settings → General, default 1). 0 means no proxy is trusted. Each proxy appends to the header, so the value the nearest trusted proxy wrote is the hops-th from the right. HTTPS-only behaviour (the session cookie's Secure flag, HSTS) turns on per request from is_https(); nothing here ever redirects to https. """ from __future__ import annotations from quart import current_app, request def trusted_value(header_values: list[str], hops: int) -> str | None: """The value written by the outermost trusted proxy, or None when there are fewer values than trusted hops (or hops is 0).""" if hops <= 0: return None values = [v.strip() for line in header_values for v in line.split(",") if v.strip()] if len(values) < hops: return None return values[-hops] def _hops() -> int: try: return max(0, int(current_app.config.get("TRUSTED_PROXY_HOPS", 1))) except (TypeError, ValueError): return 1 def is_https() -> bool: """True when this request reached Steward over HTTPS, directly or as reported by a trusted proxy.""" if request.scheme == "https": return True proto = trusted_value(request.headers.getlist("X-Forwarded-Proto"), _hops()) return (proto or "").lower() == "https" def client_ip() -> str: """The client's address as the outermost trusted proxy saw it, else the socket peer.""" forwarded = trusted_value(request.headers.getlist("X-Forwarded-For"), _hops()) return forwarded or (request.remote_addr or "")