#!/bin/sh
# vi(1) :se tabstop=2
rc_file=/usr/local/bin/._acme-challenge_helperrc

set -e

rc=0

LC_ALL=C export LC_ALL

my_prog_basename="$(basename "$0")"

. "$rc_file"

if [ -n "$CERTBOT_TOKEN" ]; then
	# HTTP-01
	_acme-challenge_helper add
	exit
fi
# DNS-01

unset DNS_defer_checks # set to defer DNS checks until later
case "$CERTBOT_REMAINING_CHALLENGES" in
	'')
		:
	;;
	0)
		[ -f "$GETCERTS_TMPF" ] || {
			echo "missing file GETCERTS_TMPF, aborting" 1>&2
			exit 1
		}
	;;
	*)
		if expr x"$CERTBOT_REMAINING_CHALLENGES" : 'x[1-9][0-9]*$' \
			>>/dev/null; then
			[ -f "$GETCERTS_TMPF" ] || {
				echo "missing file GETCERTS_TMPF, aborting" 1>&2
				exit 1
			}
			DNS_defer_checks= # set to defer DNS checks until later
		else
			echo "unexpected value" \
				"CERTBOT_REMAINING_CHALLENGES$=CERTBOT_REMAINING_CHALLENGES," \
				aborting 1>&2
			exit 1
		fi
	;;
esac

maxt=$(expr $(date +'%s') + "$DNS_max_seconds")

PID="$$"
my_maxt_timeout(){
	[ $(date +'%s') -le "$maxt" ] ||
		kill -14 "$PID" # trigger our timeout
}

_acme-challenge_helper add

[ -z "$CERTBOT_REMAINING_CHALLENGES" ] ||
		echo "$CERTBOT_DOMAIN $CERTBOT_VALIDATION ${CERTBOT_TOKEN:--}" >> "$GETCERTS_TMPF"

# We're done for now if we're deferring DNS checks:
{ [ -n "${DNS_defer_checks+y}" ] && exit; } || :

# We have DNS to check

# temporary directory for storing main DNS results,
# and cleanup thereof:
unset gotsig
for s in 1 2 3 14 15
do
	trap "gotsig=$s" "$s"
done
tmpd=$(mktemp -d)
# traps to handle our exits and possibly pending alarm
trap \
	'
		x="$?"
		[ "$x" -eq 0 ] ||
			rc="$x"
		trap - 0
		rm -rf "$tmpd" ||
			exit
		exit "$rc"
	' \
	0
trap \
	'
		trap - 0 14
		rm -rf "$tmpd" || exit
		echo "$my_prog_basename: TIMED OUT ON DNS CHECKS" 1>&2
		kill -14 "$$"
		exit 1 # should be unreachable
	' \
	14
for sig in 1 2 3 15
do
	trap \
		'
			trap - 0
			rm -rf "$tmpd" ||
				exit
			trap - '"$sig"'
			kill -'"$sig"' "$$"
			exit 1 # should be unreachable
		' \
		"$sig"
done

# if we deferred, we may have more than one (set) to check
if [ -n "$CERTBOT_REMAINING_CHALLENGES" ] && [ -f "$GETCERTS_TMPF" ]
then
	set -- $(cat "$GETCERTS_TMPF")
else
	set -- "$CERTBOT_DOMAIN" "$CERTBOT_VALIDATION"
fi

my_maxt_timeout
sleep "$sleep_before_start_DNS_checks"
my_maxt_timeout

# Do we have IPv6 DNS connectivity to The Internet?
# Check here once, rather than per loop further below
if \
	# If mymanual_auth_hook_NO_IPV6 is set (e.g. from environment),
	# then presume we don't have access to (reliably) check IPv6
	# DNS.
	# This will also aviod redundant per RR checks.
	[ -z "${mymanual_auth_hook_NO_IPV6+n}" ]
	# NSIPv6=$(dig +short dns.google.com. AAAA) &&
	# NSIPv6=$(set -- $NSIPv6; echo "$1") &&
	# [ -n "$NSIPv6" ] &&
	# AAAA=$(dig +short @"$NSIPv6" www.google.com. AAAA) &&
	# [ -n "$AAAA" ] &&
	# # and TCP:
	# AAAA=$(dig +short +tcp @"$NSIPv6" www.google.com. AAAA) &&
	# [ -n "$AAAA" ]
then
	# yes, also use IPv6 (AAAA)
	my_get_NS_IPs(){
		for ns in $NSs; do
			my_maxt_timeout
			dig +short +norecurse @"$NS" "$ns" AAAA "$ns" A
			my_maxt_timeout
		done |
		tr A-Z a-z |
		sort -u
	}
else
	# no, just use IPv4 (don't ask for AAAA)
	echo "$my_prog_basename: WARNING: skipping IPv6 NS checks" 1>&2
	my_get_NS_IPs(){
		for ns in $NSs; do
			my_maxt_timeout
			dig +short +norecurse @"$NS" "$ns" A
			my_maxt_timeout
		done |
		tr A-Z a-z |
		sort -u
	}
fi

# do remaining DNS checks
while :; do
	# more to check?:
	[ "$#" -ne 0 ] || {
		# (hopefully) avoid potential negative caching issues:
		sleep "$sleep_after_DNS_checks"
		if [ -z "$delegate" ]; then
			exit "$rc"
			# program that created GETCERTS_TMPF will also clean that up
		else
			# Since we're delegated, we also need check that delegating zones
			# are caught up to same SOA SERIAL, otherwise they may not (yet)
			# have our delegation as authoritative and could give NXDOMAIN
			# for our delegated (sub)domain!
			break
		fi
	}
	CERTBOT_DOMAIN="$1"; shift
	CERTBOT_VALIDATION="$1"; shift
	CERTBOT_TOKEN="$1"; shift
	case "$CERTBOT_TOKEN" in -) CERTBOT_TOKEN=;; esac
	. "$rc_file" # set NS, etc. accordingly for CERTBOT_DOMAIN

	# check that the data has made it to all the NS IPs,
	# we wait (sleep) and recheck until it's there

	# Check for NS records, "bottom up", starting with
	# _acme-challenge.$CERTBOT_DOMAIN up to $domain
	# notably in case of delegated zone(s)/domain(s)
	nsdomain=_acme-challenge."$CERTBOT_DOMAIN"
	while :; do
		my_maxt_timeout
		NSs=$(dig +short +norecurse @"$NS" "$nsdomain". NS)
		my_maxt_timeout
		{ [ -n "$NSs" ] && break; } || :
		[ x"$nsdomain" != x"$domain" ] || {
			echo "$my_prog_basename: failed to retrieve NS records for" \
				"_acme-challenge.$CERTBOT_DOMAIN. through $domain." 1>&2
			exit 1
		}
		# try next level up
		nsdomain="$(expr x$nsdomain. : x'[^.][^.]*\.\(..*\)\.'$)"
	done

	while :; do
		IPs="$(my_get_NS_IPs)"
		{ [ -n "$IPs" ] && break; } || {
			echo "$my_prog_basename: WARNING: no NS IPs to check for" \
				"$CERTBOT_DOMAIN" 1>&2
			my_maxt_timeout
			sleep "$sleep_before_DNS_rechecks"
			my_maxt_timeout
		}
	done

	# while we still have IP(s) to (re)check, (re)check them as needed
	while [ "$(set -- $IPs; echo "$#")" -ge 1 ]; do
		(	# subshell to limit scope of PID(s) waited for
			my_maxt_timeout
			for IP in $IPs; do
				{
					dig +noall +norecurse +answer +comments @"$IP" \
						_acme-challenge."$CERTBOT_DOMAIN". TXT \
						> "$tmpd/$IP" 2>&1 ||
					echo FAILED >> "$tmpd/$IP" # track that query failed
				} & # asyncronous/parallel for speed!
			done
			wait # wait for above asyncronous/parallel to finish
			my_maxt_timeout
		)
		for IP in $IPs; do
			if \
				perl -e '
					use warnings;
					$^W=1;
					use strict;

					while(<>){
						chomp;
						# exit 0 if matched:
						exit 0 if
							/
								\A
								_acme-challenge
								\.
								(?i:\Q'"$CERTBOT_DOMAIN"'\E)
								\.
								[\ \t]+
								\d+
								[\ \t]+
								IN
								[\ \t]+
								TXT
								[\ \t]+
								\Q"'"$CERTBOT_VALIDATION"'"\E
								\z
							/x
						;
					};
					exit 1; # exit non-zero if no matches
				' \
				"$tmpd/$IP"
			then
				# matched, remove "$IP" from "$IPs"
				IPs=$(
					for i in $IPs
					do
						[ x"$i" = x"$IP" ] ||
						echo "$i"
					done
				) || :
			fi
			rm "$tmpd/$IP" # done with the file, remove it
			[ -n "$IPs" ] ||
				continue 2 # no IPs left to check
		done
		# we have IP(s) to recheck
		my_maxt_timeout
		sleep "$sleep_before_DNS_rechecks"
		my_maxt_timeout
	done
	# we check and exit loop at top, as we may also use continue
done
{
	[ -z "$delegate" ] &&
		exit "$rc"
} || :
# Since we're delegated, we also need check that delegating zones
# are caught up to same SOA SERIAL, otherwise they may not (yet)
# have our delegation as authoritative and could give NXDOMAIN
# for our delegated (sub)domain!
# TO DO - ADD THAT HANDLING HERE!
# But then why even delegate, if the purpose of that is to aviod
# potential issues with authoritatives for delegating potentially
# not being caught up?  So, maybe just don't delegate?
exit "$rc"
