#!/usr/bin/env python3
"""Los experimentos de TLS de los apuntes, sobre la red c - mb - s (net/net.clab.yml).

Se corren en el contenedor del curso, con la topología arriba (make up):

  python3 scenarios.py rtts        cuenta RTT: TLS 1.3 frente a TLS 1.2
  python3 scenarios.py observer    lo que ve mb del handshake, sin claves
  python3 scenarios.py keylog      la misma captura, con SSLKEYLOGFILE
  python3 scenarios.py peek        sni_peek: el ClientHello sin ninguna clave
  python3 scenarios.py certs       los errores de curl con cada certificado
  python3 scenarios.py migration   UDP con connection ID frente a TCP, al cambiar de dirección
  python3 scenarios.py dash [abr]  un cliente DASH real, con una caída del enlace a la mitad
"""
import os
import shutil
import statistics
import sys
import threading
import time

import make_dash
from netlab import HERE, Lab, make_pki, wait_until

NET = Lab("net", "w13-net", "redes-w13-net")
SERVER = "10.213.2.10"
NAME = "demo.redes.test"
PORT = 443
RTT_MS = 40
RESOLVE = f"{NAME}:{PORT}:{SERVER}"
URL = f"https://{NAME}:{PORT}/"
TIMES = "%{time_connect} %{time_appconnect} %{time_starttransfer} %{ssl_verify_result}"


def start_server(cert="good", port=PORT, extra=""):
    """openssl s_server -www contesta cualquier GET con una página de estado."""
    NET.sh("s", "pkill", "-f", "openssl s_server")
    NET.daemon("s", f"cd /w13/build/pki && exec openssl s_server -accept {port} -www "
                    f"-cert {cert}.crt -key {cert}.key {extra} >/tmp/server.log 2>&1 </dev/zero")
    time.sleep(0.5)


def stop_server():
    NET.sh("s", "pkill", "-f", "openssl s_server")


def curl(*args):
    return NET.sh("c", "curl", "-s", "--max-time", "5", "--resolve", RESOLVE,
                  "--cacert", "/w13/build/pki/ca.crt", *args, URL)


def measure(*args, rounds=7):
    """Medianas de cada fase, en ms. curl abre una conexión nueva cada vez."""
    samples = []
    for _ in range(rounds):
        done = curl("-o", "/dev/null", "-w", TIMES, *args)
        parts = done.stdout.split()
        samples.append([float(value) * 1000 for value in parts[:3]])
    return [statistics.median(column) for column in zip(*samples)]


def rtts():
    start_server()
    print(f"RTT de la red: {RTT_MS} ms (retardo de netem en mb). Medianas de 7 conexiones, en ms.")
    print(f"{'':10}{'TCP listo':>10}{'TLS listo':>11}{'primer byte':>13}{'RTT del TLS':>13}{'RTT al byte':>13}")
    for title, flags in (("TLS 1.3", ["--tlsv1.3"]), ("TLS 1.2", ["--tlsv1.2", "--tls-max", "1.2"])):
        tcp, tls, first = measure(*flags)
        print(f"{title:10}{tcp:10.1f}{tls:11.1f}{first:13.1f}{(tls - tcp) / RTT_MS:13.1f}{first / RTT_MS:13.1f}")
    stop_server()


def capture(name, node="mb", device="eth1"):
    """Corre una petición mientras mb captura, y devuelve la ruta dentro del contenedor del curso."""
    path = f"/w13/captures/{name}.pcap"
    NET.sh(node, "rm", "-f", path)
    NET.daemon(node, f"tcpdump -i {device} -n -U -s0 -w {path} tcp port {PORT} >/dev/null 2>&1")
    # El archivo aparece cuando tcpdump ya abrió la interfaz. Docker puede tardar en lanzarlo.
    wait_until(lambda: NET.sh(node, "test", "-s", path).returncode == 0, seconds=15, step=0.2)
    time.sleep(0.3)
    return path


def stop_capture(node="mb"):
    time.sleep(0.8)
    NET.sh(node, "pkill", "tcpdump")
    time.sleep(0.4)


def tshark(path, *args):
    """tshark corre en c, que ve la misma carpeta en /w13."""
    return NET.sh("c", "tshark", "-r", path, *args).stdout


def observer_capture():
    start_server()
    path = capture("handshake")
    curl("--tlsv1.3", "-o", "/dev/null")
    stop_capture()
    stop_server()
    return path




def observer():
    path = observer_capture()
    print("Lo que ve mb, sin claves:")
    print(tshark(path, "-Y", "tls", "-o", "tls.keylog_file:", "-T", "fields",
                 "-e", "frame.number", "-e", "ip.src", "-e", "_ws.col.Info").rstrip())
    print("\nEl nombre del servidor, leído sin claves:")
    print(tshark(path, "-Y", "tls.handshake.type == 1", "-T", "fields",
                 "-e", "tls.handshake.extensions_server_name").rstrip())
    print("\nMensajes de certificado que se pueden leer:", tshark(path, "-Y", "tls.handshake.type == 11").count("Certificate"))


def keylog():
    NET.sh("c", "rm", "-f", "/w13/captures/keys.log")
    start_server()
    path = capture("keylog")
    NET.sh("c", "sh", "-c",
           f"SSLKEYLOGFILE=/w13/captures/keys.log curl -s --max-time 5 --tlsv1.3 --http1.1 --resolve {RESOLVE} "
           f"--cacert /w13/build/pki/ca.crt -o /dev/null {URL}")
    stop_capture()
    stop_server()
    print("Etiquetas del archivo de claves:")
    print(NET.out("c", "sh", "-c", "cut -d' ' -f1 /w13/captures/keys.log | sort").rstrip())
    print("\nLa misma captura, ahora con las claves:")
    print(tshark(path, "-Y", "tls", "-o", "tls.keylog_file:/w13/captures/keys.log", "-T", "fields",
                 "-e", "frame.number", "-e", "ip.src", "-e", "_ws.col.Info").rstrip())
    print("\nLa petición, en claro:")
    print(tshark(path, "-Y", "http.request", "-o", "tls.keylog_file:/w13/captures/keys.log", "-T", "fields",
                 "-e", "http.request.method", "-e", "http.request.uri", "-e", "http.host").rstrip())


def peek():
    NET.daemon("s", f"exec /w13/bin/sni_peek {PORT} 1 >/tmp/peek.log 2>&1")
    time.sleep(0.5)
    NET.sh("c", "curl", "-s", "--max-time", "2", "--resolve", RESOLVE, "-k", URL)
    time.sleep(0.3)
    print(NET.out("s", "cat", "/tmp/peek.log").rstrip())


def first_line(text):
    return text.strip().splitlines()[0] if text.strip() else ""


def certs():
    print(f"{'certificado':14}{'salida':>7}  mensaje de curl")
    for cert in ("good", "selfsigned", "wrongname", "expired"):
        start_server(cert)
        done = NET.sh("c", "curl", "-sS", "--max-time", "5", "--resolve", RESOLVE,
                      "--cacert", "/w13/build/pki/ca.crt", "-o", "/dev/null", URL)
        print(f"{cert:14}{done.returncode:>7}  {first_line(done.stderr) or 'bien'}")
    print("\nCon la CA del sistema, sin la CA de la clase:")
    start_server("good")
    done = NET.sh("c", "curl", "-sS", "--max-time", "5", "--resolve", RESOLVE, "-o", "/dev/null", URL)
    print(f"{'good':14}{done.returncode:>7}  {first_line(done.stderr)}")
    stop_server()


def migration():
    """Primero TCP, luego UDP: cada corrida empieza en la red original y termina en la otra."""
    NET.sh("s", "pkill", "-f", "cid_echo")
    NET.daemon("s", "exec python3 -u /w13/cid_echo.py serve 7000 >/tmp/cid.log 2>&1")
    time.sleep(0.7)
    for title, kind in (("TCP", "tcp"), ("UDP con connection ID", "udp")):
        print(f"== {title}")
        print(NET.out("c", "python3", "/w13/cid_echo.py", kind, SERVER, "7000").rstrip(), "\n")
        NET.sh("c", "python3", "/w13/cid_echo.py", "restore")
    print("== Lo que anotó el servidor")
    print(NET.out("s", "cat", "/tmp/cid.log").rstrip())
    NET.sh("s", "pkill", "-f", "cid_echo")


def link(rate):
    """Cambia la velocidad hacia el cliente. La cola queda limitada, para no medir bufferbloat."""
    NET.sh("mb", "tc", "qdisc", "change", "dev", "eth1", "root", "netem", "delay", "20ms",
           "rate", rate, "limit", "120")


def link_reset():
    NET.sh("mb", "tc", "qdisc", "change", "dev", "eth1", "root", "netem", "delay", "20ms")


def dash_run(strategy, segments=25):
    """6 Mbit/s, luego 1,2 Mbit/s por 12 s, luego 6 Mbit/s otra vez. Devuelve lo que imprime el cliente."""
    folder = os.path.join(HERE, "build", "dash")
    if not os.path.exists(os.path.join(folder, "400k", f"seg-{segments - 1}.m4s")):
        shutil.rmtree(folder, ignore_errors=True)
        make_dash.build(folder, segments, 2)
    NET.sh("s", "pkill", "-f", "http.server")
    NET.daemon("s", "exec python3 -m http.server 8000 --directory /w13/build/dash >/tmp/http.log 2>&1")
    time.sleep(1.0)
    link("6mbit")
    cuts = threading.Timer(10, link, args=("1200kbit",))
    back = threading.Timer(22, link, args=("6mbit",))
    cuts.start()
    back.start()
    try:
        return NET.out("c", "python3", "/w13/dash_client.py", f"http://{SERVER}:8000/video.mpd", strategy,
                       timeout=180)
    finally:
        cuts.cancel()
        back.cancel()
        cuts.join()
        back.join()
        link_reset()
        NET.sh("s", "pkill", "-f", "http.server")


def dash():
    strategy = sys.argv[2] if len(sys.argv) > 2 else "last"
    # El cliente corre en c, donde esta carpeta se ve como /w13.
    if strategy.endswith(".py"):
        strategy = "/w13/" + os.path.relpath(strategy, HERE)
    print(dash_run(strategy).rstrip())


COMMANDS = {"dash": dash, "rtts": rtts, "observer": observer, "keylog": keylog, "peek": peek, "certs": certs,
            "migration": migration}


def main():
    if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
        print(__doc__)
        return 2
    make_pki()
    COMMANDS[sys.argv[1]]()
    return 0


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