Sage100-vps/security/rate_limiter.py
2026-01-02 17:56:28 +03:00

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),
},
)