#!/usr/bin/env python
"""Unregistered follow-up: who is *terminating* the TLS, as the server header says it.

Not in README.md's plan. It was added after the issuer counts came back, because
the leading issuer in the top 1,000 is a certificate authority most of those
sites have never dealt with directly, and the obvious explanation — the CDN in
front of them asked for it — is measurable rather than guessable. Labelled
unregistered wherever it is reported.

    python serverhdr.py --certs ~/data/tls-issuers-certs.csv --out ~/data/tls-issuers-server.csv

One HEAD (falling back to GET) per domain that completed a handshake, following
no redirects, recording the `server` header verbatim.
"""

import argparse
import concurrent.futures as futures
import csv
import http.client
import ssl

UA = ("Mozilla/5.0 (compatible; VesperBot/1.0; +https://untilnextsession.com/agents) "
      "measuring TLS termination")


def header(domain):
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    for method in ("HEAD", "GET"):
        conn = http.client.HTTPSConnection(domain, 443, timeout=10, context=ctx)
        try:
            conn.request(method, "/", headers={"User-Agent": UA, "Accept": "*/*"})
            resp = conn.getresponse()
            return str(resp.status), (resp.getheader("server") or "")
        except Exception as exc:                   # noqa: BLE001
            last = f"{type(exc).__name__}"
        finally:
            try:
                conn.close()
            except Exception:                      # noqa: BLE001
                pass
    return "", last


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("--certs", required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--workers", type=int, default=24)
    args = ap.parse_args(argv)
    with open(args.certs, newline="") as fh:
        rows = [r for r in csv.DictReader(fh) if r["ok"] == "1"]
    out = []
    with futures.ThreadPoolExecutor(args.workers) as pool:
        jobs = {pool.submit(header, r["domain"]): r for r in rows}
        for fut in futures.as_completed(jobs):
            r = jobs[fut]
            status, server = fut.result()
            out.append({"rank": r["rank"], "domain": r["domain"],
                        "issuer_org": r["issuer_org"], "status": status,
                        "server": server})
    out.sort(key=lambda r: int(r["rank"]))
    with open(args.out, "w", newline="") as fh:
        w = csv.DictWriter(fh, fieldnames=list(out[0].keys()))
        w.writeheader()
        w.writerows(out)
    named = sum(1 for r in out if r["server"])
    print(f"{len(out)} domains, {named} named a server")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
