@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from functools import lru_cache
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def _trusted_networks(
|
||||
values: tuple[str, ...],
|
||||
) -> tuple[ipaddress.IPv4Network | ipaddress.IPv6Network, ...]:
|
||||
return tuple(ipaddress.ip_network(value, strict=False) for value in values)
|
||||
|
||||
|
||||
def _parse_ip(value: str) -> ipaddress.IPv4Address | ipaddress.IPv6Address | None:
|
||||
candidate = value.strip()
|
||||
if not candidate:
|
||||
return None
|
||||
try:
|
||||
return ipaddress.ip_address(candidate)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def client_ip(request: HttpRequest) -> str:
|
||||
"""Return a proxy-aware client IP without trusting arbitrary forwarded headers."""
|
||||
|
||||
remote = _parse_ip(request.META.get("REMOTE_ADDR", ""))
|
||||
if remote is None:
|
||||
return "unknown"
|
||||
|
||||
networks = _trusted_networks(tuple(getattr(settings, "TRUSTED_PROXY_CIDRS", ())))
|
||||
if not networks or not any(remote in network for network in networks):
|
||||
return str(remote)
|
||||
|
||||
forwarded = [
|
||||
parsed
|
||||
for item in request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")
|
||||
if (parsed := _parse_ip(item)) is not None
|
||||
]
|
||||
if not forwarded:
|
||||
return str(remote)
|
||||
|
||||
# Walk from the nearest hop back to the client. The first address outside
|
||||
# the explicitly trusted proxy ranges is the client address.
|
||||
for candidate in reversed([*forwarded, remote]):
|
||||
if not any(candidate in network for network in networks):
|
||||
return str(candidate)
|
||||
return str(forwarded[0])
|
||||
Reference in New Issue
Block a user