#!/usr/bin/env bash
#
# 0G Nova Adventure - one-command server install.
#
#   curl -fsSL https://adventure.zerognova.com/install.sh | sudo bash
#
# You are about to pipe this into a root shell, so here is exactly what it does and nothing else:
#
#   1. Downloads the server package  (about 76 MB, over HTTPS)
#   2. Verifies its signature against the public key printed below
#   3. Unpacks it and runs the install.sh inside it
#   4. Starts the two services
#
# It installs nothing else, contacts nothing else, and sends no information about your machine
# anywhere. Everything it fetches comes from adventure.zerognova.com.
#
# WHAT IT NEEDS: root, and PostgreSQL already installed. It does NOT install PostgreSQL, touch
# your firewall, or obtain a certificate - those are your machine's business.
#
# WHAT IT CHANGES: creates a system user "zerog", a PostgreSQL role and database of the same name,
# writes to /opt/zerog-nova-server, and installs two systemd units. Nothing outside those.
#
# EVERY REAL DECISION belongs to the install.sh inside the package, and arguments given here are
# passed straight through to it:
#
#   curl -fsSL https://adventure.zerognova.com/install.sh | sudo bash -s -- --prefix /srv
#   curl -fsSL https://adventure.zerognova.com/install.sh | sudo bash -s -- --help
#
# WHY A SIGNATURE CHECK, when the download already came over HTTPS and so did this script: TLS
# proves who served the bytes, not that the bytes are the release. The signing key is not on the
# web server and never has been, so a signature that verifies against the key below could not have
# been produced by anyone who merely gained control of the download. If the check fails, this
# script installs nothing and says so.
#
# The key below is a PUBLIC key. It can verify a signature and cannot create one, which is why it
# is safe to print in a script anyone can read.
#
# THE WHOLE SCRIPT IS A FUNCTION, called on the very last line. A pipe to bash executes what has
# arrived so far, so a download cut halfway through a normally written script runs half of it.
# Written this way, a truncated copy defines an incomplete function, never reaches the call, and
# does nothing at all.

set -euo pipefail

ZEROG_BASE_URL="${ZEROG_BASE_URL:-https://adventure.zerognova.com/updates}"
ZEROG_TARBALL="zerog-nova-server-linux-x64.tar.gz"

ZEROG_PUBKEY='-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy0uUKCjdt9E75eYkYtZE
Jofw3O4XxMGQdVYo7pgrTtOGydX/NcXL5orrMXU8LxsXir959hPn9ZReWY9Us52s
U+cKM17lCRIOf0/9BvXeQTmL6FC2/ef+mzFj1tGppiQRJyCbVwhzPXerWdnYGCnG
JoLGPJ/A9LKLbZqvM8vB6xth4YLfh9GkRFLHXtTd3InSjzN2pQf/c+zuKoU4ot4h
7Z4SCyZxvjQV++SvhMJ88Pc2/+ChSuSAXgrYH8LF+DsLd9l8xIsO8P2BaSZwHywK
7k1579QZiL7OwftT7XqR+GJgyq22rYp9PbhZdq28954x88JC3qzwjMu7dfB55Ra7
fwIDAQAB
-----END PUBLIC KEY-----'

# NOT local to zerog_main, on purpose. The EXIT trap below fires after that function has already
# returned, and a local would be out of scope by then - which under "set -u" turns a clean finish
# into an unbound-variable error on the very last line.
ZEROG_STAGING=""

zerog_main() {
    say()  { printf '  %s\n' "$*"; }
    step() { printf '\n== %s\n' "$*"; }
    die()  { printf '\nERROR: %s\n' "$*" >&2; exit 1; }

    printf '\n0G Nova Adventure - server install\n'

    # Checked before anything is downloaded. Finding out about sudo after 76 MB is rude.
    [ "$(id -u)" -eq 0 ] || die "Run this as root: curl -fsSL $ZEROG_BASE_URL/install.sh | sudo bash"

    for tool in curl tar openssl base64; do
        command -v "$tool" >/dev/null 2>&1 || die "This needs '$tool' and it is not installed."
    done

    # PostgreSQL is install.sh's requirement, not this script's, but it is the one thing likely to
    # be missing and it is far kinder to say so now than after the download.
    if ! command -v psql >/dev/null 2>&1; then
        printf '\n'
        say "PostgreSQL is not installed. The server needs it and the install will stop without it."
        say "On Debian or Ubuntu:  sudo apt install postgresql"
        die "Install PostgreSQL, then run this again."
    fi

    ZEROG_STAGING="$(mktemp -d -t zerog-install-XXXXXXXX)"
    # Removed however this exits, including a failed download or a rejected signature. Nothing
    # here is worth keeping: the tarball is re-fetchable and the unpacked copy is installed from,
    # not run from.
    trap 'rm -rf "$ZEROG_STAGING"' EXIT

    step "Downloading"
    say "$ZEROG_BASE_URL/$ZEROG_TARBALL"

    # --speed-limit rather than --max-time: this is ~76 MB and a slow but working connection must
    # not be killed for taking its time. What is worth aborting is a connection that has stopped
    # moving, which is what these two say - under 1 KB/s for 90 seconds straight.
    curl -fL --progress-bar --speed-time 90 --speed-limit 1024 \
        -o "$ZEROG_STAGING/$ZEROG_TARBALL" "$ZEROG_BASE_URL/$ZEROG_TARBALL" \
        || die "Could not download the server package."

    curl -fsSL --max-time 60 -o "$ZEROG_STAGING/tarball.sig.b64" "$ZEROG_BASE_URL/$ZEROG_TARBALL.sig" \
        || die "The package signature could not be downloaded, so nothing was installed."

    step "Verifying"

    printf '%s\n' "$ZEROG_PUBKEY" > "$ZEROG_STAGING/update-public.pem"

    base64 -d < "$ZEROG_STAGING/tarball.sig.b64" > "$ZEROG_STAGING/tarball.sig" 2>/dev/null \
        || die "The package signature was not readable."

    if ! openssl dgst -sha256 -verify "$ZEROG_STAGING/update-public.pem" \
            -signature "$ZEROG_STAGING/tarball.sig" "$ZEROG_STAGING/$ZEROG_TARBALL" >/dev/null 2>&1; then
        # A hard stop, unlike the equivalent check in update.sh. That one refuses an update and
        # leaves a working server running; here there is no server yet, so there is nothing to
        # protect by carrying on and every reason not to install unverified code.
        die "This package did not come from 0G Nova Adventure and was NOT installed."
    fi
    say "signature ok"

    step "Unpacking"
    tar -xzf "$ZEROG_STAGING/$ZEROG_TARBALL" -C "$ZEROG_STAGING" || die "The package could not be unpacked."

    local unpacked="$ZEROG_STAGING/zerog-nova-server"
    # -f, NOT -x. The archive does not carry Unix permission bits, so everything in it arrives
    # without the execute bit set, install.sh included. What matters is that the file is THERE;
    # how it is run is the next line's business.
    [ -f "$unpacked/install.sh" ] || die "The package does not contain install.sh - it may be corrupt."
    say "ok"

    # Handing over rather than reimplementing. Everything past this point - the database, the
    # service user, the systemd units, the closing instructions - is install.sh's job and is the
    # same whether somebody got here by this one-liner or by untarring the download themselves.
    # Any arguments given to this script were meant for it.
    # "bash install.sh", not "./install.sh". For the reason above there is no execute bit to rely
    # on, and naming the interpreter works whether or not one is set.
    cd "$unpacked"
    bash ./install.sh "$@"

    # ---------------------------------------------------------------- and start it
    #
    # install.sh deliberately stops short of starting anything, and that is right for somebody who
    # untarred the download themselves: they are mid-session on a machine they administer, they
    # were about to set a server name, and a service that starts before it is configured just
    # fills the journal with the same complaint.
    #
    # THE ONE-LINER MAKES A DIFFERENT PROMISE. Someone who pastes one command expects to end up
    # with a running server, not with homework - so the last step is taken here rather than being
    # left as an instruction. Nothing is lost by starting: with no ZEROG_SERVER_NAME set the
    # server is simply private, which is the sensible default anyway, and naming it later is one
    # edit and one restart.
    #
    # ONLY IF THERE ARE UNITS TO START. "--no-systemd" is a supported way to run install.sh, and
    # so is a machine without systemd at all; in both cases install.sh has already said what it
    # did and there is nothing here to add.
    if [ -f /etc/systemd/system/zerog-server.service ] && command -v systemctl >/dev/null 2>&1; then
        step "Starting"

        if ! systemctl start zerog-server zerog-gateway; then
            say "The services did not start. The install itself is fine - see what happened with:"
            say "  sudo journalctl -u zerog-server -n 50 --no-pager"
            return 1
        fi

        # The first start runs update.sh as an ExecStartPre and may pull a newer release before the
        # server itself is launched, so "started" is not the same as "up" for a few seconds. This
        # waits for the state to settle rather than reporting whatever is true a millisecond later.
        local waited=0
        while [ "$waited" -lt 30 ]; do
            if [ "$(systemctl is-active zerog-server)" = "active" ] \
            && [ "$(systemctl is-active zerog-gateway)" = "active" ]; then
                break
            fi
            sleep 2
            waited=$((waited + 2))
        done

        if [ "$(systemctl is-active zerog-server)" = "active" ] \
        && [ "$(systemctl is-active zerog-gateway)" = "active" ]; then
            say "zerog-server and zerog-gateway are running, and will start again at boot."
            printf '\n  Players connect to  ws://%s:8081/ws\n\n' "$(hostname -I 2>/dev/null | awk '{print $1}')"
        else
            say "Started, but not healthy yet. Check with:"
            say "  sudo systemctl status zerog-server --no-pager"
            say "  sudo journalctl -u zerog-server -n 50 --no-pager"
            return 1
        fi
    fi
}

zerog_main "$@"
