122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
from fastapi import Request
|
|
from typing import Dict
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
|
|
from config.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DeviceFingerprint:
|
|
COMPONENT_EXTRACTORS = {
|
|
"user_agent": lambda r: r.headers.get("User-Agent", ""),
|
|
"accept_language": lambda r: r.headers.get("Accept-Language", ""),
|
|
"accept_encoding": lambda r: r.headers.get("Accept-Encoding", ""),
|
|
"accept": lambda r: r.headers.get("Accept", ""),
|
|
"connection": lambda r: r.headers.get("Connection", ""),
|
|
"cache_control": lambda r: r.headers.get("Cache-Control", ""),
|
|
"client_ip": lambda r: DeviceFingerprint._get_client_ip(r),
|
|
"sec_ch_ua": lambda r: r.headers.get("Sec-CH-UA", ""),
|
|
"sec_ch_ua_platform": lambda r: r.headers.get("Sec-CH-UA-Platform", ""),
|
|
"sec_ch_ua_mobile": lambda r: r.headers.get("Sec-CH-UA-Mobile", ""),
|
|
}
|
|
|
|
@staticmethod
|
|
def _get_client_ip(request: Request) -> str:
|
|
forwarded = request.headers.get("X-Forwarded-For")
|
|
if forwarded:
|
|
return forwarded.split(",")[0].strip()
|
|
|
|
real_ip = request.headers.get("X-Real-IP")
|
|
if real_ip:
|
|
return real_ip
|
|
|
|
if request.client:
|
|
return request.client.host
|
|
|
|
return ""
|
|
|
|
@classmethod
|
|
def extract_components(cls, request: Request) -> Dict[str, str]:
|
|
components = {}
|
|
|
|
for component_name in settings.fingerprint_components:
|
|
extractor = cls.COMPONENT_EXTRACTORS.get(component_name)
|
|
if extractor:
|
|
try:
|
|
value = extractor(request)
|
|
components[component_name] = value if value else ""
|
|
except Exception as e:
|
|
logger.warning(f"Erreur extraction composant {component_name}: {e}")
|
|
components[component_name] = ""
|
|
else:
|
|
logger.warning(f"Extracteur inconnu pour composant: {component_name}")
|
|
|
|
return components
|
|
|
|
@classmethod
|
|
def generate_hash(cls, request: Request, include_ip: bool = False) -> str:
|
|
components = cls.extract_components(request)
|
|
|
|
if not include_ip and "client_ip" in components:
|
|
del components["client_ip"]
|
|
|
|
sorted_keys = sorted(components.keys())
|
|
fingerprint_data = "|".join(f"{k}:{components[k]}" for k in sorted_keys)
|
|
|
|
secret = settings.fingerprint_secret or settings.jwt_secret
|
|
|
|
signature = hmac.new(
|
|
secret.encode(), fingerprint_data.encode(), hashlib.sha256
|
|
).hexdigest()
|
|
|
|
return signature
|
|
|
|
@classmethod
|
|
def generate_from_components(cls, components: Dict[str, str]) -> str:
|
|
sorted_keys = sorted(components.keys())
|
|
fingerprint_data = "|".join(f"{k}:{components.get(k, '')}" for k in sorted_keys)
|
|
|
|
secret = settings.fingerprint_secret or settings.jwt_secret
|
|
|
|
signature = hmac.new(
|
|
secret.encode(), fingerprint_data.encode(), hashlib.sha256
|
|
).hexdigest()
|
|
|
|
return signature
|
|
|
|
@classmethod
|
|
def validate(
|
|
cls, request: Request, stored_hash: str, include_ip: bool = False
|
|
) -> bool:
|
|
if not stored_hash:
|
|
return True
|
|
|
|
current_hash = cls.generate_hash(request, include_ip=include_ip)
|
|
|
|
return hmac.compare_digest(current_hash, stored_hash)
|
|
|
|
@classmethod
|
|
def get_device_info(cls, request: Request) -> Dict[str, str]:
|
|
user_agent = request.headers.get("User-Agent", "")
|
|
|
|
return {
|
|
"user_agent": user_agent[:500] if user_agent else "",
|
|
"ip_address": cls._get_client_ip(request),
|
|
"accept_language": request.headers.get("Accept-Language", "")[:100],
|
|
"fingerprint_hash": cls.generate_hash(request),
|
|
}
|
|
|
|
|
|
def get_fingerprint_hash(request: Request) -> str:
|
|
return DeviceFingerprint.generate_hash(request)
|
|
|
|
|
|
def validate_fingerprint(request: Request, stored_hash: str) -> bool:
|
|
return DeviceFingerprint.validate(request, stored_hash)
|
|
|
|
|
|
def get_client_ip(request: Request) -> str:
|
|
return DeviceFingerprint._get_client_ip(request)
|