Added additional modificators
This commit is contained in:
parent
8dbf0f9df6
commit
ec1cccc8f7
@ -197,10 +197,13 @@ hoptchaPost('/endpoint', payload, onSuccess, onError, function renderCustom(url,
|
|||||||
|
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|-------------|--------------------------------------------------------------|---------------|
|
|-------------|--------------------------------------------------------------|---------------|
|
||||||
|
| `key` | Function or string to identify requestor (IP, user ID, etc.) | `ip` |
|
||||||
| `threshold` | Number of allowed attempts before CAPTCHA is required | `5` |
|
| `threshold` | Number of allowed attempts before CAPTCHA is required | `5` |
|
||||||
| `timeout` | Time in seconds to reset attempt count | `300` (5 min) |
|
| `timeout` | Time in seconds to reset attempt count | `300` (5 min) |
|
||||||
| `key` | Function or string to identify requestor (IP, user ID, etc.) | `ip` |
|
| `backoff` | Exponentially increase timeout if repeatedly exceeded | `False` |
|
||||||
|
| `response` | Optional custom response function on CAPTCHA failure | `None` |
|
||||||
|
| `exempt_if` | Skip protection for trusted users | For staff |
|
||||||
|
| `methods` | HTTP methods to track (POST, GET, etc.) | POST |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -25,41 +25,83 @@ SOFTWARE.
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from .validators import verify_token
|
from .validators import verify_token
|
||||||
from .settings import GENERATE_URL, PUBLIC_KEY
|
from .settings import GENERATE_URL, PUBLIC_KEY
|
||||||
|
|
||||||
def get_builtin_key_func(name):
|
# Built-in key functions
|
||||||
if name == 'ip':
|
def get_ip(request):
|
||||||
return lambda request: request.META.get('REMOTE_ADDR', 'unknown-ip')
|
return request.META.get('REMOTE_ADDR', 'unknown-ip')
|
||||||
raise ValueError(f"Unknown built-in key: '{name}'")
|
|
||||||
|
|
||||||
def hoptcha_protected(threshold=5, timeout=300, key='ip'):
|
def get_user(request):
|
||||||
"""
|
return str(request.user.id) if request.user.is_authenticated else None
|
||||||
Decorator that applies CAPTCHA verification after `threshold` failed attempts,
|
|
||||||
tracked via cache using a customizable key. The `key` can be:
|
|
||||||
- a string like 'ip' to use a built-in method
|
|
||||||
- a callable that accepts a Django request and returns a string
|
|
||||||
"""
|
|
||||||
|
|
||||||
key_func = get_builtin_key_func(key) if isinstance(key, str) else key
|
def get_user_or_ip(request):
|
||||||
|
return get_user(request) or get_ip(request)
|
||||||
|
|
||||||
|
BUILTIN_KEYS = {
|
||||||
|
'ip': get_ip,
|
||||||
|
'user': get_user,
|
||||||
|
'user_or_ip': get_user_or_ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
def hoptcha_protected(
|
||||||
|
threshold=5,
|
||||||
|
timeout=300,
|
||||||
|
key="ip",
|
||||||
|
methods=["POST"],
|
||||||
|
response=None,
|
||||||
|
exempt_if=lambda request: request.user.is_staff or request.user.is_superuser,
|
||||||
|
backoff=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Enforces CAPTCHA if request exceeds `threshold`.
|
||||||
|
|
||||||
|
- key: 'ip', 'user', 'user_or_ip', or custom function.
|
||||||
|
- threshold: # of allowed unauthenticated attempts before requiring CAPTCHA.
|
||||||
|
- timeout: seconds to keep attempt count in cache.
|
||||||
|
- backoff: exponentially increase timeout if repeatedly exceeded.
|
||||||
|
- response: optional custom response function on CAPTCHA failure.
|
||||||
|
- exempt_if: skip protection for trusted users.
|
||||||
|
- methods: HTTP methods to track (default: POST).
|
||||||
|
"""
|
||||||
|
if isinstance(key, str):
|
||||||
|
key_func = BUILTIN_KEYS.get(key)
|
||||||
|
if not key_func:
|
||||||
|
raise ValueError(f"Unknown key: {key}")
|
||||||
|
elif callable(key):
|
||||||
|
key_func = key
|
||||||
|
else:
|
||||||
|
raise TypeError("key must be a string or callable")
|
||||||
|
|
||||||
def decorator(view_func):
|
def decorator(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def _wrapped_view(request, *args, **kwargs):
|
def _wrapped_view(request, *args, **kwargs):
|
||||||
key_val = key_func(request)
|
if exempt_if and exempt_if(request):
|
||||||
cache_key = f"hoptcha-attempts:{key_val}"
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
if request.method not in methods:
|
||||||
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
|
user_key = key_func(request)
|
||||||
|
cache_key = f"hoptcha-attempts:{user_key}"
|
||||||
attempts = cache.get(cache_key, 0)
|
attempts = cache.get(cache_key, 0)
|
||||||
|
|
||||||
if attempts >= threshold:
|
if attempts >= threshold:
|
||||||
token = request.POST.get("captcha_token") or request.GET.get("captcha_token")
|
token = request.POST.get("captcha_token") or request.GET.get("captcha_token")
|
||||||
if not token or not verify_token(token):
|
if not token or not verify_token(token):
|
||||||
return JsonResponse({
|
return response(request) if response else JsonResponse({
|
||||||
"error": "CAPTCHA",
|
"error": "CAPTCHA",
|
||||||
"url": GENERATE_URL,
|
"url": GENERATE_URL,
|
||||||
"key": PUBLIC_KEY
|
"key": PUBLIC_KEY
|
||||||
}, status=400)
|
}, status=400)
|
||||||
|
else:
|
||||||
|
cache.delete(cache_key) # reset counter if passed
|
||||||
|
|
||||||
|
timeout_val = timeout * (2 ** (attempts - threshold)) if backoff and attempts >= threshold else timeout
|
||||||
|
cache.set(cache_key, attempts + 1, timeout=timeout_val)
|
||||||
|
|
||||||
cache.set(cache_key, attempts + 1, timeout=timeout)
|
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
|
|
||||||
return _wrapped_view
|
return _wrapped_view
|
||||||
return decorator
|
return decorator
|
||||||
|
Loading…
x
Reference in New Issue
Block a user