#!/usr/bin/env python3
"""Los pasos del laboratorio que necesitan Internet. Corren en el contenedor del curso.

  python3 internet.py chain [sitio]     versión de TLS y cadena de certificados
  python3 internet.py h2h3 [sitio]      HTTP/2 frente a HTTP/3, en medianas de 11 descargas
  python3 internet.py quic [sitio]      captura una conexión QUIC: qué se lee sin claves y con claves
  python3 internet.py dns               el mismo nombre, preguntado a tres resolvers públicos

Los números dependen de su conexión y del sitio.
"""
import os
import re
import statistics
import subprocess
import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
CAPTURES = os.path.join(HERE, "captures")
DEFAULT_SITE = "www.cloudflare.com"
DEFAULT_PATH = "/cdn-cgi/trace"
RESOLVERS = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]
CDN_NAMES = ["www.akamai.com", "www.cloudflare.com"]
FORMAT = "%{http_version} %{time_namelookup} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}"


def run(args, **kwargs):
    return subprocess.run(args, capture_output=True, text=True, timeout=kwargs.pop("timeout", 60), **kwargs)


def chain(site):
    """openssl s_client imprime el certificado y la cadena; nos quedamos con lo que importa."""
    done = run(["openssl", "s_client", "-connect", f"{site}:443", "-servername", site, "-tls1_3"],
               input="", timeout=20)
    for line in done.stdout.splitlines():
        if re.match(r"\s*(\d+ s:|i:|a:|v:)", line) or line.startswith(("New, ", "Verify return code", "Verification")):
            print(line)
    print(next((line.strip() for line in done.stdout.splitlines() if "Protocol" in line), ""))


def sample(flag, site, path):
    done = run(["curl", "-s", "-o", "/dev/null", flag, "--max-time", "10", "-w", FORMAT, f"https://{site}{path}"])
    parts = done.stdout.split()
    return parts[0], [float(value) * 1000 for value in parts[1:]]


def h2h3(site, path, rounds=11):
    print(f"{site}{path}: medianas de {rounds} conexiones nuevas, en ms")
    print(f"{'':8}{'versión':>8}{'DNS':>7}{'conexión':>10}{'TLS listo':>11}{'primer byte':>13}{'total':>8}")
    for title, flag in (("HTTP/2", "--http2"), ("HTTP/3", "--http3-only")):
        rows = []
        version = "?"
        for _ in range(rounds):
            version, values = sample(flag, site, path)
            rows.append(values)
        lookup, connect, tls, first, total = [statistics.median(col) for col in zip(*rows)]
        print(f"{title:8}{version:>8}{lookup:7.1f}{connect:10.1f}{tls:11.1f}{first:13.1f}{total:8.1f}")


def quic(site, path):
    """Captura en la interfaz por defecto del contenedor, filtrando el puerto 443 de UDP."""
    os.makedirs(CAPTURES, exist_ok=True)
    pcap = os.path.join(CAPTURES, "quic.pcap")
    keys = os.path.join(CAPTURES, "quic-keys.log")
    for name in (pcap, keys):
        if os.path.exists(name):
            os.remove(name)
    address = run(["dig", "+short", site, "A"]).stdout.split()[-1]
    capture = subprocess.Popen(["tcpdump", "-i", "any", "-n", "-U", "-s0", "-w", pcap, f"udp port 443 and host {address}"],
                               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    time.sleep(1.5)
    env = dict(os.environ, SSLKEYLOGFILE=keys)
    subprocess.run(["curl", "-s", "-o", "/dev/null", "--http3-only", "--resolve", f"{site}:443:{address}",
                    "--max-time", "10", f"https://{site}{path}"], env=env, check=False)
    time.sleep(1)
    capture.terminate()
    capture.wait()
    fields = ["-T", "fields", "-e", "frame.number", "-e", "udp.length", "-e", "_ws.col.Info"]
    print("Sin claves:")
    print(run(["tshark", "-r", pcap, "-c", "11", *fields]).stdout.rstrip())
    print("\nSNI y ALPN que tshark lee sin claves:")
    print(run(["tshark", "-r", pcap, "-Y", "tls.handshake.type == 1", "-T", "fields",
               "-e", "tls.handshake.extensions_server_name", "-e", "tls.handshake.extensions_alpn_str"]).stdout.strip())
    print("\nCon SSLKEYLOGFILE, los paquetes que llevan streams:")
    decrypted = run(["tshark", "-r", pcap, "-o", f"tls.keylog_file:{keys}", "-T", "fields",
                     "-e", "frame.number", "-e", "_ws.col.Protocol", "-e", "_ws.col.Info"]).stdout
    shown = [line for line in decrypted.splitlines() if "STREAM" in line]
    print("\n".join(shown[:8]))


def dns():
    for name in CDN_NAMES:
        for resolver in RESOLVERS:
            answer = run(["dig", "+short", "+time=3", f"@{resolver}", name, "A"]).stdout.split()
            addresses = [item for item in answer if not item.endswith(".")]
            print(f"{name:22} @{resolver:9} {' '.join(sorted(addresses))}")


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        return 2
    command = sys.argv[1]
    site = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_SITE
    path = DEFAULT_PATH if site == DEFAULT_SITE else "/"
    if command == "chain":
        chain(site)
    elif command == "h2h3":
        h2h3(site, path)
    elif command == "quic":
        quic(site, path)
    elif command == "dns":
        dns()
    else:
        print(__doc__)
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())
