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

1import ipaddress 

2import re 

3import uuid 

4 

5 

6def get_client_ip(request): 

7 """Extract the real client IP from X-Forwarded-For for django-ratelimit. 

8 

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. 

13 

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") 

26 

27 

28class RequestIDMiddleware: 

29 """Attach a unique request_id to each request for log correlation.""" 

30 

31 def __init__(self, get_response): 

32 self.get_response = get_response 

33 

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 

39 

40 

41class DeviceDetectionMiddleware: 

42 """Middleware to detect legacy browsers that can't run modern React. 

43 

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 

50 

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 """ 

54 

55 # Pattern to match iOS version from user agent 

56 IOS_PATTERN = re.compile(r"(?:iPhone|iPad|iPod).*OS (\d+)_") 

57 

58 # Pattern to match Chrome version 

59 CHROME_PATTERN = re.compile(r"Chrome/(\d+)\.") 

60 

61 # Pattern to match Firefox version 

62 FIREFOX_PATTERN = re.compile(r"Firefox/(\d+)\.") 

63 

64 def __init__(self, get_response): 

65 self.get_response = get_response 

66 

67 def __call__(self, request): 

68 request.is_legacy_device = self._is_legacy_device(request) 

69 return self.get_response(request) 

70 

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 

76 

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) 

85 

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 

90 

91 def _is_internet_explorer(self, ua): 

92 """Internet Explorer (all versions).""" 

93 return "MSIE " in ua or "Trident/" in ua 

94 

95 def _is_edge_legacy(self, ua): 

96 """Edge Legacy (non-Chromium, pre-2020).""" 

97 return "Edge/" in ua and "Edg/" not in ua 

98 

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 

105 

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 

← Back to Dashboard