From 45b99821725ef3359497cfe3b7b34e31101a3052 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:30:18 +0800 Subject: [PATCH 1/9] Add Spaceship DNS API --- dnsapi/dns_spaceship.sh | 197 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 dnsapi/dns_spaceship.sh diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh new file mode 100644 index 00000000..f94d9027 --- /dev/null +++ b/dnsapi/dns_spaceship.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_spaceship_info='Spaceship.com +Site: Spaceship.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_spaceship +Options: + SPACESHIP_API_KEY Spaceship API Key + SPACESHIP_API_SECRET Spaceship API Secret + SPACESHIP_ROOT_DOMAIN (Optional) Manually specify the root domain if auto-detection fails +Issues: github.com/acmesh-official/acme.sh/issues/6304 +Author: Meow +' + +# Spaceship API +# https://docs.spaceship.dev/ + +######## Public functions ##################### + +SPACESHIP_API_BASE="https://spaceship.dev/api/v1" + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_spaceship_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Adding TXT record for $fulldomain with value $txtvalue" + + # Initialize API credentials and headers + if ! _spaceship_init; then + return 1 + fi + + # Detect root zone + if ! _get_root "$fulldomain"; then + return 1 + fi + + # Extract subdomain part relative to root domain + subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") + if [ "$subdomain" = "$fulldomain" ]; then + _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" + return 1 + fi + _debug "Extracted subdomain: $subdomain for root domain: $_domain" + + # Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue) + escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') + + # Prepare payload and URL for adding TXT record + # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API + payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}" + url="$SPACESHIP_API_BASE/dns/records/$_domain" + + # Send API request + if _spaceship_api_request "PUT" "$url" "$payload"; then + _info "Successfully added TXT record for $fulldomain" + return 0 + else + _err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." + return 1 + fi +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_spaceship_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Removing TXT record for $fulldomain with value $txtvalue" + + # Initialize API credentials and headers + if ! _spaceship_init; then + return 1 + fi + + # Detect root zone + if ! _get_root "$fulldomain"; then + return 1 + fi + + # Extract subdomain part relative to root domain + subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") + if [ "$subdomain" = "$fulldomain" ]; then + _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" + return 1 + fi + _debug "Extracted subdomain: $subdomain for root domain: $_domain" + + # Escape txtvalue to prevent JSON injection + escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') + + # Prepare payload and URL for deleting TXT record + # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API + payload="{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}" + url="$SPACESHIP_API_BASE/dns/records/$_domain" + + # Send API request + if _spaceship_api_request "DELETE" "$url" "$payload"; then + _info "Successfully deleted TXT record for $fulldomain" + return 0 + else + _err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." + return 1 + fi +} + +#################### Private functions below ################################## + +_spaceship_init() { + SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}" + SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}" + + if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then + _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET." + _err "Ensure ~/.acme.sh directory has restricted permissions (chmod 700 ~/.acme.sh) to protect credentials." + return 1 + fi + + # Save credentials to account config for future renewals + _saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY" + _saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET" + + # Set common headers for API requests + export _H1="X-API-Key: $SPACESHIP_API_KEY" + export _H2="X-API-Secret: $SPACESHIP_API_SECRET" + export _H3="Content-Type: application/json" + return 0 +} + +_get_root() { + domain="$1" + + # Check if user manually specified root domain + SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readaccountconf_mutable SPACESHIP_ROOT_DOMAIN)}" + if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then + _domain="$SPACESHIP_ROOT_DOMAIN" + _debug "Using manually specified or saved root domain: $_domain" + # Ensure it's saved (in case it was read from config but not saved previously) + _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" + return 0 + fi + + # Split domain into parts and try from back to front + _debug "Detecting root zone for $domain from back to front" + _parts=$(echo "$domain" | tr '.' '\n' | wc -l) + if [ "$_parts" -lt 2 ]; then + _err "Invalid domain format for $domain" + return 1 + fi + + # Start with the last 2 parts (e.g., example.com) and move forward + i=2 + max_attempts=$((_parts + 1)) + while [ $i -le $max_attempts ]; do + _cutdomain=$(echo "$domain" | rev | cut -d . -f 1-$i | rev) + if [ -z "$_cutdomain" ]; then + break + fi + + _debug "Checking if $_cutdomain is root zone" + if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then + _domain="$_cutdomain" + _debug "Root zone found: $_domain" + # Save the detected root domain to configuration for future use + _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$_domain" + _info "Root domain $_domain saved to configuration for future use." + return 0 + fi + i=$((i + 1)) + done + + _err "Could not detect root zone for $domain after $max_attempts attempts. Please set SPACESHIP_ROOT_DOMAIN manually." + return 1 +} + +_spaceship_api_request() { + method="$1" + url="$2" + payload="$3" + + _debug "Sending $method request to $url with payload $payload" + if [ "$method" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$payload" "$url" "" "$method")" + fi + + if [ "$?" != "0" ]; then + _err "API request failed. Response: $response" + return 1 + fi + + _debug "API response: $response" + return 0 +} From 5e8b40faf65fe2d712698798a25aa209a1c0bdd9 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:10:51 +0800 Subject: [PATCH 2/9] Spaceship: fix rm --- dnsapi/dns_spaceship.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index f94d9027..53dece76 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -93,7 +93,7 @@ dns_spaceship_rm() { # Prepare payload and URL for deleting TXT record # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API - payload="{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}" + payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]" url="$SPACESHIP_API_BASE/dns/records/$_domain" # Send API request @@ -156,6 +156,7 @@ _get_root() { while [ $i -le $max_attempts ]; do _cutdomain=$(echo "$domain" | rev | cut -d . -f 1-$i | rev) if [ -z "$_cutdomain" ]; then + _debug "Reached end of domain parts." break fi From e55a54f3d4cfb92102f3a5036882a64100f563a0 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:30:43 +0800 Subject: [PATCH 3/9] Spaceship: fix get_root --- dnsapi/dns_spaceship.sh | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 53dece76..d70a9a81 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -132,47 +132,49 @@ _spaceship_init() { _get_root() { domain="$1" - # Check if user manually specified root domain + # Check manual override SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readaccountconf_mutable SPACESHIP_ROOT_DOMAIN)}" if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then _domain="$SPACESHIP_ROOT_DOMAIN" _debug "Using manually specified or saved root domain: $_domain" - # Ensure it's saved (in case it was read from config but not saved previously) _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" return 0 fi - # Split domain into parts and try from back to front - _debug "Detecting root zone for $domain from back to front" - _parts=$(echo "$domain" | tr '.' '\n' | wc -l) - if [ "$_parts" -lt 2 ]; then - _err "Invalid domain format for $domain" - return 1 - fi + _debug "Detecting root zone for '$domain'" - # Start with the last 2 parts (e.g., example.com) and move forward i=2 - max_attempts=$((_parts + 1)) - while [ $i -le $max_attempts ]; do - _cutdomain=$(echo "$domain" | rev | cut -d . -f 1-$i | rev) + p=1 + while true; do + _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + + _debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)" + if [ -z "$_cutdomain" ]; then - _debug "Reached end of domain parts." + _debug "Cut resulted in empty string, root zone not found." break fi - _debug "Checking if $_cutdomain is root zone" + # Call the API to check if this _cutdomain is a manageable zone if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then + # API call succeeded (HTTP 200 OK for GET /dns/records) _domain="$_cutdomain" - _debug "Root zone found: $_domain" - # Save the detected root domain to configuration for future use + _debug "Root zone found: '$_domain'" + + # Save the detected root domain _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$_domain" - _info "Root domain $_domain saved to configuration for future use." + _info "Root domain '$_domain' saved to configuration for future use." + return 0 fi + + _debug "API check failed for '$_cutdomain'. Continuing search." + + p=$i i=$((i + 1)) done - _err "Could not detect root zone for $domain after $max_attempts attempts. Please set SPACESHIP_ROOT_DOMAIN manually." + _err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually." return 1 } From 827315e059c1b8ceba9828a9cbaf062768eeec6d Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:49:48 +0800 Subject: [PATCH 4/9] Spaceship: valid api response --- dnsapi/dns_spaceship.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index d70a9a81..501131b8 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -195,6 +195,18 @@ _spaceship_api_request() { return 1 fi - _debug "API response: $response" - return 0 + _debug "API response body: $response" + + if [ "$method" = "GET" ]; then + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then + return 0 + fi + else + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then + return 0 + fi + fi + + _debug "API response header: $HTTP_HEADER" + return 1 } From e1d447847f0e3da3e213ae1b1a6c154bc72a40f0 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Fri, 25 Apr 2025 05:21:52 +0800 Subject: [PATCH 5/9] Spaceship: fix domain conf --- dnsapi/dns_spaceship.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 501131b8..7e9fb167 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -133,11 +133,11 @@ _get_root() { domain="$1" # Check manual override - SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readaccountconf_mutable SPACESHIP_ROOT_DOMAIN)}" + SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}" if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then _domain="$SPACESHIP_ROOT_DOMAIN" _debug "Using manually specified or saved root domain: $_domain" - _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" + _savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" return 0 fi @@ -162,7 +162,7 @@ _get_root() { _debug "Root zone found: '$_domain'" # Save the detected root domain - _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$_domain" + _savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain" _info "Root domain '$_domain' saved to configuration for future use." return 0 From d01aefd1eb17c53604604645798d77a9ace399d9 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Fri, 25 Apr 2025 05:24:05 +0800 Subject: [PATCH 6/9] Spaceship: i starts from 1 --- dnsapi/dns_spaceship.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 7e9fb167..cc3f066f 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -143,7 +143,7 @@ _get_root() { _debug "Detecting root zone for '$domain'" - i=2 + i=1 p=1 while true; do _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) From 2928d843393e839b538307ff8e04a01ca7ae738a Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:04:49 +0800 Subject: [PATCH 7/9] Spaceship: replace debug with debug2 for detailed output in complex debugging --- dnsapi/dns_spaceship.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index cc3f066f..c6db9928 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -183,7 +183,7 @@ _spaceship_api_request() { url="$2" payload="$3" - _debug "Sending $method request to $url with payload $payload" + _debug2 "Sending $method request to $url with payload $payload" if [ "$method" = "GET" ]; then response="$(_get "$url")" else @@ -195,7 +195,7 @@ _spaceship_api_request() { return 1 fi - _debug "API response body: $response" + _debug2 "API response body: $response" if [ "$method" = "GET" ]; then if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then @@ -207,6 +207,6 @@ _spaceship_api_request() { fi fi - _debug "API response header: $HTTP_HEADER" + _debug2 "API response header: $HTTP_HEADER" return 1 } From e2d09231225a2d9cbb33d64d5a49f08a6284c060 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:18:23 +0800 Subject: [PATCH 8/9] Spaceship: replace ~/.acme.sh with $LE_CONFIG_HOME for configurable paths --- dnsapi/dns_spaceship.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index c6db9928..264bdefc 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -114,7 +114,7 @@ _spaceship_init() { if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET." - _err "Ensure ~/.acme.sh directory has restricted permissions (chmod 700 ~/.acme.sh) to protect credentials." + _err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials." return 1 fi From 8b4d93cc14e3c1cf246840c5cd95409c10fd6836 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:32:46 +0800 Subject: [PATCH 9/9] Spaceship: fix doc --- dnsapi/dns_spaceship.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 264bdefc..770e22cc 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -2,7 +2,7 @@ # shellcheck disable=SC2034 dns_spaceship_info='Spaceship.com Site: Spaceship.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_spaceship +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship Options: SPACESHIP_API_KEY Spaceship API Key SPACESHIP_API_SECRET Spaceship API Secret