71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
# noqa: D100
|
|
from typing import Optional
|
|
import re
|
|
|
|
from django.http import HttpRequest
|
|
from django.core.validators import validate_ipv46_address
|
|
|
|
|
|
IPV4_WITH_PORT = re.compile(r"([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):[0-9]+")
|
|
"""Regexp matching an IPv4 address with a port number."""
|
|
|
|
IPV6_WITH_PORT = re.compile(r"\[([0-9:]+)\]:[0-9]+")
|
|
"""Regexp matching an IPv6 address with a port number."""
|
|
|
|
|
|
def clean_ip_address(request: HttpRequest) -> str:
|
|
"""Retrieve a valid IP address from the given request.
|
|
|
|
Raises a django.code.exceptions.ValidationError
|
|
if no valid IP address could be determined.
|
|
"""
|
|
ip_address = get_client_ip(request)
|
|
validate_ipv46_address(ip_address)
|
|
return ip_address
|
|
|
|
|
|
def get_client_ip(request: HttpRequest) -> str:
|
|
"""Returns the IP of the request, accounting for the possibility of being behind a proxy."""
|
|
x_forwarded_for: Optional[str] = request.META.get('HTTP_X_FORWARDED_FOR', '')
|
|
# nginx's proxy_add_x_forwarded_for adds ', '
|
|
# we want to simplify the split, so removing all spaces first
|
|
x_forwarded_for = x_forwarded_for.replace(' ', '')
|
|
if x_forwarded_for:
|
|
# X_FORWARDED_FOR returns client1,proxy1,proxy2,...
|
|
remote_addr = x_forwarded_for.split(',', 1)[0].strip()
|
|
return _remove_port_nr(remote_addr)
|
|
|
|
remote_addr = request.META.get('REMOTE_ADDR', '')
|
|
if not remote_addr:
|
|
return ''
|
|
|
|
# REMOTE_ADDR can also be 'ip1,ip2' if people mess around with HTTP
|
|
# headers (we've seen this happen). Don't trust anything in that case.
|
|
if ',' in remote_addr:
|
|
return ''
|
|
|
|
return _remove_port_nr(remote_addr)
|
|
|
|
|
|
def _remove_port_nr(remote_addr: str) -> str:
|
|
# Occasionally the port number is included in REMOTE_ADDR.
|
|
# This needs to be filtered out so that downstream requests
|
|
# can just interpret the value as actual IP address.
|
|
|
|
if len(remote_addr) > 128:
|
|
# Prevent DoS attacks by not allowing obvious nonsense.
|
|
return ''
|
|
|
|
if ':' not in remote_addr:
|
|
return remote_addr
|
|
|
|
ipv4_with_port_match = IPV4_WITH_PORT.match(remote_addr)
|
|
if ipv4_with_port_match:
|
|
return ipv4_with_port_match.group(1)
|
|
|
|
ipv6_with_port_match = IPV6_WITH_PORT.match(remote_addr)
|
|
if ipv6_with_port_match:
|
|
return ipv6_with_port_match.group(1)
|
|
|
|
return remote_addr
|