#!/usr/bin/env python3 """Build unique HTTP(S) probe list from resources.conf + connect_check catalog (C builtins). Output TSV: tag\\turl (tag = section|name|host) Prefer resources.conf paths; catalog hosts get https://host/ (captive specials as in CC). """ from __future__ import annotations import argparse import json import sys from pathlib import Path from urllib.parse import urlparse ROOT = Path(__file__).resolve().parents[1] def _captive_url(host: str, name: str = "") -> str: h = host.lower() n = (name or "").lower() if "gstatic" in h or "android.com" in h or "clients" in h and "google" in h: return f"http://{h}/generate_204" if "captive.apple" in h: return f"http://{h}/hotspot-detect.html" if "msftconnecttest" in h or "msftncsi" in h: return f"http://{h}/connecttest.txt" if "detectportal" in h: return f"http://{h}/success.txt" if "network-test.debian" in h: return f"http://{h}/" if "generate_204" in n or "captive" in n: return f"http://{h}/" return f"https://{h}/" def build(conf_path: Path, catalog_path: Path) -> list[tuple[str, str]]: by_host: dict[str, tuple[str, str]] = {} section = None for ln in conf_path.read_text(errors="replace").splitlines(): ln = ln.strip() if not ln or ln.startswith("#"): continue if ln.startswith("[") and ln.endswith("]"): section = ln[1:-1] continue parts = [p.strip() for p in ln.split("|")] if len(parts) < 2 or not parts[1].startswith("http"): continue name, url = parts[0], parts[1] host = (urlparse(url).hostname or "").lower() if not host: continue tag = f"{section or 'conf'}|{name}|{host}" by_host[host] = (tag, url) cat = json.loads(catalog_path.read_text()) for item in cat.get("items") or []: host = (item.get("host") or "").lower().strip() if not host or host in by_host: continue name = item.get("name") or host section = item.get("section") or "catalog" url = _captive_url(host, name) if section == "captive" else f"https://{host}/" # DoH / known http endpoints if section == "doh" and "cloudflare-dns" in host: url = f"https://{host}/dns-query" if section == "doh" and "dns.google" in host: url = f"https://{host}/dns-query" if "gstatic.com" in host or host.endswith("android.com"): url = f"http://{host}/generate_204" tag = f"{section}|{name}|{host}" by_host[host] = (tag, url) rows = sorted(by_host.values(), key=lambda x: (x[0].split("|")[0], x[1])) return rows def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--conf", type=Path, default=ROOT / "var" / "resources.conf") ap.add_argument( "--catalog", type=Path, default=ROOT / "var" / "connect_check_catalog.json" ) ap.add_argument("-o", "--output", type=Path, required=True) args = ap.parse_args() rows = build(args.conf, args.catalog) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text("".join(f"{t}\t{u}\n" for t, u in rows)) print(f"wrote {len(rows)} urls → {args.output}", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())