#!/bin/sh
# vi(1) :se tabstop=2

# _acme-challenge_helper {add|del|delete}

# This program must be run as:
#  root or
#  user bind
#  or if not using delegate,
#  member of group bind, or
#  can ssh to do as in the above set.

# First argument must be one of these:
#  add
#  del
#  delete

set -e

LC_ALL=C export LC_ALL

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
export PATH

# So we can later exec or refer back to ourself:
my_prog=/usr/local/bin/_acme-challenge_helper
my_prog_basename="$(basename "$my_prog")"

rc_file="$(dirname "$my_prog")/.${my_prog_basename}rc"

# check our arguments
case "$#.$1" in
	1.add|1.del)
		add_or_del=$1
	;;
	1.delete)
		add_or_del=del
	;;
	*)
		1>&2 \
		printf '%s\n%s\n' \
			'usage:' \
			"$my_prog_basename { add | del | delete }"
		exit 1
	;;
esac

case "$add_or_del" in
	add)
		# check CERTBOT_VALIDATION
		if [ -z "$CERTBOT_TOKEN" ]; then
			# DNS-01 challenge (not HTTP-01 challenge)
			re='[-0-9A-Z_a-z]\{43\}$'
		else
			# HTTP-01 challenge
			re='[-.0-9A-Z_a-z]\{87\}$'
		fi
		expr x"$CERTBOT_VALIDATION" : "x$re" >>/dev/null \
		||
		{
			echo "$my_prog_basename: invalid value:" \
				"CERTBOT_VALIDATION=$CERTBOT_VALIDATION" 1>&2
			exit 1
		}
	;;
esac

. "$rc_file"

[ -z "$host" ] || {
	# need to do it on other host
	env=
	env="$env${CERTBOT_DOMAIN:+ CERTBOT_DOMAIN=$CERTBOT_DOMAIN}"
	env="$env${CERTBOT_TOKEN:+ CERTBOT_TOKEN=$CERTBOT_TOKEN}"
	env="$env${CERTBOT_VALIDATION:+ CERTBOT_VALIDATION=$CERTBOT_VALIDATION}"
	exec ssh -ax "$host" "${env:+$env }$my_prog_basename $*"
	echo "$my_prog_basename: exec ssh -ax $host" \
		"${env:+$env }$my_prog_basename${*:+ $*} failed, aborting" 1>&2
	exit 1
}

# At this point, we've validated our arguments and are on correct host

# This program must be run as:
#  root or
#  user bind
#  or if not using delegate,
#  member of group bind
# root or
[ x0 = x$(id -u) ] ||
{
	# not root/superuser,
	if [ -z "$CERTBOT_TOKEN" ]; then
		# DNS-01 challenge (not HTTP-01 challenge)
		if ! [ -n "$delegate" ]; then
			# not delegate
			case "$(id -Gn)" in
				bind|bind\ *|*\ bind|*\ bind\ *)
					: # already member of group bind
				;;
				*)
					# not member of group bind, need to be:
					exec sudo -n -g bind \
						"$my_prog" "$@"
					printf '%s %s\n' "$my_prog_basename: exec sudo -n -g bind"
						"$my_prog${*:+ $*} failed, aborting" 1>&2
					exit 1
				;;
			esac
		else
			# delegate
			# user bind or
			[ xbind = x$(id -un) ] || {
				# not user bind, need to be:
				exec sudo -n -u bind \
					"$my_prog" "$@"
				printf '%s %s\n' "$my_prog_basename: exec sudo -n -u bind"
					"$my_prog${*:+ $*} failed, aborting" 1>&2
				exit 1
			}
		fi
	else
		# HTTP-01 challenge
		# not root, need to be:
		exec sudo -n "$my_prog" "$@"
		printf '%s %s\n' "$my_prog_basename: exec sudo -n"
			"$my_prog${*:+ $*} failed, aborting" 1>&2
		exit 1
	fi
}

if [ -z "$CERTBOT_TOKEN" ]; then
	# DNS-01 challenge (not HTTP-01 challenge)
	case "$add_or_del" in
		add)
			if [ -z "$delegate" ]; then
				# don't delegate, just add the record
				printf '%s %s %s\nsend\n' 'update' \
					"add _acme-challenge.$CERTBOT_DOMAIN. $TTL_TXT IN TXT" \
					"\"$CERTBOT_VALIDATION\"" |
				nsupdate -k "$named_key_directory"/ddns-key._acme-challenge
			else
				# delegate
				zonestatusrc=0
				zonestatus="$(rndc zonestatus \
					_acme-challenge."$CERTBOT_DOMAIN" 2>&1)" ||
				zonestatusrc="$?"
				if [ "$zonestatusrc" -eq 0 ]; then
					# zone already exists, just add the record
					printf '%s %s %s\nsend\n' 'update' \
						"add _acme-challenge.$CERTBOT_DOMAIN. $TTL_TXT IN TXT" \
						"\"$CERTBOT_VALIDATION\"" |
					nsupdate -k "$named_key_directory"/ddns-key._acme-challenge
				elif [ "$zonestatusrc" -ne 1 ]; then
						1>&2 echo "$my_prog_basename: failed to determine " \
							"zonestatus for _acme-challenge.$CERTBOT_DOMAIN"
						1>&2 printf '%s%s\n%s\n' 'rndc zonestatus ' \
							'_acme-challenge. '"$CERTBOT_DOMAIN $zonestatus"
							"$zonestatus"
						exit 1
				else # [ "$zonestatusrc" -eq 1 ]
					{
						fgrep -x -e \
							'rndc: '\''zonestatus'\'' failed: not found' \
							<<- __EOT__ >>/dev/null &&
								$zonestatus
						__EOT__
						fgrep -x -e \
							'no matching zone '\'_acme-challenge."$CERTBOT_DOMAIN"\'' in any view' \
							<<- __EOT__ >>/dev/null
								$zonestatus
						__EOT__
					} || {
						1>&2 echo "$my_prog_basename: failed to determine " \
							"zonestatus for _acme-challenge.$CERTBOT_DOMAIN"
						1>&2 printf '%s%s\n%s\n' 'rndc zonestatus ' \
							'_acme-challenge. '"$CERTBOT_DOMAIN $zonestatus"
							"$zonestatus"
						exit 1
					}
					# Not yet delegated, delegate
					(
						umask 037 &&
						>"$file"
					)
					> "$file" eval printf \''%s\n'\' \""$file_cont_"\"
					# put in delegating NS record first, otherwise
					# it doesn't make it to the delegating parent zone
					printf '%s %s\nsend\n' \
						"update add _acme-challenge.$CERTBOT_DOMAIN. $TTL" \
						"IN NS $NS" \
					|
					nsupdate -l
					rndc addzone _acme-challenge."$CERTBOT_DOMAIN" \
						"$configuration"
					{
						# no DNSSEC, or
						! [ -n "$DNSSEC" ] ||
						{
							# add DS record(s):
							DSadd="$(
								maxwaitleft=$maxwait
								while :
								do
									{
										DNSKEY="$(
											dig @::1 +norecurse +noall +answer \
												_acme-challenge."$CERTBOT_DOMAIN". DNSKEY
										)" && [ -n "$DNSKEY" ] && break
									} ||
									DNSKEY=
									[ "$maxwaitleft" -gt 0 ] || break
									sleep 1
									maxwaitleft=$((maxwaitleft - 1))
								done
								[ -n "$DNSKEY" ]
								printf '%s\n' "$DNSKEY" |
								dnssec-dsfromkey -f - \
									_acme-challenge."$CERTBOT_DOMAIN" |
								(
									while read -r x
									do
										set -- $x
										printf '%s\n' "$6" |
										grep '^0*1$' >>/dev/null &&
										continue ||
										d="$1"
										shift
										printf '%s\n' "update add $d 1 $*"
									done
								)
							)"
							[ -n "$DSadd" ]
							printf '%s\nsend\n' "$DSadd" |
							nsupdate -l
						}
					} &&
					exit # CERTBOT_VALIDATION in new delegated primary zone
					exit 1 # something failed
				fi
			fi
		;;
		del)
			# not delegated?
			if [ "$NS" != \
					"$(
						NS=::1
						dig @"$NS" +noall +norecurse +answer +short \
						+nomultiline +nosplit \
						_acme-challenge."$CERTBOT_DOMAIN". NS
					)" \
				]
			then
				# not delegated
				# just handle the validation record(s)
				printf '%s %s%s\nsend' "update delete" \
					"_acme-challenge.$CERTBOT_DOMAIN. IN TXT" \
					"${CERTBOT_VALIDATION:+ \"$CERTBOT_VALIDATION\"}" |
				nsupdate -k "$named_key_directory"/ddns-key._acme-challenge
				exit "$?"
			else
				# delegated
				# undo delegation bottom up
				# delete zone (rndc won't let us delete all NS records if
				# the zone still exists, so we need to delete zone first)
				rndc -q delzone -clean _acme-challenge."$CERTBOT_DOMAIN" &&
				{
					# work-around for at least
					# bind9 1:9.11.5.P4+dfsg-5.1+deb10u3 amd64
					# on Debian 10 buster
					# where BIND 9 fails to clean up and remove its .jbk files
					# (rndc sync -clean zone also fails to clean that up)
					# (maybe/hopefully "all better" on Debian 11 bullseye?)
					rm \
					 "$named_directory"/_acme-challenge."$CERTBOT_DOMAIN".jbk \
					 >>/dev/null 2>&1 || :
					# likewise for zone file itself at least on
					# on Debian 13 buster
					rm \
					 "$named_directory"/_acme-challenge."$CERTBOT_DOMAIN" \
					 >>/dev/null 2>&1 || :
				}
				# delete remaining relevant records
				printf '%s\nsend\n' \
					"update delete _acme-challenge.$CERTBOT_DOMAIN." |
				nsupdate -l
			fi
			exit
		;;
		*)
			2>&1 printf '%s %s\n' "$my_prog_basename: internal failure:" \
				"\$add_or_del=$add_or_del"
			exit 1
		;;
	esac
else
	# HTTP-01 challenge
	# sanity check directory
	case "$directory" in
		/*)
			:
		;;
		*)
			2>&1 printf '%s %s\n' "$my_prog_basename: bad value for" \
				"\$directory=$directory, aborting" \
			exit 1
		;;
	esac
	[ -d "$directory" ] || {
		2>&1 printf '%s %s\n' "$my_prog_basename: " \
			"\$directory=$directory not a directory, aborting" \
		exit 1
	}
	case "$directory" in
		*/)
			dir_="$directory".well-known
		;;
		*)
			dir_="$directory"/.well-known
		;;
	esac
	dir="$dir_"/acme-challenge
	# check CERTBOT_VALIDATION
	#if [ -z "$CERTBOT_TOKEN" ]; then
	case "$add_or_del" in
		add)
			(
				umask 022 &&
				mkdir -p "$dir" &&
				printf '%s\n' "$CERTBOT_VALIDATION" > "$dir/$CERTBOT_TOKEN"
			)
		;;
		del)
			rm "$dir/$CERTBOT_TOKEN" 2>>/dev/null || :
			rmdir "$dir" "$dir_" 2>>/dev/null || :
		;;
	esac
fi
