#!/bin/sh
# Neon Tunnel install-update: installs the update the app downloaded, as root.
#
#   install-update <absolute path of the .deb> <its sha256, lowercase hex>
#
# pkexec runs it for the app: the polkit action com.neonwg.neon_wg_client.update
# names this file (its exec.path) and asks for the administrator's password. The
# app has checked the download against the update manifest and passes the hash
# it checked. But the file sits in the user's own directory, where they, or any
# process running as them, can still change it after that check, and root must
# not install what someone else can still change. So the file is copied into a
# directory only root can enter, the COPY is checked against the hash and
# against the package name (an update is the neon-tunnel package, nothing
# else), and apt-get installs the copy. apt-get, not dpkg -i: it also brings the
# new version's dependencies.
#
# Exit status: 0 installed; 2 bad arguments; 3 the copy's sha256 is not the
# one given; 4 the copy is not a neon-tunnel package; 5 the copy could not be
# made; otherwise apt-get's own (100 on a failure). The app tells 0 from the
# rest; the log says why.
#
# Nothing is written to stdout or stderr, and stdin is not read. The package's
# postinst closes the old app during the upgrade and starts the new one, and
# the client that ran pkexec may be that old app: its stdout and stderr are then
# pipes to nobody, and a write into one would kill this script halfway through
# an upgrade. Everything goes to the log instead, from the first line on.
#
# Safe to run again: every run copies into a directory of its own and removes
# it on the way out.
set -eu
# First of all: pkexec does not reset the caller's umask, and the log directory
# and the log file below are created by this script. Under a umask of 000 they
# would be world-writable, and any local user could then plant update.log as a
# symlink for the next root run to append through. The private copy and apt-get
# set their own umask further down.
umask 022

# The paths. This script runs as root, so they are never taken from root's
# environment: NEON_APT, NEON_UPDATE_CACHE and NEON_UPDATE_LOG, seams for
# tool/linux/test_install_update.sh (absolute paths), are read only when pkexec
# is not running the script. pkexec always sets PKEXEC_UID for the program it
# runs, whatever the caller's environment held.
apt=/usr/bin/apt-get
cache=/var/cache/neon-tunnel
logfile=/var/log/neon-tunnel/update.log
if [ -z "${PKEXEC_UID:-}" ]; then
  apt="${NEON_APT:-$apt}"
  cache="${NEON_UPDATE_CACHE:-$cache}"
  logfile="${NEON_UPDATE_LOG:-$logfile}"
fi

# The test's dry run, before anything is created or redirected. It is honoured
# always: it can only make this script do less, and pkexec passes on only a
# short list of variables (the locale, TERM and the like), so no caller of the
# real one can ask for it.
if [ "${NEON_INSTALL_UPDATE_DRY_RUN:-}" = 1 ]; then
  printf 'apt=%s\ncache=%s\nlog=%s\n' "$apt" "$cache" "$logfile"
  exit 0
fi

# The log: the first thing, so that nothing below can reach the inherited
# stdout or stderr. A log that cannot be opened for appending (a read-only
# /var, a path that runs through a file, no room to create it) must not stop an
# update that would work, so then the output goes nowhere. That fallback does
# not cover a log that opens and then fails to take a write (a full disk, say):
# say below ignores that, so that the log can never change the exit status. No
# command substitution before the redirect: with stdout and stderr closed it
# could take their descriptors. The subshell: a failed redirection ends a shell
# that runs a special built-in such as ":".
mkdir -p -- "${logfile%/*}" 2>/dev/null || true
if ! ( : >>"$logfile" ) 2>/dev/null; then
  logfile=/dev/null
fi
exec </dev/null >>"$logfile" 2>&1

# Logging never fails the script: under set -e a write error here (a full disk)
# would turn exit 0, 2, 3 or 4 into 1, and the app reads the exit status.
say() { printf '%s install-update: %s\n' "$(date '+%Y-%m-%d %H:%M:%S %z')" "$*" || :; }
fail() { # fail CODE MESSAGE: the reason into the log, then out with that code
  say "$2"
  exit "$1"
}

say "started by uid ${PKEXEC_UID:-none} with $# argument(s): $*"

# ── the arguments ───────────────────────────────────────────────────────────
[ "$#" -eq 2 ] || fail 2 "expected 2 arguments (an absolute .deb path and its sha256), got $#"
deb=$1
want=$2
case "$deb" in
  /*.deb) ;;
  *) fail 2 "not an absolute path ending in .deb: $deb" ;;
esac
[ -f "$deb" ] || fail 2 "not a regular file: $deb"
# The class is spelled out: a range such as a-f can include capitals in some
# locales.
case "$want" in
  *[!0123456789abcdef]*) fail 2 "the sha256 must be lowercase hex: $want" ;;
esac
[ "${#want}" -eq 64 ] || fail 2 "the sha256 must be 64 characters, not ${#want}: $want"

# ── a copy only root can reach ──────────────────────────────────────────────
umask 077
mkdir -p -- "$cache" || fail 5 "cannot create $cache"
dir=$(mktemp -d "$cache/update.XXXXXX") || fail 5 "cannot create a private directory in $cache"
trap 'rm -rf -- "$dir"' EXIT
cp -- "$deb" "$dir/update.deb" || fail 5 "cannot copy $deb into $dir"
# apt-get and dpkg create files too (apt's package cache), and the 077 above is
# for the copy alone.
umask 022

# ── the checks, on the copy ─────────────────────────────────────────────────
got=$(sha256sum -- "$dir/update.deb") || got=""
got=${got%% *}
[ "$got" = "$want" ] || fail 3 "the copy's sha256 is ${got:-unreadable}, not $want: not installing"
pkg=$(dpkg-deb --field "$dir/update.deb" Package) || pkg=""
[ "$pkg" = neon-tunnel ] || fail 4 "the copy is the package '$pkg', not neon-tunnel: not installing"

# ── install ─────────────────────────────────────────────────────────────────
say "installing $dir/update.deb ($deb) with $apt"
code=0
# stdin is /dev/null, so a prompt about a changed /etc conffile could never be
# answered and would fail the upgrade. --force-confdef takes the package's
# default where there is one (a file the person never edited gets the new
# version); --force-confold keeps the person's own file where there is none.
DEBIAN_FRONTEND=noninteractive "$apt" install -y -o DPkg::Lock::Timeout=120 -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold "$dir/update.deb" || code=$?
say "apt-get exited with $code"
exit "$code"
