Skip to content

Commit e792ad6

Browse files
committed
Make Ninja's Throttling client IP lookup function configurable via a new setting 'NINJA_CLIENT_IP_CALLABLE'.
1 parent b17dcec commit e792ad6

4 files changed

Lines changed: 85 additions & 25 deletions

File tree

docs/docs/guides/throttling.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,21 @@ class NoReadsThrottle(AnonRateThrottle):
105105
return True
106106
return super().allow_request(request)
107107
```
108+
109+
## Customizing Client IP Address Lookups
110+
111+
To use custom client IP address lookup logic, change the `NINJA_CLIENT_IP_CALLABLE` setting to a suitable callable path.
112+
113+
Example
114+
115+
```Python
116+
from django.http import HttpRequest
117+
118+
def get_client_ip(request: HttpRequest) -> str:
119+
return request.META.get("REMOTE_ADDR")
120+
```
121+
122+
`settings.py`:
123+
```python
124+
NINJA_CLIENT_IP_CALLABLE = "example.utils.get_client_ip"
125+
```

ninja/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ class Settings(BaseModel):
2121

2222
# Throttling
2323
NUM_PROXIES: Optional[int] = Field(None, alias="NINJA_NUM_PROXIES")
24+
CLIENT_IP_CALLABLE: str = Field(
25+
"ninja.throttling.get_client_ip", alias="NINJA_CLIENT_IP_CALLABLE"
26+
)
2427
DEFAULT_THROTTLE_RATES: Dict[str, Optional[str]] = Field(
2528
{
2629
"auth": "10000/day",

ninja/throttling.py

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,59 @@
11
import hashlib
22
import time
3+
import warnings
34
from typing import Dict, List, Optional, Tuple
45

56
from django.core.cache import cache as default_cache
67
from django.core.exceptions import ImproperlyConfigured
78
from django.http import HttpRequest
9+
from django.utils.module_loading import import_string
10+
11+
12+
def get_client_ip(request: HttpRequest) -> Optional[str]:
13+
"""
14+
Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR
15+
if present and number of proxies is > 0. If not use all of
16+
HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR.
17+
"""
18+
from ninja.conf import settings
19+
20+
xff = request.META.get("HTTP_X_FORWARDED_FOR")
21+
remote_addr = request.META.get("REMOTE_ADDR")
22+
num_proxies = settings.NUM_PROXIES
23+
24+
if num_proxies is not None:
25+
if num_proxies == 0 or xff is None:
26+
return remote_addr
27+
addrs: List[str] = xff.split(",")
28+
client_addr = addrs[-min(num_proxies, len(addrs))]
29+
return client_addr.strip()
30+
31+
return "".join(xff.split()) if xff else remote_addr
832

933

1034
class BaseThrottle:
1135
"""
1236
Rate throttling of requests.
1337
"""
1438

39+
def __init__(self) -> None:
40+
from ninja.conf import settings
41+
42+
self.client_ip = import_string(settings.CLIENT_IP_CALLABLE)
43+
1544
def allow_request(self, request: HttpRequest) -> bool:
1645
"""
1746
Return `True` if the request should be allowed, `False` otherwise.
1847
"""
1948
raise NotImplementedError(".allow_request() must be overridden")
2049

2150
def get_ident(self, request: HttpRequest) -> Optional[str]:
22-
"""
23-
Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR
24-
if present and number of proxies is > 0. If not use all of
25-
HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR.
26-
"""
27-
from ninja.conf import settings
28-
29-
xff = request.META.get("HTTP_X_FORWARDED_FOR")
30-
remote_addr = request.META.get("REMOTE_ADDR")
31-
num_proxies = settings.NUM_PROXIES
32-
33-
if num_proxies is not None:
34-
if num_proxies == 0 or xff is None:
35-
return remote_addr
36-
addrs: List[str] = xff.split(",")
37-
client_addr = addrs[-min(num_proxies, len(addrs))]
38-
return client_addr.strip()
39-
40-
return "".join(xff.split()) if xff else remote_addr
51+
warnings.warn(
52+
".get_ident() is deprecated, use .client_ip() instead",
53+
DeprecationWarning,
54+
stacklevel=2,
55+
)
56+
return get_client_ip(request)
4157

4258
def wait(self) -> Optional[float]:
4359
"""
@@ -79,6 +95,7 @@ class SimpleRateThrottle(BaseThrottle):
7995
}
8096

8197
def __init__(self, rate: Optional[str] = None):
98+
super().__init__()
8299
self.rate: Optional[str]
83100
if rate:
84101
self.rate = rate
@@ -204,7 +221,7 @@ def get_cache_key(self, request: HttpRequest) -> Optional[str]:
204221

205222
return self.cache_format % {
206223
"scope": self.scope,
207-
"ident": self.get_ident(request),
224+
"ident": self.client_ip(request),
208225
}
209226

210227

@@ -224,7 +241,7 @@ def get_cache_key(self, request: HttpRequest) -> str:
224241
ident = hashlib.sha256(str(request.auth).encode()).hexdigest() # type: ignore
225242
# TODO: ^maybe auth should have an attribute that developer can overwrite
226243
else:
227-
ident = self.get_ident(request) # type: ignore
244+
ident = self.client_ip(request)
228245

229246
return self.cache_format % {"scope": self.scope, "ident": ident}
230247

@@ -244,6 +261,6 @@ def get_cache_key(self, request: HttpRequest) -> str:
244261
if request.user and request.user.is_authenticated:
245262
ident = request.user.pk
246263
else:
247-
ident = self.get_ident(request)
264+
ident = self.client_ip(request)
248265

249266
return self.cache_format % {"scope": self.scope, "ident": ident}

tests/test_throttling.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,18 +278,40 @@ def test_proxy_throttle():
278278

279279
th = SimpleRateThrottle("1/s")
280280
request = build_request(x_forwarded_for=None)
281-
assert th.get_ident(request) == "8.8.8.8"
281+
assert th.client_ip(request) == "8.8.8.8"
282+
assert th.get_ident(request) == "8.8.8.8" # Deprecated
282283

283284
settings.NUM_PROXIES = 0
284285
request = build_request(x_forwarded_for="8.8.8.8,127.0.0.1")
285-
assert th.get_ident(request) == "8.8.8.8"
286+
assert th.client_ip(request) == "8.8.8.8"
287+
assert th.get_ident(request) == "8.8.8.8" # Deprecated
286288

287289
settings.NUM_PROXIES = 1
288-
assert th.get_ident(request) == "127.0.0.1"
290+
assert th.client_ip(request) == "127.0.0.1"
291+
assert th.get_ident(request) == "127.0.0.1" # Deprecated
289292

290293
settings.NUM_PROXIES = None
291294

292295

296+
def custom_client_ip_callable(request) -> str:
297+
return "custom-ip"
298+
299+
300+
def test_proxy_throttle_custom_client_ip_callable():
301+
from ninja.conf import settings
302+
303+
settings.CLIENT_IP_CALLABLE = "tests.test_throttling.custom_client_ip_callable"
304+
305+
th = SimpleRateThrottle("1/s")
306+
request = build_request()
307+
assert th.client_ip(request) == "custom-ip"
308+
assert (
309+
th.get_ident(request) == "8.8.8.8"
310+
) # Note: Deprecated function preserves old behavior and ignores new CLIENT_IP_CALLABLE setting.
311+
312+
settings.CLIENT_IP_CALLABLE = "ninja.throttling.get_client_ip"
313+
314+
293315
def test_base_classes():
294316
base = BaseThrottle()
295317
with pytest.raises(NotImplementedError):

0 commit comments

Comments
 (0)