147 lines
4.9 KiB
Python
147 lines
4.9 KiB
Python
from fastapi import Request, HTTPException, status
|
|
from typing import Optional, Tuple
|
|
import logging
|
|
|
|
from config.config import settings
|
|
from services.redis_service import redis_service
|
|
from security.fingerprint import get_client_ip
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RateLimiter:
|
|
@staticmethod
|
|
def _make_key(identifier: str, action: str) -> str:
|
|
return f"{action}:{identifier}"
|
|
|
|
@classmethod
|
|
async def check_login_rate_limit(
|
|
cls, email: str, ip_address: str
|
|
) -> Tuple[bool, Optional[str], int]:
|
|
window_seconds = settings.rate_limit_login_window_minutes * 60
|
|
max_attempts = settings.rate_limit_login_attempts
|
|
|
|
email_key = cls._make_key(email.lower(), "login_email")
|
|
email_count = await redis_service.get_rate_limit_count(email_key)
|
|
|
|
if email_count >= max_attempts:
|
|
return (
|
|
False,
|
|
f"Trop de tentatives pour cet email. Reessayez dans {settings.rate_limit_login_window_minutes} minutes.",
|
|
window_seconds,
|
|
)
|
|
|
|
ip_key = cls._make_key(ip_address, "login_ip")
|
|
ip_count = await redis_service.get_rate_limit_count(ip_key)
|
|
|
|
ip_limit = max_attempts * 3
|
|
if ip_count >= ip_limit:
|
|
return (
|
|
False,
|
|
window_seconds,
|
|
)
|
|
|
|
return (True, None, 0)
|
|
|
|
@classmethod
|
|
async def record_login_attempt(
|
|
cls, email: str, ip_address: str, success: bool
|
|
) -> None:
|
|
window_seconds = settings.rate_limit_login_window_minutes * 60
|
|
|
|
if success:
|
|
email_key = cls._make_key(email.lower(), "login_email")
|
|
await redis_service.reset_rate_limit(email_key)
|
|
logger.debug(f"Rate limit reinitialise pour {email}")
|
|
else:
|
|
email_key = cls._make_key(email.lower(), "login_email")
|
|
await redis_service.increment_rate_limit(email_key, window_seconds)
|
|
|
|
ip_key = cls._make_key(ip_address, "login_ip")
|
|
await redis_service.increment_rate_limit(ip_key, window_seconds)
|
|
|
|
logger.debug(
|
|
f"Tentative echouee enregistree pour {email} depuis {ip_address}"
|
|
)
|
|
|
|
@classmethod
|
|
async def check_api_rate_limit(
|
|
cls, identifier: str, endpoint: Optional[str] = None
|
|
) -> Tuple[bool, int, int]:
|
|
window_seconds = settings.rate_limit_api_window_seconds
|
|
max_requests = settings.rate_limit_api_requests
|
|
|
|
if endpoint:
|
|
key = cls._make_key(f"{identifier}:{endpoint}", "api")
|
|
else:
|
|
key = cls._make_key(identifier, "api")
|
|
|
|
count = await redis_service.increment_rate_limit(key, window_seconds)
|
|
remaining = max(0, max_requests - count)
|
|
|
|
if count > max_requests:
|
|
return (False, remaining, window_seconds)
|
|
|
|
return (True, remaining, window_seconds)
|
|
|
|
@classmethod
|
|
async def check_password_reset_rate_limit(
|
|
cls, email: str, ip_address: str
|
|
) -> Tuple[bool, Optional[str]]:
|
|
window_seconds = 3600
|
|
max_attempts_email = 3
|
|
max_attempts_ip = 10
|
|
|
|
email_key = cls._make_key(email.lower(), "reset_email")
|
|
email_count = await redis_service.get_rate_limit_count(email_key)
|
|
|
|
if email_count >= max_attempts_email:
|
|
return (False, "Trop de demandes de reinitialisation pour cet email.")
|
|
|
|
ip_key = cls._make_key(ip_address, "reset_ip")
|
|
ip_count = await redis_service.get_rate_limit_count(ip_key)
|
|
|
|
if ip_count >= max_attempts_ip:
|
|
return (False, "Trop de demandes depuis cette adresse IP.")
|
|
|
|
await redis_service.increment_rate_limit(email_key, window_seconds)
|
|
await redis_service.increment_rate_limit(ip_key, window_seconds)
|
|
|
|
return (True, None)
|
|
|
|
@classmethod
|
|
async def check_registration_rate_limit(
|
|
cls, ip_address: str
|
|
) -> Tuple[bool, Optional[str]]:
|
|
window_seconds = 3600
|
|
max_registrations = 5
|
|
|
|
key = cls._make_key(ip_address, "register_ip")
|
|
count = await redis_service.get_rate_limit_count(key)
|
|
|
|
if count >= max_registrations:
|
|
return (False, "Trop d'inscriptions depuis cette adresse IP.")
|
|
|
|
await redis_service.increment_rate_limit(key, window_seconds)
|
|
|
|
return (True, None)
|
|
|
|
|
|
async def check_rate_limit_dependency(request: Request) -> None:
|
|
ip = get_client_ip(request)
|
|
|
|
allowed, remaining, reset_seconds = await RateLimiter.check_api_rate_limit(ip)
|
|
|
|
request.state.rate_limit_remaining = remaining
|
|
request.state.rate_limit_reset = reset_seconds
|
|
|
|
if not allowed:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
detail="Limite de requetes atteinte. Reessayez plus tard.",
|
|
headers={
|
|
"X-RateLimit-Remaining": "0",
|
|
"X-RateLimit-Reset": str(reset_seconds),
|
|
"Retry-After": str(reset_seconds),
|
|
},
|
|
)
|