#!/usr/bin/env bash quiet=0 # should i shut up?!?!? :< token="" # the api token domain="" # the domain to update record="A" # the record type ttl="60" # the new time to live content="" # the new record content usage() { printf "usage: cfdns [-hq] [-d DOMAIN] [-t TOKEN] [-r RECORD] [-l TTL] [-cC CONTENT]\n\n" printf "\t-h\t\tshow the help message\n" printf "\t-q\t\tquiet output\n" printf "\t-d DOMAIN\tthe domain name to update\n" printf "\t-t TOKEN\tthe api token\n" printf "\t-r RECORD\tthe new record type\n" printf "\t-l TTL\t\tthe new time to live\n" printf "\t-c CONTENT\tthe new contents of the record\n" printf "\t-C RAW CONTENT\tthe new conents of the record (as raw json)\n" } chk_command() { if ! command -v "$1" > /dev/null; then >&2 echo "error: command '$1' could not be found" exit 1 fi } chk_command "getopts" chk_command "curl" chk_command "jq" while getopts "hqd:t:r:l:c:C:" arg > /dev/null; do case $arg in h) usage exit 0 ;; q) quiet=1 ;; d) domain="${OPTARG}" ;; t) token="${OPTARG}" ;; r) record="${OPTARG}" ;; l) ttl="${OPTARG}" ;; c) content="\"content\": \"${OPTARG}\"" ;; C) content="${OPTARG}" ;; ?) exit 1 ;; esac done if [ ! "$cert" = "cert.pem" ] && [ "$key" = "cert.key" ]; then name=${cert%.*} key="$name.key" fi log() { if [ $quiet = 0 ]; then printf "$*\n" 1>&2 fi } step() { if [ $quiet = 0 ]; then printf "\033[32m>>> \033[0m$*\n" 1>&2 fi } error() { printf "\033[31merror: \033[0m$*\n" > /dev/stderr } die() { kill $$ } handle_error() { errs="$1" count="$(echo $errs | jq -r ". | length")" for i in $(seq 0 $(($count - 1))); do err="$(echo $errs | jq -r ".[$i]")" error "$(echo $err | jq -r '.message')" chain="$(echo $err | jq -r '.error_chain')" if ! [ "$chain" = "null" ]; then count="$(echo $chain | jq -r ". | length")" for j in $(seq 0 $(($count - 1))); do error "$(echo $chain | jq -r ".[$j].message")" done fi done } _curl() { RESPONSE=$(curl --silent --request $1 \ --url "https://api.cloudflare.com/client/v4$2" \ --header "Content-Type: application/json" \ --header "Authorization: Bearer $token" \ --data "$3") SUCCESS=$(echo $RESPONSE | jq -r ".success") if [ "$SUCCESS" != "true" ]; then errs="$(echo $RESPONSE | jq -r .errors)" handle_error "$errs" die fi echo $RESPONSE | jq ".result" } patch() { _curl "PATCH" "$1" "$2" } post() { _curl "POST" "$1" "$2" } get() { _curl "GET" "$1" "" } # Step 1: get the zone the domain is in zone=$(echo "$domain" | awk -F'.' '{print $(NF-1)"."$(NF);}' 2>/dev/null) if [ "$zone" = "" ]; then error "invalid domain: '$domain'" die fi step "fetching zones..." zones=$(get "/zones") zone_id="null" i=0 while true; do json="$(echo "$zones" | jq ".[$i]")" if [ "$json" == "null" ]; then break fi name="$(echo "$json" | jq -r ".name")" id="$(echo "$json" | jq -r ".id")" log "found zone: $name ($id)" if [ "$name" == "$zone" ]; then zone_id="$id" break fi ((i=i+1)) done if [ "$zone_id" == "null" ]; then error "could not find zone: $zone" die fi # Step 2: get the current record id if it exists step "fetching records..." records="$(get "/zones/$zone_id/dns_records")" current="null" i=0 while true; do _record="$(echo "$records" | jq ".[$i]")" if [ "$_record" == "null" ]; then break fi name="$(echo "$_record" | jq -r ".name")" type="$(echo "$_record" | jq -r ".type")" _content="$(echo "$_record" | jq -r ".name")" id="$(echo "$_record" | jq -r ".id")" log "found record: $name $type \"$_content\" ($id)" if [ "$name" == "$domain" ] && [ "$record" = "$type" ]; then current="$id" break fi ((i=i+1)) done # Step 3: update record step "updating record" update() { patch "/zones/$zone_id/dns_records/$1" "$2" } create() { post "/zones/$zone_id/dns_records" "$1" } json="{ \"name\": \"$domain\", \"proxied\": false, \"type\": \"$record\", $content }" log $(echo "$json" | jq -C) if [ "$current" == "null" ]; then log "no record found... creating new one" create "$json" else log "record found... updating" update "$current" "$json" fi step "successfully updated record"