#!/bin/sh
#
# ident "@(#)utgmtarget.sh	1.5	08/05/08 SMI"
#
# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#

PATH=/usr/bin:/bin:/usr/sbin:/sbin
export PATH

#
# Solaris 10 Trusted Extension guard
#
ORIGIN=`/usr/bin/dirname $0`
UTIL_LIB=${ORIGIN:-/opt/SUNWut/lib}/../lib/support_lib/util_lib
#
# We're sh but util_lib is ksh, so we must run it in a separate shell
#
/bin/ksh -c ". $UTIL_LIB ; FailExecInLocalZoneOnTx ; exit 0"
TXEXITVAL=$?
if [ $TXEXITVAL -ne 0 ] ; then
	exit $TXEXITVAL
fi
unset ORIGIN UTIL_LIB TXEXITVAL
#
# Solaris 10 Trusted Extension guard


UTPROP=/etc/opt/SUNWut/basedir/lib/utprop

PROGNAME="$0"

# Write a usage synopsis to stdout.  (The caller may redirect stdout
# to cause the output to go someplace else.)
#
usage() {
	CMD=`basename $PROGNAME`
	echo "$PROGNAME: manage explicit Group Manager targets"
	echo ""
	echo "    $CMD                # show target list"
	echo "    $CMD -a <host> ...  # add hosts to target list"
	echo "    $CMD -d <host> ...  # delete hosts from target list"
	echo "    $CMD -c             # clear: delete all hosts from target list"
}

# Write all args, prefixed by the program filename, to stderr.
#
errmsg() {
	ETAG=""
	case "$1" in
		i)	ETAG=" INFO;"
			shift
			;;
		w)	ETAG=" WARNING;"
			shift
			;;
		n)	ETAG=" NOTICE;"
			shift
			;;
		e)	ETAG=" ERROR;"
			shift
			;;
	esac

	CMD=`basename $PROGNAME`
	echo "${CMD}:${ETAG}" "$@" | fmt 1>&2
}

# Keep track of mutually exclusive modes.
#
MODE=""
setMode() {
	if [ -n "$MODE" ] && [ "$MODE" != "$1" ] ; then
		errmsg e "mode '$1' conflicts with mode '$MODE'"
		return 1
	fi
	MODE="$1"
	return 0
}

# Retrieve the mode name that was established by calls to setMode().
#
getMode() {
	echo "$MODE"
}

# Replace commas in a string given on stdin with newlines on stdout
#
commasToNewlines() {
	tr ',' '\012'
}

# Replace newlines in a string given on stdin with commas on stdout
#
newlinesToCommas() {
	awk 'NR > 1 {printf ","} {printf "%s",$0} END {printf "\n"}'
}

# Always run sorts in a specific locale.  This shouldn't be necessary
# for correctness (we only use 'sort' as a way of checking the
# uniqueness of target names), we do it only for consistency.
#
csort() {
	env LANG=C sort "$@"
}


UCAZ="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
LCAZ="abcdefghijklmnopqrstuvwxyz"
DIGITS="0123456789"

# Convert a string given on stdin into lower case on stdout
#
toLower() {
	# Don't use classes or ranges because they're locale-dependent
	# and their syntax is different on Solaris and Linux.
	#
	tr "$UCAZ" "$LCAZ"
}

# Is the given string an acceptable representation of an IP address?
#
isLegalIPAddr() {
	if [ $# -ne 1 ] ; then
		errmsg e "isLegalIPAddr, unexpected arg count $#"
		return 1
	fi

	echo "$1" | awk -F. ' {
	    if (NF <= 4) {
		ipaddr = 0;
		maxtail = (256 * 256 * 256 * 256);
		for (ix = 1 ; ix <= NF ; ++ix) {
		    if (((0+$ix) != ""$ix) && ($ix != 0)) {
			exit 1;
		    }
		    if ($ix < 0) {
			exit 1;
		    }
		    if (ix == NF) {
			    max = maxtail;
		    } else {
			    max = 256;
			    maxtail = maxtail / 256
		    }
		    if ((0+$ix) >= max) {
			exit 1;
		    }
		    ipaddr = ipaddr * 256;
		    ipaddr = ipaddr + $ix;
		}
		if (ipaddr >= (224 * 256 * 256 * 256)) {
		    exit 1;
		}
		exit 0;
	    }
	    exit 1;
	} '
	return $?
}

# Do basic sanity checking on the syntax of a server name.  We don't 
# care whether the name is resolvable, this is just about syntax.
#
isLegalServerName() {
	if [ $# -ne 1 ] ; then
		errmsg e "isLegalServerName, unexpected arg count $#"
		return 1
	fi

	# Eliminate all legal chars.  If anything remains then this is
	# an unacceptable name.
	#
	RESIDUE=` echo "$1" | sed -e 's/[-\.'${UCAZ}${LCAZ}${DIGITS}']//g' `
	if [ -n "$RESIDUE" ] ; then
		return 1
	fi

	# Each label (dot-separated component) must begin with a letter or
	# digit, and at least one component must contain a character other
	# than a digit.  The final component may be empty (as would be the
	# case with a fully-rooted name).
	#
	echo "$1" | awk -F. '{
	    foundnondigit = 0;
	    for (ix = 1 ; ix <= NF ; ++ix ) {
		if (length($ix) > 0) {
		    c = substr($ix, 1, 1);
		    if (index(legalfirst, c) == 0) {
			exit 1;
		    }
		    if ((0+$ix) != $ix) {
			foundnondigit = 1;
		    }
		} else if (ix != NF) {
			exit 1;
		}
	    }
	    if (foundnondigit) {
	        exit 0;
	    }
	    exit 1;
	} ' legalfirst="$LCAZ$UCAZ$DIGITS"

	return $?
}

# Do basic sanity checking on the syntax of a specified target.  We
# don't care whether the name is resolvable or reachable, this is
# solely about syntax.
#
isLegalTarget() {
	if [ $# -ne 1 ] ; then
		errmsg e "isLegalTarget, unexpected arg count $#"
		return 1
	fi

	if isLegalIPAddr "$1" ; then 
		return 0
	fi
	if isLegalServerName "$1" ; then
		return 0
	fi
	errmsg e "improper syntax for target host '$1'"
	return 1
}

# Get the raw property value in its encoded (comma-separated) form
#
GMTARGETPROPERTY=GroupManagerTargets
getGmProp() {
	$UTPROP -k $GMTARGETPROPERTY 2>/dev/null
}

# Store a new value of the property, given as an argument in its
# encoded (comma-separated) form.
#
putGmProp() {
	$UTPROP -a -s -f $GMTARGETPROPERTY "$1"
}

# Remind the operator that an SRSS restart is needed to put the updated
# target list into effect.  The reminder goes to stderr so that it
# doesn't get mixed up with any "real" output that might have been or
# might yet be generated.
#
remindRestart() {
	errmsg n "The unicast target configuration has been updated.  A"\
		"restart of Sun Ray services must be performed to" \
		"put the new unicast target configuration into effect."
}

# Add one or more targets to the list.
#
addTargets() {
	if [ $# -eq 0 ] ; then
		errmsg e "no additional target hosts specified"
		return 1
	fi

	ADDLIST=""
	for ARG in "$@" ; do
		isLegalTarget "$ARG"
		if [ $? -ne 0 ] ; then
			return 1
		fi
		ADDLIST=`echo "$ARG" ; echo "$ADDLIST"`
	done

	# Discard the empty line and force all target names to lower
	# case, in order to simplify the detection and removal of
	# duplicates both here and later.
	#
	ADDLIST=`echo "$ADDLIST" | sed -e 's/^$//' | toLower`

	DUPS=`echo "$ADDLIST" | csort | uniq -d`
	if [ -n "$DUPS" ] ; then
		for DUP in $DUPS ; do
			errmsg w "repeated argument '$DUP' skipped"
		done
		ADDLIST=`echo "$ADDLIST" | csort -u`
	fi

	# Get the existing list and fold in the new entries.  The new list is
	# generated in sorted order but that's not required by specification.
	# It's just an implementation artifact, a side effect of the 'sort -u'
	# we use to remove duplicate entries from the list.  In principle if
	# there were no dups in the new list we could skip the 'sort -u' and
	# store the list unsorted, but somebody would eventually complain
	# about inconsistent ordering so we always apply the sort.
	#
	OLDTARGETS=`getGmProp | toLower | commasToNewlines`
	NEWLIST=`echo "$ADDLIST"; echo "$OLDTARGETS"`
	
	DUPS=`echo "$NEWLIST" | csort | uniq -d`
	if [ -n "$DUPS" ] ; then
		for DUP in $DUPS ; do
			errmsg w "new target host '$DUP' is already configured"
		done
	fi

	# See whether we actually added anything to the original list.  (Maybe
	# all of the requested adds were dups.)  If we didn't add anything then
	# there's no need to perform an update, and no need to tell the operator
	# that a restart is required to put the (unchanged) target list into
	# effect.
	#
	ADDS=`(echo "$NEWLIST" ; echo "$OLDTARGETS") | csort | uniq -u`
	if [ -z "$ADDS" ] ; then
		RET=0
	else 
		NEWLIST=`echo "$NEWLIST" | csort -u | newlinesToCommas`
		putGmProp "$NEWLIST"
		RET=$?

		if [ "$RET" -eq 0 ] ; then
			remindRestart
		fi
	fi

	return "$RET"
}

# Remove all targets.  Achieved by deleting the entire property.
#
clearTargets() {
	if [ $# -ne 0 ] ; then
		errmsg e "unexpected command line argument '$1' specified"
		return 1
	fi

	$UTPROP -d $GMTARGETPROPERTY 2>/dev/null
	RET=$?

	# If the target list was already non-existent then utprop will have
	# returned a non-zero status.  There's no need to do a restart in
	# that case.
	#
	if [ "$RET" -eq 0 ] ; then
		remindRestart
	fi

	return "$RET"
}

# Remove one or more specified targets, leaving the others intact.
#
deleteTargets() {
	if [ $# -eq 0 ] ; then
		errmsg e "no target hosts specified for deletion"
		return 1
	fi

	DELLIST=""
	for ARG in "$@" ; do
	    DELLIST=`echo "$ARG"; echo "$DELLIST"`
	done

	# Discard the empty line and force all target names to lower
	# case, in order to simplify the detection and removal of
	# duplicates both here and later.
	#
	DELLIST=`echo "$DELLIST" | sed -e 's/^$//' | toLower`

	DUPS=`echo "$DELLIST" | csort | uniq -d`
	if [ -n "$DUPS" ] ; then
		for DUP in $DUPS ; do
			errmsg w "repeated argument '$DUP' skipped"
		done
	fi

	DELLIST=`echo "$DELLIST" | csort -u`

	NEWLIST=""
	MATCHEDDELS=""
	OLDTARGETS=`getGmProp | toLower | commasToNewlines`
	for OLDTARGET in $OLDTARGETS ; do
		MATCH=0
		for DEL in $DELLIST ; do
			if [ X"$OLDTARGET" = X"$DEL" ] ; then
				MATCH=1
				MATCHEDDELS=`echo "$MATCHEDDELS"; echo "$DEL"`
				break
			fi
		done
		if [ "$MATCH" -eq 0 ] ; then
			NEWLIST="$NEWLIST","$OLDTARGET"
		fi
	done

	MATCHEDDELS=`echo "$MATCHEDDELS" | sed -e 's/^$//'`
	NEWLIST=`echo "$NEWLIST" | sed -e 's/^,//'`

	# Report any names that are in $DELLIST but not in $MATCHEDDELS.
	# These are arguments that did not match any target names in the
	# old list.
	#
	UNDELS=`(echo "$MATCHEDDELS" ; echo "$DELLIST") | csort | uniq -u`
	if [ -n "$UNDELS" ] ; then
		for UNDEL in $UNDELS ; do
			errmsg w "argument '$UNDEL' not in current target list, skipped"
		done
	fi

	# Check whether we actually removed anything from the original list.
	# (Maybe all of the requested deletions were no-matches.)  If we
	# didn't remove anything then there's no need to perform an update,
	# and no need to tell the operator that a restart is required to put
	# the (unchanged) target list into effect.
	#
	if [ -z "$MATCHEDDELS" ] ; then
		RET=0;
	elif [ -z "$NEWLIST" ] ; then # all targets were deleted
		clearTargets
		RET=$?
	else 
		putGmProp "$NEWLIST"
		RET=$?

		if [ "$RET" -eq 0 ] ; then
			remindRestart
		fi
	fi

	return "$RET"
}

# Show the existing list of targets, one per line.
#
listTargets() {
	if [ $# -ne 0 ] ; then
		errmsg e "unexpected command line argument '$1' specified"
		return 1
	fi
	
	getGmProp | commasToNewlines
	return 0
}

# Show the existing list of targets in a form that's easy for authd (or,
# potentially, other SRSS programs) to parse.  This happens to be the
# same comma-separated form that we use to hold the list in the DS so no
# reformatting is necessary.  If we ever change the DS form then this
# function would be responsible for converting it into the comma-separated
# form that authd wants.
#
progListTargets() {
	if [ $# -ne 0 ] ; then
		errmsg e "unexpected command line argument '$1' specified"
		return 1
	fi
	
	getGmProp
	return 0
}

# Main program.  Everything before here was function definitions or
# static initialisation.

while getopts 'acdxD' OPTCH ; do
	case "$OPTCH" in

		# -a, -c and -d are public options and are documented
		# in the usage message and in the man page.  -x is
		# private and undocumented, for use only by internal
		# components of SRSS.
		#
		a|c|d|x)
			setMode "$OPTCH"
			if [ $? -ne 0 ] ; then
				usage 1>&2
				exit 1
			fi
			;;

		# -D is private and undocumented, for debug purposes.
		#
		D)	set -x
			;;

		*) 	usage 1>&2
			exit 1
			;;
	esac
done

shift `expr $OPTIND - 1`

# Mode is guaranteed to be valid at this point, so an empty mode is
# not a syntax error.  It means the invoker wants the default action,
# which is to list the existing targets.
#
case `getMode` in

	a)	addTargets "$@"
		;;

	c)	clearTargets "$@"
		;;

	d)	deleteTargets "$@"
		;;

	x)	progListTargets "$@"
		;;

	*)	listTargets "$@"
		;;
esac

exit $?
