#!/bin/sh
# jssh device agent — one-command installer.
#
#   curl -fsSL https://get.jssh.io | sudo sh -s -- --token <ENROLLMENT_TOKEN>
#   curl -fsSL .../install.sh | sudo sh -s -- --token <TOKEN> --url https://relay.example.com
#
# This script ONLY downloads + verifies the right static binary and places it.
# The binary's own `install` subcommand then detects the init system, enrolls
# the device, writes the service unit, and starts it — one smart binary instead
# of a per-distro packaging matrix.
#
# Automation/golden-image: set JSSH_ENROLL_TOKEN (and JSSH_RELAY_URL) in the env
# instead of passing flags. Air-gapped/testing: set JSSH_AGENT_BINARY=/path to
# skip the download and install a local binary.
set -eu

# ---- defaults (env-overridable) --------------------------------------------
JSSH_DOWNLOAD_BASE="${JSSH_DOWNLOAD_BASE:-https://get.jssh.io}"
JSSH_VERSION="${JSSH_VERSION:-latest}"
JSSH_RELAY_URL="${JSSH_RELAY_URL:-https://app.jssh.io}"
JSSH_PREFIX="${JSSH_PREFIX:-/usr/local/bin}"
JSSH_AGENT_BINARY="${JSSH_AGENT_BINARY:-}"

TOKEN="${JSSH_ENROLL_TOKEN:-}"
URL="$JSSH_RELAY_URL"
TARGET="127.0.0.1:22"
NAME=""
TAGS=""
PRINT=0

say() { printf 'jssh: %s\n' "$*"; }
err() { printf 'jssh: error: %s\n' "$*" >&2; exit 1; }
# Guard value-taking options so a missing trailing value gives a friendly error
# instead of a raw `$2: unbound variable` under `set -u`. Call: needval "$1" "$#".
needval() { [ "$2" -ge 2 ] || err "$1 needs a value"; }

usage() {
  cat <<'EOF'
jssh agent installer

Usage: install.sh [options]
  --token <TOKEN>     single-use enrollment token (or env JSSH_ENROLL_TOKEN)
  --url <https://...> relay base URL (default https://app.jssh.io / env JSSH_RELAY_URL)
  --version <v>       release to install (default: latest)
  --target <host:port> local service to expose (default 127.0.0.1:22)
  --name <n>          device name (default: hostname, chosen by the relay)
  --tag <k:v>         tag to attach (repeatable)
  --prefix <dir>      install dir for the binary (default /usr/local/bin)
  --print             show the service unit instead of installing it
  -h, --help          this help
EOF
}

# ---- args (accept --opt=val and --opt val) ---------------------------------
while [ $# -gt 0 ]; do
  # Valueless flags first, so the --opt=val splitter below can't inject a phantom
  # value (`--print=1` would otherwise fall through as "unknown option: 1").
  case "$1" in
    --print|--print=*) PRINT=1; shift; continue ;;
    -h|--help)         usage; exit 0 ;;
    --*=*)             k="${1%%=*}"; v="${1#*=}"; shift; set -- "$k" "$v" "$@"; continue ;;
  esac
  case "$1" in
    --token)   needval "$1" "$#"; TOKEN="$2"; shift 2 ;;
    --url)     needval "$1" "$#"; URL="$2"; shift 2 ;;
    --version) needval "$1" "$#"; JSSH_VERSION="$2"; shift 2 ;;
    --target)  needval "$1" "$#"; TARGET="$2"; shift 2 ;;
    --name)    needval "$1" "$#"; NAME="$2"; shift 2 ;;
    --tag)     needval "$1" "$#"; TAGS="${TAGS:+$TAGS,}$2"; shift 2 ;;
    --prefix)  needval "$1" "$#"; JSSH_PREFIX="$2"; shift 2 ;;
    *)         err "unknown option: $1 (try --help)" ;;
  esac
done

# ---- platform detection ----------------------------------------------------
os="$(uname -s)"
[ "$os" = "Linux" ] || err "this installer supports Linux only (got $os). On macOS build from source: README.md"

arch="$(uname -m)"
case "$arch" in
  x86_64|amd64)  target="x86_64-unknown-linux-musl" ;;
  aarch64|arm64) target="aarch64-unknown-linux-musl" ;;
  armv7l)        target="armv7-unknown-linux-musleabihf" ;;
  *)             err "unsupported architecture: $arch" ;;
esac
say "platform: linux/$arch -> $target"

# ---- privilege check -------------------------------------------------------
am_root=0
[ "$(id -u)" = "0" ] && am_root=1
if [ "$am_root" -ne 1 ] && [ "$PRINT" -ne 1 ]; then
  err "must run as root (installs to $JSSH_PREFIX and sets up a service). Re-run with: curl -fsSL ... | sudo sh -s -- --token <TOKEN>"
fi

# ---- helpers ---------------------------------------------------------------
fetch() { # url dest
  if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2"
  elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1"
  else err "need curl or wget to download"; fi
}

verify_sha256() { # file expected-file
  # Fail CLOSED: a missing checksum or no hashing tool aborts the install, so a
  # tampered/absent .sha256 can never silently skip verification of a root daemon.
  [ -f "$2" ] || err "checksum file missing — refusing to install an unverified binary"
  exp="$(awk '{print $1}' "$2")"
  [ -n "$exp" ] || err "empty/invalid checksum file"
  if command -v sha256sum >/dev/null 2>&1; then act="$(sha256sum "$1" | awk '{print $1}')"
  elif command -v shasum >/dev/null 2>&1; then act="$(shasum -a 256 "$1" | awk '{print $1}')"
  else err "no sha256 tool (sha256sum/shasum) found — refusing to install unverified"; fi
  [ "$exp" = "$act" ] || err "checksum mismatch (expected $exp, got $act)"
  say "checksum ok"
}

# ---- obtain the binary -----------------------------------------------------
tmp="$(mktemp -d "${TMPDIR:-/tmp}/jssh-install.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT
bin="$tmp/jssh-agent"

if [ -n "$JSSH_AGENT_BINARY" ]; then
  say "using local binary: $JSSH_AGENT_BINARY"
  cp "$JSSH_AGENT_BINARY" "$bin"
else
  base="$JSSH_DOWNLOAD_BASE/$JSSH_VERSION"
  say "downloading $base/jssh-agent-$target"
  fetch "$base/jssh-agent-$target" "$bin" || err "download failed"
  fetch "$base/jssh-agent-$target.sha256" "$tmp/sum" || err "checksum download failed ($base/jssh-agent-$target.sha256)"
  verify_sha256 "$bin" "$tmp/sum"
fi
chmod 0755 "$bin"

# ---- place it --------------------------------------------------------------
dest="$JSSH_PREFIX/jssh-agent"
if [ "$PRINT" -eq 1 ] && [ "$am_root" -ne 1 ]; then
  dest="$bin" # print mode without root: just run from temp
else
  install -m 0755 "$bin" "$dest" 2>/dev/null || { mkdir -p "$JSSH_PREFIX" && cp "$bin" "$dest" && chmod 0755 "$dest"; }
  say "installed $dest ($("$dest" --version 2>/dev/null || echo '?'))"
fi

# ---- auto-update (opt-in): set JSSH_UPDATE_ROOT=<base64 root pubkey> to enable.
# Downloads the jssh-boot launcher next to the agent; `install --update-root` then
# lays out the slots, runs the service through jssh-boot, and adds a self-update
# timer. JSSH_UPDATE_BASE = where signed root/manifest/timestamp.json are served
# (defaults to this release's download dir).
if [ -n "${JSSH_UPDATE_ROOT:-}" ]; then
  [ -n "${base:-}" ] || err "auto-update needs a download base (don't combine it with JSSH_AGENT_BINARY)"
  : "${JSSH_UPDATE_BASE:=$base}"; export JSSH_UPDATE_BASE
  bootbin="$tmp/jssh-boot"
  say "downloading $base/jssh-boot-$target (auto-update launcher)"
  fetch "$base/jssh-boot-$target" "$bootbin" || err "jssh-boot download failed"
  fetch "$base/jssh-boot-$target.sha256" "$tmp/bootsum" || err "jssh-boot checksum download failed"
  verify_sha256 "$bootbin" "$tmp/bootsum"
  install -m 0755 "$bootbin" "$JSSH_PREFIX/jssh-boot" 2>/dev/null \
    || { cp "$bootbin" "$JSSH_PREFIX/jssh-boot" && chmod 0755 "$JSSH_PREFIX/jssh-boot"; }
  say "installed $JSSH_PREFIX/jssh-boot"
fi

# ---- enroll + service (handled by the binary's `install` subcommand) -------
set -- install --relay "$URL" --target "$TARGET"
[ -n "$NAME" ] && set -- "$@" --name "$NAME"
[ -n "$TAGS" ] && set -- "$@" --tags "$TAGS"
[ "$PRINT" -eq 1 ] && set -- "$@" --print
# Slot-based auto-update: --update-base-url/--update-root-pubkey come via the env
# (JSSH_UPDATE_BASE / JSSH_UPDATE_ROOT) that `install` reads through clap.
[ -n "${JSSH_UPDATE_ROOT:-}" ] && set -- "$@" --update-root "${JSSH_UPDATE_DIR:-/var/lib/jssh}"

# Pass the single-use enrollment token via the ENVIRONMENT, never on argv: argv
# is world-readable through `ps` / `/proc/<pid>/cmdline` for the life of the
# enrollment, so a local user could scrape it. `jssh-agent install` reads it from
# JSSH_ENROLL_TOKEN (clap `env`).
[ -n "$TOKEN" ] && export JSSH_ENROLL_TOKEN="$TOKEN"
say "enrolling device and installing service..."
exec "$dest" "$@"
