diff --git a/sanic/ip.py b/sanic/ip.py new file mode 100644 index 00000000..e1cedd46 --- /dev/null +++ b/sanic/ip.py @@ -0,0 +1,69 @@ +import socket + +# CAPS R OK BCUZ WE HAZ CIDict +HEADER_PRECEDENCE_ORDER = ( + 'X_FORWARDED_FOR', + 'CLIENT_IP', + 'X_REAL_IP', + 'X_FORWARDED', + 'X_CLUSTER_CLIENT_IP', + 'FORWARDED_FOR', + 'FORWARDED', + 'VIA', + 'REMOTE_ADDR', +) + +# Private IP addresses +# http://en.wikipedia.org/wiki/List_of_assigned_/8_IPv4_address_blocks +# http://www.ietf.org/rfc/rfc3330.txt (IPv4) +# http://www.ietf.org/rfc/rfc5156.txt (IPv6) +# Regex would be ideal here, but this is keeping it simple +PRIVATE_IP_PREFIX = ( + '0.', # externally non-routable + '10.', # class A private block + '169.254.', # link-local block + '172.16.', '172.17.', '172.18.', '172.19.', + '172.20.', '172.21.', '172.22.', '172.23.', + '172.24.', '172.25.', '172.26.', '172.27.', + '172.28.', '172.29.', '172.30.', '172.31.', # class B private blocks + '192.0.2.', # reserved for documentation and example code + '192.168.', # class C private block + '255.255.255.', # IPv4 broadcast address + '2001:db8:', # reserved for documentation and example code + 'fc00:', # IPv6 private block + 'fe80:', # link-local unicast + 'ff00:', # IPv6 multicast +) + +LOOPBACK_PREFIX = ( + '127.', # IPv4 loopback device + '::1', # IPv6 loopback device +) + +NON_PUBLIC_IP_PREFIX = PRIVATE_IP_PREFIX + LOOPBACK_PREFIX + + +def is_valid_ipv4(ip_str): + try: + socket.inet_pton(socket.AF_INET, ip_str) + except AttributeError: # pragma: no cover + try: # Fall-back on legacy API or False + socket.inet_aton(ip_str) + except (AttributeError, socket.error): + return False + return ip_str.count('.') == 3 + except socket.error: + return False + return True + + +def is_valid_ipv6(ip_str): + try: + socket.inet_pton(socket.AF_INET6, ip_str) + except socket.error: + return False + return True + + +def is_valid_ip(ip_str): + return is_valid_ipv4(ip_str) or is_valid_ipv6(ip_str) diff --git a/sanic/request.py b/sanic/request.py index 26f19baf..ab70e49b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -5,6 +5,7 @@ from collections import namedtuple from http.cookies import SimpleCookie from httptools import parse_url from urllib.parse import parse_qs, urlunparse +from .ip import is_valid_ip, HEADER_PRECEDENCE_ORDER, NON_PUBLIC_IP_PREFIX try: from ujson import loads as json_loads @@ -168,8 +169,14 @@ class Request(dict): @property def ip(self): if not hasattr(self, '_ip'): - self._ip = (self.transport.get_extra_info('peername') or - (None, None)) + self._ip = None + for key in HEADER_PRECEDENCE_ORDER: + value = self.headers.get(key, self.headers.get(key.replace('_', '-'), '')).strip() + if value is not None and value != '': + for ip_str in [ip.strip().lower() for ip in value.split(',')]: + if ip_str and is_valid_ip(ip_str): + if not ip_str.startswith(NON_PUBLIC_IP_PREFIX): + self._ip = ip_str return self._ip @property