| $label<\/td> | \([^<]\{1,\}\)<\/td><\/tr>/\1/i")
+ printf "%s" "$lookup_result"
+ return 0
+}
+
+# html
+_zyxel_gs1900_get_model() {
+ html="$1"
+ model_name=$(_zyxel_html_table_lookup "$html" "Model Name:")
+ printf "%s" "$model_name"
+}
+
+# html
+_zyxel_gs1900_get_firmware_version() {
+ html="$1"
+ firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+")
+ printf "%s" "$firmware_version"
+}
+
+# version_number
+_zyxel_gs1900_parse_major_version() {
+ printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
+}
+
+# version_number
+_zyxel_gs1900_parse_minor_version() {
+ printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
+}
diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh
index 906ea443..8d9676ac 100755
--- a/dnsapi/dns_1984hosting.sh
+++ b/dnsapi/dns_1984hosting.sh
@@ -128,7 +128,7 @@ _1984hosting_login() {
_get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
- sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+ sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@@ -145,7 +145,7 @@ _1984hosting_login() {
_debug2 response "$response"
if _contains "$response" '"loggedin": true'; then
- One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+ One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
export One984HOSTING_SESSIONID_COOKIE
export One984HOSTING_CSRFTOKEN_COOKIE
diff --git a/dnsapi/dns_active24.sh b/dnsapi/dns_active24.sh
index c56dd363..0f24c53a 100755
--- a/dnsapi/dns_active24.sh
+++ b/dnsapi/dns_active24.sh
@@ -1,17 +1,17 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
-dns_active24_info='Active24.com
-Site: Active24.com
+dns_active24_info='Active24.cz
+Site: Active24.cz
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_active24
Options:
- ACTIVE24_Token API Token
+ Active24_ApiKey API Key. Called "Identifier" in the Active24 Admin
+ Active24_ApiSecret API Secret. Called "Secret key" in the Active24 Admin
Issues: github.com/acmesh-official/acme.sh/issues/2059
-Author: Milan Pála
'
-ACTIVE24_Api="https://api.active24.com"
-
-######## Public functions #####################
+Active24_Api="https://rest.active24.cz"
+# export Active24_ApiKey=ak48l3h7-ak5d-qn4t-p8gc-b6fs8c3l
+# export Active24_ApiSecret=ajvkeo3y82ndsu2smvxy3o36496dcascksldncsq
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
@@ -22,8 +22,8 @@ dns_active24_add() {
_active24_init
_info "Adding txt record"
- if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then
- if _contains "$response" "errors"; then
+ if _active24_rest POST "/v2/service/$_service_id/dns/record" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":300}"; then
+ if _contains "$response" "error"; then
_err "Add txt record error."
return 1
else
@@ -31,6 +31,7 @@ dns_active24_add() {
return 0
fi
fi
+
_err "Add txt record error."
return 1
}
@@ -44,19 +45,25 @@ dns_active24_rm() {
_active24_init
_debug "Getting txt records"
- _active24_rest GET "dns/$_domain/records/v1"
+ # The API needs to send data in body in order the filter to work
+ # TODO: web can also add content $txtvalue to filter and then get the id from response
+ _active24_rest GET "/v2/service/$_service_id/dns/record" "{\"page\":1,\"descending\":true,\"sortBy\":\"name\",\"rowsPerPage\":100,\"totalRecords\":0,\"filters\":{\"type\":[\"TXT\"],\"name\":\"${_sub_domain}\"}}"
+ #_active24_rest GET "/v2/service/$_service_id/dns/record?rowsPerPage=100"
- if _contains "$response" "errors"; then
+ if _contains "$response" "error"; then
_err "Error"
return 1
fi
- hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-)
+ # Note: it might never be more than one record actually, NEEDS more INVESTIGATION
+ record_ids=$(printf "%s" "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
+ _debug2 record_ids "$record_ids"
- for hash_id in $hash_ids; do
- _debug "Removing hash_id" "$hash_id"
- if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then
- if _contains "$response" "errors"; then
+ for redord_id in $record_ids; do
+ _debug "Removing record_id" "$redord_id"
+ _debug "txtvalue" "$txtvalue"
+ if _active24_rest DELETE "/v2/service/$_service_id/dns/record/$redord_id" ""; then
+ if _contains "$response" "error"; then
_err "Unable to remove txt record."
return 1
else
@@ -70,21 +77,15 @@ dns_active24_rm() {
return 1
}
-#################### Private functions below ##################################
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
_get_root() {
domain=$1
+ i=1
+ p=1
- if ! _active24_rest GET "dns/domains/v1"; then
+ if ! _active24_rest GET "/v1/user/self/service"; then
return 1
fi
- i=1
- p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug "h" "$h"
@@ -104,45 +105,102 @@ _get_root() {
return 1
}
-_active24_rest() {
- m=$1
- ep="$2"
- data="$3"
- _debug "$ep"
-
- export _H1="Authorization: Bearer $ACTIVE24_Token"
-
- if [ "$m" != "GET" ]; then
- _debug "data" "$data"
- response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")"
- else
- response="$(_get "$ACTIVE24_Api/$ep")"
+_active24_init() {
+ Active24_ApiKey="${Active24_ApiKey:-$(_readaccountconf_mutable Active24_ApiKey)}"
+ Active24_ApiSecret="${Active24_ApiSecret:-$(_readaccountconf_mutable Active24_ApiSecret)}"
+ #Active24_ServiceId="${Active24_ServiceId:-$(_readaccountconf_mutable Active24_ServiceId)}"
+
+ if [ -z "$Active24_ApiKey" ] || [ -z "$Active24_ApiSecret" ]; then
+ Active24_ApiKey=""
+ Active24_ApiSecret=""
+ _err "You don't specify Active24 api key and ApiSecret yet."
+ _err "Please create your key and try again."
+ return 1
fi
- if [ "$?" != "0" ]; then
- _err "error $ep"
+ #save the credentials to the account conf file.
+ _saveaccountconf_mutable Active24_ApiKey "$Active24_ApiKey"
+ _saveaccountconf_mutable Active24_ApiSecret "$Active24_ApiSecret"
+
+ _debug "A24 API CHECK"
+ if ! _active24_rest GET "/v2/check"; then
+ _err "A24 API check failed with: $response"
return 1
fi
- _debug2 response "$response"
- return 0
-}
-_active24_init() {
- ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}"
- if [ -z "$ACTIVE24_Token" ]; then
- ACTIVE24_Token=""
- _err "You didn't specify a Active24 api token yet."
- _err "Please create the token and try again."
+ if ! echo "$response" | tr -d " " | grep \"verified\":true >/dev/null; then
+ _err "A24 API check failed with: $response"
return 1
fi
- _saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_Token"
-
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
+
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
+ _active24_get_service_id "$_domain"
+ _debug _service_id "$_service_id"
+}
+
+_active24_get_service_id() {
+ _d=$1
+ if ! _active24_rest GET "/v1/user/self/zone/${_d}"; then
+ return 1
+ else
+ response=$(echo "$response" | _json_decode)
+ _service_id=$(echo "$response" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
+ fi
+}
+
+_active24_rest() {
+ m=$1
+ ep_qs=$2 # with query string
+ # ep=$2
+ ep=$(printf "%s" "$ep_qs" | cut -d '?' -f1) # no query string
+ data="$3"
+
+ _debug "A24 $ep"
+ _debug "A24 $Active24_ApiKey"
+ _debug "A24 $Active24_ApiSecret"
+
+ timestamp=$(_time)
+ datez=$(date -u +"%Y%m%dT%H%M%SZ")
+ canonicalRequest="${m} ${ep} ${timestamp}"
+ signature=$(printf "%s" "$canonicalRequest" | _hmac sha1 "$(printf "%s" "$Active24_ApiSecret" | _hex_dump | tr -d " ")" hex)
+ authorization64="$(printf "%s:%s" "$Active24_ApiKey" "$signature" | _base64)"
+
+ export _H1="Date: ${datez}"
+ export _H2="Accept: application/json"
+ export _H3="Content-Type: application/json"
+ export _H4="Authorization: Basic ${authorization64}"
+
+ _debug2 H1 "$_H1"
+ _debug2 H2 "$_H2"
+ _debug2 H3 "$_H3"
+ _debug2 H4 "$_H4"
+
+ # _sleep 1
+
+ if [ "$m" != "GET" ]; then
+ _debug2 "${m} $Active24_Api${ep_qs}"
+ _debug "data" "$data"
+ response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
+ else
+ if [ -z "$data" ]; then
+ _debug2 "GET $Active24_Api${ep_qs}"
+ response="$(_get "$Active24_Api${ep_qs}")"
+ else
+ _debug2 "GET $Active24_Api${ep_qs} with data: ${data}"
+ response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
+ fi
+ fi
+ if [ "$?" != "0" ]; then
+ _err "error $ep"
+ return 1
+ fi
+ _debug2 response "$response"
+ return 0
}
diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh
index 53a82f91..90196c69 100755
--- a/dnsapi/dns_ali.sh
+++ b/dnsapi/dns_ali.sh
@@ -97,12 +97,13 @@ _ali_rest() {
}
_ali_nonce() {
- #_head_n 1 /dev/null && return 0
+ fi
+ printf "%s" "$(date +%s)$$$(date +%N)" | _digest sha256 hex | cut -c 1-32
}
-_timestamp() {
+_ali_timestamp() {
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
}
@@ -150,7 +151,7 @@ _check_exist_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&TypeKeyWord=TXT'
query=$query'&Version=2015-01-09'
}
@@ -166,7 +167,7 @@ _add_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Type=TXT'
query=$query'&Value='$3
query=$query'&Version=2015-01-09'
@@ -182,7 +183,7 @@ _delete_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09'
}
@@ -196,7 +197,7 @@ _describe_records_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09'
}
diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh
index c88c9d9c..b76d69c2 100755
--- a/dnsapi/dns_aws.sh
+++ b/dnsapi/dns_aws.sh
@@ -161,7 +161,7 @@ _get_root() {
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g')
_debug "Checking domain: $h"
if [ -z "$h" ]; then
- _error "invalid domain"
+ _err "invalid domain"
return 1
fi
diff --git a/dnsapi/dns_azure.sh b/dnsapi/dns_azure.sh
index 3f0dfa3d..f9d84706 100644
--- a/dnsapi/dns_azure.sh
+++ b/dnsapi/dns_azure.sh
@@ -9,7 +9,7 @@ Options:
AZUREDNS_APPID App ID. App ID of the service principal
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
- AZUREDNS_BEARERTOKEN Optional Bearer Token. Used instead of service principal credentials or managed identity
+ AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
'
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
@@ -340,8 +340,17 @@ _azure_getaccess_token() {
if [ "$managedIdentity" = true ]; then
# https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
- export _H1="Metadata: true"
- response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)"
+ if [ -n "$IDENTITY_ENDPOINT" ]; then
+ # Some Azure environments may set IDENTITY_ENDPOINT (formerly MSI_ENDPOINT) to have an alternative metadata endpoint
+ url="$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/"
+ headers="X-IDENTITY-HEADER: $IDENTITY_HEADER"
+ else
+ url="http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
+ headers="Metadata: true"
+ fi
+
+ export _H1="$headers"
+ response="$(_get "$url")"
response="$(echo "$response" | _normalizeJson)"
accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
diff --git a/dnsapi/dns_beget.sh b/dnsapi/dns_beget.sh
new file mode 100755
index 00000000..5f3b1eb1
--- /dev/null
+++ b/dnsapi/dns_beget.sh
@@ -0,0 +1,281 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_beget_info='Beget.com
+Site: Beget.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_beget
+Options:
+ BEGET_User API user
+ BEGET_Password API password
+Issues: github.com/acmesh-official/acme.sh/issues/6200
+Author: ARNik
+'
+
+Beget_Api="https://api.beget.com/api"
+
+#################### Public functions ####################
+
+# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_beget_add() {
+ fulldomain=$1
+ txtvalue=$2
+ _debug "dns_beget_add() $fulldomain $txtvalue"
+ fulldomain=$(echo "$fulldomain" | _lower_case)
+
+ Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
+ Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
+
+ if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then
+ Beget_Username=""
+ Beget_Password=""
+ _err "You must export variables: Beget_Username, and Beget_Password"
+ return 1
+ fi
+
+ #save the credentials to the account conf file.
+ _saveaccountconf_mutable Beget_Username "$Beget_Username"
+ _saveaccountconf_mutable Beget_Password "$Beget_Password"
+
+ _info "Prepare subdomain."
+ if ! _prepare_subdomain "$fulldomain"; then
+ _err "Can't prepare subdomain."
+ return 1
+ fi
+
+ _info "Get domain records"
+ data="{\"fqdn\":\"$fulldomain\"}"
+ res=$(_api_call "$Beget_Api/dns/getData" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't get domain records."
+ return 1
+ fi
+
+ _info "Add new TXT record"
+ data="{\"fqdn\":\"$fulldomain\",\"records\":{"
+ data=${data}$(_parce_records "$res" "A")
+ data=${data}$(_parce_records "$res" "AAAA")
+ data=${data}$(_parce_records "$res" "CAA")
+ data=${data}$(_parce_records "$res" "MX")
+ data=${data}$(_parce_records "$res" "SRV")
+ data=${data}$(_parce_records "$res" "TXT")
+ data=$(echo "$data" | sed 's/,$//')
+ data=${data}'}}'
+
+ str=$(_txt_to_dns_json "$txtvalue")
+ data=$(_add_record "$data" "TXT" "$str")
+
+ res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't change domain records."
+ return 1
+ fi
+
+ return 0
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_beget_rm() {
+ fulldomain=$1
+ txtvalue=$2
+ _debug "dns_beget_rm() $fulldomain $txtvalue"
+ fulldomain=$(echo "$fulldomain" | _lower_case)
+
+ Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
+ Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
+
+ _info "Get current domain records"
+ data="{\"fqdn\":\"$fulldomain\"}"
+ res=$(_api_call "$Beget_Api/dns/getData" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't get domain records."
+ return 1
+ fi
+
+ _info "Remove TXT record"
+ data="{\"fqdn\":\"$fulldomain\",\"records\":{"
+ data=${data}$(_parce_records "$res" "A")
+ data=${data}$(_parce_records "$res" "AAAA")
+ data=${data}$(_parce_records "$res" "CAA")
+ data=${data}$(_parce_records "$res" "MX")
+ data=${data}$(_parce_records "$res" "SRV")
+ data=${data}$(_parce_records "$res" "TXT")
+ data=$(echo "$data" | sed 's/,$//')
+ data=${data}'}}'
+
+ str=$(_txt_to_dns_json "$txtvalue")
+ data=$(_rm_record "$data" "$str")
+
+ res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't change domain records."
+ return 1
+ fi
+
+ return 0
+}
+
+#################### Private functions below ####################
+
+# Create subdomain if needed
+# Usage: _prepare_subdomain [fulldomain]
+_prepare_subdomain() {
+ fulldomain=$1
+
+ _info "Detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+ _debug _domain_id "$_domain_id"
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
+ if [ -z "$_sub_domain" ]; then
+ _debug "$fulldomain is a root domain."
+ return 0
+ fi
+
+ _info "Get subdomain list"
+ res=$(_api_call "$Beget_Api/domain/getSubdomainList")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't get subdomain list."
+ return 1
+ fi
+
+ if _contains "$res" "\"fqdn\":\"$fulldomain\""; then
+ _debug "Subdomain $fulldomain already exist."
+ return 0
+ fi
+
+ _info "Subdomain $fulldomain does not exist. Let's create one."
+ data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}"
+ res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't create subdomain."
+ return 1
+ fi
+
+ _debug "Cleanup subdomen records"
+ data="{\"fqdn\":\"$fulldomain\",\"records\":{}}"
+ res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _debug "Can't cleanup $fulldomain records."
+ fi
+
+ data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}"
+ res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+ if ! _is_api_reply_ok "$res"; then
+ _debug "Can't cleanup www.$fulldomain records."
+ fi
+
+ return 0
+}
+
+# Usage: _get_root _acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=32436365
+_get_root() {
+ fulldomain=$1
+ i=1
+ p=1
+
+ _debug "Get domain list"
+ res=$(_api_call "$Beget_Api/domain/getList")
+ if ! _is_api_reply_ok "$res"; then
+ _err "Can't get domain list."
+ return 1
+ fi
+
+ while true; do
+ h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
+ _debug h "$h"
+
+ if [ -z "$h" ]; then
+ return 1
+ fi
+
+ if _contains "$res" "$h"; then
+ _domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2)
+ if [ "$_domain_id" ]; then
+ if [ "$h" != "$fulldomain" ]; then
+ _sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p")
+ else
+ _sub_domain=""
+ fi
+ _domain=$h
+ return 0
+ fi
+ return 1
+ fi
+ p="$i"
+ i=$(_math "$i" + 1)
+ done
+ return 1
+}
+
+# Parce DNS records from json string
+# Usage: _parce_records [j_str] [record_name]
+_parce_records() {
+ j_str=$1
+ record_name=$2
+ res="\"$record_name\":["
+ res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1)
+ res=${res}"],"
+ echo "$res"
+}
+
+# Usage: _add_record [data] [record_name] [record_data]
+_add_record() {
+ data=$1
+ record_name=$2
+ record_data=$3
+ echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/"
+}
+
+# Usage: _rm_record [data] [record_data]
+_rm_record() {
+ data=$1
+ record_data=$2
+ echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" |
+ sed "s/{,/{/g" | sed "s/,}/}/g" |
+ sed "s/\[,/\[/g" | sed "s/,\]/\]/g"
+}
+
+_txt_to_dns_json() {
+ echo "{\"ttl\":600,\"txtdata\":\"$1\"}"
+}
+
+# Usage: _api_call [api_url] [input_data]
+_api_call() {
+ api_url="$1"
+ input_data="$2"
+
+ _debug "_api_call $api_url"
+ _debug "Request: $input_data"
+
+ # res=$(curl -s -L -D ./http.header \
+ # "$api_url" \
+ # --data-urlencode login=$Beget_Username \
+ # --data-urlencode passwd=$Beget_Password \
+ # --data-urlencode input_format=json \
+ # --data-urlencode output_format=json \
+ # --data-urlencode "input_data=$input_data")
+
+ url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json"
+ if [ -n "$input_data" ]; then
+ url=${url}"&input_data="
+ url=${url}$(echo "$input_data" | _url_encode)
+ fi
+ res=$(_get "$url")
+
+ _debug "Reply: $res"
+ echo "$res"
+}
+
+# Usage: _is_api_reply_ok [api_reply]
+_is_api_reply_ok() {
+ _contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$'
+}
diff --git a/dnsapi/dns_bookmyname.sh b/dnsapi/dns_bookmyname.sh
index 668cf074..cf3f1e3e 100644
--- a/dnsapi/dns_bookmyname.sh
+++ b/dnsapi/dns_bookmyname.sh
@@ -7,7 +7,7 @@ Options:
BOOKMYNAME_USERNAME Username
BOOKMYNAME_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/3209
-Author: Neilpang
+Author: @Neilpang
'
######## Public functions #####################
diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh
index 736742f3..7b383c43 100755
--- a/dnsapi/dns_cf.sh
+++ b/dnsapi/dns_cf.sh
@@ -92,7 +92,9 @@ dns_cf_add() {
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
- elif _contains "$response" "The record already exists"; then
+ elif _contains "$response" "The record already exists" ||
+ _contains "$response" "An identical record already exists." ||
+ _contains "$response" '"code":81058'; then
_info "Already exists, OK"
return 0
else
diff --git a/dnsapi/dns_cloudns.sh b/dnsapi/dns_cloudns.sh
index 8bb0e00d..23a219da 100755
--- a/dnsapi/dns_cloudns.sh
+++ b/dnsapi/dns_cloudns.sh
@@ -197,10 +197,11 @@ _dns_cloudns_http_api_call() {
auth_user="auth-id=$CLOUDNS_AUTH_ID"
fi
+ encoded_password=$(echo "$CLOUDNS_AUTH_PASSWORD" | tr -d "\n\r" | _url_encode)
if [ -z "$2" ]; then
- data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD"
+ data="$auth_user&auth-password=$encoded_password"
else
- data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
+ data="$auth_user&auth-password=$encoded_password&$2"
fi
response="$(_get "$CLOUDNS_API/$method?$data")"
diff --git a/dnsapi/dns_constellix.sh b/dnsapi/dns_constellix.sh
index 6a50e199..7251f8b2 100644
--- a/dnsapi/dns_constellix.sh
+++ b/dnsapi/dns_constellix.sh
@@ -117,7 +117,7 @@ dns_constellix_rm() {
#################### Private functions below ##################################
_get_root() {
- domain=$1
+ domain=$(echo "$1" | _lower_case)
i=2
p=1
_debug "Detecting root zone"
@@ -156,6 +156,9 @@ _constellix_rest() {
data="$3"
_debug "$ep"
+ # Prevent rate limit
+ _sleep 2
+
rdate=$(date +"%s")"000"
hmac=$(printf "%s" "$rdate" | _hmac sha1 "$(printf "%s" "$CONSTELLIX_Secret" | _hex_dump | tr -d ' ')" | _base64)
diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh
index f57afa1f..0ef03fea 100644
--- a/dnsapi/dns_curanet.sh
+++ b/dnsapi/dns_curanet.sh
@@ -15,7 +15,7 @@ CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains"
CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token"
CURANET_ACCESS_TOKEN=""
-######## Public functions #####################
+######## Public functions ####################
#Usage: dns_curanet_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_curanet_add() {
@@ -154,7 +154,7 @@ _get_root() {
export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN"
response="$(_get "$CURANET_REST_URL/$h/Records" "" "")"
- if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then
+ if [ ! "$(echo "$response" | _egrep_o "Entity not found|Bad Request")" ]; then
_domain=$h
return 0
fi
diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh
index 04a515aa..0c74be2a 100644
--- a/dnsapi/dns_cyon.sh
+++ b/dnsapi/dns_cyon.sh
@@ -101,6 +101,8 @@ _cyon_load_parameters() {
# This header is required for curl calls.
_H1="X-Requested-With: XMLHttpRequest"
export _H1
+ _H3="User-Agent: cyon-dns-acmesh/1.0"
+ export _H3
}
_cyon_print_header() {
@@ -125,7 +127,11 @@ _cyon_print_header() {
}
_cyon_get_cookie_header() {
- printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')"
+ # Extract all cookies from the response headers (case-insensitive)
+ _cookies="$(grep -i "^set-cookie:" "$HTTP_HEADER" | sed 's/^[Ss]et-[Cc]ookie: //' | sed 's/;.*//' | tr '\n' '; ' | sed 's/; $//')"
+ if [ -n "$_cookies" ]; then
+ printf "Cookie: %s" "$_cookies"
+ fi
}
_cyon_login() {
@@ -155,7 +161,12 @@ _cyon_login() {
_get "https://my.cyon.ch/" >/dev/null
- # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
+ # Update cookie after loading main page (only if new cookies are set)
+ _new_cookies="$(_cyon_get_cookie_header)"
+ if [ -n "$_new_cookies" ]; then
+ _H2="$_new_cookies"
+ export _H2
+ fi
# 2FA authentication with OTP?
if [ -n "${CY_OTP_Secret}" ]; then
@@ -184,6 +195,13 @@ _cyon_login() {
fi
_info " success"
+
+ # Update cookie after 2FA (only if new cookies are set)
+ _new_cookies="$(_cyon_get_cookie_header)"
+ if [ -n "$_new_cookies" ]; then
+ _H2="$_new_cookies"
+ export _H2
+ fi
fi
_info ""
@@ -205,7 +223,17 @@ _cyon_change_domain_env() {
domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')"
_debug "Changing domain environment to ${domain_env}"
- gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
+ domain_page_response="$(_get "https://my.cyon.ch/domain/")"
+ _debug domain_page_response "${domain_page_response}"
+
+ # Check if we got an error response (JSON) instead of HTML
+ if printf "%s" "${domain_page_response}" | grep -q '"iserror":true'; then
+ _err " $(printf "%s" "${domain_page_response}" | _cyon_get_response_message)"
+ _err ""
+ return 1
+ fi
+
+ gloo_item_key="$(printf "%s" "${domain_page_response}" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
_debug gloo_item_key "${gloo_item_key}"
domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}"
@@ -215,10 +243,8 @@ _cyon_change_domain_env() {
if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi
- domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)"
-
# Bail if domain environment change fails.
- if [ "${domain_env_success}" != "true" ]; then
+ if [ "$(printf "%s" "${domain_env_response}" | _cyon_get_environment_change_status)" != "true" ]; then
_err " $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)"
_err ""
return 1
@@ -232,7 +258,7 @@ _cyon_add_txt() {
_info " - Adding DNS TXT entry..."
add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async"
- add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}"
+ add_txt_data="name=${fulldomain_idn}.&ttl=900&type=TXT&dnscontent=${txtvalue}"
add_txt_response="$(_post "$add_txt_data" "$add_txt_url")"
_debug add_txt_response "${add_txt_response}"
@@ -241,9 +267,10 @@ _cyon_add_txt() {
add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)"
add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)"
+ add_txt_validation="$(printf "%s" "${add_txt_response}" | _cyon_get_validation_status)"
# Bail if adding TXT entry fails.
- if [ "${add_txt_status}" != "true" ]; then
+ if [ "${add_txt_status}" != "true" ] || [ "${add_txt_validation}" != "true" ]; then
_err " ${add_txt_message}"
_err ""
return 1
@@ -305,13 +332,21 @@ _cyon_get_response_message() {
}
_cyon_get_response_status() {
- _egrep_o '"status":\w*' | cut -d : -f 2
+ _egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2
+}
+
+_cyon_get_validation_status() {
+ _egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2
}
_cyon_get_response_success() {
_egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"'
}
+_cyon_get_environment_change_status() {
+ _egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2
+}
+
_cyon_check_if_2fa_missed() {
# Did we miss the 2FA?
if test "${1#*multi_factor_form}" != "${1}"; then
diff --git a/dnsapi/dns_ddnss.sh b/dnsapi/dns_ddnss.sh
index 118b148b..0ac353d4 100644
--- a/dnsapi/dns_ddnss.sh
+++ b/dnsapi/dns_ddnss.sh
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_ddnss
Options:
DDNSS_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/2230
-Author: RaidenII, helbgd, mod242
+Author: @helbgd, @mod242
'
DDNSS_DNS_API="https://ddnss.de/upd.php"
diff --git a/dnsapi/dns_dnshome.sh b/dnsapi/dns_dnshome.sh
index 59828796..6d583246 100755
--- a/dnsapi/dns_dnshome.sh
+++ b/dnsapi/dns_dnshome.sh
@@ -7,7 +7,7 @@ Options:
DNSHOME_Subdomain Subdomain
DNSHOME_SubdomainPassword Subdomain Password
Issues: github.com/acmesh-official/acme.sh/issues/3819
-Author: dnsHome.de https://github.com/dnsHome-de
+Author: @dnsHome-de
'
# Usage: add subdomain.ddnsdomain.tld "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
diff --git a/dnsapi/dns_duckdns.sh b/dnsapi/dns_duckdns.sh
index 71594873..33d401b0 100755
--- a/dnsapi/dns_duckdns.sh
+++ b/dnsapi/dns_duckdns.sh
@@ -5,7 +5,7 @@ Site: www.DuckDNS.org
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns
Options:
DuckDNS_Token API Token
-Author: RaidenII
+Author: @RaidenII
'
DuckDNS_API="https://www.duckdns.org/update"
diff --git a/dnsapi/dns_dyn.sh b/dnsapi/dns_dyn.sh
index 94201923..9b1a97a2 100644
--- a/dnsapi/dns_dyn.sh
+++ b/dnsapi/dns_dyn.sh
@@ -8,7 +8,7 @@ Options:
DYN_Customer Customer
DYN_Username API Username
DYN_Password Secret
-Author: Gerd Naschenweng
+Author: Gerd Naschenweng <@magicdude4eva>
'
# Dyn Managed DNS API
diff --git a/dnsapi/dns_dynv6.sh b/dnsapi/dns_dynv6.sh
index 76af17f5..3e7ce8d6 100644
--- a/dnsapi/dns_dynv6.sh
+++ b/dnsapi/dns_dynv6.sh
@@ -8,7 +8,7 @@ Options:
OptionsAlt:
KEY Path to SSH private key file. E.g. "/root/.ssh/dynv6"
Issues: github.com/acmesh-official/acme.sh/issues/2702
-Author: StefanAbl
+Author: @StefanAbl
'
dynv6_api="https://dynv6.com/api/v2"
@@ -107,7 +107,7 @@ _get_domain() {
return 0
fi
done
- _err "Either their is no such host on your dnyv6 account or it cannot be accessed with this key"
+ _err "Either there is no such host on your dynv6 account, or it cannot be accessed with this key"
return 1
}
@@ -179,8 +179,8 @@ _dns_dynv6_rm_http() {
fi
}
+#Usage: _get_zone_id $record
#get the zoneid for a specifc record or zone
-#usage: _get_zone_id §record
#where $record is the record to get the id for
#returns _zone_id the id of the zone
_get_zone_id() {
@@ -189,7 +189,6 @@ _get_zone_id() {
_dynv6_rest GET zones
zones="$(echo "$response" | tr '}' '\n' | tr ',' '\n' | grep name | sed 's/\[//g' | tr -d '{' | tr -d '"')"
- #echo $zones
selected=""
for z in $zones; do
@@ -217,9 +216,9 @@ _get_zone_name() {
_zone_name="${_zone_name#name:}"
}
-#usaage _get_record_id $zone_id $record
-# where zone_id is thevalue returned by _get_zone_id
-# and record ist in the form _acme.www for an fqdn of _acme.www.example.com
+#usage _get_record_id $zone_id $record
+# where zone_id is the value returned by _get_zone_id
+# and record is in the form _acme.www for an fqdn of _acme.www.example.com
# returns _record_id
_get_record_id() {
_zone_id="$1"
@@ -234,8 +233,7 @@ _get_record_id() {
_get_record_id_from_response() {
response="$1"
- _record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep id | tr -d '"' | tr -d 'id:')"
- #_record_id="${_record_id#id:}"
+ _record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep '"id":' | tr -d '"' | tr -d 'id:' | tr -d '{')"
if [ -z "$_record_id" ]; then
_err "no such record: $record found in zone $_zone_id"
return 1
diff --git a/dnsapi/dns_easydns.sh b/dnsapi/dns_easydns.sh
index 1c96ac8f..423def2b 100644
--- a/dnsapi/dns_easydns.sh
+++ b/dnsapi/dns_easydns.sh
@@ -7,7 +7,7 @@ Options:
EASYDNS_Token API Token
EASYDNS_Key API Key
Issues: github.com/acmesh-official/acme.sh/issues/2647
-Author: Neilpang, wurzelpanzer
+Author: @Neilpang, wurzelpanzer
'
# API Documentation: https://sandbox.rest.easydns.net:3001/
diff --git a/dnsapi/dns_edgecenter.sh b/dnsapi/dns_edgecenter.sh
new file mode 100644
index 00000000..8f4ad171
--- /dev/null
+++ b/dnsapi/dns_edgecenter.sh
@@ -0,0 +1,163 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_edgecenter_info='EdgeCenter.ru
+Site: EdgeCenter.ru
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter
+Options:
+ EDGECENTER_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/issues/6313
+Author: Konstantin Ruchev
+'
+
+EDGECENTER_API="https://api.edgecenter.ru"
+DOMAIN_TYPE=
+DOMAIN_MASTER=
+
+######## Public functions #####################
+
+#Usage: dns_edgecenter_add _acme-challenge.www.domain.com "TXT_RECORD_VALUE"
+dns_edgecenter_add() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using EdgeCenter DNS API"
+
+ if ! _dns_edgecenter_init_check; then
+ return 1
+ fi
+
+ _debug "Detecting root zone for $fulldomain"
+ if ! _get_root "$fulldomain"; then
+ return 1
+ fi
+
+ subdomain="${fulldomain%."$_zone"}"
+ subdomain=${subdomain%.}
+
+ _debug "Zone: $_zone"
+ _debug "Subdomain: $subdomain"
+ _debug "TXT value: $txtvalue"
+
+ payload='{"resource_records": [ { "content": ["'"$txtvalue"'"] } ], "ttl": 60 }'
+ _dns_edgecenter_http_api_call "post" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$payload"
+
+ if _contains "$response" '"error":"rrset is already exists"'; then
+ _debug "RRSet exists, merging values"
+ _dns_edgecenter_http_api_call "get" "dns/v2/zones/$_zone/$subdomain.$_zone/txt"
+ current="$response"
+ newlist=""
+ for v in $(echo "$current" | sed -n 's/.*"content":\["\([^"]*\)"\].*/\1/p'); do
+ newlist="$newlist {\"content\":[\"$v\"]},"
+ done
+ newlist="$newlist{\"content\":[\"$txtvalue\"]}"
+ putdata="{\"resource_records\":[${newlist}]}
+"
+ _dns_edgecenter_http_api_call "put" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$putdata"
+ _info "Updated existing RRSet with new TXT value."
+ return 0
+ fi
+
+ if _contains "$response" '"exception":'; then
+ _err "Record cannot be added."
+ return 1
+ fi
+
+ _info "TXT record added successfully."
+ return 0
+}
+
+#Usage: dns_edgecenter_rm _acme-challenge.www.domain.com "TXT_RECORD_VALUE"
+dns_edgecenter_rm() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Removing TXT record for $fulldomain"
+
+ if ! _dns_edgecenter_init_check; then
+ return 1
+ fi
+
+ if ! _get_root "$fulldomain"; then
+ return 1
+ fi
+
+ subdomain="${fulldomain%."$_zone"}"
+ subdomain=${subdomain%.}
+
+ _dns_edgecenter_http_api_call "delete" "dns/v2/zones/$_zone/$subdomain.$_zone/txt"
+
+ if [ -z "$response" ]; then
+ _info "TXT record deleted successfully."
+ else
+ _info "TXT record may not have been deleted: $response"
+ fi
+ return 0
+}
+
+#################### Private functions below ##################################
+
+_dns_edgecenter_init_check() {
+ EDGECENTER_API_KEY="${EDGECENTER_API_KEY:-$(_readaccountconf_mutable EDGECENTER_API_KEY)}"
+ if [ -z "$EDGECENTER_API_KEY" ]; then
+ _err "EDGECENTER_API_KEY was not exported."
+ return 1
+ fi
+
+ _saveaccountconf_mutable EDGECENTER_API_KEY "$EDGECENTER_API_KEY"
+ export _H1="Authorization: APIKey $EDGECENTER_API_KEY"
+
+ _dns_edgecenter_http_api_call "get" "dns/v2/clients/me/features"
+ if ! _contains "$response" '"id":'; then
+ _err "Invalid API key."
+ return 1
+ fi
+ return 0
+}
+
+_get_root() {
+ domain="$1"
+ i=1
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-)
+ if [ -z "$h" ]; then
+ return 1
+ fi
+ _dns_edgecenter_http_api_call "get" "dns/v2/zones/$h"
+ if ! _contains "$response" 'zone is not found'; then
+ _zone="$h"
+ return 0
+ fi
+ i=$((i + 1))
+ done
+ return 1
+}
+
+_dns_edgecenter_http_api_call() {
+ mtd="$1"
+ endpoint="$2"
+ data="$3"
+
+ export _H1="Authorization: APIKey $EDGECENTER_API_KEY"
+
+ case "$mtd" in
+ get)
+ response="$(_get "$EDGECENTER_API/$endpoint")"
+ ;;
+ post)
+ response="$(_post "$data" "$EDGECENTER_API/$endpoint")"
+ ;;
+ delete)
+ response="$(_post "" "$EDGECENTER_API/$endpoint" "" "DELETE")"
+ ;;
+ put)
+ response="$(_post "$data" "$EDGECENTER_API/$endpoint" "" "PUT")"
+ ;;
+ *)
+ _err "Unknown HTTP method $mtd"
+ return 1
+ ;;
+ esac
+
+ _debug "HTTP $mtd response: $response"
+ return 0
+}
diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh
new file mode 100755
index 00000000..a485849a
--- /dev/null
+++ b/dnsapi/dns_efficientip.sh
@@ -0,0 +1,139 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_efficientip_info='efficientip.com
+Site: https://efficientip.com/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip
+Options:
+ EfficientIP_Creds HTTP Basic Authentication credentials. E.g. "username:password"
+ EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN.
+ EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional.
+ EfficientIP_View Name of the DNS view hosting the zone. Optional.
+OptionsAlt:
+ EfficientIP_Token_Key Alternative API token key, prefered over basic authentication.
+ EfficientIP_Token_Secret Alternative API token secret, required when using a token key.
+ EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN.
+ EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional.
+ EfficientIP_View Name of the DNS view hosting the zone. Optional.
+Issues: github.com/acmesh-official/acme.sh/issues/6325
+Author: EfficientIP-Labs
+'
+
+dns_efficientip_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _info "Using EfficientIP API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then
+ EfficientIP_Creds=""
+ EfficientIP_Token_Key=""
+ EfficientIP_Token_Secret=""
+ EfficientIP_Server=""
+ _err "You didn't specify any EfficientIP credentials or token or server (EfficientIP_Creds; EfficientIP_Token_Key; EfficientIP_Token_Secret; EfficientIP_Server)."
+ _err "Please set them via EXPORT EfficientIP_Creds=username:password or EXPORT EfficientIP_server=ip/hostname"
+ _err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey"
+ _err "and EXPORT EfficientIP_Token_Secret=yoursecret"
+ _err "then try again."
+ return 1
+ fi
+
+ if [ -z "${EfficientIP_DNS_Name}" ]; then
+ EfficientIP_DNS_Name=""
+ fi
+
+ EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode)
+
+ if [ -z "${EfficientIP_View}" ]; then
+ EfficientIP_View=""
+ fi
+
+ EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode)
+
+ _saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}"
+ _saveaccountconf EfficientIP_Token_Key "${EfficientIP_Token_Key}"
+ _saveaccountconf EfficientIP_Token_Secret "${EfficientIP_Token_Secret}"
+ _saveaccountconf EfficientIP_Server "${EfficientIP_Server}"
+ _saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}"
+ _saveaccountconf EfficientIP_View "${EfficientIP_View}"
+
+ export _H1="Accept-Language:en-US"
+ baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_ttl=300&rr_name=${fulldomain}&rr_value1=${txtvalue}"
+
+ if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}"
+ fi
+
+ if [ "${EfficientIP_ViewEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}"
+ fi
+
+ if [ -z "${EfficientIP_Token_Secret}" ] || [ -z "${EfficientIP_Token_Key}" ]; then
+ EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
+ export _H2="Authorization: Basic ${EfficientIP_CredsEncoded}"
+ else
+ TS=$(date +%s)
+ Sig=$(printf "%b\n$TS\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex)
+ EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig")
+ export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}"
+ export _H3="X-SDS-TS: ${TS}"
+ fi
+
+ result="$(_post "" "${baseurlnObject}" "" "POST")"
+
+ if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then
+ _info "DNS record successfully created"
+ return 0
+ else
+ _err "Error creating DNS record"
+ _err "${result}"
+ return 1
+ fi
+}
+
+dns_efficientip_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _info "Using EfficientIP API"
+ _debug fulldomain "${fulldomain}"
+ _debug txtvalue "${txtvalue}"
+
+ EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode)
+ EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode)
+ EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
+
+ export _H1="Accept-Language:en-US"
+
+ baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_delete?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue"
+ if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}"
+ fi
+
+ if [ "${EfficientIP_ViewEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}"
+ fi
+
+ if [ -z "$EfficientIP_Token_Secret" ] || [ -z "$EfficientIP_Token_Key" ]; then
+ EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
+ export _H2="Authorization: Basic $EfficientIP_CredsEncoded"
+ else
+ TS=$(date +%s)
+ Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex)
+ EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig")
+ export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}"
+ export _H3="X-SDS-TS: $TS"
+ fi
+
+ result="$(_post "" "${baseurlnObject}" "" "DELETE")"
+
+ if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then
+ _info "DNS Record successfully deleted"
+ return 0
+ else
+ _err "Error deleting DNS record"
+ _err "${result}"
+ return 1
+ fi
+}
diff --git a/dnsapi/dns_exoscale.sh b/dnsapi/dns_exoscale.sh
old mode 100755
new mode 100644
index 6898ce38..ddd526a4
--- a/dnsapi/dns_exoscale.sh
+++ b/dnsapi/dns_exoscale.sh
@@ -8,9 +8,9 @@ Options:
EXOSCALE_SECRET_KEY API Secret key
'
-EXOSCALE_API=https://api.exoscale.com/dns/v1
+EXOSCALE_API="https://api-ch-gva-2.exoscale.com/v2"
-######## Public functions #####################
+######## Public functions ########
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
@@ -18,159 +18,197 @@ dns_exoscale_add() {
fulldomain=$1
txtvalue=$2
- if ! _checkAuth; then
+ _debug "Using Exoscale DNS v2 API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _check_auth; then
return 1
fi
- _debug "First detect the root zone"
- if ! _get_root "$fulldomain"; then
- _err "invalid domain"
+ root_domain_id=$(_get_root_domain_id "$fulldomain")
+ if [ -z "$root_domain_id" ]; then
+ _err "Unable to determine root domain ID for $fulldomain"
return 1
fi
+ _debug root_domain_id "$root_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
+ # Always get the subdomain part first
+ sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id")
+ _debug sub_domain "$sub_domain"
- _info "Adding record"
- if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then
- if _contains "$response" "$txtvalue"; then
- _info "Added, OK"
- return 0
- fi
+ # Build the record name properly
+ if [ -z "$sub_domain" ]; then
+ record_name="_acme-challenge"
+ else
+ record_name="_acme-challenge.$sub_domain"
fi
- _err "Add txt record error."
- return 1
+ payload=$(printf '{"name":"%s","type":"TXT","content":"%s","ttl":120}' "$record_name" "$txtvalue")
+ _debug payload "$payload"
+
+ response=$(_exoscale_rest POST "/dns-domain/${root_domain_id}/record" "$payload")
+ if _contains "$response" "\"id\""; then
+ _info "TXT record added successfully."
+ return 0
+ else
+ _err "Error adding TXT record: $response"
+ return 1
+ fi
}
-# Usage: fulldomain txtvalue
-# Used to remove the txt record after validation
dns_exoscale_rm() {
fulldomain=$1
- txtvalue=$2
- if ! _checkAuth; then
+ _debug "Using Exoscale DNS v2 API for removal"
+ _debug fulldomain "$fulldomain"
+
+ if ! _check_auth; then
return 1
fi
- _debug "First detect the root zone"
- if ! _get_root "$fulldomain"; then
- _err "invalid domain"
+ root_domain_id=$(_get_root_domain_id "$fulldomain")
+ if [ -z "$root_domain_id" ]; then
+ _err "Unable to determine root domain ID for $fulldomain"
return 1
fi
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _debug "Getting txt records"
- _exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token"
- if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then
- _record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
+ record_name="_acme-challenge"
+ sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id")
+ if [ -n "$sub_domain" ]; then
+ record_name="_acme-challenge.$sub_domain"
fi
- if [ -z "$_record_id" ]; then
- _err "Can not get record id to remove."
+ record_id=$(_find_record_id "$root_domain_id" "$record_name")
+ if [ -z "$record_id" ]; then
+ _err "TXT record not found for deletion."
return 1
fi
- _debug "Deleting record $_record_id"
-
- if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then
- _err "Delete record error."
+ response=$(_exoscale_rest DELETE "/dns-domain/$root_domain_id/record/$record_id")
+ if _contains "$response" "\"state\":\"success\""; then
+ _info "TXT record deleted successfully."
+ return 0
+ else
+ _err "Error deleting TXT record: $response"
return 1
fi
-
- return 0
}
-#################### Private functions below ##################################
+######## Private helpers ########
-_checkAuth() {
+_check_auth() {
EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}"
EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}"
-
if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then
- EXOSCALE_API_KEY=""
- EXOSCALE_SECRET_KEY=""
- _err "You don't specify Exoscale application key and application secret yet."
- _err "Please create you key and try again."
+ _err "EXOSCALE_API_KEY and EXOSCALE_SECRET_KEY must be set."
return 1
fi
-
_saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY"
_saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY"
-
return 0
}
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
-# _domain_token=sdjkglgdfewsdfg
-_get_root() {
-
- if ! _exoscale_rest GET "domains"; then
- return 1
- fi
-
+_get_root_domain_id() {
domain=$1
- i=2
- p=1
+ i=1
while true; do
- h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
- _debug h "$h"
- if [ -z "$h" ]; then
- #not valid
- return 1
- fi
-
- if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
- _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
- _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
- if [ "$_domain_token" ] && [ "$_domain_id" ]; then
- _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain=$h
- return 0
+ candidate=$(printf "%s" "$domain" | cut -d . -f "${i}-100")
+ [ -z "$candidate" ] && return 1
+ _debug "Trying root domain candidate: $candidate"
+ domains=$(_exoscale_rest GET "/dns-domain")
+ # Extract from dns-domains array
+ result=$(echo "$domains" | _egrep_o '"dns-domains":\[.*\]' | _egrep_o '\{"id":"[^"]*","created-at":"[^"]*","unicode-name":"[^"]*"\}' | while read -r item; do
+ name=$(echo "$item" | _egrep_o '"unicode-name":"[^"]*"' | cut -d'"' -f4)
+ id=$(echo "$item" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4)
+ if [ "$name" = "$candidate" ]; then
+ echo "$id"
+ break
fi
- return 1
+ done)
+ if [ -n "$result" ]; then
+ echo "$result"
+ return 0
fi
- p=$i
i=$(_math "$i" + 1)
done
- return 1
}
-# returns response
+_get_sub_domain() {
+ fulldomain=$1
+ root_id=$2
+ root_info=$(_exoscale_rest GET "/dns-domain/$root_id")
+ _debug root_info "$root_info"
+ root_name=$(echo "$root_info" | _egrep_o "\"unicode-name\":\"[^\"]*\"" | cut -d\" -f4)
+ sub=${fulldomain%%."$root_name"}
+
+ if [ "$sub" = "_acme-challenge" ]; then
+ echo ""
+ else
+ # Remove _acme-challenge. prefix to get the actual subdomain
+ echo "${sub#_acme-challenge.}"
+ fi
+}
+
+_find_record_id() {
+ root_id=$1
+ name=$2
+ records=$(_exoscale_rest GET "/dns-domain/$root_id/record")
+
+ # Convert search name to lowercase for case-insensitive matching
+ name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
+
+ echo "$records" | _egrep_o '\{[^}]*"name":"[^"]*"[^}]*\}' | while read -r record; do
+ record_name=$(echo "$record" | _egrep_o '"name":"[^"]*"' | cut -d'"' -f4)
+ record_name_lower=$(echo "$record_name" | tr '[:upper:]' '[:lower:]')
+ if [ "$record_name_lower" = "$name_lower" ]; then
+ echo "$record" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d'"' -f4
+ break
+ fi
+ done
+}
+
+_exoscale_sign() {
+ k=$1
+ shift
+ hex_key=$(printf %b "$k" | _hex_dump | tr -d ' ')
+ printf %s "$@" | _hmac sha256 "$hex_key"
+}
+
_exoscale_rest() {
method=$1
- path="$2"
- data="$3"
- token="$4"
- request_url="$EXOSCALE_API/$path"
- _debug "$path"
+ path=$2
+ data=$3
- export _H1="Accept: application/json"
+ url="${EXOSCALE_API}${path}"
+ expiration=$(_math "$(date +%s)" + 300) # 5m from now
- if [ "$token" ]; then
- export _H2="X-DNS-Domain-Token: $token"
- else
- export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY"
- fi
+ # Build the message with the actual body or empty line
+ message=$(printf "%s %s\n%s\n\n\n%s" "$method" "/v2$path" "$data" "$expiration")
+ signature=$(_exoscale_sign "$EXOSCALE_SECRET_KEY" "$message" | _base64)
+ auth="EXO2-HMAC-SHA256 credential=${EXOSCALE_API_KEY},expires=${expiration},signature=${signature}"
+
+ _debug "API request: $method $url"
+ _debug "Signed message: [$message]"
+ _debug "Authorization header: [$auth]"
+
+ export _H1="Accept: application/json"
+ export _H2="Authorization: ${auth}"
if [ "$data" ] || [ "$method" = "DELETE" ]; then
export _H3="Content-Type: application/json"
_debug data "$data"
- response="$(_post "$data" "$request_url" "" "$method")"
+ response="$(_post "$data" "$url" "" "$method")"
else
- response="$(_get "$request_url" "" "" "$method")"
+ response="$(_get "$url" "" "" "$method")"
fi
- if [ "$?" != "0" ]; then
- _err "error $request_url"
+ # shellcheck disable=SC2181
+ if [ "$?" -ne 0 ]; then
+ _err "error $url"
return 1
fi
_debug2 response "$response"
+ echo "$response"
return 0
}
diff --git a/dnsapi/dns_fornex.sh b/dnsapi/dns_fornex.sh
index 91e5491b..dcaa2297 100644
--- a/dnsapi/dns_fornex.sh
+++ b/dnsapi/dns_fornex.sh
@@ -95,7 +95,7 @@ _get_root() {
return 1
fi
- if ! _rest GET "dns/domain/"; then
+ if ! _rest GET "dns/domain/?q=$h"; then
return 1
fi
diff --git a/dnsapi/dns_freedns.sh b/dnsapi/dns_freedns.sh
index 114f30e0..13d9f68b 100755
--- a/dnsapi/dns_freedns.sh
+++ b/dnsapi/dns_freedns.sh
@@ -7,7 +7,7 @@ Options:
FREEDNS_User Username
FREEDNS_Password Password
Issues: github.com/acmesh-official/acme.sh/issues/2305
-Author: David Kerr
+Author: David Kerr <@dkerr64>
'
######## Public functions #####################
diff --git a/dnsapi/dns_freemyip.sh b/dnsapi/dns_freemyip.sh
new file mode 100644
index 00000000..d598a657
--- /dev/null
+++ b/dnsapi/dns_freemyip.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_freemyip_info='FreeMyIP.com
+Site: FreeMyIP.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
+Options:
+ FREEMYIP_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/6247
+Author: Recolic Keghart , @Giova96
+'
+
+FREEMYIP_DNS_API="https://freemyip.com/update?"
+
+################ Public functions ################
+
+#Usage: dns_freemyip_add fulldomain txtvalue
+dns_freemyip_add() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Add TXT record $txtvalue for $fulldomain using freemyip.com api"
+
+ FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}"
+ if [ -z "$FREEMYIP_Token" ]; then
+ FREEMYIP_Token=""
+ _err "You don't specify FREEMYIP_Token yet."
+ _err "Please specify your token and try again."
+ return 1
+ fi
+
+ #save the credentials to the account conf file.
+ _saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token"
+
+ if _is_root_domain_published "$fulldomain"; then
+ _err "freemyip API don't allow you to set multiple TXT record for the same subdomain!"
+ _err "You must apply certificate for only one domain at a time!"
+ _err "===="
+ _err "For example, aaa.yourdomain.freemyip.com and bbb.yourdomain.freemyip.com and yourdomain.freemyip.com ALWAYS share the same TXT record. They will overwrite each other if you apply multiple domain at the same time."
+ _debug "If you are testing this workflow in github pipeline or acmetest, please set TEST_DNS_NO_SUBDOMAIN=1 and TEST_DNS_NO_WILDCARD=1"
+ return 1
+ fi
+
+ # txtvalue must be url-encoded. But it's not necessary for acme txt value.
+ _freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=$txtvalue" 2>&1
+ return $?
+}
+
+#Usage: dns_freemyip_rm fulldomain txtvalue
+dns_freemyip_rm() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Delete TXT record $txtvalue for $fulldomain using freemyip.com api"
+
+ FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}"
+ if [ -z "$FREEMYIP_Token" ]; then
+ FREEMYIP_Token=""
+ _err "You don't specify FREEMYIP_Token yet."
+ _err "Please specify your token and try again."
+ return 1
+ fi
+
+ #save the credentials to the account conf file.
+ _saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token"
+
+ # Leave the TXT record as empty or "null" to delete the record.
+ _freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=" 2>&1
+ return $?
+}
+
+################ Private functions below ################
+_get_root() {
+ _fmi_d="$1"
+
+ echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
+}
+
+# There is random failure while calling freemyip API too fast. This function automatically retry until success.
+_freemyip_get_until_ok() {
+ _fmi_url="$1"
+ for i in $(seq 1 8); do
+ _debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
+ _get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
+ _sleep 1 # DO NOT send the request too fast
+ done
+ _err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
+ return 1
+}
+
+# Verify in public dns if domain is already there.
+_is_root_domain_published() {
+ _fmi_d="$1"
+ _webroot="$(_get_root "$_fmi_d")"
+
+ _info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
+ for i in $(seq 1 3); do
+ _debug "'$_webroot' ns lookup, retry $i/3..."
+ if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
+ _debug "'$_webroot' already has a TXT record published!"
+ return 0
+ fi
+ _sleep 10 # Give it some time to propagate the TXT record
+ done
+ return 1
+}
diff --git a/dnsapi/dns_gandi_livedns.sh b/dnsapi/dns_gandi_livedns.sh
index 0516fee9..aaef07bf 100644
--- a/dnsapi/dns_gandi_livedns.sh
+++ b/dnsapi/dns_gandi_livedns.sh
@@ -23,6 +23,8 @@ dns_gandi_livedns_add() {
fulldomain=$1
txtvalue=$2
+ GANDI_LIVEDNS_KEY="${GANDI_LIVEDNS_KEY:-$(_readaccountconf_mutable GANDI_LIVEDNS_KEY)}"
+ GANDI_LIVEDNS_TOKEN="${GANDI_LIVEDNS_TOKEN:-$(_readaccountconf_mutable GANDI_LIVEDNS_TOKEN)}"
if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then
_err "No Token or API key (deprecated) specified for Gandi LiveDNS."
_err "Create your token or key and export it as GANDI_LIVEDNS_KEY or GANDI_LIVEDNS_TOKEN respectively"
@@ -31,11 +33,11 @@ dns_gandi_livedns_add() {
# Keep only one secret in configuration
if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then
- _saveaccountconf GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN"
- _clearaccountconf GANDI_LIVEDNS_KEY
+ _saveaccountconf_mutable GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN"
+ _clearaccountconf_mutable GANDI_LIVEDNS_KEY
elif [ -n "$GANDI_LIVEDNS_KEY" ]; then
- _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
- _clearaccountconf GANDI_LIVEDNS_TOKEN
+ _saveaccountconf_mutable GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
+ _clearaccountconf_mutable GANDI_LIVEDNS_TOKEN
fi
_debug "First detect the root zone"
diff --git a/dnsapi/dns_he_ddns.sh b/dnsapi/dns_he_ddns.sh
new file mode 100644
index 00000000..1fe9a7fd
--- /dev/null
+++ b/dnsapi/dns_he_ddns.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_he_ddns_info='Hurricane Electric HE.net DDNS
+Site: dns.he.net
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns
+Options:
+ HE_DDNS_KEY The DDNS key
+Issues: https://github.com/acmesh-official/acme.sh/issues/5238
+Author: Markku Leiniö
+'
+
+HE_DDNS_URL="https://dyn.dns.he.net/nic/update"
+
+######## Public functions #####################
+
+#Usage: dns_he_ddns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_he_ddns_add() {
+ fulldomain=$1
+ txtvalue=$2
+ HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}"
+ if [ -z "$HE_DDNS_KEY" ]; then
+ HE_DDNS_KEY=""
+ _err "You didn't specify a DDNS key for accessing the TXT record in HE API."
+ return 1
+ fi
+ #Save the DDNS key to the account conf file.
+ _saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY"
+
+ _info "Using Hurricane Electric DDNS API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")"
+ _info "Response: $response"
+ _contains "$response" "good" && return 0 || return 1
+}
+
+# dns_he_ddns_rm() is not doing anything because the API call always updates the
+# contents of the existing record (that the API key gives access to).
+
+dns_he_ddns_rm() {
+ fulldomain=$1
+ _debug "Delete TXT record called for '${fulldomain}', not doing anything."
+ return 0
+}
diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh
old mode 100644
new mode 100755
index 5a9cf2d9..f1bddc61
--- a/dnsapi/dns_hetzner.sh
+++ b/dnsapi/dns_hetzner.sh
@@ -212,7 +212,7 @@ _get_root() {
_response_has_error() {
unset _response_error
- err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')"
+ err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
if [ -n "$err_part" ]; then
err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh
new file mode 100644
index 00000000..4a7eea90
--- /dev/null
+++ b/dnsapi/dns_hetznercloud.sh
@@ -0,0 +1,593 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_hetznercloud_info='Hetzner Cloud DNS
+Site: Hetzner.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hetznercloud
+Options:
+ HETZNER_TOKEN API token for the Hetzner Cloud DNS API
+Optional:
+ HETZNER_TTL Custom TTL for new TXT rrsets (default 120)
+ HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1)
+ HETZNER_MAX_ATTEMPTS Number of 1s polls to wait for async actions (default 120)
+Issues: github.com/acmesh-official/acme.sh/issues
+'
+
+HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1"
+HETZNERCLOUD_TTL_DEFAULT=120
+HETZNER_MAX_ATTEMPTS_DEFAULT=120
+
+######## Public functions #####################
+
+dns_hetznercloud_add() {
+ fulldomain="$(_idn "${1}")"
+ txtvalue="${2}"
+
+ _info "Using Hetzner Cloud DNS API to add record"
+
+ if ! _hetznercloud_init; then
+ return 1
+ fi
+
+ if ! _hetznercloud_prepare_zone "${fulldomain}"; then
+ _err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
+ return 1
+ fi
+
+ if ! _hetznercloud_get_rrset; then
+ return 1
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" = "200" ]; then
+ if _hetznercloud_rrset_contains_value "${txtvalue}"; then
+ _info "TXT record already present; nothing to do."
+ return 0
+ fi
+ elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
+ _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ add_payload="$(_hetznercloud_build_add_payload "${txtvalue}")"
+ if [ -z "${add_payload}" ]; then
+ _err "Failed to build request payload."
+ return 1
+ fi
+
+ if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_add}" "${add_payload}"; then
+ return 1
+ fi
+
+ case "${_hetznercloud_last_http_code}" in
+ 200 | 201 | 202 | 204)
+ if ! _hetznercloud_handle_action_response "TXT record add"; then
+ return 1
+ fi
+ _info "Hetzner Cloud TXT record added."
+ return 0
+ ;;
+ 401 | 403)
+ _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
+ _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ 409 | 422)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the add_records request" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ *)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS add_records request failed" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ esac
+}
+
+dns_hetznercloud_rm() {
+ fulldomain="$(_idn "${1}")"
+ txtvalue="${2}"
+
+ _info "Using Hetzner Cloud DNS API to remove record"
+
+ if ! _hetznercloud_init; then
+ return 1
+ fi
+
+ if ! _hetznercloud_prepare_zone "${fulldomain}"; then
+ _err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
+ return 1
+ fi
+
+ if ! _hetznercloud_get_rrset; then
+ return 1
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" = "404" ]; then
+ _info "TXT rrset does not exist; nothing to remove."
+ return 0
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" != "200" ]; then
+ _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ if _hetznercloud_rrset_contains_value "${txtvalue}"; then
+ remove_payload="$(_hetznercloud_build_remove_payload "${txtvalue}")"
+ if [ -z "${remove_payload}" ]; then
+ _err "Failed to build remove_records payload."
+ return 1
+ fi
+ if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_remove}" "${remove_payload}"; then
+ return 1
+ fi
+ case "${_hetznercloud_last_http_code}" in
+ 200 | 201 | 202 | 204)
+ if ! _hetznercloud_handle_action_response "TXT record remove"; then
+ return 1
+ fi
+ _info "Hetzner Cloud TXT record removed."
+ return 0
+ ;;
+ 401 | 403)
+ _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
+ _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ 404)
+ _info "TXT rrset already absent after remove action."
+ return 0
+ ;;
+ 409 | 422)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the remove_records request" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ *)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS remove_records request failed" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ esac
+ else
+ _info "TXT value not present; nothing to remove."
+ return 0
+ fi
+}
+
+#################### Private functions ##################################
+
+_hetznercloud_init() {
+ HETZNER_TOKEN="${HETZNER_TOKEN:-$(_readaccountconf_mutable HETZNER_TOKEN)}"
+ if [ -z "${HETZNER_TOKEN}" ]; then
+ _err "The environment variable HETZNER_TOKEN must be set for the Hetzner Cloud DNS API."
+ return 1
+ fi
+ HETZNER_TOKEN=$(echo "${HETZNER_TOKEN}" | tr -d '"')
+ _saveaccountconf_mutable HETZNER_TOKEN "${HETZNER_TOKEN}"
+
+ HETZNER_API="${HETZNER_API:-$(_readaccountconf_mutable HETZNER_API)}"
+ if [ -z "${HETZNER_API}" ]; then
+ HETZNER_API="${HETZNERCLOUD_API_DEFAULT}"
+ fi
+ _saveaccountconf_mutable HETZNER_API "${HETZNER_API}"
+
+ HETZNER_TTL="${HETZNER_TTL:-$(_readaccountconf_mutable HETZNER_TTL)}"
+ if [ -z "${HETZNER_TTL}" ]; then
+ HETZNER_TTL="${HETZNERCLOUD_TTL_DEFAULT}"
+ fi
+ ttl_check=$(printf "%s" "${HETZNER_TTL}" | tr -d '0-9')
+ if [ -n "${ttl_check}" ]; then
+ _err "HETZNER_TTL must be an integer value."
+ return 1
+ fi
+ _saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}"
+
+ HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS:-$(_readaccountconf_mutable HETZNER_MAX_ATTEMPTS)}"
+ if [ -z "${HETZNER_MAX_ATTEMPTS}" ]; then
+ HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS_DEFAULT}"
+ fi
+ attempts_check=$(printf "%s" "${HETZNER_MAX_ATTEMPTS}" | tr -d '0-9')
+ if [ -n "${attempts_check}" ]; then
+ _err "HETZNER_MAX_ATTEMPTS must be an integer value."
+ return 1
+ fi
+ _saveaccountconf_mutable HETZNER_MAX_ATTEMPTS "${HETZNER_MAX_ATTEMPTS}"
+
+ return 0
+}
+
+_hetznercloud_prepare_zone() {
+ _hetznercloud_zone_id=""
+ _hetznercloud_zone_name=""
+ _hetznercloud_zone_name_lc=""
+ _hetznercloud_rr_name=""
+ _hetznercloud_rrset_path=""
+ _hetznercloud_rrset_action_add=""
+ _hetznercloud_rrset_action_remove=""
+ fulldomain_lc=$(printf "%s" "${1}" | sed 's/\.$//' | _lower_case)
+
+ i=2
+ p=1
+ while true; do
+ candidate=$(printf "%s" "${fulldomain_lc}" | cut -d . -f "${i}"-100)
+ if [ -z "${candidate}" ]; then
+ return 1
+ fi
+
+ if _hetznercloud_get_zone_by_candidate "${candidate}"; then
+ zone_name_lc="${_hetznercloud_zone_name_lc}"
+ if [ "${fulldomain_lc}" = "${zone_name_lc}" ]; then
+ _hetznercloud_rr_name="@"
+ else
+ suffix=".${zone_name_lc}"
+ if _endswith "${fulldomain_lc}" "${suffix}"; then
+ _hetznercloud_rr_name="${fulldomain_lc%"${suffix}"}"
+ else
+ _hetznercloud_rr_name="${fulldomain_lc}"
+ fi
+ fi
+ _hetznercloud_rrset_path=$(printf "%s" "${_hetznercloud_rr_name}" | _url_encode)
+ _hetznercloud_rrset_action_add="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/add_records"
+ _hetznercloud_rrset_action_remove="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/remove_records"
+ return 0
+ fi
+ p=${i}
+ i=$(_math "${i}" + 1)
+ done
+}
+
+_hetznercloud_get_zone_by_candidate() {
+ candidate="${1}"
+ zone_key=$(printf "%s" "${candidate}" | sed 's/[^A-Za-z0-9]/_/g')
+ zone_conf_key="HETZNERCLOUD_ZONE_ID_for_${zone_key}"
+
+ cached_zone_id=$(_readdomainconf "${zone_conf_key}")
+ if [ -n "${cached_zone_id}" ]; then
+ if _hetznercloud_api GET "/zones/${cached_zone_id}"; then
+ if [ "${_hetznercloud_last_http_code}" = "200" ]; then
+ zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
+ if _hetznercloud_parse_zone_fields "${zone_data}"; then
+ zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
+ if [ "${zone_name_lc}" = "${candidate}" ]; then
+ return 0
+ fi
+ fi
+ elif [ "${_hetznercloud_last_http_code}" = "404" ]; then
+ _cleardomainconf "${zone_conf_key}"
+ fi
+ else
+ return 1
+ fi
+ fi
+
+ if _hetznercloud_api GET "/zones/${candidate}"; then
+ if [ "${_hetznercloud_last_http_code}" = "200" ]; then
+ zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
+ if _hetznercloud_parse_zone_fields "${zone_data}"; then
+ zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
+ if [ "${zone_name_lc}" = "${candidate}" ]; then
+ _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
+ return 0
+ fi
+ fi
+ elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
+ _hetznercloud_log_http_error "Hetzner Cloud zone lookup failed" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+ else
+ return 1
+ fi
+
+ encoded_candidate=$(printf "%s" "${candidate}" | _url_encode)
+ if ! _hetznercloud_api GET "/zones?name=${encoded_candidate}"; then
+ return 1
+ fi
+ if [ "${_hetznercloud_last_http_code}" != "200" ]; then
+ if [ "${_hetznercloud_last_http_code}" = "404" ]; then
+ return 1
+ fi
+ _hetznercloud_log_http_error "Hetzner Cloud zone search failed" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ zone_data=$(_hetznercloud_extract_zone_from_list "${response}" "${candidate}")
+ if [ -z "${zone_data}" ]; then
+ return 1
+ fi
+ if ! _hetznercloud_parse_zone_fields "${zone_data}"; then
+ return 1
+ fi
+ _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
+ return 0
+}
+
+_hetznercloud_parse_zone_fields() {
+ zone_json="${1}"
+ if [ -z "${zone_json}" ]; then
+ return 1
+ fi
+ normalized=$(printf "%s" "${zone_json}" | _normalizeJson)
+ zone_id=$(printf "%s" "${normalized}" | _egrep_o '"id":[^,}]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')
+ zone_name=$(printf "%s" "${normalized}" | _egrep_o '"name":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then
+ return 1
+ fi
+ zone_name_trimmed=$(printf "%s" "${zone_name}" | sed 's/\.$//')
+ if zone_name_ascii=$(_idn "${zone_name_trimmed}"); then
+ zone_name="${zone_name_ascii}"
+ else
+ zone_name="${zone_name_trimmed}"
+ fi
+ _hetznercloud_zone_id="${zone_id}"
+ _hetznercloud_zone_name="${zone_name}"
+ _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case)
+ return 0
+}
+
+_hetznercloud_extract_zone_from_list() {
+ list_response=$(printf "%s" "${1}" | _normalizeJson)
+ candidate="${2}"
+ escaped_candidate=$(_hetznercloud_escape_regex "${candidate}")
+ printf "%s" "${list_response}" | _egrep_o "{[^{}]*\"name\":\"${escaped_candidate}\"[^{}]*}" | _head_n 1
+}
+
+_hetznercloud_escape_regex() {
+ printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/\./\\./g' | sed 's/-/\\-/g'
+}
+
+_hetznercloud_get_rrset() {
+ if [ -z "${_hetznercloud_zone_id}" ] || [ -z "${_hetznercloud_rrset_path}" ]; then
+ return 1
+ fi
+ if ! _hetznercloud_api GET "/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT"; then
+ return 1
+ fi
+ return 0
+}
+
+_hetznercloud_rrset_contains_value() {
+ wanted_value="${1}"
+ normalized=$(printf "%s" "${response}" | _normalizeJson)
+ escaped_value=$(_hetznercloud_escape_value "${wanted_value}")
+ search_pattern="\"value\":\"\\\\\"${escaped_value}\\\\\"\""
+ if _contains "${normalized}" "${search_pattern}"; then
+ return 0
+ fi
+ return 1
+}
+
+_hetznercloud_build_add_payload() {
+ value="${1}"
+ escaped_value=$(_hetznercloud_escape_value "${value}")
+ printf '{"ttl":%s,"records":[{"value":"\\"%s\\""}]}' "${HETZNER_TTL}" "${escaped_value}"
+}
+
+_hetznercloud_build_remove_payload() {
+ value="${1}"
+ escaped_value=$(_hetznercloud_escape_value "${value}")
+ printf '{"records":[{"value":"\\"%s\\""}]}' "${escaped_value}"
+}
+
+_hetznercloud_escape_value() {
+ printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g'
+}
+
+_hetznercloud_error_message() {
+ if [ -z "${response}" ]; then
+ return 1
+ fi
+ message=$(printf "%s" "${response}" | _normalizeJson | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${message}" ]; then
+ printf "%s" "${message}"
+ return 0
+ fi
+ return 1
+}
+
+_hetznercloud_log_http_error() {
+ context="${1}"
+ code="${2}"
+ message="$(_hetznercloud_error_message)"
+ if [ -n "${context}" ]; then
+ if [ -n "${message}" ]; then
+ _err "${context} (HTTP ${code}): ${message}"
+ else
+ _err "${context} (HTTP ${code})"
+ fi
+ else
+ if [ -n "${message}" ]; then
+ _err "Hetzner Cloud DNS API error (HTTP ${code}): ${message}"
+ else
+ _err "Hetzner Cloud DNS API error (HTTP ${code})"
+ fi
+ fi
+}
+
+_hetznercloud_api() {
+ method="${1}"
+ ep="${2}"
+ data="${3}"
+ retried="${4}"
+
+ if [ -z "${method}" ]; then
+ method="GET"
+ fi
+
+ if ! _startswith "${ep}" "/"; then
+ ep="/${ep}"
+ fi
+ url="${HETZNER_API}${ep}"
+
+ export _H1="Authorization: Bearer ${HETZNER_TOKEN}"
+ export _H2="Accept: application/json"
+ export _H3=""
+ export _H4=""
+ export _H5=""
+
+ : >"${HTTP_HEADER}"
+
+ if [ "${method}" = "GET" ]; then
+ response="$(_get "${url}")"
+ else
+ if [ -z "${data}" ]; then
+ data="{}"
+ fi
+ response="$(_post "${data}" "${url}" "" "${method}" "application/json")"
+ fi
+ ret="${?}"
+
+ _hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
+
+ if [ "${ret}" != "0" ]; then
+ return 1
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then
+ retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r')
+ if [ -z "${retry_after}" ]; then
+ retry_after=1
+ fi
+ _info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds."
+ _sleep "${retry_after}"
+ if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then
+ return 1
+ fi
+ return 0
+ fi
+
+ return 0
+}
+
+_hetznercloud_handle_action_response() {
+ context="${1}"
+ if [ -z "${response}" ]; then
+ return 0
+ fi
+
+ normalized=$(printf "%s" "${response}" | _normalizeJson)
+
+ failed_message=""
+ if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then
+ if [ -n "${failed_message}" ]; then
+ _err "Hetzner Cloud DNS ${context} failed: ${failed_message}"
+ else
+ _err "Hetzner Cloud DNS ${context} failed."
+ fi
+ return 1
+ fi
+
+ action_ids=""
+ if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then
+ for action_id in ${action_ids}; do
+ if [ -z "${action_id}" ]; then
+ continue
+ fi
+ if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then
+ return 1
+ fi
+ done
+ fi
+
+ return 0
+}
+
+_hetznercloud_extract_failed_action_message() {
+ normalized="${1}"
+ failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]')
+ if [ -z "${failed_section}" ]; then
+ return 1
+ fi
+ if _contains "${failed_section}" '"failed_actions":[]'; then
+ return 1
+ fi
+ message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${message}" ]; then
+ printf "%s" "${message}"
+ else
+ printf "%s" "${failed_section}"
+ fi
+ return 0
+}
+
+_hetznercloud_extract_action_ids() {
+ normalized="${1}"
+ actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]')
+ if [ -z "${actions_section}" ]; then
+ return 1
+ fi
+ action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ')
+ action_ids=$(printf "%s" "${action_ids}" | tr -s ' ')
+ action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//')
+ if [ -z "${action_ids}" ]; then
+ return 1
+ fi
+ printf "%s" "${action_ids}"
+ return 0
+}
+
+_hetznercloud_wait_for_action() {
+ action_id="${1}"
+ context="${2}"
+ attempts="0"
+
+ while true; do
+ if ! _hetznercloud_api GET "/actions/${action_id}"; then
+ return 1
+ fi
+ if [ "${_hetznercloud_last_http_code}" != "200" ]; then
+ _hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ normalized=$(printf "%s" "${response}" | _normalizeJson)
+ action_status=$(_hetznercloud_action_status_from_normalized "${normalized}")
+
+ if [ -z "${action_status}" ]; then
+ _err "Hetzner Cloud DNS ${context} action ${action_id} returned no status."
+ return 1
+ fi
+
+ if [ "${action_status}" = "success" ]; then
+ return 0
+ fi
+
+ if [ "${action_status}" = "error" ]; then
+ if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then
+ _err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}"
+ else
+ _err "Hetzner Cloud DNS ${context} action ${action_id} failed."
+ fi
+ return 1
+ fi
+
+ attempts=$(_math "${attempts}" + 1)
+ if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then
+ _err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts."
+ return 1
+ fi
+
+ _sleep 1
+ done
+}
+
+_hetznercloud_action_status_from_normalized() {
+ normalized="${1}"
+ status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ printf "%s" "${status}"
+}
+
+_hetznercloud_action_error_from_normalized() {
+ normalized="${1}"
+ error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}')
+ if [ -z "${error_section}" ]; then
+ return 1
+ fi
+ message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${message}" ]; then
+ printf "%s" "${message}"
+ return 0
+ fi
+ code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${code}" ]; then
+ printf "%s" "${code}"
+ return 0
+ fi
+ return 1
+}
diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh
new file mode 100644
index 00000000..b3211069
--- /dev/null
+++ b/dnsapi/dns_hostup.sh
@@ -0,0 +1,501 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034,SC2154
+
+dns_hostup_info='HostUp DNS
+Site: hostup.se
+Docs: https://developer.hostup.se/
+Options:
+ HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes.
+ HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
+ HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
+ HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection).
+Author: HostUp (https://cloud.hostup.se/contact/en)
+'
+
+HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api"
+HOSTUP_DEFAULT_TTL=60
+
+# Public: add TXT record
+# Usage: dns_hostup_add _acme-challenge.example.com "txt-value"
+dns_hostup_add() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using HostUp DNS API"
+
+ if ! _hostup_init; then
+ return 1
+ fi
+
+ if ! _hostup_detect_zone "$fulldomain"; then
+ _err "Unable to determine HostUp zone for $fulldomain"
+ return 1
+ fi
+
+ record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")"
+ record_name="$(_hostup_sanitize_name "$record_name")"
+ record_value="$(_hostup_json_escape "$txtvalue")"
+
+ ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
+
+ _debug "zone_id" "$HOSTUP_ZONE_ID"
+ _debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
+ _debug "record_name" "$record_name"
+ _debug "ttl" "$ttl"
+
+ request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}"
+
+ if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
+ return 1
+ fi
+
+ if ! _contains "$_hostup_response" '"success":true'; then
+ _err "HostUp DNS API: failed to create TXT record for $fulldomain"
+ _debug2 "_hostup_response" "$_hostup_response"
+ return 1
+ fi
+
+ record_id="$(_hostup_extract_record_id "$_hostup_response")"
+ if [ -n "$record_id" ]; then
+ _hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id"
+ _debug "hostup_saved_record_id" "$record_id"
+ fi
+
+ _info "Added TXT record for $fulldomain"
+ return 0
+}
+
+# Public: remove TXT record
+# Usage: dns_hostup_rm _acme-challenge.example.com "txt-value"
+dns_hostup_rm() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using HostUp DNS API"
+
+ if ! _hostup_init; then
+ return 1
+ fi
+
+ if ! _hostup_detect_zone "$fulldomain"; then
+ _err "Unable to determine HostUp zone for $fulldomain"
+ return 1
+ fi
+
+ record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
+ record_value="$txtvalue"
+
+ record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")"
+ if [ -n "$record_id_cached" ]; then
+ _debug "hostup_record_id_cached" "$record_id_cached"
+ if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then
+ _info "Deleted TXT record $record_id_cached"
+ _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
+ HOSTUP_ZONE_ID=""
+ return 0
+ fi
+ fi
+
+ if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then
+ _info "TXT record not found for $record_name_fqdn. Skipping removal."
+ _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
+ return 0
+ fi
+
+ _debug "Deleting record" "$HOSTUP_RECORD_ID"
+
+ if ! _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$HOSTUP_RECORD_ID"; then
+ return 1
+ fi
+
+ _info "Deleted TXT record $HOSTUP_RECORD_ID"
+ _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
+ HOSTUP_ZONE_ID=""
+ return 0
+}
+
+##########################
+# Private helper methods #
+##########################
+
+_hostup_init() {
+ HOSTUP_API_KEY="${HOSTUP_API_KEY:-$(_readaccountconf_mutable HOSTUP_API_KEY)}"
+ HOSTUP_API_BASE="${HOSTUP_API_BASE:-$(_readaccountconf_mutable HOSTUP_API_BASE)}"
+ HOSTUP_TTL="${HOSTUP_TTL:-$(_readaccountconf_mutable HOSTUP_TTL)}"
+ HOSTUP_ZONE_ID="${HOSTUP_ZONE_ID:-$(_readaccountconf_mutable HOSTUP_ZONE_ID)}"
+
+ if [ -z "$HOSTUP_API_BASE" ]; then
+ HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT"
+ fi
+
+ if [ -z "$HOSTUP_API_KEY" ]; then
+ HOSTUP_API_KEY=""
+ _err "HOSTUP_API_KEY is not set."
+ _err "Please export your HostUp API key with read:dns and write:dns scopes."
+ return 1
+ fi
+
+ _saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY"
+ _saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE"
+
+ if [ -n "$HOSTUP_TTL" ]; then
+ _saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
+ fi
+
+ if [ -n "$HOSTUP_ZONE_ID" ]; then
+ _saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID"
+ fi
+
+ return 0
+}
+
+_hostup_detect_zone() {
+ fulldomain="$1"
+
+ if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then
+ return 0
+ fi
+
+ HOSTUP_ZONE_DOMAIN=""
+ _debug "hostup_full_domain" "$fulldomain"
+
+ if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then
+ # Attempt to fetch domain name for provided zone ID
+ if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then
+ return 0
+ fi
+ HOSTUP_ZONE_ID=""
+ fi
+
+ if ! _hostup_load_zones; then
+ return 1
+ fi
+
+ _domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)"
+ _debug "hostup_initial_candidate" "$_domain_candidate"
+
+ while [ -n "$_domain_candidate" ]; do
+ _debug "hostup_zone_candidate" "$_domain_candidate"
+ if _hostup_lookup_zone "$_domain_candidate"; then
+ HOSTUP_ZONE_DOMAIN="$_lookup_zone_domain"
+ HOSTUP_ZONE_ID="$_lookup_zone_id"
+ return 0
+ fi
+
+ case "$_domain_candidate" in
+ *.*) ;;
+ *) break ;;
+ esac
+
+ _domain_candidate="${_domain_candidate#*.}"
+ done
+
+ HOSTUP_ZONE_ID=""
+ return 1
+}
+
+_hostup_record_name() {
+ fulldomain="$1"
+ zonedomain="$2"
+
+ # Remove trailing dot, if any
+ fulldomain="${fulldomain%.}"
+ zonedomain="${zonedomain%.}"
+
+ if [ "$fulldomain" = "$zonedomain" ]; then
+ printf "%s" "@"
+ return 0
+ fi
+
+ suffix=".$zonedomain"
+ case "$fulldomain" in
+ *"$suffix")
+ printf "%s" "${fulldomain%"$suffix"}"
+ ;;
+ *)
+ # Domain not within zone, fall back to full host
+ printf "%s" "$fulldomain"
+ ;;
+ esac
+}
+
+_hostup_sanitize_name() {
+ name="$1"
+
+ if [ -z "$name" ] || [ "$name" = "." ]; then
+ printf "%s" "@"
+ return 0
+ fi
+
+ # Remove any trailing dot
+ name="${name%.}"
+ printf "%s" "$name"
+}
+
+_hostup_fqdn() {
+ domain="$1"
+ printf "%s" "${domain%.}"
+}
+
+_hostup_fetch_zone_details() {
+ zone_id="$1"
+
+ if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
+ return 1
+ fi
+
+ zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')"
+ if [ -n "$zonedomain" ]; then
+ HOSTUP_ZONE_DOMAIN="$zonedomain"
+ return 0
+ fi
+
+ return 1
+}
+
+_hostup_load_zones() {
+ if ! _hostup_rest "GET" "/dns/zones" ""; then
+ return 1
+ fi
+
+ HOSTUP_ZONES_CACHE=""
+ data="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
+
+ while IFS= read -r line; do
+ case "$line" in
+ *'"domain_id"'*'"domain"'*)
+ zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")"
+ zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")"
+ if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
+ HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id}
+"
+ _debug "hostup_zone_loaded" "$zone_domain|$zone_id"
+ fi
+ ;;
+ esac
+ done <%s
+
+ content
+
+ %s
+
+
- ' "$_domain" "$_sub_domain")
+ ' "$_domain" "$_sub_domain" "$txtvalue")
response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
if ! _contains "$response" "Command completed successfully"; then
@@ -125,7 +132,7 @@ dns_inwx_rm() {
if ! printf "%s" "$response" | grep "count" >/dev/null; then
_info "Do not need to delete record"
else
- _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+')
+ _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+')
_info "Deleting record"
_inwx_delete_record "$_record_id"
fi
@@ -324,7 +331,7 @@ _inwx_delete_record() {
id
- %s
+ %s
@@ -362,7 +369,7 @@ _inwx_update_record() {
id
- %s
+ %s
diff --git a/dnsapi/dns_joker.sh b/dnsapi/dns_joker.sh
index 1fe33c67..401471be 100644
--- a/dnsapi/dns_joker.sh
+++ b/dnsapi/dns_joker.sh
@@ -7,7 +7,7 @@ Options:
JOKER_USERNAME Username
JOKER_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/2840
-Author:
+Author: @aattww
'
JOKER_API="https://svc.joker.com/nic/replace"
diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh
index f19333c4..9cb6327e 100644
--- a/dnsapi/dns_la.sh
+++ b/dnsapi/dns_la.sh
@@ -1,14 +1,17 @@
#!/usr/bin/env sh
+
+# LA_Id="123"
+# LA_Sk="456"
# shellcheck disable=SC2034
dns_la_info='dns.la
Site: dns.la
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la
Options:
- LA_Id API ID
- LA_Key API key
+ LA_Id APIID
+ LA_Sk APISecret
+ LA_Token 用冒号连接 APIID APISecret 再base64生成
Issues: github.com/acmesh-official/acme.sh/issues/4257
'
-
LA_Api="https://api.dns.la/api"
######## Public functions #####################
@@ -19,18 +22,23 @@ dns_la_add() {
txtvalue=$2
LA_Id="${LA_Id:-$(_readaccountconf_mutable LA_Id)}"
- LA_Key="${LA_Key:-$(_readaccountconf_mutable LA_Key)}"
+ LA_Sk="${LA_Sk:-$(_readaccountconf_mutable LA_Sk)}"
+ _log "LA_Id=$LA_Id"
+ _log "LA_Sk=$LA_Sk"
- if [ -z "$LA_Id" ] || [ -z "$LA_Key" ]; then
+ if [ -z "$LA_Id" ] || [ -z "$LA_Sk" ]; then
LA_Id=""
- LA_Key=""
+ LA_Sk=""
_err "You didn't specify a dnsla api id and key yet."
return 1
fi
#save the api key and email to the account conf file.
_saveaccountconf_mutable LA_Id "$LA_Id"
- _saveaccountconf_mutable LA_Key "$LA_Key"
+ _saveaccountconf_mutable LA_Sk "$LA_Sk"
+
+ # generate dnsla token
+ _la_token
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
@@ -42,11 +50,13 @@ dns_la_add() {
_debug _domain "$_domain"
_info "Adding record"
- if _la_rest "record.ashx?cmd=create&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&host=$_sub_domain&recordtype=TXT&recorddata=$txtvalue&recordline="; then
- if _contains "$response" '"resultid":'; then
+
+ # record type is enum in new api, 16 for TXT
+ if _la_post "{\"domainId\":\"$_domain_id\",\"type\":16,\"host\":\"$_sub_domain\",\"data\":\"$txtvalue\",\"ttl\":600}" "record"; then
+ if _contains "$response" '"id":'; then
_info "Added, OK"
return 0
- elif _contains "$response" '"code":532'; then
+ elif _contains "$response" '"msg":"与已有记录冲突"'; then
_info "Already exists, OK"
return 0
else
@@ -54,7 +64,7 @@ dns_la_add() {
return 1
fi
fi
- _err "Add txt record error."
+ _err "Add txt record failed."
return 1
}
@@ -65,7 +75,9 @@ dns_la_rm() {
txtvalue=$2
LA_Id="${LA_Id:-$(_readaccountconf_mutable LA_Id)}"
- LA_Key="${LA_Key:-$(_readaccountconf_mutable LA_Key)}"
+ LA_Sk="${LA_Sk:-$(_readaccountconf_mutable LA_Sk)}"
+
+ _la_token
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
@@ -77,27 +89,29 @@ dns_la_rm() {
_debug _domain "$_domain"
_debug "Getting txt records"
- if ! _la_rest "record.ashx?cmd=listn&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&domain=$_domain&host=$_sub_domain&recordtype=TXT&recorddata=$txtvalue"; then
+ # record type is enum in new api, 16 for TXT
+ if ! _la_get "recordList?pageIndex=1&pageSize=10&domainId=$_domain_id&host=$_sub_domain&type=16&data=$txtvalue"; then
_err "Error"
return 1
fi
- if ! _contains "$response" '"recordid":'; then
+ if ! _contains "$response" '"id":'; then
_info "Don't need to remove."
return 0
fi
- record_id=$(printf "%s" "$response" | grep '"recordid":' | cut -d : -f 2 | cut -d , -f 1 | tr -d '\r' | tr -d '\n')
+ record_id=$(printf "%s" "$response" | grep '"id":' | _head_n 1 | sed 's/.*"id": *"\([^"]*\)".*/\1/')
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
_err "Can not get record id to remove."
return 1
fi
- if ! _la_rest "record.ashx?cmd=remove&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&domain=$_domain&recordid=$record_id"; then
+ # remove record in new api is RESTful
+ if ! _la_post "" "record?id=$record_id" "DELETE"; then
_err "Delete record error."
return 1
fi
- _contains "$response" '"code":300'
+ _contains "$response" '"code":200'
}
@@ -119,12 +133,13 @@ _get_root() {
return 1
fi
- if ! _la_rest "domain.ashx?cmd=get&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domain=$h"; then
+ if ! _la_get "domain?domain=$h"; then
return 1
fi
- if _contains "$response" '"domainid":'; then
- _domain_id=$(printf "%s" "$response" | grep '"domainid":' | cut -d : -f 2 | cut -d , -f 1 | tr -d '\r' | tr -d '\n')
+ if _contains "$response" '"domain":'; then
+ _domain_id=$(echo "$response" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
+ _log "_domain_id" "$_domain_id"
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$h"
@@ -143,6 +158,21 @@ _la_rest() {
url="$LA_Api/$1"
_debug "$url"
+ if ! response="$(_get "$url" "Authorization: Basic $LA_Token" | tr -d ' ' | tr "}" ",")"; then
+ _err "Error: $url"
+ return 1
+ fi
+
+ _debug2 response "$response"
+ return 0
+}
+
+_la_get() {
+ url="$LA_Api/$1"
+ _debug "$url"
+
+ export _H1="Authorization: Basic $LA_Token"
+
if ! response="$(_get "$url" | tr -d ' ' | tr "}" ",")"; then
_err "Error: $url"
return 1
@@ -151,3 +181,29 @@ _la_rest() {
_debug2 response "$response"
return 0
}
+
+# Usage: _la_post body url [POST|PUT|DELETE]
+_la_post() {
+ body=$1
+ url="$LA_Api/$2"
+ http_method=$3
+ _debug "$body"
+ _debug "$url"
+
+ export _H1="Authorization: Basic $LA_Token"
+
+ if ! response="$(_post "$body" "$url" "" "$http_method")"; then
+ _err "Error: $url"
+ return 1
+ fi
+
+ _debug2 response "$response"
+ return 0
+}
+
+_la_token() {
+ LA_Token=$(printf "%s:%s" "$LA_Id" "$LA_Sk" | _base64)
+ _debug "$LA_Token"
+
+ return 0
+}
diff --git a/dnsapi/dns_limacity.sh b/dnsapi/dns_limacity.sh
index fb12f8c6..5734be9e 100644
--- a/dnsapi/dns_limacity.sh
+++ b/dnsapi/dns_limacity.sh
@@ -1,13 +1,13 @@
#!/usr/bin/env sh
-
-# Created by Laraveluser
-#
-# Pass credentials before "acme.sh --issue --dns dns_limacity ..."
-# --
-# export LIMACITY_APIKEY=""
-# --
-#
-# Pleas note: APIKEY must have following roles: dns.admin, domains.reader
+# shellcheck disable=SC2034
+dns_limacity_info='lima-city.de
+Site: www.lima-city.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_limacity
+Options:
+ LIMACITY_APIKEY API Key. Note: The API Key must have following roles: dns.admin, domains.reader
+Issues: github.com/acmesh-official/acme.sh/issues/4758
+Author: @Laraveluser
+'
######## Public functions #####################
diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh
new file mode 100644
index 00000000..57679127
--- /dev/null
+++ b/dnsapi/dns_mgwm.sh
@@ -0,0 +1,109 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_mgwm_info='mgw-media.de
+Site: mgw-media.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mgwm
+Options:
+ MGWM_CUSTOMER Your customer number
+ MGWM_API_HASH Your API Hash
+Issues: github.com/acmesh-official/acme.sh/issues/6669
+'
+# Base URL for the mgw-media.de API
+MGWM_API_BASE="https://api.mgw-media.de/record"
+
+######## Public functions #####################
+
+# This function is called by acme.sh to add a TXT record.
+dns_mgwm_add() {
+ fulldomain=$1
+ txtvalue=$2
+ _info "Using mgw-media.de DNS API for domain $fulldomain (add record)"
+ _debug "fulldomain: $fulldomain"
+ _debug "txtvalue: $txtvalue"
+
+ # Call the new private function to handle the API request.
+ # The 'add' action, fulldomain, type 'txt' and txtvalue are passed.
+ if _mgwm_request "add" "$fulldomain" "txt" "$txtvalue"; then
+ _info "TXT record for $fulldomain successfully added via mgw-media.de API."
+ _sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks.
+ return 0
+ else
+ # Error message already logged by _mgwm_request, but a specific one here helps.
+ _err "mgwm_add: Failed to add TXT record for $fulldomain."
+ return 1
+ fi
+}
+# This function is called by acme.sh to remove a TXT record after validation.
+dns_mgwm_rm() {
+ fulldomain=$1
+ txtvalue=$2 # This txtvalue is now used to identify the specific record to be removed.
+ _info "Removing TXT record for $fulldomain using mgw-media.de DNS API (remove record)"
+ _debug "fulldomain: $fulldomain"
+ _debug "txtvalue: $txtvalue"
+
+ # Call the new private function to handle the API request.
+ # The 'rm' action, fulldomain, type 'txt' and txtvalue are passed.
+ if _mgwm_request "rm" "$fulldomain" "txt" "$txtvalue"; then
+ _info "TXT record for $fulldomain successfully removed via mgw-media.de API."
+ return 0
+ else
+ # Error message already logged by _mgwm_request, but a specific one here helps.
+ _err "mgwm_rm: Failed to remove TXT record for $fulldomain."
+ return 1
+ fi
+}
+#################### Private functions below ##################################
+
+# _mgwm_request() encapsulates the API call logic, including
+# loading credentials, setting the Authorization header, and executing the request.
+# Arguments:
+# $1: action (e.g., "add", "rm")
+# $2: fulldomain
+# $3: type (e.g., "txt")
+# $4: content (the txtvalue)
+_mgwm_request() {
+ _action="$1"
+ _fulldomain="$2"
+ _type="$3"
+ _content="$4"
+
+ _debug "Calling _mgwm_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content"
+
+ # Load credentials from environment or acme.sh config
+ MGWM_CUSTOMER="${MGWM_CUSTOMER:-$(_readaccountconf_mutable MGWM_CUSTOMER)}"
+ MGWM_API_HASH="${MGWM_API_HASH:-$(_readaccountconf_mutable MGWM_API_HASH)}"
+
+ # Check if credentials are set
+ if [ -z "$MGWM_CUSTOMER" ] || [ -z "$MGWM_API_HASH" ]; then
+ _err "You didn't specify one or more of MGWM_CUSTOMER or MGWM_API_HASH."
+ _err "Please check these environment variables and try again."
+ return 1
+ fi
+
+ # Save credentials for automatic renewal and future calls
+ _saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER"
+ _saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH"
+
+ # Create the Basic Auth Header. acme.sh's _base64 function is used for encoding.
+ _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)"
+ export _H1="Authorization: Basic $_credentials"
+ _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials
+
+ # Construct the API URL based on the action and provided parameters.
+ _request_url="${MGWM_API_BASE}/${_action}/${_fulldomain}/${_type}/${_content}"
+ _debug "Constructed mgw-media.de API URL for action '$_action': ${_request_url}"
+
+ # Execute the HTTP GET request with the Authorization Header.
+ # The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization.
+ response="$(_get "$_request_url")"
+ _debug "mgw-media.de API response for action '$_action': $response"
+
+ # Check the API response for success. The API returns "OK" on success.
+ if [ "$response" = "OK" ]; then
+ _info "mgw-media.de API action '$_action' for record '$_fulldomain' successful."
+ return 0
+ else
+ _err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'"
+ return 1
+ fi
+}
diff --git a/dnsapi/dns_mijnhost.sh b/dnsapi/dns_mijnhost.sh
new file mode 100644
index 00000000..9f5e7710
--- /dev/null
+++ b/dnsapi/dns_mijnhost.sh
@@ -0,0 +1,214 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_mijnhost_info='mijn.host
+Site: mijn.host
+Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mijnhost
+Options:
+ MIJNHOST_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/issues/6177
+Author: @peterv99
+'
+
+######## Public functions ######################
+MIJNHOST_API="https://mijn.host/api/v2"
+
+# Add TXT record for domain verification
+dns_mijnhost_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}"
+ if [ -z "$MIJNHOST_API_KEY" ]; then
+ MIJNHOST_API_KEY=""
+ _err "You haven't specified your mijn-host API key yet."
+ _err "Please add MIJNHOST_API_KEY to the env."
+ return 1
+ fi
+
+ # Save the API key for future use
+ _saveaccountconf_mutable MIJNHOST_API_KEY "$MIJNHOST_API_KEY"
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "Invalid domain"
+ return 1
+ fi
+
+ _debug2 _sub_domain "$_sub_domain"
+ _debug2 _domain "$_domain"
+ _debug "Adding DNS record" "${fulldomain}."
+
+ # Construct the API URL
+ api_url="$MIJNHOST_API/domains/$_domain/dns"
+
+ # Getting previous records
+ _mijnhost_rest GET "$api_url" ""
+
+ if [ "$_code" != "200" ]; then
+ _err "Error getting current DNS enties ($_code)"
+ return 1
+ fi
+
+ records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://')
+
+ _debug2 "Current records" "$records"
+
+ # Build the payload for the API
+ data="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"value\":\"$txtvalue\",\"ttl\":300}"
+
+ _debug2 "Record to add" "$data"
+
+ # Updating the records
+ updated_records=$(echo "$records" | sed -E "s/\]( *$)/,$data\]/")
+
+ _debug2 "Updated records" "$updated_records"
+
+ # data
+ data="{\"records\": $updated_records}"
+
+ _mijnhost_rest PUT "$api_url" "$data"
+
+ if [ "$_code" = "200" ]; then
+ _info "DNS record succesfully added."
+ return 0
+ else
+ _err "Error adding DNS record ($_code)."
+ return 1
+ fi
+}
+
+# Remove TXT record after verification
+dns_mijnhost_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}"
+ if [ -z "$MIJNHOST_API_KEY" ]; then
+ MIJNHOST_API_KEY=""
+ _err "You haven't specified your mijn-host API key yet."
+ _err "Please add MIJNHOST_API_KEY to the env."
+ return 1
+ fi
+
+ _debug "Detecting root zone for" "${fulldomain}."
+ if ! _get_root "$fulldomain"; then
+ _err "Invalid domain"
+ return 1
+ fi
+
+ _debug "Removing DNS record for TXT value" "${txtvalue}."
+
+ # Construct the API URL
+ api_url="$MIJNHOST_API/domains/$_domain/dns"
+
+ # Get current records
+ _mijnhost_rest GET "$api_url" ""
+
+ if [ "$_code" != "200" ]; then
+ _err "Error getting current DNS enties ($_code)"
+ return 1
+ fi
+
+ _debug2 "Get current records response:" "$response"
+
+ records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://')
+
+ _debug2 "Current records:" "$records"
+
+ updated_records=$(echo "$records" | sed -E "s/\{[^}]*\"value\":\"$txtvalue\"[^}]*\},?//g" | sed 's/,]/]/g')
+
+ _debug2 "Updated records:" "$updated_records"
+
+ # Build the new payload
+ data="{\"records\": $updated_records}"
+
+ # Use the _put method to update the records
+ _mijnhost_rest PUT "$api_url" "$data"
+
+ if [ "$_code" = "200" ]; then
+ _info "DNS record removed successfully."
+ return 0
+ else
+ _err "Error removing DNS record ($_code)."
+ return 1
+ fi
+}
+
+# Helper function to detect the root zone
+_get_root() {
+ domain=$1
+
+ # Get current records
+ _debug "Getting current domains"
+ _mijnhost_rest GET "$MIJNHOST_API/domains" ""
+
+ if [ "$_code" != "200" ]; then
+ _err "error getting current domains ($_code)"
+ return 1
+ fi
+
+ # Extract root domains from response
+ rootDomains=$(echo "$response" | _egrep_o '"domain":"[^"]*"' | sed -E 's/"domain":"([^"]*)"/\1/')
+ _debug "Root domains:" "$rootDomains"
+
+ for rootDomain in $rootDomains; do
+ if _contains "$domain" "$rootDomain"; then
+ _domain="$rootDomain"
+ _sub_domain=$(echo "$domain" | sed "s/.$rootDomain//g")
+ _debug "Found root domain" "$_domain" "and subdomain" "$_sub_domain" "for" "$domain"
+ return 0
+ fi
+ done
+ return 1
+}
+
+# Helper function for rest calls
+_mijnhost_rest() {
+ m=$1
+ ep="$2"
+ data="$3"
+
+ MAX_REQUEST_RETRY_TIMES=15
+ _request_retry_times=0
+ _retry_sleep=5 #Initial sleep time in seconds.
+
+ while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do
+ _debug2 _request_retry_times "$_request_retry_times"
+ export _H1="API-Key: $MIJNHOST_API_KEY"
+ export _H2="Content-Type: application/json"
+ # clear headers from previous request to avoid getting wrong http code on timeouts
+ : >"$HTTP_HEADER"
+ _debug "$ep"
+ if [ "$m" != "GET" ]; then
+ _debug2 "data $data"
+ response="$(_post "$data" "$ep" "" "$m")"
+ else
+ response="$(_get "$ep")"
+ fi
+ _ret="$?"
+ _debug2 "response $response"
+ _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
+ _debug "http response code $_code"
+ if [ "$_code" = "401" ]; then
+ # we have an invalid API token, maybe it is expired?
+ _err "Access denied. Invalid API token."
+ return 1
+ fi
+
+ if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "400" ] || _contains "$response" "DNS records not managed by mijn.host"; then #Sometimes API errors out
+ _request_retry_times="$(_math "$_request_retry_times" + 1)"
+ _info "REST call error $_code retrying $ep in ${_retry_sleep}s"
+ _sleep "$_retry_sleep"
+ _retry_sleep="$(_math "$_retry_sleep" \* 2)"
+ continue
+ fi
+ break
+ done
+ if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then
+ _err "Error mijn.host API call was retried $MAX_REQUEST_RETRY_TIMES times."
+ _err "Calling $ep failed."
+ return 1
+ fi
+ response="$(echo "$response" | _normalizeJson)"
+ return 0
+}
diff --git a/dnsapi/dns_myapi.sh b/dnsapi/dns_myapi.sh
index c9f5eb9f..101854d5 100755
--- a/dnsapi/dns_myapi.sh
+++ b/dnsapi/dns_myapi.sh
@@ -1,12 +1,14 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_myapi_info='Custom API Example
- A sample custom DNS API script.
-Domains: example.com
+ A sample custom DNS API script description.
+Domains: example.com example.net
Site: github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_myapi
Options:
- MYAPI_Token API Token. Get API Token from https://example.com/api/. Optional.
+ MYAPI_Token API Token. Get API Token from https://example.com/api/
+ MYAPI_Variable2 Option 2. Default "default value".
+ MYAPI_Variable2 Option 3. Optional.
Issues: github.com/acmesh-official/acme.sh
Author: Neil Pang
'
diff --git a/dnsapi/dns_mydnsjp.sh b/dnsapi/dns_mydnsjp.sh
index 336c4889..4dfffaaa 100755
--- a/dnsapi/dns_mydnsjp.sh
+++ b/dnsapi/dns_mydnsjp.sh
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mydnsjp
Options:
MYDNSJP_MasterID Master ID
MYDNSJP_Password Password
-Author: epgdatacapbon
+Author: @tkmsst
'
######## Public functions #####################
diff --git a/dnsapi/dns_namecom.sh b/dnsapi/dns_namecom.sh
index 44549c9e..1062c849 100755
--- a/dnsapi/dns_namecom.sh
+++ b/dnsapi/dns_namecom.sh
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_namecom
Options:
Namecom_Username Username
Namecom_Token API Token
-Author: RaidenII
+Author: @RaidenII
'
######## Public functions #####################
diff --git a/dnsapi/dns_namesilo.sh b/dnsapi/dns_namesilo.sh
index b31e32a1..5d47a59a 100755
--- a/dnsapi/dns_namesilo.sh
+++ b/dnsapi/dns_namesilo.sh
@@ -5,7 +5,7 @@ Site: NameSilo.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_namesilo
Options:
Namesilo_Key API Key
-Author: meowthink
+Author: @meowthink
'
#Utilize API to finish dns-01 verifications.
diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh
index 1ab47a89..0c42989b 100644
--- a/dnsapi/dns_nanelo.sh
+++ b/dnsapi/dns_nanelo.sh
@@ -27,8 +27,16 @@ dns_nanelo_add() {
fi
_saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN"
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
_info "Adding TXT record to ${fulldomain}"
- response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")"
+ response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/addrecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")"
if _contains "${response}" 'success'; then
return 0
fi
@@ -51,8 +59,16 @@ dns_nanelo_rm() {
fi
_saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN"
+ _debug "First, let's detect the root zone:"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
_info "Deleting resource record $fulldomain"
- response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")"
+ response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/deleterecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")"
if _contains "${response}" 'success'; then
return 0
fi
@@ -60,3 +76,45 @@ dns_nanelo_rm() {
_err "${response}"
return 1
}
+
+#################### Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+
+_get_root() {
+ fulldomain=$1
+
+ # Fetch all zones from Nanelo
+ response="$(_get "$NANELO_API$NANELO_TOKEN/dns/getzones")" || return 1
+
+ # Extract "zones" array into space-separated list
+ zones=$(echo "$response" |
+ tr -d ' \n' |
+ sed -n 's/.*"zones":\[\([^]]*\)\].*/\1/p' |
+ tr -d '"' |
+ tr , ' ')
+ _debug zones "$zones"
+
+ bestzone=""
+ for z in $zones; do
+ case "$fulldomain" in
+ *."$z" | "$z")
+ if [ ${#z} -gt ${#bestzone} ]; then
+ bestzone=$z
+ fi
+ ;;
+ esac
+ done
+
+ if [ -z "$bestzone" ]; then
+ _err "No matching zone found for $fulldomain"
+ return 1
+ fi
+
+ _domain="$bestzone"
+ _sub_domain=$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")
+
+ return 0
+}
diff --git a/dnsapi/dns_netcup.sh b/dnsapi/dns_netcup.sh
index 687b99bc..8609adf6 100644
--- a/dnsapi/dns_netcup.sh
+++ b/dnsapi/dns_netcup.sh
@@ -19,7 +19,7 @@ client=""
dns_netcup_add() {
_debug NC_Apikey "$NC_Apikey"
- login
+ _login
if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then
_err "No Credentials given"
return 1
@@ -61,7 +61,7 @@ dns_netcup_add() {
}
dns_netcup_rm() {
- login
+ _login
fulldomain=$1
txtvalue=$2
@@ -125,7 +125,7 @@ dns_netcup_rm() {
logout
}
-login() {
+_login() {
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
_debug "$tmp"
diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh
index 5c137c3f..fd38d046 100644
--- a/dnsapi/dns_omglol.sh
+++ b/dnsapi/dns_omglol.sh
@@ -4,8 +4,8 @@ dns_omglol_info='omg.lol
Site: omg.lol
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_omglol
Options:
- OMG_ApiKey API Key from omg.lol. This is accessible from the bottom of the account page at https://home.omg.lol/account
- OMG_Address This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
+ OMG_ApiKey - API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
+ OMG_Address - Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
Issues: github.com/acmesh-official/acme.sh/issues/5299
Author: @Kholin
'
@@ -35,7 +35,7 @@ dns_omglol_add() {
_debug "omg.lol Address" "$OMG_Address"
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
return 1
fi
@@ -67,7 +67,7 @@ dns_omglol_rm() {
_debug "omg.lol Address" "$OMG_Address"
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
return 1
fi
@@ -100,18 +100,49 @@ omg_validate() {
fi
_endswith "$fulldomain" "omg.lol"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
_err "Domain name requested is not under omg.lol"
return 1
fi
_endswith "$fulldomain" "$omg_address.omg.lol"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
_err "Domain name is not a subdomain of provided omg.lol address $omg_address"
return 1
fi
- _debug "Required environment parameters are all present"
+ omg_testconnect "$omg_apikey" "$omg_address"
+ if [ 1 = $? ]; then
+ _err "Authentication to omg.lol for address $omg_address using provided API key failed"
+ return 1
+ fi
+
+ _debug "Required environment parameters are all present and validated"
+}
+
+# Validate that the address and API key are both correct and associated to each other
+omg_testconnect() {
+ omg_apikey=$1
+ omg_address=$2
+
+ _debug2 "Function" "omg_testconnect"
+ _secure_debug2 "omg.lol API key" "$omg_apikey"
+ _debug2 "omg.lol Address" "$omg_address"
+
+ authheader="$(_createAuthHeader "$omg_apikey")"
+ export _H1="$authheader"
+ endpoint="https://api.omg.lol/address/$omg_address/info"
+ _debug2 "Endpoint for validation" "$endpoint"
+
+ response=$(_get "$endpoint" "" 30)
+
+ _jsonResponseCheck "$response" "status_code" 200
+ if [ 1 = $? ]; then
+ _debug2 "Failed to query omg.lol for $omg_address with provided API key"
+ _secure_debug2 "API Key" "omg_apikey"
+ _secure_debug3 "Raw response" "$response"
+ return 1
+ fi
}
# Add (or modify) an entry for a new ACME query
diff --git a/dnsapi/dns_openprovider.sh b/dnsapi/dns_openprovider.sh
index b584fad2..2dec9934 100755
--- a/dnsapi/dns_openprovider.sh
+++ b/dnsapi/dns_openprovider.sh
@@ -2,6 +2,7 @@
# shellcheck disable=SC2034
dns_openprovider_info='OpenProvider.eu
Site: OpenProvider.eu
+Domains: OpenProvider.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_openprovider
Options:
OPENPROVIDER_USER Username
diff --git a/dnsapi/dns_openprovider_rest.sh b/dnsapi/dns_openprovider_rest.sh
new file mode 100644
index 00000000..210dc6fc
--- /dev/null
+++ b/dnsapi/dns_openprovider_rest.sh
@@ -0,0 +1,186 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_openprovider_rest_info='OpenProvider (REST)
+Domains: OpenProvider.com
+Site: OpenProvider.eu
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_openprovider_rest
+Options:
+ OPENPROVIDER_REST_USERNAME Openprovider Account Username
+ OPENPROVIDER_REST_PASSWORD Openprovider Account Password
+Issues: github.com/acmesh-official/acme.sh/issues/6122
+Author: Lambiek12
+'
+
+OPENPROVIDER_API_URL="https://api.openprovider.eu/v1beta"
+
+######## Public functions #####################
+
+# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_openprovider_rest_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _openprovider_prepare_credentials || return 1
+
+ _debug "Try fetch OpenProvider DNS zone details"
+ if ! _get_dns_zone "$fulldomain"; then
+ _err "DNS zone not found within configured OpenProvider account."
+ return 1
+ fi
+
+ if [ -n "$_domain_id" ]; then
+ addzonerecordrequestparameters="dns/zones/$_domain_name"
+ addzonerecordrequestbody="{\"id\":$_domain_id,\"name\":\"$_domain_name\",\"records\":{\"add\":[{\"name\":\"$_sub_domain\",\"ttl\":900,\"type\":\"TXT\",\"value\":\"$txtvalue\"}]}}"
+
+ if _openprovider_rest PUT "$addzonerecordrequestparameters" "$addzonerecordrequestbody"; then
+ if _contains "$response" "\"success\":true"; then
+ return 0
+ elif _contains "$response" "\"Duplicate record\""; then
+ _debug "Record already existed"
+ return 0
+ else
+ _err "Adding TXT record failed due to errors."
+ return 1
+ fi
+ fi
+ fi
+
+ _err "Adding TXT record failed due to errors."
+ return 1
+}
+
+# Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to remove the txt record after validation
+dns_openprovider_rest_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _openprovider_prepare_credentials || return 1
+
+ _debug "Try fetch OpenProvider DNS zone details"
+ if ! _get_dns_zone "$fulldomain"; then
+ _err "DNS zone not found within configured OpenProvider account."
+ return 1
+ fi
+
+ if [ -n "$_domain_id" ]; then
+ removezonerecordrequestparameters="dns/zones/$_domain_name"
+ removezonerecordrequestbody="{\"id\":$_domain_id,\"name\":\"$_domain_name\",\"records\":{\"remove\":[{\"name\":\"$_sub_domain\",\"ttl\":900,\"type\":\"TXT\",\"value\":\"\\\"$txtvalue\\\"\"}]}}"
+
+ if _openprovider_rest PUT "$removezonerecordrequestparameters" "$removezonerecordrequestbody"; then
+ if _contains "$response" "\"success\":true"; then
+ return 0
+ else
+ _err "Removing TXT record failed due to errors."
+ return 1
+ fi
+ fi
+ fi
+
+ _err "Removing TXT record failed due to errors."
+ return 1
+}
+
+#################### OpenProvider API common functions ####################
+_openprovider_prepare_credentials() {
+ OPENPROVIDER_REST_USERNAME="${OPENPROVIDER_REST_USERNAME:-$(_readaccountconf_mutable OPENPROVIDER_REST_USERNAME)}"
+ OPENPROVIDER_REST_PASSWORD="${OPENPROVIDER_REST_PASSWORD:-$(_readaccountconf_mutable OPENPROVIDER_REST_PASSWORD)}"
+
+ if [ -z "$OPENPROVIDER_REST_USERNAME" ] || [ -z "$OPENPROVIDER_REST_PASSWORD" ]; then
+ OPENPROVIDER_REST_USERNAME=""
+ OPENPROVIDER_REST_PASSWORD=""
+ _err "You didn't specify the Openprovider username or password yet."
+ return 1
+ fi
+
+ #save the credentials to the account conf file.
+ _saveaccountconf_mutable OPENPROVIDER_REST_USERNAME "$OPENPROVIDER_REST_USERNAME"
+ _saveaccountconf_mutable OPENPROVIDER_REST_PASSWORD "$OPENPROVIDER_REST_PASSWORD"
+}
+
+_openprovider_rest() {
+ httpmethod=$1
+ queryparameters=$2
+ requestbody=$3
+
+ _openprovider_rest_login
+ if [ -z "$openproviderauthtoken" ]; then
+ _err "Unable to fetch authentication token from Openprovider API."
+ return 1
+ fi
+
+ export _H1="Content-Type: application/json"
+ export _H2="Accept: application/json"
+ export _H3="Authorization: Bearer $openproviderauthtoken"
+
+ if [ "$httpmethod" != "GET" ]; then
+ response="$(_post "$requestbody" "$OPENPROVIDER_API_URL/$queryparameters" "" "$httpmethod")"
+ else
+ response="$(_get "$OPENPROVIDER_API_URL/$queryparameters")"
+ fi
+
+ if [ "$?" != "0" ]; then
+ _err "No valid parameters supplied for Openprovider API: Error $queryparameters"
+ return 1
+ fi
+
+ _debug2 response "$response"
+
+ return 0
+}
+
+_openprovider_rest_login() {
+ export _H1="Content-Type: application/json"
+ export _H2="Accept: application/json"
+
+ loginrequesturl="$OPENPROVIDER_API_URL/auth/login"
+ loginrequestbody="{\"ip\":\"0.0.0.0\",\"password\":\"$OPENPROVIDER_REST_PASSWORD\",\"username\":\"$OPENPROVIDER_REST_USERNAME\"}"
+ loginresponse="$(_post "$loginrequestbody" "$loginrequesturl" "" "POST")"
+
+ openproviderauthtoken="$(printf "%s\n" "$loginresponse" | _egrep_o '"token" *: *"[^"]*' | _head_n 1 | sed 's#^"token" *: *"##')"
+
+ export openproviderauthtoken
+}
+
+#################### Private functions ##################################
+
+# Usage: _get_dns_zone _acme-challenge.www.domain.com
+# Returns:
+# _domain_id=123456789
+# _domain_name=domain.com
+# _sub_domain=_acme-challenge.www
+_get_dns_zone() {
+ domain=$1
+ i=1
+ p=1
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ if [ -z "$h" ]; then
+ # Empty value not allowed
+ return 1
+ fi
+
+ if ! _openprovider_rest GET "dns/zones/$h" ""; then
+ return 1
+ fi
+
+ if _contains "$response" "\"name\":\"$h\""; then
+ _domain_id="$(printf "%s\n" "$response" | _egrep_o '"id" *: *[^,]*' | _head_n 1 | sed 's#^"id" *: *##')"
+ _debug _domain_id "$_domain_id"
+
+ _domain_name="$h"
+ _debug _domain_name "$_domain_name"
+
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _debug _sub_domain "$_sub_domain"
+ return 0
+ fi
+
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+
+ return 1
+}
diff --git a/dnsapi/dns_opnsense.sh b/dnsapi/dns_opnsense.sh
index d1e9c0ac..a11cfae5 100755
--- a/dnsapi/dns_opnsense.sh
+++ b/dnsapi/dns_opnsense.sh
@@ -110,15 +110,16 @@ rm_record() {
if _existingchallenge "$_domain" "$_host" "$new_challenge"; then
# Delete
if _opns_rest "POST" "/record/delRecord/${_uuid}" "\{\}"; then
- if echo "$_return_str" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then
- _opns_rest "POST" "/service/reconfigure" "{}"
+ if echo "$response" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then
_debug "Record deleted"
+ _opns_rest "POST" "/service/reconfigure" "{}"
+ _debug "Service reconfigured"
else
_err "Error deleting record $_host from domain $fulldomain"
return 1
fi
else
- _err "Error deleting record $_host from domain $fulldomain"
+ _err "Error requesting deletion of record $_host from domain $fulldomain"
return 1
fi
else
@@ -150,14 +151,17 @@ _get_root() {
return 1
fi
_debug h "$h"
- id=$(echo "$_domain_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
- if [ -n "$id" ]; then
- _debug id "$id"
- _host=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain="${h}"
- _domainid="${id}"
- return 0
- fi
+ lines=$(echo "$_domain_response" | sed 's/{/\n/g')
+ for line in $lines; do
+ id=$(echo "$line" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",.*\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
+ if [ -n "$id" ]; then
+ _debug id "$id"
+ _host=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain="${h}"
+ _domainid="${id}"
+ return 0
+ fi
+ done
p=$i
i=$(_math "$i" + 1)
done
@@ -206,13 +210,13 @@ _existingchallenge() {
return 1
fi
_uuid=""
- _uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[^\"]*\",\"enabled\":\"[01]\",\"domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
+ _uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"[01]\",\"domain\":\"[a-z0-9\-]*\",\"%domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
if [ -n "$_uuid" ]; then
_debug uuid "$_uuid"
return 0
fi
- _debug "${2}.$1{1} record not found"
+ _debug "${2}.${1} record not found"
return 1
}
diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh
index 24ad0904..9f2cd23f 100755
--- a/dnsapi/dns_ovh.sh
+++ b/dnsapi/dns_ovh.sh
@@ -201,7 +201,7 @@ dns_ovh_rm() {
if ! _ovh_rest GET "domain/zone/$_domain/record/$rid"; then
return 1
fi
- if _contains "$response" "\"target\":\"$txtvalue\""; then
+ if _contains "$response" "$txtvalue"; then
_debug "Found txt id:$rid"
if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then
return 1
diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh
index 2478e19f..ec19ad25 100755
--- a/dnsapi/dns_pdns.sh
+++ b/dnsapi/dns_pdns.sh
@@ -7,7 +7,7 @@ Options:
PDNS_Url API URL. E.g. "http://ns.example.com:8081"
PDNS_ServerId Server ID. E.g. "localhost"
PDNS_Token API Token
- PDNS_Ttl=60 Domain TTL. Default: "60".
+ PDNS_Ttl Domain TTL. Default: "60".
'
DEFAULT_PDNS_TTL=60
diff --git a/dnsapi/dns_pleskxml.sh b/dnsapi/dns_pleskxml.sh
index 6b38abcb..465bcc60 100644
--- a/dnsapi/dns_pleskxml.sh
+++ b/dnsapi/dns_pleskxml.sh
@@ -8,7 +8,7 @@ Options:
pleskxml_user Username
pleskxml_pass Password
Issues: github.com/acmesh-official/acme.sh/issues/2577
-Author: Stilez,
+Author: @Stilez, @romanlum
'
## Plesk XML API described at:
diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh
new file mode 100755
index 00000000..78756a35
--- /dev/null
+++ b/dnsapi/dns_qc.sh
@@ -0,0 +1,216 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_qc_info='QUIC.cloud
+Site: quic.cloud
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_qc
+Options:
+ QC_API_KEY QC API Key
+ QC_API_EMAIL Your account email
+'
+
+QC_Api="https://api.quic.cloud/v2"
+
+######## Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_qc_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue"
+ QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}"
+ QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}"
+
+ if [ "$QC_API_KEY" ]; then
+ _saveaccountconf_mutable QC_API_KEY "$QC_API_KEY"
+ else
+ _err "You didn't specify a QUIC.cloud api key as QC_API_KEY."
+ _err "You can get yours from here https://my.quic.cloud/up/api."
+ return 1
+ fi
+
+ if ! _contains "$QC_API_EMAIL" "@"; then
+ _err "It seems that the QC_API_EMAIL=$QC_API_EMAIL is not a valid email address."
+ _err "Please check and retry."
+ return 1
+ fi
+ #save the api key and email to the account conf file.
+ _saveaccountconf_mutable QC_API_EMAIL "$QC_API_EMAIL"
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain during add"
+ return 1
+ fi
+ _debug _domain_id "$_domain_id"
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
+ _debug "Getting txt records"
+ _qc_rest GET "zones/${_domain_id}/records"
+
+ if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
+ _err "Error failed response from QC GET: $response"
+ return 1
+ fi
+
+ # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
+ # we can not use updating anymore.
+ # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
+ # _debug count "$count"
+ # if [ "$count" = "0" ]; then
+ _info "Adding txt record"
+ if _qc_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":1800}"; then
+ if _contains "$response" "$txtvalue"; then
+ _info "Added txt record, OK"
+ return 0
+ elif _contains "$response" "Same record already exists"; then
+ _info "txt record already exists, OK"
+ return 0
+ else
+ _err "Add txt record error: $response"
+ return 1
+ fi
+ fi
+ _err "Add txt record error: POST failed: $response"
+ return 1
+
+}
+
+#fulldomain txtvalue
+dns_qc_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _debug "Enter dns_qc_rm fulldomain: $fulldomain, txtvalue: $txtvalue"
+ QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}"
+ QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}"
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain during rm"
+ return 1
+ fi
+ _debug _domain_id "$_domain_id"
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
+ _debug "Getting txt records"
+ _qc_rest GET "zones/${_domain_id}/records"
+
+ if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
+ _err "Error rm GET response: $response"
+ return 1
+ fi
+
+ _debug "Pre-jq response:" "$response"
+ # Do not use jq or subsequent code
+ #response=$(echo "$response" | jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")")
+ #_debug "get txt response" "$response"
+ #if [ "${response}" = "" ]; then
+ # _info "Don't need to remove txt records."
+ # return 0
+ #fi
+ #record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//')
+ #_debug "txt record_id" "$record_id"
+ #Instead of jq
+ array=$(echo "$response" | grep -o '\[[^]]*\]' | sed 's/^\[\(.*\)\]$/\1/')
+ if [ -z "$array" ]; then
+ _err "Expected array in QC response: $response"
+ return 1
+ fi
+ # Temporary file to hold matched content (one per line)
+ tmpfile=$(_mktemp)
+ echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile"
+ record_id=""
+
+ while IFS= read -r obj || [ -n "$obj" ]; do
+ if echo "$obj" | grep -q '"TXT"' && echo "$obj" | grep -q '"id"' && echo "$obj" | grep -q "$txtvalue"; then
+ _debug "response includes" "$obj"
+ record_id=$(echo "$obj" | sed 's/^\"id\":\([0-9]\+\).*/\1/')
+ break
+ fi
+ done <"$tmpfile"
+
+ rm "$tmpfile"
+
+ if [ -z "$record_id" ]; then
+ _info "TXT record, or $txtvalue not found, nothing to remove"
+ return 0
+ fi
+
+ #End of jq replacement
+ if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then
+ _info "Delete txt record error."
+ return 1
+ fi
+
+ _info "TXT Record ID: $record_id successfully deleted"
+ return 0
+
+}
+
+#################### Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=sdjkglgdfewsdfg
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ h=$(printf "%s" "$domain" | cut -d . -f2-)
+ _debug h "$h"
+ if [ -z "$h" ]; then
+ _err "$h ($domain) is an invalid domain"
+ return 1
+ fi
+
+ if ! _qc_rest GET "zones"; then
+ _err "qc_rest failed"
+ return 1
+ fi
+
+ if _contains "$response" "\"name\":\"$h\"" || _contains "$response" "\"name\":\"$h.\""; then
+ _domain_id=$h
+ if [ "$_domain_id" ]; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ return 0
+ fi
+ _err "Empty domain_id $h"
+ return 1
+ fi
+ _err "Missing domain_id $h"
+ return 1
+}
+
+_qc_rest() {
+ m=$1
+ ep="$2"
+ data="$3"
+ _debug "$ep"
+
+ email_trimmed=$(echo "$QC_API_EMAIL" | tr -d '"')
+ token_trimmed=$(echo "$QC_API_KEY" | tr -d '"')
+
+ export _H1="Content-Type: application/json"
+ export _H2="X-Auth-Email: $email_trimmed"
+ export _H3="X-Auth-Key: $token_trimmed"
+
+ if [ "$m" != "GET" ]; then
+ _debug data "$data"
+ response="$(_post "$data" "$QC_Api/$ep" "" "$m")"
+ else
+ response="$(_get "$QC_Api/$ep")"
+ fi
+
+ if [ "$?" != "0" ]; then
+ _err "error $ep"
+ return 1
+ fi
+ _debug2 response "$response"
+ return 0
+}
diff --git a/dnsapi/dns_rage4.sh b/dnsapi/dns_rage4.sh
index ad312759..c27fbc5f 100755
--- a/dnsapi/dns_rage4.sh
+++ b/dnsapi/dns_rage4.sh
@@ -42,6 +42,14 @@ dns_rage4_add() {
_debug _domain_id "$_domain_id"
_rage4_rest "createrecord/?id=$_domain_id&name=$fulldomain&content=$unquotedtxtvalue&type=TXT&active=true&ttl=1"
+
+ # Response after adding a TXT record should be something like this:
+ # {"status":true,"id":28160443,"error":null}
+ if ! _contains "$response" '"error":null' >/dev/null; then
+ _err "Error while adding TXT record: '$response'"
+ return 1
+ fi
+
return 0
}
@@ -63,7 +71,12 @@ dns_rage4_rm() {
_debug "Getting txt records"
_rage4_rest "getrecords/?id=${_domain_id}"
- _record_id=$(echo "$response" | sed -rn 's/.*"id":([[:digit:]]+)[^\}]*'"$txtvalue"'.*/\1/p')
+ _record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\1/p')
+ if [ -z "$_record_id" ]; then
+ _err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'."
+ return 1
+ fi
+
_rage4_rest "deleterecord/?id=${_record_id}"
return 0
}
@@ -105,8 +118,7 @@ _rage4_rest() {
token_trimmed=$(echo "$RAGE4_TOKEN" | tr -d '"')
auth=$(printf '%s:%s' "$username_trimmed" "$token_trimmed" | _base64)
- export _H1="Content-Type: application/json"
- export _H2="Authorization: Basic $auth"
+ export _H1="Authorization: Basic $auth"
response="$(_get "$RAGE4_Api$ep")"
diff --git a/dnsapi/dns_schlundtech.sh b/dnsapi/dns_schlundtech.sh
index 6d2930a2..21930110 100644
--- a/dnsapi/dns_schlundtech.sh
+++ b/dnsapi/dns_schlundtech.sh
@@ -7,7 +7,7 @@ Options:
SCHLUNDTECH_USER Username
SCHLUNDTECH_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/2246
-Author:
+Author: @mod242
'
SCHLUNDTECH_API="https://gateway.schlundtech.de"
diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh
index 8b52b24e..565f541b 100644
--- a/dnsapi/dns_selectel.sh
+++ b/dnsapi/dns_selectel.sh
@@ -4,11 +4,22 @@ dns_selectel_info='Selectel.com
Domains: Selectel.ru
Site: Selectel.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel
-Options:
- SL_Key API Key
+Options: For old API version v1 (deprecated)
+ SL_Ver API version. Use "v1".
+ SL_Key API Key
+OptionsAlt: For the current API version v2
+ SL_Ver API version. Use "v2".
+ SL_Login_ID Account ID
+ SL_Project_Name Project name
+ SL_Login_Name Service user name
+ SL_Pswd Service user password
+ SL_Expire Token lifetime. In minutes (0-1440). Default "1400"
+Issues: github.com/acmesh-official/acme.sh/issues/5126
'
-SL_Api="https://api.selectel.ru/domains/v1"
+SL_Api="https://api.selectel.ru/domains"
+auth_uri="https://cloud.api.selcloud.ru/identity/v3/auth/tokens"
+_sl_sep='#'
######## Public functions #####################
@@ -17,17 +28,14 @@ dns_selectel_add() {
fulldomain=$1
txtvalue=$2
- SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
-
- if [ -z "$SL_Key" ]; then
- SL_Key=""
- _err "You don't specify selectel.ru api key yet."
- _err "Please create you key and try again."
+ if ! _sl_init_vars; then
return 1
fi
-
- #save the api key to the account conf file.
- _saveaccountconf_mutable SL_Key "$SL_Key"
+ _debug2 SL_Ver "$SL_Ver"
+ _debug2 SL_Expire "$SL_Expire"
+ _debug2 SL_Login_Name "$SL_Login_Name"
+ _debug2 SL_Login_ID "$SL_Login_ID"
+ _debug2 SL_Project_Name "$SL_Project_Name"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
@@ -39,11 +47,63 @@ dns_selectel_add() {
_debug _domain "$_domain"
_info "Adding record"
- if _sl_rest POST "/$_domain_id/records/" "{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"$fulldomain\", \"content\": \"$txtvalue\"}"; then
- if _contains "$response" "$txtvalue" || _contains "$response" "record_already_exists"; then
+ if [ "$SL_Ver" = "v2" ]; then
+ _ext_srv1="/zones/"
+ _ext_srv2="/rrset/"
+ _text_tmp=$(echo "$txtvalue" | sed -En "s/[\"]*([^\"]*)/\1/p")
+ _text_tmp='\"'$_text_tmp'\"'
+ _data="{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"${fulldomain}.\", \"records\": [{\"content\":\"$_text_tmp\"}]}"
+ elif [ "$SL_Ver" = "v1" ]; then
+ _ext_srv1="/"
+ _ext_srv2="/records/"
+ _data="{\"type\":\"TXT\",\"ttl\":60,\"name\":\"$fulldomain\",\"content\":\"$txtvalue\"}"
+ else
+ _err "Error. Unsupported version API $SL_Ver"
+ return 1
+ fi
+ _ext_uri="${_ext_srv1}$_domain_id${_ext_srv2}"
+ _debug _ext_uri "$_ext_uri"
+ _debug _data "$_data"
+
+ if _sl_rest POST "$_ext_uri" "$_data"; then
+ if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
fi
+ if _contains "$response" "already_exists"; then
+ # record TXT with $fulldomain already exists
+ if [ "$SL_Ver" = "v2" ]; then
+ # It is necessary to add one more content to the comments
+ # read all records rrset
+ _debug "Getting txt records"
+ _sl_rest GET "${_ext_uri}"
+ # There is already a $txtvalue value, no need to add it
+ if _contains "$response" "$txtvalue"; then
+ _info "Added, OK"
+ _info "Txt record ${fulldomain} with value ${txtvalue} already exists"
+ return 0
+ fi
+ # group \1 - full record rrset; group \2 - records attribute value, exactly {"content":"\"value1\""},{"content":"\"value2\""}",...
+ _record_seg="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*${fulldomain}[^}]*records[^}]*\[(\{[^]]*\})\][^}]*}).*/\1/p")"
+ _record_array="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*${fulldomain}[^}]*records[^}]*\[(\{[^]]*\})\][^}]*}).*/\2/p")"
+ # record id
+ _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"")"
+ # preparing _data
+ _tmp_str="${_record_array},{\"content\":\"${_text_tmp}\"}"
+ _data="{\"ttl\": 60, \"records\": [${_tmp_str}]}"
+ _debug2 _record_seg "$_record_seg"
+ _debug2 _record_array "$_record_array"
+ _debug2 _record_array "$_record_id"
+ _debug "New data for record" "$_data"
+ if _sl_rest PATCH "${_ext_uri}${_record_id}" "$_data"; then
+ _info "Added, OK"
+ return 0
+ fi
+ elif [ "$SL_Ver" = "v1" ]; then
+ _info "Added, OK"
+ return 0
+ fi
+ fi
fi
_err "Add txt record error."
return 1
@@ -54,15 +114,15 @@ dns_selectel_rm() {
fulldomain=$1
txtvalue=$2
- SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
-
- if [ -z "$SL_Key" ]; then
- SL_Key=""
- _err "You don't specify slectel api key yet."
- _err "Please create you key and try again."
+ if ! _sl_init_vars "nosave"; then
return 1
fi
-
+ _debug2 SL_Ver "$SL_Ver"
+ _debug2 SL_Expire "$SL_Expire"
+ _debug2 SL_Login_Name "$SL_Login_Name"
+ _debug2 SL_Login_ID "$SL_Login_ID"
+ _debug2 SL_Project_Name "$SL_Project_Name"
+ #
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
@@ -71,91 +131,195 @@ dns_selectel_rm() {
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
-
+ #
+ if [ "$SL_Ver" = "v2" ]; then
+ _ext_srv1="/zones/"
+ _ext_srv2="/rrset/"
+ elif [ "$SL_Ver" = "v1" ]; then
+ _ext_srv1="/"
+ _ext_srv2="/records/"
+ else
+ _err "Error. Unsupported version API $SL_Ver"
+ return 1
+ fi
+ #
_debug "Getting txt records"
- _sl_rest GET "/${_domain_id}/records/"
-
+ _ext_uri="${_ext_srv1}$_domain_id${_ext_srv2}"
+ _debug _ext_uri "$_ext_uri"
+ _sl_rest GET "${_ext_uri}"
+ #
if ! _contains "$response" "$txtvalue"; then
_err "Txt record not found"
return 1
fi
-
- _record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")"
+ #
+ if [ "$SL_Ver" = "v2" ]; then
+ _record_seg="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\1/gp")"
+ _record_arr="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\3/p")"
+ elif [ "$SL_Ver" = "v1" ]; then
+ _record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")"
+ else
+ _err "Error. Unsupported version API $SL_Ver"
+ return 1
+ fi
_debug2 "_record_seg" "$_record_seg"
if [ -z "$_record_seg" ]; then
_err "can not find _record_seg"
return 1
fi
-
- _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2)"
- _debug2 "_record_id" "$_record_id"
+ # record id
+ # the following lines change the algorithm for deleting records with the value $txtvalue
+ # if you use the 1st line, then all such records are deleted at once
+ # if you use the 2nd line, then only the first entry from them is deleted
+ #_record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"")"
+ _record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"" | sed '1!d')"
if [ -z "$_record_id" ]; then
_err "can not find _record_id"
return 1
fi
-
- if ! _sl_rest DELETE "/$_domain_id/records/$_record_id"; then
- _err "Delete record error."
- return 1
+ _debug2 "_record_id" "$_record_id"
+ # delete all record type TXT with text $txtvalue
+ if [ "$SL_Ver" = "v2" ]; then
+ # actual
+ _new_arr="$(echo "$_record_seg" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\3/gp" | sed -En "s/(\},\{)/}\n{/gp" | sed "/${txtvalue}/d" | sed ":a;N;s/\n/,/;ta")"
+ # uri record for DEL or PATCH
+ _del_uri="${_ext_uri}${_record_id}"
+ _debug _del_uri "$_del_uri"
+ if [ -z "$_new_arr" ]; then
+ # remove record
+ if ! _sl_rest DELETE "${_del_uri}"; then
+ _err "Delete record error: ${_del_uri}."
+ else
+ info "Delete record success: ${_del_uri}."
+ fi
+ else
+ # update a record by removing one element in content
+ _data="{\"ttl\": 60, \"records\": [${_new_arr}]}"
+ _debug2 _data "$_data"
+ # REST API PATCH call
+ if _sl_rest PATCH "${_del_uri}" "$_data"; then
+ _info "Patched, OK: ${_del_uri}"
+ else
+ _err "Patched record error: ${_del_uri}."
+ fi
+ fi
+ else
+ # legacy
+ for _one_id in $_record_id; do
+ _del_uri="${_ext_uri}${_one_id}"
+ _debug _del_uri "$_del_uri"
+ if ! _sl_rest DELETE "${_del_uri}"; then
+ _err "Delete record error: ${_del_uri}."
+ else
+ info "Delete record success: ${_del_uri}."
+ fi
+ done
fi
return 0
}
#################### Private functions below ##################################
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
+
_get_root() {
domain=$1
- if ! _sl_rest GET "/"; then
- return 1
- fi
-
- i=2
- p=1
- while true; do
- h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
- _debug h "$h"
- if [ -z "$h" ]; then
- #not valid
+ if [ "$SL_Ver" = 'v1' ]; then
+ # version API 1
+ if ! _sl_rest GET "/"; then
return 1
fi
-
- if _contains "$response" "\"name\" *: *\"$h\","; then
- _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain=$h
- _debug "Getting domain id for $h"
- if ! _sl_rest GET "/$h"; then
+ i=2
+ p=1
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug h "$h"
+ if [ -z "$h" ]; then
return 1
fi
- _domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)"
- return 0
+ if _contains "$response" "\"name\" *: *\"$h\","; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ _debug "Getting domain id for $h"
+ if ! _sl_rest GET "/$h"; then
+ _err "Error read records of all domains $SL_Ver"
+ return 1
+ fi
+ _domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)"
+ return 0
+ fi
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+ _err "Error read records of all domains $SL_Ver"
+ return 1
+ elif [ "$SL_Ver" = "v2" ]; then
+ # version API 2
+ _ext_uri='/zones/'
+ domain="${domain}."
+ _debug "domain:: " "$domain"
+ # read records of all domains
+ if ! _sl_rest GET "$_ext_uri"; then
+ _err "Error read records of all domains $SL_Ver"
+ return 1
fi
- p=$i
- i=$(_math "$i" + 1)
- done
- return 1
+ i=1
+ p=1
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug h "$h"
+ if [ -z "$h" ]; then
+ _err "The domain was not found among the registered ones"
+ return 1
+ fi
+ _domain_record=$(echo "$response" | sed -En "s/.*(\{[^}]*id[^}]*\"name\" *: *\"$h\"[^}]*}).*/\1/p")
+ _debug "_domain_record:: " "$_domain_record"
+ if [ -n "$_domain_record" ]; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ _debug "Getting domain id for $h"
+ _domain_id=$(echo "$_domain_record" | sed -En "s/\{[^}]*\"id\" *: *\"([^\"]*)\"[^}]*\}/\1/p")
+ return 0
+ fi
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+ _err "Error read records of all domains $SL_Ver"
+ return 1
+ else
+ _err "Error. Unsupported version API $SL_Ver"
+ return 1
+ fi
}
+#################################################################
+# use: method add_url body
_sl_rest() {
m=$1
ep="$2"
data="$3"
- _debug "$ep"
- export _H1="X-Token: $SL_Key"
+ _token=$(_get_auth_token)
+ if [ -z "$_token" ]; then
+ _err "BAD key or token $ep"
+ return 1
+ fi
+ if [ "$SL_Ver" = v2 ]; then
+ _h1_name="X-Auth-Token"
+ else
+ _h1_name='X-Token'
+ fi
+ export _H1="${_h1_name}: ${_token}"
export _H2="Content-Type: application/json"
-
+ _debug2 "Full URI: " "$SL_Api/${SL_Ver}${ep}"
+ _debug2 "_H1:" "$_H1"
+ _debug2 "_H2:" "$_H2"
if [ "$m" != "GET" ]; then
_debug data "$data"
- response="$(_post "$data" "$SL_Api/$ep" "" "$m")"
+ response="$(_post "$data" "$SL_Api/${SL_Ver}${ep}" "" "$m")"
else
- response="$(_get "$SL_Api/$ep")"
+ response="$(_get "$SL_Api/${SL_Ver}${ep}")"
fi
-
+ # shellcheck disable=SC2181
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
@@ -163,3 +327,152 @@ _sl_rest() {
_debug2 response "$response"
return 0
}
+
+_get_auth_token() {
+ if [ "$SL_Ver" = 'v1' ]; then
+ # token for v1
+ _debug "Token v1"
+ _token_keystone=$SL_Key
+ elif [ "$SL_Ver" = 'v2' ]; then
+ # token for v2. Get a token for calling the API
+ _debug "Keystone Token v2"
+ token_v2=$(_readaccountconf_mutable SL_Token_V2)
+ if [ -n "$token_v2" ]; then
+ # The structure with the token was considered. Let's check its validity
+ # field 1 - SL_Login_Name
+ # field 2 - token keystone
+ # field 3 - SL_Login_ID
+ # field 4 - SL_Project_Name
+ # field 5 - Receipt time
+ # separator - '$_sl_sep'
+ _login_name=$(_getfield "$token_v2" 1 "$_sl_sep")
+ _token_keystone=$(_getfield "$token_v2" 2 "$_sl_sep")
+ _project_name=$(_getfield "$token_v2" 4 "$_sl_sep")
+ _receipt_time=$(_getfield "$token_v2" 5 "$_sl_sep")
+ _login_id=$(_getfield "$token_v2" 3 "$_sl_sep")
+ _debug2 _login_name "$_login_name"
+ _debug2 _login_id "$_login_id"
+ _debug2 _project_name "$_project_name"
+ # check the validity of the token for the user and the project and its lifetime
+ _dt_diff_minute=$((($(date +%s) - _receipt_time) / 60))
+ _debug2 _dt_diff_minute "$_dt_diff_minute"
+ [ "$_dt_diff_minute" -gt "$SL_Expire" ] && unset _token_keystone
+ if [ "$_project_name" != "$SL_Project_Name" ] || [ "$_login_name" != "$SL_Login_Name" ] || [ "$_login_id" != "$SL_Login_ID" ]; then
+ unset _token_keystone
+ fi
+ _debug "Get exists token"
+ fi
+ if [ -z "$_token_keystone" ]; then
+ # the previous token is incorrect or was not received, get a new one
+ _debug "Update (get new) token"
+ _data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}"
+ export _H1="Content-Type: application/json"
+ _result=$(_post "$_data_auth" "$auth_uri")
+ _token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p")
+ _dt_curr=$(date +%s)
+ SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}"
+ _saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2"
+ fi
+ else
+ # token set empty for unsupported version API
+ _token_keystone=""
+ fi
+ printf -- "%s" "$_token_keystone"
+}
+
+#################################################################
+# use: [non_save]
+_sl_init_vars() {
+ _non_save="${1}"
+ _debug2 _non_save "$_non_save"
+
+ _debug "First init variables"
+ # version API
+ SL_Ver="${SL_Ver:-$(_readaccountconf_mutable SL_Ver)}"
+ if [ -z "$SL_Ver" ]; then
+ SL_Ver="v1"
+ fi
+ if ! [ "$SL_Ver" = "v1" ] && ! [ "$SL_Ver" = "v2" ]; then
+ _err "You don't specify selectel.ru API version."
+ _err "Please define specify API version."
+ fi
+ _debug2 SL_Ver "$SL_Ver"
+ if [ "$SL_Ver" = "v1" ]; then
+ # token
+ SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
+
+ if [ -z "$SL_Key" ]; then
+ SL_Key=""
+ _err "You don't specify selectel.ru api key yet."
+ _err "Please create you key and try again."
+ return 1
+ fi
+ #save the api key to the account conf file.
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Key "$SL_Key"
+ fi
+ elif [ "$SL_Ver" = "v2" ]; then
+ # time expire token
+ SL_Expire="${SL_Expire:-$(_readaccountconf_mutable SL_Expire)}"
+ if [ -z "$SL_Expire" ]; then
+ SL_Expire=1400 # 23h 20 min
+ fi
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Expire "$SL_Expire"
+ fi
+ # login service user
+ SL_Login_Name="${SL_Login_Name:-$(_readaccountconf_mutable SL_Login_Name)}"
+ if [ -z "$SL_Login_Name" ]; then
+ SL_Login_Name=''
+ _err "You did not specify the selectel.ru API service user name."
+ _err "Please provide a service user name and try again."
+ return 1
+ fi
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Login_Name "$SL_Login_Name"
+ fi
+ # user ID
+ SL_Login_ID="${SL_Login_ID:-$(_readaccountconf_mutable SL_Login_ID)}"
+ if [ -z "$SL_Login_ID" ]; then
+ SL_Login_ID=''
+ _err "You did not specify the selectel.ru API user ID."
+ _err "Please provide a user ID and try again."
+ return 1
+ fi
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Login_ID "$SL_Login_ID"
+ fi
+ # project name
+ SL_Project_Name="${SL_Project_Name:-$(_readaccountconf_mutable SL_Project_Name)}"
+ if [ -z "$SL_Project_Name" ]; then
+ SL_Project_Name=''
+ _err "You did not specify the project name."
+ _err "Please provide a project name and try again."
+ return 1
+ fi
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Project_Name "$SL_Project_Name"
+ fi
+ # service user password
+ SL_Pswd="${SL_Pswd:-$(_readaccountconf_mutable SL_Pswd)}"
+ if [ -z "$SL_Pswd" ]; then
+ SL_Pswd=''
+ _err "You did not specify the service user password."
+ _err "Please provide a service user password and try again."
+ return 1
+ fi
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Pswd "$SL_Pswd" "12345678"
+ fi
+ else
+ SL_Ver=""
+ _err "You also specified the wrong version of the selectel.ru API."
+ _err "Please provide the correct API version and try again."
+ return 1
+ fi
+ if [ -z "$_non_save" ]; then
+ _saveaccountconf_mutable SL_Ver "$SL_Ver"
+ fi
+
+ return 0
+}
diff --git a/dnsapi/dns_sotoon.sh b/dnsapi/dns_sotoon.sh
new file mode 100644
index 00000000..b94a220f
--- /dev/null
+++ b/dnsapi/dns_sotoon.sh
@@ -0,0 +1,309 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_sotoon_info='Sotoon.ir
+Site: Sotoon.ir
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon
+Options:
+ Sotoon_Token API Token
+ Sotoon_WorkspaceUUID Workspace UUID
+Issues: github.com/acmesh-official/acme.sh/issues/6656
+Author: Erfan Gholizade
+'
+
+SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/global"
+
+######## Public functions #####################
+
+#Adding the txt record for validation.
+#Usage: dns_sotoon_add fulldomain TXT_record
+#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_sotoon_add() {
+ fulldomain=$1
+ txtvalue=$2
+ _info_sotoon "Using Sotoon"
+
+ Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
+ Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
+
+ if [ -z "$Sotoon_Token" ]; then
+ _err_sotoon "You didn't specify \"Sotoon_Token\" token yet."
+ _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens"
+ return 1
+ fi
+ if [ -z "$Sotoon_WorkspaceUUID" ]; then
+ _err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet."
+ _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces"
+ return 1
+ fi
+
+ #save the info to the account conf file.
+ _saveaccountconf_mutable Sotoon_Token "$Sotoon_Token"
+ _saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID"
+
+ _debug_sotoon "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err_sotoon "invalid domain"
+ return 1
+ fi
+
+ _info_sotoon "Adding record"
+
+ _debug_sotoon _domain_id "$_domain_id"
+ _debug_sotoon _sub_domain "$_sub_domain"
+ _debug_sotoon _domain "$_domain"
+
+ # First, GET the current domain zone to check for existing TXT records
+ # This is needed for wildcard certs which require multiple TXT values
+ _info_sotoon "Checking for existing TXT records"
+ if ! _sotoon_rest GET "$_domain_id"; then
+ _err_sotoon "Failed to get domain zone"
+ return 1
+ fi
+
+ # Check if there are existing TXT records for this subdomain
+ _existing_txt=""
+ if _contains "$response" "\"$_sub_domain\""; then
+ _debug_sotoon "Found existing records for $_sub_domain"
+ # Extract existing TXT values from the response
+ # The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}]
+ _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
+ _debug_sotoon "Existing TXT records: $_existing_txt"
+ fi
+
+ # Build the new record entry
+ _new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}"
+
+ # If there are existing records, append to them; otherwise create new array
+ if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then
+ # Check if this exact TXT value already exists (avoid duplicates)
+ if _contains "$_existing_txt" "\"$txtvalue\""; then
+ _info_sotoon "TXT record already exists, skipping"
+ return 0
+ fi
+ # Remove the closing bracket and append new record
+ _combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]"
+ _debug_sotoon "Combined records: $_combined_records"
+ else
+ # No existing records, create new array
+ _combined_records="[$_new_record]"
+ fi
+
+ # Prepare the DNS record data in Kubernetes CRD format
+ _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}"
+
+ _debug_sotoon "DNS record payload: $_dns_record"
+
+ # Use PATCH to update/add the record to the domain zone
+ _info_sotoon "Updating domain zone $_domain_id with TXT record"
+ if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
+ if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then
+ _info_sotoon "Added, OK"
+ return 0
+ else
+ _debug_sotoon "Response: $response"
+ _err_sotoon "Add txt record error."
+ return 1
+ fi
+ fi
+
+ _err_sotoon "Add txt record error."
+ return 1
+}
+
+#Remove the txt record after validation.
+#Usage: dns_sotoon_rm fulldomain TXT_record
+#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_sotoon_rm() {
+ fulldomain=$1
+ txtvalue=$2
+ _info_sotoon "Using Sotoon"
+ _debug_sotoon fulldomain "$fulldomain"
+ _debug_sotoon txtvalue "$txtvalue"
+
+ Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
+ Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
+
+ _debug_sotoon "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err_sotoon "invalid domain"
+ return 1
+ fi
+ _debug_sotoon _domain_id "$_domain_id"
+ _debug_sotoon _sub_domain "$_sub_domain"
+ _debug_sotoon _domain "$_domain"
+
+ _info_sotoon "Removing TXT record"
+
+ # First, GET the current domain zone to check for existing TXT records
+ if ! _sotoon_rest GET "$_domain_id"; then
+ _err_sotoon "Failed to get domain zone"
+ return 1
+ fi
+
+ # Check if there are existing TXT records for this subdomain
+ _existing_txt=""
+ if _contains "$response" "\"$_sub_domain\""; then
+ _debug_sotoon "Found existing records for $_sub_domain"
+ _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
+ _debug_sotoon "Existing TXT records: $_existing_txt"
+ fi
+
+ # If no existing records, nothing to remove
+ if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then
+ _info_sotoon "No TXT records found, nothing to remove"
+ return 0
+ fi
+
+ # Remove the specific TXT value from the array
+ # This handles the case where there are multiple TXT values (wildcard certs)
+ _remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g')
+ _debug_sotoon "Remaining records after removal: $_remaining_records"
+
+ # If no records remain, set to null to remove the subdomain entirely
+ if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then
+ _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}"
+ else
+ _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}"
+ fi
+
+ _debug_sotoon "Remove record payload: $_dns_record"
+
+ # Use PATCH to remove the record from the domain zone
+ if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
+ _info_sotoon "Record removed, OK"
+ return 0
+ else
+ _debug_sotoon "Response: $response"
+ _err_sotoon "Error removing record"
+ return 1
+ fi
+}
+
+#################### Private functions below ##################################
+
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ _debug_sotoon "Getting root domain for: $domain"
+ _debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID"
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug_sotoon "Checking domain part: $h"
+
+ if [ -z "$h" ]; then
+ #not valid
+ _err_sotoon "Could not find valid domain"
+ return 1
+ fi
+
+ _debug_sotoon "Fetching domain zones from Sotoon API"
+ if ! _sotoon_rest GET ""; then
+ _err_sotoon "Failed to get domain zones from Sotoon API"
+ _err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID"
+ return 1
+ fi
+
+ _debug2_sotoon "API Response: $response"
+
+ # Check if the response contains our domain
+ # Sotoon API uses Kubernetes CRD format with spec.origin for domain matching
+ if _contains "$response" "\"origin\":\"$h\""; then
+ _debug_sotoon "Found domain by origin: $h"
+
+ # In Kubernetes CRD format, the metadata.name is the resource identifier
+ # The name can be either:
+ # 1. Same as origin
+ # 2. Origin with dots replaced by hyphens
+ # We check both patterns in the response to determine which one exists
+
+ # Convert origin to hyphenated version for checking
+ _h_hyphenated=$(echo "$h" | tr '.' '-')
+
+ # Check if the hyphenated name exists in the response
+ if _contains "$response" "\"name\":\"$_h_hyphenated\""; then
+ _domain_id="$_h_hyphenated"
+ _debug_sotoon "Found domain ID (hyphenated): $_domain_id"
+ # Check if the origin itself is used as name
+ elif _contains "$response" "\"name\":\"$h\""; then
+ _domain_id="$h"
+ _debug_sotoon "Found domain ID (same as origin): $_domain_id"
+ else
+ # Fallback: use the hyphenated version (more common)
+ _domain_id="$_h_hyphenated"
+ _debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id"
+ fi
+
+ if [ -n "$_domain_id" ]; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ _debug_sotoon "Domain ID (metadata.name): $_domain_id"
+ _debug_sotoon "Sub domain: $_sub_domain"
+ _debug_sotoon "Domain (origin): $_domain"
+ return 0
+ fi
+ _err_sotoon "Found domain $h but could not extract domain ID"
+ return 1
+ fi
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+ return 1
+}
+
+_sotoon_rest() {
+ mtd="$1"
+ resource_id="$2"
+ data="$3"
+
+ token_trimmed=$(echo "$Sotoon_Token" | tr -d '"')
+
+ # Construct the API endpoint
+ _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/domainzones"
+
+ if [ -n "$resource_id" ]; then
+ _api_path="$_api_path/$resource_id"
+ fi
+
+ _debug_sotoon "API Path: $_api_path"
+ _debug_sotoon "Method: $mtd"
+
+ # Set authorization header - Sotoon API uses Bearer token
+ export _H1="Authorization: Bearer $token_trimmed"
+
+ if [ "$mtd" = "GET" ]; then
+ # GET request
+ _debug_sotoon "GET" "$_api_path"
+ response="$(_get "$_api_path")"
+ elif [ "$mtd" = "PATCH" ]; then
+ # PATCH Request
+ export _H2="Content-Type: application/merge-patch+json"
+ _debug_sotoon data "$data"
+ response="$(_post "$data" "$_api_path" "" "$mtd")"
+ else
+ _err_sotoon "Unknown method: $mtd"
+ return 1
+ fi
+
+ _debug2_sotoon response "$response"
+ return 0
+}
+
+#Wrappers for logging
+_info_sotoon() {
+ _info "[Sotoon]" "$@"
+}
+
+_err_sotoon() {
+ _err "[Sotoon]" "$@"
+}
+
+_debug_sotoon() {
+ _debug "[Sotoon]" "$@"
+}
+
+_debug2_sotoon() {
+ _debug2 "[Sotoon]" "$@"
+}
diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh
new file mode 100644
index 00000000..8fff4037
--- /dev/null
+++ b/dnsapi/dns_spaceship.sh
@@ -0,0 +1,212 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_spaceship_info='Spaceship.com
+Site: Spaceship.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship
+Options:
+ SPACESHIP_API_KEY API Key
+ SPACESHIP_API_SECRET API Secret
+ SPACESHIP_ROOT_DOMAIN Root domain. Manually specify the root domain if auto-detection fails. Optional.
+Issues: github.com/acmesh-official/acme.sh/issues/6304
+Author: Meow <@Meo597>
+'
+
+# 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 \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") 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 manual override
+ 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"
+ _savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN"
+ return 0
+ fi
+
+ _debug "Detecting root zone for '$domain'"
+
+ i=1
+ 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 "Cut resulted in empty string, root zone not found."
+ break
+ fi
+
+ # 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
+ _savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain"
+ _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'. Please set SPACESHIP_ROOT_DOMAIN manually."
+ return 1
+}
+
+_spaceship_api_request() {
+ method="$1"
+ url="$2"
+ payload="$3"
+
+ _debug2 "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
+
+ _debug2 "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
+
+ _debug2 "API response header: $HTTP_HEADER"
+ return 1
+}
diff --git a/dnsapi/dns_technitium.sh b/dnsapi/dns_technitium.sh
index a50db97c..7bc0dd48 100755
--- a/dnsapi/dns_technitium.sh
+++ b/dnsapi/dns_technitium.sh
@@ -1,13 +1,12 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
-dns_Technitium_info='Technitium DNS Server
-
-Site: https://technitium.com/dns/
+dns_technitium_info='Technitium DNS Server
+Site: Technitium.com/dns/
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium
Options:
Technitium_Server Server Address
Technitium_Token API Token
-Issues:https://github.com/acmesh-official/acme.sh/issues/6116
+Issues: github.com/acmesh-official/acme.sh/issues/6116
Author: Henning Reich
'
diff --git a/dnsapi/dns_tele3.sh b/dnsapi/dns_tele3.sh
index e5974951..3a3ccf8c 100644
--- a/dnsapi/dns_tele3.sh
+++ b/dnsapi/dns_tele3.sh
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#tele3
Options:
TELE3_Key API Key
TELE3_Secret API Secret
-Author: Roman Blizik
+Author: Roman Blizik <@par-pa>
'
TELE3_API="https://www.tele3.cz/acme/"
diff --git a/dnsapi/dns_tencent.sh b/dnsapi/dns_tencent.sh
index d82768b9..b148adc3 100644
--- a/dnsapi/dns_tencent.sh
+++ b/dnsapi/dns_tencent.sh
@@ -2,7 +2,7 @@
# shellcheck disable=SC2034
dns_tencent_info='Tencent.com
Site: cloud.Tencent.com
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_tencent
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_tencent
Options:
Tencent_SecretId Secret ID
Tencent_SecretKey Secret Key
diff --git a/dnsapi/dns_timeweb.sh b/dnsapi/dns_timeweb.sh
index 544564ea..7040ac9a 100644
--- a/dnsapi/dns_timeweb.sh
+++ b/dnsapi/dns_timeweb.sh
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_timeweb
Options:
TW_Token API JWT token. Get it from the control panel at https://timeweb.cloud/my/api-keys
Issues: github.com/acmesh-official/acme.sh/issues/5140
-Author: Nikolay Pronchev
+Author: Nikolay Pronchev <@nikolaypronchev>
'
TW_Api="https://api.timeweb.cloud/api/v1"
diff --git a/dnsapi/dns_transip.sh b/dnsapi/dns_transip.sh
index 2abbe34d..b3c5ed70 100644
--- a/dnsapi/dns_transip.sh
+++ b/dnsapi/dns_transip.sh
@@ -24,7 +24,7 @@ dns_transip_add() {
_debug txtvalue="$txtvalue"
_transip_setup "$fulldomain" || return 1
_info "Creating TXT record."
- if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then
+ if ! _transip_rest POST "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":60}}"; then
_err "Could not add TXT record."
return 1
fi
@@ -38,7 +38,7 @@ dns_transip_rm() {
_debug txtvalue="$txtvalue"
_transip_setup "$fulldomain" || return 1
_info "Removing TXT record."
- if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":300}}"; then
+ if ! _transip_rest DELETE "domains/$_domain/dns" "{\"dnsEntry\":{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"expire\":60}}"; then
_err "Could not remove TXT record $_sub_domain for $domain"
return 1
fi
diff --git a/dnsapi/dns_udr.sh b/dnsapi/dns_udr.sh
index f9772e10..656a0557 100644
--- a/dnsapi/dns_udr.sh
+++ b/dnsapi/dns_udr.sh
@@ -7,7 +7,7 @@ Options:
UDR_USER Username
UDR_PASS Password
Issues: github.com/acmesh-official/acme.sh/issues/3923
-Author: Andreas Scherer
+Author: Andreas Scherer <@andischerer>
'
UDR_API="https://api.domainreselling.de/api/call.cgi"
diff --git a/dnsapi/dns_variomedia.sh b/dnsapi/dns_variomedia.sh
index fa38bbb6..4620b854 100644
--- a/dnsapi/dns_variomedia.sh
+++ b/dnsapi/dns_variomedia.sh
@@ -74,7 +74,7 @@ dns_variomedia_rm() {
return 1
fi
- _record_id="$(echo "$response" | sed -E 's/,"tags":\[[^]]*\]//g' | cut -d '[' -f2 | cut -d']' -f1 | sed 's/},[ \t]*{/\},§\{/g' | tr § '\n' | grep "$_sub_domain" | grep -- "$txtvalue" | sed 's/^{//;s/}[,]?$//' | tr , '\n' | tr -d '\"' | grep ^id | cut -d : -f2 | tr -d ' ')"
+ _record_id="$(echo "$response" | sed -E 's/,"tags":\[[^]]*\]//g' | cut -d '[' -f3 | cut -d']' -f1 | sed 's/},[ \t]*{/\},§\{/g' | tr § '\n' | grep -i "$_sub_domain" | grep -- "$txtvalue" | sed 's/^{//;s/}[,]?$//' | tr , '\n' | tr -d '\"' | grep ^id | cut -d : -f2 | tr -d ' ')"
_debug _record_id "$_record_id"
if [ "$_record_id" ]; then
_info "Successfully retrieved the record id for ACME challenge."
diff --git a/dnsapi/dns_virakcloud.sh b/dnsapi/dns_virakcloud.sh
new file mode 100755
index 00000000..7ae665d2
--- /dev/null
+++ b/dnsapi/dns_virakcloud.sh
@@ -0,0 +1,229 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_virakcloud_info='VirakCloud DNS API
+Site: VirakCloud.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_virakcloud
+Options:
+ VIRAKCLOUD_API_TOKEN VirakCloud API Bearer Token
+'
+
+VIRAKCLOUD_API_URL="https://public-api.virakcloud.com/dns"
+
+######## Public functions #####################
+
+#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+#Used to add txt record
+dns_virakcloud_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}"
+
+ if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then
+ _err "You haven't configured your VirakCloud API token yet."
+ _err "Please set VIRAKCLOUD_API_TOKEN environment variable or run:"
+ _err " export VIRAKCLOUD_API_TOKEN=\"your-api-token\""
+ return 1
+ fi
+
+ _saveaccountconf_mutable VIRAKCLOUD_API_TOKEN "$VIRAKCLOUD_API_TOKEN"
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ if [ "$http_code" = "401" ]; then
+ return 1
+ fi
+ _err "Invalid domain"
+ return 1
+ fi
+
+ _debug _domain "$_domain"
+ _debug fulldomain "$fulldomain"
+
+ _info "Adding TXT record"
+
+ if _virakcloud_rest POST "domains/${_domain}/records" "{\"record\":\"${fulldomain}\",\"type\":\"TXT\",\"ttl\":3600,\"content\":\"${txtvalue}\"}"; then
+ if echo "$response" | grep -q "success" || echo "$response" | grep -q "\"data\""; then
+ _info "Added, OK"
+ return 0
+ elif echo "$response" | grep -q "already exists" || echo "$response" | grep -q "duplicate"; then
+ _info "Record already exists, OK"
+ return 0
+ else
+ _err "Add TXT record error."
+ _err "Response: $response"
+ return 1
+ fi
+ fi
+
+ _err "Add TXT record error."
+ return 1
+}
+
+#Usage: fulldomain txtvalue
+#Used to remove the txt record after validation
+dns_virakcloud_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}"
+
+ if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then
+ _err "You haven't configured your VirakCloud API token yet."
+ return 1
+ fi
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ if [ "$http_code" = "401" ]; then
+ return 1
+ fi
+ _err "Invalid domain"
+ return 1
+ fi
+
+ _debug _domain "$_domain"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ _info "Removing TXT record"
+
+ _debug "Getting list of records to find content ID"
+ if ! _virakcloud_rest GET "domains/${_domain}/records" ""; then
+ return 1
+ fi
+
+ _debug2 "Records response" "$response"
+
+ contentid=""
+ # Extract innermost objects (content objects) which look like {"id":"...","content_raw":"..."}
+ # We filter for the one containing txtvalue
+
+ target_obj=$(echo "$response" | grep -o '{[^}]*}' | grep "$txtvalue" | _head_n 1)
+
+ if [ -n "$target_obj" ]; then
+ contentid=$(echo "$target_obj" | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4)
+ fi
+
+ if [ -z "$contentid" ]; then
+ _debug "Could not find matching record ID in response"
+ _info "Record not found, may have been already removed"
+ return 0
+ fi
+
+ _debug contentid "$contentid"
+
+ if _virakcloud_rest DELETE "domains/${_domain}/records/${fulldomain}/TXT/${contentid}" ""; then
+ if echo "$response" | grep -q "success" || [ -z "$response" ]; then
+ _info "Removed, OK"
+ return 0
+ elif echo "$response" | grep -q "not found" || echo "$response" | grep -q "404"; then
+ _info "Record not found, OK"
+ return 0
+ else
+ _err "Remove TXT record error."
+ _err "Response: $response"
+ return 1
+ fi
+ fi
+
+ _err "Remove TXT record error."
+ return 1
+}
+
+#################### Private functions below ##################################
+
+#_acme-challenge.www.domain.com
+#returns
+# _domain=domain.com
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ # Optimization: skip _acme-challenge subdomain to avoid 422 errors
+ if echo "$domain" | grep -q "^_acme-challenge."; then
+ i=2
+ fi
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug h "$h"
+
+ if [ -z "$h" ]; then
+ return 1
+ fi
+
+ if ! _virakcloud_rest GET "domains/$h" ""; then
+ http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ if [ "$http_code" = "401" ]; then
+ return 1
+ fi
+ p=$i
+ i=$(_math "$i" + 1)
+ continue
+ fi
+
+ if echo "$response" | grep -q "\"name\""; then
+ _domain="$h"
+ return 0
+ fi
+
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+
+ return 1
+}
+
+_virakcloud_rest() {
+ m=$1
+ ep="$2"
+ data="$3"
+
+ _debug "$ep"
+
+ export _H1="Content-Type: application/json"
+ export _H2="Authorization: Bearer $VIRAKCLOUD_API_TOKEN"
+
+ if [ "$m" != "GET" ]; then
+ _debug data "$data"
+ response="$(_post "$data" "$VIRAKCLOUD_API_URL/$ep" "" "$m")"
+ else
+ response="$(_get "$VIRAKCLOUD_API_URL/$ep")"
+ fi
+
+ _ret="$?"
+
+ if [ "$_ret" != "0" ]; then
+ _err "error on $m $ep"
+ return 1
+ fi
+
+ http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ _debug "http response code" "$http_code"
+
+ if [ "$http_code" = "401" ]; then
+ _err "VirakCloud API returned 401 Unauthorized."
+ _err "Your VIRAKCLOUD_API_TOKEN is invalid or expired."
+ _err "Please check your API token and try again."
+ return 1
+ fi
+
+ if [ "$http_code" = "403" ]; then
+ _err "VirakCloud API returned 403 Forbidden."
+ _err "Your API token does not have permission to access this resource."
+ return 1
+ fi
+
+ if [ -n "$http_code" ] && [ "$http_code" -ge 400 ]; then
+ _err "VirakCloud API error. HTTP code: $http_code"
+ _err "Response: $response"
+ return 1
+ fi
+
+ _debug2 response "$response"
+ return 0
+}
diff --git a/dnsapi/dns_vscale.sh b/dnsapi/dns_vscale.sh
index c3915c69..faf3105d 100755
--- a/dnsapi/dns_vscale.sh
+++ b/dnsapi/dns_vscale.sh
@@ -5,7 +5,7 @@ Site: vscale.io
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_vscale
Options:
VSCALE_API_KEY API Key
-Author: Alex Loban
+Author: Alex Loban <@LAV45>
'
VSCALE_API_URL="https://api.vscale.io/v1"
diff --git a/dnsapi/dns_vultr.sh b/dnsapi/dns_vultr.sh
index 61ec3f60..4002e5de 100644
--- a/dnsapi/dns_vultr.sh
+++ b/dnsapi/dns_vultr.sh
@@ -6,7 +6,6 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_vultr
Options:
VULTR_API_KEY API Key
Issues: github.com/acmesh-official/acme.sh/issues/2374
-Author:
'
VULTR_Api="https://api.vultr.com/v2"
diff --git a/dnsapi/dns_websupport.sh b/dnsapi/dns_websupport.sh
index bfc4b23a..2374afc3 100644
--- a/dnsapi/dns_websupport.sh
+++ b/dnsapi/dns_websupport.sh
@@ -7,7 +7,7 @@ Options:
WS_ApiKey API Key. Called "Identifier" in the WS Admin
WS_ApiSecret API Secret. Called "Secret key" in the WS Admin
Issues: github.com/acmesh-official/acme.sh/issues/3486
-Author: trgo.sk , akulumbeg
+Author: trgo.sk <@trgosk>, @akulumbeg
'
# Requirements: API Key and Secret from https://admin.websupport.sk/en/auth/apiKey
diff --git a/dnsapi/dns_west_cn.sh b/dnsapi/dns_west_cn.sh
index d0bb7d49..b873bfc0 100644
--- a/dnsapi/dns_west_cn.sh
+++ b/dnsapi/dns_west_cn.sh
@@ -1,9 +1,13 @@
#!/usr/bin/env sh
-
-# West.cn Domain api
-#WEST_Username="username"
-#WEST_Key="sADDsdasdgdsf"
-#Set key at https://www.west.cn/manager/API/APIconfig.asp
+# shellcheck disable=SC2034
+dns_west_cn_info='West.cn
+Site: West.cn
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_west_cn
+Options:
+ WEST_Username API username
+ WEST_Key API Key. Set at https://www.west.cn/manager/API/APIconfig.asp
+Issues: github.com/acmesh-official/acme.sh/issues/4894
+'
REST_API="https://api.west.cn/API/v2"
diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh
index 0febbad9..dc295330 100644
--- a/dnsapi/dns_world4you.sh
+++ b/dnsapi/dns_world4you.sh
@@ -7,7 +7,7 @@ Options:
WORLD4YOU_USERNAME Username
WORLD4YOU_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/3269
-Author: Lorenz Stechauner
+Author: Lorenz Stechauner <@NerLOR>
'
WORLD4YOU_API="https://my.world4you.com/en"
@@ -202,7 +202,7 @@ _get_paketnr() {
fqdn="$1"
form="$2"
- domains=$(echo "$form" | grep ' |