@ -2,7 +2,7 @@
# Copyright (C) 2017,2018 Timothe Litt litt at acm _dot org
VERSION="1.0.3 "
VERSION="2.0 "
PROG="`basename $0`"
# This script is used to update TXT records in GoDaddy DNS server
@ -42,8 +42,8 @@ while getopts 'dhj:k:s:t:qv' opt; do
*)
cat <<EOF
Usage
$PROG [-dt -h -j JSON -k:KEY -s:SECRET -q] add domain name data [ttl]
$PROG [-dt -h -j JSON -k:KEY -s:SECRET -q] del domain name data
$PROG [-dt -h -j JSON -k:KEY -s:SECRET -q] add name data [ttl]
$PROG [-dt -h -j JSON -k:KEY -s:SECRET -q] del name data
Add or delete TXT records from GoDaddy DNS
@ -59,8 +59,6 @@ Arguments:
del - remove the specified record from the domain
domain is the domain name, e.g. example.org
name is the DNS record name to add, e.g. _acme-challenge.example.org.
Note that trailing '.' is significant in DNS.
@ -73,9 +71,6 @@ Arguments:
For minimal trace output (to override -q), define GODADDY_TRACE="y".
Options
-b Domain name(s) in which challenge records are stored
E.g. often, www.example.net is stored in example.net.
Default from GODADDY_BASE
-d Provide debugging output - all requests and responses
-h This help.
-j: Location of JSON.sh Default `dirname $0`/JSON.sh, or
@ -88,7 +83,6 @@ Options
All output, except for this help text, is to stderr.
Environment variables
GODADDY_BASE Domain name(s) in which challenge records are stored
GODADDY_JSON location of the JSOH.sh script
GODADDY_KEY default API key
GODADDY_SCRIPT location of this script, default location of JSON.sh
@ -149,19 +143,13 @@ if ! [[ "$op" =~ ^(add|del)$ ]]; then
echo "Operation must be \"add\" or \"del\"" >&2
exit 3
fi
domain="$2"
domain="${domain%'.'}"
if [ -z "$domain" ]; then
echo "'domain' parameter is required, see -h" >&2
exit 3
fi
name="$3"
name="$2"
if [ -z "$name" ]; then
echo "'name' parameter is required, see -h" >&2
exit 3
fi
! [[ "$name" =~ [.]$ ]] && name="${name}.${domain}."
data="$4 "
data="$3"
if [ -z "$data" ]; then
echo "'data' parameter is required, see -h" >&2
exit 3
@ -207,28 +195,92 @@ fi
[ -n "$DEBUG" ] && echo "$authhdr" >&2
if [ "$op" = "add" ]; then
# May need to retry due to zone cuts
while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do
reqname="$name"
# The API doesn't trim the base domain from the name (it used to)
# If specified, remove any listed base.
if [ -n "$GODADDY_BASE" ]; then
for GDB in $GODADDY_BASE; do
gdb="`echo "$GDB" | sed -e's/\\.$//;s/\\./\\\\./g;'`"
gdb="^(.+)\\.$gdb\\.?$"
if [[ "$name" =~ $gdb ]]; then
reqname="${BASH_REMATCH[1]}"
break;
fi
done
else
eval 'reqname="$''{name%'"'.$domain.'}"'"'
#strip off the last period
if [[ "$name" =~ ^.+\. ]]; then
name=${name%?}
fi
reqdomain=
reqname=
# GoDaddy REST API URL is in the format /v1/domains/{domain}/records/{type}/{name}
# for adding/updating (PUT) or deleting (DELETE) a record. The API will support up
# to three segments in domain names (ex. mydomain.com and www.mydomain.com)
# in order to determine which domain the API call will affect (both mydomain.com and
# www.mydomain.com will result in the modification of the mydomain.com domain. Any
# more than three segments (ex. sub.something.mydomain.com will result in
# the API throwing a MISMATCH_FORMAT error.
#
# Examples
# 1. If mydomain.com was provided to this script as the domain parameter, and
# _acme-challengemydomain.com was provided as the name, then the URL
# /v1/domains/mydomain.com/records/TXT/_acme-challenge will be used which
#
# 2. If www.mydomain.com was provided to this script as the domain parameter,
# and _acme-challenge.www.mydomain.com was provided as the name, then the
# URL /v1/domains/mydomain.com/records/TXT/_acme-challenge.www will be used.
# Determine the domain and the name to use for the API the URL
# The name parameter given to us is in the format challenge.domain.
# (ex _acme-challenge.mydomain.com. - note the trailing period). We will just
# use the name given us to determine the domain
while [[ "$name" =~ ^([^.]+)\.([^.]+.*) ]]; do
if [ -n "${reqname}" ]; then reqname="${reqname}."; fi
reqname="${reqname}${BASH_REMATCH[1]}"
testdomain="${BASH_REMATCH[2]}"
name=$testdomain
if [[ ! "$name" =~ [^.]+\.[^.]+ ]]; then
exit 1
fi
url="$API/$testdomain"
[ -n "$DEBUG" ] && echo "Looking for domain ${testdomain}"
response="$(curl -i -s -X GET --config - "${url}" <<EOF
header = "Content-Type: application/json"
header = "$authhdr"
EOF
)"
sts=$?
[ -n "$DEBUG" ] && cat >&2 <<EOF
Response:
curl status = $sts
--------
$response
--------
EOF
if [ $sts -ne 0 ]; then
echo "curl error $sts querying domain" >&2
exit $sts
fi
url="$API/$domain/records/TXT/$reqname"
if echo "$response" | grep -q '^HTTP/.* 200 '; then
[ -n "$DEBUG" ] && echo "Found domain ${testdomain}"
reqdomain=${testdomain}
break
fi
code="`echo "$response" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`"
if [ "$code" = 'NOT_FOUND' ]; then
continue
fi
done
if [ -z "$reqdomain" ] || [ -z "$reqname" ]; then
echo "Unable to determine domain or RR name" >&2
exit 3
fi
if [ "$op" = "add" ]; then
url="$API/$reqdomain/records/TXT/$reqname"
request='[{"data":"'$data'","ttl":'$ttl'}]'
[ -n "$DEBUG" ] && cat >&2 <<EOF
@ -252,54 +304,38 @@ curl status = $sts
$result
--------
EOF
if [ $sts -ne 0 ]; then
echo "curl error $sts adding record" >&2
exit $sts
fi
if ! echo "$result" | grep -q '^HTTP/.* 200 '; then
code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`"
msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`"
if [ "$code" = "DUPLICATE_RECORD" ]; then
if [ -n "$VERB" ]; then
echo "$msg in $domain" >&2
fi
exit 0 # Duplicate record is still success
fi
if [ "$code" = 'UNKNOWN_DOMAIN' ]; then
if [[ "$domain" =~ ^([^.]+)\.([^.]+\.[^.]+.*) ]]; then
[ -n "$DEBUG" ] && \
echo "$domain unknown, trying ${BASH_REMATCH[2]}" >&2
domain="${BASH_REMATCH[2]}"
continue;
fi
if [ $sts -ne 0 ]; then
echo "curl error $sts adding record" >&2
exit $sts
fi
if ! echo "$result" | grep -q '^HTTP/.* 200 '; then
code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`"
msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`"
if [ "$code" = "DUPLICATE_RECORD" ]; then
if [ -n "$VERB" ]; then
echo "$msg in $domain" >&2
fi
echo "Request failed $msg" >&2
exit 1
exit 0 # Duplicate record is still success
fi
[ -n "$VERB" ] && echo "$domain: added $name $ttl TXT \"$data\"" >&2
exit 0
done
echo "Request failed $msg" >&2
exit 1
fi
[ -n "$VERB" ] && echo "$reqdomain: added $reqname $ttl TXT \"$data\"" >&2
exit 0
fi
# ----- Delete
# There is no delete API
# But, it is possible to replace all TXT records.
#
# So, first query for all TXT records
# May need to retry due to zone cuts
while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do
url="$API/$domain/records/TXT"
[ -n "$DEBUG" ] && echo "Query for TXT records to: $url" >&2
if [ "$op" = "del" ]; then
url="$API/$reqdomain/records/TXT/$reqname"
[ -n "$DEBUG" ] && echo "Deleting challenge TXT records at: $url" >&2
current="$(curl -i -s -X GET --config - "$url" <<EOF
header = "$authhdr"
current="$(curl -i -s -X DELETE --config - "$url" <<EOF
header = "$authhdr"
EOF
)"
sts=$?
if [ $sts -ne 0 ]; then
echo "curl error $sts for query" >&2
@ -312,132 +348,14 @@ Response
$current
--------
EOF
if ! echo "$current" | grep -q '^HTTP/.* 200 '; then
code="`echo "$current" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`"
msg="`echo "$current" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`"
if [ "$code" = "UNKNOWN_DOMAIN" ]; then
if [[ "$domain" =~ ^([^.]+)\.([^.]+\.[^.]+.*) ]]; then
[ -n "$DEBUG" ] && echo \
"$domain unknown, trying ${BASH_REMATCH[2]}" >&2
domain="${BASH_REMATCH[2]}"
continue;
fi
fi
echo "Request failed $msg" >&2
exit 1
fi
# Remove headers
current="$(echo "$current" | sed -e'0,/^\r*$/d')"
break
done
# The zone cut is known, so the replace can't fail due to UNKNOWN domain
if [ "$current" = '[]' ]; then # No TXT records in zone
[ -n "$VERB" ] && echo "$domain: $name TXT \"$data\" does not exist" >&2
[ -n "$DEBUG" ] && echo "No TXT records in $domain" >&2
exit 1 # Intent was to change, so error status
fi
[ -n "$DEBUG" ] && echo "Response is valid"
# Prepare request to replace TXT RRSET
# Parse JSON and select only the record structures, which are [index] { ...}
current="$(echo "$current" | $JSON | sed -n -e'/^\[[0-9][0-9]*\]/{ s/^\[[0-9][0-9]*\]//; p}')"
base="$current"
[ -n "$DEBUG" ] && cat >&2 <<EOF
Old TXT RRSET:
$current
EOF
# Remove the desired record. The name must be relative. Order varies.
eval 'name="$''{name%'"'.$domain.'}"'"'
match="$(printf '"name":"%s","data":"%s","ttl":' "$name" "$data")"
cmd="$(printf 'echo %s%s%s | grep -v %s%s%s' "'" "$current" "'" "'" "$match" "'")"
eval 'new="$('"$cmd"')"'
match="$(printf '"data":"%s","name":"%s","ttl":' "$data" "$name")"
cmd="$(printf 'echo %s%s%s | grep -v %s%s%s' "'" "$current" "'" "'" "$match" "'")"
eval 'new="$('"$cmd"')"'
if [ "$new" = "$base" ]; then
[ -n "$VERB" ] && echo "$domain: $name TXT \"$data\" does not exist" >&2
exit 1 # Intent was to change DNS, so this is an error
fi
# Remove whitespace and insert needed commmas
#
fmtnew="$new"
new=$(echo "$new" | sed -e"s/}/},/g; \$s/},/}/;" | tr -d '\t\n')
if [ -z "$new" ]; then
[ -n "$VERB" ] && echo "Replacing last TXT record with a dummy (see -h)" >&2
new='{"type":"TXT","name":"_dummy.record_","data":"_This record is not used_","ttl":601}'
dummy="t"
TAB=$'\t'
fmtnew="${TAB}$new"
if [ "$fmtnew" = "$base" ]; then
[ -n "$VERB" ] && echo "This tool can't delete a placeholder when it is the only TXT record" >&2
exit 0 # Not really success, but retrying won't help.
fi
[ -n "$VERB" ] && echo "$reqdomain: deleted $reqname TXT \"$data\"" >&2
exit 0
fi
request="[$new]"
[ -n "$DEBUG" ] && cat >&2 <<EOF
New TXT RRSET will be
$fmtnew
EOF
url="$API/$domain/records/TXT"
[ -n "$DEBUG" ] && cat >&2 <<EOF
Replace (delete) request to: $url
--------
$request
--------
EOF
result="$(curl -i -s -X PUT -d "$request" --config - "$url" <<EOF
header = "Content-Type: application/json"
header = "$authhdr"
EOF
)"
sts=$?
[ -n "$DEBUG" ] && cat >&2 <<EOF
Result:
curl status = $sts
--------
$result
--------
EOF
if [ $sts -ne 0 ]; then
echo "curl error $sts deleting record" >&2
exit $sts
fi
if ! echo "$result" | grep -q '^HTTP/.* 200 '; then
result="$(echo "$result" | sed -e'0,/^\r*$/d')"
code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`"
msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`"
echo "Request failed $msg" >&2
exit 1
fi
if [ -n "$VERB" ]; then
if [ -n "$dummy" ]; then
echo "$domain: replaced $name TXT \"$data\" with a placeholder" >&2
else
echo "$domain: deleted $name TXT \"$data\"" >&2
fi
fi
exit 0