1+ import functools
2+ from typing import Any , Callable , Optional
3+ from django .conf import settings
4+ from django .http import HttpResponse , JsonResponse
5+ from django .utils .decorators import method_decorator
6+ from django .utils .module_loading import import_strin
7+ from ratelink .utils .key_generators import KeyGeneratorFunc , by_ip
8+
9+ class DjangoRateLimitMiddleware :
10+ def __init__ (self , get_response ):
11+ self .get_response = get_response
12+
13+ config = getattr (settings , 'RATE_LIMITING' , {})
14+
15+ default = config .get ('DEFAULT' , {})
16+ self .default_limit = default .get ('limit' )
17+ self .default_window = default .get ('window' )
18+
19+ key_func_path = config .get ('KEY_FUNC' )
20+ if key_func_path :
21+ self .key_func = import_string (key_func_path )()
22+ else :
23+ self .key_func = by_ip ()
24+
25+ self .skip_paths = set (config .get ('SKIP_PATHS' , []))
26+
27+ limiter_path = config .get ('LIMITER' )
28+ if limiter_path :
29+ self .limiter = import_string (limiter_path )
30+ else :
31+ try :
32+ from myapp .rate_limiter import limiter
33+ self .limiter = limiter
34+ except ImportError :
35+ self .limiter = None
36+
37+ def __call__ (self , request ):
38+ if self .limiter is None :
39+ return self .get_response (request )
40+
41+ if request .path in self .skip_paths :
42+ return self .get_response (request )
43+
44+ if hasattr (request , 'resolver_match' ) and request .resolver_match :
45+ func = request .resolver_match .func
46+ if hasattr (func , '_rate_limit_exempt' ):
47+ return self .get_response (request )
48+
49+ key = self .key_func (request )
50+
51+ allowed , state = self .limiter .check (key )
52+
53+ if not allowed :
54+ return self ._make_error_response (state )
55+
56+ response = self .get_response (request )
57+ response ['X-RateLimit-Limit' ] = str (state .get ('limit' , 0 ))
58+ response ['X-RateLimit-Remaining' ] = str (state .get ('remaining' , 0 ))
59+
60+ return response
61+
62+ def _make_error_response (self , state : dict ) -> JsonResponse :
63+ retry_after = state .get ('retry_after' , 0 )
64+
65+ response = JsonResponse ({
66+ "error" : "Rate limit exceeded" ,
67+ "limit" : state .get ('limit' , 0 ),
68+ "remaining" : 0 ,
69+ "retry_after" : retry_after
70+ }, status = 429 )
71+
72+ response ['Retry-After' ] = str (int (retry_after ))
73+ response ['X-RateLimit-Limit' ] = str (state .get ('limit' , 0 ))
74+ response ['X-RateLimit-Remaining' ] = '0'
75+
76+ return response
77+
78+
79+ def django_rate_limit (
80+ limiter : Any = None ,
81+ limit : Optional [int ] = None ,
82+ window : Optional [int ] = None ,
83+ key_func : Optional [KeyGeneratorFunc ] = None
84+ ) -> Callable :
85+ if key_func is None :
86+ key_func = by_ip ()
87+
88+ def decorator (func : Callable ) -> Callable :
89+ @functools .wraps (func )
90+ def wrapper (request , * args , ** kwargs ):
91+ actual_limiter = limiter
92+ if actual_limiter is None :
93+ try :
94+ from myapp .rate_limiter import limiter as global_limiter
95+ actual_limiter = global_limiter
96+ except ImportError :
97+ # No limiter available, skip
98+ return func (request , * args , ** kwargs )
99+
100+ key = key_func (request )
101+
102+ allowed , state = actual_limiter .check (key )
103+
104+ if not allowed :
105+ retry_after = state .get ('retry_after' , 0 )
106+ response = JsonResponse ({
107+ "error" : "Rate limit exceeded" ,
108+ "limit" : state .get ('limit' , 0 ),
109+ "remaining" : 0 ,
110+ "retry_after" : retry_after
111+ }, status = 429 )
112+
113+ response ['Retry-After' ] = str (int (retry_after ))
114+ response ['X-RateLimit-Limit' ] = str (state .get ('limit' , 0 ))
115+ response ['X-RateLimit-Remaining' ] = '0'
116+
117+ return response
118+
119+ return func (request , * args , ** kwargs )
120+
121+ return wrapper
122+
123+ return decorator
124+
125+
126+ def rate_limit_exempt (func : Callable ) -> Callable :
127+ func ._rate_limit_exempt = True
128+ return func
129+
130+
131+ class RateLimitMixin :
132+ rate_limit_limiter = None
133+ rate_limit_key_func = None
134+ rate_limit = None
135+ rate_window = None
136+
137+ @method_decorator (django_rate_limit )
138+ def dispatch (self , request , * args , ** kwargs ):
139+ if self .rate_limit_limiter :
140+ key_func = self .rate_limit_key_func or by_ip ()
141+ key = key_func (request )
142+
143+ allowed , state = self .rate_limit_limiter .check (key )
144+
145+ if not allowed :
146+ retry_after = state .get ('retry_after' , 0 )
147+ response = JsonResponse ({
148+ "error" : "Rate limit exceeded" ,
149+ "retry_after" : retry_after
150+ }, status = 429 )
151+ response ['Retry-After' ] = str (int (retry_after ))
152+ return response
153+
154+ return super ().dispatch (request , * args , ** kwargs )
155+
156+
157+ def get_rate_limiter_from_settings ():
158+ config = getattr (settings , 'RATE_LIMITING' , {})
159+ limiter_path = config .get ('LIMITER' )
160+
161+ if limiter_path :
162+ return import_string (limiter_path )
163+
164+ return None
0 commit comments