CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 13s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 32s
Rule 32 "Comments describe the product to a stranger" sweep over src/, alembic/ and tests/. Comments and docstrings now say what the code does and the reason as a present-tense constraint, briefly. Removed: - issue, task, note, rule, milestone and family-idea references - "used to" / before-and-after narration - quotes of decisions and incident stories from the deploying instance - paragraph-length arguments, where a line or two carries the fact Kept every hazard as a present-tense fact, e.g. timing oracles, the proxy hop direction, and migrations that must stay frozen. Also: - Three test names lose "still"/"used to"/"pre_m304" phrasing. - pyproject's description drops "FabledSword family". The history these comments carried stays in Scribe and in git log. A comment-stripping diff (Scribe snippet #6226 "codeonly.py — prove a diff changed only comments") shows no code changed apart from the renamed tests and the description string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
"""Reading what the proxies in front of this app say about a request.
|
|
|
|
`X-Forwarded-For` (who the client is) and `X-Forwarded-Proto` (whether they arrived
|
|
over TLS) are trusted by one rule, kept here.
|
|
|
|
A forwarding header grows left to right: each hop appends what it saw, so a caller's
|
|
forged entries sit to the left of what our proxies wrote. So we count in from the
|
|
right by the **Trusted proxy hops** setting (default 1), and a forged prefix is never
|
|
selected.
|
|
|
|
Too high a count is the dangerous direction: it believes entries no proxy of ours
|
|
wrote. When the header is shorter than configured, the socket address is used.
|
|
"""
|
|
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.
|
|
|
|
For the session cookie's `Secure` flag and HSTS. Read with the same hop count as
|
|
the address, so a forged `X-Forwarded-Proto: https` is never 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"
|