#!/usr/bin/env bash
# skyz-xfce installer v1 — Ubuntu 22.04/24.04, XFCE + noVNC + nginx + Let's Encrypt
set -Eeuo pipefail
umask 077

DOMAIN=vpn.skyz.pet
SITE_ID=sky-vpn-xfce
SITE_FILE=/etc/nginx/sites-available/${SITE_ID}.conf
SITE_LINK=/etc/nginx/sites-enabled/${SITE_ID}.conf
AUTH_FILE=/etc/nginx/.${SITE_ID}-htpasswd
AUTH_MARKER=/etc/nginx/.${SITE_ID}-auth-owned
ACME_ROOT=/var/www/${SITE_ID}-acme
STARTUP=/usr/local/libexec/${SITE_ID}-start
VNC_UNIT=/etc/systemd/system/${SITE_ID}-vnc.service
WS_UNIT=/etc/systemd/system/${SITE_ID}-websockify.service
RENEW_HOOK=/etc/letsencrypt/renewal-hooks/deploy/${SITE_ID}-reload-nginx
MARKER='# skyz-xfce installer v1'

TARGET_USER=${SUDO_USER:-}
[[ $TARGET_USER == root ]] && TARGET_USER=
EMAIL=
PUBLIC_IP=
CHECK_ONLY=0

say()  { printf '\n[XFCE] %s\n' "$*"; }
fail() { printf '\n[FEHLER] %s\n' "$*" >&2; exit 1; }

usage() {
  cat <<'EOF'
Ubuntu 22.04/24.04: XFCE unter https://vpn.skyz.pet installieren.

Auf dem richtigen Server ausführen:
  sudo bash install-xfce-vpn-skyz.sh --user sky --email DEINE-EMAIL

Optionen:
  --user NAME       Vorhandener Linux-Benutzer für die Desktop-Sitzung
  --email ADRESSE   Kontaktadresse für Let's Encrypt (nur bei Installation)
  --public-ip IP    Öffentliche IPv4 des Servers (sonst automatische Erkennung)
  --check           Nur Betriebssystem, Domain und Konflikte prüfen
  --help            Diese Hilfe anzeigen

Vorher muss der A-Record vpn.skyz.pet auf diesen Server zeigen.
Ports 80 und 443 müssen aus dem Internet erreichbar sein.
EOF
}

while (($#)); do
  case $1 in
    --user|--email|--public-ip)
      (($# >= 2)) || fail "Wert für $1 fehlt."
      case $1 in
        --user) TARGET_USER=$2 ;;
        --email) EMAIL=$2 ;;
        --public-ip) PUBLIC_IP=$2 ;;
      esac
      shift 2 ;;
    --check) CHECK_ONLY=1; shift ;;
    --help|-h) usage; exit 0 ;;
    *) fail "Unbekannte Option: $1 (siehe --help)." ;;
  esac
done

((EUID == 0)) || fail "Bitte mit sudo oder als root starten."
[[ -f /etc/os-release ]] || fail "Betriebssystem konnte nicht erkannt werden."
# shellcheck disable=SC1091
. /etc/os-release
[[ ${ID:-} == ubuntu && ( ${VERSION_ID:-} == 22.04 || ${VERSION_ID:-} == 24.04 ) ]] ||
  fail "Dieses Skript unterstützt Ubuntu 22.04 und 24.04. Erkannt: ${PRETTY_NAME:-unbekannt}."

[[ $TARGET_USER =~ ^[a-z_][a-z0-9_-]*\$?$ && $TARGET_USER != root ]] ||
  fail "Bitte mit --user einen vorhandenen, normalen Linux-Benutzer angeben."
USER_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
[[ -n $USER_HOME && -d $USER_HOME ]] || fail "Benutzer $TARGET_USER oder Home-Verzeichnis fehlt."
[[ $USER_HOME != *' '* && $USER_HOME != *%* ]] || fail "Home-Pfad mit Leerzeichen oder % wird nicht unterstützt."

if [[ -z $PUBLIC_IP ]]; then
  command -v curl >/dev/null || fail "curl fehlt. Bitte --public-ip mit der Server-IP angeben."
  PUBLIC_IP=$(curl -4fsS --max-time 10 https://api.ipify.org) ||
    fail "Öffentliche IPv4 konnte nicht erkannt werden. Bitte --public-ip angeben."
fi
python3 - "$PUBLIC_IP" <<'PY' || fail "Ungültige IPv4: $PUBLIC_IP"
import ipaddress, sys
ipaddress.IPv4Address(sys.argv[1])
PY

DNS_IPS=$(getent ahostsv4 "$DOMAIN" | awk '$2 == "STREAM" {print $1}' | sort -u || true)
[[ -n $DNS_IPS ]] || fail "Kein A-Record für $DOMAIN. Lege ihn mit Ziel $PUBLIC_IP an und starte danach erneut."
[[ $DNS_IPS == "$PUBLIC_IP" ]] ||
  fail "$DOMAIN zeigt auf ${DNS_IPS//$'\n'/, }, dieser Server hat $PUBLIC_IP. DNS korrigieren oder --public-ip prüfen."

# glibc zeigt A-Records hier oft als IPv4-mapped ::ffff:x.x.x.x an.
# Solche Adressen sind keine eigenständigen AAAA-Records.
DNS_V6=$(getent ahostsv6 "$DOMAIN" | awk '$2 == "STREAM" && tolower($1) !~ /^::ffff:/ {print $1}' | sort -u || true)
if [[ -n $DNS_V6 ]]; then
  LOCAL_V6=$(ip -6 -o addr show scope global | awk '{split($4,a,"/"); print a[1]}' | sort -u)
  while IFS= read -r address; do
    [[ -z $address ]] && continue
    grep -Fxq "$address" <<<"$LOCAL_V6" ||
      fail "$DOMAIN hat einen AAAA-Record ($address), der nicht auf diesem Server liegt. AAAA korrigieren oder entfernen."
  done <<<"$DNS_V6"
fi

for owned_file in "$SITE_FILE" "$STARTUP" "$VNC_UNIT" "$WS_UNIT" "$RENEW_HOOK"; do
  if [[ -e $owned_file ]] && ! head -n 3 "$owned_file" | grep -Fqx "$MARKER"; then
    fail "$owned_file existiert bereits und stammt nicht von diesem Skript. Es wird nicht überschrieben."
  fi
done
if [[ -e $AUTH_FILE && ! -e $AUTH_MARKER ]]; then
  fail "$AUTH_FILE existiert bereits und ist nicht als zugehörig markiert."
fi
if [[ -e $SITE_LINK || -L $SITE_LINK ]] && [[ $(readlink -f "$SITE_LINK") != "$SITE_FILE" ]]; then
  fail "$SITE_LINK zeigt auf eine andere nginx-Konfiguration."
fi

port_in_use() {
  ss -H -ltn | awk '{print $4}' | grep -Eq "(^|[.:])$1$"
}
if [[ ! -e $VNC_UNIT ]] && port_in_use 5901; then
  fail "Port 5901 wird schon verwendet. Vorhandene VNC-Sitzung prüfen."
fi
if [[ ! -e $WS_UNIT ]] && port_in_use 6081; then
  fail "Port 6081 wird schon verwendet. Vorhandenen WebSocket-Dienst prüfen."
fi

if command -v nginx >/dev/null; then
  nginx -t >/dev/null || fail "Die vorhandene nginx-Konfiguration ist fehlerhaft."
  OWNERS=$(nginx -T 2>/dev/null | awk -v host="$DOMAIN" '
    /^# configuration file / { file=$4; sub(/:$/, "", file) }
    /^[[:space:]]*server_name[[:space:]]/ && index($0, host) { print file }
  ' | sort -u)
  while IFS= read -r owner; do
    [[ -z $owner || $owner == "$SITE_LINK" ]] ||
      fail "$DOMAIN ist bereits in $owner konfiguriert. Keine bestehende Seite wird überschrieben."
  done <<<"$OWNERS"
elif port_in_use 80 || port_in_use 443; then
  fail "Port 80 oder 443 ist belegt, aber nginx ist nicht installiert. Bitte Webserver-Konflikt klären."
fi

say "Prüfung erfolgreich: Ubuntu ${VERSION_ID}, Benutzer $TARGET_USER, $DOMAIN -> $PUBLIC_IP."
if ((CHECK_ONLY)); then
  say "Keine Änderungen vorgenommen."
  exit 0
fi
[[ $EMAIL == *@*.* && $EMAIL != *[[:space:]]* ]] ||
  fail "Für das TLS-Zertifikat bitte --email DEINE-EMAIL angeben."
[[ -t 0 ]] || fail "Die erste Einrichtung braucht ein Terminal für zwei Passwörter (kein 'curl | bash')."

say "Installiere schlanke XFCE-Sitzung, TigerVNC, noVNC, websockify und nginx."
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
  xfce4 xfce4-terminal dbus-x11 xauth tigervnc-standalone-server tigervnc-tools \
  novnc websockify nginx apache2-utils curl ca-certificates
if ! command -v certbot >/dev/null; then
  apt-get install -y --no-install-recommends certbot
fi
[[ -f /usr/share/novnc/vnc.html ]] || fail "noVNC-Dateien fehlen unter /usr/share/novnc."
command -v tigervncserver >/dev/null || fail "tigervncserver konnte nicht installiert werden."
command -v tigervncpasswd >/dev/null || fail "tigervncpasswd konnte nicht installiert werden."
command -v websockify >/dev/null || fail "websockify konnte nicht installiert werden."

install -d -m 700 -o "$TARGET_USER" -g "$(id -gn "$TARGET_USER")" "$USER_HOME/.vnc"
VNC_PASSFILE=$USER_HOME/.vnc/passwd
if [[ ! -s $VNC_PASSFILE ]]; then
  say "Jetzt ein eigenes VNC-Passwort für $TARGET_USER festlegen (mindestens 6 Zeichen; nur die ersten 8 zählen)."
  runuser -u "$TARGET_USER" -- env HOME="$USER_HOME" tigervncpasswd "$VNC_PASSFILE"
fi
[[ -s $VNC_PASSFILE ]] || fail "VNC-Passwort wurde nicht angelegt."

if [[ ! -s $AUTH_FILE ]]; then
  say "Jetzt ein langes HTTPS-Zugangspasswort für den Browser-Benutzer $TARGET_USER festlegen."
  install -m 600 /dev/null "$AUTH_MARKER"
  htpasswd -B -C 12 -c "$AUTH_FILE" "$TARGET_USER"
  chown root:www-data "$AUTH_FILE"
  chmod 640 "$AUTH_FILE"
fi

install -d -m 755 /usr/local/libexec
cat >"$STARTUP" <<'EOF'
#!/bin/sh
# skyz-xfce installer v1
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
exec dbus-launch --exit-with-session startxfce4
EOF
chmod 755 "$STARTUP"

cat >"$VNC_UNIT" <<EOF
$MARKER
[Unit]
Description=XFCE Desktop für $TARGET_USER (nur localhost)
After=network.target

[Service]
Type=simple
User=$TARGET_USER
WorkingDirectory=$USER_HOME
Environment=HOME=$USER_HOME
ExecStart=/usr/bin/tigervncserver -fg :1 -localhost yes -SecurityTypes VncAuth -geometry 1600x900 -depth 24 -autokill no -xstartup $STARTUP
ExecStop=/usr/bin/tigervncserver -kill :1
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

cat >"$WS_UNIT" <<EOF
$MARKER
[Unit]
Description=Lokale WebSocket-Brücke für $DOMAIN
After=network.target ${SITE_ID}-vnc.service
Requires=${SITE_ID}-vnc.service

[Service]
Type=simple
User=www-data
ExecStart=/usr/bin/websockify 127.0.0.1:6081 127.0.0.1:5901
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now "${SITE_ID}-vnc.service"
systemctl enable --now "${SITE_ID}-websockify.service"
systemctl is-active --quiet "${SITE_ID}-vnc.service" || fail "VNC läuft nicht; siehe journalctl -u ${SITE_ID}-vnc -n 50."
systemctl is-active --quiet "${SITE_ID}-websockify.service" || fail "websockify läuft nicht; siehe journalctl -u ${SITE_ID}-websockify -n 50."

install -d -m 755 "$ACME_ROOT/.well-known/acme-challenge"

write_site() {
  local tmp backup had_old=0
  tmp=$(mktemp)
  backup=$(mktemp)
  if [[ -f $SITE_FILE ]]; then cp -a "$SITE_FILE" "$backup"; had_old=1; fi
  cat >"$tmp"
  install -m 644 "$tmp" "$SITE_FILE"
  if [[ ! -e $SITE_LINK ]]; then ln -s "$SITE_FILE" "$SITE_LINK"; fi
  if ! nginx -t; then
    if ((had_old)); then cp -a "$backup" "$SITE_FILE"; else rm -f "$SITE_LINK" "$SITE_FILE"; fi
    rm -f "$tmp" "$backup"
    fail "Neue nginx-Konfiguration ist ungültig; die vorherige wurde wiederhergestellt."
  fi
  rm -f "$tmp" "$backup"
  systemctl enable --now nginx
  systemctl reload nginx
}

write_site <<EOF
$MARKER
server {
    listen 80;
    listen [::]:80;
    server_name $DOMAIN;

    location ^~ /.well-known/acme-challenge/ {
        root $ACME_ROOT;
        default_type text/plain;
        try_files \$uri =404;
    }
    location / { return 404; }
}
EOF

nginx -T 2>/dev/null | grep -F "$MARKER" >/dev/null ||
  fail "nginx lädt $SITE_LINK nicht. Prüfe, ob /etc/nginx/sites-enabled/* eingebunden ist."

CHECK_FILE=$ACME_ROOT/.well-known/acme-challenge/${SITE_ID}-check
printf '%s\n' 'acme-reachable' >"$CHECK_FILE"
chmod 644 "$CHECK_FILE"
CHECK_REPLY=$(curl -fsS --max-time 5 --noproxy '*' \
  --resolve "$DOMAIN:80:$PUBLIC_IP" \
  "http://$DOMAIN/.well-known/acme-challenge/${SITE_ID}-check") ||
  fail "HTTP-Test über $PUBLIC_IP fehlgeschlagen. Prüfe nginx-Serverblöcke für diese IP und Port 80."
rm -f "$CHECK_FILE"
[[ $CHECK_REPLY == acme-reachable ]] || fail "nginx liefert die ACME-Challenge nicht korrekt aus."

if command -v ufw >/dev/null && ufw status | grep -q '^Status: active'; then
  say "Öffne HTTP/HTTPS in der aktiven UFW-Firewall (SSH-Regeln bleiben unberührt)."
  ufw allow 80/tcp
  ufw allow 443/tcp
fi

say "Fordere ein Let's-Encrypt-Zertifikat für $DOMAIN an."
(
  # Die Challenge-Dateien müssen für den nginx-Worker lesbar sein.
  umask 022
  certbot certonly --webroot --webroot-path "$ACME_ROOT" \
    --cert-name "$DOMAIN" --domain "$DOMAIN" --email "$EMAIL" \
    --agree-tos --non-interactive --keep-until-expiring
)
[[ -s /etc/letsencrypt/live/$DOMAIN/fullchain.pem ]] || fail "Zertifikat fehlt nach Certbot."

# Ein vorhandener 443-Listener auf genau dieser IP hat bei nginx Vorrang
# vor einem Wildcard-Listener. In dem Fall denselben Listener nutzen.
HTTPS_IPV4_LISTEN='    listen 443 ssl;'
if nginx -T 2>/dev/null | awk -v address="$PUBLIC_IP:443" \
  '$1 == "listen" && $2 == address { found = 1 } END { exit !found }'; then
  HTTPS_IPV4_LISTEN="    listen $PUBLIC_IP:443 ssl;"
fi

write_site <<EOF
$MARKER
server {
    listen 80;
    listen [::]:80;
    server_name $DOMAIN;
    location ^~ /.well-known/acme-challenge/ {
        root $ACME_ROOT;
        default_type text/plain;
        try_files \$uri =404;
    }
    location / { return 301 https://\$host\$request_uri; }
}

server {
$HTTPS_IPV4_LISTEN
    listen [::]:443 ssl;
    server_name $DOMAIN;
    ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;

    auth_basic "Geschützter Desktop";
    auth_basic_user_file $AUTH_FILE;
    add_header X-Frame-Options DENY always;
    add_header Cache-Control "no-cache" always;

    root /usr/share/novnc;
    location = / { return 302 /vnc.html?autoconnect=1&resize=remote&path=websockify; }
    location = /websockify {
        proxy_pass http://127.0.0.1:6081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
    }
    location / { try_files \$uri =404; }
}
EOF

install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
cat >"$RENEW_HOOK" <<'EOF'
#!/bin/sh
# skyz-xfce installer v1
systemctl reload nginx
EOF
chmod 755 "$RENEW_HOOK"

HTTP_STATUS=$(curl -sS --max-time 10 --noproxy '*' \
  --resolve "$DOMAIN:443:$PUBLIC_IP" \
  -o /dev/null -w '%{http_code}' "https://$DOMAIN/vnc.html") ||
  fail "HTTPS-Test fehlgeschlagen. Prüfe Zertifikat und nginx-Logs."
[[ $HTTP_STATUS == 401 ]] ||
  fail "HTTPS sollte vor der Anmeldung HTTP 401 liefern, hat aber $HTTP_STATUS geliefert."

say "Fertig: https://$DOMAIN/"
printf '1. Browser-Anmeldung: Benutzer %s und das lange HTTPS-Passwort.\n' "$TARGET_USER"
printf '2. Danach in noVNC: das separate VNC-Passwort.\n'
printf 'Status: systemctl status %s-vnc %s-websockify nginx --no-pager\n' "$SITE_ID" "$SITE_ID"
