#!/usr/bin/env bash
# perpetua -- look after this CreekX server (ADR-0018). Installed by install.sh.
#
#   sudo creekx status              is it running, which version, the licence and trial
#   sudo creekx logs [-f]           what it has been saying (-f: keep following)
#   sudo creekx restart
#   sudo creekx licence FILE        add the licence file your supplier sent
#   sudo creekx owner               a new password for the owner (or a new owner)
#   sudo creekx backup [--with-library] [--offsite]
#                                     the database, configuration and licence, into
#                                     /var/backups/perpetua (the videos too, if asked);
#                                     --offsite: also encrypted and sent to your bucket
#   sudo creekx backup-offsite      nightly encrypted backups to your own S3-compatible
#                                     bucket (asks, sends a first one, then keeps)
#   sudo creekx restore-offsite NAME [--yes]
#                                     fetch backup NAME from the bucket, decrypt, restore
#   sudo creekx restore DIR         put a backup back (this server's data is replaced)
#   sudo creekx upgrade FILE        install a newer release (perpetua-*.tar.gz; an offline
#                                     bundle upgrades without the network)
#   sudo creekx email               send email through your mail account ("forgot your
#                                     password" links): asks for the settings, and keeps
#                                     them only once a test email has gone
#   sudo creekx email test [TO]     send a test email with the saved settings
#   sudo creekx email off           stop sending email
#   sudo creekx domain NAME         use https at NAME (its DNS already points here): the
#                                     web server gets the certificate, and keeps it renewed
#   sudo creekx metrics             where Prometheus reads this server's metrics, and how
#   perpetua version

set -euo pipefail

PREFIX=/opt/perpetua
DATA=/var/lib/perpetua
SERVICE=perpetua

fail() {
  printf '%s\n' "$*" >&2
  exit 1
}
need_root() { [ "$(id -u)" = 0 ] || fail "Run it with sudo: sudo creekx $1"; }

ENV_FILE=/etc/perpetua/perpetua.env
# The settings `perpetua email` writes, and removes.
MAIL_KEYS='^(PERPETUA_MAIL|PERPETUA_MAIL_FROM|PERPETUA_SMTP_[A-Z_]+)='
# The settings `perpetua backup-offsite` writes.
BACKUP_KEYS='^PERPETUA_BACKUP_[A-Z0-9_]+='

# Runs the admin program as the service, with the configuration it would have: this
# server's, or (SETTINGS=file) one about to replace it.
admin() {
  runuser -u "$SERVICE" -- env -i PATH=/usr/bin:/bin HOME="$DATA" \
    SETTINGS="${SETTINGS:-$ENV_FILE}" \
    bash -c 'set -a; . "$SETTINGS"; set +a; exec /opt/perpetua/node/bin/node "$0" "$@"' \
    "$PREFIX/current/app/perpetua-admin.mjs" "$@"
}

# A copy of the configuration without the email settings, readable by the service: to
# change, and then put in place whole.
draft_settings() {
  local draft keys="${1:-$MAIL_KEYS}"
  draft="$(mktemp -p /etc/perpetua .perpetua.env.XXXXXX)"
  grep -v -E "$keys" "$ENV_FILE" >"$draft" || true
  chown root:"$SERVICE" "$draft"
  chmod 640 "$draft"
  printf '%s' "$draft"
}

command="${1:-help}"
[ $# -gt 0 ] && shift

case "$command" in
  version)
    cat "$PREFIX/current/VERSION"
    ;;

  status)
    need_root status
    printf 'CreekX %s\n' "$(cat "$PREFIX/current/VERSION")"
    if systemctl is-active --quiet "$SERVICE"; then echo "Service: running"; else echo "Service: NOT running (sudo creekx logs)"; fi
    if curl -fsS "http://127.0.0.1:8080/readyz" >/dev/null 2>&1; then echo "Ready:   yes"; else echo "Ready:   no"; fi
    if grep -q '^PERPETUA_MAIL=smtp' "$ENV_FILE"; then echo "Email:   on (sudo creekx email test)"; else echo "Email:   off (sudo creekx email)"; fi
    admin status
    ;;

  logs)
    need_root logs
    journalctl -u "$SERVICE" -n 200 --no-pager "$@"
    ;;

  restart)
    need_root restart
    systemctl restart "$SERVICE"
    echo "Restarted."
    ;;

  licence | license)
    need_root licence
    [ $# -ge 1 ] && [ -f "$1" ] || fail "Which file? sudo creekx licence /path/to/licence.txt"
    # The service user reads it from a copy it may read; the admin command checks it first.
    copy="$(mktemp -p "$DATA" licence.XXXXXX)"
    cp "$1" "$copy"
    chown "$SERVICE:$SERVICE" "$copy"
    status=0
    admin licence "$copy" || status=$?
    rm -f "$copy"
    exit "$status"
    ;;

  owner)
    need_root owner
    read -r -p "Owner's email: " email </dev/tty
    read -r -s -p "New password (12 characters or more): " password </dev/tty
    echo
    read -r -s -p "Again: " confirm </dev/tty
    echo
    [ "$password" = "$confirm" ] || fail "They did not match. Nothing was changed."
    printf '%s' "$password" | admin owner --email "$email" --password-stdin
    ;;

  backup)
    need_root backup
    library=no offsite=no scheduled=no
    for flag in "$@"; do
      case "$flag" in
        --with-library) library=yes ;;
        --offsite) offsite=yes ;;
        --scheduled) scheduled=yes ;;
        *) fail "Unknown: $flag. sudo creekx backup [--with-library] [--offsite]" ;;
      esac
    done
    stamp="$(date -u +%Y%m%d-%H%M%S)"
    dir="/var/backups/perpetua/$stamp"
    mkdir -p "$dir"
    chmod 700 /var/backups/perpetua "$dir"
    runuser -u postgres -- pg_dump -Fc perpetua >"$dir/database.dump"
    # The configuration holds the key the stream keys are sealed with: without it, a
    # restored database cannot send to any platform.
    cp /etc/perpetua/perpetua.env "$dir/perpetua.env"
    [ -f "$DATA/licence.txt" ] && cp "$DATA/licence.txt" "$dir/licence.txt"
    if [ "$library" = yes ]; then
      tar -czf "$dir/library.tar.gz" -C "$DATA" library
    fi
    chmod 600 "$dir"/*
    echo "Backed up to $dir"
    if [ "$offsite" = no ]; then
      echo "Copy it OFF this server: a backup on the machine it protects protects nothing."
      echo "Or set up nightly encrypted copies to your own bucket: sudo creekx backup-offsite"
      exit 0
    fi
    # Packed where the service can read it, encrypted by it (backup-offsite.ts), sent, and
    # the packing removed. Videos stay out: Settings -> Recordings to storage copies those.
    out="$DATA/backup-out"
    mkdir -p "$out"
    chown "$SERVICE:$SERVICE" "$out"
    chmod 700 "$out"
    pack="$out/$stamp.tar"
    tar -cf "$pack" --exclude=library.tar.gz -C /var/backups/perpetua "$stamp"
    chown "$SERVICE:$SERVICE" "$pack"
    chmod 600 "$pack"
    if ! admin backup-upload --file "$pack" --name "$stamp"; then
      rm -f "$pack"
      fail "Backed up here, but NOT sent off this server. Check: sudo creekx backup-offsite"
    fi
    rm -f "$pack"
    # Nightly: the last 7 kept here as well; the bucket keeps every one (its own rules
    # expire them).
    if [ "$scheduled" = yes ]; then
      find /var/backups/perpetua -mindepth 1 -maxdepth 1 -type d -name '20*' | sort | head -n -7 |
        while read -r old; do rm -rf -- "$old"; done
    fi
    ;;

  backup-offsite)
    need_root backup-offsite
    echo 'Every night, a backup of this server -- the database, configuration and licence --'
    echo 'is ENCRYPTED here and sent to a bucket you own (AWS S3, or any service that works'
    echo 'like it). Nothing is saved until a first backup has gone.'
    echo
    read -r -p "Storage endpoint, e.g. https://s3.ap-south-1.amazonaws.com: " endpoint </dev/tty
    read -r -p "Region, e.g. ap-south-1: " region </dev/tty
    read -r -p "Bucket: " bucket </dev/tty
    read -r -p "Access key: " access </dev/tty
    read -r -s -p "Secret key: " secret </dev/tty
    echo
    read -r -p "Path-style addresses (yes for MinIO and most non-AWS services) [no]: " style </dev/tty
    echo
    echo 'A passphrase encrypts every backup. KEEP IT SOMEWHERE ELSE TOO (a password manager):'
    echo 'if this server is lost, the backups cannot be opened without it, by anybody.'
    read -r -s -p "Passphrase (16 characters or more): " pass </dev/tty
    echo
    read -r -s -p "Again: " pass2 </dev/tty
    echo
    [ "$pass" = "$pass2" ] || fail "The passphrases did not match. Nothing was saved."
    [ "${#pass}" -ge 16 ] || fail "The passphrase must be 16 characters or more. Nothing was saved."
    for value in "$endpoint" "$region" "$bucket" "$access" "$secret" "$pass"; do
      case "$value" in
        *"'"*) fail "A value contains a single quote, which the settings file cannot hold. Nothing was saved." ;;
      esac
    done
    pathstyle=false
    case "$style" in y | yes | Y | YES) pathstyle=true ;; esac
    draft="$(draft_settings "$BACKUP_KEYS")"
    trap 'rm -f "$draft"' EXIT
    {
      printf "%s='%s'\n" PERPETUA_BACKUP_S3_ENDPOINT "$endpoint" PERPETUA_BACKUP_S3_REGION "$region"
      printf "%s='%s'\n" PERPETUA_BACKUP_S3_BUCKET "$bucket" PERPETUA_BACKUP_S3_ACCESS_KEY "$access"
      printf "%s='%s'\n" PERPETUA_BACKUP_S3_SECRET_KEY "$secret" PERPETUA_BACKUP_PASSPHRASE "$pass"
      printf '%s\n' "PERPETUA_BACKUP_S3_PATH_STYLE=$pathstyle"
    } >>"$draft"
    # A first backup, with the new settings, before they are kept.
    SETTINGS="$draft" "$0" backup --offsite ||
      fail "Nothing was saved. Check the bucket and keys, then: sudo creekx backup-offsite"
    mv "$draft" "$ENV_FILE"
    trap - EXIT
    systemctl enable --now perpetua-backup.timer >/dev/null 2>&1 || true
    echo "Done: a backup went now, and one goes every night. To bring one back:"
    echo "  sudo creekx restore-offsite YYYYMMDD-HHMMSS"
    ;;

  restore-offsite)
    need_root restore-offsite
    name="${1:-}"
    [[ "$name" =~ ^[0-9]{8}-[0-9]{6}$ ]] ||
      fail "Which backup? sudo creekx restore-offsite YYYYMMDD-HHMMSS (as named in your bucket)"
    out="$DATA/backup-out"
    mkdir -p "$out"
    chown "$SERVICE:$SERVICE" "$out"
    chmod 700 "$out"
    pack="$out/$name.tar"
    admin backup-fetch --name "$name" --to "$pack" || fail "Nothing was changed."
    mkdir -p /var/backups/perpetua
    chmod 700 /var/backups/perpetua
    rm -rf -- "/var/backups/perpetua/$name"
    tar -xf "$pack" -C /var/backups/perpetua
    rm -f "$pack"
    exec "$0" restore "/var/backups/perpetua/$name" "${2:-}"
    ;;

  restore)
    need_root restore
    dir="${1:-}"
    [ -n "$dir" ] && [ -f "$dir/database.dump" ] && [ -f "$dir/perpetua.env" ] ||
      fail "Which backup? sudo creekx restore /var/backups/perpetua/YYYYMMDD-HHMMSS"
    if [ "${2:-}" != "--yes" ]; then
      read -r -p "This replaces everything on this server with that backup. Type RESTORE: " answer </dev/tty
      [ "$answer" = RESTORE ] || fail "Nothing was changed."
    fi

    # 1. Check the backup BEFORE anything here changes. The files are root's alone, so they
    #    are handed to the database programs on standard input.
    runuser -u postgres -- pg_restore --list <"$dir/database.dump" >/dev/null 2>&1 ||
      fail "That backup's database could not be read. Nothing was changed."
    grep -q '^PERPETUA_INSTALLATION_ID=' "$dir/perpetua.env" &&
      grep -q '^SECRETS_MASTER_KEY=' "$dir/perpetua.env" ||
      fail "That backup's configuration is incomplete. Nothing was changed."

    # 2. Restore into a NEW database, beside the one in use: a failure here changes nothing.
    runuser -u postgres -- dropdb --if-exists perpetua_restore
    runuser -u postgres -- createdb -O perpetua_owner perpetua_restore
    if ! runuser -u postgres -- pg_restore --no-owner --role=perpetua_owner --exit-on-error \
      -d perpetua_restore <"$dir/database.dump"; then
      runuser -u postgres -- dropdb --if-exists perpetua_restore
      fail "The backup could not be restored. Nothing on this server was changed."
    fi

    # 3. Swap it in: the only moment CreekX is stopped. The database as it was is kept,
    #    as perpetua_before_restore, until the next restore.
    systemctl stop "$SERVICE"
    runuser -u postgres -- psql -q -v ON_ERROR_STOP=1 \
      -c 'DROP DATABASE IF EXISTS perpetua_before_restore' \
      -c 'ALTER DATABASE perpetua RENAME TO perpetua_before_restore' \
      -c 'ALTER DATABASE perpetua_restore RENAME TO perpetua'
    # The backup's identity and keys (the installation ID the licence names, the key the
    # stream keys are sealed with), with THIS server's database password.
    url="$(grep '^DATABASE_URL=' /etc/perpetua/perpetua.env)"
    cp /etc/perpetua/perpetua.env /etc/perpetua/perpetua.env.before-restore
    cp "$dir/perpetua.env" /etc/perpetua/perpetua.env
    sed -i "s#^DATABASE_URL=.*#$url#" /etc/perpetua/perpetua.env
    chown root:"$SERVICE" /etc/perpetua/perpetua.env /etc/perpetua/perpetua.env.before-restore
    chmod 640 /etc/perpetua/perpetua.env /etc/perpetua/perpetua.env.before-restore
    if [ -f "$dir/licence.txt" ]; then
      install -o "$SERVICE" -g "$SERVICE" -m 0640 "$dir/licence.txt" "$DATA/licence.txt"
    fi
    if [ -f "$dir/library.tar.gz" ]; then
      tar -xzf "$dir/library.tar.gz" -C "$DATA"
      chown -R "$SERVICE:$SERVICE" "$DATA/library"
    fi
    # A backup from an older version: bring its database up to this one.
    admin migrate
    systemctl start "$SERVICE"
    for _ in $(seq 1 60); do
      curl -fsS "http://127.0.0.1:8080/readyz" >/dev/null 2>&1 && break
      sleep 1
    done
    curl -fsS "http://127.0.0.1:8080/readyz" >/dev/null 2>&1 ||
      fail "Restored, but it did not start: sudo creekx logs. What was here before is kept (database perpetua_before_restore, /etc/perpetua/perpetua.env.before-restore)."
    echo "Restored from $dir, and running."
    ;;

  upgrade)
    need_root upgrade
    [ $# -ge 1 ] && [ -f "$1" ] || fail "Which release? sudo creekx upgrade perpetua-VERSION-linux-x64.tar.gz"
    work="$(mktemp -d)"
    trap 'rm -rf "$work"' EXIT
    tar -xzf "$1" -C "$work"
    installer="$(find "$work" -maxdepth 2 -name install.sh | head -1)"
    [ -n "$installer" ] || fail "That file is not an CreekX release."
    # An offline bundle upgrades offline: nothing is downloaded.
    if [ -d "$(dirname "$installer")/offline" ]; then
      bash "$installer" --offline "$(dirname "$installer")"
    else
      bash "$installer"
    fi
    ;;

  domain)
    need_root domain
    name="${1:-}"
    printf '%s' "$name" | grep -Eq '^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$' ||
      fail "Which domain? Point its DNS at this server first, then: sudo creekx domain live.example.com"
    site="https://$name"
    # Everything the installer made from the site's address, now the domain. The installer
    # then gives the web server the domain (it gets and renews the certificate) and the live
    # server its certificate for RTMPS and RTSPS.
    sed -i \
      -e "s#^PERPETUA_APP_URL=.*#PERPETUA_APP_URL=$site#" \
      -e "s#^PERPETUA_LIVE_HOST=.*#PERPETUA_LIVE_HOST=$name#" \
      -e "s#^PERPETUA_LIVE_WEBRTC_URL=.*#PERPETUA_LIVE_WEBRTC_URL=$site#" \
      -e "s#^PERPETUA_LIVE_HLS_URL=.*#PERPETUA_LIVE_HLS_URL=$site/hls#" \
      -e "s#^PERPETUA_LIVE_ORIGINS=.*#PERPETUA_LIVE_ORIGINS=$site#" \
      -e '/^PERPETUA_INSECURE_COOKIES=/d' \
      "$ENV_FILE"
    if [ -f /opt/perpetua/installer/install.sh ]; then
      bash /opt/perpetua/installer/install.sh
      echo "CreekX now answers at $site (the certificate can take a minute)."
    else
      echo "Saved. To finish, install your release again: sudo creekx upgrade FILE"
    fi
    ;;

  metrics)
    need_root metrics
    # shellcheck source=/dev/null
    site="$(. "$ENV_FILE" && echo "$PERPETUA_APP_URL")"
    # shellcheck source=/dev/null
    token="$(. "$ENV_FILE" && echo "${PERPETUA_METRICS_TOKEN:-}")"
    [ -n "$token" ] || fail "No metrics token yet. Install this release again (sudo creekx upgrade FILE) to add one."
    scheme="${site%%://*}"
    target="${site#*://}"
    echo "Prometheus reads this server's metrics at $site/metrics, with this token."
    echo "Keep it private: it is a password for the metrics only."
    echo
    echo "Add this to prometheus.yml, under scrape_configs:"
    echo
    echo "  - job_name: perpetua"
    echo "    scheme: $scheme"
    echo "    metrics_path: /metrics"
    echo "    authorization:"
    echo "      credentials: $token"
    echo "    static_configs:"
    echo "      - targets: ['$target']"
    ;;

  email)
    need_root email
    case "${1:-}" in
      test)
        to="${2:-}"
        [ -n "$to" ] || read -r -p "Send a test email to: " to </dev/tty
        admin test-email --to "$to"
        ;;

      off)
        draft="$(draft_settings)"
        mv "$draft" "$ENV_FILE"
        systemctl restart "$SERVICE"
        echo 'Email is off. "Forgot your password?" now tells people to ask you instead.'
        ;;

      "")
        echo 'CreekX sends email through a mail account you already have. Your email'
        echo 'provider lists these settings as "SMTP" or "outgoing mail". Nothing is saved'
        echo 'until a test email has gone.'
        echo
        read -r -p "Mail server, e.g. smtp.example.com: " host </dev/tty
        read -r -p "Port [587]: " port </dev/tty
        port="${port:-587}"
        read -r -p "User name (usually your email address): " user </dev/tty
        password=''
        if [ -n "$user" ]; then
          read -r -s -p "Password (an app password, if your provider has them): " password </dev/tty
          echo
        fi
        read -r -p "Send as, e.g. CreekX <hello@example.com>: " from </dev/tty
        read -r -p "Send a test email to: " to </dev/tty

        [[ "$port" =~ ^[0-9]+$ ]] || fail "The port is a number, such as 465 or 587. Nothing was saved."
        for value in "$host" "$user" "$password" "$from"; do
          case "$value" in
            *"'"*) fail "A value contains a single quote, which the settings file cannot hold. Nothing was saved." ;;
          esac
        done
        # 465 speaks TLS from the start, anything else upgrades with STARTTLS -- except a
        # relay on this very machine, where nothing crosses a network.
        security=starttls
        [ "$port" = 465 ] && security=tls
        case "$host" in 127.* | localhost | ::1) security=none ;; esac

        # Quoted alike for both readers of this file, systemd and bash: a password with a
        # space, or "Name <address>", read unquoted would be cut short -- or run.
        draft="$(draft_settings)"
        trap 'rm -f "$draft"' EXIT
        {
          printf '%s\n' PERPETUA_MAIL=smtp
          printf "%s='%s'\n" PERPETUA_SMTP_HOST "$host"
          printf '%s\n' "PERPETUA_SMTP_PORT=$port" "PERPETUA_SMTP_SECURITY=$security"
          if [ -n "$user" ]; then
            printf "%s='%s'\n" PERPETUA_SMTP_USER "$user" PERPETUA_SMTP_PASSWORD "$password"
          fi
          printf "%s='%s'\n" PERPETUA_MAIL_FROM "$from"
        } >>"$draft"
        SETTINGS="$draft" admin test-email --to "$to" ||
          fail "Nothing was saved. Check the settings with your email provider, then: sudo creekx email"
        mv "$draft" "$ENV_FILE"
        trap - EXIT
        systemctl restart "$SERVICE"
        echo 'Saved, and CreekX restarted: "Forgot your password?" links will arrive.'
        ;;

      *)
        fail "sudo creekx email | sudo creekx email test [TO] | sudo creekx email off"
        ;;
    esac
    ;;

  help | -h | --help)
    sed -n '2,20p' "$0"
    ;;

  *)
    fail "Unknown command: $command (perpetua help)"
    ;;
esac
