#!/bin/bash
# Written by Wren Linux extras/whonix.sh: updates this VM, then powers it off.
#
# Runs as root at boot, before the desktop, and only when the host asks: `whonix
# update` starts the VM with a note in its firmware (data/whonix-update.service
# won't start without it). What it does shows on the VM's own screen. Its last
# line on the serial port is how the host learns the result.
#
# Both VMs: apt. The Workstation: Tor Browser too, which apt doesn't update
# (data/whonix-update-browser, as the user, never as root).
export PATH=/usr/sbin:/usr/bin:/sbin:/bin

# As Whonix's own apt-get-noninteractive sets them, config files too: the maintainer's file comes
# in (--force-confnew), so a security fix in one reaches the VM. The old way, keeping a changed file,
# kept a fixed file out silently. Daemons aren't restarted during the upgrade (the VM powers off
# after it). Not that command itself: Wren Linux puts one edit back afterwards, below.
export DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical DEBCONF_NOWARNINGS=yes
export APT_LISTCHANGES_FRONTEND=text NEEDRESTART_MODE=l
export POLICYRCD=/usr/share/helper-scripts/policy-rc.d
take_config=(--force-confdef --force-confnew)

# The one packaged file Wren Linux edits: the Gateway's /etc/resolv.conf.whonix, with the nameserver
# line on, for bridges given as names (extras/whonix.sh). Whether it was on is written down before
# the upgrade, in a file that outlives a power cut, and the line is put back after: the
# maintainer's file has it off. Lost, this fails closed: Tor just can't resolve a bridge's name.
# If the line can't be put back (Whonix reworded it), the note stays and the update says so
# (finish), rather than leave bridges given as names failing with nothing said.
RESOLV=/etc/resolv.conf.whonix
NS_NOTE=/var/lib/wren-whonix-nameserver
ns_lost=false
note_nameserver() {
    if grep -qx 'nameserver 10\.0\.2\.3' "$RESOLV" 2>/dev/null; then : >"$NS_NOTE"; fi
}
put_nameserver_back() {
    [ -e "$NS_NOTE" ] || return 0
    if [ -f "$RESOLV" ]; then sed -i 's/^#nameserver 10\.0\.2\.3$/nameserver 10.0.2.3/' "$RESOLV"; fi
    if grep -qx 'nameserver 10\.0\.2\.3' "$RESOLV" 2>/dev/null; then
        rm -f "$NS_NOTE"
        ns_lost=false
    else
        ns_lost=true
    fi
}

outs=()
for out in /dev/tty1 /dev/ttyS0; do
    if [ -w "$out" ]; then outs+=("$out"); fi
done
exec > >(tee -a "${outs[@]}") 2>&1 </dev/null

say() { printf '\n==> %s\n' "$*"; }

# result WORDS: the last thing this says. The host counts the update as done only
# for the exact line "WREN-RESULT: ok". (The serial port can lose what is still
# waiting when the power goes: hence the sync and the pause.)
result() {
    printf '\nWREN-RESULT: %s\n' "$1"
    sync
    sleep "${WREN_UPDATE_PAUSE:-3}"
    exit 0
}

# finish: ok, unless the nameserver line couldn't be put back. (The host shows only letters,
# digits and :.,_- of this, and 200 of them.)
finish() {
    if $ns_lost; then
        say "Updated, but the line \"nameserver 10.0.2.3\" in $RESOLV couldn't be turned back on: Whonix changed that file. Bridges given as names need it: put it in by hand here (sudo nano $RESOLV), or give bridges as addresses."
        result "failed: updated, but the nameserver line for bridges given as names is off: Whonix changed resolv.conf.whonix. See the Gateway screen"
    fi
    result ok
}

# Tor may still be connecting. Error-Mode (Whonix sets it too): a server out of
# reach is a failure, not an empty success, so an update that never connected
# isn't counted as one.
fetch_lists() {
    local tries=0 most=${WREN_UPDATE_TRIES:-40}
    until apt-get update -q -o APT::Update::Error-Mode=any; do
        tries=$((tries + 1))
        [ "$tries" -lt "$most" ] || return 1
        say "No connection yet; trying again in 30 seconds ($tries of $most)"
        sleep "${WREN_UPDATE_WAIT:-30}"
    done
}

say "Updating this VM. Please leave this running: it powers off by itself."
# (an update cut off by a power cut is finished first)
note_nameserver
dpkg "${take_config[@]}" --configure -a || true
put_nameserver_back
fetch_lists || result "failed: no connection to the update servers"
# (--yes never removes an essential package: apt stops instead)
note_nameserver
if ! apt-get -y -q -o "Dpkg::Options::=${take_config[0]}" -o "Dpkg::Options::=${take_config[1]}" full-upgrade; then
    put_nameserver_back
    result "failed: apt"
fi
put_nameserver_back

# Only the Workstation has Tor Browser
if [ ! -x /usr/bin/update-torbrowser ]; then finish; fi

say "Tor Browser"
TB_USER=user
# Before the first login there is no home folder yet
if [ ! -d "/home/$TB_USER" ]; then mkhomedir_helper "$TB_USER" 027 || result "failed: no home folder for $TB_USER"; fi
# -l: a real login session, which update-torbrowser's check that Tor is up needs
# (Whonix's privleap). -s: this script is the "shell", so what is in ~/.profile doesn't
# run here: a hacked browser could have written it, and this output is what the host reads.
if runuser -l "$TB_USER" -s /usr/local/libexec/wren-update-browser; then
    finish
fi
result "failed: Tor Browser"
