#!/bin/sh
#
# Copyright (c) 2013 - 2016 M:tier Ltd.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# Author: Antoine Jacoutot <antoine@mtier.org>

# ChangeLog
# https://gitlab.com/mtier/openup

########################################################################
### DO NOT EDIT THIS FILE!!!                                         ###
### User defined variables: overrides are read from /etc/openup.conf ###
########################################################################

# URL to the latest openup version
OPENUP_URL="https://stable.mtier.org/openup"

# signify(1) public key
PKG_PUBKEY_URL="https://stable.mtier.org/mtier-$(uname -r | tr -d '.')-pkg.pub"

# PKG_PATH for currently running OpenBSD release
PKG_PATH_MAIN="http://ftp.fr.openbsd.org/pub/OpenBSD/$(uname -r)/packages/$(arch -s)"

# PKG_PATH for the corresponding release stable service
PKG_PATH_UPDATE="https://stable.mtier.org/updates/$(uname -r)/$(arch -s)"

# PKG_PATH addition for the corresponding LTS release stable service
#PKG_PATH_UPDATE_LTS="https://user%domain.tld:password@stable.mtier.org/updates-lts/$(uname -r)/$(arch -s)"

# URL to the latest vuxml (vulnerabilities database)
VUXML_URL="https://stable.mtier.org/vuxml/$(uname -r | tr -d '.').xml"

# fetch command (must behave like "ftp -o"); e.g. "wget -qO"
FETCH="ftp -Vo"

# exclusion list: pkg names without version/flavor, separated by space
EXCLUDE_PKG=""

########################################################################
### End of user defined variables                                    ###
########################################################################

usage() {
	echo
	echo "Usage: ${0##*/} [-K][-S|c]" >&2
	echo
	echo "Options:"
	echo "  -K    do not check for kernel binpatches (when running non GENERIC)"
	echo "  -S    ignore binpatch/package signatures"
	echo "  -c    check/cron mode, report only (cannot be used with -S)"
	echo
	exit 1
}

pr_err() {
	echo "!!! ${@}"
}

bye_bye() {
	rm -rf ${_TMPDIR} ${_PID}
	exit 1
}

pr() {
	if [ -z "${checkrun}" ]; then
		echo "===> ${@}"
	fi
}

get_key() {
	[ -r /etc/signify/mtier-${_REL_INT}-pkg.pub ] && return

	pr "Downloading and installing public key"
	${FETCH} ${_TMPKEY} ${PKG_PUBKEY_URL} || bye_bye
	install -m0644 ${_TMPKEY} /etc/signify/mtier-${_REL_INT}-pkg.pub || bye_bye
}

check_openupd() {
	local _U

	pr "Checking for openup update"
	_U="${FETCH} - ${OPENUP_URL} | awk -F '=' '/^_OPENUP_VERSION/ { print \$2 }'"
	_U=$(eval $_U)
	if [ -z "${_U}" ]; then
		pr_err "Cannot retrieve ${OPENUP_URL}"
		pr_err "Please verify your Internet connection, proxy settings and firewall."
		bye_bye
	fi

	if [ "${_OPENUP_VERSION}" -lt "${_U}" ]; then
		pr_err "New openup release (version ${_U}) available; please update with:"
		pr_err "${FETCH} $(readlink -f $0) ${OPENUP_URL}"
		bye_bye
	fi
}

# check that we have no installed binpatches from a previous release and
# if so remove the entries manually (we don't want pkg_delete to
# modify nor error out on installed files from newer release/binpatch)
rm_old_bp() {
	local _bp
	local _BPDB=$(ls -d /var/db/binpatch/{binpatch-,}[0-9]* 2>/dev/null |grep -v ${_REL})
	local _BPPKG=$(ls -d /var/db/pkg/binpatch* 2>/dev/null |grep -v binpatch${_REL_INT})
	if [ -n "${_BPPKG}" -o -n "${_BPDB}" ]; then
		pr "Removing old release binpatch entries"
	fi
	for _bp in ${_BPPKG} ${_BPDB}; do
		rm -rf ${_bp}
	done
}

update_binpatches() {
	local _BP _b _e

	# binpatches can only be found in PKG_PATH_UPDATE{,_LTS) and we
	# want to make sure we search in all paths and don't stop at the
	# first match we find
	for i in ${PKG_PATH_UPDATE_LTS} ${PKG_PATH_UPDATE}; do
		_BP="$(pkg_info -Q binpatch${_REL_INT}-${_ARCH} | sed 's/.[^-]*$//' | sort -u)${_BP:+ ${_BP}}"
	done

	if [ -n "${_BP}" ]; then
		for _e in ${EXCLUDE_PKG}; do
			set -A _BP -- ${_BP}
			_BP="$(for _b in ${_BP[@]}; do echo ${_b} | grep -v "^${_e}$"; done)"
		done
		_BP=$(echo "${_BP}" | tr '\n' ' ')

		pr "Installing/updating binpatch(es)"
		pkg_add ${pkgopt} ${_BP} || bye_bye
	fi
}

update_pkg() {
	local _PKG _e _p

	_PKG=$(pkg_info -q | grep -v binpatch${_REL_INT}-${_ARCH})

	if [ -n "${_PKG}" ]; then
		for _e in ${EXCLUDE_PKG}; do
			set -A _PKG -- ${_PKG}
			_PKG="$(for _p in ${_PKG[@]}; do echo ${_p} | grep -v "^${_e}-.*"; done)"
		done
		_PKG=$(echo "${_PKG}" | tr '\n' ' ')

		pr "Updating package(s)"
		pkg_add -quz ${pkgopt} ${_PKG} || bye_bye
	fi
}

# this only outputs the most recent vulnerability for each matching pkg
check_vuxml() {
	local _BP_MATCH _OUTDATED _PKG_MATCH _b _e _p

	# XXX5.6 drop redirect to /dev/null when 5.6 is out of support
	${FETCH} ${_TMPVUXML} ${VUXML_URL} >/dev/null || bye_bye
	perl -pi -e 's,\$ARCH,'"${_ARCH}"',g' ${_TMPVUXML}

	_BP_MATCH="$(grep binpatch ${_TMPVUXML} | sed -e 's,<name>,,g;s,</name>,,g;s,\$ARCH,'"${_ARCH}"',g' | sort -u)"
	for _e in ${EXCLUDE_PKG}; do
		set -A _BP_MATCH -- ${_BP_MATCH}
		_BP_MATCH="$(for _b in ${_BP_MATCH[@]}; do echo ${_b} | grep -v "^${_e}$"; done)"
	done

	_PKG_MATCH=$(pkg_info -q | grep -v binpatch${_REL_INT}-${_ARCH})
	for _e in ${EXCLUDE_PKG}; do
		set -A _PKG_MATCH -- ${_PKG_MATCH}
		_PKG_MATCH="$(for _p in ${_PKG_MATCH[@]}; do echo ${_p} | grep -v "^${_e}-.*"; done)"
	done

	# set to "quirks" if empty to prevent running pkg_add against an empty pkg list
	_OUTDATED=$((pkg_add -Iqn -Dnosig -Dunsigned ${_BP_MATCH:=quirks}; pkg_add -Iqnuz -Dnosig -Dunsigned ${_PKG_MATCH:=quirks}) 2>&1 | \
		    grep '^NOT CHECKING DIGITAL SIGNATURE FOR ' | \
		    sed -e 's,^NOT CHECKING DIGITAL SIGNATURE FOR ,,g' | \
		    grep -v '^quirks-' | \
		    perl -ne '/^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ && print "$1\n"' | \
		    sort -u)

	for p in ${_OUTDATED}
	do
		echo "--- ${p} ---\n"
		echo "Available update(s): "
		# XXX how do we print only the 1st matching range in awk?
		awk "/<name>${p}<\/name>/,/<\/vuln>/" ${_TMPVUXML} | \
			sed '/<\/vuln>/,$d' | \
			sed -n -e 's/.*<range><lt>\(.*\)<\/lt><\/range>.*/\1/p' \
			       -e 's/.*<p>\(.*\)<\/p>.*/\1/p' | uniq | \
			while read l; do echo -n "${l} "; done
			echo "\n"
	done | fmt | sed '/^$/d'
}

do_i_need_to_reboot() {
	# XXX hardcoded PKG_DBDIR
	local kern_bp_time=$(stat -qf "%Um" /var/db/pkg/binpatch${_REL_INT}-${_ARCH}-kernel-*)
	local wake_up=$(sysctl -n kern.boottime)

	if [ "${wake_up}" -lt "${kern_bp_time}" ]; then
		pr_err
		pr_err "System must be rebooted after the last kernel update"
		pr_err
	fi
}

trap "bye_bye" 1 2 3 13 15

if [ "$(id -u)" -ne 0 ]; then
	pr_err "Need root privileges to run this script"
	usage
fi

if [ -f /etc/openup.conf ]; then
	if [ $(stat -f "%SMp%SLp" /etc/openup.conf) != "------" ]; then
		pr_err "Unsecure permissions on /etc/openup.conf; please run:"
		pr_err "chmod 0600 /etc/openup.conf"
		exit 1
	fi
	. /etc/openup.conf
fi

# regex taken from fw_update(1)
set -A _REL -- $(sysctl -n kern.version | sed 's/^OpenBSD \([0-9]\.[0-9]\)\([^ ]*\).*/\1 \2/;q')
_REL_INT="$(echo ${_REL[0]} | tr -d '.')"
_OPENUP_MINREL=56
_OPENUP_VERSION=21
if [ -n "${_REL[1]}" -a "${_REL[1]}" != "-stable" ]; then _badrel=1; fi
if [ "${_REL_INT}" -lt "${_OPENUP_MINREL}" ]; then _badrel=1; fi
if [ -n "${_badrel}" ]; then
	pr_err "${_REL[0]}${_REL[1]} is not a supported release"
	exit 1
fi

# XXX5.6 drop when 5.6 is out of support
if [ "${_REL_INT}" -ge 57 ]; then
	FETCH="ftp -MVo"
fi

while getopts 'KSc' arg; do
	case ${arg} in
	K)	nokrn=1 ;;
	S)	nosig=1; pkgopt="${pkgopt} -Dnosig -Dunsigned" ;;
	c)	checkrun=1 ;;
	*)	usage ;;
	esac
done
[ $# = $(($OPTIND-1)) ] || usage

[ -n "${checkrun}" -a -n "${nosig}" ] && usage

_ARCH=$(arch -s)
_PID="/var/run/${0##*/}.pid"
_TMP="${TMPDIR:=/tmp}"
_TMPDIR=$(mktemp -dp ${_TMP} .openup-XXXXXXXXXX) || exit 1
_TMPKEY="${_TMPDIR}/key"
_TMPVUXML="${_TMPDIR}/vuxml"

export PKG_PATH=${PKG_PATH_UPDATE_LTS}:${PKG_PATH_UPDATE}:${PKG_PATH_MAIN}

if [ -f ${_PID} ]; then
	pr_err "openup is already running ($(cat ${_PID})):"
	pr_err "${_PID}"
	exit 1
fi
echo $$ >${_PID}

if [ -n "${nokrn}" ]; then
	EXCLUDE_PKG="binpatch${_REL_INT}-${_ARCH}-kernel ${EXCLUDE_PKG}"
fi
if [ -n "${EXCLUDE_PKG}" ]; then
	pr "Excluded package(s)/binpatch(es): ${EXCLUDE_PKG}"
fi

check_openupd

if [ "${checkrun}" ]; then
	check_vuxml
else
	[ -z "${nosig}" ] && get_key
	rm_old_bp
	update_binpatches
	update_pkg
fi

do_i_need_to_reboot

rm -rf ${_TMPDIR}
rm ${_PID}
