#!/bin/sh
# configuration-file reader utility
# Copyright (C) 1999-2002  Henry Spencer.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# RCSID $Id: _confread.in,v 1.75 2004/06/27 20:32:53 mcr Exp $
#
# Extract configuration info from /etc/ipsec.conf, repackage as assignments
# to shell variables or tab-delimited fields.  Success or failure is reported
# inline, as extra data, due to the vagaries of shell backquote handling.
# In the absence of --varprefix, output is tab-separated fields, like:
#	=	sectionname
#	:	parameter	value
#	!	status (empty for success, else complaint)
# In the presence of (say) "--varprefix IPSEC", output is like:
#	IPSEC_confreadsection="sectionname"
#	IPSECparameter="value"
#	IPSEC_confreadstatus="status" (same empty/complaint convention)
#
# The "--search parametername" option inverts the search:  instead of
# yielding the parameters of the specified name(s), it yields the names
# of sections with parameter <parametername> having (one of) the
# specified value(s).  In this case, --varprefix output is a list of
# names in the <prefix>_confreadnames variable.  Search values with
# white space in them are currently not handled properly.
#
# Typical usage:
# eval `ipsec _confread --varprefix IPSEC --type config setup`
# if test " $IPSEC_confreadstatus" != " "
# then
#	echo "$0: $IPSEC_confreadstatus -- aborting" 2>&1
#	exit 1
# fi

# absent default config file treated as empty
config=${IPSEC_CONFS-/etc/ipsec}/ipsec.conf
if test ! -f "$config" ; then config=/dev/null ; fi

include=yes
type=conn
fieldfmt=yes
prefix=
search=
export=0
version=
optional=0
me="ipsec _confread"

for dummy
do
	case "$1" in
	--config)	config="$2" ; shift	;;
	--noinclude)	include=		;;
	--type)		type="$2" ; shift	;;
	--varprefix)	fieldfmt=
			prefix="$2"
			shift			;;
	--export)	export=1		;;
	--search)	search="$2" ; shift	;;
	--version)	echo "$me $IPSEC_VERSION" ; exit 0	;;
	--optional)	optional=1		;;
	--)		shift ; break		;;
	-*)		echo "$0: unknown option \`$1'" >&2 ; exit 2	;;
	*)		break			;;
	esac
	shift
done

if test "$include"
then
	ipsec _include --inband $config
else
	cat $config
fi |
awk 'BEGIN {
	type = "'"$type"'"
	names = "'"$*"'"
	prefix = "'"$prefix"'"
	export = "'"$export"'"
	optional = 0 + '"$optional"'
	myid = "'"$IPSECmyid"'"
	search = "'"$search"'"
	searching = 0
	if (search != "") {
		searching = 1
		searchpat = search "[ \t]*=[ \t]*"
	}
	fieldfmt = 0
	if ("'"$fieldfmt"'" == "yes")
		fieldfmt = 1
	including = 0
	if ("'"$include"'" == "yes")
		including = 1
	filename = "'"$config"'"
	lineno = 0
	originalfilename = filename
	if (fieldfmt)
		bq = eq = "\""
	else
		bq = eq = "\\\""
	failed = 0
	insection = 0
	indefault = 0
	outputting = 0
	sawnondefault = 0
	OFS = "\t"
	o_status = "!"
	o_parm = ":"
	o_section = "="
	o_names = "%"
	o_end = "."
	n = split(names, na, " ")
	if (n == 0)
		fail("no section names supplied")
	for (i = 1; i <= n; i++) {
		if (na[i] in wanted)
			fail("section " bq na[i] eq " requested more than once")
		wanted[na[i]] = 1
		pending[na[i]] = 1
		if (!searching && na[i] !~ /^[a-zA-Z][a-zA-Z0-9._-]*$/)
			fail("invalid section name " bq na[i] eq)
	}

	good = "also alsoflip type auto authby _plutodevel connaddrfamily forceencaps"
	left = " left leftsubnet leftnexthop leftfirewall leftupdown"
	akey = " keyexchange auth pfs keylife rekey rekeymargin rekeyfuzz"
        akey = akey " dpddelay dpdtimeout dpdaction"
	akey = akey " xauth"
	akey = akey " compress"
	akey = akey " keyingtries ikelifetime disablearrivalcheck failureshunt ike"
	mkey = " spibase spi esp espenckey espauthkey espreplay_window"
	left = left " leftespenckey leftespauthkey leftahkey"
	left = left " leftespspi leftahspi leftid leftrsasigkey leftrsasigkey2"
	left = left " leftcert leftcerttype leftca leftsubnetwithin leftprotoport leftgroups"
	left = left " leftxauthclient leftxauthserver leftsendcert"
	left = left " leftmodecfgclient leftmodecfgserver"
	left = left " leftsourceip"
	mkey = mkey " ah ahkey ahreplay_window"
	right = left
	gsub(/left/, "right", right)
	n = split(good left right akey mkey, g)
	for (i = 1; i <= n; i++)
		goodnames["conn:" g[i]] = 1

	good = "also interfaces forwardcontrol myid"
	good = good " syslog klipsdebug plutodebug plutoopts plutostderrlog"
	good = good " plutorestartoncrash"
	good = good " dumpdir manualstart pluto"
	good = good " plutowait prepluto postpluto"
	good = good " fragicmp hidetos rp_filter uniqueids"
	good = good " overridemtu"
	good = good " nocrsend strictcrlpolicy crlcheckinterval ocspuri"
	good = good " nat_traversal keep_alive force_keepalive"
	good = good " disable_port_floating virtual_private"	
	n = split(good, g)
	for (i = 1; i <= n; i++)
		goodnames["config:" g[i]] = 1

	goodtypes["conn"] = 1
	goodtypes["config"] = 1

	badchars = ""
	for (i = 1; i < 32; i++)
		badchars = badchars sprintf("%c", i)
	for (i = 127; i < 128+32; i++)
		badchars = badchars sprintf("%c", i)
	badchar = "[" badchars "]"

	# if searching, seen is set of sectionnames which match
	# if not searching, seen is set of parameter names found
	seen[""] = ""
	default[""] = ""
	usesdefault[""] = ""
	orientation = 1
}



function output(code, v1, v2) {
	if (code == o_parm) {
		if (v2 == "")		# suppress empty parameters
			return
		if (privatename(v1))	# and private ones
			return
		if (v2 ~ badchar)
			fail("parameter value " bq v2 eq " contains unprintable character")
	}

	if (fieldfmt) {
		print code, v1, v2
		return
	}

	if (code == o_status) {
		v2 = v1
		v1 = "_confreadstatus"
	} else if (code == o_section) {
		v2 = v1
		v1 = "_confreadsection"
	} else if (code == o_names) {
		v2 = v1
		v1 = "_confreadnames"
	} else if (code != o_parm)
		return		# currently no variable version of o_end

	print prefix v1 "=\"" v2 "\""
	if (export)
		print "export " prefix v1
}
function searchfound(sectionname,    n, i, reflist) {
	# a hit in x is a hit in everybody who refers to x too
	n = split(refsto[sectionname], reflist, ";")
	for (i = 1; i <= n; i++)
		if (reflist[i] in seen)
			fail("duplicated parameter " bq search eq)
		else
			seen[reflist[i]] = 1
	seen[sectionname] = 1
}
function fail(msg) {
	output(o_status, ("(" filename ", line " lineno ") " msg))
	failed = 1
	while ((getline junk) > 0)
		continue
	exit
}
function badname(n) {
	if ((type ":" n) in goodnames)
		return 0
	if (privatename(n))
		return 0
	return 1
}
function privatename(n) {
	if (n ~ /^[xX][-_]/)
		return 1
	return 0
}
function orient(n) {
	if (orientation == -1) {
		if (n ~ /left/)
			gsub(/left/, "right", n)
		else if (n ~ /right/)
			gsub(/right/, "left", n)
	}
	return n
}
# in searching, referencing is transitive:  xyz->from->to
function chainref(from, to,    i, reflist, listnum) {
	if (from in refsto) {
		listnum = split(refsto[from], reflist, ";")
		for (i = 1; i <= listnum; i++)
			chainref(reflist[i], to)
	}
	if (to in refsto)
		refsto[to] = refsto[to] ";" from
	else
		refsto[to] = from
}
function jam(sn, au) {
	if (searching) {
		if (!(sn in usesdefault)) {
			usesdefault[sn] = 0
			if ("auto=" ~ searchpat && au in wanted)
				searchfound(sn)
		}
	} else {
		if (sn in pending) {
			delete pending[sn]
			orientation = wanted[sn]
			tag = bq type " " sn eq
			outputting = 1
			insection = 1
			output(o_section, sn)

			# do not accept anything from conn %default
			for (i in default)
				delete default[i]

			output(o_parm, orient("left"), "%defaultroute")
			output(o_parm, orient("leftid"), "%myid")
			output(o_parm, "leftrsasigkey", "%dnsondemand")
			output(o_parm, "rightrsasigkey", "%dnsondemand")
			output(o_parm, "auto", au)
			return 1
		}
	}
	return 0
}

# start of rules

{
	lineno++
	# lineno is now the number of this line

	# we must remember indentation because comment stripping loses it
	exdented = $0 !~ /^[ \t]/
	sub(/^[ \t]+/, "")		# get rid of leading white space
	sub(/[ \t]+$/, "")		# get rid of trailing white space
}
including && $0 ~ /^#[<>:]/ {
	# _include control line
	if ($1 ~ /^#[<>]$/) {
		filename = $2
		lineno = $3 - 1
	} else if ($0 ~ /^#:/) {
		msg = substr($0, 3)
		gsub(/"/, "\\\"", msg)
		fail(msg)
	}
	next
}
exdented {
	# any non-leading-white-space line is a section end
	### but not the end of relevant stuff, might be also= sections later
	###if (insection && !indefault && !searching && outputting)
	###	output(o_end)
	insection = 0
	indefault = 0
	outputting = 0
}
/[ \t]#/ {
	# strip trailing comments including the leading whitespace
	# tricky because we must respect quotes
	q = 0
	for (i = 1; i <= NF; i++) {
		if ($i ~ /^#/ && q % 2 == 0) {
			NF = i - 1;
			break
		}
		# using $i in gsub loses whitespace?!?
		junk = $i
		q += gsub(/"/, "&", junk)
	}
}
$0 == "" || $0 ~ /^#/ {
	# empty lines and comments are ignored
	next
}
exdented && NF != 2 {
	# bad section header
	fail("section header " bq $0 eq " has wrong number of fields (" NF ")")
}
exdented && $1 == "version" {
	version = $2 + 0
	if (version < 2.0 || 2.0 < version)
		fail("we only support version 2.0 ipsec.conf files, not " bq version eq)
	next
}
version == "" {
	fail("we only support version 2 ipsec.conf files")
}
exdented && !($1 in goodtypes) {
	# unknown section type
	fail("section type " bq $1 eq " not recognized")
}
exdented && $1 != type {
	# section header, but not one we want
	insection = 1
	next
}
exdented && $1 == "config" && $2 != "setup" {
	fail("unknown config section " bq $2 eq)
}
exdented && $2 != "%default" {
	# non-default section header of our type
	sawnondefault = 1
}
exdented && searching && $2 != "%default" {
	# section header, during search
	insection = 1
	sectionname = $2
	usesdefault[sectionname] = 1		# tentatively
	next
}
exdented && !searching && $2 in wanted {
	# one of our wanted section headers
	if (!($2 in pending))
		fail("duplicate " type " section " bq $2 eq)
	delete pending[$2]
	tag = bq type " " $2 eq
	outputting = 1
	insection = 1
	orientation = wanted[$2]
	output(o_section, $2)
	next
}
exdented && $2 == "%default" {
	# relevant default section header
	if (sawnondefault)
		fail(bq $1 " %default" eq " sections must precede non-default ones")
	tag = bq type " " $2 eq
	indefault = 1
	next
}
exdented {
	# section header, but not one we want
	insection = 1
	next
}
!insection && !indefault {
	# starts with white space but not in a section... oops
	fail("parameter is not within a section")
}
searching && $0 ~ searchpat {
	# search found the right parameter name
	match($0, searchpat)
	rest = substr($0, RLENGTH+1)
	if (rest ~ /^".*"$/)
		rest = substr(rest, 2, length(rest)-2)
	if (!indefault) {
		if (!usesdefault[sectionname])
			fail("duplicated parameter " bq search eq)
		usesdefault[sectionname] = 0
	} else if (search in default)
		fail("duplicated parameter " bq search eq)
	if (rest in wanted) {	# a hit
		if (indefault)
			default[search] = rest
		else
			searchfound(sectionname)
	} else {
		# rather a kludge, but must check this somewhere
		if (search == "auto" && rest !~ /^(add|route|start|ignore|manual)$/)
			fail("illegal auto value " bq rest eq)
	}
	next
}
!searching && !outputting && !indefault {
	# uninteresting line
	next
}
$0 ~ /"/ && $0 !~ /^[^=]+=[ \t]*"[^"]*"$/ {
	if (!searching)
		fail("mismatched quotes in parameter value")
	else
		gsub(/"/, "", $0)
}
$0 !~ /^[a-zA-Z_][a-zA-Z0-9_-]*[ \t]*=/ {
	if (searching)
		next			# just ignore it
	fail("syntax error or illegal parameter name")
}
{
	sub(/[ \t]*=[ \t]*/, "=")	# get rid of white space around =
}
$0 ~ /^(also|alsoflip)=/ {
	v = orientation
	if ($0 ~ /^alsoflip/)
		v = -v;
	if (indefault)
		fail("%default section may not contain " bq "also" eq " or " bq "alsoflip" eq " parameter")
	sub(/^(also|alsoflip)=/, "")
	if ($0 !~ /^[a-zA-Z][a-zA-Z0-9._-]*$/)
		fail("invalid section name " bq $0 eq)
	if (!searching) {
		if ($0 in wanted)
			fail("section " bq $0 eq " requested more than once")
		wanted[$0] = v
		pending[$0] = 1
	} else
		chainref(sectionname, $0)
	next
}
!outputting && !indefault {
	# uninteresting line even for a search
	next
}
{
	equal = match($0, /[=]/)
	name = substr($0, 1, equal-1)
	if (badname(name))
		fail("unknown parameter name " bq name eq)
	value = substr($0, equal+1)
	if (value ~ /^"/)
		value = substr(value, 2, length(value)-2)
	else if (value ~ /[ \t]/)
		fail("white space within non-quoted parameter " bq name eq)
}
indefault {
	if (name in default)
		fail("duplicated default parameter " bq name eq)
	default[name] = value
	next
}
{
	name = orient(name)
	if (name in seen)
		fail("duplicated parameter " bq name eq)
	seen[name] = 1
	output(o_parm, name, value)
}
END {
	if (failed)
		exit 1

	# supply default conns if relevant and not found
	if (type == "conn") {
		if (jam("packetdefault", "route")) {
			output(o_parm, "type", "tunnel")
			output(o_parm, "leftsubnet", "0.0.0.0/0")
			output(o_parm, "right", "%opportunistic")
			output(o_parm, "failureshunt", "passthrough")
			output(o_parm, "keyingtries", "3")
			output(o_parm, "ikelifetime", "1h")
			output(o_parm, "keylife", "1h")
			output(o_parm, "rekey", "no")
		}
		if (jam("clear", "route")) {
			output(o_parm, "type", "passthrough")
			output(o_parm, "authby", "never")
			output(o_parm, "right", "%group")
		}
		if (jam("clear-or-private", "route")) {
			output(o_parm, "type", "passthrough")
			output(o_parm, "right", "%opportunisticgroup")
			output(o_parm, "failureshunt", "passthrough")
			output(o_parm, "keyingtries", "3")
			output(o_parm, "ikelifetime", "1h")
			output(o_parm, "keylife", "1h")
			output(o_parm, "rekey", "no")
		}
		if (jam("private-or-clear", "route")) {
			output(o_parm, "type", "tunnel")
			output(o_parm, "right", "%opportunisticgroup")
			output(o_parm, "failureshunt", "passthrough")
			output(o_parm, "keyingtries", "3")
			output(o_parm, "ikelifetime", "1h")
			output(o_parm, "keylife", "1h")
			output(o_parm, "rekey", "no")
		}
		if (jam("private", "route")) {
			output(o_parm, "type", "tunnel")
			output(o_parm, "right", "%opportunisticgroup")
			output(o_parm, "failureshunt", "drop")
			output(o_parm, "keyingtries", "3")
			output(o_parm, "ikelifetime", "1h")
			output(o_parm, "keylife", "1h")
			output(o_parm, "rekey", "no")
		}
		if (jam("block", "route")) {
			output(o_parm, "type", "reject")
			output(o_parm, "authby", "never")
			output(o_parm, "right", "%group")
		}
	}

	filename = originalfilename
	unseen = ""
	for (i in pending)
		unseen = unseen " " i
	if (!optional && !searching && unseen != "")
		fail("did not find " type " section(s) " bq substr(unseen, 2) eq)
	if (!searching) {
		for (name in default)
			if (!(name in seen))
				output(o_parm, name, default[name])
	} else {
		if (default[search] in wanted)
			for (name in usesdefault)
				if (usesdefault[name])
					seen[name] = 1
		delete seen[""]
		if (fieldfmt)
			for (name in seen)
				output(o_section, name)
		else {
			outlist = ""
			for (name in seen)
				if (outlist == "")
					outlist = name
				else
					outlist = outlist " " name
			output(o_names, outlist)
		}
	}
	output(o_status, "")
}'
