Files

254 lines
8.1 KiB
Bash
Executable File

#!/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: <a> <a:example.com> <aaaa> and <aaaa:example.com> which will be replaced with equivalent ip address, each domain tag can be used once'
echo "Usage: [token=<token-config-override>] [zone=<zone-config-override>] [prefix=<filter-prefix-override>] $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 ---
# "<a>" becomes current public ipv4
if [[ "$content" = *'<a>'* ]]; then
ipv4=$("$scriptRoot/4-public-ip.sh")
content=$(echo "$content" | sed "s/<a>/$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
# "<a:example.com>" becomes first ipv4 of given domain
if [[ "$content" = *'<a:'* ]]; then
domain=$(echo "$content" | grep -oP '(?<=\<a:)[^>]+(?=\>)')
ipv4=$(dig +short A "$domain" | head -n 1)
content=$(echo "$content" | sed "s/<a:$domain>/$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
# "<aaaa>" becomes current public ipv6
if [[ "$content" = *'<aaaa>'* ]]; then
ipv6=$("$scriptRoot/6-public-ip.sh")
content=$(echo "$content" | sed "s/<aaaa>/$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
# "<aaaa:example.com>" becomes first ipv6 of given domain
if [[ "$content" = *'<aaaa:'* ]]; then
domain=$(echo "$content" | grep -oP '(?<=\<aaaa:)[^>]+(?=\>)')
ipv6=$(dig +short AAAA "$domain" | head -n 1)
content=$(echo "$content" | sed "s/<aaaa:$domain>/$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 ---
# "<a>" becomes current public ipv4
if [[ "$regex" = *'<a>'* ]]; then
ipv4=$("$scriptRoot/4-public-ip.sh")
regex=$(echo "$regex" | sed "s/<a>/$ipv4/g")
fi
# limit 1
# "<a:example.com>" becomes first ipv4 of given domain
if [[ "$regex" = *'<a:'* ]]; then
domain=$(echo "$regex" | grep -oP '(?<=\<a:)[^>]+(?=\>)')
ipv4=$(dig +short A "$domain" | head -n 1)
regex=$(echo "$regex" | sed "s/<a:$domain>/$ipv4/g")
fi
# "<aaaa>" becomes current public ipv6
if [[ "$regex" = *'<aaaa>'* ]]; then
ipv6=$("$scriptRoot/6-public-ip.sh")
regex=$(echo "$regex" | sed "s/<aaaa>/$ipv6/g")
fi
# limit 1
# "<aaaa:example.com>" becomes first ipv6 of given domain
if [[ "$regex" = *'<aaaa:'* ]]; then
domain=$(echo "$regex" | grep -oP '(?<=\<aaaa:)[^>]+(?=\>)')
ipv6=$(dig +short AAAA "$domain" | head -n 1)
regex=$(echo "$regex" | sed "s/<aaaa:$domain>/$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[]')