#!/bin/sh
#
# $NetBSD: postinstall,v 1.16.2.5 2003/01/26 09:54:59 jmc Exp $
#
# Copyright (c) 2002 The NetBSD Foundation, Inc.
# All rights reserved.
#
# This code is derived from software contributed to The NetBSD Foundation
# by Luke Mewburn.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
#        This product includes software developed by the NetBSD
#        Foundation, Inc. and its contributors.
# 4. Neither the name of The NetBSD Foundation nor the names of its
#    contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# postinstall
#	check for or fix configuration changes that occur
#	over time as NetBSD evolves.
#

#
# checks to add:
#	- sysctl(8) renames
#	- de* -> tlp* migration (/etc/ifconfig.de*, $ifconfig_de*,
#	  dhclient.conf, ...) ?
#	- support quiet/verbose mode ?
#	- check obsolete file lists -- need to remove non obsolete files from
#	  the sets first.
#

#
#	helper functions
#

err()
{
	exitval=$1
	shift
	echo 1>&2 "${PROGNAME}: $@"
	exit ${exitval}
}

warn()
{
	echo 1>&2 "${PROGNAME}: $@"
}

msg()
{
	echo "	$@"
}

# additem item description
#	add item to list of supported items to check/fix
#
additem()
{
	[ $# -eq 2 ] || err 2 "USAGE: additem item description"
	items="${items}${items:+ }$1"
	eval desc_$1=\"$2\"
}

# checkdir op dir mode
#	ensure dir exists, and if not, create it with the appropriate mode.
#	returns 0 if ok, 1 otherwise.
#
check_dir()
{
	[ $# -eq 3 ] || err 2 "USAGE: check_dir op dir mode"
	_cdop=$1
	_cddir=$2
	_cdmode=$3
	[ -d "${_cddir}" ] && return 0
	if [ "${_cdop}" = "check" ]; then
		msg "${_cddir} is not a directory"
		return 1
	elif ! mkdir -m "${_cdmode}" "${_cddir}" ; then
		msg "Can't create missing ${_cddir}"
		return 1
	else
		msg "Missing ${_cddir} created"
	fi
	return 0
}

# check_ids op type file id [...]
#	check if file of type "users" or "groups" contains the relevant ids
#	returns 0 if ok, 1 otherwise.
#	
check_ids()
{
	[ $# -ge 4 ] || err 2 "USAGE: checks_ids op type file id [...]"
	_op=$1
	_type=$2
	_file=$3
	shift 3
	_ids="$@"

	if [ ! -f "${_file}" ]; then
		msg "${_file} doesn't exist; can't check for missing ${_type}"
		return 1
	fi
	if [ ! -r "${_file}" ]; then
		msg "${_file} is not readable; can't check for missing ${_type}"
		return 1
	fi
	_notfixed=""
	if [ "${_op}" = "fix" ]; then
		_notfixed=${NOT_FIXED}
	fi
	_missing=$(awk -F: '
		BEGIN {
			for (x = 1; x < ARGC; x++)
				idlist[ARGV[x]]++
			ARGC=1
		}
		{
			found[$1]++
		}
		END {
			for (id in idlist) {
				if (! (id in found))
					print id
			}
		}
	' ${_ids} < ${_file})	|| return 1
	if [ -n "${_missing}" ]; then
		msg "Missing ${_type}${_notfixed}:" $(echo ${_missing})
		return 1
	fi
	return 0
}

# compare_dir op src dest mode file [file ...]
#	perform op ("check" or "fix") on files in src/ against dest/
#	returns 0 if ok, 1 otherwise.
#
compare_dir()
{
	[ $# -ge 5 ] || err 2 "USAGE: compare_dir op src dest mode file [...]"
	_op=$1
	_src=$2
	_dest=$3
	_mode=$4
	shift 4
	_files="$@"

	if [ ! -d "${_src}" ]; then
		msg "${_src} is not a directory; skipping check"
		return 1
	fi
	check_dir ${_op} ${_dest} 755 || return 1

	_cmpdir_rv=0
	for f in ${_files}; do
		fs=${_src}/${f}
		fd=${_dest}/${f}
		_error=""
		if [ ! -f "${fd}" ]; then
			_error="${fd} does not exist"
		elif ! cmp -s ${fs} ${fd} ; then
			_error="${fs} != ${fd}"
		else
			continue
		fi
		if [ "${_op}" = "check" ]; then
			msg ${_error}
			_cmpdir_rv=1
		elif ! cp -f ${fs} ${fd}; then
			msg "Can't copy ${fs} to ${fd}"
			_cmpdir_rv=1
		elif ! chmod ${_mode} ${fd}; then
			msg "Can't change mode of ${fd} to ${_mode}"
			_cmpdir_rv=1
		else
			msg "Copied ${fs} to ${fd}"
		fi
	done
	return ${_cmpdir_rv}
}

# move_file op src dest --
#	check (op == "check") or move (op != "check") from src to dest.
#	returns 0 if ok, 1 otherwise.
#
move_file()
{
	[ $# -eq 3 ] || err 2 "USAGE: move_file op src dest"
	_fm_op=$1
	_fm_src=$2
	_fm_dest=$3

	if [ -f "${_fm_src}" -a ! -f "${_fm_dest}" ]; then
		if [ "${_fm_op}" = "check" ]; then
			msg "Move ${_fm_src} to ${_fm_dest}"
			return 1
		fi
		if ! mv ${_fm_src} ${_fm_dest}; then
			msg "Can't move ${_fm_src} to ${_fm_dest}"
			return 1
		fi
		msg "Moved ${_fm_src} to ${_fm_dest}"
	fi
	return 0
}

# rcconf_is_set op name var --
#	load the rcconf for name, and check if obsolete rc.conf(5) variable
#	var is defined or not.
#	returns 0 if defined (even to ""), otherwise 1.
#
rcconf_is_set()
{
	[ $# -eq 3 ] || err 2 "USAGE: rcconf_is_set op name var"
	_rcis_op=$1
	_rcis_name=$2
	_rcis_var=$3
	_rcis_notfixed=""
	if [ "${_rcis_op}" = "fix" ]; then
		_rcis_notfixed=${NOT_FIXED}
	fi
	(
		for f in \
		    ${DEST_DIR}/etc/rc.conf \
		    ${DEST_DIR}/etc/rc.conf.d/${_rcis_name}; do
			[ -f "${f}" ] && . "${f}";
		done
		if eval "[ -n \"\${${_rcis_var}}\" \
			    -o \"\${${_rcis_var}-UNSET}\" != \"UNSET\" ]"; then
			msg \
	"Obsolete rc.conf(5) variable '\$${_rcis_var}' found.${_rcis_notfixed}"
			exit 0
		else
			exit 1
		fi
	)
}


#
#	items
#	-----
#

#
#	rc
#
additem postinstall "/etc/postinstall being up to date"
do_postinstall()
{
	[ -n "$1" ] || err 2 "USAGE: do_postinstall  fix|check"
	op=$1
	failed=0

	compare_dir ${op} ${SRC_DIR}/etc ${DEST_DIR}/etc 555 \
		postinstall
	failed=$(( ${failed} + $? ))

	return ${failed}
}

#
#	defaults
#
additem defaults "/etc/defaults/ being up to date"
do_defaults()
{
	[ -n "$1" ] || err 2 "USAGE: do_defaults  fix|check"

	compare_dir $1 ${SRC_DIR}/etc/defaults ${DEST_DIR}/etc/defaults 444 \
		daily.conf monthly.conf rc.conf security.conf weekly.conf
}

#
#	mtree
#
additem mtree "/etc/mtree/ being up to date"
do_mtree()
{
	[ -n "$1" ] || err 2 "USAGE: do_mtree  fix|check"

	compare_dir $1 ${SRC_DIR}/etc/mtree ${DEST_DIR}/etc/mtree 444 \
		NetBSD.dist special
}

#
#	gid
#
additem gid "required GIDs"
do_gid()
{
	[ -n "$1" ] || err 2 "USAGE: do_gid  fix|check"

	check_ids $1 groups "${DEST_DIR}/etc/group" \
	    named ntpd sshd
}

#
#	uid
#
additem uid "required UIDs"
do_uid()
{
	[ -n "$1" ] || err 2 "USAGE: do_uid  fix|check"

	check_ids $1 users "${DEST_DIR}/etc/master.passwd" \
	    named ntpd sshd
}


#
#	periodic
#
additem periodic "/etc/{daily,weekly,monthly,security} being up to date"
do_periodic()
{
	[ -n "$1" ] || err 2 "USAGE: do_periodic  fix|check"

	compare_dir $1 ${SRC_DIR}/etc ${DEST_DIR}/etc 644 \
		daily weekly monthly security
}

#
#	rc
#
additem rc "/etc/rc* and /etc/rc.d/ being up to date"
do_rc()
{
	[ -n "$1" ] || err 2 "USAGE: do_rc  fix|check"
	op=$1
	failed=0

	compare_dir ${op} ${SRC_DIR}/etc ${DEST_DIR}/etc 644 \
		rc rc.subr rc.shutdown
	failed=$(( ${failed} + $? ))

	compare_dir ${op} ${SRC_DIR}/etc/rc.d ${DEST_DIR}/etc/rc.d 555 \
		DAEMON LOGIN NETWORKING SERVERS accounting altqd amd \
		apmd bootparams bootconf.sh ccd cleartmp cron \
		dhclient dhcpd dhcrelay dmesg downinterfaces fsck \
		ifwatchd inetd ipfilter ipfs ipmon ipnat ipsec isdnd \
		kdc ldconfig lkm1 lkm2 lkm3 local lpd mopd motd \
		mountall mountcritlocal mountcritremote mountd moused \
		mrouted named ndbootd network newsyslog nfsd \
		nfslocking ntpd ntpdate poffd postfix ppp pwcheck \
		quota racoon rpcbind raidframe raidframeparity rarpd rbootd \
		root route6d routed rtadvd rtsold rwho savecore \
		screenblank sendmail securelevel sshd swap1 swap2 \
		sysdb sysctl syslogd timed ttys virecover wscons xdm \
		xfs ypbind yppasswdd ypserv
	failed=$(( ${failed} + $? ))

		# check for obsolete rc.d files
	for f in NETWORK fsck.sh kerberos nfsiod servers systemfs \
	    daemon gated login portmap sunndd xntpd; do
		fd=${DEST_DIR}/etc/rc.d/${f}
		[ ! -e "${fd}" ] && continue
		if [ "${op}" = "check" ]; then
			msg "Remove obsolete ${fd}"
			failed=1
		elif ! rm ${fd}; then
			msg "Can't remove obsolete ${fd}"
			failed=1
		else
			msg "Removed obsolete ${fd}"
		fi
	done

		# check for obsolete rc.conf(5) variables
	set --	amd amd_master \
		mountcritlocal critical_filesystems_beforenet \
		mountcritremote critical_filesystems \
		network ip6forwarding \
		sysctl defcorename \
		sysctl nfsiod_flags
	while [ $# -gt 1 ]; do
		if rcconf_is_set ${op} $1 $2; then
			failed=1
		fi
		shift 2
	done

	return ${failed}
}

#
#	ssh
#
additem ssh "ssh configuration update"
do_ssh()
{
	[ -n "$1" ] || err 2 "USAGE: do_ssh  fix|check"
	op=$1

	failed=0
	_etcssh=${DEST_DIR}/etc/ssh
	if ! check_dir ${op} ${_etcssh} 755; then
		failed=1
	fi

	if [ ${failed} -eq 0 ]; then
		for f in \
			    ssh_known_hosts ssh_known_hosts2 \
			    ssh_host_dsa_key ssh_host_dsa_key.pub \
			    ssh_host_rsa_key ssh_host_rsa_key.pub \
			    ssh_host_key ssh_host_key.pub \
		    ; do
			if ! move_file ${op} \
			    ${DEST_DIR}/etc/${f} ${_etcssh}/${f} ;
			then
				failed=1
			fi
		done
		for f in sshd.conf ssh.conf ; do
				# /etc/ssh/ssh{,d}.conf -> ssh{,d}_config
				#
			if ! move_file ${op} \
			    ${_etcssh}/${f} ${_etcssh}/${f%.conf}_config ;
			then
				failed=1
			fi
				# /etc/ssh{,d}.conf -> /etc/ssh/ssh{,d}_config
				#
			if ! move_file ${op} \
			    ${DEST_DIR}/etc/${f} ${_etcssh}/${f%.conf}_config ;
			then
				failed=1
			fi
		done
	fi

	sshdconf=""
	for f in \
	    ${_etcssh}/sshd_config \
	    ${_etcssh}/sshd.conf \
	    ${DEST_DIR}/etc/sshd.conf ; do
		if [ -f "${f}" ]; then
			sshdconf=${f}
			break;
		fi
	done
	if [ -n "${sshdconf}" ]; then
		awk '
			$1 ~ /^[Hh][Oo][Ss][Tt][Kk][Ee][Yy]$/ &&
			$2 ~ /^\/etc\/+ssh_host(_[dr]sa)?_key$/ {
				sub(/\/etc\/+/, "/etc/ssh/");
			}
			{ print }
		' < ${sshdconf} > ${SCRATCHDIR}/sshd_config
		if ! cmp -s ${sshdconf} ${SCRATCHDIR}/sshd_config; then
			diff ${sshdconf} ${SCRATCHDIR}/sshd_config > \
				${SCRATCHDIR}/sshd_config.diffs
			if [ "${op}" = "check" ]; then
				msg "${sshdconf} needs the following changes:"
				failed=1
			elif ! cp -f ${SCRATCHDIR}/sshd_config ${sshdconf}; then
				msg "${sshdconf} changes not applied:"
				failed=1
			else
				msg "${sshdconf} changes applied:"
			fi
			while read _line; do
				msg "	${_line}"
			done < ${SCRATCHDIR}/sshd_config.diffs
		fi
	fi

	dirsrc=${SRC_DIR}/crypto/dist/ssh
	diretc=${SRC_DIR}/etc
	modulidir=
	if [ -f "${dirsrc}"/moduli ]; then
		modulidir=$dirsrc
	elif [ -f "${diretc}"/moduli ]; then
		modulidir=$diretc
		msg "Checking for moduli from ${modulidir} instead of ${dirsrc}"
	else
		msg "Can't find source directory for etc/moduli"
		failed=1
	fi
	if [ -n "${modulidir}" ]; then
		if ! compare_dir ${op} ${modulidir} \
		    ${DEST_DIR}/etc 444 moduli; then
			failed=1
		fi
	fi

	if ! check_dir "${op}" "${DEST_DIR}/var/chroot/sshd" 755 ; then
		failed=1
	fi

	if rcconf_is_set ${op} sshd sshd_conf_dir ; then
		failed=1
	fi

	return ${failed}
}

#
#	wscons
#
additem wscons "wscons configuration file update"
do_wscons()
{
	[ -n "$1" ] || err 2 "USAGE: do_wscons  fix|check"
	op=$1

	[ -f ${DEST_DIR}/etc/wscons.conf ] || return 0

	failed=0
	notfixed=""
	if [ "${op}" = "fix" ]; then
		notfixed=${NOT_FIXED}
	fi
	while read _type _arg1 _rest; do
		if [ "${_type}" = "mux" -a "${_arg1}" = "1" ]; then
			msg \
    "Obsolete wscons.conf(5) entry \""${_type} ${_arg1}"\" found.${notfixed}"
			failed=1
		fi
	done < ${DEST_DIR}/etc/wscons.conf

	return ${failed}
}

#
#	makedev
#
additem makedev "/dev/MAKEDEV being up to date"
do_makedev()
{
	[ -n "$1" ] || err 2 "USAGE: do_makedev   fix|check"

	compare_dir $1 ${SRC_DIR}/etc/etc.${MACHINE} \
		${DEST_DIR}/dev 555 MAKEDEV || return 1
}

#
#	postfix
#
additem postfix "/etc/postfix/ being up to date"
do_postfix()
{
	[ -n "$1" ] || err 2 "USAGE: do_postfix  fix|check"
	op=$1
	failed=0

	dirsrc=${SRC_DIR}/gnu/dist/postfix/conf
	dirshare=${DEST_DIR}/usr/share/examples/postfix
	if [ -d "${dirsrc}" ]; then
		pfdir=$dirsrc
	elif [ -d "${dirshare}" ]; then
		pfdir=$dirshare
		msg "Checking from ${pfdir} instead of ${dirsrc}"
	else
		msg "Can't find source directory"
		return 1
	fi

	compare_dir ${op} ${pfdir} ${DEST_DIR}/etc/postfix 555 postfix-script
	failed=$(( ${failed} + $? ))
	compare_dir ${op} ${pfdir} \
	    ${DEST_DIR}/etc/postfix 444 post-install postfix-files
	failed=$(( ${failed} + $? ))

	return ${failed}
}


#
#	end of items
#	------------
#


usage()
{
	cat 1>&2 << _USAGE_
Usage: ${PROGNAME} [-s srcdir] [-d destdir] operation [item [...]]
	Perform post-installation checks and/or fixes on a system's
	configuration files.  If no items are provided, all checks
	or fixes are applied.

	Options:
	-s srcdir	Source directory to compare from.  [${SRC_DIR:-/}]
	-d destdir	Destination directory to check.    [${DEST_DIR:-/}]

	Operation may be one of:
		help	display this help
		list	list available items
		check	perform post-installation checks on items
		fix	apply fixes that 'check' determines need to be applied
		usage	display this usage
_USAGE_
	exit 1
}


list()
{
	echo "Supported items:"
	echo "  Item          Description"
	echo "  ----          -----------"
	for i in ${items}; do
		eval desc="\${desc_${i}}"
		printf "  %-12s  %s\n" "${i}" "${desc}"
	done
}


main()
{
	while getopts s:d: ch; do
	case ${ch} in
	s)
		SRC_DIR=${OPTARG} ;;
	d)
		DEST_DIR=${OPTARG} ;;
	*)
		usage ;;
	esac
	done
	shift $((${OPTIND} - 1))
	[ $# -gt 0 ] || usage

	[ -d "${SRC_DIR}" ]	|| err 1 "${SRC_DIR} is not a directory"
	[ -d "${DEST_DIR}" ]	|| err 1 "${DEST_DIR} is not a directory"

		# If directories are /, clear them, so various messages
		# don't have leading "//".   However, this requires
		# the use of ${foo:-/} to display the variables.
		#
	[ "${SRC_DIR}" = "/" ]	&& SRC_DIR=""
	[ "${DEST_DIR}" = "/" ]	&& DEST_DIR=""

	op=$1
	shift

	case "${op}" in

	usage|help)
		usage
		;;

	list)
		echo "Source directory: ${SRC_DIR:-/}"
		echo "Target directory: ${DEST_DIR:-/}"
		list
		;;

	check|fix)
		todo="$@"
		: ${todo:=${items}}

		# ensure that all supplied items are valid
		#
		for i in ${todo}; do
			eval desc=\"\${desc_${i}}\"
			[ -n "${desc}" ] || err 1 "Unsupported ${op} '"${i}"'"
		done

		# perform each check/fix
		#
		echo "Source directory: ${SRC_DIR:-/}"
		echo "Target directory: ${DEST_DIR:-/}"
		items_passed=
		items_failed=
		for i in ${todo}; do
			echo "${i} ${op}:"
			( eval do_${i} ${op} )
			if [ $? -eq 0 ]; then
				items_passed="${items_passed} ${i}"
			else
				items_failed="${items_failed} ${i}"
			fi
		done

		if [ "${op}" = "check" ]; then
			plural="checks"
		else
			plural="fixes"
		fi

		echo "${PROGNAME} ${plural} passed:${items_passed}"
		echo "${PROGNAME} ${plural} failed:${items_failed}"

		;;
		
	*)
		warn "Unknown operation '"${op}"'"
		usage
		;;

	esac
}

mkdtemp ()
{
	# Make sure we don't loop forever if mkdir will always fail.
	[ -d /tmp ] || err 1 /tmp is not a directory
	[ -w /tmp ] || err 1 /tmp is not writeable

	_base=/tmp/_postinstall.$$
	_serial=0

	while true; do
		_dir=${_base}.${_serial}
		mkdir -m 0700 ${_dir} && break
		_serial=$((${_serial} + 1))
	done
	echo ${_dir}
}

# defaults
#
PROGNAME=${0##*/}
SRC_DIR="/usr/src"
DEST_DIR="/"
: ${MACHINE:=$( uname -m )}	# assume native build if $MACHINE is not set
NOT_FIXED=" [NOT FIXED]"

SCRATCHDIR=$( mkdtemp ) || err 1 "Can't create scratch directory"
trap "/bin/rm -rf ${SCRATCHDIR} ; exit 0" 0 1 2 3 15	# EXIT HUP INT QUIT TERM

umask 022

main "$@"
exit 0
