#!/bin/bash

# Copyright 2014-2019 eomanis
# 
# This file is part of yabddnsd.
# 
# yabddnsd is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
# 
# yabddnsd is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with yabddnsd.  If not, see <http://www.gnu.org/licenses/>.

# TODO Sleeping time: Shorter sleep for host IP address checking, longer
#      sleep between updates
#      That way we detect a host IP address change quickly, but we do
#      not spam the dynamic DNS service provider with update requests
#      either
#      Take however into account that the "Url@@url" host IP address
#      detection methods also contact external servers; do not spam
#      those servers
# TODO If --updateProtocol is not specified make an educated guess
#      based on the domain name, for example if the domain name ends
#      with ".duckdns.org" then the update protocol is "DuckDns"
# TODO DNS lookups: Use delv instead of dig
# TODO Bash completion
# TODO Manual page: Split off the part about the systemd service into
#      its own yabddnsd.service.8 page
# TODO Always load a default configuration from "/etc/yabddnsd.conf"
#      and "/etc/yabddnsd.d/*.conf"

# Set shell options
set -o pipefail
set -o noclobber
set -o errexit
# Enable the "nounset" shell option only if this is bash>=4.4, which is
# the minimum version required for proper handling of empty arrays
# Coincidentally the "inherit_errexit" option became available starting
# with bash 4.4 as well, so we can conditionally enable it together
# with "nounset"
if test "${BASH_VERSINFO[0]}" != "" \
  && { test "${BASH_VERSINFO[0]}" -ge 5 \
    || { test "${BASH_VERSINFO[0]}" -ge 4 \
      && test "${BASH_VERSINFO[1]}" -ge 4; \
    }; \
  }; then
	set -o nounset
	shopt -qs inherit_errexit
else
	echo " WARN  Bash version is less than 4.4; this is untested, your mileage may vary; " \
"not enabling shell options \"nounset\" (buggy with arrays) and \"inherit_errexit\" (unsupported)" >&2
fi

# Semantic versioning
declare -r versionMajor=0
declare -r versionMinor=6
declare -r versionPatch=7
declare -r versionLabel=""

getVersion () {
	echo -n "${versionMajor}.${versionMinor}.${versionPatch}"
	test "$versionLabel" != "" && echo "-$versionLabel" || echo ""
}

# getObfuscatedAuthToken authToken
# 
# Obfuscates the given authentication token
getObfuscatedAuthToken () {
	local authToken="$1"; shift
	
	if test ${#authToken} -gt 8; then
		echo -n "$authToken" | sed -re 's/^(.*)....$/\1/;s/./*/g'
		echo "$authToken" | sed -re 's/^.*(....)$/\1/'
	else
		# Too short, showing half or more of the token seems like a bad
		# idea
		# Also the obfuscation code does not handle strings of less than
		# 4 characters properly anyway
		# Therefore just hide the entire string
		echo "$authToken" | sed -re 's/./*/g'
	fi
}

# isStringInRange lowerBorderIncluding upperBorderExcluding string
# 
# Returns with code 0 if the given string is lexicographically
# lowerBorderIncluding <= string < upperBorderExcluding
isStringInRange () {
	local lowerBorderIncluding="$1"; shift
	local upperBorderExcluding="$1"; shift
	local string="$1"; shift
	
	test "$string" '<' "$lowerBorderIncluding" && return 1
	test "$string"  =  "$upperBorderExcluding" && return 1
	test "$string" '>' "$upperBorderExcluding" && return 1
	return 0
}

# containsExactLine lineToFind
# 
# Walks a newline-separated list of strings from STDIN and returns
# with code 0 if any of them literally match lineToFind
containsExactLine () {
	local lineToFind="$1"; shift
	local maxCharsInclLineFeed=$(( ${#lineToFind} + 1 ))
	local line
	
	# The
	# || test "$line" != ""
	# condition ("or if $line is not empty") ensures that the final text
	# line is processed even if it does not end with a line feed
	# For read, the final line terminating without a line feed is an
	# "unexpected end-of-stream" error condition
	# If this occurs it does assign what has been read so far to $line,
	# but then it terminates with a non-zero return code, and that
	# prevents $line from being processed by the loop body unless a step
	# like that is taken
	while read -rsn "$maxCharsInclLineFeed" line || test "$line" != ""; do
		if test "$line" = "$lineToFind"; then
			return 0
		fi
	done
	return 1
}

# joinLines separator [appendFinalNewline]
# 
# Walks a newline-separated list of Strings from STDIN and prints
# them to STDOUT, separated (but not terminated) by separator
# If appendFinalNewline is given and is the string "true", then a final
# trailing newline will be written to STDOUT after all lines have been
# written
joinLines () {
	local separator="$1"; shift
	local appendFinalNewline=false
	if test $# -gt 0; then
		test "true" = "$1" && appendFinalNewline=true
		shift
	fi
	local subsequentIteration=false
	local previousLine
	local line
	
	while read -rs line || test "$line" != ""; do
		if $subsequentIteration; then
			echo -n "$previousLine"
			echo -n "$separator"
		fi
		previousLine="$line"
		subsequentIteration=true
	done
	$subsequentIteration && echo -n "$previousLine"
	$appendFinalNewline && echo ""
}

# isBetterLifetime lifetimeToTest lifetimeReference
# 
# The arguments must be empty, an integer, or the string "forever"
isBetterLifetime () {
	local lifetimeToTest="$1"; shift
	local lifetimeReference="$1"; shift
	
	if ! test "" = "$lifetimeToTest" && ! test "forever" = "$lifetimeToTest" && ! echo "$lifetimeToTest" | grep -E '^[0-9]+$' &> /dev/null; then
		# The lifetime to test is neither empty, nor "forever", nor an
		# integer, therefore it is considered invalid to begin with
		return 1
	fi
	
	if test "" = "$lifetimeToTest"; then
		# An empty lifetime always loses
		false
	elif test "forever" = "$lifetimeToTest"; then
		# A lifetime of "forever" always wins, except against another
		# lifetime of "forever"
		test "forever" != "$lifetimeReference"
	else
		# Integer lifetime
		# Always wins against an empty reference lifetime
		test "" = "$lifetimeReference" && return 0
		# Always loses against a reference lifetime of "forever"
		test "forever" = "$lifetimeReference" && return 1
		# Wins against a lesser integer reference lifetime
		test "$lifetimeToTest" -gt "$lifetimeReference"
	fi
}

# splitBySubstring string substring
# 
# Splits the given string at the first occurrence of the given
# substring and returns both parts in a newline-separated list
# The substring at which the string is split is removed
# If the substring does not occur in the given string, the string is
# returned as-is
splitBySubstring () {
	local string="$1"; shift
	local substring="$1"; shift
	local stringLength="${#string}"
	local substringLength="${#substring}"
	local -i index=0
	local testSubstring
	local -i startIndexRemainder
	
	while test "$index" -lt "$stringLength"; do
		testSubstring="${string:$index:$substringLength}"
		if test "$testSubstring" = "$substring"; then
			break
		fi
		index=$(( index + 1 ))
	done
	if test "$index" -lt "$stringLength"; then
		# Substring was found at $index
		startIndexRemainder=$(( index + substringLength ))
		echo "${string:0:$index}"
		echo "${string:$startIndexRemainder}"
	else
		# Substring was not found
		echo "$string"
	fi
}

# isMethodWithArgument string
isMethodWithArgument () {
	echo "$1" | grep -E '^.+@@.+$' &> /dev/null
}

# getMethodName methodName@@argument
getMethodName () {
	local methodAndArgument="$1"; shift
	
	# Suppress spurious confusing messages like
	# "/usr/bin/yabddnsd: line 219: echo: write error: Broken pipe"
	# caused by "head" terminating before all data has been piped
	# Also, ignore bad exit code 141 caused by SIGPIPE
	splitBySubstring "$methodAndArgument" '@@' 2> /dev/null \
		| head -n 1 || isWhitelistedExitCode $? 141
}

# getMethodArgument methodName@@argument
getMethodArgument () {
	local methodAndArgument="$1"; shift
	
	splitBySubstring "$methodAndArgument" '@@' | tail -n +2
}

# isFunction command
# 
# Returns with code 0 if the given command refers to a function
# (and not, for example, to a shell builtin or an executable)
isFunction () {
	local command="$1"; shift
	local commandType
	
	! commandType="$(type -t "$command")" && return 1
	! test "function" = "$commandType" && return 1
	return 0
}

# isWhitelistedExitCode exitCode [whitelistedExitCode...]
# 
# Returns with code 0 if exitCode is any of the given whitelisted
# possibilities
# If it isn't, returns with exitCode
isWhitelistedExitCode () {
	local exitCode="$1"; shift
	
	while test $# -ge 1; do
		test "$exitCode" -eq "$1" && return 0
		shift
	done
	return "$exitCode"
}

# getIpv4AddrAsHexString ipv4Addr
# 
# Prints the given IPv4 address as an 8-character upper case hexadecimal
# string, such as "0B2D062F" for the IPv4 address "11.45.6.47"
# 
# Prints nothing and returns with code 1 if the given string is not
# an IPv4 address
getIpv4AddrAsHexString () {
	local ipv4Addr="$1"; shift
	local -i -a octets
	local -a octetsHex=()
	local -i index
	local currentWord
	local result
	
	# Input must not be an empty string, must be exactly 1 line of text
	{ test "" != "$ipv4Addr" && test "$(echo "$ipv4Addr" | wc -l)" -eq 1; } || return 1
	# Input must have an IPv4-like structure
	{ echo "$ipv4Addr" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' &> /dev/null; } || return 1
	
	# Convert the IPv4 address to a hexadecimal string, handling each
	# octet individually
	index=0
	result=""
	while read -rsn 4 -d '.' currentWord; do
		test $index -lt 4 || return 1
		# Store the current octet into the integer array
		octets[$index]="$currentWord"
		# Crudely sanity-check the octet
		{ test "${octets[$index]}" != "" && test "${octets[$index]}" -ge 0 && test "${octets[$index]}" -le 255; } || return 1
		# Convert the octet to a 2-character hexadecimal representation
		octetsHex[$index]="$(echo "obase=16; ${octets[$index]}" | bc | sed -re 's/^(.)$/0\1/')"
		# Append the octet's 2-char hexadecimal representation to the
		# hexadecimal string of the IPv4 address
		result="${result}${octetsHex[$index]}"
		index=$(( index + 1 ))
	done < <(echo "${ipv4Addr}.")
	#$verbose && echo "DEBUG  8-character hexadecimal representation of IPv4 address \"$ipv4Addr\" is \"$result\"" >&2
	echo "$result"
}

# isPublicIpv4Addr ipv4Addr
# 
# Returns with code 0 if ipv4Addr is a public IPv4 address
isPublicIpv4Addr () {
	local ipv4Addr="$1"; shift
	local ipv4AddrHex
	
	# We opt for string comparisons instead of integer arithmetic
	# On 32-bit systems bash might use a SInt32 for integers which might
	# overflow for an IPv4 address (we would need an UInt32 at least)
	# Also whatever bash uses, it will most likely be insufficient to
	# hold an IPv6 integer, and it would be nice to use the same kind
	# of procedure for IPv4 and IPv6
	ipv4AddrHex="$(getIpv4AddrAsHexString "$ipv4Addr")" || return 1
	
	# Current network, 0.0.0.0/8
	isStringInRange	"00000000" \
					"01000000" "$ipv4AddrHex" && return 1
	# Former class A private network, 10.0.0.0/8
	isStringInRange	"0A000000" \
					"0B000000" "$ipv4AddrHex" && return 1
	# Shared address space (carrier-grade NAT), 100.64.0.0/10
	isStringInRange	"64400000" \
					"64800000" "$ipv4AddrHex" && return 1
	# Loopback addresses, 127.0.0.0/8
	isStringInRange	"7F000000" \
					"80000000" "$ipv4AddrHex" && return 1
	# Link-local addresses, 169.254.0.0/16
	isStringInRange	"A9FE0000" \
					"A9FF0000" "$ipv4AddrHex" && return 1
	# Former class B private network, 172.16.0.0/12
	isStringInRange	"AC100000" \
					"AC200000" "$ipv4AddrHex" && return 1
	# IETF Protocol assignments, 192.0.0.0/24
	isStringInRange	"C0000000" \
					"C0000100" "$ipv4AddrHex" && return 1
	# TEST-NET-1, 192.0.2.0/24
	isStringInRange	"C0000200" \
					"C0000300" "$ipv4AddrHex" && return 1
	# Former IPv6 to IPv4 relay, 192.88.99.0/24
	isStringInRange	"C0586300" \
					"C0586400" "$ipv4AddrHex" && return 1
	# Former class C private network, 192.168.0.0/16
	isStringInRange	"C0A80000" \
					"C0A90000" "$ipv4AddrHex" && return 1
	# Inter-network communication testing, 198.18.0.0/15
	isStringInRange	"C6120000" \
					"C6140000" "$ipv4AddrHex" && return 1
	# TEST-NET-2, 198.51.100.0/24
	isStringInRange	"C6336400" \
					"C6336500" "$ipv4AddrHex" && return 1
	# TEST-NET-3, 203.0.113.0/24
	isStringInRange	"CB007100" \
					"CB007200" "$ipv4AddrHex" && return 1
	# IP multicast (former class D network), 224.0.0.0/4
	isStringInRange	"E0000000" \
					"F0000000" "$ipv4AddrHex" && return 1
	# Reserved for future use (former class E network), 240.0.0.0/4
	# excepting 255.255.255.255
	isStringInRange	"F0000000" \
					"FFFFFFFF" "$ipv4AddrHex" && return 1
	# Limited broadcast destination address, 255.255.255.255/32
	test "FFFFFFFF" = "$ipv4AddrHex" && return 1
	return 0
}

# getPublicIpv4AddrFromStream
# 
# Scans STDIN for a text line containing a single public IPv4 address
# and, if a single such text line occurred, writes that IPv4 address to
# STDOUT
# If the stream did not contain such a text line, or contained multiple
# such text lines, writes nothing and returns with code 1
# The input stream may for example be a plain-text file or an (X)HTML
# document
getPublicIpv4AddrFromStream () {
	local line
	local candidateAddress
	local result
	
	while read -rsn 500 line || test "$line" != ""; do
		# Continue if the line does not contain an IPv4 address
		echo "$line" | grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' &> /dev/null || continue
		# The line contains an IPv4 address: Extract it
		if echo "$line" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' &> /dev/null; then
			# The line is already an IPv4 address, no extraction
			# required
			candidateAddress="$line"
		else
			# There is an IPv4 address embedded somewhere in the text
			# line (maybe between some HTML tags), and it needs to be
			# extracted
			candidateAddress="$(echo "$line" | sed -re 's/^.*[^0-9.]([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*$/\1/')" || return 1
		fi
		# Continue if the extraction failed
		test "" = "$candidateAddress" && continue
		# Continue if the IPv4 address is not a public address
		! isPublicIpv4Addr "$candidateAddress" && continue
		# Return with code 1 if this is the 2nd line to contain a public
		# IPv4 address
		! test -z ${result+x} && return 1
		# Store the current public IPv4 address
		result="$candidateAddress"
	done
	# Return with code 1 if none of the lines contained a public IPv4
	# address
	test -z ${result+x} && return 1
	$verbose && echo "DEBUG  Public IPv4 address read from stream: \"${result}\"" >&2
	echo "$result"
}

# getIpv4AddrOfThisHost
# 
# Determines this host's public IPv4 address
getIpv4AddrOfThisHost () {
	local methodString
	local methodName
	local -a methodArgumentIfSet
	local functionName
	local result
	
	for methodString in "${detectPublicAddrIpv4[@]}"; do
		if isMethodWithArgument "$methodString"; then
			methodName="$(getMethodName "$methodString")"
			methodArgumentIfSet=( "$(getMethodArgument "$methodString")" )
		else
			methodName="$methodString"
			methodArgumentIfSet=()
		fi
		functionName="getIpv4AddrOfThisHostFrom$methodName"
		$verbose && echo "DEBUG  Getting this host's public IPv4 address from function \"$functionName\"" >&2
		if result="$("$functionName" "${methodArgumentIfSet[@]}")" && test "$result" != ""; then
			$verbose && echo "DEBUG  Public IPv4 address is \"$result\"" >&2
			echo "$result"
			return 0
		fi
	done
	return 1
}

# getIpv4AddrOfThisHostFromFile path
# 
# Extracts this host's public IPv4 address from the given text or
# (X)HTML file
getIpv4AddrOfThisHostFromFile () {
	
	if test $# -eq 0; then
		echo " WARN  Failed to read this host's IPv4 address from file: No file path given as argument" >&2
		return 1
	fi
	getPublicIpv4AddrFromStream < "$1"
}

# getIpv4AddrOfThisHostFromNetDev [networkDevice]
# 
# Determines this host's public IPv4 address using
# ip -family inet -oneline addr show [networkDevice]
getIpv4AddrOfThisHostFromNetDev () {
	local -a networkDeviceIfSet=()
	local currentLine
	local currentNetworkDev
	local currentAddress
	local currentLifetimeSec
	local bestAddress
	local bestLifetimeSec
	
	# Read the network device argument if present
	if test $# -gt 0; then
		networkDeviceIfSet=( "$1" ); shift
	fi
	
	# Extract and validate the IP addresses from the text lines
	while read -rs currentLine; do
		currentAddress="$(echo "$currentLine" | sed -re 's/^.+ inet ([0-9.]+).*$/\1/')" || continue
		currentNetworkDev="$(echo "$currentLine" | sed -re 's/^[^:]+: ([^ ]+) .*$/\1/')" || continue
		currentLifetimeSec="$(echo "$currentLine" | sed -re 's/^.+ valid_lft ([^ ]+).*$/\1/;s/sec$//')" || continue
		isPublicIpv4Addr "$currentAddress" || continue
		$verbose && echo "DEBUG  Public IPv4 address read from network device \"${currentNetworkDev}\": \"${currentAddress}\", valid lifetime (sec): $currentLifetimeSec" >&2
		if test -z ${bestAddress+x} || isBetterLifetime "$currentLifetimeSec" "$bestLifetimeSec"; then
			# This is the first valid public IP address, or a subsequent
			# address that beats the preceding one by valid lifetime
			bestAddress="$currentAddress"
			bestLifetimeSec="$currentLifetimeSec"
		elif test "$currentLifetimeSec" = "$bestLifetimeSec"; then
			# This address has the same lifetime as the preceding one:
			# Ambiguous result, abort without a result
			unset bestAddress
			break
		fi
	done < <(ip -family inet -oneline addr show "${networkDeviceIfSet[@]}" \
				| { grep ' scope global' || true; } \
				| { grep -v ' deprecated' || true; } )
	
	# Return with an error code if no valid address was found
	test -z ${bestAddress+x} && return 1
	# Write the single valid result to STDOUT
	echo "$bestAddress"
}

upnpAvailable=false
if type upnpc &> /dev/null; then
	upnpAvailable=true
	# Determines his host's public IPv4 address using UPNP
	getIpv4AddrOfThisHostFromUpnp () {
		local candidateLines
		local result
		
		candidateLines="$(upnpc -s | grep -E '^ExternalIPAddress = [0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')" || return 1
		if test "" = "$candidateLines" || ! test "$(echo "$candidateLines" | wc -l)" -eq 1; then
			return 1
		fi
		result="$(echo "$candidateLines" | sed -re 's/^ExternalIPAddress = ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/\1/')" || return 1
		if test "" = "$result" || ! test "$(echo "$result" | wc -l)" -eq 1 || ! isPublicIpv4Addr "$result"; then
			return 1
		fi
		echo "$result"
	}
else
	echo " INFO  Public IPv4 address detection method \"Upnp\" cannot be used because the required program \"upnpc\" is not available" >&2
fi

# getIpv4AddrOfThisHostFromUrl url
# 
# Determines this host's public IPv4 address using the given website
# URL
getIpv4AddrOfThisHostFromUrl () {
	
	if test $# -eq 0; then
		echo " WARN  Failed to determine this host's IPv4 address from URL: No URL given as argument" >&2
		return 1
	fi
	wget -q4O - "$1" | getPublicIpv4AddrFromStream
}

# getIpv4AddrsOfDomain domainName [dnsServerIpv4]
# 
# Determines the given domain name's IPv4 addresses
# The addresses are returned in a newline-separated list
# The list is empty if the domain name does not have any IPv4 addresses
# Returns with a non-zero code if an error occurred while resolving the
# domain name's IPv4 addresses
getIpv4AddrsOfDomain () {
	local domainName="$1"; shift
	local dnsServerIpv4IfSet=()
	local commandType
	local result
	
	if test $# -gt 0; then
		dnsServerIpv4IfSet=( "$1" ); shift
	fi
	# Option 1: Ask the custom implementation if available
	if isFunction "getIpv4AddrsOfDomainCustom"; then
		$verbose && echo "DEBUG  Resolving IPv4 addresses of domain name \"$domainName\" using getIpv4AddrsOfDomainCustom" >&2
		if result="$(getIpv4AddrsOfDomainCustom "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
			$verbose && echo -n "DEBUG  IPv4 addresses of domain name \"$domainName\" from getIpv4AddrsOfDomainCustom: " >&2
			$verbose && echo "$result" | joinLines ", " true >&2
			echo "$result"
			return 0
		fi
	fi
	# Option 2: Use dig
	$verbose && echo "DEBUG  Resolving IPv4 addresses of domain name \"$domainName\" using dig" >&2
	if result="$(getIpv4AddrsOfDomainFromDig "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
		$verbose && echo -n "DEBUG  IPv4 addresses of domain name \"$domainName\" from dig: " >&2
		$verbose && echo "$result" | joinLines ", " true >&2
		echo "$result"
		return 0
	fi
	return 1
}

# getIpv4AddrsOfDomainFromDig domainName [dnsServerIpv4]
# 
# Determines the given domain name's IPv4 addresses using dig
# See getIpv4AddrsOfDomain for the function contract
getIpv4AddrsOfDomainFromDig () {
	local domainName="$1"; shift
	local dnsServerIpv4IfSet=()
	
	if test $# -gt 0; then
		dnsServerIpv4IfSet=( "@$1" ); shift
	fi
	dig "${dnsServerIpv4IfSet[@]}" -4 -t A -q "$domainName" \
		| { grep "^${domainName}" || true; } \
		| { grep -E $'\n'IN$'\n'A$'\n''[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true; } \
		| sed -re 's/^.+\tIN\tA\t([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)$/\1/'
}

# updateIpv4AddrIfRequired updateProtocol domainName authTokenIpv4 [dnsServerIpv4]
updateIpv4AddrIfRequired () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local dnsServerIpv4IfSet=()
	local newIpv4Addr
	local currentIpv4Addrs
	
	if test $# -gt 0; then
		dnsServerIpv4IfSet=( "$1" ); shift
	fi
	$verbose && echo "DEBUG  Checking and if required updating the current IPv4 address of domain \"${domainName}\"" >&2
	if ! newIpv4Addr="$(getIpv4AddrOfThisHost)" || test "" = "$newIpv4Addr"; then
		echo " WARN  Unable to determine the new IPv4 address (this host's address), doing nothing" >&2
		return 1
	fi
	if ! currentIpv4Addrs="$(getIpv4AddrsOfDomain "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
		echo " WARN  Unable to look up the current IPv4 addresses of \"$domainName\", doing nothing" >&2
		return 1
	fi
	$verbose && echo -n "DEBUG  The domain name \"$domainName\" has these IPv4 addresses: " >&2
	$verbose && echo "$currentIpv4Addrs" | joinLines ", " true >&2
	if echo "$currentIpv4Addrs" | containsExactLine "$newIpv4Addr"; then
		$verbose && echo "DEBUG  The domain name \"$domainName\" already has this host's IPv4 address \"$newIpv4Addr\", no update required" >&2
		return 0
	fi
	echo " INFO  Domain \"$domainName\": Updating IPv4 address to \"${newIpv4Addr}\" using protocol \"$updateProtocol\"" >&2
	if ! updateIpv4Addr "$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr"; then
		echo "ERROR  An error occurred while updating the IPv4 address of \"$domainName\" using protocol \"$updateProtocol\"" >&2
		return 1
	fi
	return 0
}

# updateIpv4Addr updateProtocol domainName authTokenIpv4 newIpv4Addr
# 
# Sets the domain name's IPv4 address
updateIpv4Addr () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	local updateFunction="updateIpv4AddrWith$updateProtocol"
	
	if ! isFunction "$updateFunction"; then
		echo "ERROR  Update function not implemented: \"${updateFunction}\"" >&2
		return 1
	fi
	"$updateFunction" "$domainName" "$authTokenIpv4" "$newIpv4Addr"
}

# updateIpv4AddrWithDuckDns domainName authTokenIpv4 newIpv4Addr
# 
# Sets the domain name's IPv4 address using the DuckDns protocol
# (duckdns.org)
updateIpv4AddrWithDuckDns () {
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	local authTokenIpv4Obfuscated
	authTokenIpv4Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv4")"
	local updateUrlIpv4="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv4}&ip=${newIpv4Addr}"
	local updateUrlIpv4Obfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv4Obfuscated}&ip=${newIpv4Addr}"
	local response
	local -i wgetExitCode
	
	$verbose && echo "DEBUG  Calling update URL: \"$updateUrlIpv4Obfuscated\"" >&2
	# Return codes related to "head" closing the pipe before "wget" is
	# done writing to it should not be considered an error
	#   3 (wget, I/O error)
	# 141 (wget, received SIGPIPE)
	response="$(wget -q4O - "$updateUrlIpv4" | head --lines 1 --bytes 3 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	$verbose && echo "DEBUG  Response started with \"$response\"" >&2
	test "$response" == "OK" && return 0
	return 1
}

# updateIpv4AddrWithFreeDns domainName authTokenIpv4 newIpv4Addr
# 
# Sets the domain name's IPv4 address using either the FreeDnsV1 or
# FreeDnsV2 protocol, depending on the type of IPv4 authentication
# token given
# (freedns.afraid.org)
updateIpv4AddrWithFreeDns () {
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	
	if test "${#authTokenIpv4}" -eq 24; then
		updateIpv4AddrWithFreeDnsV2 "$domainName" "$authTokenIpv4" "$newIpv4Addr"
	else
		updateIpv4AddrWithFreeDnsV1 "$domainName" "$authTokenIpv4" "$newIpv4Addr"
	fi
}

# updateIpv4AddrWithFreeDnsV1 domainName authTokenIpv4 newIpv4Addr
# 
# Sets the domain name's IPv4 address using the FreeDnsV1 protocol
# (freedns.afraid.org)
updateIpv4AddrWithFreeDnsV1 () {
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	local authTokenIpv4Obfuscated
	authTokenIpv4Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv4")"
	local updateUrlIpv4="${updateUrlBaseFreeDnsV1}${authTokenIpv4}&address=${newIpv4Addr}"
	local updateUrlIpv4Obfuscated="${updateUrlBaseFreeDnsV1}${authTokenIpv4Obfuscated}&address=${newIpv4Addr}"
	local response
	local -i wgetExitCode
	
	$verbose && echo "DEBUG  Calling update URL: \"$updateUrlIpv4Obfuscated\"" >&2
	response="$(wget -q4O - "$updateUrlIpv4" | head --lines 1 --bytes 72 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	$verbose && echo "DEBUG  Response started with \"$response\"" >&2
	echo "$response" | grep -E '^Updated ' &> /dev/null && return 0
	echo "$response" | grep -E '^ERROR: Address [^ ]+ has not changed.$' &> /dev/null && return 0
	return 1
}

# updateIpv4AddrWithFreeDnsV2 domainName authTokenIpv4 newIpv4Addr
# 
# Sets the domain name's IPv4 address using the FreeDnsV2 protocol
# (sync.afraid.org)
updateIpv4AddrWithFreeDnsV2 () {
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	local authTokenIpv4Obfuscated
	authTokenIpv4Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv4")"
	local updateUrlIpv4="${updateUrlBaseFreeDnsV2Ipv4}${authTokenIpv4}/?ip=${newIpv4Addr}"
	local updateUrlIpv4Obfuscated="${updateUrlBaseFreeDnsV2Ipv4}${authTokenIpv4Obfuscated}/?ip=${newIpv4Addr}"
	local response
	local -i wgetExitCode
	
	$verbose && echo "DEBUG  Calling update URL: \"$updateUrlIpv4Obfuscated\"" >&2
	response="$(wget -q4O - "$updateUrlIpv4" | head --lines 1 --bytes 26 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	$verbose && echo "DEBUG  Response started with \"$response\"" >&2
	echo "$response" | grep -E '^Updated ' &> /dev/null && return 0
	echo "$response" | grep -E '^No IP change detected for ' &> /dev/null && return 0
	return 1
}

# getIpv6AddrExpanded ipv6Addr
# 
# Prints the given IPv6 address to STDOUT in its "expanded" notation,
# such as "2001:db8:0:0:0:0:0:fe" for the IPv6 address "2001:db8::fe"
# 
# Upper case characters will be converted to lower case characters
# 
# Prints nothing and returns with code 1 if the given string is not an
# IPv6 address
getIpv6AddrExpanded () {
	local ipv6Addr="$1"; shift
	local comprSectionLoc
	local workingString
	local result
	local index
	
	# Input must not be an empty string, must be exactly 1 line of text
	{ test "" != "$ipv6Addr" && test "$(echo "$ipv6Addr" | wc -l)" -eq 1; } || return 1
	
	# ABCDEF to abcdef
	ipv6Addr="$(echo "$ipv6Addr" | tr 'A-F' 'a-f')"
	
	# Return with code 1 if the input contains illegal characters
	echo "$ipv6Addr" | grep -E '[^0-9abcdef:]' &> /dev/null && return 1
	# Return with code 1 if the input contains ":::"
	echo "$ipv6Addr" | grep -E '[:][:][:]' &> /dev/null && return 1
	# Return with code 1 if the input contains "::" multiple times
	echo "$ipv6Addr" | grep -E '[:][:][^:]+[:][:]' &> /dev/null && return 1
	
	if ! echo "$ipv6Addr" | grep '::' &> /dev/null; then
		# Input does not contain "::"
		# Return the input if it is already an expanded IPv6 address,
		# otherwise return with code 1
		echo "$ipv6Addr" | grep -E '^([0-9abcdef]{1,4}[:]){7}[0-9abcdef]{1,4}$' 2> /dev/null || return 1 && return 0
	fi
	
	if test '::' = "$ipv6Addr"; then
		# Special case "::"
		echo '0:0:0:0:0:0:0:0'
		return 0
	fi
	
	# Determine the :: compressed section's location
	if echo "$ipv6Addr" | grep '^::' &> /dev/null; then
		# Something like "::2001:db8:fe"
		comprSectionLoc=leading
	elif echo "$ipv6Addr" | grep '::$' &> /dev/null; then
		# Something like "2001:db8:fe::"
		comprSectionLoc=trailing
	else
		# Something like "2001:db8::fe"
		comprSectionLoc=enclosed
	fi
	
	# Expand the :: compressed section
	# Up to 7 iterations, which is just enough to expand a "minimal"
	# IPv6 address such as "123::" to "123:0:0:0:0:0:0:0"
	# (The special case "::" has already been dealt with and does not
	# apply here anymore)
	workingString="$ipv6Addr"
	for index in 0 1 2 3 4 5 6; do
		
		# Replace the :: section with a zero segment, which might yield
		# an expanded IPv6 address
		if test leading = $comprSectionLoc; then
			result="$(echo "$workingString" | sed -re 's/[:][:]/0:/')"
		elif test trailing = $comprSectionLoc; then
			result="$(echo "$workingString" | sed -re 's/[:][:]/:0/')"
		else
			result="$(echo "$workingString" | sed -re 's/[:][:]/:0:/')"
		fi
		
		#$verbose && echo "DEBUG  IPv6 expansion intermediate result: \"$result\"" >&2
		# Return the result if it is now an expanded IPv6 address
		echo "$result" | grep -E '^([0-9abcdef]{1,4}[:]){7}[0-9abcdef]{1,4}$' 2> /dev/null && return 0
		
		# The result is not (yet) an expanded IPv6 address:
		# Insert a zero segment into the working string and try again
		if test leading = $comprSectionLoc; then
			workingString="$(echo "$workingString" | sed -re 's/[:][:]/::0:/')"
		else
			workingString="$(echo "$workingString" | sed -re 's/[:][:]/:0::/')"
		fi
	done
	return 1
}

# getIpv6AddrAsHexString ipv6Addr
# 
# Prints the given IPv6 address as a 32-character upper case hexadecimal
# string, such as "20010DB80000000000000000000000FE" for the IPv6
# address "2001:db8::fe"
# 
# Prints nothing and returns with code 1 if the given string is not an
# IPv6 address
getIpv6AddrAsHexString () {
	local ipv6Addr="$1"; shift
	local ipv6AddrExpanded
	local -a segmentsHex=()
	local -i index
	local currentWord
	local result
	
	# Expand the IPv6 address (and return with code 1 if that fails)
	ipv6AddrExpanded="$(getIpv6AddrExpanded "$ipv6Addr")" || return 1
	
	# Pad the segments with leading zeros, handling each segment
	# individually
	index=0
	result=""
	while read -rsn 5 -d ':' currentWord; do
		test $index -lt 8 || return 1
		# Pad the current segment to 4 characters with leading zeros and
		# store it into the hex string array
		segmentsHex[$index]="$(echo "$currentWord" | sed -re 's/^(...)$/0\1/;s/^(..)$/00\1/;s/^(.)$/000\1/;')"
		# Append the padded segment to the result
		result="${result}${segmentsHex[$index]}"
		index=$(( index + 1 ))
	done < <(echo "${ipv6AddrExpanded}:")
	# Convert the whole thing to upper case
	result="$(echo "$result" | tr 'a-f' 'A-F')"
	#$verbose && echo "DEBUG  32-character hexadecimal representation of IPv6 address \"$ipv6Addr\" is \"$result\"" >&2
	echo "$result"
}

# isPublicIpv6Addr ipv6Addr
# 
# Returns with code 0 if ipv6Addr is a public IPv6 address
isPublicIpv6Addr () {
	local ipv6Addr="$1"; shift
	local ipv6AddrHex
	
	# Convert the IPv6 address to a 32-character upper case hexadecimal
	# string (and return with code 1 if that fails)
	ipv6AddrHex="$(getIpv6AddrAsHexString "$ipv6Addr")" || return 1
	
	# Unspecified address, ::/128
	test "00000000000000000000000000000000" = "$ipv6AddrHex" && return 1
	# Local host loopback address, ::1/128
	test "00000000000000000000000000000001" = "$ipv6AddrHex" && return 1
	# IPv4 mapped addresses, ::ffff:0:0/96
	isStringInRange	"00000000000000000000FFFF00000000" \
					"00000000000000000001000000000000" "$ipv6AddrHex" && return 1
	# IPv4 translated addresses, ::ffff:0:0:0/96
	isStringInRange	"0000000000000000FFFF000000000000" \
					"0000000000000000FFFF000100000000" "$ipv6AddrHex" && return 1
	# IPv4/IPv6 translation, 64:ff9b::/96
	isStringInRange	"0064FF9B000000000000000000000000" \
					"0064FF9B000000000000000100000000" "$ipv6AddrHex" && return 1
	# Discard prefix, 100::/64
	isStringInRange	"01000000000000000000000000000000" \
					"01000000000000010000000000000000" "$ipv6AddrHex" && return 1
	# Teredo tunneling, 2001::/32
	isStringInRange	"20010000000000000000000000000000" \
					"20010001000000000000000000000000" "$ipv6AddrHex" && return 1
	# ORCHIDv2, 2001:20::/28
	isStringInRange	"20010020000000000000000000000000" \
					"20010030000000000000000000000000" "$ipv6AddrHex" && return 1
	# Addresses used for documentation and examples, 2001:db8::/32
	isStringInRange	"20010DB8000000000000000000000000" \
					"20010DB9000000000000000000000000" "$ipv6AddrHex" && return 1
	# Deprecated 6to4 addressing scheme, 2002::/16
	isStringInRange	"20020000000000000000000000000000" \
					"20030000000000000000000000000000" "$ipv6AddrHex" && return 1
	# Unique local addresses, fc00::/7
	isStringInRange	"FC000000000000000000000000000000" \
					"FE000000000000000000000000000000" "$ipv6AddrHex" && return 1
	# Link-local addresses, fe80::/10
	isStringInRange	"FE800000000000000000000000000000" \
					"FEC00000000000000000000000000000" "$ipv6AddrHex" && return 1
	# Multicast addresses, ff00::/8
	isStringInRange	"FF000000000000000000000000000000" \
					"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" "$ipv6AddrHex" && return 1
	test "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" = "$ipv6AddrHex" && return 1
	return 0
}

# getPublicIpv6AddrFromStream
# 
# Scans STDIN for a text line containing a single public IPv6 address
# and, if a single such text line occurred, writes that IPv6 address to
# STDOUT
# If the stream did not contain such a text line, or contained multiple
# such text lines, writes nothing and returns with code 1
# The input stream may for example be a plain-text file or an (X)HTML
# document
getPublicIpv6AddrFromStream () {
	local line
	local candidateAddress
	local result
	
	while read -rsn 500 line || test "$line" != ""; do
		# Continue if the line does not contain an IPv6 address
		echo "$line" | grep -E '[0-9abcdef:]+' &> /dev/null || continue
		# The line contains an IPv6 address: Extract it
		if echo "$line" | grep -E '^[0-9abcdef:]+$' &> /dev/null; then
			# The line is already an IPv6 address, no extraction
			# required
			candidateAddress="$line"
		else
			# There is an IPv6 address embedded somewhere in the text
			# line (maybe between some HTML tags), and it needs to be
			# extracted
			candidateAddress="$(echo "$line" | sed -re 's/^.*[^0-9.]([0-9abcdef:]+).*$/\1/')" || return 1
		fi
		# Continue if the extraction failed
		test "" = "$candidateAddress" && continue
		# Continue if the IPv6 address is not a public address
		! isPublicIpv6Addr "$candidateAddress" && continue
		# Return with code 1 if this is the 2nd line to contain a public
		# IPv6 address
		! test -z ${result+x} && return 1
		# Store the current public IPv6 address
		result="$candidateAddress"
	done
	# Return with code 1 if none of the lines contained a public IPv6
	# address
	test -z ${result+x} && return 1
	$verbose && echo "DEBUG  Public IPv6 address read from stream: \"${result}\"" >&2
	echo "$result"
}

# getIpv6AddrOfThisHost
# 
# Determines this host's public IPv6 address
getIpv6AddrOfThisHost () {
	local methodString
	local methodName
	local -a methodArgumentIfSet
	local functionName
	local result
	
	for methodString in "${detectPublicAddrIpv6[@]}"; do
		if isMethodWithArgument "$methodString"; then
			methodName="$(getMethodName "$methodString")"
			methodArgumentIfSet=( "$(getMethodArgument "$methodString")" )
		else
			methodName="$methodString"
			methodArgumentIfSet=()
		fi
		functionName="getIpv6AddrOfThisHostFrom$methodName"
		$verbose && echo "DEBUG  Getting this host's public IPv6 address from function \"$functionName\"" >&2
		if result="$("$functionName" "${methodArgumentIfSet[@]}")" && test "$result" != ""; then
			$verbose && echo "DEBUG  Public IPv6 address is \"$result\"" >&2
			echo "$result"
			return 0
		fi
	done
	return 1
}

# getIpv6AddrOfThisHostFromFile path
# 
# Extracts this host's public IPv6 address from the given text or
# (X)HTML file
getIpv6AddrOfThisHostFromFile () {
	
	if test $# -eq 0; then
		echo " WARN  Failed to read this host's IPv6 address from file: No file path given as argument" >&2
		return 1
	fi
	getPublicIpv6AddrFromStream < "$1"
}

# getIpv6AddrOfThisHostFromNetDev [networkDevice]
# 
# Determines this host's public IPv6 address using
# ip -family inet6 -oneline addr show [networkDevice]
getIpv6AddrOfThisHostFromNetDev () {
	local -a networkDeviceIfSet=()
	local currentLine
	local currentNetworkDev
	local currentAddress
	local currentLifetimeSec
	local bestAddress
	local bestLifetimeSec
	
	# Read the network device argument if present
	if test $# -gt 0; then
		networkDeviceIfSet=( "$1" ); shift
	fi
	
	# Extract and validate the IP addresses from the text lines
	while read -rs currentLine; do
		currentAddress="$(echo "$currentLine" | sed -re 's/^.+ inet6 ([0-9abcdef:]+).*$/\1/')" || continue
		currentNetworkDev="$(echo "$currentLine" | sed -re 's/^[^:]+: ([^ ]+) .*$/\1/')" || continue
		currentLifetimeSec="$(echo "$currentLine" | sed -re 's/^.+ valid_lft ([^ ]+).*$/\1/;s/sec$//')" || continue
		isPublicIpv6Addr "$currentAddress" || continue
		$verbose && echo "DEBUG  Public IPv6 address read from network device \"${currentNetworkDev}\": \"${currentAddress}\", valid lifetime (sec): $currentLifetimeSec" >&2
		if test -z ${bestAddress+x} || isBetterLifetime "$currentLifetimeSec" "$bestLifetimeSec"; then
			# This is the first valid public IP address, or a subsequent
			# address that beats the preceding one by valid lifetime
			bestAddress="$currentAddress"
			bestLifetimeSec="$currentLifetimeSec"
		elif test "$currentLifetimeSec" = "$bestLifetimeSec"; then
			# This address has the same lifetime as the preceding one:
			# Ambiguous result, abort without a result
			unset bestAddress
			break
		fi
	done < <(ip -family inet6 -oneline addr show "${networkDeviceIfSet[@]}" \
				| { grep ' scope global' || true; } \
				| { grep -v ' deprecated' || true; } )
	
	# Return with an error code if no valid address was found
	test -z ${bestAddress+x} && return 1
	# Write the single valid result to STDOUT
	echo "$bestAddress"
}

# getIpv6AddrOfThisHostFromUrl url
# 
# Determines this host's public IPv6 address using the given website
# URL
getIpv6AddrOfThisHostFromUrl () {
	
	if test $# -eq 0; then
		echo " WARN  Failed to determine this host's IPv6 address from URL: No URL given as argument" >&2
		return 1
	fi
	wget -q6O - "$1" | getPublicIpv6AddrFromStream
}

# getIpv6AddrsOfDomain domainName [dnsServerIpv6]
# 
# Determines the given domain name's IPv6 addresses
# The addresses are returned in a newline-separated list
# The list is empty if the domain name does not have any IPv6 addresses
# Returns with a non-zero code if an error occurred while resolving the
# domain name's IPv6 addresses
getIpv6AddrsOfDomain () {
	local domainName="$1"; shift
	local dnsServerIpv6IfSet=()
	local commandType
	local result
	
	if test $# -gt 0; then
		dnsServerIpv6IfSet=( "$1" ); shift
	fi
	# Option 1: Ask the custom implementation if available
	if isFunction "getIpv6AddrsOfDomainCustom"; then
		$verbose && echo "DEBUG  Resolving IPv6 addresses of domain name \"$domainName\" using getIpv6AddrsOfDomainCustom" >&2
		if result="$(getIpv6AddrsOfDomainCustom "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
			$verbose && echo -n "DEBUG  IPv6 addresses of domain name \"$domainName\" from getIpv6AddrsOfDomainCustom: " >&2
			$verbose && echo "$result" | joinLines ", " true >&2
			echo "$result"
			return 0
		fi
	fi
	# Option 2: Use dig
	$verbose && echo "DEBUG  Resolving IPv6 addresses of domain name \"$domainName\" using dig" >&2
	if result="$(getIpv6AddrsOfDomainFromDig "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
		$verbose && echo -n "DEBUG  IPv6 addresses of domain name \"$domainName\" from dig: " >&2
		$verbose && echo "$result" | joinLines ", " true >&2
		echo "$result"
		return 0
	fi
	return 1
}

# getIpv6AddrsOfDomainFromDig domainName [dnsServerIpv6]
# 
# Determines the given domain name's IPv6 addresses using dig
# See getIpv6AddrsOfDomain for the function contract
getIpv6AddrsOfDomainFromDig () {
	local domainName="$1"; shift
	local dnsServerIpv6IfSet=()
	
	if test $# -gt 0; then
		dnsServerIpv6IfSet=( "@$1" ); shift
	fi
	dig "${dnsServerIpv6IfSet[@]}" -6 -t AAAA -q "$domainName" \
		| { grep "^${domainName}" || true; } \
		| { grep -E $'\t'IN$'\t'AAAA$'\t''[0-9abcdef:]+$' || true; } \
		| sed -re 's/^.+\tIN\tAAAA\t([0-9abcdef:]+)$/\1/'
}

# updateIpv6AddrIfRequired updateProtocol domainName authTokenIpv6 [dnsServerIpv6]
updateIpv6AddrIfRequired () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local dnsServerIpv6IfSet=()
	local newIpv6Addr
	local currentIpv6Addrs
	
	if test $# -gt 0; then
		dnsServerIpv6IfSet=("$1"); shift
	fi
	$verbose && echo "DEBUG  Checking and if required updating the current IPv6 address of domain \"${domainName}\"" >&2
	if ! newIpv6Addr="$(getIpv6AddrOfThisHost)" || test "" = "$newIpv6Addr"; then
		echo " WARN  Unable to determine the new IPv6 address (this host's address), doing nothing" >&2
		return 1
	fi
	if ! currentIpv6Addrs="$(getIpv6AddrsOfDomain "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
		echo " WARN  Unable to look up the current IPv6 addresses of \"$domainName\", doing nothing" >&2
		return 1
	fi
	$verbose && echo -n "DEBUG  The domain name \"$domainName\" has these IPv6 addresses: " >&2
	$verbose && echo "$currentIpv6Addrs" | joinLines ", " true >&2
	if echo "$currentIpv6Addrs" | containsExactLine "$newIpv6Addr"; then
		$verbose && echo "DEBUG  The domain name \"$domainName\" already has this host's IPv6 address \"$newIpv6Addr\", no update required" >&2
		return 0
	fi
	echo " INFO  Domain \"$domainName\": Updating IPv6 address to \"${newIpv6Addr}\" using protocol \"$updateProtocol\"" >&2
	if ! updateIpv6Addr "$updateProtocol" "$domainName" "$authTokenIpv6" "$newIpv6Addr"; then
		echo "ERROR  An error occurred while updating the IPv6 address of \"$domainName\" using protocol \"$updateProtocol\"" >&2
		return 1
	fi
	return 0
}

# updateIpv6Addr updateProtocol domainName authTokenIpv6 newIpv6Addr
# 
# Sets the domain name's IPv6 address
updateIpv6Addr () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	local updateFunction="updateIpv6AddrWith$updateProtocol"
	
	if ! isFunction "$updateFunction"; then
		echo "ERROR  Update function not implemented: \"${updateFunction}\"" >&2
		return 1
	fi
	"$updateFunction" "$domainName" "$authTokenIpv6" "$newIpv6Addr"
}

# updateIpv6AddrWithDuckDns domainName authTokenIpv6 newIpv6Addr
# 
# Sets the domain name's IPv6 address using the DuckDns protocol
# (duckdns.org)
updateIpv6AddrWithDuckDns () {
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	local authTokenIpv6Obfuscated
	authTokenIpv6Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv6")"
	local updateUrlIpv6="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv6}&ipv6=${newIpv6Addr}"
	local updateUrlIpv6Obfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenIpv6Obfuscated}&ipv6=${newIpv6Addr}"
	local response
	local -i wgetExitCode
	
	$verbose && echo "DEBUG  Calling update URL: \"$updateUrlIpv6Obfuscated\"" >&2
	# Since this is an update call for an IPv6 address it should be done using IPv6
	# It does however seem that neither www.duckdns.org nor duckdns.org have an IPv6 address
	response="$(wget -qO - "$updateUrlIpv6" | head --lines 1 --bytes 3 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	$verbose && echo "DEBUG  Response started with \"$response\"" >&2
	test "$response" == "OK" && return 0
	return 1
}

# updateIpv6AddrWithFreeDns domainName authTokenIpv6 newIpv6Addr
# 
# Sets the domain name's IPv6 address using either the FreeDnsV1 or
# FreeDnsV2 protocol, depending on the type of IPv6 authentication
# token given
# (freedns.afraid.org)
updateIpv6AddrWithFreeDns () {
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	
	if test "${#authTokenIpv6}" -eq 24; then
		updateIpv6AddrWithFreeDnsV2 "$domainName" "$authTokenIpv6" "$newIpv6Addr"
	else
		updateIpv6AddrWithFreeDnsV1 "$domainName" "$authTokenIpv6" "$newIpv6Addr"
	fi
}

# updateIpv6AddrWithFreeDnsV1 domainName authTokenIpv6 newIpv6Addr
# 
# Sets the domain name's IPv6 address using the FreeDnsV1 protocol
# (freedns.afraid.org)
updateIpv6AddrWithFreeDnsV1 () {
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	local authTokenIpv6Obfuscated
	authTokenIpv6Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv6")"
	local updateUrlIpv6="${updateUrlBaseFreeDnsV1}${authTokenIpv6}&address=${newIpv6Addr}"
	local updateUrlIpv6Obfuscated="${updateUrlBaseFreeDnsV1}${authTokenIpv6Obfuscated}&address=${newIpv6Addr}"
	local response
	local -i wgetExitCode
	
	$verbose && echo "DEBUG  Calling update URL: \"$updateUrlIpv6Obfuscated\"" >&2
	# Since this is an update call for an IPv6 address it should be done using IPv6
	# It does however seem that freedns.afraid.org does not have an IPv6 address
	response="$(wget -qO - "$updateUrlIpv6" | head --lines 1 --bytes 72 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	$verbose && echo "DEBUG  Response started with \"$response\"" >&2
	echo "$response" | grep -E '^Updated ' &> /dev/null && return 0
	echo "$response" | grep -E '^ERROR: Address [^ ]+ has not changed.$' &> /dev/null && return 0
	return 1
}

# updateIpv6AddrWithFreeDnsV2 domainName authTokenIpv6 newIpv6Addr
# 
# Sets the domain name's IPv6 address using the FreeDnsV2 protocol
# (v6.sync.afraid.org)
updateIpv6AddrWithFreeDnsV2 () {
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	local authTokenIpv6Obfuscated
	authTokenIpv6Obfuscated="$(getObfuscatedAuthToken "$authTokenIpv6")"
	local updateUrlIpv6="${updateUrlBaseFreeDnsV2Ipv6}${authTokenIpv6}/?ip=${newIpv6Addr}"
	local updateUrlIpv6Obfuscated="${updateUrlBaseFreeDnsV2Ipv6}${authTokenIpv6Obfuscated}/?ip=${newIpv6Addr}"
	local response
	local -i wgetExitCode
	
	$verbose && echo "DEBUG  Calling update URL: \"$updateUrlIpv6Obfuscated\"" >&2
	response="$(wget -q6O - "$updateUrlIpv6" | head --lines 1 --bytes 26 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	$verbose && echo "DEBUG  Response started with \"$response\"" >&2
	echo "$response" | grep -E '^Updated ' &> /dev/null && return 0
	echo "$response" | grep -E '^No IP change detected for ' &> /dev/null && return 0
	return 1
}

# Declare some global statics
declare updateUrlBaseFreeDnsV1="https://freedns.afraid.org/dynamic/update.php?"
declare updateUrlBaseFreeDnsV2Ipv4="https://sync.afraid.org/u/"
declare updateUrlBaseFreeDnsV2Ipv6="https://v6.sync.afraid.org/u/"
declare updateUrlBaseDuckDns="https://www.duckdns.org/update?"
declare sleepTimeDefault="6m"

# Catch missing arguments or --help as single argument
if test $# -lt 1 || { test $# -eq 1 && test "$1" = "--help"; } then
	echo -n "yabddnsd "; getVersion
	echo "Yet another bash dynamic DNS daemon"
	echo -n "
Usage:
yabddnsd
  [--domainName domainName]
  [--authTokenIpv4 authenticationTokenForIPv4]
  [--authTokenIpv6 authenticationTokenForIPv6]
  [--configFile sourcedConfigurationFile]
  [--detectPublicAddrIpv4 method[@@argument][,method[@@argument]]...]
  [--detectPublicAddrIpv6 method[@@argument][,method[@@argument]]...]
  [--dnsServerIpv4 dnsServerForIPv4]
  [--dnsServerIpv6 dnsServerForIPv6]
  [--oneShot]
  [--sleepTime sleepingTimeBetweenIterations]
  [--updateProtocol updateProtocol]
  [--verbose]
yabddnsd --help
yabddnsd --version
yabddnsd --listFunctions
yabddnsd [other options]...
   --callFunction function [functionArguments...]

Periodically checks which IP addresses are listed in the given domain
name's DNS record, and which public IP address this system has.
If the system's public IP address isn't among the DNS record's IP
addresses, the DNS record is updated to the system's public IP address.

For more information see manual page yabddnsd(8).
"
	exit 1
fi

# Catch --version as single argument
if test $# -eq 1 && test "$1" = "--version"; then
	getVersion
	exit 0
fi

# Make sure the required programs are available
allRequiredProgramsPresent=true
type grep &> /dev/null  || { echo "ERROR  Required program \"grep\" is not available" >&2;  allRequiredProgramsPresent=false; }
type sed &> /dev/null   || { echo "ERROR  Required program \"sed\" is not available" >&2;   allRequiredProgramsPresent=false; }
type wget &> /dev/null  || { echo "ERROR  Required program \"wget\" is not available" >&2;  allRequiredProgramsPresent=false; }
type ip &> /dev/null    || { echo "ERROR  Required program \"ip\" is not available" >&2;    allRequiredProgramsPresent=false; }
type tail &> /dev/null  || { echo "ERROR  Required program \"tail\" is not available" >&2;  allRequiredProgramsPresent=false; }
type bc &> /dev/null    || { echo "ERROR  Required program \"bc\" is not available" >&2;    allRequiredProgramsPresent=false; }
type sleep &> /dev/null || { echo "ERROR  Required program \"sleep\" is not available" >&2; allRequiredProgramsPresent=false; }
type dig &> /dev/null   || { echo "ERROR  Required program \"dig\" is not available" >&2;   allRequiredProgramsPresent=false; }
$allRequiredProgramsPresent || exit 1
unset allRequiredProgramsPresent

# Set default values for certain options
oneShot=false
sleepTime="$sleepTimeDefault"
verbose=false
listFunctions=false
if $upnpAvailable; then
	detectPublicAddrIpv4=( "NetDev" "Upnp" )
else
	detectPublicAddrIpv4=( "NetDev" )
fi
detectPublicAddrIpv6=( "NetDev" )

# Read and parse the arguments
# First of all, see if one or more configuration files are specified,
# and if so, source them all in the order in which they are given
nextArgIsValueForConfigFile=false
for argument in "$@"; do
	if $nextArgIsValueForConfigFile; then
		echo " INFO  Sourcing configuration file \"$argument\"" >&2
		# shellcheck source=/dev/null
		if ! source "$argument"; then
			echo "ERROR  Failed to source configuration file \"${argument}\"" >&2
			exit 1
		fi
	elif test "$argument" = "--configFile"; then
		nextArgIsValueForConfigFile=true
		continue
	fi
	nextArgIsValueForConfigFile=false
done
if $nextArgIsValueForConfigFile; then
	echo "ERROR  Trailing argument has no value: \"$argument\"" >&2
	exit 1
fi
unset nextArgIsValueForConfigFile

# Parse the other arguments, so that they overwrite whatever was
# set in the sourced configuration file(s)
while test $# -gt 0; do
	
	# Arguments without values (boolean options)
	if test "--oneShot" = "$1"; then
		oneShot=true; shift
	elif test "--verbose" = "$1"; then
		verbose=true; shift
	elif test "--listFunctions" = "$1"; then
		listFunctions=true; shift
	# Special arguments that are handled elsewhere
	elif test "--help" = "$1"; then
		echo "ERROR  Special argument \"--help\" must be given as single argument" >&2
		exit 1
	elif test "--version" = "$1"; then
		echo "ERROR  Special argument \"--version\" must be given as single argument" >&2
		exit 1
	
	# Arguments with a single value (here, this also means arguments
	# that take a value composed of comma-separated subvalues, which
	# are parsed into a bash array)
	elif test $# -lt 2; then
		echo "ERROR  Trailing argument has no value: \"$1\"" >&2
		exit 1
	elif test "--authTokenIpv4" = "$1"; then
		shift
		authTokenIpv4="$1"; shift
	elif test "--authTokenIpv6" = "$1"; then
		shift
		authTokenIpv6="$1"; shift
	elif test "--callFunction" = "$1"; then
		shift
		callFunction="$1"; shift
		# From here on it is any number of trailing function arguments,
		# therefore cease argument parsing
		break
	elif test "--configFile" = "$1"; then
		# Skip --configFile and its value because all config files have
		# already been sourced
		shift 2
	elif test "--detectPublicAddrIpv4" = "$1"; then
		shift
		detectPublicAddrIpv4=()
		while read -rsd ',' currentWord || test "$currentWord" != ""; do
			detectPublicAddrIpv4+=( "$currentWord" )
		done < <(echo -n "$1")
		shift
	elif test "--detectPublicAddrIpv6" = "$1"; then
		shift
		detectPublicAddrIpv6=()
		while read -rsd ',' currentWord || test "$currentWord" != ""; do
			detectPublicAddrIpv6+=( "$currentWord" )
		done < <(echo -n "$1")
		shift
	elif test "--dnsServerIpv4" = "$1"; then
		shift
		dnsServerIpv4="$1"; shift
	elif test "--dnsServerIpv6" = "$1"; then
		shift
		dnsServerIpv6="$1"; shift
	elif test "--domainName" = "$1"; then
		shift
		domainName="$1"; shift
	elif test "--updateProtocol" = "$1"; then
		shift
		updateProtocol="$1"; shift
	elif test "--sleepTime" = "$1"; then
		shift
		sleepTime="$1"; shift
	else
		echo "ERROR  Unknown argument \"$1\"" >&2
		exit 1
	fi
done

# Set some derived variables
# Some optional arguments are also available as arrays so that they can
# be used for function calls without if-else constructs
! test -z ${dnsServerIpv4+x} && dnsServerIpv4IfSet=("$dnsServerIpv4") || dnsServerIpv4IfSet=()
! test -z ${dnsServerIpv6+x} && dnsServerIpv6IfSet=("$dnsServerIpv6") || dnsServerIpv6IfSet=()

# Special functionality --listFunctions
if ! test "$listFunctions" = true && ! test "$listFunctions" = false; then
	echo "ERROR  --listFunctions, if set in a configuration file, must be either \"true\" or \"false\", was \"$listFunctions\"" >&2
	exit 1
fi
if $listFunctions; then
	echo -n "Functions that will be used if they are declared:
  getIpv4AddrsOfDomainCustom domainName [dnsServerIpv4]
  getIpv6AddrsOfDomainCustom domainName [dnsServerIpv6]
"
	echo "Functions declared in this script or in one of the given configuration files:"
	# Print the functions, and also append information about the arguments they
	# are called with if known
	declare -F | sed -re 's/^declare -f //' | sort \
		| sed -re 's/^(containsExactLine)$/\1 lineToFind (reads STDIN)/;'\
's/^(getIpv4AddrAsHexString)$/\1 ipv4Addr/;'\
's/^(getIpv4AddrOfThisHostFromFile)$/\1 path/;'\
's/^(getIpv4AddrOfThisHostFromNetDev)$/\1 [networkDevice]/;'\
's/^(getIpv4AddrOfThisHostFromUrl)$/\1 url/;'\
's/^(getIpv4AddrsOfDomain[^ ]*)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv6AddrOfThisHostFromFile)$/\1 path/;'\
's/^(getIpv6AddrOfThisHostFromNetDev)$/\1 [networkDevice]/;'\
's/^(getIpv6AddrOfThisHostFromUrl)$/\1 url/;'\
's/^(getIpv6AddrsOfDomain[^ ]*)$/\1 domainName [dnsServerIpv6]/;'\
's/^(getIpv6AddrAsHexString)$/\1 ipv6Addr/;'\
's/^(getIpv6AddrExpanded)$/\1 ipv6Addr/;'\
's/^(getMethodArgument)$/\1 method@@argument/;'\
's/^(getMethodName)$/\1 method[@@argument]/;'\
's/^(getObfuscatedAuthToken)$/\1 authToken/;'\
's/^(getPublicIpv.AddrFromStream)$/\1 (reads STDIN)/;'\
's/^(isBetterLifetime)$/\1 lifetimeToTest lifetimeReference/;'\
's/^(isFunction)$/\1 command/;'\
's/^(isMethodWithArgument)$/\1 method[@@argument]/;'\
's/^(isPublicIpv4Addr)$/\1 ipv4Addr/;'\
's/^(isPublicIpv6Addr)$/\1 ipv6Addr/;'\
's/^(isStringInRange)$/\1 lowerBorderIncluding upperBorderExcluding string/;'\
's/^(isWhitelistedExitCode)$/\1 exitCode [whitelistedExitCode...]/;'\
's/^(joinLines)$/\1 separator [appendFinalNewline] (reads STDIN)/;'\
's/^(splitBySubstring)$/\1 string substring/;'\
's/^(updateIpv4Addr)$/\1 updateProtocol domainName authTokenIpv4 newIpv4Addr/;'\
's/^(updateIpv4AddrIfRequired)$/\1 updateProtocol domainName authTokenIpv4 [dnsServerIpv4]/;'\
's/^(updateIpv4AddrWith.+)$/\1 domainName authTokenIpv4 newIpv4Addr/;'\
's/^(updateIpv6Addr)$/\1 updateProtocol domainName authTokenIpv6 newIpv6Addr/;'\
's/^(updateIpv6AddrIfRequired)$/\1 updateProtocol domainName authTokenIpv6 [dnsServerIpv6]/;'\
's/^(updateIpv6AddrWith.+)$/\1 domainName authTokenIpv6 newIpv6Addr/;'\
's/^(.*)$/  \1/'
	exit 0
fi

# Validate arguments that are required or typically relevant for
# --callFunction
if ! test "$verbose" = true && ! test "$verbose" = false; then
	echo "ERROR  --verbose, if set in a configuration file, must be either \"true\" or \"false\", was \"$verbose\"" >&2
	exit 1
fi
declare methodString
declare methodName
declare functionName
declare validMethod
for methodString in "${detectPublicAddrIpv4[@]}"; do
	if isMethodWithArgument "$methodString"; then
		methodName="$(getMethodName "$methodString")"
	else
		methodName="$methodString"
	fi
	functionName="getIpv4AddrOfThisHostFrom$methodName"
	isFunction "$functionName" && continue
	echo -n "ERROR  --detectPublicAddrIpv4: Invalid public IPv4 address detection method \"$methodName\" specified. Available method(s):" >&2
	while read -rs validMethod; do
		echo -n " \"$validMethod\"" >&2
	done < <(declare -F | sed -re 's/^declare -f //' \
				| { grep '^getIpv4AddrOfThisHostFrom' || true; } \
				| sed -re 's/^getIpv.AddrOfThisHostFrom//')
	echo >&2
	exit 1
done
for methodString in "${detectPublicAddrIpv6[@]}"; do
	if isMethodWithArgument "$methodString"; then
		methodName="$(getMethodName "$methodString")"
	else
		methodName="$methodString"
	fi
	functionName="getIpv6AddrOfThisHostFrom$methodName"
	isFunction "$functionName" && continue
	echo -n "ERROR  --detectPublicAddrIpv6: Invalid public IPv6 address detection method \"$methodName\" specified. Available method(s):" >&2
	while read -rs validMethod; do
		echo -n " \"$validMethod\"" >&2
	done < <(declare -F | sed -re 's/^declare -f //' \
				| { grep '^getIpv6AddrOfThisHostFrom' || true; } \
				| sed -re 's/^getIpv.AddrOfThisHostFrom//')
	echo >&2
	exit 1
done
unset methodString
unset methodName
unset functionName
unset validMethod

# Special functionality --callFunction
if ! test -z ${callFunction+x}; then
	$verbose && echo "DEBUG  Calling function \"$callFunction\" with $# argument(s)" >&2
	
	# Find out if this is a function that is supposed to return a
	# boolean result, i.e. whose return code is its only result
	booleanResultFunction=false
	if echo "$callFunction" | grep -E '^is|^contains' &> /dev/null; then
		booleanResultFunction=true
	fi
	
	# Call the function and capture its return code
	declare -i returnCode
	"$callFunction" "$@" && returnCode="$?" || returnCode="$?"
	
	# Provide some extra output regarding the function's return code
	if $booleanResultFunction; then
		if test "$returnCode" -eq 0; then
			# Regular true return
			$verbose && echo "DEBUG  Boolean function returned true (code ${returnCode})" >&2
		elif test "$returnCode" -eq 1; then
			# Regular false return
			$verbose && echo "DEBUG  Boolean function returned false (code ${returnCode})" >&2
		else
			# Uh-oh, possibly some unhandled error
			echo " WARN  Boolean function \"$callFunction\" returned false with unusual code ${returnCode}" >&2
		fi
	else
		if test "$returnCode" -eq 0; then
			$verbose && echo "DEBUG  Function returned with code $returnCode" >&2
		else
			# Be a bit more panicky if a non-boolean function returned
			# with a bad code
			echo " WARN  Function \"$callFunction\" returned with code $returnCode" >&2
		fi
	fi
	exit "$returnCode"
fi

# Validate arguments required for main functionality
if test -z ${domainName+x}; then
	echo "ERROR  No domain name specified (--domainName)" >&2
	exit 1
fi
if test -z ${authTokenIpv4+x} && test -z ${authTokenIpv6+x}; then
	echo "ERROR  No authentication token specified (--authTokenIpv4 or --authTokenIpv6)" >&2
	exit 1
fi
if { ! test -z ${authTokenIpv4+x} || ! test -z ${authTokenIpv6+x} ;} && test -z ${updateProtocol+x}; then
	echo "ERROR  No update protocol specified (--updateProtocol)" >&2
	exit 1
fi
if ! test -z ${authTokenIpv4+x} && test ${#detectPublicAddrIpv4[@]} -eq 0; then
	echo "ERROR  IPv4 authentication token configured, but no method(s) for IPv4 address detection of this host specified (--detectPublicAddrIpv4)" >&2
	exit 1
fi
if ! test -z ${authTokenIpv4+x} && ! isFunction "updateIpv4AddrWith$updateProtocol"; then
	echo "ERROR  IPv4 authentication token configured, but IPv4 function for update protocol \"$updateProtocol\" not implemented: \"updateIpv4AddrWith$updateProtocol\"" >&2
	exit 1
fi
if ! test -z ${authTokenIpv6+x} && test ${#detectPublicAddrIpv6[@]} -eq 0; then
	echo "ERROR  IPv6 authentication token configured, but no method(s) for IPv6 address detection of this host specified (--detectPublicAddrIpv6)" >&2
	exit 1
fi
if ! test -z ${authTokenIpv6+x} && ! isFunction "updateIpv6AddrWith$updateProtocol"; then
	echo "ERROR  IPv6 authentication token configured, but IPv6 function for update protocol \"$updateProtocol\" not implemented: \"updateIpv6AddrWith$updateProtocol\"" >&2
	exit 1
fi
if ! test "$oneShot" = true && ! test "$oneShot" = false; then
	echo "ERROR  --oneShot, if set in a configuration file, must be either \"true\" or \"false\", was \"$oneShot\"" >&2
	exit 1
fi

# All arguments validated and ready to go; print a summary
echo -n " INFO  This is yabddnsd " >&2; getVersion >&2
echo " INFO  Domain name: \"${domainName}\"" >&2
echo " INFO  Update protocol: \"$updateProtocol\"" >&2
! test -z ${authTokenIpv4+x} && echo " INFO  IPv4 authentication token: \"$(getObfuscatedAuthToken "$authTokenIpv4")\"" >&2
! test -z ${authTokenIpv6+x} && echo " INFO  IPv6 authentication token: \"$(getObfuscatedAuthToken "$authTokenIpv6")\"" >&2
declare method
if test ${#detectPublicAddrIpv4[@]} -gt 0; then
	echo -n " INFO  Public IPv4 address detection method(s):" >&2
	for method in "${detectPublicAddrIpv4[@]}"; do
		echo -n " \"$method\"" >&2
	done
	echo >&2
fi
if test ${#detectPublicAddrIpv6[@]} -gt 0; then
	echo -n " INFO  Public IPv6 address detection method(s):" >&2
	for method in "${detectPublicAddrIpv6[@]}"; do
		echo -n " \"$method\"" >&2
	done
	echo >&2
fi
unset method
! test -z ${dnsServerIpv4+x} && echo " INFO  IPv4 DNS server: \"${dnsServerIpv4}\"" >&2
! test -z ${dnsServerIpv6+x} && echo " INFO  IPv6 DNS server: \"${dnsServerIpv6}\"" >&2
if $oneShot; then
	echo " INFO  Option --oneShot is set, main loop will terminate after its first iteration" >&2
else
	echo " INFO  Sleeping time between iterations: \"${sleepTime}\"" >&2
fi
$verbose && echo " INFO  Verbose output is enabled" >&2

# Get going
echo " INFO  Entering main loop" >&2
while true ; do
	if ! test -z ${authTokenIpv4+x}; then
		updateIpv4AddrIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "${dnsServerIpv4IfSet[@]}" || true
	fi
	if ! test -z ${authTokenIpv6+x}; then
		updateIpv6AddrIfRequired "$updateProtocol" "$domainName" "$authTokenIpv6" "${dnsServerIpv6IfSet[@]}" || true
	fi
	if $oneShot; then
		$verbose && echo "DEBUG  Leaving the main loop because option --oneShot is set" >&2
		break
	fi
	$verbose && echo "DEBUG  Sleeping for \"$sleepTime\"" >&2
	if ! sleep "$sleepTime"; then
		echo " WARN  Calling sleep with argument \"${sleepTime}\" returned with a non-zero code, calling it with \"${sleepTimeDefault}\" instead" >&2
		sleep "$sleepTimeDefault"
	fi
done