import pytest from flask import request, url_for from app import config @pytest.mark.parametrize( ("app", "expected_ip", "expected_scheme", "expected_host", "expected_prefix"), [ (False, "127.0.0.1", "http", "localhost", ""), (True, "192.0.2.1", "https", "files.example.com", "/!/placeholder"), ], indirect=["app"], ) def test_forwarded_headers(app, client, expected_ip, expected_scheme, expected_host, expected_prefix): @app.get("/request-info") def request_info(): return { "ip": request.remote_addr, "scheme": request.scheme, "host": request.host, "prefix": request.script_root, "health_url": url_for("healthz", _external=True), } response = client.get( "/request-info", headers={ "X-Forwarded-For": "198.51.100.1, 192.0.2.1", "X-Forwarded-Proto": "http, https", "X-Forwarded-Host": "untrusted.example, files.example.com", "X-Forwarded-Port": "1234", "X-Forwarded-Prefix": "/untrusted, /!/placeholder", }, ) assert response.status_code == 200 assert response.json == { "ip": expected_ip, "scheme": expected_scheme, "host": expected_host, "prefix": expected_prefix, "health_url": f"{expected_scheme}://{expected_host}{expected_prefix}/healthz", } @pytest.mark.parametrize("app", [True], indirect=True) @pytest.mark.parametrize("extension", ["svg", "png"]) def test_image_with_stripped_proxy_prefix(app, client, extension): # Caddy removes the prefix from the path and supplies it separately in a header. response = client.get( f"/placeholder.{extension}?width=10&height=10", headers={"X-Forwarded-Prefix": "/!/placeholder"}, ) assert response.status_code == 200 assert response.mimetype == ("image/svg+xml" if extension == "svg" else "image/png") @pytest.mark.parametrize(("app", "other_client_status"), [(False, 429), (True, 200)], indirect=["app"]) @pytest.mark.parametrize("extension", ["svg", "png"]) def test_rate_limit_client_identity(app, client, monkeypatch, extension, other_client_status): monkeypatch.setattr(config, f"{extension.upper()}_RENDER_LIMIT", "1/minute") def render(width, forwarded_for): return client.get( f"/placeholder.{extension}", query_string={"width": width, "height": 10}, headers={"X-Forwarded-For": forwarded_for}, ) assert render(10, "198.51.100.1, 192.0.2.1").status_code == 200 # Changing an earlier header value cannot bypass the same client's quota. assert render(11, "198.51.100.2, 192.0.2.1").status_code == 429 # Distinct forwarded clients get separate quotas only when proxy trust is enabled. assert render(12, "192.0.2.2").status_code == other_client_status