diff --git a/.vscode/settings.json b/.vscode/settings.json index 68e31e1..bc67384 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "python.linting.enabled": false, "cSpell.words": [ "cmdclass", + "Fastly", "getattr", "ipware", "multicast", diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f18492..06e023e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 6.0.0 + +Enhancement: (breaking changes) + +- Use python-ipware under the hood +- Minor behavior changes as python-ipware is more accurate + # 5.0.2 Enhancement: diff --git a/README.md b/README.md index 92fbe84..33241ab 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,12 @@ # Alternative package -If you prefer a python only version that does not integrate with Django, you can use the [python-ipware](https://github.com/un33k/python-ipware) package instead. `django-ipware` will use python-ipware in the future. +If you prefer a python only version that does not integrate with Django directly, but allows for more flexibility and advanced features, you can use the [python-ipware](https://github.com/un33k/python-ipware) package instead. `django-ipware` is now a wrapper using python-ipware underneath. # Overview **Best attempt** to get client's IP address while keeping it **DRY**. -# Alternative package - -If you prefer a python version, you can use the [python-ipware](https://github.com/un33k/python-ipware) package instead. `python-ipware` is a newer package, with more advanced features. While this a Django specific package, `python-ipware` can be used with Django, Flask, etc. - # Notice There is no perfect `out-of-the-box` solution against fake IP addresses, aka `IP Address Spoofing`. @@ -56,30 +52,39 @@ Please use ipware `ONLY` as a complement to your `firewall` security measures! # The client's IP address is publicly routable on the Internet else: # The client's IP address is private - - # Order of precedence is (Public, Private, Loopback, None) ``` # Advanced users: - ### Precedence Order - The default meta precedence order is top to bottom. However, you may customize the order + The default meta precedence order is top to bottom. You may customize the order by providing your own `IPWARE_META_PRECEDENCE_ORDER` by adding it to your project's settings.py ```python - # The default meta precedence order + # The default meta precedence order (update as needed) IPWARE_META_PRECEDENCE_ORDER = ( - 'HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR', # , , - 'HTTP_CLIENT_IP', - 'HTTP_X_REAL_IP', - 'HTTP_X_FORWARDED', - 'HTTP_X_CLUSTER_CLIENT_IP', - 'HTTP_FORWARDED_FOR', - 'HTTP_FORWARDED', - 'HTTP_VIA', - 'REMOTE_ADDR', - ) + "X_FORWARDED_FOR", # AWS ELB (default client is `left-most` [`, , `]) + "HTTP_X_FORWARDED_FOR", # Similar to X_FORWARDED_TO + "HTTP_CLIENT_IP", # Standard headers used by providers such as Amazon EC2, Heroku etc. + "HTTP_X_REAL_IP", # Standard headers used by providers such as Amazon EC2, Heroku etc. + "HTTP_X_FORWARDED", # Squid and others + "HTTP_X_CLUSTER_CLIENT_IP", # Rackspace LB and Riverbed Stingray + "HTTP_FORWARDED_FOR", # RFC 7239 + "HTTP_FORWARDED", # RFC 7239 + "HTTP_VIA", # Squid and others + "X-CLIENT-IP", # Microsoft Azure + "X-REAL-IP", # NGINX + "X-CLUSTER-CLIENT-IP", # Rackspace Cloud Load Balancers + "X_FORWARDED", # Squid + "FORWARDED_FOR", # RFC 7239 + "CF-CONNECTING-IP", # CloudFlare + "TRUE-CLIENT-IP", # CloudFlare Enterprise, + "FASTLY-CLIENT-IP", # Firebase, Fastly + "FORWARDED", # RFC 7239 + "CLIENT-IP", # Akamai and Cloudflare: True-Client-IP and Fastly: Fastly-Client-IP + "REMOTE_ADDR", # Default + ) ``` **Alternatively**, you can provide your custom _request header meta precedence order_ when calling `get_client_ip()`. @@ -89,57 +94,6 @@ get_client_ip(request, request_header_order=['X_FORWARDED_FOR']) get_client_ip(request, request_header_order=['X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR']) ``` -### Private Prefixes - -You may customize the prefixes to indicate an IP address is private. This is done by adding your -own `IPWARE_PRIVATE_IP_PREFIX` to your project's settings.py. IP addresses matching the following -prefixes are considered _private_ & are **not** publicly routable. - -```python -# The default private IP prefixes -IPWARE_PRIVATE_IP_PREFIX = getattr(settings, - 'IPWARE_PRIVATE_IP_PREFIX', ( - '0.', # messages to software - '10.', # class A private block - '100.64.', '100.65.', '100.66.', '100.67.', '100.68.', '100.69.', - '100.70.', '100.71.', '100.72.', '100.73.', '100.74.', '100.75.', - '100.76.', '100.77.', '100.78.', '100.79.', '100.80.', '100.81.', - '100.82.', '100.83.', '100.84.', '100.85.', '100.86.', '100.87.', - '100.88.', '100.89.', '100.90.', '100.91.', '100.92.', '100.93.', - '100.94.', '100.95.', '100.96.', '100.97.', '100.98.', '100.99.', - '100.100.', '100.101.', '100.102.', '100.103.', '100.104.', '100.105.', - '100.106.', '100.107.', '100.108.', '100.109.', '100.110.', '100.111.', - '100.112.', '100.113.', '100.114.', '100.115.', '100.116.', '100.117.', - '100.118.', '100.119.', '100.120.', '100.121.', '100.122.', '100.123.', - '100.124.', '100.125.', '100.126.', '100.127.', # carrier-grade NAT - '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.0.', # reserved for IANA special purpose address registry - '192.0.2.', # reserved for documentation and example code - '192.168.', # class C private block - '198.18.', '198.19.', # reserved for inter-network communications between two separate subnets - '198.51.100.', # reserved for documentation and example code - '203.0.113.', # reserved for documentation and example code - '224.', '225.', '226.', '227.', '228.', '229.', '230.', '231.', '232.', - '233.', '234.', '235.', '236.', '237.', '238.', '239.', # multicast - '240.', '241.', '242.', '243.', '244.', '245.', '246.', '247.', '248.', - '249.', '250.', '251.', '252.', '253.', '254.', '255.', # reserved - '::', # Unspecified address - '::ffff:', '2001:10:', '2001:20:', # messages to software - '2001::', # TEREDO - '2001:2::', # benchmarking - '2001:db8:', # reserved for documentation and example code - 'fc', # IPv6 ULA (RFC4193) - NOTE: Reserved for future use, not currently in widespread use. - 'fd', # IPv6 ULA (RFC4193) - Mainly used for private network addressing - 'fe80:', # link-local unicast - 'ff00:', # IPv6 multicast - ) -) -``` - ### Trusted Proxies If your Django server is behind one or more known proxy server(s), you can filter out unwanted requests diff --git a/ipware/__version__.py b/ipware/__version__.py index a27dbf0..0a2cd3f 100644 --- a/ipware/__version__.py +++ b/ipware/__version__.py @@ -5,4 +5,4 @@ __url__ = 'https://github.com/un33k/django-ipware' __license__ = 'MIT' __copyright__ = 'Copyright 2020 Val Neekman @ Neekware Inc.' -__version__ = '5.0.2' +__version__ = '6.0.0' diff --git a/ipware/defaults.py b/ipware/defaults.py deleted file mode 100644 index 85e0fc7..0000000 --- a/ipware/defaults.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.conf import settings - -# Search for the real IP address in the following order -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For -# X-Forwarded-For: , , -# Configurable via settings.py -IPWARE_META_PRECEDENCE_ORDER = getattr(settings, - 'IPWARE_META_PRECEDENCE_ORDER', ( - 'HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR', - 'HTTP_CLIENT_IP', - 'HTTP_X_REAL_IP', - 'HTTP_X_FORWARDED', - 'HTTP_X_CLUSTER_CLIENT_IP', - 'HTTP_FORWARDED_FOR', - 'HTTP_FORWARDED', - 'HTTP_VIA', - 'HTTP_X_CLIENT_IP', - 'REMOTE_ADDR', - ) -) - -# Private IP addresses -# http://en.wikipedia.org/wiki/List_of_assigned_/8_IPv4_address_blocks -# https://en.wikipedia.org/wiki/Reserved_IP_addresses -# https://www.ietf.org/rfc/rfc1112.txt (IPv4 multicast) -# http://www.ietf.org/rfc/rfc3330.txt (IPv4) -# http://www.ietf.org/rfc/rfc5156.txt (IPv6) -# https://www.ietf.org/rfc/rfc6890.txt -# Regex would be ideal here, but this is keeping it simple -# Configurable via settings.py -IPWARE_PRIVATE_IP_PREFIX = getattr(settings, - 'IPWARE_PRIVATE_IP_PREFIX', ( - '0.', # messages to software - '10.', # class A private block - '100.64.', '100.65.', '100.66.', '100.67.', '100.68.', '100.69.', - '100.70.', '100.71.', '100.72.', '100.73.', '100.74.', '100.75.', - '100.76.', '100.77.', '100.78.', '100.79.', '100.80.', '100.81.', - '100.82.', '100.83.', '100.84.', '100.85.', '100.86.', '100.87.', - '100.88.', '100.89.', '100.90.', '100.91.', '100.92.', '100.93.', - '100.94.', '100.95.', '100.96.', '100.97.', '100.98.', '100.99.', - '100.100.', '100.101.', '100.102.', '100.103.', '100.104.', '100.105.', - '100.106.', '100.107.', '100.108.', '100.109.', '100.110.', '100.111.', - '100.112.', '100.113.', '100.114.', '100.115.', '100.116.', '100.117.', - '100.118.', '100.119.', '100.120.', '100.121.', '100.122.', '100.123.', - '100.124.', '100.125.', '100.126.', '100.127.', # carrier-grade NAT - '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.0.', # reserved for IANA special purpose address registry - '192.0.2.', # reserved for documentation and example code - '192.168.', # class C private block - '198.18.', '198.19.', # reserved for inter-network communications between two separate subnets - '198.51.100.', # reserved for documentation and example code - '203.0.113.', # reserved for documentation and example code - '224.', '225.', '226.', '227.', '228.', '229.', '230.', '231.', '232.', - '233.', '234.', '235.', '236.', '237.', '238.', '239.', # multicast - '240.', '241.', '242.', '243.', '244.', '245.', '246.', '247.', '248.', - '249.', '250.', '251.', '252.', '253.', '254.', '255.', # reserved - '::', # Unspecified address - '::ffff:', '2001:10:', '2001:20:', # messages to software - '2001::', # TEREDO - '2001:2::', # benchmarking - '2001:db8:', # reserved for documentation and example code - 'fc', # IPv6 ULA (RFC4193) - NOTE: Reserved for future use, not currently in widespread use. - 'fd', # IPv6 ULA (RFC4193) - Mainly used for private network addressing - 'fe80:', # link-local unicast - 'ff00:', # IPv6 multicast - ) -) - -IPWARE_LOOPBACK_PREFIX = ( - '127.', # IPv4 loopback device (Host) - '::1', # IPv6 loopback device (Host) -) - -IPWARE_NON_PUBLIC_IP_PREFIX = IPWARE_PRIVATE_IP_PREFIX + IPWARE_LOOPBACK_PREFIX diff --git a/ipware/ip.py b/ipware/ip.py index e064526..9a1a65c 100644 --- a/ipware/ip.py +++ b/ipware/ip.py @@ -1,6 +1,5 @@ -from . import defaults as defs -from . import utils as util - +from django.conf import settings +from python_ipware import IpWare def get_client_ip( request, @@ -9,53 +8,24 @@ def get_client_ip( proxy_trusted_ips=None, request_header_order=None, ): - client_ip = None - routable = False - - if proxy_count is None: - proxy_count = -1 - - if proxy_trusted_ips is None: - proxy_trusted_ips = [] - - if request_header_order is None: - request_header_order = defs.IPWARE_META_PRECEDENCE_ORDER + leftmost = True if proxy_order == 'left-most' else False + proxy_count = proxy_count if proxy_count is not None else 0 + proxy_list = proxy_trusted_ips if proxy_trusted_ips is not None else [] + request_header_order = request_header_order if request_header_order is not None else getattr(settings, 'IPWARE_META_PRECEDENCE_ORDER', None) - for key in request_header_order: - value = util.get_request_meta(request, key) - if value: - ips, ip_count = util.get_ips_from_string(value) + # Instantiate IpWare with values from the function arguments + ipw = IpWare(precedence=request_header_order, + leftmost=leftmost, + proxy_count=proxy_count, + proxy_list=proxy_list) - if ip_count < 1: - # we are expecting at least one IP address to process - continue + ip, _ = ipw.get_client_ip(request.META, True) - if proxy_count == 0 and ip_count > 1: - # we are not expecting requests via any proxies - continue - - if proxy_count > 0 and proxy_count != ip_count - 1: - # we are expecting requests via `proxy_count` number of proxies - continue - - if proxy_trusted_ips and ip_count < 2: - # we are expecting requests via at least one trusted proxy - continue - - if proxy_order == 'right-most' and ip_count > 1: - # we are expecting requests via proxies to be custom as per `, , ` - ips.reverse() + client_ip = None + routable = False - if proxy_trusted_ips: - for proxy in proxy_trusted_ips: - # right most proxy is the most reliable proxy that talks to the django server - if ips[-1].startswith(proxy): - client_ip, routable = util.get_ip_info(ips[0]) - if client_ip and routable: - return client_ip, routable - else: - client_ip, routable = util.get_ip_info(util.get_best_ip(client_ip, ips[0])) - if client_ip and routable: - return client_ip, routable + if ip: + client_ip = str(ip) + routable = ip.is_global return client_ip, routable diff --git a/ipware/tests/tests_common.py b/ipware/tests/tests_common.py deleted file mode 100644 index 7b989d3..0000000 --- a/ipware/tests/tests_common.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.http import HttpRequest -from django.test import TestCase - -from .. import utils as util - - -class IPv4TestCase(TestCase): - """IP address Test""" - - - def test_is_valid_ip(self): - ip = '177.139.233.139' - self.assertTrue(util.is_valid_ip(ip)) - - ip = '3ffe:1900:4545:3:200:f8ff:fe21:67cf' - self.assertTrue(util.is_valid_ip(ip)) - - def test_is_invalid_ip(self): - ip = '177.139.233.139x' - self.assertFalse(util.is_valid_ip(ip)) - - ip = '3ffe:1900:4545:3:200:f8ff:fe21:67cz' - self.assertFalse(util.is_valid_ip(ip)) - - def test_is_private_ip(self): - ip = '127.0.0.1' - self.assertTrue(util.is_private_ip(ip)) - - ip = '::1/128' - self.assertTrue(util.is_private_ip(ip)) - - def test_is_public_ip(self): - ip = '177.139.233.139' - self.assertTrue(util.is_public_ip(ip)) - - ip = '74dc::02ba' - self.assertTrue(util.is_public_ip(ip)) - - def test_is_loopback_ip(self): - ip = '127.0.0.1' - self.assertTrue(util.is_loopback_ip(ip)) - - ip = '177.139.233.139' - self.assertFalse(util.is_loopback_ip(ip)) - - ip = '10.0.0.1' - self.assertFalse(util.is_loopback_ip(ip)) - - ip = '::1/128' - self.assertTrue(util.is_loopback_ip(ip)) - - ip = '74dc::02ba' - self.assertFalse(util.is_loopback_ip(ip)) - - ip = '2001:db8:' - self.assertFalse(util.is_loopback_ip(ip)) - - def test_http_request_meta_headers(self): - request = HttpRequest() - ip_str = '192.168.255.182, 10.0.0.0, 127.0.0.1, 198.84.193.157, 177.139.233.139,' - request.META = { 'HTTP_X_FORWARDED_FOR': ip_str } - value = util.get_request_meta(request, 'HTTP_X_FORWARDED_FOR') - self.assertEqual(value, ip_str) - - def test_ips_from_strings(self): - ip_str = '192.168.255.182, 198.84.193.157, 177.139.233.139 ,' - result = util.get_ips_from_string(ip_str) - self.assertEqual(result, (['192.168.255.182', '198.84.193.157', '177.139.233.139'], 3)) - - def test_get_ip_info(self): - ip = '127.0.0.1' - result = util.get_ip_info(ip) - self.assertTrue(result, (ip, False)) - - ip = '10.0.01' - result = util.get_ip_info(ip) - self.assertTrue(result, (ip, False)) - - ip = '74dc::02ba' - result = util.get_ip_info(ip) - self.assertTrue(result, (ip, True)) - - def test_is_ipv6_private_block(self): - ip = 'fc00::1' - self.assertTrue(util.is_private_ip(ip)) - - ip = 'fd00::1' - self.assertTrue(util.is_private_ip(ip)) - - # Test an address that is outside the fc00::/7 private block range - ip = 'fb00::1' - self.assertFalse(util.is_private_ip(ip)) - ip = 'fe00::1' - self.assertFalse(util.is_private_ip(ip)) diff --git a/ipware/tests/tests_ip.py b/ipware/tests/tests_ip.py new file mode 100644 index 0000000..2d124dd --- /dev/null +++ b/ipware/tests/tests_ip.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from django.http import HttpRequest +from django.test import TestCase + +from ipware import get_client_ip + + +class IpTestCase(TestCase): + """IP address Test""" + + # run one test, to ensure we are loading python ipware correctly + # python-ipware has all the tests, so we don't need to test it here + def test_load(self): + request = HttpRequest() + request.META = { + 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', + } + result = get_client_ip(request) + self.assertEqual(result, ("177.139.233.139", True)) diff --git a/ipware/tests/tests_ipv4.py b/ipware/tests/tests_ipv4.py deleted file mode 100644 index 1b24694..0000000 --- a/ipware/tests/tests_ipv4.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.http import HttpRequest -from django.test import TestCase - -from ipware import get_client_ip - - -class IPv4TestCase(TestCase): - """IP address Test""" - - def test_meta_none(self): - request = HttpRequest() - request.META = {} - ip, routable = get_client_ip(request) - self.assertIsNone(ip) - self.assertFalse(routable) - - def test_meta_single(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_multi(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - 'REMOTE_ADDR': '177.139.233.133', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_multi_precedence_order(self): - request = HttpRequest() - request.META = { - 'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158', - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - 'REMOTE_ADDR': '177.139.233.133', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_order_left_most(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_order='left-most') - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_order_right_most(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_order='right-most') - self.assertEqual(result, ("198.84.193.158", True)) - - def test_meta_multi_precedence_private_first(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '10.0.0.0, 10.0.0.1, 10.0.0.2', - 'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158', - 'REMOTE_ADDR': '177.139.233.133', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.138", True)) - - def test_meta_multi_precedence_invalid_first(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': 'unknown, 10.0.0.1, 10.0.0.2', - 'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158', - 'REMOTE_ADDR': '177.139.233.133', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.138", True)) - - def test_meta_error_only(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': 'unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request) - self.assertEqual(result, (None, False)) - - def test_meta_error_first(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': 'unknown, 177.139.233.139, 198.84.193.157, 198.84.193.158', - 'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.138", True)) - - def test_meta_singleton(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_singleton_proxy_count(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139', - } - result = get_client_ip(request, proxy_count=1) - self.assertEqual(result, (None, False)) - - def test_meta_singleton_proxy_count_private(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '10.0.0.0', - 'HTTP_X_REAL_IP': '177.139.233.139', - } - result = get_client_ip(request, proxy_count=1) - self.assertEqual(result, (None, False)) - - def test_meta_singleton_private_fallback(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '10.0.0.0', - 'HTTP_X_REAL_IP': '177.139.233.139', - } - result = get_client_ip(request) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_trusted_ips_exact_ip_check(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_trusted_ips=['198.84.193.158']) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_trusted_ips_exact_ips_check(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_trusted_ips=['198.84.193.157', '198.84.193.158']) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_trusted_ips_subnet_start_with_check(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_trusted_ips=['198.84.193']) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_trusted_ips_does_not_start_with_check(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_trusted_ips=['84.193.158']) - self.assertEqual(result, (None, False)) - - def test_meta_proxy_trusted_ips_proxy_count(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['198.84.193.158']) - self.assertEqual(result, ("177.139.233.139", True)) - - def test_meta_proxy_trusted_ips_proxy_count_less_error(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.158', - } - result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['198.84.193.158']) - self.assertEqual(result, (None, False)) - - def test_meta_proxy_trusted_ips_proxy_count_more_error(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['198.84.193.158']) - self.assertEqual(result, (None, False)) - - def test_meta_proxy_trusted_ips_proxy_count_more_error_fallback(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - 'HTTP_X_REAL_IP': '177.139.233.139', - } - result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['198.84.193.158']) - self.assertEqual(result, (None, False)) - - def test_best_matched_ip(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '192.168.1.1', - 'REMOTE_ADDR': '177.31.233.133', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("177.31.233.133", True)) - - def test_best_matched_ip_public(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '177.31.233.122', - 'REMOTE_ADDR': '177.31.233.133', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("177.31.233.122", True)) - - def test_best_matched_ip_private(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '192.168.1.1', - 'REMOTE_ADDR': '127.0.0.1', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("192.168.1.1", False)) - - def test_best_matched_ip_private_loopback_precedence(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '127.0.0.1', - 'REMOTE_ADDR': '192.168.1.1', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("192.168.1.1", False)) - - def test_best_matched_ip_private_precedence(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '172.25.0.1', - 'REMOTE_ADDR': '172.25.0.3', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("172.25.0.3", False)) - - def test_100_low_range_public(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '100.63.0.9', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("100.63.0.9", True)) - - def test_100_block_private(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '100.76.0.9', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("100.76.0.9", False)) - - def test_100_high_range_public(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '100.128.0.9', - } - ip = get_client_ip(request) - self.assertEqual(ip, ("100.128.0.9", True)) - - def test_request_header_order_specific(self): - request = HttpRequest() - request.META = { - 'HTTP_X_REAL_IP': '192.168.1.1', - 'REMOTE_ADDR': '177.139.233.139', - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - } - ip = get_client_ip(request, request_header_order=['HTTP_X_FORWARDED_FOR']) - self.assertEqual(ip, ("177.139.233.139", True)) - - - def test_request_header_order_multiple(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '177.139.233.139, 198.84.193.157, 198.84.193.158', - 'X_FORWARDED_FOR': '177.139.233.138, 198.84.193.157, 198.84.193.158', - 'REMOTE_ADDR': '177.139.233.133', - } - ip = get_client_ip(request, request_header_order=['X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR']) - self.assertEqual(ip, ("177.139.233.138", True)) diff --git a/ipware/tests/tests_ipv6.py b/ipware/tests/tests_ipv6.py deleted file mode 100644 index 8ee8fc1..0000000 --- a/ipware/tests/tests_ipv6.py +++ /dev/null @@ -1,193 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.http import HttpRequest -from django.test import TestCase - -from ipware import get_client_ip - - -class IPv4TestCase(TestCase): - """IP address Test""" - - def test_meta_none(self): - request = HttpRequest() - request.META = {} - ip, routable = get_client_ip(request) - self.assertIsNone(ip) - self.assertFalse(routable) - - def test_meta_single(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_multi(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - 'REMOTE_ADDR': '74dc::02bc', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_multi_precedence_order(self): - request = HttpRequest() - request.META = { - 'X_FORWARDED_FOR': '74dc::02be, 74dc::02bf', - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - 'REMOTE_ADDR': '74dc::02bc', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_proxy_order_left_most(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request, proxy_order='left-most') - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_proxy_order_right_most(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request, proxy_order='right-most') - self.assertEqual(result, ("74dc::02bb", True)) - - def test_meta_multi_precedence_private_first(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '2001:db8:, ::1', - 'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - 'REMOTE_ADDR': '74dc::02bc', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_multi_precedence_invalid_first(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': 'unknown, 2001:db8:, ::1', - 'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - 'REMOTE_ADDR': '74dc::02bc', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_error_only(self): - request = HttpRequest() - request.META = { - 'X_FORWARDED_FOR': 'unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request) - self.assertEqual(result, (None, False)) - - def test_meta_error_first(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': 'unknown, 3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - 'X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_singleton(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_singleton_proxy_count(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf', - 'HTTP_X_REAL_IP': '74dc::02ba', - } - result = get_client_ip(request, proxy_count=1) - self.assertEqual(result, (None, False)) - - def test_meta_singleton_proxy_count_private(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '::1', - 'HTTP_X_REAL_IP': '3ffe:1900:4545:3:200:f8ff:fe21:67cf', - } - result = get_client_ip(request, proxy_count=1) - self.assertEqual(result, (None, False)) - - def test_meta_singleton_private_fallback(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '::1', - 'HTTP_X_REAL_IP': '3ffe:1900:4545:3:200:f8ff:fe21:67cf', - } - result = get_client_ip(request) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_proxy_trusted_ips(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request, proxy_trusted_ips=['74dc::02bb']) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_proxy_trusted_ips_proxy_count(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['74dc::02bb']) - self.assertEqual(result, ("3ffe:1900:4545:3:200:f8ff:fe21:67cf", True)) - - def test_meta_proxy_trusted_ips_proxy_count_less_error(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02bb', - } - result = get_client_ip(request, proxy_count=2, proxy_trusted_ips=['74dc::02bb']) - self.assertEqual(result, (None, False)) - - def test_meta_proxy_trusted_ips_proxy_count_more_error(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - } - result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['74dc::02bb']) - self.assertEqual(result, (None, False)) - - def test_meta_proxy_trusted_ips_proxy_count_more_error_ignore_fallback(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '3ffe:1900:4545:3:200:f8ff:fe21:67cf, 74dc::02ba, 74dc::02bb', - 'HTTP_X_REAL_IP': '74dc::02bb', - } - result = get_client_ip(request, proxy_count=1, proxy_trusted_ips=['74dc::02bb']) - self.assertEqual(result, (None, False)) - - -class IPv6EncapsulationOfIPv4TestCase(TestCase): - """IPv6 Encapsulation of IPv4 - IP address Test""" - - def test_ipv6_encapsulation_of_ipv4_private(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '::ffff:127.0.0.1', - } - result = get_client_ip(request) - self.assertEqual(result, ('127.0.0.1', False)) - - def test_ipv6_encapsulation_of_ipv4_public(self): - request = HttpRequest() - request.META = { - 'HTTP_X_FORWARDED_FOR': '::ffff:177.139.233.139', - } - result = get_client_ip(request) - self.assertEqual(result, ('177.139.233.139', True)) diff --git a/ipware/utils.py b/ipware/utils.py deleted file mode 100644 index 43961a6..0000000 --- a/ipware/utils.py +++ /dev/null @@ -1,125 +0,0 @@ -import socket - -from . import defaults as defs - - -def cleanup_ip(ip): - """ - Given ip address string, it cleans it up - """ - ip = ip.strip().lower() - if (ip.startswith('::ffff:')): - return ip.replace('::ffff:', '') - return ip - - -def is_valid_ipv4(ip_str): - """ - Check the validity of an IPv4 address - """ - 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): - """ - Check the validity of an IPv6 address - """ - try: - socket.inet_pton(socket.AF_INET6, ip_str) - except socket.error: - return False - return True - - -def is_valid_ip(ip_str): - """ - Check the validity of an IP address - """ - return is_valid_ipv4(ip_str) or is_valid_ipv6(ip_str) - - -def is_private_ip(ip_str): - """ - Returns true of ip_str is private & not routable, else return false - """ - return ip_str.startswith(defs.IPWARE_NON_PUBLIC_IP_PREFIX) - - -def is_public_ip(ip_str): - """ - Returns true of ip_str is public & routable, else return false - """ - return not is_private_ip(ip_str) - - -def is_loopback_ip(ip_str): - """ - Returns true of ip_str is public & routable, else return false - """ - return ip_str.startswith(defs.IPWARE_LOOPBACK_PREFIX) - - -def get_request_meta(request, key): - """ - Given a key, it returns a cleaned up version of the value from request.META, or None - """ - value = request.META.get(key, request.META.get(key.replace('_', '-'), '')).strip() - if value == '': - return None - return value - - -def get_ips_from_string(ip_str): - """ - Given a string, it returns a list of one or more valid IP addresses - """ - ip_list = [] - - for ip in ip_str.split(','): - clean_ip = ip.strip().lower() - if clean_ip: - ip_list.append(clean_ip) - - ip_count = len(ip_list) - if ip_count > 0 and is_valid_ip(ip_list[0]) and is_valid_ip(ip_list[-1]): - return ip_list, ip_count - - return [], 0 - - -def get_ip_info(ip_str): - """ - Given a string, it returns a tuple of (IP, Routable). - """ - ip = None - is_routable_ip = False - clean_ip = cleanup_ip(ip_str) - if is_valid_ip(clean_ip): - ip = clean_ip - is_routable_ip = is_public_ip(ip) - return ip, is_routable_ip - - -def get_best_ip(last_ip, next_ip): - """ - Given two IP addresses, it returns the the best match ip. - Order of precedence is (Public, Private, Loopback, None) - Right-most IP is returned - """ - if last_ip is None: - return next_ip - if is_public_ip(last_ip) and not is_public_ip(next_ip): - return last_ip - if is_private_ip(last_ip) and is_loopback_ip(next_ip): - return last_ip - return next_ip diff --git a/setup.py b/setup.py index fb1c8be..f0c4608 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ python_requires = ">=3.8" here = os.path.abspath(os.path.dirname(__file__)) -requires = [] +requires = ['python-ipware>=2.0.0'] test_requirements = [] about = {}