#!/bin/bash

# Copyright 2014-2020, 2023 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 --update-protocol 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 Assign and parse short (single-character) arguments
# TODO Trap SIGHUP and, if the signal is caught, immediately resume the
#      main loop

# 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
	# printMsgWarning is not declared yet so we cannot use it here
	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=11
declare -r versionPatch=0
declare -r versionLabel=""

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

# Buffers for composing multi-part log messages
messageBufferError=()
messageBufferWarning=()
messageBufferInfo=()
messageBufferDebug=()

# printMsg levelPrefix message
#
# Prints the given message to STDERR, prefixed with the given level prefix
printMsg () {
	local prefix=$1; shift

	if test $# -gt 0; then echo "$prefix $*" >&2; else true; fi
}

# appendMsgError partialMessage
#
# Appends the given partial message to the message buffer for ERROR messages
appendMsgError () {
	messageBufferError+=("$@")
}

# printMsgError message
#
# Prints the given message to STDERR, prefixed with "ERROR "
printMsgError () {
	printMsg "ERROR " "${messageBufferError[@]}" "$@"; messageBufferError=()
}

# appendMsgWarning partialMessage
#
# Appends the given partial message to the message buffer for WARN messages
appendMsgWarning () {
	messageBufferWarning+=("$@")
}

# printMsgWarning message
#
# Prints the given message to STDERR, prefixed with " WARN "
printMsgWarning () {
	printMsg " WARN " "${messageBufferWarning[@]}" "$@"; messageBufferWarning=()
}

# appendMsgInfo partialMessage
#
# Appends the given partial message to the message buffer for INFO messages
appendMsgInfo () {
	messageBufferInfo+=("$@")
}

# printMsgInfo message
#
# Prints the given message to STDERR, prefixed with " INFO "
printMsgInfo () {
	printMsg " INFO " "${messageBufferInfo[@]}" "$@"; messageBufferInfo=()
}

# appendMsgDebug partialMessage
#
# If $verbose is "true", appends the given partial message to the message
# buffer for DEBUG messages
appendMsgDebug () {

	test "$verbose" = 'true' || return 0
	messageBufferDebug+=("$@")
}

# printMsgDebug message
#
# If $verbose is "true", prints the given message to STDERR, prefixed with
# "DEBUG "
printMsgDebug () {

	test "$verbose" = 'true' || return 0
	printMsg "DEBUG " "${messageBufferDebug[@]}" "$@"; messageBufferDebug=()
}

# Prints some concise usage information to STDOUT
printUsageInfo () {

	echo -n \
"Usage:
  yabddnsd
    [--domain-name domainName]
    [--auth-token-ipv4 authenticationTokenForIPv4]
    [--auth-token-ipv6 authenticationTokenForIPv6]
    [--config-file sourcedConfigurationFile]
    [--detect-public-addr-ipv4 method[@@argument][,method[@@argument]]...]
    [--detect-public-addr-ipv6 method[@@argument][,method[@@argument]]...]
    [--dns-server dnsServer]
    [--dns-server-ipv4 dnsServerForIPv4]
    [--dns-server-ipv6 dnsServerForIPv6]
    [--one-shot]
    [--sleep-time sleepingTimeBetweenIterations]
    [--update-protocol updateProtocol]
    [--verbose]
  yabddnsd --help
  yabddnsd --version
  yabddnsd --list-functions
  yabddnsd [other options]...
     --call-function function [functionArguments...]
"
}

# Prints a short help message / summary to STDOUT
printHelpMessage () {

	echo -n "yabddnsd "; getVersion
	echo "Yet another bash dynamic DNS daemon"
	echo ""
	printUsageInfo
	echo ""
	echo -n \
"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).
"
}

# 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 prefix suffix separator [appendFinalNewline]
#
# Walks a newline-separated list of Strings from STDIN and prints
# them to STDOUT, separated (but not terminated) by separator
# The whole printed text is prefixed with prefix and suffixed with
# suffix
# 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 prefix="$1"; shift
	local suffix="$1"; shift
	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"
		else
			echo -n "$prefix"
		fi
		previousLine="$line"
		subsequentIteration=true
	done
	$subsequentIteration && echo -n "$previousLine"
	$subsequentIteration && echo -n "$suffix"
	$appendFinalNewline && echo ""
	return 0
}

# 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
}

# Returns the paths to all existing valid system-level configuration
# files, in ascending order of priority
#
# The paths are returned in a newline-separated list
getConfigurationFilesSystem () {
	getConfigurationFiles "/usr/lib" "/etc" "/run"
}

# Returns the paths to all existing valid user-level configuration
# files, in ascending order of priority
#
# The paths are returned in a newline-separated list
getConfigurationFilesUser () {
	getConfigurationFiles ~/".config"
}

# getConfigurationFiles baseDir [otherBaseDir...]
#
# Returns the paths to all existing valid configuration files, in
# ascending order of priority, in the given directories
#
# The paths are returned in a newline-separated list
# The directories must be given without trailing slash
getConfigurationFiles () {
	local suffix=".conf"
	local pathPrefix
	local configDir
	local configFile
	local baseDir

	while test $# -gt 0; do
		baseDir="$1"; shift
		pathPrefix="${baseDir}/yabddnsd/yabddnsd"
		configDir="${pathPrefix}.d"
		configFile="${pathPrefix}$suffix"

		if test -d "$configDir"; then
			find "$configDir" -mindepth 1 -maxdepth 1 -type f -name "*$suffix" -print | sort
		fi
		if test -f "$configFile"; then
			echo "$configFile"
		fi
	done
}

# 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, and also exit
	# code 1 which is seen in certain circumstances (such as when this
	# application is run from a procd init script on OpenWRT)
	splitBySubstring "$methodAndArgument" '@@' 2> /dev/null \
		| head -q -n 1 || isWhitelistedExitCode $? 1 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}.")
	#printMsgDebug "8-character hexadecimal representation of IPv4 address \"$ipv4Addr\" is \"$result\""
	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
	printMsgDebug "Public IPv4 address read from stream: \"${result}\""
	echo "$result"
}

# getIpv4AddrOfThisHostDefault
#
# Determines this host's public IPv4 address using the configured
# method(s)
getIpv4AddrOfThisHostDefault () {
	getIpv4AddrOfThisHost "${detectPublicAddrIpv4[@]}"
}

# getIpv4AddrOfThisHost [methodString...]
#
# Determines this host's public IPv4 address using the given method(s)
getIpv4AddrOfThisHost () {
	local methodString
	local methodName
	local -a methodArgumentIfSet
	local functionName
	local result

	while test $# -gt 0; do
		methodString="$1"; shift
		if isMethodWithArgument "$methodString"; then
			methodName="$(getMethodName "$methodString")"
			methodArgumentIfSet=( "$(getMethodArgument "$methodString")" )
		else
			methodName="$methodString"
			methodArgumentIfSet=()
		fi
		functionName="getIpv4AddrOfThisHostFrom$methodName"
		printMsgDebug "Getting this host's public IPv4 address from function \"$functionName\""
		if result="$("$functionName" "${methodArgumentIfSet[@]}")" && test "$result" != ""; then
			printMsgDebug "Public IPv4 address is \"$result\""
			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
		printMsgWarning "Failed to read this host's IPv4 address from file: No file path given as argument"
		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
		printMsgDebug "Public IPv4 address read from network device \"${currentNetworkDev}\": \"${currentAddress}\", valid lifetime (sec): $currentLifetimeSec"
		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; } \
				| { grep -v ' temporary' || 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"
	}
fi

# getIpv4AddrOfThisHostFromUrl url
#
# Determines this host's public IPv4 address using the given website
# URL
getIpv4AddrOfThisHostFromUrl () {

	if test $# -eq 0; then
		printMsgWarning "Failed to determine this host's IPv4 address from URL: No URL given as argument"
		return 1
	fi
	wget -q4O - "$1" | getPublicIpv4AddrFromStream
}

# getIpv4AddrsOfDomainDefault
#
# Determines the configured domain name's IPv4 addresses using the
# configured IPv4 DNS server
getIpv4AddrsOfDomainDefault () {
	getIpv4AddrsOfDomain "$domainName" "${dnsServerIpv4IfSet[@]}"
}

# 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
		printMsgDebug "Resolving IPv4 addresses of domain name \"$domainName\" using getIpv4AddrsOfDomainCustom"
		if result="$(getIpv4AddrsOfDomainCustom "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
			printMsgDebug "IPv4 addresses of domain name \"$domainName\" from getIpv4AddrsOfDomainCustom:" \
				"$(echo "$result" | joinLines "" "" ", " false)"
			echo "$result"
			return 0
		fi
	fi
	# Option 2: Use dig
	printMsgDebug "Resolving IPv4 addresses of domain name \"$domainName\" using dig"
	if result="$(getIpv4AddrsOfDomainFromDig "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
		printMsgDebug "IPv4 addresses of domain name \"$domainName\" from dig:" \
			"$(echo "$result" | joinLines "" "" ", " false)"
		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=()
	local digResponse
	local -i digExitCode

	if test $# -gt 0; then
		dnsServerIpv4IfSet=( "@$1" ); shift
	fi
	digResponse="$(dig "${dnsServerIpv4IfSet[@]}" -t A -q "$domainName" +noall +answer +short)" && digExitCode=$? || digExitCode=$?
	echo "$digResponse" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
	test "$digExitCode" = 0 || return "$digExitCode"
}

# updateIpv4AddrIfRequiredDefault
#
# Retrieves the configured domain name's IPv4 addresses using the
# configured IPv4 DNS server, and determines the current local public
# IPv4 address using the configured IPv4 address detection methods
# If required, updates the domain name's IPv4 address using the
# configured update protocol and IPv4 authentication token
updateIpv4AddrIfRequiredDefault () {
	updateIpv4AddrIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "${dnsServerIpv4IfSet[@]}"
}

# updateIpv4AddrIfRequired updateProtocol domainName authTokenIpv4 [dnsServerIpv4]
#
# Retrieves the domain name's IPv4 addresses using the given IPv4 DNS
# server and determines the current local public IPv4 address using the
# configured IPv4 address detection methods
# If required, updates the domain name's IPv4 address
updateIpv4AddrIfRequired () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local dnsServerIpv4=""

	if test $# -gt 0; then
		dnsServerIpv4="$1"; shift
	fi
	updateAddrsIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "$dnsServerIpv4" "" ""
}

# updateIpv4AddrDefault newIpv4Addr
#
# Sets the configured domain name's IPv4 address using the configured
# update protocol and IPv4 authentication token
updateIpv4AddrDefault () {
	updateIpv4Addr "$updateProtocol" "$domainName" "$authTokenIpv4" "$1"
}

# 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

	updateAddrs "$updateProtocol" "$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

	printMsgDebug "Calling update URL: \"$updateUrlIpv4Obfuscated\""
	# 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 -qO - "$updateUrlIpv4" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! test "$response" = "OK"; then
		printMsgWarning "Unexpected response from DuckDNS dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# 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

	printMsgDebug "Calling update URL: \"$updateUrlIpv4Obfuscated\""
	response="$(wget -qO - "$updateUrlIpv4" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
	&& ! echo "$response" | grep -E '^ERROR: Address [^ ]+ has not changed.$' &> /dev/null; then
		printMsgWarning "Unexpected response from FreeDNS-v1 dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# 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

	printMsgDebug "Calling update URL: \"$updateUrlIpv4Obfuscated\""
	response="$(wget -qO - "$updateUrlIpv4" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
	&& ! echo "$response" | grep -E '^No IP change detected for ' &> /dev/null; then
		printMsgWarning "Unexpected response from FreeDNS-v2 dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# 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

		#printMsgDebug "IPv6 expansion intermediate result: \"$result\""
		# 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')"
	#printMsgDebug "32-character hexadecimal representation of IPv6 address \"$ipv6Addr\" is \"$result\""
	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
	printMsgDebug "Public IPv6 address read from stream: \"${result}\""
	echo "$result"
}

# getIpv6AddrOfThisHostDefault
#
# Determines this host's public IPv6 address using the configured
# method(s)
getIpv6AddrOfThisHostDefault () {
	getIpv6AddrOfThisHost "${detectPublicAddrIpv6[@]}"
}

# getIpv6AddrOfThisHost [methodString...]
#
# Determines this host's public IPv6 address using the given method(s)
getIpv6AddrOfThisHost () {
	local methodString
	local methodName
	local -a methodArgumentIfSet
	local functionName
	local result

	while test $# -gt 0; do
		methodString="$1"; shift
		if isMethodWithArgument "$methodString"; then
			methodName="$(getMethodName "$methodString")"
			methodArgumentIfSet=( "$(getMethodArgument "$methodString")" )
		else
			methodName="$methodString"
			methodArgumentIfSet=()
		fi
		functionName="getIpv6AddrOfThisHostFrom$methodName"
		printMsgDebug "Getting this host's public IPv6 address from function \"$functionName\""
		if result="$("$functionName" "${methodArgumentIfSet[@]}")" && test "$result" != ""; then
			printMsgDebug "Public IPv6 address is \"$result\""
			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
		printMsgWarning "Failed to read this host's IPv6 address from file: No file path given as argument"
		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
		printMsgDebug "Public IPv6 address read from network device \"${currentNetworkDev}\": \"${currentAddress}\", valid lifetime (sec): $currentLifetimeSec"
		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; } \
				| { grep -v ' temporary' || 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
		printMsgWarning "Failed to determine this host's IPv6 address from URL: No URL given as argument"
		return 1
	fi
	wget -q6O - "$1" | getPublicIpv6AddrFromStream
}

# getIpv6AddrsOfDomain
#
# Determines the configured domain name's IPv6 addresses using the
# configured IPv6 DNS server
getIpv6AddrsOfDomainDefault () {
	getIpv6AddrsOfDomain "$domainName" "${dnsServerIpv6IfSet[@]}"
}

# 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
		printMsgDebug "Resolving IPv6 addresses of domain name \"$domainName\" using getIpv6AddrsOfDomainCustom"
		if result="$(getIpv6AddrsOfDomainCustom "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
			printMsgDebug "IPv6 addresses of domain name \"$domainName\" from getIpv6AddrsOfDomainCustom:" \
				"$(echo "$result" | joinLines "" "" ", " false)"
			echo "$result"
			return 0
		fi
	fi
	# Option 2: Use dig
	printMsgDebug "Resolving IPv6 addresses of domain name \"$domainName\" using dig"
	if result="$(getIpv6AddrsOfDomainFromDig "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
		printMsgDebug "IPv6 addresses of domain name \"$domainName\" from dig:" \
			"$(echo "$result" | joinLines "" "" ", " false)"
		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=()
	local digResponse
	local -i digExitCode

	if test $# -gt 0; then
		dnsServerIpv6IfSet=( "@$1" ); shift
	fi
	digResponse="$(dig "${dnsServerIpv6IfSet[@]}" -t AAAA -q "$domainName" +noall +answer +short)" && digExitCode=$? || digExitCode=$?
	# TODO More precise filtering for IPv6 addresses
	# It should be okay as-is though since this is supposed to kick out domain
	# names when a CNAME is found, and that case is covered as dots are not
	# whitelisted
	# Well, for domains that have a top level domain, anyway
	echo "$digResponse" | grep -E '^[0-9abcdef:]+$'
	test "$digExitCode" = 0 || return "$digExitCode"
}

# updateIpv6AddrIfRequiredDefault
#
# Retrieves the configured domain name's IPv6 addresses using the
# configured IPv6 DNS server, and determines the current local public
# IPv6 address using the configured IPv6 address detection methods
# If required, updates the domain name's IPv6 address using the
# configured update protocol and IPv6 authentication token
updateIpv6AddrIfRequiredDefault () {
	updateIpv6AddrIfRequired "$updateProtocol" "$domainName" "$authTokenIpv6" "${dnsServerIpv6IfSet[@]}"
}

# updateIpv6AddrIfRequired updateProtocol domainName authTokenIpv6 [dnsServerIpv6]
#
# Retrieves the domain name's IPv6 addresses using the given IPv6 DNS
# server and determines the current local public IPv6 address using the
# configured IPv6 address detection methods
# If required, updates the domain name's IPv6 address
updateIpv6AddrIfRequired () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv6="$1"; shift
	local dnsServerIpv6=""

	if test $# -gt 0; then
		dnsServerIpv6="$1"; shift
	fi
	updateAddrsIfRequired "$updateProtocol" "$domainName" "" "" "$authTokenIpv6" "$dnsServerIpv6"
}

# updateIpv6AddrDefault newIpv6Addr
#
# Sets the configured domain name's IPv6 address using the configured
# update protocol and IPv6 authentication token
updateIpv6AddrDefault () {
	updateIpv6Addr "$updateProtocol" "$domainName" "$authTokenIpv6" "$1"
}

# 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

	updateAddrs "$updateProtocol" "$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

	printMsgDebug "Calling update URL: \"$updateUrlIpv6Obfuscated\""
	response="$(wget -qO - "$updateUrlIpv6" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! test "$response" = "OK"; then
		printMsgWarning "Unexpected response from DuckDNS dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# 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

	printMsgDebug "Calling update URL: \"$updateUrlIpv6Obfuscated\""
	response="$(wget -qO - "$updateUrlIpv6" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
	&& ! echo "$response" | grep -E '^ERROR: Address [^ ]+ has not changed.$' &> /dev/null; then
		printMsgWarning "Unexpected response from FreeDNS-v1 dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# 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

	printMsgDebug "Calling update URL: \"$updateUrlIpv6Obfuscated\""
	response="$(wget -qO - "$updateUrlIpv6" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! echo "$response" | grep -E '^Updated ' &> /dev/null \
	&& ! echo "$response" | grep -E '^No IP change detected for ' &> /dev/null; then
		printMsgWarning "Unexpected response from FreeDNS-v2 dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# updateAddrsIfRequiredDefault
#
# Retrieves the configured domain name's IPv4 and IPv6 addresses and
# determines the current local public IPv4 and IPv6 addresses
# If required, updates the domain name's addresses using the configured update
# protocol and authentication tokens
updateAddrsIfRequiredDefault () {
	updateAddrsIfRequired "$updateProtocol" "$domainName" "$authTokenIpv4" "$dnsServerIpv4" "$authTokenIpv6" "$dnsServerIpv6"
}

# updateAddrsIfRequired updateProtocol domainName authTokenIpv4 dnsServerIpv4 authTokenIpv6 dnsServerIpv6
#
# Retrieves the given domain name's IPv4 and IPv6 addresses and determines the
# current local public IPv4 and IPv6 addresses
# If required, updates the domain name's addresses using the given update
# protocol and authentication tokens
updateAddrsIfRequired () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift

	local authTokenIpv4="$1"; shift
	local dnsServerIpv4="$1"; shift
	local dnsServerIpv4IfSet=()
	if test "$dnsServerIpv4" != ""; then dnsServerIpv4IfSet+=("$dnsServerIpv4"); fi
	local newIpv4Addr=""
	local currentIpv4Addrs

	local authTokenIpv6="$1"; shift
	local dnsServerIpv6="$1"; shift
	local dnsServerIpv6IfSet=()
	if test "$dnsServerIpv6" != ""; then dnsServerIpv6IfSet+=("$dnsServerIpv6"); fi
	local newIpv6Addr=""
	local currentIpv6Addrs

	printMsgDebug "Checking and if required updating the current IP addresses of domain \"${domainName}\""

	# Look up the local and DNS IPv4 addresses if the IPv4 address should be maintained
	if test "$authTokenIpv4" != ""; then
		if ! newIpv4Addr="$(getIpv4AddrOfThisHost "${detectPublicAddrIpv4[@]}")" || test "" = "$newIpv4Addr"; then
			printMsgWarning "Unable to determine this host's current IPv4 address"
		fi
		if ! currentIpv4Addrs="$(getIpv4AddrsOfDomain "$domainName" "${dnsServerIpv4IfSet[@]}")"; then
			printMsgWarning "Unable to look up the current IPv4 addresses of \"$domainName\""
		fi
		printMsgDebug "The domain name \"$domainName\" has these IPv4 addresses:" \
			"$(echo "$currentIpv4Addrs" | joinLines "" "" ", " false)"
	fi

	# Look up the local and DNS IPv6 addresses if the IPv6 address should be maintained
	if test "$authTokenIpv6" != ""; then
		if ! newIpv6Addr="$(getIpv6AddrOfThisHost "${detectPublicAddrIpv6[@]}")" || test "" = "$newIpv6Addr"; then
			printMsgWarning "Unable to determine this host's current IPv6 address"
		fi
		if ! currentIpv6Addrs="$(getIpv6AddrsOfDomain "$domainName" "${dnsServerIpv6IfSet[@]}")"; then
			printMsgWarning "Unable to look up the current IPv6 addresses of \"$domainName\""
		fi
		printMsgDebug "The domain name \"$domainName\" has these IPv6 addresses:" \
			"$(echo "$currentIpv6Addrs" | joinLines "" "" ", " false)"
	fi

	# Determine if an update request is needed
	if test "$newIpv4Addr" = "" || echo "$currentIpv4Addrs" | containsExactLine "$newIpv4Addr"; then
		newIpv4Addr=""
	fi
	# TODO Before comparing them, normalize (expand and lower-case) the IPv6 addresses
	if test "$newIpv6Addr" = "" || echo "$currentIpv6Addrs" | containsExactLine "$newIpv6Addr"; then
		newIpv6Addr=""
	fi

	# Dispatch update request(s) if required
	if test "$newIpv4Addr" != "" || test "$newIpv6Addr" != ""; then
		updateAddrs "$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr" "$authTokenIpv6" "$newIpv6Addr"
	else
		printMsgDebug "No update required for domain name \"$domainName\""
	fi
}

# updateAddrsDefault newIpv4Addr newIpv6Addr
#
# Updates the domain name's addresses to the given addresses using the
# configured update protocol and authentication tokens
updateAddrsDefault () {
	updateAddrs "$updateProtocol" "$domainName" "$authTokenIpv4" "$1" "$authTokenIpv6" "$2"
}

# updateAddrs updateProtocol domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr
#
# Updates the domain name's addresses to the given addresses using the given
# update protocol and authentication tokens
updateAddrs () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	local -i returnCode=0

	if test "$newIpv4Addr" = "" && test "$newIpv6Addr" = ""; then
		printMsgWarning "Neither IPv4 nor IPv6 address specified, doing nothing"
		return "$returnCode"
	fi

	if isFunction "updateAddrsWith$updateProtocol"; then
		# Protocol implementation supports updating both IPv4 and IPv6 addresses simultaneously
		appendMsgInfo "Domain \"$domainName\": Updating"
		test "$newIpv4Addr" != "" && appendMsgInfo "IPv4 address to \"${newIpv4Addr}\""
		test "$newIpv4Addr" != "" && test "$newIpv6Addr" != "" && appendMsgInfo "and"
		test "$newIpv6Addr" != "" && appendMsgInfo "IPv6 address to \"${newIpv6Addr}\""
		printMsgInfo "using protocol \"$updateProtocol\""
		if ! "updateAddrsWith$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr" "$authTokenIpv6" "$newIpv6Addr"; then
			printMsgWarning "An error occurred while updating the IP addresses of \"$domainName\" using protocol \"$updateProtocol\""
			if test "$newIpv4Addr" != ""; then returnCode=$(( returnCode | 2 )); fi
			if test "$newIpv6Addr" != ""; then returnCode=$(( returnCode | 4 )); fi
		fi
		return "$returnCode"

	else
		# Protocol implementation has separate functions for updating IPv4 and IPv6 addresses
		# If provided, update the IPv4 address
		if test "$newIpv4Addr" != ""; then
			if isFunction "updateIpv4AddrWith$updateProtocol"; then
				printMsgInfo "Domain \"$domainName\": Updating IPv4 address to \"${newIpv4Addr}\" using protocol \"$updateProtocol\""
				if ! "updateIpv4AddrWith$updateProtocol" "$domainName" "$authTokenIpv4" "$newIpv4Addr"; then
					printMsgWarning "An error occurred while updating the IPv4 address of \"$domainName\" using protocol \"$updateProtocol\""
					returnCode=$(( returnCode | 2 ))
				fi
			else
				printMsgError "Update function not implemented: \"updateIpv4AddrWith$updateProtocol\""
				returnCode=$(( returnCode | 2 ))
			fi
		fi
		# If provided, update the IPv6 address
		if test "$newIpv6Addr" != ""; then
			if isFunction "updateIpv6AddrWith$updateProtocol"; then
				printMsgInfo "Domain \"$domainName\": Updating IPv6 address to \"${newIpv6Addr}\" using protocol \"$updateProtocol\""
				if ! "updateIpv6AddrWith$updateProtocol" "$domainName" "$authTokenIpv6" "$newIpv6Addr"; then
					printMsgWarning "An error occurred while updating the IPv6 address of \"$domainName\" using protocol \"$updateProtocol\""
					returnCode=$(( returnCode | 4 ))
				fi
			else
				printMsgError "Update function not implemented: \"updateIpv6AddrWith$updateProtocol\""
				returnCode=$(( returnCode | 4 ))
			fi
		fi
		return $returnCode
	fi
}

# updateAddrsWithDeSec domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr
#
# Sets the domain name's IPv4 and IPv6 address using the deSEC protocol
# (update.dedyn.io)
updateAddrsWithDeSec () {
	local domainName="$1"; shift
	local authTokenIpv4="$1"; shift
	local newIpv4Addr="$1"; shift
	local authTokenIpv6="$1"; shift
	local newIpv6Addr="$1"; shift
	local authToken
	local authTokenObfuscated
	local updateUrl
	local httpHeader
	local httpHeaderObfuscated
	local response
	local -i wgetExitCode

	# Determine the single API authentication token
	if test "$authTokenIpv4" != "" && test "$authTokenIpv6" != ""; then
		if test "$authTokenIpv4" != "$authTokenIpv6"; then
			printMsgWarning "Authentication tokens for deSEC API differ, using the one for IPv4"
		fi
		authToken="$authTokenIpv4"
	elif test "$authTokenIpv4" != ""; then
		authToken="$authTokenIpv4"
	elif test "$authTokenIpv6" != ""; then
		authToken="$authTokenIpv6"
	else
		printMsgError "No authentication tokens specified"
		return 1
	fi
	authTokenObfuscated="$(getObfuscatedAuthToken "$authToken")"

	# Make sure we have some IP addresses that should be sent to the API
	if test "$newIpv4Addr" = "" && test "$newIpv6Addr" = ""; then
		printMsgWarning "Neither an IPv4 nor an IPv6 address was supplied, doing nothing"
		return 0
	fi

	# Do not unset IP addresses that have not been specified
	test "$newIpv4Addr" = "" && newIpv4Addr="preserve"
	test "$newIpv6Addr" = "" && newIpv6Addr="preserve"

	# Build the update URL and the HTTP header entry
	updateUrl="${updateUrlBaseDeSec}hostname=${domainName}&ip=${newIpv4Addr}&ipv6=${newIpv6Addr}"
	httpHeader="Authorization: Token $authToken"
	httpHeaderObfuscated="Authorization: Token $authTokenObfuscated"

	# Dispatch the update request
	printMsgDebug "Calling update URL \"$updateUrl\" with extra HTTP header line \"${httpHeaderObfuscated}\""
	response="$(wget -qO - --header="$httpHeader" "$updateUrl" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if test "$response" != "good"; then
		printMsgWarning "Unexpected response from deSEC dynDNS update API: \"$response\""
		return 1
	fi
	return 0
}

# getTxtRecordsOfDomainDefault
#
# Determines the configured domain name's TXT records using the
# configured TXT record DNS server
getTxtRecordsOfDomainDefault () {
	getTxtRecordsOfDomain "$domainName" "${dnsServerTxtIfSet[@]}"
}

# getTxtRecordsOfDomain domainName [dnsServerTxt]
#
# Determines the given domain name's TXT records
# The records are returned in a newline-separated list
# The list is empty if the domain name does not have any TXT records
# Returns with a non-zero code if an error occurred while retrieving the
# domain name's TXT records
getTxtRecordsOfDomain () {
	local domainName="$1"; shift
	local dnsServerTxtIfSet=()
	local commandType
	local result

	if test $# -gt 0; then
		dnsServerTxtIfSet=( "$1" ); shift
	fi
	# Option 1: Ask the custom implementation if available
	if isFunction "getTxtRecordsOfDomainCustom"; then
		printMsgDebug "Retrieving TXT records of domain name \"$domainName\" using getTxtRecordsOfDomainCustom"
		if result="$(getTxtRecordsOfDomainCustom "$domainName" "${dnsServerTxtIfSet[@]}")"; then
			printMsgDebug "TXT records of domain name \"$domainName\" from getTxtRecordsOfDomainCustom:" \
				"$(echo "$result" | joinLines "\"" "\"" "\", \"" false)"
			echo "$result"
			return 0
		fi
	fi
	# Option 2: Use dig
	printMsgDebug "Retrieving TXT records of domain name \"$domainName\" using dig"
	if result="$(getTxtRecordsOfDomainFromDig "$domainName" "${dnsServerTxtIfSet[@]}")"; then
		printMsgDebug "TXT records of domain name \"$domainName\" from dig:" \
			"$(echo "$result" | joinLines "\"" "\"" "\", \"" false)"
		echo "$result"
		return 0
	fi
	return 1
}

# getTxtRecordsOfDomainFromDig domainName [dnsServerTxt]
#
# Determines the given domain name's TXT records using dig
# See getTxtRecordsOfDomain for the function contract
getTxtRecordsOfDomainFromDig () {
	local domainName="$1"; shift
	local dnsServerTxtIfSet=()

	if test $# -gt 0; then
		dnsServerTxtIfSet=( "@$1" ); shift
	fi
	# TODO Fix: For a domain name that has 0 TXT records this returns a
	# single empty line ("<LF>"), but should instead return nothing ("")
	dig "${dnsServerTxtIfSet[@]}" -t TXT -q "$domainName" +noall +answer +short \
		| sed -re 's/^"([^"]*)"$/\1/'
}

# addTxtRecordDefault text
#
# Adds the given TXT record to the configured domain name using the
# configured update protocol and TXT authentication token
addTxtRecordDefault () {
	addTxtRecord "$updateProtocol" "$domainName" "$authTokenTxt" "$1"
}

# addTxtRecord updateProtocol domainName authTokenTxt text
#
# Adds the given TXT record to the domain name
addTxtRecord () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenTxt="$1"; shift
	local text="$1"; shift
	local addFunction="addTxtRecordWith$updateProtocol"

	if ! isFunction "$addFunction"; then
		printMsgError "Function not implemented: \"${addFunction}\""
		return 1
	fi
	"$addFunction" "$domainName" "$authTokenTxt" "$text"
}

# addTxtRecordWithDuckDns domainName authTokenTxt text
#
# Adds the given TXT record to the domain name using the DuckDns
# protocol (duckdns.org)
addTxtRecordWithDuckDns () {
	local domainName="$1"; shift
	local authTokenTxt="$1"; shift
	local text="$1"; shift
	local authTokenTxtObfuscated
	authTokenTxtObfuscated="$(getObfuscatedAuthToken "$authTokenTxt")"
	local updateUrlTxt="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxt}&txt=${text}"
	local updateUrlTxtObfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxtObfuscated}&txt=${text}"
	local response
	local -i wgetExitCode

	printMsgDebug "Calling update URL: \"$updateUrlTxtObfuscated\""
	response="$(wget -qO - "$updateUrlTxt" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! test "$response" = "OK"; then
		printMsgWarning "Unexpected response from DuckDNS DNS TXT record update API: \"$response\""
		return 1
	fi
	return 0
}

# removeTxtRecordDefault text
#
# Removes the given TXT record from the configured domain name using the
# configured update protocol and TXT authentication token
removeTxtRecordDefault () {
	removeTxtRecord "$updateProtocol" "$domainName" "$authTokenTxt" "$1"
}

# removeTxtRecord updateProtocol domainName authTokenTxt text
#
# Removes the given TXT record from the domain name
removeTxtRecord () {
	local updateProtocol="$1"; shift
	local domainName="$1"; shift
	local authTokenTxt="$1"; shift
	local text="$1"; shift
	local removeFunction="removeTxtRecordWith$updateProtocol"

	if ! isFunction "$removeFunction"; then
		printMsgError "Function not implemented: \"${removeFunction}\""
		return 1
	fi
	"$removeFunction" "$domainName" "$authTokenTxt" "$text"
}

# removeTxtRecordWithDuckDns domainName authTokenTxt text
#
# Removes the given TXT record from the domain name using the DuckDns
# protocol (duckdns.org)
removeTxtRecordWithDuckDns () {
	local domainName="$1"; shift
	local authTokenTxt="$1"; shift
	local text="$1"; shift
	local authTokenTxtObfuscated
	authTokenTxtObfuscated="$(getObfuscatedAuthToken "$authTokenTxt")"
	local updateUrlTxt="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxt}&txt=${text}&clear=true"
	local updateUrlTxtObfuscated="${updateUrlBaseDuckDns}domains=${domainName}&token=${authTokenTxtObfuscated}&txt=${text}&clear=true"
	local response
	local -i wgetExitCode

	printMsgDebug "Calling update URL: \"$updateUrlTxtObfuscated\""
	response="$(wget -qO - "$updateUrlTxt" | head -q -n 1 -c 120 || isWhitelistedExitCode $? 3 141)" \
		&& wgetExitCode="$?" || wgetExitCode="$?"
	test "$wgetExitCode" -eq 0 || return "$wgetExitCode"
	printMsgDebug "Response started with \"$response\""
	if ! test "$response" = "OK"; then
		printMsgWarning "Unexpected response from DuckDNS DNS TXT record update API: \"$response\""
		return 1
	fi
	return 0
}

# 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 updateUrlBaseDeSec="https://update.dedyn.io?"
declare sleepTimeDefault="2m"

# Handle missing arguments
if test $# -eq 0; then
	printHelpMessage
	exit 1
fi

# Handle --help as single argument
if test $# -eq 1 && test "$1" = "--help"; then
	printHelpMessage
	exit 0
fi

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

# Make sure the absolutely required programs are available
allRequiredProgramsPresent=true
type find &> /dev/null  || { printMsgError "Required program \"find\" is not available";  allRequiredProgramsPresent=false; }
type grep &> /dev/null  || { printMsgError "Required program \"grep\" is not available";  allRequiredProgramsPresent=false; }
type sed &> /dev/null   || { printMsgError "Required program \"sed\" is not available";   allRequiredProgramsPresent=false; }
$allRequiredProgramsPresent || exit 1
unset allRequiredProgramsPresent

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

# Scan the arguments for --verbose up front and set $verbose accordingly
for argument in "$@"; do
	if test "$argument" = "--verbose"; then
		verbose=true
		break
	fi
done
unset argument

# Source the configuration files
while read -rs configurationFile; do
	printMsgInfo "Sourcing configuration file \"$configurationFile\""
	# shellcheck source=/dev/null
	if ! source -- "$configurationFile"; then
		printMsgError "Failed to source configuration file \"${configurationFile}\""
		exit 1
	fi
done < <(getConfigurationFilesSystem; getConfigurationFilesUser)
unset configurationFile

# 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
		printMsgInfo "Sourcing argument-supplied configuration file \"$argument\""
		# shellcheck source=/dev/null
		if ! source -- "$argument"; then
			printMsgError "Failed to source configuration file \"${argument}\""
			exit 1
		fi
	elif test "$argument" = "--config-file"; then
		nextArgIsValueForConfigFile=true
		continue
	fi
	nextArgIsValueForConfigFile=false
done
if $nextArgIsValueForConfigFile; then
	printMsgError "Trailing argument has no value: \"$argument\""
	exit 1
fi
unset nextArgIsValueForConfigFile
unset argument

# 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 "--one-shot" = "$1"; then
		oneShot=true; shift
	elif test "--verbose" = "$1"; then
		verbose=true; shift
	elif test "--list-functions" = "$1"; then
		listFunctions=true; shift
	# Special arguments that are handled elsewhere
	elif test "--help" = "$1"; then
		printMsgError "Special argument \"--help\" must be given as single argument"
		exit 1
	elif test "--version" = "$1"; then
		printMsgError "Special argument \"--version\" must be given as single argument"
		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
		printMsgError "Trailing argument has no value: \"$1\""
		exit 1
	elif test "--auth-token-ipv4" = "$1"; then
		shift
		authTokenIpv4="$1"; shift
	elif test "--auth-token-ipv6" = "$1"; then
		shift
		authTokenIpv6="$1"; shift
	elif test "--call-function" = "$1"; then
		shift
		callFunction="$1"; shift
		# From here on it is any number of trailing function arguments,
		# therefore cease argument parsing
		break
	elif test "--config-file" = "$1"; then
		# Skip --config-file and its value because all config files have
		# already been sourced
		shift 2
	elif test "--detect-public-addr-ipv4" = "$1"; then
		shift
		detectPublicAddrIpv4=()
		while read -rsd ',' currentWord || test "$currentWord" != ""; do
			detectPublicAddrIpv4+=( "$currentWord" )
		done < <(echo -n "$1")
		shift
	elif test "--detect-public-addr-ipv6" = "$1"; then
		shift
		detectPublicAddrIpv6=()
		while read -rsd ',' currentWord || test "$currentWord" != ""; do
			detectPublicAddrIpv6+=( "$currentWord" )
		done < <(echo -n "$1")
		shift
	elif test "--dns-server" = "$1"; then
		shift
		dnsServer="$1"; shift
	elif test "--dns-server-ipv4" = "$1"; then
		shift
		dnsServerIpv4="$1"; shift
	elif test "--dns-server-ipv6" = "$1"; then
		shift
		dnsServerIpv6="$1"; shift
	elif test "--domain-name" = "$1"; then
		shift
		domainName="$1"; shift
	elif test "--update-protocol" = "$1"; then
		shift
		updateProtocol="$1"; shift
	elif test "--sleep-time" = "$1"; then
		shift
		sleepTime="$1"; shift
	else
		printMsgError "Unknown argument \"$1\""
		exit 1
	fi
done

# Set some derived variables
if test "$dnsServer" != ""; then
	test "$dnsServerIpv4" = "" && dnsServerIpv4="$dnsServer"
	test "$dnsServerIpv6" = "" && dnsServerIpv6="$dnsServer"
	test "$dnsServerTxt" = "" && dnsServerTxt="$dnsServer"
fi
# Some optional options are also available as arrays so that they can
# be used for function calls without if-else constructs
test "$dnsServerIpv4" != "" && dnsServerIpv4IfSet=("$dnsServerIpv4") || dnsServerIpv4IfSet=()
test "$dnsServerIpv6" != "" && dnsServerIpv6IfSet=("$dnsServerIpv6") || dnsServerIpv6IfSet=()
test "$dnsServerTxt" != "" && dnsServerTxtIfSet=("$dnsServerTxt") || dnsServerTxtIfSet=()

# Validate some very basic easy-to-validate options
if ! test "$verbose" = true && ! test "$verbose" = false; then
	printMsgError "--verbose, if set in a configuration file, must be either \"true\" or \"false\", was \"$verbose\""
	exit 1
fi

# If verbose mode is enabled, report various programs that the
# application can run without, albeit with reduced functionality
if $verbose; then
	$upnpAvailable || printMsgDebug "Public IPv4 address detection method \"Upnp\" cannot" \
		"be used because the program \"upnpc\" is not available"
	type bc &> /dev/null    || { printMsgDebug "Program \"bc\" not available, some functionality may be compromised";    }
	type dig &> /dev/null   || { printMsgDebug "Program \"dig\" not available, some functionality may be compromised";   }
	type head &> /dev/null  || { printMsgDebug "Program \"head\" not available, some functionality may be compromised";  }
	type ip &> /dev/null    || { printMsgDebug "Program \"ip\" not available, some functionality may be compromised";    }
	type sleep &> /dev/null || { printMsgDebug "Program \"sleep\" not available, some functionality may be compromised"; }
	type tail &> /dev/null  || { printMsgDebug "Program \"tail\" not available, some functionality may be compromised";  }
	type wget &> /dev/null  || { printMsgDebug "Program \"wget\" not available, some functionality may be compromised";  }
fi

# Special functionality --list-functions
if ! test "$listFunctions" = true && ! test "$listFunctions" = false; then
	printMsgError "--list-functions, if set in a configuration file, must be either \"true\" or \"false\", was \"$listFunctions\""
	exit 1
fi
if $listFunctions; then
	echo -n "Functions that will be used if they are declared:
  getIpv4AddrsOfDomainCustom domainName [dnsServerIpv4]
  getIpv6AddrsOfDomainCustom domainName [dnsServerIpv6]
  getTxtRecordsOfDomainCustom domainName [dnsServerTxt]
"
	echo "Functions declared in this script or in one of its 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/^(addTxtRecord)$/\1 updateProtocol domainName authTokenTxt text/;'\
's/^(addTxtRecordDefault)$/\1 text/;'\
's/^(addTxtRecordWith[^ ]+)$/\1 domainName authTokenTxt text/;'\
's/^(appendMsg[^ ]+)$/\1 [text].../;'\
's/^(containsExactLine)$/\1 lineToFind (reads STDIN)/;'\
's/^(getIpv4AddrAsHexString)$/\1 ipv4Addr/;'\
's/^(getIpv4AddrOfThisHost)$/\1 [method[@@argument]...]/;'\
's/^(getIpv4AddrOfThisHostFromFile)$/\1 path/;'\
's/^(getIpv4AddrOfThisHostFromNetDev)$/\1 [networkDevice]/;'\
's/^(getIpv4AddrOfThisHostFromUrl)$/\1 url/;'\
's/^(getIpv4AddrsOfDomain)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv4AddrsOfDomainCustom)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv4AddrsOfDomainFrom[^ ]*)$/\1 domainName [dnsServerIpv4]/;'\
's/^(getIpv6AddrOfThisHost)$/\1 [method[@@argument]...]/;'\
's/^(getIpv6AddrOfThisHostFromFile)$/\1 path/;'\
's/^(getIpv6AddrOfThisHostFromNetDev)$/\1 [networkDevice]/;'\
's/^(getIpv6AddrOfThisHostFromUrl)$/\1 url/;'\
's/^(getIpv6AddrsOfDomain)$/\1 domainName [dnsServerIpv6]/;'\
's/^(getIpv6AddrsOfDomainCustom)$/\1 domainName [dnsServerIpv6]/;'\
's/^(getIpv6AddrsOfDomainFrom[^ ]*)$/\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/^(getTxtRecordsOfDomain)$/\1 domainName [dnsServerTxt]/;'\
's/^(getTxtRecordsOfDomainCustom)$/\1 domainName [dnsServerTxt]/;'\
's/^(getTxtRecordsOfDomainFrom[^ ]*)$/\1 domainName [dnsServerTxt]/;'\
'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 prefix suffix separator [appendFinalNewline] (reads STDIN)/;'\
's/^(printMsg)$/\1 levelPrefix [text].../;'\
's/^(printMsg[^ ]+)$/\1 [text].../;'\
's/^(removeTxtRecord)$/\1 updateProtocol domainName authTokenTxt text/;'\
's/^(removeTxtRecordDefault)$/\1 text/;'\
's/^(removeTxtRecordWith[^ ]+)$/\1 domainName authTokenTxt text/;'\
's/^(splitBySubstring)$/\1 string substring/;'\
's/^(updateAddrs)$/\1 updateProtocol domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr/;'\
's/^(updateAddrsDefault)$/\1 newIpv4Addr newIpv6Addr/;'\
's/^(updateAddrsIfRequired)$/\1 updateProtocol domainName authTokenIpv4 dnsServerIpv4 authTokenIpv6 dnsServerIpv6/;'\
's/^(updateAddrsWith[^ ]+)$/\1 domainName authTokenIpv4 newIpv4Addr authTokenIpv6 newIpv6Addr/;'\
's/^(updateIpv4Addr)$/\1 updateProtocol domainName authTokenIpv4 newIpv4Addr/;'\
's/^(updateIpv4AddrDefault)$/\1 newIpv4Addr/;'\
's/^(updateIpv4AddrIfRequired)$/\1 updateProtocol domainName authTokenIpv4 [dnsServerIpv4]/;'\
's/^(updateIpv4AddrWith[^ ]+)$/\1 domainName authTokenIpv4 newIpv4Addr/;'\
's/^(updateIpv6Addr)$/\1 updateProtocol domainName authTokenIpv6 newIpv6Addr/;'\
's/^(updateIpv6AddrDefault)$/\1 newIpv6Addr/;'\
's/^(updateIpv6AddrIfRequired)$/\1 updateProtocol domainName authTokenIpv6 [dnsServerIpv6]/;'\
's/^(updateIpv6AddrWith[^ ]+)$/\1 domainName authTokenIpv6 newIpv6Addr/;'\
's/^(.*)$/  \1/'
	exit 0
fi

# Validate options that are required for, or typically relevant for
# --call-function
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
	appendMsgError "--detect-public-addr-ipv4: Invalid public IPv4 address detection method \"$methodName\" specified. Available method(s):"
	while read -rs validMethod; do
		appendMsgError "\"$validMethod\""
	done < <(declare -F | sed -re 's/^declare -f //' \
				| { grep '^getIpv4AddrOfThisHostFrom' || true; } \
				| sed -re 's/^getIpv.AddrOfThisHostFrom//')
	printMsgError
	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
	appendMsgError "--detect-public-addr-ipv6: Invalid public IPv6 address detection method \"$methodName\" specified. Available method(s):"
	while read -rs validMethod; do
		appendMsgError "\"$validMethod\""
	done < <(declare -F | sed -re 's/^declare -f //' \
				| { grep '^getIpv6AddrOfThisHostFrom' || true; } \
				| sed -re 's/^getIpv.AddrOfThisHostFrom//')
	printMsgError
	exit 1
done
unset methodString
unset methodName
unset functionName
unset validMethod

# Special functionality --call-function
if ! test -z ${callFunction+x}; then
	printMsgDebug "Calling function \"$callFunction\" with $# argument(s)"

	# 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
			printMsgDebug "Boolean function returned true (code ${returnCode})"
		elif test "$returnCode" -eq 1; then
			# Regular false return
			printMsgDebug "Boolean function returned false (code ${returnCode})"
		else
			# Uh-oh, possibly some unhandled error
			printMsgWarning "Boolean function \"$callFunction\" returned false with unusual code ${returnCode}"
		fi
	else
		if test "$returnCode" -eq 0; then
			printMsgDebug "Function returned with code $returnCode"
		else
			# Be a bit more panicky if a non-boolean function returned
			# with a bad code
			printMsgWarning "Function \"$callFunction\" returned with code $returnCode"
		fi
	fi
	exit "$returnCode"
fi

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

# All arguments validated and ready to go; print a summary
printMsgInfo "This is yabddnsd $(getVersion)"
printMsgInfo "Domain name: \"${domainName}\""
printMsgInfo "Update protocol: \"$updateProtocol\""
test "$authTokenIpv4" != "" && printMsgInfo "IPv4 authentication token: \"$(getObfuscatedAuthToken "$authTokenIpv4")\""
test "$authTokenIpv6" != "" && printMsgInfo "IPv6 authentication token: \"$(getObfuscatedAuthToken "$authTokenIpv6")\""
declare method
if test "$authTokenIpv4" != "" && test ${#detectPublicAddrIpv4[@]} -gt 0; then
	appendMsgInfo "Public IPv4 address detection method(s):"
	for method in "${detectPublicAddrIpv4[@]}"; do
		appendMsgInfo "\"$method\""
	done
	printMsgInfo
fi
if test "$authTokenIpv6" != "" && test ${#detectPublicAddrIpv6[@]} -gt 0; then
	appendMsgInfo "Public IPv6 address detection method(s):"
	for method in "${detectPublicAddrIpv6[@]}"; do
		appendMsgInfo "\"$method\""
	done
	printMsgInfo
fi
unset method
test "$dnsServerIpv4" != "" && printMsgInfo "IPv4 DNS server: \"${dnsServerIpv4}\""
test "$dnsServerIpv6" != "" && printMsgInfo "IPv6 DNS server: \"${dnsServerIpv6}\""
test "$dnsServerTxt" != "" && printMsgInfo "TXT record DNS server: \"${dnsServerIpv6}\""
if $oneShot; then
	printMsgInfo "One-shot operation (--one-shot) is active: Dispatching at most a single update"
else
	printMsgInfo "Sleeping time between iterations: \"${sleepTime}\""
fi
printMsgDebug "Verbose output is enabled"

# Handle one-shot operation
if $oneShot; then
	updateAddrsIfRequiredDefault
	exit $?
fi

# Get going
printMsgInfo "Entering main loop"
while true ; do
	updateAddrsIfRequiredDefault || true
	printMsgDebug "Sleeping for \"$sleepTime\""
	if ! sleep "$sleepTime"; then
		printMsgWarning "Calling sleep with argument \"${sleepTime}\" returned with a non-zero code, calling it with \"${sleepTimeDefault}\" instead"
		sleep "$sleepTimeDefault"
	fi
done