You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

379 lines
12 KiB

4 years ago
  1. #!/usr/bin/env sh
  2. # Akamai Edge DNS v2 API
  3. # User must provide Open Edgegrid API credentials to the EdgeDNS installation. The remote user in EdgeDNS must have CRUD access to
  4. # Edge DNS Zones and Recordsets, e.g. DNS—Zone Record Management authorization
  5. # Report bugs to https://control.akamai.com/apps/support-ui/#/contact-support
  6. # Values to export:
  7. # --EITHER--
  8. # *** NOT IMPLEMENTED YET ***
  9. # specify Edgegrid credentials file and section
  10. # AKAMAI_EDGERC=<full file path>
  11. # AKAMAI_EDGERC_SECTION="default"
  12. ## --OR--
  13. # specify indiviual credentials
  14. # export AKAMAI_HOST = <host>
  15. # export AKAMAI_ACCESS_TOKEN = <access token>
  16. # export AKAMAI_CLIENT_TOKEN = <client token>
  17. # export AKAMAI_CLIENT_SECRET = <client secret>
  18. ACME_EDGEDNS_VERSION="0.1.0"
  19. ######## Public functions #####################
  20. # Usage: dns_edgedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
  21. # Used to add txt record
  22. #
  23. dns_edgedns_add() {
  24. fulldomain=$1
  25. txtvalue=$2
  26. _debug "ENTERING DNS_EDGEDNS_ADD"
  27. _debug2 "fulldomain" "$fulldomain"
  28. _debug2 "txtvalue" "$txtvalue"
  29. if ! _EDGEDNS_credentials; then
  30. _err "$@"
  31. return 1
  32. fi
  33. if ! _EDGEDNS_getZoneInfo "$fulldomain"; then
  34. _err "Invalid domain"
  35. return 1
  36. fi
  37. _debug2 "Add: zone" "${zone}"
  38. acmeRecordURI=$(printf "%s/%s/names/%s/type/TXT" "${edge_endpoint}" "${zone}" "${fulldomain}")
  39. _debug3 "Add URL" "$acmeRecordURI"
  40. # Get existing TXT record
  41. _edge_result=$(_edgedns_rest GET "$acmeRecordURI")
  42. _api_status="$?"
  43. if [ "$_api_status" -ne 0 ] && [ "$_edge_result" != "404" ]; then
  44. _err "$(printf "Failure accessing Akamai Edge DNS API Server. Error: %s" "$_edge_result")"
  45. return 1
  46. fi
  47. rdata="\"$txtvalue\""
  48. record_op="POST"
  49. if [ "$_api_status" -eq 0 ]; then
  50. # record already exists. Get existing record data and update
  51. record_op="PUT"
  52. rdlist=$(echo -n "$response" | _egrep_o "\"rdata\"\\s*:\\s*\\[\\s*\"[^\"]*\"\\s*]" | cut -d : -f 2 | tr -d "[]\"")
  53. _debug2 "existing TXT found"
  54. _debug2 "record data" "$rdlist"
  55. # value already there?
  56. if _contains "$rdlist" "$txtvalue" ; then
  57. return 0
  58. fi
  59. comma=","
  60. rdata="$rdata$comma\"${txtvalue}\""
  61. fi
  62. _debug2 "new/updated rdata: " "${rdata}"
  63. # Add the txtvalue TXT Record
  64. body="{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":600, \"rdata\":"[${rdata}]"}"
  65. _debug3 "Add body '${body}'"
  66. _edge_result=$(_edgedns_rest "$record_op" "$acmeRecordURI" "$body")
  67. _api_status="$?"
  68. if [ "$_api_status" -eq 0 ]; then
  69. _log "$(printf "Text value %s added to recordset %s" "${txtvalue}" "${fulldomain}")"
  70. return 0
  71. else
  72. _err "$(printf "error adding TXT record for validation. Error: %s" "$_edge_result")"
  73. return 1
  74. fi
  75. }
  76. # Usage: dns_edgedns_rm _acme-challenge.www.domain.com
  77. # Used to delete txt record
  78. #
  79. dns_edgedns_rm() {
  80. fulldomain=$1
  81. }
  82. #################### Private functions below ##################################
  83. _EDGEDNS_credentials() {
  84. _debug "GettingEdge DNS credentials"
  85. _log $(printf "ACME DNSAPI Edge DNS version %s" ${ACME_EDGEDNS_VERSION})
  86. args_missing=0
  87. if [ -z "${AKAMAI_ACCESS_TOKEN}" ]; then
  88. AKAMAI_ACCESS_TOKEN=""
  89. AKAMAI_CLIENT_TOKEN=""
  90. AKAMAI_HOST=""
  91. AKAMAI_CLIENT_SECRET=""
  92. _err "AKAMAI_ACCESS_TOKEN is missing"
  93. args_missing=1
  94. fi
  95. if [ -z "$AKAMAI_CLIENT_TOKEN" ]; then
  96. AKAMAI_ACCESS_TOKEN=""
  97. AKAMAI_CLIENT_TOKEN=""
  98. AKAMAI_HOST=""
  99. AKAMAI_CLIENT_SECRET=""
  100. _err "AKAMAI_CLIENT_TOKEN is missing"
  101. args_missing=1
  102. fi
  103. if [ -z "${AKAMAI_HOST}" ]; then
  104. AKAMAI_ACCESS_TOKEN=""
  105. AKAMAI_CLIENT_TOKEN=""
  106. AKAMAI_HOST=""
  107. AKAMAI_CLIENT_SECRET=""
  108. _err "AKAMAI_HOST is missing"
  109. args_missing=1
  110. fi
  111. if [ -z "${AKAMAI_CLIENT_SECRET}" ]; then
  112. AKAMAI_ACCESS_TOKEN=""
  113. AKAMAI_CLIENT_TOKEN=""
  114. AKAMAI_HOST=""
  115. AKAMAI_CLIENT_SECRET=""
  116. _err "AKAMAI_CLIENT_SECRET is missing"
  117. args_missing=1
  118. fi
  119. if [ "${args_missing}" = 1 ]; then
  120. _err "You have not properly specified the EdgeDNS Open Edgegrid API credentials. Please try again."
  121. return 1
  122. else
  123. _saveaccountconf_mutable AKAMAI_ACCESS_TOKEN "${AKAMAI_ACCESS_TOKEN}"
  124. _saveaccountconf_mutable AKAMAI_CLIENT_TOKEN "${AKAMAI_CLIENT_TOKEN}"
  125. _saveaccountconf_mutable AKAMAI_HOST "${AKAMAI_HOST}"
  126. _saveaccountconf_mutable AKAMAI_CLIENT_SECRET "${AKAMAI_CLIENT_SECRET}"
  127. # Set whether curl should use secure or insecure mode
  128. fi
  129. export HTTPS_INSECURE=0 # All Edgegrid API calls are secure
  130. edge_endpoint=$(printf "https://%s/config-dns/v2/zones" "${AKAMAI_HOST}")
  131. _debug3 "Edge API Endpoint:" "${edge_endpoint}"
  132. }
  133. _EDGEDNS_getZoneInfo() {
  134. _debug "Getting Zoneinfo"
  135. zoneEnd=false
  136. curZone=$1
  137. while [ -n "${zoneEnd}" ]; do
  138. # we can strip the first part of the fulldomain, since its just the _acme-challenge string
  139. curZone="${curZone#*.}"
  140. # suffix . needed for zone -> domain.tld.
  141. # create zone get url
  142. get_zone_url=$(printf "%s/%s" "${edge_endpoint}" "${curZone}")
  143. _debug3 "Zone Get: " "${get_zone_url}"
  144. curResult=$(_edgedns_rest GET "$get_zone_url")
  145. retVal=$?
  146. if [ $retVal -ne 0 ]; then
  147. if ["$curResult" != "404" ]; then
  148. _err "$(printf "Managed zone validation failed. Error response: %s" "$retVal")"
  149. return 1
  150. fi
  151. fi
  152. if _contains "${curResult}" "\"zone\":" ; then
  153. _debug2 "Zone data" "${curResult}"
  154. zone=$(echo -n "${curResult}" | _egrep_o "\"zone\"\\s*:\\s*\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d "\"")
  155. _debug2 "Zone" "${zone}"
  156. zoneFound=""
  157. zoneEnd=""
  158. return 0
  159. fi
  160. if [ "${curZone#*.}" != "$curZone" ]; then
  161. _debug2 $(printf "%s still contains a '.' - so we can check next higher level" "$curZone")
  162. else
  163. zoneEnd=true
  164. _err "Couldn't retrieve zone data."
  165. return 1
  166. fi
  167. done
  168. _err "Failed to retrieve zone data."
  169. return 2
  170. }
  171. _edgedns_headers=""
  172. _edgedns_rest() {
  173. _debug "Handling API Request"
  174. m=$1
  175. # Assume endpoint is complete path, including query args if applicable
  176. ep=$2
  177. body_data=$3
  178. _edgedns_content_type=""
  179. _request_url_path="$ep"
  180. _request_body="$body_data"
  181. _request_method="$m"
  182. _edgedns_headers=""
  183. tab=""
  184. _edgedns_headers="${_edgedns_headers}${tab}Host: ${AKAMAI_HOST}"
  185. tab="\t"
  186. # Set in acme.sh _post/_get
  187. #_edgedns_headers="${_edgedns_headers}${tab}User-Agent:ACME DNSAPI Edge DNS version ${ACME_EDGEDNS_VERSION}"
  188. _edgedns_headers="${_edgedns_headers}${tab}Accept: application/json"
  189. if [ "$m" != "GET" ] && [ "$m" != "DELETE" ] ; then
  190. _edgedns_content_type="application/json;charset=UTF-8"
  191. _utf8_body_data="$(echo -n "$ _request_body" | iconv -t utf-8)"
  192. _utf8_body_len="$(echo -n "$_utf8_body_data" | awk '{print length}')"
  193. _edgedns_headers="${_edgedns_headers}${tab}Content-Length: ${_utf8_body_len}"
  194. fi
  195. _made_auth_header=$(_edgedns_make_auth_header)
  196. _edgedns_headers="${_edgedns_headers}${tab}Authorization: ${_made_auth_header}"
  197. _secure_debug2 "Made Auth Header" "${_made_auth_header}"
  198. hdr_indx=1
  199. work_header="${_edgedns_headers}${tab}"
  200. _debug3 "work_header" "${work_header}"
  201. while [ "${work_header}" ]; do
  202. entry="${work_header%%\\t*}"; work_header="${work_header#*\\t}"
  203. export "$(printf "_H%s=%s" "${hdr_indx}" "${entry}")"
  204. _debug2 "Request Header " "${entry}"
  205. hdr_indx=$(( hdr_indx + 1 ))
  206. done
  207. # clear headers from previous request to avoid getting wrong http code on timeouts
  208. :>"$HTTP_HEADER"
  209. _debug "$ep"
  210. if [ "$m" != "GET" ]; then
  211. _debug "Method data" "$data"
  212. # body url [needbase64] [POST|PUT|DELETE] [ContentType]
  213. response="$(_post "$_utf8_body_data" "$ep" false "$m")"
  214. else
  215. response="$(_get "$ep")"
  216. fi
  217. _ret="$?"
  218. _debug "response" "$response"
  219. _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
  220. _debug2 "http response code" "$_code"
  221. if [ "$_code" = "200" ] || [ "$_code" = "201" ]; then
  222. # All good
  223. response="$(echo "$response" | _normalizeJson)"
  224. echo -n "${response}"
  225. return 0
  226. fi
  227. if [ "$_code" = "204" ]; then
  228. # Success, no body
  229. echo -n ""
  230. return 0
  231. fi
  232. if [ "$_code" = "400" ]; then
  233. _err "Bad request presented"
  234. _log "$(printf "Headers: %s" "$_edgedns_headers")"
  235. _log "$(printf "Method: %s" "$_request_method")"
  236. _log "$(printf "URL: %s" "$ep")"
  237. _log "$(printf "Data: %s" "$data")"
  238. fi
  239. if [ "$_code" = "403" ]; then
  240. _err "access denied make sure your Edgegrid cedentials are correct."
  241. fi
  242. echo "$_code"
  243. return 1
  244. }
  245. _edgedns_eg_timestamp() {
  246. _eg_timestamp=$(date -u "+%Y%m%dT%H:%M:%S+0000")
  247. }
  248. _edgedns_new_nonce() {
  249. _nonce=$(uuidgen -r)
  250. }
  251. _edgedns_make_auth_header() {
  252. _debug "Constructing Auth Header"
  253. _edgedns_eg_timestamp
  254. _edgedns_new_nonce
  255. # "Unsigned authorization header: 'EG1-HMAC-SHA256 client_token=block;access_token=block;timestamp=20200806T14:16:33+0000;nonce=72cde72c-82d9-4721-9854-2ba057929d67;'"
  256. _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}")"
  257. _secure_debug2 "Unsigned Auth Header: " "$_auth_header"
  258. _sig="$(_edgedns_sign_request)"
  259. _signed_auth_header="$(printf "%ssignature=%s" "${_auth_header}" "${_sig}")"
  260. _secure_debug2 "Signed Auth Header: " "${_signed_auth_header}"
  261. echo -n "${_signed_auth_header}"
  262. }
  263. _edgedns_sign_request() {
  264. _debug2 "Signing http request"
  265. _signed_data=$(_edgedns_make_data_to_sign "${_auth_header}")
  266. _secure_debug2 "Returned signed data" "$_signed_data"
  267. _key=$(_edgedns_make_signing_key "${_eg_timestamp}")
  268. _signed_req=$(_edgedns_base64_hmac_sha256 "$_signed_data" "$_key")
  269. _secure_debug2 "Signed Request" "${_signed_req}"
  270. echo -n "${_signed_req}"
  271. }
  272. _edgedns_make_signing_key() {
  273. _debug2 "Creating sigining key"
  274. ts=$1
  275. _signing_key=$(_edgedns_base64_hmac_sha256 "$ts" "${AKAMAI_CLIENT_SECRET}")
  276. _secure_debug2 "Signing Key" "${_signing_key}"
  277. echo -n "${_signing_key}"
  278. }
  279. _edgedns_make_data_to_sign() {
  280. _debug2 "Processing data to sign"
  281. hdr=$1
  282. _secure_debug2 "hdr" "$hdr"
  283. content_hash=$(_edgedns_make_content_hash)
  284. path="$(echo -n "${_request_url_path}" |sed 's/https\?:\/\///')"
  285. path="${path#*$AKAMAI_HOST}"
  286. _debug "hier path" "${path}"
  287. # dont expose headers to sign so use MT string
  288. data="$(printf "%s\thttps\t%s\t%s\t%s\t%s\t%s" "${_request_method}" "${AKAMAI_HOST}" "${path}" "" "${content_hash}" "$hdr")"
  289. _secure_debug2 "Data to Sign" "${data}"
  290. echo -n "${data}"
  291. }
  292. _edgedns_make_content_hash() {
  293. _debug2 "Generating content hash"
  294. prep_body=""
  295. _hash=""
  296. _debug2 "Request method" "${_request_method}"
  297. if [ "${_request_method}" != "POST" ] || [ -z "${_request_body}" ]; then
  298. echo -n "${prep_body}"
  299. return 0
  300. fi
  301. prep_body="$(echo -n "${_request_body}")"
  302. _debug2 "Req body" "${prep_body}"
  303. _hash=$(_edgedns_base64_sha256 "${prep_body}")
  304. _debug2 "Content hash" "${_hash}"
  305. echo -n "${_hash}"
  306. }
  307. _edgedns_base64_hmac_sha256() {
  308. _debug2 "Generating hmac"
  309. data=$1
  310. key=$2
  311. encoded_data="$(echo -n "${data}" | iconv -t utf-8)"
  312. encoded_key="$(echo -n "${key}" | iconv -t utf-8)"
  313. _secure_debug2 "encoded data" "${encoded_data}"
  314. _secure_debug2 "encoded key" "${encoded_key}"
  315. #key_hex="$(_durl_replace_base64 "$key" | _dbase64 | _hex_dump | tr -d ' ')"
  316. #data_sig="$(printf "%s" "$encoded_data" | _hmac sha256 "${key_hex}" | _base64 | _url_replace)"
  317. data_sig="$(echo -n "$encoded_data" | ${ACME_OPENSSL_BIN:-openssl} dgst -sha256 -hmac $encoded_key -binary | _base64)"
  318. _secure_debug2 "data_sig:" "${data_sig}"
  319. out="$(echo -n "${data_sig}" | iconv -f utf-8)"
  320. _secure_debug2 "hmac" "${out}"
  321. echo -n "${out}"
  322. }
  323. _edgedns_base64_sha256() {
  324. _debug2 "Creating sha256 digest"
  325. trg=$1
  326. utf8_str="$(echo -n "${trg}" | iconv -t utf-8)"
  327. _secure_debug2 "digest data" "$trg"
  328. _secure_debug2 "encoded digest data" "${utf8_str}"
  329. digest="$(echo -n "${trg}" | ${ACME_OPENSSL_BIN:-openssl} dgst -sha256 -binary | _base64)"
  330. out="$(echo -n "${digest}" | iconv -f utf-8)"
  331. _secure_debug2 "digest decode" "${out}"
  332. echo -n "${out}"
  333. }
  334. #_edgedns_parse_edgerc() {
  335. # filepath=$1
  336. # section=$2
  337. #}