#!/bin/sh
#
# Copyright (c) 2013 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>

### user defined variables
OPENUP_URL="https://stable.mtier.org/openup"
PKG_CERT_URL="https://stable.mtier.org/mtier.cert"
PKG_CERT_FPRINT="DE:29:0F:7F:B8:0E:36:5A:AF:A9:BF:E0:4E:08:C2:0F:2D:50:16:97"
PKG_PATH_MAIN="http://ftp.fr.openbsd.org/pub/OpenBSD/$(uname -r)/packages/$(arch -s)"
PKG_PATH_UPDATE="https://stable.mtier.org/updates/$(uname -r)/$(arch -s)"
VUXML_URL="https://stable.mtier.org/vuxml/$(uname -r | tr -d '.').xml"
### end of user defined variables

########################################################################
### DO NOT MODIFY ANYTHING BELOW THIS LINE!!!                        ###
########################################################################

_OPENUP_VERSION=8
_OPENUP_MINREL=53

usage() {
	echo
	echo "Usage: ${0##*/} [-K][-Sn|c]" >&2
	echo
	echo "Options:"
	echo "  -K    do not check for kernel binpatches (when running non GENERIC) - NOT YET"
	echo "  -S    do not check for package signatures"
	echo "  -n    dry-run mode, no modification is done"
	echo
	echo "  -c    check/cron mode (cannot be used with -S and/or -n)"
	exit 1
}

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

if [ "$(id -u)" -ne 0 ]; then
	pr_error "Need root privileges to run this script"
	usage
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 '.')"

if [ "${_REL[1]}" == -!(stable) -o "${_REL_INT}" -lt "${_OPENUP_MINREL}" ]; then
	pr_error "${_REL[0]}${_REL[1]} is not a supported release"
	exit 1
fi

# check that we have no installed binpatches from a previous release
_BPLIST=$(pkg_info -q | grep "^binpatch")
if [ -n "${_BPLIST}" ]; then
	_i=$(echo ${_BPLIST} | wc -w)
	_v=$(echo ${_BPLIST} | grep "^binpatch${_REL_INT}" | wc -w)
	if [ "${_i}" -ne "${_v}" ]; then
		pr_error "Binpatch(es) from a previous release installed"
		exit 1
	fi
fi
unset _BPLIST

_TMPDIR="${TMPDIR:=/tmp}"
_TMPCERT=$(mktemp -p ${_TMPDIR} .openup-pkg_cert.XXXXXXXXXX) || exit 1
_TMPSPLITCERT=$(mktemp -p ${_TMPDIR} .openup-split_cert.XXXXXXXXXX) || exit 1
_TMPVUXML=$(mktemp -p ${_TMPDIR} .openup-vuxml.XXXXXXXXXX) || exit 1

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

trap "rm -f ${_TMPDIR}/.openup-*" 1 2 3 13 15

pr_bigarrow() {
	if [ -z "${checkmode}" ]; then
		echo "===> ${@}${dryrun}"
	fi
}

pr_smallarrow() {
	if [ -z "${checkmode}" ]; then
		echo "-> ${@}"
	fi
}

get_cert() {
	local _CMD

	if [ -r /etc/ssl/pkgca.pem ]; then
		split -p '^-----BEGIN CERTIFICATE-----$' /etc/ssl/pkgca.pem ${_TMPSPLITCERT}
	fi

	for i in ${_TMPSPLITCERT}* ; do
		openssl x509 -noout -in $i -fingerprint 2>/dev/null | \
			grep -q "${PKG_CERT_FPRINT}" && return
	done

	pr_bigarrow "Downloading certificate"
	_CMD="ftp -Vo ${_TMPCERT} ${PKG_CERT_URL}"
	pr_smallarrow "${_CMD}"
	if [ -z "${dryrun}" ]; then
		eval ${_CMD} || exit 1
	fi

	pr_bigarrow "Installing certificate"
	_CMD="cat ${_TMPCERT} >>/etc/ssl/pkgca.pem"
	pr_smallarrow "${_CMD}"
	if [ -z "${dryrun}" ]; then
		eval ${_CMD} || exit 1
	fi
}

check_openupd() {
	local _CMD _N

	pr_bigarrow "Checking for openup update"
	_CMD="ftp -Vo - ${OPENUP_URL} | awk -F '=' '/^_OPENUP_VERSION/ { print \$2 }'"
	pr_smallarrow "${_CMD}"
	if [ -z "${dryrun}" ]; then
		_N=$(eval ${_CMD}) || exit 1
	fi

	if [ "${_OPENUP_VERSION}" -lt "${_N}" ]; then
		pr_error "New openup release (version ${_N}) is available; please update with:"
		pr_error "ftp -Vo $(readlink -f $0) ${OPENUP_URL}"
		pr_error "Exiting"
		exit 1
	fi
}

update_binpatches() {
	local _BP _CMD

	_BP=$(pkg_info -Q binpatch${_REL_INT}-$(arch -s) | sed 's/.[^-]*$//' | sort -u | tr '\n' ' ')

	if [ -n "$_BP" ]; then
		pr_bigarrow "Updating binpatch(es)"
		_CMD="pkg_add ${pkg_opt} ${_BP}"
		pr_smallarrow "${_CMD}"
		eval ${_CMD}
	fi
}

update_pkg() {
	local _CMD

	pr_bigarrow "Updating package(s)"
	_CMD="pkg_add -u ${pkg_opt}"
	pr_smallarrow "${_CMD}"
	eval ${_CMD}
}

# this only outputs the most recent vulnerability for each pkg
check_vuxml() {
	local _BP _BP_OUTDATED _PAT _PKG_MATCH _PKG_OUTDATED

	ftp -Vo ${_TMPVUXML} ${VUXML_URL} >/dev/null
	perl -pi -e 's,\$ARCH,'"$(arch -s)"',g' ${_TMPVUXML}

	_BP=$(pkg_info -Q binpatch${_REL_INT}-$(arch -s) | sed 's/.[^-]*$//' | sort -u | tr '\n' ' ')
	_BP_OUTDATED=$(pkg_add -qnv -D nosig ${_BP} 2>/dev/null | \
		       grep '^Adding.*(pretending)' | \
		       sort -u | \
		       sed -e 's,Adding ,,;s,(pretending),,;s,->.*,,' | \
		       perl -ne '/^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ && print "$1\n"')

	_PAT=$(pkg_info -q | perl -ne '/^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ && print "$1\n"' | \
	       while read l ; do echo -n "|$l" ; done | \
	       sed 's/^|//' | sed -e 's,++,\\\++,g')
	_PKG_MATCH=$(grep -Eo '<name>('"${_PAT}"')</name>' ${_TMPVUXML} | \
		     sort -u | sed -e 's,<name>,,g;s,</name>,,g')
	_PKG_OUTDATED=$(pkg_add -qnuv -D nosig ${_PKG_MATCH} 2>/dev/null | \
			grep '(installing)(pretending)' | \
			grep -v binpatch${_REL_INT}-$(arch -s) | \
			sed -e 's,Adding ,,;s,(pretending),,;s,->.*,,' | \
			perl -ne '/^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ && print "$1\n"')

	for p in ${_BP_OUTDATED} ${_PKG_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'
}

while getopts 'KScn' arg; do
	case ${arg} in
	K)	exclude_kernel=1 ;;
	S)	nosig=1; pkg_opt="${pkg_opt} -D nosig" ;;
	c)	checkmode=1 ;;
	n)	dryrun=" (dry run)"; pkg_opt="${pkg_opt} -n -D nosig" ;;
	*)	usage ;;
	esac
done
[ $# = $(($OPTIND-1)) ] || usage

check_openupd

if [ -z "${checkmode}" ]; then
	if [ -z "${nosig}" ]; then get_cert; fi
	update_binpatches
	update_pkg
else
	if [ "${nosig}" -o "${dryrun}" ]; then
		usage
	fi
	check_vuxml
fi

rm -f ${_TMPDIR}/.openup-*
