Spaces:
Sleeping
Sleeping
| """ | |
| MetadataService β Modular client tracking service. | |
| Extracts and enriches client metadata from HTTP requests for fraud detection & analytics. | |
| Components: | |
| 1. parse_user_agent() β Browser / OS / Device detection from User-Agent string | |
| 2. get_geoip_info() β Async GeoIP lookup via ip-api.com (free tier) | |
| 3. extract_headers() β Captures security-relevant HTTP headers (Client Hints, Referer, etc.) | |
| 4. build_metadata() β Merges frontend browser_meta with server-side headers into one JSON blob | |
| """ | |
| import json | |
| from typing import Optional, Dict, Any | |
| import httpx | |
| from fastapi import Request | |
| # βββ 1. User-Agent Parsing ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def parse_user_agent(ua: Optional[str]) -> Dict[str, str]: | |
| """ | |
| Parse a User-Agent string into browser, OS, and device type. | |
| Returns {"browser": ..., "os": ..., "device_type": ...} | |
| """ | |
| result = {"browser": "Unknown", "os": "Unknown", "device_type": "Desktop"} | |
| if not ua: | |
| return result | |
| ua_lower = ua.lower() | |
| # ββ Device ββ | |
| if "mobi" in ua_lower: | |
| result["device_type"] = "Mobile" | |
| elif "tablet" in ua_lower or "ipad" in ua_lower: | |
| result["device_type"] = "Tablet" | |
| # ββ Browser (order matters: Edge contains "chrome", so check Edge first) ββ | |
| if "edg" in ua_lower: | |
| result["browser"] = "Edge" | |
| elif "opr" in ua_lower or "opera" in ua_lower: | |
| result["browser"] = "Opera" | |
| elif "chrome" in ua_lower and "chromium" not in ua_lower: | |
| result["browser"] = "Chrome" | |
| elif "firefox" in ua_lower: | |
| result["browser"] = "Firefox" | |
| elif "safari" in ua_lower: | |
| result["browser"] = "Safari" | |
| # ββ OS (order matters: iPhone/iPad UAs contain "Mac OS X", so check them first) ββ | |
| if "iphone" in ua_lower: | |
| result["os"] = "iOS" | |
| result["device_type"] = "Mobile" | |
| elif "ipad" in ua_lower: | |
| result["os"] = "iPadOS" | |
| result["device_type"] = "Tablet" | |
| elif "android" in ua_lower: | |
| result["os"] = "Android" | |
| elif "windows" in ua_lower: | |
| result["os"] = "Windows" | |
| elif "mac os" in ua_lower or "macintosh" in ua_lower: | |
| result["os"] = "MacOS" | |
| elif "cros" in ua_lower: | |
| result["os"] = "ChromeOS" | |
| elif "linux" in ua_lower: | |
| result["os"] = "Linux" | |
| return result | |
| # βββ 2. GeoIP Lookup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def get_geoip_info(ip: Optional[str]) -> Optional[str]: | |
| """ | |
| Look up IP geolocation via ip-api.com. Returns JSON string or None. | |
| Skips localhost / private-range IPs. | |
| """ | |
| if not ip or ip in ("127.0.0.1", "localhost", "::1", "0.0.0.0"): | |
| return None | |
| # Skip common private ranges | |
| if ip and ip.startswith(("10.", "172.16.", "192.168.")): | |
| return None | |
| try: | |
| async with httpx.AsyncClient(timeout=3.0) as client: | |
| resp = await client.get(f"http://ip-api.com/json/{ip}?fields=66846719") | |
| if resp.status_code == 200: | |
| data = resp.json() | |
| if data.get("status") == "success": | |
| return json.dumps({ | |
| "country": data.get("country"), | |
| "countryCode": data.get("countryCode"), | |
| "region": data.get("regionName"), | |
| "city": data.get("city"), | |
| "zip": data.get("zip"), | |
| "lat": data.get("lat"), | |
| "lon": data.get("lon"), | |
| "timezone": data.get("timezone"), | |
| "isp": data.get("isp"), | |
| "org": data.get("org"), | |
| "as": data.get("as"), | |
| "mobile": data.get("mobile"), | |
| "proxy": data.get("proxy"), | |
| "hosting": data.get("hosting"), | |
| }) | |
| except Exception: | |
| pass # Non-critical β order proceeds without geo data | |
| return None | |
| # βββ 3. Header Extraction βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_headers(request: Request) -> Dict[str, Any]: | |
| """ | |
| Pull security/analytics-relevant headers from the incoming HTTP request. | |
| """ | |
| h = request.headers | |
| return { | |
| "referer": h.get("Referer"), | |
| "accept_language": h.get("Accept-Language"), | |
| "sec_ch_ua": h.get("Sec-CH-UA"), | |
| "sec_ch_ua_mobile": h.get("Sec-CH-UA-Mobile"), | |
| "sec_ch_ua_platform": h.get("Sec-CH-UA-Platform"), | |
| "sec_fetch_site": h.get("Sec-Fetch-Site"), | |
| "x_forwarded_for": h.get("X-Forwarded-For"), | |
| } | |
| # βββ 4. Build Merged Metadata βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_metadata(frontend_json: Optional[str], request: Request) -> str: | |
| """ | |
| Merge frontend-supplied browser metadata with server-extracted headers | |
| into a single JSON string for storage. | |
| """ | |
| browser_meta = {} | |
| if frontend_json: | |
| try: | |
| browser_meta = json.loads(frontend_json) | |
| except (json.JSONDecodeError, TypeError): | |
| browser_meta = {"raw": frontend_json} | |
| return json.dumps({ | |
| "browser_meta": browser_meta, | |
| "headers": extract_headers(request), | |
| }) | |