#!/bin/sh
# Copyright (c) 2007-2009 Roy Marples
# All rights reserved

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#     * Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#     * 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.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT
# OWNER 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.

RESOLVCONF="$0"
SYSCONFDIR=@SYSCONFDIR@
LIBEXECDIR=@LIBEXECDIR@
VARDIR=@VARDIR@
# Support original resolvconf configuration layout
# as well as the openresolv config file
if [ -f "$SYSCONFDIR"/resolvconf.conf ]; then
	. "$SYSCONFDIR"/resolvconf.conf
	[ -n "$state_dir" ] && VARDIR="$state_dir"
elif [ -d "$SYSCONFDIR/resolvconf" ]; then
	SYSCONFDIR="$SYSCONFDIR/resolvconf"
	if [ -f "$SYSCONFDIR"/interface-order ]; then
		interface_order="$(cat "$SYSCONFDIR"/interface-order)"
	fi
fi
IFACEDIR="$VARDIR/interfaces"
METRICDIR="$VARDIR/metrics"
PRIVATEDIR="$VARDIR/private"

: ${dynamic_order:=tap[0-9]* tun[0-9]* vpn vpn[0-9]* ppp[0-9]* ippp[0-9]*}
: ${interface_order:=lo lo[0-9]*}

error_exit()
{
	echo "$*" >&2
	exit 1
}

usage()
{
	cat <<-EOF
	Usage: ${RESOLVCONF##*/} [options]

	Inform the system about any DNS updates.

	Options:
	  -a \$INTERFACE    Add DNS information to the specified interface
	                   (DNS supplied via stdin in resolv.conf format)
	  -m metric        Give the added DNS information a metric
	  -p               Mark the interface as private
	  -d \$INTERFACE    Delete DNS information from the specified interface
	  -f               Ignore non existant interfaces
	  -I               Init the state dir
	  -u               Run updates from our current DNS information
	  -l [\$PATTERN]    Show DNS information, optionally from interfaces
	                   that match the specified pattern
	  -i [\$PATTERN]    Show interfaces that have supplied DNS information
                   optionally from interfaces that match the specified
                   pattern
	  -v [\$PATTERN]    echo NEWDOMAIN, NEWSEARCH and NEWNS variables to
	  		   the console
	  -h               Show this help cruft
	EOF
	[ -z "$1" ] && exit 0
	echo
	error_exit "$*"
}

echo_resolv()
{
	local line=
	[ -n "$1" -a -e "$IFACEDIR/$1" ] || return 1
	echo "# resolv.conf from $1"
	# Our variable maker works of the fact each resolv.conf per interface
	# is separated by blank lines.
	# So we remove them when echoing them.
	while read line; do
		[ -n "$line" ] && echo "$line"
	done < "$IFACEDIR/$1"
	echo
}

# Parse resolv.conf's and make variables
# for domain name servers, search name servers and global nameservers
parse_resolv()
{
	local line= ns= ds= search= d= n= newns=
	local new=true iface= private=false p=

	echo "DOMAINS="
	echo "SEARCH=\"$search_domains\""
	# let our subscribers know about global nameservers
	for n in $name_servers; do
		case "$n" in
		127.*|0.0.0.0|255.255.255.255|::1) :;;
		*) newns="$newns${newns:+ }$n";;
		esac
	done
	echo "NAMESERVERS=\"$newns\""
	echo "LOCALNAMESERVERS="
	newns=

	while read line; do
		case "$line" in
		"# resolv.conf from "*)
			if ${new}; then
				iface="${line#\# resolv.conf from *}"
				new=false
				if [ -e "$PRIVATEDIR/$iface" ]; then
					private=true
				else
					# Allow expansion
					cd "$IFACEDIR"
					private=false
					for p in $private_interfaces; do
						if [ "$p" = "$iface" ]; then
							private=true
							break
						fi
					done
				fi
			fi
			;;
		"nameserver "*)
			case "${line#* }" in
			127.*|0.0.0.0|255.255.255.255|::1)
				echo "LOCALNAMESERVERS=\"\$LOCALNAMESERVERS ${line#* }\""
				continue
				;;
			esac
			ns="$ns${line#* } "
			;;
		"domain "*|"search "*)
			search="${line#* }"
			;;
		*)
			[ -n "$line" ] && continue
			if [ -n "$ns" -a -n "$search" ]; then
				newns=
				for n in $ns; do
					newns="$newns${newns:+,}$n"
				done
				ds=
				for d in $search; do
					ds="$ds${ds:+ }$d:$newns"
				done
				echo "DOMAINS=\"\$DOMAINS $ds\""
			fi
			echo "SEARCH=\"\$SEARCH $search\""
			if ! $private; then
				echo "NAMESERVERS=\"\$NAMESERVERS $ns\""
			fi
			ns=
			search=
			new=true
			;;
		esac
	done
}

uniqify()
{
	local result=
	while [ -n "$1" ]; do
		case " $result " in
		*" $1 "*);;
		*) result="$result $1";;
		esac
		shift
	done
	echo "${result# *}"
}

list_resolv()
{
	[ -d "$IFACEDIR" ] || return 0

	local report=false list= retval=0 cmd="$1"
	shift

	# If we have an interface ordering list, then use that.
	# It works by just using pathname expansion in the interface directory.
	if [ -n "$1" ]; then
		list="$@"
		$force || report=true
	else
		cd "$IFACEDIR"
		for i in $interface_order; do
			[ -e "$i" ] && list="$list $i"
		done
		for i in $dynamic_order; do
			if [ -e "$i" -a ! -e "$METRICDIR/"*" $i" ]; then
				list="$list $i"
			fi
		done
		if [ -d "$METRICDIR" ]; then
			cd "$METRICDIR"
			for i in *; do
				list="$list ${i#* }"
			done
		fi
		list="$list *"
	fi

	cd "$IFACEDIR"
	for i in $(uniqify $list); do
		# Only list interfaces which we really have
		if ! [ -e "$i" ]; then
			if $report; then
				echo "No resolv.conf for interface $i" >&2
				retval=$(($retval + 1))
			fi
			continue
		fi
		
		if [ "$cmd" = i -o "$cmd" = "-i" ]; then
			printf "$i "
		else
			echo_resolv "$i"
		fi
	done
	[ "$cmd" = i -o "$cmd" = "-i" ] && echo
	return $retval
}

make_vars()
{
	eval "$(list_resolv -l "$@" | parse_resolv)"

	# Ensure that we only list each domain once
	newdomains=
	for d in $DOMAINS; do
		dn="${d%%:*}"
		case " $newdomains" in
		*" ${dn}:"*) continue;;
		esac
		newdomains="$newdomains${newdomains:+ }$dn:"
		newns=
		for nd in $DOMAINS; do
			if [ "$dn" = "${nd%%:*}" ]; then
				ns="${nd#*:}"
				while [ -n "$ns" ]; do
					case ",$newns," in
					*,${ns%%,*},*) ;;
					*) newns="$newns${newns:+,}${ns%%,*}";;
					esac
					[ "$ns" = "${ns#*,}" ] && break
					ns="${ns#*,}"
				done
			fi
		done
		newdomains="$newdomains$newns"
	done
	echo "DOMAINS='$newdomains'"
	echo "SEARCH='$(uniqify $SEARCH)'"
	echo "NAMESERVERS='$(uniqify $NAMESERVERS)'"
	echo "LOCALNAMESERVERS='$(uniqify $LOCALNAMESERVERS)'"
}

force=false
while getopts a:d:fhIilm:puv OPT; do
	case "$OPT" in
	f) force=true;;
	h) usage;;
	m) IF_METRIC="$OPTARG";;
	p) IF_PRIVATE=1;;
	'?') ;;
	*) cmd="$OPT"; iface="$OPTARG";;
	esac
done
shift $(($OPTIND - 1))
args="$iface${iface:+ }$@"

# -I inits the state dir
if [ "$cmd" = I ]; then
	if [ -d "$VARDIR" ]; then
		rm -rf "$VARDIR"/*
	fi
	exit $?
fi

# -l lists our resolv files, optionally for a specific interface
if [ "$cmd" = l -o "$cmd" = i ]; then
	list_resolv "$cmd" "$args"
	exit $?
fi

# Not normally needed, but subscribers should be able to run independently
if [ "$cmd" = v ]; then
	make_vars "$iface"
	exit $?
fi

# Test that we have valid options
if [ "$cmd" = a -o "$cmd" = d ]; then
	if [ -z "$iface" ]; then
		usage "Interface not specified"
	fi
elif [ "$cmd" != u ]; then
	[ -n "$cmd" -a "$cmd" != h ] && usage "Unknown option $cmd"
	usage
fi
if [ "$cmd" = a ]; then
	for x in '/' \\ ' ' '*'; do
		case "$iface" in
		*[$x]*) error_exit "$x not allowed in interface name";;
		esac
	done
	for x in '.' '-' '~'; do
		case "$iface" in
		[$x]*) error_exit \
			"$x not allowed at start of interface name";;
		esac
	done
	[ "$cmd" = a -a -t 0 ] && error_exit "No file given via stdin"
fi

if [ ! -d "$IFACEDIR" ]; then
	if [ ! -d "$VARDIR" ]; then
		if [ -L "$VARDIR" ]; then
			dir="$(readlink "$VARDIR")"
			# link maybe relative
			cd "${VARDIR%/*}"
			if ! mkdir -m 0755 -p "$dir"; then
				error_exit "Failed to create needed" \
					"directory $dir"
			fi
		else
			if ! mkdir -m 0755 -p "$VARDIR"; then
				error_exit "Failed to create needed" \
					"directory $VARDIR"
			fi
		fi
	fi
	mkdir -m 0755 -p "$IFACEDIR" || \
		error_exit "Failed to create needed directory $IFACEDIR"
else
	# Delete any existing information about the interface
	if [ "$cmd" = d ]; then
		cd "$IFACEDIR"
		for i in $args; do
			if [ "$cmd" = d -a ! -e "$i" ]; then
				$force && continue
				error_exit "No resolv.conf for" \
					"interface $i"
			fi
			rm -f "$i" "$METRICDIR/"*" $i" \
				"$PRIVATEDIR/$i" || exit $?
		done
	fi
fi

if [ "$cmd" = a ]; then
	# Read resolv.conf from stdin
	resolv="$(cat)"
	# If what we are given matches what we have, then do nothing
	if [ -e "$IFACEDIR/$iface" ]; then
		if [ "$(echo "$resolv")" = \
			"$(cat "$IFACEDIR/$iface")" ]
		then
			exit 0
		fi
		rm "$IFACEDIR/$iface"
	fi
	echo "$resolv" >"$IFACEDIR/$iface" || exit $?
	[ ! -d "$METRICDIR" ] && mkdir "$METRICDIR"
	rm -f "$METRICDIR/"*" $iface"
	if [ -n "$IF_METRIC" ]; then
		# Pad metric to 6 characters, so 5 is less than 10
		while [ ${#IF_METRIC} -le 6 ]; do
			IF_METRIC="0$IF_METRIC"
		done
		echo " " >"$METRICDIR/$IF_METRIC $iface"
	fi
	case "$IF_PRIVATE" in
	[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
		if [ ! -d "$PRIVATEDIR" ]; then
			[ -e "$PRIVATEDIR" ] && rm "$PRIVATEDIR"
			mkdir "$PRIVATEDIR"
		fi
		[ -d "$PRIVATEDIR" ] && echo " " >"$PRIVATEDIR/$iface"
		;;
	*)
		if [ -e "$PRIVATEDIR/$iface" ]; then
			rm -f "$PRIVATEDIR/$iface"
		fi
		;;
	esac
fi

eval "$(make_vars)"
export RESOLVCONF DOMAINS SEARCH NAMESERVERS LOCALNAMESERVERS
: ${list_resolv:=list_resolv -l}
retval=0
for script in "$LIBEXECDIR"/*; do
	if [ -f "$script" ]; then
		if [ -x "$script" ]; then
			"$script" "$cmd" "$iface"
		else
			(. "$script" "$cmd" "$iface")
		fi
		retval=$(($retval + $?))
	fi
done
exit $retval
