#!/usr/bin/env python3
"""
generate_browser_ja4.py — Generate browser_ja4.csv with known browser TLS fingerprints.

Sources:
  • FoxIO official ja4plus-mapping.csv:
    https://github.com/FoxIO-LLC/ja4/blob/main/ja4plus-mapping.csv
  • ja4db.com public API: https://ja4db.com/api/read/
  • JA4+ specification: https://github.com/FoxIO-LLC/ja4

JA4 fingerprints identify TLS libraries, not individual browser versions.
All Chromium-based browsers (Chrome, Edge, Brave, Opera, Vivaldi, Samsung Internet)
share BoringSSL and produce identical JA4 fingerprints.
Firefox uses NSS, Safari uses Apple SecureTransport/Network.framework.

Output format (no header):
    <ja4_fingerprint>,<browser_family>,<tls_library>,<context>
"""
import argparse
import json
import os
import sys
import urllib.request

# =============================================================================
# Curated browser JA4 fingerprints from FoxIO official mapping
# Format: (ja4, browser_family, tls_library, context)
# =============================================================================
BROWSER_FINGERPRINTS = [
    # --- Chromium (Chrome, Edge, Brave, Opera, Vivaldi, Samsung Internet) ---
    # BoringSSL — all Chromium-based browsers produce identical JA4
    ("t13d1516h2_8daaf6152771_02713d6af862", "Chromium",
     "BoringSSL", "TCP fresh connection"),
    ("t13d1517h2_8daaf6152771_b0da82dd1658", "Chromium",
     "BoringSSL", "TCP with PSK extension"),
    ("t13d1517h2_8daaf6152771_b1ff8ab2d16f", "Chromium",
     "BoringSSL", "TCP alt extension set"),
    ("t13i1515h2_8daaf6152771_02713d6af862", "Chromium",
     "BoringSSL", "TCP to IP (no SNI)"),
    ("t13i1516h2_8daaf6152771_b0da82dd1658", "Chromium",
     "BoringSSL", "TCP to IP with PSK"),
    ("t13i1516h2_8daaf6152771_b1ff8ab2d16f", "Chromium",
     "BoringSSL", "TCP to IP alt ext"),
    ("q13d0312h3_55b375c5d22e_06cda9e17597", "Chromium",
     "BoringSSL", "QUIC HTTP/3 to domain"),
    ("q13i0311h3_55b375c5d22e_06cda9e17597", "Chromium",
     "BoringSSL", "QUIC HTTP/3 to IP"),

    # --- Mozilla Firefox ---
    # NSS library
    ("t13d1715h2_5b57614c22b0_7121afd63204", "Firefox",
     "NSS", "TCP to domain"),
    ("t13i1714h2_5b57614c22b0_7121afd63204", "Firefox",
     "NSS", "TCP to IP (no SNI)"),
    # Firefox with different extension counts (minor NSS updates)
    ("t13d1715h2_5b57614c22b0_3c5d18be5765", "Firefox",
     "NSS", "TCP domain (NSS variant)"),
    ("t13d1716h2_5b57614c22b0_7121afd63204", "Firefox",
     "NSS", "TCP domain (16 ext)"),

    # --- Apple Safari (macOS + iOS) ---
    # Apple SecureTransport / Network.framework
    ("t13d2014h2_a09f3c656075_14788d8d241b", "Safari",
     "SecureTransport", "TCP to domain"),
    ("t13i2013h2_a09f3c656075_14788d8d241b", "Safari",
     "SecureTransport", "TCP to IP (no SNI)"),
    # Safari variants with different extension hashes
    ("t13d1714h2_5b57614c22b0_14788d8d241b", "Safari",
     "SecureTransport", "Safari/iOS via WebKit (hybrid)"),

    # --- Chrome on iOS ---
    # Uses Apple's TLS stack, not BoringSSL (Apple enforces this on iOS)
    ("t13d1714h2_5b57614c22b0_14788d8d241b", "Chrome_iOS",
     "SecureTransport", "Chrome on iOS (Apple TLS)"),

    # --- Tor Browser ---
    # Based on Firefox ESR but with a hardened NSS configuration
    ("t13d1614h2_5b57614c22b0_7121afd63204", "Tor_Browser",
     "NSS", "TCP (hardened Firefox ESR)"),

    # --- Common mobile browser patterns ---
    # Android WebView uses BoringSSL (same as Chromium)
    ("t13d1516h2_8daaf6152771_02713d6af862", "Android_WebView",
     "BoringSSL", "Android WebView (same as Chromium)"),

    # --- Older browser versions (TLS 1.2 fallback) ---
    ("t12d1516h2_8daaf6152771_02713d6af862", "Chromium_Legacy",
     "BoringSSL", "Chromium TLS 1.2 only (old/restricted)"),
    ("t12d1715h2_5b57614c22b0_7121afd63204", "Firefox_Legacy",
     "NSS", "Firefox TLS 1.2 only (old)"),
    ("t12d2014h2_a09f3c656075_14788d8d241b", "Safari_Legacy",
     "SecureTransport", "Safari TLS 1.2 only (old)"),
]

# Unique cipher hashes that identify TLS libraries
CIPHER_HASH_FAMILIES = {
    "8daaf6152771": "Chromium/BoringSSL",
    "55b375c5d22e": "Chromium/BoringSSL (QUIC)",
    "5b57614c22b0": "Firefox/NSS",
    "a09f3c656075": "Safari/SecureTransport",
}

JA4DB_URL = "https://ja4db.com/api/read/"
FOXIO_URL = ("https://raw.githubusercontent.com/FoxIO-LLC/ja4/"
             "main/ja4plus-mapping.csv")

# Valid JA4 fingerprint: protocol prefix + hex hashes separated by underscores
# Example: t13d1516h2_8daaf6152771_02713d6af862
import re
_JA4_RE = re.compile(r'^[tdq]\d{2}[di]\d{4}[a-z0-9]{2}_[0-9a-f]{12}_[0-9a-f]{12}$')


def _is_valid_ja4(s):
    """Validate that a string is a well-formed JA4 fingerprint (ASCII hex only)."""
    return bool(_JA4_RE.match(s))


def download_ja4db():
    """Download from ja4db.com API and extract browser fingerprints."""
    entries = []
    rejected = 0
    try:
        req = urllib.request.Request(JA4DB_URL, headers={"User-Agent": "ja4-platform/1.0"})
        with urllib.request.urlopen(req, timeout=30) as r:
            data = json.loads(r.read().decode())
        for item in data:
            ja4 = item.get("ja4_fingerprint")
            if not ja4 or not _is_valid_ja4(ja4):
                if ja4:
                    rejected += 1
                continue
            app = (item.get("application") or "").strip()
            ua = (item.get("user_agent_string") or "").strip()
            if app and "browser" in app.lower():
                entries.append((ja4, app, "ja4db.com (verified)" if item.get("verified") else "ja4db.com"))
            elif ua:
                family = _classify_ua(ua)
                if family:
                    entries.append((ja4, family, f"ja4db.com UA: {ua[:60]}"))
        print(f"  [ja4db.com] Downloaded {len(entries)} browser fingerprints "
              f"(rejected {rejected} malformed)")
    except Exception as e:
        print(f"  [ja4db.com] Warning: download failed ({e})", file=sys.stderr)
    return entries


def download_foxio_mapping():
    """Download FoxIO official ja4plus-mapping.csv."""
    entries = []
    try:
        req = urllib.request.Request(FOXIO_URL, headers={"User-Agent": "ja4-platform/1.0"})
        with urllib.request.urlopen(req, timeout=15) as r:
            lines = r.read().decode().strip().split("\n")
        # Skip header
        for line in lines[1:]:
            parts = line.split(",")
            if len(parts) < 5:
                continue
            app = parts[0].strip()
            lib = parts[1].strip()
            ja4 = parts[4].strip()
            if not ja4 or not _is_valid_ja4(ja4):
                continue
            if "browser" in app.lower() or app in ("Mozilla Firefox", "Safari"):
                entries.append((ja4, app, f"FoxIO official (lib: {lib})"))
        print(f"  [FoxIO] Downloaded {len(entries)} browser fingerprints")
    except Exception as e:
        print(f"  [FoxIO] Warning: download failed ({e})", file=sys.stderr)
    return entries


def _classify_ua(ua):
    """Classify a user-agent string into a browser family."""
    ua_lower = ua.lower()
    # Order matters — check more specific strings first
    if "crios/" in ua_lower:
        return "Chrome_iOS"
    if "fxios/" in ua_lower:
        return "Firefox_iOS"
    if "edg/" in ua_lower or "edge/" in ua_lower:
        return "Edge"
    if "opr/" in ua_lower or "opera" in ua_lower:
        return "Opera"
    if "brave" in ua_lower:
        return "Brave"
    if "vivaldi" in ua_lower:
        return "Vivaldi"
    if "samsungbrowser" in ua_lower:
        return "Samsung_Internet"
    if "firefox/" in ua_lower:
        return "Firefox"
    if "safari/" in ua_lower and "chrome/" not in ua_lower:
        return "Safari"
    if "chrome/" in ua_lower:
        return "Chromium"
    return None


def main():
    parser = argparse.ArgumentParser(description="Generate browser_ja4.csv")
    parser.add_argument("--output", default="browser_ja4.csv",
                        help="Output CSV file path")
    parser.add_argument("--no-download", action="store_true",
                        help="Skip downloading from remote sources")
    args = parser.parse_args()

    # Collect all fingerprints: (ja4, browser_family, tls_library, context)
    seen = {}  # ja4 -> (browser_family, tls_library, context)

    # 1. Start with curated list (highest priority)
    for ja4, family, lib, ctx in BROWSER_FINGERPRINTS:
        if ja4 not in seen:
            seen[ja4] = (family, lib, ctx)

    # 2. Download from remote sources
    if not args.no_download:
        print("[browser_ja4] Downloading from remote sources...")
        for ja4, family, source in download_foxio_mapping():
            if ja4 not in seen:
                lib = _lib_from_cipher_hash(ja4)
                seen[ja4] = (family, lib, source)

        for ja4, family, source in download_ja4db():
            if ja4 not in seen:
                lib = _lib_from_cipher_hash(ja4)
                seen[ja4] = (family, lib, source)

    # 3. Write output (use csv module to properly handle commas in context field)
    import csv
    with open(args.output, "w", newline="") as f:
        writer = csv.writer(f)
        for ja4 in sorted(seen.keys()):
            family, lib, ctx = seen[ja4]
            writer.writerow([ja4, family, lib, ctx])

    families = set(f for f, _, _ in seen.values())
    print(f"[browser_ja4] Generated {len(seen)} unique fingerprints "
          f"covering {len(families)} browser families: {', '.join(sorted(families))}")


def _lib_from_cipher_hash(ja4):
    """Infer TLS library from the cipher hash portion of a JA4 fingerprint."""
    parts = ja4.split("_")
    if len(parts) >= 2:
        cipher_hash = parts[1]
        return CIPHER_HASH_FAMILIES.get(cipher_hash, "unknown")
    return "unknown"


if __name__ == "__main__":
    main()
