56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
"""Bounded PROXY-v1 parser and trusted reverse-proxy client-IP extraction."""
|
|
import ipaddress
|
|
|
|
CLIENT_HEADERS = ('CF-Connecting-IP', 'True-Client-IP', 'X-Real-IP')
|
|
|
|
|
|
def public_ip(value):
|
|
"""Return a normalized global client IP, or None for private/spoofed values."""
|
|
try:
|
|
ip = ipaddress.ip_address(value.strip())
|
|
except (ValueError, AttributeError):
|
|
return None
|
|
if not ip.is_global or ip.is_multicast:
|
|
return None
|
|
return str(ip)
|
|
|
|
|
|
def forwarded_client(headers):
|
|
"""Real client IP from a trusted proxy. Priority: single-hop headers, then nearest XFF hop."""
|
|
for name in CLIENT_HEADERS:
|
|
value = headers.get(name)
|
|
if value:
|
|
ip = public_ip(value)
|
|
if ip:
|
|
return ip, name
|
|
for hop in reversed((headers.get('X-Forwarded-For') or '').split(',')):
|
|
ip = public_ip(hop)
|
|
if ip:
|
|
return ip, 'X-Forwarded-For'
|
|
return None, None
|
|
|
|
|
|
def source(connection, transport, trusted):
|
|
if transport[0] != trusted:
|
|
raise ValueError('untrusted PROXY transport')
|
|
header = bytearray()
|
|
while len(header) < 108:
|
|
value = connection.recv(1)
|
|
if not value:
|
|
raise ValueError('incomplete PROXY header')
|
|
header.extend(value)
|
|
if header.endswith(b'\r\n'):
|
|
break
|
|
else:
|
|
raise ValueError('oversize PROXY header')
|
|
fields = header.decode('ascii').strip().split(' ')
|
|
if len(fields) != 6 or fields[0] != 'PROXY' or fields[1] not in ('TCP4', 'TCP6'):
|
|
raise ValueError('invalid PROXY header')
|
|
version = 4 if fields[1] == 'TCP4' else 6
|
|
if any(ipaddress.ip_address(ip).version != version for ip in fields[2:4]):
|
|
raise ValueError('PROXY address family mismatch')
|
|
ports = [int(value) for value in fields[4:]]
|
|
if any(not 0 < port <= 65535 for port in ports):
|
|
raise ValueError('invalid PROXY port')
|
|
return (fields[2], ports[0]), bytes(header)
|