#!/bin/sh
# ForestVPN comes back after a firmware upgrade (PRD FWU-2, ADR 0108, WP A-6).
#
# A firmware upgrade removes every package the user added, ForestVPN included, and keeps what the keep list of this package names
# (/lib/upgrade/keep.d/forestvpn, which includes this directory, the hotplug entry that starts it, the router's settings, its machine
# key, the software source line and the signing key). So afterwards the settings exist and the program does not: this installs it by
# name through the router's own package manager, which checks the repository's signature itself.
#
# It does nothing unless ALL of these hold: the program is absent, the settings exist (a router that was never set up, or whose
# owner removed ForestVPN on purpose, has none: removing the package deletes the settings, this directory and the hotplug entry),
# our source line is on the router and the key that signs it is too.
# It never: fetches a key, lowers the package manager's trust (no untrusted or forced install, no skipped signature check), runs
# without the key, writes to flash, or waits for netifd. It tries at growing intervals for about half an hour, then stops until the
# next interface comes up.
#
# To remove it: rm -r /etc/forestvpn-reinstall /etc/hotplug.d/iface/90-forestvpn-reinstall   (an upgrade then leaves the router
# without ForestVPN until it is installed again by hand).
#
# Test seams (environment): FVPN_REINSTALL_{ETC,ROM,FV,RUN,LISTS,WAITS}; every other command is found through PATH.
ETC=${FVPN_REINSTALL_ETC:-/etc}
ROM=${FVPN_REINSTALL_ROM:-/rom}
FV=${FVPN_REINSTALL_FV:-/usr/sbin/fv}
RUN=${FVPN_REINSTALL_RUN:-/var/run/forestvpn-reinstall}
LISTS=${FVPN_REINSTALL_LISTS:-/var/opkg-lists}
WAITS=${FVPN_REINSTALL_WAITS:-"20 60 120 240 480 960"}

FMT=opkg; command -v apk >/dev/null 2>&1 && FMT=apk
log() { logger -t forestvpn-reinstall "$*" 2>/dev/null; return 0; }

# our source line, the one the package installed: `src/gz forestvpn <base>/ipk/<arch>` (opkg), `<base>/apk/<arch>/packages.adb` (apk)
feed_line() {
	if [ $FMT = apk ]; then
		cat "$ETC"/apk/repositories.d/*.list 2>/dev/null | grep -E '^https?://[A-Za-z0-9._:/~-]+/apk/[A-Za-z0-9_.-]+/packages\.adb$' | head -n 1
	else
		cat "$ETC"/opkg/*.conf 2>/dev/null | sed -n 's/^src\/gz[ 	][ 	]*forestvpn[ 	][ 	]*//p' | grep -E '^https?://[A-Za-z0-9._:/~-]+/ipk/[A-Za-z0-9_.-]+$' | head -n 1
	fi
}
# the key that signs it. apk: ours is named forestvpn-*. opkg names a key by its fingerprint, so ours is a key the new firmware
# does not carry itself (a router whose /rom is missing counts every key: the package manager's own check still decides).
have_key() {
	if [ $FMT = apk ]; then
		ls "$ETC"/apk/keys/forestvpn-* >/dev/null 2>&1
	else
		for k in "$ETC"/opkg/keys/*; do
			[ -f "$k" ] && [ ! -e "$ROM/etc/opkg/keys/${k##*/}" ] && return 0
		done
		return 1
	fi
}
# 0 = there is something to do
due() {
	[ -x "$FV" ] && return 1
	[ -s "$ETC/config/forestvpn" ] || return 1
	LINE=$(feed_line); [ -n "$LINE" ] || { log "no source line of ours: not reinstalling"; return 1; }
	have_key || { log "the signing key of the source is not here: not reinstalling"; return 1; }
	return 0
}
attempt() {
	local out
	if [ $FMT = apk ]; then
		echo "$LINE" > "$RUN/repo.list"
		apk --repositories-file "$RUN/repo.list" update >"$RUN/pm.log" 2>&1 \
			&& apk --repositories-file "$RUN/repo.list" add forestvpn >>"$RUN/pm.log" 2>&1
	else
		rm -f "$LISTS/forestvpn" "$LISTS/forestvpn.sig"
		out=$(opkg update 2>&1)   # its status is the base feeds' too: ours is judged by the list it leaves
		case $out in *[Ss]ignature\ check\ failed*) log "opkg update: signature check failed"; return 1 ;; esac
		[ -s "$LISTS/forestvpn" ] || { log "opkg update: no list for our source"; return 1; }
		opkg install forestvpn >"$RUN/pm.log" 2>&1
	fi
	[ -x "$FV" ]
}

due || exit 0
mkdir -p "$RUN" 2>/dev/null
# one at a time; a lock left by a process that is gone is taken over
if ! mkdir "$RUN/lock" 2>/dev/null; then
	kill -0 "$(cat "$RUN/lock/pid" 2>/dev/null)" 2>/dev/null && exit 0
	rm -rf "$RUN/lock"; mkdir "$RUN/lock" 2>/dev/null || exit 0
fi
echo $$ > "$RUN/lock/pid"
trap 'rm -rf "$RUN/lock"' EXIT
trap 'exit 1' HUP INT TERM

for w in $WAITS; do
	sleep "$w"
	due || exit 0   # someone installed it meanwhile, or took the settings away
	if attempt; then log "reinstalled after a firmware upgrade"; exit 0; fi
	log "reinstall attempt failed"
done
log "gave up for now; the next interface that comes up tries again"
exit 0
