#!/bin/sh

### static variables
ddns_domain_file=/etc/ddns.conf
ddns_domain_dir=ddns
ddns_key="ddns-key"
ddns_ttl=86400

ddns_std_namelist="ns ldap" #hostname should be first

. alterator-net-functions
. bind-sh-functions

### system name
ddns_system_name()
{
    local ddns_hostname="$(hostname)"
    echo "${ddns_hostname%%.*}"
}

ddns_system_zone()
{
    local ddns_hostname="$(hostname)"
    echo "${ddns_hostname#*.}"
}

### shared secret key

# usage: ddns_key_exist <name>
# check for existance of shared secret key
ddns_key_exist()
{
    local name="$1";shift
    [ -s "$bind_root_dir/etc/$name.conf" ]
}

# usage: ddns_create_key <name> [<nsupdate>]
# create shared secret key
# if optional parameter <nsupdate> is equal to 1 then an additional keys for nsupdate will be created
ddns_create_key()
{
    local name="$1";shift
    local nsupdate="${1:-}"
    cd "$bind_root_dir/etc"

    ddns_key_exist "$name" && return 0

    local key="$(/usr/sbin/dnssec-keygen -r /dev/urandom -a HMAC-MD5 -b 512 -n USER "$name")"
    local secret="$(sed -n 's/Key:[[:space:]]\+\([^[:space:]]\+\)/\1/p' "$key.private")"

    cd - >/dev/null

    cat>"$bind_root_dir/etc/$name.conf"<<EOF
key $name {
    algorithm hmac-md5;
    secret "$secret";
};
EOF
    chmod 0644 "$bind_root_dir/etc/$name.conf"
    chown root:named "$bind_root_dir/etc/$name.conf"

    [ "$nsupdate" = "1" ] || rm -f -- "$bind_root_dir/etc/K$name.+157+"*

    bind_local_conf_include /etc/$name.conf
}

# usage: ddns_destroy_key <name>
# destroy shared secret key
ddns_destroy_key()
{
    local name="$1";shift
    cd "$bind_root_dir/etc"

    rm -f -- "$bind_root_dir/etc/K$name.+157+"*
    rm -f -- "$bind_root_dir/etc/$name.conf"

    bind_local_conf_exclude /etc/$name.conf
}

# usage: ddns_print_key <name>
# print shared secret key
ddns_print_key()
{
    local name="$1";shift
    cat "$bind_root_dir/etc/$name.conf"
}

# usage: ddns_check_key <name> <file>
# validate <file> as shared secret key for name <name>
ddns_check_key()
{
    local name
    quote_sed_regexp_variable  name "$1";shift
    local file="$1";shift
    grep -qs "^[[:space:]]*key[[:space:]]\+$name[[:space:]]\+{[[:space:]]*\$" "$file"
}

# usage: ddns_replace_key <name> <file>
# replace shared secret file with a new one
ddns_replace_key()
{
    local name="$1";shift
    local file="$1";shift

    [ -s "$file" ] || return 0

    install -pm 640 --owner root --group named "$file" "$bind_root_dir/etc/$name.conf"
    bind_local_conf_include /etc/$name.conf
}

### update zone information

# usage: ddns_update <command...>
# call nsupdate with appropriate key and action
ddns_record_update()
{
    nsupdate -k "$bind_root_dir/etc/K$ddns_key.+157+"*.private<<EOF
server localhost
update $@
send
EOF
}

### networks

# usage: ddns_net_foreach proc args
# call procedure for each available static network in system
ddns_net_foreach()
{
    local prog="$1";shift

    list_static_iface|
	while read iface; do
	    local ip="$(read_iface_addr "/etc/net/ifaces/$iface")"
	    [ -n "$ip" ] || continue
	    "$prog" "$ip" "$@"
	done
}

# usage: ddns_net_list_all
# list all available networks
ddns_net_list_all()
{
    list_static_iface|
	while read iface; do
	    local ip="$(read_iface_addr "/etc/net/ifaces/$iface")"
	    [ -n "$ip" ] || continue
	    printf '%s\t%s\n' "$ip" "$iface"
	done
}


### reverse zones, each reverse zone can contain multiple reverse domains

# usage: ddns_reverse_zone_add_ns <reverse-zone> <domain>
# add ns entry of specified domain to reverse zone
ddns_reverse_zone_add_ns()
{
    local rzone="$1";shift
    local zone="$1";shift

    ddns_record_update add $rzone $ddns_ttl NS ns.$zone
}

# usage: ddns_reverse_zone_del_ns <reverse-zone> <domain>
# remove ns entry of specified domain from reverse zone
ddns_reverse_zone_del_ns()
{
    local rzone="$1";shift
    local zone="$1";shift

    ddns_record_update delete $rzone NS ns.$zone
}

# usage: ddns_reverse_zone_list_ns <reverse-zone>
# list domains supported by reverse zone (using ns records)
ddns_reverse_zone_list_ns()
{
    local rzone="$1";shift
    dig  @localhost ns "$rzone" +short|
	sed \
	    -e 's/^ns\.//'\
	    -e 's/\.$//'
}

# usage: ddns_reverse_zone_list_ptr <reverse-zone>
# return list of ptr supported by reverse zone
ddns_reverse_zone_list_ptr()
{
    local rzone="$1";shift
    dig -t AXFR @localhost "$rzone"|
	sed -n \
	    -e 's/^\([.a-zA-Z0-9_-]\+\.in-addr\.arpa\)\.[[:space:]]\+[0-9]\+[[:space:]]\+IN[[:space:]]\+PTR[[:space:]]\+\([.a-zA-Z0-9_-]\+\)\./\1\t\2/p'
}

# usage: ddns_reverse_zone_add_ptr <reverse-ip.in-addr-arpa> <host>
# add ptr into reverse zone
ddns_reverse_zone_add_ptr()
{
    local rzone="$1";shift
    local rname="$1";shift

    ddns_record_update add $rzone $ddns_ttl PTR $rname
}

# usage: ddns_reverse_zone_del_ptr <reverse-ip.in-addr-arpa> <host>
# remove ptr from reverse zone
ddns_reverse_zone_del_ptr()
{
    local rzone="$1";shift
    local rname="$1";shift

    ddns_record_update delete $rzone PTR $rname
}

# usage: ddns_reverse_zone_sync_soa <reverse-zone>
ddns_reverse_zone_sync_soa()
{
    local rzone="$1";shift
    local rest_zone="$(ddns_reverse_zone_list_ns "$rzone"|tail -n1)"
    local new_tail="$(dig -t SOA @localhost "$rzone" +short|awk '{printf "%s %s %s %s %s\n",($3+1),$4,$5,$6,$7;}')"
    ddns_record_update add $rzone $ddns_ttl SOA ns.$rest_zone root.$rest_zone $new_tail
}

### forward zones

# usage: ddns_forward_zone_add_a <ip> <host.domain>
# add A record to zone
ddns_forward_zone_add_a()
{
    local ip="$1";shift
    local host="$1";shift

    ddns_record_update add $host $ddns_ttl A $ip
}

# usage: ddns_forward_zone_del_a <ip> <host.domain>
ddns_forward_zone_del_a()
{
    local ip="$1";shift
    local host="$1";shift

    ddns_record_update delete $host A $ip
}

# usage: ddns_forward_zone_has_a <ip> <host.domain>
ddns_forward_zone_has_a()
{
    local ip="$1";shift
    local host="$1";shift

    dig @localhost a "$host" +short|fgrep -wqs "$ip"
}

# usage: ddns_forward_zone_list_a <domain>
# list available a records from forward zone
ddns_forward_zone_list_a()
{
    local zone="$1";shift

    dig -t AXFR @localhost "$zone"|
	sed -n \
	    -e 's/^\([.a-zA-Z0-9_-]\+\)\.[[:space:]]\+[0-9]\+[[:space:]]\+IN[[:space:]]\+A[[:space:]]\+\([0-9.]\+\)/\2\t\1/p'
}

# usage: ddns_reverse_zone_add_ns <zone> <host>
# add NS entry of specified domain to forward zone
ddns_forward_zone_add_ns()
{
    local zone="$1";shift
    local host="$1";shift

    ddns_record_update add $zone $ddns_ttl NS $host
}

# usage: ddns_reverse_zone_del_ns <zone> <host>
# remove ns entry of specified domain from forward zone
ddns_forward_zone_del_ns()
{
    local zone="$1";shift
    local host="$1";shift

    ddns_record_update delete $zone NS $host
}

# usage: ddns_reverse_zone_list_ns <zone>
# list ns entries of specified domain (according forward zone)
ddns_forward_zone_list_ns()
{
    local zone="$1";shift
    dig  @localhost ns "$zone" +short|
	sed -e 's/\.$//'
}

# usage: ddns_forward_zone_list_mx <zone>
# list available mx records (with weights)
ddns_forward_zone_read_mx()
{
    local zone="$1";shift
    dig -t MX @localhost "$zone" +short|
	sort -k1,1 -n|
	sed -e 's/\.$//'
}

# usage: ddns_forward_zone_write_mx <zone>
# replace mx entries with a new one
# read list from stdin, each line constains weight and name
ddns_forward_zone_write_mx()
{
    local zone="$1";shift
    ddns_forward_zone_read_mx "$zone"|
	while read weight host;do
	    ddns_record_update delete $zone MX $weight $host
	done

    while read weight host; do
	ddns_record_update add $zone $ddns_ttl MX $weight $host
    done
}

### reverse domains, each reverse domain can contain multiple reverse zones

# reverse ip address, helper for reverse zone processing
__reverse_ip()
{
    local IFS='.'
    set $1
    echo "$4.$3.$2.$1"
}

# usage: ddns_reverse_domain_add_host <zone> <ip> <name>
# add host<->ip binding to reverse zone of dynamic dns domain
ddns_reverse_domain_add_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$1";shift
    local rzone="$(__reverse_ip "$ip").in-addr.arpa"

    if [ "$name" = "." ];then
	name="$zone"
    else
	name="$name.$zone"
    fi

    ddns_reverse_zone_add_ptr "$rzone" "$name"
}

# usage: ddns_reverse_domain_del_host <zone> <ip> <name>
# remove host<->ip binding from reverse zone of dynamic dns domain
ddns_reverse_domain_del_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$1";shift
    local rzone="$(__reverse_ip "$ip").in-addr.arpa"

    if [ "$name" = "." ];then
	name="$zone"
    else
	name="$name.$zone"
    fi

    ddns_reverse_zone_del_ptr "$rzone" "$name"
}


# usage: ddns_reverse_domain_add_net <zone> <ip-address/mask>
# add network to reverse zone of dynamic dns domain
ddns_reverse_domain_add_net()
{
    local zone="$1";shift
    local ip="$1";shift
    local rev_ip="$(__reverse_ip "${ip%%/*}")"
    local tail_ip=
    local rev_zone=
    local ddns_rzone=
    local system_name="$(ddns_system_name)"

    revdns $ip|
	while read rev_zone;do
	    ddns_rzone="$rev_zone.in-addr.arpa"
	    tail_ip="${rev_ip%.$rev_zone}"

	    if bind_domain_exists "$ddns_rzone";then
		# add an additional ns entry and binding to hostname to existing reverse zone
		ddns_reverse_zone_add_ns "$ddns_rzone" "$zone"
		[ "$tail_ip" = "$rev_ip" ] || ddns_reverse_domain_add_host "$zone" "${ip%%/*}" "$system_name"
	    else
		# create new reverse zone
		bind_domain_add "$ddns_domain_file" "$ddns_rzone"<<EOF
    type master;
    file "$ddns_domain_dir/$ddns_rzone";
    allow-update { key $ddns_key; };
EOF

		local ddns_ns_record="ns.$zone."
		local ddns_user_record="root.$zone."

		bind_zone_add "$ddns_domain_dir/$ddns_rzone"<<EOF
\$TTL	1D
@	IN	SOA	$ddns_ns_record $ddns_user_record ($(bind_serial_first) 12H 1H 1W 1H)
	IN	NS	$ddns_ns_record
$([ "$tail_ip" = "$rev_ip" ] || printf '%s\t\t\tPTR\t%s.' "$tail_ip" "$system_name.$zone")
EOF
	    fi
	done
}

# usage: ddns_reverse_domain_del_net <zone> <ip-address/mask>
# remove network from reverse zone of dynamic dns domain
ddns_reverse_domain_del_net()
{
    local zone="$1";shift
    local ip="$1";shift
    local ddns_rzone=
    revdns $ip|
	while read ddns_rzone;do
	    ddns_rzone="$ddns_rzone.in-addr.arpa"

	    local rzone_count="$(ddns_reverse_zone_list_ns "$ddns_rzone"|wc -l)"
	    if [ "$rzone_count" = "1" ];then
		# no more domains in this reverse zone, free resource
		bind_domain_del "$ddns_domain_file" "$ddns_rzone"
		bind_zone_del "$ddns_domain_dir/$ddns_rzone"
	    else
		# remove appropriate domain from this zone

		local rptr=
		local rname=
		#clear pointers for our domain
		ddns_reverse_zone_list_ptr "$ddns_rzone"|
		    while read rptr rname; do
			[ "${rname%%.$zone}" = "$rname" ] ||
			    ddns_reverse_zone_del_ptr "$rptr" "$rname"
		    done

		# remove ns
		ddns_reverse_zone_del_ns "$ddns_rzone" "$zone"

		# update soa, soa should be equal to rest ns
		ddns_reverse_zone_sync_soa "$ddns_rzone"
	    fi
	done
}

### forward domains, each forward domain contain one forward zone

# usage: ddns_create_forward_domain <zone> <type>
# create new forward zone of dynamic dns domain
# type is equal to master or slave
ddns_create_forward_domain()
{
    local zone="$1";shift
    local type="$1";shift

    case "$type" in
	master)
	    ddns_create_key "$zone-transfer-key"
	    bind_domain_add "$ddns_domain_file" "$zone"<<EOF
    type master;
    file "$ddns_domain_dir/$zone";
    allow-update { key $ddns_key; };
    allow-transfer { localhost; key $zone-transfer-key; };
    notify yes;
EOF

	    local ddns_ns_record="ns.$zone."
	    local ddns_user_record="root.$zone."
	    local system_name="$(ddns_system_name)"
	    bind_zone_add "$ddns_domain_dir/$zone"<<EOF
\$TTL	1D
@	IN	SOA	$ddns_ns_record	$ddns_user_record ($(bind_serial_first) 12H 1H 1W 1H)
	IN	NS	$ddns_ns_record
;
_kerberos._udp		SRV	0	0	88	$system_name
_kerberos._tcp		SRV	0	0	88	$system_name
_kerberos-adm._tcp	SRV	0	0	749	$system_name
_kerberos		TXT	$(echo $zone| tr '[[:lower:]]' '[[:upper:]]')
EOF
	;;
	slave)
	    control bind-slave enabled
	    bind_domain_add "$ddns_domain_file" "$zone"<<EOF
    type slave;
    file "slave/$zone";
    allow-transfer { localhost; key $zone-transfer-key; };
    masters { };
EOF
	;;
    esac
}

# usage: ddns_destroy_forward_domain <zone>
# destroy forward zone of dynamic dns domain
ddns_destroy_forward_domain()
{
    local zone="$1";shift

    ddns_destroy_key "$zone-transfer-key";
    bind_domain_del "$ddns_domain_file" "$zone"
    bind_zone_del "$ddns_domain_dir/$zone"
    bind_zone_del "slave/$zone"
}

# usage: ddns_list_forward_domain
# list available forward domains
ddns_list_forward_domain()
{
    bind_domain_list "$ddns_domain_file"|grep -v '.in-addr.arpa$'
}

# usage: ddns_forward_domain_list_host <zone>
# return list of hosts of dynamic dns domain (forward zone)
ddns_forward_domain_list_host()
{
    local zone="$1";shift

    ddns_forward_zone_list_a "$zone"|
	sed -e "s/\.$zone//" \
	    -e "s/$zone/./"
}

__full_name()
{
    local zone="$1";shift
    local name="$1";shift

    if [ "$name" = "." ];then
	echo "$zone"
    else
	echo "$name.$zone"
    fi
}

# usage: ddns_forward_domain_del_host <zone> <ip> <host>
# remove host<->ip binding from dynamic dns domain (forward zone)
ddns_forward_domain_del_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$(__full_name "$zone" "$1")";shift

    ddns_forward_zone_del_a "$ip" "$name"
}

# usage: ddns_forward_domain_add_host <zone> <ip> <host>
# add host<->ip binding into dynamic dns domain (forward zone)
ddns_forward_domain_add_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$(__full_name "$zone" "$1")";shift

    ddns_forward_zone_add_a "$ip" "$name"
}

# usage: ddns_forward_domain_has_host ip host
# check that some ip<->host binding exists
ddns_forward_domain_has_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$(__full_name "$zone" "$1")";shift

    ddns_forward_zone_has_a "$ip" "$name"
}

# usage: ddns_forward_domain_add_net <zone> <ip-address/mask>
# add network to forward zone of dynamic dns domain,
# add map standard zone names to this ip address
ddns_forward_domain_add_net()
{
    local zone="$1";shift
    local ip="$1";shift
    local simple_ip="${ip%%/*}"

    for name in '.' "$(ddns_system_name)" $ddns_std_namelist; do
	ddns_forward_domain_add_host "$zone" "$simple_ip" "$name"
    done
}

# usage: ddns_forward_domain_del_net <zone> <ip-address/mask>
# remove network to forward zone of dynamic dns domain,
# see zone and remove mappings to ip addresses from this network
ddns_forward_domain_del_net()
{
    local zone="$1";shift
    local ip="$1";shift

    local name
    local address

    ddns_forward_domain_list_host "$zone"|
    while read address name; do
	    ipv4addr_is_in_subnet "$address" "$ip" || continue
	    ddns_forward_domain_del_host "$zone" "$address" "$name"
    done
}

### joint reverse and forward domains

# usage: ddns_domain_del_net <zone> <ip-address/mask>
# remove network both from forward and reverse zones
ddns_domain_del_net()
{
    local zone="$1";shift

    ddns_forward_domain_del_net "$zone" "$1"
    ddns_reverse_domain_del_net "$zone" "$1"
}

# usage: ddns_domain_add_net <zone> <ip-address/mask>
# add network both to forward and reverse zones
ddns_domain_add_net()
{
    local zone="$1";shift

    ddns_forward_domain_add_net "$zone" "$1"
    ddns_reverse_domain_add_net "$zone" "$1"
}

__check_forward()
{
    local ip="$1";shift
    local zone="$1";shift
    ddns_forward_domain_has_host "$zone" "${ip%%/*}" "." && echo "$ip" 2>/dev/null
}

ddns_domain_list_net()
{
    local zone="$1";shift

    ddns_net_foreach __check_forward "$zone"
}

# usage: ddns_domain_list_host <zone>
# get domain contents
ddns_domain_list_host()
{
    local zone="$1";shift

    ddns_forward_domain_list_host "$zone"
}

# usage: ddns_domain_has_host <zone> <ip> <host>
# check for host existance
ddns_domain_has_host()
{
    local zone="$1";shift

    ddns_forward_domain_has_host "$zone" "$@"
}

# usage: ddns_domain_is_reserved_host host
# check for reserved host names
ddns_domain_is_reserved_host()
{
    local name="$1";shift
    local namelist=" . $ddns_std_namelist localhost $(ddns_system_name) "

    [ -z "${namelist##* $name *}" ]
}

__valid_ip()
{
    local zone="$1";shift
    local ip="$1";shift
    local rip="$(__reverse_ip "$ip")"
    local net=
    local rzone=

    # check for appropriate domain in all available reverse networks
    for rzone in "$bind_root_dir/zone/$ddns_domain_dir/"*.in-addr.arpa; do
	rzone="${rzone##*/}"
	net="${rzone%%.in-addr.arpa}"
	[ "${rip%%$net}" != "$rip" ] || continue
	ddns_reverse_zone_list_ns "$rzone"|fgrep -wqs "$zone" && return 0
    done

    return 1
}


# usage: ddns_domain_add_host <zone> <ip> <host>
# add ip<->hostname binding
ddns_domain_add_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$1";shift

    ! __valid_ip "$zone" "$ip" || ddns_reverse_domain_add_host "$zone" "$ip" "$name"
    ddns_forward_domain_add_host "$zone" "$ip" "$name"
}

# usage: ddns_domain_de_host <zone> <ip> <host>
# add ip<->hostname binding
ddns_domain_del_host()
{
    local zone="$1";shift
    local ip="$1";shift
    local name="$1";shift

    ddns_reverse_domain_del_host "$zone" "$ip" "$name" || :
    ddns_forward_domain_del_host "$zone" "$ip" "$name"
}

__add_reverse()
{
    local ip="$1";shift
    local zone="$1";shift
    ddns_reverse_domain_add_net "$zone" "$ip"
}

__add_forward()
{
    local ip="$1";shift
    local zone="$1";shift
    ddns_forward_domain_add_net "$zone" "$ip"
}

# usage: ddns_create_domain <zone> <type> [<fill-reverse>]
# create specified domain,
# type is equal to master or slave
# if optional parameter is equal to 1 then appropriate reverse zones will be also created
ddns_create_domain()
{
    local zone="$1";shift
    local type="$1";shift
    local fill_reverse="${1:-}"

    ddns_key_exist "$ddns_key" || ddns_create_key "$ddns_key" 1
    ddns_create_forward_domain "$zone" "$type"
    if [ "$type" = "master" -a "$fill_reverse" = "1" ] ;then
	ddns_net_foreach __add_reverse "$zone"
	/sbin/service bind condreload >&2 # update forward zone info
	ddns_net_foreach __add_forward "$zone"
    fi
}


# usage: ddns_destroy_domain <zone>
# destroy specified domain and release all it's resources
ddns_destroy_domain()
{
    local zone="$1";shift
    local net=

    # release networks
    ddns_domain_list_net "$zone"|
	while read net;do
	    ddns_reverse_domain_del_net "$zone" "$net"
	    ddns_forward_domain_del_net "$zone" "$net"
	done
    # destroy forward zone
    ddns_destroy_forward_domain "$zone"
}

ddns_domain_exists()
{
    bind_domain_exists "$1"
}

ddns_list_domain()
{
    ddns_list_forward_domain
}

ddns_domain_list_ns()
{
    local zone="$1";shift
    ddns_forward_zone_list_ns "$zone"
}

ddns_domain_add_ns()
{
    ddns_forward_zone_add_ns "$@"
}

ddns_domain_del_ns()
{
    ddns_forward_zone_del_ns "$@"
}

ddns_domain_read_mx()
{
    local zone="$1";shift
    local mx="$(ddns_forward_zone_read_mx "$zone"|sed 's/^[[:space:]]*[0-9]\+[[:space:]]\+//'|tr '\n' ' ')"
    echo "${mx% }"
}

ddns_domain_write_mx()
{
    local zone="$1";shift
    local hostlist="$1";shift
    local weight=10;

    echo "$hostlist"|
	tr ' ' '\n'|
	sed '/^[[:space:]]*$/d'|
	while read host; do
	    printf '%s %s\n' "$weight" "$host"
	    weight=$(($weight + 10))
	done|
	ddns_forward_zone_write_mx "$zone"
}

# usage: ddns_domain_read_type <zone>
# read domain type (master or slave)
ddns_domain_read_type()
{
    local zone="$1";shift
    bind_domain_get "$ddns_domain_file" "$zone" type
}

# usage: ddns_domain_read_master <zone>
# read masters of slave domain
# function returns space separated values
ddns_domain_read_master()
{
    local zone="$1";shift

    bind_domain_get "$ddns_domain_file" "$zone" masters|
	sed -e 's/^[[:space:]]*{[[:space:]]*//' \
	    -e 's/[[:space:]]*}[[:space:]]*$//' \
	    -e "s/[[:space:]]\+key[[:space:]]\+[^[:space:]]\+[[:space:]]*;[[:space:]]*/ /g"
}

# usage: ddns_domain_write_master <zone> <slave-list>
# write masters of slave domain
# <slave-list> is a space separated host list
ddns_domain_write_master()
{
    local zone="$1";shift
    local value="$1";shift

    local str="{"
    for i in $value;do
	str="$str $i key $zone-transfer-key;"
    done
    str="$str }"

    bind_domain_set "$ddns_domain_file" "$zone" masters "$str"
}
