"""Identification du client : X-Forwarded-For n'est cru qu'a deux conditions. `TRUST_PROXY_HEADERS` doit etre actif, ET la connexion doit venir d'un reseau declare dans `TRUSTED_PROXY_CIDRS`. Ces tests reproduisent les deux chemins : derriere un proxy approuve, et en connexion directe forgeant le meme en-tete. """ from __future__ import annotations import pytest from starlette.datastructures import Headers from starlette.requests import Request from app.config import parse_cidrs from app.services.client_ip import is_trusted_peer, normalize_ip, resolve_client_ip PROXY_NETWORKS = parse_cidrs("172.16.0.0/12,10.0.0.0/8,2001:db8::/32") def _request(headers: dict[str, str], peer: str | None = "203.0.113.9") -> Request: scope = { "type": "http", "method": "GET", "path": "/api/shares", "headers": Headers(headers).raw, "client": (peer, 12345) if peer else None, } return Request(scope) class TestWithoutTrust: def test_proxy_headers_are_ignored_by_default(self) -> None: request = _request({"x-forwarded-for": "1.2.3.4"}) assert resolve_client_ip(request, trust_proxy_headers=False) == "203.0.113.9" def test_forged_header_cannot_change_identity(self) -> None: """Un client direct ne peut pas se demultiplier pour diluer sa limite.""" seen = { resolve_client_ip( _request({"x-forwarded-for": f"10.0.0.{i}"}), trust_proxy_headers=False ) for i in range(20) } assert seen == {"203.0.113.9"} def test_falls_back_to_peer_without_headers(self) -> None: assert resolve_client_ip(_request({}), trust_proxy_headers=True) == "203.0.113.9" def test_unknown_client_is_handled(self) -> None: assert resolve_client_ip(_request({}, peer=None), trust_proxy_headers=False) == "unknown" class TestTrustedProxy: def test_approved_proxy_is_believed(self) -> None: # Le proxy (172.18.0.1) a ajoute l'adresse reellement vue. request = _request({"x-forwarded-for": "9.9.9.9, 198.51.100.7"}, peer="172.18.0.1") resolved = resolve_client_ip( request, trust_proxy_headers=True, trusted_networks=PROXY_NETWORKS ) assert resolved == "198.51.100.7" def test_direct_client_forging_the_same_header_is_not_believed(self) -> None: """Meme en-tete, connexion directe : c'est l'adresse socket qui compte.""" request = _request({"x-forwarded-for": "9.9.9.9, 198.51.100.7"}, peer="203.0.113.9") resolved = resolve_client_ip( request, trust_proxy_headers=True, trusted_networks=PROXY_NETWORKS ) assert resolved == "203.0.113.9" def test_approved_proxy_falls_back_to_real_ip(self) -> None: request = _request({"x-real-ip": "198.51.100.8"}, peer="10.1.2.3") resolved = resolve_client_ip( request, trust_proxy_headers=True, trusted_networks=PROXY_NETWORKS ) assert resolved == "198.51.100.8" def test_no_declared_network_means_no_trust(self) -> None: request = _request({"x-forwarded-for": "198.51.100.7"}, peer="172.18.0.1") resolved = resolve_client_ip(request, trust_proxy_headers=True, trusted_networks=[]) assert resolved == "172.18.0.1" def test_ipv6_proxy_is_recognised(self) -> None: request = _request({"x-forwarded-for": "198.51.100.7"}, peer="2001:db8::5") resolved = resolve_client_ip( request, trust_proxy_headers=True, trusted_networks=PROXY_NETWORKS ) assert resolved == "198.51.100.7" def test_peer_membership(self) -> None: assert is_trusted_peer("172.18.0.1", PROXY_NETWORKS) is True assert is_trusted_peer("10.255.255.254", PROXY_NETWORKS) is True assert is_trusted_peer("203.0.113.9", PROXY_NETWORKS) is False assert is_trusted_peer("2001:db8::1", PROXY_NETWORKS) is True assert is_trusted_peer("2001:dead::1", PROXY_NETWORKS) is False assert is_trusted_peer("pas-une-ip", PROXY_NETWORKS) is False assert is_trusted_peer("10.0.0.1", []) is False class TestNormalisation: @pytest.mark.parametrize( ("raw", "expected"), [ ("2001:db8::1", "2001:db8::1"), ("2001:0db8:0000:0000:0000:0000:0000:0001", "2001:db8::1"), ("2001:DB8::1", "2001:db8::1"), ("2001:0db8::0:1", "2001:db8::1"), ("[2001:db8::1]:443", "2001:db8::1"), (" 192.0.2.7 ", "192.0.2.7"), ("192.0.2.7:8080", "192.0.2.7"), ], ) def test_same_address_gives_the_same_key(self, raw: str, expected: str) -> None: assert normalize_ip(raw) == expected def test_ipv6_rewriting_cannot_reset_the_counter(self) -> None: """Plusieurs ecritures de la meme IPv6 doivent produire une seule cle.""" forms = [ "2001:db8::1", "2001:0DB8:0000:0000:0000:0000:0000:0001", "2001:db8:0:0:0:0:0:1", "[2001:db8::1]:51000", ] keys = { resolve_client_ip( _request({"x-forwarded-for": form}, peer="172.18.0.1"), trust_proxy_headers=True, trusted_networks=PROXY_NETWORKS, ) for form in forms } assert keys == {"2001:db8::1"} def test_unreadable_value_is_kept_bounded(self) -> None: assert normalize_ip("x" * 500) == "x" * 64 assert normalize_ip(" ") == "unknown"