dns_spaceship.sh 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. #!/usr/bin/env sh
  2. # shellcheck disable=SC2034
  3. dns_spaceship_info='Spaceship.com
  4. Site: Spaceship.com
  5. Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship
  6. Options:
  7. SPACESHIP_API_KEY API Key
  8. SPACESHIP_API_SECRET API Secret
  9. SPACESHIP_ROOT_DOMAIN Root domain. Manually specify the root domain if auto-detection fails. Optional.
  10. Issues: github.com/acmesh-official/acme.sh/issues/6304
  11. Author: Meow <@Meo597>
  12. '
  13. # Spaceship API
  14. # https://docs.spaceship.dev/
  15. ######## Public functions #####################
  16. SPACESHIP_API_BASE="https://spaceship.dev/api/v1"
  17. # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
  18. # Used to add txt record
  19. dns_spaceship_add() {
  20. fulldomain="$1"
  21. txtvalue="$2"
  22. _info "Adding TXT record for $fulldomain with value $txtvalue"
  23. # Initialize API credentials and headers
  24. if ! _spaceship_init; then
  25. return 1
  26. fi
  27. # Detect root zone
  28. if ! _get_root "$fulldomain"; then
  29. return 1
  30. fi
  31. # Extract subdomain part relative to root domain
  32. subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//")
  33. if [ "$subdomain" = "$fulldomain" ]; then
  34. _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain"
  35. return 1
  36. fi
  37. _debug "Extracted subdomain: $subdomain for root domain: $_domain"
  38. # Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue)
  39. escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g')
  40. # Prepare payload and URL for adding TXT record
  41. # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API
  42. payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}"
  43. url="$SPACESHIP_API_BASE/dns/records/$_domain"
  44. # Send API request
  45. if _spaceship_api_request "PUT" "$url" "$payload"; then
  46. _info "Successfully added TXT record for $fulldomain"
  47. return 0
  48. else
  49. _err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain."
  50. return 1
  51. fi
  52. }
  53. # Usage: fulldomain txtvalue
  54. # Used to remove the txt record after validation
  55. dns_spaceship_rm() {
  56. fulldomain="$1"
  57. txtvalue="$2"
  58. _info "Removing TXT record for $fulldomain with value $txtvalue"
  59. # Initialize API credentials and headers
  60. if ! _spaceship_init; then
  61. return 1
  62. fi
  63. # Detect root zone
  64. if ! _get_root "$fulldomain"; then
  65. return 1
  66. fi
  67. # Extract subdomain part relative to root domain
  68. subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//")
  69. if [ "$subdomain" = "$fulldomain" ]; then
  70. _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain"
  71. return 1
  72. fi
  73. _debug "Extracted subdomain: $subdomain for root domain: $_domain"
  74. # Escape txtvalue to prevent JSON injection
  75. escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g')
  76. # Prepare payload and URL for deleting TXT record
  77. # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API
  78. payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]"
  79. url="$SPACESHIP_API_BASE/dns/records/$_domain"
  80. # Send API request
  81. if _spaceship_api_request "DELETE" "$url" "$payload"; then
  82. _info "Successfully deleted TXT record for $fulldomain"
  83. return 0
  84. else
  85. _err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain."
  86. return 1
  87. fi
  88. }
  89. #################### Private functions below ##################################
  90. _spaceship_init() {
  91. SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}"
  92. SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}"
  93. if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then
  94. _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET."
  95. _err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials."
  96. return 1
  97. fi
  98. # Save credentials to account config for future renewals
  99. _saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY"
  100. _saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET"
  101. # Set common headers for API requests
  102. export _H1="X-API-Key: $SPACESHIP_API_KEY"
  103. export _H2="X-API-Secret: $SPACESHIP_API_SECRET"
  104. export _H3="Content-Type: application/json"
  105. return 0
  106. }
  107. _get_root() {
  108. domain="$1"
  109. # Check manual override
  110. SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}"
  111. if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then
  112. _domain="$SPACESHIP_ROOT_DOMAIN"
  113. _debug "Using manually specified or saved root domain: $_domain"
  114. _savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN"
  115. return 0
  116. fi
  117. _debug "Detecting root zone for '$domain'"
  118. i=1
  119. p=1
  120. while true; do
  121. _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
  122. _debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)"
  123. if [ -z "$_cutdomain" ]; then
  124. _debug "Cut resulted in empty string, root zone not found."
  125. break
  126. fi
  127. # Call the API to check if this _cutdomain is a manageable zone
  128. if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then
  129. # API call succeeded (HTTP 200 OK for GET /dns/records)
  130. _domain="$_cutdomain"
  131. _debug "Root zone found: '$_domain'"
  132. # Save the detected root domain
  133. _savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain"
  134. _info "Root domain '$_domain' saved to configuration for future use."
  135. return 0
  136. fi
  137. _debug "API check failed for '$_cutdomain'. Continuing search."
  138. p=$i
  139. i=$((i + 1))
  140. done
  141. _err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually."
  142. return 1
  143. }
  144. _spaceship_api_request() {
  145. method="$1"
  146. url="$2"
  147. payload="$3"
  148. _debug2 "Sending $method request to $url with payload $payload"
  149. if [ "$method" = "GET" ]; then
  150. response="$(_get "$url")"
  151. else
  152. response="$(_post "$payload" "$url" "" "$method")"
  153. fi
  154. if [ "$?" != "0" ]; then
  155. _err "API request failed. Response: $response"
  156. return 1
  157. fi
  158. _debug2 "API response body: $response"
  159. if [ "$method" = "GET" ]; then
  160. if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then
  161. return 0
  162. fi
  163. else
  164. if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then
  165. return 0
  166. fi
  167. fi
  168. _debug2 "API response header: $HTTP_HEADER"
  169. return 1
  170. }