Coverage for apps / core / middleware.py: 100%
51 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-12 10:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-12 10:49 +0000
1import ipaddress
2import re
3import uuid
6def get_client_ip(request):
7 """Extract the real client IP from X-Forwarded-For for django-ratelimit.
9 X-Forwarded-For may contain multiple IPs: "client, proxy1, proxy2".
10 We take the leftmost entry (the original client), validate it as a
11 real IP address, and return it. Falls back to REMOTE_ADDR if the
12 header is missing or every entry is malformed.
14 Used via RATELIMIT_IP_META_KEY = "apps.core.middleware.get_client_ip"
15 """
16 forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
17 if forwarded_for:
18 # Take the leftmost (client) IP, strip whitespace
19 candidate = forwarded_for.split(",")[0].strip()
20 try:
21 ipaddress.ip_address(candidate)
22 return candidate
23 except ValueError:
24 pass
25 return request.META.get("REMOTE_ADDR", "127.0.0.1")
28class RequestIDMiddleware:
29 """Attach a unique request_id to each request for log correlation."""
31 def __init__(self, get_response):
32 self.get_response = get_response
34 def __call__(self, request):
35 request.request_id = str(uuid.uuid4())[:8]
36 response = self.get_response(request)
37 response["X-Request-ID"] = request.request_id
38 return response
41class DeviceDetectionMiddleware:
42 """Middleware to detect legacy browsers that can't run modern React.
44 Sets request.is_legacy_device = True for browsers that need the legacy frontend:
45 - iOS < 11 (Safari lacks ES6 module support)
46 - Internet Explorer (all versions)
47 - Edge Legacy (non-Chromium, pre-2020)
48 - Chrome < 60
49 - Firefox < 55
51 Note: Actual redirects are handled by Nginx for performance (see nginx/nginx.conf).
52 This middleware provides the detection flag for use in views/templates if needed.
53 """
55 # Pattern to match iOS version from user agent
56 IOS_PATTERN = re.compile(r"(?:iPhone|iPad|iPod).*OS (\d+)_")
58 # Pattern to match Chrome version
59 CHROME_PATTERN = re.compile(r"Chrome/(\d+)\.")
61 # Pattern to match Firefox version
62 FIREFOX_PATTERN = re.compile(r"Firefox/(\d+)\.")
64 def __init__(self, get_response):
65 self.get_response = get_response
67 def __call__(self, request):
68 request.is_legacy_device = self._is_legacy_device(request)
69 return self.get_response(request)
71 def _is_legacy_device(self, request):
72 """Check if the request is from a browser that can't run modern React."""
73 user_agent = request.META.get("HTTP_USER_AGENT", "")
74 if not user_agent:
75 return False
77 detectors = [
78 self._is_legacy_ios,
79 self._is_internet_explorer,
80 self._is_edge_legacy,
81 self._is_old_chrome,
82 self._is_old_firefox,
83 ]
84 return any(detect(user_agent) for detect in detectors)
86 def _is_legacy_ios(self, ua):
87 """iOS < 11 (Safari lacks ES6 module support)."""
88 match = self.IOS_PATTERN.search(ua)
89 return match is not None and int(match.group(1)) < 11
91 def _is_internet_explorer(self, ua):
92 """Internet Explorer (all versions)."""
93 return "MSIE " in ua or "Trident/" in ua
95 def _is_edge_legacy(self, ua):
96 """Edge Legacy (non-Chromium, pre-2020)."""
97 return "Edge/" in ua and "Edg/" not in ua
99 def _is_old_chrome(self, ua):
100 """Chrome < 60 (excluding Edge and Opera)."""
101 if "Chrome/" not in ua or "Edg" in ua or "OPR/" in ua:
102 return False
103 match = self.CHROME_PATTERN.search(ua)
104 return match is not None and int(match.group(1)) < 60
106 def _is_old_firefox(self, ua):
107 """Firefox < 55."""
108 match = self.FIREFOX_PATTERN.search(ua)
109 return match is not None and int(match.group(1)) < 55