From ec7b1b38d84d7ceff1ac00f2bcf7b6b0fc9a16ec Mon Sep 17 00:00:00 2001 From: James Socol Date: Sun, 24 Jan 2021 21:12:15 -0500 Subject: [PATCH] Add clear exception if REMOTE_ADDR is empty Certain configurations of Django when running behind a reverse proxy that communicates with the app server over Unix sockets can lead to REMOTE_ADDR being empty or missing. In these situations, IP-based ratelimiting can behave unexpectedly or fail in unclear ways. This makes the exception for an empty REMOTE_ADDR more clear and points to the documentation. --- CHANGELOG | 1 + ratelimit/core.py | 30 ++++++++++++++++++------------ ratelimit/tests.py | 7 +++++++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index edd56e0..8754df0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,6 +14,7 @@ Minor changes: -------------- - Factor up _get_ip() logic into a single place (#218) +- Exception on empty REMOTE_ADDR is clearer (#220) 3.0.1 ===== diff --git a/ratelimit/core.py b/ratelimit/core.py index 592562b..77fa6fb 100644 --- a/ratelimit/core.py +++ b/ratelimit/core.py @@ -29,18 +29,24 @@ def _get_ip(request): ip_meta = getattr(settings, 'RATELIMIT_IP_META_KEY', None) if not ip_meta: - return request.META['REMOTE_ADDR'] - if callable(ip_meta): - return ip_meta(request) - if isinstance(ip_meta, str) and '.' in ip_meta: + ip = request.META['REMOTE_ADDR'] + if not ip: + raise ImproperlyConfigured( + 'IP address in REMOTE_ADDR is empty. This can happen when ' + 'using a reverse proxy and connecting to the app server with ' + 'Unix sockets. See the documentation for ' + 'RATELIMIT_IP_META_KEY: https://bit.ly/3iIpy2x') + elif callable(ip_meta): + ip = ip_meta(request) + elif isinstance(ip_meta, str) and '.' in ip_meta: ip_meta_fn = import_string(ip_meta) - return ip_meta_fn(request) - if ip_meta in request.META: - return request.META[ip_meta] - raise ImproperlyConfigured('Could not get IP address from "%s"' % ip_meta) - + ip = ip_meta_fn(request) + elif ip_meta in request.META: + ip = request.META[ip_meta] + else: + raise ImproperlyConfigured( + 'Could not get IP address from "%s"' % ip_meta) -def ip_mask(ip): if ':' in ip: # IPv6 mask = getattr(settings, 'RATELIMIT_IPV6_MASK', 64) @@ -56,11 +62,11 @@ def ip_mask(ip): def user_or_ip(request): if request.user.is_authenticated: return str(request.user.pk) - return ip_mask(_get_ip(request)) + return _get_ip(request) _SIMPLE_KEYS = { - 'ip': lambda r: ip_mask(_get_ip(r)), + 'ip': lambda r: _get_ip(r), 'user': lambda r: str(r.user.pk), 'user_or_ip': user_or_ip, } diff --git a/ratelimit/tests.py b/ratelimit/tests.py index de58de1..e4d34d4 100644 --- a/ratelimit/tests.py +++ b/ratelimit/tests.py @@ -622,3 +622,10 @@ def test_callable_ip_key(self): req.META['MY_THING'] = '5.6.7.8' assert '5.6.7.8' == _get_ip(req) + + def test_empty_ip(self): + req = rf.get('/') + req.META['REMOTE_ADDR'] = '' + + with self.assertRaises(ImproperlyConfigured): + _get_ip(req)