-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpbad
executable file
·343 lines (314 loc) · 11.7 KB
/
pbad
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
#!/bin/sh
usage() {
printf "\\nporkbun-automatic-dns: Dynamic DNS shell script for Porkbun
Usage: %s [-h] [-6] [-f] [-t] [-e] [-v] [-s] [-i EXTIF] [-p KEYFILE|-a APIKEY,SECRETKEY] [-l TTL] -d EXAMPLE.COM -r \"RECORD-NAMES\"
-h: Print this usage info and exit
-6: Update AAAA record(s) instead of A record(s)
-f: Overwrite an existing DNS record regardless of IP address or TTL discrepancy
-t: Just print the updates that would be made without actually creating or updating any DNS records
-e: Print debugging information to stdout
-v: Print information to stdout even if an update isn't needed
-s: Use stdin instead of the Porkbun API to determine external IP address
-i EXTIF: The name of your external network interface (optional, if provided uses ifconfig instead of the Porkbun API to determine external IP address)
-p KEYFILE: Path to the file that contains your comma-separated Porkbun API key and secret key (defaults to ~/.porkbunapi)
-a APIKEY,SECRETKEY: Your Porkbun API key and secret key, separated by a comma (optional, loaded from a file if not specified)
-l TTL: Set a custom TTL on records (optional, defaults to 10800)
-d EXAMPLE.COM: The domain to create or update DNS records for (required)
-r \"RECORD-NAMES\": A space-separated list of the name(s) of the A or AAAA record(s) to update or create (required)
pbad version: ${pbad_version}\\n\\n" "$0"
exit 1
}
#
# Set script version
#
pbad_version="1.0.1"
#
# Process parameters
#
while [ $# -gt 0 ]; do
case "$1" in
-h) help="yes";;
-6) ipv6="yes";;
-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) apikeyarg="$2"; shift;;
-l) ttl="$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 "$apikeyarg" -a ! -z "$keyfile" ]; then
printf "The -p and -a flags are incompatible. Only specify an API key and secret key using one of these methods.\\n"
exit 1
fi
if [ -z "$apikeyarg" ]; then
if [ -z "$keyfile" ]; then
keyfile="${HOME}/.porkbunapi"
fi
if [ -f "$keyfile" ]; then
keyfile_contents=$(cat "$keyfile")
apikey=$(printf "%s" "$keyfile_contents" | cut -d , -f 1 | tr -d ' ')
secretkey=$(printf "%s" "$keyfile_contents" | cut -d , -f 2 | tr -d ' ')
else
printf "Could not load API key and secret key from -a flag or %s.\\n" "$keyfile"
exit 1
fi
else
apikey=$(printf "%s" "$apikeyarg" | cut -d , -f 1 | tr -d ' ')
secretkey=$(printf "%s" "$apikeyarg" | cut -d , -f 2 | tr -d ' ')
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\\nsecretkey = %s\\ndomain = %s\\nrecords = %s\\nttl = %s\\nrecord_type = %s\\nip_regex = %s\\n---\\n\\n" "$apikey" "$secretkey" "$domain" "$records" "$ttl" "$record_type" "$ip_regex"
fi
#
# Set API address
#
if [ "$record_type" = "A" ]; then
# Force IPv4 API usage if updating A records
porkbun="https://api-ipv4.porkbun.com/api/json/v3"
else
porkbun="https://api.porkbun.com/api/json/v3"
fi
#
# Function to call Porkbun's v3 API
#
# $1 is the HTTP verb. Only POST is used by the Porkbun API.
# $2 is the API endpoint.
# $3 is the body of the request.
#
rest() {
if [ "$1" != "POST" ]; then
printf "rest() called with non-POST method. This is not supported.\\n"
exit 1
fi
rest_endpoint="${porkbun}/${2}"
if [ "$debug" = "yes" ]; then
printf "REST call:\\n---\\nEndpoint: %s\\nData: %s\\n---\\n\\n" "$2" "$3" 1>&2
fi
# Account for poorly documented and/or low rate limits
sleep 1
rest_result=$(curl \
--silent \
--show-error \
--request "$1" \
-H "User-Agent: Porkbun Automatic DNS/pbad shell script/${pbad_version}" \
-H "Content-Type: application/json" \
--http2 \
--data "${3}" \
"$rest_endpoint")
printf "%s" "$rest_result"
unset rest_endpoint
unset rest_result
}
#
# 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 Porkbun API endpoints that are used by this
# script. The function returns nothing if the field isn't found. Parent fields
# of a field are ignored. If any duplicate child fields are found, the function
# fails (this usually indicates multiple DNS records were returned, which is not
# supported.
#
# $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"
# Trim our anything like a newline and split records on ',' or a colon
# followed by anything other than '"', then trim out all braces.
fields="$(printf "%s" "$updated_json" | tr -d '\t\n\r\f ' | awk 'BEGIN{RS=",|:[^\"]"}{print $0}' | tr -d '{}[]')"
# Check for duplicate field names (this is generally not supported, even
# when the fields are from separate dicts).
field_names=$(for i in $fields; do printf "%s" "$i" | cut -d : -f 1 | tr -d '"'; done)
field_names_count=$(printf "%s" "$field_names" | grep -c '')
field_names_unique_count=$(printf "%s" "$field_names" | sort -u | grep -c '')
if [ "$field_names_count" != "$field_names_unique_count" ]; then
printf "Found duplicate field names in JSON input. This may occur if there are multiple DNS records for a single value. This configuration is not supported by pbad.\\n"
exit 1
else
for i in $fields; do
field_name=$(printf "%s" "$i" | cut -d : -f 1 | tr -d '"')
field_value=$(printf "%s" "$i" | cut -d : -f 2- | 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
fi
}
#
# Function to check for errors
#
# $1 is the JSON returned by the API
#
check_api_error() {
json="$1"
status=$(get_json_field "status" "$json")
message=$(get_json_field "message" "$json")
if [ "$status" != "SUCCESS" ]; then
printf "%s status returned by API: %s\\n" "$status" "$message"
exit 1
fi
}
#
# 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 [ "$1" = "@" ]; then
subdomain=''
else
subdomain="$1"
fi
new_record_json=$(rest "POST" "dns/editByNameType/${domain}/${record_type}/${subdomain}" "{\"secretapikey\": \"${secretkey}\", \"apikey\": \"${apikey}\", \"content\": \"${ext_ip}\", \"ttl\": \"${new_ttl}\"}")
if [ "$debug" = "yes" ]; then
printf "new_record_json:\\n---\\n%s\\n---\\n\\n" "$new_record_json"
fi
check_api_error "$new_record_json"
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 [ "$1" = "@" ]; then
subdomain=''
else
subdomain="$1"
fi
new_record_json=$(rest "POST" "dns/create/${domain}" "{\"secretapikey\": \"${secretkey}\", \"apikey\": \"${apikey}\", \"name\": \"${subdomain}\", \"type\": \"${record_type}\", \"content\": \"${ext_ip}\", \"ttl\": \"${new_ttl}\"}")
if [ "$debug" = "yes" ]; then
printf "new_record_json:\\n---\\n%s\\n---\\n\\n" "$new_record_json"
fi
check_api_error "$new_record_json"
shift
done
}
#
# Function to check existing DNS information and see if it matches the external
# IP address and desired TTL
#
# $1 is a space-separated list of record names to check
#
check() {
while [ ! -z "$1" ]; do
if [ "$1" = "@" ]; then
subdomain=''
else
subdomain="$1"
fi
record_json=$(rest "POST" "dns/retrieveByNameType/${domain}/${record_type}/${subdomain}" "{\"secretapikey\": \"${secretkey}\", \"apikey\": \"${apikey}\"}")
if [ "$debug" = "yes" ]; then
printf "record_json:\\n---\\n%s\\n---\\n\\n" "$record_json"
fi
check_api_error "$record_json"
record_value=$(get_json_field "content" "$record_json")
if [ "$debug" = "yes" ]; then
printf "record_value:\\n---\\n%s\\n---\\n\\n" "$record_value"
fi
record_ttl=$(get_json_field "ttl" "$record_json")
# 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
if [ -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="Porkbun"
ping_json=$(rest "POST" "ping" "{\"secretapikey\": \"${secretkey}\", \"apikey\": \"${apikey}\"}")
ext_ip=$(get_json_field "yourIp" "$ping_json")
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
#
# Check values of records
#
set -f
check $records
set +f
#
# If there are any mismatches, update the incorrect records
#
if [ ! -z "$records_to_update" -o ! -z "$records_to_create" ]; then
if [ "$testing" != "yes" ]; then
set -f
update $records_to_update
create $records_to_create
set +f
printf "Set the following %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 Porkbun API.\\nIn non-testing mode, we would have tried to set the following %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
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