diff --git a/.gitignore b/.gitignore index a83a2110e..667601ec7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ test/node_modules docker/dev/dnsrouter-config.json.tmp docker/dev/resolv.conf .claude +test/__pycache__ diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index 2184049c4..608c1c1d8 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -52,10 +52,16 @@ http { # Dynamically generated resolvers file include /etc/nginx/conf.d/include/resolvers[.]conf; - # Preserve the client authority, falling back when a request has no Host header. - map $http_host $forward_host { - default $http_host; - "" $host; + # Forwarded Host: nginx's normalised $host (lowercased, taken from the request line for + # absolute-form requests) plus the client's port, so upstreams that compare Host with + # Origin/Referer on non-standard ports keep working. The port is only carried over when + # the Host header names the same host nginx routed on; otherwise (no Host, absolute-form + # URI naming a different host) it falls back to bare $host. Using $host rather than the + # raw header keeps case variants and mismatched Host values out of upstream requests and + # the asset cache key. + map "$host|$http_host" $forward_host { + "~*^(?[^|]+)\|\k(?:[0-9]+)$" "$host$fwd_port"; + default $host; } # Default upstream scheme diff --git a/test/host-header-smoke.py b/test/host-header-smoke.py index a1397d878..9e6b51b0b 100644 --- a/test/host-header-smoke.py +++ b/test/host-header-smoke.py @@ -178,7 +178,10 @@ def check_image(image): for authority in authorities: for prefix in ("/", "/custom/"): for method in ("GET", "POST"): - origin = f"{scheme}://{authority}" + # nginx forwards its normalised (lowercased) $host, as browsers send + # in Host and Origin anyway + forwarded = authority.lower() + origin = f"{scheme}://{forwarded}" referer = origin + "/settings" path = prefix + "apply?one=1&two=2" payload = "rule=example" if method == "POST" else None @@ -190,7 +193,7 @@ def check_image(image): raise AssertionError((authority, path, method, str(error))) from error assert status == 200 and headers.get("Content-Type") == "application/json", ( authority, path, method, status, body) - assert json.loads(body) == {"host": authority, "origin": origin, + assert json.loads(body) == {"host": forwarded, "origin": origin, "referer": referer, "method": method, "path": path, "body": payload or ""}, body checked += 1