#!/bin/bash
# alpha-rootcmd — fixed allow-list of safe root actions for the panel console.
# Root-owned; called by www-data via NOPASSWD sudoers. Takes an ACTION key
# (+ a validated service name for svc-*). NEVER runs arbitrary commands.
set -euo pipefail
ACTION="${1:-}"
ARG="${2:-}"
ARG2="${3:-}"

svc_ok() {
  case "$1" in
    nginx|apache2|lsws|postfix|dovecot|opendkim|mariadb|docker|cron|fail2ban|\
    redis-server|memcached|pure-ftpd|named|bind9|ufw|postgresql|\
    code-server-alpha|\
    php7.4-fpm|php8.0-fpm|php8.1-fpm|php8.2-fpm|php8.3-fpm|php8.4-fpm) return 0 ;;
  esac
  # website app services (alpha-app-<sub>) — allowed if the unit actually exists
  local u="${1%.service}"
  if [[ "$u" =~ ^alpha-app-[a-z0-9]([a-z0-9._-]*[a-z0-9])?$ ]] && [ -f "/etc/systemd/system/${u}.service" ]; then return 0; fi
  # any installed PHP-FPM version (panel-managed runtimes beyond the static list)
  if [[ "$u" =~ ^php([0-9]+\.[0-9]+)-fpm$ ]] && [ -d "/etc/php/${BASH_REMATCH[1]}" ]; then return 0; fi
  return 1
}

# start/stop/restart — the serving stack (nginx, php*-fpm) is detached+delayed so
# we never drop the panel's own response (502); other services run inline.
svc_act() {
  local verb="$1" svc="$2"
  case "$svc" in
    nginx|php*-fpm)
      setsid bash -c "sleep 1; systemctl ${verb} '${svc}'" >/dev/null 2>&1 </dev/null &
      echo "OK: ${verb}ing ${svc} (web stack — applied in ~1s)" ;;
    *)
      if systemctl "$verb" "$svc" 2>&1; then echo "OK: ${verb} ${svc}"; else echo "FAIL: ${verb} ${svc}"; exit 6; fi ;;
  esac
}

case "$ACTION" in
  svc-status)   svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; systemctl status "$ARG" --no-pager -l 2>&1 | head -40 ;;
  svc-start)    svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; svc_act start "$ARG" ;;
  svc-stop)     svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; svc_act stop "$ARG" ;;
  svc-restart)  svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; svc_act restart "$ARG" ;;
  svc-reload)   svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; systemctl reload "$ARG" && echo "OK: reloaded $ARG" ;;
  svc-logs)     svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; journalctl -u "$ARG" -n 60 --no-pager 2>&1 | tail -60 ;;
  nginx-test)   nginx -t 2>&1 ;;
  nginx-reload) nginx -t 2>&1 && systemctl reload nginx && echo "OK: nginx reloaded" ;;
  cert-list)    certbot certificates 2>&1 ;;
  cert-renew-dry) certbot renew --dry-run 2>&1 | tail -40 ;;
  cert-renew)   certbot renew 2>&1 | tail -40 ;;
  mail-log)     tail -n 60 /var/log/mail.log 2>&1 ;;
  nginx-errlog) tail -n 60 /var/log/nginx/error.log 2>&1 ;;
  nginx-acclog) tail -n 40 /var/log/nginx/access.log 2>&1 ;;
  auth-log)     tail -n 60 /var/log/auth.log 2>&1 ;;
  syslog)       tail -n 60 /var/log/syslog 2>&1 ;;
  services)     systemctl list-units --type=service --state=running --no-pager --no-legend 2>&1 | head -50 ;;
  failed)       systemctl list-units --type=service --state=failed --no-pager --no-legend 2>&1 ;;
  apt-updates)  apt-get -s upgrade 2>/dev/null | grep -E "^Inst" || echo "All packages up to date." ;;
  ufw-status)   ufw status verbose 2>&1 ;;
  ports)        ss -tlnp 2>&1 | head -40 ;;
  apt-update)   apt-get update 2>&1 | tail -25 ;;
  apt-upgrade)  DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::=--force-confold upgrade 2>&1 | tail -50 ;;
  f2b-status)   fail2ban-client status 2>&1; for j in $(fail2ban-client status 2>/dev/null | sed -n "s/.*Jail list:[[:space:]]*//p" | tr "," " "); do echo; fail2ban-client status "$j" 2>&1; done ;;
  f2b-banned)   fail2ban-client banned 2>&1 ;;
  f2b-unban)    [[ "$ARG" =~ ^[0-9a-fA-F:.]{3,45}$ ]] || { echo "BAD_IP: $ARG"; exit 2; }; fail2ban-client unban "$ARG" 2>&1 ;;
  disk-usage)
    # read-only whole-disk accounting for the File Manager's usage analyzer:
    # the root filesystem's df figures + a du breakdown of the space-consuming
    # top-level and /var/lib directories (one file system, so mounts aren't
    # double-counted). Output: "DF total used avail" + "P bytes /path" lines.
    df -B1 --output=size,used,avail / | tail -1 | awk '{print "DF "$1" "$2" "$3}'
    timeout 120 du -x -B1 -s \
      /usr /lib /lib64 /libx32 /boot /bin /sbin /etc /opt /snap /home /root /srv /tmp \
      /var/log /var/cache /var/tmp /var/www /var/mail /var/vmail /var/spool /var/backups /var/snap \
      /var/lib/mysql /var/lib/postgresql /var/lib/mongodb /var/lib/docker /var/lib/lxd /var/lib/containerd \
      /var/lib/snapd /var/lib/apt /var/lib/dpkg /var/lib/alpha-quota /var/lib/alpha-vscode /var/lib/alpha-qemu \
      2>/dev/null | awk '{print "P "$1" "$2}'
    ;;
  disk-ls)
    # depth-1 breakdown of ONE directory for the usage analyzer's drill-down.
    # The path must be absolute, contain no "..", and (after canonicalisation)
    # sit under an allow-listed system root. Read-only: du gives sizes, not content.
    p="$ARG"
    case "$p" in /*) ;; *) echo "BAD_PATH"; exit 2 ;; esac
    case "$p" in *..*) echo "BAD_PATH"; exit 2 ;; esac
    rp="$(realpath -e "$p" 2>/dev/null)" || { echo "NO_PATH"; exit 2; }
    case "$rp" in
      /usr|/usr/*|/lib|/lib/*|/lib64|/lib64/*|/libx32|/libx32/*|/boot|/boot/*|/bin|/bin/*|\
      /sbin|/sbin/*|/etc|/etc/*|/opt|/opt/*|/snap|/snap/*|/home|/home/*|/root|/root/*|\
      /srv|/srv/*|/tmp|/tmp/*|/var|/var/*) ;;
      *) echo "BAD_ROOT"; exit 2 ;;
    esac
    [ -d "$rp" ] || { echo "NOT_DIR"; exit 2; }
    timeout 60 du -x -B1 --max-depth=1 "$rp" 2>/dev/null | awk -v top="$rp" '$2!=top {print "D "$1" "$2}'
    find "$rp" -maxdepth 1 -type f -printf 'F %s %p\n' 2>/dev/null
    ;;
  svc-enable)   svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; systemctl enable "$ARG" >/dev/null 2>&1; echo "OK: $ARG enabled at boot" ;;
  svc-disable)  svc_ok "$ARG" || { echo "BAD_SERVICE: $ARG"; exit 2; }; systemctl disable "$ARG" >/dev/null 2>&1; echo "OK: $ARG disabled at boot" ;;
  proc-info)
    # read-only deep info on ONE process for the dashboard's process dossier:
    # things /proc hides from www-data for other users' PIDs (exe, cwd, fd count).
    [[ "$ARG" =~ ^[0-9]{1,8}$ ]] || { echo "BAD_PID"; exit 2; }
    [ -d "/proc/$ARG" ] || { echo "NO_PROC"; exit 2; }
    echo "EXE $(readlink -f "/proc/$ARG/exe" 2>/dev/null || echo -)"
    echo "CWD $(readlink -f "/proc/$ARG/cwd" 2>/dev/null || echo -)"
    echo "FDS $(ls "/proc/$ARG/fd" 2>/dev/null | wc -l)"
    ;;
  proc-kill)
    # end ONE process (TERM, or KILL when forced). Refuses kernel threads and the
    # load-bearing stack — those are managed as services, not by killing PIDs.
    [[ "$ARG" =~ ^[0-9]{1,8}$ ]] || { echo "BAD_PID"; exit 2; }
    SIG="${ARG2:-TERM}"; [[ "$SIG" == "TERM" || "$SIG" == "KILL" ]] || { echo "BAD_SIG"; exit 2; }
    [ -d "/proc/$ARG" ] || { echo "NO_PROC"; exit 2; }
    C="$(tr -d '\0' 2>/dev/null < "/proc/$ARG/comm" || true)"
    [ "$ARG" -le 2 ] && { echo "PROTECTED: $C"; exit 3; }
    # procfs files stat as size 0 — test cmdline by CONTENT, not -s
    [ -n "$(tr -d '\0' 2>/dev/null < "/proc/$ARG/cmdline")" ] || { echo "PROTECTED: kernel thread"; exit 3; }
    case "$C" in
      systemd|systemd-*|init|sshd|nginx|apache2|litespeed|php-fpm*|mariadbd|mysqld|postgres|\
      dockerd|containerd|lxd|lxcfs|sudo|alpha-rootcmd)
        echo "PROTECTED: $C"; exit 3 ;;
    esac
    if kill "-$SIG" "$ARG" 2>&1; then echo "OK: sent SIG$SIG to $ARG ($C)"; else echo "FAIL: could not signal $ARG"; exit 6; fi
    ;;
  proc-by-path|proc-kill-path)
    # processes running FROM an add-on folder (exe inside it, or cwd inside it
    # for non-shell processes). Jailed to the two Addons roots. proc-by-path is
    # read-only; proc-kill-path TERMs the set, waits, then KILLs survivors —
    # the sweep that frees strays like a wedged bundled binary.
    p="$ARG"
    case "$p" in /*) ;; *) echo "BAD_PATH"; exit 2 ;; esac
    case "$p" in *..*) echo "BAD_PATH"; exit 2 ;; esac
    rp="$(realpath -e "$p" 2>/dev/null)" || { echo "NO_PATH"; exit 2; }
    case "$rp" in
      /var/www/Addons|/var/www/Addons/*|/var/www/alpha/Addons|/var/www/alpha/Addons/*) ;;
      *) echo "BAD_ROOT"; exit 2 ;;
    esac
    matches=""
    for d in /proc/[0-9]*; do
      pid="${d#/proc/}"
      exe="$(readlink -f "$d/exe" 2>/dev/null || true)"
      cwd="$(readlink -f "$d/cwd" 2>/dev/null || true)"
      comm="$(cat "$d/comm" 2>/dev/null || true)"   # cat, not <: a vanished PID's open error must hit the SILENCED stderr
      hit=""
      case "$exe" in "$rp"|"$rp"/*) hit=1 ;; esac
      if [ -z "$hit" ]; then
        case "$cwd" in "$rp"|"$rp"/*)
          case "$comm" in bash|sh|dash|zsh|fish|tmux|screen|sshd|sudo|su) ;; *) hit=1 ;; esac ;;
        esac
      fi
      [ -z "$hit" ] && continue
      case "$comm" in systemd|systemd-*|init|nginx|apache2|litespeed|php-fpm*|mariadbd|mysqld|postgres|dockerd|containerd|lxd|lxcfs|alpha-rootcmd) continue ;; esac
      rss=$(( $(awk '{print $2}' "$d/statm" 2>/dev/null || echo 0) * 4096 ))
      echo "PID $pid $rss $comm"
      matches="$matches $pid"
    done
    if [ "$ACTION" = "proc-kill-path" ] && [ -n "$matches" ]; then
      kill -TERM $matches 2>/dev/null || true
      sleep 1.2
      for pid in $matches; do
        [ -d "/proc/$pid" ] && { kill -KILL "$pid" 2>/dev/null || true; echo "KILLED $pid"; }
      done
      echo "OK: swept"
    fi
    ;;
  *) echo "BAD_ACTION: $ACTION"; exit 1 ;;
esac
