# shellcheck shell=sh
#
# Traefik: Binary plus systemd-Unit. **Kein Docker auf dem Hypervisor** (Handoff
# §1) — nftables bleibt einziger Eigentümer der Host-Firewallregeln, und ein
# Docker-Dienst brächte seine eigene Kette mit.
#
# ---------------------------------------------------------------------------
# Die Namen, die passen müssen
# ---------------------------------------------------------------------------
#
# `SshTraefikWriter::render()` gibt aus, was der Routen-Endpunkt später liefert,
# und die statische Konfiguration hier muss dazu passen. Nachgelesen, nicht
# geraten:
#
#   entryPoints: ["websecure"]
#   tls.certResolver: "letsencrypt"
#   service -> loadBalancer.servers[].url: "http://<backend>:80"
#
# Heißt der Entrypoint hier anders, laufen die Router ins Leere und Traefik
# meldet trotzdem einen sauberen Start.
#
# ---------------------------------------------------------------------------
# Reihenfolge: der Token kommt erst später
# ---------------------------------------------------------------------------
#
# Der `http`-Provider braucht den dauerhaften Host-Token, und den gibt es erst
# mit `POST /host/register` in Abschnitt `registered`. Die statische
# Konfiguration wird deshalb ZWEIMAL geschrieben: hier mit leerem Token, damit
# der Dienst steht und 80/443 belegt sind, und in Task 9 noch einmal mit dem
# echten. Erst dort lässt sich beweisen, dass eine Routentabelle wirklich
# abgerufen wurde.

CLUPILOT_TRAEFIK_BIN='/usr/local/bin/traefik'
CLUPILOT_TRAEFIK_DIR='/etc/traefik'
CLUPILOT_TRAEFIK_TOKEN_FILE='/etc/traefik/host-token'
CLUPILOT_TRAEFIK_API='http://127.0.0.1:8080'

# Neueste stabile v3 aus der Release-Liste.
#
# Nicht fest verdrahtet, aus demselben Grund wie bei der ISO: eine feste Nummer
# ist in ein paar Monaten eine 404 mitten in einer Übernahme.
discover_traefik_version() {
    _json="$(http_get 'https://api.github.com/repos/traefik/traefik/releases/latest' || true)"
    [ -n "$_json" ] || return 1

    printf '%s' "$_json" \
        | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\(v3[0-9.]*\)".*/\1/p' \
        | head -1
}

# Lädt das Binary und prüft es gegen die Prüfsummendatei der Ausgabe.
#
# Dieselbe Begründung wie bei der ISO: was auf 80 und 443 lauscht und die
# Zertifikate aller Kunden hält, wird nicht ungeprüft aus dem Netz übernommen.
install_traefik_binary() {
    _version="$1"
    _plain="${_version#v}"
    _base="https://github.com/traefik/traefik/releases/download/${_version}"
    _archive="traefik_${_version}_linux_amd64.tar.gz"
    _sums="traefik_${_version}_checksums.txt"

    _tmp="${CLUPILOT_WORK_DIR}/traefik"
    rm -rf "$_tmp"
    mkdir -p "$_tmp"

    if ! http_download "${_base}/${_archive}" "${_tmp}/${_archive}"; then
        log "Traefik ${_version} nicht ladbar"
        return 1
    fi

    _sumfile="$(http_get "${_base}/${_sums}" || true)"
    if [ -z "$_sumfile" ]; then
        log "Prüfsummendatei ${_sums} nicht abrufbar"
        return 1
    fi

    _expected="$(printf '%s\n' "$_sumfile" | awk -v n="$_archive" '$2 == n || $2 == "*" n { print $1; exit }')"
    if [ -z "$_expected" ]; then
        log "keine Prüfsumme für ${_archive}"
        return 1
    fi

    _actual="$(sha256sum "${_tmp}/${_archive}" | awk '{ print $1 }')"
    if [ "$_actual" != "$_expected" ]; then
        log "Traefik-Prüfsumme stimmt nicht: erwartet ${_expected}, bekommen ${_actual}"
        return 1
    fi

    tar xzf "${_tmp}/${_archive}" -C "$_tmp" traefik
    install -m 755 "${_tmp}/traefik" "$CLUPILOT_TRAEFIK_BIN"
    rm -rf "$_tmp"

    log "Traefik ${_plain} installiert, Prüfsumme stimmt"
    return 0
}

# Die statische Konfiguration.
#
# `token` darf leer sein — dann steht der Dienst, holt aber keine Tabelle. Das
# ist der Zustand zwischen Abschnitt 7 und Abschnitt 9.
write_traefik_static_config() {
    _token="${1:-}"
    _endpoint="${CLUPILOT_API}/host/routes"

    mkdir -p "$CLUPILOT_TRAEFIK_DIR"
    chmod 755 "$CLUPILOT_TRAEFIK_DIR"

    # acme.json hält die privaten Schlüssel aller Kundenzertifikate. Traefik
    # weigert sich, sie zu benutzen, wenn die Rechte weiter sind als 600 — und
    # das ist gut so.
    if [ ! -f "${CLUPILOT_TRAEFIK_DIR}/acme.json" ]; then
        : > "${CLUPILOT_TRAEFIK_DIR}/acme.json"
    fi
    chmod 600 "${CLUPILOT_TRAEFIK_DIR}/acme.json"

    cat > "${CLUPILOT_TRAEFIK_DIR}/traefik.yml" <<EOF
# Von CluPilots Bootstrap geschrieben. Nicht von Hand ändern — der nächste Lauf
# überschreibt die Datei.

global:
  checkNewVersion: false
  sendAnonymousUsage: false

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"
  # Nur auf der Loopback-Adresse. Der Zustand dieses Dienstes geht niemanden
  # außerhalb der Maschine etwas an, und nftables ließe 8080 ohnehin nicht durch.
  traefik:
    address: "127.0.0.1:8080"

# Die Namen kommen aus SshTraefikWriter::render(): der Routen-Endpunkt liefert
# entryPoints ["websecure"] und certResolver "letsencrypt". Wer sie hier ändert,
# muss sie dort mitändern — sonst laufen die Router ins Leere und Traefik startet
# trotzdem sauber.
certificatesResolvers:
  letsencrypt:
    acme:
      email: "${CLUPILOT_ACME_EMAIL}"
      storage: "${CLUPILOT_TRAEFIK_DIR}/acme.json"
      httpChallenge:
        entryPoint: web

providers:
  http:
    endpoint: "${_endpoint}"
    pollInterval: "30s"
    pollTimeout: "10s"
    headers:
      Authorization: "Bearer ${_token}"

api:
  dashboard: false
  insecure: true

ping:
  entryPoint: traefik

log:
  level: INFO
  filePath: "/var/log/traefik.log"

accessLog:
  filePath: "/var/log/traefik-access.log"
EOF
    chmod 640 "${CLUPILOT_TRAEFIK_DIR}/traefik.yml"

    if [ -n "$_token" ]; then
        umask 077
        printf '%s\n' "$_token" > "$CLUPILOT_TRAEFIK_TOKEN_FILE"
        chmod 600 "$CLUPILOT_TRAEFIK_TOKEN_FILE"
    fi
}

write_traefik_unit() {
    cat > /etc/systemd/system/traefik.service <<EOF
[Unit]
Description=Traefik (von CluPilot verwaltet)
Documentation=https://doc.traefik.io/traefik/
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
ExecStart=${CLUPILOT_TRAEFIK_BIN} --configFile=${CLUPILOT_TRAEFIK_DIR}/traefik.yml
Restart=on-failure
RestartSec=5

# Braucht 80 und 443 und sonst nichts. Ohne diese Zeile liefe ein Dienst als
# root, der von außen erreichbar ist — die eine Stelle, an der das am wenigsten
# sein darf.
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=${CLUPILOT_TRAEFIK_DIR} /var/log

[Install]
WantedBy=multi-user.target
EOF

    systemctl daemon-reload
}

traefik_answers_ping() {
    _tries="${1:-15}"
    while [ "$_tries" -gt 0 ]; do
        if http_get "${CLUPILOT_TRAEFIK_API}/ping" >/dev/null 2>&1; then
            return 0
        fi
        _tries=$((_tries - 1))
        sleep 2
    done
    return 1
}

# Lauschen 80 und 443 wirklich?
#
# „Der Dienst läuft" und „der Port ist belegt" sind zwei Aussagen. Traefik
# startet auch dann sauber, wenn ein Entrypoint wegen eines Tippfehlers gar
# nicht angelegt wurde.
traefik_ports_bound() {
    for _port in 80 443; do
        if ! ss -ltn "sport = :${_port}" 2>/dev/null | grep -q LISTEN; then
            log "Port ${_port} ist nicht belegt"
            return 1
        fi
    done
    return 0
}

# Holt der Host seine Routentabelle wirklich?
#
# Direkt gefragt, mit demselben Token und derselben Adresse, die in der
# statischen Konfiguration stehen. Über Traefiks eigene Router zu gehen wäre
# hier kein Beweis: ein frischer Host hat noch keine Kunden, also ist die
# Tabelle mit Recht leer, und „null Router" hieße dann sowohl „alles gut" als
# auch „nichts abgerufen".
host_routes_reachable() {
    _token="$1"
    [ -n "$_token" ] || return 1

    if command -v curl >/dev/null 2>&1; then
        curl -sS -f --connect-timeout 5 --max-time 15 \
            -H "Authorization: Bearer ${_token}" \
            "${CLUPILOT_API}/host/routes" >/dev/null 2>&1
        return $?
    fi

    wget -q -O /dev/null --timeout=15 \
        --header="Authorization: Bearer ${_token}" \
        "${CLUPILOT_API}/host/routes"
}
