2021-05-07 13:30:33 -06:00
|
|
|
#!/bin/sh
|
|
|
|
|
|
|
|
#
|
|
|
|
# update CloudFlare DNS records with current (dynamic) IP address
|
|
|
|
# Script by Asif Bacchus <asif@bacchus.cloud>
|
|
|
|
# Last modified: May 7, 2021
|
|
|
|
#
|
|
|
|
|
|
|
|
### text formatting presets using tput
|
|
|
|
if command -v tput >/dev/null; then
|
|
|
|
bold=$(tput bold)
|
|
|
|
cyan=$(tput setaf 6)
|
|
|
|
err=$(tput bold)$(tput setaf 1)
|
|
|
|
magenta=$(tput setaf 5)
|
|
|
|
norm=$(tput sgr0)
|
|
|
|
ok=$(tput setaf 2)
|
|
|
|
warn=$(tput bold)$(tput setaf 3)
|
|
|
|
yellow=$(tput setaf 3)
|
|
|
|
width=$(tput cols)
|
|
|
|
else
|
|
|
|
bold=""
|
|
|
|
cyan=""
|
|
|
|
err=""
|
|
|
|
magenta=""
|
|
|
|
norm=""
|
|
|
|
ok=""
|
|
|
|
warn=""
|
|
|
|
yellow=""
|
|
|
|
width=80
|
|
|
|
fi
|
|
|
|
|
|
|
|
### functions
|
|
|
|
badParam() {
|
|
|
|
if [ "$1" = "null" ]; then
|
|
|
|
printf "\n%sERROR: '%s' cannot have a NULL (empty) value.\n" "$err" "$2"
|
|
|
|
printf "%sPlease use '--help' for assistance.%s\n\n" "$cyan" "$norm"
|
|
|
|
exit 1
|
|
|
|
elif [ "$1" = "dne" ]; then
|
|
|
|
printf "\n%sERROR: '%s %s'\n" "$err" "$2" "$3"
|
|
|
|
printf "file or directory does not exist or is empty.%s\n\n" "$norm"
|
|
|
|
exit 1
|
|
|
|
elif [ "$1" = "errMsg" ]; then
|
|
|
|
printf "\n%sERROR: %s%s\n\n" "$err" "$2" "$norm"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
2021-05-07 15:32:46 -06:00
|
|
|
exitError() {
|
2021-05-08 02:05:32 -06:00
|
|
|
case "$1" in
|
|
|
|
10)
|
|
|
|
errMsg="Unable to auto-detect IP address. Try again later or supply the IP address to be used."
|
|
|
|
;;
|
|
|
|
20)
|
|
|
|
errMsg="CloudFlare authorized email address (cfEmail) is either null or undefined. Please check your CloudFlare credentials file."
|
|
|
|
;;
|
|
|
|
21)
|
|
|
|
errMsg="CloudFlare authorized API key (cfKey) is either null or undefined. Please check your CloudFlare credentials file."
|
|
|
|
;;
|
|
|
|
22)
|
|
|
|
errMsg="CloudFlare zone id (cfZoneId) is either null or undefined. Please check your CloudFlare credentials file."
|
|
|
|
;;
|
|
|
|
25)
|
|
|
|
errMsg="Unable to query CloudFlare account. Please re-check your credentials and try again later."
|
|
|
|
;;
|
|
|
|
98)
|
|
|
|
errMsg="One or more domain updates failed. Please review this log file for details."
|
|
|
|
;;
|
|
|
|
*)
|
2021-05-07 15:32:46 -06:00
|
|
|
printf "%s[%s] ERROR: An unspecified error occurred. Exiting.%s\n" "$err" "$(stamp)" "$norm" >>"$logFile"
|
|
|
|
exit 99
|
2021-05-08 02:05:32 -06:00
|
|
|
;;
|
|
|
|
esac
|
2021-05-07 15:32:46 -06:00
|
|
|
printf "%s[%s] ERROR: %s (code: %s)%s\n" "$err" "$(stamp)" "$errMsg" "$1" "$norm" >>"$logFile"
|
2021-05-08 02:05:32 -06:00
|
|
|
printf "%s[%s] -- CloudFlare DDNS update-script: execution completed with error(s) --%s\n" "$err" "$(stamp)" "$norm" >>"$logFile"
|
2021-05-07 15:32:46 -06:00
|
|
|
exit "$1"
|
|
|
|
}
|
|
|
|
|
|
|
|
exitOK() {
|
|
|
|
printf "%s[%s] -- CloudFlare DDNS update-script: execution complete --%s\n" "$ok" "$(stamp)" "$norm" >>"$logFile"
|
|
|
|
exit 0
|
|
|
|
}
|
|
|
|
|
2021-05-07 13:30:33 -06:00
|
|
|
stamp() {
|
|
|
|
(date +%F" "%T)
|
|
|
|
}
|
|
|
|
|
|
|
|
scriptHelp() {
|
|
|
|
printf "\nEventually an in-script help will be here...\n\n"
|
|
|
|
exit 0
|
|
|
|
}
|
|
|
|
|
|
|
|
### default variable values
|
|
|
|
scriptPath="$(CDPATH='' \cd -- "$(dirname -- "$0")" && pwd -P)"
|
|
|
|
scriptName="$(basename "$0")"
|
|
|
|
logFile="$scriptPath/${scriptName%.*}.log"
|
|
|
|
accountFile="$scriptPath/cloudflare.credentials"
|
|
|
|
colourizeLogFile=1
|
|
|
|
dnsRecords=""
|
|
|
|
dnsSeparator=","
|
2021-05-07 15:32:46 -06:00
|
|
|
ipAddress=""
|
2021-05-07 13:30:33 -06:00
|
|
|
ip4=1
|
|
|
|
ip6=0
|
2021-05-07 15:32:46 -06:00
|
|
|
ip4DetectionSvc="http://ipv4.icanhazip.com"
|
|
|
|
ip6DetectionSvc="http://ipv6.icanhazip.com"
|
2021-05-08 02:05:32 -06:00
|
|
|
useCFProxy="false"
|
|
|
|
invalidDomainCount=0
|
|
|
|
failedDomainCount=0
|
2021-05-07 13:30:33 -06:00
|
|
|
|
|
|
|
### process startup parameters
|
|
|
|
if [ -z "$1" ]; then
|
|
|
|
scriptHelp
|
|
|
|
fi
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
case "$1" in
|
|
|
|
-h | -\? | --help)
|
|
|
|
# display help
|
|
|
|
scriptHelp
|
|
|
|
;;
|
|
|
|
-l | --log)
|
|
|
|
# set log file location
|
|
|
|
if [ -n "$2" ]; then
|
|
|
|
logFile="${2%/}"
|
|
|
|
shift
|
|
|
|
else
|
|
|
|
badParam null "$@"
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
--nc | --no-color | --no-colour)
|
|
|
|
# do not colourize log file
|
|
|
|
colourizeLogFile=0
|
|
|
|
;;
|
|
|
|
-c | --cred* | -f)
|
|
|
|
# path to CloudFlare credentials file
|
|
|
|
if [ -n "$2" ]; then
|
|
|
|
if [ -f "$2" ] && [ -s "$2" ]; then
|
|
|
|
accountFile="${2%/}"
|
|
|
|
shift
|
|
|
|
else
|
|
|
|
badParam dne "$@"
|
|
|
|
fi
|
|
|
|
else
|
|
|
|
badParam null "$@"
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
-r | --record | --records)
|
|
|
|
# DNS records to update
|
|
|
|
if [ -n "$2" ]; then
|
|
|
|
dnsRecords=$(printf "%s" "$2" | sed -e 's/ //g')
|
|
|
|
shift
|
|
|
|
else
|
|
|
|
badParam null "$@"
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
-i | --ip | --ip-address | -a | --address)
|
|
|
|
# IP address to use (not parsed for correctness)
|
|
|
|
if [ -n "$2" ]; then
|
|
|
|
ipAddress="$2"
|
|
|
|
shift
|
|
|
|
else
|
|
|
|
badParam null "$@"
|
|
|
|
fi
|
|
|
|
;;
|
2021-05-08 02:05:32 -06:00
|
|
|
-p | --proxy)
|
|
|
|
# use CloudFlare proxy for all updated hosts
|
|
|
|
useCFProxy="true"
|
|
|
|
;;
|
2021-05-07 13:30:33 -06:00
|
|
|
-4 | --ip4 | --ipv4)
|
|
|
|
# operate in IP4 mode (default)
|
|
|
|
ip4=1
|
2021-05-07 14:03:52 -06:00
|
|
|
ip6=0
|
2021-05-07 13:30:33 -06:00
|
|
|
;;
|
|
|
|
-6 | --ip6 | --ipv6)
|
|
|
|
# operate in IP6 mode
|
|
|
|
ip6=1
|
2021-05-07 14:00:32 -06:00
|
|
|
ip4=0
|
2021-05-07 13:30:33 -06:00
|
|
|
;;
|
|
|
|
*)
|
|
|
|
printf "\n%sUnknown option: %s\n" "$err" "$1"
|
|
|
|
printf "%sUse '--help' for valid options.%s\n\n" "$cyan" "$norm"
|
|
|
|
exit 1
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
shift
|
|
|
|
done
|
|
|
|
|
|
|
|
### pre-flight checks
|
2021-05-07 13:59:07 -06:00
|
|
|
if ! command -v curl >/dev/null; then
|
2021-05-08 02:05:32 -06:00
|
|
|
printf "\n%sThis script requires 'curl' be installed and accessible. Exiting.%s\n\n" "$err" "$norm"
|
|
|
|
exit 2
|
|
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null; then
|
|
|
|
printf "\n%sThis script requires 'jq' be installed and accessible. Exiting.%s\n\n" "$err" "$norm"
|
2021-05-07 13:59:07 -06:00
|
|
|
exit 2
|
|
|
|
fi
|
2021-05-07 13:30:33 -06:00
|
|
|
[ -z "$dnsRecords" ] && badParam errMsg "You must specify at least one DNS record to update. Exiting."
|
2021-05-07 13:59:07 -06:00
|
|
|
# turn off log file colourization if parameter is set
|
|
|
|
if [ "$colourizeLogFile" -eq 0 ]; then
|
|
|
|
bold=""
|
|
|
|
cyan=""
|
|
|
|
err=""
|
|
|
|
magenta=""
|
|
|
|
norm=""
|
|
|
|
ok=""
|
|
|
|
warn=""
|
|
|
|
yellow=""
|
2021-05-07 13:30:33 -06:00
|
|
|
fi
|
|
|
|
|
2021-05-07 13:59:07 -06:00
|
|
|
### initial log entries
|
|
|
|
{
|
|
|
|
printf "%s[%s] -- CloudFlare DDNS update-script: execution starting --%s\n" "$ok" "$(stamp)" "$norm"
|
|
|
|
printf "%sParameters:\n" "$magenta"
|
|
|
|
printf "script path: %s\n" "$scriptPath/$scriptName"
|
|
|
|
printf "credentials file: %s\n" "$accountFile"
|
|
|
|
} >>"$logFile"
|
2021-05-07 13:30:33 -06:00
|
|
|
|
2021-05-07 15:32:46 -06:00
|
|
|
if [ "$ip4" -eq 1 ]; then
|
|
|
|
printf "mode: IP4\n" >>"$logFile"
|
|
|
|
elif [ "$ip6" -eq 1 ]; then
|
|
|
|
printf "mode: IP6\n" >>"$logFile"
|
|
|
|
fi
|
|
|
|
|
|
|
|
# detect and report IP address
|
|
|
|
if [ -z "$ipAddress" ]; then
|
|
|
|
# detect public ip address
|
|
|
|
if [ "$ip4" -eq 1 ]; then
|
|
|
|
if ! ipAddress="$(curl -s $ip4DetectionSvc)"; then
|
|
|
|
printf "ddns ip address: %serror%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 10
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
if [ "$ip6" -eq 1 ]; then
|
|
|
|
if ! ipAddress="$(curl -s $ip6DetectionSvc)"; then
|
|
|
|
printf "ddns ip address: %serror%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 10
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
printf "ddns ip address (detected): %s\n" "$ipAddress" >>"$logFile"
|
|
|
|
else
|
|
|
|
printf "ddns ip address (supplied): %s\n" "$ipAddress" >>"$logFile"
|
|
|
|
fi
|
|
|
|
|
|
|
|
# iterate DNS records to update
|
|
|
|
dnsRecordsToUpdate="$dnsRecords$dnsSeparator"
|
|
|
|
while [ "$dnsRecordsToUpdate" != "${dnsRecordsToUpdate#*${dnsSeparator}}" ] && { [ -n "${dnsRecordsToUpdate%%${dnsSeparator}*}" ] || [ -n "${dnsRecordsToUpdate#*${dnsSeparator}}" ]; }; do
|
|
|
|
record="${dnsRecordsToUpdate%%${dnsSeparator}*}"
|
|
|
|
dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}"
|
|
|
|
printf "updating record: %s\n" "$record" >>"$logFile"
|
|
|
|
done
|
|
|
|
|
|
|
|
printf "(end of parameter list)%s\n" "$norm" >>"$logFile"
|
|
|
|
|
2021-05-08 02:05:32 -06:00
|
|
|
### read CloudFlare credentials
|
|
|
|
printf "[%s] Reading CloudFlare credentials... " "$(stamp)" >>"$logFile"
|
|
|
|
case "$accountFile" in
|
|
|
|
/*)
|
|
|
|
# absolute path, use as-is
|
|
|
|
# shellcheck source=./cloudflare.credentials
|
|
|
|
. "$accountFile"
|
|
|
|
;;
|
|
|
|
*)
|
|
|
|
# relative path, rewrite
|
|
|
|
# shellcheck source=./cloudflare.credentials
|
|
|
|
. "./$accountFile"
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
if [ -z "$cfEmail" ]; then
|
|
|
|
printf "%sERROR%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 20
|
|
|
|
elif [ -z "$cfKey" ]; then
|
|
|
|
printf "%sERROR%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 21
|
|
|
|
elif [ -z "$cfZoneId" ]; then
|
|
|
|
printf "%sERROR%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 22
|
|
|
|
fi
|
|
|
|
printf "DONE%s\n" "$norm" >>"$logFile"
|
|
|
|
|
|
|
|
### check if records to be updated exist and if they need updating, update as required
|
|
|
|
dnsRecordsToUpdate="$dnsRecords$dnsSeparator"
|
|
|
|
if [ "$ip4" -eq 1 ]; then
|
|
|
|
recordType="A"
|
|
|
|
elif [ "$ip6" -eq 1 ]; then
|
|
|
|
recordType="AAAA"
|
|
|
|
fi
|
|
|
|
while [ "$dnsRecordsToUpdate" != "${dnsRecordsToUpdate#*${dnsSeparator}}" ] && { [ -n "${dnsRecordsToUpdate%%${dnsSeparator}*}" ] || [ -n "${dnsRecordsToUpdate#*${dnsSeparator}}" ]; }; do
|
|
|
|
record="${dnsRecordsToUpdate%%${dnsSeparator}*}"
|
|
|
|
dnsRecordsToUpdate="${dnsRecordsToUpdate#*${dnsSeparator}}"
|
|
|
|
printf "[%s] Processing %s... " "$(stamp)" "$record" >>"$logFile"
|
|
|
|
# check for existing record, else exit with error (this script does NOT create new records, only updates them!)
|
|
|
|
if ! cfResult="$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${cfZoneId}/dns_records?name=${record}&type=${recordType}" \
|
|
|
|
-H "Authorization: Bearer ${cfKey}" \
|
|
|
|
-H "Content-Type: application/json")"; then
|
|
|
|
printf "%sERROR%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 25
|
|
|
|
fi
|
|
|
|
resultCount="$(printf "%s" "$cfResult" | jq '.result_info.count')"
|
|
|
|
if [ "$resultCount" = "0" ]; then
|
|
|
|
printf "%sNOT FOUND%s\n" "$warn" "$norm" >>"$logFile"
|
|
|
|
printf "%s[%s] WARNING: Cannot find existing record to update for DNS entry: %s%s\n" "$warn" "$(stamp)" "$record" "$norm" >>"$logFile"
|
|
|
|
invalidDomainCount=$((invalidDomainCount + 1))
|
|
|
|
else
|
|
|
|
objectId=$(printf "%s" "$cfResult" | jq -r '.result | .[] | .id')
|
|
|
|
currentIpAddr=$(printf "%s" "$cfResult" | jq -r '.result | .[] | .content')
|
|
|
|
printf "FOUND: IP = %s\n" "$currentIpAddr" >>"$logFile"
|
|
|
|
# check if record needs updating
|
|
|
|
if [ "$currentIpAddr" = "$ipAddress" ]; then
|
|
|
|
printf "%s[%s] IP address for %s is already up-to-date%s\n" "$ok" "$(stamp)" "$record" "$norm" >>"$logFile"
|
|
|
|
else
|
|
|
|
# update record
|
|
|
|
printf "%s[%s] Updating IP address for %s... " "$cyan" "$(stamp)" "$record" >>"$logFile"
|
|
|
|
if [ "$ip4" -eq 1 ]; then
|
|
|
|
updateJSON="$(jq -n \
|
|
|
|
--arg key0 type --arg value0 A \
|
|
|
|
--arg key1 name --arg value1 "${record}" \
|
|
|
|
--arg key2 content --arg value2 "${ipAddress}" \
|
|
|
|
--arg key3 ttl --arg value3 1 \
|
|
|
|
--arg key4 proxied --arg value4 "${useCFProxy}" \
|
|
|
|
'{($key0):$value0,($key1):$value1,($key2):$value2,($key3):$value3,($key4):$value4}')"
|
|
|
|
elif [ "$ip6" -eq 1 ]; then
|
|
|
|
updateJSON="$(jq -n \
|
|
|
|
--arg key0 type --arg value0 AAAA \
|
|
|
|
--arg key1 name --arg value1 "${record}" \
|
|
|
|
--arg key2 content --arg value2 "${ipAddress}" \
|
|
|
|
--arg key3 ttl --arg value3 1 \
|
|
|
|
--arg key4 proxied --arg value4 "${useCFProxy}" \
|
|
|
|
'{($key0):$value0,($key1):$value1,($key2):$value2,($key3):$value3,($key4):$value4}')"
|
|
|
|
fi
|
|
|
|
if ! cfResult="$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/${cfZoneId}/dns_records/${objectId}" \
|
|
|
|
-H "Authorization: Bearer ${cfKey}" \
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
--data "${updateJSON}")"; then
|
|
|
|
printf "%sERROR%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
exitError 25
|
|
|
|
fi
|
|
|
|
updateSuccess="$(printf "%s" "$cfResult" | jq '.success')"
|
|
|
|
if [ "$updateSuccess" = "true" ]; then
|
|
|
|
printf "DONE%s\n" "$norm" >>"$logFile"
|
|
|
|
printf "%s[%s] SUCCESS: IP address for %s updated%s\n" "$ok" "$(stamp)" "$record" "$norm" >>"$logFile"
|
|
|
|
else
|
|
|
|
printf "%sFAILED%s\n" "$err" "$norm" >>"$logFile"
|
|
|
|
printf "%s[%s] ERROR: Unable to update IP address for %s%s\n" "$err" "$(stamp)" "$record" "$norm" >>"$logFile"
|
|
|
|
failedDomainCount=$((failedDomainCount + 1))
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
fi
|
|
|
|
done
|
|
|
|
|
|
|
|
# exit
|
|
|
|
if [ "$invalidDomainCount" -ne 0 ]; then
|
|
|
|
printf "%s[%s] -- WARNING: %s invalid domain(s) were supplied for updating --%s\n" "$warn" "$(stamp)" "$invalidDomainCount" "$norm" >>"$logFile"
|
|
|
|
fi
|
|
|
|
if [ "$failedDomainCount" -ne 0 ]; then
|
|
|
|
exitError 98
|
|
|
|
else
|
|
|
|
exitOK
|
|
|
|
fi
|
2021-05-07 13:59:07 -06:00
|
|
|
|
|
|
|
### exit return codes
|
|
|
|
# 0: normal exit, no errors
|
|
|
|
# 1: invalid or unknown parameter
|
2021-05-08 02:05:32 -06:00
|
|
|
# 2: cannot find or access required external program(s)
|
|
|
|
# 10: cannot auto-detect IP address
|
|
|
|
# 20: accountFile has a null or missing cfEmail variable
|
|
|
|
# 21: accountFile has a null or missing cfKey variable
|
|
|
|
# 22: accountFile has a null or missing cfZoneId variable
|
|
|
|
# 25: unable to query CloudFlare account
|
|
|
|
# 97: script completed with warnings
|
|
|
|
# 98: one or more updates failed
|
2021-05-07 15:32:46 -06:00
|
|
|
# 99: unspecified error occurred
|