#!/bin/sh
# portguard.sh — install / update / uninstall the PortGuard agent or server.
#
#   curl -fsSL https://get.lesir.cc/portguard.sh | sudo sh -s -- <action> <component> [options]
#
# Actions:    install | update | uninstall          Components: agent | server
#
# Agent:
#   install agent  --server URL --enrollment-token T [--download-token T] [--node N] [--version V]
#   update  agent  [--download-token T] [--version V]
#   uninstall agent [--purge]
#
# Server:
#   install server --enrollment-token T [--listen ADDR] [--download-token T]
#                  [--admin-user U --admin-password P] [--version V]
#   update  server [--download-token T] [--version V]
#   uninstall server [--purge]
#
# Binaries are downloaded over a token-gated Cloudflare edge, their checksums are
# verified, and (when a signing key is configured below) the checksum file's
# signature is verified with openssl. No Go runtime is required on the target.
set -eu

INSTALL_BASE="${PORTGUARD_INSTALL_BASE:-https://get.lesir.cc}"
DOWNLOAD_TOKEN="${PORTGUARD_DOWNLOAD_TOKEN:-}"

# Signing public key (PEM, ECDSA P-256). Replace the placeholder with your real
# public key to *enforce* signature verification; the private key stays in CI.
PUBKEY_PEM='-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEROApAhP8O9sAhy/0khimLMmjWGAK
xlvZ20AUSI7S8DB+VpR2UkvNXi6UMdbkof8ktiXwS20+hwXvvBrccNqLgQ==
-----END PUBLIC KEY-----'

VERSION="latest"
NODE="${PORTGUARD_NODE:-$(hostname -s 2>/dev/null || hostname)}"
SERVER="${PORTGUARD_SERVER:-}"
ENROLL_TOKEN="${PORTGUARD_ENROLLMENT_TOKEN:-}"
LISTEN="127.0.0.1:8080"
NFT_PATH="nft"
ADMIN_USER=""
ADMIN_PASSWORD=""
PURGE=0
SKIP_VERIFY=0

usage() { sed -n '2,33p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//'; }
fail() { echo "portguard: $*" >&2; exit 1; }

ACTION="${1:-}"
COMPONENT="${2:-}"
case "$ACTION" in install|update|uninstall) ;; -h|--help|help|"") usage; exit 0 ;; *) fail "unknown action: $ACTION (want install|update|uninstall)" ;; esac
case "$COMPONENT" in agent|server) ;; *) fail "unknown component: $COMPONENT (want agent|server)" ;; esac
shift 2

while [ $# -gt 0 ]; do
  case "$1" in
    --server) SERVER="$2"; shift 2 ;;
    --node) NODE="$2"; shift 2 ;;
    --enrollment-token) ENROLL_TOKEN="$2"; shift 2 ;;
    --download-token) DOWNLOAD_TOKEN="$2"; shift 2 ;;
    --version) VERSION="$2"; shift 2 ;;
    --base) INSTALL_BASE="$2"; shift 2 ;;
    --listen) LISTEN="$2"; shift 2 ;;
    --nft-path) NFT_PATH="$2"; shift 2 ;;
    --admin-user) ADMIN_USER="$2"; shift 2 ;;
    --admin-password) ADMIN_PASSWORD="$2"; shift 2 ;;
    --purge) PURGE=1; shift ;;
    --insecure-skip-verify) SKIP_VERIFY=1; shift ;;
    *) fail "unknown option: $1" ;;
  esac
done

[ "$(id -u)" = "0" ] || fail "run as root (sudo)"

BIN="/usr/local/bin/portguard-$COMPONENT"
UNIT="/etc/systemd/system/portguard-$COMPONENT.service"
STATE_DIR="/etc/portguard/$COMPONENT"

detect_arch() {
  case "$(uname -m)" in
    x86_64|amd64) ARCH="amd64" ;;
    aarch64|arm64) ARCH="arm64" ;;
    *) fail "unsupported architecture: $(uname -m)" ;;
  esac
}

dl() { # dl <url> <out>
  if [ -n "$DOWNLOAD_TOKEN" ]; then
    curl -fsSL -H "Authorization: Bearer $DOWNLOAD_TOKEN" "$1" -o "$2"
  else
    curl -fsSL "$1" -o "$2"
  fi
}

verify_signature() { # verify_signature <checksums-file>
  case "$PUBKEY_PEM" in
    *PORTGUARD_PUBKEY*)
      [ "$SKIP_VERIFY" = "1" ] && return 0
      echo "portguard: signing key not configured; skipping signature check" >&2
      return 0 ;;
  esac
  [ "$SKIP_VERIFY" = "1" ] && { echo "portguard: --insecure-skip-verify set, skipping signature check" >&2; return 0; }
  command -v openssl >/dev/null 2>&1 || fail "openssl is required to verify the release signature (or pass --insecure-skip-verify)"
  dl "$INSTALL_BASE/$COMPONENT/$VERSION/checksums.txt.sig" "$TMP/sig" || fail "signature download failed"
  printf '%s\n' "$PUBKEY_PEM" > "$TMP/pub.pem"
  openssl dgst -sha256 -verify "$TMP/pub.pem" -signature "$TMP/sig" "$1" >/dev/null 2>&1 \
    || fail "signature verification FAILED for $COMPONENT $VERSION"
  echo "portguard: signature verified"
}

fetch_binary() { # downloads + verifies into $TMP/bin
  detect_arch
  asset="portguard-${COMPONENT}_linux_${ARCH}"
  command -v curl >/dev/null 2>&1 || fail "curl is required"
  command -v sha256sum >/dev/null 2>&1 || fail "sha256sum is required"
  echo "portguard: downloading $asset ($VERSION)…"
  dl "$INSTALL_BASE/$COMPONENT/$VERSION/$asset" "$TMP/bin" || fail "download failed (check --download-token / --base)"
  dl "$INSTALL_BASE/$COMPONENT/$VERSION/checksums.txt" "$TMP/sums" || fail "checksum download failed"
  verify_signature "$TMP/sums"
  want="$(grep " ${asset}\$" "$TMP/sums" | awk '{print $1}' | head -n1)"
  [ -n "$want" ] || fail "no checksum entry for $asset"
  got="$(sha256sum "$TMP/bin" | awk '{print $1}')"
  [ "$want" = "$got" ] || fail "checksum mismatch (want $want, got $got)"
  echo "portguard: checksum verified"
}

install_binary() {
  install -m 0755 "$TMP/bin" "$BIN"
  mkdir -p "$STATE_DIR"
}

write_agent_unit() {
  cat > "$UNIT" <<EOF
[Unit]
Description=PortGuard agent
Documentation=https://github.com/lesir831/portguard
After=network-online.target nftables.service
Wants=network-online.target

[Service]
ExecStart=$BIN run --server $SERVER --node $NODE --state-dir $STATE_DIR --nft-path $NFT_PATH
Restart=always
RestartSec=5
AmbientCapabilities=CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_ADMIN
NoNewPrivileges=yes
ProtectHome=yes
ProtectSystem=full

[Install]
WantedBy=multi-user.target
EOF
}

write_server_unit() {
  cat > "$UNIT" <<EOF
[Unit]
Description=PortGuard control-plane server
Documentation=https://github.com/lesir831/portguard
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=$BIN --listen $LISTEN --sqlite-file $STATE_DIR/portguard.db --enrollment-token $ENROLL_TOKEN
Restart=always
RestartSec=5
NoNewPrivileges=yes
ProtectHome=yes

[Install]
WantedBy=multi-user.target
EOF
}

svc_enable() { systemctl daemon-reload; systemctl enable --now "portguard-$COMPONENT.service"; }
svc_restart() { systemctl daemon-reload; systemctl restart "portguard-$COMPONENT.service"; }

do_install_agent() {
  [ -n "$SERVER" ] || fail "--server is required"
  [ -n "$ENROLL_TOKEN" ] || fail "--enrollment-token is required"
  command -v nft >/dev/null 2>&1 || fail "nftables (nft) is required (e.g. apt-get install -y nftables)"
  fetch_binary
  install_binary
  echo "portguard: enrolling node $NODE…"
  "$BIN" enroll --server "$SERVER" --node "$NODE" --state-dir "$STATE_DIR" --enrollment-token "$ENROLL_TOKEN"
  write_agent_unit
  svc_enable
  echo "portguard: agent installed (node=$NODE). logs: journalctl -u portguard-agent -f"
}

do_install_server() {
  [ -n "$ENROLL_TOKEN" ] || fail "--enrollment-token is required"
  fetch_binary
  install_binary
  write_server_unit
  if [ -n "$ADMIN_USER" ] && [ -n "$ADMIN_PASSWORD" ]; then
    "$BIN" reset-password --sqlite-file "$STATE_DIR/portguard.db" --username "$ADMIN_USER" --password "$ADMIN_PASSWORD"
  fi
  svc_enable
  echo "portguard: server installed on $LISTEN. create an admin user with:"
  echo "  portguard-server reset-password --username admin"
}

do_update() {
  [ -f "$BIN" ] || fail "portguard-$COMPONENT is not installed; run 'install $COMPONENT' first"
  fetch_binary
  install -m 0755 "$TMP/bin" "$BIN"
  if [ -f "$UNIT" ]; then
    svc_restart
    echo "portguard: $COMPONENT updated to $("$BIN" version 2>/dev/null || echo "$VERSION") and restarted"
  else
    echo "portguard: $COMPONENT binary updated (no systemd unit found; not restarted)"
  fi
}

do_uninstall() {
  systemctl disable --now "portguard-$COMPONENT.service" 2>/dev/null || true
  rm -f "$UNIT" "$BIN"
  systemctl daemon-reload 2>/dev/null || true
  if [ "$PURGE" = "1" ]; then
    rm -rf "$STATE_DIR"
    echo "portguard: $COMPONENT uninstalled and state purged ($STATE_DIR)"
  else
    echo "portguard: $COMPONENT uninstalled (state kept at $STATE_DIR; use --purge to remove)"
  fi
}

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

case "$ACTION:$COMPONENT" in
  install:agent) do_install_agent ;;
  install:server) do_install_server ;;
  update:agent|update:server) do_update ;;
  uninstall:agent|uninstall:server) do_uninstall ;;
esac
