/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
+ _info "_message" "$_message"
+ if [ -z "$_message" ]; then
+ _err "Fail to upload certificate."
+ return 1
+ fi
+
+ for DOMAIN_ID in $DEPLOY_KEYHELP_DOMAIN_ID; do
+ _info "Apply certificate to domain id $DOMAIN_ID"
+ _response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=$DOMAIN_ID")
+ cert_value=$(echo "$_response" | grep "$certificate_name" | sed -n 's/.*value="\([^"]*\).*/\1/p')
+ target_type=$(echo "$_response" | grep 'target_type' | grep 'checked' | sed -n 's/.*value="\([^"]*\).*/\1/p')
+ if [ "$target_type" = "directory" ]; then
+ path=$(echo "$_response" | awk '/name="path"/{getline; print}' | sed -n 's/.*value="\([^"]*\).*/\1/p')
+ fi
+ echo "$_response" | grep "is_prefer_https" | grep "checked" >/dev/null
+ if [ $? -eq 0 ]; then
+ is_prefer_https=1
+ else
+ is_prefer_https=0
+ fi
+ echo "$_response" | grep "hsts_enabled" | grep "checked" >/dev/null
+ if [ $? -eq 0 ]; then
+ hsts_enabled=1
+ else
+ hsts_enabled=0
+ fi
+ _debug "cert_value" "$cert_value"
+ if [ -z "$cert_value" ]; then
+ _err "Fail to get certificate id."
+ return 1
+ fi
+
+ _request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
+ _response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
+ _message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
+ _info "_message" "$_message"
+ if [ -z "$_message" ]; then
+ _err "Fail to apply certificate."
+ return 1
+ fi
+ done
+
+ _info "Domain $_cdomain certificate successfully deployed to KeyHelp Domain ID $DEPLOY_KEYHELP_DOMAIN_ID."
+ return 0
+}
diff --git a/deploy/keyhelp_api.sh b/deploy/keyhelp_api.sh
new file mode 100644
index 00000000..75e9d951
--- /dev/null
+++ b/deploy/keyhelp_api.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env sh
+
+keyhelp_api_deploy() {
+ _cdomain="$1"
+ _ckey="$2"
+ _ccert="$3"
+ _cca="$4"
+
+ _debug _cdomain "$_cdomain"
+ _debug _ckey "$_ckey"
+ _debug _ccert "$_ccert"
+ _debug _cca "$_cca"
+
+ # Read config from saved values or env
+ _getdeployconf DEPLOY_KEYHELP_HOST
+ _getdeployconf DEPLOY_KEYHELP_API_KEY
+
+ _debug DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
+ _secure_debug DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
+
+ if [ -z "$DEPLOY_KEYHELP_HOST" ]; then
+ _err "KeyHelp host not found, please define DEPLOY_KEYHELP_HOST."
+ return 1
+ fi
+ if [ -z "$DEPLOY_KEYHELP_API_KEY" ]; then
+ _err "KeyHelp api key not found, please define DEPLOY_KEYHELP_API_KEY."
+ return 1
+ fi
+
+ # Save current values
+ _savedeployconf DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
+ _savedeployconf DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
+
+ _request_key="$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')"
+ _request_cert="$(tr '\n' ':' <"$_ccert" | sed 's/:/\\n/g')"
+ _request_ca="$(tr '\n' ':' <"$_cca" | sed 's/:/\\n/g')"
+
+ _request_body="{
+ \"name\": \"$_cdomain\",
+ \"components\": {
+ \"private_key\": \"$_request_key\",
+ \"certificate\": \"$_request_cert\",
+ \"ca_certificate\": \"$_request_ca\"
+ }
+ }"
+
+ _hosts="$(echo "$DEPLOY_KEYHELP_HOST" | tr "," " ")"
+ _keys="$(echo "$DEPLOY_KEYHELP_API_KEY" | tr "," " ")"
+ _i=1
+
+ for _host in $_hosts; do
+ _key="$(_getfield "$_keys" "$_i" " ")"
+ _i="$(_math "$_i" + 1)"
+
+ export _H1="X-API-Key: $_key"
+
+ _put_url="$_host/api/v2/certificates/name/$_cdomain"
+ if _post "$_request_body" "$_put_url" "" "PUT" "application/json" >/dev/null; then
+ _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ else
+ _err "Cannot make PUT request to $_put_url"
+ return 1
+ fi
+
+ if [ "$_code" = "404" ]; then
+ _info "$_cdomain not found, creating new entry at $_host"
+
+ _post_url="$_host/api/v2/certificates"
+ if _post "$_request_body" "$_post_url" "" "POST" "application/json" >/dev/null; then
+ _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ else
+ _err "Cannot make POST request to $_post_url"
+ return 1
+ fi
+ fi
+
+ if _startswith "$_code" "2"; then
+ _info "$_cdomain set at $_host"
+ else
+ _err "HTTP status code is $_code"
+ return 1
+ fi
+ done
+
+ return 0
+}
diff --git a/deploy/netlify.sh b/deploy/netlify.sh
new file mode 100644
index 00000000..8d25f74c
--- /dev/null
+++ b/deploy/netlify.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env sh
+
+# Script to deploy certificate to Netlify
+# https://docs.netlify.com/api/get-started/#authentication
+# https://open-api.netlify.com/#tag/sniCertificate
+
+# This deployment required following variables
+# export Netlify_ACCESS_TOKEN="Your Netlify Access Token"
+# export Netlify_SITE_ID="Your Netlify Site ID"
+
+# If have more than one SITE ID
+# export Netlify_SITE_ID="SITE_ID_1 SITE_ID_2"
+
+# returns 0 means success, otherwise error.
+
+######## Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+netlify_deploy() {
+ _cdomain="$1"
+ _ckey="$2"
+ _ccert="$3"
+ _cca="$4"
+ _cfullchain="$5"
+
+ _debug _cdomain "$_cdomain"
+ _debug _ckey "$_ckey"
+ _debug _ccert "$_ccert"
+ _debug _cca "$_cca"
+ _debug _cfullchain "$_cfullchain"
+
+ if [ -z "$Netlify_ACCESS_TOKEN" ]; then
+ _err "Netlify_ACCESS_TOKEN is not defined."
+ return 1
+ else
+ _savedomainconf Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
+ fi
+ if [ -z "$Netlify_SITE_ID" ]; then
+ _err "Netlify_SITE_ID is not defined."
+ return 1
+ else
+ _savedomainconf Netlify_SITE_ID "$Netlify_SITE_ID"
+ fi
+
+ _info "Deploying certificate to Netlify..."
+
+ ## upload certificate
+ string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
+ string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
+ string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
+
+ for SITE_ID in $Netlify_SITE_ID; do
+ _request_body="{\"certificate\":\"$string_ccert\",\"key\":\"$string_key\",\"ca_certificates\":\"$string_cca\"}"
+ _debug _request_body "$_request_body"
+ _debug Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
+ export _H1="Authorization: Bearer $Netlify_ACCESS_TOKEN"
+ _response=$(_post "$_request_body" "https://api.netlify.com/api/v1/sites/$SITE_ID/ssl" "" "POST" "application/json")
+
+ if _contains "$_response" "\"error\""; then
+ _err "Error in deploying $_cdomain certificate to Netlify SITE_ID $SITE_ID."
+ _err "$_response"
+ return 1
+ fi
+ _debug response "$_response"
+ _info "Domain $_cdomain certificate successfully deployed to Netlify SITE_ID $SITE_ID."
+ done
+
+ return 0
+}
diff --git a/deploy/panos.sh b/deploy/panos.sh
index a9232e79..c54d21fe 100644
--- a/deploy/panos.sh
+++ b/deploy/panos.sh
@@ -16,6 +16,7 @@
# export PANOS_TEMPLATE="" # Template Name of panorama managed devices
# export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically
# export PANOS_VSYS="Shared" # name of the vsys to import the certificate
+# export PANOS_CERTNAME="" # use a custom certificate name to work around Panorama's 31-character limit
#
# The script will automatically generate a new API key if
# no key is found, or if a saved key has expired or is invalid.
@@ -89,7 +90,7 @@ deployer() {
if [ "$type" = 'cert' ]; then
panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate"
- content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
+ content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
@@ -103,11 +104,11 @@ deployer() {
if [ "$type" = 'key' ]; then
panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key"
- content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
+ content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456"
- content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
+ content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_certname.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
if [ "$_panos_template" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
fi
@@ -242,6 +243,15 @@ panos_deploy() {
_getdeployconf PANOS_VSYS
fi
+ # PANOS_CERTNAME
+ if [ "$PANOS_CERTNAME" ]; then
+ _debug "Detected ENV variable PANOS_CERTNAME. Saving to file."
+ _savedeployconf PANOS_CERTNAME "$PANOS_CERTNAME" 1
+ else
+ _debug "Attempting to load variable PANOS_CERTNAME from file."
+ _getdeployconf PANOS_CERTNAME
+ fi
+
#Store variables
_panos_host=$PANOS_HOST
_panos_user=$PANOS_USER
@@ -249,6 +259,7 @@ panos_deploy() {
_panos_template=$PANOS_TEMPLATE
_panos_template_stack=$PANOS_TEMPLATE_STACK
_panos_vsys=$PANOS_VSYS
+ _panos_certname=$PANOS_CERTNAME
#Test API Key if found. If the key is invalid, the variable _panos_key will be unset.
if [ "$_panos_host" ] && [ "$_panos_key" ]; then
@@ -267,6 +278,12 @@ panos_deploy() {
_err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
else
+ # Use certificate name based on the first domain on the certificate if no custom certificate name is set
+ if [ -z "$_panos_certname" ]; then
+ _panos_certname="$_cdomain"
+ _savedeployconf PANOS_CERTNAME "$_panos_certname" 1
+ fi
+
# Generate a new API key if no valid API key is found
if [ -z "$_panos_key" ]; then
_debug "**** Generating new PANOS API KEY ****"
diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh
index d334853e..df34f927 100644
--- a/deploy/truenas_ws.sh
+++ b/deploy/truenas_ws.sh
@@ -71,7 +71,7 @@ with Client(uri="$_ws_uri") as c:
fullchain = file.read()
with open('$2', 'r') as file:
privatekey = file.read()
- ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True)
+ ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey}, job=True)
print("R:" + str(ret["id"]))
sys.exit(0)
else:
diff --git a/deploy/unifi.sh b/deploy/unifi.sh
index 1f274236..1d13e04f 100644
--- a/deploy/unifi.sh
+++ b/deploy/unifi.sh
@@ -143,8 +143,10 @@ unifi_deploy() {
# correct file ownership according to the directory, the keystore is placed in
_unifi_keystore_dir=$(dirname "${_unifi_keystore}")
- _unifi_keystore_dir_owner=$(find "${_unifi_keystore_dir}" -maxdepth 0 -printf '%u\n')
- _unifi_keystore_owner=$(find "${_unifi_keystore}" -maxdepth 0 -printf '%u\n')
+ # shellcheck disable=SC2012
+ _unifi_keystore_dir_owner=$(ls -ld "${_unifi_keystore_dir}" | awk '{print $3}')
+ # shellcheck disable=SC2012
+ _unifi_keystore_owner=$(ls -l "${_unifi_keystore}" | awk '{print $3}')
if ! [ "${_unifi_keystore_owner}" = "${_unifi_keystore_dir_owner}" ]; then
_debug "Changing keystore owner to ${_unifi_keystore_dir_owner}"
chown "$_unifi_keystore_dir_owner" "${_unifi_keystore}" >/dev/null 2>&1 # fail quietly if we're not running as root
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_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_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_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_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_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_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh
new file mode 100644
index 00000000..4b15088a
--- /dev/null
+++ b/dnsapi/dns_infoblox_uddi.sh
@@ -0,0 +1,244 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_infoblox_uddi_info='Infoblox UDDI
+Site: Infoblox.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infoblox_uddi
+Options:
+ Infoblox_UDDI_Key API Key for Infoblox UDDI
+ Infoblox_Portal URL, e.g. "csp.infoblox.com" or "csp.eu.infoblox.com"
+Issues: github.com/acmesh-official/acme.sh/issues
+Author: Stefan Riegel
+'
+
+Infoblox_UDDI_Api="https://"
+
+######## Public functions #####################
+
+#Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_infoblox_uddi_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
+ Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
+
+ _info "Using Infoblox UDDI API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then
+ Infoblox_UDDI_Key=""
+ Infoblox_Portal=""
+ _err "You didn't specify the Infoblox UDDI key or server (Infoblox_UDDI_Key; Infoblox_Portal)."
+ _err "Please set them via EXPORT Infoblox_UDDI_Key=your_key, EXPORT Infoblox_Portal=csp.infoblox.com and try again."
+ return 1
+ fi
+
+ _saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key"
+ _saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal"
+
+ export _H1="Authorization: Token $Infoblox_UDDI_Key"
+ export _H2="Content-Type: application/json"
+
+ _debug "First 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"
+
+ _debug "Getting existing txt records"
+ _infoblox_rest GET "dns/record?_filter=type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'"
+
+ _info "Adding record"
+ body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}"
+
+ if _infoblox_rest POST "dns/record" "$body"; then
+ if _contains "$response" "$txtvalue"; then
+ _info "Added, OK"
+ return 0
+ elif _contains "$response" '"error"'; then
+ # Check if record already exists
+ if _contains "$response" "already exists" || _contains "$response" "duplicate"; then
+ _info "Already exists, OK"
+ return 0
+ else
+ _err "Add txt record error."
+ _err "Response: $response"
+ return 1
+ fi
+ else
+ _info "Added, OK"
+ return 0
+ fi
+ fi
+ _err "Add txt record error."
+ return 1
+}
+
+#Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_infoblox_uddi_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
+ Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
+
+ if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then
+ _err "Credentials not found"
+ return 1
+ fi
+
+ _info "Using Infoblox UDDI API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ export _H1="Authorization: Token $Infoblox_UDDI_Key"
+ export _H2="Content-Type: application/json"
+
+ _debug "First 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"
+
+ _debug "Getting txt records to delete"
+ # Filter by txtvalue to support wildcard certs (multiple TXT records)
+ filter="type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'%20and%20rdata.text%20eq%20'$txtvalue'"
+ _infoblox_rest GET "dns/record?_filter=$filter"
+
+ if ! _contains "$response" '"results"'; then
+ _info "Don't need to remove, record not found."
+ return 0
+ fi
+
+ record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
+ _debug "record_id" "$record_id"
+
+ if [ -z "$record_id" ]; then
+ _info "Don't need to remove, record not found."
+ return 0
+ fi
+
+ # Extract UUID from the full record ID (format: dns/record/uuid)
+ record_uuid=$(echo "$record_id" | sed 's|.*/||')
+ _debug "record_uuid" "$record_uuid"
+
+ if ! _infoblox_rest DELETE "dns/record/$record_uuid"; then
+ _err "Delete record error."
+ return 1
+ fi
+
+ _info "Removed record successfully"
+ return 0
+}
+
+#################### Private functions below ##################################
+
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=dns/auth_zone/xxxx-xxxx
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ # Remove _acme-challenge prefix if present
+ domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//')
+
+ while true; do
+ h=$(printf "%s" "$domain_no_acme" | cut -d . -f "$i"-100)
+ _debug h "$h"
+ if [ -z "$h" ]; then
+ # not valid
+ return 1
+ fi
+
+ # Query for the zone with both trailing dot and without
+ filter="fqdn%20eq%20'$h.'%20or%20fqdn%20eq%20'$h'"
+ if ! _infoblox_rest GET "dns/auth_zone?_filter=$filter"; then
+ # API error - don't continue if we get auth errors
+ if _contains "$response" "401" || _contains "$response" "Authorization"; then
+ _err "Authentication failed. Please check your Infoblox_UDDI_Key."
+ return 1
+ fi
+ # For other errors, continue to parent domain
+ p=$i
+ i=$((i + 1))
+ continue
+ fi
+
+ # Check if response contains results (even if empty)
+ if _contains "$response" '"results"'; then
+ # Extract zone ID - must match the pattern dns/auth_zone/...
+ zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
+ if [ -n "$zone_id" ]; then
+ # Found the zone
+ _domain="$h"
+ _domain_id="$zone_id"
+
+ # Calculate subdomain
+ if [ "$_domain" = "$domain" ]; then
+ _sub_domain=""
+ else
+ _cutlength=$((${#domain} - ${#_domain} - 1))
+ _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
+ fi
+
+ return 0
+ fi
+ fi
+
+ p=$i
+ i=$((i + 1))
+ done
+
+ return 1
+}
+
+# _infoblox_rest GET "dns/record?_filter=..."
+# _infoblox_rest POST "dns/record" "{json body}"
+# _infoblox_rest DELETE "dns/record/uuid"
+_infoblox_rest() {
+ method=$1
+ ep="$2"
+ data="$3"
+
+ _debug "$ep"
+
+ # Ensure credentials are available (when called from _get_root)
+ Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
+ Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
+
+ Infoblox_UDDI_Api="https://$Infoblox_Portal/api/ddi/v1"
+ export _H1="Authorization: Token $Infoblox_UDDI_Key"
+ export _H2="Content-Type: application/json"
+
+ # Debug (masked)
+ _tok_len=$(printf "%s" "$Infoblox_UDDI_Key" | wc -c | tr -d ' \n')
+ _debug2 "Auth header set" "Token len=${_tok_len} on $Infoblox_Portal"
+
+ if [ "$method" != "GET" ]; then
+ _debug data "$data"
+ response="$(_post "$data" "$Infoblox_UDDI_Api/$ep" "" "$method")"
+ else
+ response="$(_get "$Infoblox_UDDI_Api/$ep")"
+ fi
+
+ _ret="$?"
+ _debug2 response "$response"
+
+ if [ "$_ret" != "0" ]; then
+ _err "Error: $ep"
+ return 1
+ fi
+
+ return 0
+}
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_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_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/notify/ntfy.sh b/notify/ntfy.sh
index 21e39559..3a788a84 100644
--- a/notify/ntfy.sh
+++ b/notify/ntfy.sh
@@ -14,6 +14,13 @@ ntfy_send() {
_debug "_content" "$_content"
_debug "_statusCode" "$_statusCode"
+ _priority_default="default"
+ _priority_error="high"
+
+ _tag_success="white_check_mark"
+ _tag_error="warning"
+ _tag_info="information_source"
+
NTFY_URL="${NTFY_URL:-$(_readaccountconf_mutable NTFY_URL)}"
if [ "$NTFY_URL" ]; then
_saveaccountconf_mutable NTFY_URL "$NTFY_URL"
@@ -30,7 +37,26 @@ ntfy_send() {
export _H1="Authorization: Bearer $NTFY_TOKEN"
fi
- _data="${_subject}. $_content"
+ case "$_statusCode" in
+ 0)
+ _priority="$_priority_default"
+ _tag="$_tag_success"
+ ;;
+ 1)
+ _priority="$_priority_error"
+ _tag="$_tag_error"
+ ;;
+ 2)
+ _priority="$_priority_default"
+ _tag="$_tag_info"
+ ;;
+ esac
+
+ export _H2="Priority: $_priority"
+ export _H3="Tags: $_tag"
+ export _H4="Title: $PROJECT_NAME: $_subject"
+
+ _data="$_content"
response="$(_post "$_data" "$NTFY_URL/$NTFY_TOPIC" "" "POST" "")"
if [ "$?" = "0" ] && _contains "$response" "expires"; then
diff --git a/notify/opsgenie.sh b/notify/opsgenie.sh
new file mode 100644
index 00000000..d352a18c
--- /dev/null
+++ b/notify/opsgenie.sh
@@ -0,0 +1,130 @@
+#!/usr/bin/env sh
+
+#Support OpsGenie API integration
+
+#OPSGENIE_API_KEY="" Required, opsgenie api key
+#OPSGENIE_REGION="" Optional, opsgenie region, can be EU or US (default: US)
+#OPSGENIE_PRIORITY_SUCCESS="" Optional, opsgenie priority for success (default: P5)
+#OPSGENIE_PRIORITY_ERROR="" Optional, opsgenie priority for error (default: P2)
+#OPSGENIE_PRIORITY_SKIP="" Optional, opsgenie priority for renew skipped (default: P5)
+
+_OPSGENIE_AVAIL_REGION="US,EU"
+_OPSGENIE_AVAIL_PRIORITIES="P1,P2,P3,P4,P5"
+
+opsgenie_send() {
+ _subject="$1"
+ _content="$2"
+ _status_code="$3" #0: success, 1: error, 2($RENEW_SKIP): skipped
+
+ OPSGENIE_API_KEY="${OPSGENIE_API_KEY:-$(_readaccountconf_mutable OPSGENIE_API_KEY)}"
+ if [ -z "$OPSGENIE_API_KEY" ]; then
+ OPSGENIE_API_KEY=""
+ _err "You didn't specify an OpsGenie API key OPSGENIE_API_KEY yet."
+ return 1
+ fi
+ _saveaccountconf_mutable OPSGENIE_API_KEY "$OPSGENIE_API_KEY"
+ export _H1="Authorization: GenieKey $OPSGENIE_API_KEY"
+
+ OPSGENIE_REGION="${OPSGENIE_REGION:-$(_readaccountconf_mutable OPSGENIE_REGION)}"
+ if [ -z "$OPSGENIE_REGION" ]; then
+ OPSGENIE_REGION="US"
+ _info "The OPSGENIE_REGION is not set, so use the default US as regeion."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_REGION" "$OPSGENIE_REGION"; then
+ _err "The OPSGENIE_REGION \"$OPSGENIE_REGION\" is not available, should be one of $_OPSGENIE_AVAIL_REGION"
+ OPSGENIE_REGION=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_REGION "$OPSGENIE_REGION"
+ fi
+
+ OPSGENIE_PRIORITY_SUCCESS="${OPSGENIE_PRIORITY_SUCCESS:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS)}"
+ if [ -z "$OPSGENIE_PRIORITY_SUCCESS" ]; then
+ OPSGENIE_PRIORITY_SUCCESS="P5"
+ _info "The OPSGENIE_PRIORITY_SUCCESS is not set, so use the default P5 as priority."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SUCCESS"; then
+ _err "The OPSGENIE_PRIORITY_SUCCESS \"$OPSGENIE_PRIORITY_SUCCESS\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES"
+ OPSGENIE_PRIORITY_SUCCESS=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS "$OPSGENIE_PRIORITY_SUCCESS"
+ fi
+
+ OPSGENIE_PRIORITY_ERROR="${OPSGENIE_PRIORITY_ERROR:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_ERROR)}"
+ if [ -z "$OPSGENIE_PRIORITY_ERROR" ]; then
+ OPSGENIE_PRIORITY_ERROR="P2"
+ _info "The OPSGENIE_PRIORITY_ERROR is not set, so use the default P2 as priority."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_ERROR"; then
+ _err "The OPSGENIE_PRIORITY_ERROR \"$OPSGENIE_PRIORITY_ERROR\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES"
+ OPSGENIE_PRIORITY_ERROR=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_PRIORITY_ERROR "$OPSGENIE_PRIORITY_ERROR"
+ fi
+
+ OPSGENIE_PRIORITY_SKIP="${OPSGENIE_PRIORITY_SKIP:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SKIP)}"
+ if [ -z "$OPSGENIE_PRIORITY_SKIP" ]; then
+ OPSGENIE_PRIORITY_SKIP="P5"
+ _info "The OPSGENIE_PRIORITY_SKIP is not set, so use the default P5 as priority."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SKIP"; then
+ _err "The OPSGENIE_PRIORITY_SKIP \"$OPSGENIE_PRIORITY_SKIP\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES"
+ OPSGENIE_PRIORITY_SKIP=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_PRIORITY_SKIP "$OPSGENIE_PRIORITY_SKIP"
+ fi
+
+ case "$OPSGENIE_REGION" in
+ "US")
+ _opsgenie_url="https://api.opsgenie.com/v2/alerts"
+ ;;
+ "EU")
+ _opsgenie_url="https://api.eu.opsgenie.com/v2/alerts"
+ ;;
+ *)
+ _err "opsgenie region error."
+ return 1
+ ;;
+ esac
+
+ case $_status_code in
+ 0)
+ _priority=$OPSGENIE_PRIORITY_SUCCESS
+ ;;
+ 1)
+ _priority=$OPSGENIE_PRIORITY_ERROR
+ ;;
+ 2)
+ _priority=$OPSGENIE_PRIORITY_SKIP
+ ;;
+ *)
+ _priority=$OPSGENIE_PRIORITY_ERROR
+ ;;
+ esac
+
+ _subject_json=$(echo "$_subject" | _json_encode)
+ _content_json=$(echo "$_content" | _json_encode)
+ _subject_underscore=$(echo "$_subject" | sed 's/ /_/g')
+ _alias_json=$(echo "acme.sh-$(hostname)-$_subject_underscore-$(date +%Y%m%d)" | base64 --wrap=0 | _json_encode)
+
+ _data="{
+ \"message\": \"$_subject_json\",
+ \"alias\": \"$_alias_json\",
+ \"description\": \"$_content_json\",
+ \"tags\": [
+ \"acme.sh\",
+ \"host:$(hostname)\"
+ ],
+ \"entity\": \"$(hostname -f)\",
+ \"priority\": \"$_priority\"
+}"
+
+ if response=$(_post "$_data" "$_opsgenie_url" "" "" "application/json"); then
+ if ! _contains "$response" error; then
+ _info "opsgenie send success."
+ return 0
+ fi
+ fi
+ _err "opsgenie send error."
+ _err "$response"
+ return 1
+}
diff --git a/notify/telegram.sh b/notify/telegram.sh
index 7da05729..4ed50a65 100644
--- a/notify/telegram.sh
+++ b/notify/telegram.sh
@@ -34,8 +34,8 @@ telegram_send() {
fi
_saveaccountconf_mutable TELEGRAM_BOT_URLBASE "$TELEGRAM_BOT_URLBASE"
- _subject="$(printf "%s" "$_subject" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([-_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')"
- _content="$(printf "%s" "$_content" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([-_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')"
+ _subject="$(printf "%s" "$_subject" | sed -E 's/([][()~`>#+=|{}.!*_\\-])/\\\\\1/g')"
+ _content="$(printf "%s" "$_content" | sed -E 's/([][()~`>#+=|{}.!*_\\-])/\\\\\1/g')"
_content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)"
_data="{\"text\": \"$_content\", "
_data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", "