"""Shared Cloudflare proxy profile resolution.""" from dataclasses import dataclass from functools import lru_cache import re from typing import get_args from app.control.proxy.config import resolve_clearance_config from app.control.proxy.models import ProxyLease @dataclass(frozen=True) class ProxyProfile: cf_cookies: str = "" user_agent: str = "" cf_clearance: str = "" browser: str = "" def extract_cookie_value(cookie_header: str, name: str) -> str: if not cookie_header: return "" match = re.search(rf"(?:^|;\s*){re.escape(name)}=([^;]*)", cookie_header) return match.group(1) if match else "" @lru_cache(maxsize=1) def _supported_browser_profiles() -> set[str]: try: from curl_cffi.requests.impersonate import BrowserTypeLiteral return {str(item) for item in get_args(BrowserTypeLiteral)} except Exception: return set() def _supported_browser(candidate: str) -> str: if not candidate: return "" supported = _supported_browser_profiles() if not supported or candidate in supported: return candidate family = re.match(r"[a-z_]+", candidate) if family and family.group(0) in supported: return family.group(0) return "" def browser_from_user_agent(user_agent: str) -> str: ua = user_agent or "" lower = ua.lower() firefox = re.search(r"firefox/(\d+)", lower) if firefox: return _supported_browser(f"firefox{firefox.group(1)}") or _supported_browser( "firefox" ) edge = re.search(r"edg/(\d+)", lower) if edge: return _supported_browser(f"edge{edge.group(1)}") or _supported_browser("edge") chrome = re.search(r"(?:chrome|chromium|crios)/(\d+)", lower) if chrome: suffix = "_android" if "android" in lower else "" exact = _supported_browser(f"chrome{chrome.group(1)}{suffix}") return exact or _supported_browser("chrome_android" if suffix else "chrome") safari = "safari/" in lower and "chrome/" not in lower and "chromium/" not in lower if safari: return _supported_browser( "safari_ios" if ("iphone" in lower or "ipad" in lower) else "safari" ) return "" def resolve_proxy_profile(lease: ProxyLease | None) -> ProxyProfile: """Resolve cookies, UA, clearance and curl_cffi browser from one source order. Flat legacy keys win over the nested v2 clearance keys because external refresher sidecars write the flat values at runtime. The resolved browser is derived from the effective User-Agent first, keeping header UA, client-hints and curl_cffi impersonation aligned. """ cfg = resolve_clearance_config() if lease is not None: cookies = lease.cf_cookies or cfg.cf_cookies user_agent = lease.user_agent or cfg.user_agent clearance = ( extract_cookie_value(lease.cf_cookies, "cf_clearance") or cfg.cf_clearance ) else: cookies = cfg.cf_cookies user_agent = cfg.user_agent clearance = cfg.cf_clearance browser = ( browser_from_user_agent(user_agent) or _supported_browser(cfg.browser) or "chrome120" ) return ProxyProfile( cf_cookies=cookies, user_agent=user_agent, cf_clearance=clearance, browser=browser, ) __all__ = [ "ProxyProfile", "browser_from_user_agent", "extract_cookie_value", "resolve_proxy_profile", ]