"""Limitation de debit derriere un reverse proxy, de bout en bout. Deux chemins sont reproduits sur l'application reelle : * une connexion venant d'un proxy approuve, qui transmet l'adresse du client ; * une connexion directe qui forge le meme en-tete. Dans le premier cas la limite doit suivre l'adresse transmise, dans le second l'adresse reelle de la socket. """ from __future__ import annotations from collections.abc import Iterator import pytest from fastapi.testclient import TestClient from app.main import app from tests.conftest import build_map PROXY = "172.18.0.1" DIRECT = "203.0.113.9" def _client(peer: str) -> Iterator[TestClient]: with TestClient(app, client=(peer, 40000)) as test_client: yield test_client @pytest.fixture() def behind_proxy(configure) -> Iterator[TestClient]: configure( trust_proxy_headers=True, trusted_proxy_cidrs="172.16.0.0/12,10.0.0.0/8", share_create_rate_limit="2/hour", ) yield from _client(PROXY) @pytest.fixture() def direct_client(configure) -> Iterator[TestClient]: configure( trust_proxy_headers=True, trusted_proxy_cidrs="172.16.0.0/12,10.0.0.0/8", share_create_rate_limit="2/hour", ) yield from _client(DIRECT) def _create(client: TestClient, forwarded_for: str | None) -> int: headers = {"X-Forwarded-For": forwarded_for} if forwarded_for else {} return client.post( "/api/shares", json={"title": "x", "map": build_map()}, headers=headers ).status_code def test_limit_follows_the_address_transmitted_by_an_approved_proxy( behind_proxy: TestClient, ) -> None: assert _create(behind_proxy, "198.51.100.10") == 201 assert _create(behind_proxy, "198.51.100.10") == 201 assert _create(behind_proxy, "198.51.100.10") == 429 # Un autre client, derriere le meme proxy, garde son propre compteur. assert _create(behind_proxy, "198.51.100.11") == 201 def test_approved_proxy_ignores_the_part_forged_by_the_client( behind_proxy: TestClient, ) -> None: """Le client ecrit "9.9.9.9" ; le proxy ajoute l'adresse qu'il a vue.""" assert _create(behind_proxy, "9.9.9.9, 198.51.100.20") == 201 assert _create(behind_proxy, "9.9.9.9, 198.51.100.20") == 201 assert _create(behind_proxy, "9.9.9.9, 198.51.100.20") == 429 # Changer la partie forgee ne remet pas le compteur a zero. assert _create(behind_proxy, "1.1.1.1, 198.51.100.20") == 429 def test_direct_client_cannot_escape_the_limit_by_forging_the_header( direct_client: TestClient, ) -> None: assert _create(direct_client, "10.0.0.1") == 201 assert _create(direct_client, "10.0.0.2") == 201 # Adresse forgee differente a chaque fois : la socket, elle, ne change pas. assert _create(direct_client, "10.0.0.3") == 429 assert _create(direct_client, "10.0.0.4") == 429 assert _create(direct_client, None) == 429 def test_ipv6_rewriting_does_not_reset_the_counter(behind_proxy: TestClient) -> None: assert _create(behind_proxy, "2001:db8::1") == 201 assert _create(behind_proxy, "2001:0db8:0000:0000:0000:0000:0000:0001") == 201 assert _create(behind_proxy, "2001:DB8:0:0:0:0:0:1") == 429 def test_headers_are_ignored_when_trust_is_disabled(configure) -> None: configure(trust_proxy_headers=False, share_create_rate_limit="2/hour") with TestClient(app, client=(PROXY, 40000)) as client: assert _create(client, "198.51.100.30") == 201 assert _create(client, "198.51.100.31") == 201 assert _create(client, "198.51.100.32") == 429