Browse Source

Consolidate get_client_ip/ratelimit_ip into ratelimit.py; clean up settings access

- Move get_client_ip() and ratelimit_ip() from utils.py to
  sapl/middleware/ratelimit.py (canonical location).
  utils.py re-exports both via a single import line so all existing
  callers (comissoes, crud, norma, sessao, painel, parlamentares,
  protocoloadm) keep working with zero changes.
- get_client_ip() is now used inside RateLimitMiddleware instead of
  the weaker _get_ip(): gains ip_mask() for IPv6 /64 collapsing and
  HTTP_X_REAL_IP fallback.
- Replace getattr(settings, 'X', default) with settings.X throughout
  __init__: settings.py always defines these vars, defaults were
  duplicated and would silently drift. django.conf.settings proxy also
  honours @override_settings in tests, unlike direct module imports.
- Replace getattr(..., []) or [] with set(settings.RATE_LIMIT_WHITELIST_IPS):
  the cast in settings.py always returns a list, the double guard was
  redundant.
- Remove unused _get_ip() and 'from sapl.settings import RATE_LIMITER_RATE'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rate-limiter-2026
Edward Ribeiro 3 weeks ago
parent
commit
0e1a14e12a
  1. 47
      sapl/middleware/ratelimit.py
  2. 18
      sapl/utils.py

47
sapl/middleware/ratelimit.py

@ -51,11 +51,31 @@ def _sha256(s):
return hashlib.sha256(s.encode()).hexdigest()
def _get_ip(request):
return (
request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
or request.META.get('REMOTE_ADDR', '')
)
def get_client_ip(request):
"""
Return the real client IP, applying django-ratelimit's ip_mask so that
IPv6 /64 subnets are collapsed to a single key (prevents per-address
rotation attacks). Also checks HTTP_X_REAL_IP for nginx setups that
use that header instead of X-Forwarded-For.
Canonical source imported from here by other SAPL modules.
"""
from ratelimit.core import ip_mask
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
else:
ip = (
request.META.get('HTTP_X_REAL_IP')
or request.META.get('REMOTE_ADDR')
or '0.0.0.0'
)
return ip_mask(ip)
def ratelimit_ip(group, request):
"""Key function for django-ratelimit decorators (group param is ignored)."""
return get_client_ip(request)
def _is_suspicious_headers(request):
@ -80,17 +100,10 @@ class RateLimitMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.dry_run = getattr(settings, 'RATELIMIT_DRY_RUN', True)
anon_rate = getattr(settings, 'RATE_LIMITER_RATE', '35/m')
auth_rate = getattr(settings, 'RATE_LIMITER_RATE_AUTHENTICATED', '120/m')
self.anon_threshold, self.anon_window = _parse_rate(anon_rate)
self.auth_threshold, self.auth_window = _parse_rate(auth_rate)
self.whitelist = set(
getattr(settings, 'RATE_LIMIT_WHITELIST_IPS', []) or []
)
self.dry_run = settings.RATELIMIT_DRY_RUN
self.anon_threshold, self.anon_window = _parse_rate(settings.RATE_LIMITER_RATE)
self.auth_threshold, self.auth_window = _parse_rate(settings.RATE_LIMITER_RATE_AUTHENTICATED)
self.whitelist = set(settings.RATE_LIMIT_WHITELIST_IPS)
self._rl_cache = caches['ratelimit']
def __call__(self, request):
@ -116,7 +129,7 @@ class RateLimitMiddleware:
# ------------------------------------------------------------------
def _evaluate(self, request):
ip = _get_ip(request)
ip = get_client_ip(request)
if ip in self.whitelist:
return {'action': 'pass', 'ip': ip}

18
sapl/utils.py

@ -401,21 +401,9 @@ def xstr(s):
return '' if s is None else str(s)
def get_client_ip(request):
from ratelimit.core import ip_mask
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR') or '0.0.0.0'
return ip_mask(ip)
def ratelimit_ip(group, request):
"""
Ignore group param in django-ratelimit==3.0.1
"""
return get_client_ip(request)
# Canonical implementations live in sapl.middleware.ratelimit.
# Re-exported here so existing import sites keep working unchanged.
from sapl.middleware.ratelimit import get_client_ip, ratelimit_ip # noqa: F401, E402
def get_base_url(request):

Loading…
Cancel
Save