]> git.proxmox.com Git - mirror_acme.sh.git/blob - dnsapi/dns_edgedns.sh
Merge pull request #3734 from acmesh-official/dev
[mirror_acme.sh.git] / dnsapi / dns_edgedns.sh
1 #!/usr/bin/env sh
2
3 # Akamai Edge DNS v2 API
4 # User must provide Open Edgegrid API credentials to the EdgeDNS installation. The remote user in EdgeDNS must have CRUD access to
5 # Edge DNS Zones and Recordsets, e.g. DNS—Zone Record Management authorization
6
7 # Report bugs to https://control.akamai.com/apps/support-ui/#/contact-support
8
9 # Values to export:
10 # --EITHER--
11 # *** TBD. NOT IMPLEMENTED YET ***
12 # specify Edgegrid credentials file and section
13 # AKAMAI_EDGERC=<full file path>
14 # AKAMAI_EDGERC_SECTION="default"
15 ## --OR--
16 # specify indiviual credentials
17 # export AKAMAI_HOST = <host>
18 # export AKAMAI_ACCESS_TOKEN = <access token>
19 # export AKAMAI_CLIENT_TOKEN = <client token>
20 # export AKAMAI_CLIENT_SECRET = <client secret>
21
22 ACME_EDGEDNS_VERSION="0.1.0"
23
24 ######## Public functions #####################
25
26 # Usage: dns_edgedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
27 # Used to add txt record
28 #
29 dns_edgedns_add() {
30 fulldomain=$1
31 txtvalue=$2
32 _debug "ENTERING DNS_EDGEDNS_ADD"
33 _debug2 "fulldomain" "$fulldomain"
34 _debug2 "txtvalue" "$txtvalue"
35
36 if ! _EDGEDNS_credentials; then
37 _err "$@"
38 return 1
39 fi
40 if ! _EDGEDNS_getZoneInfo "$fulldomain"; then
41 _err "Invalid domain"
42 return 1
43 fi
44
45 _debug2 "Add: zone" "$zone"
46 acmeRecordURI=$(printf "%s/%s/names/%s/types/TXT" "$edge_endpoint" "$zone" "$fulldomain")
47 _debug3 "Add URL" "$acmeRecordURI"
48 # Get existing TXT record
49 _edge_result=$(_edgedns_rest GET "$acmeRecordURI")
50 _api_status="$?"
51 _debug3 "_edge_result" "$_edge_result"
52 if [ "$_api_status" -ne 0 ]; then
53 if [ "$curResult" = "FATAL" ]; then
54 _err "$(printf "Fatal error: acme API function call : %s" "$retVal")"
55 fi
56 if [ "$_edge_result" != "404" ]; then
57 _err "$(printf "Failure accessing Akamai Edge DNS API Server. Error: %s" "$_edge_result")"
58 return 1
59 fi
60 fi
61 rdata="\"${txtvalue}\""
62 record_op="POST"
63 if [ "$_api_status" -eq 0 ]; then
64 # record already exists. Get existing record data and update
65 record_op="PUT"
66 rdlist="${_edge_result#*\"rdata\":[}"
67 rdlist="${rdlist%%]*}"
68 rdlist=$(echo "$rdlist" | tr -d '"' | tr -d "\\\\")
69 _debug3 "existing TXT found"
70 _debug3 "record data" "$rdlist"
71 # value already there?
72 if _contains "$rdlist" "$txtvalue"; then
73 return 0
74 fi
75 _txt_val=""
76 while [ "$_txt_val" != "$rdlist" ] && [ "${rdlist}" ]; do
77 _txt_val="${rdlist%%,*}"
78 rdlist="${rdlist#*,}"
79 rdata="${rdata},\"${_txt_val}\""
80 done
81 fi
82 # Add the txtvalue TXT Record
83 body="{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":600, \"rdata\":"[${rdata}]"}"
84 _debug3 "Add body '${body}'"
85 _edge_result=$(_edgedns_rest "$record_op" "$acmeRecordURI" "$body")
86 _api_status="$?"
87 if [ "$_api_status" -eq 0 ]; then
88 _log "$(printf "Text value %s added to recordset %s" "$txtvalue" "$fulldomain")"
89 return 0
90 else
91 _err "$(printf "error adding TXT record for validation. Error: %s" "$_edge_result")"
92 return 1
93 fi
94 }
95
96 # Usage: dns_edgedns_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
97 # Used to delete txt record
98 #
99 dns_edgedns_rm() {
100 fulldomain=$1
101 txtvalue=$2
102 _debug "ENTERING DNS_EDGEDNS_RM"
103 _debug2 "fulldomain" "$fulldomain"
104 _debug2 "txtvalue" "$txtvalue"
105
106 if ! _EDGEDNS_credentials; then
107 _err "$@"
108 return 1
109 fi
110 if ! _EDGEDNS_getZoneInfo "$fulldomain"; then
111 _err "Invalid domain"
112 return 1
113 fi
114 _debug2 "RM: zone" "${zone}"
115 acmeRecordURI=$(printf "%s/%s/names/%s/types/TXT" "${edge_endpoint}" "$zone" "$fulldomain")
116 _debug3 "RM URL" "$acmeRecordURI"
117 # Get existing TXT record
118 _edge_result=$(_edgedns_rest GET "$acmeRecordURI")
119 _api_status="$?"
120 if [ "$_api_status" -ne 0 ]; then
121 if [ "$curResult" = "FATAL" ]; then
122 _err "$(printf "Fatal error: acme API function call : %s" "$retVal")"
123 fi
124 if [ "$_edge_result" != "404" ]; then
125 _err "$(printf "Failure accessing Akamai Edge DNS API Server. Error: %s" "$_edge_result")"
126 return 1
127 fi
128 fi
129 _debug3 "_edge_result" "$_edge_result"
130 record_op="DELETE"
131 body=""
132 if [ "$_api_status" -eq 0 ]; then
133 # record already exists. Get existing record data and update
134 rdlist="${_edge_result#*\"rdata\":[}"
135 rdlist="${rdlist%%]*}"
136 rdlist=$(echo "$rdlist" | tr -d '"' | tr -d "\\\\")
137 _debug3 "rdlist" "$rdlist"
138 if [ -n "$rdlist" ]; then
139 record_op="PUT"
140 comma=""
141 rdata=""
142 _txt_val=""
143 while [ "$_txt_val" != "$rdlist" ] && [ "$rdlist" ]; do
144 _txt_val="${rdlist%%,*}"
145 rdlist="${rdlist#*,}"
146 _debug3 "_txt_val" "$_txt_val"
147 _debug3 "txtvalue" "$txtvalue"
148 if ! _contains "$_txt_val" "$txtvalue"; then
149 rdata="${rdata}${comma}\"${_txt_val}\""
150 comma=","
151 fi
152 done
153 if [ -z "$rdata" ]; then
154 record_op="DELETE"
155 else
156 # Recreate the txtvalue TXT Record
157 body="{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":600, \"rdata\":"[${rdata}]"}"
158 _debug3 "body" "$body"
159 fi
160 fi
161 fi
162 _edge_result=$(_edgedns_rest "$record_op" "$acmeRecordURI" "$body")
163 _api_status="$?"
164 if [ "$_api_status" -eq 0 ]; then
165 _log "$(printf "Text value %s removed from recordset %s" "$txtvalue" "$fulldomain")"
166 return 0
167 else
168 _err "$(printf "error removing TXT record for validation. Error: %s" "$_edge_result")"
169 return 1
170 fi
171 }
172
173 #################### Private functions below ##################################
174
175 _EDGEDNS_credentials() {
176 _debug "GettingEdge DNS credentials"
177 _log "$(printf "ACME DNSAPI Edge DNS version %s" ${ACME_EDGEDNS_VERSION})"
178 args_missing=0
179 if [ -z "$AKAMAI_ACCESS_TOKEN" ]; then
180 AKAMAI_ACCESS_TOKEN=""
181 AKAMAI_CLIENT_TOKEN=""
182 AKAMAI_HOST=""
183 AKAMAI_CLIENT_SECRET=""
184 _err "AKAMAI_ACCESS_TOKEN is missing"
185 args_missing=1
186 fi
187 if [ -z "$AKAMAI_CLIENT_TOKEN" ]; then
188 AKAMAI_ACCESS_TOKEN=""
189 AKAMAI_CLIENT_TOKEN=""
190 AKAMAI_HOST=""
191 AKAMAI_CLIENT_SECRET=""
192 _err "AKAMAI_CLIENT_TOKEN is missing"
193 args_missing=1
194 fi
195 if [ -z "$AKAMAI_HOST" ]; then
196 AKAMAI_ACCESS_TOKEN=""
197 AKAMAI_CLIENT_TOKEN=""
198 AKAMAI_HOST=""
199 AKAMAI_CLIENT_SECRET=""
200 _err "AKAMAI_HOST is missing"
201 args_missing=1
202 fi
203 if [ -z "$AKAMAI_CLIENT_SECRET" ]; then
204 AKAMAI_ACCESS_TOKEN=""
205 AKAMAI_CLIENT_TOKEN=""
206 AKAMAI_HOST=""
207 AKAMAI_CLIENT_SECRET=""
208 _err "AKAMAI_CLIENT_SECRET is missing"
209 args_missing=1
210 fi
211
212 if [ "$args_missing" = 1 ]; then
213 _err "You have not properly specified the EdgeDNS Open Edgegrid API credentials. Please try again."
214 return 1
215 else
216 _saveaccountconf_mutable AKAMAI_ACCESS_TOKEN "$AKAMAI_ACCESS_TOKEN"
217 _saveaccountconf_mutable AKAMAI_CLIENT_TOKEN "$AKAMAI_CLIENT_TOKEN"
218 _saveaccountconf_mutable AKAMAI_HOST "$AKAMAI_HOST"
219 _saveaccountconf_mutable AKAMAI_CLIENT_SECRET "$AKAMAI_CLIENT_SECRET"
220 # Set whether curl should use secure or insecure mode
221 fi
222 export HTTPS_INSECURE=0 # All Edgegrid API calls are secure
223 edge_endpoint=$(printf "https://%s/config-dns/v2/zones" "$AKAMAI_HOST")
224 _debug3 "Edge API Endpoint:" "$edge_endpoint"
225
226 }
227
228 _EDGEDNS_getZoneInfo() {
229 _debug "Getting Zoneinfo"
230 zoneEnd=false
231 curZone=$1
232 while [ -n "$zoneEnd" ]; do
233 # we can strip the first part of the fulldomain, since its just the _acme-challenge string
234 curZone="${curZone#*.}"
235 # suffix . needed for zone -> domain.tld.
236 # create zone get url
237 get_zone_url=$(printf "%s/%s" "$edge_endpoint" "$curZone")
238 _debug3 "Zone Get: " "${get_zone_url}"
239 curResult=$(_edgedns_rest GET "$get_zone_url")
240 retVal=$?
241 if [ "$retVal" -ne 0 ]; then
242 if [ "$curResult" = "FATAL" ]; then
243 _err "$(printf "Fatal error: acme API function call : %s" "$retVal")"
244 fi
245 if [ "$curResult" != "404" ]; then
246 _err "$(printf "Managed zone validation failed. Error response: %s" "$retVal")"
247 return 1
248 fi
249 fi
250 if _contains "$curResult" "\"zone\":"; then
251 _debug2 "Zone data" "${curResult}"
252 zone=$(echo "${curResult}" | _egrep_o "\"zone\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")
253 _debug3 "Zone" "${zone}"
254 zoneEnd=""
255 return 0
256 fi
257
258 if [ "${curZone#*.}" != "$curZone" ]; then
259 _debug3 "$(printf "%s still contains a '.' - so we can check next higher level" "$curZone")"
260 else
261 zoneEnd=true
262 _err "Couldn't retrieve zone data."
263 return 1
264 fi
265 done
266 _err "Failed to retrieve zone data."
267 return 2
268 }
269
270 _edgedns_headers=""
271
272 _edgedns_rest() {
273 _debug "Handling API Request"
274 m=$1
275 # Assume endpoint is complete path, including query args if applicable
276 ep=$2
277 body_data=$3
278 _edgedns_content_type=""
279 _request_url_path="$ep"
280 _request_body="$body_data"
281 _request_method="$m"
282 _edgedns_headers=""
283 tab=""
284 _edgedns_headers="${_edgedns_headers}${tab}Host: ${AKAMAI_HOST}"
285 tab="\t"
286 # Set in acme.sh _post/_get
287 #_edgedns_headers="${_edgedns_headers}${tab}User-Agent:ACME DNSAPI Edge DNS version ${ACME_EDGEDNS_VERSION}"
288 _edgedns_headers="${_edgedns_headers}${tab}Accept: application/json,*/*"
289 if [ "$m" != "GET" ] && [ "$m" != "DELETE" ]; then
290 _edgedns_content_type="application/json"
291 _debug3 "_request_body" "$_request_body"
292 _body_len=$(echo "$_request_body" | tr -d "\n\r" | awk '{print length}')
293 _edgedns_headers="${_edgedns_headers}${tab}Content-Length: ${_body_len}"
294 fi
295 _edgedns_make_auth_header
296 _edgedns_headers="${_edgedns_headers}${tab}Authorization: ${_signed_auth_header}"
297 _secure_debug2 "Made Auth Header" "$_signed_auth_header"
298 hdr_indx=1
299 work_header="${_edgedns_headers}${tab}"
300 _debug3 "work_header" "$work_header"
301 while [ "$work_header" ]; do
302 entry="${work_header%%\\t*}"
303 work_header="${work_header#*\\t}"
304 export "$(printf "_H%s=%s" "$hdr_indx" "$entry")"
305 _debug2 "Request Header " "$entry"
306 hdr_indx=$((hdr_indx + 1))
307 done
308
309 # clear headers from previous request to avoid getting wrong http code on timeouts
310 : >"$HTTP_HEADER"
311 _debug2 "$ep"
312 if [ "$m" != "GET" ]; then
313 _debug3 "Method data" "$data"
314 # body url [needbase64] [POST|PUT|DELETE] [ContentType]
315 response=$(_post "$_request_body" "$ep" false "$m" "$_edgedns_content_type")
316 else
317 response=$(_get "$ep")
318 fi
319 _ret="$?"
320 if [ "$_ret" -ne 0 ]; then
321 _err "$(printf "acme.sh API function call failed. Error: %s" "$_ret")"
322 echo "FATAL"
323 return "$_ret"
324 fi
325 _debug2 "response" "${response}"
326 _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
327 _debug2 "http response code" "$_code"
328 if [ "$_code" = "200" ] || [ "$_code" = "201" ]; then
329 # All good
330 response="$(echo "${response}" | _normalizeJson)"
331 echo "$response"
332 return 0
333 fi
334
335 if [ "$_code" = "204" ]; then
336 # Success, no body
337 echo "$_code"
338 return 0
339 fi
340
341 if [ "$_code" = "400" ]; then
342 _err "Bad request presented"
343 _log "$(printf "Headers: %s" "$_edgedns_headers")"
344 _log "$(printf "Method: %s" "$_request_method")"
345 _log "$(printf "URL: %s" "$ep")"
346 _log "$(printf "Data: %s" "$data")"
347 fi
348
349 if [ "$_code" = "403" ]; then
350 _err "access denied make sure your Edgegrid cedentials are correct."
351 fi
352
353 echo "$_code"
354 return 1
355 }
356
357 _edgedns_eg_timestamp() {
358 _debug "Generating signature Timestamp"
359 _debug3 "Retriving ntp time"
360 _timeheaders="$(_get "https://www.ntp.org" "onlyheader")"
361 _debug3 "_timeheaders" "$_timeheaders"
362 _ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")"
363 _debug3 "_ntpdate" "$_ntpdate"
364 _ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')"
365 _debug3 "_NTPDATE" "$_ntpdate"
366 _ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")"
367 _debug3 "_ntptime" "$_ntptime"
368 _eg_timestamp=$(date -u "+%Y%m%dT")
369 _eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")"
370 _debug "_eg_timestamp" "$_eg_timestamp"
371 }
372
373 _edgedns_new_nonce() {
374 _debug "Generating Nonce"
375 _nonce=$(echo "EDGEDNS$(_time)" | _digest sha1 hex | cut -c 1-32)
376 _debug3 "_nonce" "$_nonce"
377 }
378
379 _edgedns_make_auth_header() {
380 _debug "Constructing Auth Header"
381 _edgedns_new_nonce
382 _edgedns_eg_timestamp
383 # "Unsigned authorization header: 'EG1-HMAC-SHA256 client_token=block;access_token=block;timestamp=20200806T14:16:33+0000;nonce=72cde72c-82d9-4721-9854-2ba057929d67;'"
384 _auth_header="$(printf "EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;" "$AKAMAI_CLIENT_TOKEN" "$AKAMAI_ACCESS_TOKEN" "$_eg_timestamp" "$_nonce")"
385 _secure_debug2 "Unsigned Auth Header: " "$_auth_header"
386
387 _edgedns_sign_request
388 _signed_auth_header="$(printf "%ssignature=%s" "$_auth_header" "$_signed_req")"
389 _secure_debug2 "Signed Auth Header: " "${_signed_auth_header}"
390 }
391
392 _edgedns_sign_request() {
393 _debug2 "Signing http request"
394 _edgedns_make_data_to_sign "$_auth_header"
395 _secure_debug2 "Returned signed data" "$_mdata"
396 _edgedns_make_signing_key "$_eg_timestamp"
397 _edgedns_base64_hmac_sha256 "$_mdata" "$_signing_key"
398 _signed_req="$_hmac_out"
399 _secure_debug2 "Signed Request" "$_signed_req"
400 }
401
402 _edgedns_make_signing_key() {
403 _debug2 "Creating sigining key"
404 ts=$1
405 _edgedns_base64_hmac_sha256 "$ts" "$AKAMAI_CLIENT_SECRET"
406 _signing_key="$_hmac_out"
407 _secure_debug2 "Signing Key" "$_signing_key"
408
409 }
410
411 _edgedns_make_data_to_sign() {
412 _debug2 "Processing data to sign"
413 hdr=$1
414 _secure_debug2 "hdr" "$hdr"
415 _edgedns_make_content_hash
416 path="$(echo "$_request_url_path" | tr -d "\n\r" | sed 's/https\?:\/\///')"
417 path="${path#*$AKAMAI_HOST}"
418 _debug "hier path" "$path"
419 # dont expose headers to sign so use MT string
420 _mdata="$(printf "%s\thttps\t%s\t%s\t%s\t%s\t%s" "$_request_method" "$AKAMAI_HOST" "$path" "" "$_hash" "$hdr")"
421 _secure_debug2 "Data to Sign" "$_mdata"
422 }
423
424 _edgedns_make_content_hash() {
425 _debug2 "Generating content hash"
426 _hash=""
427 _debug2 "Request method" "${_request_method}"
428 if [ "$_request_method" != "POST" ] || [ -z "$_request_body" ]; then
429 return 0
430 fi
431 _debug2 "Req body" "$_request_body"
432 _edgedns_base64_sha256 "$_request_body"
433 _hash="$_sha256_out"
434 _debug2 "Content hash" "$_hash"
435 }
436
437 _edgedns_base64_hmac_sha256() {
438 _debug2 "Generating hmac"
439 data=$1
440 key=$2
441 encoded_data="$(echo "$data" | iconv -t utf-8)"
442 encoded_key="$(echo "$key" | iconv -t utf-8)"
443 _secure_debug2 "encoded data" "$encoded_data"
444 _secure_debug2 "encoded key" "$encoded_key"
445
446 encoded_key_hex=$(printf "%s" "$encoded_key" | _hex_dump | tr -d ' ')
447 data_sig="$(echo "$encoded_data" | tr -d "\n\r" | _hmac sha256 "$encoded_key_hex" | _base64)"
448
449 _secure_debug2 "data_sig:" "$data_sig"
450 _hmac_out="$(echo "$data_sig" | tr -d "\n\r" | iconv -f utf-8)"
451 _secure_debug2 "hmac" "$_hmac_out"
452 }
453
454 _edgedns_base64_sha256() {
455 _debug2 "Creating sha256 digest"
456 trg=$1
457 _secure_debug2 "digest data" "$trg"
458 digest="$(echo "$trg" | tr -d "\n\r" | _digest "sha256")"
459 _sha256_out="$(echo "$digest" | tr -d "\n\r" | iconv -f utf-8)"
460 _secure_debug2 "digest decode" "$_sha256_out"
461 }
462
463 #_edgedns_parse_edgerc() {
464 # filepath=$1
465 # section=$2
466 #}