"""Identification du client pour la limitation de debit. Le point sensible : `X-Forwarded-For` est un en-tete que n'importe quel client peut ecrire. Le croire sans condition permettrait a un client direct de changer d'identite a chaque requete et de contourner toute limite. La confiance est donc doublement conditionnee : 1. `TRUST_PROXY_HEADERS` doit etre actif ; 2. la connexion doit provenir d'un reseau declare dans `TRUSTED_PROXY_CIDRS`. Sans ces deux conditions, l'en-tete est ignore et seule l'adresse reelle de la socket compte. OpenMind suppose **un reverse proxy frontal de confiance** : on lit la derniere valeur de la chaine, celle que ce proxy a ajoutee, les precedentes pouvant avoir ete forgees par le client. """ from __future__ import annotations import ipaddress from collections.abc import Sequence from starlette.requests import Request FORWARDED_FOR = "x-forwarded-for" REAL_IP = "x-real-ip" UNKNOWN_CLIENT = "unknown" IpNetwork = ipaddress.IPv4Network | ipaddress.IPv6Network def normalize_ip(value: str) -> str: """Forme canonique d'une adresse, pour que la cle de limitation soit unique. Sans cela, `2001:db8::1` et `2001:0db8:0000:0000:0000:0000:0000:0001` designeraient deux clients differents, et il suffirait de reecrire son adresse autrement pour repartir avec un compteur neuf. """ text = value.strip() if not text: return UNKNOWN_CLIENT # Un port peut trainer dans un en-tete mal configure : [::1]:443, 1.2.3.4:80. if text.startswith("[") and "]" in text: text = text[1 : text.index("]")] elif text.count(":") == 1 and "." in text: text = text.split(":", 1)[0] try: return str(ipaddress.ip_address(text)) except ValueError: # Adresse illisible : on la garde telle quelle, bornee, plutot que de # regrouper tous les clients douteux sous une meme cle. return text[:64] def is_trusted_peer(host: str, networks: Sequence[IpNetwork]) -> bool: if not networks: return False try: address = ipaddress.ip_address(host) except ValueError: return False return any(address in network for network in networks) def resolve_client_ip( request: Request, *, trust_proxy_headers: bool, trusted_networks: Sequence[IpNetwork] = (), ) -> str: peer = request.client.host if request.client and request.client.host else "" if trust_proxy_headers and is_trusted_peer(peer, trusted_networks): forwarded = request.headers.get(FORWARDED_FOR) if forwarded: parts = [part.strip() for part in forwarded.split(",") if part.strip()] if parts: return normalize_ip(parts[-1]) real_ip = request.headers.get(REAL_IP) if real_ip and real_ip.strip(): return normalize_ip(real_ip) return normalize_ip(peer) if peer else UNKNOWN_CLIENT