#!/usr/bin/env bash quiet=false dryrun=false print_usage() { echo '"-t": test (dry-run) flag, display what would be updated, without committing the change' echo '"-q": quiet flag, to suppress "already set to" messages' echo '"-f": query filters, see: https://developers.cloudflare.com/api/operations/dns-records-for-a-zone-list-dns-records#Query-Parameters' echo '"-c": create record, if no records match our query, we can create one, takes json object of record to make, see: https://developers.cloudflare.com/api/resources/dns/subresources/records/models/a_record/#(schema)' echo 'when using regular expressions, use "\1" for first capture group, no look-forward or look-behind, nor non-greedy wildcards' echo 'content can use the following tags: and which will be replaced with equivalent ip address, each domain tag can be used once' echo "Usage: [token=] [zone=] [prefix=] $0 [-q] [-f 'query-filters'] [-c 'create-record-json'] ('content' | 'regex-replace' 'regex-find')" } # known private ranges local4='192.168.0.0/16 172.16.0.0/12 10.0.0.0/8' local6='fc00::/7 fe80::/10' # handle all arguments provided while getopts 'c:f:qt' flag; do case "$flag" in c) newRecord="$OPTARG" ;; f) filters="$OPTARG" ;; q) quiet=true ;; t) dryrun=true ;; *) echo "unknown option provided '$flag'" print_usage exit 1 ;; esac done shift "$((OPTIND-1))" content="$1" regex="$2" # load in defaults from config scriptRoot=$(dirname "$0") configFile="$scriptRoot/config.sh" if [[ -f "$configFile" ]]; then source "$configFile" # you can have a prefix in your config, and override it for a specific call if [[ -z "$prefix" ]]; then prefix="$CLOUDFLARE_FILTER_PREFIX" fi if [[ -n "$prefix" ]]; then filters="$prefix$filters" fi if [[ -z "$zone" ]]; then zone="$CLOUDFLARE_ZONE" fi if [[ -z "$token" ]]; then token="$CLOUDFLARE_TOKEN" fi fi # stop if we do not have the required information if [ -z "$zone" -o -z "$content" -o -z "$token" ]; then echo 'token, zone, and content are all required' print_usage exit 1 fi # --- update content --- # "" becomes current public ipv4 if [[ "$content" = *''* ]]; then ipv4=$("$scriptRoot/4-public-ip.sh") content=$(echo "$content" | sed "s//$ipv4/g") # check if address is actually public if [[ -n $(command -v "grepcidr") ]]; then if [[ -z $(echo "$ipv4" | grepcidr -v "$local4") ]]; then echo "IPv4 lookup failed, stopping" exit 2 fi fi fi # limit 1 # "" becomes first ipv4 of given domain if [[ "$content" = *']+(?=\>)') ipv4=$(dig +short A "$domain" | head -n 1) content=$(echo "$content" | sed "s//$ipv4/g") # check if address is actually public if [[ -n $(command -v "grepcidr") ]]; then if [[ -z $(echo "$ipv4" | grepcidr -v "$local4") ]]; then echo "IPv4 lookup for '$domain' failed, stopping" exit 2 fi fi fi # "" becomes current public ipv6 if [[ "$content" = *''* ]]; then ipv6=$("$scriptRoot/6-public-ip.sh") content=$(echo "$content" | sed "s//$ipv6/g") # check if address is actually public if [[ -n $(command -v "grepcidr") ]]; then if [[ -z $(echo "$ipv6" | grepcidr -v "$local6") ]]; then echo "IPv6 lookup failed, stopping" exit 2 fi fi fi # limit 1 # "" becomes first ipv6 of given domain if [[ "$content" = *']+(?=\>)') ipv6=$(dig +short AAAA "$domain" | head -n 1) content=$(echo "$content" | sed "s//$ipv6/g") # check if address is actually public if [[ -n $(command -v "grepcidr") ]]; then if [[ -z $(echo "$ipv6" | grepcidr -v "$local6") ]]; then echo "IPv6 lookup for '$domain' failed, stopping" exit 2 fi fi fi # --- update regex --- # "" becomes current public ipv4 if [[ "$regex" = *''* ]]; then ipv4=$("$scriptRoot/4-public-ip.sh") regex=$(echo "$regex" | sed "s//$ipv4/g") fi # limit 1 # "" becomes first ipv4 of given domain if [[ "$regex" = *']+(?=\>)') ipv4=$(dig +short A "$domain" | head -n 1) regex=$(echo "$regex" | sed "s//$ipv4/g") fi # "" becomes current public ipv6 if [[ "$regex" = *''* ]]; then ipv6=$("$scriptRoot/6-public-ip.sh") regex=$(echo "$regex" | sed "s//$ipv6/g") fi # limit 1 # "" becomes first ipv6 of given domain if [[ "$regex" = *']+(?=\>)') ipv6=$(dig +short AAAA "$domain" | head -n 1) regex=$(echo "$regex" | sed "s//$ipv6/g") fi # --- get matching dns records --- results=$(curl --silent --request GET \ --url "https://api.cloudflare.com/client/v4/zones/$zone/dns_records?$filters" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $token" ) if [[ -z "$regex" ]]; then newContent="$content" if [[ -z "$newContent" ]]; then if [[ "$quiet" = false ]]; then echo 'Content calculated to blank string, stopping.' fi exit 1 fi fi while read -r result; do if [[ -z "$result" ]]; then if [[ -n "$newRecord" ]]; then create=$(curl --silent --request POST \ --url "https://api.cloudflare.com/client/v4/zones/$zone/dns_records" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $token" \ --data "$newRecord" ) # display if successful, and any messages success=$(echo "$create" | jq -r '.success,.messages[]') domain=$(echo "$newRecord" | jq -r '.name') if [[ "$success" == "true" ]]; then echo "created $domain record" exit 0 else echo "failed to create $domain" exit 1 fi else echo 'No DNS Records found to update, stopping.' exit 1 fi fi # echo "$result" | jq -C '.' id=$(echo "$result" | jq -r '.id') oldContent=$(echo "$result" | jq -r '.content') type=$(echo "$result" | jq -r '.type') name=$(echo "$result" | jq -r '.name') if [[ -n "$regex" ]]; then newContent=$(echo "$oldContent" | sed -E "s/$regex/$content/g") if [[ -z "$newContent" ]]; then if [[ "$quiet" = false ]]; then echo 'Content calculated to blank string, skipping.' fi continue fi # echo "current '$oldContent'" # echo "find '$regex'" # echo "replace '$content'" # echo "new '$newContent'" fi if [[ "$newContent" = "$oldContent" ]]; then if [[ "$quiet" = false ]]; then echo "$type:$name is already set to '$newContent'" fi else echo "updating $type:$name" echo "now '$newContent'" echo "was '$oldContent'" if [[ "$dryrun" = true ]]; then echo 'true *actually just a dry-run*' elif [[ -z "$newContent" ]]; then echo 'false empty content has no effect, update skipped' else update=$(curl --silent --request PATCH \ --url "https://api.cloudflare.com/client/v4/zones/$zone/dns_records/$id" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $token" \ --data "{\"content\": \"$newContent\"}" ) # display if successful, and any messages echo "$update" | jq -r '.success,.messages[]' fi fi done <<< $(echo "$results" | jq -c '.result[]')