#!/bin/sh
# Copyright 1999-2010 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

# /etc/pdnsd/pdnsd.conf updater
# Written by Oldrich Jedlicka (oldium.pro@seznam.cz)

# Implementation notes:
# * The assumptions are same as for other scripts like bind and dnsmasq:
#   - We assume that we are a local dns cache - after all, why would a server
#     use resolvconf?
#   - Now that we have assumed this, we also assume that generic DHCP clients
#     will enter their domains and search domains ONLY in the "search" field
#     in their resolv.confs and VPN clients will put the domain they are for
#     into the domain field only.
#   - This allows pdnsd to forward domains for a specific VPN domain to the
#     VPN nameserver and everything else to the standard name servers.
#
# HOW-TO CONFIGURE:
#
# To get this working, you need to do only two steps
#
# 1. Create a basic configuration of /etc/pdnsd/pdnsd.conf, you can use
#    /etc/pdnsd/pdnsd.conf.example to start.
#
#    Additional configuration will be created automatically be resolvconf.
#    The generated server sections has labels starting with "resolvconf", so
#
#        DO NOT USE "resolvconf" IN YOUR LABELS!
#
#    Check if the status_ctl is set to "on", otherwise the configuration
#    will not be automatically reloaded - see sample config file.
#
#    You are free to edit automatically created server sections, but always
#    write one option per line. There are few options that are always recreated
#    and your changes in them will be lost. Here is the list (with example
#    values):
#
#        preset=on;
#        ip="192.168.0.1","192.168.0.2";
#        include=".net",".com";'
#
#    The exclude directive in "resolvconf" server section is partly recreated.
#    Known (configured) domains in the form ".<domain>" or ".<domain>." are
#    added and removed automatically, unknown domains (also those not in 
#    the format above) found in this directive are kept.
#
#    The sample configuration file /etc/pdnsd/pdnsd.conf prepared to work
#    with resolvconf would look like this:
#
#global {
#	perm_cache=2048;
#	run_as="pdnsd";
#	status_ctl = on;   # Important to enable status control
#	run_ipv4=on;
#	par_queries=2;     # How many servers are probed in parallel
#	interface = "lo";  # Interface on which the pdnsd listens
#}
#
# 2. The last step is to configure dns configuration for /etc/resolv.conf.
#
#    a) For <net-dns/openresolv-2.0, configure the lo interface to use
#    127.0.0.1. In Gentoo we set it up like so in /etc/conf.d/net
#
#      dns_servers_lo=( "127.0.0.1" )
#
#    b) For net-dns/openresolv-2* the configuration has been changed. Add
#    the following line to the top of your /etc/resolvconf/resolv.conf.d/base
#    (possibly create one)
#
#      nameserver 127.0.0.1
#
#    c) For net-dns/openresolv-3* the configuration has been changed again.
#    Uncomment the following line in your /etc/resolvconf.conf
#
#      # If you run a local name server, you should uncomment the below line and
#      # configure your subscribers configuration files below.
#      name_servers=127.0.0.1
#

# pdnsd config file
PDNSDCONFIG="/etc/pdnsd/pdnsd.conf"

# Backup suffix
BACKUPSUFFIX=".backup"

# Load our variables from resolvconf
eval "$(/sbin/resolvconf -v)"

COMMENT='
# Automatically generated by resolvconf.
#
# Following server sections are automatically enabled and disabled.
#
# !!! WARNING !!!
# DO NOT RENAME LABELS!
#
# No section will be deleted and only some options are automatically changed.
# Feel free to add your own options, but do not use pair comments /* */ as they
# are not recognised.
#
# DO NOT USE resolvconf ANYWHERE IN YOUR LABELS!
#
# Automatically changed options are (with examples):
#   preset=on;
#   ip="192.168.0.1","192.168.0.2";
#   include=".net",".com";
#   exclude=".domain.net",".domain.com";
#   policy=excluded;
#
# The exclude directive is changed automatically only in "resolvconf" server
# section. Not handled servers are kept in the directive.
#'
BASIC_SETTINGS='server {
	label="resolvconf";
	preset=off;
}'

INSTALLATION_CHECK='^[[:space:]]*label[[:space:]]*=[[:space:]]*"resolvconf"'

###
# Sed script configuration
#
# Composed sequence of lines:
#
# (1) SED_LOOP with @MATCH_LABELS@ substituted by several SED_MATCH_ONE_LABEL
# (2) SED_EDIT_ONE_SERVER several times
# (3) SED_ADDING with new servers
#
# Notes:
#
# * @LABEL@ is a string "resolvconf-<domain>" or "resolvconf" for global
#   section
# * @RULE@ is @LABEL@ with translated characters '-' and '.' into '_'.
###

###
# Main loop with label match - it will redirect the processing to
# SED_EDIT_ONE_SERVER, when the label match is found. Special match is
# for "resolvconf" label - the control flow is redirected to SED_ADDING to
# allow adding new sections.
#
# To summarize: Old sections are edited as they appear in the file and new
# sections are added before the "resolvconf" section.
SED_LOOP=\
'/^[[:space:]]*server[[:space:]]*[\{]/ b server;
p; d;
:server; h;
:server_loop; n;
/^[[:space:]]*server[[:space:]]*[\{]/ { x; p; b server_loop; };
@MATCH_LABELS@/^[[:space:]]*label[[:space:]]*=[[:space:]]*"resolvconf"/ { H; b adding; };
/^[[:space:]]*[\}]/ { H; x; p; d; };
H;
b server_loop;'

###
# Match for one label with a jump to SED_EDIT_ONE_SERVER
SED_MATCH_ONE_LABEL=\
'/^[[:space:]]*label[[:space:]]*=[[:space:]]*"@LABEL@"/ { H; x; b main_@RULE@; };'

###
# Editing one server. New lines are put into @SETUP@, lines are composed
# in function compose_lines(). After the new lines are added, all "preset",
# "ip" and "include" options are removed (not printed).
#
# Sanity checks: Check if there is a second label or another server directive.
# In both cases, there is some error in the file, so go to the beginning by
# jumping to SED_LOOP's :server.
SED_EDIT_ONE_SERVER=\
':main_@RULE@;
p; @SETUP@
:loop_@RULE@;
n;
/^[[:space:]]*server[[:space:]]*[\{]/ b server;
/^[[:space:]]*label[[:space:]]*=/ b server;
/^[[:space:]]*preset[[:space:]]*=/ b loop_@RULE@;
/^[[:space:]]*ip[[:space:]]*=/ b loop_@RULE@;
/^[[:space:]]*include[[:space:]]*=/ b loop_@RULE@;
/^[[:space:]]*policy[[:space:]]*=/ b loop_@RULE@;
/^[[:space:]]*exclude[[:space:]]*=/ b exclude_logic_@RULE@;
p;
/^[[:space:]]*[\}]/ d;
b loop_@RULE@;

:exclude_logic_@RULE@;
@EXCLUDE_LOGIC@
b loop_@RULE@;
'

###
# Add new servers. All lines composed by function compose_lines() are put into
# @SETUP@. Then the control flow is returned to one special SED_EDIT_ONE_SERVER
# section with label "resolvconf".
SED_ADDING=\
':adding;
@SETUP@
x; b main_resolvconf;
'


###
# Edit the domain list (include/exclude). All empty fields and matching domains
# are removed. Unmaintained domains (not in resolvconf-<domain>) are kept. All
# domains should be in a pipe (|) separated list and should begin, but not end
# with a dot. The list is put into @DOMAINS@. The control flow continues, where
# it ended in SED_EDIT_ONE_SERVER.
#
SED_DOMAIN_LIST_LOGIC=\
'h;
s/^([[:space:]]*@DIRECTIVE@[[:space:]]*=[[:space:]]*).*/\\1/;
x;
s/^[[:space:]]*@DIRECTIVE@[[:space:]]*=[[:space:]]*//;

:@DIRECTIVE@_loop_@RULE@;
/([[:space:]]*("[^"]"*|[^,;]*)[[:space:]]*,)*[[:space:]]*("(@DOMAINS@|)\.?"|(@DOMAINS@)\.?|,)[[:space:]]*[,;]/ {
	s/(([[:space:]]*("[^"]"*|[^,;]*)[[:space:]]*,)*[[:space:]]*)("(@DOMAINS@|)\.?"|(@DOMAINS@)\.?|,)[[:space:]]*([,;])/\\1\\7/;
	b @DIRECTIVE@_loop_@RULE@;
};
	
s/^[,;]//g;
/^[[:space:]]*$/ b @DIRECTIVE@_end_@RULE@;
H; x; s/\\n//; p;
:@DIRECTIVE@_end_@RULE@;
'

################################################################################
# Functions

###
# char* [] uniqify(char* list[])
#
# Uniqify the items in the list
uniqify() {
	local result=
	while [ -n "$1" ] ; do
		case " ${result} " in
			*" $1 "*) ;;
			*) result="${result} $1" ;;
		esac
		shift
	done
	printf "%s" "${result# *}"
}

###
# char *make_pdnsd_label(char *domain)
#
# Translate domain name into pdnsd's label
make_pdnsd_label() {
	local domain="$1"
	if [ -n "${domain}" ] ; then
		printf "%s" "resolvconf-${domain}"
	else
		printf "%s" "resolvconf"
	fi
}


###
# char *replace(char *text, char *search, char *replace)
#
# Replaces all the found "search" occurences in the "text" with the "replace".
replace() {
	local text="$1" search="$2" replace="$3"
	local pre post="${text}" result=""
	while [ x"${pre}" != x"${text}" ] ; do
		post="${text}"
		pre="${text%%$search*}"
		post="${text#*$search}"
		if [ x"${pre}" != x"${text}" ] ; then
			result="${result}${pre}${replace}"
			text="${post}"
		fi
	done
	printf "%s" "${result}${post}"
}

###
# char *make_sed_label(char *pdnsd_label)
#
# Translate pdnsd's label into sed's label
make_sed_label() {
	local label="$1"
	label="$(replace "${label}" "-" "_")"
	label="$(replace "${label}" "." "_")"
	printf "%s" "${label}"
}

# char *compose_lines(...)
#
# Compose a sed command that prints lines
compose_lines() {
	local line result
	for line in "$@" ; do
		result="${result}i\\\\\\n$(replace "${line}" "	" "\\t")\\n"
	done
	printf "%s" "${result}"
}

###
# char *build_settings(char *nameservers, char *domains, char *directive)
#
# Builds configuration part @SETUP@ of sed script. The directive parameter denotes
# if the domains are to be included ("include") or excluded ("exclude"). This
# involves options like
#
# (1) # [nameserver list is empty]
#     preset=off;
#
# (2) # [domain list is empty]
#     preset=on;
#     ip="address","address"...;
#
# (3) # [directive=="include"]
#     preset=on;
#     ip="address","address"...;
#     include=".domain.",".domain."...;
#     policy=excluded;
#
# (4) # [directive=="exclude"]
#     preset=on;
#     ip="address","address"...;
#     exclude=".domain.",".domain."...;
#     policy=included;
#
# Note: Currently there will always be only one domain in "include" directive.
#
build_settings() {
	local ns="$1" domains="$2" directive="$3"
	if [ -n "${ns}" ] ; then
		local x list_ns list_domains
		for x in ${ns} ; do
			list_ns="${list_ns},\"${x}\""
		done
		list_ns="${list_ns#,}"
		if [ -n "${domains}" ] ; then
			for x in ${domains} ; do
				list_domains="${list_domains},\".${x}.\""
			done
			list_domains="${list_domains#,}"
			if [ x"$directive" = x"include" ]; then
				compose_lines \
					"	preset=on;" \
					"	ip=${list_ns};" \
					"	include=${list_domains};" \
					"	policy=excluded;"
			else
				compose_lines \
					"	preset=on;" \
					"	ip=${list_ns};" \
					"	exclude=${list_domains};" \
					"	policy=included;"
			fi
		else
			compose_lines \
				"	preset=on;" \
				"	ip=${list_ns};"
		fi
	else
		compose_lines \
			"	preset=off;"
	fi
}

###
# char *build_match_labels(char *domains...)
#
# Build the label match part of the sed script
#
build_match_labels() {
	local domain result label destination new_match
	for domain in "$@" ; do
		label="$(make_pdnsd_label "${domain}")"
		rule="$(make_sed_label "${label}")"
		new_match="$(replace "${SED_MATCH_ONE_LABEL}" "@LABEL@" "${label}")"
		new_match="$(replace "${new_match}" "@RULE@" "${rule}")"
		result="${result}${new_match}\n"
	done
	printf "%s" "${result}"
}

###
# char *build_domain_list_logic(char *domains, char *directive)
#
# Build a logic for changing (removing) domains from a directive.
#
build_domain_list_logic() {
	local domains="$1" directive="$2"
	local x domain_list logic
	
	# Domains should be pipe separated list
	for x in ${domains}; do
		x=".${x%.}"
		x="$(replace "${x}" "." "\\.")"
		domain_list="${domain_list}|${x}"
	done
	domain_list="${domain_list#|}"
	
	if [ -z "${domain_list}" ]; then
		logic="p;"
	else
		logic="$(replace "${SED_DOMAIN_LIST_LOGIC}" "@DOMAINS@" "${domain_list}")"
		logic="$(replace "${logic}" "@DIRECTIVE@" "${directive}")"
	fi
	printf "%s" "${logic}"
}

###
# char *build_edit_part(char *domain, char *nameservers, \
#                       char *add_domains, char *remove_domains,
#                       char *directive)
#
# Build edit part of the sed script for a particular domain. Domain can be
# empty in the case it is the "resolvconf" server part.
#
build_edit_part() {
	local domain="$1" nameservers="$2" add_domains="$3" remove_domains="$4"
	local directive="$5"
	local setup label rule logic result
	
	setup="$(build_settings "${nameservers}" "${add_domains}" "${directive}")"
	label="$(make_pdnsd_label "${domain}")"
	rule="$(make_sed_label "${label}")"
	logic="$(build_domain_list_logic "${remove_domains}" "${directive}")"
	result="$(replace "${SED_EDIT_ONE_SERVER}" "@SETUP@" "${setup}")"
	result="$(replace "${result}" "@EXCLUDE_LOGIC@" "${logic}")"
	result="$(replace "${result}" "@RULE@" "${rule}")"
	printf "%s\n" "${result}"
}

###
# char *get_domain_nameservers(char *domain, char *domain_config...)
#
# Get the list of nameservers belonging to one particular domain.
#
# Domain configuration is a space separated list of pair <domain>,<ip>.
#
get_domain_nameservers() {
	local domain="$1" ns
	shift
	for x in "$@" ; do
		if [ x"${x%,*}" = x"${domain}" ] ; then
			ns="${ns} ${x#*,}"
		fi
	done
	ns="$(uniqify ${ns})"
	printf "%s" "${ns}"
}

###
# char *build_domain_edit_part(char *domain, char *domain_config...)
#
# Parse the list of domain configurations and build settings for one particular
# domain for the sed script.
#
# Domain configuration is a space separated list of pair <domain>,<ip>.
#
build_domain_edit_part() {
	local domain="$1" ns
	shift
	ns="$(get_domain_nameservers "${domain}" "$@")"
	build_edit_part "${domain}" "${ns}" "${domain}" "" "include"
}

###
# char *build_add_part(char *add, char *domains...)
#
# Build add part of the sed script for all domains that needs to be added
#
build_add_part() {
	local add="$1" x label rule add_part new_part result
	shift
	for x in ${add} ; do
		local domain="${x}" ns
		ns="$(get_domain_nameservers "${domain}" "$@")"
		label="$(make_pdnsd_label "${domain}")"
		rule="$(make_sed_label ${label})"
		new_part="$(compose_lines "server {" "	label=\"${label}\";")"
		new_part="${new_part}$(build_settings "${ns}" "${domain}" "include")"
		new_part="${new_part}$(compose_lines "}" "")"
		add_part="${add_part}${new_part}"
	done
	result="$(replace "${SED_ADDING}" "@SETUP@" "${add_part}")"
	printf "%s" "${result}"
}

###
# char *build_sed_script(char *nameservers, char *domain_config, 
#                        char *change, char *add,
#                        char *active_domains, char *known_domains)
#
# Build the full sed script from the list of nameservers, list of domains
# (in format <domain>,<ip>), list of changed domains, list of added domains,
# list of activly used domains and a list of all known domains.
#
build_sed_script() {
	local ns="$1" domain_config="$2" change="$3" add="$4"
	local active_domains="$5" known_domains="$6"

	local match_labels="$(build_match_labels ${change})"
	
	local edit_changed x
	for x in ${change} ; do
		edit_changed="${edit_changed}$( \
			build_domain_edit_part "${x}" ${domain_config})"
	done
	edit_changed="${edit_changed}$( \
		build_edit_part "" "${ns}" "${active_domains}" "${known_domains}" "exclude")"
	
	local added
	added="$(build_add_part "${add}" ${domain_config})"
	
	local full
	full="$(replace "${SED_LOOP}" "@MATCH_LABELS@" "${match_labels}")"
	printf "%b\n" "${full}"
	printf "%b\n" "${edit_changed}"
	printf "%b" "${added}"
}

###
# char *read_configured_domains(char *config_file)
#
# Reads labels of servers starting with resolvconf* from the configuration file.
#
read_configured_domains() {
	local config_file="$1" result
	result="\
		$(sed -nre 's/^[[:space:]]+label=\"?resolvconf-([^;\"]*)\";.*/\1/p' \
		${config_file})"
	printf "%s" "${result}"
}

###
# void installation_check(char *config_file)
#
# Check if the pdnsd is installed and can be configured. Prepare also the file
# for resolvconf.
#
installation_check() {
	local config_file="$1"
	if [ -e "${config_file}" ] ; then
		if ! grep ${INSTALLATION_CHECK} "${config_file}" >/dev/null 2>&1; then
			printf "%s\n" "${COMMENT}" >> "${config_file}"
			printf "\n%s\n" "${BASIC_SETTINGS}" >> "${config_file}"
		fi
		return 0
	else
		return 1
	fi
}

###
# void initialization(char *config_file)
#
# Setup basic variables NAMESERVERS, DOMAINS an CONFIGURED_DOMAINS
#
initialization() {
	local config_file="$1"
	
	# Compatibility fix for new openresolv-2.0
	if [ -z "${NEWNS}" -a -n "${NAMESERVERS}" ]; then
		NEWDOMAIN=$(replace "${DOMAINS}" ":" ",")
		NEWSEARCH=
		NEWNS=${NAMESERVERS}
		DOMAINS=
		NAMESERVERS=
		SEARCH=
	fi
	
	for N in ${NEWNS} ; do
		NAMESERVERS="${NAMESERVERS} ${N}"
	done
	
	for N in ${NEWSEARCH} ; do
		NAMESERVERS="${NAMESERVERS} ${N#*,}"
	done
	
	for DN in ${NEWDOMAIN} ; do
		DOMAINS="${DOMAINS} ${DN%%,*}"
	done
	
	CONFIGURED_DOMAINS=$(read_configured_domains ${config_file})

	NAMESERVERS=$(uniqify ${NAMESERVERS})
	DOMAINS=$(uniqify ${DOMAINS})
	CONFIGURED_DOMAINS=$(uniqify ${CONFIGURED_DOMAINS})
}

###
# void find_changed_and_added(char *configured, char *domains)
#
# Find already configured and newly added domains. Sets variables
# CHANGE_DOMAINS, ADD_DOMAINS and KNOWN_DOMAINS.
#
find_changed_and_added() {
	local configured="$1" domains="$2" x
	
	KNOWN_DOMAINS="${CONFIGURED_DOMAINS} ${DOMAINS}"
	
	# Find what has to be disabled
	for x in ${configured} ; do
		case " ${domains} " in
			*" ${x} "*) ;;
			*) CHANGE_DOMAINS="${CHANGE_DOMAINS} ${x}" ;;
		esac
	done

	# Find what has to be added
	for x in ${domains} ; do
		case " ${configured} " in
			*" ${x} "*) CHANGE_DOMAINS="${CHANGE_DOMAINS} ${x}" ;;
			*) ADD_DOMAINS="${ADD_DOMAINS} ${x}" ;;
		esac
	done

	ADD_DOMAINS=$(uniqify ${ADD_DOMAINS})
	CHANGE_DOMAINS=$(uniqify ${CHANGE_DOMAINS})
	KNOWN_DOMAINS=$(uniqify ${KNOWN_DOMAINS})
}

###
# bool make_configuration_change(char *config_file, char *backup_suffix,
#                                char *sed_script)
#
# Applies any configuration change. Returns true, if there was a change.
#
make_configuration_change() {
	local config_file="$1" backup_suffix="$2" sed_script="$3"
	local old_config new_config

	old_config="$(cat "${config_file}")"
	
	# Sanity check: add '}' at the end of the file
	new_config=$( (printf "%s" "${old_config}" && printf "\n}" ) | \
		sed -nre "${sed_script}")
	# Now remove what we added
	new_config=${new_config%?\}}

	if [ x"${old_config}" != x"${new_config}" ] ; then
		cp ${config_file} ${config_file}${backup_suffix}
		printf "%s\n" "${new_config}" > "${config_file}"
		return 0
	else
		return 1
	fi
}

################################################################################
# Main part

# Check, if pdnsd configuration file is installed and possibly prepare it
installation_check "${PDNSDCONFIG}" || exit 0

# Basic initialization of NAMESERVERS, DOMAINS and CONFIGURED_DOMAINS
initialization "${PDNSDCONFIG}"

find_changed_and_added "${CONFIGURED_DOMAINS}" "${DOMAINS}"

sed_script="$(build_sed_script "${NAMESERVERS}" "${NEWDOMAIN}" \
	"${CHANGE_DOMAINS}" "${ADD_DOMAINS}" \
	"${DOMAINS}" "${KNOWN_DOMAINS}")"

# Check if the config changed
if make_configuration_change "${PDNSDCONFIG}" "${BACKUPSUFFIX}" "${sed_script}" ; then
	# Checks for running pdnsd
	[ -x /usr/sbin/pdnsd-ctl ] || exit 0
	[ -e /var/cache/pdnsd/pdnsd.status ] || exit 0

	# Reload config files
	/usr/sbin/pdnsd-ctl config >/dev/null 2>&1
fi

exit 0
