Spaces:
Sleeping
Sleeping
File size: 5,851 Bytes
b2be963 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | """
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),
})
|