#!/bin/sh /etc/rc.common

START=25
USE_PROCD=1
PROG=/usr/sbin/dhcpd

WS=$'[\t ]'
TTL=3600
PREFIX="update add"

lease_file=/tmp/dhcpd.leases
config_file=/tmp/run/dhcpd.conf

dyndir=/var/run/dhcp

session_key_name=local-ddns
session_key_file=/var/run/named/session.key

time2seconds() {
	local timestring=$1
	local multiplier number suffix

	suffix="${timestring//[0-9 ]}"
	number="${timestring%%$suffix}"
	[ "$number$suffix" != "$timestring" ] && return 1
	case "$suffix" in
		"" | s)
			multiplier=1
			;;
		m)
			multiplier=60
			;;
		h)
			multiplier=3600
			;;
		d)
			multiplier=86400
			;;
		w)
			multiplier=604800
			;;
		*)
			return 1
			;;
	esac
	echo $(( number * multiplier ))
}

explode() {
	echo "${1//\./ }"
}

trim() {
	local str="$1" prev

	while true; do
		prev="$str"
		str="${str%%$WS}"
		[ "$str" = "$prev" ] && break
	done
	while true; do
		prev="$str"
		str="${str##$WS}"
		[ "$str" = "$prev" ] && break
	done
	echo "$str"
}

rfc1918_prefix() {
	local subnet="${1%/*}"
	set -- $(explode "$subnet")

	case "$1.$2" in
	10.*)
		echo "$1" ;;
	172.1[6789]|172.2[0-9]|172.3[01]|192.168)
		echo "$1.$2" ;;
	esac
}

no_ipv6() {
	[ -n "$(named-checkconf -px \
		| sed -r -ne '1N; N; /^\tlisten-on-v6  ?\{\n\t\t"none";\n\t\};$/{ p; q; }; D')" ]
}

# duplicated from dnsmasq init script
hex_to_hostid() {
	local var="$1"
	local hex="${2#0x}" # strip optional "0x" prefix

	if [ -n "${hex//[0-9a-fA-F]/}" ]; then
		# is invalid hex literal
		return 1
	fi

	# convert into host id
	export "$var=$(
		printf "%0x:%0x" \
		$(((0x$hex >> 16) % 65536)) \
		$(( 0x$hex        % 65536))
		)"

	return 0
}

typeof() {
	echo "$1" | awk '
/^\d+\.\d+\.\d+\.\d+$/		{ print "ip\n"; next; }
/^(true|false)$/		{ print "bool\n"; next; }
/^\d+$/				{ print "integer\n"; next; }
/^"[^"]*"$/			{ print "string\n"; next; }
/^[0-9a-fA-F]{2,2}(:[0-9a-fA-F]{2,2})*$/	{ print "hexstring\n"; next; }
				{ print "other\n"; next; }
'
}

update() {
	local lhs="$1" family="$2" type="$3"
	shift 3

	[ $dynamicdns -eq 1 ] && \
		echo -e "$PREFIX" "$lhs $family $type $@\nsend" >> $dyn_file
}

rev_str() {
	local str="$1" delim="$2"
	local frag result="" IFS="$delim"

	for frag in $str; do
		result="$frag${result:+$delim}$result"
	done

	echo "$result"
}

write_empty_zone() {
	local zpath
	zpath="$1"

	cat > "$zpath" <<EOF
;
; BIND empty zone created by isc-dhcp-server at $(date)
;
\$TTL	604800
@	IN	SOA	localhost. root.localhost. (
			      1		; Serial
			 604800		; Refresh
			  86400		; Retry
			2419200		; Expire
			 604800 )	; Negative Cache TTL
;
@	IN	NS	localhost.
EOF
}

create_empty_zone() {
	local zone error zpath command
	zone="$1"
	zpath="$dyndir/db.$zone"

	if [ ! -d "$dyndir" ]; then
		mkdir -p "$dyndir" || return 1
		chown bind:bind "$dyndir" || return 1
	fi

	write_empty_zone "$zpath"
	chown bind:bind "$zpath" || return 1
	chmod 0664 "$zpath" || return 1

	# if the zone doesn't exist, or a RFC-1918 in-addr.arpa zone, then
	# we need to add it, otherwise we need to create it.
	if ! rndc zonestatus $zone >/dev/null 2>&1; then
		command="addzone"
	else
		command="modzone"
	fi

	case "$zone" in
		10.in-addr.arpa|1[6789].172.in-addr.arpa|2[0-9].172.in-addr.arpa|3[01].172.in-addr.arpa|168.192.in-addr.arpa)
		command="addzone" ;;
	esac

	if ! error=$(rndc $command $zone "{
		type primary;
		file \"$zpath\";
		update-policy {
			grant $session_key_name zonesub any;
		};
	};" 2>&1); then
		case "$error" in
			*"already exists"*)
				;;
			*)
				logger -p info -t isc-dhcp "Failed to add zone $zone: $error"
				return 1
				;;
		esac
	fi
}

append_routes() {
	local tuple="$(trim "$1")"
	local network prefix router subnet compacted octet

	subnet="${tuple%%$WS*}"

	network="${subnet%/[0-9]*}"

	prefix="${subnet#*/}"

	set -- $(explode "$network")

	case $((($prefix + 7) / 8)) in
	0)
		compacted= ;;
	1)
		compacted="$1" ;;
	2)
		compacted="$1 $2" ;;
	3)
		compacted="$1 $2 $3" ;;
	4)
		compacted="$1 $2 $3 $4" ;;
	esac

	router="${tuple#${subnet}$WS}"

	for octet in $prefix $compacted $(explode "$router"); do
		append routes "$octet" ", "
	done
}

append_dhcp_options() {
	local tuple="$1"

	# strip redundant "option:" prefix
	tuple="${tuple#option:}"

	local tag="${tuple%%,*}"
	local values="${tuple#$tag,}"

	local formatted value
	local IFS=$', \n'
	for value in $values; do
		# detect type of $value and quote if necessary
		case $(typeof "$value") in
		ip|bool|integer|string|hexstring)
			;;
		other)
			value="\"$value\""
			;;
		esac
		append formatted "$value" ", "
	done
	echo " option $tag $formatted;"
}

static_cname_add() {
	local cfg="$1"
	local cname target

	config_get cname "$cfg" "cname"
	[ -n "$cname" ] || return 0
	config_get target "$cfg" "target"
	[ -n "$target" ] || return 0

	update "$cname.$domain." IN CNAME "$target.$domain."
}

static_cnames() {
	config_foreach static_cname_add cname "$@"
}

static_domain_add() {
	local cfg="$1"
	local name ip ips revip

	config_get name "$cfg" "name"
	[ -n "$name" ] || return 0
	config_get ip "$cfg" "ip"
	[ -n "$ip" ] || return 0

	ips="$ip"
	for ip in $ips; do
		revip="$(rev_str "$ip" ".")"

		update "$name.$domain." IN A "$ip"
		[ -n "$(rfc1918_prefix "$ip")" ] && \
			update "$revip.in-addr.arpa." IN PTR "$name.$domain."
	done
}

static_domains() {
	config_foreach static_domain_add domain "$@"
}

static_mxhost_add() {
	local cfg="$1"
	local domain2 relay pref

	config_get domain2 "$cfg" "domain"
	[ -n "$domain2" ] || return 0
	config_get relay "$cfg" "relay"
	[ -n "$relay" ] || return 0
	config_get pref "$cfg" "pref"
	[ -n "$pref" ] || return 0

	if [ "$domain2" = "@" ]; then
		update "$domain." IN MX "$pref" "$relay.$domain."
	else
		update "$domain2.$domain." IN MX "$pref" "$relay.$domain."
	fi
}

static_mxhosts() {
	config_foreach static_mxhost_add mxhost "$@"
}

static_srvhost_add() {
	local cfg="$1"
	local srv target port priority weight

	config_get srv "$cfg" "srv"
	[ -n "$srv" ] || return 0
	config_get target "$cfg" "target"
	[ -n "$target" ] || return 0
	config_get port "$cfg" "port"
	[ -n "$port" ] || return 0
	config_get priority "$cfg" "priority"
	[ -n "$priority" ] || return 0
	config_get weight "$cfg" "weight"
	[ -n "$weight" ] || return 0

	update "$srv.$domain." IN SRV "$priority" "$weight" "$port" "$target.$domain."
}

static_srvhosts() {
	config_foreach static_srvhost_add srvhost "$@"
}

static_host_add() {
	local cfg="$1"
	local broadcast hostid macn macs mac name ip ips revip leasetime
	local force_send extra_options option

	config_get macs "$cfg" "mac"
	[ -n "$macs" ] || return 0
	config_get name "$cfg" "name"
	[ -n "$name" ] || return 0
	config_get ip "$cfg" "ip"
	[ -n "$ip" ] || return 0

	config_get_bool broadcast "$cfg" "broadcast" 0
	config_get dns "$cfg" "dns"
	config_get gateway "$cfg" "gateway"
	config_get leasetime "$cfg" "leasetime"
	if [ -n "$leasetime" ]; then
		leasetime="$(time2seconds "$leasetime")"
		[ $? -ne 0 ] && return 1
	fi

	config_get hostid "$cfg" "hostid"
	if [ -n "$hostid" ]; then
		hex_to_hostid hostid "$hostid" || return 1
	fi

	config_get force_send "$cfg" "force_send"
	extra_options=
	for option in ${force_send//,/ }; do
		case "$option" in
		hostname)
			append extra_options "0c" ", " ;;
		domain-name)
			append extra_options "0f" ", " ;;
		renewal-time)
			append extra_options "3a" ", " ;;
		rebinding-time)
			append extra_options "3b" ", " ;;
		fqdn)
			append extra_options "51" ", " ;;
		routes)
			append extra_options "79" ", " ;;
		*)
			echo "unknown option: $option" >&2 ;;
		esac
	done

	macn=0
	for mac in $macs; do
		macn=$(( macn + 1 ))
	done

	for mac in $macs; do
		local secname="$name"
		if [ $macn -gt 1 ]; then
			secname="${name}-${mac//:}"
		fi
		echo "host $secname {"
		echo " hardware ethernet $mac;"
		echo " fixed-address $ip;"
		echo " option host-name \"$name\";"
		if [ $broadcast -eq 1 ]; then
			echo " always-broadcast true;"
		fi
		if [ -n "$leasetime" ]; then
			echo " default-lease-time $leasetime;"
			echo " max-lease-time $leasetime;"
		fi
		if [ -n "$hostid" ]; then
			echo " option dhcp-client-identifier $hostid;"
		fi
		if [ -n "$dns" ]; then
			echo " option domain-name-servers $dns;"
		fi
		if [ -n "$gateway" ]; then
			echo " option routers $gateway;"
		fi

		local routes=
		config_list_foreach "$cfg" "routes" append_routes
		[ -n "$routes" ] && echo " option classless-ipv4-route $routes;"

		config_list_foreach "$cfg" "dhcp_option" append_dhcp_options
		if [ -n "$extra_options" ]; then
			echo -e " if exists dhcp-parameter-request-list {\n  option dhcp-parameter-request-list = concat(option dhcp-parameter-request-list, $extra_options);\n }"
		fi
		echo "}"
	done

	ips="$ip"
	for ip in $ips; do
		revip="$(rev_str "$ip" ".")"

		update "$name.$domain." IN A "$ip"
		update "$revip.in-addr.arpa." IN PTR "$name.$domain."
	done
}

static_hosts() {
	config_foreach static_host_add host "$@"
}

gen_dhcp_subnet() {
	local cfg="$1"

	echo "subnet $NETWORK netmask $NETMASK {"
	if [ -n "$START" ] && [ -n "$END" ]; then
		echo " range $START $END;"
	fi
	echo " option subnet-mask $netmask;"
	# check for 0.0.0.0 until all active releases of ipcalc.sh omit it
	# for small networks:
	if [ -n "$BROADCAST" ] && [ "$BROADCAST" != "0.0.0.0" ]; then
		echo " option broadcast-address $BROADCAST;"
	fi
	if [ $dynamicdhcp -eq 0 ]; then
		if [ $authoritative -eq 1 ]; then
			echo " deny unknown-clients;"
		else
			echo " ignore unknown-clients;"
		fi
	fi
	if [ -n "$leasetime" ]; then
		echo " default-lease-time $leasetime;"
		echo " max-lease-time $leasetime;"
	fi
	if [ $defaultroute -eq 1 ]; then
		echo " option routers $gateway;"
	fi
	echo " option domain-name-servers $DNS;"

	[ -n "$domain" ] && echo " option domain-name \"$domain\";"

	local routes=
	config_list_foreach "$cfg" "routes" append_routes
	[ -n "$routes" ] && echo " option classless-ipv4-route $routes;"

	config_list_foreach "$cfg" "dhcp_option" append_dhcp_options
	echo "}"
}

dhcpd_add() {
	local cfg="$1" synthesize="$2"
	local dhcp6range="::"
	local dynamicdhcp defaultroute end gateway ifname ignore leasetime limit net netmask
	local proto networkid start subnet domain
	local IP NETMASK BROADCAST NETWORK PREFIX DNS START END

	config_get_bool ignore "$cfg" "ignore" 0
	[ $ignore -eq 1 ] && return 0

	config_get net "$cfg" "interface"
	[ -n "$net" ] || return 0

	config_get start "$cfg" "start"
	config_get limit "$cfg" "limit"

	case "$start:$limit" in
	":*"|"*:")
		echo "dhcpd: start/limit must be used together in $cfg" >&2
		return 0
	esac

	network_get_subnet subnet "$net" || return 0
	network_get_device ifname "$net" || return 0
	network_get_protocol proto "$net" || return 0

	[ "$proto" != "static" ] && return 0

	local octets="$(rfc1918_prefix "$subnet")"

	[ -n "$octets" ] && append rfc1918_nets "$octets"

	[ $synthesize -eq 0 ] && return 0

	config_get_bool dynamicdhcp "$cfg" "dynamicdhcp" 1

	config_get_bool defaultroute "$cfg" "default_route" 1

	append dhcp_ifs "$ifname"

	if [ -z "$start$limit" ]; then
		if ! ipcalc "$subnet"; then
			echo "invalid subnet param: $start" >&2
			return 1
		fi
	elif ! ipcalc "$subnet" "$start" "$limit"; then
		echo "invalid range params: $subnet start: $start limit: $limit" >&2
		return 1
	fi

	config_get netmask "$cfg" "netmask" "$NETMASK"
	config_get leasetime "$cfg" "leasetime"
	if [ -n "$leasetime" ]; then
		leasetime="$(time2seconds "$leasetime")"
		[ $? -ne 0 ] && return 1
	fi

	if network_get_dnsserver dnsserver "$net" ; then
		for dnsserv in $dnsserver ; do
			DNS="$DNS${DNS:+, }$dnsserv"
		done
	else
		DNS="$IP"
	fi

	if ! network_get_gateway gateway "$net" ; then
		gateway="$IP"
	fi

	config_get domain "$cfg" "domain"

	gen_dhcp_subnet "$cfg"
}

general_config() {
	local always_broadcast boot_unknown_clients log_facility
	local default_lease_time max_lease_time

	config_get_bool always_broadcast "isc_dhcpd" "always_broadcast" 0
	config_get_bool authoritative "isc_dhcpd" "authoritative" 1
	config_get_bool boot_unknown_clients "isc_dhcpd" "boot_unknown_clients" 1
	config_get default_lease_time "isc_dhcpd" "default_lease_time" 3600
	config_get max_lease_time "isc_dhcpd" "max_lease_time" 86400
	config_get log_facility "isc_dhcpd" "log_facility"

	config_get domain "isc_dhcpd" "domain"
	config_get_bool dynamicdns "isc_dhcpd" dynamicdns 0

	[ $always_broadcast -eq 1 ] && echo "always-broadcast true;"
	[ $authoritative -eq 1 ] && echo "authoritative;"
	[ $boot_unknown_clients -eq 0 ] && echo "boot-unknown-clients false;"

	default_lease_time="$(time2seconds "$default_lease_time")"
	[ $? -ne 0 ] && return 1
	max_lease_time="$(time2seconds "$max_lease_time")"
	[ $? -ne 0 ] && return 1

	if [ $dynamicdns -eq 1 ]; then
		rndc freeze

		create_empty_zone "$domain"

		local mynet

		for mynet in $rfc1918_nets; do
			mynet="$(rev_str "$mynet" ".")"
			create_empty_zone "$mynet.in-addr.arpa"
		done

		rndc thaw

		cat <<EOF
ddns-domainname "$domain.";
ddns-update-style standard;
ddns-updates on;
ignore client-updates;

update-static-leases on;
use-host-decl-names on;
update-conflict-detection off;
update-optimization off;

include "$session_key_file";

zone $domain. {
 primary 127.0.0.1;
 key $session_key_name;
}

EOF

		for mynet in $rfc1918_nets; do
			mynet="$(rev_str "$mynet" ".")"
			cat <<EOF
zone $mynet.in-addr.arpa. {
 primary 127.0.0.1;
 key $session_key_name;
}

EOF
		done
	fi

	if [ -n "$log_facility" ]; then
		echo "log-facility $log_facility;"
	fi
	echo "default-lease-time $default_lease_time;"
	echo "max-lease-time $max_lease_time;"

	[ -n "$domain" ] && echo "option domain-name \"$domain\";"

	echo -e "\n# additional codes\noption classless-ipv4-route code 121 = array of { unsigned integer 8 };\n"

	rm -f /tmp/resolv.conf
	echo "# This file is generated by the DHCPD service" > /tmp/resolv.conf
	[ -n "$domain" ] && echo "domain $domain" >> /tmp/resolv.conf
	echo "nameserver 127.0.0.1" >> /tmp/resolv.conf
}

# base procd hooks

boot() {
	DHCPD_BOOT=1
	start "$@"
}

start_service() {
	local domain dhcp_ifs authoritative dynamicdns

	if [ -n "$DHCPD_BOOT" ]; then
		return 0
	fi

	if [ ! -e $lease_file ]; then
		touch $lease_file
	fi

	if [ -e "/etc/dhcpd.conf" ]; then
		config_file="/etc/dhcpd.conf"
	else
		. /lib/functions/network.sh

		local dyn_file="$(mktemp -u /tmp/dhcpd.XXXXXX)"

		config_load dhcp

		local rfc1918_nets=""

		# alas we have to make 2 passes...
		dhcp_ifs=
		config_foreach dhcpd_add dhcp 0

		rfc1918_nets="$(echo "$rfc1918_nets" | tr ' ' $'\n' | sort | uniq | tr $'\n' ' ')"

		general_config > $config_file

		if [ $dynamicdns -eq 1 ]; then
			cat <<EOF > "$dyn_file"
; Generated by /etc/init.d/dhcpd at $(date)

ttl $TTL

EOF
		fi

		rfc1918_nets=

		dhcp_ifs=
		config_foreach dhcpd_add dhcp 1 >> $config_file

		static_hosts >> $config_file

		static_cnames >> $config_file

		static_domains >> $config_file

		static_mxhosts >> $config_file

		static_srvhosts >> $config_file

		if [ $dynamicdns -eq 1 ]; then
			local args=

			no_ipv6 && args="-4"

			nsupdate -l -v $args "$dyn_file"

		fi

		rm -f "$dyn_file"

		[ -z "$dhcp_ifs" ] && return 0
	fi

	procd_open_instance
	procd_set_param command $PROG -q -f -cf $config_file -lf $lease_file $dhcp_ifs
	procd_close_instance
}

reload_service() {
	rc_procd start_service "$@"
	procd_send_signal dhcpd "$@"
}

add_interface_trigger() {
	local cfg="$1"
	local net ignore ifname

	config_get net "$cfg" interface
	config_get_bool ignore "$cfg" ignore 0

	if [ -n "$net" ] && [ $ignore -eq 0 ]; then
		network_get_device ifname "$net"
		procd_add_reload_interface_trigger "$ifname"
	fi
}

service_triggers() {
	if [ -n "$DHCPD_BOOT" ]; then
		# Make the first start robust to slow interfaces; wait a while
		procd_add_raw_trigger "interface.*.up" 5000 /etc/init.d/dhcpd restart

	else
		# reload with normal parameters
		procd_add_reload_trigger "network" "dhcp"
		config_load dhcp
		config_foreach add_interface_trigger dhcp
	fi
}

