diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 289a58e..8fba131 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.8-v7.3.7'] + python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.8'] steps: - uses: actions/checkout@v2 diff --git a/CHANGES.rst b/CHANGES.rst index 38f77ea..ca78c18 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,24 @@ Changes ======= +0.9.4 +----- + +- Remove port number from IP address string when behind reverse proxy [@ndrsn] + +0.9.3 +----- + +- Drop Python 3.6 support from package specifiers. + +0.9.2 +----- + +- Drop Python 3.6 support. - Drop Django 3.1 support. - Confirm support for Django 4.0 - Confirm support for Python 3.10 +- Drop Django 2.2 support. 0.9.1 ----- diff --git a/README.rst b/README.rst index 6d761e4..96d5036 100644 --- a/README.rst +++ b/README.rst @@ -108,8 +108,8 @@ Admin pages Requirements ------------ -* Python: 3.6, 3.7, 3.8, 3.9, 3.10, PyPy -* Django: 2.2, 3.x, 4.x +* Python: 3.7, 3.8, 3.9, 3.10, PyPy +* Django: 3.x, 4.x * Redis diff --git a/defender/__init__.py b/defender/__init__.py index 9def504..c75ee0e 100644 --- a/defender/__init__.py +++ b/defender/__init__.py @@ -1,3 +1,3 @@ -VERSION = (0, 9, 1) +VERSION = (0, 9, 4) __version__ = ".".join((map(str, VERSION))) diff --git a/defender/ci_settings.py b/defender/ci_settings.py index b1167b8..af001db 100644 --- a/defender/ci_settings.py +++ b/defender/ci_settings.py @@ -45,12 +45,11 @@ TEMPLATES = [ "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", ], }, }, ] -if django.VERSION >= (3, 2): - TEMPLATES[0]["OPTIONS"]["context_processors"].append("django.template.context_processors.request") SECRET_KEY = os.environ.get("SECRET_KEY", "too-secret-for-test") diff --git a/defender/test_settings.py b/defender/test_settings.py index 082c2eb..6045482 100644 --- a/defender/test_settings.py +++ b/defender/test_settings.py @@ -41,12 +41,11 @@ TEMPLATES = [ "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", ], }, }, ] -if django.VERSION >= (3, 2): - TEMPLATES[0]["OPTIONS"]["context_processors"].append("django.template.context_processors.request") SECRET_KEY = os.environ.get("SECRET_KEY", "too-secret-for-test") diff --git a/defender/tests.py b/defender/tests.py index dcf556f..20cf1fa 100644 --- a/defender/tests.py +++ b/defender/tests.py @@ -1075,3 +1075,27 @@ class TestUtils(DefenderTestCase): utils.add_login_attempt_to_db(request, True, username=username) self.assertEqual(AccessAttempt.objects.filter(username=username).count(), 1) + + def test_ip_address_strip_port_number(self): + """ Test the strip_port_number() method """ + # IPv4 with/without port + self.assertEqual(utils.strip_port_number("192.168.1.1"), "192.168.1.1") + self.assertEqual(utils.strip_port_number( + "192.168.1.1:8000"), "192.168.1.1") + + # IPv6 with/without port + self.assertEqual(utils.strip_port_number( + "2001:db8:85a3:0:0:8a2e:370:7334"), "2001:db8:85a3:0:0:8a2e:370:7334") + self.assertEqual(utils.strip_port_number( + "[2001:db8:85a3:0:0:8a2e:370:7334]:123456"), "2001:db8:85a3:0:0:8a2e:370:7334") + + @patch("defender.config.BEHIND_REVERSE_PROXY", True) + def test_get_ip_strips_port_number(self): + """ make sure the IP address is stripped of its port number """ + req = HttpRequest() + req.META["HTTP_X_FORWARDED_FOR"] = "1.2.3.4:123456" + self.assertEqual(utils.get_ip(req), "1.2.3.4") + + req = HttpRequest() + req.META["HTTP_X_FORWARDED_FOR"] = "[2001:db8::1]:123456" + self.assertEqual(utils.get_ip(req), "2001:db8::1") diff --git a/defender/utils.py b/defender/utils.py index e390584..658377c 100644 --- a/defender/utils.py +++ b/defender/utils.py @@ -1,4 +1,5 @@ import logging +import re from django.http import HttpResponse from django.http import HttpResponseRedirect @@ -43,15 +44,51 @@ def get_ip_address_from_request(request): return "127.0.0.1" +ipv4_with_port = re.compile(r"^(\d+\.\d+\.\d+\.\d+):\d+") +ipv6_with_port = re.compile(r"^\[([^\]]+)\]:\d+") + + +def strip_port_number(ip_address_string): + """ strips port number from IPv4 or IPv6 address """ + ip_address = None + + if ipv4_with_port.match(ip_address_string): + match = ipv4_with_port.match(ip_address_string) + ip_address = match[1] + elif ipv6_with_port.match(ip_address_string): + match = ipv6_with_port.match(ip_address_string) + ip_address = match[1] + + """ + If it's not a valid IP address, we prefer to return + the string as-is instead of returning a potentially + corrupted string: + """ + if is_valid_ip(ip_address): + return ip_address + + return ip_address_string + + def get_ip(request): """ get the ip address from the request """ if config.BEHIND_REVERSE_PROXY: ip_address = request.META.get(config.REVERSE_PROXY_HEADER, "") ip_address = ip_address.split(",", 1)[0].strip() + if ip_address == "": ip_address = get_ip_address_from_request(request) + else: + """ + Some reverse proxies will include a port number with the + IP address; as this port may change from request to request, + and thus make it appear to be different IP addresses, we'll + want to remove the port number, if present: + """ + ip_address = strip_port_number(ip_address) else: ip_address = get_ip_address_from_request(request) + return ip_address diff --git a/setup.py b/setup.py index d657a1a..0f7a6c8 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ setup( classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Django", - "Framework :: Django :: 2.2", "Framework :: Django :: 3.2", "Framework :: Django :: 4.0", "Intended Audience :: Developers", @@ -40,8 +39,6 @@ setup( "Programming Language :: Python", 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", diff --git a/tox.ini b/tox.ini index 9f9e88f..148f05b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] envlist = # list of supported Django/Python versions: - py{36,37,38,39,py3}-dj{22,32} + py{37,38,39,py3}-dj{32} py{38,39,310}-dj{40,main} py38-{lint,docs} [gh-actions] python = - 3.6: py36 3.7: py37 3.8: py38 3.9: py39 @@ -17,7 +16,6 @@ python = [testenv] deps = -rrequirements.txt - dj22: django>=2.2,<2.3 dj32: django>=3.2,<4.0 dj40: django>=4.0,<4.1 djmain: https://github.com/django/django/archive/main.tar.gz