|
|
|
@ -0,0 +1,418 @@ |
|
|
|
#!/bin/bash |
|
|
|
|
|
|
|
# Copyright (2017) Timothe Litt litt at acm _dot org |
|
|
|
|
|
|
|
VERSION="1.0.1" |
|
|
|
PROG="`basename $0`" |
|
|
|
|
|
|
|
# This script is used to update TXT records in GoDaddy DNS server |
|
|
|
# It depends on JSON.sh from https://github.com/dominictarr/JSON.sh |
|
|
|
# Place it in (or softlink it to) the same directory as this script, |
|
|
|
# or specify its location with GODADDY_JSON |
|
|
|
# |
|
|
|
# See the usage text below, 00GoDaddy-README.txt, dns_add_godaddy |
|
|
|
# and dns_del_godaddy for additional information. |
|
|
|
# |
|
|
|
# It may be freely used providing this notice is included with |
|
|
|
# all copies. The name of the author may not be used to endorse |
|
|
|
# any other product or derivative work. No warranty is provided |
|
|
|
# and the user assumes all responsibility for use of this software. |
|
|
|
# |
|
|
|
# Bug reports are welcome at https://github.com/tlhackque/getssl/issues. |
|
|
|
|
|
|
|
API='https://api.godaddy.com/v1/domains' |
|
|
|
APISIGNUP='https://developer.godaddy.com/getstarted' |
|
|
|
GETJSON='https://github.com/dominictarr/JSON.sh' |
|
|
|
|
|
|
|
VERB="y" |
|
|
|
DEBUG="$GODADDY_DEBUG" |
|
|
|
[ -z "$JSON" ] && JSON="$GODADDY_JSON" |
|
|
|
[ -z "$JSON" ] && JSON="`dirname $0`/JSON.sh" |
|
|
|
|
|
|
|
while getopts 'dhj:k:s:t:qv' opt; do |
|
|
|
case $opt in |
|
|
|
d) DEBUG="Y" ;; |
|
|
|
j) JSON="$OPTARG" ;; |
|
|
|
k) GODADDY_KEY="$OPTARG" ;; |
|
|
|
s) GODADDY_SECRET="$OPTARG" ;; |
|
|
|
t) TRACE="$OPTARG" ;; |
|
|
|
q) VERB= ;; |
|
|
|
v) echo "dns_godaddy version $VERSION"; exit 0 ;; |
|
|
|
*) |
|
|
|
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 |
|
|
|
|
|
|
|
Add or delete TXT records from GoDaddy DNS |
|
|
|
|
|
|
|
Obtain the Key and Secret from $APISIGNUP |
|
|
|
You must obtain a "Production" key - NOT the "Test" key you're required |
|
|
|
to get first. |
|
|
|
|
|
|
|
With getssl, this script is called from the dns_add_godaddy and |
|
|
|
dns_del_godaddy wrapper scripts. |
|
|
|
|
|
|
|
Arguments: |
|
|
|
add - add the specified record to the domain |
|
|
|
|
|
|
|
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. |
|
|
|
|
|
|
|
data is the record data, e.g. "myverificationtoken" |
|
|
|
|
|
|
|
ttl is optional, and defaults to the GoDaddy minimum of 600. |
|
|
|
|
|
|
|
If it is necessary to turn on debugging externally, define |
|
|
|
GODADDY_DEBUG="y" (any non-null string will do). |
|
|
|
For minimal trace output (to override -q), define GODADDY_TRACE="y". |
|
|
|
|
|
|
|
Options |
|
|
|
-d Provide debugging output - all requests and responses |
|
|
|
-h This help. |
|
|
|
-j: Location of JSON.sh Default `dirname $0`/JSON.sh, or |
|
|
|
the GODADDY_JSON variable. |
|
|
|
-k: The GoDaddy API key Default from GODADDY_KEY |
|
|
|
-s: The GoDaddy API secret Default from GODADDY_SECRET |
|
|
|
-t: Detailed protocol trace data is appended to specified file |
|
|
|
-q Quiet - omit normal success messages, |
|
|
|
|
|
|
|
All output, except for this help text, is to stderr. |
|
|
|
|
|
|
|
Environment variables |
|
|
|
GODADDY_JSON location of the JSOH.sh script |
|
|
|
GODADDY_KEY default API key |
|
|
|
GODADDY_SCRIPT location of this script, default location of JSON.sh |
|
|
|
GODADDY_SECRET default API secret |
|
|
|
GODADDY_TRACE forces -q off if true |
|
|
|
GODADDY_TFILE appends protocol trace to file. Overrides -t |
|
|
|
|
|
|
|
BUGS |
|
|
|
Due to a limitation of the gOdADDY API, deleting the last TXT record |
|
|
|
would be too risky for my taste. So in that case, I replace it with |
|
|
|
_dummy.record_.domain. TXT "Ihis record is not used". This record is |
|
|
|
not automatically deleted by this script, though it's perfectly OK to |
|
|
|
do so manually. (Via another mechanism, or if it's no longer the last.) |
|
|
|
|
|
|
|
This really shouldn't happen often, since most domains have at least |
|
|
|
one TXT record - for SPF, tracking APIs, etc. |
|
|
|
|
|
|
|
Report any issues to https://github.com/tlhackque/getssl/issues |
|
|
|
EOF |
|
|
|
exit 0 |
|
|
|
;; |
|
|
|
esac |
|
|
|
done |
|
|
|
shift $((OPTIND-1)) |
|
|
|
|
|
|
|
# Check for JSON -- required for delete, but if records are added, |
|
|
|
# we assume they'll be deleted later & don't want to strand them. |
|
|
|
|
|
|
|
[[ "$JSON" =~ ^~ ]] && \ |
|
|
|
eval 'JSON=`readlink -nf ' $JSON '`' |
|
|
|
if [ ! -x "$JSON" ]; then |
|
|
|
cat <<EOF >&2 |
|
|
|
$0: requires JSON.sh as "$JSON" |
|
|
|
|
|
|
|
The full path to JSON.sh can be specified with -j, or the |
|
|
|
GODADDY_JSON environment variable. |
|
|
|
|
|
|
|
You can obtain a copy from $GETJSON |
|
|
|
|
|
|
|
Then place or softlink it to $JSON or set GODADDY_JSON. |
|
|
|
EOF |
|
|
|
exit 2 |
|
|
|
fi |
|
|
|
|
|
|
|
if [ -z "$GODADDY_KEY" ] || [ -z "$GODADDY_SECRET" ]; then |
|
|
|
echo "GODADDY_KEY and GODADDY secret must be defined" >&2 |
|
|
|
exit 3 |
|
|
|
fi |
|
|
|
|
|
|
|
[ -n "$DEBUG" ] && VERB="y" |
|
|
|
[ -n "$GODADDY_TRACE" ] && VERB="Y" |
|
|
|
[ -n "$GODADDY_TFILE" ] && TRACE="$GODADDY_TFILE" |
|
|
|
|
|
|
|
# Get parameters & validate |
|
|
|
|
|
|
|
op="$1" |
|
|
|
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" |
|
|
|
if [ -z "$name" ]; then |
|
|
|
echo "'name' parameter is required, see -h" >&2 |
|
|
|
exit 3 |
|
|
|
fi |
|
|
|
! [[ "$name" =~ [.]$ ]] && name="${name}.${domain}." |
|
|
|
data="$4" |
|
|
|
if [ -z "$data" ]; then |
|
|
|
echo "'data' parameter is required, see -h" >&2 |
|
|
|
exit 3 |
|
|
|
fi |
|
|
|
|
|
|
|
if [ "$op" = 'del' ]; then |
|
|
|
ttl= |
|
|
|
elif [ -z "$5" ]; then |
|
|
|
ttl="600" # GoDaddy minimum TTL is 600 |
|
|
|
elif ! [[ "$5" =~ ^[0-9]+$ ]]; then |
|
|
|
echo "TTL $5 is not numeric" >&2 |
|
|
|
exit 3 |
|
|
|
elif [ $5 -lt 600 ]; then |
|
|
|
[ -n "$VERB" ] && \ |
|
|
|
echo "$5 is less than GoDaddy minimum of 600; increased to 600" >&2 |
|
|
|
ttl="600" |
|
|
|
else |
|
|
|
ttl="$5" |
|
|
|
fi |
|
|
|
|
|
|
|
# --- Done with parameters |
|
|
|
|
|
|
|
[ -n "$DEBUG" ] && \ |
|
|
|
echo "$PROG: $op $domain $name \"$data\" $ttl" >&2 |
|
|
|
|
|
|
|
# Authorization header has secret and key |
|
|
|
|
|
|
|
authhdr="Authorization: sso-key $GODADDY_KEY:$GODADDY_SECRET" |
|
|
|
|
|
|
|
if [ -n "$TRACE" ]; then |
|
|
|
function timestamp { local tm="`LC_TIME=C date '+%T.%N'`" |
|
|
|
local class="$1"; shift |
|
|
|
echo "${tm:0:15} ** ${class}: $*" >>"$TRACE" |
|
|
|
} |
|
|
|
timestamp 'Info' "$PROG" "V$VERSION" 'Starting new protocol trace' |
|
|
|
timestamp 'Args' "$@" |
|
|
|
curl --help | grep -q -- --trace-time && CURL_TFLAGS="--trace-time" # 7.14.0 |
|
|
|
function curl { |
|
|
|
command curl ${CURL_TFLAGS} --trace-ascii % "$@" 2>>"$TRACE" |
|
|
|
} |
|
|
|
[ -n "$VERB" ] && echo "Appending protocol trace to $TRACE" |
|
|
|
fi |
|
|
|
|
|
|
|
[ -n "$DEBUG" ] && echo "$authhdr" >&2 |
|
|
|
|
|
|
|
if [ "$op" = "add" ]; then |
|
|
|
# May need to retry due to zone cuts |
|
|
|
|
|
|
|
while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do |
|
|
|
|
|
|
|
url="$API/$domain/records/TXT/$name" |
|
|
|
|
|
|
|
request='{"data":"'$data'","ttl":'$ttl'}' |
|
|
|
[ -n "$DEBUG" ] && cat >&2 <<EOF |
|
|
|
Add 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 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 |
|
|
|
fi |
|
|
|
echo "Request failed $msg" >&2 |
|
|
|
exit 1 |
|
|
|
fi |
|
|
|
[ -n "$VERB" ] && echo "$domain: added $name $ttl TXT \"$data\"" >&2 |
|
|
|
exit 0 |
|
|
|
done |
|
|
|
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 |
|
|
|
|
|
|
|
current="$(curl -i -s -X GET --config - "$url" <<EOF |
|
|
|
header = "$authhdr" |
|
|
|
EOF |
|
|
|
)" |
|
|
|
sts=$? |
|
|
|
if [ $sts -ne 0 ]; then |
|
|
|
echo "curl error $sts for query" >&2 |
|
|
|
exit $sts |
|
|
|
fi |
|
|
|
[ -n "$DEBUG" ] && cat >&2 <<EOF |
|
|
|
|
|
|
|
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. |
|
|
|
|
|
|
|
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"')"' |
|
|
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|