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.

358 lines
14 KiB

  1. #!/bin/bash
  2. # vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab:
  3. # See:
  4. # https://developer.rackspace.com/docs/cloud-dns/v1/api-reference/
  5. # Rackspace API authentication:
  6. # Create file .rackspace.auth with your Rackspace API user credentials.
  7. # The API user needs permission: DNS, Creator (View, Create, Edit) for adding to work.
  8. # For deletion to work: DNS, Admin (View, Create, Edit, Delete) is needed.
  9. # Example of a .rackspace.auth file:
  10. # { "user": "my rackspace user", "key": "my rackspace API key" }
  11. # Example usage:
  12. # ./acme.sh --keylength 4096 --issue -d "example.com" --dns dns_rackspace --dnssleep 10
  13. ######## Public functions #####################
  14. RACKSPACE_DOMAIN=0
  15. RACKSPACE_DOMAIN_ID=0
  16. RACKSPACE_RETRY=0
  17. #Usage: dns_add _acme-challenge.www.domain.com "XKrxp6q0HG9i01zxXp5CPBs"
  18. dns_rackspace_add() {
  19. local fulldomain=$1
  20. local txtvalue=$2
  21. _info "Using Rackspace Cloud DNS API to add challenge into $fulldomain"
  22. _debug fulldomain "$fulldomain"
  23. _debug txtvalue "$txtvalue"
  24. _rackspace_sanity
  25. _rackspace_authenticate
  26. # At this point, there is an authenticated session token that we can use.
  27. local token_file=/tmp/.acme.rackspace.$EUID.token
  28. local token=$(jq -r ".access.token.id" "$token_file")
  29. local api_url=$(jq -r ".access.serviceCatalog[0].endpoints[0].publicURL" "$token_file")
  30. local json_data
  31. # Try to find a domain from Rackspace that will have the new TXT-record.
  32. # Start by stripping the hard-coded word "_acme-challenge." from the FQDN.
  33. # Remainin record is a potential domain name in Rackspace.
  34. if [[ ! "$fulldomain" =~ ^_acme-challenge\.(.+)$ ]]; then
  35. _err "Failed to extract domain name from $fulldomain. Fatal error, cannot continue."
  36. exit 1
  37. fi
  38. _rackspace_get_domain "${BASH_REMATCH[1]}" "$api_url"
  39. if [ $? -gt 0 ]; then
  40. # If the internal operation fails, an error will be emitted in the _rackspace_get_domain().
  41. # Ultimately, there is no way this operation can continue.
  42. exit 1
  43. fi
  44. local text_rr=${fulldomain%.$RACKSPACE_DOMAIN}
  45. if [ "$fulldomain" == "$text_rr" ]; then
  46. _err "Found domain $RACKSPACE_DOMAIN for $fulldomain, but failed to create a RR for it. Fatal error, cannot continue."
  47. exit 1
  48. fi
  49. _info "Using domain $RACKSPACE_DOMAIN on Rackspace Cloud DNS. Adding $text_rr."
  50. # Add a record
  51. read -r -d '' json_data <<END_OF_JSON
  52. {
  53. "records" : [{
  54. "name" : "$fulldomain",
  55. "type" : "TXT",
  56. "data" : "$txtvalue"
  57. }]
  58. }
  59. END_OF_JSON
  60. _debug "Rackspace API URL to use for adding: $api_url/domains/$RACKSPACE_DOMAIN_ID/records"
  61. json_data=$(curl --silent -X POST --data "$json_data" -H "X-Auth-Token: $token" -H "Content-Type: application/json" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records")
  62. if [ $? -gt 0 ]; then
  63. _err "Failed to add record to Rackspace Cloud DNS. Fatal error, cannot continue."
  64. exit 1
  65. fi
  66. local status=$(echo "$json_data" | jq -r '.status')
  67. if [ -z "$status" ] || [ "$status" == "null" ]; then
  68. local code=$(echo "$json_data" | jq -r '."error-message"')
  69. if [ -n "$code" ] && [ "$code" != "null" ]; then
  70. _err "Failed to add record to Rackspace Cloud DNS. No permission to add! Fatal error: $code"
  71. else
  72. code=$(echo "$json_data" | jq -r .code)
  73. _err "Failed to add record to Rackspace Cloud DNS. Status: HTTP/$code"
  74. fi
  75. exit 1
  76. fi
  77. if [ "$status" == "RUNNING" ]; then
  78. local callback_url=$(echo "$json_data" | jq -r '.callbackUrl')
  79. if [ -z "$callback_url" ]; then
  80. _err "Attempt to add record to Rackspace Cloud DNS most likely failed. There is no callback URL to query the operation status from."
  81. return 1
  82. fi
  83. while [ "$status" == "RUNNING" ]; do
  84. sleep 2
  85. json_data=$(curl --silent -H "X-Auth-Token: $token" "$callback_url")
  86. if [ $? -gt 0 ]; then
  87. _err "Failed to query DNS add status from $callback_url"
  88. return 1
  89. fi
  90. status=$(echo "$json_data" | jq -r '.status')
  91. done
  92. if [ "$status" == "ERROR" ]; then
  93. _err "Failed to add record to Rackspace Cloud DNS, add status is: $status."
  94. # See, if the add failed because the record already exists.
  95. json_data=$(curl --silent -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records?type=TXT&name=$fulldomain")
  96. if [ $? == 0 ] && [ -n "$json_data" ] && [ $RACKSPACE_RETRY == 0 ] ; then
  97. # We have something ...
  98. local record_id=$(echo "$json_data" | jq -r '.records[0].id')
  99. if [ -n "$record_id" ] || [ "$record_id" != "null" ]; then
  100. # Attempt to delete the record
  101. _info "Found existing record! Deleting record $record_id on domain $RACKSPACE_DOMAIN."
  102. json_data=$(curl --silent -X DELETE -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records/$record_id")
  103. local status=$(echo "$json_data" | jq -r '.status')
  104. if [ "$status" == "RUNNING" ]; then
  105. _info "Succesfully deleted the record. Retrying ..."
  106. RACKSPACE_RETRY=1
  107. sleep 3
  108. # Call myself with same arguments.
  109. dns_rackspace_add "$1" "$2"
  110. return $?
  111. else
  112. _err "Failed to delete the record. Nothing else to try."
  113. fi
  114. fi
  115. fi
  116. # end if $status = ERROR
  117. fi
  118. fi
  119. if [ "$status" != "COMPLETED" ]; then
  120. _err "Failed to add record to Rackspace Cloud DNS, add status is: $status."
  121. return 1
  122. fi
  123. return 0
  124. }
  125. #Usage: fulldomain txtvalue
  126. #Remove the txt record after validation.
  127. dns_rackspace_rm() {
  128. local fulldomain=$1
  129. local txtvalue=$2
  130. _info "Using Rackspace Cloud DNS API to remove challenge $fulldomain"
  131. _debug fulldomain "$fulldomain"
  132. _debug txtvalue "$txtvalue"
  133. _rackspace_sanity
  134. _rackspace_authenticate
  135. # At this point, there is an authenticated session token that we can use.
  136. local token_file=/tmp/.acme.rackspace.$EUID.token
  137. local token=$(jq -r ".access.token.id" "$token_file")
  138. local api_url=$(jq -r ".access.serviceCatalog[0].endpoints[0].publicURL" "$token_file")
  139. local json_data
  140. # Try to find a domain from Rackspace that will have an existing TXT-record.
  141. if [[ ! "$fulldomain" =~ ^_acme-challenge\.(.+)$ ]]; then
  142. _err "Failed to extract domain name from $fulldomain."
  143. return 1
  144. fi
  145. _rackspace_get_domain "${BASH_REMATCH[1]}" "$api_url"
  146. if [ $? -gt 0 ]; then
  147. # If the internal operation fails, an error will be emitted in the _rackspace_get_domain().
  148. return 1
  149. fi
  150. local text_rr=${fulldomain%.$RACKSPACE_DOMAIN}
  151. _info "Using domain $RACKSPACE_DOMAIN on Rackspace Cloud DNS. Trying to find and remove $text_rr."
  152. _debug "Rackspace API URL to use for searching: $api_url/domains/$RACKSPACE_DOMAIN_ID/records"
  153. # Go search for TXT-records
  154. json_data=$(curl --silent -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records?type=TXT&name=$fulldomain")
  155. if [ $? -gt 0 ]; then
  156. _err "Failed to search for TXT-record $fulldomain on Rackspace Cloud DNS."
  157. return 1
  158. fi
  159. local record_id=$(echo "$json_data" | jq -r '.records[0].id')
  160. if [ -z "$record_id" ] || [ "$record_id" == "null" ]; then
  161. _err "TXT-record $fulldomain was not found. Nothing to delete."
  162. return 1
  163. fi
  164. _info "Deleting record $record_id on domain $RACKSPACE_DOMAIN."
  165. json_data=$(curl --silent -X DELETE -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains/$RACKSPACE_DOMAIN_ID/records/$record_id")
  166. if [ $? -gt 0 ]; then
  167. _err "Failed to delete record to Rackspace Cloud DNS. API-call failed."
  168. return 1
  169. fi
  170. local status=$(echo "$json_data" | jq -r '.status')
  171. if [ -z "$status" ] || [ "$status" == "null" ]; then
  172. status=$(echo "$json_data" | jq -r '."error-message"')
  173. if [ -n "$status" ] && [ "$status" != "null" ]; then
  174. _err "Failed to delete record to Rackspace Cloud DNS. No permission to delete! Error: $status"
  175. else
  176. _err "Failed to delete record to Rackspace Cloud DNS. Unknown reason."
  177. fi
  178. return 1
  179. fi
  180. if [ "$status" != "RUNNING" ]; then
  181. _err "Failed to delete record to Rackspace Cloud DNS."
  182. return 1
  183. fi
  184. return 0
  185. }
  186. #################### Private functions below ##################################
  187. _rackspace_sanity() {
  188. local needed=('curl' 'jq')
  189. local cmd
  190. for cmd in "${needed[@]}"; do
  191. which "$cmd" >& /dev/null
  192. if [ $? -gt 0 ]; then
  193. _err "Rackspace Cloud DNS API needs command: $cmd"
  194. exit 1
  195. fi
  196. done
  197. }
  198. _rackspace_get_domain() {
  199. local domain_to_use="$1"
  200. local api_url="$2"
  201. local domain_to_check
  202. # Get list of all domains this API user can manage.
  203. local json_data=$(curl --silent -H "X-Auth-Token: $token" -H "Accept: application/json" "$api_url/domains")
  204. if [ $? -gt 0 ] || [ -z "$json_data" ]; then
  205. _err "Failed to retrieve domain list from Rackspace Cloud DNS API. Fatal error, cannot continue."
  206. return 1
  207. fi
  208. local status=$(echo "$json_data" | jq -r '."error-message"')
  209. if [ -n "$status" ] && [ "$status" != "null" ]; then
  210. _err "Configured user has no permission to retrieve domain list from Rackspace Cloud DNS API. Fatal error, cannot continue."
  211. return 1
  212. fi
  213. # Iterate the domain list reverse-sorted. That will do a longest match comparison if there are
  214. # subdomain used for the request, but will also match a shorter domain.
  215. local matching_domain_idx hostmaster_email
  216. while [ -z "$matching_domain_idx" ] && [[ $domain_to_use =~ \..+$ ]]; do
  217. local domain_idx=0
  218. while [ "$domain_to_check" != "null" ]; do
  219. domain_to_check=$(echo "$json_data" | jq -r '.domains|sort_by(.name)|reverse|.['$domain_idx'].name')
  220. if [ "$domain_to_use" == "$domain_to_check" ]; then
  221. matching_domain_idx=$domain_idx
  222. hostmaster_email=$(echo "$json_data" | jq -r '.domains|sort_by(.name)|reverse|.['$domain_idx'].emailAddress')
  223. RACKSPACE_DOMAIN="$domain_to_use"
  224. RACKSPACE_DOMAIN_ID=$(echo "$json_data" | jq -r '.domains|sort_by(.name)|reverse|.['$domain_idx'].id')
  225. break
  226. fi
  227. domain_idx=$(( domain_idx+1 ))
  228. done
  229. if [ -z "$matching_domain_idx" ]; then
  230. # Eat out one level from the domain, if nothing found
  231. domain_to_use=${domain_to_use#*.}
  232. domain_to_check=''
  233. fi
  234. done
  235. if [ -z "$matching_domain_idx" ]; then
  236. _err "Failed to find the domain for $fulldomain to add a record. Fatal error, cannot continue."
  237. return 1
  238. fi
  239. if [ -z "$RACKSPACE_DOMAIN_ID" ] || [ "$RACKSPACE_DOMAIN_ID" == "null" ]; then
  240. _err "Failed to get domain ID for domain $domain_to_use. Fatal error, cannot add record."
  241. return 1
  242. fi
  243. return 0
  244. }
  245. _rackspace_authenticate() {
  246. local token_file=/tmp/.acme.rackspace.$EUID.token
  247. if [ ! -e "$token_file" ]; then
  248. _rackspace_get_token
  249. fi
  250. local token=$(jq -r --exit-status .access.token.id "$token_file")
  251. if [ $? -gt 0 ]; then
  252. _rackspace_get_token
  253. token=$(jq -r --exit-status .access.token.id "$token_file")
  254. if [ $? -gt 0 ]; then
  255. _err "Failed to read access token from $token_file"
  256. exit 1
  257. fi
  258. fi
  259. curl --silent -H "X-Auth-Token: $token" "https://identity.api.rackspacecloud.com/v2.0/tokens/$token" | jq --exit-status .access.token.tenant > /dev/null
  260. if [ $? -gt 0 ]; then
  261. _err "Failed to verify access token from $token_file"
  262. exit 1
  263. fi
  264. }
  265. _rackspace_get_token() {
  266. local token_file=/tmp/.acme.rackspace.$EUID.token
  267. local creds_file user key
  268. local auth_json stat umask code
  269. creds_file="$_SCRIPT_HOME/.rackspace.auth"
  270. if [ ! -e "$creds_file" ]; then
  271. creds_file="$LE_WORKING_DIR/.rackspace.auth"
  272. if [ ! -e "$creds_file" ]; then
  273. _err "Rackspace Cloud DNS API needs credentials in .rackspace.auth file in $_SCRIPT_HOME/ or $LE_WORKING_DIR/"
  274. exit 1
  275. fi
  276. fi
  277. user=$(jq -r .user "$creds_file")
  278. key=$(jq -r .key "$creds_file")
  279. if [ -z "$user" ] || [ -z "$key" ]; then
  280. _err "Failed to read Rackspace Cloud DNS API credentials from $creds_file"
  281. exit 1
  282. fi
  283. umask=$(umask)
  284. umask 0077
  285. auth_json="{\"auth\":{\"RAX-KSKEY:apiKeyCredentials\":{\"username\":\"$user\",\"apiKey\":\"$key\"}}}"
  286. curl --silent https://identity.api.rackspacecloud.com/v2.0/tokens -X POST -d "$auth_json" -H "Content-type: application/json" > $token_file
  287. stat=$?
  288. umask $umask
  289. if [ $stat -gt 0 ]; then
  290. _err "Failed to make an authentication request into Rackspace Cloud DNS API"
  291. exit 1
  292. fi
  293. jq . $token_file > /dev/null
  294. if [ $? -gt 0 ]; then
  295. _err "Failed to retrieve authentication JSON from Rackspace Cloud DNS API"
  296. exit 1
  297. fi
  298. code=$(jq -r --exit-status .access.token.tenant "$token_file")
  299. stat=$?
  300. if [ $stat -gt 0 ]; then
  301. code=$(jq -r .unauthorized.code "$token_file")
  302. _err "Failed to authenticate into Rackspace Cloud DNS API ($stat). Status: HTTP/$code"
  303. rm -f "$token_file"
  304. exit 1
  305. fi
  306. }