-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathgad
executable file
·493 lines (459 loc) · 18.3 KB
/
gad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
#!/bin/sh
usage() {
printf "\\nUsage: %s [-h] [-x] [-6] [-f] [-t] [-e] [-v] [-s] [-i EXT_IF] [-p KEYFILE|-a APIKEY] [-l TTL] -d EXAMPLE.COM -r \"RECORD-NAMES\"
-h: Print this usage info and exit
-x: Use Gandi's legacy XML-RPC API
-6: Update AAAA record(s) instead of A record(s)
-f: Force the creation of a new zonefile regardless of IP address or TTL discrepancy
-t: On Gandi's legacy DNS platform, if a new version of the zonefile is created, don't activate it; on LiveDNS, just print the updates that would be made if this flag wasn't used.
-e: Print debugging information to stdout
-v: Print information to stdout even if an update isn't needed
-s: Use stdin instead of OpenDNS to determine external IP address
-i EXT_IF: The name of your external network interface (optional, if provided uses ifconfig instead of OpenDNS to determine external IP address
-p KEYFILE: Path to the file that contains your PAT or API key (defaults to ~/.gandiapi)
-a APIKEY: Your PAT (for LiveDNS) or API key (for the legacy API) provided by Gandi (optional, loaded from a file if not specified)
-l TTL: Set a custom TTL on records (optional, and only supported on LiveDNS)
-d EXAMPLE.COM: The domain name whose active zonefile will be updated (required)
-r RECORD-NAMES: A space-separated list of the name(s) of the A or AAAA record(s) to update or create (required)
gad version: ${gad_version}\\n\\n" "$0"
exit 1
}
#
# Set script version
#
gad_version="2.3.0"
#
# Process parameters
#
while [ $# -gt 0 ]; do
case "$1" in
-h) help="yes";;
-x) legacy="yes";;
-5) legacy="no";;
-6) ipv6="yes";;
-l) ttl="$2"; shift;;
-f) force="yes";;
-t) testing="yes";;
-e) debug="yes";;
-v) verbose="yes";;
-s) stdin_ip="yes";;
-i) ext_if="$2"; shift;;
-p) keyfile="$2"; shift;;
-a) apikey="$2"; shift;;
-d) domain="$2"; shift;;
-r) records="$2"; shift;;
*) usage; break
esac
shift
done
if [ -z "$domain" -o -z "$records" -o "$help" = "yes" ]; then
usage
fi
if [ ! -z "$apikey" -a ! -z "$keyfile" ]; then
printf "The -p and -a flags are incompatible. Only specify a PAT or API key using one method.\\n"
exit 1
fi
if [ -z "$apikey" ]; then
if [ -z "$keyfile" ]; then
keyfile="${HOME}/.gandiapi"
fi
if [ -f "$keyfile" ]; then
apikey=$(cat "$keyfile")
else
printf "Could not load PAT or API key from -a flag or %s.\\n" "$keyfile"
exit 1
fi
fi
if [ ! -z "$ttl" -a "$legacy" = "yes" ]; then
printf "Setting a custom TTL on records is not supported on Gandi's legacy DNS platform.\\n"
exit 1
fi
if [ "$ipv6" = "yes" ]; then
record_type="AAAA"
ip_regex="\([0-9A-Fa-f:]*\)"
inet="inet6"
else
record_type="A"
ip_regex="\([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\)"
inet="inet"
fi
if [ "$debug" = "yes" ]; then
printf "Initial variables:\\n---\\napikey = %s\\ndomain = %s\\nrecords = %s\\nttl (only relevant with LiveDNS) = %s\\nrecord_type = %s\\nip_regex = %s\\n---\\n\\n" "$apikey" "$domain" "$records" "$ttl" "$record_type" "$ip_regex"
fi
#
# Set API address
#
if [ "$legacy" != "yes" ]; then
gandi="api.gandi.net:443"
else
gandi="rpc.gandi.net:443"
fi
#
# Function to call Gandi's v5/LiveDNS REST API
#
# $1 is the HTTP verb. Only GET, PUT, and POST are used in this script.
# $2 is the API endpoint
# $3 is the body of the request. If the verb is GET and a third parameter is
# provided, it is ignored.
#
rest() {
if [ "$debug" = "yes" ]; then
printf "REST call to endpoint:\\n---\\n%s\\n---\\n\\n" "$2" 1>&2
fi
# Throw away third argument to function if verb is GET
if [ "$1" != "GET" ]; then
tmp_json="$3"
fi
tmp_request="${1} /v5/livedns/${2} HTTP/1.1
User-Agent: Gandi Automatic DNS shell script/${gad_version}
Host: $(printf "%s" "$gandi" | cut -d ':' -f 1 -)
Connection: close
Content-Type: application/json
Content-Length: $(printf "%s" "$tmp_json" | wc -c | tr -d "[:space:]")
Authorization: Bearer ${apikey}
"
if [ "$1" != "GET" ]; then
tmp_message="${tmp_request}${tmp_json}"
else
tmp_message="$tmp_request"
fi
if [ "$debug" = "yes" ]; then
printf "Sending REST message tmp_message:\\n---\\n%s\\n---\\n\\n" "$tmp_message" 1>&2
fi
printf "%s" "$tmp_message" | openssl s_client -quiet -connect "$gandi" 2> /dev/null | tail -1
unset tmp_json
unset tmp_request
unset tmp_message
}
#
# Function to get a specified field from JSON
#
# This probably won't work with arbitrary JSON for input, but it works for the
# JSON that is returned by the Gandi API endpoints that are used by this
# script. The function returns nothing if the field isn't found.
#
# $1 is the field that you want to get the value of
# $2 is the JSON content
#
get_json_field() {
# Set $updated_json to the provided JSON content
updated_json="$2"
# This helps handle JSON lists by editing list values to be separated by
# spaces instead of commas, so we can split JSON fields on commas with awk
# later, and loop over them to find the value we want
while true; do
# Find the first instance of [ and replace content up to the first comma
# with the content followed by a space
updated_json=$(printf "%s" "$updated_json" | sed 's/\(\[[^,]*"\),/\1 /g')
# Check for the pattern again, and restart the loop if grep returns 0
# indicating the pattern was found. Otherwise break out of this loop.
if printf "%s" "$updated_json" | grep -e '\[[^,]*",' > /dev/null; then
continue
else
break
fi
done
# Set internal field separator to a new line, so our for loop loops over the
# records returned on separate lines by the awk command below
IFS='
'
# Trim out curly braces, anything like a newline, split records on "," or
# ", " with awk, replace any leading whitespace with sed, and loop over
# records.
for i in $(printf "%s" "$updated_json" | tr -d '{}\t\n\r\f' | awk 'BEGIN{RS=", ?"}{print $0}' | sed 's/^ //'); do
# Find the current field name and value, trimming out any double quotes
# and brackets.
field_name=$(printf "%s" "$i" | cut -d: -f1 | tr -d '"' | tr -d '[]')
field_value=$(printf "%s" "$i" | cut -d: -f2- | tr -d '"' | tr -d '[]')
# If the current field name matches the one we're looking for, print the
# value and break out of this loop
if [ "$field_name" = "$1" ]; then
printf "%s" "$field_value"
break
fi
done
unset IFS
}
#
# Function to check for errors
#
# $1 is the JSON returned by the API
#
check_http_error() {
json="$1"
code=$(get_json_field "code" "$json")
if [ ! -z "$code" ]; then
case "$code" in
401) printf "Error: Received 401 Unauthorized from the Gandi API.\\n\\nFull response: %s\\n" "$json"; exit 1;;
403) printf "Error: Received 403 Forbidden from the Gandi API.\\n\\nFull response: %s\\n" "$json"; exit 1;;
esac
fi
}
#
# Function to call Gandi's legacy XML-RPC API
#
# $1 is the API method
# $2 and all subsequent arguments are datatype/value pairs (for the first pair,
# $2 would be the datatype and $3 would be the value) or structs ($2 would
# be "struct", $3 would be the name of the struct, $4 would be the datatype,
# and $5 would be the value. Structs must come after any datatype/value
# pairs.
#
rpc() {
if [ "$debug" = "yes" ]; then
printf "RPC call to methodName:\\n---\\n%s\\n---\\n\\n" "$1" 1>&2
fi
tmp_xml="<?xml version=\"1.0\"?>
<methodCall>
<methodName>${1}</methodName>
<params>
<param>
<value><string>${apikey}</string></value>
</param>"
shift
while [ ! -z "$1" ]; do
if [ "$1" != "struct" ]; then
tmp_xml="${tmp_xml}
<param>
<value><${1}>${2}</${1}></value>
</param>"
shift; shift
else
tmp_xml="${tmp_xml}
<param>
<value>
<struct>"
shift;
while [ ! -z "$1" ]; do
if [ "$1" != "struct" ]; then
tmp_xml="${tmp_xml}
<member>
<name>${1}</name>
<value><${2}>${3}</${2}></value>
</member>"
shift; shift; shift;
else
break
fi
done
tmp_xml="${tmp_xml}
</struct>
</value>
</param>"
fi
done
tmp_xml="${tmp_xml}
</params>
</methodCall>"
tmp_post="POST /xmlrpc/ HTTP/1.1
User-Agent: Gandi Automatic DNS shell script/${gad_version}
Host: $(printf "%s" "$gandi" | cut -d ':' -f 1 -)
Content-Type: text/xml
Content-Length: $(printf "%s" "$tmp_xml" | wc -c | tr -d "[:space:]")
"
tmp_message="${tmp_post}${tmp_xml}"
if [ "$debug" = "yes" ]; then
printf "Sending XML-RPC message tmp_message:\\n---\\n%s\\n---\\n\\n" "$tmp_message" 1>&2
fi
printf "%s" "$tmp_message" | openssl s_client -quiet -connect "$gandi" 2> /dev/null
unset tmp_xml
unset tmp_post
unset tmp_message
}
#
# Function to update existing DNS records with a new value
#
# $1 is a space-separated list of record names to update
#
update() {
while [ ! -z "$1" ]; do
if [ "$legacy" != "yes" ]; then
new_record_json=$(rest "PUT" "domains/${domain}/records/${1}/${record_type}" "{\"rrset_ttl\": ${new_ttl}, \"rrset_values\": [\"${ext_ip}\"]}")
check_http_error "$new_record_json"
new_record_message=$(get_json_field "message" "$new_record_json")
if [ "$debug" = "yes" ]; then
printf "new_record_json:\\n---\\n%s\\n---\\n\\n" "$new_record_json"
printf "new_record_message:\\n---\\n%s\\n---\\n\\n" "$new_record_message"
fi
else
new_record_id=$(rpc "domain.zone.record.list" "int" "$zone_id" "int" "$new_version_id" "struct" "name" "string" "$1" "type" "string" "$record_type" | grep -A 1 ">id<" | sed -n 's/.*<string>\([0-9]*\).*/\1/p')
if [ "$debug" = "yes" ]; then
printf "new_record_id:\\n---\\n%s\\n---\\n\\n" "$new_record_id"
fi
rpc "domain.zone.record.update" "int" "$zone_id" "int" "$new_version_id" "struct" "id" "int" "$new_record_id" "struct" "name" "string" "$1" "type" "string" "$record_type" "value" "string" "$ext_ip"
fi
shift
done
}
#
# Function to create new DNS records
#
# $1 is a space-separated list of record names to create
#
create() {
while [ ! -z "$1" ]; do
if [ "$legacy" != "yes" ]; then
new_record_json=$(rest "POST" "domains/${domain}/records/${1}/${record_type}" "{\"rrset_ttl\": ${new_ttl}, \"rrset_values\": [\"${ext_ip}\"]}")
check_http_error "$new_record_json"
new_record_message=$(get_json_field "message" "$new_record_json")
if [ "$debug" = "yes" ]; then
printf "new_record_json:\\n---\\n%s\\n---\\n\\n" "$new_record_json"
printf "new_record_message:\\n---\\n%s\\n---\\n\\n" "$new_record_message"
fi
else
rpc "domain.zone.record.add" "int" "$zone_id" "int" "$new_version_id" "struct" "name" "string" "$1" "type" "string" "$record_type" "value" "string" "$ext_ip"
fi
shift
done
}
#
# Function to check existing DNS information and see if it matches the external
# IP address (and TTL in the case of LiveDNS)
#
# $1 is a space-separated list of record names to check
#
check() {
while [ ! -z "$1" ]; do
if [ "$legacy" != "yes" ]; then
record_json=$(rest "GET" "domains/${domain}/records/${1}/${record_type}")
check_http_error "$record_json"
if [ "$debug" = "yes" ]; then
printf "record_json:\\n---\\n%s\\n---\\n\\n" "$record_json"
fi
record_value=$(get_json_field "rrset_values" "$record_json")
if [ "$debug" = "yes" ]; then
printf "record_value:\\n---\\n%s\\n---\\n\\n" "$record_value"
fi
record_ttl=$(get_json_field "rrset_ttl" "$record_json")
record_count=$(printf "%s" "$record_value" | wc -w)
# If a custom TTL wasn't provided, just set it to the existing one.
# If the record TTL is empty (because the record doesn't exist) and
# no custom TTL was provided, set a default.
if [ -z "$record_ttl" -a -z "$ttl" ]; then
new_ttl="10800"
elif [ -z "$ttl" ]; then
new_ttl="$record_ttl"
else
new_ttl="$ttl"
fi
else
record_value=$(rpc "domain.zone.record.list" "int" "$zone_id" "int" "0" "struct" "name" "string" "$1" "type" "string" "$record_type" | grep -A 1 ">value<" | sed -n "s/.*<string>${ip_regex}.*/\1/p")
record_count=$(printf "%s" "$record_value" | wc -w)
fi
if [ "$record_count" -gt "1" ]; then
printf "Sorry, but gad does not support updating multiple records with the same name.\\n"
exit 1
elif [ -z "$record_value" ]; then
if [ -z "$records_to_create" ]; then
records_to_create="$1"
else
records_to_create="${records_to_create} ${1}"
fi
elif [ "$ext_ip" != "$record_value" -o "$new_ttl" != "$record_ttl" -o "$force" = "yes" ]; then
if [ -z "$records_to_update" ]; then
records_to_update="$1"
else
records_to_update="${records_to_update} ${1}"
fi
fi
if [ "$debug" = "yes" ]; then
printf "Results after checking record:\\n---\\nrecord: %s\\nrecord_value: %s\\nrecords_to_create: %s\\nrecords_to_update: %s\\n---\\n\\n" "$1" "$record_value" "$records_to_create" "$records_to_update"
fi
shift
done
}
#
# Get correct IP address
#
if [ "$stdin_ip" = "yes" ]; then
ext_ip_method="standard input"
read ext_ip
elif [ ! -z "$ext_if" ]; then
ext_ip_method="ifconfig ${ext_if}"
ext_ip=$(ifconfig "$ext_if" | sed -n "s/.*${inet} \(addr:\)* *${ip_regex}.*/\2/p" | head -1)
else
ext_ip_method="OpenDNS"
if [ "$record_type" = "A" ]; then
ext_ip=$(dig -4 "$record_type" +short @resolver1.opendns.com myip.opendns.com)
else
ext_ip=$(dig "$record_type" +short @resolver1.opendns.com myip.opendns.com)
fi
fi
if [ -z "$ext_ip" ]; then
printf "Failed to determine external IP address with %s. See above error.\\n" "$ext_ip_method"
exit 1
fi
if [ "$debug" = "yes" ]; then
printf "IP information:\\n---\\next_ip_method: %s\\next_ip: %s\\n---\\n\\n" "$ext_ip_method" "$ext_ip"
fi
#
# Get the active zonefile for the domain (not required for v5)
#
if [ "$legacy" != "yes" ]; then
if [ "$debug" = "yes" ]; then
printf "Using v5/LiveDNS. No zone ID required.\\n\\n"
fi
else
if [ "$debug" = "yes" ]; then
printf "Using legacy API. Looking up active zone ID.\\n"
fi
zone_id=$(rpc "domain.info" "string" "$domain" | grep -A 1 zone_id | sed -n 's/.*<int>\([0-9]*\).*/\1/p')
if [ -z "$zone_id" ]; then
printf "No zone_id returned. This is expected with Gandi's test API or if you send a LiveDNS API key or PAT to Gandi's legacy API. Use gad's -t flag for testing or the -5 flag for LiveDNS.\\n"
exit 1
fi
if [ "$debug" = "yes" ]; then
printf "zone_id:\\n---\\n%s\\n---\\n\\n" "$zone_id"
fi
fi
#
# Check values of records in the active version of the zonefile
#
set -f
check $records
set +f
#
# If there are any mismatches, create a new version of the zonefile, update the incorrect records, and activate it
#
if [ ! -z "$records_to_update" -o ! -z "$records_to_create" ]; then
if [ "$legacy" != "yes" ]; then
new_snapshot_json=$(rest "POST" "domains/${domain}/snapshots" "")
check_http_error "$new_snapshot_json"
new_snapshot_id=$(get_json_field "id" "$new_snapshot_json")
if [ "$debug" = "yes" ]; then
printf "new_snapshot_json:\\n---\\n%s\\n---\\n\\n" "$new_snapshot_json"
printf "new_snapshot_id:\\n---\\n%s\\n---\\n\\n" "$new_snapshot_id"
fi
if [ "$testing" != "yes" ]; then
set -f
update $records_to_update
create $records_to_create
set +f
printf "Created a new snapshot and tried to update the following live %s records to %s with TTL of %s seconds: %s %s\\n" "$record_type" "$ext_ip" "$new_ttl" "$records_to_update" "$records_to_create"
else
printf "Testing mode! Not sending any updates to the LiveDNS API.\\nIn non-testing mode, gad would have tried to update the following live %s records to %s with TTL of %s seconds: %s %s\\n" "$record_type" "$ext_ip" "$new_ttl" "$records_to_update" "$records_to_create"
fi
else
new_version_id=$(rpc "domain.zone.version.new" "int" "$zone_id" | sed -n 's/.*<int>\([0-9]*\).*/\1/p')
if [ "$debug" = "yes" ]; then
printf "new_version_id:\\n---\\n%s\\n---\\n\\n" "$new_version_id"
fi
set -f
update $records_to_update
create $records_to_create
set +f
if [ "$testing" != "yes" ]; then
printf "Activating version %s of the zonefile for domain %s...\\n\\nopenssl s_client output and domain.zone.version.set() method response:\\n\\n" "$new_version_id" "$domain"
rpc "domain.zone.version.set" "int" "$zone_id" "int" "$new_version_id"
printf "\\nTried to update the following %s records to %s: %s %s\\n\\nThere is no error checking on the RPCs so check the web interface if you want to be sure the update was successful, or look at the methodResponse from domain.zone.version.set() above (a response of \"1\" means success).\\n" "$record_type" "$ext_ip" "$records_to_update" "$records_to_create"
else
printf "Created version %s of the zonefile for domain %s.\\n\\nTried to update the following %s records to %s: %s %s\\n\\nThere is no error checking on the RPCs so check the web interface if you want to be sure the update was successful.\\n" "$new_version_id" "$domain" "$record_type" "$ext_ip" "$records_to_update" "$records_to_create"
fi
exit
fi
else
if [ "$verbose" = "yes" ]; then
printf "External IP address %s detected with %s and TTL value of %s matches records: %s. No update needed. Exiting.\\n" "$ext_ip" "$ext_ip_method" "$new_ttl" "$records"
fi
exit
fi