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)