From b0ca4435fdbee019ac72f8df0cb304e1de5deffc Mon Sep 17 00:00:00 2001 From: Ciaran Walsh Date: Wed, 21 Feb 2024 00:21:09 +0000 Subject: [PATCH 001/689] Fix for empty error objects in response breaking extraction of domain validation types Fix for empty error objects in the response which mess up the extraction of domain validation types due to the closing brace in the error object prematurely matching the end of the search pattern. This seems to be a recent change with ZeroSSL in particular where "error":{} is being included in responses. There could potentially be a related issue if there is a complex error object ever returned in the validation check response where an embedded sub-object could lead to an incomplete extraction of the error message, roughly around line 5040. Adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 --- acme.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 38ccaade..34ac49e8 100755 --- a/acme.sh +++ b/acme.sh @@ -4722,7 +4722,8 @@ $_authorizations_map" _debug keyauthorization "$keyauthorization" fi - entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 + entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$keyauthorization" -a -z "$entry" ]; then @@ -6283,7 +6284,8 @@ _deactivate() { fi _debug "Trigger validation." vtype="$(_getIdType "$_d_domain")" - entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 + entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then _err "Error, can not get domain token $d" From 54eba51b35b7ec48f5e4eecfad3139e6a6ed34f4 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Wed, 20 Mar 2024 19:14:00 +0800 Subject: [PATCH 002/689] Add deployhook for Netlify --- deploy/netlify.sh | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 deploy/netlify.sh diff --git a/deploy/netlify.sh b/deploy/netlify.sh new file mode 100644 index 00000000..3b854018 --- /dev/null +++ b/deploy/netlify.sh @@ -0,0 +1,62 @@ +#!/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" + +# 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') + _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/$Netlify_SITE_ID/ssl" "" "POST" "application/json") + + if _contains "$_response" "\"error\""; then + _err "Error in deploying $_cdomain certificate to Netlify." + _err "$_response" + return 1 + fi + _debug response "$_response" + _info "Domain $_cdomain certificate successfully deployed to Netlify." + return 0 +} \ No newline at end of file From c508984f564fc99235e5d99142f0af6972430d0c Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:16:53 +0800 Subject: [PATCH 003/689] Add deployhook for Edgio --- deploy/edgio.sh | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 deploy/edgio.sh diff --git a/deploy/edgio.sh b/deploy/edgio.sh new file mode 100644 index 00000000..604b00e8 --- /dev/null +++ b/deploy/edgio.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env sh + +# Here is a script to deploy cert to edgio using its API +# https://docs.edg.io/guides/v7/develop/rest_api/authentication +# https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts + +# This deployment required following variables +# export EDGIO_CLIENT_ID="Your Edgio Client ID" +# export EDGIO_CLIENT_SECRET="Your Edgio Client Secret" +# export EDGIO_ENVIRONMENT_ID="Your Edgio Environment ID" + +#returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +edgio_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 "$EDGIO_CLIENT_ID" ]; then + _err "EDGIO_CLIENT_ID is not defined." + return 1 + else + _savedomainconf EDGIO_CLIENT_ID "$EDGIO_CLIENT_ID" + fi + + if [ -z "$EDGIO_CLIENT_SECRET" ]; then + _err "EDGIO_CLIENT_SECRET is not defined." + return 1 + else + _savedomainconf EDGIO_CLIENT_SECRET "$EDGIO_CLIENT_SECRET" + fi + + if [ -z "$EDGIO_ENVIRONMENT_ID" ]; then + _err "EDGIO_ENVIRONMENT_ID is not defined." + return 1 + else + _savedomainconf EDGIO_ENVIRONMENT_ID "$EDGIO_ENVIRONMENT_ID" + fi + + _info "Getting access token" + _data="client_id=$EDGIO_CLIENT_ID&client_secret=$EDGIO_CLIENT_SECRET&grant_type=client_credentials&scope=app.config" + _debug Get_access_token_data "$_data" + _response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded" ) + _debug Get_access_token_response "$_response" + _access_token=$(echo "$_response" | _json_decode | _egrep_o '"access_token":"[^"]*' | cut -d : -f 2 | tr -d '"') + _debug _access_token "$_access_token" + if [ -z "$_access_token" ]; then + _err "Error in getting access token" + return 1 + fi + + _info "Uploading 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') + _data="{\"environment_id\":\"$EDGIO_ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}" + _debug Upload_certificate_data "$_data" + _H1="Authorization: Bearer $_access_token" + _response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json") + + if _contains "$_response" "message"; then + _err "Error in deploying $_cdomain certificate to Edgio." + _err "$_response" + return 1 + fi + _debug Upload_certificate_response "$_response" + _info "Domain $_cdomain certificate successfully deployed to Edgio." + return 0 +} \ No newline at end of file From d1a1d1da8f550bf80a8339a7c15735619e581a2b Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:16:44 +0800 Subject: [PATCH 004/689] Add deployhook for CacheFly --- deploy/cachefly.sh | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 deploy/cachefly.sh diff --git a/deploy/cachefly.sh b/deploy/cachefly.sh new file mode 100644 index 00000000..0e436d26 --- /dev/null +++ b/deploy/cachefly.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env sh + +# Script to deploy certificate to CacheFly +# https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post + +# This deployment required following variables +# export CACHEFLY_TOKEN="Your CacheFly API Token" + +# returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +CACHEFLY_API_BASE="https://api.cachefly.com/api/2.5" + +cachefly_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 "$CACHEFLY_TOKEN" ]; then + _err "CACHEFLY_TOKEN is not defined." + return 1 + else + _savedomainconf CACHEFLY_TOKEN "$CACHEFLY_TOKEN" + fi + + _info "Deploying certificate to CacheFly..." + + ## upload certificate + string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n') + string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') + + _request_body="{\"certificate\":\"$string_fullchain\",\"certificateKey\":\"$string_key\"}" + _debug _request_body "$_request_body" + _debug CACHEFLY_TOKEN "$CACHEFLY_TOKEN" + export _H1="Authorization: Bearer $CACHEFLY_TOKEN" + _response=$(_post "$_request_body" "$CACHEFLY_API_BASE/certificates" "" "POST" "application/json") + + if _contains "$_response" "message"; then + _err "Error in deploying $_cdomain certificate to CacheFly." + _err "$_response" + return 1 + fi + _debug response "$_response" + _info "Domain $_cdomain certificate successfully deployed to CacheFly." + return 0 +} \ No newline at end of file From 696182cfa4ceff50cd7c5b05a15cc591c55173bd Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:05:43 +0800 Subject: [PATCH 005/689] deployhook Edgio: Support multiple Environment ID --- deploy/edgio.sh | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/deploy/edgio.sh b/deploy/edgio.sh index 604b00e8..aadaa98a 100644 --- a/deploy/edgio.sh +++ b/deploy/edgio.sh @@ -9,7 +9,10 @@ # export EDGIO_CLIENT_SECRET="Your Edgio Client Secret" # export EDGIO_ENVIRONMENT_ID="Your Edgio Environment ID" -#returns 0 means success, otherwise error. +# If have more than one Environment ID +# export EDGIO_ENVIRONMENT_ID="ENVIRONMENT_ID_1 ENVIRONMENT_ID_2" + +# returns 0 means success, otherwise error. ######## Public functions ##################### @@ -64,17 +67,20 @@ edgio_deploy() { 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') - _data="{\"environment_id\":\"$EDGIO_ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}" - _debug Upload_certificate_data "$_data" - _H1="Authorization: Bearer $_access_token" - _response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json") - if _contains "$_response" "message"; then - _err "Error in deploying $_cdomain certificate to Edgio." - _err "$_response" - return 1 - fi - _debug Upload_certificate_response "$_response" - _info "Domain $_cdomain certificate successfully deployed to Edgio." + for ENVIRONMENT_ID in $EDGIO_ENVIRONMENT_ID; do + _data="{\"environment_id\":\"$ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}" + _debug Upload_certificate_data "$_data" + _H1="Authorization: Bearer $_access_token" + _response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json") + if _contains "$_response" "message"; then + _err "Error in deploying $_cdomain certificate to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID." + _err "$_response" + return 1 + fi + _debug Upload_certificate_response "$_response" + _info "Domain $_cdomain certificate successfully deployed to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID." + done + return 0 } \ No newline at end of file From 3b46060caa7a94e04926099ba32118efd07cc116 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:06:09 +0800 Subject: [PATCH 006/689] deployhook Netlify: Support multiple Site ID --- deploy/netlify.sh | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/deploy/netlify.sh b/deploy/netlify.sh index 3b854018..2ff9bcb6 100644 --- a/deploy/netlify.sh +++ b/deploy/netlify.sh @@ -8,6 +8,9 @@ # 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 ##################### @@ -45,18 +48,22 @@ netlify_deploy() { 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') - _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/$Netlify_SITE_ID/ssl" "" "POST" "application/json") - if _contains "$_response" "\"error\""; then - _err "Error in deploying $_cdomain certificate to Netlify." - _err "$_response" - return 1 - fi - _debug response "$_response" - _info "Domain $_cdomain certificate successfully deployed to Netlify." + 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 } \ No newline at end of file From e7284df1df2eb586ec89bf69e19086779d63ff02 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Thu, 21 Mar 2024 21:44:33 +0800 Subject: [PATCH 007/689] Add deployhook for DirectAdmin --- deploy/directadmin.sh | 80 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 deploy/directadmin.sh diff --git a/deploy/directadmin.sh b/deploy/directadmin.sh new file mode 100644 index 00000000..23d46df9 --- /dev/null +++ b/deploy/directadmin.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env sh + +# Script to deploy certificate to DirectAdmin +# https://docs.directadmin.com/directadmin/customizing-workflow/api-all-about.html#creating-a-login-key +# https://docs.directadmin.com/changelog/version-1.24.4.html#cmd-api-catch-all-pop-passwords-frontpage-protected-dirs-ssl-certs + +# This deployment required following variables +# export DirectAdmin_ENDPOINT="example.com:2222" +# export DirectAdmin_USERNAME="Your DirectAdmin Username" +# export DirectAdmin_KEY="Your DirectAdmin Login Key or Password" +# export DirectAdmin_MAIN_DOMAIN="Your DirectAdmin Main Domain, NOT Subdomain" + +# returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +directadmin_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 "$DirectAdmin_ENDPOINT" ]; then + _err "DirectAdmin_ENDPOINT is not defined." + return 1 + else + _savedomainconf DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT" + fi + if [ -z "$DirectAdmin_USERNAME" ]; then + _err "DirectAdmin_USERNAME is not defined." + return 1 + else + _savedomainconf DirectAdmin_USERNAME "$DirectAdmin_USERNAME" + fi + if [ -z "$DirectAdmin_KEY" ]; then + _err "DirectAdmin_KEY is not defined." + return 1 + else + _savedomainconf DirectAdmin_KEY "$DirectAdmin_KEY" + fi + if [ -z "$DirectAdmin_MAIN_DOMAIN" ]; then + _err "DirectAdmin_MAIN_DOMAIN is not defined." + return 1 + else + _savedomainconf DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN" + fi + + _info "Deploying certificate to DirectAdmin..." + + # upload certificate + string_cfullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n') + string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') + + _request_body="{\"domain\":\"$DirectAdmin_MAIN_DOMAIN\",\"action\":\"save\",\"type\":\"paste\",\"certificate\":\"$string_key\n$string_cfullchain\n\"}" + _debug _request_body "$_request_body" + _debug DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT" + _debug DirectAdmin_USERNAME "$DirectAdmin_USERNAME" + _debug DirectAdmin_KEY "$DirectAdmin_KEY" + _debug DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN" + _response=$(_post "$_request_body" "https://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json") + + if _contains "$_response" "error=1"; then + _err "Error in deploying $_cdomain certificate to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN." + _err "$_response" + return 1 + fi + + _info "$_response" + _info "Domain $_cdomain certificate successfully deployed to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN." + + return 0 +} \ No newline at end of file From 295af0168753caf491a86745f0c8ef6b6bc207be Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Thu, 28 Mar 2024 23:07:14 +0800 Subject: [PATCH 008/689] Add deployhook for KeyHelp --- deploy/keyhelp.sh | 111 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 deploy/keyhelp.sh diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh new file mode 100644 index 00000000..b792f021 --- /dev/null +++ b/deploy/keyhelp.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env sh + +# Script to deploy certificate to KeyHelp +# This deployment required following variables +# export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com" +# export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username" +# export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password" +# export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID" + +# Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID. +# https://DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=xxx + +# If have more than one domain name +# export DEPLOY_KEYHELP_DOMAIN_ID="111 222 333" + +keyhelp_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 "$DEPLOY_KEYHELP_BASEURL" ]; then + _err "DEPLOY_KEYHELP_BASEURL is not defined." + return 1 + else + _savedomainconf DEPLOY_KEYHELP_BASEURL "$DEPLOY_KEYHELP_BASEURL" + fi + + if [ -z "$DEPLOY_KEYHELP_USERNAME" ]; then + _err "DEPLOY_KEYHELP_USERNAME is not defined." + return 1 + else + _savedomainconf DEPLOY_KEYHELP_USERNAME "$DEPLOY_KEYHELP_USERNAME" + fi + + if [ -z "$DEPLOY_KEYHELP_PASSWORD" ]; then + _err "DEPLOY_KEYHELP_PASSWORD is not defined." + return 1 + else + _savedomainconf DEPLOY_KEYHELP_PASSWORD "$DEPLOY_KEYHELP_PASSWORD" + fi + + if [ -z "$DEPLOY_KEYHELP_DOMAIN_ID" ]; then + _err "DEPLOY_KEYHELP_DOMAIN_ID is not defined." + return 1 + else + _savedomainconf DEPLOY_KEYHELP_DOMAIN_ID "$DEPLOY_KEYHELP_DOMAIN_ID" + fi + + _info "Logging in to keyhelp panel" + username_encoded="$(printf "%s" "${DEPLOY_KEYHELP_USERNAME}" | _url_encode)" + password_encoded="$(printf "%s" "${DEPLOY_KEYHELP_PASSWORD}" | _url_encode)" + _H1="Content-Type: application/x-www-form-urlencoded" + _response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?submit=1&username=$username_encoded&password=$password_encoded" "TRUE") + _cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)" + + # If cookies is not empty then logon successful + if [ -z "$_cookie" ]; then + _err "Fail to get cookie." + return 1 + fi + _debug "cookie" "$_cookie" + + _info "Uploading certificate" + _date=$(date +"%Y%m%d") + encoded_key="$(_url_encode <"$_ckey")" + encoded_ccert="$(_url_encode <"$_ccert")" + encoded_cca="$(_url_encode <"$_cca")" + certificate_name="$_cdomain-$_date" + + _request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca" + _H1="Cookie: $_cookie" + _response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "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 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') + _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&certificate_type=custom&certificate_id=$cert_value" + _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 +} \ No newline at end of file From c466f063c82ed23e695612d946ac9dcf98f71a1c Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:59:12 +0800 Subject: [PATCH 009/689] add newline at end of file --- deploy/cachefly.sh | 2 +- deploy/directadmin.sh | 2 +- deploy/edgio.sh | 2 +- deploy/keyhelp.sh | 2 +- deploy/netlify.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/deploy/cachefly.sh b/deploy/cachefly.sh index 0e436d26..325b2230 100644 --- a/deploy/cachefly.sh +++ b/deploy/cachefly.sh @@ -53,4 +53,4 @@ cachefly_deploy() { _debug response "$_response" _info "Domain $_cdomain certificate successfully deployed to CacheFly." return 0 -} \ No newline at end of file +} diff --git a/deploy/directadmin.sh b/deploy/directadmin.sh index 23d46df9..84818f93 100644 --- a/deploy/directadmin.sh +++ b/deploy/directadmin.sh @@ -77,4 +77,4 @@ directadmin_deploy() { _info "Domain $_cdomain certificate successfully deployed to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN." return 0 -} \ No newline at end of file +} diff --git a/deploy/edgio.sh b/deploy/edgio.sh index aadaa98a..1b0569cb 100644 --- a/deploy/edgio.sh +++ b/deploy/edgio.sh @@ -83,4 +83,4 @@ edgio_deploy() { done return 0 -} \ No newline at end of file +} diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh index b792f021..58f13152 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -108,4 +108,4 @@ keyhelp_deploy() { _info "Domain $_cdomain certificate successfully deployed to KeyHelp Domain ID $DEPLOY_KEYHELP_DOMAIN_ID." return 0 -} \ No newline at end of file +} diff --git a/deploy/netlify.sh b/deploy/netlify.sh index 2ff9bcb6..fb254a32 100644 --- a/deploy/netlify.sh +++ b/deploy/netlify.sh @@ -66,4 +66,4 @@ netlify_deploy() { done return 0 -} \ No newline at end of file +} From bfba44fbadc142a0f8dd87e107953e17970a4a90 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Sun, 7 Apr 2024 12:36:19 +0000 Subject: [PATCH 010/689] format adjustment --- deploy/cachefly.sh | 2 +- deploy/edgio.sh | 6 +++--- deploy/keyhelp.sh | 8 ++++---- deploy/netlify.sh | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/deploy/cachefly.sh b/deploy/cachefly.sh index 325b2230..7841b20b 100644 --- a/deploy/cachefly.sh +++ b/deploy/cachefly.sh @@ -44,7 +44,7 @@ cachefly_deploy() { _debug CACHEFLY_TOKEN "$CACHEFLY_TOKEN" export _H1="Authorization: Bearer $CACHEFLY_TOKEN" _response=$(_post "$_request_body" "$CACHEFLY_API_BASE/certificates" "" "POST" "application/json") - + if _contains "$_response" "message"; then _err "Error in deploying $_cdomain certificate to CacheFly." _err "$_response" diff --git a/deploy/edgio.sh b/deploy/edgio.sh index 1b0569cb..1acd0c8f 100644 --- a/deploy/edgio.sh +++ b/deploy/edgio.sh @@ -23,7 +23,7 @@ edgio_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" - + _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" @@ -50,11 +50,11 @@ edgio_deploy() { else _savedomainconf EDGIO_ENVIRONMENT_ID "$EDGIO_ENVIRONMENT_ID" fi - + _info "Getting access token" _data="client_id=$EDGIO_CLIENT_ID&client_secret=$EDGIO_CLIENT_SECRET&grant_type=client_credentials&scope=app.config" _debug Get_access_token_data "$_data" - _response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded" ) + _response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded") _debug Get_access_token_response "$_response" _access_token=$(echo "$_response" | _json_decode | _egrep_o '"access_token":"[^"]*' | cut -d : -f 2 | tr -d '"') _debug _access_token "$_access_token" diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh index 58f13152..839b874c 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -92,8 +92,8 @@ keyhelp_deploy() { target_type=$(echo "$_response" | grep 'target_type' | grep 'checked' | sed -n 's/.*value="\([^"]*\).*/\1/p') _debug "cert_value" "$cert_value" if [ -z "$cert_value" ]; then - _err "Fail to get certificate id." - return 1 + _err "Fail to get certificate id." + return 1 fi _request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&certificate_type=custom&certificate_id=$cert_value" @@ -101,8 +101,8 @@ keyhelp_deploy() { _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 + _err "Fail to apply certificate." + return 1 fi done diff --git a/deploy/netlify.sh b/deploy/netlify.sh index fb254a32..8d25f74c 100644 --- a/deploy/netlify.sh +++ b/deploy/netlify.sh @@ -48,14 +48,14 @@ netlify_deploy() { 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" From 1116b73a08aae1b58c8edb6fbb016d82ac3364c1 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:47:43 +0800 Subject: [PATCH 011/689] deployhook KeyHelp: Support enabling the Enforce HTTPS option --- deploy/keyhelp.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh index 839b874c..0750e415 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -5,6 +5,7 @@ # export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com" # export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username" # export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password" +# export DEPLOY_KEYHELP_ENFORCE_HTTPS="1" # 0 or 1, input 1 to enable Enforce HTTP to HTTPS redirection. # export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID" # Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID. @@ -54,6 +55,11 @@ keyhelp_deploy() { _savedomainconf DEPLOY_KEYHELP_DOMAIN_ID "$DEPLOY_KEYHELP_DOMAIN_ID" fi + # Optional DEPLOY_KEYHELP_ENFORCE_HTTPS + _getdeployconf DEPLOY_KEYHELP_ENFORCE_HTTPS + # set default values for DEPLOY_KEYHELP_ENFORCE_HTTPS + [ -n "${DEPLOY_KEYHELP_ENFORCE_HTTPS}" ] || DEPLOY_KEYHELP_ENFORCE_HTTPS="1" + _info "Logging in to keyhelp panel" username_encoded="$(printf "%s" "${DEPLOY_KEYHELP_USERNAME}" | _url_encode)" password_encoded="$(printf "%s" "${DEPLOY_KEYHELP_PASSWORD}" | _url_encode)" @@ -96,7 +102,7 @@ keyhelp_deploy() { return 1 fi - _request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&certificate_type=custom&certificate_id=$cert_value" + _request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&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" From 3f40380c69d75bbf09bdb9e4cdb1c007fe437655 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:57:51 +0800 Subject: [PATCH 012/689] deployhook Directadmin: Support for selecting the scheme of DirectAdmin , HTTP or HTTPS --- deploy/directadmin.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deploy/directadmin.sh b/deploy/directadmin.sh index 84818f93..3f60a088 100644 --- a/deploy/directadmin.sh +++ b/deploy/directadmin.sh @@ -5,6 +5,7 @@ # https://docs.directadmin.com/changelog/version-1.24.4.html#cmd-api-catch-all-pop-passwords-frontpage-protected-dirs-ssl-certs # This deployment required following variables +# export DirectAdmin_SCHEME="https" # Optional, https or http, defaults to https # export DirectAdmin_ENDPOINT="example.com:2222" # export DirectAdmin_USERNAME="Your DirectAdmin Username" # export DirectAdmin_KEY="Your DirectAdmin Login Key or Password" @@ -53,6 +54,11 @@ directadmin_deploy() { _savedomainconf DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN" fi + # Optional SCHEME + _getdeployconf DirectAdmin_SCHEME + # set default values for DirectAdmin_SCHEME + [ -n "${DirectAdmin_SCHEME}" ] || DirectAdmin_SCHEME="https" + _info "Deploying certificate to DirectAdmin..." # upload certificate @@ -65,7 +71,7 @@ directadmin_deploy() { _debug DirectAdmin_USERNAME "$DirectAdmin_USERNAME" _debug DirectAdmin_KEY "$DirectAdmin_KEY" _debug DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN" - _response=$(_post "$_request_body" "https://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json") + _response=$(_post "$_request_body" "$DirectAdmin_SCHEME://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json") if _contains "$_response" "error=1"; then _err "Error in deploying $_cdomain certificate to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN." From 2beb2f5659e968423f91c8db2d634d85177fff2a Mon Sep 17 00:00:00 2001 From: Manuel Sanchez Pinar Date: Thu, 4 Jul 2024 14:03:20 +0200 Subject: [PATCH 013/689] fix: rage4 - add error 400 and TXT cleanup The following error happens if the header is set to 'Content-Type: application/json': {"statusCode":400,"message":"One or more errors occurred!", "errors":{"serializerErrors":["The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. LineNumber: 0 | BytePositionInLine: 0."]}} Fix TXT removal --- dnsapi/dns_rage4.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) 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")" From 2f5ea120cb18d56d9d21da07034cb679457b3c94 Mon Sep 17 00:00:00 2001 From: b1n23 <97284148+b1n23@users.noreply.github.com> Date: Tue, 16 Jul 2024 00:25:53 +0800 Subject: [PATCH 014/689] deployhook KeyHelp: fix bug --- deploy/keyhelp.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh index 0750e415..97f9c21c 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -5,7 +5,6 @@ # export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com" # export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username" # export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password" -# export DEPLOY_KEYHELP_ENFORCE_HTTPS="1" # 0 or 1, input 1 to enable Enforce HTTP to HTTPS redirection. # export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID" # Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID. @@ -96,13 +95,28 @@ keyhelp_deploy() { _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&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS" + _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" From d5b5bcef5631ae2e04d9df7c19be0947351145ab Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 10 Dec 2024 20:54:20 +0100 Subject: [PATCH 015/689] support ARI, not finished yet https://github.com/acmesh-official/acme.sh/issues/4944 --- acme.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/acme.sh b/acme.sh index 9842e3f1..bc146996 100755 --- a/acme.sh +++ b/acme.sh @@ -2746,6 +2746,7 @@ _clearAPI() { ACME_REVOKE_CERT="" ACME_NEW_NONCE="" ACME_AGREEMENT="" + ACME_RENEWAL_INFO="" } #server @@ -2790,6 +2791,9 @@ _initAPI() { ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3) export ACME_AGREEMENT + ACME_RENEWAL_INFO=$(echo "$response" | _egrep_o 'renewalInfo" *: *"[^"]*"' | cut -d '"' -f 3) + export ACME_RENEWAL_INFO + _debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE" _debug "ACME_NEW_AUTHZ" "$ACME_NEW_AUTHZ" _debug "ACME_NEW_ORDER" "$ACME_NEW_ORDER" @@ -2797,6 +2801,7 @@ _initAPI() { _debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT" _debug "ACME_AGREEMENT" "$ACME_AGREEMENT" _debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE" + _debug "ACME_RENEWAL_INFO" "$ACME_RENEWAL_INFO" if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then return 0 fi @@ -6416,6 +6421,36 @@ deactivate() { done } +#cert +_getAKI() { + _cert="$1" + openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :' +} + +#cert +_getSerial() { + _cert="$1" + openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2 +} + +#cert +_get_ARI() { + _cert="$1" + _aki=$(_getAKI "$_cert") + _ser=$(_getSerial "$_cert") + _debug2 "_aki" "$_aki" + _debug2 "_ser" "$_ser" + + _akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)" + _debug2 "_akiurl" "$_akiurl" + _serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)" + _debug2 "_serurl" "$_serurl" + + _ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl" + _get "$_ARI_URL" + +} + # Detect profile file if not specified as environment variable _detect_profile() { if [ -n "$PROFILE" -a -f "$PROFILE" ]; then From 5ddffc9172e9dd00c90f4251e0e37310525db337 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 10 Dec 2024 21:01:37 +0100 Subject: [PATCH 016/689] fix format --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index bc146996..4290d7a1 100755 --- a/acme.sh +++ b/acme.sh @@ -6441,9 +6441,9 @@ _get_ARI() { _debug2 "_aki" "$_aki" _debug2 "_ser" "$_ser" - _akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)" + _akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)" _debug2 "_akiurl" "$_akiurl" - _serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)" + _serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)" _debug2 "_serurl" "$_serurl" _ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl" From ee661e5d7112674cf432a6dacc6455b11c54f38e Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 10 Dec 2024 21:02:54 +0100 Subject: [PATCH 017/689] fix format --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 4290d7a1..b7558ee5 100755 --- a/acme.sh +++ b/acme.sh @@ -6445,7 +6445,7 @@ _get_ARI() { _debug2 "_akiurl" "$_akiurl" _serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)" _debug2 "_serurl" "$_serurl" - + _ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl" _get "$_ARI_URL" From 29342e036f5b3e821be3770686bdef022623ef61 Mon Sep 17 00:00:00 2001 From: kir Date: Tue, 11 Mar 2025 08:07:38 +0000 Subject: [PATCH 018/689] Update _get_root url in dnsapi/dns_fornex.sh --- dnsapi/dns_fornex.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From dd29f970a2462c5bd4a6dfd6b0b89b05b368cf7c Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Tue, 25 Mar 2025 19:27:17 -0400 Subject: [PATCH 019/689] Use endpoint environment variable for managed identities if set Some environments in azure don't use the default metadata endpoint, and instead inject an env var that should be used. --- dnsapi/dns_azure.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_azure.sh b/dnsapi/dns_azure.sh index 03feaf63..f9d84706 100644 --- a/dnsapi/dns_azure.sh +++ b/dnsapi/dns_azure.sh @@ -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 \") From 45b99821725ef3359497cfe3b7b34e31101a3052 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:30:18 +0800 Subject: [PATCH 020/689] Add Spaceship DNS API --- dnsapi/dns_spaceship.sh | 197 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 dnsapi/dns_spaceship.sh diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh new file mode 100644 index 00000000..f94d9027 --- /dev/null +++ b/dnsapi/dns_spaceship.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_spaceship_info='Spaceship.com +Site: Spaceship.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_spaceship +Options: + SPACESHIP_API_KEY Spaceship API Key + SPACESHIP_API_SECRET Spaceship API Secret + SPACESHIP_ROOT_DOMAIN (Optional) Manually specify the root domain if auto-detection fails +Issues: github.com/acmesh-official/acme.sh/issues/6304 +Author: Meow +' + +# Spaceship API +# https://docs.spaceship.dev/ + +######## Public functions ##################### + +SPACESHIP_API_BASE="https://spaceship.dev/api/v1" + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_spaceship_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Adding TXT record for $fulldomain with value $txtvalue" + + # Initialize API credentials and headers + if ! _spaceship_init; then + return 1 + fi + + # Detect root zone + if ! _get_root "$fulldomain"; then + return 1 + fi + + # Extract subdomain part relative to root domain + subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") + if [ "$subdomain" = "$fulldomain" ]; then + _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" + return 1 + fi + _debug "Extracted subdomain: $subdomain for root domain: $_domain" + + # Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue) + escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') + + # Prepare payload and URL for adding TXT record + # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API + payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}" + url="$SPACESHIP_API_BASE/dns/records/$_domain" + + # Send API request + if _spaceship_api_request "PUT" "$url" "$payload"; then + _info "Successfully added TXT record for $fulldomain" + return 0 + else + _err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." + return 1 + fi +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_spaceship_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Removing TXT record for $fulldomain with value $txtvalue" + + # Initialize API credentials and headers + if ! _spaceship_init; then + return 1 + fi + + # Detect root zone + if ! _get_root "$fulldomain"; then + return 1 + fi + + # Extract subdomain part relative to root domain + subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") + if [ "$subdomain" = "$fulldomain" ]; then + _err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" + return 1 + fi + _debug "Extracted subdomain: $subdomain for root domain: $_domain" + + # Escape txtvalue to prevent JSON injection + escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') + + # Prepare payload and URL for deleting TXT record + # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API + payload="{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}" + url="$SPACESHIP_API_BASE/dns/records/$_domain" + + # Send API request + if _spaceship_api_request "DELETE" "$url" "$payload"; then + _info "Successfully deleted TXT record for $fulldomain" + return 0 + else + _err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." + return 1 + fi +} + +#################### Private functions below ################################## + +_spaceship_init() { + SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}" + SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}" + + if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then + _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET." + _err "Ensure ~/.acme.sh directory has restricted permissions (chmod 700 ~/.acme.sh) to protect credentials." + return 1 + fi + + # Save credentials to account config for future renewals + _saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY" + _saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET" + + # Set common headers for API requests + export _H1="X-API-Key: $SPACESHIP_API_KEY" + export _H2="X-API-Secret: $SPACESHIP_API_SECRET" + export _H3="Content-Type: application/json" + return 0 +} + +_get_root() { + domain="$1" + + # Check if user manually specified root domain + SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readaccountconf_mutable SPACESHIP_ROOT_DOMAIN)}" + if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then + _domain="$SPACESHIP_ROOT_DOMAIN" + _debug "Using manually specified or saved root domain: $_domain" + # Ensure it's saved (in case it was read from config but not saved previously) + _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" + return 0 + fi + + # Split domain into parts and try from back to front + _debug "Detecting root zone for $domain from back to front" + _parts=$(echo "$domain" | tr '.' '\n' | wc -l) + if [ "$_parts" -lt 2 ]; then + _err "Invalid domain format for $domain" + return 1 + fi + + # Start with the last 2 parts (e.g., example.com) and move forward + i=2 + max_attempts=$((_parts + 1)) + while [ $i -le $max_attempts ]; do + _cutdomain=$(echo "$domain" | rev | cut -d . -f 1-$i | rev) + if [ -z "$_cutdomain" ]; then + break + fi + + _debug "Checking if $_cutdomain is root zone" + if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then + _domain="$_cutdomain" + _debug "Root zone found: $_domain" + # Save the detected root domain to configuration for future use + _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$_domain" + _info "Root domain $_domain saved to configuration for future use." + return 0 + fi + i=$((i + 1)) + done + + _err "Could not detect root zone for $domain after $max_attempts attempts. Please set SPACESHIP_ROOT_DOMAIN manually." + return 1 +} + +_spaceship_api_request() { + method="$1" + url="$2" + payload="$3" + + _debug "Sending $method request to $url with payload $payload" + if [ "$method" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$payload" "$url" "" "$method")" + fi + + if [ "$?" != "0" ]; then + _err "API request failed. Response: $response" + return 1 + fi + + _debug "API response: $response" + return 0 +} From 5e8b40faf65fe2d712698798a25aa209a1c0bdd9 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:10:51 +0800 Subject: [PATCH 021/689] Spaceship: fix rm --- dnsapi/dns_spaceship.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index f94d9027..53dece76 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -93,7 +93,7 @@ dns_spaceship_rm() { # Prepare payload and URL for deleting TXT record # Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API - payload="{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}" + payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]" url="$SPACESHIP_API_BASE/dns/records/$_domain" # Send API request @@ -156,6 +156,7 @@ _get_root() { while [ $i -le $max_attempts ]; do _cutdomain=$(echo "$domain" | rev | cut -d . -f 1-$i | rev) if [ -z "$_cutdomain" ]; then + _debug "Reached end of domain parts." break fi From e55a54f3d4cfb92102f3a5036882a64100f563a0 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:30:43 +0800 Subject: [PATCH 022/689] Spaceship: fix get_root --- dnsapi/dns_spaceship.sh | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 53dece76..d70a9a81 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -132,47 +132,49 @@ _spaceship_init() { _get_root() { domain="$1" - # Check if user manually specified root domain + # Check manual override SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readaccountconf_mutable SPACESHIP_ROOT_DOMAIN)}" if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then _domain="$SPACESHIP_ROOT_DOMAIN" _debug "Using manually specified or saved root domain: $_domain" - # Ensure it's saved (in case it was read from config but not saved previously) _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" return 0 fi - # Split domain into parts and try from back to front - _debug "Detecting root zone for $domain from back to front" - _parts=$(echo "$domain" | tr '.' '\n' | wc -l) - if [ "$_parts" -lt 2 ]; then - _err "Invalid domain format for $domain" - return 1 - fi + _debug "Detecting root zone for '$domain'" - # Start with the last 2 parts (e.g., example.com) and move forward i=2 - max_attempts=$((_parts + 1)) - while [ $i -le $max_attempts ]; do - _cutdomain=$(echo "$domain" | rev | cut -d . -f 1-$i | rev) + p=1 + while true; do + _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + + _debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)" + if [ -z "$_cutdomain" ]; then - _debug "Reached end of domain parts." + _debug "Cut resulted in empty string, root zone not found." break fi - _debug "Checking if $_cutdomain is root zone" + # Call the API to check if this _cutdomain is a manageable zone if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then + # API call succeeded (HTTP 200 OK for GET /dns/records) _domain="$_cutdomain" - _debug "Root zone found: $_domain" - # Save the detected root domain to configuration for future use + _debug "Root zone found: '$_domain'" + + # Save the detected root domain _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$_domain" - _info "Root domain $_domain saved to configuration for future use." + _info "Root domain '$_domain' saved to configuration for future use." + return 0 fi + + _debug "API check failed for '$_cutdomain'. Continuing search." + + p=$i i=$((i + 1)) done - _err "Could not detect root zone for $domain after $max_attempts attempts. Please set SPACESHIP_ROOT_DOMAIN manually." + _err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually." return 1 } From 827315e059c1b8ceba9828a9cbaf062768eeec6d Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Tue, 15 Apr 2025 20:49:48 +0800 Subject: [PATCH 023/689] Spaceship: valid api response --- dnsapi/dns_spaceship.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index d70a9a81..501131b8 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -195,6 +195,18 @@ _spaceship_api_request() { return 1 fi - _debug "API response: $response" - return 0 + _debug "API response body: $response" + + if [ "$method" = "GET" ]; then + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then + return 0 + fi + else + if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then + return 0 + fi + fi + + _debug "API response header: $HTTP_HEADER" + return 1 } From c6a9825c0a2c6c4852a869a9cbf4864d1e270ccb Mon Sep 17 00:00:00 2001 From: asavin Date: Fri, 18 Apr 2025 17:25:55 +0200 Subject: [PATCH 024/689] Initial commit --- dnsapi/dns_efficientip.sh | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100755 dnsapi/dns_efficientip.sh diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh new file mode 100755 index 00000000..fe5538bd --- /dev/null +++ b/dnsapi/dns_efficientip.sh @@ -0,0 +1,125 @@ +#!/bin/sh +export 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_Token_Key Alternative API token key identifier, prefered over basic authentication. + EfficientIP_Token_Secret Alternative API token secret, required when using a token key. + EfficientIP_Server EfficientIP SOLIDserver Management IP or FQDN. + EfficientIP_DNS_Name Name of the DNS smart or server. + EfficientIP_View Name of the DNS view (optional). +Issues: github.com/acmesh-official/acme.sh/issues/ +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 set via EXPORT EfficientIP_Token_Key=yourkey" + _err "and EXPORT EfficientIP_Token_Secret=yoursecret" + _err "and try again." + return 1 + fi + + _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}" + + EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) + EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) + + export _H1="Accept-Language:en-US" + baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?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\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | openssl dgst -sha3-256 | cut -d '=' -f 2 | tr -d ' ') + 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 "Successfully created the txt record" + return 0 + else + _err "Error encountered during record addition" + _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}" | openssl dgst -sha3-256 | cut -d '=' -f 2 | tr -d ' ') + EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig" | _base64) + 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 "Successfully deleted the txt record" + return 0 + else + _err "Error encountered during record delete" + _err "${result}" + return 1 + fi +} \ No newline at end of file From 218934e76722697be3c258a7205daf8c1b5e26c0 Mon Sep 17 00:00:00 2001 From: asavin Date: Fri, 18 Apr 2025 18:06:12 +0200 Subject: [PATCH 025/689] Remove export ? --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index fe5538bd..9c06514c 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -1,5 +1,5 @@ #!/bin/sh -export dns_efficientip_info='efficientip.com +dns_efficientip_info='efficientip.com Site: https://efficientip.com/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip Options: From a0c5ef4e6fb9acc47ac6b56bf29081b8b4cbb6ce Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 11:17:14 +0200 Subject: [PATCH 026/689] Fixing shellcheck issues --- dnsapi/dns_efficientip.sh | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 9c06514c..d04aec5d 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -14,7 +14,6 @@ Author: EfficientIP-Labs ' dns_efficientip_add() { - fulldomain=$1 txtvalue=$2 @@ -22,7 +21,7 @@ dns_efficientip_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - if ([ -z "$EfficientIP_Creds" ] && ([ -z "$EfficientIP_Token_Key" ] || [ -z "$EfficientIP_Token_Secret" ])) || [ -z "$EfficientIP_Server" ]; then + if ([ -z "${EfficientIP_Creds}" ] && ([ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ])) || [ -z "${EfficientIP_Server}" ]; then EfficientIP_Creds="" EfficientIP_Token_Key="" EfficientIP_Token_Secret="" @@ -35,6 +34,16 @@ dns_efficientip_add() { 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}" @@ -42,15 +51,13 @@ dns_efficientip_add() { _saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}" _saveaccountconf EfficientIP_View "${EfficientIP_View}" - EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) - EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) - export _H1="Accept-Language:en-US" - baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue" + baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?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 From 7c610124d9cb6f2427bbf8357c2f5d20917426bb Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 11:32:23 +0200 Subject: [PATCH 027/689] Updating Options to meet OptionsAlt pre-requisites --- dnsapi/dns_efficientip.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index d04aec5d..89fb48f5 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -4,11 +4,16 @@ 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_Token_Key Alternative API token key identifier, prefered over basic authentication. + 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 or FQDN. - EfficientIP_DNS_Name Name of the DNS smart or server. - EfficientIP_View Name of the DNS view (optional). + 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/ Author: EfficientIP-Labs ' From 1f77b8926680008b2c11038598f120be8b12edce Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 11:40:01 +0200 Subject: [PATCH 028/689] Updating issue ID --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 89fb48f5..c6638fcc 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -14,7 +14,7 @@ OptionsAlt: 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/ +Issues: github.com/acmesh-official/acme.sh/issues/6325 Author: EfficientIP-Labs ' From 75603755023b9fba0d6710fbe391f99fc7c577ec Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:23:11 +0200 Subject: [PATCH 029/689] Update for testing github action pipeline --- dnsapi/dns_efficientip.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index c6638fcc..eb19fe38 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -78,13 +78,17 @@ dns_efficientip_add() { export _H3="X-SDS-TS: ${TS}" fi - result="$(_post "" "${baseurlnObject}" "" "POST")" + if [ -n "${GITHUB_ACTIONS+1}" ]; then + result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" + else + result="$(_post "" "${baseurlnObject}" "" "POST")" + fi; if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Successfully created the txt record" + _info "Record successfully created" return 0 else - _err "Error encountered during record addition" + _err "Error creating the record" _err "${result}" return 1 fi @@ -124,13 +128,17 @@ dns_efficientip_rm() { export _H3="X-SDS-TS: $TS" fi - result="$(_post "" "${baseurlnObject}" "" "DELETE")" + if [ -n "${GITHUB_ACTIONS+1}" ]; then + result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" + else + result="$(_post "" "${baseurlnObject}" "" "DELETE")" + fi if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Successfully deleted the txt record" + _info "Record successfully deleted" return 0 else - _err "Error encountered during record delete" + _err "Error deleting the record" _err "${result}" return 1 fi From e089a3d8a152aaff32bd3cc3e7d786226a949901 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:31:17 +0200 Subject: [PATCH 030/689] Update for testing github action pipeline --- dnsapi/dns_efficientip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index eb19fe38..b39d8fba 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -33,9 +33,9 @@ dns_efficientip_add() { 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 set via EXPORT EfficientIP_Token_Key=yourkey" + _err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey" _err "and EXPORT EfficientIP_Token_Secret=yoursecret" - _err "and try again." + _err "then try again." return 1 fi From eabd7592fe3942bc38b501c2cd25a25eda9c56ac Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:41:22 +0200 Subject: [PATCH 031/689] Fixing sh syntax --- dnsapi/dns_efficientip.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index b39d8fba..ab946e3e 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -41,12 +41,14 @@ dns_efficientip_add() { if [ -z "${EfficientIP_DNS_Name}" ]; then EfficientIP_DNS_Name="" - fi; + fi + EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) if [ -z "${EfficientIP_View}" ]; then EfficientIP_View="" - fi; + fi + EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) _saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}" @@ -82,7 +84,7 @@ dns_efficientip_add() { result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" else result="$(_post "" "${baseurlnObject}" "" "POST")" - fi; + fi if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then _info "Record successfully created" @@ -113,6 +115,7 @@ dns_efficientip_rm() { if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}" fi + if [ "${EfficientIP_ViewEncoded}" != "" ]; then baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}" fi @@ -142,4 +145,4 @@ dns_efficientip_rm() { _err "${result}" return 1 fi -} \ No newline at end of file +} From 9eeb979c7bfdc9ed6a5455223e10f0761691029b Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:49:41 +0200 Subject: [PATCH 032/689] Fixing shellcheck issue --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index ab946e3e..292d6b5e 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -26,7 +26,7 @@ dns_efficientip_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - if ([ -z "${EfficientIP_Creds}" ] && ([ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ])) || [ -z "${EfficientIP_Server}" ]; then + if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then EfficientIP_Creds="" EfficientIP_Token_Key="" EfficientIP_Token_Secret="" From 5bc01aa2518bdde413bc8a0dffce705d1c6d602c Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:56:49 +0200 Subject: [PATCH 033/689] Disabling SC2034 --- dnsapi/dns_efficientip.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 292d6b5e..9dc2e374 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -1,4 +1,5 @@ #!/bin/sh +# shellcheck disable=SC2034 dns_efficientip_info='efficientip.com Site: https://efficientip.com/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip From 59a43ce5d1cea2803b3f3d138b5459463c7d253b Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 16:27:36 +0200 Subject: [PATCH 034/689] Disabling SC2034 --- dnsapi/dns_efficientip.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 9dc2e374..d1fdccf6 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -14,7 +14,6 @@ OptionsAlt: 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 ' From 90e9d8ff52bd26c2dd99f6b5d71ccd099a8d9389 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 16:38:37 +0200 Subject: [PATCH 035/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index d1fdccf6..9a73303f 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -145,4 +145,4 @@ dns_efficientip_rm() { _err "${result}" return 1 fi -} +} \ No newline at end of file From 5bb09f469f96eb6c3cf9d72a4ac504c409484f51 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 17:06:33 +0200 Subject: [PATCH 036/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 9a73303f..bed5c1d6 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -80,7 +80,7 @@ dns_efficientip_add() { export _H3="X-SDS-TS: ${TS}" fi - if [ -n "${GITHUB_ACTIONS+1}" ]; then + if [ -n "${TEST_DNS+1}" ]; then result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" else result="$(_post "" "${baseurlnObject}" "" "POST")" @@ -131,7 +131,7 @@ dns_efficientip_rm() { export _H3="X-SDS-TS: $TS" fi - if [ -n "${GITHUB_ACTIONS+1}" ]; then + if [ -n "${TEST_DNS+1}" ]; then result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" else result="$(_post "" "${baseurlnObject}" "" "DELETE")" @@ -145,4 +145,4 @@ dns_efficientip_rm() { _err "${result}" return 1 fi -} \ No newline at end of file +} From 7a0450a7f466b21e37c452aa1bac8e343c448391 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 17:55:23 +0200 Subject: [PATCH 037/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index bed5c1d6..546b8dc2 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -90,7 +90,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the record" + _err "Error creating the DNS record" _err "${result}" return 1 fi @@ -141,7 +141,7 @@ dns_efficientip_rm() { _info "Record successfully deleted" return 0 else - _err "Error deleting the record" + _err "Error deleting the DNS record" _err "${result}" return 1 fi From 30d5d1aea9a9825eca1bcdc4898a162bb2b8f621 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 18:01:29 +0200 Subject: [PATCH 038/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 546b8dc2..a44cab98 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -80,11 +80,7 @@ dns_efficientip_add() { export _H3="X-SDS-TS: ${TS}" fi - if [ -n "${TEST_DNS+1}" ]; then - result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" - else - result="$(_post "" "${baseurlnObject}" "" "POST")" - fi + result="$(_post "" "${baseurlnObject}" "" "POST")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then _info "Record successfully created" @@ -131,11 +127,7 @@ dns_efficientip_rm() { export _H3="X-SDS-TS: $TS" fi - if [ -n "${TEST_DNS+1}" ]; then - result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" - else - result="$(_post "" "${baseurlnObject}" "" "DELETE")" - fi + result="$(_post "" "${baseurlnObject}" "" "DELETE")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then _info "Record successfully deleted" From 1ce8d3ae9bf03146bc6cf0142cce8e4f4898db8c Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 23 Apr 2025 21:42:39 +0200 Subject: [PATCH 039/689] start 3.1.2 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index dd21785d..4d75ab62 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.1 +VER=3.1.2 PROJECT_NAME="acme.sh" From 24a1b93842dda7ce74a83a6165e7a642c499df07 Mon Sep 17 00:00:00 2001 From: Joe Bauser Date: Sat, 2 Mar 2024 13:01:59 -0500 Subject: [PATCH 040/689] Add deploy/zyxel_gs1900.sh Add support for deploying to the Zyxel GS1900 line of switches as long as those switches are running at least firmware V2.80. Tested on a Zyxel GS1900-8 and GS1900-24E Resolves #5042 --- deploy/zyxel_gs1900.sh | 500 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 deploy/zyxel_gs1900.sh diff --git a/deploy/zyxel_gs1900.sh b/deploy/zyxel_gs1900.sh new file mode 100644 index 00000000..443a5b05 --- /dev/null +++ b/deploy/zyxel_gs1900.sh @@ -0,0 +1,500 @@ +#!/usr/bin/env sh + +# Deploy certificates to Zyxel GS1900 series switches +# +# This script uses the https web administration interface in order +# to upload updated certificates to Zyxel GS1900 series switches. +# Only a few models have been tested but untested switches from the +# same model line may work as well. If you test and confirm a switch +# as working please submit a pull request updating this compatibility +# list! +# +# Known Issues: +# 1. This is a consumer grade switch and is a bit underpowered +# the longer the RSA key size the slower your switch web UI +# will be. RSA 2048 will work, RSA 4096 will work but you may +# experience performance problems. +# 2. You must use RSA certificates. The switch will reject EC-256 +# and EC-384 certificates in firmware 2.80 +# See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/ +# +# Current GS1900 Switch Compatibility: +# GS1900-8 - Working as of firmware V2.80 +# GS1900-8HP - Untested +# GS1900-10HP - Untested +# GS1900-16 - Untested +# GS1900-24 - Untested +# GS1900-24E - Working as of firmware V2.80 +# GS1900-24EP - Untested +# GS1900-24HP - Untested +# GS1900-48 - Untested +# GS1900-48HP - Untested +# +# Prerequisite Setup Steps: +# 1. Install at least firmware V2.80 on your switch +# 2. Enable HTTPS web management on your switch +# +# Usage: +# 1. Ensure the switch has firmware V2.80 or later. +# 2. Ensure the switch has HTTPS management enabled. +# 3. Set the appropriate environment variables for your environment. +# +# DEPLOY_ZYXEL_SWITCH - The switch hostname. (Default: _cdomain) +# DEPLOY_ZYXEL_SWITCH_USER - The webadmin user. (Default: admin) +# DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch. +# DEPLOY_ZYXEL_SWITCH_REBOOT - If "1" reboot after update. (Default: "0") +# +# 4. Run the deployment plugin: +# acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com +# +# returns 0 means success, otherwise error. + +#domain keyfile certfile cafile fullchain +zyxel_gs1900_deploy() { + _zyxel_gs1900_minimum_firmware_version="v2.80" + + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug2 _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf DEPLOY_ZYXEL_SWITCH + _getdeployconf DEPLOY_ZYXEL_SWITCH_USER + _getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD + _getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT + + if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then + DEPLOY_ZYXEL_SWITCH="$_cdomain" + fi + + if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then + DEPLOY_ZYXEL_SWITCH_USER="admin" + fi + + if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then + DEPLOY_ZYXEL_SWITCH_PASSWORD="1234" + fi + + if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then + DEPLOY_ZYXEL_SWITCH_REBOOT="0" + fi + + _savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH" + _savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER" + _savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD" + _savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT" + + _debug DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH" + _debug DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER" + _secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD" + _debug DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT" + + _zyxel_switch_base_uri="https://${DEPLOY_ZYXEL_SWITCH}" + + _info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}." + _zyxel_gs1900_deployment_precheck || return $? + + _zyxel_gs1900_should_update + if [ "$?" != "0" ]; then + _info "The switch already has our certificate installed. No update required." + return 0 + else + _info "The switch does not yet have our certificate installed." + fi + + _info "Logging into the switch web interface." + _zyxel_gs1900_login || return $? + + _info "Validating the switch is compatible with this deployment process." + _zyxel_gs1900_validate_device_compatibility || return $? + + _info "Uploading the certificate." + _zyxel_gs1900_upload_certificate || return $? + + if [ "$DEPLOY_ZYXEL_SWITCH_REBOOT" = "1" ]; then + _info "Rebooting the switch." + _zyxel_gs1900_trigger_reboot || return $? + fi + + return 0 +} + +_zyxel_gs1900_deployment_precheck() { + # Initialize the keylength if it isn't already + if [ -z "$Le_Keylength" ]; then + Le_Keylength="" + fi + + if _isEccKey "$Le_Keylength"; then + _info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!" + _info "You can continue, but your switch may reject your key." + elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then + _info "Warning: Your RSA key length is greater than 2048!" + _info "You can continue, but you may experience performance issues in the web administration interface." + fi + + # Check the server for some common failure modes prior to authentication and certificate upload in order to avoid + # sending a certificate when we may not want to. + test_login_response=$(_post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" 2>&1) + test_login_page_exitcode="$?" + _debug3 "Test Login Response: ${test_login_response}" + if [ "$test_login_page_exitcode" -ne "0" ]; then + if { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then + _err "The SSL certificate at $_zyxel_switch_base_uri could not be validated." + _err "Please double check your hostname, port, and that you are actually connecting to your switch." + _err "If the problem persists then please ensure that the certificate is not self-signed, has not" + _err "expired, and matches the switch hostname. If you expect validation to fail then you can disable" + _err "certificate validation by running with --insecure." + return 1 + elif [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then + _debug3 "Intentionally ignore curl exit code 56 in our precheck" + else + _err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri." + return 1 + fi + fi +} + +_zyxel_gs1900_login() { + # Login to the switch and set the appropriate auth cookie in _H1 + username_encoded=$(printf "%s" "$DEPLOY_ZYXEL_SWITCH_USER" | _url_encode) + password_encoded=$(_zyxel_gs1900_password_obfuscate "$DEPLOY_ZYXEL_SWITCH_PASSWORD" | _url_encode) + + login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n') + auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n') + if [ "$auth_response" != "OK" ]; then + _err "Login failed due to invalid credentials." + _err "Please double check the configured username and password and try again." + return 1 + fi + + sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';') + _secure_debug2 "sessionid" "$sessionid" + + export _H1="Cookie: $sessionid" + _secure_debug2 "_H1" "$_H1" + + return 0 +} + +_zyxel_gs1900_validate_device_compatibility() { + # Check the switches model and firmware version and throw errors + # if this script isn't compatible. + device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n') + + model_name=$(_zyxel_gs1900_get_model "$device_info_html") + _debug2 "model_name" "$model_name" + if [ -z "$model_name" ]; then + _err "Could not find the switch model name." + _err "Please re-run with --debug and report a bug." + return $? + fi + + if ! expr "$model_name" : "GS1900-" >/dev/null; then + _err "Switch is an unsupported model: $model_name" + return 1 + fi + + firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html") + _debug2 "firmware_version" "$firmware_version" + if [ -z "$firmware_version" ]; then + _err "Could not find the switch firmware version." + _err "Please re-run with --debug and report a bug." + return $? + fi + + _debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version" + minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version") + _debug2 "minimum_major_version" "$minimum_major_version" + minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version") + _debug2 "minimum_minor_version" "$minimum_minor_version" + + _debug2 "firmware_version" "$firmware_version" + firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version") + _debug2 "firmware_major_version" "$firmware_major_version" + firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version") + _debug2 "firmware_minor_version" "$firmware_minor_version" + + _ret=0 + if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then + _ret=1 + elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then + _ret=1 + fi + + if [ "$_ret" != "0" ]; then + _err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version." + fi + + return $? +} + +_zyxel_gs1900_should_update() { + # Get the remote certificate serial number + _remote_cert=$(${ACME_OPENSSL_BIN:-openssl} s_client -showcerts -connect "${DEPLOY_ZYXEL_SWITCH}:443" 2>/dev/null "${upload_post_request}" + + _info "Upload certificate to the switch" + + # Unfortunately we cannot rely upon the switch response across switch models + # to return a consistent body return - so we cannot inspect the result of this + # upload to determine success. + upload_response=$(_zyxel_upload_pkcs12 "${upload_post_request}" "${upload_post_boundary}" 2>&1) + _debug3 "Upload response: ${upload_response}" + rm "${upload_post_request}" + + # Pause for a few seconds to give the switch a chance to process the certificate + # For some reason I've found this to be necessary on my GS1900-24E + _debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate." + sleep "4" + + # Check to see whether or not our update was successful + _ret=0 + _zyxel_gs1900_should_update + if [ "$?" != "0" ]; then + _info "The certificate was updated successfully" + else + _ret=1 + _err "The certificate upload does not appear to have worked." + _err "The remote certificate does not match the certificate we tried to upload." + _err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug." + fi + + # ensure the temporary files are cleaned up + [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}" + + return $_ret +} + +# make the certificate upload request using either +# --data binary with @ for file access in CURL +# or using --post-file for wget to ensure we upload +# the pkcs12 without getting tripped up on null bytes +# +# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker] +_zyxel_upload_pkcs12() { + bodyfilename="$1" + multipartformmarker="$2" + _post_url="${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi" + httpmethod="POST" + _postContentType="multipart/form-data; boundary=${multipartformmarker}" + + if [ -z "$httpmethod" ]; then + httpmethod="POST" + fi + _debug $httpmethod + _debug "_post_url" "$_post_url" + _debug2 "bodyfilename" "$bodyfilename" + _debug2 "_postContentType" "$_postContentType" + + _inithttp + + if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then + _CURL="$_ACME_CURL" + if [ "$HTTPS_INSECURE" ]; then + _CURL="$_CURL --insecure " + fi + if [ "$httpmethod" = "HEAD" ]; then + _CURL="$_CURL -I " + fi + _debug "_CURL" "$_CURL" + + response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data-binary "@${bodyfilename}" "$_post_url")" + + _ret="$?" + if [ "$_ret" != "0" ]; then + _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" + if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then + _err "Here is the curl dump log:" + _err "$(cat "$_CURL_DUMP")" + fi + fi + elif [ "$_ACME_WGET" ]; then + _WGET="$_ACME_WGET" + if [ "$HTTPS_INSECURE" ]; then + _WGET="$_WGET --no-check-certificate " + fi + _debug "_WGET" "$_WGET" + + response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-file="${bodyfilename}" "$_post_url" 2>"$HTTP_HEADER")" + + _ret="$?" + if [ "$_ret" = "8" ]; then + _ret=0 + _debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later." + fi + if [ "$_ret" != "0" ]; then + _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" + fi + if _contains "$_WGET" " -d "; then + # Demultiplex wget debug output + cat "$HTTP_HEADER" >&2 + _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" + fi + # remove leading whitespaces from header to match curl format + _sed_i 's/^ //g' "$HTTP_HEADER" + else + _ret="$?" + _err "Neither curl nor wget have been found, cannot make $httpmethod request." + fi + _debug "_ret" "$_ret" + printf "%s" "$response" + return $_ret +} + +_zyxel_gs1900_trigger_reboot() { + # Trigger a reboot via the management reboot page in the web ui + reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n') + reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g') + _secure_debug2 "reboot_xss_value" "$reboot_xss_value" + + reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded") + reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...") + + if [ -z "$reboot_message" ]; then + _err "Failed to trigger switch reboot!" + return 1 + fi + + return 0 +} + +# password +_zyxel_gs1900_password_obfuscate() { + # Return the password obfuscated via the same method used by the + # switch's web UI login process + echo "$1" | awk '{ + encoded = ""; + password = $1; + allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + len = length($1); + pwi = length($1); + + for (i=1; i <= (321 - pwi); i++) + { + if (0 == i % 5 && pwi > 0) + { + encoded = (encoded)(substr(password, pwi--, 1)); + } + else if (i == 123) + { + if (len < 10) + { + encoded = (encoded)(0); + } + else + { + encoded = (encoded)(int(len / 10)); + } + } + else if (i == 289) + { + encoded = (encoded)(len % 10) + } + else + { + encoded = (encoded)(substr(allowed, int(rand() * length(allowed)), 1)) + } + } + printf("%s", encoded); + }' +} + +# html label +_zyxel_html_table_lookup() { + # Look up a value in the html representing the status page of the switch + # when provided with the html of the page and the label (i.e. "Model Name:") + html="$1" + label=$(printf "%s" "$2" | tr -d ' ') + lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's//\n/g' | sed 's/]*>//g' | tr -d ' ' | grep -i "$label" | sed "s/$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' +} From f29bfd995d3204398d7d7346f25092de01a39efc Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 10:15:56 +0200 Subject: [PATCH 041/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index a44cab98..997f58f4 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record" + _err "Error creating the DNS record using EIP" _err "${result}" return 1 fi From 4d933c23a8660ab472445f22f688c97eb3c97170 Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 10:43:46 +0200 Subject: [PATCH 042/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 997f58f4..bd95eb42 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record using EIP" + _err "Error creating the DNS record EIP" _err "${result}" return 1 fi From 91081ade3c82949c9a492232d9cb042966dcb507 Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 10:50:57 +0200 Subject: [PATCH 043/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index bd95eb42..997f58f4 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record EIP" + _err "Error creating the DNS record using EIP" _err "${result}" return 1 fi From 9f09dcd18cb6538875f5937199dc1358b6b270cb Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 11:13:38 +0200 Subject: [PATCH 044/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 997f58f4..e9d190b7 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record using EIP" + _err "Error creating the DNS record with EIP" _err "${result}" return 1 fi From 7f1423dd6f77a073359a2300408e111b5ee1176c Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 13:33:28 +0200 Subject: [PATCH 045/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index e9d190b7..f2eaaf3b 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -59,7 +59,7 @@ dns_efficientip_add() { _saveaccountconf EfficientIP_View "${EfficientIP_View}" export _H1="Accept-Language:en-US" - baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_name=${fulldomain}&rr_value1=${txtvalue}" + 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}" @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record with EIP" + _err "Error creating the DNS record" _err "${result}" return 1 fi From 419738fbd5e3f7ad67f67a322e2f2cac18658dfd Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 16:05:20 +0200 Subject: [PATCH 046/689] Triggering pipeline with DNS_WILDCARD --- dnsapi/dns_efficientip.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index f2eaaf3b..afcd9af4 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -83,10 +83,10 @@ dns_efficientip_add() { result="$(_post "" "${baseurlnObject}" "" "POST")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Record successfully created" + _info "DNS record successfully created" return 0 else - _err "Error creating the DNS record" + _err "Error creating DNS record" _err "${result}" return 1 fi @@ -130,10 +130,10 @@ dns_efficientip_rm() { result="$(_post "" "${baseurlnObject}" "" "DELETE")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Record successfully deleted" + _info "DNS Record successfully deleted" return 0 else - _err "Error deleting the DNS record" + _err "Error deleting DNS record" _err "${result}" return 1 fi From e1d447847f0e3da3e213ae1b1a6c154bc72a40f0 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Fri, 25 Apr 2025 05:21:52 +0800 Subject: [PATCH 047/689] Spaceship: fix domain conf --- dnsapi/dns_spaceship.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 501131b8..7e9fb167 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -133,11 +133,11 @@ _get_root() { domain="$1" # Check manual override - SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readaccountconf_mutable SPACESHIP_ROOT_DOMAIN)}" + SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}" if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then _domain="$SPACESHIP_ROOT_DOMAIN" _debug "Using manually specified or saved root domain: $_domain" - _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" + _savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" return 0 fi @@ -162,7 +162,7 @@ _get_root() { _debug "Root zone found: '$_domain'" # Save the detected root domain - _saveaccountconf_mutable SPACESHIP_ROOT_DOMAIN "$_domain" + _savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain" _info "Root domain '$_domain' saved to configuration for future use." return 0 From d01aefd1eb17c53604604645798d77a9ace399d9 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Fri, 25 Apr 2025 05:24:05 +0800 Subject: [PATCH 048/689] Spaceship: i starts from 1 --- dnsapi/dns_spaceship.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 7e9fb167..cc3f066f 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -143,7 +143,7 @@ _get_root() { _debug "Detecting root zone for '$domain'" - i=2 + i=1 p=1 while true; do _cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) From e08f9080c2c90e933052c4047dc6be5f37f65783 Mon Sep 17 00:00:00 2001 From: asavin Date: Fri, 18 Apr 2025 17:25:55 +0200 Subject: [PATCH 049/689] Initial commit --- dnsapi/dns_efficientip.sh | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100755 dnsapi/dns_efficientip.sh diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh new file mode 100755 index 00000000..fe5538bd --- /dev/null +++ b/dnsapi/dns_efficientip.sh @@ -0,0 +1,125 @@ +#!/bin/sh +export 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_Token_Key Alternative API token key identifier, prefered over basic authentication. + EfficientIP_Token_Secret Alternative API token secret, required when using a token key. + EfficientIP_Server EfficientIP SOLIDserver Management IP or FQDN. + EfficientIP_DNS_Name Name of the DNS smart or server. + EfficientIP_View Name of the DNS view (optional). +Issues: github.com/acmesh-official/acme.sh/issues/ +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 set via EXPORT EfficientIP_Token_Key=yourkey" + _err "and EXPORT EfficientIP_Token_Secret=yoursecret" + _err "and try again." + return 1 + fi + + _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}" + + EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) + EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) + + export _H1="Accept-Language:en-US" + baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?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\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | openssl dgst -sha3-256 | cut -d '=' -f 2 | tr -d ' ') + 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 "Successfully created the txt record" + return 0 + else + _err "Error encountered during record addition" + _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}" | openssl dgst -sha3-256 | cut -d '=' -f 2 | tr -d ' ') + EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig" | _base64) + 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 "Successfully deleted the txt record" + return 0 + else + _err "Error encountered during record delete" + _err "${result}" + return 1 + fi +} \ No newline at end of file From 8ca90297e730305f00ac645d03180446efc20dc2 Mon Sep 17 00:00:00 2001 From: asavin Date: Fri, 18 Apr 2025 18:06:12 +0200 Subject: [PATCH 050/689] Remove export ? --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index fe5538bd..9c06514c 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -1,5 +1,5 @@ #!/bin/sh -export dns_efficientip_info='efficientip.com +dns_efficientip_info='efficientip.com Site: https://efficientip.com/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip Options: From f7d8abe8ea94a1673c50e86b9479140e3ba69342 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 11:17:14 +0200 Subject: [PATCH 051/689] Fixing shellcheck issues --- dnsapi/dns_efficientip.sh | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 9c06514c..d04aec5d 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -14,7 +14,6 @@ Author: EfficientIP-Labs ' dns_efficientip_add() { - fulldomain=$1 txtvalue=$2 @@ -22,7 +21,7 @@ dns_efficientip_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - if ([ -z "$EfficientIP_Creds" ] && ([ -z "$EfficientIP_Token_Key" ] || [ -z "$EfficientIP_Token_Secret" ])) || [ -z "$EfficientIP_Server" ]; then + if ([ -z "${EfficientIP_Creds}" ] && ([ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ])) || [ -z "${EfficientIP_Server}" ]; then EfficientIP_Creds="" EfficientIP_Token_Key="" EfficientIP_Token_Secret="" @@ -35,6 +34,16 @@ dns_efficientip_add() { 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}" @@ -42,15 +51,13 @@ dns_efficientip_add() { _saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}" _saveaccountconf EfficientIP_View "${EfficientIP_View}" - EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) - EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) - export _H1="Accept-Language:en-US" - baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue" + baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?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 From 8484565e951845e47d24901dd0123a6dc73520cf Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 11:32:23 +0200 Subject: [PATCH 052/689] Updating Options to meet OptionsAlt pre-requisites --- dnsapi/dns_efficientip.sh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index d04aec5d..89fb48f5 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -4,11 +4,16 @@ 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_Token_Key Alternative API token key identifier, prefered over basic authentication. + 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 or FQDN. - EfficientIP_DNS_Name Name of the DNS smart or server. - EfficientIP_View Name of the DNS view (optional). + 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/ Author: EfficientIP-Labs ' From 67855f21d45136741a4efdde94990a3b7a9acaed Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 11:40:01 +0200 Subject: [PATCH 053/689] Updating issue ID --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 89fb48f5..c6638fcc 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -14,7 +14,7 @@ OptionsAlt: 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/ +Issues: github.com/acmesh-official/acme.sh/issues/6325 Author: EfficientIP-Labs ' From 4d7cb7de5f78fa788927eaa89dd863bb66e8d7dd Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:23:11 +0200 Subject: [PATCH 054/689] Update for testing github action pipeline --- dnsapi/dns_efficientip.sh | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index c6638fcc..eb19fe38 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -78,13 +78,17 @@ dns_efficientip_add() { export _H3="X-SDS-TS: ${TS}" fi - result="$(_post "" "${baseurlnObject}" "" "POST")" + if [ -n "${GITHUB_ACTIONS+1}" ]; then + result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" + else + result="$(_post "" "${baseurlnObject}" "" "POST")" + fi; if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Successfully created the txt record" + _info "Record successfully created" return 0 else - _err "Error encountered during record addition" + _err "Error creating the record" _err "${result}" return 1 fi @@ -124,13 +128,17 @@ dns_efficientip_rm() { export _H3="X-SDS-TS: $TS" fi - result="$(_post "" "${baseurlnObject}" "" "DELETE")" + if [ -n "${GITHUB_ACTIONS+1}" ]; then + result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" + else + result="$(_post "" "${baseurlnObject}" "" "DELETE")" + fi if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Successfully deleted the txt record" + _info "Record successfully deleted" return 0 else - _err "Error encountered during record delete" + _err "Error deleting the record" _err "${result}" return 1 fi From 1f056998f3017ae63848cb763a546f57251900a2 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:31:17 +0200 Subject: [PATCH 055/689] Update for testing github action pipeline --- dnsapi/dns_efficientip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index eb19fe38..b39d8fba 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -33,9 +33,9 @@ dns_efficientip_add() { 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 set via EXPORT EfficientIP_Token_Key=yourkey" + _err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey" _err "and EXPORT EfficientIP_Token_Secret=yoursecret" - _err "and try again." + _err "then try again." return 1 fi From 292026288af0d2f0e5cea0bbb304b1ecf4e14e6c Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:41:22 +0200 Subject: [PATCH 056/689] Fixing sh syntax --- dnsapi/dns_efficientip.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index b39d8fba..ab946e3e 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -41,12 +41,14 @@ dns_efficientip_add() { if [ -z "${EfficientIP_DNS_Name}" ]; then EfficientIP_DNS_Name="" - fi; + fi + EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) if [ -z "${EfficientIP_View}" ]; then EfficientIP_View="" - fi; + fi + EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) _saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}" @@ -82,7 +84,7 @@ dns_efficientip_add() { result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" else result="$(_post "" "${baseurlnObject}" "" "POST")" - fi; + fi if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then _info "Record successfully created" @@ -113,6 +115,7 @@ dns_efficientip_rm() { if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}" fi + if [ "${EfficientIP_ViewEncoded}" != "" ]; then baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}" fi @@ -142,4 +145,4 @@ dns_efficientip_rm() { _err "${result}" return 1 fi -} \ No newline at end of file +} From c9287071e3a836ae0c2deaf02e2c3de7879f2d71 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:49:41 +0200 Subject: [PATCH 057/689] Fixing shellcheck issue --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index ab946e3e..292d6b5e 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -26,7 +26,7 @@ dns_efficientip_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - if ([ -z "${EfficientIP_Creds}" ] && ([ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ])) || [ -z "${EfficientIP_Server}" ]; then + if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then EfficientIP_Creds="" EfficientIP_Token_Key="" EfficientIP_Token_Secret="" From af92bbca2ac10b9e7ecf4c042fb696cdfeb67d46 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 15:56:49 +0200 Subject: [PATCH 058/689] Disabling SC2034 --- dnsapi/dns_efficientip.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 292d6b5e..9dc2e374 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -1,4 +1,5 @@ #!/bin/sh +# shellcheck disable=SC2034 dns_efficientip_info='efficientip.com Site: https://efficientip.com/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip From a1eee5923a5b4f947bd9fea8b4be351f548d203f Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 16:27:36 +0200 Subject: [PATCH 059/689] Disabling SC2034 --- dnsapi/dns_efficientip.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 9dc2e374..d1fdccf6 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -14,7 +14,6 @@ OptionsAlt: 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 ' From 13631ea2de465e44153d0da0e685e228aee7c427 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 16:38:37 +0200 Subject: [PATCH 060/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index d1fdccf6..9a73303f 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -145,4 +145,4 @@ dns_efficientip_rm() { _err "${result}" return 1 fi -} +} \ No newline at end of file From 74ca0fb76307370b3e40f6b014af893c5fb0c4ae Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 17:06:33 +0200 Subject: [PATCH 061/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 9a73303f..bed5c1d6 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -80,7 +80,7 @@ dns_efficientip_add() { export _H3="X-SDS-TS: ${TS}" fi - if [ -n "${GITHUB_ACTIONS+1}" ]; then + if [ -n "${TEST_DNS+1}" ]; then result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" else result="$(_post "" "${baseurlnObject}" "" "POST")" @@ -131,7 +131,7 @@ dns_efficientip_rm() { export _H3="X-SDS-TS: $TS" fi - if [ -n "${GITHUB_ACTIONS+1}" ]; then + if [ -n "${TEST_DNS+1}" ]; then result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" else result="$(_post "" "${baseurlnObject}" "" "DELETE")" @@ -145,4 +145,4 @@ dns_efficientip_rm() { _err "${result}" return 1 fi -} \ No newline at end of file +} From 42febe97b56071054a208acc0b0d415ac9010fe2 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 17:55:23 +0200 Subject: [PATCH 062/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index bed5c1d6..546b8dc2 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -90,7 +90,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the record" + _err "Error creating the DNS record" _err "${result}" return 1 fi @@ -141,7 +141,7 @@ dns_efficientip_rm() { _info "Record successfully deleted" return 0 else - _err "Error deleting the record" + _err "Error deleting the DNS record" _err "${result}" return 1 fi From c421e2ddfcea8cc0996a39a157ebabb4ff55cb91 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 22 Apr 2025 18:01:29 +0200 Subject: [PATCH 063/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 546b8dc2..a44cab98 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -80,11 +80,7 @@ dns_efficientip_add() { export _H3="X-SDS-TS: ${TS}" fi - if [ -n "${TEST_DNS+1}" ]; then - result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" - else - result="$(_post "" "${baseurlnObject}" "" "POST")" - fi + result="$(_post "" "${baseurlnObject}" "" "POST")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then _info "Record successfully created" @@ -131,11 +127,7 @@ dns_efficientip_rm() { export _H3="X-SDS-TS: $TS" fi - if [ -n "${TEST_DNS+1}" ]; then - result="$(printf "[{\"ret_oid\": \"%d\"}]" "42")" - else - result="$(_post "" "${baseurlnObject}" "" "DELETE")" - fi + result="$(_post "" "${baseurlnObject}" "" "DELETE")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then _info "Record successfully deleted" From 67fd35127c714a9392aeec3efb672220f7c3dc22 Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 10:15:56 +0200 Subject: [PATCH 064/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index a44cab98..997f58f4 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record" + _err "Error creating the DNS record using EIP" _err "${result}" return 1 fi From 3baa5e145f56aaab785150762c24d4599728d72d Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 10:43:46 +0200 Subject: [PATCH 065/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 997f58f4..bd95eb42 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record using EIP" + _err "Error creating the DNS record EIP" _err "${result}" return 1 fi From 947e872850c07f7b534f447a42b022c712013ea6 Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 10:50:57 +0200 Subject: [PATCH 066/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index bd95eb42..997f58f4 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record EIP" + _err "Error creating the DNS record using EIP" _err "${result}" return 1 fi From c2762d3b6f91a158ba88fe5d627cd1097a67383a Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 11:13:38 +0200 Subject: [PATCH 067/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 997f58f4..e9d190b7 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record using EIP" + _err "Error creating the DNS record with EIP" _err "${result}" return 1 fi From ca4cb018d07c996f73118c8354dbfc263058711f Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 13:33:28 +0200 Subject: [PATCH 068/689] Triggering another action pipeline --- dnsapi/dns_efficientip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index e9d190b7..f2eaaf3b 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -59,7 +59,7 @@ dns_efficientip_add() { _saveaccountconf EfficientIP_View "${EfficientIP_View}" export _H1="Accept-Language:en-US" - baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_name=${fulldomain}&rr_value1=${txtvalue}" + 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}" @@ -86,7 +86,7 @@ dns_efficientip_add() { _info "Record successfully created" return 0 else - _err "Error creating the DNS record with EIP" + _err "Error creating the DNS record" _err "${result}" return 1 fi From a2e52dadb96ace9a69b4fccbcb15dd97d289c6df Mon Sep 17 00:00:00 2001 From: asavin Date: Thu, 24 Apr 2025 16:05:20 +0200 Subject: [PATCH 069/689] Triggering pipeline with DNS_WILDCARD --- dnsapi/dns_efficientip.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index f2eaaf3b..afcd9af4 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -83,10 +83,10 @@ dns_efficientip_add() { result="$(_post "" "${baseurlnObject}" "" "POST")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Record successfully created" + _info "DNS record successfully created" return 0 else - _err "Error creating the DNS record" + _err "Error creating DNS record" _err "${result}" return 1 fi @@ -130,10 +130,10 @@ dns_efficientip_rm() { result="$(_post "" "${baseurlnObject}" "" "DELETE")" if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then - _info "Record successfully deleted" + _info "DNS Record successfully deleted" return 0 else - _err "Error deleting the DNS record" + _err "Error deleting DNS record" _err "${result}" return 1 fi From b5e3883891e31b5082f5f7520c6ac68fad441eeb Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 26 Apr 2025 16:47:20 +0200 Subject: [PATCH 070/689] update --- .github/workflows/pr_dns.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml index 58630e8b..25096c7e 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -20,12 +20,26 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: `**Welcome** + READ ME !!!!! + + + Read me !!!!!! + + First thing: don't send PR to the master branch, please send to the dev branch instead. - Please make sure you've read our [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test). + + + Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test). + + Then reply on this message, otherwise, your code will not be reviewed or merged. + + Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2 - We look forward to reviewing your Pull request shortly ✨ + + 注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试. + ` }) From 2928d843393e839b538307ff8e04a01ca7ae738a Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:04:49 +0800 Subject: [PATCH 071/689] Spaceship: replace debug with debug2 for detailed output in complex debugging --- dnsapi/dns_spaceship.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index cc3f066f..c6db9928 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -183,7 +183,7 @@ _spaceship_api_request() { url="$2" payload="$3" - _debug "Sending $method request to $url with payload $payload" + _debug2 "Sending $method request to $url with payload $payload" if [ "$method" = "GET" ]; then response="$(_get "$url")" else @@ -195,7 +195,7 @@ _spaceship_api_request() { return 1 fi - _debug "API response body: $response" + _debug2 "API response body: $response" if [ "$method" = "GET" ]; then if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then @@ -207,6 +207,6 @@ _spaceship_api_request() { fi fi - _debug "API response header: $HTTP_HEADER" + _debug2 "API response header: $HTTP_HEADER" return 1 } From e2d09231225a2d9cbb33d64d5a49f08a6284c060 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:18:23 +0800 Subject: [PATCH 072/689] Spaceship: replace ~/.acme.sh with $LE_CONFIG_HOME for configurable paths --- dnsapi/dns_spaceship.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index c6db9928..264bdefc 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -114,7 +114,7 @@ _spaceship_init() { if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then _err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET." - _err "Ensure ~/.acme.sh directory has restricted permissions (chmod 700 ~/.acme.sh) to protect credentials." + _err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials." return 1 fi From 8b4d93cc14e3c1cf246840c5cd95409c10fd6836 Mon Sep 17 00:00:00 2001 From: Meo597 <197331664+Meo597@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:32:46 +0800 Subject: [PATCH 073/689] Spaceship: fix doc --- dnsapi/dns_spaceship.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 264bdefc..770e22cc 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -2,7 +2,7 @@ # shellcheck disable=SC2034 dns_spaceship_info='Spaceship.com Site: Spaceship.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_spaceship +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship Options: SPACESHIP_API_KEY Spaceship API Key SPACESHIP_API_SECRET Spaceship API Secret From eb00852a714b3dfa0c556e4fb3806ade46392a11 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 1 May 2025 13:28:20 +0200 Subject: [PATCH 074/689] remove ocsp for letsencrypt server --- acme.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/acme.sh b/acme.sh index 4d75ab62..e9eb6b94 100755 --- a/acme.sh +++ b/acme.sh @@ -5504,6 +5504,13 @@ renew() { if [ -z "$Le_Keylength" ]; then Le_Keylength=2048 fi + if [ "$CA_LETSENCRYPT_V2" = "$Le_API" ]; then + #letsencrypt doesn't support ocsp anymore + if [ "$Le_OCSP_Staple" ]; then + export Le_OCSP_Staple="" + _cleardomainconf Le_OCSP_Staple + fi + fi issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" res="$?" if [ "$res" != "0" ]; then From 42aaf7c2a020c3aeb84031d31ff9006de6aef670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Vyb=C3=ADhal?= Date: Sun, 12 Jan 2025 16:41:00 +0100 Subject: [PATCH 075/689] dns_active24: rewrite for supporting new v2 API --- dnsapi/dns_active24.sh | 172 +++++++++++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 57 deletions(-) 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,21 +105,98 @@ _get_root() { return 1 } -_active24_rest() { - m=$1 - ep="$2" - data="$3" - _debug "$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)}" - 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")" + 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 + #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 + + if ! echo "$response" | tr -d " " | grep \"verified\":true >/dev/null; then + _err "A24 API check failed with: $response" + return 1 + fi + + _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 @@ -126,23 +204,3 @@ _active24_rest() { _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." - 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" -} From ca73e1f024992fdc1ec7a1fe4383a9d477526bdf Mon Sep 17 00:00:00 2001 From: emueller Date: Mon, 12 May 2025 10:28:35 +0200 Subject: [PATCH 076/689] added deploy/kemplm.sh for deploying certs on Kemp Loadmaster --- deploy/kemplm.sh | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100755 deploy/kemplm.sh diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh new file mode 100755 index 00000000..937cbbca --- /dev/null +++ b/deploy/kemplm.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env sh + +#Here is a script to deploy cert to a Kemp Loadmaster. + +#returns 0 means success, otherwise error. + +#DEPLOY_KEMP_TOKEN="token" +#DEPLOY_KEMP_URL="https://kemplm.example.com" + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +kemplm_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 ! _exists jq; then + _err "jq not found" + fi + + # Rename wildcard certs, kemp accepts only alphanumeric names + _kemp_domain=$(echo "${_cdomain}" | sed 's/\*/wildcard/') + _debug _kemp_domain "$_kemp_domain" + + # Clear traces of incorrectly stored values + _clearaccountconf DEPLOY_KEMP_TOKEN + _clearaccountconf DEPLOY_KEMP_URL + + # Read config from saved values or env + _getdeployconf DEPLOY_KEMP_TOKEN + _getdeployconf DEPLOY_KEMP_URL + + _debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL" + _secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN" + + if [ -z "$DEPLOY_KEMP_TOKEN" ]; then + _err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN." + return 1 + fi + if [ -z "$DEPLOY_KEMP_URL" ]; then + _err "Kemp Loadmaster url is not found, please define DEPLOY_KEMP_URL." + return 1 + fi + + # Save current values + _savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN" + _savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL" + + # Do not check for a valid SSL certificate + export HTTPS_INSECURE=1 + + # Check if certificate is already installed + _info "Check if certificate is already present" + _post_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}" + _debug3 _post_request "${_post_request}" + _kemp_cert_count=$(_post "${_post_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}") + _debug2 _kemp_cert_count "${_kemp_cert_count}" + + _kemp_replace_cert=1 + if [ "${_kemp_cert_count}" -eq 0 ]; then + _kemp_replace_cert=0 + _info "Certificate does not exist on Kemp Loadmaster" + else + _info "Certificate already exists on Kemp Loadmaster" + fi + _debug _kemp_replace_cert "${_kemp_replace_cert}" + + # Upload new certificate to Kemp Loadmaster + _kemp_upload_cert=$(_mktemp) + cat "${_cfullchain}" "${_ckey}" | base64 -w 0 > "${_kemp_upload_cert}" + + _info "Uploading certificate to Kemp Loadmaster" + _post_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"$(cat ${_kemp_upload_cert})\"}" + _debug3 _post_request "${_post_request}" + _kemp_post_result=$(_post "${_post_request}" "${DEPLOY_KEMP_URL}/accessv2") + _retval=$? + _debug2 _kemp_post_result "${_kemp_post_result}" + if [ "${_retval}" -eq 0 ]; then + _kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status') + _kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message') + if [ "${_kemp_post_status}" = "ok" ]; then + _info "Upload successful" + else + _err "Upload failed: ${_kemp_post_message}" + fi + else + _err "Upload failed" + _retval=1 + fi + + rm "${_kemp_upload_cert}" + + return $retval +} From 7543d5220cfa01f42041e8d95ecc952b81e92987 Mon Sep 17 00:00:00 2001 From: emueller Date: Mon, 12 May 2025 10:45:01 +0200 Subject: [PATCH 077/689] fixed kemplm.sh formatting --- deploy/kemplm.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh index 937cbbca..3f762d75 100755 --- a/deploy/kemplm.sh +++ b/deploy/kemplm.sh @@ -23,11 +23,11 @@ kemplm_deploy() { _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" - if ! _exists jq; then - _err "jq not found" - fi + if ! _exists jq; then + _err "jq not found" + fi - # Rename wildcard certs, kemp accepts only alphanumeric names + # Rename wildcard certs, kemp accepts only alphanumeric names _kemp_domain=$(echo "${_cdomain}" | sed 's/\*/wildcard/') _debug _kemp_domain "$_kemp_domain" @@ -76,7 +76,7 @@ kemplm_deploy() { # Upload new certificate to Kemp Loadmaster _kemp_upload_cert=$(_mktemp) - cat "${_cfullchain}" "${_ckey}" | base64 -w 0 > "${_kemp_upload_cert}" + cat "${_cfullchain}" "${_ckey}" | base64 -w 0 >"${_kemp_upload_cert}" _info "Uploading certificate to Kemp Loadmaster" _post_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"$(cat ${_kemp_upload_cert})\"}" From bf2e99efa69b76bc5e495a73cbcf97221dce40ff Mon Sep 17 00:00:00 2001 From: emueller Date: Mon, 12 May 2025 10:52:35 +0200 Subject: [PATCH 078/689] fixed quoting in kemplm.sh --- deploy/kemplm.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh index 3f762d75..fbe25cd8 100755 --- a/deploy/kemplm.sh +++ b/deploy/kemplm.sh @@ -79,7 +79,8 @@ kemplm_deploy() { cat "${_cfullchain}" "${_ckey}" | base64 -w 0 >"${_kemp_upload_cert}" _info "Uploading certificate to Kemp Loadmaster" - _post_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"$(cat ${_kemp_upload_cert})\"}" + _post_data=$(cat "${_kemp_upload_cert}") + _post_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_post_data}\"}" _debug3 _post_request "${_post_request}" _kemp_post_result=$(_post "${_post_request}" "${DEPLOY_KEMP_URL}/accessv2") _retval=$? @@ -99,5 +100,5 @@ kemplm_deploy() { rm "${_kemp_upload_cert}" - return $retval + return $_retval } From 184cb0b9a8ae73940514a7759173d678e8a7cbe8 Mon Sep 17 00:00:00 2001 From: Adrian Fedoreanu Date: Thu, 15 May 2025 16:01:24 +0200 Subject: [PATCH 079/689] dns_1984.hosting.sh: fix session cookie name --- dnsapi/dns_1984hosting.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From b82f6801cd24f647510355f1818da2b1368c56a9 Mon Sep 17 00:00:00 2001 From: ymol-spraaklab Date: Fri, 16 May 2025 15:40:36 +0200 Subject: [PATCH 080/689] Set DNS Record TTL to 60 instead of 300 sec --- dnsapi/dns_transip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 99a4cf9e07f20c906fbd4f021b4bb6bd76f5c5dd Mon Sep 17 00:00:00 2001 From: Attackwave <51136146+Attackwave@users.noreply.github.com> Date: Fri, 16 May 2025 22:44:25 +0200 Subject: [PATCH 081/689] Quickfix TrueNAS 25.04 --- deploy/truenas_ws.sh | 125 +++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 47 deletions(-) diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index 940cde2e..bdc1b846 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -52,6 +52,39 @@ _ws_call() { return 0 } +# Upload certificate with webclient api +_ws_upload_cert() { + + /usr/bin/env python - < Date: Sat, 17 May 2025 21:28:26 +0200 Subject: [PATCH 082/689] fix pr --- .github/workflows/pr_dns.yml | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml index 25096c7e..50eb2adb 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -20,26 +20,14 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body: `**Welcome** - READ ME !!!!! - - - Read me !!!!!! - - - First thing: don't send PR to the master branch, please send to the dev branch instead. - - - Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test). - - - Then reply on this message, otherwise, your code will not be reviewed or merged. - - - Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2 - - - 注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试. - + READ ME !!!!! + Read me !!!!!! + First thing: don't send PR to the master branch, please send to the dev branch instead. + Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide). + You MUST pass the [DNS-API-Test](../wiki/DNS-API-Test). + Then reply on this message, otherwise, your code will not be reviewed or merged. + Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2 + 注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试. ` }) From 4f5a70b80404288f8a5b591d541d964b591b5db9 Mon Sep 17 00:00:00 2001 From: Joe Bauser Date: Sat, 17 May 2025 21:25:39 -0400 Subject: [PATCH 083/689] Apply suggested fixes from shfmt diffs --- deploy/zyxel_gs1900.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/zyxel_gs1900.sh b/deploy/zyxel_gs1900.sh index 443a5b05..37cf6005 100644 --- a/deploy/zyxel_gs1900.sh +++ b/deploy/zyxel_gs1900.sh @@ -245,7 +245,7 @@ _zyxel_gs1900_should_update() { _debug2 "_remote_cert_serial" "$_remote_cert_serial" # Get our certificate serial number - _our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial < "${_ccert}") + _our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial <"${_ccert}") _debug2 "_our_cert_serial" "$_our_cert_serial" [ "${_remote_cert_serial}" != "${_our_cert_serial}" ] From e0da5f170304dc373a4a647d83fbe0a13a53ec7e Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Mon, 19 May 2025 09:49:21 +0800 Subject: [PATCH 084/689] Update dns_la.sh --- dnsapi/dns_la.sh | 109 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 26 deletions(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index f19333c4..97437897 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -1,14 +1,18 @@ #!/usr/bin/env sh -# 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 -Issues: github.com/acmesh-official/acme.sh/issues/4257 -' +# LA_Id="123" +# LA_Sk="456" +# LA_Token="" +# +#Site: dns.la +#Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la +#Options: +#我的账户 API 密钥 中获取 APIID APISecret +# LA_Id APIID +# LA_Key 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 +23,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 +51,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 +65,7 @@ dns_la_add() { return 1 fi fi - _err "Add txt record error." + _err "Add txt record failed." return 1 } @@ -65,7 +76,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 +90,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 -n1 | 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 +134,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 +159,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 +182,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 -w 0) + _debug "$LA_Token" + + return 0 +} From 9e7d1b9ce75373c4233790527054925f42da5d16 Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Mon, 19 May 2025 13:16:30 +0800 Subject: [PATCH 085/689] Update dns_la.sh --- dnsapi/dns_la.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index 97437897..ba8ebcac 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -203,7 +203,7 @@ _la_post() { } _la_token() { - LA_Token=$(printf "%s:%s" "$LA_Id" "$LA_Sk" | base64 -w 0) + LA_Token=$(printf "%s:%s" "$LA_Id" "$LA_Sk" | _base64) _debug "$LA_Token" return 0 From 55282851c4a890369bece6f3c5b8082f91f2d1ad Mon Sep 17 00:00:00 2001 From: emueller Date: Mon, 19 May 2025 09:18:29 +0200 Subject: [PATCH 086/689] implemented all suggestions --- deploy/kemplm.sh | 50 +++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh index fbe25cd8..e44e06dc 100755 --- a/deploy/kemplm.sh +++ b/deploy/kemplm.sh @@ -11,30 +11,27 @@ #domain keyfile certfile cafile fullchain kemplm_deploy() { - _cdomain="$1" - _ckey="$2" - _ccert="$3" - _cca="$4" - _cfullchain="$5" + _domain="$1" + _key_file="$2" + _cert_file="$3" + _ca_file="$4" + _fullchain_file="$5" - _debug _cdomain "$_cdomain" - _debug _ckey "$_ckey" - _debug _ccert "$_ccert" - _debug _cca "$_cca" - _debug _cfullchain "$_cfullchain" + _debug _domain "$_domain" + _debug _key_file "$_key_file" + _debug _cert_file "$_cert_file" + _debug _ca_file "$_ca_file" + _debug _fullchain_file "$_fullchain_file" if ! _exists jq; then _err "jq not found" + return 1 fi - # Rename wildcard certs, kemp accepts only alphanumeric names - _kemp_domain=$(echo "${_cdomain}" | sed 's/\*/wildcard/') + # Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename + _kemp_domain=$(echo "${_domain}" | sed 's/\*\.//') _debug _kemp_domain "$_kemp_domain" - # Clear traces of incorrectly stored values - _clearaccountconf DEPLOY_KEMP_TOKEN - _clearaccountconf DEPLOY_KEMP_URL - # Read config from saved values or env _getdeployconf DEPLOY_KEMP_TOKEN _getdeployconf DEPLOY_KEMP_URL @@ -47,7 +44,7 @@ kemplm_deploy() { return 1 fi if [ -z "$DEPLOY_KEMP_URL" ]; then - _err "Kemp Loadmaster url is not found, please define DEPLOY_KEMP_URL." + _err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL." return 1 fi @@ -55,14 +52,11 @@ kemplm_deploy() { _savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN" _savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL" - # Do not check for a valid SSL certificate - export HTTPS_INSECURE=1 - # Check if certificate is already installed _info "Check if certificate is already present" - _post_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}" - _debug3 _post_request "${_post_request}" - _kemp_cert_count=$(_post "${_post_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}") + _list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}" + _debug3 _list_request "${_list_request}" + _kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}") _debug2 _kemp_cert_count "${_kemp_cert_count}" _kemp_replace_cert=1 @@ -76,13 +70,13 @@ kemplm_deploy() { # Upload new certificate to Kemp Loadmaster _kemp_upload_cert=$(_mktemp) - cat "${_cfullchain}" "${_ckey}" | base64 -w 0 >"${_kemp_upload_cert}" + cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}" _info "Uploading certificate to Kemp Loadmaster" - _post_data=$(cat "${_kemp_upload_cert}") - _post_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_post_data}\"}" - _debug3 _post_request "${_post_request}" - _kemp_post_result=$(_post "${_post_request}" "${DEPLOY_KEMP_URL}/accessv2") + _add_data=$(cat "${_kemp_upload_cert}") + _add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}" + _debug3 _add_request "${_add_request}" + _kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2") _retval=$? _debug2 _kemp_post_result "${_kemp_post_result}" if [ "${_retval}" -eq 0 ]; then From f132010acb927065cef8294b4a772afe6301490f Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Mon, 19 May 2025 15:08:54 +0300 Subject: [PATCH 087/689] dns_edgecenter.sh: fix structural info --- dnsapi/dns_edgecenter.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dnsapi/dns_edgecenter.sh b/dnsapi/dns_edgecenter.sh index cdd150df..8f4ad171 100644 --- a/dnsapi/dns_edgecenter.sh +++ b/dnsapi/dns_edgecenter.sh @@ -1,13 +1,13 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 - -# EdgeCenter DNS API integration for acme.sh -# Author: Konstantin Ruchev -dns_edgecenter_info='edgecenter DNS API -Site: https://edgecenter.ru -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_edgecenter +dns_edgecenter_info='EdgeCenter.ru +Site: EdgeCenter.ru +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter Options: - EDGECENTER_API_KEY auth APIKey' + 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= From 133ae8555a7114bf314d5a81be27f5e7a808e807 Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Mon, 19 May 2025 15:15:46 +0300 Subject: [PATCH 088/689] dns_freemyip.sh: fix strutural info --- dnsapi/dns_freemyip.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_freemyip.sh b/dnsapi/dns_freemyip.sh index 0bad3809..d598a657 100644 --- a/dnsapi/dns_freemyip.sh +++ b/dnsapi/dns_freemyip.sh @@ -1,11 +1,11 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 dns_freemyip_info='FreeMyIP.com -Site: 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/{XXXX} +Issues: github.com/acmesh-official/acme.sh/issues/6247 Author: Recolic Keghart , @Giova96 ' From 500cfbc19c08feab8763fd141181a5820290747e Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Mon, 19 May 2025 21:29:33 +0800 Subject: [PATCH 089/689] Update dns_la.sh --- dnsapi/dns_la.sh | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index ba8ebcac..7c3765cd 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -2,17 +2,16 @@ # LA_Id="123" # LA_Sk="456" -# LA_Token="" -# -#Site: dns.la -#Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la -#Options: -#我的账户 API 密钥 中获取 APIID APISecret -# LA_Id APIID -# LA_Key APISecret -# LA_Token 用冒号连接 APIID APISecret 再base64生成 -#Issues: github.com/acmesh-official/acme.sh/issues/4257 -#' +# shellcheck disable=SC2034 +LA_Token='dns.la +Site: dns.la +Docs: https://www.dns.la/docs/ApiDoc +Options: + LA_Id APIID + LA_Key APISecret + LA_Token 用冒号连接 APIID APISecret 再base64生成 +Issues: github.com/acmesh-official/acme.sh/issues/4257 +' LA_Api="https://api.dns.la/api" ######## Public functions ##################### From cddf098f47cde203dee97335176ef04bc7f3cdcc Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Tue, 20 May 2025 20:28:59 +0800 Subject: [PATCH 090/689] Update dns_la.sh --- dnsapi/dns_la.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index 7c3765cd..651b74c0 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -5,10 +5,10 @@ # shellcheck disable=SC2034 LA_Token='dns.la Site: dns.la -Docs: https://www.dns.la/docs/ApiDoc +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la Options: LA_Id APIID - LA_Key APISecret + LA_Sk APISecret LA_Token 用冒号连接 APIID APISecret 再base64生成 Issues: github.com/acmesh-official/acme.sh/issues/4257 ' From c8f1e4119719a911087dc266090ca04eb7dd20c1 Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Tue, 20 May 2025 20:29:44 +0800 Subject: [PATCH 091/689] Update dns_la.sh --- dnsapi/dns_la.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index 651b74c0..c2934b54 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -5,7 +5,7 @@ # shellcheck disable=SC2034 LA_Token='dns.la Site: dns.la -Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la Options: LA_Id APIID LA_Sk APISecret From 8241b078ced81178796ab76fad5b80baed44040f Mon Sep 17 00:00:00 2001 From: YANGJINZE <91786638+KincaidYang@users.noreply.github.com> Date: Fri, 23 May 2025 17:54:56 +0800 Subject: [PATCH 092/689] docs (dns_tencent) : update documentation links --- dnsapi/dns_tencent.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5a085f25142ce84f51b7f7d18674352fefa5e56e Mon Sep 17 00:00:00 2001 From: asavin Date: Sun, 25 May 2025 18:36:57 +0200 Subject: [PATCH 093/689] Addressing #discussion_r2105799190 --- acme.sh | 2 +- dnsapi/dns_efficientip.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index e9eb6b94..93e18a5c 100755 --- a/acme.sh +++ b/acme.sh @@ -1017,7 +1017,7 @@ _digest() { outputhex="$2" - if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then + if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then if [ "$outputhex" ]; then ${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' else diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index afcd9af4..7dcac294 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -74,7 +74,7 @@ dns_efficientip_add() { export _H2="Authorization: Basic ${EfficientIP_CredsEncoded}" else TS=$(date +%s) - Sig=$(printf "%b\n$TS\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | openssl dgst -sha3-256 | cut -d '=' -f 2 | tr -d ' ') + 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}" From 28687ad7c7ac849d3201f6c7d8add0065718b60e Mon Sep 17 00:00:00 2001 From: Marcel Schlegel Date: Sat, 31 May 2025 15:02:25 +0200 Subject: [PATCH 094/689] Issue 3968: Fix missing api password encoding. --- dnsapi/dns_cloudns.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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")" From 19678db9333f901219befcbd388fc454aa6b7119 Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Fri, 6 Jun 2025 02:06:27 +0800 Subject: [PATCH 095/689] Update dns_la.sh --- dnsapi/dns_la.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index c2934b54..772f8845 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -3,7 +3,7 @@ # LA_Id="123" # LA_Sk="456" # shellcheck disable=SC2034 -LA_Token='dns.la +dns_la_info='dns.la Site: dns.la Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la Options: From bff1064dbd205db86832c876151304b97bf9d78f Mon Sep 17 00:00:00 2001 From: Lambiek12 Date: Sun, 8 Jun 2025 15:39:10 +0200 Subject: [PATCH 096/689] Add new dnsapi support for OpenProvider.eu using new REST API --- dnsapi/dns_openprovider_rest.sh | 193 ++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 dnsapi/dns_openprovider_rest.sh diff --git a/dnsapi/dns_openprovider_rest.sh b/dnsapi/dns_openprovider_rest.sh new file mode 100644 index 00000000..a6725fcc --- /dev/null +++ b/dnsapi/dns_openprovider_rest.sh @@ -0,0 +1,193 @@ +#!/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" + + _debug httpmethod "$httpmethod" + _debug requestfullurl "$OPENPROVIDER_API_URL/$queryparameters" + _debug queryparameters "$queryparameters" + + if [ "$httpmethod" != "GET" ]; then + _debug requestbody "$requestbody" + + 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" *: *"##')" + _debug openproviderauthtoken "$openproviderauthtoken" + + 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 +} From 06d3739a8dc9b235aadc0460d7ddcbb2e867a04e Mon Sep 17 00:00:00 2001 From: Lambiek12 Date: Sun, 8 Jun 2025 17:29:39 +0200 Subject: [PATCH 097/689] Cleanup duplicate debug log output based on DNS test run --- dnsapi/dns_openprovider_rest.sh | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/dnsapi/dns_openprovider_rest.sh b/dnsapi/dns_openprovider_rest.sh index a6725fcc..3b8d20d2 100644 --- a/dnsapi/dns_openprovider_rest.sh +++ b/dnsapi/dns_openprovider_rest.sh @@ -113,14 +113,8 @@ _openprovider_rest() { export _H1="Content-Type: application/json" export _H2="Accept: application/json" export _H3="Authorization: Bearer $openproviderauthtoken" - - _debug httpmethod "$httpmethod" - _debug requestfullurl "$OPENPROVIDER_API_URL/$queryparameters" - _debug queryparameters "$queryparameters" - + if [ "$httpmethod" != "GET" ]; then - _debug requestbody "$requestbody" - response="$(_post "$requestbody" "$OPENPROVIDER_API_URL/$queryparameters" "" "$httpmethod")" else response="$(_get "$OPENPROVIDER_API_URL/$queryparameters")" @@ -145,7 +139,6 @@ _openprovider_rest_login() { loginresponse="$(_post "$loginrequestbody" "$loginrequesturl" "" "POST")" openproviderauthtoken="$(printf "%s\n" "$loginresponse" | _egrep_o '"token" *: *"[^"]*' | _head_n 1 | sed 's#^"token" *: *"##')" - _debug openproviderauthtoken "$openproviderauthtoken" export openproviderauthtoken } From fcd358eb71e0c8ca49b11e2fbca133c9c5937844 Mon Sep 17 00:00:00 2001 From: Lambiek12 Date: Sun, 8 Jun 2025 17:35:09 +0200 Subject: [PATCH 098/689] Resolve spellcheck error --- dnsapi/dns_openprovider_rest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_openprovider_rest.sh b/dnsapi/dns_openprovider_rest.sh index 3b8d20d2..210dc6fc 100644 --- a/dnsapi/dns_openprovider_rest.sh +++ b/dnsapi/dns_openprovider_rest.sh @@ -113,7 +113,7 @@ _openprovider_rest() { 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 From f2b248243c1de3f1ddb740cab828f1eb580d744a Mon Sep 17 00:00:00 2001 From: Erwin Oegema Date: Tue, 10 Jun 2025 10:36:06 +0200 Subject: [PATCH 099/689] Configure 10 second timeout to ACME_DIRECTORY API call --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index e9eb6b94..b8630742 100755 --- a/acme.sh +++ b/acme.sh @@ -2761,7 +2761,7 @@ _initAPI() { _request_retry_times=0 while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do _request_retry_times=$(_math "$_request_retry_times" + 1) - response=$(_get "$_api_server") + response=$(_get "$_api_server" "" 10) if [ "$?" != "0" ]; then _debug2 "response" "$response" _info "Cannot init API for: $_api_server." From 242085d6765e43ad08a6b1863ec586c789a3f483 Mon Sep 17 00:00:00 2001 From: laDanz Date: Tue, 17 Jun 2025 14:05:40 +0200 Subject: [PATCH 100/689] add support for AIX style netstat --- acme.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acme.sh b/acme.sh index e9eb6b94..e8378754 100755 --- a/acme.sh +++ b/acme.sh @@ -1401,6 +1401,12 @@ _ss() { return 0 fi + if [ "$(uname)" = "AIX" ]; then + _debug "Using: AIX netstat" + netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port " + return 0 + fi + if _exists "netstat"; then _debug "Using: netstat" if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then From ca08ce42626d98cf7d9112f1b41c771218b2d23c Mon Sep 17 00:00:00 2001 From: asavin Date: Mon, 23 Jun 2025 08:59:33 +0200 Subject: [PATCH 101/689] Fixing forgottent openssl ref --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 7dcac294..4a09c5bb 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -121,7 +121,7 @@ dns_efficientip_rm() { export _H2="Authorization: Basic $EfficientIP_CredsEncoded" else TS=$(date +%s) - Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | openssl dgst -sha3-256 | cut -d '=' -f 2 | tr -d ' ') + Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex) EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig" | _base64) export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}" export _H3="X-SDS-TS: $TS" From 4a16aaacb6a47d60dee9da2cd4f20fb101f5a47b Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 25 Jun 2025 21:27:08 +0200 Subject: [PATCH 102/689] add --- .github/workflows/wiki-monitor.yml | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/wiki-monitor.yml diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml new file mode 100644 index 00000000..b86347cf --- /dev/null +++ b/.github/workflows/wiki-monitor.yml @@ -0,0 +1,32 @@ +name: Notify via Issue on Wiki Edit + +on: + gollum: + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Generate wiki change message + run: | + echo "Wiki page:" > wiki-change-msg.txt + echo "User: ${{ github.actor }}" >> wiki-change-msg.txt + echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" >> wiki-change-msg.txt + echo "" >> wiki-change-msg.txt + for page in $(jq -r '.gollum.pages[].html_url' "$GITHUB_EVENT_PATH"); do + echo "Path: $page" >> wiki-change-msg.txt + done + + - name: Create issue to notify Neilpang + uses: peter-evans/create-issue-from-file@v5 + with: + title: "Wiki page" + content-filepath: ./wiki-change-msg.txt + assignees: Neilpang + env: + TZ: Asia/Shanghai + + + + + From b025e7f0f280b33bbca418fa53a6634f4adcdb4f Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 25 Jun 2025 21:46:58 +0200 Subject: [PATCH 103/689] fix for wiki --- .github/workflows/wiki-monitor.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index b86347cf..1955043e 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -9,18 +9,17 @@ jobs: steps: - name: Generate wiki change message run: | - echo "Wiki page:" > wiki-change-msg.txt + sudo apt-get update && sudo apt-get install -y jq + echo "Wiki page edited" > wiki-change-msg.txt echo "User: ${{ github.actor }}" >> wiki-change-msg.txt echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" >> wiki-change-msg.txt echo "" >> wiki-change-msg.txt - for page in $(jq -r '.gollum.pages[].html_url' "$GITHUB_EVENT_PATH"); do - echo "Path: $page" >> wiki-change-msg.txt - done + jq -r '.gollum.pages[] | "Page: \(.html_url) (action: \(.action))"' "$GITHUB_EVENT_PATH" >> wiki-change-msg.txt - name: Create issue to notify Neilpang uses: peter-evans/create-issue-from-file@v5 with: - title: "Wiki page" + title: "Wiki edited" content-filepath: ./wiki-change-msg.txt assignees: Neilpang env: From 89071f722642248e54df7f627ed40943c5b622c7 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 25 Jun 2025 21:51:55 +0200 Subject: [PATCH 104/689] minor --- .github/workflows/wiki-monitor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 1955043e..c9432841 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -14,7 +14,8 @@ jobs: echo "User: ${{ github.actor }}" >> wiki-change-msg.txt echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" >> wiki-change-msg.txt echo "" >> wiki-change-msg.txt - jq -r '.gollum.pages[] | "Page: \(.html_url) (action: \(.action))"' "$GITHUB_EVENT_PATH" >> wiki-change-msg.txt + cat "$GITHUB_EVENT_PATH" + jq -r '.gollum.pages // [] | .[] | "Page: \(.html_url) (action: \(.action))"' "$GITHUB_EVENT_PATH" >> wiki-change-msg.txt - name: Create issue to notify Neilpang uses: peter-evans/create-issue-from-file@v5 From 6966b3810ddf13fc0874116dfb8060aa58177e97 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 25 Jun 2025 22:01:11 +0200 Subject: [PATCH 105/689] minor --- .github/workflows/wiki-monitor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index c9432841..c29d92c7 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -15,7 +15,7 @@ jobs: echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" >> wiki-change-msg.txt echo "" >> wiki-change-msg.txt cat "$GITHUB_EVENT_PATH" - jq -r '.gollum.pages // [] | .[] | "Page: \(.html_url) (action: \(.action))"' "$GITHUB_EVENT_PATH" >> wiki-change-msg.txt + jq -r '.pages // [] | .[] | "Page: \(.html_url) (action: \(.action))"' "$GITHUB_EVENT_PATH" >> wiki-change-msg.txt - name: Create issue to notify Neilpang uses: peter-evans/create-issue-from-file@v5 From 424d33faa082e3f41789ee5ec2ee7e3e280fcdc4 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 25 Jun 2025 22:11:35 +0200 Subject: [PATCH 106/689] wiki --- .github/workflows/wiki-monitor.yml | 42 +++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index c29d92c7..1e216f2e 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -7,15 +7,43 @@ jobs: notify: runs-on: ubuntu-latest steps: + - name: Checkout wiki repository + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }}.wiki + path: wiki + - name: Generate wiki change message run: | - sudo apt-get update && sudo apt-get install -y jq - echo "Wiki page edited" > wiki-change-msg.txt - echo "User: ${{ github.actor }}" >> wiki-change-msg.txt - echo "Time: $(date '+%Y-%m-%d %H:%M:%S')" >> wiki-change-msg.txt - echo "" >> wiki-change-msg.txt - cat "$GITHUB_EVENT_PATH" - jq -r '.pages // [] | .[] | "Page: \(.html_url) (action: \(.action))"' "$GITHUB_EVENT_PATH" >> wiki-change-msg.txt + actor="${{ github.actor }}" + sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH") + page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH") + page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") + page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") + page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") + now="$(date '+%Y-%m-%d %H:%M:%S')" + + cd wiki + prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1) + if [ -n "$prev_sha" ]; then + git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff + else + echo "(no diff)" > ../wiki.diff + fi + cd .. + { + echo "Wiki edited" + echo -n "User: " + echo "[$actor]($sender_url)" + echo "Time: $now" + echo "Page: [$page_name]($page_url) (Action: $page_action)" + echo "" + echo "----" + echo "### diff:" + echo '```diff' + cat wiki.diff + echo '```' + } > wiki-change-msg.txt - name: Create issue to notify Neilpang uses: peter-evans/create-issue-from-file@v5 From 74fdf649d3883f1fee6fdb0128f8f146ef9c9e83 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 25 Jun 2025 22:14:30 +0200 Subject: [PATCH 107/689] wiki --- .github/workflows/wiki-monitor.yml | 56 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 1e216f2e..89497580 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -15,35 +15,35 @@ jobs: - name: Generate wiki change message run: | - actor="${{ github.actor }}" - sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH") - page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH") - page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") - page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") - page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") - now="$(date '+%Y-%m-%d %H:%M:%S')" + actor="${{ github.actor }}" + sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH") + page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH") + page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") + page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") + page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") + now="$(date '+%Y-%m-%d %H:%M:%S')" - cd wiki - prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1) - if [ -n "$prev_sha" ]; then - git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff - else - echo "(no diff)" > ../wiki.diff - fi - cd .. - { - echo "Wiki edited" - echo -n "User: " - echo "[$actor]($sender_url)" - echo "Time: $now" - echo "Page: [$page_name]($page_url) (Action: $page_action)" - echo "" - echo "----" - echo "### diff:" - echo '```diff' - cat wiki.diff - echo '```' - } > wiki-change-msg.txt + cd wiki + prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1) + if [ -n "$prev_sha" ]; then + git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff + else + echo "(no diff)" > ../wiki.diff + fi + cd .. + { + echo "Wiki edited" + echo -n "User: " + echo "[$actor]($sender_url)" + echo "Time: $now" + echo "Page: [$page_name]($page_url) (Action: $page_action)" + echo "" + echo "----" + echo "### diff:" + echo '```diff' + cat wiki.diff + echo '```' + } > wiki-change-msg.txt - name: Create issue to notify Neilpang uses: peter-evans/create-issue-from-file@v5 From 2bea808251d3e0c65fab47dafba0fec636128a6a Mon Sep 17 00:00:00 2001 From: OPPO9008 <41640509+OPPO9008@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:15:46 +0800 Subject: [PATCH 108/689] Update dns_la.sh --- dnsapi/dns_la.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_la.sh b/dnsapi/dns_la.sh index 772f8845..9cb6327e 100644 --- a/dnsapi/dns_la.sh +++ b/dnsapi/dns_la.sh @@ -100,7 +100,7 @@ dns_la_rm() { return 0 fi - record_id=$(printf "%s" "$response" | grep '"id":' | head -n1 | sed 's/.*"id": *"\([^"]*\)".*/\1/') + 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." From 76b68f7ccb3063a0d065ab1009d70156d3f5d135 Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 6 Jul 2025 01:34:21 +0300 Subject: [PATCH 109/689] dnsapi: dns_mydnsjp.sh fix author The @epgdatacapbon was renamed to @tkmsst Signed-off-by: Sergey Ponomarev --- dnsapi/dns_mydnsjp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ##################### From 01ed3c332648104652c6b7af3ff4518ebe3a32f2 Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 6 Jul 2025 01:36:40 +0300 Subject: [PATCH 110/689] dnsapi: dns_ddnss.sh remove RaidenII from authors He made the DuckDNS script that was used for this script but he can't support the script. Signed-off-by: Sergey Ponomarev --- dnsapi/dns_ddnss.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_ddnss.sh b/dnsapi/dns_ddnss.sh index 118b148b..a624a268 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" From c6819cbd6b40d95e1b0f03273ee6a598cd9794a6 Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 6 Jul 2025 01:40:53 +0300 Subject: [PATCH 111/689] dnsapi: fix authors: use @ for GitHub profiles Signed-off-by: Sergey Ponomarev --- dnsapi/dns_bookmyname.sh | 2 +- dnsapi/dns_ddnss.sh | 2 +- dnsapi/dns_dnshome.sh | 2 +- dnsapi/dns_duckdns.sh | 2 +- dnsapi/dns_dyn.sh | 2 +- dnsapi/dns_dynv6.sh | 2 +- dnsapi/dns_easydns.sh | 2 +- dnsapi/dns_freedns.sh | 2 +- dnsapi/dns_joker.sh | 2 +- dnsapi/dns_mijnhost.sh | 2 +- dnsapi/dns_namecom.sh | 2 +- dnsapi/dns_namesilo.sh | 2 +- dnsapi/dns_pleskxml.sh | 2 +- dnsapi/dns_schlundtech.sh | 2 +- dnsapi/dns_spaceship.sh | 2 +- dnsapi/dns_tele3.sh | 2 +- dnsapi/dns_timeweb.sh | 2 +- dnsapi/dns_udr.sh | 2 +- dnsapi/dns_vscale.sh | 2 +- dnsapi/dns_websupport.sh | 2 +- dnsapi/dns_world4you.sh | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) 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_ddnss.sh b/dnsapi/dns_ddnss.sh index a624a268..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: 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..0c9491f8 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" 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_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_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_mijnhost.sh b/dnsapi/dns_mijnhost.sh index 9dafc702..52a81632 100644 --- a/dnsapi/dns_mijnhost.sh +++ b/dnsapi/dns_mijnhost.sh @@ -5,7 +5,7 @@ Domains: mijn.host Site: mijn.host Docs: https://mijn.host/api/doc/ Issues: https://github.com/acmesh-official/acme.sh/issues/6177 -Author: peterv99 +Author: @peterv99 Options: MIJNHOST_API_KEY API Key ' 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_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_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_spaceship.sh b/dnsapi/dns_spaceship.sh index 770e22cc..5e92a4fe 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -8,7 +8,7 @@ Options: SPACESHIP_API_SECRET Spaceship API Secret SPACESHIP_ROOT_DOMAIN (Optional) Manually specify the root domain if auto-detection fails Issues: github.com/acmesh-official/acme.sh/issues/6304 -Author: Meow +Author: Meow <@Meo597> ' # Spaceship API 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_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_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_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_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_world4you.sh b/dnsapi/dns_world4you.sh index 46cdc4fe..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" From daf183e2cc8b1e11ecb754fd8eddd56c1b954c12 Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 6 Jul 2025 01:41:58 +0300 Subject: [PATCH 112/689] dnsapi: dns_vultr.sh remove empty author Signed-off-by: Sergey Ponomarev --- dnsapi/dns_vultr.sh | 1 - 1 file changed, 1 deletion(-) 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" From 85ec6343ff2f4388bf4e5cae77d5481f5dfcb46d Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 6 Jul 2025 01:42:39 +0300 Subject: [PATCH 113/689] dnsapi: dns_mijnhost.sh rearrange fields, use user docs instead of API docs Signed-off-by: Sergey Ponomarev --- dnsapi/dns_mijnhost.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_mijnhost.sh b/dnsapi/dns_mijnhost.sh index 52a81632..9f5e7710 100644 --- a/dnsapi/dns_mijnhost.sh +++ b/dnsapi/dns_mijnhost.sh @@ -1,16 +1,15 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 dns_mijnhost_info='mijn.host -Domains: mijn.host Site: mijn.host -Docs: https://mijn.host/api/doc/ -Issues: https://github.com/acmesh-official/acme.sh/issues/6177 -Author: @peterv99 +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 ###################### Constants for your mijn-host API +######## Public functions ###################### MIJNHOST_API="https://mijn.host/api/v2" # Add TXT record for domain verification From 8113711b7aff45ac243492c1b3f49282727ac1b1 Mon Sep 17 00:00:00 2001 From: Sergey Ponomarev Date: Sun, 6 Jul 2025 01:43:16 +0300 Subject: [PATCH 114/689] dnsapi: fix Structured DNS Info Signed-off-by: Sergey Ponomarev --- dnsapi/dns_beget.sh | 2 +- dnsapi/dns_he_ddns.sh | 1 + dnsapi/dns_selectel.sh | 38 ++++++++++++++++---------------------- dnsapi/dns_spaceship.sh | 6 +++--- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/dnsapi/dns_beget.sh b/dnsapi/dns_beget.sh index aa43caed..5f3b1eb1 100755 --- a/dnsapi/dns_beget.sh +++ b/dnsapi/dns_beget.sh @@ -7,7 +7,7 @@ Options: BEGET_User API user BEGET_Password API password Issues: github.com/acmesh-official/acme.sh/issues/6200 -Author: ARNik arnik@arnik.ru +Author: ARNik ' Beget_Api="https://api.beget.com/api" diff --git a/dnsapi/dns_he_ddns.sh b/dnsapi/dns_he_ddns.sh index cd7d1ec2..1fe9a7fd 100644 --- a/dnsapi/dns_he_ddns.sh +++ b/dnsapi/dns_he_ddns.sh @@ -5,6 +5,7 @@ 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ö ' diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh index 434bc483..565f541b 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -1,27 +1,21 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 - -# dns_selectel_info='Selectel.com -# Domains: Selectel.ru -# Site: Selectel.com -# Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel -# Options: -# Variables that must be defined before running -# SL_Ver can take one of the values 'v1' or 'v2', default is 'v1' -# SL_Ver='v1', when using version API legacy (v1) -# SL_Ver='v2', when using version API actual (v2) -# when using API version v1, i.e. SL_Ver is 'v1' or not defined: -# SL_Key - API Key, required -# when using API version v2: -# SL_Ver - required as 'v2' -# SL_Login_ID - account ID, required -# SL_Project_Name - name project, required -# SL_Login_Name - service user name, required -# SL_Pswd - service user password, required -# SL_Expire - token lifetime in minutes (0-1440), default 1400 minutes -# -# Issues: github.com/acmesh-official/acme.sh/issues/5126 -# +dns_selectel_info='Selectel.com +Domains: Selectel.ru +Site: Selectel.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel +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" auth_uri="https://cloud.api.selcloud.ru/identity/v3/auth/tokens" diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 5e92a4fe..8fff4037 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -4,9 +4,9 @@ dns_spaceship_info='Spaceship.com Site: Spaceship.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship Options: - SPACESHIP_API_KEY Spaceship API Key - SPACESHIP_API_SECRET Spaceship API Secret - SPACESHIP_ROOT_DOMAIN (Optional) Manually specify the root domain if auto-detection fails + 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> ' From 014a7814260025cac0aa7d3c0e95ac2cfb4d5230 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Sun, 6 Jul 2025 20:12:10 -0400 Subject: [PATCH 115/689] Create localcopy deploy-hook Deploy-hook to very simply copy files to set directories and then execute whatever reloadcmd the admin needs afterwards. This can be useful for configurations where the "multideploy" hook (in development) is used or when an admin wants ACME.SH to renew certs but needs to manually configure deployment via an external script (e.g. The deploy-freenas script for TrueNAS Core/Scale https://github.com/danb35/deploy-freenas/ Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 deploy/localcopy.sh diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh new file mode 100644 index 00000000..3b4fc219 --- /dev/null +++ b/deploy/localcopy.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env sh + +# Deploy-hook to very simply copy files to set directories and then +# execute whatever reloadcmd the admin needs afterwards. This can be +# useful for configurations where the "multideploy" hook (in development) +# is used or when an admin wants ACME.SH to renew certs but needs to +# manually configure deployment via an external script +# (e.g. The deploy-freenas script for TrueNAS Core/Scale +# https://github.com/danb35/deploy-freenas/ ) +# +# +# Environment variables to be utilized are as follows: +# +# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer +# DEPLOY_LOCALCOPY_CERTKEY - /path/to/target/cert.key +# DEPLOY_LOCALCOPY_FULLCHAIN - /path/to/target/fullchain.cer +# DEPLOY_LOCALCOPY_CA - /path/to/target/ca.cer +# DEPLOY_LOCALCOPY_RELOADCMD - "echo 'this is my cmd'" + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +localcopy_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" + + _getdeployconf DEPLOY_LOCALCOPY_CERTIFICATE + _getdeployconf DEPLOY_LOCALCOPY_CERTKEY + _getdeployconf DEPLOY_LOCALCOPY_FULLCHAIN + _getdeployconf DEPLOY_LOCALCOPY_CA + _getdeployconf DEPLOY_LOCALCOPY_RELOADCMD + + if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then + _info "Copying certificate" + _debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE" + if ! eval "cp $_ccert $DEPLOY_LOCALCOPY_CERTIFICATE"; then + _err "Failed to copy certificate, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" + fi + + if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then + _info "Copying certificate key" + _debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY" + if ! eval "cp $_ckey $DEPLOY_LOCALCOPY_CERTKEY"; then + _err "Failed to copy certificate key, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY" + fi + + if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then + _info "Copying fullchain" + _debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN" + if ! eval "cp $_cfullchain $DEPLOY_LOCALCOPY_FULLCHAIN"; then + _err "Failed to copy fullchain, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN" + fi + + if [ "$DEPLOY_LOCALCOPY_CA" ]; then + _info "Copying CA" + _debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA" + if ! eval "cp $_cca $DEPLOY_LOCALCOPY_CA"; then + _err "Failed to copy CA, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_CA "$DEPLOY_LOCALCOPY_CA" + fi + + _reload=$DEPLOY_LOCALCOPY_RELOADCMD + _debug "Running reloadcmd $_reload" + + if [ -z "$_reload" ]; then + _info "Reloadcmd not provided, skipping." + else + _info "Reloading" + if eval "$_reload"; then + _info "Reload successful." + _savedeployconf DEPLOY_LOCALCOPY_RELOADCMD "$DEPLOY_LOCALCOPY_RELOADCMD" "base64" + else + _err "Reload failed." + return 1 + fi + fi + + _info "$(__green "'localcopy' deploy success")" + return 0 +} From c2f8b4d1f2821e927b3b8b614c6e0eaf73fc7170 Mon Sep 17 00:00:00 2001 From: pileus-lines Date: Mon, 23 Jun 2025 22:50:32 +0200 Subject: [PATCH 116/689] Update dns_infomaniak.sh because infomaniak API v1 no longer works --- dnsapi/dns_infomaniak.sh | 100 ++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index ea5ef461..34795888 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infomaniak Options: INFOMANIAK_API_TOKEN API Token Issues: github.com/acmesh-official/acme.sh/issues/3188 + ' # To use this API you need visit the API dashboard of your account @@ -65,33 +66,32 @@ dns_infomaniak_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - fqdn=${fulldomain#_acme-challenge.} - # guess which base domain to add record to - zone_and_id=$(_find_zone "$fqdn") - if [ -z "$zone_and_id" ]; then - _err "cannot find zone to modify" + zone=$(_get_zone "$fulldomain") + if [ -z "$zone" ]; then + _err "cannot find zone:<${zone}> to modify" return 1 fi - zone=${zone_and_id% *} - domain_id=${zone_and_id#* } # extract first part of domain key=${fulldomain%."$zone"} - _debug "zone:$zone id:$domain_id key:$key" + _debug "key:$key" + _debug "txtvalue: $txtvalue" # payload data="{\"type\": \"TXT\", \"source\": \"$key\", \"target\": \"$txtvalue\", \"ttl\": $INFOMANIAK_TTL}" # API call - response=$(_post "$data" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record") - if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then - _info "Record added" - _debug "Response: $response" - return 0 + response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records") + if [ -n "$response" ]; then + if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then + _info "Record added" + _debug "response: $response" + return 0 + fi fi - _err "could not create record" + _err "Could not create record." _debug "Response: $response" return 1 } @@ -106,7 +106,7 @@ dns_infomaniak_rm() { if [ -z "$INFOMANIAK_API_TOKEN" ]; then INFOMANIAK_API_TOKEN="" - _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN" + _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN." return 1 fi @@ -138,63 +138,53 @@ dns_infomaniak_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - fqdn=${fulldomain#_acme-challenge.} - # guess which base domain to add record to - zone_and_id=$(_find_zone "$fqdn") - if [ -z "$zone_and_id" ]; then - _err "cannot find zone to modify" + zone=$(_get_zone "$fulldomain") + if [ -z "$zone" ]; then + _err "cannot find zone:<$zone> to modify" return 1 fi - zone=${zone_and_id% *} - domain_id=${zone_and_id#* } # extract first part of domain key=${fulldomain%."$zone"} + key=$(echo "$key" | _lower_case) - _debug "zone:$zone id:$domain_id key:$key" + _debug "zone:$zone" + _debug "key:$key" # find previous record - # shellcheck disable=SC1004 - record_id=$(_get "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}\ -{/g' | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source_idn":"'"$fulldomain"'".*"target_idn":"'"$txtvalue"'".*/\1/p') - if [ -z "$record_id" ]; then - _err "could not find record to delete" - return 1 - fi + # shellcheck disable=SC2086 + response=$(_get "${INFOMANIAK_API_URL}/2/zones/${zone}/records" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}{/g') + record_id=$(echo "$response" | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source":"'"$key"'".*"target":"\\"'"$txtvalue"'\\"".*/\1/p') + _debug "key: $key" + _debug "txtvalue: $txtvalue" _debug "record_id: $record_id" - # API call - response=$(_post "" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record/$record_id" "" DELETE) - if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then - _info "Record deleted" - return 0 + if [ -z "$record_id" ]; then + _err "could not find record to delete" + _debug "response: $response" + return 1 fi - _err "could not delete record" + + # API call + response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE) + if [ -n "$response" ]; then + if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then + _info "Record deleted" + return 0 + fi + fi + _err "Could not delete record." + _debug "Response: $response" return 1 } #################### Private functions below ################################## -_get_domain_id() { +_get_zone() { domain="$1" - + # Whatever the domain is, you can get the fqdn with the following. # shellcheck disable=SC1004 - _get "${INFOMANIAK_API_URL}/1/product?service_name=domain&customer_name=$domain" | sed 's/.*"data":\[{\(.*\)}\]}/\1/; s/,/\ -/g' | sed -n 's/^"id":\(.*\)/\1/p' -} - -_find_zone() { - zone="$1" - - # find domain in list, removing . parts sequentialy - while _contains "$zone" '\.'; do - _debug "testing $zone" - id=$(_get_domain_id "$zone") - if [ -n "$id" ]; then - echo "$zone $id" - return - fi - zone=${zone#*.} - done + response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/') + echo "${response%%\"*}" } From 3b0f624302294c3d817e5c3218a263905dff9f67 Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Thu, 10 Jul 2025 10:55:05 +0200 Subject: [PATCH 117/689] Support certificate profile selection --- acme.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index d70e323b..46c1124c 100755 --- a/acme.sh +++ b/acme.sh @@ -4416,6 +4416,7 @@ issue() { _preferred_chain="${15}" _valid_from="${16}" _valid_to="${17}" + _certificate_profile="${18}" if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" @@ -4491,6 +4492,11 @@ issue() { else _cleardomainconf "Le_Preferred_Chain" fi + if [ "$_certificate_profile" ]; then + _savedomainconf "Le_Certificate_Profile" "$_certificate_profile" + else + _cleardomainconf "Le_Certificate_Profile" + fi Le_API="$ACME_DIRECTORY" _savedomainconf "Le_API" "$Le_API" @@ -4622,6 +4628,9 @@ issue() { if [ "$_notAfter" ]; then _newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\"" fi + if [ "$_certificate_profile" ]; then + _newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\"" + fi _debug "STEP 1, Ordering a Certificate" if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then _err "Error creating new order." @@ -5503,6 +5512,7 @@ renew() { Le_PostHook="$(_readdomainconf Le_PostHook)" Le_RenewHook="$(_readdomainconf Le_RenewHook)" Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)" + Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)" # When renewing from an old version, the empty Le_Keylength means 2048. # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over # time but an empty value implies 2048 specifically. @@ -5517,7 +5527,7 @@ renew() { _cleardomainconf Le_OCSP_Staple fi fi - issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" res="$?" if [ "$res" != "0" ]; then return "$res" @@ -6989,6 +6999,8 @@ Parameters: If no match, the default offered chain will be used. (default: empty) See: $_PREFERRED_CHAIN_WIKI + --certificate-profile If the CA offers profiles, select the desired profile + --valid-to Request the NotAfter field of the cert. See: $_VALIDITY_WIKI --valid-from Request the NotBefore field of the cert. @@ -7364,6 +7376,7 @@ _process() { _preferred_chain="" _valid_from="" _valid_to="" + _certificate_profile="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7682,6 +7695,10 @@ _process() { _valid_to="$2" shift ;; + --certificate-profile) + _certificate_profile="$2" + shift + ;; --httpport) _httpport="$2" Le_HTTPPort="$_httpport" @@ -7957,7 +7974,7 @@ _process() { uninstall) uninstall "$_nocron" ;; upgrade) upgrade ;; issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" ;; deploy) deploy "$_domain" "$_deploy_hook" "$_ecc" From 0c98dc54fee5194e5240e9e71e6d13f105894cb8 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Sun, 13 Jul 2025 11:18:10 +1000 Subject: [PATCH 118/689] Fix logged typo when running pre hook --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d70e323b..252d0227 100755 --- a/acme.sh +++ b/acme.sh @@ -3513,7 +3513,7 @@ _on_before_issue() { _debug _chk_alt_domains "$_chk_alt_domains" #run pre hook if [ "$_chk_pre_hook" ]; then - _info "Runing pre hook:'$_chk_pre_hook'" + _info "Running pre hook:'$_chk_pre_hook'" if ! ( export Le_Domain="$_chk_main_domain" export Le_Alt="$_chk_alt_domains" From 40e58ed12d14319f5e355055b7a1c973519dedcb Mon Sep 17 00:00:00 2001 From: David Beitey Date: Sun, 13 Jul 2025 11:40:34 +1000 Subject: [PATCH 119/689] Run post hook when _on_before_issue errors --- acme.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/acme.sh b/acme.sh index d70e323b..8ce5bacc 100755 --- a/acme.sh +++ b/acme.sh @@ -4502,6 +4502,7 @@ issue() { if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then _err "_on_before_issue." + _on_issue_err "$_post_hook" return 1 fi From 06c1911a2830c7ccb7d08cf3ec210921bfbb5038 Mon Sep 17 00:00:00 2001 From: PrivacyFreak <220089342+privacyfr3ak@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:48:32 +0000 Subject: [PATCH 120/689] fix keystore ownership read for unifi.sh --- deploy/unifi.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/unifi.sh b/deploy/unifi.sh index 1f274236..2af46b4a 100644 --- a/deploy/unifi.sh +++ b/deploy/unifi.sh @@ -143,8 +143,8 @@ 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') + _unifi_keystore_dir_owner=$(ls -ld "${_unifi_keystore_dir}" | awk '{print $3}') + _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 From 3252e0ce2e26fae5cc50edd9f61bd2b7a96b15b3 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:14:12 -0400 Subject: [PATCH 121/689] Add outputs for PFX and PEM Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 52 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index 3b4fc219..38ae9599 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -8,13 +8,17 @@ # (e.g. The deploy-freenas script for TrueNAS Core/Scale # https://github.com/danb35/deploy-freenas/ ) # +# If the same file is configured for the certificate key +# and the certificate and/or full chain, a combined PEM file will +# be output instead. # # Environment variables to be utilized are as follows: # -# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer # DEPLOY_LOCALCOPY_CERTKEY - /path/to/target/cert.key +# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer # DEPLOY_LOCALCOPY_FULLCHAIN - /path/to/target/fullchain.cer # DEPLOY_LOCALCOPY_CA - /path/to/target/ca.cer +# DEPLOY_LOCALCOPY_PFX - /path/to/target/cert.pfx # DEPLOY_LOCALCOPY_RELOADCMD - "echo 'this is my cmd'" ######## Public functions ##################### @@ -26,18 +30,53 @@ localcopy_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" + _cpfx="$6" _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" + _debug _cpfx "$_cpfx" _getdeployconf DEPLOY_LOCALCOPY_CERTIFICATE _getdeployconf DEPLOY_LOCALCOPY_CERTKEY _getdeployconf DEPLOY_LOCALCOPY_FULLCHAIN _getdeployconf DEPLOY_LOCALCOPY_CA _getdeployconf DEPLOY_LOCALCOPY_RELOADCMD + _getdeployconf DEPLOY_LOCALCOPY_PFX + _combined_target="" + _combined_srccert="" + + if [ "$DEPLOY_LOCALCOPY_CERTKEY" ] && + { [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ] || + [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; }; then + + _combined_target="$DEPLOY_LOCALCOPY_CERTKEY" + _savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY" + + if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then + _combined_srccert="$_ccert" + _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" + DEPLOY_LOCALCOPY_CERTIFICATE="" + fi + if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then + _combined_srccert="$_cfullchain" + _savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN" + DEPLOY_LOCALCOPY_FULLCHAIN="" + fi + DEPLOY_LOCALCOPY_CERTKEY="" + _info "Creating combined PEM at $_combined_target" + _tmpfile="$(mktemp)" + if ! cat "$_combined_srccert" "$_ckey" >"$_tmpfile"; then + _err "Failed to build combined PEM file" + return 1 + fi + if ! mv "$_tmpfile" "$_combined_target"; then + _err "Failed to move combined PEM into place" + return 1 + fi + fi if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _info "Copying certificate" @@ -46,7 +85,6 @@ localcopy_deploy() { _err "Failed to copy certificate, aborting." return 1 fi - _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" fi if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then @@ -79,6 +117,16 @@ localcopy_deploy() { _savedeployconf DEPLOY_LOCALCOPY_CA "$DEPLOY_LOCALCOPY_CA" fi + if [ "$DEPLOY_LOCALCOPY_PFX" ]; then + _info "Copying PFX" + _debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX" + if ! eval "cp $_cpfx $DEPLOY_LOCALCOPY_PFX"; then + _err "Failed to copy PFX, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_PFX "$DEPLOY_LOCALCOPY_PFX" + fi + _reload=$DEPLOY_LOCALCOPY_RELOADCMD _debug "Running reloadcmd $_reload" From 45c4a98f1d8bcb28a051de8437be790a3a168266 Mon Sep 17 00:00:00 2001 From: Viktor Polyakov Date: Mon, 11 Aug 2025 16:05:09 +0300 Subject: [PATCH 122/689] feat: add message_thread_id to telegram notifications --- notify/telegram.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) mode change 100644 => 100755 notify/telegram.sh diff --git a/notify/telegram.sh b/notify/telegram.sh old mode 100644 new mode 100755 index ccbd1533..97dd2861 --- a/notify/telegram.sh +++ b/notify/telegram.sh @@ -1,10 +1,14 @@ -#!/usr/bin/env sh +#!/usr/bin/bash #Support Telegram Bots #TELEGRAM_BOT_APITOKEN="" #TELEGRAM_BOT_CHATID="" #TELEGRAM_BOT_URLBASE="" +#TELEGRAM_BOT_THREADID="" + +# To get TELEGRAM_BOT_THREADID, just copy the link of the message from the thread. +# https://t.me/c/123456789/XXX/1520 - XXX is the TELEGRAM_BOT_THREADID telegram_send() { _subject="$1" @@ -28,6 +32,12 @@ telegram_send() { fi _saveaccountconf_mutable TELEGRAM_BOT_CHATID "$TELEGRAM_BOT_CHATID" + TELEGRAM_BOT_THREADID="${TELEGRAM_BOT_THREADID:-$(_readaccountconf_mutable TELEGRAM_BOT_THREADID)}" + if [ -z "$TELEGRAM_BOT_THREADID" ]; then + TELEGRAM_BOT_THREADID="" + fi + _saveaccountconf_mutable TELEGRAM_BOT_THREADID "$TELEGRAM_BOT_THREADID" + TELEGRAM_BOT_URLBASE="${TELEGRAM_BOT_URLBASE:-$(_readaccountconf_mutable TELEGRAM_BOT_URLBASE)}" if [ -z "$TELEGRAM_BOT_URLBASE" ]; then TELEGRAM_BOT_URLBASE="https://api.telegram.org" @@ -39,6 +49,9 @@ telegram_send() { _content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)" _data="{\"text\": \"$_content\", " _data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", " + if [ -n "$TELEGRAM_BOT_THREADID" ]; then + _data="$_data\"message_thread_id\": \"$TELEGRAM_BOT_THREADID\", " + fi _data="$_data\"parse_mode\": \"MarkdownV2\", " _data="$_data\"disable_web_page_preview\": \"1\"}" From 1f486fc9a524ead28d4b801f450b097eb3a58311 Mon Sep 17 00:00:00 2001 From: keryfan <35259207+keryfan@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:12:09 +0300 Subject: [PATCH 123/689] Upload latest dev branch to master (#3) * Fix for empty error objects in response breaking extraction of domain validation types Fix for empty error objects in the response which mess up the extraction of domain validation types due to the closing brace in the error object prematurely matching the end of the search pattern. This seems to be a recent change with ZeroSSL in particular where "error":{} is being included in responses. There could potentially be a related issue if there is a complex error object ever returned in the validation check response where an embedded sub-object could lead to an incomplete extraction of the error message, roughly around line 5040. Adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 * Add new dnsapi support for OpenProvider.eu using new REST API * Cleanup duplicate debug log output based on DNS test run * Resolve spellcheck error * Configure 10 second timeout to ACME_DIRECTORY API call * add support for AIX style netstat * add * fix for wiki * minor * minor * wiki * wiki * dnsapi: dns_mydnsjp.sh fix author The @epgdatacapbon was renamed to @tkmsst Signed-off-by: Sergey Ponomarev * dnsapi: dns_ddnss.sh remove RaidenII from authors He made the DuckDNS script that was used for this script but he can't support the script. Signed-off-by: Sergey Ponomarev * dnsapi: fix authors: use @ for GitHub profiles Signed-off-by: Sergey Ponomarev * dnsapi: dns_vultr.sh remove empty author Signed-off-by: Sergey Ponomarev * dnsapi: dns_mijnhost.sh rearrange fields, use user docs instead of API docs Signed-off-by: Sergey Ponomarev * dnsapi: fix Structured DNS Info Signed-off-by: Sergey Ponomarev * Fix logged typo when running pre hook * Run post hook when _on_before_issue errors --------- Signed-off-by: Sergey Ponomarev Co-authored-by: Ciaran Walsh Co-authored-by: Lambiek12 Co-authored-by: Erwin Oegema Co-authored-by: laDanz Co-authored-by: neil Co-authored-by: neil Co-authored-by: Sergey Ponomarev Co-authored-by: David Beitey Co-authored-by: Jan-willem van Kampen --- .github/workflows/wiki-monitor.yml | 60 ++++++++++ acme.sh | 17 ++- dnsapi/dns_beget.sh | 2 +- dnsapi/dns_bookmyname.sh | 2 +- dnsapi/dns_ddnss.sh | 2 +- dnsapi/dns_dnshome.sh | 2 +- dnsapi/dns_duckdns.sh | 2 +- dnsapi/dns_dyn.sh | 2 +- dnsapi/dns_dynv6.sh | 2 +- dnsapi/dns_easydns.sh | 2 +- dnsapi/dns_freedns.sh | 2 +- dnsapi/dns_he_ddns.sh | 1 + dnsapi/dns_joker.sh | 2 +- dnsapi/dns_mijnhost.sh | 9 +- dnsapi/dns_mydnsjp.sh | 2 +- dnsapi/dns_namecom.sh | 2 +- dnsapi/dns_namesilo.sh | 2 +- dnsapi/dns_openprovider_rest.sh | 186 +++++++++++++++++++++++++++++ dnsapi/dns_pleskxml.sh | 2 +- dnsapi/dns_schlundtech.sh | 2 +- dnsapi/dns_selectel.sh | 38 +++--- dnsapi/dns_spaceship.sh | 8 +- dnsapi/dns_tele3.sh | 2 +- dnsapi/dns_timeweb.sh | 2 +- dnsapi/dns_udr.sh | 2 +- dnsapi/dns_vscale.sh | 2 +- dnsapi/dns_vultr.sh | 1 - dnsapi/dns_websupport.sh | 2 +- dnsapi/dns_world4you.sh | 2 +- 29 files changed, 305 insertions(+), 57 deletions(-) create mode 100644 .github/workflows/wiki-monitor.yml create mode 100644 dnsapi/dns_openprovider_rest.sh diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml new file mode 100644 index 00000000..89497580 --- /dev/null +++ b/.github/workflows/wiki-monitor.yml @@ -0,0 +1,60 @@ +name: Notify via Issue on Wiki Edit + +on: + gollum: + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Checkout wiki repository + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }}.wiki + path: wiki + + - name: Generate wiki change message + run: | + actor="${{ github.actor }}" + sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH") + page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH") + page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") + page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") + page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") + now="$(date '+%Y-%m-%d %H:%M:%S')" + + cd wiki + prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1) + if [ -n "$prev_sha" ]; then + git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff + else + echo "(no diff)" > ../wiki.diff + fi + cd .. + { + echo "Wiki edited" + echo -n "User: " + echo "[$actor]($sender_url)" + echo "Time: $now" + echo "Page: [$page_name]($page_url) (Action: $page_action)" + echo "" + echo "----" + echo "### diff:" + echo '```diff' + cat wiki.diff + echo '```' + } > wiki-change-msg.txt + + - name: Create issue to notify Neilpang + uses: peter-evans/create-issue-from-file@v5 + with: + title: "Wiki edited" + content-filepath: ./wiki-change-msg.txt + assignees: Neilpang + env: + TZ: Asia/Shanghai + + + + + diff --git a/acme.sh b/acme.sh index e9eb6b94..d9ae208a 100755 --- a/acme.sh +++ b/acme.sh @@ -1401,6 +1401,12 @@ _ss() { return 0 fi + if [ "$(uname)" = "AIX" ]; then + _debug "Using: AIX netstat" + netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port " + return 0 + fi + if _exists "netstat"; then _debug "Using: netstat" if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then @@ -2761,7 +2767,7 @@ _initAPI() { _request_retry_times=0 while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do _request_retry_times=$(_math "$_request_retry_times" + 1) - response=$(_get "$_api_server") + response=$(_get "$_api_server" "" 10) if [ "$?" != "0" ]; then _debug2 "response" "$response" _info "Cannot init API for: $_api_server." @@ -3507,7 +3513,7 @@ _on_before_issue() { _debug _chk_alt_domains "$_chk_alt_domains" #run pre hook if [ "$_chk_pre_hook" ]; then - _info "Runing pre hook:'$_chk_pre_hook'" + _info "Running pre hook:'$_chk_pre_hook'" if ! ( export Le_Domain="$_chk_main_domain" export Le_Alt="$_chk_alt_domains" @@ -4496,6 +4502,7 @@ issue() { if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then _err "_on_before_issue." + _on_issue_err "$_post_hook" return 1 fi @@ -4755,7 +4762,8 @@ $_authorizations_map" _debug keyauthorization "$keyauthorization" fi - entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 + entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$keyauthorization" -a -z "$entry" ]; then @@ -6344,7 +6352,8 @@ _deactivate() { fi _debug "Trigger validation." vtype="$(_getIdType "$_d_domain")" - entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 + entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" _debug entry "$entry" if [ -z "$entry" ]; then _err "$d: Cannot get domain token" diff --git a/dnsapi/dns_beget.sh b/dnsapi/dns_beget.sh index aa43caed..5f3b1eb1 100755 --- a/dnsapi/dns_beget.sh +++ b/dnsapi/dns_beget.sh @@ -7,7 +7,7 @@ Options: BEGET_User API user BEGET_Password API password Issues: github.com/acmesh-official/acme.sh/issues/6200 -Author: ARNik arnik@arnik.ru +Author: ARNik ' Beget_Api="https://api.beget.com/api" 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_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..0c9491f8 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" 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_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_he_ddns.sh b/dnsapi/dns_he_ddns.sh index cd7d1ec2..1fe9a7fd 100644 --- a/dnsapi/dns_he_ddns.sh +++ b/dnsapi/dns_he_ddns.sh @@ -5,6 +5,7 @@ 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ö ' 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_mijnhost.sh b/dnsapi/dns_mijnhost.sh index 9dafc702..9f5e7710 100644 --- a/dnsapi/dns_mijnhost.sh +++ b/dnsapi/dns_mijnhost.sh @@ -1,16 +1,15 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 dns_mijnhost_info='mijn.host -Domains: mijn.host Site: mijn.host -Docs: https://mijn.host/api/doc/ -Issues: https://github.com/acmesh-official/acme.sh/issues/6177 -Author: peterv99 +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 ###################### Constants for your mijn-host API +######## Public functions ###################### MIJNHOST_API="https://mijn.host/api/v2" # Add TXT record for domain verification 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_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_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_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 434bc483..565f541b 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -1,27 +1,21 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 - -# dns_selectel_info='Selectel.com -# Domains: Selectel.ru -# Site: Selectel.com -# Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel -# Options: -# Variables that must be defined before running -# SL_Ver can take one of the values 'v1' or 'v2', default is 'v1' -# SL_Ver='v1', when using version API legacy (v1) -# SL_Ver='v2', when using version API actual (v2) -# when using API version v1, i.e. SL_Ver is 'v1' or not defined: -# SL_Key - API Key, required -# when using API version v2: -# SL_Ver - required as 'v2' -# SL_Login_ID - account ID, required -# SL_Project_Name - name project, required -# SL_Login_Name - service user name, required -# SL_Pswd - service user password, required -# SL_Expire - token lifetime in minutes (0-1440), default 1400 minutes -# -# Issues: github.com/acmesh-official/acme.sh/issues/5126 -# +dns_selectel_info='Selectel.com +Domains: Selectel.ru +Site: Selectel.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel +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" auth_uri="https://cloud.api.selcloud.ru/identity/v3/auth/tokens" diff --git a/dnsapi/dns_spaceship.sh b/dnsapi/dns_spaceship.sh index 770e22cc..8fff4037 100644 --- a/dnsapi/dns_spaceship.sh +++ b/dnsapi/dns_spaceship.sh @@ -4,11 +4,11 @@ dns_spaceship_info='Spaceship.com Site: Spaceship.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship Options: - SPACESHIP_API_KEY Spaceship API Key - SPACESHIP_API_SECRET Spaceship API Secret - SPACESHIP_ROOT_DOMAIN (Optional) Manually specify the root domain if auto-detection fails + 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 +Author: Meow <@Meo597> ' # Spaceship API 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_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_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_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_world4you.sh b/dnsapi/dns_world4you.sh index 46cdc4fe..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" From 1b5e66f9c2e4907408c05164010d7cd4422d6051 Mon Sep 17 00:00:00 2001 From: wout Date: Wed, 23 Jul 2025 10:12:32 +0200 Subject: [PATCH 124/689] Add sleep before each REST call to Constellix to prevent rate limit --- dnsapi/dns_constellix.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dnsapi/dns_constellix.sh b/dnsapi/dns_constellix.sh index 6a50e199..480541ed 100644 --- a/dnsapi/dns_constellix.sh +++ b/dnsapi/dns_constellix.sh @@ -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) From ab22c8ca1cb89cda5e47d510de10e12ffabd39a0 Mon Sep 17 00:00:00 2001 From: wout Date: Tue, 12 Aug 2025 19:04:19 +0200 Subject: [PATCH 125/689] Convert domain to lower case, needed for Constellix REST API --- dnsapi/dns_constellix.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_constellix.sh b/dnsapi/dns_constellix.sh index 480541ed..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" From bcf0afb25ef9f159da397db73f1050c3b92f56d0 Mon Sep 17 00:00:00 2001 From: Tobias Grave Date: Fri, 15 Aug 2025 09:02:57 +0200 Subject: [PATCH 126/689] Variomedia API: Fix DNS deletion issues --- dnsapi/dns_variomedia.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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." From 8713918bdb4061082e59b12a5e96ae03b0af0bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 20 Aug 2025 12:41:27 +0800 Subject: [PATCH 127/689] Fix ipv6 cert cannot be found --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d9ae208a..4b48036a 100755 --- a/acme.sh +++ b/acme.sh @@ -5565,7 +5565,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - for di in "${CERT_HOME}"/*.*/; do + for di in "${CERT_HOME}"/*/; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" From 5b02e8633441ece592bc2b9f21bd52b7ea38af25 Mon Sep 17 00:00:00 2001 From: asauerwein Date: Wed, 20 Aug 2025 17:47:36 +0200 Subject: [PATCH 128/689] add template_stack option to push to device --- deploy/panos.sh | 75 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index 0dc1b2f0..2ed6a230 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -7,20 +7,26 @@ # # Firewall admin with superuser and IP address is required. # -# REQURED: +# REQUIRED: # export PANOS_HOST="" # export PANOS_USER="" #User *MUST* have Commit and Import Permissions in XML API for Admin Role # export PANOS_PASS="" # # OPTIONAL -# export PANOS_TEMPLATE="" #Template Name of panorama managed devices +# 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 # # The script will automatically generate a new API key if # no key is found, or if a saved key has expired or is invalid. + + + # This function is to parse the XML response from the firewall parse_response() { type=$2 + _debug "API Response: $1" if [ "$type" = 'keygen' ]; then status=$(echo "$1" | sed 's/^.*\(['\'']\)\([a-z]*\)'\''.*/\2/g') if [ "$status" = "success" ]; then @@ -30,6 +36,13 @@ parse_response() { message="PAN-OS Key could not be set." fi else + if [ "$type" = 'commit' ]; then + job_id=$(echo "$1" | sed 's/^.*\(\)\(.*\)<\/job>.*/\2/g') + _commit_job_id=$job_id + elif [ "$type" = 'job_status' ]; then + job_status=$(echo "$1" | tr -d '\n' | sed 's/^.*\([^<]*\)<\/result>.*/\1/g') + _commit_job_status=$job_status + fi status=$(echo "$1" | tr -d '\n' | sed 's/^.*"\([a-z]*\)".*/\1/g') message=$(echo "$1" | tr -d '\n' | sed 's/.*\(\|\|\)\([^<]*\).*/\2/g') _debug "Firewall message: $message" @@ -44,7 +57,7 @@ parse_response() { #This function is used to deploy to the firewall deployer() { content="" - type=$1 # Types are keytest, keygen, cert, key, commit + type=$1 # Types are keytest, keygen, cert, key, commit, job_status, push panos_url="https://$_panos_host/api/" #Test API Key by performing a lookup @@ -84,6 +97,9 @@ deployer() { if [ "$_panos_template" ]; then content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template" fi + if [ "$_panos_vsys" ]; then + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys" + fi fi if [ "$type" = 'key' ]; then panos_url="${panos_url}?type=import" @@ -96,6 +112,9 @@ deployer() { if [ "$_panos_template" ]; then content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template" fi + if [ "$_panos_vsys" ]; then + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys" + fi fi #Close multipart content="$content${nl}--$delim--${nl}${nl}" @@ -118,6 +137,22 @@ deployer() { content="type=commit&action=partial&key=$_panos_key&cmd=$cmd" fi + # Query job status + if [ "$type" = 'job_status' ]; then + echo "**** Querying job $_commit_job_id status ****" + H1="Content-Type: application/x-www-form-urlencoded" + cmd=$(printf "%s" "$_commit_job_id" | _url_encode) + content="type=op&key=$_panos_key&cmd=$cmd" + fi + + # Push changes + if [ "$type" = 'push' ]; then + echo "**** Pushing changes ****" + H1="Content-Type: application/x-www-form-urlencoded" + cmd=$(printf "%s" "$_panos_template_stack$_panos_user" | _url_encode) + content="type=commit&action=all&key=$_panos_key&cmd=$cmd" + fi + response=$(_post "$content" "$panos_url" "" "POST") parse_response "$response" "$type" # Saving response to variables @@ -126,6 +161,8 @@ deployer() { if [ "$response_status" = "success" ]; then _debug "Successfully deployed $type" return 0 + elif [ "$_commit_job_status" ]; then + _debug "Commit Job Status = $_commit_job_status" else _err "Deploy of type $type failed. Try deploying with --debug to troubleshoot." _debug "$message" @@ -191,11 +228,31 @@ panos_deploy() { _getdeployconf PANOS_TEMPLATE fi + # PANOS_TEMPLATE_STACK + if [ "$PANOS_TEMPLATE_STACK" ]; then + _debug "Detected ENV variable PANOS_TEMPLATE_STACK. Saving to file." + _savedeployconf PANOS_TEMPLATE_STACK "$PANOS_TEMPLATE_STACK" 1 + else + _debug "Attempting to load variable PANOS_TEMPLATE_STACK from file." + _getdeployconf PANOS_TEMPLATE_STACK + fi + + # PANOS_TEMPLATE_STACK + if [ "$PANOS_VSYS" ]; then + _debug "Detected ENV variable PANOS_VSYS. Saving to file." + _savedeployconf PANOS_VSYS "$PANOS_VSYS" 1 + else + _debug "Attempting to load variable PANOS_VSYS from file." + _getdeployconf PANOS_VSYS + fi + #Store variables _panos_host=$PANOS_HOST _panos_user=$PANOS_USER _panos_pass=$PANOS_PASS _panos_template=$PANOS_TEMPLATE + _panos_template_stack=$PANOS_TEMPLATE_STACK + _panos_vsys=$PANOS_VSYS #Test API Key if found. If the key is invalid, the variable _panos_key will be unset. if [ "$_panos_host" ] && [ "$_panos_key" ]; then @@ -229,6 +286,18 @@ panos_deploy() { deployer cert deployer key deployer commit + if [ "$_panos_template_stack" ]; then + # try to get job status for 20 times in 30 sec interval + for ((i = 0 ; i < 20 ; i++ )); do + deployer job_status + if [[ "$_commit_job_status" == "OK" ]]; then + echo "Commit finished!" + break + fi + sleep 30 + done + deployer push + fi fi fi } From fdb1e8c2e46143a84aa8d26d49d5c1dce50aea74 Mon Sep 17 00:00:00 2001 From: asauerwein Date: Wed, 20 Aug 2025 18:37:25 +0200 Subject: [PATCH 129/689] fix usage of H1 header change to while loop use global variable for loop fix if statement to be sh compliant shfmt --- deploy/panos.sh | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index 2ed6a230..a9232e79 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -20,8 +20,8 @@ # The script will automatically generate a new API key if # no key is found, or if a saved key has expired or is invalid. - - +_COMMIT_WAIT_INTERVAL=30 # query commit status every 30 seconds +_COMMIT_WAIT_ITERATIONS=20 # query commit status 20 times (20*30 = 600 seconds = 10 minutes) # This function is to parse the XML response from the firewall parse_response() { @@ -59,11 +59,11 @@ deployer() { content="" type=$1 # Types are keytest, keygen, cert, key, commit, job_status, push panos_url="https://$_panos_host/api/" + export _H1="Content-Type: application/x-www-form-urlencoded" #Test API Key by performing a lookup if [ "$type" = 'keytest' ]; then _debug "**** Testing saved API Key ****" - _H1="Content-Type: application/x-www-form-urlencoded" # Get Version Info to test key content="type=version&key=$_panos_key" ## Exclude all scopes for the empty commit @@ -74,7 +74,6 @@ deployer() { # Generate API Key if [ "$type" = 'keygen' ]; then _debug "**** Generating new API Key ****" - _H1="Content-Type: application/x-www-form-urlencoded" content="type=keygen&user=$_panos_user&password=$_panos_pass" # content="$content${nl}--$delim${nl}Content-Disposition: form-data; type=\"keygen\"; user=\"$_panos_user\"; password=\"$_panos_pass\"${nl}Content-Type: application/octet-stream${nl}${nl}" fi @@ -99,7 +98,7 @@ deployer() { fi if [ "$_panos_vsys" ]; then content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys" - fi + fi fi if [ "$type" = 'key' ]; then panos_url="${panos_url}?type=import" @@ -114,7 +113,7 @@ deployer() { fi if [ "$_panos_vsys" ]; then content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys" - fi + fi fi #Close multipart content="$content${nl}--$delim--${nl}${nl}" @@ -125,7 +124,6 @@ deployer() { # Commit changes if [ "$type" = 'commit' ]; then _debug "**** Committing changes ****" - export _H1="Content-Type: application/x-www-form-urlencoded" #Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution! if [ "$FORCE" ]; then _debug "Force switch detected. Committing ALL changes to the firewall." @@ -140,7 +138,6 @@ deployer() { # Query job status if [ "$type" = 'job_status' ]; then echo "**** Querying job $_commit_job_id status ****" - H1="Content-Type: application/x-www-form-urlencoded" cmd=$(printf "%s" "$_commit_job_id" | _url_encode) content="type=op&key=$_panos_key&cmd=$cmd" fi @@ -148,7 +145,6 @@ deployer() { # Push changes if [ "$type" = 'push' ]; then echo "**** Pushing changes ****" - H1="Content-Type: application/x-www-form-urlencoded" cmd=$(printf "%s" "$_panos_template_stack$_panos_user" | _url_encode) content="type=commit&action=all&key=$_panos_key&cmd=$cmd" fi @@ -288,13 +284,15 @@ panos_deploy() { deployer commit if [ "$_panos_template_stack" ]; then # try to get job status for 20 times in 30 sec interval - for ((i = 0 ; i < 20 ; i++ )); do - deployer job_status - if [[ "$_commit_job_status" == "OK" ]]; then - echo "Commit finished!" - break - fi - sleep 30 + i=0 + while [ "$i" -lt $_COMMIT_WAIT_ITERATIONS ]; do + deployer job_status + if [ "$_commit_job_status" = "OK" ]; then + echo "Commit finished!" + break + fi + sleep $_COMMIT_WAIT_INTERVAL + i=$((i + 1)) done deployer push fi From d7c428fc8d6bb123b890ec669b606eaf620b9c25 Mon Sep 17 00:00:00 2001 From: Richard Glidden Date: Mon, 30 Jun 2025 14:14:26 -0400 Subject: [PATCH 130/689] feat: Add ability to deploy to remote TrueNAS instances --- deploy/truenas_ws.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index bdc1b846..74a46530 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -39,13 +39,13 @@ _ws_call() { _debug "_ws_call arg2" "$2" _debug "_ws_call arg3" "$3" if [ $# -eq 3 ]; then - _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3") + _ws_response=$(midclt --uri $_ws_uri -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3") fi if [ $# -eq 2 ]; then - _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2") + _ws_response=$(midclt --uri $_ws_uri -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2") fi if [ $# -eq 1 ]; then - _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1") + _ws_response=$(midclt --uri $_ws_uri -K "$DEPLOY_TRUENAS_APIKEY" call "$1") fi _debug "_ws_response" "$_ws_response" printf "%s" "$_ws_response" @@ -60,7 +60,7 @@ _ws_upload_cert() { import sys from truenas_api_client import Client -with Client() as c: +with Client(uri="$_ws_uri") as c: ### Login with API key print("I:Trying to upload new certificate...") @@ -175,6 +175,16 @@ truenas_ws_deploy() { _debug _file_ca "$_file_ca" _debug _file_fullchain "$_file_fullchain" + ########## Default values for hostname and protocol + [ -n "${DEPLOY_TRUENAS_HOSTNAME}" ] || DEPLOY_TRUENAS_HOSTNAME="localhost" + [ -n "${DEPLOY_TRUENAS_PROTOCOL}" ] || DEPLOY_TRUENAS_PROTOCOL="ws" + + _debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" + _debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" + + _ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket" + _debug _ws_uri "$_ws_uri" + ########## Environment check _info "Checking environment variables..." @@ -304,7 +314,7 @@ truenas_ws_deploy() { _info "Restarting WebUI..." _ws_response=$(_ws_call "system.general.ui_restart") _info "Waiting for UI restart..." - sleep 6 + sleep 15 ########## Certificates From 6d40ac86449cdb80b52c6ffb647eac560174c84c Mon Sep 17 00:00:00 2001 From: Richard Glidden Date: Mon, 30 Jun 2025 14:51:36 -0400 Subject: [PATCH 131/689] chore: Fix shellcheck errors --- deploy/truenas_ws.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index 74a46530..ea6fc7e6 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -39,13 +39,13 @@ _ws_call() { _debug "_ws_call arg2" "$2" _debug "_ws_call arg3" "$3" if [ $# -eq 3 ]; then - _ws_response=$(midclt --uri $_ws_uri -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3") + _ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3") fi if [ $# -eq 2 ]; then - _ws_response=$(midclt --uri $_ws_uri -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2") + _ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2") fi if [ $# -eq 1 ]; then - _ws_response=$(midclt --uri $_ws_uri -K "$DEPLOY_TRUENAS_APIKEY" call "$1") + _ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1") fi _debug "_ws_response" "$_ws_response" printf "%s" "$_ws_response" From 5aae3333bc653340c45981b6bd798ec90509301a Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 20:44:24 +0100 Subject: [PATCH 132/689] Show proxmox deploy scripts response only on debug --- deploy/proxmoxbs.sh | 12 +++++++++++- deploy/proxmoxve.sh | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh index d1146454..e8528e8f 100644 --- a/deploy/proxmoxbs.sh +++ b/deploy/proxmoxbs.sh @@ -115,6 +115,16 @@ HEREDOC _info "Push certificates to server" export HTTPS_INSECURE=1 export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}" - _post "$_json_payload" "$_target_url" "" POST "application/json" + response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") + _retval=$? + if [ "${_retval}" -eq 0 ]; then + _debug3 response "$response" + _info "Certificate successfully deployed" + return 0 + else + _err "Certificate deployment failed" + _debug "Response" "$response" + return 1 + fi } diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh index f9de590c..8c67f7de 100644 --- a/deploy/proxmoxve.sh +++ b/deploy/proxmoxve.sh @@ -127,6 +127,16 @@ HEREDOC _info "Push certificates to server" export HTTPS_INSECURE=1 export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}" - _post "$_json_payload" "$_target_url" "" POST "application/json" + response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") + _retval=$? + if [ "${_retval}" -eq 0 ]; then + _debug3 response "$response" + _info "Certificate successfully deployed" + return 0 + else + _err "Certificate deployment failed" + _debug "Response" "$response" + return 1 + fi } From d366b7e4fc7799bcb1a43213e2566096e1c19a28 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 1 Sep 2025 19:54:36 +0100 Subject: [PATCH 133/689] Fix diff in wiki notifications (use full clone) The checkout action fetches one single commit, so attempts to find previous states of a page result in error. Adding fetch-depth:0 to the configuration fetches all commits and makes finding the previous commit that changed a page possible in the github action. --- .github/workflows/wiki-monitor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 89497580..b0332775 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -12,6 +12,7 @@ jobs: with: repository: ${{ github.repository }}.wiki path: wiki + fetch-depth: 0 - name: Generate wiki change message run: | @@ -58,3 +59,4 @@ jobs: + From 04e254923939d0d600cd8b29dfe3b34ca7421052 Mon Sep 17 00:00:00 2001 From: Guillaume PELURE Date: Tue, 2 Sep 2025 21:13:38 +0200 Subject: [PATCH 134/689] socat rejects TCP-LISTEN on ipv6 only networks --- acme.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d70e323b..5c4d6543 100755 --- a/acme.sh +++ b/acme.sh @@ -2538,15 +2538,17 @@ _startserver() { _NC="socat" if [ "$Le_Listen_V6" ]; then _NC="$_NC -6" + SOCAT_OPTIONS=TCP6-LISTEN else _NC="$_NC -4" + SOCAT_OPTIONS=TCP4-LISTEN fi if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then _NC="$_NC -d -d -v" fi - SOCAT_OPTIONS=TCP-LISTEN:$Le_HTTPPort,crlf,reuseaddr,fork + SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork #Adding bind to local-address if [ "$ncaddr" ]; then From 39cb87dc4bf481460ddba4d2754f07d351da07e4 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Sep 2025 22:08:55 +0200 Subject: [PATCH 135/689] fix for DragonflyBSD just move "date -u -j -f" before the linux branch. --- acme.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/acme.sh b/acme.sh index d9ae208a..bfd26bd6 100755 --- a/acme.sh +++ b/acme.sh @@ -1811,6 +1811,10 @@ _time() { # 2022-04-01 08:10:33 to 1648800633 #or 2022-04-01T08:10:33Z to 1648800633 _date2time() { + #Mac/BSD + if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then + return + fi #Linux if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then return @@ -1820,10 +1824,6 @@ _date2time() { if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then return fi - #Mac/BSD - if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then - return - fi #Omnios if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then return From e0e3cdc316f75af54e411f844093a9cff08781ac Mon Sep 17 00:00:00 2001 From: Eric Fu Date: Sat, 6 Sep 2025 22:31:50 +0800 Subject: [PATCH 136/689] Fix sed command in telegram notifier --- notify/telegram.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify/telegram.sh b/notify/telegram.sh index ccbd1533..c0621ae7 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 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')" + _content="$(printf "%s" "$_content" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')" _content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)" _data="{\"text\": \"$_content\", " _data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", " From 30faf500eb4f9394c3d43dbbba5eda43e4947e34 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 7 Sep 2025 10:09:27 +0200 Subject: [PATCH 137/689] fix https://github.com/acmesh-official/acme.sh/pull/6499#issuecomment-3259771356 --- acme.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d335990c..3316b25f 100755 --- a/acme.sh +++ b/acme.sh @@ -2539,9 +2539,11 @@ _startserver() { if [ "$Le_Listen_V6" ]; then _NC="$_NC -6" SOCAT_OPTIONS=TCP6-LISTEN - else + elif [ "$Le_Listen_V4" ]; then _NC="$_NC -4" SOCAT_OPTIONS=TCP4-LISTEN + else + SOCAT_OPTIONS=TCP-LISTEN fi if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then From 93dc22a71f62e28b28788974f5474c6331e9fb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?= <18481195+AnTheMaker@users.noreply.github.com> Date: Sun, 7 Sep 2025 16:57:10 +0200 Subject: [PATCH 138/689] Support Nanelo DNS Team- & Workspace-specific API keys Nanelo Team- and Workspace-specific API keys require the "domain" parameter to be set containing the DNS zone name (unlike the Domain-specific API keys). So I've added a function to detect the root DNS zone and set the required parameter as described here: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide#3-detect-which-part-is-your-root-zone --- dnsapi/dns_nanelo.sh | 62 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index 1ab47a89..cc3573a0 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="$(_get "$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 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="$(_get "$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 +} From 31d72645838a1781dc5e38c0b1291e94b30ad0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?= <18481195+AnTheMaker@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:14:06 +0200 Subject: [PATCH 139/689] Fix pattern matching for best zone selection --- dnsapi/dns_nanelo.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index cc3573a0..abf3cf47 100644 --- a/dnsapi/dns_nanelo.sh +++ b/dnsapi/dns_nanelo.sh @@ -100,7 +100,7 @@ _get_root() { bestzone="" for z in $zones; do case "$fulldomain" in - *.$z|$z) + *."$z"|"$z") if [ ${#z} -gt ${#bestzone} ]; then bestzone=$z fi From 5aa964cde997a091b47af6a0b355f236d8631391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?= <18481195+AnTheMaker@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:14:40 +0200 Subject: [PATCH 140/689] Formatting using shfmt to format the code --- dnsapi/dns_nanelo.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index abf3cf47..8bb95136 100644 --- a/dnsapi/dns_nanelo.sh +++ b/dnsapi/dns_nanelo.sh @@ -90,21 +90,21 @@ _get_root() { 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 , ' ') + 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 - ;; + *."$z" | "$z") + if [ ${#z} -gt ${#bestzone} ]; then + bestzone=$z + fi + ;; esac done From d8a92a2e658d4ea120dd71d41928e55dc713deb9 Mon Sep 17 00:00:00 2001 From: An <18481195+AnTheMaker@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:27:57 +0200 Subject: [PATCH 141/689] switch nanelo api to post requests --- dnsapi/dns_nanelo.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index 8bb95136..23f306a7 100644 --- a/dnsapi/dns_nanelo.sh +++ b/dnsapi/dns_nanelo.sh @@ -36,7 +36,7 @@ dns_nanelo_add() { _debug _domain "$_domain" _info "Adding TXT record to ${fulldomain}" - response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&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 @@ -68,7 +68,7 @@ dns_nanelo_rm() { _debug _domain "$_domain" _info "Deleting resource record $fulldomain" - response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&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 From 8608e9cd3a49c1a9f948e19ea3a62fac62a24aef Mon Sep 17 00:00:00 2001 From: Richard Glidden Date: Fri, 12 Sep 2025 22:22:30 -0400 Subject: [PATCH 142/689] Save and read variables --- deploy/truenas_ws.sh | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index ea6fc7e6..a204d352 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -175,25 +175,31 @@ truenas_ws_deploy() { _debug _file_ca "$_file_ca" _debug _file_fullchain "$_file_fullchain" - ########## Default values for hostname and protocol - [ -n "${DEPLOY_TRUENAS_HOSTNAME}" ] || DEPLOY_TRUENAS_HOSTNAME="localhost" - [ -n "${DEPLOY_TRUENAS_PROTOCOL}" ] || DEPLOY_TRUENAS_PROTOCOL="ws" - - _debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" - _debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" - - _ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket" - _debug _ws_uri "$_ws_uri" - ########## Environment check _info "Checking environment variables..." _getdeployconf DEPLOY_TRUENAS_APIKEY + _getdeployconf DEPLOY_TRUENAS_HOSTNAME + _getdeployconf DEPLOY_TRUENAS_PROTOCOL # Check API Key if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable." return 1 fi + # Check Hostname, default to localhost if not set + if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then + _info "TrueNAS hostname not set. Using 'localhost'." + DEPLOY_TRUENAS_HOSTNAME="localhost" + fi + # Check protocol, default to ws if not set + if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then + _info "TrueNAS protocol not set. Using 'ws'." + DEPLOY_TRUENAS_PROTOCOL="ws" + fi + _ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket" + _debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" + _debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" + _debug _ws_uri "$_ws_uri" _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY" _info "Environment variables: OK" @@ -215,6 +221,8 @@ truenas_ws_deploy() { return 2 fi _savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY" + _savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" + _savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" _info "TrueNAS health: OK" ########## System info From a1ea2a5aa6a63c1c99daffc66c1463e3a2ac88ab Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 14 Sep 2025 10:35:21 +0200 Subject: [PATCH 143/689] fix tr https://github.com/acmesh-official/acme.sh/issues/6511#issuecomment-3282521860 --- acme.sh | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 3316b25f..02986a33 100755 --- a/acme.sh +++ b/acme.sh @@ -436,14 +436,28 @@ _secure_debug3() { fi } +__USE_TR_TAG="" +if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ] ; then + __USE_TR_TAG="1" +fi +export __USE_TR_TAG + _upper_case() { + if [ "$__USE_TR_TAG" ]; then + LANG=C tr '[:lower:]' '[:upper:]' + else # shellcheck disable=SC2018,SC2019 - tr '[a-z]' '[A-Z]' + LANG=C tr '[a-z]' '[A-Z]' + fi } _lower_case() { - # shellcheck disable=SC2018,SC2019 - tr '[A-Z]' '[a-z]' + if [ "$__USE_TR_TAG" ]; then + LANG=C tr '[:upper:]' '[:lower:]' + else + # shellcheck disable=SC2018,SC2019 + LANG=C tr '[A-Z]' '[a-z]' + fi } _startswith() { From d76f4b27b0f2eae8b75d41459eff6e51094045ca Mon Sep 17 00:00:00 2001 From: benyamin-codez <115509179+benyamin-codez@users.noreply.github.com> Date: Sun, 7 Sep 2025 23:54:45 +1000 Subject: [PATCH 144/689] dnsapi/dns_opnsense.sh: Refresh for OPNsense v25.7 series Updates the dns_opnsense.sh Bourne shell script for OPNSense v25.7 series: 1. Fixes historical error in rm_record() [used incorrect response variable] 2. Improves debug messaging in rm_record() 3. Fixes _get_root() for change in OPNsense API * Response is now split into pseudo-rows * We now iterate through pseudo-rows for matching domainname field 4. Fixes _existingchallenge() for change in OPNsense API * Fixes unreliable regex for uuid * Adds domain regex and %domain field 5. Fixes historical error in _existingchallenge() [incorrect variable syntax] Resolves #6467 Signed-off-by: benyamin-codez <115509179+benyamin-codez@users.noreply.github.com> --- dnsapi/dns_opnsense.sh | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) 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 } From df350e6660df5b61e353a5daed7d7a1ee1f5ba4a Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 15 Sep 2025 19:34:54 +0200 Subject: [PATCH 145/689] fix format --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 02986a33..9105a0a4 100755 --- a/acme.sh +++ b/acme.sh @@ -437,7 +437,7 @@ _secure_debug3() { } __USE_TR_TAG="" -if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ] ; then +if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ]; then __USE_TR_TAG="1" fi export __USE_TR_TAG @@ -446,7 +446,7 @@ _upper_case() { if [ "$__USE_TR_TAG" ]; then LANG=C tr '[:lower:]' '[:upper:]' else - # shellcheck disable=SC2018,SC2019 + # shellcheck disable=SC2018,SC2019 LANG=C tr '[a-z]' '[A-Z]' fi } From 44c7473ef97bcae83e829a4a7e6b2ddcfc0d8369 Mon Sep 17 00:00:00 2001 From: Min Wang Date: Tue, 16 Sep 2025 15:09:12 +0800 Subject: [PATCH 146/689] fix bug for #6510 --- notify/telegram.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify/telegram.sh b/notify/telegram.sh index c0621ae7..7da05729 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 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([-_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')" + _content="$(printf "%s" "$_content" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([-_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')" _content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)" _data="{\"text\": \"$_content\", " _data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", " From 1b00ced7adf0c9ffe39c2f5e883395f7abefc2cc Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Tue, 16 Sep 2025 09:20:31 +0200 Subject: [PATCH 147/689] Add `--profile` as option for selecting certificate profile --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index ead7602b..7a8e4e9b 100755 --- a/acme.sh +++ b/acme.sh @@ -7702,7 +7702,7 @@ _process() { _valid_to="$2" shift ;; - --certificate-profile) + --certificate-profile | --profile) _certificate_profile="$2" shift ;; From 070cd0f4dfe983d093e1661aab8b73678cf5b259 Mon Sep 17 00:00:00 2001 From: Richard Glidden Date: Tue, 16 Sep 2025 22:19:16 -0400 Subject: [PATCH 148/689] Use _sleep instead of sleep --- deploy/truenas_ws.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index a204d352..d334853e 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -121,7 +121,7 @@ _ws_check_jobid() { # n/a _ws_get_job_result() { while true; do - sleep 2 + _sleep 2 _ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]") if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then _ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')" @@ -322,7 +322,7 @@ truenas_ws_deploy() { _info "Restarting WebUI..." _ws_response=$(_ws_call "system.general.ui_restart") _info "Waiting for UI restart..." - sleep 15 + _sleep 15 ########## Certificates From c3ec827fdd87ae7a595f7c84aa7395a52321f6df Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 19 Sep 2025 20:54:09 +0200 Subject: [PATCH 149/689] remove buypass --- acme.sh | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/acme.sh b/acme.sh index 9105a0a4..4d849845 100755 --- a/acme.sh +++ b/acme.sh @@ -23,9 +23,6 @@ _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY" CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory" CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" -CA_BUYPASS="https://api.buypass.com/acme/directory" -CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory" - CA_ZEROSSL="https://acme.zerossl.com/v2/DV90" _ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email" @@ -42,14 +39,12 @@ CA_NAMES=" ZeroSSL.com,zerossl LetsEncrypt.org,letsencrypt LetsEncrypt.org_test,letsencrypt_test,letsencrypttest -BuyPass.com,buypass -BuyPass.com_test,buypass_test,buypasstest SSL.com,sslcom Google.com,google Google.com_test,googletest,google_test " -CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST" +CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST" DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" @@ -5478,10 +5473,6 @@ renew() { _info "Switching back to $CA_LETSENCRYPT_V2" Le_API="$CA_LETSENCRYPT_V2" ;; - "$CA_BUYPASS_TEST") - _info "Switching back to $CA_BUYPASS" - Le_API="$CA_BUYPASS" - ;; "$CA_GOOGLE_TEST") _info "Switching back to $CA_GOOGLE" Le_API="$CA_GOOGLE" From 471e0c05f9b69dd2ae2ce7c7968b7f497334c18d Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 20 Sep 2025 10:38:43 +0200 Subject: [PATCH 150/689] remove mageia --- .github/workflows/Linux.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml index c74e9d3e..f3352a41 100644 --- a/.github/workflows/Linux.yml +++ b/.github/workflows/Linux.yml @@ -26,7 +26,7 @@ jobs: Linux: strategy: matrix: - os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3"] + os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "gentoo/stage3"] runs-on: ubuntu-latest env: TEST_LOCAL: 1 From f22b490a10bf07317096511e59d7791fc13b4fb7 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 21 Sep 2025 18:04:59 +0200 Subject: [PATCH 151/689] remove buypass --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9a5c106b..a8cb5403 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,6 @@ https://github.com/acmesh-official/acmetest - [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default) - Letsencrypt.org CA -- [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-CA) - [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) - [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) - [Pebble strict Mode](https://github.com/letsencrypt/pebble) From 5954f0dde54488d68bdd4c92ea2f0ab8ea1ee869 Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Mon, 22 Sep 2025 12:11:50 +0200 Subject: [PATCH 152/689] Change to `--cert-profile` --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 7a8e4e9b..d157c2a4 100755 --- a/acme.sh +++ b/acme.sh @@ -7702,7 +7702,7 @@ _process() { _valid_to="$2" shift ;; - --certificate-profile | --profile) + --certificate-profile | --cert-profile) _certificate_profile="$2" shift ;; From 604e6873ba43609dc686f869e5c42a7d31547030 Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Mon, 22 Sep 2025 12:12:17 +0200 Subject: [PATCH 153/689] Add short name + wiki link to help --- acme.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d157c2a4..ef3c592d 100755 --- a/acme.sh +++ b/acme.sh @@ -180,6 +180,8 @@ _VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity" _DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck" +_PROFILESELECTION_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Profile-selection" + _DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead." _DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR" @@ -7006,7 +7008,8 @@ Parameters: If no match, the default offered chain will be used. (default: empty) See: $_PREFERRED_CHAIN_WIKI - --certificate-profile If the CA offers profiles, select the desired profile + --cert-profile, --certificate-profile If the CA offers profiles, select the desired profile + See: $_PROFILESELECTION_WIKI --valid-to Request the NotAfter field of the cert. See: $_VALIDITY_WIKI From 11995b958ab560b2e8e60c816ba79200163047e9 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Sep 2025 22:57:42 +0200 Subject: [PATCH 154/689] add actalis.com CA --- acme.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index e895bbdc..23b03039 100755 --- a/acme.sh +++ b/acme.sh @@ -32,6 +32,8 @@ CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc" CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory" CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory" +CA_ACTALIS="https://acme-api.actalis.com/acme/directory" + DEFAULT_CA=$CA_ZEROSSL DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST @@ -42,9 +44,10 @@ LetsEncrypt.org_test,letsencrypt_test,letsencrypttest SSL.com,sslcom Google.com,google Google.com_test,googletest,google_test +Actalis.com,actalis.com,actalis " -CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST" +CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST,$CA_ACTALIS" DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)" From e5214ea2e58c9a7fd5fb95f651a5cfc1193787f4 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Sep 2025 23:12:05 +0200 Subject: [PATCH 155/689] add Actalis.com CA --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a8cb5403..f7038f59 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ https://github.com/acmesh-official/acmetest - Letsencrypt.org CA - [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) - [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) +- [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) - [Pebble strict Mode](https://github.com/letsencrypt/pebble) - Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA From b244c76dd5f316c55812c9ebd41012625e58e5a0 Mon Sep 17 00:00:00 2001 From: Steven Zhu Date: Sat, 27 Sep 2025 17:29:12 -0400 Subject: [PATCH 156/689] Add --list-profiles command to show CA profiles This commit introduces a new command, `--list-profiles`, to allow users to discover the certificate profiles supported by a Certificate Authority. The command queries the `meta.profiles` object within the ACME directory JSON for the selected server and formats the output for readability. If a CA does not publish profiles in its directory, the command reports that none were found. Usage: acme.sh --list-profiles [--server letsencrypt] --- acme.sh | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/acme.sh b/acme.sh index 23b03039..24ebc5d4 100755 --- a/acme.sh +++ b/acme.sh @@ -5838,6 +5838,49 @@ list() { } +list_profiles() { + _initpath + _initAPI + + _l_server_url="$ACME_DIRECTORY" + _l_server_name="$(_getCAShortName "$_l_server_url")" + _info "Fetching profiles from $_l_server_name ($_l_server_url)..." + + # _initAPI fetches the directory, so we just need to parse its response. + response=$(_get "$_l_server_url" "" 10) + if [ "$?" != "0" ]; then + _err "Failed to connect to CA directory: $_l_server_url" + return 1 + fi + + # Isolate the profiles object using the script's regex tool + profiles_json=$(echo "$response" | _egrep_o '"profiles" *: *\{[^\}]*\}') + + if [ -z "$profiles_json" ]; then + _info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint." + return 0 + fi + + # Strip the outer layer to get the key-value pairs + profiles_kv=$(echo "$profiles_json" | sed 's/"profiles" *: *{//' | sed 's/}$//' | tr ',' '\n') + + printf "\n%-15s %s\n" "name" "info" + printf -- "--------------------------------------------------------------------\n" + + _old_IFS="$IFS" + IFS=' +' + for pair in $profiles_kv; do + # Trim quotes and whitespace + _name=$(echo "$pair" | cut -d: -f1 | tr -d '" \t') + _info_url=$(echo "$pair" | cut -d: -f2- | sed 's/^ *//' | tr -d '"') + printf "%-15s %s\n" "$_name" "$_info_url" + done + IFS="$_old_IFS" + + return 0 +} + _deploy() { _d="$1" _hooks="$2" @@ -7498,6 +7541,9 @@ _process() { --set-default-chain) _CMD="setdefaultchain" ;; + --list-profiles) + _CMD="list_profiles" + ;; -d | --domain) _dvalue="$2" @@ -8063,6 +8109,9 @@ _process() { setdefaultchain) setdefaultchain "$_preferred_chain" ;; + list_profiles) + list_profiles + ;; *) if [ "$_CMD" ]; then _err "Invalid command: $_CMD" From 80748b9fe0a3e9af6f8db7b97fb9bea69e8d7206 Mon Sep 17 00:00:00 2001 From: Steven Zhu Date: Sat, 27 Sep 2025 17:37:37 -0400 Subject: [PATCH 157/689] Quick Patch --- acme.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 24ebc5d4..308db1aa 100755 --- a/acme.sh +++ b/acme.sh @@ -5846,15 +5846,14 @@ list_profiles() { _l_server_name="$(_getCAShortName "$_l_server_url")" _info "Fetching profiles from $_l_server_name ($_l_server_url)..." - # _initAPI fetches the directory, so we just need to parse its response. response=$(_get "$_l_server_url" "" 10) if [ "$?" != "0" ]; then _err "Failed to connect to CA directory: $_l_server_url" return 1 fi - # Isolate the profiles object using the script's regex tool - profiles_json=$(echo "$response" | _egrep_o '"profiles" *: *\{[^\}]*\}') + normalized_response=$(echo "$response" | _normalizeJson) + profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *\{[^\}]*\}') if [ -z "$profiles_json" ]; then _info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint." From 0f5093c0b7555c149ae2d7ab01d548dfd7844b2c Mon Sep 17 00:00:00 2001 From: Steven Zhu Date: Sat, 27 Sep 2025 17:52:44 -0400 Subject: [PATCH 158/689] Remove space --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 308db1aa..e5a0885f 100755 --- a/acme.sh +++ b/acme.sh @@ -7542,7 +7542,7 @@ _process() { ;; --list-profiles) _CMD="list_profiles" - ;; + ;; -d | --domain) _dvalue="$2" @@ -8110,7 +8110,7 @@ _process() { ;; list_profiles) list_profiles - ;; + ;; *) if [ "$_CMD" ]; then _err "Invalid command: $_CMD" From d439933b52a0f251afaaa780acecff85d6eb3c29 Mon Sep 17 00:00:00 2001 From: Steven Zhu Date: Sun, 28 Sep 2025 19:20:08 -0400 Subject: [PATCH 159/689] add Profile column to --list output This commit adds a new "Profile" column to the output of the `--list` command. The column displays the value of the `Le_Certificate_Profile` variable stored in each domain's respective configuration file. If a profile is not set for a certificate, the column is left empty. This enhances the utility of the list command by providing more at-a-glance information about each certificate's configuration, which is particularly useful for CAs that support different certificate profiles. --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index e5a0885f..7caec290 100755 --- a/acme.sh +++ b/acme.sh @@ -5804,7 +5804,7 @@ list() { _sep="|" if [ "$_raw" ]; then if [ -z "$_domain" ]; then - printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}CA${_sep}Created${_sep}Renew" + printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew" fi for di in "${CERT_HOME}"/*.*/; do d=$(basename "$di") @@ -5819,7 +5819,7 @@ list() { . "$DOMAIN_CONF" _ca="$(_getCAShortName "$Le_API")" if [ -z "$_domain" ]; then - printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" + printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_Certificate_Profile${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr" else if [ "$_domain" = "$d" ]; then cat "$DOMAIN_CONF" From 17da49bb782b797209531cb00cfcc8c3ee0370a8 Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Thu, 9 Oct 2025 13:16:28 +0200 Subject: [PATCH 160/689] add keyhelp deploy hook --- deploy/keyhelp.sh | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 deploy/keyhelp.sh diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh new file mode 100644 index 00000000..224a7ea8 --- /dev/null +++ b/deploy/keyhelp.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env sh + +keyhelp_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 +} From f7cc72be354c9cf90fc16e270fa0f7bb01ea1825 Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Thu, 9 Oct 2025 13:28:04 +0200 Subject: [PATCH 161/689] add missing double quotes --- deploy/keyhelp.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh index 224a7ea8..944ca5aa 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -50,7 +50,7 @@ keyhelp_deploy() { for _host in $_hosts; do _key="$(_getfield "$_keys" "$_i" " ")" - _i="$(_math $_i + 1)" + _i="$(_math "$_i" + 1)" export _H1="X-API-Key: $_key" From 3c3ec2c97cde17b1566db80526ceaf504310d002 Mon Sep 17 00:00:00 2001 From: Philipp Klapp Date: Mon, 13 Oct 2025 09:05:59 +0200 Subject: [PATCH 162/689] Script created for Hetzner Cloud --- dnsapi/dns_hetznercloud.sh | 431 +++++++++++++++++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 dnsapi/dns_hetznercloud.sh diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh new file mode 100644 index 00000000..e5b5b0d6 --- /dev/null +++ b/dnsapi/dns_hetznercloud.sh @@ -0,0 +1,431 @@ +#!/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) +Issues: github.com/acmesh-official/acme.sh/issues +' + +HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1" +HETZNERCLOUD_TTL_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) + _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) + _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}" + + 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 + _hetznercloud_zone_id="${zone_id}" + _hetznercloud_zone_name="${zone_name}" + _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | sed 's/\.$//' | _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 +} From 25c564bae1efb7d01388b63752976957aa5dfb6b Mon Sep 17 00:00:00 2001 From: Vmichelin Date: Tue, 14 Oct 2025 10:10:19 +0200 Subject: [PATCH 163/689] fix #6555 : ovh dns api enable to remove record --- dnsapi/dns_ovh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From c4671272c0987d2de0caebafb0216f5e061c9eb7 Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 14 Oct 2025 13:35:15 +0200 Subject: [PATCH 164/689] Yet another push to try the test suite --- dnsapi/dns_efficientip.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index 4a09c5bb..a5f6b09d 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -93,7 +93,6 @@ dns_efficientip_add() { } dns_efficientip_rm() { - fulldomain=$1 txtvalue=$2 From 65bd3d67b41d975019f194bdf3cb9e01680ba771 Mon Sep 17 00:00:00 2001 From: An <18481195+AnTheMaker@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:51:28 +0200 Subject: [PATCH 165/689] nanelo dns: minor log improvements --- dnsapi/dns_nanelo.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh index 23f306a7..0c42989b 100644 --- a/dnsapi/dns_nanelo.sh +++ b/dnsapi/dns_nanelo.sh @@ -59,7 +59,7 @@ dns_nanelo_rm() { fi _saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN" - _debug "First detect the root zone" + _debug "First, let's detect the root zone:" if ! _get_root "$fulldomain"; then _err "invalid domain" return 1 From a2c2b7ffee184d3e8f4c4ccf63685ffdb68232c0 Mon Sep 17 00:00:00 2001 From: DuolaD Date: Sat, 18 Oct 2025 11:41:26 +0800 Subject: [PATCH 166/689] Fixed the issue where Telegram bots would not push notifications. --- notify/telegram.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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\", " From ef76831d37161af4876202f0cde2596956aab9fb Mon Sep 17 00:00:00 2001 From: asavin Date: Sat, 18 Oct 2025 14:16:51 +0200 Subject: [PATCH 167/689] Addressing #pullrequestreview-3353279982 --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index a5f6b09d..f12a2a85 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env sh # shellcheck disable=SC2034 dns_efficientip_info='efficientip.com Site: https://efficientip.com/ From 3cdce86339d8ace8ba62c4ed756138bad669457d Mon Sep 17 00:00:00 2001 From: Jens Spanier Date: Tue, 21 Oct 2025 11:34:46 +0200 Subject: [PATCH 168/689] rename to keyhelp_api --- deploy/{keyhelp.sh => keyhelp_api.sh} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename deploy/{keyhelp.sh => keyhelp_api.sh} (99%) diff --git a/deploy/keyhelp.sh b/deploy/keyhelp_api.sh similarity index 99% rename from deploy/keyhelp.sh rename to deploy/keyhelp_api.sh index 944ca5aa..75e9d951 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp_api.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -keyhelp_deploy() { +keyhelp_api_deploy() { _cdomain="$1" _ckey="$2" _ccert="$3" From 7ca8a9e449a16969b30f5c69420f55b14edec6b0 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 24 Oct 2025 11:34:25 -0400 Subject: [PATCH 169/689] QUIC.cloud support for acme.sh --- dnsapi/dns_qc.sh | 188 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100755 dnsapi/dns_qc.sh diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh new file mode 100755 index 00000000..64956bd5 --- /dev/null +++ b/dnsapi/dns_qc.sh @@ -0,0 +1,188 @@ +#!/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 + _savedomainconf QC_API_KEY "$QC_API_KEY" + else + _err "You didn't specify a QUIC.cloud are api key and email yet." + _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. + _savedomainconf QC_API_EMAIL "$QC_API_EMAIL" + + _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" + _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 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, OK" + return 0 + elif _contains "$response" "Same record already exists"; then + _info "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" + 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 + + response=$(echo "$response"|jq ".result[] | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") + if [ "${response}" = "" ]; then + _info "Don't need to remove." + else + record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + _info "TXT Record ID: $record_id successfully deleted" + fi + +} + +#################### 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 + _debug "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 +} From 48c48cb344d9f748dbc3c0dab1d7b1a62e5de7ba Mon Sep 17 00:00:00 2001 From: Roy Orbitson Date: Mon, 27 Oct 2025 16:02:33 +1030 Subject: [PATCH 170/689] Choose an IP address family for outgoing requests Useful where remote endpoints filter requests by IP address, but one's Internet connection has a stable IP for only one address family, e.g.: a dynamic IPv6 prefix and a static IPv4 address; or a static IPv6 prefix and CGNAT IPv4. --- acme.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/acme.sh b/acme.sh index 7caec290..98b827e8 100755 --- a/acme.sh +++ b/acme.sh @@ -1897,6 +1897,11 @@ _inithttp() { if [ -z "$_ACME_CURL" ] && _exists "curl"; then _ACME_CURL="curl --silent --dump-header $HTTP_HEADER " + if [ "$ACME_USE_IPV6_REQUESTS" ]; then + _ACME_CURL="$_ACME_CURL --ipv6 " + elif [ "$ACME_USE_IPV4_REQUESTS" ]; then + _ACME_CURL="$_ACME_CURL --ipv4 " + fi if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_CURL="$_ACME_CURL -L " fi @@ -1924,6 +1929,11 @@ _inithttp() { if [ -z "$_ACME_WGET" ] && _exists "wget"; then _ACME_WGET="wget -q" + if [ "$ACME_USE_IPV6_REQUESTS" ]; then + _ACME_WGET="$_ACME_WGET --inet6-only " + elif [ "$ACME_USE_IPV4_REQUESTS" ]; then + _ACME_WGET="$_ACME_WGET --inet4-only " + fi if [ "$ACME_HTTP_NO_REDIRECTS" ]; then _ACME_WGET="$_ACME_WGET --max-redirect 0 " fi @@ -7076,6 +7086,8 @@ Parameters: --alpn Use standalone alpn mode. --stateless Use stateless mode. See: $_STATELESS_WIKI + --request-v4 Force client requests to use ipv4. + --request-v6 Force client requests to use ipv6. --apache Use Apache mode. --dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted. @@ -7255,6 +7267,20 @@ _processAccountConf() { _saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET" fi + if [ "$_request_v6" ]; then + _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6" + _clearaccountconf "ACME_USE_IPV4_REQUESTS" + elif [ "$ACME_USE_IPV6_REQUESTS" ]; then + _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS" + _clearaccountconf "ACME_USE_IPV4_REQUESTS" + elif [ "$_request_v4" ]; then + _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4" + _clearaccountconf "ACME_USE_IPV6_REQUESTS" + elif [ "$ACME_USE_IPV4_REQUESTS" ]; then + _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS" + _clearaccountconf "ACME_USE_IPV6_REQUESTS" + fi + } _checkSudo() { @@ -7420,6 +7446,8 @@ _process() { _local_address="" _log_level="" _auto_upgrade="" + _request_v4="" + _request_v6="" _listen_v4="" _listen_v6="" _openssl_bin="" @@ -7885,6 +7913,18 @@ _process() { fi AUTO_UPGRADE="$_auto_upgrade" ;; + --request-v4) + _request_v4="1" + ACME_USE_IPV4_REQUESTS="1" + _request_v6="" + ACME_USE_IPV6_REQUESTS="" + ;; + --request-v6) + _request_v6="1" + ACME_USE_IPV6_REQUESTS="1" + _request_v4="" + ACME_USE_IPV4_REQUESTS="" + ;; --listen-v4) _listen_v4="1" Le_Listen_V4="$_listen_v4" From e25e30dcdd5e20ab3ff27f9fb1abe1027090e103 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 27 Oct 2025 12:00:01 -0400 Subject: [PATCH 171/689] Added wiki doc --- wiki/dnsapi/dns_qc | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 wiki/dnsapi/dns_qc diff --git a/wiki/dnsapi/dns_qc b/wiki/dnsapi/dns_qc new file mode 100644 index 00000000..50b1879e --- /dev/null +++ b/wiki/dnsapi/dns_qc @@ -0,0 +1,27 @@ +# Use QUIC.cloud DNS API + +This uses the QUIC.cloud DNS API. + +## Obtain an API key from the QUIC.cloude system. If you do not already have one, once logged into QUIC.cloud: + +- Select the Human icon at the top right of the screen and select **Edit Profile** +- On the left side of the screen, press the **API Access** item. +- Press the **Generate Key** button. It will present you with a token you will need below. + +## Use the API + +You will need to provide 2 environment variables to acme.sh: + +- **QC_API_KEY**: This is the API Token value obtained in the QUIC.cloud screens. +- **QC_API_EMAIL**: This is the email you used in your QUIC.cloud configuration + +## Using in OpenLiteSpeed + +This feature is fully supported and documented in version 1.9 and later of OpenLiteSpeed. See the OpenLiteSpeed (documentation)[https://docs.openlitespeed.org/config/advanced/acme/). + +## License + +Copyright: acme.sh wiki contributors + +License: GNU General Public License version 3 or any later version + From 68eb6defd356a62dd0b6c8255846f2851e667da3 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 27 Oct 2025 12:04:08 -0400 Subject: [PATCH 172/689] Removed false wiki page --- wiki/dnsapi/dns_qc | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 wiki/dnsapi/dns_qc diff --git a/wiki/dnsapi/dns_qc b/wiki/dnsapi/dns_qc deleted file mode 100644 index 50b1879e..00000000 --- a/wiki/dnsapi/dns_qc +++ /dev/null @@ -1,27 +0,0 @@ -# Use QUIC.cloud DNS API - -This uses the QUIC.cloud DNS API. - -## Obtain an API key from the QUIC.cloude system. If you do not already have one, once logged into QUIC.cloud: - -- Select the Human icon at the top right of the screen and select **Edit Profile** -- On the left side of the screen, press the **API Access** item. -- Press the **Generate Key** button. It will present you with a token you will need below. - -## Use the API - -You will need to provide 2 environment variables to acme.sh: - -- **QC_API_KEY**: This is the API Token value obtained in the QUIC.cloud screens. -- **QC_API_EMAIL**: This is the email you used in your QUIC.cloud configuration - -## Using in OpenLiteSpeed - -This feature is fully supported and documented in version 1.9 and later of OpenLiteSpeed. See the OpenLiteSpeed (documentation)[https://docs.openlitespeed.org/config/advanced/acme/). - -## License - -Copyright: acme.sh wiki contributors - -License: GNU General Public License version 3 or any later version - From 7c5b9a5b922e5bed34f64fda6b58b257820955b9 Mon Sep 17 00:00:00 2001 From: Dennis Schmidt Date: Thu, 30 Oct 2025 09:17:13 +0000 Subject: [PATCH 173/689] Add priority, tags and title to ntfy notification Make the ntfy.sh notifications easier to distinguish at a first glance. --- notify/ntfy.sh | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/notify/ntfy.sh b/notify/ntfy.sh index 21e39559..ecb65879 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 From 3d21ac4525b2b60bd5efd1b9d23e9822a8be0ded Mon Sep 17 00:00:00 2001 From: Dennis Schmidt Date: Fri, 31 Oct 2025 08:40:34 +0000 Subject: [PATCH 174/689] CS Make shfmt happy --- notify/ntfy.sh | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/notify/ntfy.sh b/notify/ntfy.sh index ecb65879..3a788a84 100644 --- a/notify/ntfy.sh +++ b/notify/ntfy.sh @@ -38,18 +38,18 @@ ntfy_send() { fi case "$_statusCode" in - 0) - _priority="$_priority_default" - _tag="$_tag_success" - ;; - 1) - _priority="$_priority_error" - _tag="$_tag_error" - ;; - 2) - _priority="$_priority_default" - _tag="$_tag_info" - ;; + 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" From b65f432ee02b1ad45224e97cf89e5144680b9d41 Mon Sep 17 00:00:00 2001 From: Philipp Klapp Date: Sat, 1 Nov 2025 13:44:11 +0100 Subject: [PATCH 175/689] Normalize Hetzner zone names to punycode --- dnsapi/dns_hetznercloud.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh index e5b5b0d6..e53ac23a 100644 --- a/dnsapi/dns_hetznercloud.sh +++ b/dnsapi/dns_hetznercloud.sh @@ -290,9 +290,15 @@ _hetznercloud_parse_zone_fields() { 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}" | sed 's/\.$//' | _lower_case) + _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case) return 0 } From 5f8146050464c9f0c6ae69160e672ad3bce388b3 Mon Sep 17 00:00:00 2001 From: Philipp Klapp Date: Sun, 2 Nov 2025 04:07:58 +0100 Subject: [PATCH 176/689] Wait for Hetzner Cloud DNS actions to complete before returning --- dnsapi/dns_hetznercloud.sh | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh index e53ac23a..4a7eea90 100644 --- a/dnsapi/dns_hetznercloud.sh +++ b/dnsapi/dns_hetznercloud.sh @@ -8,11 +8,13 @@ Options: 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 ##################### @@ -57,6 +59,9 @@ dns_hetznercloud_add() { 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 ;; @@ -116,6 +121,9 @@ dns_hetznercloud_rm() { 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 ;; @@ -171,6 +179,17 @@ _hetznercloud_init() { 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 } @@ -435,3 +454,140 @@ _hetznercloud_api() { 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 +} From 693b1f7a74a52ed3a1499f64b9ebfe2bfc70f0ad Mon Sep 17 00:00:00 2001 From: Richard Glidden Date: Sun, 2 Nov 2025 22:50:55 -0500 Subject: [PATCH 177/689] Fix TrueNAS deploy fails on TrueNAS 25.10 --- deploy/truenas_ws.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From b7c8601540d1296e5409387f171c1898620b8c38 Mon Sep 17 00:00:00 2001 From: Peter Lindegaard Hansen Date: Mon, 3 Nov 2025 16:18:15 +0100 Subject: [PATCH 178/689] Update dns_curanet.sh --- dnsapi/dns_curanet.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh index f57afa1f..42bc28f2 100644 --- a/dnsapi/dns_curanet.sh +++ b/dnsapi/dns_curanet.sh @@ -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 From d187b982eb922c4ba09ec8085655a760794f7edf Mon Sep 17 00:00:00 2001 From: Peter Lindegaard Hansen Date: Mon, 3 Nov 2025 18:14:27 +0100 Subject: [PATCH 179/689] Update dns_curanet.sh --- dnsapi/dns_curanet.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh index 42bc28f2..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() { From 0fa53d62cbfcea7ae5626f3d90bd66e4bef2c731 Mon Sep 17 00:00:00 2001 From: seagleNet Date: Tue, 4 Nov 2025 09:35:47 +0100 Subject: [PATCH 180/689] feat: Add notify plugin for opsgenie --- notify/opsgenie.sh | 130 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 notify/opsgenie.sh 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 +} From c5f41479a909f1a7cad58b74dcaedc880c7fc495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20V=C3=A1mos?= Date: Fri, 7 Nov 2025 16:16:30 +0100 Subject: [PATCH 181/689] Bump Alpine version from 3.21 to 3.22 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7523f0af..d8f8b265 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.22 RUN apk --no-cache add -f \ openssl \ From 59a286b0b76b088915455ead9e9015037d5fd579 Mon Sep 17 00:00:00 2001 From: privacyfr3ak <220089342+privacyfr3ak@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:59:10 -0500 Subject: [PATCH 182/689] disable shellcheck --- deploy/unifi.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deploy/unifi.sh b/deploy/unifi.sh index 2af46b4a..1d13e04f 100644 --- a/deploy/unifi.sh +++ b/deploy/unifi.sh @@ -143,7 +143,9 @@ unifi_deploy() { # correct file ownership according to the directory, the keystore is placed in _unifi_keystore_dir=$(dirname "${_unifi_keystore}") + # 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}" From 839d611f642ee5e739ecb6006e0347d2067d2ccf Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 9 Nov 2025 18:50:09 +0100 Subject: [PATCH 183/689] use ghcr.io/letsencrypt/pebble:latest --- .github/workflows/PebbleStrict.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index b0326332..af6aab4f 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -65,7 +65,7 @@ jobs: run: | docker run --rm -itd --name=pebble \ -e PEBBLE_VA_ALWAYS_VALID=1 \ - -p 14000:14000 -p 15000:15000 letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict + -p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest From 9a994e7f36532169da970dcada70b537bfdb6128 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 9 Nov 2025 19:03:30 +0100 Subject: [PATCH 184/689] fix --- .github/workflows/PebbleStrict.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index af6aab4f..729874ce 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -65,7 +65,7 @@ jobs: run: | docker run --rm -itd --name=pebble \ -e PEBBLE_VA_ALWAYS_VALID=1 \ - -p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict + -p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest -config /test/config/pebble-config.json -strict - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest From 57f8221bab028429b0a699464d7f4bf40c61701b Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 9 Nov 2025 19:58:25 +0100 Subject: [PATCH 185/689] fix --request-v4/6 https://github.com/acmesh-official/acme.sh/pull/6582 --- acme.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/acme.sh b/acme.sh index 98b827e8..8ac9b366 100755 --- a/acme.sh +++ b/acme.sh @@ -7086,8 +7086,6 @@ Parameters: --alpn Use standalone alpn mode. --stateless Use stateless mode. See: $_STATELESS_WIKI - --request-v4 Force client requests to use ipv4. - --request-v6 Force client requests to use ipv6. --apache Use Apache mode. --dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted. @@ -7149,6 +7147,8 @@ Parameters: --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted. --listen-v4 Force standalone/tls server to listen at ipv4. --listen-v6 Force standalone/tls server to listen at ipv6. + --request-v4 Force client requests to use ipv4 to connect to the CA server. + --request-v6 Force client requests to use ipv6 to connect to the CA server. --openssl-bin Specifies a custom openssl bin location. --use-wget Force to use wget, if you have both curl and wget installed. --yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode. @@ -7270,15 +7270,19 @@ _processAccountConf() { if [ "$_request_v6" ]; then _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6" _clearaccountconf "ACME_USE_IPV4_REQUESTS" - elif [ "$ACME_USE_IPV6_REQUESTS" ]; then - _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS" - _clearaccountconf "ACME_USE_IPV4_REQUESTS" + ACME_USE_IPV4_REQUESTS= elif [ "$_request_v4" ]; then _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4" _clearaccountconf "ACME_USE_IPV6_REQUESTS" + ACME_USE_IPV6_REQUESTS= + elif [ "$ACME_USE_IPV6_REQUESTS" ]; then + _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS" + _clearaccountconf "ACME_USE_IPV4_REQUESTS" + ACME_USE_IPV4_REQUESTS= elif [ "$ACME_USE_IPV4_REQUESTS" ]; then _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS" _clearaccountconf "ACME_USE_IPV6_REQUESTS" + ACME_USE_IPV6_REQUESTS= fi } From 4a7f35dea7f8a91545976245e89730bc3f3ffeb1 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 15 Nov 2025 11:12:00 +0100 Subject: [PATCH 186/689] remove clientauth https://github.com/acmesh-official/acme.sh/issues/6610 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 8ac9b366..95337cc8 100755 --- a/acme.sh +++ b/acme.sh @@ -1271,7 +1271,7 @@ _createcsr() { _savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse" printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf" else - printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf" + printf "\nextendedKeyUsage=serverAuth\n" >>"$csrconf" fi if [ "$acmeValidationv1" ]; then From 66bad853aec3778303feec693bf9194644e27f36 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 15 Nov 2025 11:27:04 +0100 Subject: [PATCH 187/689] remove ClearLinux --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f7038f59..e6a8966a 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,6 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) |18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux |19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia |10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux -|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux |22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 |23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) |24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) From a6ff1d69248868531daf0390e5747f50cdefd735 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 16 Nov 2025 09:30:41 +0100 Subject: [PATCH 188/689] add logo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e6a8966a..6953cc71 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![zerossl.com](https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05)](https://zerossl.com/?fromacme.sh) + # An ACME Shell script: acme.sh [![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml) From 0d1f9edf3fb57b901da0c2f21cd593b4f683e2ba Mon Sep 17 00:00:00 2001 From: Joe Bauser Date: Mon, 17 Nov 2025 15:24:40 -0500 Subject: [PATCH 189/689] README.md clarify keylength arg and ECC default Reorder and reword small portions of the keylength documentation and make the ECC cert default explicitly stated in part 2 to avoid confusion. Fixes #6590 --- README.md | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6953cc71..05656044 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,8 @@ The certs will be placed in `~/.acme.sh/example.com/` The certs will be renewed automatically every **60** days. +The certs will default to ECC certificates. + More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert @@ -359,27 +361,11 @@ Ok, it's done. **Please use dns api mode instead.** -# 10. Issue ECC certificates +# 10. Issue certificates of different key types and lengths (ECC or RSA) -Just set the `keylength` parameter with a prefix `ec-`. +Just set the `keylength` to a valid, supported, value. -For example: - -### Single domain ECC certificate - -```bash -acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256 -``` - -### SAN multi domain ECC certificate - -```bash -acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength ec-256 -``` - -Please look at the `keylength` parameter above. - -Valid values are: +Valid values for the `keylength` parameter are: 1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)** 2. **ec-384 (secp384r1, "ECDSA P-384")** @@ -388,6 +374,19 @@ Valid values are: 5. **3072 (RSA3072)** 6. **4096 (RSA4096)** +For example: + +### Single domain with ECDSA P-384 certificate + +```bash +acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384 +``` + +### SAN multi domain with RSA4096 certificate + +```bash +acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096 +``` # 11. Issue Wildcard certificates From 6715320e78ea1baa4ec1437166f73215b3fbc2a2 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 18 Nov 2025 21:41:43 +0100 Subject: [PATCH 190/689] fix https://github.com/acmesh-official/acme.sh/issues/6610 https://github.com/acmesh-official/acme.sh/issues/6617#issuecomment-3546341480 --- acme.sh | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/acme.sh b/acme.sh index 95337cc8..00d2d2d5 100755 --- a/acme.sh +++ b/acme.sh @@ -1250,7 +1250,7 @@ _idn() { fi } -#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 +#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage _createcsr() { _debug _createcsr domain="$1" @@ -1259,6 +1259,7 @@ _createcsr() { csr="$4" csrconf="$5" acmeValidationv1="$6" + extusage="$7" _debug2 domain "$domain" _debug2 domainlist "$domainlist" _debug2 csrkey "$csrkey" @@ -1267,11 +1268,10 @@ _createcsr() { printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf" - if [ "$Le_ExtKeyUse" ]; then - _savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse" - printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf" + if [ "$extusage" ]; then + printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf" else - printf "\nextendedKeyUsage=serverAuth\n" >>"$csrconf" + printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf" fi if [ "$acmeValidationv1" ]; then @@ -4445,6 +4445,7 @@ issue() { _valid_from="${16}" _valid_to="${17}" _certificate_profile="${18}" + _extended_key_usage="${19}" if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" @@ -4589,12 +4590,25 @@ issue() { return 1 fi fi - if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then + _keyusage="$_extended_key_usage" + if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then + if [ -z "$_keyusage" ]; then + #https://github.com/acmesh-official/acme.sh/issues/6610 + #google accepts serverauth only + _keyusage="serverAuth" + fi + fi + if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then _err "Error creating CSR." _clearup _on_issue_err "$_post_hook" return 1 fi + if [ "$_extended_key_usage" ]; then + _savedomainconf "Le_ExtKeyUse" "$_extended_key_usage" + else + _cleardomainconf "Le_ExtKeyUse" + fi fi _savedomainconf "Le_Keylength" "$_key_length" @@ -5553,7 +5567,7 @@ renew() { _cleardomainconf Le_OCSP_Staple fi fi - issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse" res="$?" if [ "$res" != "0" ]; then return "$res" @@ -7469,6 +7483,7 @@ _process() { _valid_from="" _valid_to="" _certificate_profile="" + _extended_key_usage="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7864,7 +7879,7 @@ _process() { shift ;; --extended-key-usage) - Le_ExtKeyUse="$2" + _extended_key_usage="$2" shift ;; --ocsp-must-staple | --ocsp) @@ -8081,7 +8096,7 @@ _process() { uninstall) uninstall "$_nocron" ;; upgrade) upgrade ;; issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage" ;; deploy) deploy "$_domain" "$_deploy_hook" "$_ecc" From 9a74c86327bd4731f1bfc3494df2b8a911548555 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Wed, 19 Nov 2025 09:06:55 -0500 Subject: [PATCH 191/689] Commit to force initial test --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 64956bd5..aa4ff891 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -141,7 +141,7 @@ _get_root() { fi if ! _qc_rest GET "zones"; then - _debug "qc_rest failed" + _err "qc_rest failed" return 1 fi From c950b67e4b8e87bd72a6ea762a704883633095c3 Mon Sep 17 00:00:00 2001 From: Mason <36799194+WongIong@users.noreply.github.com> Date: Wed, 19 Nov 2025 23:27:45 +0800 Subject: [PATCH 192/689] Adapting to Cloudflare's new response --- dnsapi/dns_cf.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 736742f3..b2749972 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 From a9f96bf7097f37fe52b1a79b9b41582f6ff37632 Mon Sep 17 00:00:00 2001 From: Mason <36799194+WongIong@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:29:44 +0800 Subject: [PATCH 193/689] Reformat using shfmt --- dnsapi/dns_cf.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index b2749972..7b383c43 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -92,9 +92,9 @@ dns_cf_add() { if _contains "$response" "$txtvalue"; then _info "Added, OK" return 0 - elif _contains "$response" "The record already exists" \ - || _contains "$response" "An identical record already exists." \ - || _contains "$response" '"code":81058'; 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 From 3d3053f4277b0218faae29a3adf19b13d32709af Mon Sep 17 00:00:00 2001 From: Antoni Company Date: Thu, 20 Nov 2025 10:06:37 +0000 Subject: [PATCH 194/689] feat: Add custom filename for panos --- deploy/panos.sh | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index a9232e79..8f911fba 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_FILENAME="" # use a custom filename 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_filename" 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_filename" 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_filename.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 @@ -168,7 +169,6 @@ deployer() { # This is the main function that will call the other functions to deploy everything. panos_deploy() { - _cdomain=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename _ckey="$2" _cfullchain="$5" @@ -242,6 +242,15 @@ panos_deploy() { _getdeployconf PANOS_VSYS fi + # PANOS_FILENAME + if [ "$PANOS_FILENAME" ]; then + _debug "Detected ENV variable PANOS_FILENAME. Saving to file." + _savedeployconf PANOS_FILENAME "$PANOS_FILENAME" 1 + else + _debug "Attempting to load variable PANOS_FILENAME from file." + _getdeployconf PANOS_FILENAME + fi + #Store variables _panos_host=$PANOS_HOST _panos_user=$PANOS_USER @@ -249,6 +258,7 @@ panos_deploy() { _panos_template=$PANOS_TEMPLATE _panos_template_stack=$PANOS_TEMPLATE_STACK _panos_vsys=$PANOS_VSYS + _panos_filename=$PANOS_FILENAME #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 +277,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 filename based on the first domain on the certificate if no custom filename is set + if [ -z "$_panos_filename" ]; then + _panos_filename=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename + _savedeployconf PANOS_FILENAME "$_panos_filename" 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 ****" From 9381835a7c48e432b440d0dab24ed5aba683ee35 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 24 Oct 2025 11:34:25 -0400 Subject: [PATCH 195/689] QUIC.cloud support for acme.sh --- dnsapi/dns_qc.sh | 188 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100755 dnsapi/dns_qc.sh diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh new file mode 100755 index 00000000..64956bd5 --- /dev/null +++ b/dnsapi/dns_qc.sh @@ -0,0 +1,188 @@ +#!/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 + _savedomainconf QC_API_KEY "$QC_API_KEY" + else + _err "You didn't specify a QUIC.cloud are api key and email yet." + _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. + _savedomainconf QC_API_EMAIL "$QC_API_EMAIL" + + _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" + _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 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, OK" + return 0 + elif _contains "$response" "Same record already exists"; then + _info "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" + 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 + + response=$(echo "$response"|jq ".result[] | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") + if [ "${response}" = "" ]; then + _info "Don't need to remove." + else + record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + _info "TXT Record ID: $record_id successfully deleted" + fi + +} + +#################### 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 + _debug "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 +} From 72a6a5ce047d6376744bc9a7813f58fbdbf036fc Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 27 Oct 2025 12:00:01 -0400 Subject: [PATCH 196/689] Added wiki doc --- wiki/dnsapi/dns_qc | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 wiki/dnsapi/dns_qc diff --git a/wiki/dnsapi/dns_qc b/wiki/dnsapi/dns_qc new file mode 100644 index 00000000..50b1879e --- /dev/null +++ b/wiki/dnsapi/dns_qc @@ -0,0 +1,27 @@ +# Use QUIC.cloud DNS API + +This uses the QUIC.cloud DNS API. + +## Obtain an API key from the QUIC.cloude system. If you do not already have one, once logged into QUIC.cloud: + +- Select the Human icon at the top right of the screen and select **Edit Profile** +- On the left side of the screen, press the **API Access** item. +- Press the **Generate Key** button. It will present you with a token you will need below. + +## Use the API + +You will need to provide 2 environment variables to acme.sh: + +- **QC_API_KEY**: This is the API Token value obtained in the QUIC.cloud screens. +- **QC_API_EMAIL**: This is the email you used in your QUIC.cloud configuration + +## Using in OpenLiteSpeed + +This feature is fully supported and documented in version 1.9 and later of OpenLiteSpeed. See the OpenLiteSpeed (documentation)[https://docs.openlitespeed.org/config/advanced/acme/). + +## License + +Copyright: acme.sh wiki contributors + +License: GNU General Public License version 3 or any later version + From cf5fd403e85c06c1c5ec4110e8112b50308b1fa2 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 27 Oct 2025 12:04:08 -0400 Subject: [PATCH 197/689] Removed false wiki page --- wiki/dnsapi/dns_qc | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 wiki/dnsapi/dns_qc diff --git a/wiki/dnsapi/dns_qc b/wiki/dnsapi/dns_qc deleted file mode 100644 index 50b1879e..00000000 --- a/wiki/dnsapi/dns_qc +++ /dev/null @@ -1,27 +0,0 @@ -# Use QUIC.cloud DNS API - -This uses the QUIC.cloud DNS API. - -## Obtain an API key from the QUIC.cloude system. If you do not already have one, once logged into QUIC.cloud: - -- Select the Human icon at the top right of the screen and select **Edit Profile** -- On the left side of the screen, press the **API Access** item. -- Press the **Generate Key** button. It will present you with a token you will need below. - -## Use the API - -You will need to provide 2 environment variables to acme.sh: - -- **QC_API_KEY**: This is the API Token value obtained in the QUIC.cloud screens. -- **QC_API_EMAIL**: This is the email you used in your QUIC.cloud configuration - -## Using in OpenLiteSpeed - -This feature is fully supported and documented in version 1.9 and later of OpenLiteSpeed. See the OpenLiteSpeed (documentation)[https://docs.openlitespeed.org/config/advanced/acme/). - -## License - -Copyright: acme.sh wiki contributors - -License: GNU General Public License version 3 or any later version - From d0d97a40a62c85efcfdb96c9affd682847889c1f Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Wed, 19 Nov 2025 09:06:55 -0500 Subject: [PATCH 198/689] Commit to force initial test --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 64956bd5..aa4ff891 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -141,7 +141,7 @@ _get_root() { fi if ! _qc_rest GET "zones"; then - _debug "qc_rest failed" + _err "qc_rest failed" return 1 fi From d2539c3f1aefb9cf0d7d29f488fb37f28b690188 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 20 Nov 2025 21:18:40 +0100 Subject: [PATCH 199/689] fix https://github.com/acmesh-official/acme.sh/issues/6402 --- acme.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/acme.sh b/acme.sh index 00d2d2d5..6578d414 100755 --- a/acme.sh +++ b/acme.sh @@ -5242,6 +5242,16 @@ $_authorizations_map" return 1 fi break + elif _contains "$response" "\"ready\""; then + _info "Order status is 'ready', let's sleep and retry." + _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') + _debug "_retryafter" "$_retryafter" + if [ "$_retryafter" ]; then + _info "Sleeping for $_retryafter seconds then retrying" + _sleep $_retryafter + else + _sleep 2 + fi elif _contains "$response" "\"processing\""; then _info "Order status is 'processing', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') From b500ac3dbbd20d70de487636c2486375aade488a Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Thu, 20 Nov 2025 15:38:56 -0500 Subject: [PATCH 200/689] Updated secret and dns_qc.sh --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index aa4ff891..1eeef97f 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -107,7 +107,7 @@ dns_qc_rm() { if [ "${response}" = "" ]; then _info "Don't need to remove." else - record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') + record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') _debug "record_id" "$record_id" if [ -z "$record_id" ]; then _err "Can not get record id to remove." From 0f42b06b48b488fc3b90fd40671673acd1066735 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 21 Nov 2025 08:02:59 -0500 Subject: [PATCH 201/689] Trying again to fix shfmt error --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 1eeef97f..66444aa6 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -103,7 +103,7 @@ dns_qc_rm() { return 1 fi - response=$(echo "$response"|jq ".result[] | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") + response=$(echo "$response"|jq ".result[]" | select(.content == \"$txtvalue\") | select(.type == \"TXT\")) if [ "${response}" = "" ]; then _info "Don't need to remove." else From 90d2ff8fad85f4bd92aafabaf24928435421c455 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 21 Nov 2025 15:27:38 -0500 Subject: [PATCH 202/689] Better fixes for shfmt errors --- dnsapi/dns_qc.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 66444aa6..0ec53864 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -59,13 +59,13 @@ dns_qc_add() { # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) # _debug count "$count" # if [ "$count" = "0" ]; then - _info "Adding record" + _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, OK" + _info "Added txt record, OK" return 0 elif _contains "$response" "Same record already exists"; then - _info "Already exists, OK" + _info "txt record already exists, OK" return 0 else _err "Add txt record error: $response" @@ -103,18 +103,19 @@ dns_qc_rm() { return 1 fi - response=$(echo "$response"|jq ".result[]" | select(.content == \"$txtvalue\") | select(.type == \"TXT\")) + 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." + _info "Don't need to remove txt records." else record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') - _debug "record_id" "$record_id" + _debug "txt record_id" "$record_id" if [ -z "$record_id" ]; then - _err "Can not get record id to remove." + _err "Can not get txt record id to remove." return 1 fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then - _err "Delete record error." + _err "Delete txt record error." return 1 fi _info "TXT Record ID: $record_id successfully deleted" From 5e76ea820ca42f76055e57de5769325a4c34531b Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 21 Nov 2025 16:01:14 -0500 Subject: [PATCH 203/689] Additional shfmt issues --- dnsapi/dns_qc.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 0ec53864..ab02f17e 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -104,18 +104,18 @@ dns_qc_rm() { fi response=$(echo "$response"|jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") - _debug get txt response "$response" + _debug "get txt response" "$response" if [ "${response}" = "" ]; then _info "Don't need to remove txt records." else record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') _debug "txt record_id" "$record_id" if [ -z "$record_id" ]; then - _err "Can not get txt record id to remove." + _info "Can not get txt record id to remove." return 1 fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then - _err "Delete txt record error." + _info "Delete txt record error." return 1 fi _info "TXT Record ID: $record_id successfully deleted" From ded539b11c365359082c3801fb2057593cae535f Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 21 Nov 2025 16:04:23 -0500 Subject: [PATCH 204/689] Additional shfmt issues --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index ab02f17e..723bbea8 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -112,7 +112,7 @@ dns_qc_rm() { _debug "txt record_id" "$record_id" if [ -z "$record_id" ]; then _info "Can not get txt record id to remove." - return 1 + return 0 fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then _info "Delete txt record error." From 20ef8cd369f712d08bbb094f1dd95809065fcd6d Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 21 Nov 2025 16:22:42 -0500 Subject: [PATCH 205/689] Additional shfmt issues --- dnsapi/dns_qc.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 723bbea8..ef246a2b 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -111,7 +111,7 @@ dns_qc_rm() { record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') _debug "txt record_id" "$record_id" if [ -z "$record_id" ]; then - _info "Can not get txt record id to remove." + #_info "Can not get txt record id to remove." return 0 fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then @@ -120,7 +120,7 @@ dns_qc_rm() { fi _info "TXT Record ID: $record_id successfully deleted" fi - + return 0 } #################### Private functions below ################################## From 88e9681481b3694b2fab4cf7663306eca8c6e386 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 21 Nov 2025 16:26:24 -0500 Subject: [PATCH 206/689] Additional shfmt issues --- dnsapi/dns_qc.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index ef246a2b..c30d4595 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -110,10 +110,6 @@ dns_qc_rm() { else record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') _debug "txt record_id" "$record_id" - if [ -z "$record_id" ]; then - #_info "Can not get txt record id to remove." - return 0 - fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then _info "Delete txt record error." return 1 From 46a2608783ea647042ea2a6d06c1c0e37bd91f05 Mon Sep 17 00:00:00 2001 From: Antoni Company Date: Sat, 22 Nov 2025 09:22:32 +0000 Subject: [PATCH 207/689] fix: Renamed filaname to certname - Changed filename to certname to better reflect the actual issue at hand. - Restored _cdomain variable to its original place for clarity. --- deploy/panos.sh | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index 8f911fba..c54d21fe 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -16,7 +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_FILENAME="" # use a custom filename to work around Panorama's 31-character limit +# 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. @@ -90,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$_panos_filename" + 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")" @@ -104,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$_panos_filename" + 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 "$_panos_filename.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 @@ -169,6 +169,7 @@ deployer() { # This is the main function that will call the other functions to deploy everything. panos_deploy() { + _cdomain=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename _ckey="$2" _cfullchain="$5" @@ -242,13 +243,13 @@ panos_deploy() { _getdeployconf PANOS_VSYS fi - # PANOS_FILENAME - if [ "$PANOS_FILENAME" ]; then - _debug "Detected ENV variable PANOS_FILENAME. Saving to file." - _savedeployconf PANOS_FILENAME "$PANOS_FILENAME" 1 + # 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_FILENAME from file." - _getdeployconf PANOS_FILENAME + _debug "Attempting to load variable PANOS_CERTNAME from file." + _getdeployconf PANOS_CERTNAME fi #Store variables @@ -258,7 +259,7 @@ panos_deploy() { _panos_template=$PANOS_TEMPLATE _panos_template_stack=$PANOS_TEMPLATE_STACK _panos_vsys=$PANOS_VSYS - _panos_filename=$PANOS_FILENAME + _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 @@ -277,10 +278,10 @@ 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 filename based on the first domain on the certificate if no custom filename is set - if [ -z "$_panos_filename" ]; then - _panos_filename=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename - _savedeployconf PANOS_FILENAME "$_panos_filename" 1 + # 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 From 9b30bd5a0356c83ad5c02d3040ae8daee4dbdbea Mon Sep 17 00:00:00 2001 From: ZeroSSL-Andreas Date: Tue, 25 Nov 2025 14:41:31 +0100 Subject: [PATCH 208/689] Update README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 05656044..4afd90a8 100644 --- a/README.md +++ b/README.md @@ -523,3 +523,20 @@ Your donation makes **acme.sh** better: 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) + +# 21. About this repository + +> [!NOTE] +> This repository is officially maintained by ZeroSSL as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community! +> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com. +> +> All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL. +

+ + + + + ZeroSSL + + +

From 75ee17aeeb9560cf45b0193efa48f4f46bcdbaab Mon Sep 17 00:00:00 2001 From: asavin Date: Tue, 25 Nov 2025 14:47:26 +0100 Subject: [PATCH 209/689] Remove unecessary base64 encoding --- dnsapi/dns_efficientip.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh index f12a2a85..a485849a 100755 --- a/dnsapi/dns_efficientip.sh +++ b/dnsapi/dns_efficientip.sh @@ -121,7 +121,7 @@ dns_efficientip_rm() { 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" | _base64) + EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig") export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}" export _H3="X-SDS-TS: $TS" fi From 705fbcd570dfec12b9851cdd9b047020c60e5185 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 27 Nov 2025 22:13:18 +0100 Subject: [PATCH 210/689] fix https://github.com/acmesh-official/acme.sh/issues/6124#issuecomment-3586650156 --- Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8f8b265..88edc4a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,8 @@ RUN apk --no-cache add -f \ jq \ cronie +ENV LE_WORKING_DIR=/acmebin + ENV LE_CONFIG_HOME=/acme.sh ARG AUTO_UPGRADE=1 @@ -30,7 +32,7 @@ COPY ./notify /install_acme.sh/notify RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - +RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - RUN for verb in help \ version \ @@ -64,7 +66,7 @@ RUN for verb in help \ set-default-ca \ set-default-chain \ ; do \ - printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \ + printf -- "%b" "#!/usr/bin/env sh\n$LE_WORKING_DIR/acme.sh --${verb} --config-home $LE_CONFIG_HOME \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \ ; done RUN printf "%b" '#!'"/usr/bin/env sh\n \ @@ -72,7 +74,7 @@ if [ \"\$1\" = \"daemon\" ]; then \n \ exec crond -n -s -m off \n \ else \n \ exec -- \"\$@\"\n \ -fi\n" >/entry.sh && chmod +x /entry.sh +fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME VOLUME /acme.sh From c5566eafebaa04edd30074058e2eefba5b5bfc1e Mon Sep 17 00:00:00 2001 From: SunMar Date: Fri, 28 Nov 2025 09:44:50 +0100 Subject: [PATCH 211/689] fix "dns_aws.sh: line 164: _error: command not found" #6443 --- dnsapi/dns_aws.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ac0df6bc885db5f67e2ccebecfacbedd011974f4 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 29 Nov 2025 16:36:14 +0100 Subject: [PATCH 212/689] start 3.1.3 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 6578d414..da67fa14 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.2 +VER=3.1.3 PROJECT_NAME="acme.sh" From 5c6d8aacbeeb4063822064c27bdaf4a144975cb7 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 29 Nov 2025 22:38:02 +0100 Subject: [PATCH 213/689] Add files via upload --- dnsapi/dns_infoblox_uddi.sh | 220 ++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 dnsapi/dns_infoblox_uddi.sh diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh new file mode 100644 index 00000000..545ce41d --- /dev/null +++ b/dnsapi/dns_infoblox_uddi.sh @@ -0,0 +1,220 @@ +#!/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 +' + +######## 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" + + zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" + _debug "Fetching zones from: $zone_url" + zone_result="$(_get "$zone_url")" + _debug2 "zone_result: $zone_result" + + if [ "$?" != "0" ]; then + _err "Error fetching zones from Infoblox API" + return 1 + fi + + fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') + _debug "Looking for zone matching domain: $fulldomain_no_acme" + + zone_fqdn="" + temp_domain="$fulldomain_no_acme" + + while [ -n "$temp_domain" ]; do + _debug "Checking if '$temp_domain' is a zone..." + if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then + zone_fqdn="$temp_domain" + _debug "Found matching zone: $zone_fqdn" + break + fi + temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') + if ! echo "$temp_domain" | grep -q '\.'; then + break + fi + done + + if [ -z "$zone_fqdn" ]; then + _err "Could not determine zone for domain $fulldomain" + _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" + return 1 + fi + + zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + + _debug "zone_id: $zone_id" + + if [ -z "$zone_id" ]; then + _err "Could not find zone ID for $zone_fqdn" + _debug "Zone result: $zone_result" + return 1 + fi + + _debug "Extracting name_in_zone from fulldomain='$fulldomain' with zone_fqdn='$zone_fqdn'" + name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") + _debug "name_in_zone after removing zone: '$name_in_zone'" + name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') + _debug "name_in_zone final: '$name_in_zone'" + + baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" + + body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" + + _debug "POST URL: $baseurl" + _debug "POST body: $body" + result="$(_post "$body" "$baseurl" "" "POST")" + _debug "POST result: $result" + + if echo "$result" | grep -q '"id"'; then + record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') + _info "Successfully created TXT record with ID: $record_id" + return 0 + else + _err "Error encountered during record addition" + _err "Response: $result" + return 1 + fi +} + +#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" + + zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" + _debug "Fetching zones from: $zone_url" + zone_result="$(_get "$zone_url")" + _debug2 "zone_result: $zone_result" + + if [ "$?" != "0" ]; then + _err "Error fetching zones from Infoblox API" + return 1 + fi + + fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') + _debug "Looking for zone matching domain: $fulldomain_no_acme" + + zone_fqdn="" + temp_domain="$fulldomain_no_acme" + + while [ -n "$temp_domain" ]; do + _debug "Checking if '$temp_domain' is a zone..." + if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then + zone_fqdn="$temp_domain" + _debug "Found matching zone: $zone_fqdn" + break + fi + temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') + if ! echo "$temp_domain" | grep -q '\.'; then + break + fi + done + + if [ -z "$zone_fqdn" ]; then + _err "Could not determine zone for domain $fulldomain" + _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" + return 1 + fi + + zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + + _debug "zone_id: $zone_id" + + if [ -z "$zone_id" ]; then + _err "Could not find zone ID for $zone_fqdn" + _debug "Zone result: $zone_result" + return 1 + fi + + name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') + _debug "name_in_zone: $name_in_zone" + + filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id'" + filter_encoded=$(_url_encode "$filter") + geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" + _debug "GET URL: $geturl" + + result="$(_get "$geturl")" + _debug "GET result: $result" + + if echo "$result" | grep -q '"results":'; then + record_count=$(echo "$result" | jq -r '.results | length') + _debug "Found $record_count result(s)" + + record_id=$(echo "$result" | jq -r '.results[] | select(.rdata.text == "'"$txtvalue"'") | .id' | head -1) + + if [ -n "$record_id" ]; then + record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') + _debug "Found record UUID: $record_uuid" + + delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" + _debug "DELETE URL: $delurl" + rmResult="$(_post "" "$delurl" "" "DELETE")" + + if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then + _info "Successfully deleted the txt record" + return 0 + else + _err "Error occurred during txt record delete" + _err "Response: $rmResult" + return 1 + fi + else + _err "Record to delete didn't match an existing record (no matching txtvalue found)" + _debug "Looking for txtvalue: $txtvalue" + return 1 + fi + else + _err "Record to delete didn't match an existing record (no results found)" + _debug "Response: $result" + return 1 + fi +} From 657b7195d6427c0bc2110c609b0a27365361597f Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:06:20 +0100 Subject: [PATCH 215/689] Fix Authorization header format --- dnsapi/dns_infoblox_uddi.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 545ce41d..c49a7f82 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -35,7 +35,7 @@ dns_infoblox_uddi_add() { _saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key" _saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal" - export _H1="Authorization: token $Infoblox_UDDI_Key" + export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" @@ -126,7 +126,7 @@ dns_infoblox_uddi_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - export _H1="Authorization: token $Infoblox_UDDI_Key" + export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" From eeb91de6a369e209558147f49dbe9a98b08a9a0e Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:13:52 +0100 Subject: [PATCH 216/689] Replace jq with shell-based JSON parsing --- dnsapi/dns_infoblox_uddi.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index c49a7f82..674090be 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -73,7 +73,7 @@ dns_infoblox_uddi_add() { return 1 fi - zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) _debug "zone_id: $zone_id" @@ -164,7 +164,7 @@ dns_infoblox_uddi_rm() { return 1 fi - zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) _debug "zone_id: $zone_id" @@ -186,10 +186,8 @@ dns_infoblox_uddi_rm() { _debug "GET result: $result" if echo "$result" | grep -q '"results":'; then - record_count=$(echo "$result" | jq -r '.results | length') - _debug "Found $record_count result(s)" - - record_id=$(echo "$result" | jq -r '.results[] | select(.rdata.text == "'"$txtvalue"'") | .id' | head -1) + record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^"]*"' | _egrep_o 'dns/record/[^"]*' | _head_n 1) + _debug "Found record_id: $record_id" if [ -n "$record_id" ]; then record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') From ca35e8c1189b2aa75cf022913154c10aaee30732 Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:32:28 +0100 Subject: [PATCH 217/689] Fix zone_id extraction to query correct zone --- dnsapi/dns_infoblox_uddi.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 674090be..8dfeab5f 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -73,7 +73,14 @@ dns_infoblox_uddi_add() { return 1 fi - zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) + # Fetch exact zone_id for the matched fqdn using server-side filtering + filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" + filter_encoded=$(_url_encode "$filter") + zone_query="$zone_url?_filter=$filter_encoded" + _debug "Fetching zone_id with filter: $zone_query" + zone_lookup="$(_get "$zone_query")" + _debug2 "zone_lookup: $zone_lookup" + zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') _debug "zone_id: $zone_id" @@ -164,7 +171,14 @@ dns_infoblox_uddi_rm() { return 1 fi - zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) + # Fetch exact zone_id for the matched fqdn using server-side filtering + filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" + filter_encoded=$(_url_encode "$filter") + zone_query="$zone_url?_filter=$filter_encoded" + _debug "Fetching zone_id with filter: $zone_query" + zone_lookup="$(_get "$zone_query")" + _debug2 "zone_lookup: $zone_lookup" + zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') _debug "zone_id: $zone_id" @@ -177,7 +191,7 @@ dns_infoblox_uddi_rm() { name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') _debug "name_in_zone: $name_in_zone" - filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id'" + filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" filter_encoded=$(_url_encode "$filter") geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" _debug "GET URL: $geturl" @@ -186,7 +200,7 @@ dns_infoblox_uddi_rm() { _debug "GET result: $result" if echo "$result" | grep -q '"results":'; then - record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^"]*"' | _egrep_o 'dns/record/[^"]*' | _head_n 1) + record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') _debug "Found record_id: $record_id" if [ -n "$record_id" ]; then From 490b9e2d09d998cee6736176e8c7eefa08b6029b Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:39:43 +0100 Subject: [PATCH 218/689] Clean up debug statements --- dnsapi/dns_infoblox_uddi.sh | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 8dfeab5f..ebf4484c 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -39,7 +39,6 @@ dns_infoblox_uddi_add() { export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - _debug "Fetching zones from: $zone_url" zone_result="$(_get "$zone_url")" _debug2 "zone_result: $zone_result" @@ -77,12 +76,11 @@ dns_infoblox_uddi_add() { filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" filter_encoded=$(_url_encode "$filter") zone_query="$zone_url?_filter=$filter_encoded" - _debug "Fetching zone_id with filter: $zone_query" zone_lookup="$(_get "$zone_query")" _debug2 "zone_lookup: $zone_lookup" zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "zone_id: $zone_id" + _debug zone_id "$zone_id" if [ -z "$zone_id" ]; then _err "Could not find zone ID for $zone_fqdn" @@ -90,20 +88,16 @@ dns_infoblox_uddi_add() { return 1 fi - _debug "Extracting name_in_zone from fulldomain='$fulldomain' with zone_fqdn='$zone_fqdn'" name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") - _debug "name_in_zone after removing zone: '$name_in_zone'" name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') - _debug "name_in_zone final: '$name_in_zone'" + _debug name_in_zone "$name_in_zone" baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" - _debug "POST URL: $baseurl" - _debug "POST body: $body" result="$(_post "$body" "$baseurl" "" "POST")" - _debug "POST result: $result" + _debug2 result "$result" if echo "$result" | grep -q '"id"'; then record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') @@ -137,7 +131,6 @@ dns_infoblox_uddi_rm() { export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - _debug "Fetching zones from: $zone_url" zone_result="$(_get "$zone_url")" _debug2 "zone_result: $zone_result" @@ -175,12 +168,11 @@ dns_infoblox_uddi_rm() { filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" filter_encoded=$(_url_encode "$filter") zone_query="$zone_url?_filter=$filter_encoded" - _debug "Fetching zone_id with filter: $zone_query" zone_lookup="$(_get "$zone_query")" _debug2 "zone_lookup: $zone_lookup" zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "zone_id: $zone_id" + _debug zone_id "$zone_id" if [ -z "$zone_id" ]; then _err "Could not find zone ID for $zone_fqdn" @@ -189,15 +181,14 @@ dns_infoblox_uddi_rm() { fi name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') - _debug "name_in_zone: $name_in_zone" + _debug name_in_zone "$name_in_zone" filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" filter_encoded=$(_url_encode "$filter") geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" - _debug "GET URL: $geturl" result="$(_get "$geturl")" - _debug "GET result: $result" + _debug2 result "$result" if echo "$result" | grep -q '"results":'; then record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') @@ -205,10 +196,9 @@ dns_infoblox_uddi_rm() { if [ -n "$record_id" ]; then record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') - _debug "Found record UUID: $record_uuid" + _debug record_uuid "$record_uuid" delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" - _debug "DELETE URL: $delurl" rmResult="$(_post "" "$delurl" "" "DELETE")" if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then @@ -221,7 +211,6 @@ dns_infoblox_uddi_rm() { fi else _err "Record to delete didn't match an existing record (no matching txtvalue found)" - _debug "Looking for txtvalue: $txtvalue" return 1 fi else From 890ab4a7bbfc9e950ba0ae103796e066442c25ee Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sun, 30 Nov 2025 00:48:15 +0100 Subject: [PATCH 219/689] Refactor dns_infoblox_uddi.sh: Fix zone detection and add wildcard cert support - Added _get_root() helper function for proper zone detection - Fixed zone ID extraction to match dns/auth_zone/* pattern - Added _infoblox_rest() wrapper for API calls with proper auth - Improved error handling for authentication failures - Added support for wildcard certificates (multiple TXT records) - Filter by exact txtvalue when deleting records - Follow acme.sh best practices and conventions Tested with: - Standard domain certificates - Wildcard certificates (*.domain.com) - Multiple subdomains - Staging and production Let's Encrypt --- dnsapi/dns_infoblox_uddi.sh | 299 +++++++++++++++++++----------------- 1 file changed, 161 insertions(+), 138 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index ebf4484c..54cfe47b 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -10,6 +10,8 @@ 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" @@ -38,76 +40,42 @@ dns_infoblox_uddi_add() { export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" - zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - zone_result="$(_get "$zone_url")" - _debug2 "zone_result: $zone_result" - - if [ "$?" != "0" ]; then - _err "Error fetching zones from Infoblox API" + _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" - fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') - _debug "Looking for zone matching domain: $fulldomain_no_acme" + _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'" - zone_fqdn="" - temp_domain="$fulldomain_no_acme" + _info "Adding record" + body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" - while [ -n "$temp_domain" ]; do - _debug "Checking if '$temp_domain' is a zone..." - if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then - zone_fqdn="$temp_domain" - _debug "Found matching zone: $zone_fqdn" - break + 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 - temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') - if ! echo "$temp_domain" | grep -q '\.'; then - break - fi - done - - if [ -z "$zone_fqdn" ]; then - _err "Could not determine zone for domain $fulldomain" - _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" - return 1 - fi - - # Fetch exact zone_id for the matched fqdn using server-side filtering - filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" - filter_encoded=$(_url_encode "$filter") - zone_query="$zone_url?_filter=$filter_encoded" - zone_lookup="$(_get "$zone_query")" - _debug2 "zone_lookup: $zone_lookup" - zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - - _debug zone_id "$zone_id" - - if [ -z "$zone_id" ]; then - _err "Could not find zone ID for $zone_fqdn" - _debug "Zone result: $zone_result" - return 1 - fi - - name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") - name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') - _debug name_in_zone "$name_in_zone" - - baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" - - body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" - - result="$(_post "$body" "$baseurl" "" "POST")" - _debug2 result "$result" - - if echo "$result" | grep -q '"id"'; then - record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') - _info "Successfully created TXT record with ID: $record_id" - return 0 - else - _err "Error encountered during record addition" - _err "Response: $result" - return 1 fi + _err "Add txt record error." + return 1 } #Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -130,92 +98,147 @@ dns_infoblox_uddi_rm() { export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" - zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - zone_result="$(_get "$zone_url")" - _debug2 "zone_result: $zone_result" + _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" - if [ "$?" != "0" ]; then - _err "Error fetching zones from Infoblox API" + _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 - fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') - _debug "Looking for zone matching domain: $fulldomain_no_acme" + _info "Removed record successfully" + return 0 +} - zone_fqdn="" - temp_domain="$fulldomain_no_acme" +#################### Private functions below ################################## - while [ -n "$temp_domain" ]; do - _debug "Checking if '$temp_domain' is a zone..." - if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then - zone_fqdn="$temp_domain" - _debug "Found matching zone: $zone_fqdn" - break - fi - temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') - if ! echo "$temp_domain" | grep -q '\.'; then - break - fi - done +#_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 - if [ -z "$zone_fqdn" ]; then - _err "Could not determine zone for domain $fulldomain" - _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" - return 1 - fi + # Remove _acme-challenge prefix if present + domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//') - # Fetch exact zone_id for the matched fqdn using server-side filtering - filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" - filter_encoded=$(_url_encode "$filter") - zone_query="$zone_url?_filter=$filter_encoded" - zone_lookup="$(_get "$zone_query")" - _debug2 "zone_lookup: $zone_lookup" - zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - - _debug zone_id "$zone_id" - - if [ -z "$zone_id" ]; then - _err "Could not find zone ID for $zone_fqdn" - _debug "Zone result: $zone_result" - return 1 - fi - - name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') - _debug name_in_zone "$name_in_zone" - - filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" - filter_encoded=$(_url_encode "$filter") - geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" - - result="$(_get "$geturl")" - _debug2 result "$result" - - if echo "$result" | grep -q '"results":'; then - record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "Found record_id: $record_id" - - if [ -n "$record_id" ]; then - record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') - _debug record_uuid "$record_uuid" - - delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" - rmResult="$(_post "" "$delurl" "" "DELETE")" - - if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then - _info "Successfully deleted the txt record" - return 0 - else - _err "Error occurred during txt record delete" - _err "Response: $rmResult" - return 1 - fi - else - _err "Record to delete didn't match an existing record (no matching txtvalue found)" + 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 - _err "Record to delete didn't match an existing record (no results found)" - _debug "Response: $result" + response="$(_get "$Infoblox_UDDI_Api/$ep")" + fi + + _ret="$?" + _debug2 response "$response" + + if [ "$_ret" != "0" ]; then + _err "Error: $ep" return 1 fi + + return 0 } From 36b8ca2bc07d0e46dd65fc8d8365d9ca1797d786 Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sun, 30 Nov 2025 00:49:36 +0100 Subject: [PATCH 220/689] Fix shfmt formatting: Remove trailing whitespace --- dnsapi/dns_infoblox_uddi.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 54cfe47b..4b15088a 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -183,7 +183,7 @@ _get_root() { # Found the zone _domain="$h" _domain_id="$zone_id" - + # Calculate subdomain if [ "$_domain" = "$domain" ]; then _sub_domain="" @@ -191,7 +191,7 @@ _get_root() { _cutlength=$((${#domain} - ${#_domain} - 1)) _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") fi - + return 0 fi fi From 9980ad0fef9634b105c59711dd5f470a4b35f080 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:33:19 +0100 Subject: [PATCH 221/689] add HostUp DNS --- dnsapi/dns_hostup.sh | 473 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 dnsapi/dns_hostup.sh diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh new file mode 100644 index 00000000..4da0dd9d --- /dev/null +++ b/dnsapi/dns_hostup.sh @@ -0,0 +1,473 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034,SC2154 + +dns_hostup_info='HostUp DNS +Site: hostup.se +Docs: https://hostup.se/en/support/api-autentisering/ +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" | tr 'A-Z' 'a-z')" + _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 + + if ! printf "%s" "$_domain_candidate" | _contains "."; then + break + fi + + _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 < Date: Mon, 1 Dec 2025 15:48:48 +0100 Subject: [PATCH 223/689] Update dns_hostup.sh bug fix Omnios fial --- dnsapi/dns_hostup.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 4da0dd9d..8d0600f7 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -182,9 +182,10 @@ _hostup_detect_zone() { return 0 fi - if ! printf "%s" "$_domain_candidate" | _contains "."; then - break - fi + case "$_domain_candidate" in + *.*) ;; + *) break ;; + esac _domain_candidate="${_domain_candidate#*.}" done From d97b4477b2dcf2753ea0afbd24eea8487625aed2 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:55:17 +0100 Subject: [PATCH 224/689] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 8d0600f7..3547f006 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -171,7 +171,7 @@ _hostup_detect_zone() { return 1 fi - _domain_candidate="$(printf "%s" "$fulldomain" | tr 'A-Z' 'a-z')" + _domain_candidate="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]')" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -181,10 +181,10 @@ _hostup_detect_zone() { HOSTUP_ZONE_ID="$_lookup_zone_id" return 0 fi - + case "$_domain_candidate" in - *.*) ;; - *) break ;; + *.*) ;; + *) break ;; esac _domain_candidate="${_domain_candidate#*.}" @@ -210,7 +210,7 @@ _hostup_record_name() { suffix=".$zonedomain" case "$fulldomain" in *"$suffix") - printf "%s" "${fulldomain%$suffix}" + printf "%s" "${fulldomain%"$suffix"}" ;; *) # Domain not within zone, fall back to full host @@ -371,7 +371,7 @@ _hostup_record_key() { zone_id="$1" domain="$2" safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" - safe_domain="$(printf "%s" "$domain" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]/_/g')" + safe_domain="$(printf "%s" "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" printf "%s_%s" "$safe_zone" "$safe_domain" } @@ -449,7 +449,7 @@ _hostup_rest() { _debug2 "_hostup_response" "$_hostup_response" case "$http_status" in - 200|201|204) return 0 ;; + 200 | 201 | 204) return 0 ;; 401) _err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions." return 1 From 64a6ea68fa704ce4df35b03cff7c29fe531c38b3 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:58:36 +0100 Subject: [PATCH 225/689] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 3547f006..ca49096f 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -181,7 +181,7 @@ _hostup_detect_zone() { HOSTUP_ZONE_ID="$_lookup_zone_id" return 0 fi - + case "$_domain_candidate" in *.*) ;; *) break ;; From 51b4fa00800ae2aad88a81fe76783d374044318c Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:19:16 +0100 Subject: [PATCH 226/689] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 49 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index ca49096f..347f34d1 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -319,10 +319,14 @@ _hostup_find_record() { records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" while IFS= read -r line; do - case "$line" in + # Normalize line to make TXT value matching reliable + line_clean="$(printf "%s" "$line" | tr -d '\r\n')" + line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')" + + case "$line_clean" in *'"type":"TXT"'*'"name"'*'"value"'*) - name_value="$(printf "%s" "$line" | _hostup_json_extract "name")" - record_value="$(printf "%s" "$line" | _hostup_json_extract "value")" + name_value="$(_hostup_json_extract "name" "$line_clean")" + record_value="$(_hostup_json_extract "value" "$line_value_clean")" _debug "hostup_record_raw" "$record_value" if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then @@ -337,7 +341,7 @@ _hostup_find_record() { _debug "hostup_record_value" "$record_value" if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then - record_id="$(printf "%s" "$line" | _hostup_json_extract "id")" + record_id="$(_hostup_json_extract "id" "$line_clean")" if [ -n "$record_id" ]; then HOSTUP_RECORD_ID="$record_id" return 0 @@ -354,13 +358,30 @@ EOF _hostup_json_extract() { key="$1" - printf "%s" "$line" | - _egrep_o "\"$key\":\"[^\"]*\"" | - head -n1 | - cut -d : -f2- | - sed 's/^"//' | - sed 's/"$//' | - sed 's/\\"/"/g' + input="${2:-$line}" + + # First try to extract quoted values (strings) + quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | head -n1)" + if [ -n "$quoted_match" ]; then + printf "%s" "$quoted_match" | + cut -d : -f2- | + sed 's/^"//' | + sed 's/"$//' | + sed 's/\\"/"/g' + return 0 + fi + + # Fallback for unquoted values (e.g., numeric IDs) + unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | head -n1)" + if [ -n "$unquoted_match" ]; then + printf "%s" "$unquoted_match" | + cut -d : -f2- | + tr -d '", ' | + tr -d '\r\n' + return 0 + fi + + return 1 } _hostup_json_escape() { @@ -398,6 +419,12 @@ _hostup_clear_record_id() { } _hostup_extract_record_id() { + record_id="$(_hostup_json_extract "id" "$1")" + if [ -n "$record_id" ]; then + printf "%s" "$record_id" + return 0 + fi + printf "%s" "$1" | _egrep_o '"id":[0-9]+' | head -n1 | cut -d: -f2 } From 2775def93aa25b978791bb63c896f4df30f5d1d1 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:05:19 -0500 Subject: [PATCH 227/689] Use 'cat' instead of 'cp', removed use of temp file, keeps permissions Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index 38ae9599..f420e62f 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -67,14 +67,21 @@ localcopy_deploy() { fi DEPLOY_LOCALCOPY_CERTKEY="" _info "Creating combined PEM at $_combined_target" - _tmpfile="$(mktemp)" - if ! cat "$_combined_srccert" "$_ckey" >"$_tmpfile"; then - _err "Failed to build combined PEM file" - return 1 - fi - if ! mv "$_tmpfile" "$_combined_target"; then - _err "Failed to move combined PEM into place" - return 1 + if [ -f "$_combined_target" ]; then + if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi + else + if ! touch "$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi + chmod 600 "$_combined_target" + if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi fi fi From f39a6fe517e56bc4b4e6bca190326366e8b98014 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:06:50 -0500 Subject: [PATCH 228/689] Use cat instead of cp for all files Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 80 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index f420e62f..ddb7d4b6 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -66,29 +66,35 @@ localcopy_deploy() { DEPLOY_LOCALCOPY_FULLCHAIN="" fi DEPLOY_LOCALCOPY_CERTKEY="" - _info "Creating combined PEM at $_combined_target" - if [ -f "$_combined_target" ]; then - if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then - _err "Failed to create PEM file" - return 1 - fi - else - if ! touch "$_combined_target"; then - _err "Failed to create PEM file" - return 1 - fi - chmod 600 "$_combined_target" - if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _info "Creating combined PEM" + _debug "Creating combined PEM at $_combined_target" + if ! [ -f "$_combined_target" ]; then + if ! ( + touch "$_combined_target" + chmod 600 "$_combined_target" + ); then _err "Failed to create PEM file" return 1 fi fi + if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi fi - if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _info "Copying certificate" _debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE" - if ! eval "cp $_ccert $DEPLOY_LOCALCOPY_CERTIFICATE"; then + if ! [ -f "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_CERTIFICATE" + chmod 600 "$DEPLOY_LOCALCOPY_CERTIFICATE" + ); then + _err "Failed to copy certificate, aborting." + return 1 + fi + fi + if ! cat "$_ccert" >"$DEPLOY_LOCALCOPY_CERTIFICATE"; then _err "Failed to copy certificate, aborting." return 1 fi @@ -97,7 +103,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then _info "Copying certificate key" _debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY" - if ! eval "cp $_ckey $DEPLOY_LOCALCOPY_CERTKEY"; then + if ! [ -f "$DEPLOY_LOCALCOPY_CERTKEY" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_CERTKEY" + chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY" + ); then + _err "Failed to copy certificate key, aborting." + return 1 + fi + fi + if ! cat "$_ckey" >"$DEPLOY_LOCALCOPY_CERTKEY"; then _err "Failed to copy certificate key, aborting." return 1 fi @@ -107,7 +122,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then _info "Copying fullchain" _debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN" - if ! eval "cp $_cfullchain $DEPLOY_LOCALCOPY_FULLCHAIN"; then + if ! [ -f "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_FULLCHAIN" + chmod 600 "$DEPLOY_LOCALCOPY_FULLCHAIN" + ); then + _err "Failed to copy fullchain, aborting." + return 1 + fi + fi + if ! cat "$_cfullchain" >"$DEPLOY_LOCALCOPY_FULLCHAIN"; then _err "Failed to copy fullchain, aborting." return 1 fi @@ -117,7 +141,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_CA" ]; then _info "Copying CA" _debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA" - if ! eval "cp $_cca $DEPLOY_LOCALCOPY_CA"; then + if ! [ -f "$DEPLOY_LOCALCOPY_CA" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_CA" + chmod 600 "$DEPLOY_LOCALCOPY_CA" + ); then + _err "Failed to copy CA, aborting." + return 1 + fi + fi + if ! cat "$_cca" >"$DEPLOY_LOCALCOPY_CA"; then _err "Failed to copy CA, aborting." return 1 fi @@ -127,7 +160,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_PFX" ]; then _info "Copying PFX" _debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX" - if ! eval "cp $_cpfx $DEPLOY_LOCALCOPY_PFX"; then + if ! [ -f "$DEPLOY_LOCALCOPY_PFX" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_PFX" + chmod 600 "$DEPLOY_LOCALCOPY_PFX" + ); then + _err "Failed to copy PFX, aborting." + return 1 + fi + fi + if ! cat "$_cpfx" >"$DEPLOY_LOCALCOPY_PFX"; then _err "Failed to copy PFX, aborting." return 1 fi From d3930639db5a0a0cd6fbf75d02d8c538568a1816 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 5 Dec 2025 08:58:41 -0500 Subject: [PATCH 229/689] Updated secrets and put back guards --- dnsapi/dns_qc.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index c30d4595..3ea8403c 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -110,6 +110,10 @@ dns_qc_rm() { else record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') _debug "txt record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get txt record id to remove." + return 1 + fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then _info "Delete txt record error." return 1 From 67a389cbbf781492bbb9d554d8f0da45c77e5da3 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 5 Dec 2025 10:35:52 -0500 Subject: [PATCH 230/689] Minor change and setup secrets again --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 3ea8403c..6d1a9eb4 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -111,7 +111,7 @@ dns_qc_rm() { record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') _debug "txt record_id" "$record_id" if [ -z "$record_id" ]; then - _err "Can not get txt record id to remove." + _err "Can not get txt record id to remove. Run in debug mode." return 1 fi if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then From 6b6d22c5ba0d7645444d26629793d22f51e19841 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 5 Dec 2025 13:52:59 -0500 Subject: [PATCH 231/689] shfmt updates --- dnsapi/dns_qc.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 6d1a9eb4..6d4f7299 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -34,7 +34,7 @@ dns_qc_add() { _err "Please check and retry." return 1 fi - #save the api key and email to the account conf file. + #save the api key and email to the account conf file. _savedomainconf QC_API_EMAIL "$QC_API_EMAIL" _debug "First detect the root zone" @@ -103,12 +103,12 @@ dns_qc_rm() { return 1 fi - response=$(echo "$response"|jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") + 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." else - record_id=$(echo "$response" | grep \"id\"| awk -F ' ' '{print $2}'| sed 's/,$//') + record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//') _debug "txt record_id" "$record_id" if [ -z "$record_id" ]; then _err "Can not get txt record id to remove. Run in debug mode." From bee01c938a234021d4aa8d6bcb2cb6d421262573 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Dec 2025 21:46:05 +0100 Subject: [PATCH 232/689] add comment --- .github/workflows/wiki-monitor.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index b0332775..a79d70a4 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -22,6 +22,7 @@ jobs: page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") + page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH") now="$(date '+%Y-%m-%d %H:%M:%S')" cd wiki @@ -35,9 +36,11 @@ jobs: { echo "Wiki edited" echo -n "User: " - echo "[$actor]($sender_url)" + echo "@$actor [$actor]($sender_url)" echo "Time: $now" echo "Page: [$page_name]($page_url) (Action: $page_action)" + echo "Comment: $page_summary" + echo "[Click here to Revert](${page_url}/_history)" echo "" echo "----" echo "### diff:" From a1857af6de5dcee0ec109a36b45dbb4b0326e067 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 5 Dec 2025 15:53:17 -0500 Subject: [PATCH 233/689] Update error message and secrets --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 6d4f7299..cea411e8 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -24,7 +24,7 @@ dns_qc_add() { if [ "$QC_API_KEY" ]; then _savedomainconf QC_API_KEY "$QC_API_KEY" else - _err "You didn't specify a QUIC.cloud are api key and email yet." + _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 From ed1bd01592afba97749f2b51573b8c35f2372c31 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Fri, 5 Dec 2025 16:29:49 -0500 Subject: [PATCH 234/689] Save account information differently --- dnsapi/dns_qc.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index cea411e8..7ae2f1cd 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -22,7 +22,7 @@ dns_qc_add() { QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" if [ "$QC_API_KEY" ]; then - _savedomainconf QC_API_KEY "$QC_API_KEY" + _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." @@ -35,7 +35,7 @@ dns_qc_add() { return 1 fi #save the api key and email to the account conf file. - _savedomainconf QC_API_EMAIL "$QC_API_EMAIL" + _saveaccountconf_mutable QC_API_EMAIL "$QC_API_EMAIL" _debug "First detect the root zone" if ! _get_root "$fulldomain"; then From 45cb36f6d909dc5ed5b5534088aed088b9c3e55f Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Dec 2025 22:31:45 +0100 Subject: [PATCH 235/689] fix https://github.com/acmesh-official/acme.sh/issues/6246#issuecomment-3610998032 --- dnsapi/dns_ali.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index 53a82f91..cbee773e 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -97,9 +97,10 @@ _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() { From 3b2c2b16b2472c986f16a00eb24f27456f57a318 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 6 Dec 2025 11:23:28 +0100 Subject: [PATCH 236/689] minor --- dnsapi/dns_ali.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index cbee773e..90196c69 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -103,7 +103,7 @@ _ali_nonce() { 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" } @@ -151,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' } @@ -167,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' @@ -183,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' } @@ -197,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' } From 875cf056b7e293c67b4ae9549455a20fa798da62 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 8 Dec 2025 08:40:44 -0500 Subject: [PATCH 237/689] Submit dns_qc.sh for review --- dns_qc.sh | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100755 dns_qc.sh diff --git a/dns_qc.sh b/dns_qc.sh new file mode 100755 index 00000000..78a243ae --- /dev/null +++ b/dns_qc.sh @@ -0,0 +1,201 @@ +#!/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 + _savedomainconf QC_API_KEY "$QC_API_KEY" + else + _err "You didn't specify a QUIC.cloud are api key and email yet." + _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. + _savedomainconf QC_API_EMAIL "$QC_API_EMAIL" + + _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" + _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" + 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 + + 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." + else + record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//') + _debug "txt record_id" "$record_id" + + if [[ -z "$record_id" ]]; then + _err "Can not get txt record id to remove. Run in debug mode." + return 1 + fi + + 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" + fi + +} + +#################### Private functions below ################################# +#_acme-challenge.www.domain.com +#returns +#_sub_domain=_acme-challenge.www +#_domain=domain.com +#_domain_id=sdjkglgdfewsdfg +_get_root() { + domain=$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" + +} From 5fcca7c7e0e8144e555987328caccf60eaa533d3 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 8 Dec 2025 08:48:30 -0500 Subject: [PATCH 238/689] Retry correct commit --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 7ae2f1cd..0765c697 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -17,7 +17,7 @@ dns_qc_add() { fulldomain=$1 txtvalue=$2 - _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" + _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)}" From 4965c704d7ea39675bc4131b04264611dd6f1aaa Mon Sep 17 00:00:00 2001 From: ufozone Date: Sun, 7 Dec 2025 15:07:50 +0100 Subject: [PATCH 239/689] Initial commit for mgw-media.de --- dnsapi/dns_mgwm.sh | 112 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 dnsapi/dns_mgwm.sh diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh new file mode 100644 index 00000000..7715b645 --- /dev/null +++ b/dnsapi/dns_mgwm.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 + +# DNS provider information for acme.sh +dns_mgwm_info='mgw-media.de +Site: mgw-media.de +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mgwm +Options: + MGWM_CUSTOMER Your customer number + MGWM_API_HASH Your API Hash +Issues: github.com/acmesh-official/acme.sh +' + +# 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" + _debug "fulldomain: $fulldomain" + _debug "txtvalue: $txtvalue" + + # Call private function to load and save environment variables and set up the Basic Auth Header. + if ! _mgwm_init_env; then + return 1 + fi + + # Construct the API URL for adding a record. + _add_url="${MGWM_API_BASE}/add/${fulldomain}/txt/${txtvalue}" + _debug "Calling MGWM ADD URL: ${_add_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 "" "$_add_url" "" "GET" "$_H1")" + _debug "MGWM add response: $response" + + # Check the API response for success. The API returns "OK" on success. + if [ "$response" = "OK" ]; then + _info "TXT record for $fulldomain successfully added via MGWM API." + _sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks. + return 0 + else + _err "mgwm_add: Failed to add TXT record for $fulldomain. Unexpected API Response: '$response'" + 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" + _debug "fulldomain: $fulldomain" + _debug "txtvalue: $txtvalue" + + # Call private function to load and save environment variables and set up the Basic Auth Header. + if ! _mgwm_init_env; then + return 1 + fi + + # Construct the API URL for removing a record. + # To delete a specific record by its value (as required by ACME v2 for multiple TXT records), + # the txtvalue must be part of the URL, similar to the add action. + _rm_url="${MGWM_API_BASE}/rm/${fulldomain}/txt/${txtvalue}" + _debug "Calling MGWM RM URL: ${_rm_url}" + + # Execute the HTTP GET request with the Authorization Header. + response="$(_get "" "$_rm_url" "" "GET" "$_H1")" + _debug "MGWM rm response: $response" + + # Check the API response for success. The API returns "OK" on success. + if [ "$response" = "OK" ]; then + _info "TXT record for $fulldomain successfully removed via MGWM API." + return 0 + else + _err "mgwm_rm: Failed to remove TXT record for $fulldomain. Unexpected API Response: '$response'" + return 1 + fi +} + +#################### Private functions below ################################## + +# _mgwm_init_env() loads the mgw-media.de API credentials (customer number and hash) +# from environment variables or acme.sh's configuration, saves them, and +# prepares the global _H1 variable for Basic Authorization header. +_mgwm_init_env() { + # 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 + return 0 +} From daf7f7c268cc33170e40321ee5b02d96fc2b9960 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:29:24 +0100 Subject: [PATCH 240/689] Refactor dns_mgwm.sh for better API integration Refactor DNS API script to improve credential handling and update API endpoint. --- dnsapi/dns_mgwm.sh | 99 +++++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index 7715b645..b3e21726 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -2,17 +2,18 @@ # shellcheck disable=SC2034 # DNS provider information for acme.sh -dns_mgwm_info='mgw-media.de +dns_mgwm_info='MGW-MEDIA.DE Site: mgw-media.de Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mgwm Options: - MGWM_CUSTOMER Your customer number - MGWM_API_HASH Your API Hash + MGWM_CUSTOMER Your customer number (username for Basic Auth). + MGWM_API_HASH Your API Hash (password for Basic Auth). Issues: github.com/acmesh-official/acme.sh +Author: (Your Name or generated by AI) ' -# Base URL for the mgw-media.de API -MGWM_API_BASE="https://api.mgw-media.de/record" +# Base endpoint for the MGW-MEDIA.DE API (parameters will be added as query strings) +MGWM_API_ENDPOINT="https://api.mgw-media.de/record" ######## Public functions ##################### # This function is called by acme.sh to add a TXT record. @@ -24,16 +25,32 @@ dns_mgwm_add() { _debug "fulldomain: $fulldomain" _debug "txtvalue: $txtvalue" - # Call private function to load and save environment variables and set up the Basic Auth Header. - if ! _mgwm_init_env; then + # 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 - # Construct the API URL for adding a record. - _add_url="${MGWM_API_BASE}/add/${fulldomain}/txt/${txtvalue}" + # 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 directly in this function's scope + _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)" + # Export _H1 so _get function can pick it up + export _H1="Authorization: Basic $_credentials" + _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials + + # Construct the API URL for adding a record with query parameters + _add_url="${MGWM_API_ENDPOINT}?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" _debug "Calling MGWM ADD URL: ${_add_url}" - # Execute the HTTP GET request with the Authorization Header. + # Execute the HTTP GET request with the Authorization Header (_H1) # The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization. response="$(_get "" "$_add_url" "" "GET" "$_H1")" _debug "MGWM add response: $response" @@ -52,24 +69,39 @@ dns_mgwm_add() { # 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. + txtvalue=$2 # This value is not used by the RM API in this case. _info "Removing TXT record for $fulldomain using mgw-media.de DNS API" _debug "fulldomain: $fulldomain" - _debug "txtvalue: $txtvalue" + _debug "txtvalue: $txtvalue" # Still logging for completeness, but not used in URL - # Call private function to load and save environment variables and set up the Basic Auth Header. - if ! _mgwm_init_env; then + # 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 - # Construct the API URL for removing a record. - # To delete a specific record by its value (as required by ACME v2 for multiple TXT records), - # the txtvalue must be part of the URL, similar to the add action. - _rm_url="${MGWM_API_BASE}/rm/${fulldomain}/txt/${txtvalue}" + # Save credentials (important for future renewals if not saved by add function) + _saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER" + _saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH" + + # Create the Basic Auth Header directly in this function's scope + _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)" + # Export _H1 so _get function can pick it up + export _H1="Authorization: Basic $_credentials" + _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials + + # Construct the API URL for removing a record with query parameters + # The RM API from mgw-media.de does not expect a 'content' parameter. + _rm_url="${MGWM_API_ENDPOINT}?action=rm&fulldomain=${fulldomain}&type=txt" _debug "Calling MGWM RM URL: ${_rm_url}" - # Execute the HTTP GET request with the Authorization Header. + # Execute the HTTP GET request with the Authorization Header (_H1) response="$(_get "" "$_rm_url" "" "GET" "$_H1")" _debug "MGWM rm response: $response" @@ -84,29 +116,6 @@ dns_mgwm_rm() { } #################### Private functions below ################################## - -# _mgwm_init_env() loads the mgw-media.de API credentials (customer number and hash) -# from environment variables or acme.sh's configuration, saves them, and -# prepares the global _H1 variable for Basic Authorization header. -_mgwm_init_env() { - # 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 - return 0 -} +# The _mgwm_init_env function has been inlined into dns_mgwm_add and dns_mgwm_rm +# to ensure credentials and the Authorization header are set correctly within +# each function's sub-shell context. From 11eaad1fa742d4d55e7a3bc17675b581e2281b59 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:32:30 +0100 Subject: [PATCH 241/689] Update API URLs to include .php extension --- dnsapi/dns_mgwm.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index b3e21726..42c6d93a 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -47,7 +47,7 @@ dns_mgwm_add() { _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials # Construct the API URL for adding a record with query parameters - _add_url="${MGWM_API_ENDPOINT}?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" + _add_url="${MGWM_API_ENDPOINT}.php?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" _debug "Calling MGWM ADD URL: ${_add_url}" # Execute the HTTP GET request with the Authorization Header (_H1) @@ -98,7 +98,7 @@ dns_mgwm_rm() { # Construct the API URL for removing a record with query parameters # The RM API from mgw-media.de does not expect a 'content' parameter. - _rm_url="${MGWM_API_ENDPOINT}?action=rm&fulldomain=${fulldomain}&type=txt" + _rm_url="${MGWM_API_ENDPOINT}.php?action=rm&fulldomain=${fulldomain}&type=txt" _debug "Calling MGWM RM URL: ${_rm_url}" # Execute the HTTP GET request with the Authorization Header (_H1) From e94c6be4a1c877a06afcc0c1b4530b864b777bd5 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:41:27 +0100 Subject: [PATCH 242/689] Update MGWM API endpoint to IPv4 --- dnsapi/dns_mgwm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index 42c6d93a..a3aabb53 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -13,7 +13,7 @@ Author: (Your Name or generated by AI) ' # Base endpoint for the MGW-MEDIA.DE API (parameters will be added as query strings) -MGWM_API_ENDPOINT="https://api.mgw-media.de/record" +MGWM_API_ENDPOINT="https://ipv4.api.mgw-media.de/record" ######## Public functions ##################### # This function is called by acme.sh to add a TXT record. From 2ba615555cf214edc447e4435bb20ee30b4340a9 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:59:04 +0100 Subject: [PATCH 243/689] Refactor dns_mgwm.sh for improved API interaction Refactor MGWM API script to improve clarity and functionality. Update API endpoint and streamline credential handling. --- dnsapi/dns_mgwm.sh | 110 +++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index a3aabb53..c02b7a6d 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -9,11 +9,12 @@ Options: MGWM_CUSTOMER Your customer number (username for Basic Auth). MGWM_API_HASH Your API Hash (password for Basic Auth). Issues: github.com/acmesh-official/acme.sh -Author: (Your Name or generated by AI) +Author: Generated by AI (with user input) ' -# Base endpoint for the MGW-MEDIA.DE API (parameters will be added as query strings) -MGWM_API_ENDPOINT="https://ipv4.api.mgw-media.de/record" +# Direct endpoint for the PHP script with query parameters +# This variable replaces MGWM_API_BASE when using query-parameter-based URLs directly. +MGWM_API_ENDPOINT="https://api.mgw-media.de/record.php" ######## Public functions ##################### # This function is called by acme.sh to add a TXT record. @@ -25,34 +26,20 @@ dns_mgwm_add() { _debug "fulldomain: $fulldomain" _debug "txtvalue: $txtvalue" - # 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." + # Call private function to load and save environment variables and set up the Basic Auth Header. + if ! _mgwm_init_env; then 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 directly in this function's scope - _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)" - # Export _H1 so _get function can pick it up - export _H1="Authorization: Basic $_credentials" - _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials - - # Construct the API URL for adding a record with query parameters - _add_url="${MGWM_API_ENDPOINT}.php?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" + # Construct the API URL using query parameters. + # This targets the record.php script directly, passing action, fulldomain, type, and content. + _add_url="${MGWM_API_ENDPOINT}?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" _debug "Calling MGWM ADD URL: ${_add_url}" - # Execute the HTTP GET request with the Authorization Header (_H1) - # The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization. - response="$(_get "" "$_add_url" "" "GET" "$_H1")" + # Execute the HTTP GET request. + # Correct parameters for _get(): url="$1", onlyheader="$2", t="$3" + # The Authorization Header (_H1) is automatically picked up by _get() from the environment. + response="$(_get "$_add_url" "" "")" # <-- KORRIGIERTER AUFRUF VON _get() _debug "MGWM add response: $response" # Check the API response for success. The API returns "OK" on success. @@ -69,40 +56,26 @@ dns_mgwm_add() { # This function is called by acme.sh to remove a TXT record after validation. dns_mgwm_rm() { fulldomain=$1 - txtvalue=$2 # This value is not used by the RM API in this case. + 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" _debug "fulldomain: $fulldomain" - _debug "txtvalue: $txtvalue" # Still logging for completeness, but not used in URL + _debug "txtvalue: $txtvalue" - # 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." + # Call private function to load and save environment variables and set up the Basic Auth Header. + if ! _mgwm_init_env; then return 1 fi - # Save credentials (important for future renewals if not saved by add function) - _saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER" - _saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH" - - # Create the Basic Auth Header directly in this function's scope - _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)" - # Export _H1 so _get function can pick it up - export _H1="Authorization: Basic $_credentials" - _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials - - # Construct the API URL for removing a record with query parameters - # The RM API from mgw-media.de does not expect a 'content' parameter. - _rm_url="${MGWM_API_ENDPOINT}.php?action=rm&fulldomain=${fulldomain}&type=txt" + # Construct the API URL for removing a record. + # This targets the record.php script directly, passing action, fulldomain, type, and content. + _rm_url="${MGWM_API_ENDPOINT}?action=rm&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" _debug "Calling MGWM RM URL: ${_rm_url}" - # Execute the HTTP GET request with the Authorization Header (_H1) - response="$(_get "" "$_rm_url" "" "GET" "$_H1")" + # Execute the HTTP GET request. + # Correct parameters for _get(): url="$1", onlyheader="$2", t="$3" + # The Authorization Header (_H1) is automatically picked up by _get() from the environment. + response="$(_get "$_rm_url" "" "")" # <-- KORRIGIERTER AUFRUF VON _get() _debug "MGWM rm response: $response" # Check the API response for success. The API returns "OK" on success. @@ -116,6 +89,35 @@ dns_mgwm_rm() { } #################### Private functions below ################################## -# The _mgwm_init_env function has been inlined into dns_mgwm_add and dns_mgwm_rm -# to ensure credentials and the Authorization header are set correctly within -# each function's sub-shell context. + +# _mgwm_init_env() loads the mgw-media.de API credentials (customer number and hash) +# from environment variables or acme.sh's configuration, saves them, and +# prepares the global _H1 variable for Basic Authorization header. +_mgwm_init_env() { + # 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 + return 0 +} + +# The _get_root function, often found in other acme.sh DNS API scripts, +# is not necessary for the MGW-MEDIA.DE API. +# The MGW-MEDIA.DE API directly accepts the complete FQDN (fulldomain) +# in its URL path and handles the extraction of the subdomain and root domain internally. +# Therefore, no custom _get_root implementation is needed here. From 546c2d47d5b6a740eb7cbf874e81529106a1e66d Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:11:42 +0100 Subject: [PATCH 244/689] Refactor DNS API for mgw-media.de Updated DNS API script for mgw-media.de to use new base URL and improved API request structure. --- dnsapi/dns_mgwm.sh | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index c02b7a6d..f38c184e 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -2,19 +2,17 @@ # shellcheck disable=SC2034 # DNS provider information for acme.sh -dns_mgwm_info='MGW-MEDIA.DE +dns_mgwm_info='mgw-media.de Site: mgw-media.de Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mgwm Options: - MGWM_CUSTOMER Your customer number (username for Basic Auth). - MGWM_API_HASH Your API Hash (password for Basic Auth). + MGWM_CUSTOMER Your customer number + MGWM_API_HASH Your API Hash Issues: github.com/acmesh-official/acme.sh -Author: Generated by AI (with user input) ' -# Direct endpoint for the PHP script with query parameters -# This variable replaces MGWM_API_BASE when using query-parameter-based URLs directly. -MGWM_API_ENDPOINT="https://api.mgw-media.de/record.php" +# 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. @@ -31,15 +29,14 @@ dns_mgwm_add() { return 1 fi - # Construct the API URL using query parameters. - # This targets the record.php script directly, passing action, fulldomain, type, and content. - _add_url="${MGWM_API_ENDPOINT}?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" + # Construct the API URL for adding a record. + #_add_url="${MGWM_API_BASE}/add/${fulldomain}/txt/${txtvalue}" + _add_url="${MGWM_API_BASE}.php?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" _debug "Calling MGWM ADD URL: ${_add_url}" - # Execute the HTTP GET request. - # Correct parameters for _get(): url="$1", onlyheader="$2", t="$3" - # The Authorization Header (_H1) is automatically picked up by _get() from the environment. - response="$(_get "$_add_url" "" "")" # <-- KORRIGIERTER AUFRUF VON _get() + # 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 "$_add_url")" _debug "MGWM add response: $response" # Check the API response for success. The API returns "OK" on success. @@ -68,14 +65,14 @@ dns_mgwm_rm() { fi # Construct the API URL for removing a record. - # This targets the record.php script directly, passing action, fulldomain, type, and content. - _rm_url="${MGWM_API_ENDPOINT}?action=rm&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" + # To delete a specific record by its value (as required by ACME v2 for multiple TXT records), + # the txtvalue must be part of the URL, similar to the add action. + #_rm_url="${MGWM_API_BASE}/rm/${fulldomain}/txt/${txtvalue}" + _rm_url="${MGWM_API_BASE}.php?action=rm&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" _debug "Calling MGWM RM URL: ${_rm_url}" - # Execute the HTTP GET request. - # Correct parameters for _get(): url="$1", onlyheader="$2", t="$3" - # The Authorization Header (_H1) is automatically picked up by _get() from the environment. - response="$(_get "$_rm_url" "" "")" # <-- KORRIGIERTER AUFRUF VON _get() + # Execute the HTTP GET request with the Authorization Header. + response="$(_get "$_rm_url")" _debug "MGWM rm response: $response" # Check the API response for success. The API returns "OK" on success. @@ -115,9 +112,3 @@ _mgwm_init_env() { _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials return 0 } - -# The _get_root function, often found in other acme.sh DNS API scripts, -# is not necessary for the MGW-MEDIA.DE API. -# The MGW-MEDIA.DE API directly accepts the complete FQDN (fulldomain) -# in its URL path and handles the extraction of the subdomain and root domain internally. -# Therefore, no custom _get_root implementation is needed here. From d8722c46d9b8ed938122ef54ec89e440878263ac Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:26:29 +0100 Subject: [PATCH 245/689] Consolidate API request logic in dns_mgwm.sh Refactor DNS API functions to use a unified request handler. --- dnsapi/dns_mgwm.sh | 100 ++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index f38c184e..ca48584c 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -1,6 +1,5 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 - # DNS provider information for acme.sh dns_mgwm_info='mgw-media.de Site: mgw-media.de @@ -10,87 +9,66 @@ Options: MGWM_API_HASH Your API Hash Issues: github.com/acmesh-official/acme.sh ' - # 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" + _info "Using mgw-media.de DNS API for domain $fulldomain (add record)" _debug "fulldomain: $fulldomain" _debug "txtvalue: $txtvalue" - # Call private function to load and save environment variables and set up the Basic Auth Header. - if ! _mgwm_init_env; then - return 1 - fi - - # Construct the API URL for adding a record. - #_add_url="${MGWM_API_BASE}/add/${fulldomain}/txt/${txtvalue}" - _add_url="${MGWM_API_BASE}.php?action=add&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" - _debug "Calling MGWM ADD URL: ${_add_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 "$_add_url")" - _debug "MGWM add response: $response" - - # Check the API response for success. The API returns "OK" on success. - if [ "$response" = "OK" ]; then + # Call the new private function to handle the API request. + # The 'add' action, fulldomain, type 'txt' and txtvalue are passed. + if _mgwm_perform_api_request "add" "$fulldomain" "txt" "$txtvalue"; then _info "TXT record for $fulldomain successfully added via MGWM API." _sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks. return 0 else - _err "mgwm_add: Failed to add TXT record for $fulldomain. Unexpected API Response: '$response'" + # Error message already logged by _mgwm_perform_api_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" + _info "Removing TXT record for $fulldomain using mgw-media.de DNS API (remove record)" _debug "fulldomain: $fulldomain" _debug "txtvalue: $txtvalue" - # Call private function to load and save environment variables and set up the Basic Auth Header. - if ! _mgwm_init_env; then - return 1 - fi - - # Construct the API URL for removing a record. - # To delete a specific record by its value (as required by ACME v2 for multiple TXT records), - # the txtvalue must be part of the URL, similar to the add action. - #_rm_url="${MGWM_API_BASE}/rm/${fulldomain}/txt/${txtvalue}" - _rm_url="${MGWM_API_BASE}.php?action=rm&fulldomain=${fulldomain}&type=txt&content=${txtvalue}" - _debug "Calling MGWM RM URL: ${_rm_url}" - - # Execute the HTTP GET request with the Authorization Header. - response="$(_get "$_rm_url")" - _debug "MGWM rm response: $response" - - # Check the API response for success. The API returns "OK" on success. - if [ "$response" = "OK" ]; then + # Call the new private function to handle the API request. + # The 'rm' action, fulldomain, type 'txt' and txtvalue are passed. + if _mgwm_perform_api_request "rm" "$fulldomain" "txt" "$txtvalue"; then _info "TXT record for $fulldomain successfully removed via MGWM API." return 0 else - _err "mgwm_rm: Failed to remove TXT record for $fulldomain. Unexpected API Response: '$response'" + # Error message already logged by _mgwm_perform_api_request, but a specific one here helps. + _err "mgwm_rm: Failed to remove TXT record for $fulldomain." return 1 fi } - #################### Private functions below ################################## -# _mgwm_init_env() loads the mgw-media.de API credentials (customer number and hash) -# from environment variables or acme.sh's configuration, saves them, and -# prepares the global _H1 variable for Basic Authorization header. -_mgwm_init_env() { +# _mgwm_perform_api_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_perform_api_request() { + _action="$1" + _fulldomain="$2" + _type="$3" + _content="$4" + + _debug "Calling _mgwm_perform_api_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content" + + # --- Start of _mgwm_init_env logic (now embedded here) --- # 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)}" @@ -110,5 +88,25 @@ _mgwm_init_env() { _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 - return 0 + # --- End of _mgwm_init_env logic --- + + # Construct the API URL based on the action and provided parameters. + _request_url="${MGWM_API_BASE}.php?action=${_action}&fulldomain=${_fulldomain}&type=${_type}&content=${_content}" + _debug "Constructed MGWM 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 "MGWM API response for action '$_action': $response" + + # Check the API response for success. The API returns "OK" on success. + if [ "$response" = "OK" ]; then + _info "MGWM API action '$_action' for record '$_fulldomain' successful." + return 0 + else + _err "mgwm_perform_api_request: Failed API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'" + return 1 + fi } + +# The original _mgwm_init_env function is now removed as its logic is integrated into _mgwm_perform_api_request. From 503ca1e9c277721c095aee8b86a6f35c18498ff1 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:34:01 +0100 Subject: [PATCH 246/689] Change MGWM_API_BASE to use IP address --- dnsapi/dns_mgwm.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index ca48584c..5fa6c216 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -10,7 +10,8 @@ Options: Issues: github.com/acmesh-official/acme.sh ' # Base URL for the mgw-media.de API -MGWM_API_BASE="https://api.mgw-media.de/record" +#MGWM_API_BASE="https://api.mgw-media.de/record" +MGWM_API_BASE="http://217.114.220.70/record" ######## Public functions ##################### # This function is called by acme.sh to add a TXT record. dns_mgwm_add() { @@ -88,6 +89,7 @@ _mgwm_perform_api_request() { _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 + export _H2="Host: api.mgw-media.de" # --- End of _mgwm_init_env logic --- # Construct the API URL based on the action and provided parameters. From 95da407de86f37d021aa07d44010072c8da01327 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:02:51 +0100 Subject: [PATCH 247/689] Refactor DNS API script to use new request function --- dnsapi/dns_mgwm.sh | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index 5fa6c216..bb0cb650 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -10,9 +10,10 @@ Options: Issues: github.com/acmesh-official/acme.sh ' # Base URL for the mgw-media.de API -#MGWM_API_BASE="https://api.mgw-media.de/record" -MGWM_API_BASE="http://217.114.220.70/record" +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 @@ -23,12 +24,12 @@ dns_mgwm_add() { # Call the new private function to handle the API request. # The 'add' action, fulldomain, type 'txt' and txtvalue are passed. - if _mgwm_perform_api_request "add" "$fulldomain" "txt" "$txtvalue"; then - _info "TXT record for $fulldomain successfully added via MGWM API." + 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_perform_api_request, but a specific one here helps. + # 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 @@ -43,33 +44,32 @@ dns_mgwm_rm() { # Call the new private function to handle the API request. # The 'rm' action, fulldomain, type 'txt' and txtvalue are passed. - if _mgwm_perform_api_request "rm" "$fulldomain" "txt" "$txtvalue"; then - _info "TXT record for $fulldomain successfully removed via MGWM API." + 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_perform_api_request, but a specific one here helps. + # 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_perform_api_request() encapsulates the API call logic, including +# _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_perform_api_request() { +_mgwm_request() { _action="$1" _fulldomain="$2" _type="$3" _content="$4" - _debug "Calling _mgwm_perform_api_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content" + _debug "Calling _mgwm_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content" - # --- Start of _mgwm_init_env logic (now embedded here) --- # 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)}" @@ -89,26 +89,22 @@ _mgwm_perform_api_request() { _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 - export _H2="Host: api.mgw-media.de" - # --- End of _mgwm_init_env logic --- # Construct the API URL based on the action and provided parameters. - _request_url="${MGWM_API_BASE}.php?action=${_action}&fulldomain=${_fulldomain}&type=${_type}&content=${_content}" - _debug "Constructed MGWM API URL for action '$_action': ${_request_url}" + _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 "MGWM API response for action '$_action': $response" + _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 "MGWM API action '$_action' for record '$_fulldomain' successful." + _info "mgw-media.de API action '$_action' for record '$_fulldomain' successful." return 0 else - _err "mgwm_perform_api_request: Failed API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'" + _err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'" return 1 fi } - -# The original _mgwm_init_env function is now removed as its logic is integrated into _mgwm_perform_api_request. From 0d2955b48d51846bc86ed4259d679569e80dccf5 Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:11:40 +0100 Subject: [PATCH 248/689] Update documentation links in dns_mgwm.sh --- dnsapi/dns_mgwm.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index bb0cb650..28f1342d 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -3,11 +3,11 @@ # DNS provider information for acme.sh dns_mgwm_info='mgw-media.de Site: mgw-media.de -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mgwm +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: 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" From f142f37064c6d0ea837a77c4a08499a38d0cbeaa Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Sun, 7 Dec 2025 20:11:56 +0100 Subject: [PATCH 249/689] Remove DNS provider information comment Removed comment about DNS provider information. --- dnsapi/dns_mgwm.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index 28f1342d..618e90e2 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -1,6 +1,5 @@ #!/usr/bin/env sh # shellcheck disable=SC2034 -# DNS provider information for acme.sh dns_mgwm_info='mgw-media.de Site: mgw-media.de Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mgwm From 329dab9a67b48b43e31ab3b8ebb7acf511399b0a Mon Sep 17 00:00:00 2001 From: Gilles Filippini Date: Sun, 16 Nov 2025 15:07:15 +0100 Subject: [PATCH 250/689] Use '_mutable' functions for authentication variables Fixes #6081. --- dnsapi/dns_gandi_livedns.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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" From ad3783170e70653033f88de7ead7c44c990c9f9d Mon Sep 17 00:00:00 2001 From: "Markus G." <29913712+ufozone@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:31:40 +0100 Subject: [PATCH 251/689] Fix formatting issues in dns_mgwm.sh script --- dnsapi/dns_mgwm.sh | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh index 618e90e2..57679127 100644 --- a/dnsapi/dns_mgwm.sh +++ b/dnsapi/dns_mgwm.sh @@ -24,13 +24,13 @@ dns_mgwm_add() { # 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 + _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 + # 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. @@ -44,12 +44,12 @@ dns_mgwm_rm() { # 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 + _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 + # 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 ################################## @@ -100,10 +100,10 @@ _mgwm_request() { # 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 + _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 + _err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'" + return 1 fi } From e8708a748903f7d52ac07b3281755de5345dacd5 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 8 Dec 2025 21:12:49 +0100 Subject: [PATCH 252/689] fix solaris --- .github/workflows/DNS.yml | 4 +++- .github/workflows/Solaris.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index be9d3aae..ccce2ff6 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -441,7 +441,9 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' copyback: false - prepare: pkgutil -y -i socat + prepare: | + pkgutil -U + pkgutil -y -i socat run: | pkg set-mediator -v -I default@1.1 openssl export PATH=/usr/gnu/bin:$PATH diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 95bcd8d1..0ba3d2eb 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -66,7 +66,9 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" - prepare: pkgutil -y -i socat curl wget + prepare: | + pkgutil -U + pkgutil -y -i socat curl wget copyback: false run: | cd ../acmetest \ From 1413aa332bb48d6cb60a4ef7114fe0be57f3e303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Miri=C4=87?= <1009277+imiric@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:12:27 +0100 Subject: [PATCH 253/689] fix: update Exoscale DNS script This updates the Exoscale DNS script to work with v2 of their API. --- dnsapi/dns_exoscale.sh | 226 ++++++++++++++++++++++++----------------- 1 file changed, 132 insertions(+), 94 deletions(-) mode change 100755 => 100644 dnsapi/dns_exoscale.sh 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 + + url="${EXOSCALE_API}${path}" + expiration=$(_math "$(date +%s)" + 300) # 5m from now + + # 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" - - if [ "$token" ]; then - export _H2="X-DNS-Domain-Token: $token" - else - export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY" - fi + 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 } From e5dea48d3cc3bdb49696398e5d17edb515046279 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 9 Dec 2025 07:40:00 -0500 Subject: [PATCH 254/689] Retry pull request with HTTPS_INSECURE=1 --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 0765c697..7ae2f1cd 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -17,7 +17,7 @@ dns_qc_add() { fulldomain=$1 txtvalue=$2 - _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue." + _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)}" From 30639737441dc8ea33a5b1c9ffb53c9517713922 Mon Sep 17 00:00:00 2001 From: fratoro <7229526+fratoro@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:49:12 +0100 Subject: [PATCH 255/689] Update dns_cyon to use unique user-agent and all cookies --- dnsapi/dns_cyon.sh | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index a585e772..882ebc48 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,6 +161,13 @@ _cyon_login() { _get "https://my.cyon.ch/" >/dev/null + # 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 + # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request. # 2FA authentication with OTP? @@ -184,6 +197,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 +225,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}" From 70bc5a6fbac33463e2e22dcb21e29b639dd33ffd Mon Sep 17 00:00:00 2001 From: fratoro <7229526+fratoro@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:57:26 +0100 Subject: [PATCH 256/689] Update dns_cyon to use unique user-agent and all cookies --- dnsapi/dns_cyon.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index 882ebc48..0c74be2a 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -168,8 +168,6 @@ _cyon_login() { export _H2 fi - # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request. - # 2FA authentication with OTP? if [ -n "${CY_OTP_Secret}" ]; then _info " - Authorising with OTP code..." From 5017c12324967423cb44ae7d0b6d5b9ee354317d Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Thu, 11 Dec 2025 09:03:38 -0500 Subject: [PATCH 257/689] Trying verification again --- dnsapi/dns_qc.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 7ae2f1cd..2376d718 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -39,7 +39,7 @@ dns_qc_add() { _debug "First detect the root zone" if ! _get_root "$fulldomain"; then - _err "invalid domain" + _err "invalid domain during add" return 1 fi _debug _domain_id "$_domain_id" @@ -59,7 +59,7 @@ dns_qc_add() { # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) # _debug count "$count" # if [ "$count" = "0" ]; then - _info "Adding txt record" + _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" @@ -88,7 +88,7 @@ dns_qc_rm() { _debug "First detect the root zone" if ! _get_root "$fulldomain"; then - _err "invalid domain" + _err "invalid domain during rm" return 1 fi _debug _domain_id "$_domain_id" From 6f66e294defe1b0e1d5de1ff57cd596308ce013a Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Thu, 11 Dec 2025 15:43:15 -0500 Subject: [PATCH 258/689] Yet another try --- dns_qc.sh | 201 ----------------------------------------------- dnsapi/dns_qc.sh | 2 +- 2 files changed, 1 insertion(+), 202 deletions(-) delete mode 100755 dns_qc.sh diff --git a/dns_qc.sh b/dns_qc.sh deleted file mode 100755 index 78a243ae..00000000 --- a/dns_qc.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/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 - _savedomainconf QC_API_KEY "$QC_API_KEY" - else - _err "You didn't specify a QUIC.cloud are api key and email yet." - _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. - _savedomainconf QC_API_EMAIL "$QC_API_EMAIL" - - _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" - _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" - 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 - - 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." - else - record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//') - _debug "txt record_id" "$record_id" - - if [[ -z "$record_id" ]]; then - _err "Can not get txt record id to remove. Run in debug mode." - return 1 - fi - - 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" - fi - -} - -#################### Private functions below ################################# -#_acme-challenge.www.domain.com -#returns -#_sub_domain=_acme-challenge.www -#_domain=domain.com -#_domain_id=sdjkglgdfewsdfg -_get_root() { - domain=$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" - -} diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 2376d718..b7267f63 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -59,7 +59,7 @@ dns_qc_add() { # count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) # _debug count "$count" # if [ "$count" = "0" ]; then - _info "Adding txt record." + _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" From b4042d5ccb5082643f4e4eb3ac63bf9437205f4a Mon Sep 17 00:00:00 2001 From: as-kholin Date: Tue, 16 Dec 2025 14:51:42 -0500 Subject: [PATCH 259/689] Updated checks for empty parameters to actually trigger, and added a validation check against the omg.lol API to confirm address and apikey are good before proceeding --- dnsapi/dns_omglol.sh | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index df080bcf..ddde8f2d 100644 --- a/dnsapi/dns_omglol.sh +++ b/dnsapi/dns_omglol.sh @@ -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,48 @@ 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" + + export _H1=$(_createAuthHeader "$omg_apikey") + 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 From 4a7e5d07209fa3d50ac9ecf3b7df2febef903084 Mon Sep 17 00:00:00 2001 From: as-kholin Date: Tue, 16 Dec 2025 15:00:05 -0500 Subject: [PATCH 260/689] Updating Auth header to satisfy shellcheck --- dnsapi/dns_omglol.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index ddde8f2d..652cfd15 100644 --- a/dnsapi/dns_omglol.sh +++ b/dnsapi/dns_omglol.sh @@ -129,7 +129,8 @@ omg_testconnect() { _secure_debug2 "omg.lol API key" "$omg_apikey" _debug2 "omg.lol Address" "$omg_address" - export _H1=$(_createAuthHeader "$omg_apikey") + authheader="$(_createAuthHeader "$omg_apikey")" + export _H1="$authheader" endpoint="https://api.omg.lol/address/$omg_address/info" _debug2 "Endpoint for validation" "$endpoint" From 85ff92170b1810fe7005c65f240c2b34f0cfe468 Mon Sep 17 00:00:00 2001 From: as-kholin Date: Wed, 17 Dec 2025 17:15:10 -0500 Subject: [PATCH 261/689] Updated comment to be more clear on variable vs. definition --- dnsapi/dns_omglol.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index 652cfd15..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. 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 + 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 ' From 6ca19fb003b3393b066c5b8453e0cde8ad20e5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Lal?= Date: Thu, 18 Dec 2025 09:50:31 +0100 Subject: [PATCH 262/689] Ensure ssh.sh sets 600 permissions on keyfile --- deploy/ssh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index c66e2e19..3039c4ea 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -239,7 +239,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi else # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" + _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE; chmod 600 $DEPLOY_SSH_KEYFILE;" _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then From e3b1bccb6aa5e1cda2a1d65c5bae66cec54edb30 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Thu, 18 Dec 2025 16:00:55 +0000 Subject: [PATCH 263/689] Fixes to support INWX again Fixes #6688 --- dnsapi/dns_inwx.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 808fc3a9..89ab67ee 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -125,7 +125,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 +324,7 @@ _inwx_delete_record() { id - %s + %s @@ -362,7 +362,7 @@ _inwx_update_record() { id - %s + %s From 00aaed1b14aaaf15a5c548f36e9b60ca20b7d069 Mon Sep 17 00:00:00 2001 From: Sergey Parfenov Date: Fri, 19 Dec 2025 18:05:01 +0300 Subject: [PATCH 264/689] Fix strongswan deploy hook Make it more resistant to deploy hooks api change by passing custom arguments first --- deploy/strongswan.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/deploy/strongswan.sh b/deploy/strongswan.sh index 14567d17..80353c54 100644 --- a/deploy/strongswan.sh +++ b/deploy/strongswan.sh @@ -33,7 +33,7 @@ strongswan_deploy() { return 1 fi _info _confdir "${_confdir}" - __deploy_cert "$@" "stroke" "${_confdir}" + __deploy_cert "stroke" "${_confdir}" "$@" ${_ipsec} reload fi # For modern vici mode @@ -50,7 +50,7 @@ strongswan_deploy() { _err "no swanctl config dir is found" return 1 fi - __deploy_cert "$@" "vici" "${_confdir}" + __deploy_cert "vici" "${_confdir}" "$@" ${_swanctl} --load-creds fi if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then @@ -63,13 +63,13 @@ strongswan_deploy() { #################### Private functions below ################################## __deploy_cert() { - _cdomain="${1}" - _ckey="${2}" - _ccert="${3}" - _cca="${4}" - _cfullchain="${5}" - _swan_mode="${6}" - _confdir="${7}" + _swan_mode="${1}" + _confdir="${2}" + _cdomain="${3}" + _ckey="${4}" + _ccert="${5}" + _cca="${6}" + _cfullchain="${7}" _debug _cdomain "${_cdomain}" _debug _ckey "${_ckey}" _debug _ccert "${_ccert}" From 987882ea37f28b581b44614d93b0a9ab2f9823e4 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 17:49:24 +0100 Subject: [PATCH 265/689] fix delete --- dnsapi/dns_inwx.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 89ab67ee..afe2c465 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -110,11 +110,17 @@ dns_inwx_rm() { %s + + content + + %s + + - ' "$_domain" "$_sub_domain") + ' "$_domain" "$_sub_domain" "$txtvalue") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" if ! _contains "$response" "Command completed successfully"; then From 1c65c04b54b56a24c6f45d122b594c126b6a45c2 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 17:51:13 +0100 Subject: [PATCH 266/689] update dns_inwx_info --- dnsapi/dns_inwx.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index afe2c465..d83e08d6 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx Options: INWX_User Username INWX_Password Password + INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional) ' # Dependencies: From 27ebf09c5c3834011e29d5adf0f12d0f43540107 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:02:18 +0100 Subject: [PATCH 267/689] Improve _htmlEscape function robustness by using printf instead of echo small commit to trigger github actions --- dnsapi/dns_inwx.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index d83e08d6..a7ce9769 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -172,10 +172,10 @@ _inwx_check_cookie() { _htmlEscape() { _s="$1" - _s=$(echo "$_s" | sed "s/&/&/g") - _s=$(echo "$_s" | sed "s//\>/g") - _s=$(echo "$_s" | sed 's/"/\"/g') + _s=$(printf '%s' "$_s" | sed "s/&/&/g") + _s=$(printf '%s' "$_s" | sed "s//\>/g") + _s=$(printf '%s' "$_s" | sed 's/"/\"/g') printf -- %s "$_s" } From a5ad15be0212cefc2ffa99bcd4c611d15576d4ee Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:07:46 +0100 Subject: [PATCH 268/689] Add oathtool to Docker job for 2FA support --- .github/workflows/DNS.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index ccce2ff6..8f7ebd56 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -67,6 +67,8 @@ jobs: TokenName5: ${{ secrets.TokenName5}} steps: - uses: actions/checkout@v4 + - name: Install oathtool + run: sudo apt-get update && sudo apt-get install -y oathtool - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file @@ -116,7 +118,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install tools - run: brew install socat + run: brew install socat oath-toolkit - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest @@ -173,7 +175,7 @@ jobs: shell: cmd - name: Install cygwin additional packages run: | - C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git + C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,oath-toolkit shell: cmd - name: Set ENV shell: cmd @@ -230,7 +232,7 @@ jobs: - uses: vmactions/freebsd-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - prepare: pkg install -y socat curl + prepare: pkg install -y socat curl oath-toolkit usesh: true copyback: false run: | @@ -281,7 +283,7 @@ jobs: - uses: vmactions/openbsd-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - prepare: pkg_add socat curl libiconv + prepare: pkg_add socat curl libiconv oath-toolkit usesh: true copyback: false run: | @@ -333,7 +335,7 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | - /usr/sbin/pkg_add curl socat + /usr/sbin/pkg_add curl socat oath-toolkit usesh: true copyback: false run: | @@ -385,7 +387,7 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | - pkg install -y curl socat libnghttp2 + pkg install -y curl socat libnghttp2 oath-toolkit usesh: true copyback: false run: | @@ -443,7 +445,7 @@ jobs: copyback: false prepare: | pkgutil -U - pkgutil -y -i socat + pkgutil -y -i socat oathtool run: | pkg set-mediator -v -I default@1.1 openssl export PATH=/usr/gnu/bin:$PATH @@ -494,7 +496,7 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' copyback: false - prepare: pkg install socat + prepare: pkg install socat oath-toolkit run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" From 0e5aab346f7958fdbe35ffa6a133614a59826a3f Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:17:05 +0100 Subject: [PATCH 269/689] Add oathtool to acmetest Docker image for 2FA support --- .github/workflows/DNS.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 8f7ebd56..781c13fb 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -67,10 +67,15 @@ jobs: TokenName5: ${{ secrets.TokenName5}} steps: - uses: actions/checkout@v4 - - name: Install oathtool - run: sudo apt-get update && sudo apt-get install -y oathtool - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Ensure oathtool in test container + run: | + cd ../acmetest + # Add oathtool installation to acmetest Dockerfile if it exists + if [ -f Dockerfile ]; then + sed -i 's/tzdata/tzdata oath-toolkit-oathtool/g' Dockerfile + fi - name: Set env file run: | cd ../acmetest From e92d0a74926855b607fdfb948eafe6bf44e0fdfa Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:20:56 +0100 Subject: [PATCH 270/689] Fix: Install oathtool at runtime in test container --- .github/workflows/DNS.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 781c13fb..e6319c09 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -69,13 +69,11 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Ensure oathtool in test container + - name: Install oathtool in test container run: | cd ../acmetest - # Add oathtool installation to acmetest Dockerfile if it exists - if [ -f Dockerfile ]; then - sed -i 's/tzdata/tzdata oath-toolkit-oathtool/g' Dockerfile - fi + # Modify letest.sh to install oathtool at runtime + sed -i '/TEST_LOCAL skip setup/a apt-get update -qq && apt-get install -y -qq oathtool > /dev/null 2>&1 || true' letest.sh - name: Set env file run: | cd ../acmetest From b6523c230110f5477f3d4a45a020e4b99d230e69 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:42:28 +0100 Subject: [PATCH 271/689] Install oathtool in container via _setup function --- .github/workflows/DNS.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index e6319c09..64fa85d2 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -69,11 +69,11 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Install oathtool in test container + - name: Patch acmetest to install oathtool in container run: | cd ../acmetest - # Modify letest.sh to install oathtool at runtime - sed -i '/TEST_LOCAL skip setup/a apt-get update -qq && apt-get install -y -qq oathtool > /dev/null 2>&1 || true' letest.sh + # Add oathtool installation before the test runs inside container + sed -i '/^_setup() {$/a \ if command -v apt-get >/dev/null 2>&1; then\n apt-get update -qq && apt-get install -y -qq oathtool >/dev/null 2>&1 || true\n fi' letest.sh - name: Set env file run: | cd ../acmetest From cba0ff832116b2fc322c3dd51401322789a62d4a Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 19:09:23 +0100 Subject: [PATCH 272/689] Add oathtool to ubuntu package list in plat.conf --- .github/workflows/DNS.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 64fa85d2..84cebf94 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -69,11 +69,11 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Patch acmetest to install oathtool in container + - name: Add oathtool to ubuntu package list run: | cd ../acmetest - # Add oathtool installation before the test runs inside container - sed -i '/^_setup() {$/a \ if command -v apt-get >/dev/null 2>&1; then\n apt-get update -qq && apt-get install -y -qq oathtool >/dev/null 2>&1 || true\n fi' letest.sh + # Add oathtool to the ubuntu platform package list + sed -i 's/unzip,openssl,cron,socat,curl,idn,wget/unzip,openssl,cron,socat,curl,idn,wget,oathtool/' plat.conf - name: Set env file run: | cd ../acmetest From 65892453be0cff43cf1b6f996f5cbc2707d540d7 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 19:23:13 +0100 Subject: [PATCH 273/689] take a fork of acmetest --- .github/workflows/DNS.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 84cebf94..5650c9e8 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -68,12 +68,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Clone acmetest - run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Add oathtool to ubuntu package list - run: | - cd ../acmetest - # Add oathtool to the ubuntu platform package list - sed -i 's/unzip,openssl,cron,socat,curl,idn,wget/unzip,openssl,cron,socat,curl,idn,wget,oathtool/' plat.conf + run: cd .. && git clone --depth=1 https://github.com/flybyray/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file run: | cd ../acmetest From 3fb4c313ec3d28967ed22de769872ef8336d40a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E6=B6=9B?= Date: Fri, 19 Dec 2025 16:41:22 +0800 Subject: [PATCH 274/689] Support list IPv6 address certificate --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index ba0f62bc..053f16db 100755 --- a/acme.sh +++ b/acme.sh @@ -5840,7 +5840,7 @@ list() { if [ -z "$_domain" ]; then printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew" fi - for di in "${CERT_HOME}"/*.*/; do + for di in "${CERT_HOME}"/{*.*,*:*}/; do d=$(basename "$di") _debug d "$d" ( From 1b2630dc0d4030f4507bcfeba60bdc2e102c1410 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Sat, 20 Dec 2025 13:26:38 +0100 Subject: [PATCH 275/689] stop using oathtool --- .github/workflows/DNS.yml | 2 +- dnsapi/dns_inwx.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 5650c9e8..39d2a121 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -68,7 +68,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Clone acmetest - run: cd .. && git clone --depth=1 https://github.com/flybyray/acmetest.git && cp -r acme.sh acmetest/ + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file run: | cd ../acmetest diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index a7ce9769..08809bf0 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx Options: INWX_User Username INWX_Password Password - INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional) + INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional requires oathtool) ' # Dependencies: From 5fb42b7339b82d3397b2003dedd3302b439d745c Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Sat, 20 Dec 2025 14:53:01 +0100 Subject: [PATCH 276/689] remove prior additions which tried to use oathtool in tests --- .github/workflows/DNS.yml | 16 ++++++++-------- dnsapi/dns_inwx.sh | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 39d2a121..ccce2ff6 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -116,7 +116,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install tools - run: brew install socat oath-toolkit + run: brew install socat - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest @@ -173,7 +173,7 @@ jobs: shell: cmd - name: Install cygwin additional packages run: | - C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,oath-toolkit + C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git shell: cmd - name: Set ENV shell: cmd @@ -230,7 +230,7 @@ jobs: - uses: vmactions/freebsd-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - prepare: pkg install -y socat curl oath-toolkit + prepare: pkg install -y socat curl usesh: true copyback: false run: | @@ -281,7 +281,7 @@ jobs: - uses: vmactions/openbsd-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - prepare: pkg_add socat curl libiconv oath-toolkit + prepare: pkg_add socat curl libiconv usesh: true copyback: false run: | @@ -333,7 +333,7 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | - /usr/sbin/pkg_add curl socat oath-toolkit + /usr/sbin/pkg_add curl socat usesh: true copyback: false run: | @@ -385,7 +385,7 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | - pkg install -y curl socat libnghttp2 oath-toolkit + pkg install -y curl socat libnghttp2 usesh: true copyback: false run: | @@ -443,7 +443,7 @@ jobs: copyback: false prepare: | pkgutil -U - pkgutil -y -i socat oathtool + pkgutil -y -i socat run: | pkg set-mediator -v -I default@1.1 openssl export PATH=/usr/gnu/bin:$PATH @@ -494,7 +494,7 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' copyback: false - prepare: pkg install socat oath-toolkit + prepare: pkg install socat run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 08809bf0..2cf91404 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -172,10 +172,10 @@ _inwx_check_cookie() { _htmlEscape() { _s="$1" - _s=$(printf '%s' "$_s" | sed "s/&/&/g") - _s=$(printf '%s' "$_s" | sed "s//\>/g") - _s=$(printf '%s' "$_s" | sed 's/"/\"/g') + _s=$(echo "$_s" | sed "s/&/&/g") + _s=$(echo "$_s" | sed "s//\>/g") + _s=$(echo "$_s" | sed 's/"/\"/g') printf -- %s "$_s" } From f85de2b0d3560bab9fb553b53e9564edcad1b01d Mon Sep 17 00:00:00 2001 From: Erfan Gholizade Date: Wed, 3 Dec 2025 18:26:58 +0330 Subject: [PATCH 277/689] Added Sotoon dns api handle the case of metadata 404 in old api domains fix index of domain start fix shfmt Revert "handle the case of metadata 404 in old api domains" This reverts commit 9fe4616664b897c9891271006e7489b10bb818ca. fix 404 on dot ad hyphen fix shfmt --- dnsapi/dns_sotoon.sh | 319 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 dnsapi/dns_sotoon.sh diff --git a/dnsapi/dns_sotoon.sh b/dnsapi/dns_sotoon.sh new file mode 100644 index 00000000..4a0fc034 --- /dev/null +++ b/dnsapi/dns_sotoon.sh @@ -0,0 +1,319 @@ +#!/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 + Sotoon_WorkspaceName Workspace Name +Issues: github.com/acmesh-official/acme.sh/issues/6656 +Author: Erfan Gholizade +' + +SOTOON_API_URL="https://api.sotoon.ir/delivery/v2/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)}" + Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" + + 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 + if [ -z "$Sotoon_WorkspaceName" ]; then + _err_sotoon "You didn't specify \"Sotoon_WorkspaceName\" Workspace Name 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" + _saveaccountconf_mutable Sotoon_WorkspaceName "$Sotoon_WorkspaceName" + + _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)}" + Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" + + _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" + _debug_sotoon "Sotoon WorkspaceName: $Sotoon_WorkspaceName" + + 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, and Sotoon_WorkspaceName" + 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/namespaces/$Sotoon_WorkspaceName/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]" "$@" +} From 03d8d3bc1b20fe356feb823069424868e149898e Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:29:50 +0100 Subject: [PATCH 278/689] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 347f34d1..af722297 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -171,7 +171,7 @@ _hostup_detect_zone() { return 1 fi - _domain_candidate="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]')" + _domain_candidate="$(_lower_case "$fulldomain")" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -361,7 +361,7 @@ _hostup_json_extract() { input="${2:-$line}" # First try to extract quoted values (strings) - quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | head -n1)" + quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)" if [ -n "$quoted_match" ]; then printf "%s" "$quoted_match" | cut -d : -f2- | @@ -372,7 +372,7 @@ _hostup_json_extract() { fi # Fallback for unquoted values (e.g., numeric IDs) - unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | head -n1)" + unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)" if [ -n "$unquoted_match" ]; then printf "%s" "$unquoted_match" | cut -d : -f2- | @@ -392,7 +392,7 @@ _hostup_record_key() { zone_id="$1" domain="$2" safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" - safe_domain="$(printf "%s" "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" + safe_domain="$(printf "%s" "$(_lower_case "$domain")" | sed 's/[^a-z0-9]/_/g')" printf "%s_%s" "$safe_zone" "$safe_domain" } @@ -425,7 +425,7 @@ _hostup_extract_record_id() { return 0 fi - printf "%s" "$1" | _egrep_o '"id":[0-9]+' | head -n1 | cut -d: -f2 + printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2 } _hostup_delete_record_by_id() { From e321b3c75c0eabe6341f9d7b5643a9e9ef4019a6 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:53:21 +0100 Subject: [PATCH 279/689] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index af722297..b3211069 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -3,7 +3,7 @@ dns_hostup_info='HostUp DNS Site: hostup.se -Docs: https://hostup.se/en/support/api-autentisering/ +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). @@ -171,7 +171,7 @@ _hostup_detect_zone() { return 1 fi - _domain_candidate="$(_lower_case "$fulldomain")" + _domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -392,7 +392,7 @@ _hostup_record_key() { zone_id="$1" domain="$2" safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" - safe_domain="$(printf "%s" "$(_lower_case "$domain")" | sed 's/[^a-z0-9]/_/g')" + safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')" printf "%s_%s" "$safe_zone" "$safe_domain" } From 0eb40c6ce6ce5c0bf17f39893e1b6ea4056ae362 Mon Sep 17 00:00:00 2001 From: DreamSlave <50408721+mq00fc@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:00:22 +0800 Subject: [PATCH 280/689] Update timestamp variable in ali_cdn.sh --- deploy/ali_cdn.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh index 70a2e532..3c28674e 100644 --- a/deploy/ali_cdn.sh +++ b/deploy/ali_cdn.sh @@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_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=2018-05-10' } From fd6a14de8a69193a5367dfaae71857d804a83528 Mon Sep 17 00:00:00 2001 From: DreamSlave <50408721+mq00fc@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:00:31 +0800 Subject: [PATCH 281/689] Update timestamp function in ali_dcdn.sh --- deploy/ali_dcdn.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh index 14ac500a..27d3a726 100644 --- a/deploy/ali_dcdn.sh +++ b/deploy/ali_dcdn.sh @@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_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=2018-01-15' } From 1d26d4fc913abf6358e449fc4713b16cc0781b1f Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Mon, 22 Dec 2025 16:42:26 -0500 Subject: [PATCH 282/689] Detect missing jq --- dnsapi/dns_qc.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index b7267f63..81a1e636 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -18,6 +18,11 @@ dns_qc_add() { txtvalue=$2 _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" + if ! _exists jq; then + _err "jq not found" + return 1 + fi + QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}" QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" From 94783f46ad0c512b74a4f3ac760860c4ef0c2d61 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 23 Dec 2025 07:53:33 -0500 Subject: [PATCH 283/689] Retry to pass workflow --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 81a1e636..aa58d0dc 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -19,7 +19,7 @@ dns_qc_add() { _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" if ! _exists jq; then - _err "jq not found" + _err "In dns_qc jq not found" return 1 fi From f1aac43f0f9c701e47b8ab52d64495cd9b1d0287 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 23 Dec 2025 09:10:49 -0500 Subject: [PATCH 284/689] Retry for workflow --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index aa58d0dc..ed784f28 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -19,7 +19,7 @@ dns_qc_add() { _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" if ! _exists jq; then - _err "In dns_qc jq not found" + _err "In dns_qc jq not found." return 1 fi From 383557df61d616f0b5d1e01c97bba48d2cb74553 Mon Sep 17 00:00:00 2001 From: Erfan Gholizade Date: Wed, 24 Dec 2025 17:19:17 +0330 Subject: [PATCH 285/689] improve: change sotoon api to v2.1 with simplification --- dnsapi/dns_sotoon.sh | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/dnsapi/dns_sotoon.sh b/dnsapi/dns_sotoon.sh index 4a0fc034..b94a220f 100644 --- a/dnsapi/dns_sotoon.sh +++ b/dnsapi/dns_sotoon.sh @@ -6,12 +6,11 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon Options: Sotoon_Token API Token Sotoon_WorkspaceUUID Workspace UUID - Sotoon_WorkspaceName Workspace Name Issues: github.com/acmesh-official/acme.sh/issues/6656 Author: Erfan Gholizade ' -SOTOON_API_URL="https://api.sotoon.ir/delivery/v2/global" +SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/global" ######## Public functions ##################### @@ -25,7 +24,6 @@ dns_sotoon_add() { Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" - Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" if [ -z "$Sotoon_Token" ]; then _err_sotoon "You didn't specify \"Sotoon_Token\" token yet." @@ -37,16 +35,10 @@ dns_sotoon_add() { _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" return 1 fi - if [ -z "$Sotoon_WorkspaceName" ]; then - _err_sotoon "You didn't specify \"Sotoon_WorkspaceName\" Workspace Name 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" - _saveaccountconf_mutable Sotoon_WorkspaceName "$Sotoon_WorkspaceName" _debug_sotoon "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -130,7 +122,6 @@ dns_sotoon_rm() { Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" - Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" _debug_sotoon "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -197,7 +188,6 @@ _get_root() { _debug_sotoon "Getting root domain for: $domain" _debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID" - _debug_sotoon "Sotoon WorkspaceName: $Sotoon_WorkspaceName" while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) @@ -212,7 +202,7 @@ _get_root() { _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, and Sotoon_WorkspaceName" + _err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID" return 1 fi @@ -271,7 +261,7 @@ _sotoon_rest() { token_trimmed=$(echo "$Sotoon_Token" | tr -d '"') # Construct the API endpoint - _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/namespaces/$Sotoon_WorkspaceName/domainzones" + _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/domainzones" if [ -n "$resource_id" ]; then _api_path="$_api_path/$resource_id" From f4a575fee15054eec1b02c82836ed41053a52e81 Mon Sep 17 00:00:00 2001 From: jwaterwater Date: Thu, 25 Dec 2025 14:48:44 +0800 Subject: [PATCH 286/689] bug fixed --- deploy/ali_cdn.sh | 2 +- deploy/ali_dcdn.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh index 70a2e532..3c28674e 100644 --- a/deploy/ali_cdn.sh +++ b/deploy/ali_cdn.sh @@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_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=2018-05-10' } diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh index 14ac500a..27d3a726 100644 --- a/deploy/ali_dcdn.sh +++ b/deploy/ali_dcdn.sh @@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_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=2018-01-15' } From b9c877adb973d6efa128664ded3761aee1fa0872 Mon Sep 17 00:00:00 2001 From: xiagw Date: Sat, 27 Dec 2025 14:33:03 +0800 Subject: [PATCH 287/689] fix: update timestamp variable for CDN and DCDN SSL certificate queries --- deploy/ali_cdn.sh | 2 +- deploy/ali_dcdn.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh index 70a2e532..3c28674e 100644 --- a/deploy/ali_cdn.sh +++ b/deploy/ali_cdn.sh @@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_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=2018-05-10' } diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh index 14ac500a..27d3a726 100644 --- a/deploy/ali_dcdn.sh +++ b/deploy/ali_dcdn.sh @@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_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=2018-01-15' } From 5a730bf00d705c7914dc630ecc5149b3bd9ad87e Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:42:11 +0100 Subject: [PATCH 288/689] implemented checking deploy file --- deploy/multideploy.sh | 114 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 deploy/multideploy.sh diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh new file mode 100644 index 00000000..ba57afa1 --- /dev/null +++ b/deploy/multideploy.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env sh + +# MULTIDEPLOY_CONFIG="default" + +######## Public functions ##################### + +MULTIDEPLOY_VERSION="1.0" +MULTIDEPLOY_FILENAME="multideploy.yaml" + +# domain keyfile certfile cafile fullchain pfx +multideploy_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + _cpfx="$6" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + _debug _cpfx "$_cpfx" + + DOMAIN_DIR=$_cdomain + if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then + DOMAIN_DIR="$DOMAIN_DIR"_ecc + fi + _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" + + _preprocess_deployfile "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" + + MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" + if [ -z "$MULTIDEPLOY_CONFIG" ]; then + MULTIDEPLOY_CONFIG="default" + _info "MULTIDEPLOY_CONFIG is not set, so I will use 'default'." + else + _savedeployconf "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" + _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" + fi + + # TODO: Deploy to services +} + +#################### Private functions below ##################### + +# deploy_filepath +_preprocess_deployfile() { + deploy_file="$1" + + # Check if yq is installed + if ! command -v yq >/dev/null 2>&1; then + _err "yq is not installed! Please install yq and try again." + return 1 + fi + + # Check if deploy file exists and create a default template if not + if [ -f "$deploy_file" ]; then + _debug3 "Deploy file found." + _check_deployfile "$deploy_file" "$MULTIDEPLOY_CONFIG" + else + # TODO: Replace URL with wiki link + _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." + return 1 + fi +} + +# deploy_filepath deploy_config +_check_deployfile() { + deploy_file="$1" + deploy_config="$3" + + # Check version + deploy_file_version=$(yq '.version' "$deploy_file") + if [ "$MULTIDEPLOY_VERSION" != "$deploy_file_version" ]; then + _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $deploy_file_version." + return 1 + fi + + # Check if config exists + if ! yq e ".configs[] | select(.name == \"$deploy_config\")" "$deploy_file" >/dev/null; then + _err "Config '$deploy_config' not found." + return 1 + fi + + # Extract all services from config + services=$(yq e ".configs[] | select(.name == \"$deploy_config\").services[]" "$deploy_file") + + if [ -z "$services" ]; then + _err "Config '$deploy_config' does not have any services to deploy to." + return 1 + fi + + # Check if extracted services exist in services list + for service in $services; do + if ! yq e ".services[] | select(.name == \"$service\")" "$deploy_file" >/dev/null; then + _err "Service '$service' not found." + return 1 + fi + + # Check if service has hook + if ! yq e ".services[] | select(.name == \"$service\").hook" "$deploy_file" >/dev/null; then + _err "Service '$service' does not have a hook." + return 1 + fi + + # Check if service has environment + if ! yq e ".services[] | select(.name == \"$service\").environment" "$deploy_file" >/dev/null; then + _err "Service '$service' does not an environment." + return 1 + fi + done +} From 3c184486c3b0749bfa6d1812071d3f099a155815 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Thu, 20 Feb 2025 21:56:28 +0100 Subject: [PATCH 289/689] fixed indents --- deploy/multideploy.sh | 158 +++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index ba57afa1..08e0aba6 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -9,106 +9,106 @@ MULTIDEPLOY_FILENAME="multideploy.yaml" # domain keyfile certfile cafile fullchain pfx multideploy_deploy() { - _cdomain="$1" - _ckey="$2" - _ccert="$3" - _cca="$4" - _cfullchain="$5" - _cpfx="$6" + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + _cpfx="$6" - _debug _cdomain "$_cdomain" - _debug _ckey "$_ckey" - _debug _ccert "$_ccert" - _debug _cca "$_cca" - _debug _cfullchain "$_cfullchain" - _debug _cpfx "$_cpfx" + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + _debug _cpfx "$_cpfx" - DOMAIN_DIR=$_cdomain - if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then - DOMAIN_DIR="$DOMAIN_DIR"_ecc - fi - _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" + DOMAIN_DIR=$_cdomain + if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then + DOMAIN_DIR="$DOMAIN_DIR"_ecc + fi + _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" - _preprocess_deployfile "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" + _preprocess_deployfile "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" - MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" - if [ -z "$MULTIDEPLOY_CONFIG" ]; then - MULTIDEPLOY_CONFIG="default" - _info "MULTIDEPLOY_CONFIG is not set, so I will use 'default'." - else - _savedeployconf "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" - _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" - fi + MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" + if [ -z "$MULTIDEPLOY_CONFIG" ]; then + MULTIDEPLOY_CONFIG="default" + _info "MULTIDEPLOY_CONFIG is not set, so I will use 'default'." + else + _savedeployconf "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" + _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" + fi - # TODO: Deploy to services + # TODO: Deploy to services } #################### Private functions below ##################### # deploy_filepath _preprocess_deployfile() { - deploy_file="$1" + deploy_file="$1" - # Check if yq is installed - if ! command -v yq >/dev/null 2>&1; then - _err "yq is not installed! Please install yq and try again." - return 1 - fi + # Check if yq is installed + if ! command -v yq >/dev/null 2>&1; then + _err "yq is not installed! Please install yq and try again." + return 1 + fi - # Check if deploy file exists and create a default template if not - if [ -f "$deploy_file" ]; then - _debug3 "Deploy file found." - _check_deployfile "$deploy_file" "$MULTIDEPLOY_CONFIG" - else - # TODO: Replace URL with wiki link - _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." - return 1 - fi + # Check if deploy file exists and create a default template if not + if [ -f "$deploy_file" ]; then + _debug3 "Deploy file found." + _check_deployfile "$deploy_file" "$MULTIDEPLOY_CONFIG" + else + # TODO: Replace URL with wiki link + _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." + return 1 + fi } # deploy_filepath deploy_config _check_deployfile() { - deploy_file="$1" - deploy_config="$3" + deploy_file="$1" + deploy_config="$3" - # Check version - deploy_file_version=$(yq '.version' "$deploy_file") - if [ "$MULTIDEPLOY_VERSION" != "$deploy_file_version" ]; then - _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $deploy_file_version." - return 1 + # Check version + deploy_file_version=$(yq '.version' "$deploy_file") + if [ "$MULTIDEPLOY_VERSION" != "$deploy_file_version" ]; then + _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $deploy_file_version." + return 1 + fi + + # Check if config exists + if ! yq e ".configs[] | select(.name == \"$deploy_config\")" "$deploy_file" >/dev/null; then + _err "Config '$deploy_config' not found." + return 1 + fi + + # Extract all services from config + services=$(yq e ".configs[] | select(.name == \"$deploy_config\").services[]" "$deploy_file") + + if [ -z "$services" ]; then + _err "Config '$deploy_config' does not have any services to deploy to." + return 1 + fi + + # Check if extracted services exist in services list + for service in $services; do + if ! yq e ".services[] | select(.name == \"$service\")" "$deploy_file" >/dev/null; then + _err "Service '$service' not found." + return 1 fi - # Check if config exists - if ! yq e ".configs[] | select(.name == \"$deploy_config\")" "$deploy_file" >/dev/null; then - _err "Config '$deploy_config' not found." - return 1 + # Check if service has hook + if ! yq e ".services[] | select(.name == \"$service\").hook" "$deploy_file" >/dev/null; then + _err "Service '$service' does not have a hook." + return 1 fi - # Extract all services from config - services=$(yq e ".configs[] | select(.name == \"$deploy_config\").services[]" "$deploy_file") - - if [ -z "$services" ]; then - _err "Config '$deploy_config' does not have any services to deploy to." - return 1 + # Check if service has environment + if ! yq e ".services[] | select(.name == \"$service\").environment" "$deploy_file" >/dev/null; then + _err "Service '$service' does not an environment." + return 1 fi - - # Check if extracted services exist in services list - for service in $services; do - if ! yq e ".services[] | select(.name == \"$service\")" "$deploy_file" >/dev/null; then - _err "Service '$service' not found." - return 1 - fi - - # Check if service has hook - if ! yq e ".services[] | select(.name == \"$service\").hook" "$deploy_file" >/dev/null; then - _err "Service '$service' does not have a hook." - return 1 - fi - - # Check if service has environment - if ! yq e ".services[] | select(.name == \"$service\").environment" "$deploy_file" >/dev/null; then - _err "Service '$service' does not an environment." - return 1 - fi - done + done } From b2eb1d2bbc8fed4f940afa1fb20a4e0beb33bf3a Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Thu, 20 Feb 2025 23:02:56 +0100 Subject: [PATCH 290/689] refactored getting services --- deploy/multideploy.sh | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 08e0aba6..2a83a19f 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -85,7 +85,7 @@ _check_deployfile() { fi # Extract all services from config - services=$(yq e ".configs[] | select(.name == \"$deploy_config\").services[]" "$deploy_file") + services=$(_get_services_list "$deploy_file" "$deploy_config") if [ -z "$services" ]; then _err "Config '$deploy_config' does not have any services to deploy to." @@ -112,3 +112,29 @@ _check_deployfile() { fi done } + +# deploy_filepath deploy_config +_get_services_list() { + deploy_file="$1" + deploy_config="$2" + + services=$(yq e ".configs[] | select(.name == \"$deploy_config\").services[]" "$deploy_file") + echo "$services" +} + +# deploy_filepath service_names +_get_full_services_list() { + deploy_file="$1" + shift + service_names="$*" + + full_services="" + for service in $service_names; do + full_service=$(yq e ".services[] | select(.name == \"$service\")" "$deploy_file") + full_services="$full_services +$full_service" + done + + echo "$full_services" +} + From 0ed5e21232ddcf821f47a1bd223fd05511494775 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:49:08 +0100 Subject: [PATCH 291/689] fixed formatting and private var names --- deploy/multideploy.sh | 68 +++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 2a83a19f..245c95a6 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -29,7 +29,7 @@ multideploy_deploy() { fi _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" - _preprocess_deployfile "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" + _preprocess_deployfile "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" || return 1 MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" if [ -z "$MULTIDEPLOY_CONFIG" ]; then @@ -47,7 +47,7 @@ multideploy_deploy() { # deploy_filepath _preprocess_deployfile() { - deploy_file="$1" + _deploy_file="$1" # Check if yq is installed if ! command -v yq >/dev/null 2>&1; then @@ -56,9 +56,9 @@ _preprocess_deployfile() { fi # Check if deploy file exists and create a default template if not - if [ -f "$deploy_file" ]; then + if [ -f "$_deploy_file" ]; then _debug3 "Deploy file found." - _check_deployfile "$deploy_file" "$MULTIDEPLOY_CONFIG" + _check_deployfile "$_deploy_file" "$MULTIDEPLOY_CONFIG" else # TODO: Replace URL with wiki link _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." @@ -66,48 +66,48 @@ _preprocess_deployfile() { fi } -# deploy_filepath deploy_config +# deploy_filepath _deploy_config _check_deployfile() { - deploy_file="$1" - deploy_config="$3" + _deploy_file="$1" + _deploy_config="$3" # Check version - deploy_file_version=$(yq '.version' "$deploy_file") - if [ "$MULTIDEPLOY_VERSION" != "$deploy_file_version" ]; then - _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $deploy_file_version." + _deploy_file_version=$(yq '.version' "$_deploy_file") + if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then + _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." return 1 fi # Check if config exists - if ! yq e ".configs[] | select(.name == \"$deploy_config\")" "$deploy_file" >/dev/null; then - _err "Config '$deploy_config' not found." + if ! yq e ".configs[] | select(.name == \"$_deploy_config\")" "$_deploy_file" >/dev/null; then + _err "Config '$_deploy_config' not found." return 1 fi # Extract all services from config - services=$(_get_services_list "$deploy_file" "$deploy_config") + _services=$(_get_services_list "$_deploy_file" "$_deploy_config") - if [ -z "$services" ]; then - _err "Config '$deploy_config' does not have any services to deploy to." + if [ -z "$_services" ]; then + _err "Config '$_deploy_config' does not have any services to deploy to." return 1 fi # Check if extracted services exist in services list - for service in $services; do - if ! yq e ".services[] | select(.name == \"$service\")" "$deploy_file" >/dev/null; then - _err "Service '$service' not found." + for _service in $_services; do + if ! yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file" >/dev/null; then + _err "Service '$_service' not found." return 1 fi # Check if service has hook - if ! yq e ".services[] | select(.name == \"$service\").hook" "$deploy_file" >/dev/null; then - _err "Service '$service' does not have a hook." + if ! yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file" >/dev/null; then + _err "Service '$_service' does not have a hook." return 1 fi # Check if service has environment - if ! yq e ".services[] | select(.name == \"$service\").environment" "$deploy_file" >/dev/null; then - _err "Service '$service' does not an environment." + if ! yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file" >/dev/null; then + _err "Service '$_service' does not an environment." return 1 fi done @@ -115,26 +115,26 @@ _check_deployfile() { # deploy_filepath deploy_config _get_services_list() { - deploy_file="$1" - deploy_config="$2" + _deploy_file="$1" + _deploy_config="$2" - services=$(yq e ".configs[] | select(.name == \"$deploy_config\").services[]" "$deploy_file") - echo "$services" + _services=$(yq e ".configs[] | select(.name == \"$_deploy_config\").services[]" "$_deploy_file") + echo "$_services" } # deploy_filepath service_names _get_full_services_list() { - deploy_file="$1" + _deploy_file="$1" shift - service_names="$*" + _service_names="$*" - full_services="" - for service in $service_names; do - full_service=$(yq e ".services[] | select(.name == \"$service\")" "$deploy_file") - full_services="$full_services -$full_service" + _full_services="" + for _service in $_service_names; do + _full_service=$(yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file") + _full_services="$_full_services +$_full_service" done - echo "$full_services" + echo "$_full_services" } From 67d58a12e76944389fab15632e078c3c10717b79 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:50:11 +0100 Subject: [PATCH 292/689] implemented handling envs --- deploy/multideploy.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 245c95a6..d1642538 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -138,3 +138,25 @@ $_full_service" echo "$_full_services" } +# env_list +_export_envs() { + _env_list="$1" + + for _env in $_env_list; do + _key=$(echo "$_env" | cut -d '=' -f1) + _value=$(echo "$_env" | cut -d '=' -f2-) + _savedomainconf "$_key" "$_value" + _secure_debug3 "Saved $_key" "$_value" + done +} + +_clear_envs() { + _env_list="$1" + + for _env in $_env_list; do + _key=$(echo "$_env" | cut -d '=' -f1) + _debug3 "Deleting key" "$_key" + _cleardomainconf "SAVED_$_key" + unset "$_key" + done +} From 23e1a53ec8d7b33e44fe81d5cf19534f1a5de296 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:50:35 +0100 Subject: [PATCH 293/689] implemented deploying to services --- deploy/multideploy.sh | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index d1642538..caca02e2 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -40,7 +40,16 @@ multideploy_deploy() { _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" fi - # TODO: Deploy to services + # Deploy to services + _services=$(_get_services_list "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_CONFIG") + _full_services=$(_get_full_services_list "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" "$_services") + _deploy_services "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" "$_full_services" + + # Save deployhook for renewals + _debug2 "Setting Le_DeployHook" + _savedomainconf "Le_DeployHook" "multideploy" + + return 0 } #################### Private functions below ##################### @@ -160,3 +169,34 @@ _clear_envs() { unset "$_key" done } + +# deploy_filepath services_array +_deploy_services() { + _deploy_file="$1" + shift + _services="$*" + + for _service in $_services; do + _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") + _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") + _export_envs "$_envs" + _deploy_service "$_service" "$_hook" + _clear_envs "$_envs" + done +} + +_deploy_service() { + _name="$1" + _hook="$2" + + _debug2 "SERVICE" "$_name" + _debug2 "HOOK" "$_hook" + + _info "$(__green "Deploying") to '$_name' using '$_hook'" + if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then + _debug2 "User wants to use ECC." + deploy "$_cdomain" "$_hook" "isEcc" + else + deploy "$_cdomain" "$_hook" + fi +} From 34eb2a655a8da40827478931ce984ad3f265356f Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:50:50 +0100 Subject: [PATCH 294/689] added yq to dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 88edc4a2..4852cde4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN apk --no-cache add -f \ tar \ libidn \ jq \ + yq \ cronie ENV LE_WORKING_DIR=/acmebin From fb0926dc81ccbdcbfc833f07de307f029e4289bc Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:51:02 +0100 Subject: [PATCH 295/689] implemented checking for different kinds of deploy file --- deploy/multideploy.sh | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index caca02e2..9df2c423 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -5,7 +5,8 @@ ######## Public functions ##################### MULTIDEPLOY_VERSION="1.0" -MULTIDEPLOY_FILENAME="multideploy.yaml" +MULTIDEPLOY_FILENAME="multideploy.yml" +MULTIDEPLOY_FILENAME2="multideploy.yaml" # domain keyfile certfile cafile fullchain pfx multideploy_deploy() { @@ -56,18 +57,30 @@ multideploy_deploy() { # deploy_filepath _preprocess_deployfile() { - _deploy_file="$1" - # Check if yq is installed if ! command -v yq >/dev/null 2>&1; then _err "yq is not installed! Please install yq and try again." return 1 fi + _debug3 "yq is installed." - # Check if deploy file exists and create a default template if not - if [ -f "$_deploy_file" ]; then - _debug3 "Deploy file found." - _check_deployfile "$_deploy_file" "$MULTIDEPLOY_CONFIG" + # Check if deploy file exists + for file in "$@"; do + _debug3 "Checking file" "$DOMAIN_PATH/$file" + if [ -f "$DOMAIN_PATH/$file" ]; then + _debug3 "File found" + if [ -n "$found_file" ]; then + _err "Multiple deploy files found. Please keep only one deploy file." + return 1 + fi + found_file="$file" + else + _debug3 "File not found" + fi + done + + if [ -n "$found_file" ]; then + _check_deployfile "$DOMAIN_PATH/$found_file" "$MULTIDEPLOY_CONFIG" else # TODO: Replace URL with wiki link _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." From db1dc4de0deb66b75bf83a4b1d6a274b7b5e6f09 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:52:02 +0100 Subject: [PATCH 296/689] added debug messages --- deploy/multideploy.sh | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 9df2c423..3c243c04 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -30,8 +30,6 @@ multideploy_deploy() { fi _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" - _preprocess_deployfile "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" || return 1 - MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" if [ -z "$MULTIDEPLOY_CONFIG" ]; then MULTIDEPLOY_CONFIG="default" @@ -91,7 +89,10 @@ _preprocess_deployfile() { # deploy_filepath _deploy_config _check_deployfile() { _deploy_file="$1" - _deploy_config="$3" + _deploy_config="$2" + + _debug2 "Deploy file" "$_deploy_file" + _debug2 "Deploy config" "$_deploy_config" # Check version _deploy_file_version=$(yq '.version' "$_deploy_file") @@ -99,23 +100,29 @@ _check_deployfile() { _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." return 1 fi + _debug2 "Deploy file version is compatible: $_deploy_file_version" # Check if config exists if ! yq e ".configs[] | select(.name == \"$_deploy_config\")" "$_deploy_file" >/dev/null; then _err "Config '$_deploy_config' not found." return 1 fi + _debug2 "Config found: $_deploy_config" # Extract all services from config _services=$(_get_services_list "$_deploy_file" "$_deploy_config") + _debug2 "Services" "$_services" if [ -z "$_services" ]; then _err "Config '$_deploy_config' does not have any services to deploy to." return 1 fi + _debug2 "Config has services." # Check if extracted services exist in services list for _service in $_services; do + _debug2 "Checking service" "$_service" + # Check if service exists if ! yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file" >/dev/null; then _err "Service '$_service' not found." return 1 @@ -140,6 +147,10 @@ _get_services_list() { _deploy_file="$1" _deploy_config="$2" + _debug2 "Getting services list" + _debug3 "Deploy file" "$_deploy_file" + _debug3 "Deploy config" "$_deploy_config" + _services=$(yq e ".configs[] | select(.name == \"$_deploy_config\").services[]" "$_deploy_file") echo "$_services" } @@ -150,6 +161,9 @@ _get_full_services_list() { shift _service_names="$*" + _debug3 "Deploy file" "$_deploy_file" + _debug3 "Service names" "$_service_names" + _full_services="" for _service in $_service_names; do _full_service=$(yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file") @@ -164,6 +178,8 @@ $_full_service" _export_envs() { _env_list="$1" + _secure_debug3 "Exporting envs" "$_env_list" + for _env in $_env_list; do _key=$(echo "$_env" | cut -d '=' -f1) _value=$(echo "$_env" | cut -d '=' -f2-) @@ -175,6 +191,8 @@ _export_envs() { _clear_envs() { _env_list="$1" + _secure_debug3 "Clearing envs" "$_env_list" + for _env in $_env_list; do _key=$(echo "$_env" | cut -d '=' -f1) _debug3 "Deleting key" "$_key" @@ -189,6 +207,9 @@ _deploy_services() { shift _services="$*" + _debug3 "Deploy file" "$_deploy_file" + _debug3 "Services" "$_services" + for _service in $_services; do _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") From 768de270bf6943047f3f52d38b0af28c423dd765 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:23:13 +0100 Subject: [PATCH 297/689] improved preprocessing and fixed bug with wrong param of services --- deploy/multideploy.sh | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 3c243c04..7b4e1400 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -39,10 +39,13 @@ multideploy_deploy() { _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" fi + OLDIFS=$IFS + file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME2") || return 1 + _debug3 "File" "$file" + # Deploy to services - _services=$(_get_services_list "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_CONFIG") - _full_services=$(_get_full_services_list "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" "$_services") - _deploy_services "$DOMAIN_DIR/$MULTIDEPLOY_FILENAME" "$_full_services" + _services=$(_get_services_list "$file" "$MULTIDEPLOY_CONFIG") + _deploy_services "$file" "$_services" # Save deployhook for renewals _debug2 "Setting Le_DeployHook" @@ -84,6 +87,8 @@ _preprocess_deployfile() { _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." return 1 fi + + echo "$DOMAIN_PATH/$found_file" } # deploy_filepath _deploy_config @@ -155,25 +160,6 @@ _get_services_list() { echo "$_services" } -# deploy_filepath service_names -_get_full_services_list() { - _deploy_file="$1" - shift - _service_names="$*" - - _debug3 "Deploy file" "$_deploy_file" - _debug3 "Service names" "$_service_names" - - _full_services="" - for _service in $_service_names; do - _full_service=$(yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file") - _full_services="$_full_services -$_full_service" - done - - echo "$_full_services" -} - # env_list _export_envs() { _env_list="$1" @@ -211,6 +197,7 @@ _deploy_services() { _debug3 "Services" "$_services" for _service in $_services; do + _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") _export_envs "$_envs" From ba7c368ee535c198187320a740d5da816126c51b Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:23:29 +0100 Subject: [PATCH 298/689] fixed IFS problems --- deploy/multideploy.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 7b4e1400..9e2fb1d2 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -66,6 +66,7 @@ _preprocess_deployfile() { _debug3 "yq is installed." # Check if deploy file exists + IFS=$(printf '\n') for file in "$@"; do _debug3 "Checking file" "$DOMAIN_PATH/$file" if [ -f "$DOMAIN_PATH/$file" ]; then @@ -79,6 +80,7 @@ _preprocess_deployfile() { _debug3 "File not found" fi done + IFS=$OLDIFS if [ -n "$found_file" ]; then _check_deployfile "$DOMAIN_PATH/$found_file" "$MULTIDEPLOY_CONFIG" @@ -124,6 +126,7 @@ _check_deployfile() { fi _debug2 "Config has services." + IFS=$(printf '\n') # Check if extracted services exist in services list for _service in $_services; do _debug2 "Checking service" "$_service" @@ -145,6 +148,7 @@ _check_deployfile() { return 1 fi done + IFS=$OLDIFS } # deploy_filepath deploy_config @@ -166,25 +170,27 @@ _export_envs() { _secure_debug3 "Exporting envs" "$_env_list" - for _env in $_env_list; do - _key=$(echo "$_env" | cut -d '=' -f1) - _value=$(echo "$_env" | cut -d '=' -f2-) + IFS=$(printf '\n') + echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do _savedomainconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" done + IFS=$OLDIFS } _clear_envs() { _env_list="$1" _secure_debug3 "Clearing envs" "$_env_list" + env_pairs=$(echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value') - for _env in $_env_list; do - _key=$(echo "$_env" | cut -d '=' -f1) + IFS=$(printf '\n') + echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" _cleardomainconf "SAVED_$_key" unset "$_key" done + IFS="$OLDIFS" } # deploy_filepath services_array @@ -196,14 +202,17 @@ _deploy_services() { _debug3 "Deploy file" "$_deploy_file" _debug3 "Services" "$_services" + IFS=$(printf '\n') for _service in $_services; do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") + _export_envs "$_envs" _deploy_service "$_service" "$_hook" _clear_envs "$_envs" done + IFS=$OLDIFS } _deploy_service() { From 2cc5e66517908eed21bbc03ba5066919dd27365e Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:31:24 +0100 Subject: [PATCH 299/689] added docs --- deploy/multideploy.sh | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 9e2fb1d2..129bb38f 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -151,7 +151,13 @@ _check_deployfile() { IFS=$OLDIFS } -# deploy_filepath deploy_config +# Description: +# This function retrieves a list of services from the deploy configuration file. +# Arguments: +# $1 - The path to the deploy configuration file. +# $2 - The name of the deploy configuration to use. +# Usage: +# _get_services_list "" "" _get_services_list() { _deploy_file="$1" _deploy_config="$2" @@ -164,7 +170,12 @@ _get_services_list() { echo "$_services" } -# env_list +# Description: This function takes a list of environment variables in YAML format, +# parses them, and exports each key-value pair as environment variables. +# Arguments: +# $1 - A string containing the list of environment variables in YAML format. +# Usage: +# _export_envs "$env_list" _export_envs() { _env_list="$1" @@ -178,6 +189,13 @@ _export_envs() { IFS=$OLDIFS } +# Description: +# This function takes a YAML formatted string of environment variables, parses it, +# and clears each environment variable. It logs the process of clearing each variable. +# Arguments: +# $1 - A YAML formatted string containing environment variable key-value pairs. +# Usage: +# _clear_envs "" _clear_envs() { _env_list="$1" @@ -188,7 +206,7 @@ _clear_envs() { echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" _cleardomainconf "SAVED_$_key" - unset "$_key" + unset -v "$_key" done IFS="$OLDIFS" } From 74ed0354a3f135884b37c5b5adf3557a22afb2d2 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:44:46 +0100 Subject: [PATCH 300/689] added docs and enhanced log messages --- deploy/multideploy.sh | 57 +++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 129bb38f..81754f1c 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -2,13 +2,21 @@ # MULTIDEPLOY_CONFIG="default" -######## Public functions ##################### - MULTIDEPLOY_VERSION="1.0" MULTIDEPLOY_FILENAME="multideploy.yml" MULTIDEPLOY_FILENAME2="multideploy.yaml" -# domain keyfile certfile cafile fullchain pfx +# Description: This function handles the deployment of certificates to multiple services. +# It processes the provided certificate files and deploys them according to the +# configuration specified in the MULTIDEPLOY_CONFIG. +# +# Parameters: +# _cdomain - The domain name for which the certificate is issued. +# _ckey - The private key file for the certificate. +# _ccert - The certificate file. +# _cca - The CA (Certificate Authority) file. +# _cfullchain - The full chain certificate file. +# _cpfx - The PFX (Personal Information Exchange) file. multideploy_deploy() { _cdomain="$1" _ckey="$2" @@ -40,7 +48,10 @@ multideploy_deploy() { fi OLDIFS=$IFS - file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME2") || return 1 + if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME2"); then + _err "Failed to preprocess deploy file." + return 1 + fi _debug3 "File" "$file" # Deploy to services @@ -54,9 +65,13 @@ multideploy_deploy() { return 0 } -#################### Private functions below ##################### - -# deploy_filepath +# Description: +# This function preprocesses the deploy file by checking if 'yq' is installed, +# verifying the existence of the deploy file, and ensuring only one deploy file is present. +# Arguments: +# $@ - Posible deploy file names. +# Usage: +# _preprocess_deployfile "" "" _preprocess_deployfile() { # Check if yq is installed if ! command -v yq >/dev/null 2>&1; then @@ -93,7 +108,13 @@ _preprocess_deployfile() { echo "$DOMAIN_PATH/$found_file" } -# deploy_filepath _deploy_config +# Description: +# This function checks the deploy file for version compatibility and the existence of the specified configuration and services. +# Arguments: +# $1 - The path to the deploy configuration file. +# $2 - The name of the deploy configuration to use. +# Usage: +# _check_deployfile "" "" _check_deployfile() { _deploy_file="$1" _deploy_config="$2" @@ -144,7 +165,7 @@ _check_deployfile() { # Check if service has environment if ! yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file" >/dev/null; then - _err "Service '$_service' does not an environment." + _err "Service '$_service' does not have an environment." return 1 fi done @@ -211,7 +232,13 @@ _clear_envs() { IFS="$OLDIFS" } -# deploy_filepath services_array +# Description: +# This function deploys services listed in the deploy configuration file. +# Arguments: +# $1 - The path to the deploy configuration file. +# $2 - The list of services to deploy. +# Usage: +# _deploy_services "" "" _deploy_services() { _deploy_file="$1" shift @@ -220,8 +247,7 @@ _deploy_services() { _debug3 "Deploy file" "$_deploy_file" _debug3 "Services" "$_services" - IFS=$(printf '\n') - for _service in $_services; do + printf '%s\n' "$_services" | while IFS= read -r _service; do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") @@ -230,9 +256,14 @@ _deploy_services() { _deploy_service "$_service" "$_hook" _clear_envs "$_envs" done - IFS=$OLDIFS } +# Description: Deploys a service using the specified hook. +# Arguments: +# $1 - The name of the service to deploy. +# $2 - The hook to use for deployment. +# Usage: +# _deploy_service _deploy_service() { _name="$1" _hook="$2" From 88d4637ee31e4d27a6e9978d8bfb1b52e55ef8d7 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:02:55 +0100 Subject: [PATCH 301/689] added header doc --- deploy/multideploy.sh | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 81754f1c..bea4622b 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -1,6 +1,25 @@ #!/usr/bin/env sh -# MULTIDEPLOY_CONFIG="default" +################################################################################ +# ACME.sh 3rd party deploy plugin for multiple (same) services +################################################################################ +# Authors: tomo2403 (creator), https://github.com/tomo2403 +# Updated: 2024-07-03 +# Issues: https://github.com/acmesh-official/acme.sh/issues/XXXXX +################################################################################ +# Usage (shown values are the examples): +# 1. Set optional environment variables +# - export MULTIDEPLOY_CONFIG="default" - "default" will be automatically used if not set" +# +# 2. Run command: +# acme.sh --deploy --deploy-hook multideploy -d example.com +################################################################################ +# Dependencies: +# - yq +################################################################################ +# Return value: +# 0 means success, otherwise error. +################################################################################ MULTIDEPLOY_VERSION="1.0" MULTIDEPLOY_FILENAME="multideploy.yml" From c16e059535b873a46f6ee2a47f8810843f0b2292 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:15:42 +0100 Subject: [PATCH 302/689] allowed using varaibles in deploy file --- deploy/multideploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index bea4622b..58df0482 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -223,6 +223,7 @@ _export_envs() { IFS=$(printf '\n') echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do + _value=$(eval echo "$_value") _savedomainconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" done From 88cde7be6db1901932f9458f624b0362f348edd4 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Fri, 21 Feb 2025 19:26:56 +0100 Subject: [PATCH 303/689] fixed missing wiki link --- deploy/multideploy.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 58df0482..9e3f4e17 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -119,8 +119,7 @@ _preprocess_deployfile() { if [ -n "$found_file" ]; then _check_deployfile "$DOMAIN_PATH/$found_file" "$MULTIDEPLOY_CONFIG" else - # TODO: Replace URL with wiki link - _err "Deploy file not found. Go to https://CHANGE_URL_TO_WIKI to see how to create one." + _err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." return 1 fi From c1e17c366fd367cc68a3b89d9eca2abd161cbbde Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Sat, 1 Mar 2025 13:26:53 +0100 Subject: [PATCH 304/689] Update links in multideploy.sh --- deploy/multideploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 9e3f4e17..b002f068 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -4,8 +4,8 @@ # ACME.sh 3rd party deploy plugin for multiple (same) services ################################################################################ # Authors: tomo2403 (creator), https://github.com/tomo2403 -# Updated: 2024-07-03 -# Issues: https://github.com/acmesh-official/acme.sh/issues/XXXXX +# Updated: 2025-03-01 +# Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403 ################################################################################ # Usage (shown values are the examples): # 1. Set optional environment variables From 95c75460513f8faa5cdfbee869cc70d0354c9bce Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:11:31 +0100 Subject: [PATCH 305/689] removed configs and implemented specification of deploy file name --- deploy/multideploy.sh | 67 +++++++++++-------------------------------- 1 file changed, 17 insertions(+), 50 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index b002f068..24177e66 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -9,7 +9,7 @@ ################################################################################ # Usage (shown values are the examples): # 1. Set optional environment variables -# - export MULTIDEPLOY_CONFIG="default" - "default" will be automatically used if not set" +# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set" # # 2. Run command: # acme.sh --deploy --deploy-hook multideploy -d example.com @@ -22,12 +22,10 @@ ################################################################################ MULTIDEPLOY_VERSION="1.0" -MULTIDEPLOY_FILENAME="multideploy.yml" -MULTIDEPLOY_FILENAME2="multideploy.yaml" # Description: This function handles the deployment of certificates to multiple services. # It processes the provided certificate files and deploys them according to the -# configuration specified in the MULTIDEPLOY_CONFIG. +# configuration specified in the multideploy file. # # Parameters: # _cdomain - The domain name for which the certificate is issued. @@ -57,25 +55,24 @@ multideploy_deploy() { fi _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" - MULTIDEPLOY_CONFIG="${MULTIDEPLOY_CONFIG:-$(_getdeployconf MULTIDEPLOY_CONFIG)}" - if [ -z "$MULTIDEPLOY_CONFIG" ]; then - MULTIDEPLOY_CONFIG="default" - _info "MULTIDEPLOY_CONFIG is not set, so I will use 'default'." + MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" + if [ -z "$MULTIDEPLOY_FILENAME" ]; then + MULTIDEPLOY_FILENAME="multideploy.yml" + _info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'." else - _savedeployconf "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" - _debug2 "MULTIDEPLOY_CONFIG" "$MULTIDEPLOY_CONFIG" + _savedeployconf "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" + _debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" fi OLDIFS=$IFS - if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME2"); then + if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then _err "Failed to preprocess deploy file." return 1 fi _debug3 "File" "$file" # Deploy to services - _services=$(_get_services_list "$file" "$MULTIDEPLOY_CONFIG") - _deploy_services "$file" "$_services" + _deploy_services "$file" # Save deployhook for renewals _debug2 "Setting Le_DeployHook" @@ -90,7 +87,7 @@ multideploy_deploy() { # Arguments: # $@ - Posible deploy file names. # Usage: -# _preprocess_deployfile "" "" +# _preprocess_deployfile "" "?" _preprocess_deployfile() { # Check if yq is installed if ! command -v yq >/dev/null 2>&1; then @@ -117,7 +114,7 @@ _preprocess_deployfile() { IFS=$OLDIFS if [ -n "$found_file" ]; then - _check_deployfile "$DOMAIN_PATH/$found_file" "$MULTIDEPLOY_CONFIG" + _check_deployfile "$DOMAIN_PATH/$found_file" else _err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." return 1 @@ -132,13 +129,10 @@ _preprocess_deployfile() { # $1 - The path to the deploy configuration file. # $2 - The name of the deploy configuration to use. # Usage: -# _check_deployfile "" "" +# _check_deployfile "" _check_deployfile() { _deploy_file="$1" - _deploy_config="$2" - _debug2 "Deploy file" "$_deploy_file" - _debug2 "Deploy config" "$_deploy_config" # Check version _deploy_file_version=$(yq '.version' "$_deploy_file") @@ -148,19 +142,12 @@ _check_deployfile() { fi _debug2 "Deploy file version is compatible: $_deploy_file_version" - # Check if config exists - if ! yq e ".configs[] | select(.name == \"$_deploy_config\")" "$_deploy_file" >/dev/null; then - _err "Config '$_deploy_config' not found." - return 1 - fi - _debug2 "Config found: $_deploy_config" - # Extract all services from config - _services=$(_get_services_list "$_deploy_file" "$_deploy_config") + _services=$(yq e '.services[].name' "$_deploy_file") _debug2 "Services" "$_services" if [ -z "$_services" ]; then - _err "Config '$_deploy_config' does not have any services to deploy to." + _err "Config does not have any services to deploy to." return 1 fi _debug2 "Config has services." @@ -190,25 +177,6 @@ _check_deployfile() { IFS=$OLDIFS } -# Description: -# This function retrieves a list of services from the deploy configuration file. -# Arguments: -# $1 - The path to the deploy configuration file. -# $2 - The name of the deploy configuration to use. -# Usage: -# _get_services_list "" "" -_get_services_list() { - _deploy_file="$1" - _deploy_config="$2" - - _debug2 "Getting services list" - _debug3 "Deploy file" "$_deploy_file" - _debug3 "Deploy config" "$_deploy_config" - - _services=$(yq e ".configs[] | select(.name == \"$_deploy_config\").services[]" "$_deploy_file") - echo "$_services" -} - # Description: This function takes a list of environment variables in YAML format, # parses them, and exports each key-value pair as environment variables. # Arguments: @@ -260,10 +228,9 @@ _clear_envs() { # _deploy_services "" "" _deploy_services() { _deploy_file="$1" - shift - _services="$*" - _debug3 "Deploy file" "$_deploy_file" + + _services=$(yq e '.services[].name' "$_deploy_file") _debug3 "Services" "$_services" printf '%s\n' "$_services" | while IFS= read -r _service; do From 17e0bbcbb67c32fff1cb620d08a0c1b277565332 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:16:07 +0100 Subject: [PATCH 306/689] fixed formatting --- deploy/multideploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 24177e66..c297878c 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -229,7 +229,7 @@ _clear_envs() { _deploy_services() { _deploy_file="$1" _debug3 "Deploy file" "$_deploy_file" - + _services=$(yq e '.services[].name' "$_deploy_file") _debug3 "Services" "$_services" From 88e4d64c1a242342a60f7aa1bddd4ba842c5b500 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:24:32 +0200 Subject: [PATCH 307/689] fixed IFS problems for some hooks --- deploy/multideploy.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index c297878c..b728ee77 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -233,7 +233,9 @@ _deploy_services() { _services=$(yq e '.services[].name' "$_deploy_file") _debug3 "Services" "$_services" - printf '%s\n' "$_services" | while IFS= read -r _service; do + _service_list=$(printf '%s\n' "$_services") + + for _service in $(printf '%s\n' "$_service_list"); do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") From c1c49d5a01e929d326b8a7f4a8fb2d3780f6e8ec Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Sun, 13 Apr 2025 16:38:28 +0200 Subject: [PATCH 308/689] simplified deploy method --- deploy/multideploy.sh | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index b728ee77..93809e6e 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -260,10 +260,5 @@ _deploy_service() { _debug2 "HOOK" "$_hook" _info "$(__green "Deploying") to '$_name' using '$_hook'" - if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then - _debug2 "User wants to use ECC." - deploy "$_cdomain" "$_hook" "isEcc" - else - deploy "$_cdomain" "$_hook" - fi + _deploy "$_cdomain" "$_hook" } From a55d40be972e7ebd4f64599211b0e861ed2f83e9 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Tue, 27 May 2025 22:07:22 +0200 Subject: [PATCH 309/689] fixed bug with envs due to the use of a wrong function --- deploy/multideploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 93809e6e..c77d1fdf 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -191,7 +191,7 @@ _export_envs() { IFS=$(printf '\n') echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do _value=$(eval echo "$_value") - _savedomainconf "$_key" "$_value" + _savedeployconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" done IFS=$OLDIFS From e5b47f6402567d75db58edcb5b4b67194de58202 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Wed, 28 May 2025 20:02:07 +0200 Subject: [PATCH 310/689] implemented exiting with 1 if at least one deployment fails --- deploy/multideploy.sh | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index c77d1fdf..5b323303 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -72,13 +72,17 @@ multideploy_deploy() { _debug3 "File" "$file" # Deploy to services - _deploy_services "$file" + if _deploy_services "$file"; then + _deploymentOk=0 + else + _deploymentOk=1 + fi # Save deployhook for renewals _debug2 "Setting Le_DeployHook" _savedomainconf "Le_DeployHook" "multideploy" - return 0 + return "$_deploymentOk" } # Description: @@ -235,15 +239,26 @@ _deploy_services() { _service_list=$(printf '%s\n' "$_services") + _errors="" for _service in $(printf '%s\n' "$_service_list"); do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") _export_envs "$_envs" - _deploy_service "$_service" "$_hook" + if ! _deploy_service "$_service" "$_hook"; then + _errors="$_service, $_errors" + fi _clear_envs "$_envs" done + + if [ -n "$_errors" ]; then + _err "Deployment failed for services: $_errors" + return 1 + else + _debug "All services deployed successfully." + return 0 + fi } # Description: Deploys a service using the specified hook. @@ -261,4 +276,5 @@ _deploy_service() { _info "$(__green "Deploying") to '$_name' using '$_hook'" _deploy "$_cdomain" "$_hook" + return $? } From 093f36b4d670a03b571ea68f28b4d180ad6f6261 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Wed, 28 May 2025 20:19:18 +0200 Subject: [PATCH 311/689] implemented exiting with the number of failed deployments --- deploy/multideploy.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 5b323303..fd04043a 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -72,17 +72,14 @@ multideploy_deploy() { _debug3 "File" "$file" # Deploy to services - if _deploy_services "$file"; then - _deploymentOk=0 - else - _deploymentOk=1 - fi + _deploy_services "$file" + _exitCode="$?" # Save deployhook for renewals _debug2 "Setting Le_DeployHook" _savedomainconf "Le_DeployHook" "multideploy" - return "$_deploymentOk" + return "$_exitCode" } # Description: @@ -239,7 +236,8 @@ _deploy_services() { _service_list=$(printf '%s\n' "$_services") - _errors="" + _failedServices="" + _failedCount=0 for _service in $(printf '%s\n' "$_service_list"); do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") @@ -247,18 +245,21 @@ _deploy_services() { _export_envs "$_envs" if ! _deploy_service "$_service" "$_hook"; then - _errors="$_service, $_errors" + _failedServices="$_service, $_failedServices" + _failedCount=$((_failedCount + 1)) fi _clear_envs "$_envs" done - if [ -n "$_errors" ]; then - _err "Deployment failed for services: $_errors" - return 1 + _debug3 "Failed services" "$_failedServices" + _debug2 "Failed count" "$_failedCount" + if [ -n "$_failedServices" ]; then + _info "$(__red "Deployment failed") for services: $_failedServices" else _debug "All services deployed successfully." - return 0 fi + + return "$_failedCount" } # Description: Deploys a service using the specified hook. From 7b16526e7f0f217a4d0e02505d32f4260e884c40 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Sat, 31 May 2025 19:21:10 +0200 Subject: [PATCH 312/689] removed dead code --- deploy/multideploy.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index fd04043a..85a8dfd4 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -49,12 +49,6 @@ multideploy_deploy() { _debug _cfullchain "$_cfullchain" _debug _cpfx "$_cpfx" - DOMAIN_DIR=$_cdomain - if echo "$DOMAIN_PATH" | grep -q "$ECC_SUFFIX"; then - DOMAIN_DIR="$DOMAIN_DIR"_ecc - fi - _debug2 "DOMAIN_DIR" "$DOMAIN_DIR" - MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" if [ -z "$MULTIDEPLOY_FILENAME" ]; then MULTIDEPLOY_FILENAME="multideploy.yml" From 37c25aa107e0c05e2ddc4131cc0d9efc0dad5971 Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Sat, 31 May 2025 20:19:49 +0200 Subject: [PATCH 313/689] removed unneeded return value --- deploy/multideploy.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 85a8dfd4..88a2c6cc 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -271,5 +271,4 @@ _deploy_service() { _info "$(__green "Deploying") to '$_name' using '$_hook'" _deploy "$_cdomain" "$_hook" - return $? } From d375012c5db3d688daba40425430778ac0106aaf Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Tue, 1 Jul 2025 16:02:50 +0200 Subject: [PATCH 314/689] fixed yml file env list --- deploy/multideploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 88a2c6cc..9ab16d0d 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -235,7 +235,7 @@ _deploy_services() { for _service in $(printf '%s\n' "$_service_list"); do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") - _envs=$(yq e ".services[] | select(.name == \"$_service\").environment[]" "$_deploy_file") + _envs=$(yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") _export_envs "$_envs" if ! _deploy_service "$_service" "$_hook"; then From f850e8d0e466514262ecb6ee6d051da228dad115 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 13:45:34 +0100 Subject: [PATCH 315/689] Support spaces in service names - Prefer using a pipe to `while read` - But use a temp file when the loop needs to modify variables that need to be used outside the loop, as the pipe creates a subshell and modifications do not survive after the loop exits. --- deploy/multideploy.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 9ab16d0d..f5d6f587 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -147,9 +147,8 @@ _check_deployfile() { fi _debug2 "Config has services." - IFS=$(printf '\n') # Check if extracted services exist in services list - for _service in $_services; do + echo "$_services" | while read -r _service; do _debug2 "Checking service" "$_service" # Check if service exists if ! yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file" >/dev/null; then @@ -169,7 +168,6 @@ _check_deployfile() { return 1 fi done - IFS=$OLDIFS } # Description: This function takes a list of environment variables in YAML format, @@ -225,14 +223,15 @@ _deploy_services() { _deploy_file="$1" _debug3 "Deploy file" "$_deploy_file" - _services=$(yq e '.services[].name' "$_deploy_file") - _debug3 "Services" "$_services" + _tempfile=$(mktemp) + trap "rm -f $_tempfile" EXIT - _service_list=$(printf '%s\n' "$_services") + yq e '.services[].name' "$_deploy_file" > $_tempfile + _debug3 "Services" "$(cat $_tempfile)" _failedServices="" _failedCount=0 - for _service in $(printf '%s\n' "$_service_list"); do + while read -r _service; do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") @@ -243,7 +242,7 @@ _deploy_services() { _failedCount=$((_failedCount + 1)) fi _clear_envs "$_envs" - done + done < "$_tempfile" _debug3 "Failed services" "$_failedServices" _debug2 "Failed count" "$_failedCount" From 986a6138ebda928297a859f12ab72f2b040d7e27 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:19:44 +0100 Subject: [PATCH 316/689] Fix config file checks The config file checks were returning okay even when there were errors. The yq tool returns "null" when it cannot find what's queried, but exists with a 0 rc still. --- deploy/multideploy.sh | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index f5d6f587..5ed62ec3 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -127,7 +127,7 @@ _preprocess_deployfile() { # _check_deployfile "" _check_deployfile() { _deploy_file="$1" - _debug2 "Deploy file" "$_deploy_file" + _debug2 "check: Deploy file" "$_deploy_file" # Check version _deploy_file_version=$(yq '.version' "$_deploy_file") @@ -135,38 +135,44 @@ _check_deployfile() { _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." return 1 fi - _debug2 "Deploy file version is compatible: $_deploy_file_version" + _debug2 "check: Deploy file version is compatible: $_deploy_file_version" # Extract all services from config _services=$(yq e '.services[].name' "$_deploy_file") - _debug2 "Services" "$_services" if [ -z "$_services" ]; then _err "Config does not have any services to deploy to." return 1 fi - _debug2 "Config has services." + _debug2 "check: Config has services." + echo "$_services" | while read -r _service; do + _debug3 " - $_service" + done # Check if extracted services exist in services list echo "$_services" | while read -r _service; do - _debug2 "Checking service" "$_service" + _debug2 "check: Checking service: $_service" # Check if service exists - if ! yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file" >/dev/null; then + _service_config=$(yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file") + if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then _err "Service '$_service' not found." return 1 fi + _secure_debug3 "check: Service '$_service' configuration" "$_service_config" - # Check if service has hook - if ! yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file" >/dev/null; then + _service_hook=$(echo "$_service_config" | yq e ".hook" -) + if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then _err "Service '$_service' does not have a hook." return 1 fi + _debug3 "check: Service '$_service' hook" "$_service_hook" - # Check if service has environment - if ! yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file" >/dev/null; then + _service_environment=$(echo "$_service_config" | yq e ".environment" -) + if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then _err "Service '$_service' does not have an environment." return 1 fi + _secure_debug3 "check: Service '$_service' environment" "$_service_environment" done } From 1d8788767f0173423a03201ece84562268beccb9 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:20:38 +0100 Subject: [PATCH 317/689] Make failure to check file stop the deployment Before this, checker issues were only logged. This stops the deployment if any configuration is incorrect. --- deploy/multideploy.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 5ed62ec3..18b6d7ce 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -108,12 +108,15 @@ _preprocess_deployfile() { done IFS=$OLDIFS - if [ -n "$found_file" ]; then - _check_deployfile "$DOMAIN_PATH/$found_file" - else + if [ -z "$found_file" ]; + then _err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." return 1 fi + if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then + _err "Deploy file is not valid: $DOMAIN_PATH/$found_file" + return 1 + fi echo "$DOMAIN_PATH/$found_file" } From 1eee4dee9cfd165587f8a95104b5f4251c440416 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:22:21 +0100 Subject: [PATCH 318/689] Update dependency name from yq to yq-go --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4852cde4..36b2adac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN apk --no-cache add -f \ tar \ libidn \ jq \ - yq \ + yq-go \ cronie ENV LE_WORKING_DIR=/acmebin From a961e03a597537f09ce9475bd9609ddb858d3773 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:32:52 +0100 Subject: [PATCH 319/689] Explain the use of eval --- deploy/multideploy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 18b6d7ce..8c6f4267 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -192,6 +192,7 @@ _export_envs() { IFS=$(printf '\n') echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do + # Using eval to expand nested variables in the configuration file _value=$(eval echo "$_value") _savedeployconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" From 69dd2cf78b71d1e616011428b5500a15f463f9d3 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:36:07 +0100 Subject: [PATCH 320/689] Explain _clear_envs rationale --- deploy/multideploy.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 8c6f4267..8a2fbd37 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -203,6 +203,14 @@ _export_envs() { # Description: # This function takes a YAML formatted string of environment variables, parses it, # and clears each environment variable. It logs the process of clearing each variable. +# +# Note: Environment variables for a hook may be optional and differ between +# services using the same hook. +# If one service sets optional environment variables and another does not, the +# variables may persist and affect subsequent deployments. +# Clearing these variables after each service ensures that only the +# environment variables explicitly specified for each service in the deploy +# file are used. # Arguments: # $1 - A YAML formatted string containing environment variable key-value pairs. # Usage: From 8a788651745b39ba15d8b6a078180822f91cd1da Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:41:12 +0100 Subject: [PATCH 321/689] Quote paths to prevent word splitting --- deploy/multideploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 8a2fbd37..2db9a758 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -244,8 +244,8 @@ _deploy_services() { _tempfile=$(mktemp) trap "rm -f $_tempfile" EXIT - yq e '.services[].name' "$_deploy_file" > $_tempfile - _debug3 "Services" "$(cat $_tempfile)" + yq e '.services[].name' "$_deploy_file" > "$_tempfile" + _debug3 "Services" "$(cat "$_tempfile")" _failedServices="" _failedCount=0 From 4f0a4850a68ed0b7882f463fe755516fee4b1ecc Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 19:43:49 +0100 Subject: [PATCH 322/689] Remove unnecessary resetting of IFS --- deploy/multideploy.sh | 7 ------- 1 file changed, 7 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 2db9a758..eec7b8ac 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -58,7 +58,6 @@ multideploy_deploy() { _debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" fi - OLDIFS=$IFS if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then _err "Failed to preprocess deploy file." return 1 @@ -92,7 +91,6 @@ _preprocess_deployfile() { _debug3 "yq is installed." # Check if deploy file exists - IFS=$(printf '\n') for file in "$@"; do _debug3 "Checking file" "$DOMAIN_PATH/$file" if [ -f "$DOMAIN_PATH/$file" ]; then @@ -106,7 +104,6 @@ _preprocess_deployfile() { _debug3 "File not found" fi done - IFS=$OLDIFS if [ -z "$found_file" ]; then @@ -190,14 +187,12 @@ _export_envs() { _secure_debug3 "Exporting envs" "$_env_list" - IFS=$(printf '\n') echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do # Using eval to expand nested variables in the configuration file _value=$(eval echo "$_value") _savedeployconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" done - IFS=$OLDIFS } # Description: @@ -221,13 +216,11 @@ _clear_envs() { _secure_debug3 "Clearing envs" "$_env_list" env_pairs=$(echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value') - IFS=$(printf '\n') echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" _cleardomainconf "SAVED_$_key" unset -v "$_key" done - IFS="$OLDIFS" } # Description: From 6b66e734a92d1d6a5b2bfedeb81ded69f04d02ec Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Sun, 31 Aug 2025 20:02:56 +0100 Subject: [PATCH 323/689] Remove explicit save of the deployhook acme.sh takes care of that --- deploy/multideploy.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index eec7b8ac..e1b8bf87 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -68,10 +68,6 @@ multideploy_deploy() { _deploy_services "$file" _exitCode="$?" - # Save deployhook for renewals - _debug2 "Setting Le_DeployHook" - _savedomainconf "Le_DeployHook" "multideploy" - return "$_exitCode" } From b8b1f1e9b40765dbe917f510e205a5f81f70b0f9 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 1 Sep 2025 00:04:42 +0100 Subject: [PATCH 324/689] Remove config logging when checking Because it causes a mysterious crash and it's honestly not worth it. --- deploy/multideploy.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index e1b8bf87..2d2a7a37 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -154,21 +154,18 @@ _check_deployfile() { _err "Service '$_service' not found." return 1 fi - _secure_debug3 "check: Service '$_service' configuration" "$_service_config" _service_hook=$(echo "$_service_config" | yq e ".hook" -) if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then _err "Service '$_service' does not have a hook." return 1 fi - _debug3 "check: Service '$_service' hook" "$_service_hook" _service_environment=$(echo "$_service_config" | yq e ".environment" -) if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then _err "Service '$_service' does not have an environment." return 1 fi - _secure_debug3 "check: Service '$_service' environment" "$_service_environment" done } From ab7835ec58a74d002dfc2eba593000789edda9b3 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 1 Sep 2025 00:06:07 +0100 Subject: [PATCH 325/689] Fix eval bug by quoting Before this, the eval call would try to run some commands (if they were compound commands) in the yaml file on the machine running acme.sh Eval might not be worth it for the little benefit it brings. --- deploy/multideploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 2d2a7a37..460e06d5 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -182,7 +182,7 @@ _export_envs() { echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do # Using eval to expand nested variables in the configuration file - _value=$(eval echo "$_value") + _value=$(eval 'echo "'"$_value"'"') _savedeployconf "$_key" "$_value" _secure_debug3 "Saved $_key" "$_value" done From 96f38655b4bc8a7147eec36742c5ac3e10779384 Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Mon, 1 Sep 2025 00:07:44 +0100 Subject: [PATCH 326/689] Use file descriptor 3 for main deployment loop Before this, some deployment scripts would interact with STDIN and that would cause this loop to skip some elements. By using descriptor 3 we avoid clashing with the very common stdin and stdout. --- deploy/multideploy.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 460e06d5..590cfba7 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -235,7 +235,7 @@ _deploy_services() { _failedServices="" _failedCount=0 - while read -r _service; do + while read -r _service <&3; do _debug2 "Service" "$_service" _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") _envs=$(yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") @@ -246,7 +246,7 @@ _deploy_services() { _failedCount=$((_failedCount + 1)) fi _clear_envs "$_envs" - done < "$_tempfile" + done 3< "$_tempfile" _debug3 "Failed services" "$_failedServices" _debug2 "Failed count" "$_failedCount" From 61b59831c491e1f038c64bd31f863fc8fd8e280b Mon Sep 17 00:00:00 2001 From: tomo <49612544+tomo2403@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:31:56 +0200 Subject: [PATCH 327/689] minor code style adjustments in multideploy script --- deploy/multideploy.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 590cfba7..9e8b7164 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -101,8 +101,7 @@ _preprocess_deployfile() { fi done - if [ -z "$found_file" ]; - then + if [ -z "$found_file" ]; then _err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." return 1 fi @@ -228,9 +227,9 @@ _deploy_services() { _debug3 "Deploy file" "$_deploy_file" _tempfile=$(mktemp) - trap "rm -f $_tempfile" EXIT + trap 'rm -f $_tempfile' EXIT - yq e '.services[].name' "$_deploy_file" > "$_tempfile" + yq e '.services[].name' "$_deploy_file" >"$_tempfile" _debug3 "Services" "$(cat "$_tempfile")" _failedServices="" @@ -246,7 +245,7 @@ _deploy_services() { _failedCount=$((_failedCount + 1)) fi _clear_envs "$_envs" - done 3< "$_tempfile" + done 3<"$_tempfile" _debug3 "Failed services" "$_failedServices" _debug2 "Failed count" "$_failedCount" From 11cae37405e2f82b6356fd662551be625a16cb6f Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Sat, 20 Dec 2025 11:04:04 -0500 Subject: [PATCH 328/689] make compatible with both yq versions kislyuk yq (used by Debian packages) does not accept `yq e` and also returns strings with double quotes. mikefarah's yq-go (used by Alpine) accepts `yq e` and `yq`. replace `yq e` with `yq` and also use `-r` switch to remove double quoting to ensure uniform return values from both yq versions. Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/multideploy.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 9e8b7164..ef920f64 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -125,7 +125,7 @@ _check_deployfile() { _debug2 "check: Deploy file" "$_deploy_file" # Check version - _deploy_file_version=$(yq '.version' "$_deploy_file") + _deploy_file_version=$(yq -r '.version' "$_deploy_file") if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then _err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." return 1 @@ -133,7 +133,7 @@ _check_deployfile() { _debug2 "check: Deploy file version is compatible: $_deploy_file_version" # Extract all services from config - _services=$(yq e '.services[].name' "$_deploy_file") + _services=$(yq -r '.services[].name' "$_deploy_file") if [ -z "$_services" ]; then _err "Config does not have any services to deploy to." @@ -148,19 +148,19 @@ _check_deployfile() { echo "$_services" | while read -r _service; do _debug2 "check: Checking service: $_service" # Check if service exists - _service_config=$(yq e ".services[] | select(.name == \"$_service\")" "$_deploy_file") + _service_config=$(yq -r ".services[] | select(.name == \"$_service\")" "$_deploy_file") if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then _err "Service '$_service' not found." return 1 fi - _service_hook=$(echo "$_service_config" | yq e ".hook" -) + _service_hook=$(echo "$_service_config" | yq -r ".hook" -) if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then _err "Service '$_service' does not have a hook." return 1 fi - _service_environment=$(echo "$_service_config" | yq e ".environment" -) + _service_environment=$(echo "$_service_config" | yq -r ".environment" -) if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then _err "Service '$_service' does not have an environment." return 1 @@ -179,7 +179,7 @@ _export_envs() { _secure_debug3 "Exporting envs" "$_env_list" - echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do + echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do # Using eval to expand nested variables in the configuration file _value=$(eval 'echo "'"$_value"'"') _savedeployconf "$_key" "$_value" @@ -206,7 +206,7 @@ _clear_envs() { _env_list="$1" _secure_debug3 "Clearing envs" "$_env_list" - env_pairs=$(echo "$_env_list" | yq e -r 'to_entries | .[] | .key + "=" + .value') + env_pairs=$(echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value') echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" @@ -229,15 +229,15 @@ _deploy_services() { _tempfile=$(mktemp) trap 'rm -f $_tempfile' EXIT - yq e '.services[].name' "$_deploy_file" >"$_tempfile" + yq -r '.services[].name' "$_deploy_file" >"$_tempfile" _debug3 "Services" "$(cat "$_tempfile")" _failedServices="" _failedCount=0 while read -r _service <&3; do _debug2 "Service" "$_service" - _hook=$(yq e ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") - _envs=$(yq e ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") + _hook=$(yq -r ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") + _envs=$(yq -r ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") _export_envs "$_envs" if ! _deploy_service "$_service" "$_hook"; then From fc7168e11d3b5af9f7f7e93f7887d797639c9cb4 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Dec 2025 11:20:22 +0100 Subject: [PATCH 329/689] change default renew to 30 days change default renew to 30 days and fix readme --- README.md | 390 ++++++++++++++++++++++++++++++------------------------ acme.sh | 2 +- 2 files changed, 220 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index 4afd90a8..d6ddf36e 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,70 @@ -[![zerossl.com](https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05)](https://zerossl.com/?fromacme.sh) +

+ + zerossl.com + +

-# An ACME Shell script: acme.sh +

🔐 acme.sh

+

An ACME Protocol Client Written Purely in Shell

-[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml) -[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml) -[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml) -[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml) -[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml) -[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml) -[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml) -[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml) -[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml) +

+ FreeBSD + OpenBSD + NetBSD + MacOS + Ubuntu + Windows + Solaris + DragonFlyBSD + Omnios +

-![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg) -![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg) -![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg) +

+ Shellcheck + PebbleStrict + DockerHub +

+ +

+ Financial Contributors on Open Collective + Join the chat at Gitter + Docker stars + Docker pulls +

- -[![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Docker stars](https://img.shields.io/docker/stars/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub") -[![Docker pulls](https://img.shields.io/docker/pulls/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub") +--- +## ✨ Features +- 🐚 An ACME protocol client written **purely in Shell** (Unix shell) language +- 📜 Full ACME protocol implementation +- 🔑 Support **ECDSA** certificates +- 🌐 Support **SAN** and **wildcard** certificates +- ⚡ Simple, powerful and very easy to use — only **3 minutes** to learn! +- 🔧 Compatible with **Bash**, **dash** and **sh** +- 🚫 No dependencies on Python +- 🔄 One script to issue, renew and install your certificates automatically +- 👤 **DOES NOT** require `root/sudoer` access +- 🐳 Docker ready +- 🌍 IPv6 ready +- 📧 Cron job notifications for renewal or error -- An ACME protocol client written purely in Shell (Unix shell) language. -- Full ACME protocol implementation. -- Support ECDSA certs -- Support SAN and wildcard certs -- Simple, powerful and very easy to use. You only need 3 minutes to learn it. -- Bash, dash and sh compatible. -- Purely written in Shell with no dependencies on python. -- Just one script to issue, renew and install your certificates automatically. -- DOES NOT require `root/sudoer` access. -- Docker ready -- IPv6 ready -- Cron job notifications for renewal or error etc. +> 💡 It's probably the **easiest & smartest** shell script to automatically issue & renew free certificates. -It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates. +

+ 📚 Wiki • + 🐳 Docker Guide • + 🐦 Twitter +

-Wiki: https://github.com/acmesh-official/acme.sh/wiki +--- -For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker) +## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E) -Twitter: [@neilpangxa](https://twitter.com/neilpangxa) +--- - -# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E) - -# Who: +## 🏆 Who Uses acme.sh? - [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/) - [ruby-china.org](https://ruby-china.org/topics/31983) - [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management) @@ -62,7 +78,9 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) - [lnmp.org](https://lnmp.org/) - [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials) -# Tested OS +--- + +## 🖥️ Tested OS | NO | Status| Platform| |----|-------|---------| @@ -91,50 +109,60 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) |24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) -Check our [testing project](https://github.com/acmesh-official/acmetest): +> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) +> +> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org) -https://github.com/acmesh-official/acmetest +--- -# Supported CA +## 🏛️ Supported CA -- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default) -- Letsencrypt.org CA -- [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) -- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) -- [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) -- [Pebble strict Mode](https://github.com/letsencrypt/pebble) -- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA +| CA | Status | +|---|---| +| [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) | ⭐ **Default** | +| Letsencrypt.org CA | ✅ Supported | +| [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) | ✅ Supported | +| [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) | ✅ Supported | +| [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) | ✅ Supported | +| [Pebble strict Mode](https://github.com/letsencrypt/pebble) | ✅ Supported | +| Any [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA | ✅ Supported | -# Supported modes +--- -- Webroot mode -- Standalone mode -- Standalone tls-alpn mode -- Apache mode -- Nginx mode -- DNS mode -- [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) -- [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) +## ⚙️ Supported Modes +| Mode | Description | +|------|-------------| +| 📁 Webroot mode | Use existing webroot directory | +| 🖥️ Standalone mode | Built-in webserver on port 80 | +| 🔐 Standalone tls-alpn mode | Built-in webserver on port 443 | +| 🪶 Apache mode | Use Apache for verification | +| ⚡ Nginx mode | Use Nginx for verification | +| 🌐 DNS mode | Use DNS TXT records | +| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification | +| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification | -# 1. How to install +--- -### 1. Install online +## 📖 Usage Guide -Check this project: https://github.com/acmesh-official/get.acme.sh +### 1️⃣ How to Install + +#### 📥 Install Online + +> Check this project: https://github.com/acmesh-official/get.acme.sh ```bash curl https://get.acme.sh | sh -s email=my@example.com ``` -Or: +**Or:** ```bash wget -O - https://get.acme.sh | sh -s email=my@example.com ``` - -### 2. Or, Install from git +#### 📦 Install from Git Clone this project and launch installation: @@ -144,11 +172,11 @@ cd ./acme.sh ./acme.sh --install -m my@example.com ``` -You `don't have to be root` then, although `it is recommended`. +> 💡 You `don't have to be root` then, although `it is recommended`. -Advanced Installation: https://github.com/acmesh-official/acme.sh/wiki/How-to-install +📚 **Advanced Installation:** https://github.com/acmesh-official/acme.sh/wiki/How-to-install -The installer will perform 3 actions: +**The installer will perform 3 actions:** 1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`. All certs will be placed in this folder too. @@ -161,17 +189,19 @@ Cron entry example: 0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null ``` -After the installation, you must close the current terminal and reopen it to make the alias take effect. +> ⚠️ After the installation, you must close the current terminal and reopen it to make the alias take effect. -Ok, you are ready to issue certs now. +✅ **You are ready to issue certs now!** -Show help message: +**Show help message:** ```sh -root@v1:~# acme.sh -h +acme.sh -h ``` -# 2. Just issue a cert +--- + +### 2️⃣ Issue a Certificate **Example 1:** Single domain. @@ -206,19 +236,21 @@ You must point and bind all the domains to the same webroot dir: `/home/wwwroot/ The certs will be placed in `~/.acme.sh/example.com/` -The certs will be renewed automatically every **60** days. +> 🔄 The certs will be renewed automatically every **30** days. -The certs will default to ECC certificates. +> 🔐 The certs will default to **ECC** certificates. -More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +--- -# 3. Install the cert to Apache/Nginx etc. +### 3️⃣ Install the Certificate to Apache/Nginx After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers. -You **MUST** use this command to copy the certs to the target files, **DO NOT** use the certs files in **~/.acme.sh/** folder, they are for internal use only, the folder structure may change in the future. -**Apache** example: +> ⚠️ **IMPORTANT:** You **MUST** use this command to copy the certs to the target files. **DO NOT** use the certs files in `~/.acme.sh/` folder — they are for internal use only, the folder structure may change in the future. + +#### 🪶 Apache Example: ```bash acme.sh --install-cert -d example.com \ --cert-file /path/to/certfile/in/apache/cert.pem \ @@ -227,7 +259,7 @@ acme.sh --install-cert -d example.com \ --reloadcmd "service apache2 force-reload" ``` -**Nginx** example: +#### ⚡ Nginx Example: ```bash acme.sh --install-cert -d example.com \ --key-file /path/to/keyfile/in/nginx/key.pem \ @@ -241,91 +273,89 @@ The ownership and permission info of existing files are preserved. You can pre-c Install/copy the cert/key to the production Apache or Nginx path. -The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`. +> 🔄 The cert will be renewed every **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically. +> ⚠️ **IMPORTANT:** The `reloadcmd` is very important. The cert can be automatically renewed, but without a correct `reloadcmd`, the cert may not be flushed to your server (like nginx or apache), then your website will not be able to show the renewed cert. -**Please take care: The reloadcmd is very important. The cert can be automatically renewed, but, without a correct 'reloadcmd' the cert may not be flushed to your server(like nginx or apache), then your website will not be able to show renewed cert in 60 days.** +--- -# 4. Use Standalone server to issue cert +### 4️⃣ Use Standalone Server to Issue Certificate -**(requires you to be root/sudoer or have permission to listen on port 80 (TCP))** +> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP) -Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again. +> ⚠️ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again. ```bash acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com ``` -More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert -# 5. Use Standalone ssl server to issue cert +--- -**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))** +### 5️⃣ Use Standalone TLS Server to Issue Certificate -Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again. +> 🔐 Requires root/sudoer or permission to listen on port **443** (TCP) + +> ⚠️ Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again. ```bash acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com ``` -More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +--- -# 6. Use Apache mode +### 6️⃣ Use Apache Mode -**(requires you to be root/sudoer, since it is required to interact with Apache server)** +> 🔐 Requires root/sudoer to interact with Apache server If you are running a web server, it is recommended to use the `Webroot mode`. Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder. -Just set string "apache" as the second argument and it will force use of apache plugin automatically. - ```sh acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com ``` -**This apache mode is only to issue the cert, it will not change your apache config files. -You will need to configure your website config files to use the cert by yourself. -We don't want to mess with your apache server, don't worry.** +> 💡 **Note:** This Apache mode is only to issue the cert, it will **not** change your Apache config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Apache server, don't worry! -More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert -# 7. Use Nginx mode +--- -**(requires you to be root/sudoer, since it is required to interact with Nginx server)** +### 7️⃣ Use Nginx Mode + +> 🔐 Requires root/sudoer to interact with Nginx server If you are running a web server, it is recommended to use the `Webroot mode`. -Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder. +Particularly, if you are running an Nginx server, you can use Nginx mode instead. This mode doesn't write any files to your web root folder. -Just set string "nginx" as the second argument. - -It will configure nginx server automatically to verify the domain and then restore the nginx config to the original version. - -So, the config is not changed. +It will configure Nginx server automatically to verify the domain and then restore the Nginx config to the original version. So, the config is not changed. ```sh acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com ``` -**This nginx mode is only to issue the cert, it will not change your nginx config files. -You will need to configure your website config files to use the cert by yourself. -We don't want to mess with your nginx server, don't worry.** +> 💡 **Note:** This Nginx mode is only to issue the cert, it will **not** change your Nginx config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Nginx server, don't worry! -More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert +📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert -# 8. Automatic DNS API integration +--- + +### 8️⃣ Automatic DNS API Integration If your DNS provider supports API access, we can use that API to automatically issue the certs. -You don't have to do anything manually! +> ✨ **You don't have to do anything manually!** -### Currently acme.sh supports most of the dns providers: +📚 **Currently acme.sh supports most DNS providers:** https://github.com/acmesh-official/acme.sh/wiki/dnsapi -https://github.com/acmesh-official/acme.sh/wiki/dnsapi +--- -# 9. Use DNS manual mode: +### 9️⃣ Use DNS Manual Mode See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first. @@ -355,67 +385,74 @@ Then just rerun with `renew` argument: acme.sh --renew -d example.com ``` -Ok, it's done. +✅ **Done!** -**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.** +> ⚠️ **WARNING:** This is DNS manual mode — it **cannot** be renewed automatically. You will have to add a new TXT record to your domain manually when you renew your cert. **Please use DNS API mode instead.** -**Please use dns api mode instead.** +--- -# 10. Issue certificates of different key types and lengths (ECC or RSA) +### 🔟 Issue Certificates of Different Key Types (ECC or RSA) -Just set the `keylength` to a valid, supported, value. +Just set the `keylength` to a valid, supported value. -Valid values for the `keylength` parameter are: +**Valid values for the `keylength` parameter:** -1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)** -2. **ec-384 (secp384r1, "ECDSA P-384")** -3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)** -4. **2048 (RSA2048)** -5. **3072 (RSA3072)** -6. **4096 (RSA4096)** +| Key Length | Description | +|------------|-------------| +| `ec-256` | prime256v1, "ECDSA P-256" ⭐ **Default** | +| `ec-384` | secp384r1, "ECDSA P-384" | +| `ec-521` | secp521r1, "ECDSA P-521" ⚠️ Not supported by Let's Encrypt yet | +| `2048` | RSA 2048-bit | +| `3072` | RSA 3072-bit | +| `4096` | RSA 4096-bit | -For example: +**Examples:** -### Single domain with ECDSA P-384 certificate +#### Single domain with ECDSA P-384 certificate ```bash acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384 ``` -### SAN multi domain with RSA4096 certificate +#### SAN multi domain with RSA4096 certificate ```bash acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096 ``` -# 11. Issue Wildcard certificates +--- -It's simple, just give a wildcard domain as the `-d` parameter. +### 1️⃣1️⃣ Issue Wildcard Certificates + +It's simple! Just give a wildcard domain as the `-d` parameter: ```sh -acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf +acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf ``` -# 12. How to renew the certs +--- -No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days. +### 1️⃣2️⃣ How to Renew Certificates -However, you can also force to renew a cert: +> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days. + +However, you can force a renewal: ```sh acme.sh --renew -d example.com --force ``` -or, for ECC cert: +**For ECC cert:** ```sh acme.sh --renew -d example.com --force --ecc ``` +--- -# 13. How to stop cert renewal +### 1️⃣3️⃣ How to Stop Certificate Renewal To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: @@ -425,73 +462,78 @@ acme.sh --remove -d example.com [--ecc] The cert/key file is not removed from the disk. -You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself. +> 💡 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) manually. +--- -# 14. How to upgrade `acme.sh` +### 1️⃣4️⃣ How to Upgrade acme.sh -acme.sh is in constant development, so it's strongly recommended to use the latest code. +> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code. -You can update acme.sh to the latest code: +**Update to latest:** ```sh acme.sh --upgrade ``` -You can also enable auto upgrade: +**Enable auto upgrade:** ```sh acme.sh --upgrade --auto-upgrade ``` -Then **acme.sh** will be kept up to date automatically. - -Disable auto upgrade: +**Disable auto upgrade:** ```sh acme.sh --upgrade --auto-upgrade 0 ``` +--- -# 15. Issue a cert from an existing CSR +### 1️⃣5️⃣ Issue a Certificate from an Existing CSR -https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR +📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR +--- -# 16. Send notifications in cronjob +### 1️⃣6️⃣ Send Notifications in Cronjob -https://github.com/acmesh-official/acme.sh/wiki/notify +📚 https://github.com/acmesh-official/acme.sh/wiki/notify +--- -# 17. Under the Hood +### 1️⃣7️⃣ Under the Hood -Speak ACME language using shell, directly to "Let's Encrypt". +> 🔧 Speak ACME language using shell, directly to "Let's Encrypt". -TODO: +--- +### 1️⃣8️⃣ Acknowledgments -# 18. Acknowledgments +| Project | Link | +|---------|------| +| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny | +| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme | -1. Acme-tiny: https://github.com/diafygi/acme-tiny -2. ACME protocol: https://github.com/ietf-wg-acme/acme +--- +## 👥 Contributors -## Contributors - -### Code Contributors +### 💻 Code Contributors This project exists thanks to all the people who contribute. + -### Financial Contributors +### 💰 Financial Contributors Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)] -#### Individuals +#### 👤 Individuals -#### Organizations +#### 🏢 Organizations Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)] @@ -506,25 +548,31 @@ Support this project with your organization. Your logo will show up here with a +--- +### 1️⃣9️⃣ License & Others -# 19. License & Others +📄 **License:** GPLv3 -License is GPLv3 +⭐ Please **Star** and **Fork** this project! -Please Star and Fork me. +🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome. -[Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome. +--- +### 2️⃣0️⃣ Donate -# 20. Donate -Your donation makes **acme.sh** better: +> 💝 Your donation makes **acme.sh** better! -1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) +| Method | Link | +|--------|------| +| PayPal / Alipay(支付宝) / Wechat(微信) | [https://donate.acme.sh/](https://donate.acme.sh/) | -[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) +📜 [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) -# 21. About this repository +--- + +### 2️⃣1️⃣ About This Repository > [!NOTE] > This repository is officially maintained by ZeroSSL as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community! @@ -532,7 +580,7 @@ Your donation makes **acme.sh** better: > > All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL.

- + diff --git a/acme.sh b/acme.sh index 053f16db..5cd2cb3f 100755 --- a/acme.sh +++ b/acme.sh @@ -65,7 +65,7 @@ ID_TYPE_IP="ip" LOCAL_ANY_ADDRESS="0.0.0.0" -DEFAULT_RENEW=60 +DEFAULT_RENEW=30 NO_VALUE="no" From 361e7c5ad4e26d2b790c187d2cb36555cc040d03 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Dec 2025 11:26:59 +0100 Subject: [PATCH 330/689] use nfs for vms use nfs for vms --- .github/workflows/DNS.yml | 12 ++++++------ .github/workflows/DragonFlyBSD.yml | 2 +- .github/workflows/FreeBSD.yml | 2 +- .github/workflows/NetBSD.yml | 2 +- .github/workflows/Omnios.yml | 2 +- .github/workflows/OpenBSD.yml | 2 +- .github/workflows/Solaris.yml | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index ccce2ff6..18b763c1 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -232,7 +232,7 @@ jobs: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg install -y socat curl usesh: true - copyback: false + sync: nfs run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" @@ -283,7 +283,7 @@ jobs: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg_add socat curl libiconv usesh: true - copyback: false + sync: nfs run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" @@ -335,7 +335,7 @@ jobs: prepare: | /usr/sbin/pkg_add curl socat usesh: true - copyback: false + sync: nfs run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" @@ -387,7 +387,7 @@ jobs: prepare: | pkg install -y curl socat libnghttp2 usesh: true - copyback: false + sync: nfs run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" @@ -440,7 +440,7 @@ jobs: - uses: vmactions/solaris-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - copyback: false + sync: nfs prepare: | pkgutil -U pkgutil -y -i socat @@ -493,7 +493,7 @@ jobs: - uses: vmactions/omnios-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' - copyback: false + sync: nfs prepare: pkg install socat run: | if [ "${{ secrets.TokenName1}}" ] ; then diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index 5c56168f..b047a210 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -63,7 +63,7 @@ jobs: prepare: | pkg install -y curl socat libnghttp2 usesh: true - copyback: false + sync: nfs run: | cd ../acmetest \ && ./letest.sh diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 961907e8..a4fca67c 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -68,7 +68,7 @@ jobs: "8080": "80" prepare: pkg install -y socat curl wget usesh: true - copyback: false + sync: nfs run: | cd ../acmetest \ && ./letest.sh diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index a4f90f68..13b70350 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -63,7 +63,7 @@ jobs: prepare: | /usr/sbin/pkg_add curl socat usesh: true - copyback: false + sync: nfs run: | cd ../acmetest \ && ./letest.sh diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index 882cedf6..5d0af1b1 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -67,7 +67,7 @@ jobs: nat: | "8080": "80" prepare: pkg install socat wget - copyback: false + sync: nfs run: | cd ../acmetest \ && ./letest.sh diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index d5697c10..98e18545 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -68,7 +68,7 @@ jobs: "8080": "80" prepare: pkg_add socat curl wget libnghttp2 usesh: true - copyback: false + sync: nfs run: | cd ../acmetest \ && ./letest.sh diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 0ba3d2eb..21a16d1a 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -69,7 +69,7 @@ jobs: prepare: | pkgutil -U pkgutil -y -i socat curl wget - copyback: false + sync: nfs run: | cd ../acmetest \ && ./letest.sh From 49a3d586a3673b41fb08169a37a970464f86bc03 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Dec 2025 11:37:39 +0100 Subject: [PATCH 331/689] Add OpenIndiana CI workflow Introduces a GitHub Actions workflow for OpenIndiana to automate testing of shell scripts. --- .github/workflows/OpenIndiana.yml | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/OpenIndiana.yml diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml new file mode 100644 index 00000000..19b1efaa --- /dev/null +++ b/.github/workflows/OpenIndiana.yml @@ -0,0 +1,75 @@ +name: OpenIndiana +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/OpenIndiana.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/OpenIndiana.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + OpenIndiana: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/openindiana-vm@v0 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkg install socat curl + sync: nfs + run: | + cd ../acmetest \ + && ./letest.sh + + From 76fdac59bc77757d2691c010be0b89a52ba6da47 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Dec 2025 11:41:58 +0100 Subject: [PATCH 332/689] Add OpenIndiana CI workflow and badge Introduced a new OpenIndiana job to the DNS GitHub Actions workflow for testing, including necessary environment variables and steps. Updated README to display the OpenIndiana workflow status badge. --- .github/workflows/DNS.yml | 49 +++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 50 insertions(+) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 18b763c1..b200f56b 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -515,3 +515,52 @@ jobs: ./letest.sh + OpenIndiana: + runs-on: ubuntu-latest + needs: Omnios + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/openindiana-vm@v0 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + sync: nfs + prepare: pkg install socat + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + + diff --git a/README.md b/README.md index d6ddf36e..149bca2b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Solaris DragonFlyBSD Omnios + OpenIndiana

From 47f24126f5ec95e192d1697f8588097ea902a2a6 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 27 Dec 2025 11:47:16 +0100 Subject: [PATCH 333/689] Update supported OS table in README add openindiana --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 149bca2b..41b6b272 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ |8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD |9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD |10|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios -|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian -|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS +|11|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana +|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian |13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE |14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) |15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux @@ -104,10 +104,10 @@ |17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux |18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux |19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia -|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux -|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 -|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) -|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) +|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux +|21|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 +|22|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) +|23|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From 57db3889325ef95896d2677306e7d20d4d3360c8 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:25:28 -0500 Subject: [PATCH 334/689] Docker with non-root using supercronic Replaces cronie with supercronic to allow non-root users to have cronjobs. Creates user/group acme:acme UID:1000/GID:1000 with home directory pointing to LE_CONFIG_HOME (default: /acme.sh) 'crontab' is generated in LE_CONFIG_HOME which is used by supercronic. Note that `acme.sh --installcronjob` and `--uninstallcronjob` when run as a non-root user will fail but neither of should be used in `daemon` mode anyway. Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36b2adac..64d14909 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN apk --no-cache add -f \ libidn \ jq \ yq-go \ - cronie + supercronic ENV LE_WORKING_DIR=/acmebin @@ -30,10 +30,12 @@ COPY ./deploy /install_acme.sh/deploy COPY ./dnsapi /install_acme.sh/dnsapi COPY ./notify /install_acme.sh/notify +RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H -u 1000 acme + RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ - -RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - +RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh \ + && crontab -l | grep acme.sh | sed 's#> /dev/null##' > $LE_CONFIG_HOME/crontab RUN for verb in help \ version \ @@ -72,12 +74,15 @@ RUN for verb in help \ RUN printf "%b" '#!'"/usr/bin/env sh\n \ if [ \"\$1\" = \"daemon\" ]; then \n \ - exec crond -n -s -m off \n \ + echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \ + exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \ else \n \ exec -- \"\$@\"\n \ fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME VOLUME /acme.sh +USER 1000:1000 + ENTRYPOINT ["/entry.sh"] CMD ["--help"] From 6f5a0c5d5e961a7886cd3fae6ac72daab94787b1 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:45:41 -0500 Subject: [PATCH 335/689] have entry.sh (instead of dockerfile) generate crontab file Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 64d14909..626f835d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,8 +34,7 @@ RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh \ - && crontab -l | grep acme.sh | sed 's#> /dev/null##' > $LE_CONFIG_HOME/crontab +RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh RUN for verb in help \ version \ @@ -74,6 +73,13 @@ RUN for verb in help \ RUN printf "%b" '#!'"/usr/bin/env sh\n \ if [ \"\$1\" = \"daemon\" ]; then \n \ + if [ ! -f \"\$LE_CONFIG_HOME/crontab\" ]; then \n \ + echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \ + time=\$(date -u \"+%s\") \n \ + random_minute=\$((\$time % 60)) \n \ + random_hour=\$((\$time / 60 % 24)) \n \ + echo \"\$random_minute \$random_hour * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \ + fi \n \ echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \ exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \ else \n \ @@ -82,7 +88,5 @@ fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmo VOLUME /acme.sh -USER 1000:1000 - ENTRYPOINT ["/entry.sh"] CMD ["--help"] From e03f8d3ad61d62f75ef34494fdb27109093debc4 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Sun, 28 Dec 2025 12:03:02 -0500 Subject: [PATCH 336/689] fix: savedeployconf for DEPLOY_LOCALCOPY_CERTIFICATE Co-authored-by: Kevin Hoser <45083826+hoser21@users.noreply.github.com> --- deploy/localcopy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index ddb7d4b6..b10fd1f0 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -98,6 +98,7 @@ localcopy_deploy() { _err "Failed to copy certificate, aborting." return 1 fi + _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" fi if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then From 21d52b5995b94f873c074662988972ad8392c5f9 Mon Sep 17 00:00:00 2001 From: Hugo Haakseth Date: Tue, 30 Dec 2025 10:57:12 +0100 Subject: [PATCH 337/689] Store pfx password base64 encoded --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 5cd2cb3f..1401870b 100755 --- a/acme.sh +++ b/acme.sh @@ -1466,7 +1466,7 @@ _toPkcs() { ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca" fi if [ "$?" = "0" ]; then - _savedomainconf "Le_PFXPassword" "$pfxPassword" + _savedomainconf "Le_PFXPassword" "$pfxPassword" "base64" fi } @@ -5450,10 +5450,10 @@ $_authorizations_map" _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" #convert to pkcs12 + Le_PFXPassword="$(_readdomainconf Le_PFXPassword)" if [ "$Le_PFXPassword" ]; then _toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword" fi - export CERT_PFX_PATH if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then _savedomainconf "Le_RealCertPath" "$_real_cert" From 4219f7b2f69364104b51ff9d3a3fac6dcb7d0c76 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:21:51 -0500 Subject: [PATCH 338/689] align logic to acme.sh installcert(), fix perms on non-key files Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 57 +++++++-------------------------------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index b10fd1f0..9a1a0fcf 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -48,13 +48,13 @@ localcopy_deploy() { _combined_target="" _combined_srccert="" + # Create PEM file if [ "$DEPLOY_LOCALCOPY_CERTKEY" ] && { [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ] || [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; }; then _combined_target="$DEPLOY_LOCALCOPY_CERTKEY" _savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY" - if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _combined_srccert="$_ccert" _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" @@ -69,31 +69,18 @@ localcopy_deploy() { _info "Creating combined PEM" _debug "Creating combined PEM at $_combined_target" if ! [ -f "$_combined_target" ]; then - if ! ( - touch "$_combined_target" - chmod 600 "$_combined_target" - ); then - _err "Failed to create PEM file" - return 1 - fi + touch "$_combined_target" || return 1 + chmod 600 "$_combined_target" fi if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then _err "Failed to create PEM file" return 1 fi fi + if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _info "Copying certificate" _debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE" - if ! [ -f "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_CERTIFICATE" - chmod 600 "$DEPLOY_LOCALCOPY_CERTIFICATE" - ); then - _err "Failed to copy certificate, aborting." - return 1 - fi - fi if ! cat "$_ccert" >"$DEPLOY_LOCALCOPY_CERTIFICATE"; then _err "Failed to copy certificate, aborting." return 1 @@ -105,13 +92,8 @@ localcopy_deploy() { _info "Copying certificate key" _debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY" if ! [ -f "$DEPLOY_LOCALCOPY_CERTKEY" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_CERTKEY" - chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY" - ); then - _err "Failed to copy certificate key, aborting." - return 1 - fi + touch "$DEPLOY_LOCALCOPY_CERTKEY" || return 1 + chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY" fi if ! cat "$_ckey" >"$DEPLOY_LOCALCOPY_CERTKEY"; then _err "Failed to copy certificate key, aborting." @@ -123,15 +105,6 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then _info "Copying fullchain" _debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN" - if ! [ -f "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_FULLCHAIN" - chmod 600 "$DEPLOY_LOCALCOPY_FULLCHAIN" - ); then - _err "Failed to copy fullchain, aborting." - return 1 - fi - fi if ! cat "$_cfullchain" >"$DEPLOY_LOCALCOPY_FULLCHAIN"; then _err "Failed to copy fullchain, aborting." return 1 @@ -142,15 +115,6 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_CA" ]; then _info "Copying CA" _debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA" - if ! [ -f "$DEPLOY_LOCALCOPY_CA" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_CA" - chmod 600 "$DEPLOY_LOCALCOPY_CA" - ); then - _err "Failed to copy CA, aborting." - return 1 - fi - fi if ! cat "$_cca" >"$DEPLOY_LOCALCOPY_CA"; then _err "Failed to copy CA, aborting." return 1 @@ -162,13 +126,8 @@ localcopy_deploy() { _info "Copying PFX" _debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX" if ! [ -f "$DEPLOY_LOCALCOPY_PFX" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_PFX" - chmod 600 "$DEPLOY_LOCALCOPY_PFX" - ); then - _err "Failed to copy PFX, aborting." - return 1 - fi + touch "$DEPLOY_LOCALCOPY_PFX" || return 1 + chmod 600 "$DEPLOY_LOCALCOPY_PFX" fi if ! cat "$_cpfx" >"$DEPLOY_LOCALCOPY_PFX"; then _err "Failed to copy PFX, aborting." From 6a98b9f81e9057cd0eda1427a446da20cc305d1d Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:44:46 -0500 Subject: [PATCH 339/689] chown /acme.sh to non-root user and set HOME to /acme.sh Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 626f835d..15439e5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,8 @@ ENV LE_WORKING_DIR=/acmebin ENV LE_CONFIG_HOME=/acme.sh +ENV HOME=/acme.sh + ARG AUTO_UPGRADE=1 ENV AUTO_UPGRADE=$AUTO_UPGRADE @@ -36,6 +38,8 @@ RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/ RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh +RUN chown -R acme:acme $LE_CONFIG_HOME + RUN for verb in help \ version \ install \ From 162cfebbbb1122a53f3b62f46365084a5052cf05 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 15:36:06 -0500 Subject: [PATCH 340/689] Removed jq requirement --- dnsapi/dns_qc.sh | 57 ++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index ed784f28..1073200f 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -18,11 +18,6 @@ dns_qc_add() { txtvalue=$2 _debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" - if ! _exists jq; then - _err "In dns_qc jq not found." - return 1 - fi - QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}" QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" @@ -108,23 +103,43 @@ dns_qc_rm() { return 1 fi - 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." - else - record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//') - _debug "txt record_id" "$record_id" - if [ -z "$record_id" ]; then - _err "Can not get txt record id to remove. Run in debug mode." - return 1 - fi - 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" + _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" + 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, noting 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 } From e031457cfacbf3abd35a51bbc56d443c09a975b4 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 15:48:29 -0500 Subject: [PATCH 341/689] shfmt fixes --- dnsapi/dns_qc.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 1073200f..35e99fa2 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -130,15 +130,18 @@ dns_qc_rm() { fi done < $tmpfile rm $tmpfile + if [ -z "$record_id" ]; then - _info "TXT record, or $txtvalue not found, noting to remove" + _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 } From 185d92f1e7d7ad62d84178d06bce9ae0be49775e Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 15:50:39 -0500 Subject: [PATCH 342/689] shfmt fixes --- dnsapi/dns_qc.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 35e99fa2..68c33ebc 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -141,9 +141,10 @@ dns_qc_rm() { _info "Delete txt record error." return 1 fi - + _info "TXT Record ID: $record_id successfully deleted" return 0 + } #################### Private functions below ################################## From 0b66acf332c539466ea4039b8e473246b3a26eba Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 15:54:23 -0500 Subject: [PATCH 343/689] shfmt fixes --- dnsapi/dns_qc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 68c33ebc..9fcf977a 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -144,7 +144,7 @@ dns_qc_rm() { _info "TXT Record ID: $record_id successfully deleted" return 0 - + } #################### Private functions below ################################## From b4f30ff02678035e56339b44d0238d7583abaead Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 16:07:01 -0500 Subject: [PATCH 344/689] Updated for shfmt --- dnsapi/dns_qc.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 9fcf977a..65e1c90b 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -121,14 +121,14 @@ dns_qc_rm() { fi # Temporary file to hold matched content (one per line) tmpfile=$(_mktemp) - echo $array | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' > "$tmpfile" + echo $array | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile" 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 + 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/' ) + record_id=$(echo $obj | sed 's/^\"id\":\([0-9]\+\).*/\1/') break fi - done < $tmpfile + done <$tmpfile rm $tmpfile if [ -z "$record_id" ]; then From 397c0605e575b70f9d76c4863a40c7dde374d5b4 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 16:11:39 -0500 Subject: [PATCH 345/689] Double quote for globbing --- dnsapi/dns_qc.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 65e1c90b..efa54fda 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -121,15 +121,15 @@ dns_qc_rm() { fi # Temporary file to hold matched content (one per line) tmpfile=$(_mktemp) - echo $array | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile" + echo $array | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' > "$tmpfile" 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 + 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 + done < "$tmpfile" + rm "$tmpfile" if [ -z "$record_id" ]; then _info "TXT record, or $txtvalue not found, nothing to remove" From 778b4a38edef9f8d4aa5f6d9494af37db838f6fc Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 16:17:03 -0500 Subject: [PATCH 346/689] Missed several double quote issues --- dnsapi/dns_qc.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index efa54fda..f48eeb08 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -114,18 +114,18 @@ dns_qc_rm() { #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/') + 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" + echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' > "$tmpfile" 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 + 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/') + record_id=$(echo "$obj" | sed 's/^\"id\":\([0-9]\+\).*/\1/') break fi done < "$tmpfile" From f9ffdbe4074bce3bab40220cdfb2fa337c303360 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 16:23:01 -0500 Subject: [PATCH 347/689] Initialize record_id --- dnsapi/dns_qc.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index f48eeb08..ecf5d387 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -122,6 +122,8 @@ dns_qc_rm() { # 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" @@ -129,6 +131,7 @@ dns_qc_rm() { break fi done < "$tmpfile" + rm "$tmpfile" if [ -z "$record_id" ]; then From cf2f9ef2518dcefa3fa8f2bb50841fc56bb01b43 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 16:27:21 -0500 Subject: [PATCH 348/689] Missed additional quotes --- dnsapi/dns_qc.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index ecf5d387..756939cb 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -125,13 +125,13 @@ dns_qc_rm() { 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 + 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 From 6a37f23b143f8701b19631c7d3052a63b450f602 Mon Sep 17 00:00:00 2001 From: Bob Perper Date: Tue, 30 Dec 2025 16:31:16 -0500 Subject: [PATCH 349/689] Ran shfmt locally --- dnsapi/dns_qc.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_qc.sh b/dnsapi/dns_qc.sh index 756939cb..78756a35 100755 --- a/dnsapi/dns_qc.sh +++ b/dnsapi/dns_qc.sh @@ -121,16 +121,16 @@ dns_qc_rm() { fi # Temporary file to hold matched content (one per line) tmpfile=$(_mktemp) - echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' > "$tmpfile" + 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 + 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" + done <"$tmpfile" rm "$tmpfile" From 2ad984d8ada9cf4e67ee0c6e61fd76c92d195291 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 1 Jan 2026 13:26:02 +0000 Subject: [PATCH 350/689] feat(qiniu): make forceHttps configurable via environment variable Add QINIU_FORCE_HTTPS environment variable (default: false) to allow configuring HTTPS redirect behavior for CDN domains. --- deploy/qiniu.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deploy/qiniu.sh b/deploy/qiniu.sh index 02250ed3..3737ed4e 100644 --- a/deploy/qiniu.sh +++ b/deploy/qiniu.sh @@ -8,6 +8,8 @@ # export QINIU_CDN_DOMAIN="cdn.example.com" # If you have more than one domain, just # export QINIU_CDN_DOMAIN="cdn1.example.com cdn2.example.com" +# Optional: force HTTPS redirect (default: false) +# export QINIU_FORCE_HTTPS="true" QINIU_API_BASE="https://api.qiniu.com" @@ -44,6 +46,12 @@ qiniu_deploy() { QINIU_CDN_DOMAIN="$_cdomain" fi + if [ -z "$QINIU_FORCE_HTTPS" ]; then + QINIU_FORCE_HTTPS="false" + else + _savedomainconf QINIU_FORCE_HTTPS "$QINIU_FORCE_HTTPS" + fi + ## upload certificate string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n') string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') @@ -69,7 +77,7 @@ qiniu_deploy() { _debug certId "$_certId" ## update domain ssl config - update_body="{\"certid\":$_certId,\"forceHttps\":false}" + update_body="{\"certid\":$_certId,\"forceHttps\":$QINIU_FORCE_HTTPS}" for domain in $QINIU_CDN_DOMAIN; do update_path="/domain/$domain/httpsconf" update_access_token="$(_make_access_token "$update_path")" From ef2089ceb1fc384971b7ab237c0816c255c6dbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Fri, 2 Jan 2026 14:48:11 +0800 Subject: [PATCH 351/689] Update directory iteration pattern in acme.sh --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 4b48036a..fb1d3e12 100755 --- a/acme.sh +++ b/acme.sh @@ -5565,7 +5565,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - for di in "${CERT_HOME}"/*/; do + for di in "${CERT_HOME}"/*[.:]*/; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" From b08bb2ef69b087ebf36f0cae37471b1e0561f68c Mon Sep 17 00:00:00 2001 From: Jacobo de Vera Date: Fri, 2 Jan 2026 12:08:22 +0000 Subject: [PATCH 352/689] Fix list command for POSIX sh by avoiding brace expansion --- acme.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index fc2afcc8..645de117 100755 --- a/acme.sh +++ b/acme.sh @@ -5845,7 +5845,8 @@ list() { if [ -z "$_domain" ]; then printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew" fi - for di in "${CERT_HOME}"/{*.*,*:*}/; do + for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do + [ -d "$di" ] || continue d=$(basename "$di") _debug d "$d" ( From 045e4dee2ed765a8a9d8f8c44fdd6358ce9aae70 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 2 Jan 2026 16:24:06 +0100 Subject: [PATCH 353/689] use openindiana-vm@v1 --- .github/workflows/DNS.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index b200f56b..c0c51a84 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -539,7 +539,7 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - uses: vmactions/openindiana-vm@v0 + - uses: vmactions/openindiana-vm@v1 with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index 19b1efaa..d17803de 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -61,7 +61,7 @@ jobs: run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - uses: vmactions/openindiana-vm@v0 + - uses: vmactions/openindiana-vm@v1 with: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | From 94c670a759eadc1e9acb88f3dfa61c8a340c7b61 Mon Sep 17 00:00:00 2001 From: Jens Spanier <42373861+JensSpanier@users.noreply.github.com> Date: Sun, 4 Jan 2026 11:49:35 +0100 Subject: [PATCH 354/689] Remove asterisks and line breaks --- notify/pushover.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notify/pushover.sh b/notify/pushover.sh index 0f99739a..c59ec026 100644 --- a/notify/pushover.sh +++ b/notify/pushover.sh @@ -46,8 +46,8 @@ pushover_send() { fi export _H1="Content-Type: application/json" - _content="$(printf "*%s*\n" "$_content" | _json_encode)" - _subject="$(printf "*%s*\n" "$_subject" | _json_encode)" + _content="$(printf "%s" "$_content" | _json_encode)" + _subject="$(printf "%s" "$_subject" | _json_encode)" _data="{\"token\": \"$PUSHOVER_TOKEN\",\"user\": \"$PUSHOVER_USER\",\"title\": \"$_subject\",\"message\": \"$_content\",\"sound\": \"$PUSHOVER_SOUND\", \"device\": \"$PUSHOVER_DEVICE\", \"priority\": \"$PUSHOVER_PRIORITY\"}" response="$(_post "$_data" "$PUSHOVER_URI")" From 877cbe04c915bdb00c48612e0f6f3e51082c89d4 Mon Sep 17 00:00:00 2001 From: Amin Sharifi Date: Sun, 4 Jan 2026 16:18:18 +0330 Subject: [PATCH 355/689] Add VirakCloud DNS API support with add and remove TXT record functions --- dnsapi/dns_virakcloud.sh | 229 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100755 dnsapi/dns_virakcloud.sh diff --git a/dnsapi/dns_virakcloud.sh b/dnsapi/dns_virakcloud.sh new file mode 100755 index 00000000..ade4796b --- /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 +} From ef035248c35976ff849eda4011e67f08bac41831 Mon Sep 17 00:00:00 2001 From: Amin Sharifi Date: Sun, 4 Jan 2026 16:29:30 +0330 Subject: [PATCH 356/689] Add VirakCloud DNS API support --- dnsapi/dns_virakcloud.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dnsapi/dns_virakcloud.sh b/dnsapi/dns_virakcloud.sh index ade4796b..84da5773 100755 --- a/dnsapi/dns_virakcloud.sh +++ b/dnsapi/dns_virakcloud.sh @@ -43,6 +43,7 @@ dns_virakcloud_add() { _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" From 70462b5ac3f257d8814002427b0693f67b437b3c Mon Sep 17 00:00:00 2001 From: Amin Sharifi Date: Sun, 4 Jan 2026 16:33:37 +0330 Subject: [PATCH 357/689] run `~/shfmt -l -w -i 2 dnsapi/dns_virakcloud.sh` and Remove unnecessary blank lines in dns_virakcloud.sh --- dnsapi/dns_virakcloud.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_virakcloud.sh b/dnsapi/dns_virakcloud.sh index 84da5773..7ae665d2 100755 --- a/dnsapi/dns_virakcloud.sh +++ b/dnsapi/dns_virakcloud.sh @@ -43,7 +43,6 @@ dns_virakcloud_add() { _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" @@ -101,11 +100,11 @@ dns_virakcloud_rm() { 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) + contentid=$(echo "$target_obj" | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4) fi if [ -z "$contentid" ]; then @@ -197,7 +196,7 @@ _virakcloud_rest() { fi _ret="$?" - + if [ "$_ret" != "0" ]; then _err "error on $m $ep" return 1 From 35f99c545c2c9dd9bca8211e3ab29bbcd123d259 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 5 Jan 2026 21:10:58 +0100 Subject: [PATCH 358/689] add debug wiki --- .github/workflows/DNS.yml | 42 +++++++++++++++++++++++++++--- .github/workflows/DragonFlyBSD.yml | 6 ++++- .github/workflows/FreeBSD.yml | 6 ++++- .github/workflows/NetBSD.yml | 8 ++++-- .github/workflows/Omnios.yml | 6 ++++- .github/workflows/OpenBSD.yml | 6 ++++- .github/workflows/OpenIndiana.yml | 6 ++++- .github/workflows/Solaris.yml | 6 ++++- 8 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index c0c51a84..4634ed96 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -251,7 +251,11 @@ jobs: fi cd ../acmetest ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" @@ -302,7 +306,11 @@ jobs: fi cd ../acmetest ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" @@ -354,7 +362,11 @@ jobs: fi cd ../acmetest ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" @@ -406,7 +418,11 @@ jobs: fi cd ../acmetest ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" @@ -464,6 +480,11 @@ jobs: fi cd ../acmetest ./letest.sh + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" Omnios: @@ -513,6 +534,12 @@ jobs: fi cd ../acmetest ./letest.sh + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + OpenIndiana: @@ -562,5 +589,12 @@ jobs: fi cd ../acmetest ./letest.sh + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index b047a210..dda8c99f 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -67,5 +67,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index a4fca67c..21123c4a 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -72,5 +72,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 13b70350..40421552 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -67,5 +67,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index 5d0af1b1..20eb24d7 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -71,5 +71,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index 98e18545..fab6e4fd 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -72,5 +72,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index d17803de..abad376c 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -71,5 +71,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 21a16d1a..2388da71 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -73,5 +73,9 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" From 2092d6061b0ec963534b8a39b0624be96c07e2c4 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 5 Jan 2026 21:38:37 +0100 Subject: [PATCH 359/689] fix https://github.com/acmesh-official/acme.sh/issues/6736#issuecomment-3707981300 --- acme.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 645de117..2d2ef796 100755 --- a/acme.sh +++ b/acme.sh @@ -5749,6 +5749,10 @@ signcsr() { _local_addr="${11}" _challenge_alias="${12}" _preferred_chain="${13}" + _valid_f="${14}" + _valid_t="${15}" + _cert_prof="${16}" + _en_key_usage="${17}" _csrsubj=$(_readSubjectFromCSR "$_csrfile") if [ "$?" != "0" ]; then @@ -5792,7 +5796,7 @@ signcsr() { _info "Copying CSR to: $CSR_PATH" cp "$_csrfile" "$CSR_PATH" - issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" + issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" "$_valid_f" "$_valid_t" "$_cert_prof" "$_en_key_usage" } @@ -8148,7 +8152,7 @@ _process() { deploy "$_domain" "$_deploy_hook" "$_ecc" ;; signcsr) - signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" + signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage" ;; showcsr) showcsr "$_csr" "$_domain" From 903a53991d6cf01170864d09585b2406f5c5f16e Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 5 Jan 2026 22:06:33 +0100 Subject: [PATCH 360/689] fix bugs --- acme.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 2cec6592..d2e3b766 100755 --- a/acme.sh +++ b/acme.sh @@ -4470,7 +4470,7 @@ issue() { Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) _debug Le_NextRenewTime "$Le_NextRenewTime" if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then - _valid_to_saved=$(_readdomainconf Le_Valid_to) + _valid_to_saved=$(_readdomainconf Le_Valid_To) if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then _info "The domain is set to be valid to: $_valid_to_saved" _info "It cannot be renewed automatically" @@ -5568,6 +5568,10 @@ renew() { Le_RenewHook="$(_readdomainconf Le_RenewHook)" Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)" Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)" + Le_Valid_From="$(_readdomainconf Le_Valid_From)" + Le_Valid_To="$(_readdomainconf Le_Valid_To)" + Le_ExtKeyUse="$(_readdomainconf Le_ExtKeyUse)" + # When renewing from an old version, the empty Le_Keylength means 2048. # Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over # time but an empty value implies 2048 specifically. From 5ad2bea129b6a497aae30e50f64ac387a629d0f6 Mon Sep 17 00:00:00 2001 From: Denis Kurz Date: Sat, 10 Jan 2026 01:03:37 +0100 Subject: [PATCH 361/689] fix typos --- dnsapi/dns_dynv6.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_dynv6.sh b/dnsapi/dns_dynv6.sh index 0c9491f8..a68b5fb9 100644 --- a/dnsapi/dns_dynv6.sh +++ b/dnsapi/dns_dynv6.sh @@ -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() { @@ -217,9 +217,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" From 880d93f7f7645995b716f39e9666cb76acc10351 Mon Sep 17 00:00:00 2001 From: Denis Kurz Date: Sat, 10 Jan 2026 01:06:48 +0100 Subject: [PATCH 362/689] fix(dynv6): allow 'id' in dns challenge If the random dns challenge string happens to contain 'id', the parsing method passed a broken, mingled mix of the record's data and id field, instead of just the id. As a result, deleting the TXT record failed. We now specifically look for '"id":', which cannot appear as part of the challenge string. --- dnsapi/dns_dynv6.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_dynv6.sh b/dnsapi/dns_dynv6.sh index a68b5fb9..d5d49094 100644 --- a/dnsapi/dns_dynv6.sh +++ b/dnsapi/dns_dynv6.sh @@ -234,7 +234,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="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep '"id":' | tr -d '"' | tr -d 'id:' | tr -d '{')" #_record_id="${_record_id#id:}" if [ -z "$_record_id" ]; then _err "no such record: $record found in zone $_zone_id" From b37867d027d6b54e3946193fa2d672de225b1c5f Mon Sep 17 00:00:00 2001 From: Denis Kurz Date: Sat, 10 Jan 2026 01:26:06 +0100 Subject: [PATCH 363/689] remove unused code --- dnsapi/dns_dynv6.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/dnsapi/dns_dynv6.sh b/dnsapi/dns_dynv6.sh index d5d49094..3e7ce8d6 100644 --- a/dnsapi/dns_dynv6.sh +++ b/dnsapi/dns_dynv6.sh @@ -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 @@ -235,7 +234,6 @@ _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:' | tr -d '{')" - #_record_id="${_record_id#id:}" if [ -z "$_record_id" ]; then _err "no such record: $record found in zone $_zone_id" return 1 From 5e670e0d93199c7a74146c9c93c4c1eed92e16fe Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 11 Jan 2026 21:02:47 +0100 Subject: [PATCH 364/689] support haiku --- .github/workflows/DNS.yml | 53 ++++++++++++++++++ .github/workflows/Haiku.yml | 79 +++++++++++++++++++++++++++ acme.sh | 104 +++++++++++++++++++++++++----------- 3 files changed, 205 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/Haiku.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 4634ed96..957d0e97 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -597,4 +597,57 @@ jobs: + Haiku: + runs-on: ubuntu-latest + needs: OpenIndiana + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v4 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/haiku-vm@v1 + with: + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + sync: rsync + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml new file mode 100644 index 00000000..3ae50051 --- /dev/null +++ b/.github/workflows/Haiku.yml @@ -0,0 +1,79 @@ +name: Haiku +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Haiku.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Haiku.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + Haiku: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" + # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v4 + - uses: vmactions/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/haiku-vm@v1 + with: + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkg install socat curl + sync: nfs + run: | + cd ../acmetest \ + && ./letest.sh + - name: onError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + diff --git a/acme.sh b/acme.sh index d2e3b766..1a3763c3 100755 --- a/acme.sh +++ b/acme.sh @@ -250,6 +250,13 @@ _dlg_versions() { socat -V 2>&1 else _debug "socat doesn't exist." + if _exists "python3"; then + python3 -V 2>&1 + elif _exists "python2"; then + python2 -V 2>&1 + elif _exists "python"; then + python -V 2>&1 + fi fi } @@ -2559,41 +2566,76 @@ _startserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" - _NC="socat" - if [ "$Le_Listen_V6" ]; then - _NC="$_NC -6" - SOCAT_OPTIONS=TCP6-LISTEN - elif [ "$Le_Listen_V4" ]; then - _NC="$_NC -4" - SOCAT_OPTIONS=TCP4-LISTEN - else - SOCAT_OPTIONS=TCP-LISTEN - fi + if _exists "socat"; then + _NC="socat" + if [ "$Le_Listen_V6" ]; then + _NC="$_NC -6" + SOCAT_OPTIONS=TCP6-LISTEN + elif [ "$Le_Listen_V4" ]; then + _NC="$_NC -4" + SOCAT_OPTIONS=TCP4-LISTEN + else + SOCAT_OPTIONS=TCP-LISTEN + fi - if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then - _NC="$_NC -d -d -v" - fi + if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then + _NC="$_NC -d -d -v" + fi - SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork + SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork - #Adding bind to local-address - if [ "$ncaddr" ]; then - SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}" - fi + #Adding bind to local-address + if [ "$ncaddr" ]; then + SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}" + fi - _content_len="$(printf "%s" "$content" | wc -c)" - _debug _content_len "$_content_len" - _debug "_NC" "$_NC $SOCAT_OPTIONS" - export _SOCAT_ERR="$(_mktemp)" - $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ + _content_len="$(printf "%s" "$content" | wc -c)" + _debug _content_len "$_content_len" + _debug "_NC" "$_NC $SOCAT_OPTIONS" + export _SOCAT_ERR="$(_mktemp)" + $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ echo 'HTTP/1.0 200 OK'; \ echo 'Content-Length\: $_content_len'; \ echo ''; \ printf '%s' '$content';" 2>"$_SOCAT_ERR" & - serverproc="$!" + serverproc="$!" + else + _PYTHON="" + if _exists "python3"; then + _PYTHON="python3" + elif _exists "python2"; then + _PYTHON="python2" + elif _exists "python"; then + _PYTHON="python" + fi + if [ "$_PYTHON" ]; then + _debug "Using python: $_PYTHON" + _AF="socket.AF_INET" + _BIND_ADDR="0.0.0.0" + if [ "$Le_Listen_V6" ]; then + _AF="socket.AF_INET6" + _BIND_ADDR="::" + fi + if [ "$ncaddr" ]; then + _BIND_ADDR="$ncaddr" + fi + export _SOCAT_ERR="$(_mktemp)" + $_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3]; +while True: + c,a=s.accept() + c.sendall(res.encode() if hasattr(res, 'encode') else res) + c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & + serverproc="$!" + _NC="$_PYTHON" + else + _err "Please install socat or python first for standalone mode." + return 1 + fi + fi + if [ -f "$_SOCAT_ERR" ]; then if grep "Permission denied" "$_SOCAT_ERR" >/dev/null; then - _err "socat: $(cat $_SOCAT_ERR)" + _err "$_NC: $(cat $_SOCAT_ERR)" _err "Can not listen for user: $(whoami)" _err "Maybe try with root again?" rm -f "$_SOCAT_ERR" @@ -3557,9 +3599,9 @@ _on_before_issue() { fi fi - if _hasfield "$_chk_web_roots" "$NO_VALUE"; then - if ! _exists "socat"; then - _err "Please install socat tools first." + if _hasfield "$_chk_web_roots" "$NO_VALUE" && [ "$_chk_web_roots" = "$NO_VALUE" ]; then + if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then + _err "Please install socat or python tools first." return 1 fi fi @@ -6664,9 +6706,9 @@ _precheck() { return 1 fi - if ! _exists "socat"; then - _err "It is recommended to install socat first." - _err "We use socat for the standalone server, which is used for standalone mode." + if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then + _err "It is recommended to install socat or python first." + _err "We use socat or python for the standalone server, which is used for standalone mode." _err "If you don't want to use standalone mode, you may ignore this warning." fi From 09009794e02b83606b025bbf72c83e1b1851bfe0 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 11 Jan 2026 21:10:37 +0100 Subject: [PATCH 365/689] fix --- .github/workflows/Haiku.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 3ae50051..3cf8d5db 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -66,8 +66,8 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" - prepare: pkg install socat curl - sync: nfs + sync: rsync + copyback: false run: | cd ../acmetest \ && ./letest.sh From 3e36b618237c19c7347bba3fcb718a36fea0eeab Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 11 Jan 2026 21:14:29 +0100 Subject: [PATCH 366/689] fix haiku --- .github/workflows/DNS.yml | 2 ++ .github/workflows/Haiku.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 957d0e97..73e19b89 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -625,6 +625,8 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: rsync + copyback: false + prepare: pkgman install -y cronie run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 3cf8d5db..6034cc4f 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -66,6 +66,7 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" + prepare: pkgman install -y cronie sync: rsync copyback: false run: | From d8c062defb622ff08d3a333dd283cac300c385f6 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 11 Jan 2026 21:21:08 +0100 Subject: [PATCH 367/689] fix haiku --- .github/workflows/Haiku.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 6034cc4f..4b78f930 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -23,6 +23,7 @@ concurrency: jobs: Haiku: strategy: + fail-fast: false matrix: include: - TEST_ACME_Server: "LetsEncrypt.org_test" From d57ab0ab7dd29e844360c52baeefb0f8f51e85b1 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 11 Jan 2026 21:28:45 +0100 Subject: [PATCH 368/689] fix haiku --- .github/workflows/DNS.yml | 5 ++++- .github/workflows/Haiku.yml | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 73e19b89..1a37b8a9 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -626,7 +626,10 @@ jobs: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: rsync copyback: false - prepare: pkgman install -y cronie + prepare: | + mkdir -p /boot/home/.cache + pkgman install -y cronie + run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 4b78f930..1dbfc2c4 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -67,7 +67,9 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" - prepare: pkgman install -y cronie + prepare: | + mkdir -p /boot/home/.cache + pkgman install -y cronie sync: rsync copyback: false run: | From 40b29c1879193e7a8da824f1ffa8f195e573b276 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 11 Jan 2026 21:37:02 +0100 Subject: [PATCH 369/689] support Haiku OS --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 41b6b272..740e5ef0 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ DragonFlyBSD Omnios OpenIndiana + Haiku

@@ -108,6 +109,7 @@ |21|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 |22|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) |23|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) +|24|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From 892b3ca219e3c8c9594d7cfd697e4fde1d455638 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 13 Jan 2026 21:11:00 +0100 Subject: [PATCH 370/689] fix account.conf permission https://github.com/acmesh-official/acme.sh/issues/6708#issuecomment-3745737079 --- acme.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acme.sh b/acme.sh index 1a3763c3..7d9ee2d2 100755 --- a/acme.sh +++ b/acme.sh @@ -2358,6 +2358,7 @@ _setopt() { fi if [ ! -f "$__conf" ]; then touch "$__conf" + chmod 600 "$__conf" fi if [ -n "$(_tail_c 1 <"$__conf")" ]; then echo >>"$__conf" @@ -6671,6 +6672,7 @@ _initconf() { #NO_TIMESTAMP=1 " >"$ACCOUNT_CONF_PATH" + chmod 600 "$ACCOUNT_CONF_PATH" fi } From e2882c536b892d6c2c9dc368d723d161b610caea Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 13 Jan 2026 21:43:46 +0100 Subject: [PATCH 371/689] disable notifications for myself disable notifications for myself --- .github/workflows/pr_dns.yml | 1 + .github/workflows/pr_notify.yml | 1 + .github/workflows/wiki-monitor.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml index 50eb2adb..558ebf48 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -11,6 +11,7 @@ on: jobs: welcome: runs-on: ubuntu-latest + if: github.actor != 'neilpang' steps: - uses: actions/github-script@v6 with: diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml index b6b03c67..416ed721 100644 --- a/.github/workflows/pr_notify.yml +++ b/.github/workflows/pr_notify.yml @@ -13,6 +13,7 @@ on: jobs: welcome: runs-on: ubuntu-latest + if: github.actor != 'neilpang' steps: - uses: actions/github-script@v6 with: diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index a79d70a4..59cd0e5b 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -6,6 +6,7 @@ on: jobs: notify: runs-on: ubuntu-latest + if: github.actor != 'neilpang' steps: - name: Checkout wiki repository uses: actions/checkout@v4 From 282b048557f8f734792454238677bfb9bdb2c74a Mon Sep 17 00:00:00 2001 From: Patrick Zbinden Date: Wed, 14 Jan 2026 21:05:32 +0100 Subject: [PATCH 372/689] Fix dns_cyon to use correct regex --- dnsapi/dns_cyon.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index 0c74be2a..d4b6b6e8 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -332,11 +332,11 @@ _cyon_get_response_message() { } _cyon_get_response_status() { - _egrep_o '"status":[a-zA-z0-9]*' | 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 + _egrep_o '"valid":[a-zA-Z0-9]*' | cut -d : -f 2 } _cyon_get_response_success() { @@ -344,7 +344,7 @@ _cyon_get_response_success() { } _cyon_get_environment_change_status() { - _egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2 + _egrep_o '"authenticated":[a-zA-Z0-9]*' | cut -d : -f 2 } _cyon_check_if_2fa_missed() { From 0cef5edac285a168fbfff3823a393118ea22258b Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 21 Jan 2026 20:00:16 +0100 Subject: [PATCH 373/689] fix https://github.com/acmesh-official/acme.sh/issues/6196#issuecomment-3777530678 --- .github/workflows/DNS.yml | 22 +++++++++++----------- .github/workflows/DragonFlyBSD.yml | 2 +- .github/workflows/FreeBSD.yml | 2 +- .github/workflows/Haiku.yml | 2 +- .github/workflows/Linux.yml | 2 +- .github/workflows/MacOS.yml | 2 +- .github/workflows/NetBSD.yml | 2 +- .github/workflows/Omnios.yml | 2 +- .github/workflows/OpenBSD.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- .github/workflows/PebbleStrict.yml | 4 ++-- .github/workflows/Solaris.yml | 2 +- .github/workflows/Ubuntu.yml | 2 +- .github/workflows/Windows.yml | 2 +- .github/workflows/dockerhub.yml | 2 +- .github/workflows/shellcheck.yml | 4 ++-- .github/workflows/wiki-monitor.yml | 2 +- acme.sh | 17 ++--------------- 18 files changed, 31 insertions(+), 44 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 1a37b8a9..fbe1e61f 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -66,7 +66,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file @@ -114,7 +114,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: brew install socat - name: Clone acmetest @@ -165,7 +165,7 @@ jobs: - name: Set git to use LF run: | git config --global core.autocrlf false - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install cygwin base packages with chocolatey run: | choco config get cacheLocation @@ -224,7 +224,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/freebsd-vm@v1 @@ -279,7 +279,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 @@ -334,7 +334,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/netbsd-vm@v1 @@ -390,7 +390,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/dragonflybsd-vm@v1 @@ -450,7 +450,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/solaris-vm@v1 @@ -508,7 +508,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/omnios-vm@v1 @@ -563,7 +563,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openindiana-vm@v1 @@ -618,7 +618,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/haiku-vm@v1 diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index dda8c99f..f3a85920 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -45,7 +45,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 21123c4a..e9ccf7ac 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 1dbfc2c4..bfbde398 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -52,7 +52,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml index f3352a41..9f3d3f38 100644 --- a/.github/workflows/Linux.yml +++ b/.github/workflows/Linux.yml @@ -33,7 +33,7 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) TEST_ACME_Server: "LetsEncrypt.org_test" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: | cd .. \ diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml index f5d73ec9..21793c3e 100644 --- a/.github/workflows/MacOS.yml +++ b/.github/workflows/MacOS.yml @@ -44,7 +44,7 @@ jobs: CA_EMAIL: ${{ matrix.CA_EMAIL }} TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: brew install socat - name: Clone acmetest diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 40421552..e8107d91 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -45,7 +45,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index 20eb24d7..a166e26b 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index fab6e4fd..b34c795b 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index abad376c..6447911b 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index 729874ce..946d993a 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -33,7 +33,7 @@ jobs: TEST_CA: "Pebble Intermediate CA" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: sudo apt-get install -y socat - name: Run Pebble @@ -58,7 +58,7 @@ jobs: TEST_IPCERT: 1 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: sudo apt-get install -y socat - name: Run Pebble diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 2388da71..f5ce713b 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml index e580828f..5ebf2d0d 100644 --- a/.github/workflows/Ubuntu.yml +++ b/.github/workflows/Ubuntu.yml @@ -70,7 +70,7 @@ jobs: TestingDomain: ${{ matrix.TestingDomain }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: sudo apt-get install -y socat wget - name: Start StepCA diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index c1fd1085..4c195917 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -49,7 +49,7 @@ jobs: - name: Set git to use LF run: | git config --global core.autocrlf false - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install cygwin base packages with chocolatey run: | choco config get cacheLocation diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 49173b4b..0d9046df 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -43,7 +43,7 @@ jobs: if: "contains(needs.CheckToken.outputs.hasToken, 'true')" steps: - name: checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 746727d4..eb10b2b0 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -22,7 +22,7 @@ jobs: ShellCheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Shellcheck run: sudo apt-get install -y shellcheck - name: DoShellcheck @@ -31,7 +31,7 @@ jobs: shfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install shfmt run: curl -sSL https://github.com/mvdan/sh/releases/download/v3.1.2/shfmt_v3.1.2_linux_amd64 -o ~/shfmt && chmod +x ~/shfmt - name: shfmt diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 59cd0e5b..a706529a 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -9,7 +9,7 @@ jobs: if: github.actor != 'neilpang' steps: - name: Checkout wiki repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }}.wiki path: wiki diff --git a/acme.sh b/acme.sh index c66730cb..720e3c68 100755 --- a/acme.sh +++ b/acme.sh @@ -595,10 +595,6 @@ if [ "$(printf '\x41')" != 'A' ]; then _URGLY_PRINTF=1 fi -_ESCAPE_XARGS="" -if _exists xargs && [ "$(printf %s '\\x41' | xargs printf)" = 'A' ]; then - _ESCAPE_XARGS=1 -fi _h2b() { if _exists xxd; then @@ -618,17 +614,8 @@ _h2b() { jc="" _debug2 _URGLY_PRINTF "$_URGLY_PRINTF" if [ -z "$_URGLY_PRINTF" ]; then - if [ "$_ESCAPE_XARGS" ] && _exists xargs; then - _debug2 "xargs" - echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/g' | xargs printf - else - for h in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/ \1/g'); do - if [ -z "$h" ]; then - break - fi - printf "\x$h%s" - done - fi + # shellcheck disable=SC2059 + printf "$(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\x\1/g')" else for c in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\)/ \1/g'); do if [ -z "$ic" ]; then From bfd1f9bf6ce1a6aead0a711b7400e773010bed36 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 16:43:26 +0100 Subject: [PATCH 374/689] [CLOUD-31] add acme.sh opusdns provider --- dnsapi/dns_opusdns.sh | 379 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100755 dnsapi/dns_opusdns.sh diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh new file mode 100755 index 00000000..cf088a07 --- /dev/null +++ b/dnsapi/dns_opusdns.sh @@ -0,0 +1,379 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 +dns_opusdns_info='OpusDNS.com +Site: OpusDNS.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_opusdns +Options: + OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys + OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional. + OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional. + OPUSDNS_Polling_Interval DNS propagation check interval in seconds. Default "6". Optional. + OPUSDNS_Propagation_Timeout Maximum time to wait for DNS propagation in seconds. Default "120". Optional. +Issues: github.com/acmesh-official/acme.sh/issues/XXXX +Author: OpusDNS Team +' + +OPUSDNS_API_Endpoint_Default="https://api.opusdns.com" +OPUSDNS_TTL_Default=60 +OPUSDNS_Polling_Interval_Default=6 +OPUSDNS_Propagation_Timeout_Default=120 + +######## Public functions ########### + +# Add DNS TXT record +# Usage: dns_opusdns_add _acme-challenge.example.com "token_value" +dns_opusdns_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Using OpusDNS DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + # Load and validate credentials + OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" + if [ -z "$OPUSDNS_API_Key" ]; then + _err "OPUSDNS_API_Key not set. Please set it and try again." + _err "You can create an API key at your OpusDNS dashboard." + return 1 + fi + + # Save credentials for future use + _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" + + # Load optional configuration + OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" + if [ -z "$OPUSDNS_API_Endpoint" ]; then + OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" + fi + _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" + + OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" + if [ -z "$OPUSDNS_TTL" ]; then + OPUSDNS_TTL="$OPUSDNS_TTL_Default" + fi + _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" + + OPUSDNS_Polling_Interval="${OPUSDNS_Polling_Interval:-$OPUSDNS_Polling_Interval_Default}" + OPUSDNS_Propagation_Timeout="${OPUSDNS_Propagation_Timeout:-$OPUSDNS_Propagation_Timeout_Default}" + + _debug "API Endpoint: $OPUSDNS_API_Endpoint" + _debug "TTL: $OPUSDNS_TTL" + + # Detect zone from FQDN + if ! _get_zone "$fulldomain"; then + _err "Failed to detect zone for domain: $fulldomain" + return 1 + fi + + _info "Detected zone: $_zone" + _debug "Record name: $_record_name" + + # Add the TXT record + if ! _opusdns_add_record "$_zone" "$_record_name" "$txtvalue"; then + _err "Failed to add TXT record" + return 1 + fi + + _info "TXT record added successfully" + + # Wait for DNS propagation + if ! _opusdns_wait_for_propagation "$fulldomain" "$txtvalue"; then + _err "Warning: DNS record may not have propagated yet" + _err "Certificate issuance may fail. Please check your DNS configuration." + # Don't fail here - let ACME client decide + fi + + return 0 +} + +# Remove DNS TXT record +# Usage: dns_opusdns_rm _acme-challenge.example.com "token_value" +dns_opusdns_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Removing OpusDNS DNS record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + # Load credentials + OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" + OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" + OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" + + if [ -z "$OPUSDNS_API_Endpoint" ]; then + OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" + fi + + if [ -z "$OPUSDNS_TTL" ]; then + OPUSDNS_TTL="$OPUSDNS_TTL_Default" + fi + + if [ -z "$OPUSDNS_API_Key" ]; then + _err "OPUSDNS_API_Key not found" + return 1 + fi + + # Detect zone from FQDN + if ! _get_zone "$fulldomain"; then + _err "Failed to detect zone for domain: $fulldomain" + # Don't fail cleanup - best effort + return 0 + fi + + _info "Detected zone: $_zone" + _debug "Record name: $_record_name" + + # Remove the TXT record (need to pass txtvalue) + if ! _opusdns_remove_record "$_zone" "$_record_name" "$txtvalue"; then + _err "Warning: Failed to remove TXT record (this is usually not critical)" + # Don't fail cleanup - best effort + return 0 + fi + + _info "TXT record removed successfully" + return 0 +} + +######## Private functions ########### + +# Detect zone from FQDN by querying OpusDNS API +# Sets global variables: _zone, _record_name +_get_zone() { + domain=$1 + _debug "Detecting zone for: $domain" + + # Remove trailing dot if present + domain=$(echo "$domain" | sed 's/\.$//') + + # Get all zones from OpusDNS with pagination support + export _H1="X-Api-Key: $OPUSDNS_API_Key" + + zones="" + page=1 + has_more=1 + + while [ $has_more -eq 1 ]; do + _debug2 "Fetching zones page $page" + response=$(_get "$OPUSDNS_API_Endpoint/v1/dns?page=$page&page_size=100") + if [ $? -ne 0 ]; then + _err "Failed to query zones from OpusDNS API (page $page)" + _debug "Response: $response" + return 1 + fi + + _debug2 "Zones response (page $page): $response" + + # Extract zone names from this page (try jq first, fallback to grep/sed) + if _exists jq; then + page_zones=$(echo "$response" | jq -r '.results[].name' 2>/dev/null | sed 's/\.$//') + has_next=$(echo "$response" | jq -r '.has_next_page // false' 2>/dev/null) + else + # Fallback: extract zone names using grep/sed + # Note: This simple parser does not handle escaped quotes in zone names. + # Zone names with escaped quotes are extremely rare and would require jq. + page_zones=$(echo "$response" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"\\]*"' | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"/\1/' | sed 's/\.$//') + has_next=$(echo "$response" | grep -oE '"has_next_page"[[:space:]]*:[[:space:]]*(true|false)' | grep -o 'true\|false') + fi + + # Append zones from this page + if [ -n "$page_zones" ]; then + if [ -z "$zones" ]; then + zones="$page_zones" + else + zones="$zones +$page_zones" + fi + fi + + # Check if there are more pages + if [ "$has_next" = "true" ]; then + page=$((page + 1)) + else + has_more=0 + fi + done + + if [ -z "$zones" ]; then + _err "No zones found in OpusDNS account" + _debug "API Response: $response" + return 1 + fi + + _debug2 "Available zones (all pages): $zones" + + # Find longest matching zone + _zone="" + _zone_length=0 + + for zone in $zones; do + zone_with_dot="${zone}." + if _endswith "$domain." "$zone_with_dot"; then + zone_length=${#zone} + if [ $zone_length -gt $_zone_length ]; then + _zone="$zone" + _zone_length=$zone_length + fi + fi + done + + if [ -z "$_zone" ]; then + _err "No matching zone found for domain: $domain" + _err "Available zones: $zones" + return 1 + fi + + # Calculate record name (subdomain part) + # Use parameter expansion instead of sed to avoid regex metacharacter issues + _record_name="${domain%.${_zone}}" + # Handle case where domain equals zone (remove trailing dot if present) + if [ "$_record_name" = "$domain" ]; then + _record_name="${domain%${_zone}}" + _record_name="${_record_name%.}" + fi + + if [ -z "$_record_name" ]; then + _record_name="@" + fi + + return 0 +} + +# Add TXT record using OpusDNS API +_opusdns_add_record() { + zone=$1 + record_name=$2 + txtvalue=$3 + + _debug "Adding TXT record: $record_name.$zone = $txtvalue" + + # Escape all JSON special characters in txtvalue + # Order matters: escape backslashes first, then other characters + escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') + + # Build JSON payload + # Note: TXT records need quotes around the value in rdata + json_payload="{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" + + _debug2 "JSON payload: $json_payload" + + # Send PATCH request + export _H1="X-Api-Key: $OPUSDNS_API_Key" + export _H2="Content-Type: application/json" + + response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") + status=$? + + _debug2 "API Response: $response" + + if [ $status -ne 0 ]; then + _err "Failed to add TXT record" + _err "API Response: $response" + return 1 + fi + + # Check for error in response (OpusDNS returns JSON error even on failure) + # Use anchored pattern to avoid matching field names like "error_count" + if echo "$response" | grep -q '"error":'; then + _err "API returned error: $response" + return 1 + fi + + return 0 +} + +# Remove TXT record using OpusDNS API +_opusdns_remove_record() { + zone=$1 + record_name=$2 + txtvalue=$3 + + _debug "Removing TXT record: $record_name.$zone = $txtvalue" + + # Escape all JSON special characters in txtvalue (same as add) + escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') + + # Build JSON payload for removal - needs complete record specification + json_payload="{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" + + _debug2 "JSON payload: $json_payload" + + # Send PATCH request + export _H1="X-Api-Key: $OPUSDNS_API_Key" + export _H2="Content-Type: application/json" + + response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") + status=$? + + _debug2 "API Response: $response" + + if [ $status -ne 0 ]; then + _err "Failed to remove TXT record" + _err "API Response: $response" + return 1 + fi + + return 0 +} + +# Wait for DNS propagation by checking OpusDNS authoritative nameservers +_opusdns_wait_for_propagation() { + fulldomain=$1 + txtvalue=$2 + + _info "Waiting for DNS propagation to authoritative nameservers (max ${OPUSDNS_Propagation_Timeout}s)..." + + max_attempts=$((OPUSDNS_Propagation_Timeout / OPUSDNS_Polling_Interval)) + # Ensure at least one attempt even if interval > timeout + if [ "$max_attempts" -lt 1 ]; then + max_attempts=1 + fi + attempt=1 + + # OpusDNS authoritative nameservers + nameservers="ns1.opusdns.com ns2.opusdns.net" + + while [ $attempt -le $max_attempts ]; do + _debug "Propagation check attempt $attempt/$max_attempts" + + all_propagated=1 + + # Check all OpusDNS authoritative nameservers + for ns in $nameservers; do + if _exists dig; then + result=$(dig @$ns +short "$fulldomain" TXT 2>/dev/null | tr -d '"') + elif _exists nslookup; then + result=$(nslookup -type=TXT "$fulldomain" $ns 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') + else + _err "Neither dig nor nslookup found. Cannot verify DNS propagation." + return 1 + fi + + _debug2 "DNS query result from $ns: $result" + + if ! echo "$result" | grep -qF "$txtvalue"; then + _debug "Record not yet on $ns" + all_propagated=0 + else + _debug "Record found on $ns ✓" + fi + done + + if [ $all_propagated -eq 1 ]; then + _info "DNS record propagated to all OpusDNS nameservers!" + return 0 + fi + + if [ $attempt -lt $max_attempts ]; then + _debug "Record not propagated to all nameservers yet, waiting ${OPUSDNS_Polling_Interval}s..." + sleep "$OPUSDNS_Polling_Interval" + fi + + attempt=$((attempt + 1)) + done + + _err "DNS record did not propagate to all nameservers within ${OPUSDNS_Propagation_Timeout} seconds" + return 1 +} From 01c93b9bbd0b76488c8201876bb20ccbbfbb7cc3 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:04:43 +0100 Subject: [PATCH 376/689] Fix shellcheck and shfmt issues - Add double quotes around variables to prevent globbing - Fix parameter expansion quoting in ${domain%.${_zone}} - Remove trailing whitespace for shfmt compliance --- dnsapi/dns_opusdns.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index cf088a07..feb5507e 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -102,7 +102,7 @@ dns_opusdns_rm() { OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - + if [ -z "$OPUSDNS_API_Endpoint" ]; then OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" fi @@ -150,11 +150,11 @@ _get_zone() { # Get all zones from OpusDNS with pagination support export _H1="X-Api-Key: $OPUSDNS_API_Key" - + zones="" page=1 has_more=1 - + while [ $has_more -eq 1 ]; do _debug2 "Fetching zones page $page" response=$(_get "$OPUSDNS_API_Endpoint/v1/dns?page=$page&page_size=100") @@ -212,7 +212,7 @@ $page_zones" zone_with_dot="${zone}." if _endswith "$domain." "$zone_with_dot"; then zone_length=${#zone} - if [ $zone_length -gt $_zone_length ]; then + if [ "$zone_length" -gt "$_zone_length" ]; then _zone="$zone" _zone_length=$zone_length fi @@ -227,10 +227,10 @@ $page_zones" # Calculate record name (subdomain part) # Use parameter expansion instead of sed to avoid regex metacharacter issues - _record_name="${domain%.${_zone}}" + _record_name="${domain%."${_zone}"}" # Handle case where domain equals zone (remove trailing dot if present) if [ "$_record_name" = "$domain" ]; then - _record_name="${domain%${_zone}}" + _record_name="${domain%"${_zone}"}" _record_name="${_record_name%.}" fi @@ -343,9 +343,9 @@ _opusdns_wait_for_propagation() { # Check all OpusDNS authoritative nameservers for ns in $nameservers; do if _exists dig; then - result=$(dig @$ns +short "$fulldomain" TXT 2>/dev/null | tr -d '"') + result=$(dig @"$ns" +short "$fulldomain" TXT 2>/dev/null | tr -d '"') elif _exists nslookup; then - result=$(nslookup -type=TXT "$fulldomain" $ns 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') + result=$(nslookup -type=TXT "$fulldomain" "$ns" 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') else _err "Neither dig nor nslookup found. Cannot verify DNS propagation." return 1 From dc65223da1c99dcd7351f7c4d6e9c410f9ff7386 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:10:12 +0100 Subject: [PATCH 377/689] Remove all trailing whitespace for shfmt compliance --- dnsapi/dns_opusdns.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index feb5507e..a57d2188 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -177,7 +177,7 @@ _get_zone() { page_zones=$(echo "$response" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"\\]*"' | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"/\1/' | sed 's/\.$//') has_next=$(echo "$response" | grep -oE '"has_next_page"[[:space:]]*:[[:space:]]*(true|false)' | grep -o 'true\|false') fi - + # Append zones from this page if [ -n "$page_zones" ]; then if [ -z "$zones" ]; then @@ -187,7 +187,7 @@ _get_zone() { $page_zones" fi fi - + # Check if there are more pages if [ "$has_next" = "true" ]; then page=$((page + 1)) @@ -233,7 +233,7 @@ $page_zones" _record_name="${domain%"${_zone}"}" _record_name="${_record_name%.}" fi - + if [ -z "$_record_name" ]; then _record_name="@" fi @@ -339,7 +339,7 @@ _opusdns_wait_for_propagation() { _debug "Propagation check attempt $attempt/$max_attempts" all_propagated=1 - + # Check all OpusDNS authoritative nameservers for ns in $nameservers; do if _exists dig; then From 30c9332327b2c3f5a2a5aacf7bc5389b76cc126d Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:28:41 +0100 Subject: [PATCH 378/689] Remove custom DNS propagation check acme.sh handles DNS propagation checking internally via --dnssleep and _check_dns_record. Custom propagation checks are unnecessary and can conflict with acme.sh's own timing. Removed: - _opusdns_wait_for_propagation() function - OPUSDNS_Polling_Interval option - OPUSDNS_Propagation_Timeout option Script is now consistent with other DNS API implementations (Cloudflare, AWS, etc.) which don't have custom propagation checks. --- dnsapi/dns_opusdns.sh | 75 ------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index a57d2188..6c23a904 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -8,16 +8,12 @@ Options: OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional. OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional. - OPUSDNS_Polling_Interval DNS propagation check interval in seconds. Default "6". Optional. - OPUSDNS_Propagation_Timeout Maximum time to wait for DNS propagation in seconds. Default "120". Optional. Issues: github.com/acmesh-official/acme.sh/issues/XXXX Author: OpusDNS Team ' OPUSDNS_API_Endpoint_Default="https://api.opusdns.com" OPUSDNS_TTL_Default=60 -OPUSDNS_Polling_Interval_Default=6 -OPUSDNS_Propagation_Timeout_Default=120 ######## Public functions ########### @@ -55,9 +51,6 @@ dns_opusdns_add() { fi _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" - OPUSDNS_Polling_Interval="${OPUSDNS_Polling_Interval:-$OPUSDNS_Polling_Interval_Default}" - OPUSDNS_Propagation_Timeout="${OPUSDNS_Propagation_Timeout:-$OPUSDNS_Propagation_Timeout_Default}" - _debug "API Endpoint: $OPUSDNS_API_Endpoint" _debug "TTL: $OPUSDNS_TTL" @@ -77,14 +70,6 @@ dns_opusdns_add() { fi _info "TXT record added successfully" - - # Wait for DNS propagation - if ! _opusdns_wait_for_propagation "$fulldomain" "$txtvalue"; then - _err "Warning: DNS record may not have propagated yet" - _err "Certificate issuance may fail. Please check your DNS configuration." - # Don't fail here - let ACME client decide - fi - return 0 } @@ -317,63 +302,3 @@ _opusdns_remove_record() { return 0 } - -# Wait for DNS propagation by checking OpusDNS authoritative nameservers -_opusdns_wait_for_propagation() { - fulldomain=$1 - txtvalue=$2 - - _info "Waiting for DNS propagation to authoritative nameservers (max ${OPUSDNS_Propagation_Timeout}s)..." - - max_attempts=$((OPUSDNS_Propagation_Timeout / OPUSDNS_Polling_Interval)) - # Ensure at least one attempt even if interval > timeout - if [ "$max_attempts" -lt 1 ]; then - max_attempts=1 - fi - attempt=1 - - # OpusDNS authoritative nameservers - nameservers="ns1.opusdns.com ns2.opusdns.net" - - while [ $attempt -le $max_attempts ]; do - _debug "Propagation check attempt $attempt/$max_attempts" - - all_propagated=1 - - # Check all OpusDNS authoritative nameservers - for ns in $nameservers; do - if _exists dig; then - result=$(dig @"$ns" +short "$fulldomain" TXT 2>/dev/null | tr -d '"') - elif _exists nslookup; then - result=$(nslookup -type=TXT "$fulldomain" "$ns" 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') - else - _err "Neither dig nor nslookup found. Cannot verify DNS propagation." - return 1 - fi - - _debug2 "DNS query result from $ns: $result" - - if ! echo "$result" | grep -qF "$txtvalue"; then - _debug "Record not yet on $ns" - all_propagated=0 - else - _debug "Record found on $ns ✓" - fi - done - - if [ $all_propagated -eq 1 ]; then - _info "DNS record propagated to all OpusDNS nameservers!" - return 0 - fi - - if [ $attempt -lt $max_attempts ]; then - _debug "Record not propagated to all nameservers yet, waiting ${OPUSDNS_Polling_Interval}s..." - sleep "$OPUSDNS_Polling_Interval" - fi - - attempt=$((attempt + 1)) - done - - _err "DNS record did not propagate to all nameservers within ${OPUSDNS_Propagation_Timeout} seconds" - return 1 -} From 25a3ee48df9c6af194bcc2c0dbd65b441481b0cb Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:58:09 +0100 Subject: [PATCH 379/689] Fix zone pagination parsing - Fixed jq path: .pagination.has_next_page instead of .has_next_page - Fixed grep fallback: remove rrsets before extracting zone names to avoid matching nested 'name' fields - Simplified has_next_page detection with simple grep -q - Added debug output for page zones and has_next status --- dnsapi/dns_opusdns.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index 6c23a904..f6bd507e 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -151,18 +151,27 @@ _get_zone() { _debug2 "Zones response (page $page): $response" - # Extract zone names from this page (try jq first, fallback to grep/sed) + # Extract zone names from this page + # The API returns: {"results":[{"name":"zone.com.",...},...],"pagination":{"has_next_page":true,...}} if _exists jq; then page_zones=$(echo "$response" | jq -r '.results[].name' 2>/dev/null | sed 's/\.$//') - has_next=$(echo "$response" | jq -r '.has_next_page // false' 2>/dev/null) + has_next=$(echo "$response" | jq -r '.pagination.has_next_page // false' 2>/dev/null) else # Fallback: extract zone names using grep/sed - # Note: This simple parser does not handle escaped quotes in zone names. - # Zone names with escaped quotes are extremely rare and would require jq. - page_zones=$(echo "$response" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"\\]*"' | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"/\1/' | sed 's/\.$//') - has_next=$(echo "$response" | grep -oE '"has_next_page"[[:space:]]*:[[:space:]]*(true|false)' | grep -o 'true\|false') + # Extract only top-level zone names from results array (before rrsets) + # Pattern: "results":[{"...","name":"zonename.com.","domain_parts": + page_zones=$(echo "$response" | sed 's/,"rrsets":\[[^]]*\]//g' | grep -o '"results":\[.*\]' | grep -o '"name":"[^"]*"' | sed 's/"name":"//g;s/"//g;s/\.$//') + # Extract has_next_page from pagination object + if echo "$response" | grep -q '"has_next_page":true'; then + has_next="true" + else + has_next="false" + fi fi + _debug2 "Page $page zones: $page_zones" + _debug2 "Has next page: $has_next" + # Append zones from this page if [ -n "$page_zones" ]; then if [ -z "$zones" ]; then From 163eb1acb9a7f1170e407d14358599a563dbd68f Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 18:11:01 +0100 Subject: [PATCH 380/689] Simplify zone detection with API check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of fetching all zones and matching, iterate through domain parts and check each against the API until a valid zone is found. Same approach as GoDaddy DNS plugin. Example: _acme-challenge.test.example.com - Try: test.example.com → 404 - Try: example.com → 200 ✓ → zone found! Script reduced from 304 to 255 lines. --- dnsapi/dns_opusdns.sh | 110 ++++++++++-------------------------------- 1 file changed, 26 insertions(+), 84 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index f6bd507e..b9337b89 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -124,7 +124,8 @@ dns_opusdns_rm() { ######## Private functions ########### -# Detect zone from FQDN by querying OpusDNS API +# Detect zone from FQDN by checking against OpusDNS API +# Iterates through domain parts until a valid zone is found # Sets global variables: _zone, _record_name _get_zone() { domain=$1 @@ -133,100 +134,41 @@ _get_zone() { # Remove trailing dot if present domain=$(echo "$domain" | sed 's/\.$//') - # Get all zones from OpusDNS with pagination support export _H1="X-Api-Key: $OPUSDNS_API_Key" - zones="" - page=1 - has_more=1 + # Start from position 2 (skip first part like _acme-challenge) + i=2 + p=1 + while true; do + # Extract potential zone (domain parts from position i onwards) + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug "Trying zone: $h" - while [ $has_more -eq 1 ]; do - _debug2 "Fetching zones page $page" - response=$(_get "$OPUSDNS_API_Endpoint/v1/dns?page=$page&page_size=100") - if [ $? -ne 0 ]; then - _err "Failed to query zones from OpusDNS API (page $page)" - _debug "Response: $response" + if [ -z "$h" ]; then + # No more parts to try + _err "Could not find a valid zone for: $domain" return 1 fi - _debug2 "Zones response (page $page): $response" + # Check if this zone exists in OpusDNS + response=$(_get "$OPUSDNS_API_Endpoint/v1/dns/$h") - # Extract zone names from this page - # The API returns: {"results":[{"name":"zone.com.",...},...],"pagination":{"has_next_page":true,...}} - if _exists jq; then - page_zones=$(echo "$response" | jq -r '.results[].name' 2>/dev/null | sed 's/\.$//') - has_next=$(echo "$response" | jq -r '.pagination.has_next_page // false' 2>/dev/null) - else - # Fallback: extract zone names using grep/sed - # Extract only top-level zone names from results array (before rrsets) - # Pattern: "results":[{"...","name":"zonename.com.","domain_parts": - page_zones=$(echo "$response" | sed 's/,"rrsets":\[[^]]*\]//g' | grep -o '"results":\[.*\]' | grep -o '"name":"[^"]*"' | sed 's/"name":"//g;s/"//g;s/\.$//') - # Extract has_next_page from pagination object - if echo "$response" | grep -q '"has_next_page":true'; then - has_next="true" - else - has_next="false" - fi + if _contains "$response" '"name"'; then + # Zone found + _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _zone="$h" + _debug "Found zone: $_zone" + _debug "Record name: $_record_name" + return 0 fi - _debug2 "Page $page zones: $page_zones" - _debug2 "Has next page: $has_next" - - # Append zones from this page - if [ -n "$page_zones" ]; then - if [ -z "$zones" ]; then - zones="$page_zones" - else - zones="$zones -$page_zones" - fi - fi - - # Check if there are more pages - if [ "$has_next" = "true" ]; then - page=$((page + 1)) - else - has_more=0 - fi + _debug "$h not found, trying next" + p="$i" + i=$(_math "$i" + 1) done - if [ -z "$zones" ]; then - _err "No zones found in OpusDNS account" - _debug "API Response: $response" - return 1 - fi - - _debug2 "Available zones (all pages): $zones" - - # Find longest matching zone - _zone="" - _zone_length=0 - - for zone in $zones; do - zone_with_dot="${zone}." - if _endswith "$domain." "$zone_with_dot"; then - zone_length=${#zone} - if [ "$zone_length" -gt "$_zone_length" ]; then - _zone="$zone" - _zone_length=$zone_length - fi - fi - done - - if [ -z "$_zone" ]; then - _err "No matching zone found for domain: $domain" - _err "Available zones: $zones" - return 1 - fi - - # Calculate record name (subdomain part) - # Use parameter expansion instead of sed to avoid regex metacharacter issues - _record_name="${domain%."${_zone}"}" - # Handle case where domain equals zone (remove trailing dot if present) - if [ "$_record_name" = "$domain" ]; then - _record_name="${domain%"${_zone}"}" - _record_name="${_record_name%.}" - fi + return 1 +} if [ -z "$_record_name" ]; then _record_name="@" From 9e584e346debc9f8d008ab5bcf11489b8926c22c Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 18:13:09 +0100 Subject: [PATCH 381/689] Refactor: Add _opusdns_api helper, simplify code - Added _opusdns_api() for all API requests - Added _opusdns_init() for config initialization - Removed duplicate code in add/rm functions - Removed dead code (orphaned lines 173-178) - Script now 158 lines (was 255, originally 379) --- dnsapi/dns_opusdns.sh | 231 ++++++++++++------------------------------ 1 file changed, 67 insertions(+), 164 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index b9337b89..19205256 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -18,7 +18,6 @@ OPUSDNS_TTL_Default=60 ######## Public functions ########### # Add DNS TXT record -# Usage: dns_opusdns_add _acme-challenge.example.com "token_value" dns_opusdns_add() { fulldomain=$1 txtvalue=$2 @@ -27,44 +26,17 @@ dns_opusdns_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - # Load and validate credentials - OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" - if [ -z "$OPUSDNS_API_Key" ]; then - _err "OPUSDNS_API_Key not set. Please set it and try again." - _err "You can create an API key at your OpusDNS dashboard." + if ! _opusdns_init; then return 1 fi - # Save credentials for future use - _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" - - # Load optional configuration - OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" - if [ -z "$OPUSDNS_API_Endpoint" ]; then - OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" - fi - _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" - - OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - if [ -z "$OPUSDNS_TTL" ]; then - OPUSDNS_TTL="$OPUSDNS_TTL_Default" - fi - _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" - - _debug "API Endpoint: $OPUSDNS_API_Endpoint" - _debug "TTL: $OPUSDNS_TTL" - - # Detect zone from FQDN if ! _get_zone "$fulldomain"; then - _err "Failed to detect zone for domain: $fulldomain" return 1 fi - _info "Detected zone: $_zone" - _debug "Record name: $_record_name" + _info "Zone: $_zone, Record: $_record_name" - # Add the TXT record - if ! _opusdns_add_record "$_zone" "$_record_name" "$txtvalue"; then + if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then _err "Failed to add TXT record" return 1 fi @@ -74,7 +46,6 @@ dns_opusdns_add() { } # Remove DNS TXT record -# Usage: dns_opusdns_rm _acme-challenge.example.com "token_value" dns_opusdns_rm() { fulldomain=$1 txtvalue=$2 @@ -83,38 +54,19 @@ dns_opusdns_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - # Load credentials - OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" - OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" - OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - - if [ -z "$OPUSDNS_API_Endpoint" ]; then - OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" - fi - - if [ -z "$OPUSDNS_TTL" ]; then - OPUSDNS_TTL="$OPUSDNS_TTL_Default" - fi - - if [ -z "$OPUSDNS_API_Key" ]; then - _err "OPUSDNS_API_Key not found" + if ! _opusdns_init; then return 1 fi - # Detect zone from FQDN if ! _get_zone "$fulldomain"; then - _err "Failed to detect zone for domain: $fulldomain" - # Don't fail cleanup - best effort + _err "Zone not found, cleanup skipped" return 0 fi - _info "Detected zone: $_zone" - _debug "Record name: $_record_name" + _info "Zone: $_zone, Record: $_record_name" - # Remove the TXT record (need to pass txtvalue) - if ! _opusdns_remove_record "$_zone" "$_record_name" "$txtvalue"; then - _err "Warning: Failed to remove TXT record (this is usually not critical)" - # Don't fail cleanup - best effort + if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then + _err "Warning: Failed to remove TXT record" return 0 fi @@ -124,132 +76,83 @@ dns_opusdns_rm() { ######## Private functions ########### -# Detect zone from FQDN by checking against OpusDNS API -# Iterates through domain parts until a valid zone is found -# Sets global variables: _zone, _record_name -_get_zone() { - domain=$1 - _debug "Detecting zone for: $domain" +# Initialize and validate configuration +_opusdns_init() { + OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" + OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" + OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - # Remove trailing dot if present - domain=$(echo "$domain" | sed 's/\.$//') + if [ -z "$OPUSDNS_API_Key" ]; then + _err "OPUSDNS_API_Key not set" + return 1 + fi + + [ -z "$OPUSDNS_API_Endpoint" ] && OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" + [ -z "$OPUSDNS_TTL" ] && OPUSDNS_TTL="$OPUSDNS_TTL_Default" + + _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" + _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" + _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" + + _debug "Endpoint: $OPUSDNS_API_Endpoint" + return 0 +} + +# Make API request +# Usage: _opusdns_api METHOD PATH [DATA] +_opusdns_api() { + method=$1 + path=$2 + data=$3 export _H1="X-Api-Key: $OPUSDNS_API_Key" + export _H2="Content-Type: application/json" + + url="$OPUSDNS_API_Endpoint$path" + _debug2 "API: $method $url" + [ -n "$data" ] && _debug2 "Data: $data" + + if [ -n "$data" ]; then + response=$(_post "$data" "$url" "" "$method") + else + response=$(_get "$url") + fi + + if [ $? -ne 0 ]; then + _err "API request failed" + _debug "Response: $response" + return 1 + fi + + _debug2 "Response: $response" + return 0 +} + +# Detect zone from FQDN +# Sets: _zone, _record_name +_get_zone() { + domain=$(echo "$1" | sed 's/\.$//') + _debug "Finding zone for: $domain" - # Start from position 2 (skip first part like _acme-challenge) i=2 p=1 while true; do - # Extract potential zone (domain parts from position i onwards) h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug "Trying zone: $h" if [ -z "$h" ]; then - # No more parts to try - _err "Could not find a valid zone for: $domain" + _err "No valid zone found for: $domain" return 1 fi - # Check if this zone exists in OpusDNS - response=$(_get "$OPUSDNS_API_Endpoint/v1/dns/$h") - - if _contains "$response" '"name"'; then - # Zone found - _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _debug "Trying: $h" + if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"name"'; then _zone="$h" - _debug "Found zone: $_zone" - _debug "Record name: $_record_name" + _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + [ -z "$_record_name" ] && _record_name="@" return 0 fi - _debug "$h not found, trying next" p="$i" i=$(_math "$i" + 1) done - - return 1 -} - - if [ -z "$_record_name" ]; then - _record_name="@" - fi - - return 0 -} - -# Add TXT record using OpusDNS API -_opusdns_add_record() { - zone=$1 - record_name=$2 - txtvalue=$3 - - _debug "Adding TXT record: $record_name.$zone = $txtvalue" - - # Escape all JSON special characters in txtvalue - # Order matters: escape backslashes first, then other characters - escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') - - # Build JSON payload - # Note: TXT records need quotes around the value in rdata - json_payload="{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" - - _debug2 "JSON payload: $json_payload" - - # Send PATCH request - export _H1="X-Api-Key: $OPUSDNS_API_Key" - export _H2="Content-Type: application/json" - - response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") - status=$? - - _debug2 "API Response: $response" - - if [ $status -ne 0 ]; then - _err "Failed to add TXT record" - _err "API Response: $response" - return 1 - fi - - # Check for error in response (OpusDNS returns JSON error even on failure) - # Use anchored pattern to avoid matching field names like "error_count" - if echo "$response" | grep -q '"error":'; then - _err "API returned error: $response" - return 1 - fi - - return 0 -} - -# Remove TXT record using OpusDNS API -_opusdns_remove_record() { - zone=$1 - record_name=$2 - txtvalue=$3 - - _debug "Removing TXT record: $record_name.$zone = $txtvalue" - - # Escape all JSON special characters in txtvalue (same as add) - escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') - - # Build JSON payload for removal - needs complete record specification - json_payload="{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" - - _debug2 "JSON payload: $json_payload" - - # Send PATCH request - export _H1="X-Api-Key: $OPUSDNS_API_Key" - export _H2="Content-Type: application/json" - - response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") - status=$? - - _debug2 "API Response: $response" - - if [ $status -ne 0 ]; then - _err "Failed to remove TXT record" - _err "API Response: $response" - return 1 - fi - - return 0 } From 2e85e6f9bb0587450d9fea755203538c6dedab13 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 18:13:51 +0100 Subject: [PATCH 382/689] Fix zone detection: check for dnssec_status instead of name The error response also contains 'name' in 'zone_name' field, causing false positives. Check for 'dnssec_status' which only exists in valid zone responses. --- dnsapi/dns_opusdns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index 19205256..2ef401eb 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -145,7 +145,7 @@ _get_zone() { fi _debug "Trying: $h" - if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"name"'; then + if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"dnssec_status"'; then _zone="$h" _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") [ -z "$_record_name" ] && _record_name="@" From 9c245eb37a2f6568a5f316ca7ba23a230acf4452 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Fri, 23 Jan 2026 09:20:00 +0100 Subject: [PATCH 383/689] fix: start zone detection from i=1 per acme.sh convention --- dnsapi/dns_opusdns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index 2ef401eb..37177696 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -134,7 +134,7 @@ _get_zone() { domain=$(echo "$1" | sed 's/\.$//') _debug "Finding zone for: $domain" - i=2 + i=1 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) From e44809c18074aeb886e25aea9adb29aa194c4027 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 24 Jan 2026 07:49:18 +0100 Subject: [PATCH 384/689] Update acme.sh --- acme.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/acme.sh b/acme.sh index 720e3c68..7a3c35a8 100755 --- a/acme.sh +++ b/acme.sh @@ -595,7 +595,6 @@ if [ "$(printf '\x41')" != 'A' ]; then _URGLY_PRINTF=1 fi - _h2b() { if _exists xxd; then if _contains "$(xxd --help 2>&1)" "assumes -c30"; then From 477277bd2dbab0d4f4d3db9592e979fc553679b3 Mon Sep 17 00:00:00 2001 From: JF DAGUIN <74184010+jf-lines@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:00:25 +0100 Subject: [PATCH 385/689] Rewrite token scope and URL to add one Updated comments for clarity and formatting. --- dnsapi/dns_infomaniak.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index 34795888..0ae32b47 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -9,12 +9,13 @@ Issues: github.com/acmesh-official/acme.sh/issues/3188 ' -# To use this API you need visit the API dashboard of your account -# once logged into https://manager.infomaniak.com add /api/dashboard to the URL -# +# To use this API you need visit the API dashboard of your account. # Note: the URL looks like this: -# https://manager.infomaniak.com/v3//api/dashboard -# Then generate a token with the scope Domain +# https://manager.infomaniak.com/v3//ng/profile/user/token/list +# Then generate a token with following scopes : +# - domain:read +# - dns:read +# - dns:write # this is given as an environment variable INFOMANIAK_API_TOKEN # base variables From 6a60695549c6417afb7ef73e0b5b00879376b4e0 Mon Sep 17 00:00:00 2001 From: David Gallay Date: Tue, 3 Feb 2026 10:51:42 +0100 Subject: [PATCH 386/689] Allowing panos deploy-hook to only depend on PANOS_KEY. Previous version add bugs that were not properly using the _api_key. It also enforced to provide PANOS_USER and PANOS_PASSWORD which can be very constraining. PANOS_KEY now has precedence. If not provided, the script falls back to PANOS_USER and PANOS_PASSWORD. --- deploy/panos.sh | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index c54d21fe..019d8c62 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -207,13 +207,12 @@ panos_deploy() { fi # PANOS_KEY - _getdeployconf PANOS_KEY if [ "$PANOS_KEY" ]; then - _debug "Detected saved key." - _panos_key=$PANOS_KEY + _debug "Detected ENV variable PANOS_KEY. Saving to file." + _savedeployconf PANOS_KEY "$PANOS_KEY" 1 else - _debug "No key detected" - unset _panos_key + _debug "Attempting to load variable PANOS_KEY from file." + _getdeployconf PANOS_KEY fi # PANOS_TEMPLATE @@ -256,6 +255,7 @@ panos_deploy() { _panos_host=$PANOS_HOST _panos_user=$PANOS_USER _panos_pass=$PANOS_PASS + _panos_key=$PANOS_KEY _panos_template=$PANOS_TEMPLATE _panos_template_stack=$PANOS_TEMPLATE_STACK _panos_vsys=$PANOS_VSYS @@ -271,12 +271,6 @@ panos_deploy() { if [ -z "$_panos_host" ]; then _err "No host found. If this is your first time deploying, please set PANOS_HOST in ENV variables. You can delete it after you have successfully deployed the certs." return 1 - elif [ -z "$_panos_user" ]; then - _err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs." - return 1 - elif [ -z "$_panos_pass" ]; then - _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 @@ -286,6 +280,13 @@ panos_deploy() { # Generate a new API key if no valid API key is found if [ -z "$_panos_key" ]; then + if [ -z "$_panos_user" ]; then + _err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs." + return 1 + elif [ -z "$_panos_pass" ]; then + _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 + fi _debug "**** Generating new PANOS API KEY ****" deployer keygen _savedeployconf PANOS_KEY "$_panos_key" 1 From 61e986f23c212211010488365d10aa775a3feb9d Mon Sep 17 00:00:00 2001 From: dga-nagra <147379886+dga-nagra@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:01:44 +0100 Subject: [PATCH 387/689] Conditionnaly change permissions (#1) --- deploy/ssh.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 3039c4ea..848380a5 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -238,8 +238,10 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d return $_err_code fi else + # If file doesn't exist, create it and change its permissions. + _cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;" # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE; chmod 600 $DEPLOY_SSH_KEYFILE;" + _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then From 7236ba2d7c594a79111d1f62b42ef5fcedb9f4d6 Mon Sep 17 00:00:00 2001 From: alexandergott-afk Date: Thu, 5 Feb 2026 09:20:51 +0100 Subject: [PATCH 389/689] Update dns_nsupdate.sh --- dnsapi/dns_nsupdate.sh | 62 ++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index d5dbbcbc..e2df39a3 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_nsupdate Options: NSUPDATE_SERVER Server hostname. Default: "localhost". NSUPDATE_SERVER_PORT Server port. Default: "53". - NSUPDATE_KEY File path to TSIG key. + NSUPDATE_KEY File path to TSIG key. Default: "" NSUPDATE_ZONE Domain zone to update. Optional. ' @@ -22,8 +22,6 @@ dns_nsupdate_add() { NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}" NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}" - _checkKeyFile || return 1 - # save the dns server and key to the account conf file. _saveaccountconf_mutable NSUPDATE_SERVER "${NSUPDATE_SERVER}" _saveaccountconf_mutable NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}" @@ -33,6 +31,7 @@ dns_nsupdate_add() { [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53 + [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" [ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT="" _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" @@ -40,19 +39,36 @@ dns_nsupdate_add() { [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" if [ -z "${NSUPDATE_ZONE}" ]; then #shellcheck disable=SC2086 - nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT < Date: Sat, 7 Feb 2026 22:10:48 +0800 Subject: [PATCH 390/689] Change shebang to use env for portability --- notify/telegram.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify/telegram.sh b/notify/telegram.sh index 97dd2861..c6532dc6 100755 --- a/notify/telegram.sh +++ b/notify/telegram.sh @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/usr/bin/env sh #Support Telegram Bots From 4807df0c3e24786d42d1f07579313e2c557414a4 Mon Sep 17 00:00:00 2001 From: alexandergott-afk Date: Mon, 9 Feb 2026 10:03:26 +0100 Subject: [PATCH 391/689] Fix Tab --- dnsapi/dns_nsupdate.sh | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index e2df39a3..c2fa8fc1 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -39,36 +39,36 @@ dns_nsupdate_add() { [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" if [ -z "${NSUPDATE_ZONE}" ]; then #shellcheck disable=SC2086 - if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT < Date: Mon, 9 Feb 2026 10:39:27 +0100 Subject: [PATCH 392/689] too many spaces removed --- dnsapi/dns_nsupdate.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index c2fa8fc1..9b14553b 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -40,13 +40,13 @@ dns_nsupdate_add() { if [ -z "${NSUPDATE_ZONE}" ]; then #shellcheck disable=SC2086 if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT < Date: Thu, 12 Feb 2026 15:24:45 +0100 Subject: [PATCH 393/689] Update dns_nsupdate.sh --- dnsapi/dns_nsupdate.sh | 95 ++++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 9b14553b..0b54b358 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -34,42 +34,49 @@ dns_nsupdate_add() { [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" [ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT="" + NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ') + _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" - if [ -z "${NSUPDATE_ZONE}" ]; then - #shellcheck disable=SC2086 - if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT < Date: Thu, 12 Feb 2026 15:27:23 +0100 Subject: [PATCH 394/689] Allow more than one DNS server for HA environments --- dnsapi/dns_nsupdate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 0b54b358..cc57cc38 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -98,9 +98,9 @@ dns_nsupdate_rm() { [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53 [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" - + NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ') - + _info "removing ${fulldomain}. txt" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" From 83424e7ba4c8e116ca5d39bf352c84258482d8c0 Mon Sep 17 00:00:00 2001 From: alexandergott-afk Date: Thu, 12 Feb 2026 15:32:53 +0100 Subject: [PATCH 395/689] Add the information from my last accepted pull that the TSIG key is optional. --- dnsapi/dns_nsupdate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index cc57cc38..8d7fe2c0 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_nsupdate Options: NSUPDATE_SERVER Server hostname. Default: "localhost". NSUPDATE_SERVER_PORT Server port. Default: "53". - NSUPDATE_KEY File path to TSIG key. Default: "" + NSUPDATE_KEY File path to TSIG key. Default: "". Optional. NSUPDATE_ZONE Domain zone to update. Optional. ' From acaaca89ab9e5fef82c6ceabc0b6ed1ed3062c7c Mon Sep 17 00:00:00 2001 From: emueller Date: Tue, 17 Feb 2026 11:03:53 +0100 Subject: [PATCH 396/689] fixed checking for existing domain on loadmaster --- deploy/kemplm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh index e44e06dc..4e4a78e4 100755 --- a/deploy/kemplm.sh +++ b/deploy/kemplm.sh @@ -56,7 +56,7 @@ kemplm_deploy() { _info "Check if certificate is already present" _list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}" _debug3 _list_request "${_list_request}" - _kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}") + _kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "^${_kemp_domain}$") _debug2 _kemp_cert_count "${_kemp_cert_count}" _kemp_replace_cert=1 From 020a4bb5b30b0ea633c615d15bf6628623da9559 Mon Sep 17 00:00:00 2001 From: infinitydev Date: Tue, 17 Feb 2026 12:12:17 +0000 Subject: [PATCH 397/689] check Proxmox VE API response for errors --- deploy/proxmoxve.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh index 8c67f7de..b6298ee7 100644 --- a/deploy/proxmoxve.sh +++ b/deploy/proxmoxve.sh @@ -128,13 +128,15 @@ HEREDOC export HTTPS_INSECURE=1 export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}" response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") + response="$(echo "$response" | _json_decode | _normalizeJson)" + message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"') _retval=$? - if [ "${_retval}" -eq 0 ]; then + if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then _debug3 response "$response" _info "Certificate successfully deployed" return 0 else - _err "Certificate deployment failed" + _err "Certificate deployment failed: $message" _debug "Response" "$response" return 1 fi From bef0fdb1ae449d8bfbc73a59f3ab208e9efda99a Mon Sep 17 00:00:00 2001 From: infinitydev Date: Tue, 17 Feb 2026 19:28:25 +0100 Subject: [PATCH 398/689] check Proxmox Backup Server API response for errors --- deploy/proxmoxbs.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh index e8528e8f..179b0369 100644 --- a/deploy/proxmoxbs.sh +++ b/deploy/proxmoxbs.sh @@ -116,13 +116,15 @@ HEREDOC export HTTPS_INSECURE=1 export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}" response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") + response="$(echo "$response" | _json_decode | _normalizeJson)" + message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"') _retval=$? - if [ "${_retval}" -eq 0 ]; then + if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then _debug3 response "$response" _info "Certificate successfully deployed" return 0 else - _err "Certificate deployment failed" + _err "Certificate deployment failed: $message" _debug "Response" "$response" return 1 fi From 70f9e255d3229ba7681ba7878eb251bca7bd9a9b Mon Sep 17 00:00:00 2001 From: Travis Date: Wed, 18 Feb 2026 09:29:47 -0800 Subject: [PATCH 399/689] Add Expiry TTL option for Technitium DNS API --- dnsapi/dns_technitium.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_technitium.sh b/dnsapi/dns_technitium.sh index 7bc0dd48..282b56ca 100755 --- a/dnsapi/dns_technitium.sh +++ b/dnsapi/dns_technitium.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium Options: Technitium_Server Server Address Technitium_Token API Token + Technitium_Expiry_Ttl Number of seconds before DNS server auto-deletes the acme record Issues: github.com/acmesh-official/acme.sh/issues/6116 Author: Henning Reich ' @@ -15,7 +16,10 @@ dns_technitium_add() { _Technitium_account fulldomain=$1 txtvalue=$2 - response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" + expiryTtl=${Technitium_Expirty_Ttl:-$(_readaccountconf_mutable Technitium_Expiry_Ttl)} + expiryTtl=${expiryTtl:-0} + + response="$(_get "$Technitium_Server/api/zones/records/add?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}&expiryTtl=$expiryTtl")" if _contains "$response" '"status":"ok"'; then return 0 fi @@ -28,6 +32,14 @@ dns_technitium_rm() { _Technitium_account fulldomain=$1 txtvalue=$2 + expiryTtl=${Technitium_Expirty_Ttl:-$(_readaccountconf_mutable Technitium_Expiry_Ttl)} + expiryTtl=${expiryTtl:-0} + + if [ "$expiryTtl" -ne 0 ]; then + _info "DNS record is configured to be auto-removed after $expiryTtl seconds. Remove operation is bypassed." + return 0 + fi + response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" if _contains "$response" '"status":"ok"'; then return 0 From b5bfd08e3566afb63b87a35f74238439dbb3e59b Mon Sep 17 00:00:00 2001 From: Peter Gerber Date: Sat, 21 Feb 2026 04:36:29 +0100 Subject: [PATCH 400/689] Fix IPv6 URL when trying to fetch challenge ourselves for debugging Fixes the following error when debugging is enabled: [Sat Feb 21 04:00:22 CET 2026] Here is the curl dump log: [Sat Feb 21 04:00:22 CET 2026] * URL rejected: Port number was not a decimal number between 0 and 65535 * closing connection #-1 IPv6 addresses in URLs need to be written like this: http://[2001:43:5::250] --- acme.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 7a3c35a8..d6804648 100755 --- a/acme.sh +++ b/acme.sh @@ -5181,7 +5181,12 @@ $_authorizations_map" if [ "$DEBUG" ]; then if [ "$vtype" = "$VTYPE_HTTP" ]; then _debug "Debug: GET token URL." - _get "http://$d/.well-known/acme-challenge/$token" "" 1 + if _isIPv6 "$d"; then + host="[$d]" + else + host="$d" + fi + _get "http://$host/.well-known/acme-challenge/$token" "" 1 fi fi _clearupwebbroot "$_currentRoot" "$removelevel" "$token" From 8ca1c83b95ec9c72cc97784f8fc5623ad9af5198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike-Christian=20M=C3=BCller?= Date: Mon, 23 Feb 2026 09:24:06 +0100 Subject: [PATCH 401/689] Fixed missing error return value when certificate upload fails. --- deploy/kemplm.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/kemplm.sh b/deploy/kemplm.sh index 4e4a78e4..4cdfcbbe 100755 --- a/deploy/kemplm.sh +++ b/deploy/kemplm.sh @@ -86,6 +86,7 @@ kemplm_deploy() { _info "Upload successful" else _err "Upload failed: ${_kemp_post_message}" + _retval=1 fi else _err "Upload failed" From fdd2e4f19af246d7546ac8e050beca46fd5e84e4 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 1 Mar 2026 13:13:07 +0800 Subject: [PATCH 402/689] add copilot --- .github/copilot-instructions.md | 67 ++++++++++++++++++++++++++++++ .github/workflows/DNS.yml | 16 +++---- .github/workflows/DragonFlyBSD.yml | 2 +- .github/workflows/FreeBSD.yml | 2 +- .github/workflows/Haiku.yml | 2 +- .github/workflows/NetBSD.yml | 2 +- .github/workflows/Omnios.yml | 2 +- .github/workflows/OpenBSD.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- .github/workflows/Solaris.yml | 2 +- 10 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d407607a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,67 @@ +# GitHub Copilot Shell Scripting (sh) Review Instructions + +## 🎯 Overall Goal + +Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files). Ensure the code exhibits the highest levels of robustness, security, and portability. +The review must focus on risks unique to Shell scripting, such as proper quoting, robust error handling, and the secure execution of external commands. + +## 📝 Required Output Format + +Please adhere to the previous format: organize the feedback into a single, structured report, using the three-level marking system: + +1. **🔴 Critical Issues (Must Fix Before Merge)** +2. **🟡 Suggestions (Improvements to Consider)** +3. **✅ Good Practices (Points to Commend)** + +--- + +## 🔍 Focus Areas and Rules for Shell + +### 1. Robustness and Error Handling + +* **Shebang:** Check that the script starts with the correct Shebang, must be "#!/usr/bin/env sh". +* **Startup Options:** **(🔴 Critical)** Enforce the use of the following combination at the start of the script for safety and robustness: + * `set -e`: Exit immediately if a command exits with a non-zero status. + * `set -u`: Treat unset variables as an error and exit. + * `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails. +* **Exit Codes:** Ensure functions and the main script use `exit 0` for success and a non-zero exit code upon failure. +* **Temporary Files:** Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks. + +### 2. Security and Quoting + +* **Variable Quoting:** **(🔴 Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**. +* **Hardcoded Secrets:** **(🔴 Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details. +* **Untrusted Input:** Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use. +* **Avoid `eval`:** Warn against and suggest alternatives to using `eval`, as it can lead to arbitrary code execution. + +### 3. Readability and Maintainability + +* **Function Usage:** Recommend wrapping complex or reusable logic within clearly named functions. +* **Local Variables:** Check that variables inside functions are declared using the `local` keyword to avoid unintentionally modifying global state. +* **Naming Convention:** Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. +* **Test Conditions:** Encourage the use of Bash's **double brackets `[[ ... ]]`** for conditional tests, as it is generally safer and more powerful (e.g., supports pattern matching and avoids Word Splitting) than single brackets `[ ... ]`. +* **Command Substitution:** Encourage using `$(command)` over backticks `` `command` `` for command substitution, as it is easier to nest and improves readability. + +### 4. External Commands and Environment + +* **`for` Loops:** Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces. +* **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. +* **Do not use `head -n`.** Use the `_head_n()` function instead. +* **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. + +--- + +### 5. Review Rules for Files Under `dnsapi/`: + +* **Each file must contain a `{filename}_add` function** for adding DNS TXT records. It should use `_readaccountconf_mutable` to read the API key and `_saveaccountconf_mutable` to save it. Do not use `_saveaccountconf` or `_readaccountconf`. + + +## ❌ Things to Avoid + +* Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or Prettier. +* Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable. + + + + + diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index fbe1e61f..61e025e4 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -251,7 +251,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -306,7 +306,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -362,7 +362,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -418,7 +418,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -480,7 +480,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -534,7 +534,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -589,7 +589,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" @@ -648,7 +648,7 @@ jobs: fi cd ../acmetest ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index f3a85920..9c696621 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -67,7 +67,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index e9ccf7ac..7539d17c 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -72,7 +72,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index bfbde398..a6ba2793 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -75,7 +75,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index e8107d91..ea04e49d 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -67,7 +67,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index a166e26b..28446a47 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -71,7 +71,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index b34c795b..e79b3870 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -72,7 +72,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index 6447911b..f5133c89 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -71,7 +71,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index f5ce713b..1ae828ed 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -73,7 +73,7 @@ jobs: run: | cd ../acmetest \ && ./letest.sh - - name: onError + - name: DebugOnError if: ${{ failure() }} run: | echo "See how to debug in VM:" From f0146bd90ea5d9f358e6ddae7b4f3f887c1165e8 Mon Sep 17 00:00:00 2001 From: Travis Date: Tue, 3 Mar 2026 05:31:51 -0800 Subject: [PATCH 403/689] shfmt edit --- dnsapi/dns_technitium.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_technitium.sh b/dnsapi/dns_technitium.sh index 282b56ca..fbe44606 100755 --- a/dnsapi/dns_technitium.sh +++ b/dnsapi/dns_technitium.sh @@ -39,7 +39,7 @@ dns_technitium_rm() { _info "DNS record is configured to be auto-removed after $expiryTtl seconds. Remove operation is bypassed." return 0 fi - + response="$(_get "$Technitium_Server/api/zones/records/delete?token=$Technitium_Token&domain=$fulldomain&type=TXT&text=${txtvalue}")" if _contains "$response" '"status":"ok"'; then return 0 From 03860a49785dae181edb49a1d3125366459c1acb Mon Sep 17 00:00:00 2001 From: Ludwig <5264048+ludwig-v@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:37:29 +0100 Subject: [PATCH 404/689] dnsapi/dns_me: ignore "already exists" error for multi-domain support (#6830) --- dnsapi/dns_me.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dnsapi/dns_me.sh b/dnsapi/dns_me.sh index 43c903cd..163fe8db 100644 --- a/dnsapi/dns_me.sh +++ b/dnsapi/dns_me.sh @@ -53,6 +53,8 @@ dns_me_add() { _info "Added" #todo: check if the record takes effect return 0 + elif printf -- "%s" "$response" | grep -q "already exists"; then + _info "Record already exists, skipping." else _err "Add txt record error." return 1 From cc677ba9f1b201ebe7548f5ea636fdcc2efd5576 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 8 Mar 2026 18:43:29 +0800 Subject: [PATCH 405/689] minor --- .github/workflows/DragonFlyBSD.yml | 2 +- .github/workflows/FreeBSD.yml | 2 +- .github/workflows/Haiku.yml | 2 +- .github/workflows/Linux.yml | 7 +++++++ .github/workflows/MacOS.yml | 7 +++++++ .github/workflows/NetBSD.yml | 2 +- .github/workflows/Omnios.yml | 2 +- .github/workflows/OpenBSD.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- .github/workflows/Solaris.yml | 2 +- .github/workflows/Windows.yml | 7 +++++++ 11 files changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index 9c696621..9d4d8acb 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -46,7 +46,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 7539d17c..09b544f6 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -52,7 +52,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index a6ba2793..4324545e 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -53,7 +53,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml index 9f3d3f38..17462033 100644 --- a/.github/workflows/Linux.yml +++ b/.github/workflows/Linux.yml @@ -34,6 +34,13 @@ jobs: TEST_ACME_Server: "LetsEncrypt.org_test" steps: - uses: actions/checkout@v6 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 80 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV - name: Clone acmetest run: | cd .. \ diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml index 21793c3e..3869b504 100644 --- a/.github/workflows/MacOS.yml +++ b/.github/workflows/MacOS.yml @@ -47,6 +47,13 @@ jobs: - uses: actions/checkout@v6 - name: Install tools run: brew install socat + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 80 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV - name: Clone acmetest run: | cd .. \ diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index ea04e49d..4021cd7e 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -46,7 +46,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index 28446a47..a156800c 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -52,7 +52,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index e79b3870..8a91cd2e 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -52,7 +52,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index f5133c89..dca29741 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -52,7 +52,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 1ae828ed..397469b5 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -52,7 +52,7 @@ jobs: ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - uses: actions/checkout@v6 - - uses: vmactions/cf-tunnel@v0 + - uses: anyvm-org/cf-tunnel@v0 id: tunnel with: protocol: http diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index 4c195917..c628ff5b 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -67,6 +67,13 @@ jobs: shell: cmd run: | echo "PATH=%PATH%" + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 80 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV - name: Clone acmetest shell: cmd run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ From fc8a61f10ff6226679ae428a01b4adb68f229490 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 13 Mar 2026 18:41:02 +0800 Subject: [PATCH 406/689] update --- .github/workflows/dockerhub.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 0d9046df..0e7ba748 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -50,7 +50,7 @@ jobs: uses: docker/setup-qemu-action@v2 - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5.5.1 + uses: docker/metadata-action@v6 with: images: ${DOCKER_IMAGE} - name: Set up Docker Buildx From 3198c1af6e61b033e2cea8d4ebccd083dc44003e Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 13 Mar 2026 20:10:22 +0800 Subject: [PATCH 407/689] fix https://github.com/acmesh-official/acme.sh/issues/6856#issuecomment-4054175916 --- .github/workflows/DNS.yml | 3 ++- .github/workflows/DragonFlyBSD.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 61e025e4..0104595d 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -397,7 +397,8 @@ jobs: with: envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | - pkg install -y curl socat libnghttp2 + pkg install -y libnghttp2 + pkg install -y curl socat usesh: true sync: nfs run: | diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index 9d4d8acb..f32d0916 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -61,7 +61,8 @@ jobs: nat: | "8080": "80" prepare: | - pkg install -y curl socat libnghttp2 + pkg install -y libnghttp2 + pkg install -y curl socat usesh: true sync: nfs run: | From 8aea731bd4f7c44615cb2af70c519a96b63fbb35 Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Mon, 16 Mar 2026 13:17:18 +0100 Subject: [PATCH 408/689] Add dns_czechia DNS API plugin (#6764) * Create dns_czechia.sh This PR adds a DNS API plugin for CZECHIA.COM / RegZone (ZONER a.s.). --- dnsapi/dns_czechia.sh | 201 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 dnsapi/dns_czechia.sh diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh new file mode 100644 index 00000000..f0f4c32e --- /dev/null +++ b/dnsapi/dns_czechia.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env sh + +# dns_czechia.sh - CZECHIA.COM/ZONER DNS API for acme.sh (DNS-01) +# +# Documentation: https://api.czechia.com/swagger/index.html + +#shellcheck disable=SC2034 +dns_czechia_info='[ + {"name":"CZ_AuthorizationToken","usage":"Your API token from CZECHIA.COM/Zoner administration.","required":"1"}, + {"name":"CZ_Zones","usage":"Managed zones separated by comma or space (e.g. \"example.com\").","required":"1"}, + {"name":"CZ_API_BASE","usage":"Defaults to https://api.czechia.com","required":"0"} +]' + +dns_czechia_add() { + fulldomain="$1" + txtvalue="$2" + + _debug "dns_czechia_add fulldomain='$fulldomain'" + + if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then + _err "dns_czechia_add: missing fulldomain or txtvalue" + return 1 + fi + + _czechia_load_conf || return 1 + + _current_zone=$(_czechia_pick_zone "$fulldomain") + if [ -z "$_current_zone" ]; then + _err "No matching zone found for $fulldomain. Please check CZ_Zones." + return 1 + fi + + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + + if [ -z "$_cz" ] || [ -z "$_tk" ]; then + _err "Missing zone or CZ_AuthorizationToken." + return 1 + fi + + _url="$CZ_API_BASE/api/DNS/$_cz/TXT" + _fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//') + + if [ "$_fd" = "$_cz" ]; then + _h="@" + else + # Remove the literal "." suffix from _fd, if present + _h=${_fd%."$_cz"} + [ "$_h" = "$_fd" ] && _h="@" + fi + [ -z "$_h" ] && _h="@" + + _info "Adding TXT record for $_h in zone $_cz" + + _h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g') + _txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g') + _body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}" + + _debug "URL: $_url" + _debug "Body: $_body" + + export _H1="Content-Type: application/json" + export _H2="AuthorizationToken: $_tk" + + _res="$(_post "$_body" "$_url" "" "POST")" + _post_exit="$?" + _debug2 "Response: $_res" + + if [ "$_post_exit" -ne 0 ]; then + _err "API request failed. exit code $_post_exit" + return 1 + fi + + if _contains "$_res" "already exists"; then + _info "Record already exists, skipping." + return 0 + fi + + _nres="$(_normalizeJson "$_res")" + if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then + _nres="$_res" + fi + + if _contains "$_nres" "\"status\":4" || _contains "$_nres" "\"status\":5" || _contains "$_nres" "\"errors\""; then + _err "API error: $_res" + return 1 + fi + + return 0 +} + +dns_czechia_rm() { + fulldomain="$1" + txtvalue="$2" + + _debug "dns_czechia_rm fulldomain='$fulldomain'" + + if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then + _err "dns_czechia_rm: missing fulldomain or txtvalue" + return 1 + fi + + _czechia_load_conf || return 1 + + _current_zone=$(_czechia_pick_zone "$fulldomain") + if [ -z "$_current_zone" ]; then + _err "No matching zone found for $fulldomain. Please check CZ_Zones configuration." + return 1 + fi + + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + + if [ -z "$_cz" ] || [ -z "$_tk" ]; then + _err "Missing zone or CZ_AuthorizationToken." + return 1 + fi + + _url="$CZ_API_BASE/api/DNS/$_cz/TXT" + _fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//') + + if [ "$_fd" = "$_cz" ]; then + _h="@" + else + _h=$(printf "%s" "$_fd" | sed "s/\.$_cz$//") + [ "$_h" = "$_fd" ] && _h="@" + fi + [ -z "$_h" ] && _h="@" + + _h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g') + _txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g') + _body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}" + + _debug "URL: $_url" + _debug "Body: $_body" + + export _H1="Content-Type: application/json" + export _H2="AuthorizationToken: $_tk" + + _res="$(_post "$_body" "$_url" "" "DELETE")" + _post_exit="$?" + _debug2 "Response: $_res" + + if [ "$_post_exit" -ne 0 ]; then + _err "CZECHIA DNS API DELETE request failed for $_fd: exit code $_post_exit, response: $_res" + return 1 + fi + + _res_normalized=$(printf '%s' "$_res" | _normalizeJson) + + if _contains "$_res_normalized" '"isError":true'; then + _err "CZECHIA DNS API reported an error while deleting TXT for $_fd: $_res" + return 1 + fi + + return 0 +} + +_czechia_load_conf() { + CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}" + if [ -z "$CZ_AuthorizationToken" ]; then + _err "Missing CZ_AuthorizationToken" + return 1 + fi + + CZ_Zones="${CZ_Zones:-$(_readaccountconf_mutable CZ_Zones)}" + if [ -z "$CZ_Zones" ]; then + _err "Missing CZ_Zones" + return 1 + fi + + CZ_API_BASE="${CZ_API_BASE:-$(_readaccountconf_mutable CZ_API_BASE)}" + [ -z "$CZ_API_BASE" ] && CZ_API_BASE="https://api.czechia.com" + + _saveaccountconf_mutable CZ_AuthorizationToken "$CZ_AuthorizationToken" + _saveaccountconf_mutable CZ_Zones "$CZ_Zones" + _saveaccountconf_mutable CZ_API_BASE "$CZ_API_BASE" + + return 0 +} + +_czechia_pick_zone() { + _fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//') + _best_zone="" + + _zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g') + for _z in $_zones_space; do + _clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + [ -z "$_clean_z" ] && continue + + case "$_fd" in + "$_clean_z" | *."$_clean_z") + if [ ${#_clean_z} -gt ${#_best_zone} ]; then + _best_zone="$_clean_z" + fi + ;; + esac + done + + printf "%s" "$_best_zone" +} From 5842e6ff4f5f67cb953b65736295256431f1a29d Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 10:40:10 +0800 Subject: [PATCH 409/689] don't switch from test back to production ca --- acme.sh | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/acme.sh b/acme.sh index d6804648..75bf7047 100755 --- a/acme.sh +++ b/acme.sh @@ -5555,16 +5555,17 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" - case "$Le_API" in - "$CA_LETSENCRYPT_V2_TEST") - _info "Switching back to $CA_LETSENCRYPT_V2" - Le_API="$CA_LETSENCRYPT_V2" - ;; - "$CA_GOOGLE_TEST") - _info "Switching back to $CA_GOOGLE" - Le_API="$CA_GOOGLE" - ;; - esac +#don't switch it back +# case "$Le_API" in +# "$CA_LETSENCRYPT_V2_TEST") +# _info "Switching back to $CA_LETSENCRYPT_V2" +# Le_API="$CA_LETSENCRYPT_V2" +# ;; +# "$CA_GOOGLE_TEST") +# _info "Switching back to $CA_GOOGLE" +# Le_API="$CA_GOOGLE" +# ;; +# esac if [ "$_server" ]; then Le_API="$_server" From e21be4455f2cdcb8bd32171c88d76ee21298b2dc Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 10:41:58 +0800 Subject: [PATCH 410/689] format --- acme.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/acme.sh b/acme.sh index 75bf7047..184f15cc 100755 --- a/acme.sh +++ b/acme.sh @@ -5555,17 +5555,17 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" -#don't switch it back -# case "$Le_API" in -# "$CA_LETSENCRYPT_V2_TEST") -# _info "Switching back to $CA_LETSENCRYPT_V2" -# Le_API="$CA_LETSENCRYPT_V2" -# ;; -# "$CA_GOOGLE_TEST") -# _info "Switching back to $CA_GOOGLE" -# Le_API="$CA_GOOGLE" -# ;; -# esac + #don't switch it back + # case "$Le_API" in + # "$CA_LETSENCRYPT_V2_TEST") + # _info "Switching back to $CA_LETSENCRYPT_V2" + # Le_API="$CA_LETSENCRYPT_V2" + # ;; + # "$CA_GOOGLE_TEST") + # _info "Switching back to $CA_GOOGLE" + # Le_API="$CA_GOOGLE" + # ;; + # esac if [ "$_server" ]; then Le_API="$_server" From af5e592fe45cc318a6cb702c5ae1ed3eb7d6d9ef Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 10:47:11 +0800 Subject: [PATCH 411/689] fix https://github.com/acmesh-official/acme.sh/issues/6866#issuecomment-4080403721 --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 184f15cc..db0eb92e 100755 --- a/acme.sh +++ b/acme.sh @@ -5285,7 +5285,7 @@ $_authorizations_map" _info "Order status is 'ready', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ]; then + if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then _info "Sleeping for $_retryafter seconds then retrying" _sleep $_retryafter else @@ -5295,7 +5295,7 @@ $_authorizations_map" _info "Order status is 'processing', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ]; then + if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then _info "Sleeping for $_retryafter seconds then retrying" _sleep $_retryafter else From e26ce2f19ca3b277d38015a8e905b5ebfabbee9e Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 13:02:16 +0800 Subject: [PATCH 412/689] fix https://github.com/acmesh-official/acme.sh/issues/4924#issuecomment-4069887654 --- acme.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/acme.sh b/acme.sh index db0eb92e..b57228d3 100755 --- a/acme.sh +++ b/acme.sh @@ -5765,6 +5765,9 @@ ${_skipped_msg} fi fi + if [ "$_TREAT_SKIP_AS_SUCCESS" ] && [ "$_ret" = "$RENEW_SKIP" ]; then + _ret=0 + fi return "$_ret" } @@ -6983,6 +6986,7 @@ cron() { _info "Automatically upgraded to: $VER" fi + _TREAT_SKIP_AS_SUCCESS="1" renewAll _ret="$?" _ACME_IN_CRON="" @@ -7230,6 +7234,7 @@ Parameters: --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. --listraw Only used for '--list' command, list the certs in raw format. -se, --stop-renew-on-error Only valid for '--renew-all' command. Stop if one cert has error in renewal. + --treat-skip-as-success Only valid for '--renew-all' command. Treat skipped certs as success, return 0 instead of $RENEW_SKIP. --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. --ca-bundle Specifies the path to the CA certificate bundle to verify api server's certificate. --ca-path Specifies directory containing CA certificates in PEM format, used by wget or curl. @@ -7710,6 +7715,9 @@ _process() { -f | --force) FORCE="1" ;; + --treat-skip-as-success | --treatskipassuccess) + _TREAT_SKIP_AS_SUCCESS="1" + ;; --staging | --test) STAGE="1" ;; From c397bd6573976755e08cfee98cfd4d428679f1d2 Mon Sep 17 00:00:00 2001 From: heximcz Date: Sun, 22 Mar 2026 04:36:29 +0100 Subject: [PATCH 413/689] Add BEST-HOSTING DNS API (#6859) * Add BEST-HOSTING DNS API --- dnsapi/dns_bh.sh | 202 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100755 dnsapi/dns_bh.sh diff --git a/dnsapi/dns_bh.sh b/dnsapi/dns_bh.sh new file mode 100755 index 00000000..fbb69ef2 --- /dev/null +++ b/dnsapi/dns_bh.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_bh_info='Best-Hosting.cz +Site: best-hosting.cz +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bh +Options: + BH_API_USER API User identifier. + BH_API_KEY API Secret key. +Issues: github.com/acmesh-official/acme.sh/issues/6854 +Author: @heximcz +' + +BH_Api="https://best-hosting.cz/api/v1" + +######## Public functions ##################### + +# Usage: dns_bh_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_bh_add() { + fulldomain=$1 + txtvalue=$2 + + # --- 1. Credentials --- + BH_API_USER="${BH_API_USER:-$(_readaccountconf_mutable BH_API_USER)}" + BH_API_KEY="${BH_API_KEY:-$(_readaccountconf_mutable BH_API_KEY)}" + + if [ -z "$BH_API_USER" ] || [ -z "$BH_API_KEY" ]; then + BH_API_USER="" + BH_API_KEY="" + _err "You must specify BH_API_USER and BH_API_KEY." + return 1 + fi + + _saveaccountconf_mutable BH_API_USER "$BH_API_USER" + _saveaccountconf_mutable BH_API_KEY "$BH_API_KEY" + + # --- 2. Add TXT record --- + _info "Adding TXT record for $fulldomain" + + json_payload="{\"fulldomain\":\"$fulldomain\",\"txtvalue\":\"$txtvalue\"}" + if ! _bh_rest POST "dns" "$json_payload"; then + _err "Failed to add DNS record." + return 1 + fi + + _norm_add=$(printf "%s" "$response" | tr -d '[:space:]') + if ! _contains "$_norm_add" '"status":"success"'; then + _err "API error: $response" + return 1 + fi + + record_id=$(printf "%s" "$_norm_add" | _egrep_o '"id":[0-9]+' | cut -d':' -f2) + _debug record_id "$record_id" + + if [ -z "$record_id" ]; then + _err "Could not parse record ID from response." + return 1 + fi + + # Sanitize key — replace dots and hyphens with underscores + _conf_key=$(printf "%s" "BH_record_ids_${fulldomain}" | tr '.-' '_') + + # Wildcard support: store space-separated list of IDs + # First call stores "111", second call stores "111 222" + _existing_ids=$(_readdomainconf "$_conf_key") + if [ -z "$_existing_ids" ]; then + _savedomainconf "$_conf_key" "$record_id" + else + _savedomainconf "$_conf_key" "$_existing_ids $record_id" + fi + + _info "DNS TXT record added successfully." + return 0 +} + +# Usage: dns_bh_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_bh_rm() { + fulldomain=$1 + txtvalue=$2 + + # --- 1. Credentials --- + BH_API_USER="${BH_API_USER:-$(_readaccountconf_mutable BH_API_USER)}" + BH_API_KEY="${BH_API_KEY:-$(_readaccountconf_mutable BH_API_KEY)}" + + if [ -z "$BH_API_USER" ] || [ -z "$BH_API_KEY" ]; then + BH_API_USER="" + BH_API_KEY="" + _err "You must specify BH_API_USER and BH_API_KEY." + return 1 + fi + + # Sanitize key — same as in add + _conf_key=$(printf "%s" "BH_record_ids_${fulldomain}" | tr '.-' '_') + + # --- 2. Load stored record ID(s) --- + _existing_ids=$(_readdomainconf "$_conf_key") + _debug _existing_ids "$_existing_ids" + + if [ -z "$_existing_ids" ]; then + _err "Could not find record ID for $fulldomain." + return 1 + fi + + record_id="" + _remaining_ids="" + + # Find the record ID that matches both the name and txtvalue + for _id in $_existing_ids; do + if ! _bh_rest GET "dns/$_id"; then + _debug "Failed to query record id $_id, skipping." + + # Keep it in the list so a later run can try again + if [ -z "$_remaining_ids" ]; then + _remaining_ids="$_id" + else + _remaining_ids="$_remaining_ids $_id" + fi + continue + fi + + _match_name=0 + _match_content=0 + _norm_response=$(printf "%s" "$response" | tr -d '[:space:]') + + case "$_norm_response" in + *"\"name\":\"$fulldomain\""*) + _match_name=1 + ;; + esac + case "$_norm_response" in + *"\"content\":\"$txtvalue\""*) + _match_content=1 + ;; + esac + + if [ "$_match_name" -eq 1 ] && [ "$_match_content" -eq 1 ]; then + record_id="$_id" + _debug "Matched record id" "$record_id" + # Do not add this ID to _remaining_ids; it will be deleted + continue + fi + + # Not a match — keep ID for potential future cleanups + if [ -z "$_remaining_ids" ]; then + _remaining_ids="$_id" + else + _remaining_ids="$_remaining_ids $_id" + fi + done + + if [ -z "$record_id" ]; then + _err "Could not find matching TXT record for $fulldomain with the given value." + return 1 + fi + + # --- 3. Delete record --- + _info "Removing TXT record for $fulldomain" + + if ! _bh_rest DELETE "dns/$record_id"; then + _err "Failed to remove DNS record." + return 1 + fi + + # Update stored list — remove used ID + if [ -z "$_remaining_ids" ]; then + _cleardomainconf "$_conf_key" + else + _savedomainconf "$_conf_key" "$_remaining_ids" + fi + + _info "DNS TXT record removed successfully." + return 0 +} + +#################### Private functions ##################### + +_bh_rest() { + m="$1" + ep="$2" + data="$3" + _debug "$ep" + + _credentials="$(printf "%s:%s" "$BH_API_USER" "$BH_API_KEY" | _base64)" + + export _H1="Authorization: Basic $_credentials" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + if [ "$m" = "GET" ]; then + response="$(_get "$BH_Api/$ep")" + else + _debug data "$data" + response="$(_post "$data" "$BH_Api/$ep" "" "$m")" + fi + + if [ "$?" != "0" ]; then + _err "Error calling $m $BH_Api/$ep" + return 1 + fi + + _debug2 response "$response" + return 0 +} From 3c8c7353622c1436e42d2a9ad68b0694f6c6ef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pavli=C4=8D?= Date: Sun, 22 Mar 2026 04:40:44 +0100 Subject: [PATCH 414/689] [dnsapi] add subreg.cz dns hook (#6848) * Add DNS hook for subreg.cz --- dnsapi/dns_subreg.sh | 220 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 dnsapi/dns_subreg.sh diff --git a/dnsapi/dns_subreg.sh b/dnsapi/dns_subreg.sh new file mode 100644 index 00000000..5e7e7ced --- /dev/null +++ b/dnsapi/dns_subreg.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_subreg_info='Subreg.cz +Site: subreg.cz +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_subreg +Options: + SUBREG_API_USERNAME API username + SUBREG_API_PASSWORD API password +Issues: github.com/acmesh-official/acme.sh/issues/6835 +Author: Tomas Pavlic +' + +# Subreg SOAP API +# https://subreg.cz/manual/ + +SUBREG_API_URL="https://soap.subreg.cz/cmd.php" + +######## Public functions ##################### + +# Usage: dns_subreg_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_subreg_add() { + fulldomain=$1 + txtvalue=$2 + + SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}" + SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}" + if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then + _err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set." + return 1 + fi + + _saveaccountconf_mutable SUBREG_API_USERNAME "$SUBREG_API_USERNAME" + _saveaccountconf_mutable SUBREG_API_PASSWORD "$SUBREG_API_PASSWORD" + + if ! _subreg_login; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Cannot determine root domain for: $fulldomain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _subreg_soap "Add_DNS_Record" "$_domain$_sub_domainTXT$txtvalue0120" + if _subreg_ok; then + _record_id="$(_subreg_map_get record_id)" + + if [ -z "$_record_id" ]; then + _err "Subreg API did not return a record_id for TXT record on $fulldomain" + _err "$response" + return 1 + fi + + _savedomainconf "$(_subreg_record_id_key "$txtvalue")" "$_record_id" + return 0 + fi + _err "Failed to add TXT record." + _err "$response" + return 1 +} + +# Usage: dns_subreg_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_subreg_rm() { + fulldomain=$1 + txtvalue=$2 + + SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}" + SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}" + if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then + _err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set." + return 1 + fi + + if ! _subreg_login; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Cannot determine root domain for: $fulldomain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _record_id="$(_readdomainconf "$(_subreg_record_id_key "$txtvalue")")" + if [ -z "$_record_id" ]; then + _err "Could not find saved record ID for $fulldomain" + return 1 + fi + + _debug "Deleting record ID: $_record_id" + _subreg_soap "Delete_DNS_Record" "$_domain$_record_id" + if _subreg_ok; then + + _cleardomainconf "$(_subreg_record_id_key "$txtvalue")" + return 0 + fi + + _err "Failed to delete TXT record." + _err "$response" + return 1 +} + +#################### Private functions ##################### + +# Build a domain-conf key for storing the record ID of a given TXT value. +# Base64url chars include '-' which is invalid in shell variable names, so replace with '_'. +_subreg_record_id_key() { + printf 'SUBREG_RECORD_ID_%s' "$(printf '%s' "$1" | tr '-' '_')" +} + +# Check if the current $response contains a successful status in the ns2:Map format: +# statusok +_subreg_ok() { + [ "$(_subreg_map_get status)" = "ok" ] +} + +# Extract the value for a given key from the ns2:Map response. +# Usage: _subreg_map_get keyname +# Reads from $response +_subreg_map_get() { + _key="$1" + echo "$response" | tr -d '\n\r' | _egrep_o ">${_key}]*>[^<]*" | sed 's/.*]*>//;s/<\/value>//' +} + +# Login and store session token in _subreg_ssid +_subreg_login() { + _debug "Logging in to Subreg API as $SUBREG_API_USERNAME" + _subreg_soap_noauth "Login" "$SUBREG_API_USERNAME$SUBREG_API_PASSWORD" + if ! _subreg_ok; then + _err "Subreg login failed." + _err "$response" + return 1 + fi + _subreg_ssid="$(_subreg_map_get ssid)" + if [ -z "$_subreg_ssid" ]; then + _err "Subreg login: could not extract session token (ssid)." + return 1 + fi + _debug "Subreg login: session token (ssid) obtained" + return 0 +} + +# _get_root _acme-challenge.www.domain.com +# returns _sub_domain and _domain +_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + _err "Unable to retrieve DNS zone matching domain: $domain" + return 1 + fi + + _subreg_soap "Get_DNS_Zone" "$h" + + if _subreg_ok; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="$h" + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done +} + +# Send a SOAP request without authentication (used for Login) +# _subreg_soap_noauth command inner_xml +_subreg_build_soap() { + _cmd="$1" + _data_inner="$2" + + _soap_body=" + + + + + ${_data_inner} + + + +" + + export _H1="Content-Type: text/xml" + export _H2="SOAPAction: http://soap.subreg.cz/soap#${_cmd}" + response="$(_post "$_soap_body" "$SUBREG_API_URL" "" "POST" "text/xml")" +} + +# Send an authenticated SOAP request (requires _subreg_ssid to be set) +# _subreg_soap command inner_xml +_subreg_soap_noauth() { + _cmd="$1" + _inner="$2" + + _subreg_build_soap "$_cmd" "$_inner" +} + +# Send an authenticated SOAP request (requires _subreg_ssid to be set) +# _subreg_soap command inner_xml +_subreg_soap() { + _cmd="$1" + _inner="$2" + _inner_with_ssid="${_subreg_ssid}${_inner}" + + _subreg_build_soap "$_cmd" "$_inner_with_ssid" +} From bf486bb98868e5e459e4d81a90c6450739313744 Mon Sep 17 00:00:00 2001 From: orangepizza Date: Sat, 28 Mar 2026 10:26:47 +0900 Subject: [PATCH 415/689] Update copilot instruction to match actual PR rule (#6873) old version had instruction to use bash-only [[ ]] test, remove it and add rules for DNS script writing from https://github.com/acmesh-official/acme.sh/issues/343 --- .github/copilot-instructions.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d407607a..af0e8147 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -39,7 +39,6 @@ Please adhere to the previous format: organize the feedback into a single, struc * **Function Usage:** Recommend wrapping complex or reusable logic within clearly named functions. * **Local Variables:** Check that variables inside functions are declared using the `local` keyword to avoid unintentionally modifying global state. * **Naming Convention:** Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. -* **Test Conditions:** Encourage the use of Bash's **double brackets `[[ ... ]]`** for conditional tests, as it is generally safer and more powerful (e.g., supports pattern matching and avoids Word Splitting) than single brackets `[ ... ]`. * **Command Substitution:** Encourage using `$(command)` over backticks `` `command` `` for command substitution, as it is easier to nest and improves readability. ### 4. External Commands and Environment @@ -48,13 +47,14 @@ Please adhere to the previous format: organize the feedback into a single, struc * **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. * **Do not use `head -n`.** Use the `_head_n()` function instead. * **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. +* **keep it sh compatible, do not use bash-only syntax.** We need to cross platforms between Linux/BSD/Mac. --- ### 5. Review Rules for Files Under `dnsapi/`: * **Each file must contain a `{filename}_add` function** for adding DNS TXT records. It should use `_readaccountconf_mutable` to read the API key and `_saveaccountconf_mutable` to save it. Do not use `_saveaccountconf` or `_readaccountconf`. - +* **keep it shell only** Do not add more dependencies. common tools, such as grep or sed etc are ok to use. do not depend on python or perl etc. ## ❌ Things to Avoid @@ -64,4 +64,3 @@ Please adhere to the previous format: organize the feedback into a single, struc - From fe5d2e3ef777d21ca1bb876fb929947d60d8d253 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Apr 2026 11:33:05 +0800 Subject: [PATCH 416/689] fix rule --- .github/copilot-instructions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index af0e8147..88a45fd7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,6 +47,9 @@ Please adhere to the previous format: organize the feedback into a single, struc * **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. * **Do not use `head -n`.** Use the `_head_n()` function instead. * **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. +* **Do not use `awk`.** Use the `cut` and `sed` instead. +* **Do not use `[:space:]` or `[:punct:]`.** +* **Do not use `grep -E` or `grep -O`, .** Use the `_egrep_o` function instead. * **keep it sh compatible, do not use bash-only syntax.** We need to cross platforms between Linux/BSD/Mac. --- From ef49a9fd23ad2e5dca5a23d41afb870e8b7f9926 Mon Sep 17 00:00:00 2001 From: Florian Heigl Date: Fri, 3 Apr 2026 05:38:21 +0200 Subject: [PATCH 417/689] Update synology_dsm.sh (#6894) quote variable name so message isn't missing the variable that needs to be fixed. this was reported in #2727 (feedback for hook) --- deploy/synology_dsm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 3bfc9b02..f6cc2cd0 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -353,7 +353,7 @@ synology_dsm_deploy() { _debug2 SYNO_CREATE "$SYNO_CREATE" if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then - _err "Unable to find certificate: $SYNO_CERTIFICATE and $SYNO_CREATE is not set." + _err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set." _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" return 1 fi From cf9c70a6c797076b45f16d73baa6f090cc66ecb0 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Apr 2026 21:16:40 +0800 Subject: [PATCH 418/689] Update copilot-instructions.md --- .github/copilot-instructions.md | 210 ++++++++++++++++++++++++-------- 1 file changed, 161 insertions(+), 49 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 88a45fd7..cd21b65a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,69 +1,181 @@ -# GitHub Copilot Shell Scripting (sh) Review Instructions +# GitHub Copilot Shell Scripting (sh) Review Instructions for acme.sh -## 🎯 Overall Goal +## Overall Goal -Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files). Ensure the code exhibits the highest levels of robustness, security, and portability. +Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files) for the [acme.sh](https://github.com/acmesh-official/acme.sh) project. Ensure the code exhibits the highest levels of robustness, security, and portability. The review must focus on risks unique to Shell scripting, such as proper quoting, robust error handling, and the secure execution of external commands. -## 📝 Required Output Format +## Required Output Format -Please adhere to the previous format: organize the feedback into a single, structured report, using the three-level marking system: +Organize the feedback into a single, structured report, using the three-level marking system: -1. **🔴 Critical Issues (Must Fix Before Merge)** -2. **🟡 Suggestions (Improvements to Consider)** -3. **✅ Good Practices (Points to Commend)** +1. **Critical Issues (Must Fix Before Merge)** +2. **Suggestions (Improvements to Consider)** +3. **Good Practices (Points to Commend)** --- -## 🔍 Focus Areas and Rules for Shell +## Shell Compatibility -### 1. Robustness and Error Handling - -* **Shebang:** Check that the script starts with the correct Shebang, must be "#!/usr/bin/env sh". -* **Startup Options:** **(🔴 Critical)** Enforce the use of the following combination at the start of the script for safety and robustness: - * `set -e`: Exit immediately if a command exits with a non-zero status. - * `set -u`: Treat unset variables as an error and exit. - * `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails. -* **Exit Codes:** Ensure functions and the main script use `exit 0` for success and a non-zero exit code upon failure. -* **Temporary Files:** Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks. - -### 2. Security and Quoting - -* **Variable Quoting:** **(🔴 Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**. -* **Hardcoded Secrets:** **(🔴 Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details. -* **Untrusted Input:** Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use. -* **Avoid `eval`:** Warn against and suggest alternatives to using `eval`, as it can lead to arbitrary code execution. - -### 3. Readability and Maintainability - -* **Function Usage:** Recommend wrapping complex or reusable logic within clearly named functions. -* **Local Variables:** Check that variables inside functions are declared using the `local` keyword to avoid unintentionally modifying global state. -* **Naming Convention:** Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. -* **Command Substitution:** Encourage using `$(command)` over backticks `` `command` `` for command substitution, as it is easier to nest and improves readability. - -### 4. External Commands and Environment - -* **`for` Loops:** Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces. -* **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. -* **Do not use `head -n`.** Use the `_head_n()` function instead. -* **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. -* **Do not use `awk`.** Use the `cut` and `sed` instead. -* **Do not use `[:space:]` or `[:punct:]`.** -* **Do not use `grep -E` or `grep -O`, .** Use the `_egrep_o` function instead. -* **keep it sh compatible, do not use bash-only syntax.** We need to cross platforms between Linux/BSD/Mac. +- **POSIX sh only** -- all scripts must target `sh`, not `bash`. No bash-isms allowed. +- **Shebang**: always use `#!/usr/bin/env sh` (not `#!/bin/sh`, not `#!/usr/bin/env bash`). +- **Use `return`, never `exit`** -- scripts are sourced, not executed as subprocesses. `exit` would kill the parent shell. +- **Cross-platform**: code must work on Linux, macOS, FreeBSD, Solaris, and BusyBox environments. --- -### 5. Review Rules for Files Under `dnsapi/`: +## Robustness and Error Handling -* **Each file must contain a `{filename}_add` function** for adding DNS TXT records. It should use `_readaccountconf_mutable` to read the API key and `_saveaccountconf_mutable` to save it. Do not use `_saveaccountconf` or `_readaccountconf`. -* **keep it shell only** Do not add more dependencies. common tools, such as grep or sed etc are ok to use. do not depend on python or perl etc. +- **(Critical)** Enforce the use of the following combination at the start of the script for safety and robustness: + - `set -e`: Exit immediately if a command exits with a non-zero status. + - `set -u`: Treat unset variables as an error and exit. + - `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails. +- **Always check return values** of function calls. If an error occurs, there must be a way to stop execution. +- **Return 1** after `_err` messages: + ```sh + if [ -z "$VARIABLE" ]; then + _err "VARIABLE is required" + return 1 + fi + ``` +- Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks. -## ❌ Things to Avoid +--- -* Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or Prettier. -* Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable. +## Security and Quoting +- **(Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**. +- **(Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details. +- Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use. +- Avoid `eval` -- warn against and suggest alternatives, as it can lead to arbitrary code execution. +--- +## Use Built-in Helper Functions +Never use raw shell commands when acme.sh provides a wrapper function. This is the most critical rule for portability. + +| Instead of | Use | +|---|---| +| `tr '[:upper:]' '[:lower:]'` | `_lower_case()` | +| `head -n 1` | `_head_n 1` | +| `openssl dgst` / `openssl` | `_digest()` / `_hmac()` | +| `date` | `_utc_date()` with `sed`/`tr` | +| `curl` / `wget` | `_get()` or `_post()` | +| `sleep` | `_sleep` | +| `base64` / `openssl base64` | `_base64()` | +| `$(( ))` arithmetic | `_math()` | +| `grep -E` / `grep -Po` | `_egrep_o()` | +| `printf` | `echo` | +| `idn` command | `_idn()` / `_is_idn()` | + +When fixing a pattern issue, fix **all instances** in the file, not just the one highlighted. + +--- + +## Forbidden External Tools + +Do not use these commands -- they are not portable across all target platforms: + +- `jq` (parse JSON with built-in string manipulation) +- `grep -A` (removed throughout the project) +- `grep -Po` (Perl regex not available everywhere) +- `rev`, `xargs`, `iconv` +- If you must depend on an external tool, check with `_exists` first: + ```sh + if ! _exists jq; then + _err "jq is required" + return 1 + fi + ``` +- Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces. + +--- + +## Configuration Management + +Use the correct save/read functions depending on hook type: + +- **DNS hooks**: `_readaccountconf_mutable` to read API keys, `_saveaccountconf_mutable` to save them. Do not use `_saveaccountconf` or `_readaccountconf`. +- **Deploy hooks**: `_savedeployconf` / `_getdeployconf` +- **Notification hooks**: use account conf functions. +- Save operations should only happen in the correct lifecycle function (e.g., `_issue()`). +- Use environment variables for all configurable values -- do not introduce hardcoded config files. +- Do not clear account conf without a clear reason. + +--- + +## DNS API Conventions + +- Read the [DNS API Dev Guide](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide) before writing a DNS plugin. +- Each file under `dnsapi/` must contain a `{filename}_add` function for adding DNS TXT records. +- The `_get_root()` loop counter `i` must start from `1` (not `2`) to support DNS alias mode. +- The `dns_*_rm()` function must remove records **by TXT value**, not by replacing/updating. See [#1261](https://github.com/acmesh-official/acme.sh/issues/1261). +- Preserve the `dns_*_info` metadata variable block in each DNS script header. + +--- + +## Variable Naming + +- Use CamelCase with provider prefix: `KINGHOST_Username` (not `KINGHOST_username`). +- Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. +- Avoid confusingly similar names. Prefer one variable with comma-separated values over multiple variables (e.g., `CZ_Zones` with comma support instead of separate `CZ_Zone` and `CZ_Zones`). +- Do not define variables with the same name in different scopes. +- Variables inside functions should be declared using the `local` keyword to avoid unintentionally modifying global state. + +--- + +## Code Style + +- Use `shfmt` for formatting -- CI enforces it. +- Reduce indentation where possible. +- Single space, not double spaces. +- No trailing semicolons after `return` statements. +- Add a newline at the end of every file. +- Use `$(command)` over backticks `` `command` `` for command substitution. + +--- + +## Simplicity + +- Prefer hardcoded sensible defaults over unnecessary configuration variables (e.g., use `3600` for TTL instead of a `DESEC_TTL` variable). +- Reject over-engineered solutions. If it can be done in one line, do it in one line. +- Follow existing patterns in the codebase -- new hooks should look like existing hooks. +- Respect user choices: do not `chmod` files that already exist; the user's permissions take priority. + +--- + +## Documentation Requirements + +Before a PR can be merged, the following documentation must be provided: + +- **Wiki page**: add or update the relevant page: + - DNS APIs: [dnsapi](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) or [dnsapi2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2) + - Deploy hooks: [deployhooks](https://github.com/acmesh-official/acme.sh/wiki/deployhooks) + - Notification hooks: [notify](https://github.com/acmesh-official/acme.sh/wiki/notify) + - Options: [Options-and-Params](https://github.com/acmesh-official/acme.sh/wiki/Options-and-Params) +- **In-code usage**: add usage examples in the help text of `acme.sh` itself. +- **README**: add website URLs for new DNS providers. + +--- + +## CI and Merge Hygiene + +- All CI checks must pass before merge. +- Rebase to the latest `dev` branch frequently -- do not use merge commits. +- Enable GitHub Actions on your fork to catch errors early. +- Run the [DNS API Test](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Test) workflow for DNS plugins. +- For Docker changes, ensure the Dockerfile includes any required dependencies. + +--- + +## Debug Logging + +- Use `_debug2` (not `_debug3` or other levels) unless there is a specific reason for a different level. + +--- + +## Things to Avoid in Reviews + +- Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or `shfmt`. +- Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable. From d66264e741e9dba207a5a9158580dacb340dc14e Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Mon, 16 Mar 2026 13:17:18 +0100 Subject: [PATCH 419/689] Add dns_czechia DNS API plugin (#6764) * Create dns_czechia.sh This PR adds a DNS API plugin for CZECHIA.COM / RegZone (ZONER a.s.). --- dnsapi/dns_czechia.sh | 201 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 dnsapi/dns_czechia.sh diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh new file mode 100644 index 00000000..f0f4c32e --- /dev/null +++ b/dnsapi/dns_czechia.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env sh + +# dns_czechia.sh - CZECHIA.COM/ZONER DNS API for acme.sh (DNS-01) +# +# Documentation: https://api.czechia.com/swagger/index.html + +#shellcheck disable=SC2034 +dns_czechia_info='[ + {"name":"CZ_AuthorizationToken","usage":"Your API token from CZECHIA.COM/Zoner administration.","required":"1"}, + {"name":"CZ_Zones","usage":"Managed zones separated by comma or space (e.g. \"example.com\").","required":"1"}, + {"name":"CZ_API_BASE","usage":"Defaults to https://api.czechia.com","required":"0"} +]' + +dns_czechia_add() { + fulldomain="$1" + txtvalue="$2" + + _debug "dns_czechia_add fulldomain='$fulldomain'" + + if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then + _err "dns_czechia_add: missing fulldomain or txtvalue" + return 1 + fi + + _czechia_load_conf || return 1 + + _current_zone=$(_czechia_pick_zone "$fulldomain") + if [ -z "$_current_zone" ]; then + _err "No matching zone found for $fulldomain. Please check CZ_Zones." + return 1 + fi + + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + + if [ -z "$_cz" ] || [ -z "$_tk" ]; then + _err "Missing zone or CZ_AuthorizationToken." + return 1 + fi + + _url="$CZ_API_BASE/api/DNS/$_cz/TXT" + _fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//') + + if [ "$_fd" = "$_cz" ]; then + _h="@" + else + # Remove the literal "." suffix from _fd, if present + _h=${_fd%."$_cz"} + [ "$_h" = "$_fd" ] && _h="@" + fi + [ -z "$_h" ] && _h="@" + + _info "Adding TXT record for $_h in zone $_cz" + + _h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g') + _txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g') + _body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}" + + _debug "URL: $_url" + _debug "Body: $_body" + + export _H1="Content-Type: application/json" + export _H2="AuthorizationToken: $_tk" + + _res="$(_post "$_body" "$_url" "" "POST")" + _post_exit="$?" + _debug2 "Response: $_res" + + if [ "$_post_exit" -ne 0 ]; then + _err "API request failed. exit code $_post_exit" + return 1 + fi + + if _contains "$_res" "already exists"; then + _info "Record already exists, skipping." + return 0 + fi + + _nres="$(_normalizeJson "$_res")" + if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then + _nres="$_res" + fi + + if _contains "$_nres" "\"status\":4" || _contains "$_nres" "\"status\":5" || _contains "$_nres" "\"errors\""; then + _err "API error: $_res" + return 1 + fi + + return 0 +} + +dns_czechia_rm() { + fulldomain="$1" + txtvalue="$2" + + _debug "dns_czechia_rm fulldomain='$fulldomain'" + + if [ -z "$fulldomain" ] || [ -z "$txtvalue" ]; then + _err "dns_czechia_rm: missing fulldomain or txtvalue" + return 1 + fi + + _czechia_load_conf || return 1 + + _current_zone=$(_czechia_pick_zone "$fulldomain") + if [ -z "$_current_zone" ]; then + _err "No matching zone found for $fulldomain. Please check CZ_Zones configuration." + return 1 + fi + + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + + if [ -z "$_cz" ] || [ -z "$_tk" ]; then + _err "Missing zone or CZ_AuthorizationToken." + return 1 + fi + + _url="$CZ_API_BASE/api/DNS/$_cz/TXT" + _fd=$(printf "%s" "$fulldomain" | _lower_case | sed 's/\.$//') + + if [ "$_fd" = "$_cz" ]; then + _h="@" + else + _h=$(printf "%s" "$_fd" | sed "s/\.$_cz$//") + [ "$_h" = "$_fd" ] && _h="@" + fi + [ -z "$_h" ] && _h="@" + + _h_esc=$(printf "%s" "$_h" | sed 's/\\/\\\\/g; s/"/\\"/g') + _txt_esc=$(printf "%s" "$txtvalue" | sed 's/\\/\\\\/g; s/"/\\"/g') + _body="{\"hostName\":\"$_h_esc\",\"text\":\"$_txt_esc\",\"ttl\":300,\"publishZone\":1}" + + _debug "URL: $_url" + _debug "Body: $_body" + + export _H1="Content-Type: application/json" + export _H2="AuthorizationToken: $_tk" + + _res="$(_post "$_body" "$_url" "" "DELETE")" + _post_exit="$?" + _debug2 "Response: $_res" + + if [ "$_post_exit" -ne 0 ]; then + _err "CZECHIA DNS API DELETE request failed for $_fd: exit code $_post_exit, response: $_res" + return 1 + fi + + _res_normalized=$(printf '%s' "$_res" | _normalizeJson) + + if _contains "$_res_normalized" '"isError":true'; then + _err "CZECHIA DNS API reported an error while deleting TXT for $_fd: $_res" + return 1 + fi + + return 0 +} + +_czechia_load_conf() { + CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}" + if [ -z "$CZ_AuthorizationToken" ]; then + _err "Missing CZ_AuthorizationToken" + return 1 + fi + + CZ_Zones="${CZ_Zones:-$(_readaccountconf_mutable CZ_Zones)}" + if [ -z "$CZ_Zones" ]; then + _err "Missing CZ_Zones" + return 1 + fi + + CZ_API_BASE="${CZ_API_BASE:-$(_readaccountconf_mutable CZ_API_BASE)}" + [ -z "$CZ_API_BASE" ] && CZ_API_BASE="https://api.czechia.com" + + _saveaccountconf_mutable CZ_AuthorizationToken "$CZ_AuthorizationToken" + _saveaccountconf_mutable CZ_Zones "$CZ_Zones" + _saveaccountconf_mutable CZ_API_BASE "$CZ_API_BASE" + + return 0 +} + +_czechia_pick_zone() { + _fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//') + _best_zone="" + + _zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g') + for _z in $_zones_space; do + _clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + [ -z "$_clean_z" ] && continue + + case "$_fd" in + "$_clean_z" | *."$_clean_z") + if [ ${#_clean_z} -gt ${#_best_zone} ]; then + _best_zone="$_clean_z" + fi + ;; + esac + done + + printf "%s" "$_best_zone" +} From d050f3458badd936d8be192665fafca53d548b30 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 10:40:10 +0800 Subject: [PATCH 420/689] don't switch from test back to production ca --- acme.sh | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/acme.sh b/acme.sh index d6804648..75bf7047 100755 --- a/acme.sh +++ b/acme.sh @@ -5555,16 +5555,17 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" - case "$Le_API" in - "$CA_LETSENCRYPT_V2_TEST") - _info "Switching back to $CA_LETSENCRYPT_V2" - Le_API="$CA_LETSENCRYPT_V2" - ;; - "$CA_GOOGLE_TEST") - _info "Switching back to $CA_GOOGLE" - Le_API="$CA_GOOGLE" - ;; - esac +#don't switch it back +# case "$Le_API" in +# "$CA_LETSENCRYPT_V2_TEST") +# _info "Switching back to $CA_LETSENCRYPT_V2" +# Le_API="$CA_LETSENCRYPT_V2" +# ;; +# "$CA_GOOGLE_TEST") +# _info "Switching back to $CA_GOOGLE" +# Le_API="$CA_GOOGLE" +# ;; +# esac if [ "$_server" ]; then Le_API="$_server" From 605299947ec4f8f7342664e464d9d825b5f2bca5 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 10:41:58 +0800 Subject: [PATCH 421/689] format --- acme.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/acme.sh b/acme.sh index 75bf7047..184f15cc 100755 --- a/acme.sh +++ b/acme.sh @@ -5555,17 +5555,17 @@ renew() { . "$DOMAIN_CONF" _debug Le_API "$Le_API" -#don't switch it back -# case "$Le_API" in -# "$CA_LETSENCRYPT_V2_TEST") -# _info "Switching back to $CA_LETSENCRYPT_V2" -# Le_API="$CA_LETSENCRYPT_V2" -# ;; -# "$CA_GOOGLE_TEST") -# _info "Switching back to $CA_GOOGLE" -# Le_API="$CA_GOOGLE" -# ;; -# esac + #don't switch it back + # case "$Le_API" in + # "$CA_LETSENCRYPT_V2_TEST") + # _info "Switching back to $CA_LETSENCRYPT_V2" + # Le_API="$CA_LETSENCRYPT_V2" + # ;; + # "$CA_GOOGLE_TEST") + # _info "Switching back to $CA_GOOGLE" + # Le_API="$CA_GOOGLE" + # ;; + # esac if [ "$_server" ]; then Le_API="$_server" From 13d64966537e34f358eda6b06028b13fc150a757 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 10:47:11 +0800 Subject: [PATCH 422/689] fix https://github.com/acmesh-official/acme.sh/issues/6866#issuecomment-4080403721 --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 184f15cc..db0eb92e 100755 --- a/acme.sh +++ b/acme.sh @@ -5285,7 +5285,7 @@ $_authorizations_map" _info "Order status is 'ready', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ]; then + if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then _info "Sleeping for $_retryafter seconds then retrying" _sleep $_retryafter else @@ -5295,7 +5295,7 @@ $_authorizations_map" _info "Order status is 'processing', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') _debug "_retryafter" "$_retryafter" - if [ "$_retryafter" ]; then + if [ "$_retryafter" ] && [ $_retryafter -gt 0 ]; then _info "Sleeping for $_retryafter seconds then retrying" _sleep $_retryafter else From 3b503a009c165db1e2591d61b4227335de76d68f Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 21 Mar 2026 13:02:16 +0800 Subject: [PATCH 423/689] fix https://github.com/acmesh-official/acme.sh/issues/4924#issuecomment-4069887654 --- acme.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/acme.sh b/acme.sh index db0eb92e..b57228d3 100755 --- a/acme.sh +++ b/acme.sh @@ -5765,6 +5765,9 @@ ${_skipped_msg} fi fi + if [ "$_TREAT_SKIP_AS_SUCCESS" ] && [ "$_ret" = "$RENEW_SKIP" ]; then + _ret=0 + fi return "$_ret" } @@ -6983,6 +6986,7 @@ cron() { _info "Automatically upgraded to: $VER" fi + _TREAT_SKIP_AS_SUCCESS="1" renewAll _ret="$?" _ACME_IN_CRON="" @@ -7230,6 +7234,7 @@ Parameters: --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. --listraw Only used for '--list' command, list the certs in raw format. -se, --stop-renew-on-error Only valid for '--renew-all' command. Stop if one cert has error in renewal. + --treat-skip-as-success Only valid for '--renew-all' command. Treat skipped certs as success, return 0 instead of $RENEW_SKIP. --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. --ca-bundle Specifies the path to the CA certificate bundle to verify api server's certificate. --ca-path Specifies directory containing CA certificates in PEM format, used by wget or curl. @@ -7710,6 +7715,9 @@ _process() { -f | --force) FORCE="1" ;; + --treat-skip-as-success | --treatskipassuccess) + _TREAT_SKIP_AS_SUCCESS="1" + ;; --staging | --test) STAGE="1" ;; From 9aad08ef14785fcca54d3b0687b3f0c9b9dc6f89 Mon Sep 17 00:00:00 2001 From: heximcz Date: Sun, 22 Mar 2026 04:36:29 +0100 Subject: [PATCH 424/689] Add BEST-HOSTING DNS API (#6859) * Add BEST-HOSTING DNS API --- dnsapi/dns_bh.sh | 202 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100755 dnsapi/dns_bh.sh diff --git a/dnsapi/dns_bh.sh b/dnsapi/dns_bh.sh new file mode 100755 index 00000000..fbb69ef2 --- /dev/null +++ b/dnsapi/dns_bh.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_bh_info='Best-Hosting.cz +Site: best-hosting.cz +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bh +Options: + BH_API_USER API User identifier. + BH_API_KEY API Secret key. +Issues: github.com/acmesh-official/acme.sh/issues/6854 +Author: @heximcz +' + +BH_Api="https://best-hosting.cz/api/v1" + +######## Public functions ##################### + +# Usage: dns_bh_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_bh_add() { + fulldomain=$1 + txtvalue=$2 + + # --- 1. Credentials --- + BH_API_USER="${BH_API_USER:-$(_readaccountconf_mutable BH_API_USER)}" + BH_API_KEY="${BH_API_KEY:-$(_readaccountconf_mutable BH_API_KEY)}" + + if [ -z "$BH_API_USER" ] || [ -z "$BH_API_KEY" ]; then + BH_API_USER="" + BH_API_KEY="" + _err "You must specify BH_API_USER and BH_API_KEY." + return 1 + fi + + _saveaccountconf_mutable BH_API_USER "$BH_API_USER" + _saveaccountconf_mutable BH_API_KEY "$BH_API_KEY" + + # --- 2. Add TXT record --- + _info "Adding TXT record for $fulldomain" + + json_payload="{\"fulldomain\":\"$fulldomain\",\"txtvalue\":\"$txtvalue\"}" + if ! _bh_rest POST "dns" "$json_payload"; then + _err "Failed to add DNS record." + return 1 + fi + + _norm_add=$(printf "%s" "$response" | tr -d '[:space:]') + if ! _contains "$_norm_add" '"status":"success"'; then + _err "API error: $response" + return 1 + fi + + record_id=$(printf "%s" "$_norm_add" | _egrep_o '"id":[0-9]+' | cut -d':' -f2) + _debug record_id "$record_id" + + if [ -z "$record_id" ]; then + _err "Could not parse record ID from response." + return 1 + fi + + # Sanitize key — replace dots and hyphens with underscores + _conf_key=$(printf "%s" "BH_record_ids_${fulldomain}" | tr '.-' '_') + + # Wildcard support: store space-separated list of IDs + # First call stores "111", second call stores "111 222" + _existing_ids=$(_readdomainconf "$_conf_key") + if [ -z "$_existing_ids" ]; then + _savedomainconf "$_conf_key" "$record_id" + else + _savedomainconf "$_conf_key" "$_existing_ids $record_id" + fi + + _info "DNS TXT record added successfully." + return 0 +} + +# Usage: dns_bh_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_bh_rm() { + fulldomain=$1 + txtvalue=$2 + + # --- 1. Credentials --- + BH_API_USER="${BH_API_USER:-$(_readaccountconf_mutable BH_API_USER)}" + BH_API_KEY="${BH_API_KEY:-$(_readaccountconf_mutable BH_API_KEY)}" + + if [ -z "$BH_API_USER" ] || [ -z "$BH_API_KEY" ]; then + BH_API_USER="" + BH_API_KEY="" + _err "You must specify BH_API_USER and BH_API_KEY." + return 1 + fi + + # Sanitize key — same as in add + _conf_key=$(printf "%s" "BH_record_ids_${fulldomain}" | tr '.-' '_') + + # --- 2. Load stored record ID(s) --- + _existing_ids=$(_readdomainconf "$_conf_key") + _debug _existing_ids "$_existing_ids" + + if [ -z "$_existing_ids" ]; then + _err "Could not find record ID for $fulldomain." + return 1 + fi + + record_id="" + _remaining_ids="" + + # Find the record ID that matches both the name and txtvalue + for _id in $_existing_ids; do + if ! _bh_rest GET "dns/$_id"; then + _debug "Failed to query record id $_id, skipping." + + # Keep it in the list so a later run can try again + if [ -z "$_remaining_ids" ]; then + _remaining_ids="$_id" + else + _remaining_ids="$_remaining_ids $_id" + fi + continue + fi + + _match_name=0 + _match_content=0 + _norm_response=$(printf "%s" "$response" | tr -d '[:space:]') + + case "$_norm_response" in + *"\"name\":\"$fulldomain\""*) + _match_name=1 + ;; + esac + case "$_norm_response" in + *"\"content\":\"$txtvalue\""*) + _match_content=1 + ;; + esac + + if [ "$_match_name" -eq 1 ] && [ "$_match_content" -eq 1 ]; then + record_id="$_id" + _debug "Matched record id" "$record_id" + # Do not add this ID to _remaining_ids; it will be deleted + continue + fi + + # Not a match — keep ID for potential future cleanups + if [ -z "$_remaining_ids" ]; then + _remaining_ids="$_id" + else + _remaining_ids="$_remaining_ids $_id" + fi + done + + if [ -z "$record_id" ]; then + _err "Could not find matching TXT record for $fulldomain with the given value." + return 1 + fi + + # --- 3. Delete record --- + _info "Removing TXT record for $fulldomain" + + if ! _bh_rest DELETE "dns/$record_id"; then + _err "Failed to remove DNS record." + return 1 + fi + + # Update stored list — remove used ID + if [ -z "$_remaining_ids" ]; then + _cleardomainconf "$_conf_key" + else + _savedomainconf "$_conf_key" "$_remaining_ids" + fi + + _info "DNS TXT record removed successfully." + return 0 +} + +#################### Private functions ##################### + +_bh_rest() { + m="$1" + ep="$2" + data="$3" + _debug "$ep" + + _credentials="$(printf "%s:%s" "$BH_API_USER" "$BH_API_KEY" | _base64)" + + export _H1="Authorization: Basic $_credentials" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + if [ "$m" = "GET" ]; then + response="$(_get "$BH_Api/$ep")" + else + _debug data "$data" + response="$(_post "$data" "$BH_Api/$ep" "" "$m")" + fi + + if [ "$?" != "0" ]; then + _err "Error calling $m $BH_Api/$ep" + return 1 + fi + + _debug2 response "$response" + return 0 +} From 4cb1c6e1eaebad6bfb042ee22c538734fce528c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pavli=C4=8D?= Date: Sun, 22 Mar 2026 04:40:44 +0100 Subject: [PATCH 425/689] [dnsapi] add subreg.cz dns hook (#6848) * Add DNS hook for subreg.cz --- dnsapi/dns_subreg.sh | 220 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 dnsapi/dns_subreg.sh diff --git a/dnsapi/dns_subreg.sh b/dnsapi/dns_subreg.sh new file mode 100644 index 00000000..5e7e7ced --- /dev/null +++ b/dnsapi/dns_subreg.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_subreg_info='Subreg.cz +Site: subreg.cz +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_subreg +Options: + SUBREG_API_USERNAME API username + SUBREG_API_PASSWORD API password +Issues: github.com/acmesh-official/acme.sh/issues/6835 +Author: Tomas Pavlic +' + +# Subreg SOAP API +# https://subreg.cz/manual/ + +SUBREG_API_URL="https://soap.subreg.cz/cmd.php" + +######## Public functions ##################### + +# Usage: dns_subreg_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_subreg_add() { + fulldomain=$1 + txtvalue=$2 + + SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}" + SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}" + if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then + _err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set." + return 1 + fi + + _saveaccountconf_mutable SUBREG_API_USERNAME "$SUBREG_API_USERNAME" + _saveaccountconf_mutable SUBREG_API_PASSWORD "$SUBREG_API_PASSWORD" + + if ! _subreg_login; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Cannot determine root domain for: $fulldomain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _subreg_soap "Add_DNS_Record" "$_domain$_sub_domainTXT$txtvalue0120" + if _subreg_ok; then + _record_id="$(_subreg_map_get record_id)" + + if [ -z "$_record_id" ]; then + _err "Subreg API did not return a record_id for TXT record on $fulldomain" + _err "$response" + return 1 + fi + + _savedomainconf "$(_subreg_record_id_key "$txtvalue")" "$_record_id" + return 0 + fi + _err "Failed to add TXT record." + _err "$response" + return 1 +} + +# Usage: dns_subreg_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_subreg_rm() { + fulldomain=$1 + txtvalue=$2 + + SUBREG_API_USERNAME="${SUBREG_API_USERNAME:-$(_readaccountconf_mutable SUBREG_API_USERNAME)}" + SUBREG_API_PASSWORD="${SUBREG_API_PASSWORD:-$(_readaccountconf_mutable SUBREG_API_PASSWORD)}" + if [ -z "$SUBREG_API_USERNAME" ] || [ -z "$SUBREG_API_PASSWORD" ]; then + _err "SUBREG_API_USERNAME and SUBREG_API_PASSWORD are not set." + return 1 + fi + + if ! _subreg_login; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Cannot determine root domain for: $fulldomain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _record_id="$(_readdomainconf "$(_subreg_record_id_key "$txtvalue")")" + if [ -z "$_record_id" ]; then + _err "Could not find saved record ID for $fulldomain" + return 1 + fi + + _debug "Deleting record ID: $_record_id" + _subreg_soap "Delete_DNS_Record" "$_domain$_record_id" + if _subreg_ok; then + + _cleardomainconf "$(_subreg_record_id_key "$txtvalue")" + return 0 + fi + + _err "Failed to delete TXT record." + _err "$response" + return 1 +} + +#################### Private functions ##################### + +# Build a domain-conf key for storing the record ID of a given TXT value. +# Base64url chars include '-' which is invalid in shell variable names, so replace with '_'. +_subreg_record_id_key() { + printf 'SUBREG_RECORD_ID_%s' "$(printf '%s' "$1" | tr '-' '_')" +} + +# Check if the current $response contains a successful status in the ns2:Map format: +# statusok +_subreg_ok() { + [ "$(_subreg_map_get status)" = "ok" ] +} + +# Extract the value for a given key from the ns2:Map response. +# Usage: _subreg_map_get keyname +# Reads from $response +_subreg_map_get() { + _key="$1" + echo "$response" | tr -d '\n\r' | _egrep_o ">${_key}]*>[^<]*" | sed 's/.*]*>//;s/<\/value>//' +} + +# Login and store session token in _subreg_ssid +_subreg_login() { + _debug "Logging in to Subreg API as $SUBREG_API_USERNAME" + _subreg_soap_noauth "Login" "$SUBREG_API_USERNAME$SUBREG_API_PASSWORD" + if ! _subreg_ok; then + _err "Subreg login failed." + _err "$response" + return 1 + fi + _subreg_ssid="$(_subreg_map_get ssid)" + if [ -z "$_subreg_ssid" ]; then + _err "Subreg login: could not extract session token (ssid)." + return 1 + fi + _debug "Subreg login: session token (ssid) obtained" + return 0 +} + +# _get_root _acme-challenge.www.domain.com +# returns _sub_domain and _domain +_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + _err "Unable to retrieve DNS zone matching domain: $domain" + return 1 + fi + + _subreg_soap "Get_DNS_Zone" "$h" + + if _subreg_ok; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="$h" + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done +} + +# Send a SOAP request without authentication (used for Login) +# _subreg_soap_noauth command inner_xml +_subreg_build_soap() { + _cmd="$1" + _data_inner="$2" + + _soap_body=" + + + + + ${_data_inner} + + + +" + + export _H1="Content-Type: text/xml" + export _H2="SOAPAction: http://soap.subreg.cz/soap#${_cmd}" + response="$(_post "$_soap_body" "$SUBREG_API_URL" "" "POST" "text/xml")" +} + +# Send an authenticated SOAP request (requires _subreg_ssid to be set) +# _subreg_soap command inner_xml +_subreg_soap_noauth() { + _cmd="$1" + _inner="$2" + + _subreg_build_soap "$_cmd" "$_inner" +} + +# Send an authenticated SOAP request (requires _subreg_ssid to be set) +# _subreg_soap command inner_xml +_subreg_soap() { + _cmd="$1" + _inner="$2" + _inner_with_ssid="${_subreg_ssid}${_inner}" + + _subreg_build_soap "$_cmd" "$_inner_with_ssid" +} From 12f639116ce83a78e6eca18a3c9de1bc4acb3a79 Mon Sep 17 00:00:00 2001 From: orangepizza Date: Sat, 28 Mar 2026 10:26:47 +0900 Subject: [PATCH 426/689] Update copilot instruction to match actual PR rule (#6873) old version had instruction to use bash-only [[ ]] test, remove it and add rules for DNS script writing from https://github.com/acmesh-official/acme.sh/issues/343 --- .github/copilot-instructions.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d407607a..af0e8147 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -39,7 +39,6 @@ Please adhere to the previous format: organize the feedback into a single, struc * **Function Usage:** Recommend wrapping complex or reusable logic within clearly named functions. * **Local Variables:** Check that variables inside functions are declared using the `local` keyword to avoid unintentionally modifying global state. * **Naming Convention:** Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. -* **Test Conditions:** Encourage the use of Bash's **double brackets `[[ ... ]]`** for conditional tests, as it is generally safer and more powerful (e.g., supports pattern matching and avoids Word Splitting) than single brackets `[ ... ]`. * **Command Substitution:** Encourage using `$(command)` over backticks `` `command` `` for command substitution, as it is easier to nest and improves readability. ### 4. External Commands and Environment @@ -48,13 +47,14 @@ Please adhere to the previous format: organize the feedback into a single, struc * **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. * **Do not use `head -n`.** Use the `_head_n()` function instead. * **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. +* **keep it sh compatible, do not use bash-only syntax.** We need to cross platforms between Linux/BSD/Mac. --- ### 5. Review Rules for Files Under `dnsapi/`: * **Each file must contain a `{filename}_add` function** for adding DNS TXT records. It should use `_readaccountconf_mutable` to read the API key and `_saveaccountconf_mutable` to save it. Do not use `_saveaccountconf` or `_readaccountconf`. - +* **keep it shell only** Do not add more dependencies. common tools, such as grep or sed etc are ok to use. do not depend on python or perl etc. ## ❌ Things to Avoid @@ -64,4 +64,3 @@ Please adhere to the previous format: organize the feedback into a single, struc - From 4aeb7bbab0a66a0e3dd62f26424050b4f88b1bb4 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Apr 2026 11:33:05 +0800 Subject: [PATCH 427/689] fix rule --- .github/copilot-instructions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index af0e8147..88a45fd7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,6 +47,9 @@ Please adhere to the previous format: organize the feedback into a single, struc * **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. * **Do not use `head -n`.** Use the `_head_n()` function instead. * **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. +* **Do not use `awk`.** Use the `cut` and `sed` instead. +* **Do not use `[:space:]` or `[:punct:]`.** +* **Do not use `grep -E` or `grep -O`, .** Use the `_egrep_o` function instead. * **keep it sh compatible, do not use bash-only syntax.** We need to cross platforms between Linux/BSD/Mac. --- From c2c5c3cdb70eb6058702a1a1b27dfd98a2d13884 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Apr 2026 21:16:40 +0800 Subject: [PATCH 428/689] Update copilot-instructions.md --- .github/copilot-instructions.md | 210 ++++++++++++++++++++++++-------- 1 file changed, 161 insertions(+), 49 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 88a45fd7..cd21b65a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,69 +1,181 @@ -# GitHub Copilot Shell Scripting (sh) Review Instructions +# GitHub Copilot Shell Scripting (sh) Review Instructions for acme.sh -## 🎯 Overall Goal +## Overall Goal -Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files). Ensure the code exhibits the highest levels of robustness, security, and portability. +Your role is to act as a rigorous yet helpful senior engineer, reviewing Shell script code (`.sh` files) for the [acme.sh](https://github.com/acmesh-official/acme.sh) project. Ensure the code exhibits the highest levels of robustness, security, and portability. The review must focus on risks unique to Shell scripting, such as proper quoting, robust error handling, and the secure execution of external commands. -## 📝 Required Output Format +## Required Output Format -Please adhere to the previous format: organize the feedback into a single, structured report, using the three-level marking system: +Organize the feedback into a single, structured report, using the three-level marking system: -1. **🔴 Critical Issues (Must Fix Before Merge)** -2. **🟡 Suggestions (Improvements to Consider)** -3. **✅ Good Practices (Points to Commend)** +1. **Critical Issues (Must Fix Before Merge)** +2. **Suggestions (Improvements to Consider)** +3. **Good Practices (Points to Commend)** --- -## 🔍 Focus Areas and Rules for Shell +## Shell Compatibility -### 1. Robustness and Error Handling - -* **Shebang:** Check that the script starts with the correct Shebang, must be "#!/usr/bin/env sh". -* **Startup Options:** **(🔴 Critical)** Enforce the use of the following combination at the start of the script for safety and robustness: - * `set -e`: Exit immediately if a command exits with a non-zero status. - * `set -u`: Treat unset variables as an error and exit. - * `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails. -* **Exit Codes:** Ensure functions and the main script use `exit 0` for success and a non-zero exit code upon failure. -* **Temporary Files:** Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks. - -### 2. Security and Quoting - -* **Variable Quoting:** **(🔴 Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**. -* **Hardcoded Secrets:** **(🔴 Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details. -* **Untrusted Input:** Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use. -* **Avoid `eval`:** Warn against and suggest alternatives to using `eval`, as it can lead to arbitrary code execution. - -### 3. Readability and Maintainability - -* **Function Usage:** Recommend wrapping complex or reusable logic within clearly named functions. -* **Local Variables:** Check that variables inside functions are declared using the `local` keyword to avoid unintentionally modifying global state. -* **Naming Convention:** Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. -* **Command Substitution:** Encourage using `$(command)` over backticks `` `command` `` for command substitution, as it is easier to nest and improves readability. - -### 4. External Commands and Environment - -* **`for` Loops:** Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces. -* **Use existing acme.sh functions whenever possible.** For example: do not use `tr '[:upper:]' '[:lower:]'`, use `_lower_case` instead. -* **Do not use `head -n`.** Use the `_head_n()` function instead. -* **Do not use `curl` or `wget`.** Use the `_post()` and `_get()` functions instead. -* **Do not use `awk`.** Use the `cut` and `sed` instead. -* **Do not use `[:space:]` or `[:punct:]`.** -* **Do not use `grep -E` or `grep -O`, .** Use the `_egrep_o` function instead. -* **keep it sh compatible, do not use bash-only syntax.** We need to cross platforms between Linux/BSD/Mac. +- **POSIX sh only** -- all scripts must target `sh`, not `bash`. No bash-isms allowed. +- **Shebang**: always use `#!/usr/bin/env sh` (not `#!/bin/sh`, not `#!/usr/bin/env bash`). +- **Use `return`, never `exit`** -- scripts are sourced, not executed as subprocesses. `exit` would kill the parent shell. +- **Cross-platform**: code must work on Linux, macOS, FreeBSD, Solaris, and BusyBox environments. --- -### 5. Review Rules for Files Under `dnsapi/`: +## Robustness and Error Handling -* **Each file must contain a `{filename}_add` function** for adding DNS TXT records. It should use `_readaccountconf_mutable` to read the API key and `_saveaccountconf_mutable` to save it. Do not use `_saveaccountconf` or `_readaccountconf`. -* **keep it shell only** Do not add more dependencies. common tools, such as grep or sed etc are ok to use. do not depend on python or perl etc. +- **(Critical)** Enforce the use of the following combination at the start of the script for safety and robustness: + - `set -e`: Exit immediately if a command exits with a non-zero status. + - `set -u`: Treat unset variables as an error and exit. + - `set -o pipefail`: Ensure the whole pipeline fails if any command in the pipe fails. +- **Always check return values** of function calls. If an error occurs, there must be a way to stop execution. +- **Return 1** after `_err` messages: + ```sh + if [ -z "$VARIABLE" ]; then + _err "VARIABLE is required" + return 1 + fi + ``` +- Check for the use of `mktemp` when creating temporary files to prevent race conditions and security risks. -## ❌ Things to Avoid +--- -* Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or Prettier. -* Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable. +## Security and Quoting +- **(Critical)** Check that all variable expansions (like `$VAR` and `$(COMMAND)`) are properly enclosed in **double quotes** (i.e., `"$VAR"` and `"$(COMMAND)"`) to prevent **Word Splitting** and **Globbing**. +- **(Critical)** Find and flag any hardcoded passwords, keys, tokens, or authentication details. +- Verify that all user input, command-line arguments (`$1`, `$2`, etc.), or environment variables are rigorously validated and sanitized before use. +- Avoid `eval` -- warn against and suggest alternatives, as it can lead to arbitrary code execution. +--- +## Use Built-in Helper Functions +Never use raw shell commands when acme.sh provides a wrapper function. This is the most critical rule for portability. + +| Instead of | Use | +|---|---| +| `tr '[:upper:]' '[:lower:]'` | `_lower_case()` | +| `head -n 1` | `_head_n 1` | +| `openssl dgst` / `openssl` | `_digest()` / `_hmac()` | +| `date` | `_utc_date()` with `sed`/`tr` | +| `curl` / `wget` | `_get()` or `_post()` | +| `sleep` | `_sleep` | +| `base64` / `openssl base64` | `_base64()` | +| `$(( ))` arithmetic | `_math()` | +| `grep -E` / `grep -Po` | `_egrep_o()` | +| `printf` | `echo` | +| `idn` command | `_idn()` / `_is_idn()` | + +When fixing a pattern issue, fix **all instances** in the file, not just the one highlighted. + +--- + +## Forbidden External Tools + +Do not use these commands -- they are not portable across all target platforms: + +- `jq` (parse JSON with built-in string manipulation) +- `grep -A` (removed throughout the project) +- `grep -Po` (Perl regex not available everywhere) +- `rev`, `xargs`, `iconv` +- If you must depend on an external tool, check with `_exists` first: + ```sh + if ! _exists jq; then + _err "jq is required" + return 1 + fi + ``` +- Warn against patterns like `for i in $(cat file)` or `for i in $(ls)` and recommend the more robust `while IFS= read -r line` pattern for safely processing file contents or filenames that might contain spaces. + +--- + +## Configuration Management + +Use the correct save/read functions depending on hook type: + +- **DNS hooks**: `_readaccountconf_mutable` to read API keys, `_saveaccountconf_mutable` to save them. Do not use `_saveaccountconf` or `_readaccountconf`. +- **Deploy hooks**: `_savedeployconf` / `_getdeployconf` +- **Notification hooks**: use account conf functions. +- Save operations should only happen in the correct lifecycle function (e.g., `_issue()`). +- Use environment variables for all configurable values -- do not introduce hardcoded config files. +- Do not clear account conf without a clear reason. + +--- + +## DNS API Conventions + +- Read the [DNS API Dev Guide](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide) before writing a DNS plugin. +- Each file under `dnsapi/` must contain a `{filename}_add` function for adding DNS TXT records. +- The `_get_root()` loop counter `i` must start from `1` (not `2`) to support DNS alias mode. +- The `dns_*_rm()` function must remove records **by TXT value**, not by replacing/updating. See [#1261](https://github.com/acmesh-official/acme.sh/issues/1261). +- Preserve the `dns_*_info` metadata variable block in each DNS script header. + +--- + +## Variable Naming + +- Use CamelCase with provider prefix: `KINGHOST_Username` (not `KINGHOST_username`). +- Variable names should use uppercase letters and underscores (e.g., `MY_VARIABLE`), or follow established project conventions. +- Avoid confusingly similar names. Prefer one variable with comma-separated values over multiple variables (e.g., `CZ_Zones` with comma support instead of separate `CZ_Zone` and `CZ_Zones`). +- Do not define variables with the same name in different scopes. +- Variables inside functions should be declared using the `local` keyword to avoid unintentionally modifying global state. + +--- + +## Code Style + +- Use `shfmt` for formatting -- CI enforces it. +- Reduce indentation where possible. +- Single space, not double spaces. +- No trailing semicolons after `return` statements. +- Add a newline at the end of every file. +- Use `$(command)` over backticks `` `command` `` for command substitution. + +--- + +## Simplicity + +- Prefer hardcoded sensible defaults over unnecessary configuration variables (e.g., use `3600` for TTL instead of a `DESEC_TTL` variable). +- Reject over-engineered solutions. If it can be done in one line, do it in one line. +- Follow existing patterns in the codebase -- new hooks should look like existing hooks. +- Respect user choices: do not `chmod` files that already exist; the user's permissions take priority. + +--- + +## Documentation Requirements + +Before a PR can be merged, the following documentation must be provided: + +- **Wiki page**: add or update the relevant page: + - DNS APIs: [dnsapi](https://github.com/acmesh-official/acme.sh/wiki/dnsapi) or [dnsapi2](https://github.com/acmesh-official/acme.sh/wiki/dnsapi2) + - Deploy hooks: [deployhooks](https://github.com/acmesh-official/acme.sh/wiki/deployhooks) + - Notification hooks: [notify](https://github.com/acmesh-official/acme.sh/wiki/notify) + - Options: [Options-and-Params](https://github.com/acmesh-official/acme.sh/wiki/Options-and-Params) +- **In-code usage**: add usage examples in the help text of `acme.sh` itself. +- **README**: add website URLs for new DNS providers. + +--- + +## CI and Merge Hygiene + +- All CI checks must pass before merge. +- Rebase to the latest `dev` branch frequently -- do not use merge commits. +- Enable GitHub Actions on your fork to catch errors early. +- Run the [DNS API Test](https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Test) workflow for DNS plugins. +- For Docker changes, ensure the Dockerfile includes any required dependencies. + +--- + +## Debug Logging + +- Use `_debug2` (not `_debug3` or other levels) unless there is a specific reason for a different level. + +--- + +## Things to Avoid in Reviews + +- Do not comment on purely stylistic issues like spacing or indentation, which should be handled by tools like ShellCheck or `shfmt`. +- Do not be overly verbose unless a significant issue is found. Keep feedback concise and actionable. From 346acc3f33fde361b12d078c420076d54e4c222b Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Apr 2026 10:40:35 +0800 Subject: [PATCH 429/689] Update copilot-instructions.md --- .github/copilot-instructions.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cd21b65a..0a121fd7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,6 +58,7 @@ Never use raw shell commands when acme.sh provides a wrapper function. This is t | Instead of | Use | |---|---| | `tr '[:upper:]' '[:lower:]'` | `_lower_case()` | +| `tr '[:lower:]' '[:upper:]'` | `_upper_case()` | | `head -n 1` | `_head_n 1` | | `openssl dgst` / `openssl` | `_digest()` / `_hmac()` | | `date` | `_utc_date()` with `sed`/`tr` | @@ -68,6 +69,14 @@ Never use raw shell commands when acme.sh provides a wrapper function. This is t | `grep -E` / `grep -Po` | `_egrep_o()` | | `printf` | `echo` | | `idn` command | `_idn()` / `_is_idn()` | +| `mktemp` | `_mktemp()` | +| `[:space:]` | ` ` | +| `[:alnum:]` | `A-Za-z0-9` | +| `[:alpha:]` | `A-Za-z` | +| `[:digit:]` | `0-9` | +| `awk` | `cut` / `sed` / `while read` loops | + + When fixing a pattern issue, fix **all instances** in the file, not just the one highlighted. From 50dbdd781bcecfc3a6df1e74af22e9f84698418c Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Apr 2026 10:46:29 +0800 Subject: [PATCH 430/689] fix DEFAULT_RENEW fix https://github.com/acmesh-official/acme.sh/issues/2217#issuecomment-4155894630 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index b57228d3..0578e1ac 100755 --- a/acme.sh +++ b/acme.sh @@ -65,7 +65,7 @@ ID_TYPE_IP="ip" LOCAL_ANY_ADDRESS="0.0.0.0" -DEFAULT_RENEW=30 +DEFAULT_RENEW="${DEFAULT_RENEW:-30}" NO_VALUE="no" From 3509f6404fd28bc756288ece75a2027f9c6f2ddf Mon Sep 17 00:00:00 2001 From: Stefan Bottelier <109357022+0x53746566616E@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:59:09 +0200 Subject: [PATCH 431/689] Add bHosted.nl DNS API (#6864) Add bHosted.nl DNS API (#6864) --- dnsapi/dns_bhosted.sh | 373 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 dnsapi/dns_bhosted.sh diff --git a/dnsapi/dns_bhosted.sh b/dnsapi/dns_bhosted.sh new file mode 100644 index 00000000..1493c60a --- /dev/null +++ b/dnsapi/dns_bhosted.sh @@ -0,0 +1,373 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 +dns_bhosted_info='bHosted.nl DNS API +Site: bHosted.nl +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bhosted +Options: + BHOSTED_Username API username + BHOSTED_Password API password (MD5 hash like bHosted web services example) + BHOSTED_TTL TTL for TXT record (default: 300) + BHOSTED_SLD Optional override (useful for multi-part TLDs like co.uk) + BHOSTED_TLD Optional override (useful for multi-part TLDs like co.uk) +Notes: + - Plugin uses addrecord + delrecord for DNS-01 challenge + - Record ID is retrieved from addrecord XML response and cached for cleanup +' + +BHOSTED_API_ROOT="https://webservices.bhosted.com/dns" + +############ Public functions ##################### + +# Usage: dns_bhosted_add _acme-challenge.www.example.com "txt-value" +dns_bhosted_add() { + fulldomain="$1" + txtvalue="$2" + + _debug "fulldomain" "$fulldomain" + _debug "txtvalue" "$txtvalue" + + _bhosted_load_credentials || return 1 + _bhosted_get_root "$fulldomain" || return 1 + + _info "Adding TXT record: ${_bhosted_name}.${_domain}" + + BHOSTED_TTL="${BHOSTED_TTL:-$(_readaccountconf_mutable BHOSTED_TTL)}" + BHOSTED_TTL="${BHOSTED_TTL:-300}" + _saveaccountconf_mutable BHOSTED_TTL "$BHOSTED_TTL" + + _bhosted_api_add_txt "$_bhosted_sld" "$_bhosted_tld" "$_bhosted_name" "$txtvalue" "$BHOSTED_TTL" || return 1 + + # Extract and cache record id in-memory for cleanup in this run + _rec_id="$(_bhosted_extract_id "$response")" + if [ -n "$_rec_id" ]; then + _hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")" + _debug "_hash" "$_hash" + _debug "_rec_id" "$_rec_id" + _bhosted_mem_set_id "$_hash" "$_rec_id" + else + _err "TXT record added but no record id found in response." + _err "Cleanup may fail unless bHosted addrecord returns ...." + _debug2 "add response" "$response" + return 1 + fi + + return 0 +} + +# Usage: dns_bhosted_rm _acme-challenge.www.example.com "txt-value" +dns_bhosted_rm() { + fulldomain="$1" + txtvalue="$2" + + _debug "fulldomain" "$fulldomain" + _debug "txtvalue" "$txtvalue" + + _bhosted_load_credentials || return 1 + _bhosted_get_root "$fulldomain" || return 1 + + _hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")" + _rec_id="$(_bhosted_mem_get_id "$_hash")" + + if [ -z "$_rec_id" ]; then + _err "No cached bHosted record id found for cleanup." + _err "Please delete TXT manually in bHosted DNS for: ${_bhosted_name}.${_domain}" + return 1 + fi + + _info "Removing TXT record id=${_rec_id}: ${_bhosted_name}.${_domain}" + _bhosted_api_del_record "$_bhosted_sld" "$_bhosted_tld" "$_rec_id" || return 1 + + return 0 +} + +######## Private functions ##################### + +_bhosted_load_credentials() { + BHOSTED_Username="${BHOSTED_Username:-$(_readaccountconf_mutable BHOSTED_Username)}" + BHOSTED_Password="${BHOSTED_Password:-$(_readaccountconf_mutable BHOSTED_Password)}" + + if [ -z "$BHOSTED_Username" ] || [ -z "$BHOSTED_Password" ]; then + BHOSTED_Username="" + BHOSTED_Password="" + _err "You didn't specify bHosted credentials." + _err "Please export BHOSTED_Username and BHOSTED_Password (MD5 hash)." + return 1 + fi + + _saveaccountconf_mutable BHOSTED_Username "$BHOSTED_Username" + _saveaccountconf_mutable BHOSTED_Password "$BHOSTED_Password" + + return 0 +} + +# Determine root zone and host part +# Supports simple domains automatically (example.com, example.nl) +# For multi-part TLDs (example.co.uk), set: +# BHOSTED_SLD=example +# BHOSTED_TLD=co.uk +_bhosted_get_root() { + domain="$1" + + BHOSTED_SLD="${BHOSTED_SLD:-$(_readdomainconf BHOSTED_SLD)}" + BHOSTED_TLD="${BHOSTED_TLD:-$(_readdomainconf BHOSTED_TLD)}" + + if [ -n "$BHOSTED_SLD" ] && [ -n "$BHOSTED_TLD" ]; then + _savedomainconf BHOSTED_SLD "$BHOSTED_SLD" + _savedomainconf BHOSTED_TLD "$BHOSTED_TLD" + + _domain="${BHOSTED_SLD}.${BHOSTED_TLD}" + case "$domain" in + *."$_domain") ;; + "$_domain") ;; + *) + _err "BHOSTED_SLD/BHOSTED_TLD do not match requested domain: $domain" + return 1 + ;; + esac + + _bhosted_sld="$BHOSTED_SLD" + _bhosted_tld="$BHOSTED_TLD" + _bhosted_name="${domain%."$_domain"}" + if [ "$_bhosted_name" = "$domain" ]; then + _bhosted_name="" + fi + + [ -n "$_bhosted_name" ] || _bhosted_name="@" + + _debug "_domain" "$_domain" + _debug "_bhosted_sld" "$_bhosted_sld" + _debug "_bhosted_tld" "$_bhosted_tld" + _debug "_bhosted_name" "$_bhosted_name" + return 0 + fi + + # Auto-parse: assume last label = tld, label before = sld + # Works for .nl / .com / .org etc. + _bhosted_tld="$(printf "%s" "$domain" | awk -F. '{print $NF}')" + _bhosted_sld="$(printf "%s" "$domain" | awk -F. '{print $(NF-1)}')" + + if [ -z "$_bhosted_sld" ] || [ -z "$_bhosted_tld" ]; then + _err "Could not parse SLD/TLD from domain: $domain" + return 1 + fi + + _domain="${_bhosted_sld}.${_bhosted_tld}" + _bhosted_name="${domain%."$_domain"}" + if [ "$_bhosted_name" = "$domain" ]; then + _bhosted_name="" + fi + + [ -n "$_bhosted_name" ] || _bhosted_name="@" + + _debug "_domain" "$_domain" + _debug "_bhosted_sld" "$_bhosted_sld" + _debug "_bhosted_tld" "$_bhosted_tld" + _debug "_bhosted_name" "$_bhosted_name" + + return 0 +} + +_bhosted_api_add_txt() { + _sld="$1" + _tld="$2" + _name="$3" + _content="$4" + _ttl="$5" + + _u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)" + _u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)" + _u_sld="$(printf "%s" "$_sld" | _url_encode)" + _u_tld="$(printf "%s" "$_tld" | _url_encode)" + _u_name="$(printf "%s" "$_name" | _url_encode)" + _u_content="$(printf "%s" "$_content" | _url_encode)" + _u_ttl="$(printf "%s" "$_ttl" | _url_encode)" + + _data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&type=TXT&name=${_u_name}&content=${_u_content}&ttl=${_u_ttl}" + + _debug "bHosted add endpoint" "${BHOSTED_API_ROOT}/addrecord" + response="$(_post "$_data" "${BHOSTED_API_ROOT}/addrecord")" + _ret="$?" + + _debug2 "bHosted add response" "$response" + + if [ "$_ret" != "0" ]; then + _err "bHosted addrecord request failed" + return 1 + fi + + if _bhosted_response_has_error "$response"; then + _err "bHosted addrecord returned an error" + _debug2 "response" "$response" + return 1 + fi + + return 0 +} + +_bhosted_api_del_record() { + _sld="$1" + _tld="$2" + _id="$3" + + _u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)" + _u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)" + _u_sld="$(printf "%s" "$_sld" | _url_encode)" + _u_tld="$(printf "%s" "$_tld" | _url_encode)" + _u_id="$(printf "%s" "$_id" | _url_encode)" + + _url="${BHOSTED_API_ROOT}/delrecord" + _data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&id=${_u_id}" + + _debug "bHosted delete endpoint" "$_url" + response="$(_post "$_data" "$_url")" + _ret="$?" + + _debug2 "bHosted delete response" "$response" + + if [ "$_ret" != "0" ]; then + _err "bHosted delrecord request failed" + return 1 + fi + + if _bhosted_response_has_error "$response"; then + _err "bHosted delrecord returned an error" + _debug2 "response" "$response" + return 1 + fi + + return 0 +} + +# Extract XML tag value from response, e.g. 12345 +_bhosted_xml_value() { + _tag="$1" + _resp="$2" + + # Flatten response to simplify parsing + _flat="$(printf "%s" "$_resp" | tr -d '\r\n\t')" + printf "%s" "$_flat" | sed -n "s:.*<${_tag}>\\([^<]*\\).*:\\1:p" | _head_n 1 +} + +# Return code convention: +# return 0 => response HAS error +# return 1 => response has NO error (success) +_bhosted_response_has_error() { + _resp="$1" + + # Empty response = error + if [ -z "$_resp" ]; then + _debug "Empty API response" + return 0 + fi + + # Prefer explicit bHosted XML response fields + if _contains "$_resp" ""; then + _errors="$(_bhosted_xml_value "errors" "$_resp")" + _done="$(_bhosted_xml_value "done" "$_resp")" + _subcommand="$(_bhosted_xml_value "subcommand" "$_resp")" + _id="$(_bhosted_xml_value "id" "$_resp")" + + _debug "bHosted XML subcommand" "$_subcommand" + _debug "bHosted XML id" "$_id" + _debug "bHosted XML errors" "$_errors" + _debug "bHosted XML done" "$_done" + + # Success according to provided format + if [ "$_errors" = "0" ] && [ "$_done" = "true" ]; then + return 1 + fi + + _debug "bHosted XML indicates failure" + return 0 + fi + + # Fallback for unexpected/non-XML responses + _resp_lc="$(_lower_case "$_resp")" + + if _contains "$_resp_lc" "error"; then + _debug "Detected 'error' in response" + return 0 + fi + if _contains "$_resp_lc" "fout"; then + _debug "Detected 'fout' in response" + return 0 + fi + if _contains "$_resp_lc" "invalid"; then + _debug "Detected 'invalid' in response" + return 0 + fi + if _contains "$_resp_lc" "failed"; then + _debug "Detected 'failed' in response" + return 0 + fi + if _contains "$_resp_lc" "denied"; then + _debug "Detected 'denied' in response" + return 0 + fi + + # If no explicit error markers found, assume success + return 1 +} + +# Extract record id from response +# Supports bHosted XML first, then generic fallbacks +_bhosted_extract_id() { + _resp="$1" + + # bHosted XML: 12345 + _id="$(_bhosted_xml_value "id" "$_resp" | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + # JSON: "id":12345 + _id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + # key=value: id=12345 + _id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + # "record id 12345" / "recordid 12345" + _id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + return 1 +} + +# Create a unique config key for cached record ids +_bhosted_cache_hash() { + _fd="$1" + _tv="$2" + # md5 hex of fulldomain|txtvalue + printf "%s|%s" "$_fd" "$_tv" | _digest md5 hex +} + +_bhosted_cache_key() { + _hash="$1" + printf "%s" "BHOSTED_TXT_ID_${_hash}" +} + +_bhosted_mem_set_id() { + _hash="$1" + _id="$2" + _key="$(_bhosted_cache_key "$_hash")" + _savedomainconf "$_key" "$_id" +} + +_bhosted_mem_get_id() { + _hash="$1" + _key="$(_bhosted_cache_key "$_hash")" + _readdomainconf "$_key" +} From 08b2186afed6d441cb0a9bde1ee6c7f2e5458a89 Mon Sep 17 00:00:00 2001 From: Lorenz Stechauner Date: Wed, 8 Apr 2026 16:00:42 +0200 Subject: [PATCH 432/689] dns_world4you: Adapt to latest record id changes (#6897) --- dnsapi/dns_world4you.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index dc295330..f59715ac 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -61,7 +61,7 @@ dns_world4you_add() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') + msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to add record: Unknown error" echo "$ret" >'error-01.html' @@ -110,7 +110,7 @@ dns_world4you_rm() { return 3 fi - recordid=$(printf "TXT:%s.:\"%s\"" "$fqdn" "$value" | _base64) + recordid=$(echo "$form" | grep 'data-records="' | sed 's/.*"\([^"]*\)".*/\1/;s/"/"/g;s/},{/}\n{/g' | grep '"type":"TXT"' | grep "\"name\":\"$fqdn\"" | grep "\"value\":\"$value\"" | sed 's/^.*"id":"\([^"]*\)".*$/\1/') _debug recordid "$recordid" _resethttp @@ -125,7 +125,7 @@ dns_world4you_rm() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') + msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to remove record: Unknown error" echo "$ret" >'error-01.html' From 618735d11e6c8cdb0de26db7ca8e0e5de71ef08d Mon Sep 17 00:00:00 2001 From: Jordan Russell Date: Thu, 9 Apr 2026 02:07:30 +1200 Subject: [PATCH 433/689] [dnsapi] add SiteHost DNS API hook (#6891) Co-authored-by: Jordan Russell --- dnsapi/dns_sitehost.sh | 220 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100755 dnsapi/dns_sitehost.sh diff --git a/dnsapi/dns_sitehost.sh b/dnsapi/dns_sitehost.sh new file mode 100755 index 00000000..94a0ee93 --- /dev/null +++ b/dnsapi/dns_sitehost.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_sitehost_info='SiteHost +Site: sitehost.nz +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sitehost +Options: + SITEHOST_API_KEY API Key + SITEHOST_CLIENT_ID Client ID. The numeric client ID for your SiteHost account. +Issues: github.com/acmesh-official/acme.sh/issues/6892 +Author: Jordan Russell +' + +SITEHOST_API="https://api.sitehost.nz/1.5" + +######## Public functions ##################### + +# Usage: dns_sitehost_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_sitehost_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _sitehost_load_creds; then + return 1 + fi + + _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" + + # SiteHost expects the full record name as the name parameter + _info "Adding TXT record for ${fulldomain}" + if _sitehost_rest POST "dns/add_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&type=TXT&name=$(printf '%s' "${fulldomain}" | _url_encode)&content=$(printf '%s' "${txtvalue}" | _url_encode)"; then + if _contains "$response" '"status":true'; then + _info "TXT record added successfully." + return 0 + fi + fi + + _err "Could not add TXT record for ${fulldomain}" + _err "$response" + return 1 +} + +# Usage: dns_sitehost_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Remove the txt record after validation. +dns_sitehost_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _sitehost_load_creds; then + return 1 + fi + + _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" + + _debug "Getting TXT records for ${_domain}" + if ! _sitehost_rest GET "dns/list_records.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)"; then + _err "Could not list DNS records" + _err "$response" + return 1 + fi + + if ! _contains "$response" '"status":true'; then + _err "Error listing DNS records" + _err "$response" + return 1 + fi + + # Extract record ID matching our fulldomain, type TXT, and txtvalue + # Response format: {"return":[{"id":"123","name":"...","type":"TXT","content":"..."},...]} + # SiteHost returns flat single-line JSON objects in the records array + # Escape regex metacharacters in values before grep matching + _fulldomain_grep="$(printf "%s" "$fulldomain" | sed 's/[][\\.^$*]/\\&/g')" + _txtvalue_grep="$(printf "%s" "$txtvalue" | sed 's/[][\\.^$*]/\\&/g')" + # Use field-specific matching to avoid false positives from substring matches + _record_id="$(echo "$response" | _egrep_o '\{[^}]*\}' | grep '"name" *: *"'"${_fulldomain_grep}"'"' | grep '"type" *: *"TXT"' | grep '"content" *: *"'"${_txtvalue_grep}"'"' | _head_n 1 | _egrep_o '"id" *: *"?[0-9]+"?' | _egrep_o '[0-9]+')" + + if [ -z "$_record_id" ]; then + _info "TXT record not found, nothing to remove." + return 0 + fi + + _debug _record_id "$_record_id" + + _info "Deleting TXT record ${_record_id} for ${fulldomain}" + if _sitehost_rest POST "dns/delete_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&record_id=$(printf '%s' "${_record_id}" | _url_encode)"; then + if _contains "$response" '"status":true'; then + _info "TXT record deleted successfully." + return 0 + fi + fi + + _err "Could not delete TXT record for ${fulldomain}" + _err "$response" + return 1 +} + +#################### Private functions below ################################## + +_sitehost_load_creds() { + SITEHOST_API_KEY="${SITEHOST_API_KEY:-$(_readaccountconf_mutable SITEHOST_API_KEY)}" + SITEHOST_CLIENT_ID="${SITEHOST_CLIENT_ID:-$(_readaccountconf_mutable SITEHOST_CLIENT_ID)}" + + if [ -z "$SITEHOST_API_KEY" ] || [ -z "$SITEHOST_CLIENT_ID" ]; then + SITEHOST_API_KEY="" + SITEHOST_CLIENT_ID="" + _err "You didn't specify SITEHOST_API_KEY and/or SITEHOST_CLIENT_ID." + _err "Please export them and try again." + return 1 + fi + + _saveaccountconf_mutable SITEHOST_API_KEY "$SITEHOST_API_KEY" + _saveaccountconf_mutable SITEHOST_CLIENT_ID "$SITEHOST_CLIENT_ID" + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + + _debug "Getting domain list" + + # Fetch ALL pages of domains first so we can match the most specific zone + # (a more specific zone on a later page must take precedence over a broader match) + _all_domains="" + _page=1 + + while true; do + if ! _sitehost_rest GET "dns/list_domains.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&filters%5Bpage_number%5D=${_page}"; then + _err "Could not list domains" + return 1 + fi + + if ! _contains "$response" '"status":true'; then + _err "Error listing domains" + _err "$response" + return 1 + fi + + _all_domains="${_all_domains} ${response}" + + _total_pages=$(echo "$response" | _egrep_o '"total_pages" *: *[0-9]+' | _egrep_o '[0-9]+') + if [ -z "$_total_pages" ] || [ "$_page" -ge "$_total_pages" ]; then + break + fi + + _page=$(_math "$_page" + 1) + done + + # Try each subdomain level, most specific first + _i=1 + _p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "${_i}"-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 + fi + + if echo "$_all_domains" | grep -F "\"${h}\"" >/dev/null 2>&1; then + if [ "$_i" = "1" ]; then + # DNS alias mode - fulldomain is the zone itself + _sub_domain="" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"${_p}") + fi + _domain="${h}" + return 0 + fi + + _p="${_i}" + _i=$(_math "$_i" + 1) + done + + return 1 +} + +# Usage: _sitehost_rest method endpoint data +_sitehost_rest() { + m="$1" + ep="$2" + data="$3" + url="${SITEHOST_API}/${ep}" + + _debug url "$url" + + _apikey="$(printf "%s" "${SITEHOST_API_KEY}" | _url_encode)" + + if [ "$m" = "GET" ]; then + response="$(_get "${url}?apikey=${_apikey}&${data}")" + else + _debug2 data "$data" + response="$(_post "apikey=${_apikey}&${data}" "$url")" + fi + + if [ "$?" != "0" ]; then + _err "error ${ep}" + return 1 + fi + + response="$(printf '%s' "$response" | tr -d '\r')" + + _debug2 response "$response" + return 0 +} From 5b5ef91d88d1f18e82c8610067aab679683da27a Mon Sep 17 00:00:00 2001 From: brevilo Date: Wed, 8 Apr 2026 16:18:26 +0200 Subject: [PATCH 434/689] Fix off-by-one error preventing the final poll to succeed (#6865) When the final poll (`_link_cert_retry` at 29) returns, the status is never checked again. So even a `valid` status goes unnoticed. It's a pre-test loop after all. Co-authored-by: Oliver Behnke --- acme.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/acme.sh b/acme.sh index 0578e1ac..d54ad470 100755 --- a/acme.sh +++ b/acme.sh @@ -5324,6 +5324,11 @@ $_authorizations_map" _link_cert_retry="$(_math $_link_cert_retry + 1)" done + # cover case where the final poll returned 'valid' + if [ -z "$Le_LinkCert" ] && _contains "$response" "\"status\":\"valid\""; then + Le_LinkCert="$(echo "$response" | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" + fi + if [ -z "$Le_LinkCert" ]; then _err "Signing failed. Could not get Le_LinkCert, and stopped retrying after reaching the retry limit." _err "$response" From 0894955895313f40d6c3b331fd9b0f3cce003c2f Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 8 Apr 2026 22:33:30 +0800 Subject: [PATCH 435/689] fix https://github.com/acmesh-official/acme.sh/pull/6731#issuecomment-3733144962 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d54ad470..454cfea8 100755 --- a/acme.sh +++ b/acme.sh @@ -5672,7 +5672,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - for di in "${CERT_HOME}"/*[.:]*/; do + for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" From f3e61a8ef477325126a0ba34d5e184f26ca1a5cf Mon Sep 17 00:00:00 2001 From: Mitchell van Bijleveld <106330077+mitchellvanbijleveld@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:37:50 +0200 Subject: [PATCH 436/689] Don't mark restart http as failed if json returns false because it was not restarted (#6906) --- deploy/synology_dsm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index f6cc2cd0..e28a4036 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -387,7 +387,7 @@ synology_dsm_deploy() { if echo "$response" | grep '"restart_httpd":true' >/dev/null; then _info "Restart HTTP services succeeded." else - _info "Restart HTTP services failed." + _info "Restart HTTP services not necessary." fi _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" _logout From afba1455b8142ded3cf07a6763c012b501ef0cd8 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Tue, 14 Apr 2026 20:44:30 +0800 Subject: [PATCH 437/689] add dnsapi for baidu cloud dns (#6844) --- dnsapi/dns_baidu.sh | 548 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 dnsapi/dns_baidu.sh diff --git a/dnsapi/dns_baidu.sh b/dnsapi/dns_baidu.sh new file mode 100644 index 00000000..8651deab --- /dev/null +++ b/dnsapi/dns_baidu.sh @@ -0,0 +1,548 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 + +# Global variables for returning results (avoid stdout pollution from logging) +_BAIDU_FIND_RESULT="" +_BAIDU_BCE_AUTH_RESULT="" + +: "${BAIDU_LOG_LEVEL:=2}" + +_baidu_log_ts() { + date +} + +_baidu_log_ge() { + _want="$1" + [ "${BAIDU_LOG_LEVEL:-0}" -ge "$_want" ] +} + +_baidu_log() { + _lvl="$1" + _tag="$2" + _msg="$3" + if [ "$_lvl" = "0" ] || _baidu_log_ge "$_lvl"; then + printf -- "[%s] %s %s\n" "$(_baidu_log_ts)" "$_tag" "$_msg" + fi +} + +_baidu_err() { + _baidu_log 0 "baidu_bcd.err" "$1" + return 1 +} + +_baidu_info() { + _baidu_log 1 "baidu_bcd.info" "$1" + return 0 +} + +_baidu_debug() { + _baidu_log 2 "$1" "$2" + return 0 +} + +dns_baidu_info='Baidu Cloud BCD DNS +Site: cloud.baidu.com +Docs: https://cloud.baidu.com/doc/BCD/ +Signature: https://cloud.baidu.com/doc/Reference/s/njwvz1yfu +Options: + Baidu_AK AccessKeyId + Baidu_SK SecretAccessKey +OptionsAlt: + Baidu_BCD_Host API host, default: bcd.baidubce.com + Baidu_BCD_Version API version number, default: 1 + Baidu_BCD_Expire Signature expiration seconds, default: 3600 + Baidu_View Resolve view, default: DEFAULT + Baidu_TTL Resolve ttl seconds, default: 300 + Baidu_RM_Max Max records to delete in one run, default: 20 +' + +BAIDU_BCD_DEFAULT_HOST="bcd.baidubce.com" + +# --- Public API --- +dns_baidu_add() { + fulldomain=$(_idn "$1") + txtvalue=$2 + + if ! _baidu_prepare_record "$fulldomain"; then + _baidu_err "baidu_prepare_record failed for add: $fulldomain" + return 1 + fi + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name" + return 1 + fi + _existing_ids="$_BAIDU_FIND_RESULT" + if [ "$_existing_ids" ]; then + _baidu_info "txt exists, skip add: $_record_domain.$_zone_name" + return 0 + fi + + _ttl="${Baidu_TTL:-300}" + _ttl="$(_baidu_trim_ws "$_ttl")" + case "$_ttl" in + "" | *[!0-9]*) + _ttl="300" + ;; + esac + _view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")" + txtvalue="$(_baidu_trim_ws "$txtvalue")" + _record_domain="$(_baidu_trim_ws "$_record_domain")" + _zone_name="$(_baidu_trim_ws "$_zone_name")" + + _body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")" + + if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then + _baidu_err "baidu_bcd_post failed: add record" + return 1 + fi + + if _baidu_is_api_error "$response"; then + _baidu_err "$response" + return 1 + fi + + return 0 +} + +dns_baidu_rm() { + fulldomain=$(_idn "$1") + txtvalue=$2 + + if ! _baidu_prepare_record "$fulldomain"; then + _baidu_err "baidu_prepare_record failed for delete: $fulldomain" + return 1 + fi + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name" + return 1 + fi + _ids="$_BAIDU_FIND_RESULT" + if [ -z "$_ids" ]; then + _baidu_info "no matching txt to delete: $_record_domain.$_zone_name" + return 0 + fi + + _rm_max="${Baidu_RM_Max:-20}" + _rm_max="$(_baidu_trim_ws "$_rm_max")" + case "$_rm_max" in + "" | *[!0-9]*) + _rm_max="20" + ;; + esac + _rm_cnt="$(printf "%s\n" "$_ids" | sed '/^$/d' | wc -l | tr -d ' ')" + if [ "$_rm_cnt" ] && [ "$_rm_cnt" -gt "$_rm_max" ]; then + _baidu_err "Refusing to delete $_rm_cnt records (limit: $_rm_max)" + return 1 + fi + + for _rid in $_ids; do + _body="$(_baidu_payload_delete "$_zone_name" "$_rid")" + if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then + _baidu_err "baidu_bcd_post failed: delete recordId=$_rid" + return 1 + fi + if _baidu_is_api_error "$response"; then + _baidu_err "$response" + return 1 + fi + done + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name" + return 1 + fi + _left_ids="$_BAIDU_FIND_RESULT" + if [ -z "$_left_ids" ]; then + return 0 + fi + if [ -n "$_left_ids" ]; then + _baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records" + return 1 + fi + + return 0 +} + +# --- Config / Record Context --- +_baidu_load_credentials() { + Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}" + Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}" + + Baidu_AK="$(_baidu_trim_ws "$Baidu_AK")" + Baidu_SK="$(_baidu_trim_ws "$Baidu_SK")" + + if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then + _baidu_err "Baidu_AK and Baidu_SK are required" + return 1 + fi + + _saveaccountconf_mutable Baidu_AK "$Baidu_AK" + _saveaccountconf_mutable Baidu_SK "$Baidu_SK" + + BAIDU_BCD_HOST="${Baidu_BCD_Host:-$BAIDU_BCD_DEFAULT_HOST}" + BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}" + + return 0 +} + +_baidu_prepare_record() { + _fulldomain="$1" + if ! _baidu_load_credentials; then + _baidu_err "baidu_load_credentials failed" + return 1 + fi + if ! _baidu_get_root "$_fulldomain"; then + _baidu_err "Could not find zone for $_fulldomain" + return 1 + fi + _record_domain="$_sub_domain" + _zone_name="$_domain" + return 0 +} + +# --- Zone / Records --- +_baidu_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + _baidu_err "invalid domain: $domain" + return 1 + fi + + if ! _baidu_bcd_post "/domain/resolve/list" "$(_baidu_payload_list "$h" 1 1)"; then + _baidu_err "baidu_bcd_post failed: list zones" + return 1 + fi + if ! _baidu_is_api_error "$response" && (_contains "$response" "\"totalCount\"" || _contains "$response" "\"result\""); then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + if [ "$_sub_domain" = "$_domain" ]; then + _sub_domain="@" + fi + _baidu_info "zone matched: $_domain (host: $_sub_domain)" + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done +} + +_baidu_find_record_ids() { + _zone_name="$1" + _record_domain="$2" + _rdtype="$3" + _rdata="$4" + + # Reset global result variable + _BAIDU_FIND_RESULT="" + + _zone_name_e="$(_baidu_json_escape "$_zone_name")" + _record_domain_e="$(_baidu_json_escape "$_record_domain")" + _rdtype_e="$(_baidu_json_escape "$_rdtype")" + _rdata_e="$(_baidu_json_escape "$_rdata")" + + _page=1 + _page_size=100 + _ids="" + + _max_page="" + while true; do + if ! _baidu_bcd_post "/domain/resolve/list" "$(_baidu_payload_list "$_zone_name" "$_page" "$_page_size")"; then + _baidu_err "baidu_bcd_post failed: list records" + return 1 + fi + + if _baidu_is_api_error "$response"; then + _baidu_err "baidu_bcd error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")" + return 1 + fi + + _normalized="$( + printf "%s" "$response" | _normalizeJson + )" + + if [ -z "$_max_page" ]; then + _total="$(_baidu_parse_totalcount "$_normalized")" + _max_page="$(_baidu_calc_max_page "$_total" "$_page_size")" + fi + + _records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g') + while IFS= read -r _line; do + _id="$(_baidu_match_record_id "$_line" "$_record_domain_e" "$_rdtype_e" "$_rdata_e")" + if [ "$_id" ]; then + _ids="$_ids $_id" + fi + done < Date: Tue, 14 Apr 2026 20:57:22 +0800 Subject: [PATCH 438/689] Add Gname.com dnsapi support (#6808) * add gname dns acme.sh --- dnsapi/dns_gname.sh | 303 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 dnsapi/dns_gname.sh diff --git a/dnsapi/dns_gname.sh b/dnsapi/dns_gname.sh new file mode 100644 index 00000000..886b3dc5 --- /dev/null +++ b/dnsapi/dns_gname.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_gname_info='GNAME +Site: www.gname.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gname +Options: + GNAME_APPID Your APPID + GNAME_APPKEY Your APPKEY + GNAME_TTL DNS resolution record TTL value, default 120. +Issues: github.com/acmesh-official/acme.sh/issues/6874 +Author: GNDevProd +' + +GNAME_TLD_Api="https://www.gname.com/request/tlds?lx=all" +GNAME_Api="https://api.gname.com" +GNAME_TLDS_CACHE="" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CAS3" +dns_gname_add() { + fulldomain=$1 + txtvalue=$(printf "%s" "$2" | _url_encode) + #Compatible with gname API RFC 1738 standard URL encoding + txtvalue=$(printf '%s' "$txtvalue" | sed 's/%20/+/g') + + GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" + GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" + GNAME_TTL="${GNAME_TTL:-$(_readaccountconf_mutable GNAME_TTL)}" + GNAME_TTL="${GNAME_TTL:-120}" + + if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then + GNAME_APPID="" + GNAME_APPKEY="" + _err "You have not configured the APPID and APPKEY for the GNAME API." + _err "You can get yours from here https://www.gname.com/domain/api." + return 1 + fi + + _saveaccountconf_mutable GNAME_APPID "$GNAME_APPID" + _saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY" + _saveaccountconf_mutable GNAME_TTL "$GNAME_TTL" + + if ! _extract_domain "$fulldomain"; then + _err "Failed to extract domain. Please check your network or API response." + return 1 + fi + + gntime=$(date +%s) + + #If the hostname is empty, you need to replace it with @. + final_hostname=$(printf "%s" "${ext_hostname:-@}" | _url_encode) + + # Parameters need to be sorted by key + body="appid=$GNAME_APPID&exist=1&gntime=$gntime&jlz=$txtvalue&lang=us&lx=TXT&mx=0&ttl=$GNAME_TTL&xl=0&ym=$ext_domain&zj=$final_hostname" + + _info "Adding TXT record for $ext_domain, host: $final_hostname" + + if _post_to_api "/api/resolution/add" "$body"; then + _info "Successfully added DNS record." + return 0 + else + _err "Failed to add DNS record via Gname API." + return 1 + fi +} + +#Usage: remove _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CASc" +dns_gname_rm() { + fulldomain=$1 + txtvalue=$2 + + GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" + GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" + + if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then + GNAME_APPID="" + GNAME_APPKEY="" + _err "You have not configured the APPID and APPKEY for the GNAME API." + _err "You can get yours from here https://www.gname.com/domain/api." + return 1 + fi + + _saveaccountconf_mutable GNAME_APPID "$GNAME_APPID" + _saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY" + + if ! _extract_domain "$fulldomain"; then + _err "Failed to extract domain. Please check your network or API response." + return 1 + fi + + final_hostname="${ext_hostname:-@}" + + _debug "Query DNS record ID $ext_domain $final_hostname $txtvalue" + + if ! record_id=$(_get_record_id "$ext_domain" "$final_hostname" "$txtvalue"); then + _err "Error occurred during record lookup. Skipping deletion to avoid errors." + return 1 + fi + + if [ -z "$record_id" ]; then + _info "DNS record not found, skip removing." + return 0 + fi + + _debug "DNS record ID:$record_id" + gntime=$(date +%s) + body="appid=$GNAME_APPID&gntime=$gntime&jxid=$record_id&lang=us&ym=$ext_domain" + + if ! _post_to_api "/api/resolution/delete" "$body"; then + _err "DNS record deletion failed" + return 1 + fi + + _info "DNS record deletion successful" + return 0 +} + +# Find the DNS record ID by hostname, record type, and record value. +_get_record_id() { + target_ym="$1" + target_zjt="$2" + target_jxz="$3" + target_lx="TXT" + + GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" + GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" + gntime=$(date +%s) + body="appid=$GNAME_APPID&gntime=$gntime&limit=1000&lx=$target_lx&page=1&ym=$target_ym" + + if ! _post_to_api "/api/resolution/list" "$body"; then + _err "Query and parsing records failed" + return 1 + fi + + clean_response=$(echo "$post_response" | tr -d '\r') + records=$(echo "$clean_response" | sed 's/.*"data":\[//; s/\],"count".*//; s/},/}\n/g' | grep "^{") + matched_rows=$(echo "$records" | grep -Fi "\"zjt\":\"$target_zjt\"") + + if [ -z "$matched_rows" ]; then + _debug "No records found for host: $target_zjt" + return 0 + fi + + exact_row=$(echo "$matched_rows" | grep -F "\"jxz\":\"$target_jxz\"" | _head_n 1) + dns_record_id="" + if [ -n "$exact_row" ]; then + dns_record_id=$(echo "$exact_row" | _egrep_o "\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '"') + fi + + if [ -n "$dns_record_id" ]; then + _debug "Successfully found exact record ID: $dns_record_id" + printf "%s" "$dns_record_id" + return 0 + fi + + _debug "Can not find exact DNS record match for: $target_zjt" + return 0 +} + +# Request GNAME API,post_response: Response content +_post_to_api() { + uri=$1 + body=$2 + url="$GNAME_Api$uri" + gntoken=$(_gntoken "$body") + body="$body&gntoken=$gntoken" + post_response="$(_post "$body" "$url" "" "POST" "application/x-www-form-urlencoded")" + + http_err_code=$? + if [ "$http_err_code" != "0" ]; then + _err "POST API $url request failed:$http_err_code" + return 1 + fi + + normalized_response="$(echo "$post_response" | _normalizeJson)" + if [ -z "$normalized_response" ]; then + _err "Failed to normalize JSON response for [$uri]" + return 1 + fi + + ret_code=$(echo "$normalized_response" | sed 's/.*"code":\([-0-9]*\).*/\1/') + + if [ "$ret_code" = "1" ]; then + return 0 + fi + + if [ "$uri" = "/api/resolution/add" ]; then + if _contains "$normalized_response" "the same host records and record values"; then + _info "DNS record already exists, treat as success." + return 0 + fi + fi + + ret_msg=$(echo "$normalized_response" | sed 's/.*"msg":"\([^"]*\)".*/\1/') + _err "POST API $url error: [$ret_code] $ret_msg" + _debug "Full response: $normalized_response" + return 1 +} + +# Split the complete domain into a host and a main domain. +# example, www.gname.com can be split into ext_hostname=www,ext_domain=gname.com +_extract_domain() { + + host="$1" + + # Prioritize reading from the cache and reduce network caching + if [ -z "$GNAME_TLDS_CACHE" ]; then + GNAME_TLDS_CACHE=$(_get_suffixes_json) + fi + + if [ -z "$GNAME_TLDS_CACHE" ]; then + _err "The list of domain suffixes is empty after retrieval; cannot extract domain" + return 1 + fi + + main_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"main":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ') + sub_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"sub":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ') + suffix_list=$(echo "$main_part $sub_part" | tr -s ' ' | sed 's/^[ ]//;s/[ ]$//') + + dot_count=$(echo "$host" | _egrep_o "\." | wc -l) + + if [ "$dot_count" -eq 0 ]; then + _err "Invalid domain format: $host (missing dot)" + return 1 + fi + + if [ "$dot_count" -eq 1 ]; then + ext_hostname="" + ext_domain="$host" + + elif [ "$dot_count" -gt 1 ]; then + matched_suffix="" + for suffix in $suffix_list; do + case "$host" in + *".$suffix") + if [ -z "$matched_suffix" ] || [ "${#suffix}" -gt "${#matched_suffix}" ]; then + matched_suffix="$suffix" + fi + ;; + esac + done + + if [ -n "$matched_suffix" ]; then + prefix="${host%."$matched_suffix"}" + main_name="${prefix##*.}" + ext_domain="$main_name.$matched_suffix" + else + _tld="${host##*.}" + _tmp="${host%.*}" + _main="${_tmp##*.}" + ext_domain="$_main.$_tld" + fi + + if [ "$host" = "$ext_domain" ]; then + ext_hostname="" + else + ext_hostname="${host%."$ext_domain"}" + fi + + fi + _debug "ext_hostname:$ext_hostname" + _debug "ext_domain:$ext_domain" + return 0 +} + +# Obtain the list of domain suffixes via API +_get_suffixes_json() { + _debug "GET request URL: $GNAME_TLD_Api Retrieves a list of domain suffixes." + + if ! response="$(_get "$GNAME_TLD_Api")"; then + _err "Failed to retrieve list of domain suffixes" + return 1 + fi + + if [ -z "$response" ]; then + _err "The list of domain suffixes is empty" + return 1 + fi + + normalized_response="$(echo "$response" | _normalizeJson)" + if [ -z "$normalized_response" ]; then + _err "Failed to normalize JSON response for domain suffix list" + return 1 + fi + + if ! _contains "$normalized_response" "\"code\":1"; then + _err "Failed to retrieve list of domain name suffixes; code is not 1" + return 1 + fi + + echo "$normalized_response" + return 0 +} + +# Generate API authentication signature +_gntoken() { + data_to_sign="$1" + full_data="${data_to_sign}${GNAME_APPKEY}" + hash=$(printf "%s" "$full_data" | _digest md5 hex | tr -d ' ') + hash_upper=$(echo "$hash" | _upper_case) + printf "%s" "$hash_upper" +} From 6efd6d5b5a7c582f35807d758a264252bb69e2cf Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Wed, 1 Apr 2026 05:10:04 +0700 Subject: [PATCH 439/689] Add BytePlus ALB deployment script This script deploys SSL/TLS certificates issued by acme.sh to BytePlus Application Load Balancer (ALB), supporting automatic renewal with zero-downtime certificate rotation. --- deploy/byteplus_alb.sh | 449 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 deploy/byteplus_alb.sh diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh new file mode 100644 index 00000000..0cffa750 --- /dev/null +++ b/deploy/byteplus_alb.sh @@ -0,0 +1,449 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034,SC2154 +# +# acme.sh deploy hook: BytePlus Application Load Balancer (ALB) +# https://github.com/acmesh-official/acme.sh/wiki/deployhooks +# +# Deploys SSL/TLS certificates issued by acme.sh to BytePlus ALB. +# Supports automatic renewal with zero-downtime certificate rotation. +# +# ┌─────────────────────────────────────────────────────────────────────┐ +# │ FIRST TIME (new domain) │ +# │ 1. acme.sh --issue -d example.com -w /var/www/html/ │ +# │ 2. acme.sh --deploy -d example.com --deploy-hook byteplus_alb │ +# │ → UploadCertificate → saves CertificateId │ +# │ 3. Manually assign cert to ALB Listener (one-time only) │ +# │ │ +# │ RENEWAL (fully automatic) │ +# │ acme.sh cron triggers renew → deploy hook runs automatically │ +# │ → ReplaceCertificate (UpdateMode=new) — single API call │ +# │ → All attached listeners updated, old cert auto-deleted │ +# └─────────────────────────────────────────────────────────────────────┘ +# +# Required environment variables: +# export BYTEPLUS_ACCESS_KEY="AKAPxxxxxxxxxx" +# export BYTEPLUS_SECRET_KEY="your-secret-key" +# +# Optional environment variables: +# export BYTEPLUS_REGION="ap-southeast-3" # default: ap-southeast-3 +# export BYTEPLUS_HOST="alb.ap-southeast-3.byteplusapi.com" # custom API host +# export BYTEPLUS_PROJECT_NAME="live" # default: "default" project +# export BYTEPLUS_CERT_NAME="" # default: acme-{domain}-{YYYYMMDD-HHMM} +# export BYTEPLUS_CERT_DESCRIPTION="" # default: empty +# export BYTEPLUS_DELETE_OLD_CERT="true" # default: true — auto-delete after replace +# +# API notes: +# - All BytePlus ALB APIs use GET with query string parameters +# - Request signing: HMAC-SHA256 with signed headers host;x-date +# - PublicKey/PrivateKey are URL-encoded (RFC 3986) in query string +# - ReplaceCertificate with UpdateMode=new uploads + replaces in 1 call +# +# Dependencies: curl, openssl, awk (standard on most Linux) +# +# Docs: +# Signing — https://docs.byteplus.com/en/docs/byteplus-platform/reference-how-to-calculate-a-signature +# ALB API — https://docs.byteplus.com/en/docs/byteplus-alb + +# ══════════════════════════════════════════════════════════════════════════════ +# Constants +# ══════════════════════════════════════════════════════════════════════════════ + +# SHA-256 hash of empty string (used for GET requests with no body) +_BYTEPLUS_EMPTY_HASH="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + +# ══════════════════════════════════════════════════════════════════════════════ +# Main deploy function — called by acme.sh +# ══════════════════════════════════════════════════════════════════════════════ + +byteplus_alb_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" + + # ── 1. Load & validate credentials ────────────────────────────────────────── + + # Preserve environment values before _getdeployconf (which may reset them) + _env_project_name="${BYTEPLUS_PROJECT_NAME:-}" + _env_delete_old="${BYTEPLUS_DELETE_OLD_CERT:-}" + + _getdeployconf BYTEPLUS_ACCESS_KEY + _getdeployconf BYTEPLUS_SECRET_KEY + _getdeployconf BYTEPLUS_REGION + _getdeployconf BYTEPLUS_HOST + _getdeployconf BYTEPLUS_PROJECT_NAME + _getdeployconf BYTEPLUS_DELETE_OLD_CERT + _getdeployconf BYTEPLUS_CERT_NAME + _getdeployconf BYTEPLUS_CERT_DESCRIPTION + + # Restore from environment if _getdeployconf cleared them + if [ -z "$BYTEPLUS_PROJECT_NAME" ] && [ -n "$_env_project_name" ]; then + _debug "Restoring BYTEPLUS_PROJECT_NAME from environment" + BYTEPLUS_PROJECT_NAME="$_env_project_name" + fi + if [ -z "$BYTEPLUS_DELETE_OLD_CERT" ] && [ -n "$_env_delete_old" ]; then + BYTEPLUS_DELETE_OLD_CERT="$_env_delete_old" + fi + + # Validate required credentials + if [ -z "$BYTEPLUS_ACCESS_KEY" ]; then + _err "BYTEPLUS_ACCESS_KEY is not set." + _err "Please run: export BYTEPLUS_ACCESS_KEY=\"your-access-key\"" + return 1 + fi + if [ -z "$BYTEPLUS_SECRET_KEY" ]; then + _err "BYTEPLUS_SECRET_KEY is not set." + _err "Please run: export BYTEPLUS_SECRET_KEY=\"your-secret-key\"" + return 1 + fi + + # Save credentials for future runs + _savedeployconf BYTEPLUS_ACCESS_KEY "$BYTEPLUS_ACCESS_KEY" + _savedeployconf BYTEPLUS_SECRET_KEY "$BYTEPLUS_SECRET_KEY" + + # Region (default: ap-southeast-3) + BYTEPLUS_REGION="${BYTEPLUS_REGION:-ap-southeast-3}" + _savedeployconf BYTEPLUS_REGION "$BYTEPLUS_REGION" + + # Project name + if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then + _savedeployconf BYTEPLUS_PROJECT_NAME "$BYTEPLUS_PROJECT_NAME" + _info "Using project: $BYTEPLUS_PROJECT_NAME" + else + _info "WARNING: BYTEPLUS_PROJECT_NAME is not set. Cert will go to 'default' project." + fi + + # Delete old cert toggle (default: true) + BYTEPLUS_DELETE_OLD_CERT="${BYTEPLUS_DELETE_OLD_CERT:-true}" + _savedeployconf BYTEPLUS_DELETE_OLD_CERT "$BYTEPLUS_DELETE_OLD_CERT" + + # API host — custom override or auto-build from region + if [ -n "$BYTEPLUS_HOST" ]; then + _BYTEPLUS_HOST="$BYTEPLUS_HOST" + _savedeployconf BYTEPLUS_HOST "$BYTEPLUS_HOST" + else + _BYTEPLUS_HOST="alb.${BYTEPLUS_REGION}.byteplusapi.com" + fi + _info "Using API host: $_BYTEPLUS_HOST" + _BYTEPLUS_SERVICE="alb" + + # ── 2. Build certificate name ──────────────────────────────────────────────── + + _date_tag=$(date -u +%Y%m%d-%H%M) + # Replace wildcard * and dots for a valid cert name + _safe_domain=$(echo "$_cdomain" | sed 's/\*\.//g' | sed 's/\./-/g') + # Underscore version for bash variable names (hyphens not allowed in var names) + _conf_key=$(echo "$_cdomain" | sed 's/\*\.//g' | sed 's/\./_/g') + + if [ -z "$BYTEPLUS_CERT_NAME" ]; then + BYTEPLUS_CERT_NAME="acme-${_safe_domain}-${_date_tag}" + fi + + # Enforce BytePlus naming rules: start with letter, max 128 chars + BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | cut -c1-128) + + _info "Certificate name: $BYTEPLUS_CERT_NAME" + + # ── 3. Read cert and key ───────────────────────────────────────────────────── + # BytePlus requires NO blank lines between PEM blocks in the certificate chain + + _public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r') + _private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r') + + if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then + _err "Failed to read certificate or key file." + return 1 + fi + + # ── 4. Deploy: first-time upload or renewal replace ───────────────────────── + + _getdeployconf "BYTEPLUS_CERT_ID_${_conf_key}" + _old_cert_id=$(eval echo "\$BYTEPLUS_CERT_ID_${_conf_key}") + + if [ -z "$_old_cert_id" ]; then + _byteplus_first_time_deploy + else + _byteplus_renewal_deploy + fi + + # Check if deploy step set _new_cert_id + if [ -z "$_new_cert_id" ]; then + return 1 + fi + + # ── 5. Save new CertificateId for next renewal ─────────────────────────────── + + _savedeployconf "BYTEPLUS_CERT_ID_${_conf_key}" "$_new_cert_id" + _info "Saved CertificateId '$_new_cert_id' for domain '$_cdomain'." + + return 0 +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Deploy: First time — UploadCertificate +# ══════════════════════════════════════════════════════════════════════════════ + +_byteplus_first_time_deploy() { + _info "No previous CertificateId found. Uploading new certificate..." + + if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then + _upload_response=$(_byteplus_alb_api "UploadCertificate" \ + "CertificateType=Server" \ + "CertificateName=${BYTEPLUS_CERT_NAME}" \ + "ProjectName=${BYTEPLUS_PROJECT_NAME}" \ + "PublicKey=${_public_key}" \ + "PrivateKey=${_private_key}") + else + _upload_response=$(_byteplus_alb_api "UploadCertificate" \ + "CertificateType=Server" \ + "CertificateName=${BYTEPLUS_CERT_NAME}" \ + "PublicKey=${_public_key}" \ + "PrivateKey=${_private_key}") + fi + + _debug2 _upload_response "$_upload_response" + + _new_cert_id=$(_byteplus_extract_cert_id "$_upload_response") + + if [ -z "$_new_cert_id" ]; then + _err "UploadCertificate failed: $(_byteplus_extract_error "$_upload_response")" + _debug2 "Full response" "$_upload_response" + return 1 + fi + + _info "Certificate uploaded. CertificateId: $_new_cert_id" + + # Set description if provided + if [ -n "$BYTEPLUS_CERT_DESCRIPTION" ]; then + _info "Setting certificate description..." + _byteplus_alb_api "ModifyCertificateAttributes" \ + "CertificateId=${_new_cert_id}" \ + "CertificateName=${BYTEPLUS_CERT_NAME}" \ + "Description=${BYTEPLUS_CERT_DESCRIPTION}" >/dev/null + fi + + _info "" + _info "╔══════════════════════════════════════════════════════════════════╗" + _info "║ ACTION REQUIRED (one-time only) ║" + _info "║ Assign CertificateId '$_new_cert_id'" + _info "║ to your ALB Listener in BytePlus Console. ║" + _info "║ After that, all future renewals will be fully automatic. ║" + _info "╚══════════════════════════════════════════════════════════════════╝" + _info "" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Deploy: Renewal — ReplaceCertificate (UpdateMode=new) +# ══════════════════════════════════════════════════════════════════════════════ + +_byteplus_renewal_deploy() { + _info "Replacing old certificate '$_old_cert_id' (UpdateMode=new)..." + + if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then + _replace_response=$(_byteplus_alb_api "ReplaceCertificate" \ + "OldCertificateId=${_old_cert_id}" \ + "UpdateMode=new" \ + "CertificateName=${BYTEPLUS_CERT_NAME}" \ + "ProjectName=${BYTEPLUS_PROJECT_NAME}" \ + "PublicKey=${_public_key}" \ + "PrivateKey=${_private_key}") + else + _replace_response=$(_byteplus_alb_api "ReplaceCertificate" \ + "OldCertificateId=${_old_cert_id}" \ + "UpdateMode=new" \ + "CertificateName=${BYTEPLUS_CERT_NAME}" \ + "PublicKey=${_public_key}" \ + "PrivateKey=${_private_key}") + fi + + _debug2 _replace_response "$_replace_response" + + _new_cert_id=$(_byteplus_extract_cert_id "$_replace_response") + + if [ -z "$_new_cert_id" ]; then + _err "ReplaceCertificate failed: $(_byteplus_extract_error "$_replace_response")" + _debug2 "Full response" "$_replace_response" + return 1 + fi + + _info "Certificate replaced successfully on all attached listeners." + _info "New CertificateId: $_new_cert_id" + + # Auto-cleanup old certificate + if [ "$BYTEPLUS_DELETE_OLD_CERT" = "true" ]; then + _byteplus_delete_old_cert "$_old_cert_id" + else + _info "Auto-delete disabled. Old certificate '$_old_cert_id' kept in inventory." + fi +} + +# ══════════════════════════════════════════════════════════════════════════════ +# Delete old certificate (with retry) +# ══════════════════════════════════════════════════════════════════════════════ + +_byteplus_delete_old_cert() { + _del_cert_id="$1" + + _info "Waiting 5s for cert status to settle..." + sleep 5 + + _info "Deleting old certificate '$_del_cert_id'..." + _del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}") + + if echo "$_del_response" | grep -q '"Error"'; then + _info "Delete failed, retrying in 10s..." + sleep 10 + _del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}") + + if echo "$_del_response" | grep -q '"Error"'; then + _info "Warning: Could not delete old certificate '$_del_cert_id'." + _info "Error: $(_byteplus_extract_error "$_del_response")" + _info "Please remove it manually from BytePlus Console." + else + _info "Old certificate '$_del_cert_id' deleted (retry succeeded)." + fi + else + _info "Old certificate '$_del_cert_id' deleted." + fi +} + +# ══════════════════════════════════════════════════════════════════════════════ +# JSON response helpers +# ══════════════════════════════════════════════════════════════════════════════ + +# Extract CertificateId from API response JSON +_byteplus_extract_cert_id() { + echo "$1" | _egrep_o '"CertificateId"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"' +} + +# Extract error message from API response JSON +_byteplus_extract_error() { + _code=$(echo "$1" | _egrep_o '"Code"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"') + _msg=$(echo "$1" | _egrep_o '"Message"\s*:\s*"[^"]*"' | head -1 | _egrep_o '"[^"]*"$' | tr -d '"') + if [ -n "$_code" ]; then + printf '%s — %s' "$_code" "$_msg" + else + printf '%s' "$1" + fi +} + +# ══════════════════════════════════════════════════════════════════════════════ +# BytePlus ALB API caller +# ══════════════════════════════════════════════════════════════════════════════ + +# Usage: _byteplus_alb_api ACTION [param1=val1] [param2=val2] ... +# All parameters sent via GET query string. Signing: HMAC-SHA256, host;x-date. +_byteplus_alb_api() { + _action="$1" + shift + + # Build query string — all params go in URL + _query_params="Action=${_action}&Version=2020-04-01" + + for _param in "$@"; do + _pname="${_param%%=*}" + _pval="${_param#*=}" + _query_params="${_query_params}&${_pname}=$(_byteplus_urlencode "$_pval")" + done + + # Timestamps + _x_date=$(date -u +%Y%m%dT%H%M%SZ) + _date_only=$(date -u +%Y%m%d) + + # Sort query params for canonical request + _sorted_query=$(echo "$_query_params" | tr '&' '\n' | sort | tr '\n' '&' | sed 's/&$//') + + # Canonical headers — only host and x-date + _canonical_headers="host:${_BYTEPLUS_HOST} +x-date:${_x_date} +" + _signed_headers="host;x-date" + + # Canonical request + _canonical_request="GET +/ +${_sorted_query} +${_canonical_headers} +${_signed_headers} +${_BYTEPLUS_EMPTY_HASH}" + + _debug2 _canonical_request "$_canonical_request" + + # Hash of canonical request + _cr_hash=$(printf '%s' "$_canonical_request" | openssl dgst -sha256 | awk '{print $NF}') + + # Credential scope + _credential_scope="${_date_only}/${BYTEPLUS_REGION}/${_BYTEPLUS_SERVICE}/request" + + # String to sign + _string_to_sign="HMAC-SHA256 +${_x_date} +${_credential_scope} +${_cr_hash}" + + _debug2 _string_to_sign "$_string_to_sign" + + # Signing key derivation (HMAC chain) + _k_date=$(printf '%s' "$_date_only" | openssl dgst -sha256 -hmac "$BYTEPLUS_SECRET_KEY" | awk '{print $NF}') + _k_region=$(printf '%s' "$BYTEPLUS_REGION" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_date}" | awk '{print $NF}') + _k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_region}" | awk '{print $NF}') + _k_signing=$(printf '%s' "request" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_service}" | awk '{print $NF}') + + # Final signature + _signature=$(printf '%s' "$_string_to_sign" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_signing}" | awk '{print $NF}') + + # Authorization header + _auth="HMAC-SHA256 Credential=${BYTEPLUS_ACCESS_KEY}/${_credential_scope}, SignedHeaders=${_signed_headers}, Signature=${_signature}" + + _debug2 _auth "$_auth" + + # Build URL and execute GET request + _url="https://${_BYTEPLUS_HOST}/?${_sorted_query}" + + _response=$(curl -s --connect-timeout 10 --max-time 60 -X GET \ + -H "Authorization: ${_auth}" \ + -H "X-Date: ${_x_date}" \ + -H "Host: ${_BYTEPLUS_HOST}" \ + "${_url}") + + _debug2 "_byteplus_alb_api response [$_action]" "$_response" + printf '%s' "$_response" +} + +# ══════════════════════════════════════════════════════════════════════════════ +# URL encode (RFC 3986) — awk-based for performance +# ══════════════════════════════════════════════════════════════════════════════ + +_byteplus_urlencode() { + printf '%s' "$1" | awk 'BEGIN { + for (i = 0; i <= 255; i++) { + c = sprintf("%c", i) + if (c ~ /[a-zA-Z0-9.~_\-]/) + safe[i] = c + else + safe[i] = sprintf("%%%02X", i) + } + } + { + n = length($0) + for (i = 1; i <= n; i++) { + c = substr($0, i, 1) + printf "%s", safe[ord(c)] + } + # Print newline as %0A (except trailing, which command substitution strips) + if (NR > 0) printf "%%0A" + } + function ord(c, i2) { + for (i2 = 0; i2 <= 255; i2++) + if (sprintf("%c", i2) == c) return i2 + return 0 + } + END { }' | sed 's/%0A$//' +} From 86d98b046189f38162a61b49bc3177cac1cfa7ff Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:55:12 +0700 Subject: [PATCH 440/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 0cffa750..642f8bc4 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -139,8 +139,8 @@ byteplus_alb_deploy() { _date_tag=$(date -u +%Y%m%d-%H%M) # Replace wildcard * and dots for a valid cert name _safe_domain=$(echo "$_cdomain" | sed 's/\*\.//g' | sed 's/\./-/g') - # Underscore version for bash variable names (hyphens not allowed in var names) - _conf_key=$(echo "$_cdomain" | sed 's/\*\.//g' | sed 's/\./_/g') + # Safe identifier version for deployconf keys: map all non [A-Za-z0-9_] to _ + _conf_key=$(echo "$_cdomain" | sed 's/^\*\.//' | sed 's/[^A-Za-z0-9_]/_/g') if [ -z "$BYTEPLUS_CERT_NAME" ]; then BYTEPLUS_CERT_NAME="acme-${_safe_domain}-${_date_tag}" From 044371b00a53dcedcb26980101616f2ccf6fe959 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:57:10 +0700 Subject: [PATCH 441/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 52 +++++------------------------------------- 1 file changed, 6 insertions(+), 46 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 642f8bc4..c23b4479 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -191,52 +191,12 @@ byteplus_alb_deploy() { # ══════════════════════════════════════════════════════════════════════════════ _byteplus_first_time_deploy() { - _info "No previous CertificateId found. Uploading new certificate..." - - if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then - _upload_response=$(_byteplus_alb_api "UploadCertificate" \ - "CertificateType=Server" \ - "CertificateName=${BYTEPLUS_CERT_NAME}" \ - "ProjectName=${BYTEPLUS_PROJECT_NAME}" \ - "PublicKey=${_public_key}" \ - "PrivateKey=${_private_key}") - else - _upload_response=$(_byteplus_alb_api "UploadCertificate" \ - "CertificateType=Server" \ - "CertificateName=${BYTEPLUS_CERT_NAME}" \ - "PublicKey=${_public_key}" \ - "PrivateKey=${_private_key}") - fi - - _debug2 _upload_response "$_upload_response" - - _new_cert_id=$(_byteplus_extract_cert_id "$_upload_response") - - if [ -z "$_new_cert_id" ]; then - _err "UploadCertificate failed: $(_byteplus_extract_error "$_upload_response")" - _debug2 "Full response" "$_upload_response" - return 1 - fi - - _info "Certificate uploaded. CertificateId: $_new_cert_id" - - # Set description if provided - if [ -n "$BYTEPLUS_CERT_DESCRIPTION" ]; then - _info "Setting certificate description..." - _byteplus_alb_api "ModifyCertificateAttributes" \ - "CertificateId=${_new_cert_id}" \ - "CertificateName=${BYTEPLUS_CERT_NAME}" \ - "Description=${BYTEPLUS_CERT_DESCRIPTION}" >/dev/null - fi - - _info "" - _info "╔══════════════════════════════════════════════════════════════════╗" - _info "║ ACTION REQUIRED (one-time only) ║" - _info "║ Assign CertificateId '$_new_cert_id'" - _info "║ to your ALB Listener in BytePlus Console. ║" - _info "║ After that, all future renewals will be fully automatic. ║" - _info "╚══════════════════════════════════════════════════════════════════╝" - _info "" + _info "No previous CertificateId found." + _err "Refusing to upload certificate material because this hook passes PublicKey/PrivateKey as request parameters." + _err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings." + _err "Please upload the certificate to BytePlus manually for the initial deployment, set BYTEPLUS_CERT_ID, and rerun." + _err "This hook must be updated to send PublicKey and PrivateKey in a POST body before automatic first-time upload can be enabled safely." + return 1 } # ══════════════════════════════════════════════════════════════════════════════ From 4178c33524c2b360fc07c04fedb3a071cdccfa81 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:57:37 +0700 Subject: [PATCH 442/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index c23b4479..f87be9a0 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -367,12 +367,24 @@ ${_cr_hash}" # Build URL and execute GET request _url="https://${_BYTEPLUS_HOST}/?${_sorted_query}" - _response=$(curl -s --connect-timeout 10 --max-time 60 -X GET \ - -H "Authorization: ${_auth}" \ - -H "X-Date: ${_x_date}" \ - -H "Host: ${_BYTEPLUS_HOST}" \ - "${_url}") + _saved_H1="${_H1:-}" + _saved_H2="${_H2:-}" + _saved_H3="${_H3:-}" + _H1="Authorization: ${_auth}" + _H2="X-Date: ${_x_date}" + _H3="Host: ${_BYTEPLUS_HOST}" + _response="$(_get "$_url")" + _request_ret="$?" + + _H1="$_saved_H1" + _H2="$_saved_H2" + _H3="$_saved_H3" + + if [ "$_request_ret" != "0" ]; then + _err "byteplus_alb_api request failed for [$_action]" + return 1 + fi _debug2 "_byteplus_alb_api response [$_action]" "$_response" printf '%s' "$_response" } From d5c8060a65f9ecd92fec34db16dd3b575c118394 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:58:15 +0700 Subject: [PATCH 443/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index f87be9a0..ab6decc5 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -390,32 +390,9 @@ ${_cr_hash}" } # ══════════════════════════════════════════════════════════════════════════════ -# URL encode (RFC 3986) — awk-based for performance +# URL encode (RFC 3986) # ══════════════════════════════════════════════════════════════════════════════ _byteplus_urlencode() { - printf '%s' "$1" | awk 'BEGIN { - for (i = 0; i <= 255; i++) { - c = sprintf("%c", i) - if (c ~ /[a-zA-Z0-9.~_\-]/) - safe[i] = c - else - safe[i] = sprintf("%%%02X", i) - } - } - { - n = length($0) - for (i = 1; i <= n; i++) { - c = substr($0, i, 1) - printf "%s", safe[ord(c)] - } - # Print newline as %0A (except trailing, which command substitution strips) - if (NR > 0) printf "%%0A" - } - function ord(c, i2) { - for (i2 = 0; i2 <= 255; i2++) - if (sprintf("%c", i2) == c) return i2 - return 0 - } - END { }' | sed 's/%0A$//' + _url_encode "$1" } From ad71a785ec454b22c421d7c349a6119afe961735 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:58:28 +0700 Subject: [PATCH 444/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index ab6decc5..f3b99232 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # shellcheck disable=SC2034,SC2154 # # acme.sh deploy hook: BytePlus Application Load Balancer (ALB) From 28f1f07f49983af7ec839325ca3d2ad38e5fb1eb Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:58:52 +0700 Subject: [PATCH 445/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index f3b99232..196f708b 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -318,7 +318,7 @@ _byteplus_alb_api() { _date_only=$(date -u +%Y%m%d) # Sort query params for canonical request - _sorted_query=$(echo "$_query_params" | tr '&' '\n' | sort | tr '\n' '&' | sed 's/&$//') + _sorted_query=$(echo "$_query_params" | tr '&' '\n' | LC_ALL=C sort | tr '\n' '&' | sed 's/&$//') # Canonical headers — only host and x-date _canonical_headers="host:${_BYTEPLUS_HOST} From 8eea7ca307abd365ffffd25ee8a7a648894c1ded Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:59:17 +0700 Subject: [PATCH 446/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 196f708b..6a01184c 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -334,7 +334,8 @@ ${_canonical_headers} ${_signed_headers} ${_BYTEPLUS_EMPTY_HASH}" - _debug2 _canonical_request "$_canonical_request" + # Do not log _canonical_request because the query string may contain + # URL-encoded certificate or private key material. # Hash of canonical request _cr_hash=$(printf '%s' "$_canonical_request" | openssl dgst -sha256 | awk '{print $NF}') From 934870fc7769cab740759175ce42560bf8814dcf Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 11:59:26 +0700 Subject: [PATCH 447/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 6a01184c..3d8818f1 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -338,7 +338,7 @@ ${_BYTEPLUS_EMPTY_HASH}" # URL-encoded certificate or private key material. # Hash of canonical request - _cr_hash=$(printf '%s' "$_canonical_request" | openssl dgst -sha256 | awk '{print $NF}') + _cr_hash=$(_digest "sha256" "hex" "$_canonical_request") # Credential scope _credential_scope="${_date_only}/${BYTEPLUS_REGION}/${_BYTEPLUS_SERVICE}/request" From 8587c3e74467e21998074a9942abe4790cbda90e Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:00:39 +0700 Subject: [PATCH 448/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 3d8818f1..30cd89a5 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -252,14 +252,14 @@ _byteplus_delete_old_cert() { _del_cert_id="$1" _info "Waiting 5s for cert status to settle..." - sleep 5 + _sleep 5 _info "Deleting old certificate '$_del_cert_id'..." _del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}") if echo "$_del_response" | grep -q '"Error"'; then _info "Delete failed, retrying in 10s..." - sleep 10 + _sleep 10 _del_response=$(_byteplus_alb_api "DeleteCertificate" "CertificateId=${_del_cert_id}") if echo "$_del_response" | grep -q '"Error"'; then From d0e123cb027aa24b5615a4ddbc101362c2256fbe Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:01:06 +0700 Subject: [PATCH 449/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 30cd89a5..26661918 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -363,7 +363,7 @@ ${_cr_hash}" # Authorization header _auth="HMAC-SHA256 Credential=${BYTEPLUS_ACCESS_KEY}/${_credential_scope}, SignedHeaders=${_signed_headers}, Signature=${_signature}" - _debug2 _auth "$_auth" + _secure_debug2 _auth "$_auth" # Build URL and execute GET request _url="https://${_BYTEPLUS_HOST}/?${_sorted_query}" From 668427f2855ca67cfee80841e32ccd6c2df35dc5 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:01:28 +0700 Subject: [PATCH 450/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 26661918..eada236c 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -147,7 +147,15 @@ byteplus_alb_deploy() { fi # Enforce BytePlus naming rules: start with letter, max 128 chars - BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | sed 's/[^a-zA-Z0-9._-]/-/g' | cut -c1-128) + BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | sed 's/[^A-Za-z0-9._-]/-/g') + case "$BYTEPLUS_CERT_NAME" in + [A-Za-z]*) + ;; + *) + BYTEPLUS_CERT_NAME="a$BYTEPLUS_CERT_NAME" + ;; + esac + BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | cut -c1-128) _info "Certificate name: $BYTEPLUS_CERT_NAME" From 75642a125216b19e52eb77770b0c883de5d0d83e Mon Sep 17 00:00:00 2001 From: Achmad Alif Nasrulloh Date: Sun, 5 Apr 2026 12:11:31 +0700 Subject: [PATCH 451/689] Update bteplus_alb.sh --- deploy/byteplus_alb.sh | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index eada236c..31831b72 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -346,7 +346,8 @@ ${_BYTEPLUS_EMPTY_HASH}" # URL-encoded certificate or private key material. # Hash of canonical request - _cr_hash=$(_digest "sha256" "hex" "$_canonical_request") + # _digest is provided by acme.sh and works across OpenSSL versions. + _cr_hash=$(printf '%s' "$_canonical_request" | _digest sha256 hex) # Credential scope _credential_scope="${_date_only}/${BYTEPLUS_REGION}/${_BYTEPLUS_SERVICE}/request" @@ -360,20 +361,29 @@ ${_cr_hash}" _debug2 _string_to_sign "$_string_to_sign" # Signing key derivation (HMAC chain) - _k_date=$(printf '%s' "$_date_only" | openssl dgst -sha256 -hmac "$BYTEPLUS_SECRET_KEY" | awk '{print $NF}') - _k_region=$(printf '%s' "$BYTEPLUS_REGION" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_date}" | awk '{print $NF}') - _k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_region}" | awk '{print $NF}') - _k_signing=$(printf '%s' "request" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_service}" | awk '{print $NF}') + # _hmac reads data from stdin and returns a hex digest. + # acme.sh's _hmac abstracts away OpenSSL version differences, so this works + # on both modern (-mac HMAC -macopt hexkey:) and older (-hmac) OpenSSL builds. + # + # The first step seeds the chain from the raw secret key, so we convert it + # to hex first with _hex_dump (also an acme.sh built-in). + _secret_hex=$(printf '%s' "$BYTEPLUS_SECRET_KEY" | _hex_dump | tr -d ' \n') + _k_date=$(printf '%s' "$_date_only" | _hmac sha256 "$_secret_hex" hex) + _k_region=$(printf '%s' "$BYTEPLUS_REGION" | _hmac sha256 "$_k_date" hex) + _k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | _hmac sha256 "$_k_region" hex) + _k_signing=$(printf '%s' "request" | _hmac sha256 "$_k_service" hex) # Final signature - _signature=$(printf '%s' "$_string_to_sign" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${_k_signing}" | awk '{print $NF}') + _signature=$(printf '%s' "$_string_to_sign" | _hmac sha256 "$_k_signing" hex) # Authorization header _auth="HMAC-SHA256 Credential=${BYTEPLUS_ACCESS_KEY}/${_credential_scope}, SignedHeaders=${_signed_headers}, Signature=${_signature}" _secure_debug2 _auth "$_auth" - # Build URL and execute GET request + # Build URL and execute GET request via acme.sh's _get helper. + # _get handles exit-status checking, respects _H1/_H2/_H3 extra headers, + # and provides consistent error handling across platforms. _url="https://${_BYTEPLUS_HOST}/?${_sorted_query}" _saved_H1="${_H1:-}" From a739bf3e3adc421cbcc8bce2c6e0a61d39f7b21c Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:42:55 +0700 Subject: [PATCH 452/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 41 ++++------------------------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 31831b72..abaf443e 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -213,43 +213,10 @@ _byteplus_first_time_deploy() { _byteplus_renewal_deploy() { _info "Replacing old certificate '$_old_cert_id' (UpdateMode=new)..." - - if [ -n "$BYTEPLUS_PROJECT_NAME" ]; then - _replace_response=$(_byteplus_alb_api "ReplaceCertificate" \ - "OldCertificateId=${_old_cert_id}" \ - "UpdateMode=new" \ - "CertificateName=${BYTEPLUS_CERT_NAME}" \ - "ProjectName=${BYTEPLUS_PROJECT_NAME}" \ - "PublicKey=${_public_key}" \ - "PrivateKey=${_private_key}") - else - _replace_response=$(_byteplus_alb_api "ReplaceCertificate" \ - "OldCertificateId=${_old_cert_id}" \ - "UpdateMode=new" \ - "CertificateName=${BYTEPLUS_CERT_NAME}" \ - "PublicKey=${_public_key}" \ - "PrivateKey=${_private_key}") - fi - - _debug2 _replace_response "$_replace_response" - - _new_cert_id=$(_byteplus_extract_cert_id "$_replace_response") - - if [ -z "$_new_cert_id" ]; then - _err "ReplaceCertificate failed: $(_byteplus_extract_error "$_replace_response")" - _debug2 "Full response" "$_replace_response" - return 1 - fi - - _info "Certificate replaced successfully on all attached listeners." - _info "New CertificateId: $_new_cert_id" - - # Auto-cleanup old certificate - if [ "$BYTEPLUS_DELETE_OLD_CERT" = "true" ]; then - _byteplus_delete_old_cert "$_old_cert_id" - else - _info "Auto-delete disabled. Old certificate '$_old_cert_id' kept in inventory." - fi + _err "Refusing to replace certificate material because this hook passes PublicKey/PrivateKey as request parameters." + _err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings." + _err "Please replace the certificate in BytePlus manually for renewal until this hook is updated to send PublicKey and PrivateKey in a POST body safely." + return 1 } # ══════════════════════════════════════════════════════════════════════════════ From 3843495397058dece66891e4cff564d80a6d861b Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:43:11 +0700 Subject: [PATCH 453/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index abaf443e..42b544c7 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -348,24 +348,28 @@ ${_cr_hash}" _secure_debug2 _auth "$_auth" - # Build URL and execute GET request via acme.sh's _get helper. - # _get handles exit-status checking, respects _H1/_H2/_H3 extra headers, - # and provides consistent error handling across platforms. - _url="https://${_BYTEPLUS_HOST}/?${_sorted_query}" + # Send request parameters in the POST body instead of the URL query string. + # This avoids exposing sensitive or large values in debug-logged URLs and + # reduces the risk of exceeding URL length limits. + _url="https://${_BYTEPLUS_HOST}/" + _body="$_sorted_query" _saved_H1="${_H1:-}" _saved_H2="${_H2:-}" _saved_H3="${_H3:-}" + _saved_H4="${_H4:-}" _H1="Authorization: ${_auth}" _H2="X-Date: ${_x_date}" _H3="Host: ${_BYTEPLUS_HOST}" - _response="$(_get "$_url")" + _H4="Content-Type: application/x-www-form-urlencoded" + _response="$(_post "$_body" "$_url" "" "POST")" _request_ret="$?" _H1="$_saved_H1" _H2="$_saved_H2" _H3="$_saved_H3" + _H4="$_saved_H4" if [ "$_request_ret" != "0" ]; then _err "byteplus_alb_api request failed for [$_action]" From 5c94af86f381a52d3960269b5d4f3d26646f1512 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:43:25 +0700 Subject: [PATCH 454/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 42b544c7..28c229e8 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -384,5 +384,5 @@ ${_cr_hash}" # ══════════════════════════════════════════════════════════════════════════════ _byteplus_urlencode() { - _url_encode "$1" + printf '%s' "$1" | _url_encode } From a1b94db94d11fbabbcc41c94b0061bfc2cd74c31 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:43:37 +0700 Subject: [PATCH 455/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 28c229e8..76b3b381 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -5,16 +5,18 @@ # https://github.com/acmesh-official/acme.sh/wiki/deployhooks # # Deploys SSL/TLS certificates issued by acme.sh to BytePlus ALB. -# Supports automatic renewal with zero-downtime certificate rotation. +# Supports automatic renewal with zero-downtime certificate rotation +# for certificates that have already been uploaded and have a saved +# BytePlus CertificateId. # # ┌─────────────────────────────────────────────────────────────────────┐ # │ FIRST TIME (new domain) │ # │ 1. acme.sh --issue -d example.com -w /var/www/html/ │ -# │ 2. acme.sh --deploy -d example.com --deploy-hook byteplus_alb │ -# │ → UploadCertificate → saves CertificateId │ -# │ 3. Manually assign cert to ALB Listener (one-time only) │ +# │ 2. Upload/import the certificate to BytePlus ALB manually │ +# │ 3. Save/configure the existing CertificateId for this hook │ +# │ 4. Manually assign cert to ALB Listener (one-time only) │ # │ │ -# │ RENEWAL (fully automatic) │ +# │ RENEWAL (fully automatic after CertificateId is configured) │ # │ acme.sh cron triggers renew → deploy hook runs automatically │ # │ → ReplaceCertificate (UpdateMode=new) — single API call │ # │ → All attached listeners updated, old cert auto-deleted │ From 73a682e561ce3e7ae6618e8a86b2f8897250aa9f Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:43:52 +0700 Subject: [PATCH 456/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 76b3b381..35db4c64 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -204,7 +204,8 @@ _byteplus_first_time_deploy() { _info "No previous CertificateId found." _err "Refusing to upload certificate material because this hook passes PublicKey/PrivateKey as request parameters." _err "Uploading a private key in the request URL can leak it via logs, proxies, and process listings." - _err "Please upload the certificate to BytePlus manually for the initial deployment, set BYTEPLUS_CERT_ID, and rerun." + _err "Please upload the certificate to BytePlus manually for the initial deployment, set BYTEPLUS_CERT_ID_${_conf_key} to that CertificateId, and rerun." + _err "This hook stores CertificateId values per domain using deployconf, so the variable name must include the current domain-specific suffix." _err "This hook must be updated to send PublicKey and PrivateKey in a POST body before automatic first-time upload can be enabled safely." return 1 } From 00090d24b8aee4cc1d7f500cb575b5581413b2a5 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:44:19 +0700 Subject: [PATCH 457/689] Update deploy/byteplus_alb.sh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deploy/byteplus_alb.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 35db4c64..fd333703 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -83,7 +83,6 @@ byteplus_alb_deploy() { _getdeployconf BYTEPLUS_PROJECT_NAME _getdeployconf BYTEPLUS_DELETE_OLD_CERT _getdeployconf BYTEPLUS_CERT_NAME - _getdeployconf BYTEPLUS_CERT_DESCRIPTION # Restore from environment if _getdeployconf cleared them if [ -z "$BYTEPLUS_PROJECT_NAME" ] && [ -n "$_env_project_name" ]; then From f89a9a5de3438f34656c9d23d75e4e9f61e7390d Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:56:51 +0700 Subject: [PATCH 458/689] Add new header variable _H5 in byteplus_alb.sh Added a new header variable _H5 to the byteplus_alb.sh script. --- deploy/byteplus_alb.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index fd333703..e89ab966 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -360,11 +360,14 @@ ${_cr_hash}" _saved_H2="${_H2:-}" _saved_H3="${_H3:-}" _saved_H4="${_H4:-}" + _saved_H5="${_H5:-}" _H1="Authorization: ${_auth}" _H2="X-Date: ${_x_date}" _H3="Host: ${_BYTEPLUS_HOST}" _H4="Content-Type: application/x-www-form-urlencoded" + _H5="" + _response="$(_post "$_body" "$_url" "" "POST")" _request_ret="$?" @@ -372,6 +375,7 @@ ${_cr_hash}" _H2="$_saved_H2" _H3="$_saved_H3" _H4="$_saved_H4" + _H5="$_saved_H5" if [ "$_request_ret" != "0" ]; then _err "byteplus_alb_api request failed for [$_action]" From 539b46adc9fa865299f59cfc29530f51fdc5d344 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 14 Apr 2026 21:19:25 +0800 Subject: [PATCH 459/689] fix https://github.com/acmesh-official/acme.sh/issues/6898#issuecomment-4207794240 --- acme.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 454cfea8..e780f61d 100755 --- a/acme.sh +++ b/acme.sh @@ -1599,6 +1599,7 @@ createCSR() { domain="$1" domainlist="$2" _isEcc="$3" + _csreku="$4" _initpath "$domain" "$_isEcc" @@ -1612,7 +1613,7 @@ createCSR() { _err "Please create it first." return 1 fi - _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" + _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_csreku" } @@ -8259,7 +8260,7 @@ _process() { createDomainKey "$_domain" "$_keylength" ;; createCSR) - createCSR "$_domain" "$_altdomains" "$_ecc" + createCSR "$_domain" "$_altdomains" "$_ecc" "$_extended_key_usage" ;; setnotify) setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" "$_notify_source" From 9882d534af8a826b72e41643e91e1e020f64fa41 Mon Sep 17 00:00:00 2001 From: Antoni Company Date: Mon, 20 Apr 2026 10:28:17 +0100 Subject: [PATCH 460/689] fix: commit overhaul (#6915) - Removed scope exclusion for "standard commit". - If 'device-and-networks' is excluded (previous behaviour), a certificate for Panorama (always outside of a template) will not be committed (imported to the config but never applied to Panorama). Therefore, panos.sh was only working for certificates used in templates and applied to devices, but not for the Panorama certificate itself. - According to the official documentation and the XML API Browser, there is no 'policy-and-objects' that can be excluded. - Although it is not mandatory that the user account is solely dedicated to replace certificates and to perform no other type of operations, it is recommended. If such recommendation is applied, the only changes being committed would be in relation to certificates. Therefore, it should be safe not to exclude any scopes. - Changed the order for "force commit" from '' (unofficial) to '' (official). Both work, but it is recommended to use what is part of the official documentation and/or XML API Browser. - Removed unofficial 'policy-and-objects' from commented out code (see above). - Replaced 'exclude' with 'excluded' from commented out code, as per the official documentation. Both work, but see above. - Replaced 'acmekeytest' with $_panos_user in the commented out code. Official documentation: https://docs.paloaltonetworks.com/ngfw/api/pan-os-xml-api-request-types-and-actions/commit XML API Browser: https:///api --- deploy/panos.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index 019d8c62..00badffc 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -68,8 +68,8 @@ deployer() { # Get Version Info to test key content="type=version&key=$_panos_key" ## Exclude all scopes for the empty commit - #_exclude_scope="excludeexcludeexclude" - #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scopeacmekeytest" + #_exclude_scope="excludedexcluded" + #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scope$_panos_user" fi # Generate API Key @@ -128,10 +128,9 @@ deployer() { #Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution! if [ "$FORCE" ]; then _debug "Force switch detected. Committing ALL changes to the firewall." - cmd=$(printf "%s" "$_panos_user" | _url_encode) + cmd=$(printf "%s" "$_panos_user" | _url_encode) else - _exclude_scope="excludeexclude" - cmd=$(printf "%s" "$_exclude_scope$_panos_user" | _url_encode) + cmd=$(printf "%s" "$_panos_user" | _url_encode) fi content="type=commit&action=partial&key=$_panos_key&cmd=$cmd" fi From e9b0cafac52673f5c9aac96917a55356b44b82a0 Mon Sep 17 00:00:00 2001 From: Achmad Alif Nasrulloh Date: Fri, 24 Apr 2026 11:21:43 +0700 Subject: [PATCH 461/689] Fix byteplus_alb.sh --- deploy/byteplus_alb.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index e89ab966..8443bb99 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -150,11 +150,11 @@ byteplus_alb_deploy() { # Enforce BytePlus naming rules: start with letter, max 128 chars BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | sed 's/[^A-Za-z0-9._-]/-/g') case "$BYTEPLUS_CERT_NAME" in - [A-Za-z]*) - ;; - *) - BYTEPLUS_CERT_NAME="a$BYTEPLUS_CERT_NAME" - ;; + [A-Za-z]*) ;; + + *) + BYTEPLUS_CERT_NAME="a$BYTEPLUS_CERT_NAME" + ;; esac BYTEPLUS_CERT_NAME=$(echo "$BYTEPLUS_CERT_NAME" | cut -c1-128) @@ -337,10 +337,10 @@ ${_cr_hash}" # The first step seeds the chain from the raw secret key, so we convert it # to hex first with _hex_dump (also an acme.sh built-in). _secret_hex=$(printf '%s' "$BYTEPLUS_SECRET_KEY" | _hex_dump | tr -d ' \n') - _k_date=$(printf '%s' "$_date_only" | _hmac sha256 "$_secret_hex" hex) - _k_region=$(printf '%s' "$BYTEPLUS_REGION" | _hmac sha256 "$_k_date" hex) - _k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | _hmac sha256 "$_k_region" hex) - _k_signing=$(printf '%s' "request" | _hmac sha256 "$_k_service" hex) + _k_date=$(printf '%s' "$_date_only" | _hmac sha256 "$_secret_hex" hex) + _k_region=$(printf '%s' "$BYTEPLUS_REGION" | _hmac sha256 "$_k_date" hex) + _k_service=$(printf '%s' "$_BYTEPLUS_SERVICE" | _hmac sha256 "$_k_region" hex) + _k_signing=$(printf '%s' "request" | _hmac sha256 "$_k_service" hex) # Final signature _signature=$(printf '%s' "$_string_to_sign" | _hmac sha256 "$_k_signing" hex) @@ -367,7 +367,7 @@ ${_cr_hash}" _H3="Host: ${_BYTEPLUS_HOST}" _H4="Content-Type: application/x-www-form-urlencoded" _H5="" - + _response="$(_post "$_body" "$_url" "" "POST")" _request_ret="$?" From 4b8b23bb90a5d8b29b9fb4095d114607d0ac0afb Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 24 Apr 2026 21:47:01 +0200 Subject: [PATCH 462/689] fix ca name --- .github/workflows/Ubuntu.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml index 5ebf2d0d..36dfdbe6 100644 --- a/.github/workflows/Ubuntu.yml +++ b/.github/workflows/Ubuntu.yml @@ -37,8 +37,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 - TEST_ACME_Server: "ZeroSSL.com" - CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - CA: "ZeroSSL RSA Domain Secure Site CA" + CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + CA: "ZeroSSL RSA DV SSL CA 2" CA_EMAIL: "githubtest@acme.sh" TEST_PREFERRED_CHAIN: "" - TEST_ACME_Server: "https://localhost:9000/acme/acme/directory" From 0b2187ab3f59740c0f41098015c4de812a0bc23a Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 24 Apr 2026 22:43:25 +0200 Subject: [PATCH 463/689] add MidnightBSD --- .github/workflows/DNS.yml | 66 +++++++++++++++++++++++++- .github/workflows/DragonFlyBSD.yml | 5 +- .github/workflows/FreeBSD.yml | 5 +- .github/workflows/Haiku.yml | 5 +- .github/workflows/MacOS.yml | 4 +- .github/workflows/MidnightBSD.yml | 74 ++++++++++++++++++++++++++++++ .github/workflows/NetBSD.yml | 5 +- .github/workflows/Omnios.yml | 5 +- .github/workflows/OpenBSD.yml | 5 +- .github/workflows/OpenIndiana.yml | 5 +- .github/workflows/Solaris.yml | 5 +- .github/workflows/Windows.yml | 4 +- README.md | 32 +++++++------ 13 files changed, 184 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/MidnightBSD.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 0104595d..00d180b9 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -229,6 +229,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/freebsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg install -y socat curl usesh: true @@ -284,6 +285,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg_add socat curl libiconv usesh: true @@ -339,6 +341,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/netbsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | /usr/sbin/pkg_add curl socat @@ -395,6 +398,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/dragonflybsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | pkg install -y libnghttp2 @@ -430,9 +434,65 @@ jobs: - Solaris: + MidnightBSD: runs-on: ubuntu-latest needs: DragonFlyBSD + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v6 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/midnightbsd-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + prepare: mport install socat curl || true + usesh: true + sync: nfs + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + + + Solaris: + runs-on: ubuntu-latest + needs: MidnightBSD env: TEST_DNS : ${{ secrets.TEST_DNS }} TestingDomain: ${{ secrets.TestingDomain }} @@ -456,6 +516,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/solaris-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: | @@ -514,6 +575,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/omnios-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: pkg install socat @@ -569,6 +631,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openindiana-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: pkg install socat @@ -624,6 +687,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/haiku-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: rsync copyback: false diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index f32d0916..c8cbc985 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -31,8 +31,8 @@ jobs: CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -57,6 +57,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/dragonflybsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 09b544f6..50fcab32 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -37,8 +37,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -63,6 +63,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/freebsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 4324545e..9884ebeb 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -38,8 +38,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -64,6 +64,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/haiku-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml index 3869b504..ef9580a6 100644 --- a/.github/workflows/MacOS.yml +++ b/.github/workflows/MacOS.yml @@ -31,8 +31,8 @@ jobs: CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: macos-latest diff --git a/.github/workflows/MidnightBSD.yml b/.github/workflows/MidnightBSD.yml new file mode 100644 index 00000000..15024833 --- /dev/null +++ b/.github/workflows/MidnightBSD.yml @@ -0,0 +1,74 @@ +name: MidnightBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/MidnightBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/MidnightBSD.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + MidnightBSD: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v6 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/midnightbsd-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: mport install socat curl wget || true + usesh: true + sync: nfs + run: | + cd ../acmetest \ + && ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 4021cd7e..16d0ae2d 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -31,8 +31,8 @@ jobs: CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -57,6 +57,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/netbsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index a156800c..eb486b35 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -37,8 +37,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -63,6 +63,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/omnios-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index 8a91cd2e..4fdb76c5 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -37,8 +37,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -63,6 +63,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index dca29741..b5061ba7 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -37,8 +37,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -63,6 +63,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openindiana-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 397469b5..3393269a 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -37,8 +37,8 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) ACME_USE_WGET: 1 #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest @@ -63,6 +63,7 @@ jobs: run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/solaris-vm@v1 with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index c628ff5b..1de120f9 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -31,8 +31,8 @@ jobs: CA_EMAIL: "" TEST_PREFERRED_CHAIN: (STAGING) #- TEST_ACME_Server: "ZeroSSL.com" - # CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" - # CA: "ZeroSSL RSA Domain Secure Site CA" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: windows-latest diff --git a/README.md b/README.md index 740e5ef0..188af0e1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Windows Solaris DragonFlyBSD + MidnightBSD Omnios OpenIndiana Haiku @@ -95,21 +96,22 @@ |7|[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)|OpenBSD |8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD |9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD -|10|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios -|11|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana -|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian -|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE -|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) -|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux -|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora -|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux -|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux -|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia -|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux -|21|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 -|22|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) -|23|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) -|24|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS +|10|[![MidnightBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml)|MidnightBSD +|11|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios +|12|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana +|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian +|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE +|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl) +|16|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux +|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|fedora +|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux +|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux +|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia +|21|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux +|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 +|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) +|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) +|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From fd6ff7b1731b39ed638a38eda5555fc988c71145 Mon Sep 17 00:00:00 2001 From: Adam Bodnar <1891200+abodnar@users.noreply.github.com> Date: Sun, 26 Apr 2026 07:29:40 -0500 Subject: [PATCH 464/689] Add dns_cpanel_uapi DNS API plugin (#6878) * Add dns_cpanel_uapi.sh --- dnsapi/dns_cpanel_uapi.sh | 269 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100755 dnsapi/dns_cpanel_uapi.sh diff --git a/dnsapi/dns_cpanel_uapi.sh b/dnsapi/dns_cpanel_uapi.sh new file mode 100755 index 00000000..02a777ae --- /dev/null +++ b/dnsapi/dns_cpanel_uapi.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_cpanel_uapi_info='cPanel UAPI + Manage DNS via cPanel UAPI. Works with API tokens and Two-Factor Authentication. +Site: cpanel.net +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_cpanel_uapi +Options: + cPanel_Username Username + cPanel_Apitoken API Token + cPanel_Hostname Server URL. E.g. "https://hostname:port" + cPanel_TTL optional TXT record TTL in seconds. Default: 120 +Issues: github.com/acmesh-official/acme.sh/issues/6877 +Author: Adam Bodnar +' + +######## Public functions ##################### + +# Used to add txt record +dns_cpanel_uapi_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Adding TXT record via cPanel UAPI" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _cpanel_uapi_get_root; then + _err "No matching root domain for $fulldomain found" + return 1 + fi + + # Build the record name relative to the zone + _escaped_domain=$(echo "$_domain" | sed 's/\./\\./g') + _record_name=$(echo "$fulldomain" | sed "s/\.${_escaped_domain}$//") + _debug "Record name: $_record_name in zone $_domain" + + # Get the current SOA serial (required by mass_edit_zone) + if ! _cpanel_uapi_get_serial "$_domain"; then + _err "Failed to get zone serial for $_domain" + return 1 + fi + _debug "Zone serial: $_serial" + + # Use configurable TTL, default 120 seconds + _ttl="${cPanel_TTL:-$(_readaccountconf_mutable cPanel_TTL)}" + case "$_ttl" in + "") + _ttl=120 + ;; + *[!0-9]*) + _debug "Invalid cPanel_TTL provided, falling back to default 120" + _ttl=120 + ;; + esac + + # Build JSON and URL-encode it for the add parameter + _add_json=$(printf '{"dname":"%s","ttl":%s,"record_type":"TXT","data":["%s"]}' "$_record_name" "$_ttl" "$txtvalue") + _debug "add_json: $_add_json" + _add_json_encoded=$(printf '%s' "$_add_json" | _url_encode) + _debug "add_json (encoded): $_add_json_encoded" + + if ! _cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&add=${_add_json_encoded}"; then + _err "Request to add TXT record failed for zone $_domain" + return 1 + fi + _debug "_result: $_result" + + if _contains "$_result" '"status":1'; then + _info "TXT record added successfully" + return 0 + fi + _err "Failed to add TXT record." + _err "Response: $_result" + return 1 +} + +# Used to remove the txt record after validation +dns_cpanel_uapi_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Removing TXT record via cPanel UAPI" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _cpanel_uapi_get_root; then + _err "No matching root domain for $fulldomain found" + return 1 + fi + + if ! _cpanel_uapi_findentry; then + _info "Entry doesn't exist, nothing to delete" + return 0 + fi + + _debug "Deleting record with line_index=$_line_index" + if ! _cpanel_uapi_get_serial "$_domain"; then + _err "Failed to get zone serial for $_domain" + return 1 + fi + if ! _cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&remove=${_line_index}"; then + _err "Request to remove TXT record failed for zone $_domain" + return 1 + fi + _debug "_result: $_result" + + if _contains "$_result" '"status":1'; then + _info "TXT record removed successfully" + return 0 + fi + _err "Failed to remove TXT record." + _err "Response: $_result" + return 1 +} + +#################### Private functions below ################################## + +_cpanel_uapi_checkcredentials() { + cPanel_Username="${cPanel_Username:-$(_readaccountconf_mutable cPanel_Username)}" + cPanel_Apitoken="${cPanel_Apitoken:-$(_readaccountconf_mutable cPanel_Apitoken)}" + cPanel_Hostname="${cPanel_Hostname:-$(_readaccountconf_mutable cPanel_Hostname)}" + + if [ -z "$cPanel_Username" ] || [ -z "$cPanel_Apitoken" ] || [ -z "$cPanel_Hostname" ]; then + cPanel_Username="" + cPanel_Apitoken="" + cPanel_Hostname="" + _err "You haven't specified cPanel_Username, cPanel_Apitoken, and cPanel_Hostname." + return 1 + fi + + # Remove trailing slash from hostname if present + cPanel_Hostname=$(echo "$cPanel_Hostname" | sed 's|/$||') + + _saveaccountconf_mutable cPanel_Username "$cPanel_Username" + _saveaccountconf_mutable cPanel_Apitoken "$cPanel_Apitoken" + _saveaccountconf_mutable cPanel_Hostname "$cPanel_Hostname" + + if [ -n "$cPanel_TTL" ]; then + case "$cPanel_TTL" in + *[!0-9]*) + _info "Ignoring invalid cPanel_TTL: $cPanel_TTL" + cPanel_TTL="" + ;; + *) + _saveaccountconf_mutable cPanel_TTL "$cPanel_TTL" + ;; + esac + fi + return 0 +} + +_cpanel_uapi_request() { + export _H1="Authorization: cpanel $cPanel_Username:$cPanel_Apitoken" + _result=$(_get "$cPanel_Hostname/$1") + return $? +} + +_cpanel_uapi_get_root() { + if ! _cpanel_uapi_checkcredentials; then return 1; fi + + if ! _cpanel_uapi_request "execute/DomainInfo/list_domains"; then + _err "Request to cPanel API failed while listing domains" + return 1 + fi + _debug "DomainInfo response length: ${#_result}" + + if ! _contains "$_result" '"status":1'; then + _err "cPanel UAPI request failed. Is the API token correct?" + _debug "Response: $_result" + return 1 + fi + + # Extract main_domain + _main_domain=$(echo "$_result" | _egrep_o '"main_domain":"[^"]*"' | _head_n 1 | sed 's/.*"main_domain":"//;s/"//') + _debug "main_domain: $_main_domain" + + # Extract addon_domains (array of strings) + _addon_domains=$(echo "$_result" | _egrep_o '"addon_domains":\[[^]]*\]' | sed 's/.*"addon_domains":\[//;s/\]$//' | _egrep_o '"[a-zA-Z0-9._-]+"' | sed 's/"//g') + _debug "addon_domains: $_addon_domains" + + # Build list of all domains to check + _all_domains="$_main_domain $_addon_domains" + _debug "All domains: $_all_domains" + + # Find the matching root domain (prefer longest match) + _best_match="" + _best_len=0 + for _check_domain in $_all_domains; do + if [ -z "$_check_domain" ]; then continue; fi + if _endswith "$fulldomain" "$_check_domain"; then + _len=${#_check_domain} + if [ "$_len" -gt "$_best_len" ]; then + _best_match="$_check_domain" + _best_len="$_len" + fi + fi + done + + if [ -n "$_best_match" ]; then + _domain="$_best_match" + _debug "Root domain: $_domain" + return 0 + fi + return 1 +} + +_cpanel_uapi_get_serial() { + _zone="$1" + if ! _cpanel_uapi_request "execute/DNS/parse_zone?zone=${_zone}"; then + _err "Request to parse zone failed for $_zone" + return 1 + fi + + # Split JSON records onto separate lines using a POSIX-portable sed literal newline + # (\\n in sed replacement is a GNU/BusyBox extension; a backslash-newline works everywhere) + _soa_line=$(echo "$_result" | sed 's/},{/},\ +{/g' | grep '"record_type":"SOA"' | _head_n 1) + _debug "SOA line: $_soa_line" + + if [ -z "$_soa_line" ]; then + _err "SOA record not found for zone $_zone" + _debug "parse_zone response: $_result" + return 1 + fi + + # Extract the third element from data_b64 array (serial is index 2, 0-based) + # data_b64 format: ["ns","admin","SERIAL","refresh","retry","expire","minimum"] + _serial_b64=$(echo "$_soa_line" | _egrep_o '"data_b64":\[[^]]*\]' | sed 's/"data_b64":\[//;s/\]//' | sed 's/"//g' | cut -d',' -f3) + _debug "serial_b64: $_serial_b64" + + if [ -z "$_serial_b64" ]; then + _err "Could not extract serial from SOA record" + return 1 + fi + + _serial=$(printf '%s' "$_serial_b64" | _dbase64) + _debug "Decoded serial: $_serial" + + if [ -z "$_serial" ]; then + _err "Failed to decode serial" + return 1 + fi + return 0 +} + +_cpanel_uapi_findentry() { + _debug "Finding TXT entry for $fulldomain with value $txtvalue" + + if ! _cpanel_uapi_request "execute/DNS/parse_zone?zone=${_domain}"; then + _err "Request to parse zone failed for $_domain" + return 1 + fi + _debug "parse_zone result length: ${#_result}" + + # Base64-encode the txtvalue to match against data_b64 in the response + _b64_txtvalue=$(printf '%s' "$txtvalue" | _base64) + _debug "b64_txtvalue: $_b64_txtvalue" + + # Split records onto separate lines, find matching TXT record by base64 value + _line_index=$(echo "$_result" | sed 's/},{/},\ +{/g' | grep '"record_type":"TXT"' | grep -F "$_b64_txtvalue" | _egrep_o '"line_index":[0-9]+' | _head_n 1 | cut -d: -f2) + _debug "line_index: $_line_index" + + if [ -n "$_line_index" ]; then + _debug "Entry found with line_index=$_line_index" + return 0 + fi + return 1 +} From 15a0f52577a1f5c0d3d5bee0437a94637b0560eb Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 28 Apr 2026 09:09:40 +0200 Subject: [PATCH 465/689] fix https://github.com/acmesh-official/acme.sh/issues/6930#issuecomment-4327834577 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 15439e5a..55a9cc67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.22 +FROM alpine:3.23 RUN apk --no-cache add -f \ openssl \ From 10e7e458af9618004b9fd9f37560706b9e4d3061 Mon Sep 17 00:00:00 2001 From: firestormisp <53045434+firestormisp@users.noreply.github.com> Date: Fri, 1 May 2026 10:43:51 +0200 Subject: [PATCH 466/689] Add dns_firestorm.sh plugin for Firestorm.ch DNS API (#6829) Firestorm.ch is a Swiss hosting provider with managed DNS (PowerDNS). This plugin allows customers to automate Let's Encrypt DNS-01 challenges via the Firestorm DNS API. --- dnsapi/dns_firestorm.sh | 110 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 dnsapi/dns_firestorm.sh diff --git a/dnsapi/dns_firestorm.sh b/dnsapi/dns_firestorm.sh new file mode 100644 index 00000000..808c2b89 --- /dev/null +++ b/dnsapi/dns_firestorm.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_firestorm_info='Firestorm.ch +Site: firestorm.ch +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm +Options: + FST_Key Customer ID + FST_Secret API Secret + FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns". +Issues: github.com/acmesh-official/acme.sh/issues/6839 +Author: FireStorm GmbH +' + +FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns" + +######## Public functions ##################### + +# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_firestorm_add() { + fulldomain=$1 + txtvalue=$2 + + FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}" + FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}" + FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}" + + if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then + _err "FST_Key and FST_Secret must be set" + _err "Get your API credentials at https://admin.firestorm.ch" + return 1 + fi + + FST_Url="${FST_Url:-$FST_Url_DEFAULT}" + + _saveaccountconf_mutable FST_Key "$FST_Key" + _saveaccountconf_mutable FST_Secret "$FST_Secret" + if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then + _saveaccountconf_mutable FST_Url "$FST_Url" + else + _clearaccountconf_mutable FST_Url + fi + + subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//') + + _info "Adding TXT record for $fulldomain" + _debug "Subdomain" "$subdomain" + _debug "TXT value" "$txtvalue" + + body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}" + + response="$(_firestorm_api "update" "$body")" + + if _contains "$response" "$txtvalue"; then + _info "TXT record added successfully" + return 0 + fi + + _err "Failed to add TXT record: $response" + return 1 +} + +# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_firestorm_rm() { + fulldomain=$1 + txtvalue=$2 + + FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}" + FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}" + FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}" + FST_Url="${FST_Url:-$FST_Url_DEFAULT}" + + if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then + _err "FST_Key and FST_Secret must be set" + return 1 + fi + + subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//') + + _info "Removing TXT record for $fulldomain" + + body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}" + + response="$(_firestorm_api "remove" "$body")" + + if _contains "$response" "removed"; then + _info "TXT record removed" + return 0 + fi + + _err "Failed to remove TXT record: $response" + return 1 +} + +#################### Private functions below ################################## + +# Escape special characters for safe JSON string interpolation +_json_safe() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +_firestorm_api() { + action=$1 + data=$2 + + export _H1="X-Api-User: $FST_Key" + export _H2="X-Api-Key: $FST_Secret" + export _H3="Content-Type: application/json" + + _post "$data" "$FST_Url/$action" "" "POST" +} From 8e71268d039a06325d63cf6571598e6007f6b48d Mon Sep 17 00:00:00 2001 From: Alexey Pakhomov Date: Fri, 1 May 2026 13:58:19 +0500 Subject: [PATCH 467/689] Fix write error $_CRONTAB -l in crontab file (#6920) --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index e780f61d..71e40d24 100755 --- a/acme.sh +++ b/acme.sh @@ -6274,13 +6274,13 @@ installcronjob() { return 1 fi _info "Installing cron job" - if ! $_CRONTAB -l | grep "$PROJECT_ENTRY --cron"; then + if ! $_CRONTAB -l 2>/dev/null | grep "$PROJECT_ENTRY --cron"; then if _exists uname && uname -a | grep SunOS >/dev/null; then _CRONTAB_STDIN="$_CRONTAB --" else _CRONTAB_STDIN="$_CRONTAB -" fi - $_CRONTAB -l | { + $_CRONTAB -l 2>/dev/null | { cat echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" } | $_CRONTAB_STDIN From 96f87844cd518329f295334e97746ca27179fcb3 Mon Sep 17 00:00:00 2001 From: nsantorelli Date: Fri, 1 May 2026 11:10:15 +0200 Subject: [PATCH 468/689] Add EuroDNS DNS API plugin (dns_eurodns) (#6903) Co-authored-by: Nicolas Santorelli --- dnsapi/dns_eurodns.sh | 267 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 dnsapi/dns_eurodns.sh diff --git a/dnsapi/dns_eurodns.sh b/dnsapi/dns_eurodns.sh new file mode 100644 index 00000000..0fac4cb5 --- /dev/null +++ b/dnsapi/dns_eurodns.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_eurodns_info='EuroDNS +Site: eurodns.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns +Options: + EURODNS_APP_ID Application ID + EURODNS_API_KEY API Key + EURODNS_TTL TTL. Default: "600". +Issues: github.com/acmesh-official/acme.sh/issues +Author: Nicolas Santorelli +' + +# +# EuroDNS DNS API +# +# EuroDNS API documentation: +# https://docapi.eurodns.com +# +# Usage: +# export EURODNS_APP_ID="your-app-id" +# export EURODNS_API_KEY="your-api-key" +# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com +# +# The credentials will be saved in ~/.acme.sh/account.conf +# +# Optional: +# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL +# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS) +# + +EURODNS_API_DEFAULT="https://rest-api.eurodns.com" +EURODNS_TTL_DEFAULT=600 + +######## Public functions ##################### + +#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_eurodns_add() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + + _info "Using EuroDNS DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}" + EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}" + EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}" + EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}" + EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}" + EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}" + + if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then + EURODNS_APP_ID="" + EURODNS_API_KEY="" + _err "You didn't specify EuroDNS App ID and API Key." + _err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again." + return 1 + fi + + _saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID" + _saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY" + if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then + _saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL" + fi + if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then + _saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL" + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + _info "Adding TXT record" + if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then + _info "Added TXT record successfully." + return 0 + else + _err "Failed to add TXT record." + return 1 + fi +} + +#Usage: fulldomain txtvalue +dns_eurodns_rm() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + + _info "Using EuroDNS DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}" + EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}" + EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}" + EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}" + + if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then + EURODNS_APP_ID="" + EURODNS_API_KEY="" + _err "You didn't specify EuroDNS App ID and API Key." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + _info "Removing TXT record" + if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then + _info "Removed TXT record successfully." + return 0 + else + _err "Failed to remove TXT record." + return 1 + fi +} + +#################### Private functions below ################################## + +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 + fi + + _eurodns_rest GET "dns-zones/$h" + if [ "$?" != "0" ]; then + if [ "$_code" = "404" ]; then + _debug "Zone $h not found, continuing..." + else + _err "API error looking up zone $h" + return 1 + fi + p=$i + i=$(_math "$i" + 1) + continue + fi + + if _contains "$response" '"name"'; then + if [ "$i" = "1" ]; then + _sub_domain="@" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + fi + _domain=$h + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done + + return 1 +} + +_eurodns_add_txt_record() { + domain=$1 + subdomain=$2 + txtvalue=$3 + + data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]' + + _debug "Adding TXT record via API" + if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then + if _contains "$response" "$txtvalue"; then + return 0 + fi + fi + _err "Failed to add TXT record" + return 1 +} + +_eurodns_rm_txt_record() { + domain=$1 + subdomain=$2 + txtvalue=$3 + + _debug "Getting current zone data for $domain" + + if ! _eurodns_rest GET "dns-zones/$domain"; then + _err "Failed to get zone data" + return 1 + fi + + zone_data=$(echo "$response" | _normalizeJson) + _debug2 zone_data "$zone_data" + + # Find the record ID matching our TXT record + record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1) + _debug record_id "$record_id" + + if [ -z "$record_id" ]; then + _info "TXT record not found or already removed" + return 0 + fi + + _debug "Deleting TXT record $record_id" + if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then + _err "Failed to delete TXT record" + return 1 + fi + + return 0 +} + +# Usage: _eurodns_rest METHOD ENDPOINT [DATA] +_eurodns_rest() { + method=$1 + endpoint=$2 + data="$3" + + export _H1="X-APP-ID: $EURODNS_APP_ID" + export _H2="X-API-KEY: $EURODNS_API_KEY" + export _H3="Content-Type: application/json" + + url="$EURODNS_API_URL/$endpoint" + + _debug2 url "$url" + _debug2 method "$method" + _debug2 data "$data" + + : >"$HTTP_HEADER" + + if [ "$method" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url" "" "$method")" + fi + + _ret="$?" + unset _H1 _H2 _H3 + _debug2 response "$response" + + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug2 _code "$_code" + + if [ "$_ret" != "0" ]; then + _err "Error calling API: $endpoint" + return 1 + fi + + if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then + if [ "$_code" != "404" ]; then + _err "API error (HTTP $_code): $response" + fi + return 1 + fi + + return 0 +} From cbb8e9068c2ee602c436bfd67e99e4c032f576fa Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 1 May 2026 12:56:00 +0200 Subject: [PATCH 469/689] support dns-persist-01 --- README.md | 68 +++++++++++++++++++----- acme.sh | 155 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 197 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 188af0e1..b2f22f3f 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ | 🌐 DNS mode | Use DNS TXT records | | 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification | | 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification | +| 📌 DNS persist mode | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) | --- @@ -396,7 +397,50 @@ acme.sh --renew -d example.com --- -### 🔟 Issue Certificates of Different Key Types (ECC or RSA) +### 🔟 Use DNS Persist Mode + +📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/) + +DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals. + +#### 🪄 Step 1: Print the TXT record value + +```bash +acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] +``` + +Options: + +| Flag | Description | +|------|-------------| +| `--server ` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. | +| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. | +| `--dns-persist-ca-name ` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. | + +You should get an output like: + +```sh +TXT domain: _validation-persist.example.com +TXT value: "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789" +``` + +#### ✍️ Step 2: Add the TXT record to your DNS + +Add the printed `TXT domain` / `TXT value` pair as a TXT record at your DNS provider, then wait for it to propagate. + +#### 📜 Step 3: Issue the certificate + +```bash +acme.sh --issue -d example.com --dns-persist +``` + +✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly. + +> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed. + +--- + +### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA) Just set the `keylength` to a valid, supported value. @@ -427,7 +471,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com - --- -### 1️⃣1️⃣ Issue Wildcard Certificates +### 1️⃣2️⃣ Issue Wildcard Certificates It's simple! Just give a wildcard domain as the `-d` parameter: @@ -439,7 +483,7 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf --- -### 1️⃣2️⃣ How to Renew Certificates +### 1️⃣3️⃣ How to Renew Certificates > 🔄 No need to renew manually! All certs will be renewed automatically every **30** days. @@ -457,7 +501,7 @@ acme.sh --renew -d example.com --force --ecc --- -### 1️⃣3️⃣ How to Stop Certificate Renewal +### 1️⃣4️⃣ How to Stop Certificate Renewal To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: @@ -471,7 +515,7 @@ The cert/key file is not removed from the disk. --- -### 1️⃣4️⃣ How to Upgrade acme.sh +### 1️⃣5️⃣ How to Upgrade acme.sh > 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code. @@ -495,25 +539,25 @@ acme.sh --upgrade --auto-upgrade 0 --- -### 1️⃣5️⃣ Issue a Certificate from an Existing CSR +### 1️⃣6️⃣ Issue a Certificate from an Existing CSR 📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR --- -### 1️⃣6️⃣ Send Notifications in Cronjob +### 1️⃣7️⃣ Send Notifications in Cronjob 📚 https://github.com/acmesh-official/acme.sh/wiki/notify --- -### 1️⃣7️⃣ Under the Hood +### 1️⃣8️⃣ Under the Hood > 🔧 Speak ACME language using shell, directly to "Let's Encrypt". --- -### 1️⃣8️⃣ Acknowledgments +### 1️⃣9️⃣ Acknowledgments | Project | Link | |---------|------| @@ -555,7 +599,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 1️⃣9️⃣ License & Others +### 2️⃣0️⃣ License & Others 📄 **License:** GPLv3 @@ -565,7 +609,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣0️⃣ Donate +### 2️⃣1️⃣ Donate > 💝 Your donation makes **acme.sh** better! @@ -577,7 +621,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣1️⃣ About This Repository +### 2️⃣2️⃣ About This Repository > [!NOTE] > This repository is officially maintained by ZeroSSL as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community! diff --git a/acme.sh b/acme.sh index 71e40d24..dbed1359 100755 --- a/acme.sh +++ b/acme.sh @@ -59,6 +59,7 @@ DEFAULT_OPENSSL_BIN="openssl" VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" VTYPE_ALPN="tls-alpn-01" +VTYPE_DNS_PERSIST="dns-persist-01" ID_TYPE_DNS="dns" ID_TYPE_IP="ip" @@ -71,6 +72,7 @@ NO_VALUE="no" W_DNS="dns" W_ALPN="alpn" +W_DNS_PERSIST="dns_persist" DNS_ALIAS_PREFIX="=" MODE_STATELESS="stateless" @@ -4028,6 +4030,85 @@ deactivateaccount() { fi } +#domain wildcard ca_name +#Print the TXT record(s) the user must add to enable persistent DNS validation +#per draft-ietf-acme-dns-persist-01. +makednspersistvalue() { + _mdpv_domain="$1" + _mdpv_wildcard="$2" + _mdpv_ca_name="$3" + + if [ -z "$_mdpv_domain" ]; then + _err "Please specify a domain with -d." + return 1 + fi + + _initpath + + _accUri="$(_readcaconf ACCOUNT_URL)" + if [ -z "$_accUri" ]; then + _info "No account is registered for $ACME_DIRECTORY yet, registering one now..." + if ! _regAccount "$DEFAULT_ACCOUNT_KEY_LENGTH"; then + _err "Cannot register account." + return 1 + fi + _accUri="$(_readcaconf ACCOUNT_URL)" + fi + + if [ -z "$_accUri" ]; then + _err "Cannot determine the ACME account URL." + return 1 + fi + _debug "Account URL" "$_accUri" + + _txt_name="_validation-persist.$_mdpv_domain" + + _txt_suffix="; accounturi=$_accUri" + if [ "$_mdpv_wildcard" = "1" ]; then + _txt_suffix="$_txt_suffix; policy=wildcard" + fi + + if [ -n "$_mdpv_ca_name" ]; then + _info "" + _info "Add the following DNS TXT record to enable persistent DNS validation:" + _info "" + _info "$(printf 'TXT domain: %s' "$(__green "$_txt_name")")" + _info "$(printf 'TXT value: %s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")" + _info "" + return 0 + fi + + _info "Fetching ACME directory: $ACME_DIRECTORY" + _dir_resp="$(_get "$ACME_DIRECTORY" "" 30)" + if [ "$?" != "0" ] || [ -z "$_dir_resp" ]; then + _err "Cannot fetch ACME directory: $ACME_DIRECTORY" + return 1 + fi + _dir_resp="$(echo "$_dir_resp" | _json_decode)" + _debug2 _dir_resp "$_dir_resp" + + _caa_array="$(echo "$_dir_resp" | tr -d ' \r\n\t' | _egrep_o '"caaIdentities":\[[^]]*\]')" + _debug2 _caa_array "$_caa_array" + _caaids="$(echo "$_caa_array" | sed 's/.*\[//' | sed 's/\].*//' | tr ',' '\n' | tr -d '"')" + _debug2 _caaids "$_caaids" + + if [ -z "$_caaids" ]; then + _err "The directory does not include 'caaIdentities'. Please specify --dns-persist-ca-name explicitly." + return 1 + fi + + _info "" + _info "Add ANY ONE of the following DNS TXT records to enable persistent DNS validation." + _info "(You only need to add one; pick whichever issuer identity you prefer.)" + for _id in $_caaids; do + [ -z "$_id" ] && continue + _info "" + _info "$(printf 'TXT domain: %s' "$(__green "$_txt_name")")" + _info "$(printf 'TXT value : %s' "$(__green "\"$_id$_txt_suffix\"")")" + done + _info "" +} + # domain folder file _findHook() { _hookdomain="$1" @@ -4806,7 +4887,9 @@ $_authorizations_map" vtype="$VTYPE_HTTP" #todo, v2 wildcard force to use dns - if _startswith "$_currentRoot" "$W_DNS"; then + if [ "$_currentRoot" = "$W_DNS_PERSIST" ]; then + vtype="$VTYPE_DNS_PERSIST" + elif _startswith "$_currentRoot" "$W_DNS"; then vtype="$VTYPE_DNS" fi @@ -4864,18 +4947,7 @@ $_authorizations_map" fi if [ -z "$keyauthorization" ]; then - token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" - _debug token "$token" - - if [ -z "$token" ]; then - _err "Cannot get domain token $entry" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" - _debug uri "$uri" if [ -z "$uri" ]; then @@ -4884,8 +4956,26 @@ $_authorizations_map" _on_issue_err "$_post_hook" return 1 fi - keyauthorization="$token.$thumbprint" - _debug keyauthorization "$keyauthorization" + + if [ "$vtype" = "$VTYPE_DNS_PERSIST" ]; then + # dns-persist-01 challenges have no token; the TXT record is + # provisioned out-of-band. Use a non-empty placeholder so the + # downstream code does not treat this entry as already verified. + keyauthorization="$VTYPE_DNS_PERSIST" + _debug keyauthorization "$keyauthorization" + else + token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token "$token" + + if [ -z "$token" ]; then + _err "Cannot get domain token $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" + fi fi dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url" @@ -7158,6 +7248,8 @@ Commands: --update-account Update account info. --register-account Register account key. --deactivate-account Deactivate the account. + --make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation + (draft-ietf-acme-dns-persist-01). Use with -d . --create-account-key Create an account private key, professional use. --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. @@ -7205,6 +7297,10 @@ Parameters: --dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted. See: $_DNS_API_WIKI + --dns-persist Use dns-persist-01 validation (draft-ietf-acme-dns-persist-01). + Requires the persistent _validation-persist TXT record to already + exist. Use '--make-dns-persist-value' to print the value to add. + --dnssleep The time in seconds to wait for all the txt records to propagate in dns api mode. It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically. -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521. @@ -7215,6 +7311,14 @@ Parameters: --eab-kid Key Identifier for External Account Binding. --eab-hmac-key HMAC key for External Account Binding. + --dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the + generated TXT record so the issuer is also authorized for wildcards + and subdomains (draft-ietf-acme-dns-persist-01). + --dns-persist-ca-name Used with '--make-dns-persist-value'. Use the given CA identity domain + (e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If + omitted, the identities are read from the ACME directory's + 'caaIdentities' field and one record is printed per identity. + These parameters are to install the cert to nginx/Apache or any other server after issue/renew a cert: @@ -7585,6 +7689,8 @@ _process() { _valid_to="" _certificate_profile="" _extended_key_usage="" + _dns_persist_wildcard="" + _dns_persist_ca_name="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7679,6 +7785,16 @@ _process() { --deactivate-account) _CMD="deactivateaccount" ;; + --make-dns-persist-value | --makednspersistvalue) + _CMD="makednspersistvalue" + ;; + --dns-persist-wildcard | --dnspersistwildcard) + _dns_persist_wildcard="1" + ;; + --dns-persist-ca-name | --dnspersistcaname) + _dns_persist_ca_name="$2" + shift + ;; --set-notify) _CMD="setnotify" ;; @@ -7822,6 +7938,14 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; + --dns-persist) + wvalue="$W_DNS_PERSIST" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; --dnssleep) _dnssleep="$2" Le_DNSSleep="$_dnssleep" @@ -8238,6 +8362,9 @@ _process() { deactivateaccount) deactivateaccount ;; + makednspersistvalue) + makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" + ;; list) list "$_listraw" "$_domain" ;; From 8d5a5a0e0d96e0510d3e4e274204e8da1ea8b52b Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 1 May 2026 14:43:53 +0200 Subject: [PATCH 470/689] support ARI by default --- acme.sh | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/acme.sh b/acme.sh index dbed1359..9e37a406 100755 --- a/acme.sh +++ b/acme.sh @@ -4072,8 +4072,8 @@ makednspersistvalue() { _info "" _info "Add the following DNS TXT record to enable persistent DNS validation:" _info "" - _info "$(printf 'TXT domain: %s' "$(__green "$_txt_name")")" - _info "$(printf 'TXT value: %s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")" + _info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")" + _info "$(printf 'TXT persist value :%s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")" _info "" return 0 fi @@ -4103,8 +4103,8 @@ makednspersistvalue() { for _id in $_caaids; do [ -z "$_id" ] && continue _info "" - _info "$(printf 'TXT domain: %s' "$(__green "$_txt_name")")" - _info "$(printf 'TXT value : %s' "$(__green "\"$_id$_txt_suffix\"")")" + _info "$(printf 'TXT persist domain:%s' "$(__green "$_txt_name")")" + _info "$(printf 'TXT persist value :%s' "$(__green "\"$_id$_txt_suffix\"")")" done _info "" } @@ -4790,13 +4790,41 @@ issue() { if [ "$_certificate_profile" ]; then _newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\"" fi + + # RFC 9773 Section 5: include "replaces" only when this is an actual + # renewal (--renew path), the CA advertises renewalInfo, and a prior + # cert exists. --issue (even with --force) is not a renewal per RFC 9773 + # which speaks of "a clear predecessor certificate" issued by this CA. + _replaces_certID="" + if [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then + _replaces_certID="$(_getARICertID "$CERT_PATH")" + _debug "Adding ARI replaces" "$_replaces_certID" + fi + _debug "STEP 1, Ordering a Certificate" - if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then + _newOrderReplacesObj="$_newOrderObj" + if [ "$_replaces_certID" ]; then + _newOrderReplacesObj="$_newOrderObj,\"replaces\": \"$_replaces_certID\"" + fi + if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderReplacesObj}"; then _err "Error creating new order." _clearup _on_issue_err "$_post_hook" return 1 fi + # RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs + # (Let's Encrypt) may also reject with a malformed error if the prior cert + # was issued by a different issuer / different CA. Retry without "replaces" + # whenever the failure mentions ARI or the replaces field. + if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then + _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." + if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then + _err "Error creating new order." + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + fi if _contains "$response" "invalid"; then if echo "$response" | _normalizeJson | grep '"status":"invalid"' >/dev/null 2>&1; then _err "Create new order with invalid status." @@ -5581,6 +5609,30 @@ $_authorizations_map" Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") fi + + # RFC 9773 ARI: if the CA exposes renewalInfo, override Le_NextRenewTime + # with a time picked at random within the suggestedWindow. This both gives + # the CA full control over renewal scheduling and disperses renewals across + # the network so all clients don't hit the CA at the same instant. + if [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then + _ari_resp_new="$(_get_ARI "$CERT_PATH")" + _debug2 "_ari_resp_new" "$_ari_resp_new" + _ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')" + _ari_end_new="$(echo "$_ari_resp_new" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')" + if [ "$_ari_start_new" ] && [ "$_ari_end_new" ]; then + _ari_start_t_new="$(_date2time "$(echo "$_ari_start_new" | sed 's/\.[0-9]*//')")" + _ari_end_t_new="$(_date2time "$(echo "$_ari_end_new" | sed 's/\.[0-9]*//')")" + if [ "$_ari_start_t_new" ] && [ "$_ari_end_t_new" ] && [ "$_ari_end_t_new" -gt "$_ari_start_t_new" ]; then + _ari_window=$(_math "$_ari_end_t_new" - "$_ari_start_t_new") + _ari_offset=$(_math "$(_time)" % "$_ari_window") + Le_NextRenewTime=$(_math "$_ari_start_t_new" + "$_ari_offset") + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + _info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")" + _info "Next renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" + fi + fi + fi + _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" @@ -5676,7 +5728,31 @@ renew() { _debug2 "initpath again." _initpath "$Le_Domain" "$_isEcc" - if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + # ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check. + # If the window has started, renew now even if Le_NextRenewTime is in the future. + _ari_should_renew="" + if [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then + if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then + _ari_resp="$(_get_ARI "$CERT_PATH")" + _debug2 "_ari_resp" "$_ari_resp" + _ari_start="$(echo "$_ari_resp" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')" + _ari_end="$(echo "$_ari_resp" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')" + _debug "ARI suggestedWindow.start" "$_ari_start" + _debug "ARI suggestedWindow.end" "$_ari_end" + if [ "$_ari_start" ]; then + _ari_start_t="$(_date2time "$(echo "$_ari_start" | sed 's/\.[0-9]*//')")" + _debug "_ari_start_t" "$_ari_start_t" + if [ "$_ari_start_t" ] && [ "$(_time)" -ge "$_ari_start_t" ]; then + _info "ARI suggestedWindow has started ($(__green "$_ari_start")), proceeding with renewal." + _ari_should_renew="1" + else + _info "ARI suggestedWindow starts at: $(__green "$_ari_start")" + fi + fi + fi + fi + + if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then _info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")" _info "Add '$(__red '--force')' to force renewal." if [ -z "$_ACME_IN_RENEWALL" ]; then @@ -6698,21 +6774,29 @@ _getSerial() { } #cert -_get_ARI() { +#Compute the ARI/replaces certID for a cert: base64url(AKI).base64url(Serial) +#per RFC 9773 Section 4.1. +_getARICertID() { _cert="$1" _aki=$(_getAKI "$_cert") _ser=$(_getSerial "$_cert") _debug2 "_aki" "$_aki" _debug2 "_ser" "$_ser" - _akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)" + _akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)" _debug2 "_akiurl" "$_akiurl" - _serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)" + _serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)" _debug2 "_serurl" "$_serurl" - _ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl" - _get "$_ARI_URL" + printf "%s.%s" "$_akiurl" "$_serurl" +} +#cert +_get_ARI() { + _cert="$1" + _ari_certID="$(_getARICertID "$_cert")" + _ARI_URL="$ACME_RENEWAL_INFO/$_ari_certID" + _get "$_ARI_URL" } # Detect profile file if not specified as environment variable From 0d772313500e7091598d07ecba02033e0e8432c2 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 1 May 2026 15:10:27 +0200 Subject: [PATCH 471/689] add --dns-persist-days --- README.md | 43 ++++++++++++++++++++++++++++++++++++++----- acme.sh | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b2f22f3f..b87eeedd 100644 --- a/README.md +++ b/README.md @@ -406,7 +406,7 @@ DNS persist mode lets you place a **single, long‑lived `_validation-persist` T #### 🪄 Step 1: Print the TXT record value ```bash -acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] +acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365] ``` Options: @@ -416,17 +416,18 @@ Options: | `--server ` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. | | `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. | | `--dns-persist-ca-name ` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. | +| `--dns-persist-days ` | Adds `persistUntil=` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. | You should get an output like: ```sh -TXT domain: _validation-persist.example.com -TXT value: "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789" +TXT persist domain:_validation-persist.example.com +TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789" ``` #### ✍️ Step 2: Add the TXT record to your DNS -Add the printed `TXT domain` / `TXT value` pair as a TXT record at your DNS provider, then wait for it to propagate. +Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate. #### 📜 Step 3: Issue the certificate @@ -485,7 +486,7 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf ### 1️⃣3️⃣ How to Renew Certificates -> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days. +> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below). However, you can force a renewal: @@ -499,6 +500,38 @@ acme.sh --renew -d example.com --force acme.sh --renew -d example.com --force --ecc ``` +#### 📡 ACME Renewal Information (ARI) — RFC 9773 + +If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**: + +| What | When | Why | +|------|------|-----| +| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) | +| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant | +| 🔗 **Sends `replaces=`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) | +| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) | + +**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true: + +1. `--force` is given +2. The CA's **ARI `suggestedWindow` has started** +3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI) + +You can see the resulting next renewal time (already ARI-picked when applicable) in: + +```sh +acme.sh --info -d example.com +# Look for: Le_NextRenewTimeStr=... +``` + +For the live ARI window the CA is currently advertising, run with `--debug 2`: + +```sh +acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow' +``` + +> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change. + --- ### 1️⃣4️⃣ How to Stop Certificate Renewal diff --git a/acme.sh b/acme.sh index 9e37a406..1dabd2a3 100755 --- a/acme.sh +++ b/acme.sh @@ -4030,19 +4030,33 @@ deactivateaccount() { fi } -#domain wildcard ca_name +#domain wildcard ca_name days #Print the TXT record(s) the user must add to enable persistent DNS validation #per draft-ietf-acme-dns-persist-01. makednspersistvalue() { _mdpv_domain="$1" _mdpv_wildcard="$2" _mdpv_ca_name="$3" + _mdpv_days="$4" if [ -z "$_mdpv_domain" ]; then _err "Please specify a domain with -d." return 1 fi + if [ -n "$_mdpv_days" ]; then + case "$_mdpv_days" in + '' | *[!0-9]*) + _err "--dns-persist-days must be a positive integer, got: $_mdpv_days" + return 1 + ;; + esac + if [ "$_mdpv_days" -lt 1 ]; then + _err "--dns-persist-days must be at least 1." + return 1 + fi + fi + _initpath _accUri="$(_readcaconf ACCOUNT_URL)" @@ -4067,6 +4081,11 @@ makednspersistvalue() { if [ "$_mdpv_wildcard" = "1" ]; then _txt_suffix="$_txt_suffix; policy=wildcard" fi + if [ -n "$_mdpv_days" ]; then + _persist_until=$(_math "$(_time)" + "$_mdpv_days" \* 86400) + _txt_suffix="$_txt_suffix; persistUntil=$_persist_until" + _info "persistUntil set to $(__green "$(_time2str "$_persist_until")") ($_mdpv_days days from now)" + fi if [ -n "$_mdpv_ca_name" ]; then _info "" @@ -7402,6 +7421,10 @@ Parameters: (e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If omitted, the identities are read from the ACME directory's 'caaIdentities' field and one record is printed per identity. + --dns-persist-days Used with '--make-dns-persist-value'. Add a 'persistUntil' field to + the TXT record so the record self-expires N days from now (the CA + will refuse new validations against the record after that time). + If omitted, the record has no expiry. These parameters are to install the cert to nginx/Apache or any other server after issue/renew a cert: @@ -7775,6 +7798,7 @@ _process() { _extended_key_usage="" _dns_persist_wildcard="" _dns_persist_ca_name="" + _dns_persist_days="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7879,6 +7903,10 @@ _process() { _dns_persist_ca_name="$2" shift ;; + --dns-persist-days | --dnspersistdays) + _dns_persist_days="$2" + shift + ;; --set-notify) _CMD="setnotify" ;; @@ -8447,7 +8475,7 @@ _process() { deactivateaccount ;; makednspersistvalue) - makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" + makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" "$_dns_persist_days" ;; list) list "$_listraw" "$_domain" From fe5490e0eca7cf1b4cc1079a5239db8e9a09ff7b Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 1 May 2026 15:55:09 +0200 Subject: [PATCH 472/689] fix OpenIndiana.yml --- .github/workflows/DNS.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 00d180b9..c727ba1b 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -634,7 +634,7 @@ jobs: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs - prepare: pkg install socat + prepare: pkg update || true; pkg install socat run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index b5061ba7..99733b44 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -67,7 +67,7 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" - prepare: pkg install socat curl + prepare: pkg update || true; pkg install socat curl sync: nfs run: | cd ../acmetest \ From 3230d00c3d6aa6239000f3f00a3d2af4004b8cd1 Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Fri, 1 May 2026 16:02:46 +0200 Subject: [PATCH 473/689] Allow renew time relative to the expiration date (#4457) * Allow renew time relative to the expiration date --- acme.sh | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 1dabd2a3..0f97bcea 100755 --- a/acme.sh +++ b/acme.sh @@ -1015,6 +1015,24 @@ _checkcert() { fi } +#file +_enddate() { + _cf="$1" + _res="$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -enddate -in "$_cf")" + if [ "$?" != "0" ] || [ -z "$_res" ]; then + return 1 + fi + + case "$_res" in + notAfter=*) + echo "${_res#notAfter=}" + ;; + *) + return 1 + ;; + esac +} + #Usage: hashalg [outputhex] #Output Base64-encoded digest _digest() { @@ -1846,6 +1864,25 @@ _date2time() { return 1 } +#support the output format of openssl -enddate: +# Apr 01 08:10:33 2022 GMT to 1641283833 +_ssldate2time() { + #Linux + if date -u -d "$1" +"%s" 2>/dev/null; then + return + fi + #Solaris + if gdate -u -d "$1" +"%s" 2>/dev/null; then + return + fi + #Mac/BSD + if date -j -f "%b %d %T %Y %Z" "$1" +"%s" 2>/dev/null; then + return + fi + _err "Cannot parse _ssldate2time $1" + return 1 +} + _utc_date() { date -u "+%Y-%m-%d %H:%M:%S" } @@ -5564,7 +5601,7 @@ $_authorizations_map" Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime") _savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr" - if [ -z "$Le_RenewalDays" ] || [ "$Le_RenewalDays" -lt "0" ]; then + if [ -z "$Le_RenewalDays" ]; then Le_RenewalDays="$DEFAULT_RENEW" else _savedomainconf "Le_RenewalDays" "$Le_RenewalDays" @@ -5623,6 +5660,20 @@ $_authorizations_map" Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") fi fi + elif [ "$Le_RenewalDays" -lt "0" ]; then + _enddate_value=$(_enddate "$CERT_PATH") + if [ "$?" != "0" ] || [ -z "$_enddate_value" ]; then + _err "Failed to get certificate end date for $CERT_PATH" + return 1 + fi + + _endtime=$(_ssldate2time "$_enddate_value") + if [ "$?" != "0" ] || [ -z "$_endtime" ]; then + _err "Cannot parse _enddate_value: $_enddate_value" + return 1 + fi + Le_NextRenewTime=$(_math "$_endtime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") else Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) @@ -7446,6 +7497,7 @@ Parameters: -m, --email Specifies the account email, only valid for the '--install' and '--update-account' command. --accountkey Specifies the account key path, only valid for the '--install' command. --days Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days. + Negative values could be used to specify a number of days relative to the expiration date of the certificate. --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. --local-address Specifies the standalone/tls server listening address, in case you have multiple ip addresses. From 355b121c79e643d8b382f273cfb262acb475b355 Mon Sep 17 00:00:00 2001 From: Curd Becker <12437061+curdbecker@users.noreply.github.com> Date: Fri, 1 May 2026 17:15:57 +0200 Subject: [PATCH 474/689] Add deployment plugin for Windows RDP via OpenSSH (#6925) * Add deployment plugin for Windows RDP via OpenSSH --- acme.sh | 19 +++++ deploy/windows_rdp.sh | 158 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 deploy/windows_rdp.sh diff --git a/acme.sh b/acme.sh index 0f97bcea..6c191c92 100755 --- a/acme.sh +++ b/acme.sh @@ -1057,6 +1057,25 @@ _digest() { } +#Usage: certpath hashalg +#Output certificate fingerprint without colons +_fingerprint() { + cert="$1" + alg="$2" + if [ -z "$alg" ]; then + _usage "Usage: _fingerprint certpath hashalg" + return 1 + fi + + if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then + # openssl prints "SHA1 Fingerprint=AA:BB:CC:..."; strip prefix and colons. + ${ACME_OPENSSL_BIN:-openssl} x509 -in "$cert" -noout -fingerprint -"$alg" | sed 's/.*=//; s/://g' + else + _err "$alg is not supported yet" + return 1 + fi +} + #Usage: hashalg secret_hex [outputhex] #Output binary hmac _hmac() { diff --git a/deploy/windows_rdp.sh b/deploy/windows_rdp.sh new file mode 100644 index 00000000..e708e9a7 --- /dev/null +++ b/deploy/windows_rdp.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env sh + +# install a certificate on a Windows host over OpenSSH and bind it to the Remote +# Desktop listener (RDP-Tcp). +# +# One ssh invocation does the whole job: +# * the PFX is built locally, base64'd, and embedded as a string literal +# inside a generated PowerShell script; +# * the script is piped to `powershell.exe -Command -` over ssh. No scp, +# no temp files on the Windows host. +# +# First run: +# export DEPLOY_WIN_RDP_HOST=winserver.example.com +# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp +# +# Available variables: +# DEPLOY_WIN_RDP_HOST required SSH host +# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config) +# DEPLOY_WIN_RDP_PORT optional SSH port, default 22 +# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g. +# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes" +# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp +# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install. +# Active RDP sessions will drop! + +windows_rdp_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 ! _exists "ssh"; then + _err "ssh is required but was not found in PATH." + return 1 + fi + + # ---- configuration ------------------------------------------------------ + _getdeployconf DEPLOY_WIN_RDP_HOST + _getdeployconf DEPLOY_WIN_RDP_USER + _getdeployconf DEPLOY_WIN_RDP_PORT + _getdeployconf DEPLOY_WIN_RDP_SSH_OPTS + _getdeployconf DEPLOY_WIN_RDP_LISTENER + _getdeployconf DEPLOY_WIN_RDP_RESTART + + if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then + _err "DEPLOY_WIN_RDP_HOST must be set." + return 1 + fi + + _savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST" + [ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER" + [ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT" + [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS" + [ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER" + [ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART" + + _port="${DEPLOY_WIN_RDP_PORT:-22}" + _listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}" + if [ -n "$DEPLOY_WIN_RDP_USER" ]; then + _target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST" + else + _target="$DEPLOY_WIN_RDP_HOST" + fi + _pfx_pass="acme" + + # ---- build thumbprint + PFX locally ------------------------------------ + _thumb="$(_fingerprint "$_ccert" 'sha1')" + if [ -z "$_thumb" ]; then + _err "Failed to compute certificate thumbprint." + return 1 + fi + _debug "Thumbprint: $_thumb" + + _debug "Building PFX at $_pfx_file" + _pfx_file="$(_mktemp)" + if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then + _err "Failed to build PFX archive." + rm -f "$_pfx_file" + return 1 + fi + _pfx_b64=$(_base64 "multiline" <"$_pfx_file") + rm -f "$_pfx_file" + + # ---- build installer script -------------------------------------------- + if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then + _restart_ps='Restart-Service -Name TermService -Force' + else + _restart_ps='# New RdP connections will pick up the new cert automatically.' + fi + + # Escape every literal `$` with `\$` so the shell does not expand it. + # Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener. + _ps1=$( + cat < Date: Sat, 2 May 2026 10:50:12 +0200 Subject: [PATCH 475/689] revert --- .github/workflows/DNS.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index c727ba1b..00d180b9 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -634,7 +634,7 @@ jobs: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs - prepare: pkg update || true; pkg install socat + prepare: pkg install socat run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index 99733b44..b5061ba7 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -67,7 +67,7 @@ jobs: envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" - prepare: pkg update || true; pkg install socat curl + prepare: pkg install socat curl sync: nfs run: | cd ../acmetest \ From 798531968736ed1211f553f32a49c7c7f0e3cef0 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 2 May 2026 11:22:48 +0200 Subject: [PATCH 476/689] add wiki --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b87eeedd..ba5d3591 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ | 🌐 DNS mode | Use DNS TXT records | | 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification | | 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification | -| 📌 DNS persist mode | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) | +| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) | --- @@ -399,6 +399,8 @@ acme.sh --renew -d example.com ### 🔟 Use DNS Persist Mode +📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode + 📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/) DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals. @@ -502,6 +504,8 @@ acme.sh --renew -d example.com --force --ecc #### 📡 ACME Renewal Information (ARI) — RFC 9773 +📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI + If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**: | What | When | Why | From 7b19070d98ae6022fe1e202d96b5f7041c4fd8cb Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 5 May 2026 20:26:37 +0200 Subject: [PATCH 477/689] fix ari https://github.com/acmesh-official/acme.sh/issues/6942#issuecomment-4381535765 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 6c191c92..7e00c76a 100755 --- a/acme.sh +++ b/acme.sh @@ -6853,7 +6853,7 @@ deactivate() { #cert _getAKI() { _cert="$1" - openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :' + openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | _egrep_o "[A-F0-9:]+" | tr -d ':' } #cert From 36667ab6568207b31c4828c0c0b23ea0e2efd469 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:03:42 +0200 Subject: [PATCH 478/689] fix ari https://github.com/acmesh-official/acme.sh/issues/6942#issuecomment-4382355708 --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 7e00c76a..1ff1d9a9 100755 --- a/acme.sh +++ b/acme.sh @@ -6853,13 +6853,13 @@ deactivate() { #cert _getAKI() { _cert="$1" - openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | _egrep_o "[A-F0-9:]+" | tr -d ':' + ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ': ' | sed "s/keyid//" } #cert _getSerial() { _cert="$1" - openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2 + ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -serial -noout | cut -d = -f 2 } #cert From ac75c54ade31363a84bb5b226e34912eb287c833 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:21:21 +0200 Subject: [PATCH 479/689] support NO_ARI=1 https://github.com/acmesh-official/acme.sh/discussions/6938 --- acme.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 1ff1d9a9..4d12446a 100755 --- a/acme.sh +++ b/acme.sh @@ -4870,8 +4870,12 @@ issue() { # renewal (--renew path), the CA advertises renewalInfo, and a prior # cert exists. --issue (even with --force) is not a renewal per RFC 9773 # which speaks of "a clear predecessor certificate" issued by this CA. + # NO_ARI=1 (env, account.conf, or ca.conf) disables ARI entirely, so the + # "replaces" field is also omitted. _replaces_certID="" - if [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then + if [ "$NO_ARI" = "1" ]; then + _debug "NO_ARI=1, omitting ARI 'replaces' field from newOrder" + elif [ "$_ACME_IS_RENEW" = "1" ] && [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then _replaces_certID="$(_getARICertID "$CERT_PATH")" _debug "Adding ARI replaces" "$_replaces_certID" fi @@ -5703,7 +5707,11 @@ $_authorizations_map" # with a time picked at random within the suggestedWindow. This both gives # the CA full control over renewal scheduling and disperses renewals across # the network so all clients don't hit the CA at the same instant. - if [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then + # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and fall back to + # the legacy time-based renewal calculation. + if [ "$NO_ARI" = "1" ]; then + _debug "NO_ARI=1, skipping ARI suggestedWindow override" + elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then _ari_resp_new="$(_get_ARI "$CERT_PATH")" _debug2 "_ari_resp_new" "$_ari_resp_new" _ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')" @@ -5819,8 +5827,12 @@ renew() { # ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check. # If the window has started, renew now even if Le_NextRenewTime is in the future. + # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only + # Le_NextRenewTime for the renewal decision. _ari_should_renew="" - if [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then + if [ "$NO_ARI" = "1" ]; then + _debug "NO_ARI=1, skipping ARI suggestedWindow check" + elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then _ari_resp="$(_get_ARI "$CERT_PATH")" _debug2 "_ari_resp" "$_ari_resp" From 47378b563001a975b3bfd6380c1a482ad0b28729 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:23:52 +0200 Subject: [PATCH 480/689] start 3.1.4 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 4d12446a..bee84471 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.3 +VER=3.1.4 PROJECT_NAME="acme.sh" From eaf4b62ba94bcb51edeabf84d2f055d2b9c59dde Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:47:25 +0200 Subject: [PATCH 481/689] fix for ari --- acme.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/acme.sh b/acme.sh index bee84471..df11e6f3 100755 --- a/acme.sh +++ b/acme.sh @@ -6884,6 +6884,16 @@ _getARICertID() { _debug2 "_aki" "$_aki" _debug2 "_ser" "$_ser" + # RFC 9773 Section 4.1 requires the DER-encoded INTEGER value bytes of + # serialNumber. When the high bit of the first byte is set (>= 0x80) DER + # prepends a 0x00 sign byte to keep the integer positive; openssl's hex + # output strips that, so add it back. Boulder (LE) accepts either form, + # but Sectigo (ZeroSSL) is strict and rejects newOrder with HTTP 401 + # "replaces field does not identify a certificate" if the byte is missing. + case "$_ser" in + [89aAbBcCdDeEfF]*) _ser="00$_ser" ;; + esac + _akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)" _debug2 "_akiurl" "$_akiurl" _serurl="$(echo "$_ser" | _h2b | _base64 | _url_replace)" From 1687cbd5b483b916559ddf4636ecbafee1656b28 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 6 May 2026 20:56:52 +0200 Subject: [PATCH 482/689] fix format --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index df11e6f3..ece30867 100755 --- a/acme.sh +++ b/acme.sh @@ -6891,7 +6891,7 @@ _getARICertID() { # but Sectigo (ZeroSSL) is strict and rejects newOrder with HTTP 401 # "replaces field does not identify a certificate" if the byte is missing. case "$_ser" in - [89aAbBcCdDeEfF]*) _ser="00$_ser" ;; + [89aAbBcCdDeEfF]*) _ser="00$_ser" ;; esac _akiurl="$(echo "$_aki" | _h2b | _base64 | _url_replace)" From 85408bfb4dd0414cf2153267252d5aee3227817a Mon Sep 17 00:00:00 2001 From: hebbet Date: Thu, 14 May 2026 13:41:25 +0200 Subject: [PATCH 483/689] Update renewal condition in acme.sh script --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index ece30867..aa9d34a0 100755 --- a/acme.sh +++ b/acme.sh @@ -4655,7 +4655,7 @@ issue() { if [ -f "$DOMAIN_CONF" ]; then Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime) _debug Le_NextRenewTime "$Le_NextRenewTime" - if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then _valid_to_saved=$(_readdomainconf Le_Valid_To) if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then _info "The domain is set to be valid to: $_valid_to_saved" From 010bd1111a7f44c02f6d8415a1ab915cd145226d Mon Sep 17 00:00:00 2001 From: Tom Sommer Date: Sun, 24 May 2026 22:20:40 +0200 Subject: [PATCH 484/689] Improve Simply.com API (#6933) Improve Simply.com API (#6933) --- dnsapi/dns_simply.sh | 92 +++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh index e0ad16e2..74e891ad 100644 --- a/dnsapi/dns_simply.sh +++ b/dnsapi/dns_simply.sh @@ -8,11 +8,7 @@ Options: SIMPLY_ApiKey API Key ' -#SIMPLY_Api="https://api.simply.com/2/" -SIMPLY_Api_Default="https://api.simply.com/2" - -#This is used for determining success of REST call -SIMPLY_SUCCESS_CODE='"status":200' +SIMPLY_Api="https://api.simply.com/2" ######## Public functions ##################### #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -72,7 +68,16 @@ dns_simply_rm() { return 1 fi - records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') + case "$_simply_http_code" in + 2*) ;; + *) + _err "Failed to fetch DNS records (HTTP $_simply_http_code)" + _err "$response" + return 1 + ;; + esac + + records=$(echo "$response" | tr '{' "\n" | grep -E 'record_id|type|data|name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') nr_of_deleted_records=0 _info "Fetching txt record" @@ -95,7 +100,7 @@ dns_simply_rm() { if [ "$record_id" -gt 0 ]; then - if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then + if ! _simply_delete_record "$_domain" "$record_id"; then _err "Record with id $record_id could not be deleted" return 1 fi @@ -122,14 +127,9 @@ dns_simply_rm() { #################### Private functions below ################################## _simply_load_config() { - SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}" SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}" SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}" - if [ -z "$SIMPLY_Api" ]; then - SIMPLY_Api="$SIMPLY_Api_Default" - fi - if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then SIMPLY_AccountName="" SIMPLY_ApiKey="" @@ -144,9 +144,6 @@ _simply_load_config() { } _simply_save_config() { - if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then - _saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api" - fi _saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName" _saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey" } @@ -163,26 +160,39 @@ _simply_get_all_records() { _get_root() { domain=$1 + + if ! _simply_rest GET "my/products/"; then + return 1 + fi + + case "$_simply_http_code" in + 2*) ;; + *) + _err "Failed to fetch product list (HTTP $_simply_http_code)" + _err "$response" + return 1 + ;; + esac + i=2 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) if [ -z "$h" ]; then - #not valid return 1 fi - if ! _simply_rest GET "my/products/$h/dns/"; then - return 1 - fi + _domain=$(printf "%s" "$response" | tr '}' '\n' | + grep -F -e "\"object\":\"$h\"" -e "\"name\":\"$h\"" -e "\"name_idn\":\"$h\"" | + sed -n 's/.*"object":"\([^"]*\)".*/\1/p' | + _head_n 1) - if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then - _debug "$h not found" - else + if [ -n "$_domain" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain="$h" return 0 fi + + _debug "No Simply.com product found for $h" p="$i" i=$(_math "$i" + 1) done @@ -194,39 +204,44 @@ _simply_add_record() { sub_domain=$2 txtval=$3 - data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}" + data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 120}" if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then - _err "Adding record not successfull!" + _err "Adding record not successful!" return 1 fi - if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then - _err "Call to API not sucessfull, see below message for more details" + case "$_simply_http_code" in + 2*) ;; + *) + _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details" _err "$response" return 1 - fi + ;; + esac return 0 } _simply_delete_record() { domain=$1 - sub_domain=$2 - record_id=$3 + record_id=$2 _debug record_id "Delete record with id $record_id" if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then - _err "Deleting record not successfull!" + _err "Deleting record not successful!" return 1 fi - if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then - _err "Call to API not sucessfull, see below message for more details" + case "$_simply_http_code" in + 2*) ;; + *) + _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details" _err "$response" return 1 - fi + ;; + esac return 0 } @@ -248,17 +263,24 @@ _simply_rest() { export _H2="Content-Type: application/json" + : >"$HTTP_HEADER" + if [ "$m" != "GET" ]; then response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")" else response="$(_get "$SIMPLY_Api/$ep")" fi - if [ "$?" != "0" ]; then + _ret="$?" + unset _H1 _H2 + + if [ "$_ret" != "0" ]; then _err "error $ep" return 1 fi + _simply_http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d' ' -f2 | tr -d '\r\n')" + response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" From ce07759cede1fddc70d303200137bc157e04e2fd Mon Sep 17 00:00:00 2001 From: Markus Ebner Date: Sun, 24 May 2026 22:24:00 +0200 Subject: [PATCH 485/689] [dnsapi] add IP-Projects dns hook (#6959) --- dnsapi/dns_ipprojects.sh | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 dnsapi/dns_ipprojects.sh diff --git a/dnsapi/dns_ipprojects.sh b/dnsapi/dns_ipprojects.sh new file mode 100644 index 00000000..dadd05f0 --- /dev/null +++ b/dnsapi/dns_ipprojects.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_ipprojects_info='IP-Projects DNS +Site: ip-projects.de/ +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects +Options: + IPP_Apikey API Key +Issues: github.com/acmesh-official/acme.sh/issues/6958 +Author: Markus Ebner +' + +IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}" +IPP_API="https://api.ip-projects.de/v1/dns/acme" + +######## Public functions ######## + +dns_ipprojects_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Using IP-Projects DNS API to add record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _IPP_load_credentials; then + return 1 + fi + + _IPP_api_request "add" "$fulldomain" "$txtvalue" +} + +dns_ipprojects_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Using IP-Projects DNS API to remove record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _IPP_load_credentials; then + return 1 + fi + + _IPP_api_request "remove" "$fulldomain" "$txtvalue" +} + +######## Private helpers ######## + +_IPP_load_credentials() { + IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}" + + if [ -z "$IPP_Apikey" ]; then + _err "You must export IPP_Apikey" + _err "e.g.: export IPP_Apikey=\"your_api_key\"" + return 1 + fi + + _saveaccountconf_mutable IPP_Apikey "$IPP_Apikey" + return 0 +} + +_IPP_api_request() { + action="$1" + domain="$2" + value="$3" + + url="$IPP_API/$action" + + data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}" + _debug url "$url" + _debug data "$data" + export _H1="X-API-Key: $IPP_Apikey" + + response="$(_post "$data" "$url" "" "POST" "application/json")" + ret="$?" + _ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') + + _debug response "$response" + + if [ "$ret" != "0" ]; then + _err "HTTP request failed" + return 1 + fi + + if [ "$_ipprojects_last_http_code" != "200" ]; then + _err "API returned an error [code: ${_ipprojects_last_http_code}]" + return 1 + fi + + return 0 +} From d9ce7fefa1f3106b39101b7864980f90d2616d9a Mon Sep 17 00:00:00 2001 From: "Simon V." <218359733+sim0n-v@users.noreply.github.com> Date: Sun, 24 May 2026 22:25:52 +0200 Subject: [PATCH 486/689] ARI - Add support for switching ACME Server during renewal (#6983) * ARI - Add support for switching ACME Server during renewal https://github.com/acmesh-official/acme.sh/issues/6964 * Restore old condition while adding malformed --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index aa9d34a0..f67749c0 100755 --- a/acme.sh +++ b/acme.sh @@ -4895,7 +4895,7 @@ issue() { # (Let's Encrypt) may also reject with a malformed error if the prior cert # was issued by a different issuer / different CA. Retry without "replaces" # whenever the failure mentions ARI or the replaces field. - if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then + if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then _err "Error creating new order." From 206f4494ac5977c359e33382c23575deab5c8cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20N=C3=A6ss?= <102598494+InvisibleDuck@users.noreply.github.com> Date: Sun, 24 May 2026 22:34:56 +0200 Subject: [PATCH 487/689] Add Poweradmin DNS API plugin (dns_poweradmin) (#6943) * Add Poweradmin DNS API plugin (dns_poweradmin) --- dnsapi/dns_poweradmin.sh | 238 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 dnsapi/dns_poweradmin.sh diff --git a/dnsapi/dns_poweradmin.sh b/dnsapi/dns_poweradmin.sh new file mode 100644 index 00000000..db31fa4f --- /dev/null +++ b/dnsapi/dns_poweradmin.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 + +# Credits to the authors of dnsapi/dns_pdns.sh as this reuses much of that code. + +dns_poweradmin_info='Poweradmin API +Site: https://www.poweradmin.org/ +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_poweradmin +Options: +POWERADMIN_URL API URL (with scheme). E.g. "https://poweradmin.example.com" or "http://192.168.0.10:8080" +POWERADMIN_API_KEY API Token "pwa_xxxx" +POWERADMIN_API_VERSION Optionally override Poweradmin API version. +Issues: https://github.com/acmesh-official/acme.sh/issues/6912 +Author: Jakob Næss +' + +######## Public functions #################### + +# Usage: dns_poweradmin_add _acme-challenge.www.domain.com "123456789ABCDEF" +# fulldomain +# txtvalue +dns_poweradmin_add() { + fulldomain=$1 + txtvalue=$2 + + POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}" + POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}" + + if [ -z "$POWERADMIN_URL" ]; then + POWERADMIN_URL="" + _err "You didn't specify Poweradmin URL." + _err "Please set POWERADMIN_URL and try again." + return 1 + fi + + if [ -z "$POWERADMIN_API_KEY" ]; then + POWERADMIN_API_KEY="" + _err "You didn't specify Poweradmin token." + _err "Please set POWERADMIN_API_KEY and try again." + return 1 + fi + + # Save the api addr, key, and version to the account conf file. + _saveaccountconf_mutable POWERADMIN_URL "$POWERADMIN_URL" + _saveaccountconf_mutable POWERADMIN_API_KEY "$POWERADMIN_API_KEY" + _saveaccountconf_mutable POWERADMIN_API_VERSION "$POWERADMIN_API_VERSION" + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain "$_domain" + _debug _zone_id "$_zone_id" + + if ! _set_record "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +# Usage: dns_poweradmin_rm _acme-challenge.www.domain.com "123456789ABCDEF" +# fulldomain +# txtvalue +dns_poweradmin_rm() { + fulldomain=$1 + txtvalue=$2 + + POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}" + POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}" + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain "$_domain" + _debug _zone_id "$_zone_id" + + if ! _rm_record "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +######## Private functions below ##################### + +_set_record() { + _info "Adding TXT record" + full=$1 + new_challenge=$2 + + data='{"name":"'$full'","type":"TXT","content":"'$new_challenge'","ttl":60}' + + if ! _poweradmin_rest "POST" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records" "$data" "application/json"; then + _err "Failed to add TXT record" + return 1 + fi + + return 0 +} + +_rm_record() { + _info "Remove TXT record" + full=$1 + txtvalue=$2 + + if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then + _err "Failed to retrieve records" + return 1 + fi + + # The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]} + _txt_record_obj=$( + printf '%s\n' "$response" | + sed 's/^.*"data":\[//; s/\],"message":.*$//' | + awk '{ gsub(/},{/, "}\n{"); print }' | + grep -F "\"name\":\"$full\"" | + grep -F "\"type\":\"TXT\"" | + grep -F "\"content\":\"$txtvalue\"" | + _head_n 1 + ) + + if [ -z "$_txt_record_obj" ]; then + _info "TXT record not found for $full with content $txtvalue" + return 0 + fi + + record_id=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' | _head_n 1) + record_type=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p' | _head_n 1) + record_name=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | _head_n 1) + record_content=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | _head_n 1) + + _debug2 "_txt_record_obj=$_txt_record_obj" + _debug2 "record id: $record_id" + _debug2 "record type: $record_type" + _debug2 "record name: $record_name" + _debug2 "record content: $record_content" + + if [ "$record_type" != "TXT" ]; then + _err "Refusing to delete non-TXT record id=$record_id type=$record_type name=$full" + return 1 + fi + + if ! _poweradmin_rest "DELETE" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records/$record_id"; then + _err "Failed to delete TXT record" + return 1 + fi + + _info "Record deleted successfully" + return 0 +} + +# _acme-challenge.www.domain.com +# returns +# _domain=domain.com +# _zone_id=220 +_get_root() { + domain=$1 + i=1 + + if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones"; then + _err "Failed to retrieve zones" + return 1 + fi + + _zones_response="$response" + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + + if [ -z "$h" ]; then + _debug "Root domain not found for $domain" + return 1 + fi + + zone_obj=$( + printf '%s' "$_zones_response" | + sed 's/},{/}\n{/g' | + grep -F "\"name\":\"$h\"" | + _head_n 1 + ) + + if [ -n "$zone_obj" ]; then + _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2) + _domain="$h" + _debug "Found zone: $_domain with id: $_zone_id" + return 0 + fi + + i=$(_math "$i" + 1) + done +} + +_poweradmin_rest() { + method=$1 + ep=$2 + data=$3 + ct=$4 + + export _H1="X-API-Key: $POWERADMIN_API_KEY" + + if [ "$method" = "GET" ]; then + response="$(_get "$POWERADMIN_URL$ep")" + else + _debug "API call: $method $ep" + _debug "Content-Type: $ct" + _debug "Payload: $data" + response="$(_post "$data" "$POWERADMIN_URL$ep" "" "$method" "$ct")" + fi + + # Clear _H1 variable + unset -v _H1 + + if [ "$?" != "0" ]; then + _err "API error on $method $ep" + _debug "Response: $response" + return 1 + fi + + if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then + _err "API reported failure on $method $ep" + _debug "Response: $response" + return 1 + fi + + _debug2 "API Response: $response" + return 0 +} From b7e9214e2d65b1099f6cd0dfc321ffa7b06159b0 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 8 May 2026 20:44:48 +0200 Subject: [PATCH 488/689] minor --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index f67749c0..6ab2d8d8 100755 --- a/acme.sh +++ b/acme.sh @@ -5785,7 +5785,7 @@ renew() { _debug "_renewServer" "$_renewServer" _initpath "$Le_Domain" "$_isEcc" - + _info "Renew: $Le_Domain" _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _info "$(__green "Renewing: '$Le_Domain'")" if [ ! -f "$DOMAIN_CONF" ]; then From 5713c1d39d3dd89f6b6698aaf26181ea9bb8c382 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 30 May 2026 11:48:06 +0200 Subject: [PATCH 489/689] remove dns_hetzner.sh https://github.com/acmesh-official/acme.sh/issues/6990#issuecomment-4576551997 --- dnsapi/dns_hetzner.sh | 256 ------------------------------------------ 1 file changed, 256 deletions(-) delete mode 100755 dnsapi/dns_hetzner.sh diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh deleted file mode 100755 index f1bddc61..00000000 --- a/dnsapi/dns_hetzner.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_hetzner_info='Hetzner.com -Site: Hetzner.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner -Options: - HETZNER_Token API Token -Issues: github.com/acmesh-official/acme.sh/issues/2943 -' - -HETZNER_Api="https://dns.hetzner.com/api/v1" - -######## Public functions ##################### - -# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -# Used to add txt record -# Ref: https://dns.hetzner.com/api-docs/ -dns_hetzner_add() { - full_domain=$1 - txt_value=$2 - - HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}" - - if [ -z "$HETZNER_Token" ]; then - HETZNER_Token="" - _err "You didn't specify a Hetzner api token." - _err "You can get yours from here https://dns.hetzner.com/settings/api-token." - return 1 - fi - - #save the api key and email to the account conf file. - _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token" - - _debug "First detect the root zone" - - if ! _get_root "$full_domain"; then - _err "Invalid domain" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting TXT records" - if ! _find_record "$_sub_domain" "$txt_value"; then - return 1 - fi - - if [ -z "$_record_id" ]; then - _info "Adding record" - if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then - if _contains "$response" "$txt_value"; then - _info "Record added, OK" - _sleep 2 - return 0 - fi - fi - _err "Add txt record error${_response_error}" - return 1 - else - _info "Found record id: $_record_id." - _info "Record found, do nothing." - return 0 - # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same - #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then - # if _contains "$response" "$txt_value"; then - # _info "Modified, OK" - # return 0 - # fi - #fi - #_err "Add txt record error (modify)." - #return 1 - fi -} - -# Usage: full_domain txt_value -# Used to remove the txt record after validation -dns_hetzner_rm() { - full_domain=$1 - txt_value=$2 - - HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}" - - _debug "First detect the root zone" - if ! _get_root "$full_domain"; then - _err "Invalid domain" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting TXT records" - if ! _find_record "$_sub_domain" "$txt_value"; then - return 1 - fi - - if [ -z "$_record_id" ]; then - _info "Remove not needed. Record not found." - else - if ! _hetzner_rest DELETE "records/$_record_id"; then - _err "Delete record error${_response_error}" - return 1 - fi - _sleep 2 - _info "Record deleted" - fi -} - -#################### Private functions below ################################## -#returns -# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error -_find_record() { - unset _record_id - _record_name=$1 - _record_value=$2 - - if [ -z "$_record_value" ]; then - _record_value='[^"]*' - fi - - _debug "Getting all records" - _hetzner_rest GET "records?zone_id=${_domain_id}" - - if _response_has_error; then - _err "Error${_response_error}" - return 1 - else - _record_id=$( - echo "$response" | - grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" | - grep "\"value\":\"$_record_value\"" | - while read -r record; do - # test for type and - if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then - echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \" - break - fi - done - ) - fi -} - -#_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 - - domain_without_acme=$(echo "$domain" | cut -d . -f 2-) - domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g') - - _debug "Reading zone_id for '$domain_without_acme' from config..." - HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name") - if [ "$HETZNER_Zone_ID" ]; then - _debug "Found, using: $HETZNER_Zone_ID" - if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then - _debug "Zone with id '$HETZNER_Zone_ID' does not exist." - _cleardomainconf "$domain_param_name" - unset HETZNER_Zone_ID - else - if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then - _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1) - if [ "$_domain" ]; then - _cut_length=$((${#domain} - ${#_domain} - 1)) - _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length") - _domain_id="$HETZNER_Zone_ID" - return 0 - else - return 1 - fi - else - return 1 - fi - fi - fi - - _debug "Trying to get zone id by domain name for '$domain_without_acme'." - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - if [ -z "$h" ]; then - #not valid - return 1 - fi - _debug h "$h" - - _hetzner_rest GET "zones?name=$h" - - if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then - _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - HETZNER_Zone_ID=$_domain_id - _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID" - return 0 - fi - return 1 - fi - p=$i - i=$(_math "$i" + 1) - done - return 1 -} - -#returns -# _response_error -_response_has_error() { - unset _response_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) - err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \") - - if [ -n "$err_code" ] && [ -n "$err_message" ]; then - _response_error=" - message: ${err_message}, code: ${err_code}" - return 0 - fi - fi - - return 1 -} - -#returns -# response -_hetzner_rest() { - m=$1 - ep="$2" - data="$3" - _debug "$ep" - - key_trimmed=$(echo "$HETZNER_Token" | tr -d \") - - export _H1="Content-TType: application/json" - export _H2="Auth-API-Token: $key_trimmed" - - if [ "$m" != "GET" ]; then - _debug data "$data" - response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")" - else - response="$(_get "$HETZNER_Api/$ep")" - fi - - if [ "$?" != "0" ] || _response_has_error; then - _debug "Error$_response_error" - return 1 - fi - _debug2 response "$response" - return 0 -} From dfbe2c5bff139a89b7a782d365870aee8b9eaa50 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 4 Jun 2026 20:15:12 +0100 Subject: [PATCH 490/689] fix _getAKI() on OpenBSD (#7007) The order of the arguments does matter for OpenBSD's grep (bug or feature). --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 6ab2d8d8..00192868 100755 --- a/acme.sh +++ b/acme.sh @@ -6865,7 +6865,7 @@ deactivate() { #cert _getAKI() { _cert="$1" - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ': ' | sed "s/keyid//" + ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep -A 1 "X509v3 Authority Key Identifier" | _tail_n 1 | tr -d ': ' | sed "s/keyid//" } #cert From c7c903fba3188ac2c9063a9fa9b14a2f991bbe25 Mon Sep 17 00:00:00 2001 From: terafin Date: Thu, 4 Jun 2026 12:25:38 -0700 Subject: [PATCH 491/689] ci: add GitHub Container Registry (ghcr.io) publishing (#7005) --- .github/workflows/dockerhub.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 0e7ba748..d10c17a8 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -41,6 +41,9 @@ jobs: runs-on: ubuntu-latest needs: CheckToken if: "contains(needs.CheckToken.outputs.hasToken, 'true')" + permissions: + contents: read + packages: write steps: - name: checkout code uses: actions/checkout@v6 @@ -58,6 +61,9 @@ jobs: - name: login to docker hub run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + - name: login to ghcr + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - name: build and push the image run: | if [[ $GITHUB_REF == refs/tags/* ]]; then @@ -73,6 +79,8 @@ jobs: fi fi + echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV" + DOCKER_LABELS=() while read -r label; do DOCKER_LABELS+=(--label "${label}") @@ -84,3 +92,9 @@ jobs: --output "type=image,push=true" \ --build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \ --platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x . + - name: mirror the image to ghcr (best-effort) + run: | + docker buildx imagetools create \ + --tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \ + ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \ + || echo "::warning::GHCR mirror failed; Docker Hub publish unaffected" From 2e4e5d7955530932673e446f4be41e32e2204c6b Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 4 Jun 2026 22:34:24 +0200 Subject: [PATCH 492/689] add tribblix --- .github/workflows/DNS.yml | 58 ++++++++++++++++++++++++- .github/workflows/Tribblix.yml | 79 ++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/Tribblix.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 00d180b9..232c9b0f 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -661,9 +661,65 @@ jobs: - Haiku: + Tribblix: runs-on: ubuntu-latest needs: OpenIndiana + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Tribblix doesn't accept the expired ISRG X1 root + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v6 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/tribblix-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + sync: nfs + prepare: zap install socat + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + + + Haiku: + runs-on: ubuntu-latest + needs: Tribblix env: TEST_DNS : ${{ secrets.TEST_DNS }} TestingDomain: ${{ secrets.TestingDomain }} diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml new file mode 100644 index 00000000..cd43e0e3 --- /dev/null +++ b/.github/workflows/Tribblix.yml @@ -0,0 +1,79 @@ +name: Tribblix +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Tribblix.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Tribblix.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + Tribblix: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v6 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/tribblix-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: zap install socat curl wget + sync: nfs + run: | + cd ../acmetest \ + && ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/README.md b/README.md index ba5d3591..22700b4c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ MidnightBSD Omnios OpenIndiana + Tribblix Haiku

@@ -112,6 +113,7 @@ |23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) |24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) |25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS +|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From 0e5f1518aab3937eb690aa421ed48078c79b2ecd Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 4 Jun 2026 22:57:50 +0200 Subject: [PATCH 493/689] upgrade --- .github/workflows/dockerhub.yml | 4 ++-- .github/workflows/issue.yml | 2 +- .github/workflows/pr_dns.yml | 2 +- .github/workflows/pr_notify.yml | 2 +- .github/workflows/wiki-monitor.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index d10c17a8..7dc42290 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -50,14 +50,14 @@ jobs: with: persist-credentials: false - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v4 - name: Extract Docker metadata id: meta uses: docker/metadata-action@v6 with: images: ${DOCKER_IMAGE} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v4 - name: login to docker hub run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index e92b0411..c659fce5 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -7,7 +7,7 @@ jobs: comment: runs-on: ubuntu-latest steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v9 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml index 558ebf48..19763a15 100644 --- a/.github/workflows/pr_dns.yml +++ b/.github/workflows/pr_dns.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest if: github.actor != 'neilpang' steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v9 with: script: | await github.rest.issues.createComment({ diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml index 416ed721..76ae76f6 100644 --- a/.github/workflows/pr_notify.yml +++ b/.github/workflows/pr_notify.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: github.actor != 'neilpang' steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v9 with: script: | await github.rest.issues.createComment({ diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index a706529a..7e5d7ca3 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -51,7 +51,7 @@ jobs: } > wiki-change-msg.txt - name: Create issue to notify Neilpang - uses: peter-evans/create-issue-from-file@v5 + uses: peter-evans/create-issue-from-file@v6 with: title: "Wiki edited" content-filepath: ./wiki-change-msg.txt From b4634719514509d2485aff87c23800fee116ac59 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Jun 2026 19:22:25 +0200 Subject: [PATCH 494/689] fix localaddress https://github.com/acmesh-official/acme.sh/issues/7009#issuecomment-4633681701 --- acme.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acme.sh b/acme.sh index 00192868..e6272c9f 100755 --- a/acme.sh +++ b/acme.sh @@ -5312,6 +5312,8 @@ $_authorizations_map" fi fi elif [ "$vtype" = "$VTYPE_ALPN" ]; then + _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")" + _ncIndex="$(_math $_ncIndex + 1)" acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")" _debug acmevalidationv1 "$acmevalidationv1" if ! _starttlsserver "$d" "" "$Le_TLSPort" "$keyauthorization" "$_ncaddr" "$acmevalidationv1"; then From a2f046306e5089b2ab82b247a319218b3aa30278 Mon Sep 17 00:00:00 2001 From: Adrian Fedoreanu Date: Fri, 5 Jun 2026 19:28:08 +0200 Subject: [PATCH 495/689] dns_1984hosting: cleanup, memoize zone id (#6978) * dns_1984hosting: cleanup, memoize zone id, optional OTP --- dnsapi/dns_1984hosting.sh | 51 +++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh index 8d9676ac..8ed9b8ef 100755 --- a/dnsapi/dns_1984hosting.sh +++ b/dnsapi/dns_1984hosting.sh @@ -7,6 +7,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting Options: One984HOSTING_Username Username One984HOSTING_Password Password + One984HOSTING_TOTP_Secret Base32 TOTP shared secret. Required only if the account has 2FA enabled. Requires oathtool. Used to mint the OTP code automatically at login so cron renewals keep working. Issues: github.com/acmesh-official/acme.sh/issues/2851 Author: Adrian Fedoreanu ' @@ -124,11 +125,28 @@ _1984hosting_login() { _debug "Login to 1984Hosting as user $One984HOSTING_Username." username=$(printf '%s' "$One984HOSTING_Username" | _url_encode) password=$(printf '%s' "$One984HOSTING_Password" | _url_encode) - url="https://1984.hosting/api/auth/" - _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 'cookie1984nammnamm=[^;]*;' | tr -d ';')" + # When 2FA is enabled, mint a fresh TOTP code from the stored shared secret. + # Empty otpkey is accepted by the server when 2FA is off. + otpkey="" + if [ -n "$One984HOSTING_TOTP_Secret" ]; then + if ! _exists oathtool; then + _err "oathtool is required to use One984HOSTING_TOTP_Secret for 2FA. Please install it." + return 1 + fi + otpcode="$(oathtool --base32 --totp "$One984HOSTING_TOTP_Secret" 2>/dev/null)" + if [ -z "$otpcode" ]; then + _err "Failed to generate TOTP code from One984HOSTING_TOTP_Secret." + return 1 + fi + otpkey="$(printf '%s' "$otpcode" | _url_encode)" + fi + + # Fetch the login page to obtain CSRF and session cookies. + # Note: _get sets the global 'url', so assign the auth URL afterwards. + _get "https://1984.hosting/accounts/login/" >/dev/null + csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')" + sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')" if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then _err "One or more cookies are empty: '$csrftoken', '$sessionid'." @@ -140,17 +158,23 @@ _1984hosting_login() { csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1) export _H3="X-CSRFToken: $csrf_header" - response="$(_post "username=$username&password=$password&otpkey=" $url)" + url="https://1984.hosting/api/auth/" + response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")" response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" if _contains "$response" '"loggedin": true'; then - 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 ';')" + One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')" + One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')" export One984HOSTING_SESSIONID_COOKIE export One984HOSTING_CSRFTOKEN_COOKIE _saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username" _saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password" + if [ -n "$One984HOSTING_TOTP_Secret" ]; then + _saveaccountconf_mutable One984HOSTING_TOTP_Secret "$One984HOSTING_TOTP_Secret" + else + _clearaccountconf_mutable One984HOSTING_TOTP_Secret + fi _saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE" _saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE" return 0 @@ -161,6 +185,7 @@ _1984hosting_login() { _check_credentials() { One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}" One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}" + One984HOSTING_TOTP_Secret="${One984HOSTING_TOTP_Secret:-$(_readaccountconf_mutable One984HOSTING_TOTP_Secret)}" if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then One984HOSTING_Username="" One984HOSTING_Password="" @@ -225,9 +250,15 @@ _get_root() { # Usage: _get_zone_id url domain.com # Returns zone id for domain.com +# Memoized per-domain so add/rm don't re-fetch the same zone list within a run. +# Keyed on domain (not url) since the url is always the domains listing. _get_zone_id() { url=$1 domain=$2 + if [ "$_zone_id_for" = "$domain" ] && [ -n "$_zone_id" ]; then + _debug2 _zone_id "$_zone_id (cached)" + return 0 + fi _htmlget "$url" "$domain" _zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)" _debug2 _zone_id "$_zone_id" @@ -235,6 +266,7 @@ _get_zone_id() { _err "Error getting _zone_id for $2." return 1 fi + _zone_id_for="$domain" return 0 } @@ -257,9 +289,8 @@ _htmlget() { # Add extra headers to request _authpost() { - url="https://1984.hosting/domains" - _get_zone_id "$url" "$_domain" - csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")" + _get_zone_id "https://1984.hosting/domains" "$_domain" + csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)" export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE" export _H2="Referer: https://1984.hosting/domains/$_zone_id" export _H3="X-CSRFToken: $csrf_header" From 58d9c8d7f613c1975f694df417d78dda805d5629 Mon Sep 17 00:00:00 2001 From: rajcz Date: Fri, 5 Jun 2026 19:38:50 +0200 Subject: [PATCH 496/689] acme.sh: validate cert response before writing .cer (#7006) --- acme.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/acme.sh b/acme.sh index e6272c9f..a7397be1 100755 --- a/acme.sh +++ b/acme.sh @@ -5544,6 +5544,13 @@ $_authorizations_map" return 1 fi + if ! _contains "$response" "$BEGIN_CERT"; then + response="$(echo "$response" | _dbase64 "multiline" | tr -d '\0' | _normalizeJson)" + _err "Signing failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" + _on_issue_err "$_post_hook" + return 1 + fi + echo "$response" >"$CERT_PATH" _split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH" if [ -z "$_preferred_chain" ]; then @@ -5563,6 +5570,11 @@ $_authorizations_map" _err "$response" continue fi + + if ! _contains "$response" "$BEGIN_CERT"; then + _debug2 "Skipping alternate cert link due to unexpected response format." + continue + fi _relcert="$CERT_PATH.alt" _relfullchain="$CERT_FULLCHAIN_PATH.alt" _relca="$CA_CERT_PATH.alt" From 9b597b3f1bb424b2b782c54b74955bd9efa37c8e Mon Sep 17 00:00:00 2001 From: aitor422 Date: Fri, 5 Jun 2026 20:02:14 +0000 Subject: [PATCH 497/689] Add CDMON Api (#6984) * Added CDMon DNS API --- dnsapi/dns_cdmon.sh | 137 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 dnsapi/dns_cdmon.sh diff --git a/dnsapi/dns_cdmon.sh b/dnsapi/dns_cdmon.sh new file mode 100644 index 00000000..470fb5fe --- /dev/null +++ b/dnsapi/dns_cdmon.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 + +dns_cdmon_info='cdmon +Site: www.cdmon.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon +Options: + CDMON_Key API Key +' + +CDMON_Api="https://api-domains.cdmon.services/api-domains" + +######## Public functions ##################### +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Used to add txt record +dns_cdmon_add() { + fulldomain=$1 + txtvalue=$2 + + CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}" + + if [ -z "$CDMON_Key" ]; then + CDMON_Key="" + _err "You didn't specify your cdmon api key yet." + _err "Please create your key and try again." + return 1 + fi + + _saveaccountconf_mutable CDMON_Key "$CDMON_Key" + + _debug "First, we 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 record" + if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then + if _contains "$response" "\"status\":\"ok\""; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 +} + +# Usage: fulldomain txtvalue +# Used to remove the txt record after validation +dns_cdmon_rm() { + fulldomain=$1 + txtvalue=$2 + + CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}" + _debug "First, we detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Removing record" + if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then + if _contains "$response" "\"status\":\"ok\""; then + _info "Deleted, OK" + return 0 + else + _err "Delete txt record error." + return 1 + fi + fi + _err "Delete txt record error." + return 1 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=1 + + if ! _cdmon_rest "domains/list"; then + return 1 + fi + + 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" "\"domain\":\"$h\""; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_cdmon_rest() { + ep="$1" + data="$2" + _debug "$ep" + + key_trimmed=$(echo "$CDMON_Key" | tr -d '"') + + export _H1="Content-Type: application/json" + export _H2="apikey: $key_trimmed" + + _debug data "$data" + response="$(_post "$data" "$CDMON_Api/$ep")" + _ret="$?" + + unset _H1 _H2 + + if [ "$_ret" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} From d98fa53f627ab08e17616a8037cea86e496aa917 Mon Sep 17 00:00:00 2001 From: Bill <80264737+billzee@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:06:49 -0400 Subject: [PATCH 498/689] Updated AWS Route53 service endpoint to the dual-stack endpoint (#6994) * Update to dual-stack service endpoint --- dnsapi/dns_aws.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index b76d69c2..1face1c8 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -11,7 +11,8 @@ Options: # All `_sleep` commands are included to avoid Route53 throttling, see # https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests -AWS_HOST="route53.amazonaws.com" +# Updated from "route53.amazonaws.com" +AWS_HOST="route53.global.api.aws" AWS_URL="https://$AWS_HOST" AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API" From 4575877d48643494d6e5769c0468386ea6a80a77 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Jun 2026 23:01:31 +0200 Subject: [PATCH 499/689] add GhostBSD --- .github/workflows/DNS.yml | 58 +++++++++++++++++++++++- .github/workflows/GhostBSD.yml | 80 ++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/GhostBSD.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 232c9b0f..06dd29ac 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -260,7 +260,7 @@ jobs: - OpenBSD: + GhostBSD: runs-on: ubuntu-latest needs: FreeBSD env: @@ -281,6 +281,62 @@ jobs: TokenName5: ${{ secrets.TokenName5}} steps: - uses: actions/checkout@v6 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/ghostbsd-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + prepare: pkg install -y socat curl + usesh: true + sync: nfs + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + + + OpenBSD: + runs-on: ubuntu-latest + needs: GhostBSD + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml new file mode 100644 index 00000000..2dd2412b --- /dev/null +++ b/.github/workflows/GhostBSD.yml @@ -0,0 +1,80 @@ +name: GhostBSD +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/GhostBSD.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/GhostBSD.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + GhostBSD: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v6 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/ghostbsd-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: pkg install -y socat curl wget + usesh: true + sync: nfs + run: | + cd ../acmetest \ + && ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/README.md b/README.md index 22700b4c..3a697691 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Solaris DragonFlyBSD MidnightBSD + GhostBSD Omnios OpenIndiana Tribblix @@ -114,6 +115,7 @@ |24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) |25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS |26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix +|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From db098055de3ea8012190db7e71d02a0e229a0e75 Mon Sep 17 00:00:00 2001 From: SpeedGriffon <5631890+SpeedGriffon@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:23:03 +0200 Subject: [PATCH 500/689] Fix RouterOS deploy (#7034) * routeros: save ROUTER_OS_ADDITIONAL_SERVICES as base64 * routeros: remove cer_3 --- deploy/routeros.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/routeros.sh b/deploy/routeros.sh index ef9c6954..328fabbd 100644 --- a/deploy/routeros.sh +++ b/deploy/routeros.sh @@ -125,7 +125,7 @@ routeros_deploy() { _savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT" _savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD" _savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD" - _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" + _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64" # push key to routeros if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then @@ -143,6 +143,7 @@ comment=\"generated by routeros deploy script in acme.sh\" \ source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\ \n/certificate remove [ find name=$_cdomain.cer_1 ];\ \n/certificate remove [ find name=$_cdomain.cer_2 ];\ +\n/certificate remove [ find name=$_cdomain.cer_3 ];\ \ndelay 1;\ \n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\ \n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\ From 365d2d10f3d5e170d6e9b92d2f79b2c8b86bdd75 Mon Sep 17 00:00:00 2001 From: regisvidal-bitmapz Date: Fri, 19 Jun 2026 14:24:20 +0200 Subject: [PATCH 501/689] Fix dns_namesilo_rm failing to remove TXT record (#6969) * Fixes #6907 --- dnsapi/dns_namesilo.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_namesilo.sh b/dnsapi/dns_namesilo.sh index 5d47a59a..df5871cf 100755 --- a/dnsapi/dns_namesilo.sh +++ b/dnsapi/dns_namesilo.sh @@ -65,7 +65,7 @@ dns_namesilo_rm() { if _namesilo_rest GET "dnsListRecords?version=1&type=xml&key=$Namesilo_Key&domain=$_domain"; then retcode=$(printf "%s\n" "$response" | _egrep_o "300") if [ "$retcode" ]; then - _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$fulldomain" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1) + _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$_sub_domain$txtvalue" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1) _debug _record_id "$_record_id" if [ "$_record_id" ]; then _info "Successfully retrieved the record id for ACME challenge." From d60c75b4e30a26addd8b9d0bfdeeed39c84ed51e Mon Sep 17 00:00:00 2001 From: ZeroSSL-Andreas Date: Fri, 19 Jun 2026 17:06:57 +0200 Subject: [PATCH 502/689] New Banner Updated README to include responsive images for dark and light modes. --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 22700b4c..792ef979 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@

- - zerossl.com - + + + + + + + + ZeroSSL + +

🔐 acme.sh

From 0dc97187e129d90bb42168f8cca9d9b425ad8c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ko=C5=82odziejczak?= <31549762+mrl5@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:41:15 +0200 Subject: [PATCH 503/689] docs: introduce contributing doc (#7052) prevents friction and frustrations like in issue #7050 closes #7050 --- CONTRIBUTING.md | 8 ++++++++ README.md | 2 ++ 2 files changed, 10 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..33294ce7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +# Contributing + +1. Do NOT send pull request to `master` branch. +Please send to `dev` branch instead. +Any PR to `master` branch will NOT be merged. + +2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide +You will NOT get any review without passing this guide. You also need to fix the CI errors. diff --git a/README.md b/README.md index 3a697691..44a73e83 100644 --- a/README.md +++ b/README.md @@ -615,6 +615,8 @@ This project exists thanks to all the people who contribute. +If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md). + ### 💰 Financial Contributors Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)] From b3579ff18dcf3c530293b096e232fab4b4b87216 Mon Sep 17 00:00:00 2001 From: Jeroen Moors Date: Sun, 28 Jun 2026 19:27:41 +0200 Subject: [PATCH 504/689] Implement support for DNS Level27 (#7043) * Add Level27 DNS API support Implements dns_level27_add and dns_level27_rm for the Level27 (level27.eu) DNS API, used for ACME dns-01 challenges. - Authenticates with a persistent API key via the Authorization header. - Resolves the registered zone with domains?filter and exact fullname match (supports DNS alias mode). - Removes the challenge record by its exact TXT value, leaving other records intact (wildcard-safe). - Optional LEVEL27_API override for non-default/staging endpoints. * A little better documentation --------- Co-authored-by: Jeroen Moors --- dnsapi/dns_level27.sh | 197 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 dnsapi/dns_level27.sh diff --git a/dnsapi/dns_level27.sh b/dnsapi/dns_level27.sh new file mode 100644 index 00000000..3fbaf810 --- /dev/null +++ b/dnsapi/dns_level27.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_level27_info='Level27 +Site: Level27.be +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_level27 +Options: + LEVEL27_API_KEY API key. Get one from the Level27 control panel (https://app.level27.eu/account/profile/security). +OptionsAlt: + LEVEL27_API API base URL. Optional. Default "https://api.level27.eu/v1". +Issues: github.com/acmesh-official/acme.sh/issues +Author: Jeroen Moors +' + +LEVEL27_API_DEFAULT="https://api.level27.eu/v1" + +######## Public functions ##################### + +# Usage: dns_level27_add _acme-challenge.www.example.com "TXT-value" +dns_level27_add() { + fulldomain="$(_idn "$1")" + txtvalue="$2" + + _info "Using Level27 to add a TXT record for $fulldomain" + + if ! _level27_init; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Could not determine the root zone for $fulldomain at Level27." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _level27_data="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\"}" + if ! _level27_rest POST "domains/$_domain_id/records" "$_level27_data"; then + _err "Could not add the TXT record." + return 1 + fi + + if _contains "$response" "\"id\":"; then + _info "TXT record added." + return 0 + fi + + _err "Unexpected response while adding the TXT record." + return 1 +} + +# Usage: dns_level27_rm _acme-challenge.www.example.com "TXT-value" +dns_level27_rm() { + fulldomain="$(_idn "$1")" + txtvalue="$2" + + _info "Using Level27 to remove the TXT record for $fulldomain" + + if ! _level27_init; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Could not determine the root zone for $fulldomain at Level27." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _level27_rest GET "domains/$_domain_id/records?type=TXT"; then + _err "Could not list the existing TXT records." + return 1 + fi + + _record_id="$(_level27_find_record_id "$response" "$txtvalue")" + if [ -z "$_record_id" ]; then + _info "No matching TXT record found; nothing to remove." + return 0 + fi + _debug _record_id "$_record_id" + + if ! _level27_rest DELETE "domains/$_domain_id/records/$_record_id"; then + _err "Could not remove the TXT record." + return 1 + fi + + _info "TXT record removed." + return 0 +} + +#################### Private functions below ################################## + +# Reads and validates the API credentials and endpoint, and stores them for renewals. +_level27_init() { + LEVEL27_API_KEY="${LEVEL27_API_KEY:-$(_readaccountconf_mutable LEVEL27_API_KEY)}" + if [ -z "$LEVEL27_API_KEY" ]; then + LEVEL27_API_KEY="" + _err "You must export the variable LEVEL27_API_KEY before using the Level27 DNS API." + _err "Get an API key from the Level27 control panel (https://app.level27.eu/account/profile/security)." + return 1 + fi + LEVEL27_API_KEY="$(echo "$LEVEL27_API_KEY" | tr -d '"')" + _saveaccountconf_mutable LEVEL27_API_KEY "$LEVEL27_API_KEY" + + LEVEL27_API="${LEVEL27_API:-$(_readaccountconf_mutable LEVEL27_API)}" + if [ -z "$LEVEL27_API" ]; then + LEVEL27_API="$LEVEL27_API_DEFAULT" + fi + _saveaccountconf_mutable LEVEL27_API "$LEVEL27_API" + + # Remove a trailing slash so endpoints can be appended consistently. + LEVEL27_API="$(echo "$LEVEL27_API" | sed 's#/$##')" + return 0 +} + +# Usage: _get_root _acme-challenge.www.example.com +# Splits the full domain into the registered zone and the subdomain part. +# Sets: _domain, _domain_id, _sub_domain +_get_root() { + domain=$1 + i=1 + p=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 ! _level27_rest GET "domains?filter=$h"; then + return 1 + fi + + _level27_zones="$(echo "$response" | _normalizeJson)" + if _contains "$_level27_zones" "\"fullname\":\"$h\""; then + _domain_line="$(echo "$_level27_zones" | sed 's/},{/}\n{/g' | grep "\"fullname\":\"$h\"" | _head_n 1)" + _domain_id="$(echo "$_domain_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2)" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +# Usage: _level27_find_record_id "" "" +# Prints the id of the TXT record whose content matches the value, or nothing. +_level27_find_record_id() { + _records="$(echo "$1" | _normalizeJson | sed 's/},{/}\n{/g')" + _wanted="$2" + _record_line="$(echo "$_records" | grep "\"content\":\"$_wanted\"" | _head_n 1)" + if [ -z "$_record_line" ]; then + # Some APIs store TXT content wrapped in quotes. + _record_line="$(echo "$_records" | grep "\"content\":\"\\\\\"$_wanted\\\\\"\"" | _head_n 1)" + fi + if [ -z "$_record_line" ]; then + return 0 + fi + echo "$_record_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2 +} + +# Usage: _level27_rest [data] +# Performs an authenticated API call and stores the body in $response. +_level27_rest() { + m="$1" + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Authorization: $LEVEL27_API_KEY" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + if [ "$m" != "GET" ]; then + _debug2 data "$data" + response="$(_post "$data" "$LEVEL27_API/$ep" "" "$m")" + else + response="$(_get "$LEVEL27_API/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "Error querying the Level27 API endpoint: $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} From 2998106bd1cebebf681bafec23b19113bb6e21ac Mon Sep 17 00:00:00 2001 From: Alexander Stehlik Date: Wed, 1 Jul 2026 12:54:24 +0200 Subject: [PATCH 505/689] fix(dns_desec): fix rate limit and compatibility issues (#7027) * fix(dns_desec): sleep after DNS record change to prevent rate limit issues Also: make sure the subname is lowercase to fix tests where the acmetestXyzRandomName subdomain is used. * fix: make regexes POSIX-compatible (for OpenBSD) * chore: use _sleep instead of sleep to follow acme.sh standards --- dnsapi/dns_desec.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_desec.sh b/dnsapi/dns_desec.sh index d6b9c355..275babea 100644 --- a/dnsapi/dns_desec.sh +++ b/dnsapi/dns_desec.sh @@ -39,6 +39,7 @@ dns_desec_add() { _err "invalid domain" return 1 fi + _sub_domain=$(echo "$_sub_domain" | _lower_case) _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" @@ -48,7 +49,7 @@ dns_desec_add() { _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/" if [ "$_code" = "200" ]; then - oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" + oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" _debug "existing TXT found" _debug oldtxtvalues "$oldtxtvalues" if [ -n "$oldtxtvalues" ]; then @@ -100,7 +101,7 @@ dns_desec_rm() { _err "invalid domain" return 1 fi - + _sub_domain=$(echo "$_sub_domain" | _lower_case) _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" @@ -110,7 +111,7 @@ dns_desec_rm() { _desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/" if [ "$_code" = "200" ]; then - oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" + oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")" _debug "existing TXT found" _debug oldtxtvalues "$oldtxtvalues" if [ -n "$oldtxtvalues" ]; then @@ -150,6 +151,8 @@ _desec_rest() { if [ "$m" != "GET" ]; then _secure_debug2 data "$data" response="$(_post "$data" "$ep" "" "$m")" + _info "Sleeping 1s to respect deSEC write rate limit" + _sleep 1 else response="$(_get "$ep")" fi From c38182897d55e811aa8e09c00f68890afbc8360f Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 1 Jul 2026 18:59:14 +0800 Subject: [PATCH 506/689] fix ghostbsd --- .github/workflows/DNS.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 06dd29ac..a972ae1a 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -263,6 +263,8 @@ jobs: GhostBSD: runs-on: ubuntu-latest needs: FreeBSD + # GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run + continue-on-error: true env: TEST_DNS : ${{ secrets.TEST_DNS }} TestingDomain: ${{ secrets.TestingDomain }} From 0d53d29f7efa6b40c6193c1b9b338da180cd24f7 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:44:21 +0200 Subject: [PATCH 507/689] Update dns_hostup.sh to v2 API (#7014) * Update dns_hostup.sh Update to v2 api support; developer.hostup.se * Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 326 ++++++++++++++++++++++++++----------------- 1 file changed, 201 insertions(+), 125 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index b3211069..73189189 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -6,13 +6,13 @@ 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_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api/v2). HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds). - HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection). + HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection. Author: HostUp (https://cloud.hostup.se/contact/en) ' -HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api" +HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2" HOSTUP_DEFAULT_TTL=60 # Public: add TXT record @@ -20,6 +20,7 @@ HOSTUP_DEFAULT_TTL=60 dns_hostup_add() { fulldomain="$1" txtvalue="$2" + hostup_add_txtvalue="$2" _info "Using HostUp DNS API" @@ -34,31 +35,34 @@ dns_hostup_add() { record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")" record_name="$(_hostup_sanitize_name "$record_name")" - record_value="$(_hostup_json_escape "$txtvalue")" + hostup_add_record_value="$(_hostup_json_escape "$hostup_add_txtvalue")" - ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}" + raw_ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}" + ttl="$(_hostup_normalize_ttl "$raw_ttl")" + if [ -z "$ttl" ]; then + _err "HOSTUP_TTL must be a whole number between 60 and 86400 seconds." + return 1 + fi + if [ -n "$HOSTUP_TTL" ]; then + HOSTUP_TTL="$ttl" + _saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL" + fi _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 + record_name_fqdn="$(_hostup_fqdn "$fulldomain")" + if _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$hostup_add_txtvalue"; then + _info "TXT record already exists for $fulldomain" + return 0 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 + request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$hostup_add_record_value\",\"ttl\":$ttl}" - 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" + if ! _hostup_rest "POST" "/dns-zones/$HOSTUP_ZONE_ID/records" "$request_body"; then + return 1 fi _info "Added TXT record for $fulldomain" @@ -85,20 +89,9 @@ dns_hostup_rm() { 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" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value" return 0 fi @@ -109,7 +102,7 @@ dns_hostup_rm() { fi _info "Deleted TXT record $HOSTUP_RECORD_ID" - _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value" HOSTUP_ZONE_ID="" return 0 } @@ -127,21 +120,18 @@ _hostup_init() { if [ -z "$HOSTUP_API_BASE" ]; then HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT" fi + HOSTUP_API_BASE="$(_hostup_normalize_api_base "$HOSTUP_API_BASE")" 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." + _err "Please export your HostUp API key with read:dns, write:dns, and read:domains 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 @@ -149,11 +139,80 @@ _hostup_init() { return 0 } +_hostup_normalize_api_base() { + api_base="${1%/}" + + case "$api_base" in + */api/v2) + printf "%s" "$api_base" + ;; + */api) + printf "%s/v2" "$api_base" + ;; + *) + printf "%s" "$api_base" + ;; + esac +} + +_hostup_normalize_ttl() { + ttl_value="$1" + + case "$ttl_value" in + "" | *[!0-9]*) + return 1 + ;; + esac + + while [ "${ttl_value#0}" != "$ttl_value" ]; do + ttl_value="${ttl_value#0}" + done + [ -z "$ttl_value" ] && ttl_value=0 + + case "$ttl_value" in + ??????*) + return 1 + ;; + esac + + if [ "$ttl_value" -lt 60 ] || [ "$ttl_value" -gt 86400 ]; then + return 1 + fi + + printf "%s" "$ttl_value" +} + +_hostup_domain_in_zone() { + host="$(printf "%s" "${1%.}" | _lower_case)" + zone="$(printf "%s" "${2%.}" | _lower_case)" + + if [ -z "$host" ] || [ -z "$zone" ]; then + return 1 + fi + + if [ "$host" = "$zone" ]; then + return 0 + fi + + case "$host" in + *."$zone") + return 0 + ;; + esac + + return 1 +} + _hostup_detect_zone() { fulldomain="$1" if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then - return 0 + if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then + return 0 + fi + _debug "hostup_cached_zone_mismatch" "$HOSTUP_ZONE_DOMAIN" + HOSTUP_ZONE_ID="" + HOSTUP_ZONE_DOMAIN="" fi HOSTUP_ZONE_DOMAIN="" @@ -162,16 +221,16 @@ _hostup_detect_zone() { 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 + if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then + return 0 + fi + _debug "hostup_forced_zone_mismatch" "$HOSTUP_ZONE_DOMAIN" fi HOSTUP_ZONE_ID="" + HOSTUP_ZONE_DOMAIN="" fi - if ! _hostup_load_zones; then - return 1 - fi - - _domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)" + _domain_candidate="$(printf "%s" "${fulldomain%.}" | _lower_case)" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -240,11 +299,11 @@ _hostup_fqdn() { _hostup_fetch_zone_details() { zone_id="$1" - if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then + 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 '"')" + zonedomain="$(_hostup_json_extract "name" "$_hostup_response")" if [ -n "$zonedomain" ]; then HOSTUP_ZONE_DOMAIN="$zonedomain" return 0 @@ -254,7 +313,7 @@ _hostup_fetch_zone_details() { } _hostup_load_zones() { - if ! _hostup_rest "GET" "/dns/zones" ""; then + if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then return 1 fi @@ -263,9 +322,9 @@ _hostup_load_zones() { 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")" + *'"id"'*'"name"'*) + zone_id="$(_hostup_json_extract "id" "$line")" + zone_domain="$(_hostup_json_extract "name" "$line")" if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id} " @@ -290,9 +349,30 @@ _hostup_lookup_zone() { _lookup_zone_id="" _lookup_zone_domain="" + encoded_domain="$(printf "%s" "$lookup_domain" | _url_encode)" + if _hostup_rest "GET" "/dns-zones?name=$encoded_domain&limit=1" ""; then + zone_id="$(_hostup_json_extract "id" "$_hostup_response")" + zone_domain="$(_hostup_json_extract "name" "$_hostup_response")" + if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then + zone_domain_lower="$(printf "%s" "$zone_domain" | _lower_case)" + if [ "$zone_domain_lower" = "$lookup_domain" ]; then + _lookup_zone_domain="$zone_domain" + _lookup_zone_id="$zone_id" + HOSTUP_ZONE_DOMAIN="$zone_domain" + HOSTUP_ZONE_ID="$zone_id" + return 0 + fi + fi + fi + + if [ -z "$HOSTUP_ZONES_CACHE" ] && ! _hostup_load_zones; then + return 1 + fi + while IFS='|' read -r domain zone_id; do [ -z "$domain" ] && continue - if [ "$domain" = "$lookup_domain" ]; then + domain_lower="$(printf "%s" "$domain" | _lower_case)" + if [ "$domain_lower" = "$lookup_domain" ]; then _lookup_zone_domain="$domain" _lookup_zone_id="$zone_id" HOSTUP_ZONE_DOMAIN="$domain" @@ -307,50 +387,50 @@ EOF } _hostup_find_record() { - zone_id="$1" - fqdn="$2" - txtvalue="$3" + _hostup_find_zone_id="$1" + _hostup_find_fqdn="$2" + _hostup_find_txtvalue="$3" - if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then + _hostup_find_encoded_name="$(printf "%s" "$_hostup_find_fqdn" | _url_encode)" + if ! _hostup_rest "GET" "/dns-zones/$_hostup_find_zone_id/records?type=TXT&name=$_hostup_find_encoded_name" ""; then return 1 fi HOSTUP_RECORD_ID="" - records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" + _hostup_find_records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" - while IFS= read -r line; do + while IFS= read -r _hostup_find_line; do # Normalize line to make TXT value matching reliable - line_clean="$(printf "%s" "$line" | tr -d '\r\n')" - line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')" + _hostup_find_line_clean="$(printf "%s" "$_hostup_find_line" | tr -d '\r\n')" + _hostup_find_line_value_clean="$(printf "%s" "$_hostup_find_line_clean" | sed 's/\\"//g')" - case "$line_clean" in - *'"type":"TXT"'*'"name"'*'"value"'*) - name_value="$(_hostup_json_extract "name" "$line_clean")" - record_value="$(_hostup_json_extract "value" "$line_value_clean")" + _hostup_find_record_type="$(_hostup_json_extract "type" "$_hostup_find_line_clean")" + [ "$_hostup_find_record_type" != "TXT" ] && continue - _debug "hostup_record_raw" "$record_value" - if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then - record_value="${record_value#\"}" - record_value="${record_value%\"}" - fi - if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then - record_value="${record_value#\'}" - record_value="${record_value%\'}" - fi - record_value="$(printf "%s" "$record_value" | tr -d '\r\n')" - _debug "hostup_record_value" "$record_value" + _hostup_find_name_value="$(_hostup_json_extract "name" "$_hostup_find_line_clean")" + _hostup_find_record_value="$(_hostup_json_extract "value" "$_hostup_find_line_value_clean")" - if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then - record_id="$(_hostup_json_extract "id" "$line_clean")" - if [ -n "$record_id" ]; then - HOSTUP_RECORD_ID="$record_id" - return 0 - fi + _debug "hostup_record_raw" "$_hostup_find_record_value" + if [ "${_hostup_find_record_value#\"}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\"}" != "$_hostup_find_record_value" ]; then + _hostup_find_record_value="${_hostup_find_record_value#\"}" + _hostup_find_record_value="${_hostup_find_record_value%\"}" + fi + if [ "${_hostup_find_record_value#\'}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\'}" != "$_hostup_find_record_value" ]; then + _hostup_find_record_value="${_hostup_find_record_value#\'}" + _hostup_find_record_value="${_hostup_find_record_value%\'}" + fi + _hostup_find_record_value="$(printf "%s" "$_hostup_find_record_value" | tr -d '\r\n')" + _debug "hostup_record_value" "$_hostup_find_record_value" + + if [ "$_hostup_find_name_value" = "$_hostup_find_fqdn" ] && [ "$_hostup_find_record_value" = "$_hostup_find_txtvalue" ]; then + _hostup_find_record_id="$(_hostup_json_extract "id" "$_hostup_find_line_clean")" + if [ -n "$_hostup_find_record_id" ]; then + HOSTUP_RECORD_ID="$_hostup_find_record_id" + return 0 fi - ;; - esac + fi done < Date: Wed, 1 Jul 2026 14:55:55 +0200 Subject: [PATCH 508/689] Adding custom Port definitions for truenas (#7033) * closing bracket and adding port for customer installations * adding savedeployconfig * fixing shfmt * changeing --------- Co-authored-by: neil --- deploy/truenas_ws.sh | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh index df34f927..33e3dfa0 100644 --- a/deploy/truenas_ws.sh +++ b/deploy/truenas_ws.sh @@ -16,7 +16,12 @@ # # # API KEY # # Use the folowing URL to create a new API token: /ui/apikeys -# export DEPLOY_TRUENAS_APIKEY=" Date: Wed, 1 Jul 2026 15:05:07 +0200 Subject: [PATCH 509/689] fix(dns_infomaniak): correctly detect API errors (#7048) The add/rm success check never rejected anything: for any non-empty API response it always reported "Record added"/"Record deleted" and returned 0, so the _err branch was dead code. A valid key looked fine only because the API call genuinely created the record; an invalid key returning {"result":"error"} produced the same "Record added" output even though nothing was created. Root cause, in: if [ -n "$response" ]; then if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then - _contains() ignores stdin (it reads only $1 and $2), so the piped "$response" was discarded. - The pattern '"result":"success"' was passed as $1 (the haystack), leaving $2 (the needle) empty, so it ran: echo '"result":"success"' | grep -- "" >/dev/null 2>&1 grep with an empty pattern always matches. - That grep output is redirected to /dev/null, so the command substitution always captured "", making [ ! "" ] always true. Fix: call _contains "$response" '"result":"success"' directly and branch on its exit code, so error responses now correctly fail (return 1). Co-authored-by: neil --- dnsapi/dns_infomaniak.sh | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index 0ae32b47..52417fef 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -85,12 +85,10 @@ dns_infomaniak_add() { # API call response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records") - if [ -n "$response" ]; then - if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then - _info "Record added" - _debug "response: $response" - return 0 - fi + if _contains "$response" '"result":"success"'; then + _info "Record added" + _debug "response: $response" + return 0 fi _err "Could not create record." _debug "Response: $response" @@ -169,11 +167,10 @@ dns_infomaniak_rm() { # API call response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE) - if [ -n "$response" ]; then - if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then - _info "Record deleted" - return 0 - fi + if _contains "$response" '"result":"success"'; then + _info "Record deleted" + _debug "response: $response" + return 0 fi _err "Could not delete record." _debug "Response: $response" From d0fcafe29b157b727a34a8961137022b3bb11950 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 1 Jul 2026 21:22:36 +0800 Subject: [PATCH 510/689] fix https://github.com/acmesh-official/acme.sh/issues/7035 --- acme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index a7397be1..59700442 100755 --- a/acme.sh +++ b/acme.sh @@ -7512,8 +7512,8 @@ Parameters: --dnssleep The time in seconds to wait for all the txt records to propagate in dns api mode. It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically. - -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521. - -ak, --accountkeylength Specifies the account key length: 2048, 3072, 4096 + -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521. + -ak, --accountkeylength Specifies the account key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521. --log [file] Specifies the log file. Defaults to \"$DEFAULT_LOG_FILE\" if argument is omitted. --log-level <1|2> Specifies the log level, default is $DEFAULT_LOG_LEVEL. --syslog <0|3|6|7> Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug. From a7ccfcf91d5843840f113a60e65ed11af3d438e8 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 1 Jul 2026 21:33:24 +0800 Subject: [PATCH 511/689] fix --- .github/workflows/GhostBSD.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml index 2dd2412b..c77fdf2e 100644 --- a/.github/workflows/GhostBSD.yml +++ b/.github/workflows/GhostBSD.yml @@ -42,6 +42,8 @@ jobs: # CA_EMAIL: "githubtest@acme.sh" # TEST_PREFERRED_CHAIN: "" runs-on: ubuntu-latest + # GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run + continue-on-error: true env: TEST_LOCAL: 1 TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} From 81100db2f3f4ead0c6e28a48a939aaee7f401a08 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 1 Jul 2026 21:59:41 +0800 Subject: [PATCH 512/689] fix https://github.com/acmesh-official/acme.sh/issues/6498 --- dnsapi/dns_joker.sh | 51 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_joker.sh b/dnsapi/dns_joker.sh index 401471be..0ad80327 100644 --- a/dnsapi/dns_joker.sh +++ b/dnsapi/dns_joker.sh @@ -35,9 +35,28 @@ dns_joker_add() { return 1 fi + # Joker's /nic/replace overwrites all TXT records at the label on every call, + # and the API is not readable, so accumulate the values locally (keyed by the + # full record name) and re-send the whole set each time. This is required so a + # wildcard cert (base + *.domain both validating under the same + # _acme-challenge label) does not overwrite its own first challenge value. + _joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_') + _joker_values=$(_readdomainconf "$_joker_conf_key") + if [ -z "$_joker_values" ]; then + _joker_values="$txtvalue" + elif ! _contains " $_joker_values " " $txtvalue "; then + _joker_values="$_joker_values $txtvalue" + fi + + _joker_value_params="" + for _joker_v in $_joker_values; do + _joker_value_params="$_joker_value_params&value=$_joker_v" + done + _info "Adding TXT record" - if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value=$txtvalue"; then + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then if _startswith "$response" "OK"; then + _savedomainconf "$_joker_conf_key" "$_joker_values" _info "Added, OK" return 0 fi @@ -59,10 +78,36 @@ dns_joker_rm() { return 1 fi + # Remove only this value from the accumulated set and replace the label with + # whatever remains (an empty value clears the label's TXT records entirely). + _joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_') + _joker_values=$(_readdomainconf "$_joker_conf_key") + _joker_remaining="" + for _joker_v in $_joker_values; do + if [ "$_joker_v" != "$txtvalue" ]; then + _joker_remaining="$_joker_remaining $_joker_v" + fi + done + _joker_remaining=$(printf "%s" "$_joker_remaining" | sed 's/^ *//') + + _joker_value_params="" + for _joker_v in $_joker_remaining; do + _joker_value_params="$_joker_value_params&value=$_joker_v" + done + if [ -z "$_joker_value_params" ]; then + _joker_value_params="&value=" + fi + _info "Removing TXT record" - # TXT record is removed by setting its value to empty. - if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value="; then + # TXT record is removed by replacing the label with the remaining values + # (or an empty value, which clears all TXT records at the label). + if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then if _startswith "$response" "OK"; then + if [ -z "$_joker_remaining" ]; then + _cleardomainconf "$_joker_conf_key" + else + _savedomainconf "$_joker_conf_key" "$_joker_remaining" + fi _info "Removed, OK" return 0 fi From c83eed499473861631d4316f9a6831db1251b5b9 Mon Sep 17 00:00:00 2001 From: bluenenschloss Date: Thu, 2 Jul 2026 07:00:49 +0200 Subject: [PATCH 513/689] dns_inwx: fix IDN zone detection without python dependency (#7056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * dns_inwx: fix IDN zone detection without python dependency INWX returns zone names in Unicode form (e.g. lünenschloß.de) even when the domain was registered as an IDN. When acme.sh passes the SAN in punycode (xn--lnenschlo-o1a42a.de), _contains never matches and _get_root falls through to the TLD, placing the TXT record in the wrong zone. Previous fix used python3 which is not available in all environments (BusyBox, BSD, minimal containers). Replace with _idn()-based approach: extract values from the nameserver.list XML response, encode each via _idn(), and compare to $h. When a match is found, use the original Unicode zone name for createRecord. Fixes #7038 * dns_inwx: fix shebang, use _egrep_o, shfmt cleanup - Revert shebang to #!/usr/bin/env sh (POSIX sh, fixes ShellCheck) - Replace grep -o with _egrep_o for portability - shfmt -i 2: drop backslash continuation after pipe, fix indentation Requested by @neilpang * fix: drop closing from _egrep_o pattern to avoid sed delimiter collision --------- Co-authored-by: bluenenschloss --- dnsapi/dns_inwx.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 2cf91404..dba23846 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -312,6 +312,22 @@ _get_root() { _domain="$h" return 0 fi + # IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode, + # encode each zone name via _idn() and compare — no python dependency. + if _contains "$h" "xn--"; then + _zone_unicode=$(printf "%s" "$response" | _egrep_o '[^<]*' | + sed 's/<[^>]*>//g' | while IFS= read -r _z; do + if [ "$(_idn "$_z")" = "$h" ]; then + printf "%s" "$_z" + break + fi + done) + if [ -n "$_zone_unicode" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain="$_zone_unicode" + return 0 + fi + fi p=$i i=$(_math "$i" + 1) done From b039ff3087e84ce664eedb048bb2e539f881f218 Mon Sep 17 00:00:00 2001 From: pxMan79 Date: Thu, 2 Jul 2026 13:02:30 +0800 Subject: [PATCH 514/689] fix(dns_baidu): prefer new Baidu DNS API with legacy BCD fallback (#6992) * fix(acme): prefer new Baidu DNS API with legacy BCD fallback Keep the existing BCD implementation and add fallback support for the newer Baidu DNS record API. Prefer the new API by default, then fall back to the legacy BCD API to reduce compatibility risk. * fix(dns_baidu): route through _get/_post + restore legacy BCD auth headers Per review: _baidu_dns_call now uses _get/_post with _H1.._H5 (no raw curl, no __HTTP_STATUS__ parsing); _baidu_bcd_post restores _H1.._H5 so the legacy BCD path sends the Authorization signature again (fixes 401). --------- Co-authored-by: neil --- dnsapi/dns_baidu.sh | 329 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 278 insertions(+), 51 deletions(-) diff --git a/dnsapi/dns_baidu.sh b/dnsapi/dns_baidu.sh index 8651deab..dfad8eeb 100644 --- a/dnsapi/dns_baidu.sh +++ b/dnsapi/dns_baidu.sh @@ -49,26 +49,95 @@ Options: Baidu_SK SecretAccessKey OptionsAlt: Baidu_BCD_Host API host, default: bcd.baidubce.com + Baidu_DNS_Host New DNS API host, default: dns.baidubce.com + Baidu_API_Preference Engine preference, default: auto Baidu_BCD_Version API version number, default: 1 Baidu_BCD_Expire Signature expiration seconds, default: 3600 Baidu_View Resolve view, default: DEFAULT + Baidu_Line New DNS line, default: default Baidu_TTL Resolve ttl seconds, default: 300 Baidu_RM_Max Max records to delete in one run, default: 20 ' BAIDU_BCD_DEFAULT_HOST="bcd.baidubce.com" +BAIDU_DNS_DEFAULT_HOST="dns.baidubce.com" # --- Public API --- dns_baidu_add() { fulldomain=$(_idn "$1") txtvalue=$2 - if ! _baidu_prepare_record "$fulldomain"; then - _baidu_err "baidu_prepare_record failed for add: $fulldomain" + if ! _baidu_run_with_fallback "add" "$fulldomain" "$txtvalue"; then + _baidu_err "all baidu api engines failed for add: $fulldomain" return 1 fi - if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + return 0 +} + +dns_baidu_rm() { + fulldomain=$(_idn "$1") + txtvalue=$2 + + if ! _baidu_run_with_fallback "rm" "$fulldomain" "$txtvalue"; then + _baidu_err "all baidu api engines failed for delete: $fulldomain" + return 1 + fi + + return 0 +} + +_baidu_run_with_fallback() { + _action="$1" + _fulldomain="$2" + _txtvalue="$3" + + if ! _baidu_load_credentials; then + _baidu_err "baidu_load_credentials failed" + return 1 + fi + + for _baidu_api_engine in $(_baidu_engine_order); do + if ! _baidu_prepare_record "$_fulldomain"; then + _baidu_info "prepare failed for engine: $_baidu_api_engine" + continue + fi + + if [ "$_action" = "add" ]; then + if _baidu_add_record "$_txtvalue"; then + return 0 + fi + else + if _baidu_rm_record "$_txtvalue"; then + return 0 + fi + fi + + _baidu_info "engine failed, try next if available: $_baidu_api_engine" + done + + return 1 +} + +_baidu_engine_order() { + _pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")" + case "$_pref" in + legacy) + printf "%s" "legacy new" + ;; + new) + printf "%s" "new legacy" + ;; + *) + printf "%s" "new legacy" + ;; + esac +} + +_baidu_add_record() { + _txtvalue="$1" + + if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then _baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name" return 1 fi @@ -85,16 +154,28 @@ dns_baidu_add() { _ttl="300" ;; esac - _view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")" - txtvalue="$(_baidu_trim_ws "$txtvalue")" + + txtvalue="$(_baidu_trim_ws "$_txtvalue")" _record_domain="$(_baidu_trim_ws "$_record_domain")" _zone_name="$(_baidu_trim_ws "$_zone_name")" - _body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")" - - if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then - _baidu_err "baidu_bcd_post failed: add record" - return 1 + if [ "$_baidu_api_engine" = "new" ]; then + _line="$(_baidu_trim_ws "${Baidu_Line:-default}")" + if [ -z "$_line" ]; then + _line="default" + fi + _body="$(_baidu_payload_add_txt_dns "$_record_domain" "$txtvalue" "$_ttl" "$_line")" + if ! _baidu_dns_call "POST" "/v1/dns/zone/${_zone_name}/record" "$_body"; then + _baidu_err "baidu_dns_call failed: add record" + return 1 + fi + else + _view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")" + _body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")" + if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then + _baidu_err "baidu_bcd_post failed: add record" + return 1 + fi fi if _baidu_is_api_error "$response"; then @@ -105,16 +186,10 @@ dns_baidu_add() { return 0 } -dns_baidu_rm() { - fulldomain=$(_idn "$1") - txtvalue=$2 +_baidu_rm_record() { + _txtvalue="$1" - if ! _baidu_prepare_record "$fulldomain"; then - _baidu_err "baidu_prepare_record failed for delete: $fulldomain" - return 1 - fi - - if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then _baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name" return 1 fi @@ -138,28 +213,37 @@ dns_baidu_rm() { fi for _rid in $_ids; do - _body="$(_baidu_payload_delete "$_zone_name" "$_rid")" - if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then - _baidu_err "baidu_bcd_post failed: delete recordId=$_rid" - return 1 - fi - if _baidu_is_api_error "$response"; then - _baidu_err "$response" - return 1 + if [ "$_baidu_api_engine" = "new" ]; then + if ! _baidu_dns_call "DELETE" "/v1/dns/zone/${_zone_name}/record/${_rid}" ""; then + _baidu_err "baidu_dns_call failed: delete recordId=$_rid" + return 1 + fi + else + _body="$(_baidu_payload_delete "$_zone_name" "$_rid")" + if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then + _baidu_err "baidu_bcd_post failed: delete recordId=$_rid" + return 1 + fi + if _baidu_is_api_error "$response"; then + _baidu_err "$response" + return 1 + fi fi done - if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then - _baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name" - return 1 - fi - _left_ids="$_BAIDU_FIND_RESULT" - if [ -z "$_left_ids" ]; then - return 0 - fi - if [ -n "$_left_ids" ]; then - _baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records" - return 1 + if [ "$_baidu_api_engine" = "legacy" ]; then + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then + _baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name" + return 1 + fi + _left_ids="$_BAIDU_FIND_RESULT" + if [ -z "$_left_ids" ]; then + return 0 + fi + if [ -n "$_left_ids" ]; then + _baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records" + return 1 + fi fi return 0 @@ -182,6 +266,7 @@ _baidu_load_credentials() { _saveaccountconf_mutable Baidu_SK "$Baidu_SK" BAIDU_BCD_HOST="${Baidu_BCD_Host:-$BAIDU_BCD_DEFAULT_HOST}" + BAIDU_DNS_HOST="${Baidu_DNS_Host:-$BAIDU_DNS_DEFAULT_HOST}" BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}" return 0 @@ -189,13 +274,16 @@ _baidu_load_credentials() { _baidu_prepare_record() { _fulldomain="$1" - if ! _baidu_load_credentials; then - _baidu_err "baidu_load_credentials failed" - return 1 - fi - if ! _baidu_get_root "$_fulldomain"; then - _baidu_err "Could not find zone for $_fulldomain" - return 1 + if [ "$_baidu_api_engine" = "new" ]; then + if ! _baidu_get_root_dns "$_fulldomain"; then + _baidu_err "Could not find zone by new dns api for $_fulldomain" + return 1 + fi + else + if ! _baidu_get_root "$_fulldomain"; then + _baidu_err "Could not find zone by legacy bcd api for $_fulldomain" + return 1 + fi fi _record_domain="$_sub_domain" _zone_name="$_domain" @@ -234,6 +322,43 @@ _baidu_get_root() { done } +_baidu_get_root_dns() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + _baidu_err "invalid domain: $domain" + return 1 + fi + + if ! _baidu_dns_call "GET" "/v1/dns/zone/${h}/record" ""; then + _baidu_info "baidu_dns_call failed: list zones" + elif ! _baidu_is_api_error "$response" && (_contains "$response" "\"records\"" || _contains "$response" "\"maxKeys\""); then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + if [ "$_sub_domain" = "$_domain" ]; then + _sub_domain="@" + fi + _baidu_info "zone matched by dns api: $_domain (host: $_sub_domain)" + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done +} + +_baidu_find_record_ids_current() { + if [ "$_baidu_api_engine" = "new" ]; then + _baidu_find_record_ids_dns "$@" + else + _baidu_find_record_ids "$@" + fi +} + _baidu_find_record_ids() { _zone_name="$1" _record_domain="$2" @@ -293,6 +418,39 @@ EOF _BAIDU_FIND_RESULT="$_ids" } +_baidu_find_record_ids_dns() { + _zone_name="$1" + _record_domain="$2" + _rdtype="$3" + _rdata="$4" + _BAIDU_FIND_RESULT="" + + if ! _baidu_dns_call "GET" "/v1/dns/zone/${_zone_name}/record" ""; then + _baidu_err "baidu_dns_call failed: list records" + return 1 + fi + + if _baidu_is_api_error "$response"; then + _baidu_err "baidu_dns error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")" + return 1 + fi + + _normalized="$(printf "%s" "$response" | _normalizeJson)" + _records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g') + _ids="" + + while IFS= read -r _line; do + _id="$(_baidu_match_record_id_dns "$_line" "$_record_domain" "$_rdtype" "$_rdata")" + if [ "$_id" ]; then + _ids="$_ids $_id" + fi + done < Date: Thu, 2 Jul 2026 07:06:41 +0200 Subject: [PATCH 515/689] Implemented support for Aruba Business DNS API (#7042) * Add support for arubabusiness api * Fix formatting * record names are always converted to lowercase * Docs * remove leftover unconditional authentication call lowercase urlencoded body + x-www-form-urlencoded content-type cleanup header variables cleanup typos grammar * Strengthen _ab_rest failure checks Properly process parallel lists in _ab_dns_record_id Remove hard fails when a txt record already exists * fix json parsing * Fix formatting --------- Co-authored-by: Manwe-Sulimo --- dnsapi/dns_arubabusiness.sh | 490 ++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 dnsapi/dns_arubabusiness.sh diff --git a/dnsapi/dns_arubabusiness.sh b/dnsapi/dns_arubabusiness.sh new file mode 100644 index 00000000..90b3f18d --- /dev/null +++ b/dnsapi/dns_arubabusiness.sh @@ -0,0 +1,490 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 +dns_arubabusiness_info='ArubaBusiness +Site: business.aruba.it +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness +Options: + AB_Key Your ArubaBusiness API Key + AB_User Your account user + AB_Pass Your account password +' + +# +# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type. +# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges +# + +ARUBABUSINESS_API='https://api.arubabusiness.it' + +######## Public functions ######## + +# +# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333 +# +# Add a new TXT record whose name and value match the given domain and value +# +# Variables +# _full_domain: $1 - the name of the TXT record +# _txt_value: $2 - the value of the TXT record +# _body +# dns_details +# domain_id +# dns_record_id +# response +# +dns_arubabusiness_add() { + _full_domain=$1 + _txt_value=$2 + + if ! _ab_authenticate; then + return 1 + fi + + if ! _ab_domain_id "$_full_domain"; then + return 1 + fi + + if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then + # This is very unlikely, but allow the process to use the existing record + _info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)" + return 0 + fi + + _body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }" + + _debug "Adding TXT record with name: $_full_domain and value: $_txt_value" + + if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then + _err "Failed to add TXT record with name: $_full_domain" + return 1 + fi + + _info "Sleeping 10 seconds to let ArubaBusiness do its magic" + _sleep 10 + + # Refresh dns details and check that the record was really added + if ! _ab_dns_details "$root_domain"; then + return 1 + fi + + if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then + # This should never happen + _err "The TXT record with name: $_full_domain was not set" + _err "Please check that the dns records are clean" + return 1 + fi + + _info "Added TXT record with id: $dns_record_id" + return 0 +} + +# +# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333 +# +# Remove the TXT record whose name and value match the given domain and value +# +# Variables +# _full_domain: $1 - the name of the TXT record +# _txt_value: $2 - the value of the TXT record +# dns_details +# dns_record_id +# +dns_arubabusiness_rm() { + _full_domain=$1 + _txt_value=$2 + + if ! _ab_authenticate; then + return 1 + fi + + if ! _ab_domain_id "$_full_domain"; then + return 1 + fi + + if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then + _err "Could not retrieve the record id for: $_full_domain" + return 1 + fi + + _debug "Deleting TXT record: $dns_record_id" + if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then + _err "Failed to delete TXT record: $dns_record_id" + return 1 + fi + + _info "Deleted TXT record: $dns_record_id" + return 0 +} + +######## Private functions ######## + +# +# Usage: _ab_domain_id _acme-challenge.www.domain.com +# +# Split the input domain into subdomain + root domain and get the id of the root domain +# +# Variables +# _full_domain: $1 - the domain whose root needs to be extracted +# _domain_sections +# _current_index +# _candidate_subdomain +# _candidate_domain +# sub_domain +# root_domain +# domain_id +# dns_details: a json containing all dns records registered on the root domain +# +# Example +# _get_root _acme-challenge.www.domain.com +# +# Should return +# sub_domain=_acme-challenge.www +# root_domain=domain.com +# domain_id=123123123123 +# dns_details="{JSON_CONTENT}" +# +_ab_domain_id() { + _full_domain=$1 + + _info "Attempting to retrieve root domain details for: $_full_domain" + + _domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1) + + if [ "$_domain_sections" -lt 1 ]; then + _err "Invalid input $_full_domain" + return 1 + fi + + _current_index=1 + while true; do + _candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi) + _candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections") + + if ! _ab_dns_details "$_candidate_domain"; then + _debug2 "Could not fetch dns details for: $_candidate_domain" + _current_index=$(_math "$_current_index" + 1) + + # Fail if there are no candidates left + if [ "$_current_index" -gt "$_domain_sections" ]; then + _err "Could not determine the root domain for: $_full_domain" + return 1 + fi + else + sub_domain="$_candidate_subdomain" + root_domain="$_candidate_domain" + # Extract the domain id, which is an integer and contains no commas + domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')" + + if [ -z "$domain_id" ]; then + _err "Could not determine the domain id for: $root_domain" + return 1 + fi + + _debug "Retrieved root domain id: $domain_id" + return 0 + fi + done +} + +# +# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}" +# +# Extract the record id of the first TXT record whose name and content match the input values +# +# Variables +# _record_name: $1 +# _txt_value: $2 +# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain' +# _record_ids +# _record_names +# _record_types +# _record_contents +# _record_ids_count +# _record_names_count +# _record_types_count +# _record_contents_count +# _i +# dns_record_id +# +# Notes +# TXT correspond to record type 5 +# ArubaBusiness appends a terminating dot (.) to the record name +# The content field may contain the following character sequence: \" +# All record names are always converted to lowercase +# +_ab_dns_record_id() { + _record_name=$1 + _txt_value=$2 + _dns_details=$3 + + _record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case) + + # Extract the record ids, which are integers and contain no commas, colons or spaces + # The first id is skipped because it refers to the domain id + _record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ') + + # Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes + # The first name is skipped because it refers to the domain name + _record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ') + + # Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces + # The first type is skipped because it refers to the domain type + _record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ') + + # Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals + # Note: There is no domain related entry here + # Note: A " character is appended at the end of each content to make it easier to process the list later + _record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#') + + _info "IDS: $_record_ids" + _info "NAMES: $_record_names" + _info "TYPEs: $_record_types" + _info "CONTENTS: $_record_contents" + + _record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l) + _record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l) + _record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l) + _record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l) + + _info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count" + + if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then + _err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count" + return 1 + fi + + _info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value" + + _i=1 + while [ "$_i" -le "$_record_ids_count" ]; do + _current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i") + _current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i") + _current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i") + + if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then + dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i") + _info "Found matching record with id: $dns_record_id" + return 0 + else + _debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'" + fi + _i=$(_math "$_i" + 1) + done + + _debug2 "No matching record was found in $_dns_details" + return 1 +} + +# +# Usage: _ab_dns_details domain.com +# +# Retrieve dns info for the given input domain +# +# Variables +# _domain: $1 +# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0) +# response +# +_ab_dns_details() { + _domain=$1 + + if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then + return 1 + fi + + dns_details="$response" + return 0 +} + +# +# Usage: _ab_authenticate +# +# Read account conf, update domain conf and perform user authentication to acquire an access token +# +# Variables +# AB_Key +# AB_User +# AB_Pass +# AB_Token +# +_ab_authenticate() { + AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}" + AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}" + AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}" + + if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then + AB_Key="" + AB_User="" + AB_Pass="" + _err "Either the ArubaBusiness API key, the user or the password has not been defined yet." + _err "Please configure them and try again." + return 1 + fi + + _saveaccountconf_mutable AB_Key "$AB_Key" + _saveaccountconf_mutable AB_User "$AB_User" + _saveaccountconf_mutable AB_Pass "$AB_Pass" + + if ! _ab_get_token || [ -z "$AB_Token" ]; then + _err "Failed to acquire an access token" + return 1 + fi + + return 0 +} + +# +# Usage: _ab_get_token +# +# Try acquiring a temporary access token. The token should have a 24h lifespan +# +# Variables +# _ab_user_enc +# _ab_pass_enc +# _ab_authdata +# AB_User +# AB_Pass +# AB_Token +# response +# _H2 +# +_ab_get_token() { + _ab_user_enc=$(printf "%s" "$AB_User" | _url_encode) + _ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode) + _ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc" + + _H2="Content-Type: application/x-www-form-urlencoded" + + if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then + _err "Authentication failure" + return 1 + fi + + AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')" + + if [ -z "$AB_Token" ]; then + _err "Could not extract access token" + return 1 + fi + + _debug "Acquired access token" + return 0 +} + +# +# Usage: _ab_rest POST "example/endpoint" "password=123" +# +# Perform a REST request using the given method, endpoint and data +# +# Variables +# _method: $1 - The http method +# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API) +# _data: $3 - The body of the request (optional) +# _key_trimmed +# _token_trimmed +# _ret_code +# AB_Key +# AB_Token +# ARUBABUSINESS_API +# _H1 +# _H2 +# _H3 +# _H4 +# +_ab_rest() { + _method=$1 + _endpoint="$2" + _data="$3" + + _key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"') + _token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"') + + _H1="Accept: application/json" + + if [ -z "$_H2" ]; then + # Default to application/json + _H2="Content-Type: application/json" + fi + + if [ "$_key_trimmed" ]; then + _H3="Authorization-Key: $_key_trimmed" + else + _err "Missing Api Key" + _ab_cleanup_headers + return 1 + fi + + if [ "$_token_trimmed" ]; then + _H4="Authorization: Bearer $_token_trimmed" + else + _debug "No access token set" + fi + + if [ "$_method" != "GET" ]; then + response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")" + else + response="$(_get "$ARUBABUSINESS_API/$_endpoint")" + fi + + _ret_code=$? + + if [ "$_ret_code" = "0" ] && _ab_call_is_success; then + # Normalize the json response + response="$(printf "%s" "$response" | _normalizeJson)" + _ret_code=0 + else + _err "Failed to call endpoint: $_endpoint" + _ret_code=1 + fi + + _ab_cleanup_headers + + return $_ret_code +} + +# +# Usage: _ab_cleanup_headers +# +# Unset header variables to avoid interfering with other calls +# +# Variables +# _H1 +# _H2 +# _H3 +# _H4 +# +_ab_cleanup_headers() { + # Cleanup request headers + unset _H1 _H2 _H3 _H4 _H5 + + # Cleanup response headers + if [ -f "$HTTP_HEADER" ]; then + : >"$HTTP_HEADER" + fi +} + +# +# Usage: _ab_call_is_success +# +# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled) +# +# Variables +# _status +# _http_status +# _success_http_codes +# HTTP_HEADER +# +_ab_call_is_success() { + _success_http_codes="200 201 202 204" + if [ -f "$HTTP_HEADER" ]; then + _http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2) + for _status in $_success_http_codes; do + if [ "$_status" = "$_http_status" ]; then + return 0 + fi + done + fi + + return 1 +} From e52d75d762fc389966915d7952328e390d24424e Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 2 Jul 2026 13:09:48 +0800 Subject: [PATCH 516/689] fix https://github.com/acmesh-official/acme.sh/issues/7062 --- dnsapi/dns_selfhost.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_selfhost.sh b/dnsapi/dns_selfhost.sh index 4912dfdf..40cc0210 100644 --- a/dnsapi/dns_selfhost.sh +++ b/dnsapi/dns_selfhost.sh @@ -18,7 +18,7 @@ dns_selfhost_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txt" - SELFHOSTDNS_UPDATE_URL="https://selfhost.de/cgi-bin/api.pl" + SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl" # Get values, but don't save until we successfully validated SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}" From 5433ea86c8fcf10c038f75f93cf3a5f12e4a3afe Mon Sep 17 00:00:00 2001 From: Toni Karppi Date: Thu, 2 Jul 2026 07:10:29 +0200 Subject: [PATCH 517/689] Add Glesys dnsapi provider (#7059) * Add Glesys dnsapi provider * Fix typo in error message for dns_glesys_add * Use API to get record id in Glesys provider * Use listrecords API endpoint to find root domain * Remove record id parsin from add function --- dnsapi/dns_glesys.sh | 263 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 dnsapi/dns_glesys.sh diff --git a/dnsapi/dns_glesys.sh b/dnsapi/dns_glesys.sh new file mode 100644 index 00000000..008abd12 --- /dev/null +++ b/dnsapi/dns_glesys.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_glesys_info='Glesys +Site: Glesys.se +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys +Options: + GLESYS_API_KEY Generated API key. + GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345). + GLESYS_API API endpoint. Default "https://api.glesys.com/domain". + GLESYS_TTL TXT record TTL. Default 120. +Issues: https://github.com/acmesh-official/acme.sh/issues/7057 +Author: Toni Karppi +' + +GLESYS_API_DEFAULT="https://api.glesys.com/domain" +GLESYS_TTL_DEFAULT="120" + +######## Public functions ##################################################### + +# Usage: +# dns_glesys_add _acme-challenge.www.example.com "txt-value" +dns_glesys_add() { + fulldomain="$1" + txtvalue="$2" + + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _glesys_init || return 1 + + if ! _glesys_get_root "$fulldomain"; then + _err "Could not find root zone for $fulldomain" + return 1 + fi + + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + host_value="${_sub_domain:-@}" + _debug _host_value "$host_value" + + data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}" + + _debug2 data "$data" + + if ! _glesys_rest POST "/addrecord" "$data"; then + _err "Failed to send HTTP request to add TXT record" + return 1 + fi + + response_code=$( + printf "%s" "$response" | + tr -d '\r\n\t ' | + _egrep_o '"code":"?[0-9]+' | + _egrep_o '[0-9]+$' + ) + + _debug response_code "$response_code" + + if [ "$response_code" != "200" ]; then + _err "GleSYS API responded with an unexpected status when attempting to add TXT record" + _debug2 "API response" "$response" + return 1 + fi + + _info "TXT record added" + + return 0 +} + +# Usage: +# dns_glesys_rm _acme-challenge.www.example.com "txt-value" +dns_glesys_rm() { + fulldomain="$1" + txtvalue="$2" + + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + _glesys_init || return 1 + + if ! _glesys_get_root "$fulldomain"; then + _err "Could not find root zone for $fulldomain" + return 1 + fi + + if ! _glesys_find_record_id "$txtvalue"; then + _info "TXT record not present, skip removal" + return 0 + fi + + _debug _record_id "$_record_id" + + if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then + _err "Failed to send HTTP request to remove TXT record" + return 1 + fi + + response_code=$( + printf "%s" "$response" | + tr -d '\r\n\t ' | + _egrep_o '"code":"?[0-9]+' | + _egrep_o '[0-9]+$' + ) + + _debug response_code "$response_code" + + if [ "$response_code" != "200" ]; then + _err "GleSYS API responded with unexpected status when attempting to remove TXT record" + _debug2 "API response" "$response" + return 1 + fi + + _info "TXT record removed" + + return 0 +} + +######## Private functions #################################################### + +_glesys_find_record_id() { + txtvalue="$1" + + _debug txtvalue "$txtvalue" + + if [ -z "$txtvalue" ]; then + return 1 + fi + + _record_id="" + + _debug "Looking for TXT record with value" "$txtvalue" + + if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then + _err "Failed to list DNS records" + return 1 + fi + + records="$( + printf "%s" "$response" | + tr -d '\r\n\t ' | + sed 's/},{/}\ +{/g' + )" + + _debug2 records "$records" + + expected_data="\"data\":\"$txtvalue\"" + + _record_id="$( + printf "%s\n" "$records" | + while IFS= read -r record; do + printf "%s" "$record" | grep -q '"type":"TXT"' || continue + printf "%s" "$record" | grep -Fq "$expected_data" || continue + + printf "%s" "$record" | + grep -E -o '"recordid":"?[0-9]+' | + grep -E -o '[0-9]+$' + + break + done + )" + + _debug _record_id "$_record_id" + + if [ -z "$_record_id" ]; then + return 1 + fi + + return 0 +} + +# Finds: +# _domain example.com +# _sub_domain _acme-challenge.www +_glesys_get_root() { + domain="$1" + i=1 + + while true; do + h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)" + + if [ -z "$h" ]; then + return 1 + fi + + if _glesys_rest GET "/listrecords?domainname=$h"; then + response_code=$( + printf "%s" "$response" | + tr -d '\r\n\t ' | + _egrep_o '"code":"?[0-9]+' | + _egrep_o '[0-9]+$' + ) + + _debug response_code "$response_code" + + if [ "$response_code" = "200" ]; then + cut_len="$((${#domain} - ${#h} - 1))" + _domain="$h" + _sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")" + return 0 + fi + fi + + i="$((i + 1))" + done +} + +_glesys_init() { + [ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT" + [ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT" + + _debug GLESYS_API "$GLESYS_API" + _debug GLESYS_TTL "$GLESYS_TTL" + + GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}" + GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}" + + if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then + _err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider" + return 1 + fi + + _secure_debug GLESYS_API_KEY "$GLESYS_API_KEY" + _secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID" + + _glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)" + _secure_debug2 _glesys_basic_auth "$_glesys_basic_auth" + + _saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY" + _saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID" + + return 0 +} + +_glesys_rest() { + method="$1" + path="$2" + data="$3" + + export _H1="Authorization: Basic $_glesys_basic_auth" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + url="$GLESYS_API$path" + _debug "$method $url" + + if [ "$method" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url" "" "$method")" + fi + + ret="$?" + _debug2 response "$response" + _debug ret "$ret" + + if [ "$ret" != "0" ]; then + return 1 + fi + + return 0 +} From a50fad865fd63cafe83efc3007b02748fdad9568 Mon Sep 17 00:00:00 2001 From: MarFri <163347538+M4rFri@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:08:59 +0200 Subject: [PATCH 518/689] DNS_IONOS double sending content type & case sensitive mismatch (#7028) * double sending content type results in error from ionos * Normalize fulldomain to lowercase in the _ionos_get_record function. --- dnsapi/dns_ionos.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_ionos.sh b/dnsapi/dns_ionos.sh index 9a464253..00662e82 100755 --- a/dnsapi/dns_ionos.sh +++ b/dnsapi/dns_ionos.sh @@ -16,7 +16,7 @@ IONOS_TXT_TTL=60 # minimum accepted by API IONOS_TXT_PRIO=10 dns_ionos_add() { - fulldomain=$1 + fulldomain="$(echo "$1" | _lower_case)" txtvalue=$2 if ! _ionos_init; then @@ -34,7 +34,7 @@ dns_ionos_add() { } dns_ionos_rm() { - fulldomain=$1 + fulldomain="$(echo "$1" | _lower_case)" txtvalue=$2 if ! _ionos_init; then @@ -146,7 +146,7 @@ _ionos_rest() { if [ "$method" != "GET" ]; then export _H2="Accept: application/json" - export _H3="Content-Type: application/json" + export _H3= _response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")" else From ccd2f04c338d2a4dffaa55539003014b713bba23 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 2 Jul 2026 23:15:29 +0800 Subject: [PATCH 519/689] fix https://github.com/acmesh-official/acme.sh/issues/6986 --- acme.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/acme.sh b/acme.sh index 59700442..ad8a3839 100755 --- a/acme.sh +++ b/acme.sh @@ -4707,6 +4707,13 @@ issue() { else _cleardomainconf "Le_ChallengeAlias" fi + # Save Le_DNSSleep unconditionally here: the save inside the dns_entries + # branch is skipped when all authorizations are already valid (e.g. issuing + # the ECC twin of a just-issued RSA cert), which left the setting out of + # that cert's conf. https://github.com/acmesh-official/acme.sh/issues/6986 + if [ "$Le_DNSSleep" ]; then + _savedomainconf "Le_DNSSleep" "$Le_DNSSleep" + fi if [ "$_preferred_chain" ]; then _savedomainconf "Le_Preferred_Chain" "$_preferred_chain" "base64" else From 42e13fa7970fcd69f54be937a3488d61644c7a33 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 2 Jul 2026 23:18:26 +0800 Subject: [PATCH 520/689] fix https://github.com/acmesh-official/acme.sh/issues/6851 --- dnsapi/dns_infomaniak.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index 52417fef..43ade8ce 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -129,7 +129,7 @@ dns_infomaniak_rm() { fi export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN" - export _H2="ContentType: application/json" + export _H2="Content-Type: application/json" fulldomain=$1 txtvalue=$2 From 116c05fbff85574b0065af7846cebe9c91c181e9 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 2 Jul 2026 23:30:56 +0800 Subject: [PATCH 521/689] fix https://github.com/acmesh-official/acme.sh/issues/6400 --- acme.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index ad8a3839..d714d421 100755 --- a/acme.sh +++ b/acme.sh @@ -7738,9 +7738,16 @@ _checkSudo() { return 0 fi if [ -n "$SUDO_COMMAND" ]; then - #it's a normal user doing "sudo su", or `sudo -i` or `sudo -s`, or `sudo su acmeuser1` - _endswith "$SUDO_COMMAND" /bin/su || _contains "$SUDO_COMMAND" "/bin/su " || grep "^$SUDO_COMMAND\$" /etc/shells >/dev/null 2>&1 - return $? + #The SUDO_* env vars are often inherited into shells that were not + #started as `sudo acme.sh` at all (e.g. `sudo su - user`, or + #`sudo pct enter ` on Proxmox, which copies them into the + #container). Only warn when sudo was used to run acme.sh itself; + #anything else means the sudo happened further up and is fine. + #https://github.com/acmesh-official/acme.sh/issues/6400 + if _contains "$SUDO_COMMAND" "$PROJECT_ENTRY"; then + return 1 + fi + return 0 fi #otherwise return 1 From ced8d72808810c69703f52eda1f48accdd9181fa Mon Sep 17 00:00:00 2001 From: "Simon V." <218359733+sim0n-v@users.noreply.github.com> Date: Fri, 3 Jul 2026 09:12:32 +0200 Subject: [PATCH 522/689] ARI - Run cron job more frequently (#6939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ARI - Run cron job more frequently With ACME Renewal Info (RFC9773 §4.3), fetching renewal window should be more frequent, e.g. in case of revocation incident. "For instance, a server that needs to revoke certificates within 24 hours of notification of a problem might choose to reserve twelve hours for investigation, six hours for clients to fetch updated RenewalInfo objects, and six hours for clients to perform a renewal." More flexible option is to run the cron job every hour and only refresh ARI when the last one + Retry-After header is in the past. * Fix cron job schedule for certificate renewal * Fix random_hour syntax in cron job installation * Update Windows task scheduler to run more frequently Add support for randomized hour and update frequency Ref: * [/mo](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create#to-schedule-a-task-to-run-every-n-hours) * [/SC HOURLY](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks-create#parameters) * Add padding for StartTime (/ST) in SCHTASKS.exe * New Banner Updated README to include responsive images for dark and light modes. * rebase * Reset README --------- Co-authored-by: ZeroSSL-Andreas --- acme.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index d714d421..ad7a6325 100755 --- a/acme.sh +++ b/acme.sh @@ -6470,6 +6470,7 @@ _install_win_taskscheduler() { _lesh="$1" _centry="$2" _randomminute="$3" + _randomhour="$4" if ! _exists cygpath; then _err "cygpath not found" return 1 @@ -6539,7 +6540,7 @@ installcronjob() { fi _t=$(_time) random_minute=$(_math $_t % 60) - random_hour=$(_math $_t / 60 % 24) + random_hour=$(_math $_t / 60 % 6) if ! _exists "$_CRONTAB" && _exists "fcrontab"; then _CRONTAB="fcrontab" @@ -6548,7 +6549,7 @@ installcronjob() { if ! _exists "$_CRONTAB"; then if _exists cygpath && _exists schtasks.exe; then _info "It seems you are on Windows, let's install the Windows scheduler task." - if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute"; then + if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute" "$random_hour"; then _info "Successfully installed Windows scheduler task." return 0 else @@ -6570,7 +6571,7 @@ installcronjob() { fi $_CRONTAB -l 2>/dev/null | { cat - echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" + echo "$random_minute $random_hour,$(_math $random_hour + 6),$(_math $random_hour + 12),$(_math $random_hour + 18) * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" } | $_CRONTAB_STDIN fi if [ "$?" != "0" ]; then From fdf528c26c72884981dec9600834c8411a87ca32 Mon Sep 17 00:00:00 2001 From: Howtin Date: Fri, 3 Jul 2026 18:09:32 +0800 Subject: [PATCH 523/689] feat: Add Volcano Engine DNS API (#7069) * feat: add volcengine dns api * fix(volcengine): address review findings and fix record matching Code review fixes: - fix format string usage in signature computation (use printf %b / %s) - clear _H1.._H5 header state at start of request to avoid leaking conditionally-set headers into subsequent requests - check ListZones return status in _get_root - document Volcengine_SESSION_TOKEN option and fix duplicate "and" typo - fix Docs and Issues sections - remove and update some code comments Functional fixes: - stop matching ListRecords results by FQDN string: Volcengine lowercases the Host/FQDN in responses, so a case-sensitive compare against $fulldomain failed for mixed-case names, making rm silently skip deletion and add lose idempotency. ListRecords is already filtered by ZID+Host+Value+SearchMode:exact, so just extract RecordID from the result instead. - reset _record_id at the start of add/rm to avoid stale state leaking across calls within the same process - tag created records with Remark "acme.sh" for easier identification - adjust debug levels: hide Authorization header behind _debug2, surface response at _debug Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: wenxuan70 Co-authored-by: Claude Opus 4.8 --- dnsapi/dns_volcengine.sh | 297 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100755 dnsapi/dns_volcengine.sh diff --git a/dnsapi/dns_volcengine.sh b/dnsapi/dns_volcengine.sh new file mode 100755 index 00000000..2cc805d5 --- /dev/null +++ b/dnsapi/dns_volcengine.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_volcengine_info='Volcano Engine DNS API +Site: https://www.volcengine.com/docs/6758/155086 +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_volcengine +Options: + Volcengine_ACCESS_KEY_ID API Key ID + Volcengine_SECRET_ACCESS_KEY API Secret + Volcengine_SESSION_TOKEN Session Token. Optional, only needed when using temporary STS credentials. +Issues: github.com/acmesh-official/acme.sh/issues/7064 +' + +Volcengine_HOST="dns.volcengineapi.com" +Volcengine_URL="https://$Volcengine_HOST" + +######## Public functions ##################### + +#fulldomain txtvalue +dns_volcengine_add() { + fulldomain=$1 + txtvalue=$2 + _record_id="" + + Volcengine_ACCESS_KEY_ID="${Volcengine_ACCESS_KEY_ID:-$(_readaccountconf_mutable Volcengine_ACCESS_KEY_ID)}" + Volcengine_SECRET_ACCESS_KEY="${Volcengine_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable Volcengine_SECRET_ACCESS_KEY)}" + + if [ -z "$Volcengine_ACCESS_KEY_ID" ] || [ -z "$Volcengine_SECRET_ACCESS_KEY" ]; then + Volcengine_ACCESS_KEY_ID="" + Volcengine_SECRET_ACCESS_KEY="" + _err "You haven't specified the volcengine dns api key id and api key secret yet." + return 1 + fi + + #save the api key and email to the account conf file. + _saveaccountconf_mutable Volcengine_ACCESS_KEY_ID "$Volcengine_ACCESS_KEY_ID" + _saveaccountconf_mutable Volcengine_SECRET_ACCESS_KEY "$Volcengine_SECRET_ACCESS_KEY" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + _sleep 1 + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # _info "Getting existing records for $fulldomain" + if ! volcengine_rest POST "" "Action=ListRecords&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"SearchMode\":\"exact\"}"; then + _sleep 1 + return 1 + fi + + # ListRecords already filtered by ZID + Host + Value + SearchMode:exact, + # so any returned record is our target. Don't match on FQDN: Volcengine + # lowercases the Host/FQDN in the response, which would break a + # case-sensitive string compare against $fulldomain. + _record_id="$(echo "$response" | _egrep_o "\"RecordID\":\"[0-9]+\"," | cut -d: -f2 | cut -d, -f1 | tr -d '"')" + _debug "_record_id" "$_record_id" + + if [ "$_record_id" ] && _contains "$response" "$txtvalue"; then + _info "The TXT record already exists. Skipping." + _sleep 1 + return 0 + fi + + _debug "Adding records" + + if volcengine_rest POST "" "Action=CreateRecord&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"Remark\":\"acme.sh\"}"; then + _info "TXT record updated successfully." + _sleep 1 + return 0 + fi + + _sleep 1 + return 1 +} + +#fulldomain txtvalue +dns_volcengine_rm() { + fulldomain=$1 + txtvalue=$2 + _record_id="" + + Volcengine_ACCESS_KEY_ID="${Volcengine_ACCESS_KEY_ID:-$(_readaccountconf_mutable Volcengine_ACCESS_KEY_ID)}" + Volcengine_SECRET_ACCESS_KEY="${Volcengine_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable Volcengine_SECRET_ACCESS_KEY)}" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + _sleep 1 + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Getting existing records for $fulldomain" + + if ! volcengine_rest POST "" "Action=ListRecords&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"SearchMode\":\"exact\"}"; then + _sleep 1 + return 1 + fi + + # ListRecords already filtered by ZID + Host + Value + SearchMode:exact, + # so any returned record is our target. Don't match on FQDN: Volcengine + # lowercases the Host/FQDN in the response, which would break a + # case-sensitive string compare against $fulldomain. + _record_id="$(echo "$response" | _egrep_o "\"RecordID\":\"[0-9]+\"," | cut -d: -f2 | cut -d, -f1 | tr -d '"')" + _debug "_record_id" "$_record_id" + + if [ -z "$_record_id" ]; then + _debug "no records exist, skip" + _sleep 1 + return 0 + fi + + if volcengine_rest POST "" "Action=DeleteRecord&Version=2018-08-01" "{\"RecordID\":\"$_record_id\"}"; then + _info "TXT record deleted successfully." + _sleep 1 + return 0 + fi + _sleep 1 + return 1 +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=1 + p=1 + + # iterate over names (a.b.c.d -> b.c.d -> c.d -> d) + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug "Checking domain: $h" + if [ -z "$h" ]; then + _err "invalid domain" + return 1 + fi + + # iterate over paginated result for list_hosted_zones + if ! volcengine_rest POST "" "Action=ListZones&Version=2018-08-01" "{\"Key\":\"$h\",\"SearchMode\":\"exact\"}"; then + return 1 + fi + if _contains "$response" "\"ZoneName\":\"$h\""; then + _domain_id=$(printf "%s" "$response" | _egrep_o "\"ZID\":[0-9]+," | cut -d: -f2 | cut -d, -f1) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + return 0 + fi + _err "Can't find domain with id: $h" + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +#method uri qstr data +volcengine_rest() { + mtd="$1" + ep="$2" + qsr="$3" + data="$4" + + _debug mtd "$mtd" + _debug ep "$ep" + _debug qsr "$qsr" + _debug data "$data" + + # clear any header state left over from a previous request so that + # conditionally-set headers (e.g. x-content-sha256, x-security-token) + # can't leak into the next request + _H1="" + _H2="" + _H3="" + _H4="" + _H5="" + + CanonicalURI="/$ep" + _debug2 CanonicalURI "$CanonicalURI" + + CanonicalQueryString="$qsr" + _debug2 CanonicalQueryString "$CanonicalQueryString" + + RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")" + _debug2 RequestDate "$RequestDate" + + Hash="sha256" + + _H1="X-Date: $RequestDate" + _debug2 _H1 "$_H1" + + volcengine_host="$Volcengine_HOST" + CanonicalHeaders="host:$volcengine_host\n" + SignedHeaders="host" + + if [ -n "$data" ]; then + XContentSha256="$(printf "%s" "$data" | _digest "$Hash" hex)" + _H4="x-content-sha256: $XContentSha256" + _debug2 _H4 "$_H4" + + CanonicalHeaders="${CanonicalHeaders}x-content-sha256:$XContentSha256\n" + SignedHeaders="${SignedHeaders};x-content-sha256" + fi + + CanonicalHeaders="${CanonicalHeaders}x-date:$RequestDate\n" + SignedHeaders="${SignedHeaders};x-date" + + if [ -n "$Volcengine_SESSION_TOKEN" ]; then + _H3="x-security-token: $Volcengine_SESSION_TOKEN" + CanonicalHeaders="${CanonicalHeaders}x-security-token:$Volcengine_SESSION_TOKEN\n" + SignedHeaders="${SignedHeaders};x-security-token" + fi + + _debug2 CanonicalHeaders "$CanonicalHeaders" + _debug2 SignedHeaders "$SignedHeaders" + + RequestPayload="$data" + _debug2 RequestPayload "$RequestPayload" + + CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$(printf "%s" "$RequestPayload" | _digest "$Hash" hex)" + _debug2 CanonicalRequest "$CanonicalRequest" + + HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)" + _debug2 HashedCanonicalRequest "$HashedCanonicalRequest" + + Algorithm="HMAC-SHA256" + _debug2 Algorithm "$Algorithm" + + RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)" + _debug2 RequestDateOnly "$RequestDateOnly" + + Region="cn-beijing" + Service="dns" + + CredentialScope="$RequestDateOnly/$Region/$Service/request" + _debug2 CredentialScope "$CredentialScope" + + StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest" + + _debug2 StringToSign "$StringToSign" + + kSecret="$Volcengine_SECRET_ACCESS_KEY" + + _secure_debug2 kSecret "$kSecret" + + kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")" + _secure_debug2 kSecretH "$kSecretH" + + kDateH="$(printf "%s" "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)" + _debug2 kDateH "$kDateH" + + kRegionH="$(printf "%s" "$Region" | _hmac "$Hash" "$kDateH" hex)" + _debug2 kRegionH "$kRegionH" + + kServiceH="$(printf "%s" "$Service" | _hmac "$Hash" "$kRegionH" hex)" + _debug2 kServiceH "$kServiceH" + + kSigningH="$(printf "%s" "request" | _hmac "$Hash" "$kServiceH" hex)" + _debug2 kSigningH "$kSigningH" + + signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)" + _debug2 signature "$signature" + + Authorization="$Algorithm Credential=$Volcengine_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" + _debug2 Authorization "$Authorization" + + _H2="Authorization: $Authorization" + _debug2 _H2 "$_H2" + + url="$Volcengine_URL/$ep" + if [ "$qsr" ]; then + url="$Volcengine_URL/$ep?$qsr" + fi + + if [ "$mtd" = "GET" ]; then + response="$(_get "$url")" + else + response="$(_post "$data" "$url" "" "POST" "application/json")" + fi + + _ret="$?" + _debug response "$response" + if [ "$_ret" = "0" ]; then + if _contains "$response" "\"Error\":{"; then + _err "Response error:$response" + return 1 + fi + fi + + return "$_ret" +} From f03895819262753b8b9699437967daadb4aeb891 Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Fri, 3 Jul 2026 12:12:25 +0200 Subject: [PATCH 524/689] fix(dns_czechia): read _normalizeJson input from stdin, not as an argument (#7077) _normalizeJson reads its JSON from stdin (sed | sed | tr) and ignores any positional argument. dns_czechia_add() called it as `_normalizeJson "$_res"`, so the response was discarded and the inner sed blocked reading from stdin. When issuing for a single domain, or for a record that already exists, the "already exists" branch returns early and never reaches this call, which is why the bug stayed hidden. With multiple domains, the first record often short-circuits on "already exists" while the next, freshly-added record reaches the broken call and hangs on interactive runs (or consumes unrelated stdin non-interactively). Pipe the response into _normalizeJson via stdin, matching dns_czechia_rm() and every other dnsapi plugin. Co-authored-by: Claude Opus 4.8 (1M context) --- dnsapi/dns_czechia.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index f0f4c32e..e2ffcf50 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -76,7 +76,7 @@ dns_czechia_add() { return 0 fi - _nres="$(_normalizeJson "$_res")" + _nres="$(printf '%s' "$_res" | _normalizeJson)" if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then _nres="$_res" fi From 01d6d469148ac98949787355ff6156562163154d Mon Sep 17 00:00:00 2001 From: ZeroSSL-Andreas Date: Fri, 19 Jun 2026 17:06:57 +0200 Subject: [PATCH 525/689] New Banner Updated README to include responsive images for dark and light modes. --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 44a73e83..b93a8a50 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@

- - zerossl.com - + + + + + + + + ZeroSSL + +

🔐 acme.sh

From 9900adb0076f88ca943b0f527662aa5d9f208e50 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 19:01:10 +0800 Subject: [PATCH 526/689] fix https://github.com/acmesh-official/acme.sh/issues/4756 --- acme.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acme.sh b/acme.sh index ad7a6325..1b3af51f 100755 --- a/acme.sh +++ b/acme.sh @@ -7199,6 +7199,12 @@ install() { if [ "$_DEFAULT_CERT_HOME" != "$CERT_HOME" ]; then _saveaccountconf "CERT_HOME" "$CERT_HOME" + # Create the custom cert home now instead of on first issuance, so the + # user can see --install honored it. + # https://github.com/acmesh-official/acme.sh/issues/4756 + if [ ! -d "$CERT_HOME" ]; then + mkdir -p "$CERT_HOME" + fi fi if [ "$_DEFAULT_ACCOUNT_KEY_PATH" != "$ACCOUNT_KEY_PATH" ]; then From 1241649501437ca0603703f4c8190a15b8a23313 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 19:38:21 +0800 Subject: [PATCH 527/689] fix https://github.com/acmesh-official/acme.sh/issues/7009 --- acme.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/acme.sh b/acme.sh index 1b3af51f..5529fe88 100755 --- a/acme.sh +++ b/acme.sh @@ -5838,6 +5838,14 @@ renew() { fi _info "Renewing using Le_API=$Le_API" + # Honor --local-address given on the renew/renewAll command line: it overrides + # the value saved at issue time (and gets re-saved by issue() below), so certs + # issued before the machine gained multiple addresses can still be renewed. + # https://github.com/acmesh-official/acme.sh/issues/7009 + if [ "$_local_address" ]; then + Le_LocalAddress="$_local_address" + fi + _clearAPI _clearCA export ACME_DIRECTORY="$Le_API" From eabd23a55164c9a75044573584d24a8505666fa1 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 19:38:42 +0800 Subject: [PATCH 528/689] fix https://github.com/acmesh-official/acme.sh/issues/6963 --- dnsapi/dns_namecheap.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh index 5527b357..cca59735 100755 --- a/dnsapi/dns_namecheap.sh +++ b/dnsapi/dns_namecheap.sh @@ -264,8 +264,16 @@ _set_namecheap_TXT() { _debug hosts "$hosts" if [ -z "$hosts" ]; then - _err "Hosts not found" - return 1 + # An empty host list is only acceptable when the API positively confirms + # a successful getHosts reply: setHosts below REPLACES all records, so + # proceeding on a malformed/unparsed response would wipe the whole zone. + # https://github.com/acmesh-official/acme.sh/issues/6963 + if _contains "$response" "Status=\"OK\"" && _contains "$response" "DomainDNSGetHostsResult"; then + _debug "No existing host records, adding the TXT record as the first one" + else + _err "Hosts not found" + return 1 + fi fi _namecheap_reset_hostList From ad99628e50e4614c33082114856c92a417c76555 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 20:04:32 +0800 Subject: [PATCH 529/689] fix https://github.com/acmesh-official/acme.sh/issues/6917 --- acme.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/acme.sh b/acme.sh index 5529fe88..1c68845f 100755 --- a/acme.sh +++ b/acme.sh @@ -5751,6 +5751,17 @@ $_authorizations_map" fi fi + # Warn when the scheduled renewal falls after the cert has already expired, + # e.g. a 1-day cert from an internal CA combined with the default 60-day + # schedule, which computes from the creation date and never looks at + # notAfter. https://github.com/acmesh-official/acme.sh/issues/6917 + _renew_chk_enddate="$(_enddate "$CERT_PATH")" + _renew_chk_endtime="$(_ssldate2time "$_renew_chk_enddate")" + if [ "$Le_NextRenewTime" ] && [ "$_renew_chk_endtime" ] && [ "$Le_NextRenewTime" -ge "$_renew_chk_endtime" ]; then + _info "$(__red "WARNING: the cert expires at $_renew_chk_enddate, BEFORE the next scheduled renewal time $Le_NextRenewTimeStr.")" + _info "$(__red "The cert will already be expired when the renewal runs. If your CA issues short-lived certs, use a negative --days value (e.g. --days -1) to renew relative to the expiry time.")" + fi + _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" From 92bd80c07daf03b4ed362184f1b4514826ffba0f Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 20:07:42 +0800 Subject: [PATCH 530/689] fix https://github.com/acmesh-official/acme.sh/issues/6914 --- dnsapi/dns_dnsexit.sh | 115 +++++++++--------------------------------- 1 file changed, 25 insertions(+), 90 deletions(-) diff --git a/dnsapi/dns_dnsexit.sh b/dnsapi/dns_dnsexit.sh index ec3b07a4..6b10891c 100644 --- a/dnsapi/dns_dnsexit.sh +++ b/dnsapi/dns_dnsexit.sh @@ -5,14 +5,11 @@ Site: DNSExit.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit Options: DNSEXIT_API_KEY API Key - DNSEXIT_AUTH_USER Username - DNSEXIT_AUTH_PASS Password Issues: github.com/acmesh-official/acme.sh/issues/4719 Author: Samuel Jimenez ' DNSEXIT_API_URL="https://api.dnsexit.com/dns/" -DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp" ######## Public functions ##################### #Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -28,20 +25,7 @@ dns_dnsexit_add() { return 1 fi - _debug 'First detect the root zone' - if ! _get_root "$fulldomain"; then - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then - _err "$response" - return 1 - fi - - _debug2 _response "$response" - return 0 + _dnsexit_zone_op add ',"ttl":0,"overwrite":false' } #Usage: fulldomain txtvalue @@ -58,54 +42,43 @@ dns_dnsexit_rm() { return 1 fi - _debug 'First detect the root zone' - if ! _get_root "$fulldomain"; then - _err "$response" - return 1 - fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then - _err "$response" - return 1 - fi - - _debug2 _response "$response" - return 0 + _dnsexit_zone_op delete '' } #################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -_get_root() { - domain=$1 +# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp) +# was shut down by DNSExit and now returns 503, and the JSON API offers no +# zone-list call. So find the root zone by attempting the actual operation at +# each domain level: the API answers "code":0 only when the domain matches a +# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914 +#Usage: _dnsexit_zone_op +_dnsexit_zone_op() { + _op="$1" + _extra="$2" i=1 while true; do - _domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$_domain" + _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100) + _debug _domain "$_domain" if [ -z "$_domain" ]; then + _err "Could not find the root zone of $fulldomain in your DNSExit account" return 1 fi - _debug login "$DNSEXIT_AUTH_USER" - _debug password "$DNSEXIT_AUTH_PASS" - _debug domain "$_domain" + _sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")" + if [ "$_sub_domain" = "$fulldomain" ]; then + _sub_domain="" + fi + _debug _sub_domain "$_sub_domain" - _dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain" - - if _contains "$response" "0=$_domain"; then - _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")" - return 0 - else - _debug "Go to next level of $_domain" + if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then + if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then + _debug2 _response "$response" + return 0 + fi + _debug "Zone $_domain was not accepted, trying the next level" "$response" fi i=$(_math "$i" + 1) done - - return 1 } _dnsexit_rest() { @@ -136,27 +109,7 @@ _dnsexit_rest() { return 0 } -_dnsexit_http() { - m=GET - param="$1" - _debug param "$param" - _debug get "$DNSEXIT_HOSTS_URL?$param" - - response="$(_get "$DNSEXIT_HOSTS_URL?$param")" - - _debug response "$response" - - if [ "$?" != "0" ]; then - _err "Error $param" - return 1 - fi - - _debug2 response "$response" - return 0 -} - get_account_info() { - DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}" if test -z "$DNSEXIT_API_KEY"; then DNSEXIT_API_KEY='' @@ -166,23 +119,5 @@ get_account_info() { _saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY" - DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}" - if test -z "$DNSEXIT_AUTH_USER"; then - DNSEXIT_AUTH_USER="" - _err 'DNSEXIT_AUTH_USER was not exported' - return 1 - fi - - _saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER" - - DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}" - if test -z "$DNSEXIT_AUTH_PASS"; then - DNSEXIT_AUTH_PASS="" - _err 'DNSEXIT_AUTH_PASS was not exported' - return 1 - fi - - _saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS" - return 0 } From 7e7c0ee984bd2548453f436382c65de3345ecfe0 Mon Sep 17 00:00:00 2001 From: magyarsz <699745+magyarsz@users.noreply.github.com> Date: Fri, 3 Jul 2026 15:42:52 +0200 Subject: [PATCH 531/689] Merge pull request #6720 from magyarsz/dev Fix a logical error in the `renew` function --- acme.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/acme.sh b/acme.sh index 1c68845f..b65d41ef 100755 --- a/acme.sh +++ b/acme.sh @@ -5936,11 +5936,8 @@ renew() { fi issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse" res="$?" - if [ "$res" != "0" ]; then - return "$res" - fi - if [ "$Le_DeployHook" ]; then + if [ "$Le_DeployHook" ] && [ "$res" = "0" ]; then _deploy "$Le_Domain" "$Le_DeployHook" res="$?" fi From 5038d12d6278b1fc888f80373f68999bece3b688 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 21:55:23 +0800 Subject: [PATCH 532/689] forbid using --days together with --valid-to https://github.com/acmesh-official/acme.sh/pull/6572 --- acme.sh | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/acme.sh b/acme.sh index b65d41ef..affe7b78 100755 --- a/acme.sh +++ b/acme.sh @@ -5752,14 +5752,18 @@ $_authorizations_map" fi # Warn when the scheduled renewal falls after the cert has already expired, - # e.g. a 1-day cert from an internal CA combined with the default 60-day + # e.g. a 1-day cert from an internal CA combined with the default 30-day # schedule, which computes from the creation date and never looks at - # notAfter. https://github.com/acmesh-official/acme.sh/issues/6917 - _renew_chk_enddate="$(_enddate "$CERT_PATH")" - _renew_chk_endtime="$(_ssldate2time "$_renew_chk_enddate")" - if [ "$Le_NextRenewTime" ] && [ "$_renew_chk_endtime" ] && [ "$Le_NextRenewTime" -ge "$_renew_chk_endtime" ]; then - _info "$(__red "WARNING: the cert expires at $_renew_chk_enddate, BEFORE the next scheduled renewal time $Le_NextRenewTimeStr.")" - _info "$(__red "The cert will already be expired when the renewal runs. If your CA issues short-lived certs, use a negative --days value (e.g. --days -1) to renew relative to the expiry time.")" + # notAfter. Skip the warning for a fixed-date --valid-to: there + # Le_NextRenewTime equals the expiry by design and the non-renewable state + # was already reported above. https://github.com/acmesh-official/acme.sh/issues/6917 + if [ -z "$_valid_to" ] || _startswith "$_valid_to" "+"; then + _renew_chk_enddate="$(_enddate "$CERT_PATH")" + _renew_chk_endtime="$(_ssldate2time "$_renew_chk_enddate")" + if [ "$Le_NextRenewTime" ] && [ "$_renew_chk_endtime" ] && [ "$Le_NextRenewTime" -ge "$_renew_chk_endtime" ]; then + _info "$(__red "WARNING: the cert expires at $_renew_chk_enddate, BEFORE the next scheduled renewal time $Le_NextRenewTimeStr.")" + _info "$(__red "The cert will already be expired when the renewal runs. If your CA issues short-lived certs, use a negative --days value (e.g. --days -1) to renew relative to the expiry time.")" + fi fi _savedomainconf "Le_NextRenewTimeStr" "$Le_NextRenewTimeStr" @@ -8568,6 +8572,15 @@ _process() { _debug2 LE_WORKING_DIR "$LE_WORKING_DIR" + # --days and --valid-to are mutually exclusive by design: --valid-to pins + # the cert lifetime and the renewal time follows the expiry, so a + # creation-based --days schedule can not apply. + if [ "$_days" ] && [ "$_valid_to" ]; then + _err "--days can not be used together with --valid-to." + _err "With --valid-to, the renewal time is derived from the expiry time automatically." + return 1 + fi + if [ "$DEBUG" ]; then version if [ "$_server" ]; then From 92a1b4710838866a4a409be593f4fb7df9ffdd21 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 22:26:50 +0800 Subject: [PATCH 533/689] forbid spaces in the --home/--config-home path https://github.com/acmesh-official/acme.sh/issues/2163 --- acme.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/acme.sh b/acme.sh index affe7b78..23adba93 100755 --- a/acme.sh +++ b/acme.sh @@ -2842,6 +2842,17 @@ __initHome() { _debug "Using config home: $LE_CONFIG_HOME" export LE_CONFIG_HOME + # Paths with whitespace break the unquoted $_CURL/$_WGET command expansion, + # so fail early with a clear error instead of a cryptic curl/wget failure. + # https://github.com/acmesh-official/acme.sh/issues/2163 + case "$LE_WORKING_DIR$LE_CONFIG_HOME" in + *" "*) + _err "The --home or --config-home path can not contain spaces: '$LE_WORKING_DIR'" + _err "Please install $PROJECT_NAME to a path without spaces." + exit 1 + ;; + esac + _DEFAULT_ACCOUNT_CONF_PATH="$LE_CONFIG_HOME/account.conf" if [ -z "$ACCOUNT_CONF_PATH" ]; then From 61400500e2ddcf400682aa9dacf27aa891aa9411 Mon Sep 17 00:00:00 2001 From: Trekky12 Date: Fri, 3 Jul 2026 16:31:31 +0200 Subject: [PATCH 534/689] Suppress 'signal process started' message when nginx config is restored (related to issue #4995) (#6747) --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 23adba93..3cb4e248 100755 --- a/acme.sh +++ b/acme.sh @@ -3524,7 +3524,7 @@ _restoreNginx() { done _info "Reloading nginx" - if ! nginx -s reload >/dev/null; then + if ! nginx -s reload >/dev/null 2>&1; then _err "An error occurred while reloading nginx, please open an issue on $PROJECT." return 1 fi From f4d2db64efb54175d57f0aaa9f4a97c9c1e2c2ab Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 22:49:23 +0800 Subject: [PATCH 535/689] dns_gd: skip readback check when GoDaddy API returns UNKNOWN_DOMAIN https://github.com/acmesh-official/acme.sh/issues/6517 --- dnsapi/dns_gd.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_gd.sh b/dnsapi/dns_gd.sh index ee66ee19..e08f2a05 100755 --- a/dnsapi/dns_gd.sh +++ b/dnsapi/dns_gd.sh @@ -69,7 +69,12 @@ dns_gd_add() { return 1 fi - if ! _contains "$response" "$txtvalue"; then + if _contains "$response" "UNKNOWN_DOMAIN"; then + # GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even + # though the PUT above succeeded; skip the local readback check and let + # acme.sh's own DNS propagation check verify the record was published. + _info "GoDaddy API won't allow reading the record back; skipping local verification." + elif ! _contains "$response" "$txtvalue"; then _err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!" return 1 fi From 20254cbaf0755175024b2a9e0aaa87f9d998fc2e Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 23:02:35 +0800 Subject: [PATCH 536/689] dns_dnsimple: support user tokens (dnsimple_u_*) https://github.com/acmesh-official/acme.sh/issues/6491 --- dnsapi/dns_dnsimple.sh | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/dnsapi/dns_dnsimple.sh b/dnsapi/dns_dnsimple.sh index 10a3821d..e262239a 100644 --- a/dnsapi/dns_dnsimple.sh +++ b/dnsapi/dns_dnsimple.sh @@ -5,6 +5,7 @@ Site: DNSimple.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple Options: DNSimple_OAUTH_TOKEN OAuth Token + DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts. Issues: github.com/pho3nixf1re/acme.sh/issues ' @@ -28,7 +29,7 @@ dns_dnsimple_add() { _saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN" if ! _get_account_id; then - _err "failed to retrive account id" + _err "failed to retrieve account id" return 1 fi @@ -57,7 +58,7 @@ dns_dnsimple_rm() { fulldomain=$1 if ! _get_account_id; then - _err "failed to retrive account id" + _err "failed to retrieve account id" return 1 fi @@ -122,13 +123,16 @@ _get_root() { # returns _account_id _get_account_id() { - _debug "retrive account id" - if ! _dnsimple_rest GET "whoami"; then - return 1 + DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf DNSimple_ACCOUNT_ID)}" + if [ "$DNSimple_ACCOUNT_ID" ]; then + _saveaccountconf DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID" + _account_id="$DNSimple_ACCOUNT_ID" + _debug _account_id "$_account_id" + return 0 fi - if _contains "$response" "\"account\":null"; then - _err "no account associated with this token" + _debug "retrieve account id" + if ! _dnsimple_rest GET "whoami"; then return 1 fi @@ -137,7 +141,25 @@ _get_account_id() { return 1 fi + if _contains "$response" "\"account\":null"; then + # the whoami of a user token (dnsimple_u_*) carries no account, + # so list the accounts the token can access instead + # https://github.com/acmesh-official/acme.sh/issues/6491 + if ! _dnsimple_rest GET "accounts"; then + return 1 + fi + fi + _account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1) + if [ -z "$_account_id" ]; then + _err "no account associated with this token" + return 1 + fi + if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then + _err "The token has access to multiple accounts, please pick one and set it explicitly:" + _err "export DNSimple_ACCOUNT_ID=" + return 1 + fi _debug _account_id "$_account_id" return 0 From 0ce8c24736971db8b1a73b07d0d010cbec3cb13e Mon Sep 17 00:00:00 2001 From: szakharchenko Date: Fri, 3 Jul 2026 18:25:38 +0300 Subject: [PATCH 537/689] dev_mythic_beasts: Fix header name: Accepts => Accept (#6428) --- dnsapi/dns_mythic_beasts.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_mythic_beasts.sh b/dnsapi/dns_mythic_beasts.sh index 1529e1e7..a49ab8ab 100755 --- a/dnsapi/dns_mythic_beasts.sh +++ b/dnsapi/dns_mythic_beasts.sh @@ -186,7 +186,7 @@ _oauth2() { _oauth2_std() { # HTTP Basic Authentication _H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)" - _H2="Accepts: application/json" + _H2="Accept: application/json" export _H1 _H2 body="grant_type=client_credentials" @@ -210,7 +210,7 @@ _oauth2_std() { } _oauth2_github() { - _H1="Accepts: application/json" + _H1="Accept: application/json" export _H1 body="{\"login\":{\"handle\":\"$MB_AK\",\"pass\":\"$MB_AS\",\"floating\":1}}" @@ -241,7 +241,7 @@ _mb_rest() { fi _H1="Authorization: Bearer $MB_TK" - _H2="Accepts: application/json" + _H2="Accept: application/json" export _H1 _H2 if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then # body url [needbase64] [POST|PUT|DELETE] [ContentType] From b974bbd6d656e24126838f70bbcdc768db0220e6 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 23:28:19 +0800 Subject: [PATCH 538/689] fix upgrade with a relative --home path https://github.com/acmesh-official/acme.sh/issues/6477 --- acme.sh | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 3cb4e248..b345de8e 100755 --- a/acme.sh +++ b/acme.sh @@ -2834,11 +2834,31 @@ __initHome() { _debug "Using default home: $DEFAULT_INSTALL_HOME" LE_WORKING_DIR="$DEFAULT_INSTALL_HOME" fi + # Convert a relative --home to an absolute path: later code cd's around + # (e.g. installOnline extracts and enters the archive dir), where a + # relative path would point into the wrong directory. + # https://github.com/acmesh-official/acme.sh/issues/6477 + case "$LE_WORKING_DIR" in + /*) ;; + *) + if [ -d "$LE_WORKING_DIR" ]; then + LE_WORKING_DIR="$(cd "$LE_WORKING_DIR" && pwd)" + fi + ;; + esac export LE_WORKING_DIR if [ -z "$LE_CONFIG_HOME" ]; then LE_CONFIG_HOME="$LE_WORKING_DIR" fi + case "$LE_CONFIG_HOME" in + /*) ;; + *) + if [ -d "$LE_CONFIG_HOME" ]; then + LE_CONFIG_HOME="$(cd "$LE_CONFIG_HOME" && pwd)" + fi + ;; + esac _debug "Using config home: $LE_CONFIG_HOME" export LE_CONFIG_HOME @@ -7676,7 +7696,9 @@ installOnline() { cd "$PROJECT_NAME-$_branch" chmod +x $PROJECT_ENTRY - if ./$PROJECT_ENTRY --install "$@"; then + ./$PROJECT_ENTRY --install "$@" + _install_rc="$?" + if [ "$_install_rc" = "0" ]; then _info "Install success!" fi @@ -7684,6 +7706,9 @@ installOnline() { rm -rf "$PROJECT_NAME-$_branch" rm -f "$localname" + # Propagate the install result so a failed upgrade is not reported as + # success. https://github.com/acmesh-official/acme.sh/issues/6477 + exit "$_install_rc" ) } From ac5624536b1321c8ac47d1b2d25946b9a8032144 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 23:30:16 +0800 Subject: [PATCH 539/689] dns_gd: fix root zone detection for API-restricted accounts https://github.com/acmesh-official/acme.sh/issues/4487 --- dnsapi/dns_gd.sh | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/dnsapi/dns_gd.sh b/dnsapi/dns_gd.sh index e08f2a05..c92bdfa2 100755 --- a/dnsapi/dns_gd.sh +++ b/dnsapi/dns_gd.sh @@ -150,8 +150,8 @@ dns_gd_rm() { # _domain=domain.com _get_root() { domain=$1 - i=2 - p=1 + i=1 + p=0 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) if [ -z "$h" ]; then @@ -159,17 +159,41 @@ _get_root() { return 1 fi - if ! _gd_rest GET "domains/$h"; then - return 1 + # The record name is whatever precedes the candidate zone. Do not assume + # _acme-challenge here: with DNS alias mode it can be any name, and the + # record may even sit at the zone apex (name "@"). + if [ "$p" = "0" ]; then + _probe_sub="@" + else + _probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p") fi - if _contains "$response" '"code":"NOT_FOUND"'; then - _debug "$h not found" - else - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + # Probe with the records endpoint instead of "GET domains/$h": since + # 2024-05 GoDaddy rejects the domain details call for accounts with + # fewer than 10 domains, while record-level calls keep working. + # https://github.com/acmesh-official/acme.sh/issues/4487 + if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then + return 1 + fi + if _startswith "$response" '\['; then + _sub_domain="$_probe_sub" _domain="$h" return 0 fi + + # Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone + # even though writes succeed (see issue #6517); fall back to the domain + # details call for them. + if ! _gd_rest GET "domains/$h"; then + return 1 + fi + if _contains "$response" '"domainId"'; then + _sub_domain="$_probe_sub" + _domain="$h" + return 0 + fi + + _debug "$h not found" p="$i" i=$(_math "$i" + 1) done From 780f2ad5dcf42b9b317192fc4031dd86b5d197d5 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 23:44:12 +0800 Subject: [PATCH 541/689] dns_ali: do not rely on "_url_encode upper-hex" so the signature works with older bundled libraries (e.g. Proxmox VE) https://github.com/acmesh-official/acme.sh/issues/6272 --- dnsapi/dns_ali.sh | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index 90196c69..62e54e0c 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -69,8 +69,8 @@ _ali_rest() { ign="$2" mtd="${3:-GET}" - signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64) - signature=$(printf "%s" "$signature" | _url_encode upper-hex) + signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _ali_urlencode_upper)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64) + signature=$(printf "%s" "$signature" | _ali_urlencode_upper) url="$endpoint?Signature=$signature" if [ "$mtd" = "GET" ]; then @@ -96,6 +96,20 @@ _ali_rest() { fi } +# stdin stdout +# The Aliyun signature requires percent-encoding with upper-case hex. +# Do not use "_url_encode upper-hex" here: this file is also bundled by +# third parties (e.g. Proxmox VE proxmox-acme) whose older copies of the +# acme.sh function library ignore the upper-hex argument and output +# lower-case hex, which invalidates the signature. +# https://github.com/acmesh-official/acme.sh/issues/6272 +_ali_urlencode_upper() { + { + _url_encode + echo + } | sed 's/%a/%A/g;s/%b/%B/g;s/%c/%C/g;s/%d/%D/g;s/%e/%E/g;s/%f/%F/g;s/%\(.\)a/%\1A/g;s/%\(.\)b/%\1B/g;s/%\(.\)c/%\1C/g;s/%\(.\)d/%\1D/g;s/%\(.\)e/%\1E/g;s/%\(.\)f/%\1F/g' +} + _ali_nonce() { if [ "$ACME_OPENSSL_BIN" ]; then "$ACME_OPENSSL_BIN" rand -hex 16 2>/dev/null && return 0 From 7fb40f0ccf32bc16589c63d7a478b42f3fcb0fe5 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 3 Jul 2026 23:55:23 +0800 Subject: [PATCH 542/689] fix https://github.com/acmesh-official/acme.sh/issues/6609 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index b345de8e..b4861b4c 100755 --- a/acme.sh +++ b/acme.sh @@ -3888,7 +3888,7 @@ _regAccount() { mkdir -p "$CA_DIR" - if [ ! -f "$ACCOUNT_KEY_PATH" ]; then + if [ ! -s "$ACCOUNT_KEY_PATH" ]; then if ! _create_account_key "$_reg_length"; then _err "Error creating account key." return 1 From 0df051577ca1344b75fdce80840f642a640f3e0c Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 00:06:26 +0800 Subject: [PATCH 543/689] fix https://github.com/acmesh-official/acme.sh/issues/6609 --- acme.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/acme.sh b/acme.sh index b4861b4c..dfc056b8 100755 --- a/acme.sh +++ b/acme.sh @@ -1179,6 +1179,11 @@ _createkey() { length="$1" f="$2" _debug2 "_createkey for file:$f" + if ! _exists "${ACME_OPENSSL_BIN:-openssl}"; then + _err "Please install openssl first. ACME_OPENSSL_BIN=$ACME_OPENSSL_BIN" + _err "We need openssl to generate keys." + return 1 + fi eccname="$length" if _startswith "$length" "ec-"; then length=$(printf "%s" "$length" | cut -d '-' -f 2-100) @@ -1201,6 +1206,7 @@ _createkey() { _debug "Using length $length" + _new_key_file="" if ! [ -e "$f" ]; then if ! touch "$f" >/dev/null 2>&1; then _f_path="$(dirname "$f")" @@ -1214,6 +1220,7 @@ _createkey() { return 1 fi chmod 600 "$f" + _new_key_file="1" fi if _isEccKey "$length"; then @@ -1222,6 +1229,10 @@ _createkey() { echo "$_opkey" >"$f" else _err "Error encountered for ECC key named $eccname" + #do not leave an empty file behind, or the next run would treat the key as existing + if [ "$_new_key_file" ]; then + rm -f "$f" + fi return 1 fi else @@ -1234,6 +1245,10 @@ _createkey() { echo "$_opkey" >"$f" else _err "Error encountered for RSA key of length $length" + #do not leave an empty file behind, or the next run would treat the key as existing + if [ "$_new_key_file" ]; then + rm -f "$f" + fi return 1 fi fi From 2229330c48bcf7c9cf866e595bc6588a9574bde0 Mon Sep 17 00:00:00 2001 From: Artur Klauser Date: Fri, 3 Jul 2026 09:32:45 -0700 Subject: [PATCH 544/689] Fix typo in synology_dsm.sh (#6406) Fix typo in an error message. --- deploy/synology_dsm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index e28a4036..6fce1f19 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -276,7 +276,7 @@ synology_dsm_deploy() { if [ -n "$error_code" ]; then if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then _cleardeployconf SYNO_DEVICE_ID - _err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window." + _err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window." elif [ "$error_code" == "404" ]; then _err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window." elif [ "$error_code" == "406" ]; then From f4dc9fd9d110a27870b78395ec1741434a82208c Mon Sep 17 00:00:00 2001 From: Laurent Grawet Date: Fri, 3 Jul 2026 18:35:02 +0200 Subject: [PATCH 545/689] haproxy.sh: allows certificate deployment to multiple hosts (#5180) * haproxy.sh: allows certificate deployment to multiple hosts * Update deploy/haproxy.sh Co-authored-by: Matt Simerson * Update deploy/haproxy.sh Co-authored-by: Matt Simerson --------- Co-authored-by: Matt Simerson --- deploy/haproxy.sh | 103 ++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index 19509e3b..b618a65b 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -43,7 +43,8 @@ # needing to reload HAProxy. Default is "no". # # Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat -# address format. +# address format. The certificate can be deployed to a comma separated ',' list +# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999") # # export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock" # @@ -193,7 +194,6 @@ haproxy_deploy() { _issuer="${_pem}.issuer" _ocsp="${_pem}.ocsp" _reload="${Le_Deploy_haproxy_reload}" - _statssock="${Le_Deploy_haproxy_stats_socket}" _info "Deploying PEM file" # Create a temporary PEM file @@ -327,62 +327,65 @@ haproxy_deploy() { # Update certificate over HAProxy stats socket or master CLI. if _exists socat; then - # look for the certificate on the stats socket, to chose between updating or creating one - _socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'" - _debug _socat_cert_cmd "${_socat_cert_cmd}" - eval "${_socat_cert_cmd}" - _ret=$? - if [ "${_ret}" != "0" ]; then - _newcert="1" - _info "Creating new certificate '${_pem}' over HAProxy ${_socketname}." - # certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate. - _socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'" - _debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}" - eval "${_socat_crtlist_show_cmd}" + IFS=',' + for _statssock in ${Le_Deploy_haproxy_stats_socket}; do + # look for the certificate on the stats socket, to choose between updating or creating one + _socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'" + _debug _socat_cert_cmd "${_socat_cert_cmd}" + eval "${_socat_cert_cmd}" _ret=$? if [ "${_ret}" != "0" ]; then - _err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'" - return "${_ret}" + _newcert="1" + _info "Creating new certificate '${_pem}' over HAProxy ${_socketname}." + # certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate. + _socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'" + _debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}" + eval "${_socat_crtlist_show_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'" + return "${_ret}" + fi + # create a new certificate + _socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'" + _debug _socat_new_cmd "${_socat_new_cmd}" + eval "${_socat_new_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Couldn't create '${_pem}' in haproxy" + return "${_ret}" + fi + else + _info "Update existing certificate '${_pem}' over HAProxy ${_socketname}." fi - # create a new certificate - _socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'" - _debug _socat_new_cmd "${_socat_new_cmd}" - eval "${_socat_new_cmd}" - _ret=$? - if [ "${_ret}" != "0" ]; then - _err "Couldn't create '${_pem}' in haproxy" - return "${_ret}" - fi - else - _info "Update existing certificate '${_pem}' over HAProxy ${_socketname}." - fi - _socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" - _secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" - eval "${_socat_cert_set_cmd}" - _ret=$? - if [ "${_ret}" != "0" ]; then - _err "Can't update '${_pem}' in haproxy" - return "${_ret}" - fi - _socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'" - _debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}" - eval "${_socat_cert_commit_cmd}" - _ret=$? - if [ "${_ret}" != "0" ]; then - _err "Can't commit '${_pem}' in haproxy" - return ${_ret} - fi - if [ "${_newcert}" = "1" ]; then - # if this is a new certificate, it needs to be inserted into the crt-list` - _socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'" - _debug _socat_cert_add_cmd "${_socat_cert_add_cmd}" - eval "${_socat_cert_add_cmd}" + _socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" + _secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" + eval "${_socat_cert_set_cmd}" _ret=$? if [ "${_ret}" != "0" ]; then _err "Can't update '${_pem}' in haproxy" return "${_ret}" fi - fi + _socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'" + _debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}" + eval "${_socat_cert_commit_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Can't commit '${_pem}' in haproxy" + return ${_ret} + fi + if [ "${_newcert}" = "1" ]; then + # if this is a new certificate, it needs to be inserted into the crt-list` + _socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'" + _debug _socat_cert_add_cmd "${_socat_cert_add_cmd}" + eval "${_socat_cert_add_cmd}" + _ret=$? + if [ "${_ret}" != "0" ]; then + _err "Can't update '${_pem}' in haproxy" + return "${_ret}" + fi + fi + done else _err "'socat' is not available, couldn't update over ${_socketname}" fi From 0a6abaf8a1336a2bc33c1847850176ef0cb7471d Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 00:39:35 +0800 Subject: [PATCH 546/689] fix https://github.com/acmesh-official/acme.sh/issues/6388 --- acme.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index dfc056b8..5a41f16b 100755 --- a/acme.sh +++ b/acme.sh @@ -6381,7 +6381,13 @@ deploy() { fi _debug2 DOMAIN_CONF "$DOMAIN_CONF" - . "$DOMAIN_CONF" + # The cert dir may exist without a domain conf (e.g. the conf was deleted, or + # the cert was placed here manually). Deploy can still proceed using env-provided + # settings, and _savedomainconf below will recreate the conf, so only source it + # when present instead of failing on a missing file. + if [ -f "$DOMAIN_CONF" ]; then + . "$DOMAIN_CONF" + fi _savedomainconf Le_DeployHook "$_hooks" From b4de9e86217281eff474a2af7b40620618c1f869 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 10:49:32 +0800 Subject: [PATCH 547/689] fix docker deploy hook on podman, check exec ExitCode instead of response body (#4977) --- deploy/docker.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/deploy/docker.sh b/deploy/docker.sh index 264963ae..7fdcf604 100755 --- a/deploy/docker.sh +++ b/deploy/docker.sh @@ -189,10 +189,22 @@ _docker_exec() { _debug2 cjson "$cjson" execid="$(echo "$cjson" | cut -d '"' -f 4)" _debug execid "$execid" - ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")" + #Detach:true is required for podman's docker-compatible API: with + #Detach:false it streams the command output on the connection, so the + #non-empty response was misread as an error (issue #4977). The real + #result is checked via the exec inspect ExitCode below instead. + ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")" _debug2 ejson "$ejson" - if [ "$ejson" ]; then - _err "$ejson" + _et=0 + ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")" + while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do + sleep 1 + _et="$(_math "$_et" + 1)" + ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")" + done + _debug2 ijson "$ijson" + if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then + _err "docker exec error: $ijson" return 1 fi else From 6cd0c00a21ecea6b4f1f25c4b715c1f2282c4cff Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 11:23:11 +0800 Subject: [PATCH 548/689] extract authorizations parsing into _authorizations_from_order, fix IPv6 urls (#6326) --- acme.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 5a41f16b..a21bfd71 100755 --- a/acme.sh +++ b/acme.sh @@ -918,6 +918,15 @@ _json_decode() { echo "$_j_str" } +#extract the authorization URLs from an order response on stdin, as a +#comma-separated list. The entries are quoted URL strings and a quote cannot +#occur inside a URL, so the first '"]' is always the end of the array. A +#char-class scan would stop early on the brackets of an IPv6 host +#(https://[2001:db8::1]/...). Outputs nothing if the field is missing. +_authorizations_from_order() { + sed -n 's/.*"authorizations" *: *\[//p' | sed 's/" *\].*//' | tr -d '" ' +} + #options file _sed_i() { options="$1" @@ -4981,7 +4990,7 @@ issue() { #for dns manual mode _savedomainconf "Le_OrderFinalize" "$Le_OrderFinalize" - _authorizations_seg="$(echo "$response" | _json_decode | _egrep_o '"authorizations" *: *\[[^\[]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')" + _authorizations_seg="$(echo "$response" | _json_decode | _authorizations_from_order)" _debug2 _authorizations_seg "$_authorizations_seg" if [ -z "$_authorizations_seg" ]; then _err "_authorizations_seg not found." @@ -6839,7 +6848,7 @@ _deactivate() { _err "Cannot get new order for domain." return 1 fi - _authorizations_seg="$(echo "$response" | _egrep_o '"authorizations" *: *\[[^\]*\]' | cut -d '[' -f 2 | tr -d ']' | tr -d '"')" + _authorizations_seg="$(echo "$response" | _json_decode | _authorizations_from_order)" _debug2 _authorizations_seg "$_authorizations_seg" if [ -z "$_authorizations_seg" ]; then _err "_authorizations_seg not found." From 7653eaab31e3519bf03cd46ff3a22f72f55e7651 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 11:44:32 +0800 Subject: [PATCH 549/689] fix https://github.com/acmesh-official/acme.sh/issues/4879#issuecomment-2942728895 --- dnsapi/dns_hostingde.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dnsapi/dns_hostingde.sh b/dnsapi/dns_hostingde.sh index 41ccab2b..ed675b42 100644 --- a/dnsapi/dns_hostingde.sh +++ b/dnsapi/dns_hostingde.sh @@ -40,6 +40,11 @@ _hostingde_apiKey() { return 1 fi + # The endpoint is the base URL only; the api path is appended below. + # hosting.de's own docs show the full api URL, so strip it if pasted in. + # https://github.com/acmesh-official/acme.sh/issues/6896 + HOSTINGDE_ENDPOINT="$(echo "$HOSTINGDE_ENDPOINT" | sed 's|/api/dns/v1/json||; s|/*$||')" + _saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY" _saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT" } From e64529ab501b217ba453ee43144c478ef3288ab7 Mon Sep 17 00:00:00 2001 From: "Simon V." <218359733+sim0n-v@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:50:55 +0200 Subject: [PATCH 550/689] ARI - Add support for Mass Revocation (#6953) * ARI - Add support for Mass Revocation * feat: update ARI each time NextRenewTime is not within the suggestedWindow * Remove _ari_should_renew and add condition on Le_NextRenewTime * Add support for ARI explanationURL * Fix debug variable _d_ari * New Banner Updated README to include responsive images for dark and light modes. * multiple fix * fix * fix shfmt * Reset README --------- Co-authored-by: ZeroSSL-Andreas --- acme.sh | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/acme.sh b/acme.sh index a21bfd71..8e8186e0 100755 --- a/acme.sh +++ b/acme.sh @@ -5928,7 +5928,6 @@ renew() { # If the window has started, renew now even if Le_NextRenewTime is in the future. # Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only # Le_NextRenewTime for the renewal decision. - _ari_should_renew="" if [ "$NO_ARI" = "1" ]; then _debug "NO_ARI=1, skipping ARI suggestedWindow check" elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then @@ -5939,20 +5938,38 @@ renew() { _ari_end="$(echo "$_ari_resp" | _egrep_o '"end" *: *"[^"]*' | sed 's/.*"//')" _debug "ARI suggestedWindow.start" "$_ari_start" _debug "ARI suggestedWindow.end" "$_ari_end" - if [ "$_ari_start" ]; then + if [ "$_ari_start" ] && [ "$_ari_end" ]; then _ari_start_t="$(_date2time "$(echo "$_ari_start" | sed 's/\.[0-9]*//')")" + _ari_end_t="$(_date2time "$(echo "$_ari_end" | sed 's/\.[0-9]*//')")" + _ari_explanation_url="$(echo "$_ari_resp" | _egrep_o '"explanationURL" *: *"[^"]*' | sed 's/.*"//')" _debug "_ari_start_t" "$_ari_start_t" - if [ "$_ari_start_t" ] && [ "$(_time)" -ge "$_ari_start_t" ]; then - _info "ARI suggestedWindow has started ($(__green "$_ari_start")), proceeding with renewal." - _ari_should_renew="1" - else - _info "ARI suggestedWindow starts at: $(__green "$_ari_start")" + _debug "_ari_end_t" "$_ari_end_t" + _debug "_ari_explanation_url" "$_ari_explanation_url" + _debug "Le_NextRenewTime" "$Le_NextRenewTime" + # Update ARI if needed + if [ "$_ari_start_t" ] && [ "$_ari_end_t" ] && [ "$Le_NextRenewTime" ] && [ "$_ari_end_t" -gt "$_ari_start_t" ] && ([ "$Le_NextRenewTime" -lt "$_ari_start_t" ] || [ "$Le_NextRenewTime" -gt "$_ari_end_t" ]); then + _ari_old_time_str="$Le_NextRenewTimeStr" + _info "Current renewal time: $(__green "$_ari_old_time_str")" + _ari_window=$(_math "$_ari_end_t" - "$_ari_start_t") + _ari_offset=$(_math "$(_time)" % "$_ari_window") + Le_NextRenewTime=$(_math "$_ari_start_t" + "$_ari_offset") + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") + _info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")" + _info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")" + _savedomainconf Le_NextRenewTime "$Le_NextRenewTime" + _savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr" + fi + if [ "$Le_NextRenewTime" ] && [ "$(_time)" -ge "$Le_NextRenewTime" ]; then + _info "ARI suggested renewal has passed ($(__green "$Le_NextRenewTimeStr")), proceeding with renewal." + if [ "$_ari_explanation_url" ]; then + _info "For more information on this renewal: $(__green "$_ari_explanation_url")" + fi fi fi fi fi - if [ -z "$FORCE" ] && [ -z "$_ari_should_renew" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then _info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")" _info "Add '$(__red '--force')' to force renewal." if [ -z "$_ACME_IN_RENEWALL" ]; then @@ -6044,12 +6061,19 @@ renewAll() { fi d=$(basename "$di") _debug d "$d" + _d_ari="$di.ari" + _debug _d_ari "$_d_ari" ( if _endswith "$d" "$ECC_SUFFIX"; then _isEcc=$(echo "$d" | cut -d "$ECC_SEP" -f 2) d=$(echo "$d" | cut -d "$ECC_SEP" -f 1) fi renew "$d" "$_isEcc" "$_server" + rc="$?" + if [ "$rc" = "0" ] && [ "$_ari_explanation_url" ]; then + echo "$_ari_explanation_url" >"$_d_ari" + fi + return $rc ) rc="$?" _debug "Return code: $rc" @@ -6064,8 +6088,13 @@ renewAll() { _send_notify "Renew $d success" "Good, the cert is renewed." "$NOTIFY_HOOK" 0 fi fi + _renewal_explanation="" + if [ -f "$_d_ari" ]; then + _renewal_explanation=" ($(cat "$_d_ari"))" + rm -f "$_d_ari" + fi - _success_msg="${_success_msg} $d + _success_msg="${_success_msg} $d$_renewal_explanation " elif [ "$rc" = "$RENEW_SKIP" ]; then if [ $_error_level -gt $NOTIFY_LEVEL_SKIP ]; then From 8f2a476d21c13cde3b0c97ed0074c8d6b259d3f4 Mon Sep 17 00:00:00 2001 From: wardhus Date: Sat, 4 Jul 2026 10:54:05 +0200 Subject: [PATCH 551/689] Add Calrissia.be API (#6811) Co-authored-by: Ward --- dnsapi/dns_calrissia.sh | 137 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 dnsapi/dns_calrissia.sh diff --git a/dnsapi/dns_calrissia.sh b/dnsapi/dns_calrissia.sh new file mode 100644 index 00000000..01ca3092 --- /dev/null +++ b/dnsapi/dns_calrissia.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_calrissia_info='Calrissia.be DNS API +Site: calrissia.be +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_calrissia +Options: + CALRISSIA_TOKEN Personal access token +Issues: github.com/acmesh-official/acme.sh/issues/6809 +Author: Ward Hus +' + +CALRISSIA_API="https://my.calrissia.com/api" + +dns_calrissia_add() { + fulldomain="$1" + txtvalue="$2" + + _calrissia_load_token || return 1 + + if ! _calrissia_get_root "$fulldomain"; then + _err "Unable to find domain in Calrissia account for: $fulldomain" + return 1 + fi + + _debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'" + _info "Adding TXT record for $fulldomain" + + _body="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120,\"prio\":0}" + _response="$(_calrissia_request POST "/domain/$_domain_id/record" "$_body")" + + if ! _contains "$_response" '"id"'; then + _err "Failed to create TXT record: $_response" + return 1 + fi + + return 0 +} + +dns_calrissia_rm() { + fulldomain="$1" + txtvalue="$2" + + _calrissia_load_token || return 1 + + if ! _calrissia_get_root "$fulldomain"; then + _err "Unable to find domain in Calrissia account for: $fulldomain" + return 1 + fi + + _debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'" + + # Look the record up from the API instead of relying on local state. + # The record list is embedded in the domain object. + _response="$(_calrissia_request GET "/domain/$_domain_id")" + _debug2 "Response: $_response" + + # Split the record objects onto separate lines, then match on both the + # subdomain name and the TXT value to find the record id to delete. + _record_id="$(printf "%s" "$_response" | + tr '{}' '\n' | + grep "\"name\" *: *\"$_sub_domain\"" | + grep "\"content\" *: *\"$txtvalue\"" | + _egrep_o '"id" *: *[0-9]+' | + _head_n 1 | + _egrep_o '[0-9]+')" + + if [ -z "$_record_id" ]; then + _info "No matching TXT record found for $fulldomain; nothing to remove" + return 0 + fi + + _info "Removing TXT record id=$_record_id from domain id=$_domain_id" + if ! _response="$(_calrissia_request DELETE "/domain/$_domain_id/record/$_record_id")" || _contains "$_response" '"error"'; then + _err "Failed to remove TXT record: $_response" + return 1 + fi + return 0 +} + +#################### +# Private helpers # +#################### + +_calrissia_load_token() { + CALRISSIA_TOKEN="${CALRISSIA_TOKEN:-$(_readaccountconf_mutable CALRISSIA_TOKEN)}" + if [ -z "$CALRISSIA_TOKEN" ]; then + _err "CALRISSIA_TOKEN is not set. Generate one at https://identity.calrissia.com under API Keys." + return 1 + fi + _saveaccountconf_mutable CALRISSIA_TOKEN "$CALRISSIA_TOKEN" +} + +# Sets _domain, _domain_id, _sub_domain for a given FQDN. +_calrissia_get_root() { + _fqdn="$1" + + i=1 + while true; do + _candidate="$(printf "%s" "$_fqdn" | cut -d . -f "$i"-)" + [ -z "$_candidate" ] && return 1 + + _debug "Trying root domain: $_candidate" + _response="$(_calrissia_request GET "/domain?full_domain_name=$_candidate")" + _debug2 "Response: $_response" + + _domain_id="$(printf "%s" "$_response" | + _egrep_o '"id" *: *[0-9]+' | + _head_n 1 | + _egrep_o '[0-9]+')" + + if [ -n "$_domain_id" ]; then + if [ "$i" = "1" ]; then + # The FQDN itself is the zone apex, e.g. a challenge-alias domain. + _sub_domain="" + else + _sub_domain="$(printf "%s" "$_fqdn" | cut -d . -f "1-$((i - 1))")" + fi + _domain="$_candidate" + return 0 + fi + + i=$((i + 1)) + done +} + +_calrissia_request() { + _method="$1" + _path="$2" + _body="$3" + export _H1="Authorization: Bearer $CALRISSIA_TOKEN" + export _H2="Accept: application/json" + if [ "$_method" = "GET" ]; then + _get "$CALRISSIA_API$_path" + else + _post "$_body" "$CALRISSIA_API$_path" "" "$_method" "application/json" + fi +} From 1a3682346166989a02894b4fa401ec9127445938 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 16:44:07 +0800 Subject: [PATCH 552/689] https://github.com/acmesh-official/acme.sh/issues/3201 --- deploy/synology_dsm.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 6fce1f19..502bc59b 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -322,8 +322,8 @@ synology_dsm_deploy() { _savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN" _savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME" else - _savedeployconf SYNO_USERNAME "$SYNO_USERNAME" - _savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" + _savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64" + _savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64" _savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID" _savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME" fi From 6df2d9e451443ac3d935b7dfcb14b6bd146dea7e Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 18:19:45 +0800 Subject: [PATCH 553/689] dns_da: document that special characters in DA_Api credentials must be percent-encoded https://github.com/acmesh-official/acme.sh/issues/3468 --- dnsapi/dns_da.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_da.sh b/dnsapi/dns_da.sh index 36251b05..d9cf6247 100755 --- a/dnsapi/dns_da.sh +++ b/dnsapi/dns_da.sh @@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API Site: DirectAdmin.com/api.php Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da Options: - DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443" + DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40". DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept Issues: github.com/TigerP/acme.sh/issues ' From bbfb6f50aec5bddb397b5f852c5d0d1f3d5690df Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 18:46:59 +0800 Subject: [PATCH 554/689] deploy/cpanel_uapi: strip YAML double quotes around wildcard domains in list_domains output fix https://github.com/acmesh-official/acme.sh/issues/6115 --- deploy/cpanel_uapi.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deploy/cpanel_uapi.sh b/deploy/cpanel_uapi.sh index e5381b61..16b622bb 100644 --- a/deploy/cpanel_uapi.sh +++ b/deploy/cpanel_uapi.sh @@ -194,7 +194,8 @@ __cpanel_parse_response() { printf("%s%s=%s\n", prefix, $2, $3); } }' | - sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' + sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' | + sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains) } # Load parameter by prefix+name - fallback to default if not set, and save to config From bcbfe25d08b90a133ac9da87c832889eb1975fe0 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 18:50:29 +0800 Subject: [PATCH 555/689] haproxy.sh: use two-argument -header form for LibreSSL (#3438) --- deploy/haproxy.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index b618a65b..66a2e83e 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -272,12 +272,18 @@ haproxy_deploy() { _cafile_argument="" fi _debug _cafile_argument "${_cafile_argument}" - # if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed + # OpenSSL 1.1+ expects -header Host=value (one argument), while + # LibreSSL keeps the old two-argument form -header Host value at any + # version (3.x/4.x), so it must be detected by name, not by number. + _openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1) _openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2) + _debug _openssl_name "${_openssl_name}" _debug _openssl_version "${_openssl_version}" _openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1) _openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2) - if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then + if [ "${_openssl_name}" = "LibreSSL" ]; then + _header_sep=" " + elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then _header_sep="=" else _header_sep=" " From 33704fc27479a86d8aa6e6b14c27d9b3ab82b87d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Gouin?= Date: Sat, 4 Jul 2026 12:54:07 +0200 Subject: [PATCH 556/689] Allow creation of ACME account with EAB directly from `--issue` command (#5087) * formalized _eab_id and _eab_kid and added EAB parameters to _regAccount on --issue * Update acme.sh * Update acme.sh --- acme.sh | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/acme.sh b/acme.sh index 8e8186e0..6bf59fbb 100755 --- a/acme.sh +++ b/acme.sh @@ -3872,10 +3872,10 @@ _on_issue_success() { #account_key_length eab-kid eab-hmac-key registeraccount() { _account_key_length="$1" - _eab_id="$2" + _eab_kid="$2" _eab_hmac_key="$3" _initpath - _regAccount "$_account_key_length" "$_eab_id" "$_eab_hmac_key" + _regAccount "$_account_key_length" "$_eab_kid" "$_eab_hmac_key" } __calcAccountKeyHash() { @@ -3905,7 +3905,7 @@ _getAccountEmail() { _regAccount() { _initpath _reg_length="$1" - _eab_id="$2" + _eab_kid="$2" _eab_hmac_key="$3" _debug3 _regAccount "$_regAccount" _initAPI @@ -3922,13 +3922,13 @@ _regAccount() { if ! _calcjwk "$ACCOUNT_KEY_PATH"; then return 1 fi - if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then - _savecaconf CA_EAB_KEY_ID "$_eab_id" + if [ "$_eab_kid" ] && [ "$_eab_hmac_key" ]; then + _savecaconf CA_EAB_KEY_ID "$_eab_kid" _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" fi - _eab_id=$(_readcaconf "CA_EAB_KEY_ID") + _eab_kid=$(_readcaconf "CA_EAB_KEY_ID") _eab_hmac_key=$(_readcaconf "CA_EAB_HMAC_KEY") - _secure_debug3 _eab_id "$_eab_id" + _secure_debug3 _eab_kid "$_eab_kid" _secure_debug3 _eab_hmac_key "$_eab_hmac_key" _email="$(_getAccountEmail)" if [ "$_email" ]; then @@ -3936,7 +3936,7 @@ _regAccount() { fi if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then - if [ -z "$_eab_id" ] || [ -z "$_eab_hmac_key" ]; then + if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then _info "No EAB credentials found for ZeroSSL, let's obtain them" if [ -z "$_email" ]; then _info "$(__green "$PROJECT_NAME is using ZeroSSL as default CA now.")" @@ -3952,10 +3952,10 @@ _regAccount() { return 1 fi _secure_debug2 _eabresp "$_eabresp" - _eab_id="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')" - _secure_debug2 _eab_id "$_eab_id" - if [ -z "$_eab_id" ]; then - _err "Cannot resolve _eab_id" + _eab_kid="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_kid"' | cut -d : -f 2 | tr -d '"')" + _secure_debug2 _eab_kid "$_eab_kid" + if [ -z "$_eab_kid" ]; then + _err "Cannot resolve _eab_kid" return 1 fi _eab_hmac_key="$(echo "$_eabresp" | tr ',}' '\n\n' | grep '"eab_hmac_key"' | cut -d : -f 2 | tr -d '"')" @@ -3964,12 +3964,12 @@ _regAccount() { _err "Cannot resolve _eab_hmac_key" return 1 fi - _savecaconf CA_EAB_KEY_ID "$_eab_id" + _savecaconf CA_EAB_KEY_ID "$_eab_kid" _savecaconf CA_EAB_HMAC_KEY "$_eab_hmac_key" fi fi - if [ "$_eab_id" ] && [ "$_eab_hmac_key" ]; then - eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_id\",\"url\":\"${ACME_NEW_ACCOUNT}\"}" + if [ "$_eab_kid" ] && [ "$_eab_hmac_key" ]; then + eab_protected="{\"alg\":\"HS256\",\"kid\":\"$_eab_kid\",\"url\":\"${ACME_NEW_ACCOUNT}\"}" _debug3 eab_protected "$eab_protected" eab_protected64=$(printf "%s" "$eab_protected" | _base64 | _url_replace) @@ -4798,7 +4798,7 @@ issue() { _debug2 _saved_account_key_hash "$_saved_account_key_hash" if [ -z "$ACCOUNT_URL" ] || [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then - if ! _regAccount "$_accountkeylength"; then + if ! _regAccount "$_accountkeylength" "$_eab_kid" "$_eab_hmac_key"; then _on_issue_err "$_post_hook" return 1 fi From 917bebd46033fad6a167d768e9c04cc82520bff3 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 19:06:26 +0800 Subject: [PATCH 557/689] dns_huaweicloud: add optional HUAWEICLOUD_Region (default ap-southeast-1) The DNS endpoint and IAM token scope project were hardcoded to ap-southeast-1, which fails for accounts without that region enabled. fix https://github.com/acmesh-official/acme.sh/issues/5302 --- dnsapi/dns_huaweicloud.sh | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_huaweicloud.sh b/dnsapi/dns_huaweicloud.sh index ee2d2b8e..83fcc625 100644 --- a/dnsapi/dns_huaweicloud.sh +++ b/dnsapi/dns_huaweicloud.sh @@ -7,11 +7,11 @@ Options: HUAWEICLOUD_Username Username HUAWEICLOUD_Password Password HUAWEICLOUD_DomainName DomainName + HUAWEICLOUD_Region Region. E.g. "cn-north-4". Optional, defaults to "ap-southeast-1". Issues: github.com/acmesh-official/acme.sh/issues/3265 ' iam_api="https://iam.myhuaweicloud.com" -dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work ######## Public functions ##################### @@ -30,6 +30,7 @@ dns_huaweicloud_add() { HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}" HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}" + HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}" # Check information if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then @@ -37,8 +38,11 @@ dns_huaweicloud_add() { return 1 fi + _huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}" + dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com" + unset token # Clear token - token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")" + token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")" if [ -z "${token}" ]; then # Check token _err "dns_api(dns_huaweicloud): Error getting token." return 1 @@ -65,6 +69,9 @@ dns_huaweicloud_add() { _saveaccountconf_mutable HUAWEICLOUD_Username "${HUAWEICLOUD_Username}" _saveaccountconf_mutable HUAWEICLOUD_Password "${HUAWEICLOUD_Password}" _saveaccountconf_mutable HUAWEICLOUD_DomainName "${HUAWEICLOUD_DomainName}" + if [ -n "${HUAWEICLOUD_Region}" ]; then + _saveaccountconf_mutable HUAWEICLOUD_Region "${HUAWEICLOUD_Region}" + fi return 0 } @@ -81,6 +88,7 @@ dns_huaweicloud_rm() { HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}" HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}" HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}" + HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}" # Check information if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then @@ -88,8 +96,11 @@ dns_huaweicloud_rm() { return 1 fi + _huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}" + dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com" + unset token # Clear token - token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")" + token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")" if [ -z "${token}" ]; then # Check token _err "dns_api(dns_huaweicloud): Error getting token." return 1 @@ -298,6 +309,7 @@ _get_token() { _username=$1 _password=$2 _domain_name=$3 + _region_name=$4 _debug "Getting Token" body="{ @@ -318,7 +330,7 @@ _get_token() { }, \"scope\": { \"project\": { - \"name\": \"ap-southeast-1\" + \"name\": \"${_region_name}\" } } } From a1051260635487670f28188156caec4f6dfe40fe Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 19:14:36 +0800 Subject: [PATCH 558/689] _date2time: pass date via argv to python to prevent code injection (#6463) https://github.com/acmesh-official/acme.sh/issues/6463 --- acme.sh | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 6bf59fbb..148945b9 100755 --- a/acme.sh +++ b/acme.sh @@ -1895,12 +1895,13 @@ _date2time() { if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then return fi - #Omnios - if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then + #Omnios. Pass the date as argv (sys.argv[1]) instead of interpolating it into + #the -c program text, so a quote in the input cannot inject Python code. + if python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" "$1" 2>/dev/null; then return fi #Omnios - if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then + if python3 -c "import datetime,sys; print(int(datetime.datetime.strptime(sys.argv[1], \"%Y-%m-%dT%H:%M:%SZ\").replace(tzinfo=datetime.timezone.utc).timestamp()))" "$1" 2>/dev/null; then return fi _err "Cannot parse _date2time $1" @@ -2992,6 +2993,11 @@ _initAPI() { return 0 fi _err "Cannot init API for $_api_server" + if [ "$_api_server" = "$CA_ZEROSSL" ]; then + _info "$(__green "If this host is IPv6-only: ZeroSSL currently has no IPv6 endpoint.")" + _info "$(__green "Try another CA, e.g.: $PROJECT_ENTRY --set-default-ca --server letsencrypt")" + _info "See: $(__green "https://github.com/acmesh-official/acme.sh/issues/6872")" + fi return 1 } From d3e12694b9a945664c963b3336270273d309e991 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 19:56:37 +0800 Subject: [PATCH 559/689] fix "identifiers are duplicated" when signing a CSR with a wildcard CN also present in SAN _contains matches with grep regex, so the '*' in "DNS:*.example.com," never matched and the subject was appended to the identifiers a second time. Escape the wildcard before the check, the same way the sed removal already does. fix https://github.com/acmesh-official/acme.sh/issues/5251 --- acme.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/acme.sh b/acme.sh index 148945b9..8256d5f0 100755 --- a/acme.sh +++ b/acme.sh @@ -1423,12 +1423,13 @@ _readSubjectAltNamesFromCSR() { _dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" _debug _dnsAltnames "$_dnsAltnames" - if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then + # escape the wildcard '*' so it is not taken as a regex operator by grep/sed below + _excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')" + _debug _excapedAlgnames "$_excapedAlgnames" + _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')" + _debug _escapedSubject "$_escapedSubject" + if _contains "$_excapedAlgnames," "DNS:$_escapedSubject,"; then _debug "AltNames contains subject" - _excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')" - _debug _excapedAlgnames "$_excapedAlgnames" - _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')" - _debug _escapedSubject "$_escapedSubject" _dnsAltnames="$(echo "$_excapedAlgnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")" _debug _dnsAltnames "$_dnsAltnames" else From 843a7efa7ddd2b069c96249de6261100e08b6af5 Mon Sep 17 00:00:00 2001 From: xiaopc Date: Sat, 4 Jul 2026 20:14:32 +0800 Subject: [PATCH 560/689] fix(gcore_cdn): renew login api url (#5143) https://api.gcore.com/docs/iam#tag/Account --- deploy/gcore_cdn.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/gcore_cdn.sh b/deploy/gcore_cdn.sh index fd17cc25..93e9e32c 100644 --- a/deploy/gcore_cdn.sh +++ b/deploy/gcore_cdn.sh @@ -57,7 +57,7 @@ gcore_cdn_deploy() { _request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}" _debug _request "$_request" export _H1="Content-Type:application/json" - _response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login") + _response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login") _debug _response "$_response" _regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$" _debug _regex "$_regex" From ede9a86d46d93f56d5b36ce2873c0d19489604bc Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 20:21:17 +0800 Subject: [PATCH 561/689] Accept both 401 and 403 for deactivated account detection RFC 8555 sec 7.3.6 requires 401 (Unauthorized) when a request is signed by a deactivated account, which ZeroSSL follows, while Boulder (Let's Encrypt) historically returns 403. Check both codes in _regAccount and deactivateaccount. fix https://github.com/acmesh-official/acme.sh/issues/5138 --- acme.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 8256d5f0..e7e9eb66 100755 --- a/acme.sh +++ b/acme.sh @@ -4042,7 +4042,9 @@ _regAccount() { _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" _savecaconf CA_KEY_HASH "$CA_KEY_HASH" - if [ "$code" = '403' ]; then + #RFC 8555 sec 7.3.6 requires 401 for requests from a deactivated account, + #but Boulder (Let's Encrypt) historically returns 403. Accept both. + if [ "$code" = '403' ] || [ "$code" = '401' ]; then _err "It seems that the account key has been deactivated, please use a new account key." return 1 fi @@ -4122,7 +4124,8 @@ deactivateaccount() { if _send_signed_request "$_accUri" "$_djson" && _contains "$response" '"deactivated"'; then _info "Successfully deactivated account $_accUri." _accid=$(echo "$response" | _egrep_o "\"id\" *: *[^,]*," | cut -d : -f 2 | tr -d ' ,') - elif [ "$code" = "403" ]; then + elif [ "$code" = "403" ] || [ "$code" = "401" ]; then + #RFC 8555 sec 7.3.6: 401 from a deactivated account; Boulder returns 403 _info "The account is already deactivated." _accid=$(_getfield "$_accUri" "999" "/") else From fbf3b41c541694348584024b5d6703b3ddfbde95 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 20:28:56 +0800 Subject: [PATCH 562/689] dns_inwx: fix _get_root false zone match for single-letter subdomains _get_root matched the candidate zone with _contains (grep), which treats the domain as a regex. For "-d g." the candidate "g." matched "..." because '.' matches the '>' after "string" and the 'g' comes from the "" tag, so "g." was wrongly taken as the root zone (sub=_acme-challenge instead of _acme-challenge.g). Anchor the match to $h and escape dots so the zone is compared literally. Fixes #5129 --- dnsapi/dns_inwx.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index dba23846..460d4d28 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -307,13 +307,18 @@ _get_root() { return 1 fi - if _contains "$response" "$h"; then + # Anchor the match to the XML tag and escape dots so $h is compared + # literally: _contains uses grep, which treats "$h" as a regex, and a + # bare "g.berlight.de" would match "berlight.de" (the 'g' from + # "" plus '.' matching '>'). See issue #5129. + _hregex=$(printf "%s" "$h" | sed 's/\./\\./g') + if _contains "$response" "$_hregex"; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$h" return 0 fi # IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode, - # encode each zone name via _idn() and compare — no python dependency. + # encode each zone name via _idn() and compare -- no python dependency. if _contains "$h" "xn--"; then _zone_unicode=$(printf "%s" "$response" | _egrep_o '[^<]*' | sed 's/<[^>]*>//g' | while IFS= read -r _z; do From b92516f79edef6a286dd8ba52305b8d541830410 Mon Sep 17 00:00:00 2001 From: Ramon Date: Sat, 4 Jul 2026 15:07:18 +0200 Subject: [PATCH 563/689] add application/json to acmedns (#5066) --- dnsapi/dns_acmedns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_acmedns.sh b/dnsapi/dns_acmedns.sh index f3f50233..b109a4e5 100755 --- a/dnsapi/dns_acmedns.sh +++ b/dnsapi/dns_acmedns.sh @@ -71,7 +71,7 @@ dns_acmedns_add() { data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}" _debug data "$data" - response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")" + response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")" _debug response "$response" if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then From 988afd0f59545ca6cc57b9317701c3156934e261 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 21:34:35 +0800 Subject: [PATCH 564/689] _isIPv4: do not glob segments, require exactly 4 octets The unquoted splitting let a "*" segment expand against files in the current directory, so "*.*.*.*" could pass as a valid IPv4 address (issue 4971). The old code also accepted "", "1.2.3", "1.2.3.4.5", "1..2.3" and bare numbers. Split with IFS under set -f, require 4 octets, and validate each as a 1-3 digit number <= 255. Based on https://github.com/acmesh-official/acme.sh/pull/4974 fix https://github.com/acmesh-official/acme.sh/issues/4971 --- acme.sh | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/acme.sh b/acme.sh index e7e9eb66..b62b19cf 100755 --- a/acme.sh +++ b/acme.sh @@ -4598,16 +4598,25 @@ _match_issuer() { #ip _isIPv4() { - for seg in $(echo "$1" | tr '.' ' '); do - _debug2 seg "$seg" - if [ "$(echo "$seg" | tr -d '[0-9]')" ]; then - #not all number + #splitting must not glob: a "*" segment would match files in cwd + set -f + _ipv4_saved_ifs="$IFS" + IFS='.' + # shellcheck disable=SC2086 + set -- $1 + IFS="$_ipv4_saved_ifs" + set +f + if [ $# -ne 4 ]; then + return 1 + fi + for _ipv4_seg in "$@"; do + _debug2 _ipv4_seg "$_ipv4_seg" + case "$_ipv4_seg" in + *[!0-9]* | "") return 1 ;; + esac + if [ "${#_ipv4_seg}" -gt 3 ] || [ "$_ipv4_seg" -gt 255 ]; then return 1 fi - if [ $seg -ge 0 ] && [ $seg -lt 256 ]; then - continue - fi - return 1 done return 0 } From 9764f67619065a5aaa6869a595eac30c9286357c Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 21:58:57 +0800 Subject: [PATCH 565/689] dns_cn: convert IDN domain to punycode before API calls Core-Networks' API rejects Unicode domain names with "invalid domain"; it requires punycode. dns_cn_add / dns_cn_rm passed the raw challenge domain straight through, so IDN certs failed at the TXT add step (issue #4804). Run fulldomain through _idn() in both functions. For ASCII/punycode input _idn() is a pass-through, so non-IDN domains are unaffected. Fixes #4804 --- dnsapi/dns_cn.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_cn.sh b/dnsapi/dns_cn.sh index 79698e88..e06a2be6 100644 --- a/dnsapi/dns_cn.sh +++ b/dnsapi/dns_cn.sh @@ -15,7 +15,8 @@ CN_API="https://beta.api.core-networks.de" ######## Public functions ##################### dns_cn_add() { - fulldomain=$1 + # Core-Networks API requires punycode for IDN domains + fulldomain=$(_idn "$1") txtvalue=$2 if ! _cn_login; then @@ -58,7 +59,8 @@ dns_cn_add() { } dns_cn_rm() { - fulldomain=$1 + # Core-Networks API requires punycode for IDN domains + fulldomain=$(_idn "$1") txtvalue=$2 if ! _cn_login; then From 4978782fb8df3a72d2f79a8360123f872be8a3df Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 23:40:31 +0800 Subject: [PATCH 566/689] renewAll: error out if CERT_HOME is not a directory With a misconfigured $HOME / CERT_HOME the glob over "$CERT_HOME"/*.* matches nothing, so renewAll silently does nothing and returns success -- --renew-all / --cron appears to work while renewing no certificates. Check that CERT_HOME is a directory up front and return 1 with a clear error instead. Closes #4508 --- acme.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/acme.sh b/acme.sh index b62b19cf..4ac94da3 100755 --- a/acme.sh +++ b/acme.sh @@ -6072,6 +6072,10 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 + if ! [ -d "$CERT_HOME" ]; then + _err "$CERT_HOME is not a directory, please check your configuration." + return 1 + fi for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do _debug di "$di" if ! [ -d "$di" ]; then From 77047eb0efaf21b0019b8389891b00388f830eee Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Jul 2026 23:55:16 +0800 Subject: [PATCH 567/689] fix CSR reading on systems without a default openssl.cnf (e.g. NetBSD) "openssl req -noout -in" aborts when the default config file is missing; reading a CSR needs no config, so pass -config /dev/null explicitly. Stock NetBSD does not install /etc/openssl/openssl.cnf, so --signcsr never worked there. --- acme.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 4ac94da3..4bc4b2ba 100755 --- a/acme.sh +++ b/acme.sh @@ -1405,7 +1405,9 @@ _readSubjectFromCSR() { _usage "_readSubjectFromCSR mycsr.csr" return 1 fi - ${ACME_OPENSSL_BIN:-openssl} req -noout -in "$_csrfile" -subject | tr ',' "\n" | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d ' \n' + # -config /dev/null: reading a CSR needs no config, but a missing default + # openssl.cnf is fatal on some systems (e.g. NetBSD does not install one) + ${ACME_OPENSSL_BIN:-openssl} req -noout -in "$_csrfile" -subject -config /dev/null | tr ',' "\n" | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d ' \n' } #_csrfile @@ -1420,7 +1422,7 @@ _readSubjectAltNamesFromCSR() { _csrsubj="$(_readSubjectFromCSR "$_csrfile")" _debug _csrsubj "$_csrsubj" - _dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" + _dnsAltnames="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" -config /dev/null | grep "^ *DNS:.*" | tr -d ' \n')" _debug _dnsAltnames "$_dnsAltnames" # escape the wildcard '*' so it is not taken as a regex operator by grep/sed below @@ -1447,7 +1449,7 @@ _readKeyLengthFromCSR() { return 1 fi - _outcsr="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile")" + _outcsr="$(${ACME_OPENSSL_BIN:-openssl} req -noout -text -in "$_csrfile" -config /dev/null)" _debug2 _outcsr "$_outcsr" if _contains "$_outcsr" "Public Key Algorithm: id-ecPublicKey"; then _debug "ECC CSR" From 0eb5cc8384c306fc3adcbbe8d00bb85608751749 Mon Sep 17 00:00:00 2001 From: Foster Snowhill Date: Sun, 5 Jul 2026 06:03:48 +0200 Subject: [PATCH 568/689] dns_desec: fix advertised token variable name (#7081) This must've been a copy-paste error from `dns_ddnss`. Fixes: 6b7b5caf54ea ("DNS provider API: structured description") --- dnsapi/dns_desec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_desec.sh b/dnsapi/dns_desec.sh index 275babea..e5e4809a 100644 --- a/dnsapi/dns_desec.sh +++ b/dnsapi/dns_desec.sh @@ -4,7 +4,7 @@ dns_desec_info='deSEC.io Site: desec.readthedocs.io/en/latest/ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec Options: - DDNSS_Token API Token + DEDYN_TOKEN API Token Issues: github.com/acmesh-official/acme.sh/issues/2180 Author: Zheng Qian ' From 524d96a3a8004eb7674bd74b1ba7b6f67f4e808a Mon Sep 17 00:00:00 2001 From: Jan Forman <47356271+jforman96@users.noreply.github.com> Date: Sun, 5 Jul 2026 06:44:07 +0200 Subject: [PATCH 569/689] Add WEDOS WAPI DNS API (dns_wedos) (#7072) * Add WEDOS WAPI DNS API (dns_wedos) * dns_wedos: fix response parsing on systems without egrep -o * dns_wedos: report WAPI auth errors, UTC fallback for hosts ignoring TZ --- dnsapi/dns_wedos.sh | 217 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 dnsapi/dns_wedos.sh diff --git a/dnsapi/dns_wedos.sh b/dnsapi/dns_wedos.sh new file mode 100644 index 00000000..d1f353e2 --- /dev/null +++ b/dnsapi/dns_wedos.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_wedos_info='WEDOS.com +Site: wedos.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_wedos +Options: + WEDOS_Username WAPI login (account email) + WEDOS_Wapipass WAPI password +Issues: github.com/acmesh-official/acme.sh/issues/7071 +Author: Jan Forman +' + +WEDOS_Api="https://api.wedos.com/wapi/json" + +######## Public functions ##################### + +#Usage: dns_wedos_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_wedos_add() { + fulldomain=$(echo "$1" | _lower_case) + txtvalue=$2 + + if ! _wedos_init; then + return 1 + fi + + _debug "Detecting root zone for $fulldomain" + if ! _get_root "$fulldomain"; then + _err "Cannot determine root zone for: $fulldomain" + return 1 + fi + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + _info "Adding TXT record: $_sub_domain.$_domain" + if ! _wedos_request "dns-row-add" "{\"domain\":\"$_domain\",\"name\":\"$_sub_domain\",\"ttl\":\"300\",\"type\":\"TXT\",\"rdata\":\"$txtvalue\"}"; then + _err "Failed to add TXT record" + return 1 + fi + + _info "Committing DNS changes for $_domain" + if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then + _err "Failed to commit DNS changes" + return 1 + fi + + return 0 +} + +#Usage: dns_wedos_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_wedos_rm() { + fulldomain=$(echo "$1" | _lower_case) + txtvalue=$2 + + if ! _wedos_init; then + return 1 + fi + + _debug "Detecting root zone for $fulldomain" + if ! _get_root "$fulldomain"; then + _err "Cannot determine root zone for: $fulldomain" + return 1 + fi + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + # _get_root leaves the dns-rows-list response for $_domain in $response + _debug "Looking up row IDs for TXT value: $txtvalue" + _row_ids=$(echo "$response" | tr '{' '\n' | grep -F -- "\"rdata\":\"$txtvalue\"" | grep -F -- "\"name\":\"$_sub_domain\"" | _egrep_o '"ID": *"[0-9]*"' | tr -dc '0-9\n') + _debug _row_ids "$_row_ids" + + if [ -z "$_row_ids" ]; then + _info "TXT record not found, nothing to remove" + return 0 + fi + + for _row_id in $_row_ids; do + _info "Removing TXT record ID $_row_id from $_domain" + if ! _wedos_request "dns-row-delete" "{\"domain\":\"$_domain\",\"row_id\":\"$_row_id\"}"; then + _err "Failed to delete TXT record" + return 1 + fi + done + + _info "Committing DNS changes for $_domain" + if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then + _err "Failed to commit DNS changes" + return 1 + fi + + return 0 +} + +#################### Private functions below ################################## + +_wedos_init() { + WEDOS_Username="${WEDOS_Username:-$(_readaccountconf_mutable WEDOS_Username)}" + WEDOS_Wapipass="${WEDOS_Wapipass:-$(_readaccountconf_mutable WEDOS_Wapipass)}" + + if [ -z "$WEDOS_Username" ] || [ -z "$WEDOS_Wapipass" ]; then + WEDOS_Username="" + WEDOS_Wapipass="" + _err "You didn't specify the WEDOS WAPI credentials yet." + _err "Please export WEDOS_Username and WEDOS_Wapipass and try again." + return 1 + fi + + _saveaccountconf_mutable WEDOS_Username "$WEDOS_Username" + _saveaccountconf_mutable WEDOS_Wapipass "$WEDOS_Wapipass" + return 0 +} + +# WAPI auth token: sha1(login + sha1(password) + hour), where the hour is +# the current hour on the WEDOS servers (Europe/Prague timezone). +# The POSIX TZ string is used so no tzdata is required on the client. +_wedos_auth() { + if [ "$_wedos_utc" ]; then + # fallback: WAPI accepts 1 hour of skew, UTC+1 fits both CET and CEST + _wedos_hour=$(date -u +%H) + _wedos_hour=$(printf '%02d' "$(((${_wedos_hour#0} + 1) % 24))") + else + _wedos_hour=$(TZ='CET-1CEST,M3.5.0,M10.5.0/3' date +%H) + fi + _wedos_phash=$(printf '%s' "$WEDOS_Wapipass" | _digest sha1 hex) + printf '%s' "${WEDOS_Username}${_wedos_phash}${_wedos_hour}" | _digest sha1 hex +} + +#Usage: _wedos_request +#Returns 0 and sets $response on WAPI code 1000, returns 1 otherwise. +_wedos_request() { + _wedos_cmd="$1" + _wedos_data="$2" + + _wedos_token=$(_wedos_auth) + _secure_debug _wedos_token "$_wedos_token" + + _wedos_json="{\"request\":{\"user\":\"$WEDOS_Username\",\"auth\":\"$_wedos_token\",\"command\":\"$_wedos_cmd\",\"data\":$_wedos_data}}" + _debug2 "WAPI command: $_wedos_cmd" + _debug2 "WAPI data: $_wedos_data" + + # _post sends the global _H1.._H5 headers with every request; clear them so + # headers from earlier API calls are not leaked to the WAPI endpoint. + export _H1="" + export _H2="" + export _H3="" + export _H4="" + export _H5="" + + _wedos_body="request=$(printf '%s' "$_wedos_json" | _url_encode)" + response=$(_post "$_wedos_body" "$WEDOS_Api" "" "POST" "application/x-www-form-urlencoded") + if [ "$?" != "0" ]; then + _err "WAPI request failed for command '$_wedos_cmd'" + return 1 + fi + _debug2 "WAPI response: $response" + + _wedos_code=$(echo "$response" | _egrep_o '"code": *[0-9]*' | _head_n 1 | tr -dc '0-9') + _debug2 "WAPI result code: $_wedos_code" + if [ "$_wedos_code" = "1000" ]; then + return 0 + fi + + # some systems ignore the TZ variable (Haiku), sending a wrong auth hour; + # retry once with the UTC fallback in _wedos_auth + if [ "$_wedos_code" = "2050" ] && [ -z "$_wedos_utc" ]; then + _wedos_utc=1 + _wedos_request "$_wedos_cmd" "$_wedos_data" + return $? + fi + + # 2050 = bad credentials, 2051 = IP not whitelisted, 2052 = IP blocked + if [ "$_wedos_code" = "2050" ] || [ "$_wedos_code" = "2051" ] || [ "$_wedos_code" = "2052" ]; then + _wedos_result=$(echo "$response" | _egrep_o '"result": *"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _err "WAPI authentication error $_wedos_code: $_wedos_result" + _err "Check WEDOS_Username, WEDOS_Wapipass and the WAPI IP whitelist." + _wedos_autherr=1 + return 1 + fi + + _debug "WAPI error for command '$_wedos_cmd': $response" + return 1 +} + +# Determine the registered domain (_domain) and subdomain prefix (_sub_domain) +# by walking up the labels and calling dns-rows-list until WAPI accepts one. +# _acme-challenge.www.example.co.uk +# -> _sub_domain=_acme-challenge.www _domain=example.co.uk +# The full domain itself is tried first, so a zone apex (e.g. DNS alias mode +# pointing at the registered domain) resolves to an empty _sub_domain. +_get_root() { + _gr_full="$1" + _gr_i=1 + _wedos_autherr="" + while true; do + _gr_candidate=$(printf '%s' "$_gr_full" | cut -d . -f "${_gr_i}"-100) + _debug2 "Checking zone candidate: $_gr_candidate" + if [ -z "$_gr_candidate" ]; then + return 1 + fi + + if _wedos_request "dns-rows-list" "{\"domain\":\"$_gr_candidate\"}"; then + _domain="$_gr_candidate" + if [ "$_gr_i" = "1" ]; then + _sub_domain="" + else + _sub_domain=$(printf '%s' "$_gr_full" | cut -d . -f 1-"$((_gr_i - 1))") + fi + return 0 + fi + + # auth error hits every candidate, stop the walk + if [ "$_wedos_autherr" ]; then + return 1 + fi + + _gr_i=$((_gr_i + 1)) + done +} From 1e2cd50fc9759d046ef16d8dd7ada263fe257078 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 15:44:12 +0800 Subject: [PATCH 570/689] deploy/haproxy: use printf instead of "echo -e" for the stats socket payload dash's echo has no -e flag and sends a literal "-e " prefix to the socket, so haproxy rejects the command and the hot update always fails on Debian/Ubuntu (/bin/sh = dash). Also accept "Transaction updated", which haproxy replies when an uncommitted transaction already exists. fix https://github.com/acmesh-official/acme.sh/issues/6165 --- deploy/haproxy.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/haproxy.sh b/deploy/haproxy.sh index 66a2e83e..9736e6ff 100644 --- a/deploy/haproxy.sh +++ b/deploy/haproxy.sh @@ -364,7 +364,9 @@ haproxy_deploy() { else _info "Update existing certificate '${_pem}' over HAProxy ${_socketname}." fi - _socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'" + # printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket. + # "Transaction updated" is replied instead of "created" when an uncommitted transaction exists. + _socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'" _secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}" eval "${_socat_cert_set_cmd}" _ret=$? From 31b13caf8bfe8f95a49b19662d507280368eeba0 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 15:51:38 +0800 Subject: [PATCH 571/689] DNS.yml: fix workflow warnings - replace deprecated set-output with GITHUB_OUTPUT - untap aws/tap before brew install to silence tap trust warning - inject safe.directory=* for cygwin git so the checkout post step no longer fails with dubious ownership (exit 128) --- .github/workflows/DNS.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index a972ae1a..5417068f 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -26,9 +26,9 @@ jobs: id: step_one run: | if [ "${{secrets.TokenName1}}" ] ; then - echo "::set-output name=hasToken::true" + echo "hasToken=true" >> "$GITHUB_OUTPUT" else - echo "::set-output name=hasToken::false" + echo "hasToken=false" >> "$GITHUB_OUTPUT" fi - name: Check the value run: echo ${{ steps.step_one.outputs.hasToken }} @@ -116,7 +116,9 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install tools - run: brew install socat + run: | + brew untap aws/tap || true + brew install socat - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest @@ -176,9 +178,14 @@ jobs: C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git shell: cmd - name: Set ENV - shell: cmd + shell: bash run: | - echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV% + echo 'PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin' >> "$GITHUB_ENV" + # cygwin git sees the runner workspace as owned by another user and + # fails with "dubious ownership" (exit 128) in the checkout post step + echo 'GIT_CONFIG_COUNT=1' >> "$GITHUB_ENV" + echo 'GIT_CONFIG_KEY_0=safe.directory' >> "$GITHUB_ENV" + echo 'GIT_CONFIG_VALUE_0=*' >> "$GITHUB_ENV" - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest From 1cd63e1480bbb692a937e1e04709971b5e2e9382 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:02:52 +0800 Subject: [PATCH 572/689] _createcsr: omit CN from the CSR subject when it exceeds 64 characters (#4867) --- acme.sh | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/acme.sh b/acme.sh index 4bc4b2ba..98f237a7 100755 --- a/acme.sh +++ b/acme.sh @@ -1307,6 +1307,23 @@ _idn() { } #_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage +#cn +#The x509 Common Name is limited to 64 characters (RFC 5280 ub-common-name, +#enforced by openssl in ASN1_mbstring_ncopy), and an IP address or an empty +#name is not usable as CN either. When this rejects the name, _createcsr +#omits CN from the CSR subject and the CA takes the identifiers from the +#subjectAltName extension (issue 4867). +_is_valid_cn() { + _cn_v="$1" + if [ -z "$_cn_v" ] || [ "${#_cn_v}" -gt 64 ]; then + return 1 + fi + if _isIP "$_cn_v"; then + return 1 + fi + return 0 +} + _createcsr() { _debug _createcsr domain="$1" @@ -1370,16 +1387,16 @@ _createcsr() { _csr_cn="$(_idn "$domain")" _debug2 _csr_cn "$_csr_cn" if _contains "$(uname -a)" "MINGW"; then - if _isIP "$_csr_cn"; then - ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//O=$PROJECT_NAME" -config "$csrconf" -out "$csr" - else + if _is_valid_cn "$_csr_cn"; then ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr" + else + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "//O=$PROJECT_NAME" -config "$csrconf" -out "$csr" fi else - if _isIP "$_csr_cn"; then - ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/O=$PROJECT_NAME" -config "$csrconf" -out "$csr" - else + if _is_valid_cn "$_csr_cn"; then ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" + else + ${ACME_OPENSSL_BIN:-openssl} req -new -sha256 -key "$csrkey" -subj "/O=$PROJECT_NAME" -config "$csrconf" -out "$csr" fi fi } From 24895a15c864adfb461bfe6026a2b934da78060f Mon Sep 17 00:00:00 2001 From: laineus Date: Sun, 5 Jul 2026 17:05:33 +0900 Subject: [PATCH 573/689] Add dns_muumuu: muumuu-domain.com DNS API (#7012) * Add dns_muumuu: muumuu-domain.com DNS API * Fix: remove local keyword for POSIX sh compatibility * Fix: lowercase fulldomain for API compatibility * Style: use echo instead of printf for lower_case (consistent with other plugins) * Fix: prefix rest vars, clear _H4/_H5, guard record_id, update Issues URL --- dnsapi/dns_muumuu.sh | 167 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100755 dnsapi/dns_muumuu.sh diff --git a/dnsapi/dns_muumuu.sh b/dnsapi/dns_muumuu.sh new file mode 100755 index 00000000..8ef0b8c8 --- /dev/null +++ b/dnsapi/dns_muumuu.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_muumuu_info='muumuu-domain.com +Site: muumuu-domain.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_muumuu +Options: + MUUMUU_PAT Personal Access Token (scopes: domains:read, dns:read, dns:write) +Issues: github.com/acmesh-official/acme.sh/issues/7011 +' + +MUUMUU_API="https://muumuu-domain.com/api/v2" + +######## Public functions ##################### + +dns_muumuu_add() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue="$2" + + _info "Using muumuu-domain.com DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}" + if [ -z "$MUUMUU_PAT" ]; then + _err "MUUMUU_PAT is not set." + _err "Please create a Personal Access Token at https://muumuu-domain.com" + _err "with scopes: domains:read, dns:read, dns:write" + return 1 + fi + _saveaccountconf_mutable MUUMUU_PAT "$MUUMUU_PAT" + + if ! _muumuu_get_root "$fulldomain"; then + _err "Unable to find the root domain for $fulldomain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _info "Adding TXT record for ${fulldomain}" + body="{\"fqdn\":\"${fulldomain}.\",\"type\":\"TXT\",\"value\":\"${txtvalue}\",\"ttl\":3600}" + if _muumuu_rest POST "/me/domains/${_domain_id}/dns-records" "$body"; then + if [ "$_muumuu_code" = "201" ]; then + _info "TXT record added successfully" + return 0 + fi + fi + + _err "Failed to add TXT record (HTTP ${_muumuu_code})" + return 1 +} + +dns_muumuu_rm() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue="$2" + + _info "Using muumuu-domain.com DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}" + if [ -z "$MUUMUU_PAT" ]; then + _err "MUUMUU_PAT is not set." + return 1 + fi + + if ! _muumuu_get_root "$fulldomain"; then + _err "Unable to find the root domain for $fulldomain" + return 1 + fi + _debug _domain_id "$_domain_id" + + _info "Looking up TXT record for ${fulldomain}" + if ! _muumuu_rest GET "/me/domains/${_domain_id}/dns-records?type=TXT&fqdn=${fulldomain}."; then + _err "Failed to list TXT records" + return 1 + fi + + record_id=$(echo "$response" | _egrep_o "\"id\":[0-9]+[^}]*\"value\":\"${txtvalue}\"" | _egrep_o "\"id\":[0-9]+" | _head_n 1 | cut -d: -f2) + if [ -z "$record_id" ]; then + _info "TXT record not found, nothing to remove" + return 0 + fi + _debug record_id "$record_id" + + if _muumuu_rest DELETE "/me/domains/${_domain_id}/dns-records/${record_id}"; then + if [ "$_muumuu_code" = "204" ]; then + _info "TXT record deleted successfully" + return 0 + fi + fi + + _err "Failed to delete TXT record (HTTP ${_muumuu_code})" + return 1 +} + +#################### Private functions below ################################## + +# _acme-challenge.www.example.com +# sets: +# _domain_id MU00000001 +# _sub_domain _acme-challenge.www +# _domain example.com +_muumuu_get_root() { + domain="$1" + i=1 + p=0 + h="" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + return 1 + fi + if ! _muumuu_rest GET "/me/domains?fqdn=${h}&page-size=1"; then + return 1 + fi + if [ "$_muumuu_code" = "401" ] || [ "$_muumuu_code" = "403" ]; then + _err "Authentication failed (HTTP ${_muumuu_code}). Check MUUMUU_PAT." + return 1 + fi + if _contains "$response" "\"fqdn\":\"${h}\""; then + _domain_id=$(echo "$response" | _egrep_o "\"id\":\"MU[0-9]+\"" | _head_n 1 | cut -d: -f2 | tr -d '"') + _domain="$h" + if [ "$p" = "0" ]; then + _sub_domain="" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + fi + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done +} + +_muumuu_rest() { + _muumuu_method="$1" + _muumuu_path="$2" + _muumuu_data="$3" + _muumuu_url="${MUUMUU_API}${_muumuu_path}" + + export _H1="Authorization: Bearer ${MUUMUU_PAT}" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + export _H4="" + export _H5="" + + _secure_debug2 data "$_muumuu_data" + + if [ "$_muumuu_method" = "GET" ]; then + response="$(_get "$_muumuu_url")" + else + response="$(_post "$_muumuu_data" "$_muumuu_url" "" "$_muumuu_method")" + fi + _muumuu_ret="$?" + _muumuu_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + _debug "HTTP code: ${_muumuu_code}" + _secure_debug2 response "$response" + + if [ "$_muumuu_ret" != "0" ]; then + _err "Error accessing ${_muumuu_url}" + return 1 + fi + + response="$(printf "%s" "$response" | _normalizeJson)" + return 0 +} From cacafc9c23947b5a5d4c2b2ddad56eca9e56d1da Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:18:27 +0800 Subject: [PATCH 574/689] add Nginx workflow to test the --nginx mode Runs le_test_nginx from acmetest against Pebble: nginx listens on Pebble's HTTP-01 validation port with an aaPanel/BT style "location ^~ /" reverse proxy block, the regression case of #6125. --- .github/workflows/Nginx.yml | 66 +++++++++++++++++++++++++++++++++++++ acme.sh | 11 +++++-- 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/Nginx.yml diff --git a/.github/workflows/Nginx.yml b/.github/workflows/Nginx.yml new file mode 100644 index 00000000..2ca9d64a --- /dev/null +++ b/.github/workflows/Nginx.yml @@ -0,0 +1,66 @@ +name: Nginx +on: + push: + paths: + - '*.sh' + - '.github/workflows/Nginx.yml' + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Nginx.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + Nginx: + runs-on: ubuntu-latest + env: + TestingDomain: example.com + TEST_ACME_Server: https://localhost:14000/dir + HTTPS_INSECURE: 1 + TEST_LOCAL: 1 + TEST_CA: "Pebble Intermediate CA" + TEST_NGINX: 1 + CASE: le_test_nginx + steps: + - uses: actions/checkout@v6 + - name: Install tools + run: sudo apt-get install -y socat nginx + - name: Run Pebble + run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d + - name: Set up Pebble + run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4 + - name: Set up nginx + # a backend on 8081 plus a site with an aaPanel/BT style + # "location ^~ /" proxy block that shadows plain regex locations + # (regression for #6125); the site listens on 5002, which is the + # HTTP-01 validation port in Pebble's default config + run: | + sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF' + server { + listen 127.0.0.1:8081; + location / { + default_type text/plain; + return 200 "backend"; + } + } + server { + listen 5002 default_server; + server_name example.com; + location ^~ / { + proxy_pass http://127.0.0.1:8081; + proxy_set_header Host $http_host; + } + } + EOF + sudo nginx -t + sudo systemctl restart nginx + curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + run: cd ../acmetest && sudo --preserve-env ./letest.sh diff --git a/acme.sh b/acme.sh index 98f237a7..1b9c0b89 100755 --- a/acme.sh +++ b/acme.sh @@ -3448,9 +3448,14 @@ _setNginx() { fi echo "$NGINX_START -location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { - default_type text/plain; - return 200 \"\$1.$_thumbpt\"; +location ^~ /.well-known/acme-challenge/ { + # the ^~ prefix wins over regex-skipping blocks like \"location ^~ /\", + # the nested regex location still captures the token as \$1 + location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { + default_type text/plain; + return 200 \"\$1.$_thumbpt\"; + } + return 404; } #NGINX_START " >>"$FOUND_REAL_NGINX_CONF" From 504540e67ccf564dcdfd8cd369a26ff12127748e Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:29:58 +0800 Subject: [PATCH 575/689] dnsapi/dns_autodns: escape XML special characters in credentials (#5317) --- dnsapi/dns_autodns.sh | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_autodns.sh b/dnsapi/dns_autodns.sh index ce566978..e26d699b 100644 --- a/dnsapi/dns_autodns.sh +++ b/dnsapi/dns_autodns.sh @@ -139,12 +139,21 @@ _get_autodns_zone() { return 1 } +# Escape the XML special characters (& < > ' ") so that credentials +# containing them do not break the request document (issue 5317). +_autodns_xml_encode() { + sed "s/&/\&/g;s//\>/g;s/'/\'/g;s/\"/\"/g" +} + _build_request_auth_xml() { + _autodns_user_xml="$(printf "%s" "$AUTODNS_USER" | _autodns_xml_encode)" + _autodns_password_xml="$(printf "%s" "$AUTODNS_PASSWORD" | _autodns_xml_encode)" + _autodns_context_xml="$(printf "%s" "$AUTODNS_CONTEXT" | _autodns_xml_encode)" printf " %s %s %s - " "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT" + " "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml" } # Arguments: From 507baff2ef8b4391c32b68d4b818f8fe439214dc Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:30:46 +0800 Subject: [PATCH 576/689] deploy/docker: allow setting key file mode and owner in the container The docker deploy hook copied the key file preserving the source mode (root:root 0600), so a non-root container service (uid >= 1000) could not read it. Add DEPLOY_DOCKER_CONTAINER_KEY_MODE and DEPLOY_DOCKER_CONTAINER_KEY_OWNER, applied via chmod/chown inside the container after the key is copied and before the reload command. Closes #5333 --- deploy/docker.sh | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/deploy/docker.sh b/deploy/docker.sh index 7fdcf604..276172aa 100755 --- a/deploy/docker.sh +++ b/deploy/docker.sh @@ -3,6 +3,8 @@ #DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx" #DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem" +#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640" +#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000" #DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem" #DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem" #DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem" @@ -71,6 +73,18 @@ docker_deploy() { _savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE" fi + _getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE + _debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" + if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" + fi + + _getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER + _debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" + if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then + _savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" + fi + _getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE _debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then @@ -112,6 +126,20 @@ docker_deploy() { if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then return 1 fi + if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then + _info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER" + if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then + _err "Can not change owner of key file in container" + return 1 + fi + fi + if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then + _info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE" + if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then + _err "Can not change mode of key file in container" + return 1 + fi + fi fi if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then From 1f94fd7fd596f0bee2e11fbfd033c43a0eeb3d78 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:31:51 +0800 Subject: [PATCH 577/689] add Apache workflow to test the --apache mode Runs le_test_apache from acmetest against Pebble, with Apache listening on Pebble's HTTP-01 validation port. --- .github/workflows/Apache.yml | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/Apache.yml diff --git a/.github/workflows/Apache.yml b/.github/workflows/Apache.yml new file mode 100644 index 00000000..b17abbd1 --- /dev/null +++ b/.github/workflows/Apache.yml @@ -0,0 +1,50 @@ +name: Apache +on: + push: + paths: + - '*.sh' + - '.github/workflows/Apache.yml' + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Apache.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + Apache: + runs-on: ubuntu-latest + env: + TestingDomain: example.com + TEST_ACME_Server: https://localhost:14000/dir + HTTPS_INSECURE: 1 + TEST_LOCAL: 1 + TEST_CA: "Pebble Intermediate CA" + TEST_APACHE: 1 + CASE: le_test_apache + steps: + - uses: actions/checkout@v6 + - name: Install tools + run: sudo apt-get install -y socat apache2 + - name: Run Pebble + run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d + - name: Set up Pebble + run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4 + - name: Set up Apache + # Apache serves on 5002, which is the HTTP-01 validation port in + # Pebble's default config; acme.sh appends the challenge Alias to + # the main config itself + run: | + echo "Listen 5002" | sudo tee /etc/apache2/ports.conf + sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf + sudo apache2ctl configtest + sudo systemctl restart apache2 + curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404" + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Run acmetest + run: cd ../acmetest && sudo --preserve-env ./letest.sh From 4256e3532b37b5ff27ad5a534e50aef01003c82a Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:32:57 +0800 Subject: [PATCH 578/689] _regAccount: error out clearly when the eab-hmac-key cannot be base64-decoded An undecodable key (e.g. broken LibreSSL base64 -d -A) used to produce the cryptic "Usage: _hmac hashalg secret [outputhex]" and an empty EAB signature that the CA rejects with 403. https://github.com/acmesh-official/acme.sh/issues/4082 --- acme.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/acme.sh b/acme.sh index 1b9c0b89..42d94f79 100755 --- a/acme.sh +++ b/acme.sh @@ -4014,6 +4014,10 @@ _regAccount() { key_hex="$(_durl_replace_base64 "$_eab_hmac_key" | _dbase64 | _hex_dump | tr -d ' ')" _debug3 key_hex "$key_hex" + if [ -z "$key_hex" ]; then + _err "Cannot base64-decode the eab-hmac-key. Please check the value, and your openssl version." + return 1 + fi eab_signature=$(printf "%s" "$eab_sign_t" | _hmac sha256 $key_hex | _base64 | _url_replace) _debug3 eab_signature "$eab_signature" From defd64022d8864153d8a6caeb18c76066be3dca3 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:53:14 +0800 Subject: [PATCH 579/689] dnsapi/dns_namecom: probe the root zone with GetDomain instead of listing all domains The domain list is paginated at 1000 entries per page and only the first page was fetched, so accounts with more than 1000 domains never found the root zone. fix https://github.com/acmesh-official/acme.sh/issues/5051 --- dnsapi/dns_namecom.sh | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_namecom.sh b/dnsapi/dns_namecom.sh index 1062c849..1ba6a6e5 100755 --- a/dnsapi/dns_namecom.sh +++ b/dnsapi/dns_namecom.sh @@ -153,10 +153,9 @@ _namecom_get_root() { i=2 p=1 - if ! _namecom_rest GET "domains"; then - return 1 - fi - + # Probe each candidate with GetDomain (GET /v4/domains/{domainName}) instead + # of listing all domains: the list is paginated at 1000 domains per page, so + # larger accounts never found their domain on the first page. # Need to exclude the last field (tld) numfields=$(echo "$domain" | _egrep_o "\." | wc -l) while [ "$i" -le "$numfields" ]; do @@ -166,7 +165,7 @@ _namecom_get_root() { return 1 fi - if _contains "$response" "$host"; then + if _namecom_rest GET "domains/$host" && _contains "$response" "\"domainName\":\"$host\""; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _domain="$host" return 0 From 7def43481a41f35c30e45b5407878145e8ffa53c Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 16:57:59 +0800 Subject: [PATCH 580/689] dns_regru: require a dot boundary in root zone matching _get_root matched a registered domain anywhere as a substring of the challenge domain, so with both "test.com.ru" and "subtest.com.ru" in the account, issuing for subtest.com.ru wrongly picked test.com.ru as the root (it is a substring of "sub-test.com.ru"). Anchor the match to a '.' boundary so a shorter domain no longer matches a longer subdomain label. Fixes the issue reported in #5036 (thanks @koledas) Closes #5036 --- dnsapi/dns_regru.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_regru.sh b/dnsapi/dns_regru.sh index be5ae117..edf8b464 100644 --- a/dnsapi/dns_regru.sh +++ b/dnsapi/dns_regru.sh @@ -96,8 +96,8 @@ _get_root() { for ITEM in ${domains_list}; do IDN_ITEM=${ITEM} - case "${domain}" in - *${IDN_ITEM}*) + case ".${domain}" in + *.${IDN_ITEM}*) _domain="$(_idn "${ITEM}")" _debug _domain "${_domain}" return 0 From 934711e51d10c765f67900892e2759ae3dd37dae Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 17:09:51 +0800 Subject: [PATCH 581/689] notify/aws_ses: add container/instance IAM role auth (IMDSv2) aws_ses_send calls `_use_container_role || _use_instance_role` when no static AWS keys are set, but those functions were never defined -- only _use_metadata was -- so role-based auth silently fell through to the "no api key" error. Add both, using the current IMDSv2-capable versions from dns_aws.sh, and set the IMDSv2 token header in _use_metadata so the credential fetch works on IMDSv2-only instances. Closes #4742 --- notify/aws_ses.sh | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/notify/aws_ses.sh b/notify/aws_ses.sh index 07e0c48c..735e6204 100644 --- a/notify/aws_ses.sh +++ b/notify/aws_ses.sh @@ -83,7 +83,43 @@ aws_ses_send() { response="$(aws_rest POST "" "" "$_data")" } +_use_container_role() { + # automatically set if running inside ECS + if [ -z "$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ]; then + _debug "No ECS environment variable detected" + return 1 + fi + _use_metadata "169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" +} + +_use_instance_role() { + _instance_role_name_url="http://169.254.169.254/latest/meta-data/iam/security-credentials/" + + if _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 401; then + _debug "Using IMDSv2" + _token_url="http://169.254.169.254/latest/api/token" + export _H1="X-aws-ec2-metadata-token-ttl-seconds: 21600" + _token="$(_post "" "$_token_url" "" "PUT")" + _secure_debug3 "_token" "$_token" + if [ -z "$_token" ]; then + _debug "Unable to fetch IMDSv2 token from instance metadata" + return 1 + fi + export _H1="X-aws-ec2-metadata-token: $_token" + fi + + if ! _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 200; then + _debug "Unable to fetch IAM role from instance metadata" + return 1 + fi + + _instance_role_name=$(_get "$_instance_role_name_url" "" 1) + _debug "_instance_role_name" "$_instance_role_name" + _use_metadata "$_instance_role_name_url$_instance_role_name" "$_token" +} + _use_metadata() { + export _H1="X-aws-ec2-metadata-token: $2" _aws_creds="$( _get "$1" "" 1 | _normalizeJson | From e964157bffedb865b532e24fcf8abe6b1e0fe1ab Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 17:13:24 +0800 Subject: [PATCH 582/689] _install_win_taskscheduler: zero-pad the minute in the schtasks /ST value (#4950) --- acme.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 42d94f79..d3e364bb 100755 --- a/acme.sh +++ b/acme.sh @@ -6658,8 +6658,10 @@ _install_win_taskscheduler() { _info "$PROJECT_NAME will not save your password." _info "Please input your Windows password for: $(__green "$_myname")" _password="$(__read_password)" - #SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null - echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_randomminute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null + #schtasks.exe /ST requires the HH:mm format, so the minute must be zero-padded (issue 4950) + _st_minute="$(printf "%02d" "$_randomminute")" + #SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_st_minute" '/RU' "$_myname" '/RP' "$_password" '/TR' "$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'" >/dev/null + echo SCHTASKS.exe '/create' '/SC' 'DAILY' '/TN' "$_WINDOWS_SCHEDULER_NAME" '/F' '/ST' "00:$_st_minute" '/RU' "$_myname" '/RP' "$_password" '/TR' "\"$_winbash -l -c '$_lesh --cron --home \"$LE_WORKING_DIR\" $_centry'\"" | cmd.exe >/dev/null echo } From d2b3772631f7055f27a267ae94b83e0f4c99962f Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 17:13:59 +0800 Subject: [PATCH 583/689] deploy/panos: do not commit when the cert or key import failed (#4716) Committing after a failed import leaves a mismatched cert/key pair on the firewall (PAN-OS does not validate the pair at commit time), which can lock the admin out of the https management interface. --- deploy/panos.sh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index 00badffc..fcfd6fb5 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -296,9 +296,20 @@ panos_deploy() { _err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again" return 1 else - deployer cert - deployer key - deployer commit + # A commit of a failed import would leave a mismatched cert/key pair + # on the firewall and can lock the admin out of the management + # interface, see https://github.com/acmesh-official/acme.sh/issues/4716 + if ! deployer cert; then + _err "Cert import failed. Aborting without committing." + return 1 + fi + if ! deployer key; then + _err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config." + return 1 + fi + if ! deployer commit; then + return 1 + fi if [ "$_panos_template_stack" ]; then # try to get job status for 20 times in 30 sec interval i=0 From 1746fbdb255204bbb417e1ca3298699863630eb0 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 17:37:40 +0800 Subject: [PATCH 584/689] support multiple account emails (#1309) ACCOUNT_EMAIL / --email now accepts a comma- or space-separated list and registers all of them as ACME contact entries. The ZeroSSL EAB endpoint takes a single address, so the first one is used there. --- acme.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index d3e364bb..931ab750 100755 --- a/acme.sh +++ b/acme.sh @@ -3917,6 +3917,16 @@ __calc_account_thumbprint() { printf "%s" "$jwk" | tr -d ' ' | _digest "sha256" | _url_replace } +#Reads a comma- or space-separated email list from stdin and prints +#the ACME contact list items: "mailto:a@example.com","mailto:b@example.com" +_mailto_contacts() { + _mc_out="" + for _mc_m in $(tr ',' ' '); do + _mc_out="$_mc_out,\"mailto:$_mc_m\"" + done + echo "$_mc_out" | cut -c 2- +} + _getAccountEmail() { if [ "$ACCOUNT_EMAIL" ]; then echo "$ACCOUNT_EMAIL" @@ -3976,7 +3986,9 @@ _regAccount() { _info "See: $(__green "$_ZEROSSL_WIKI")" return 1 fi - _eabresp=$(_post "email=$_email" $_ZERO_EAB_ENDPOINT) + #the ZeroSSL EAB endpoint takes a single address, use the first one + _eab_email="$(echo "$_email" | tr ',' ' ' | awk '{print $1}')" + _eabresp=$(_post "email=$_eab_email" $_ZERO_EAB_ENDPOINT) if [ "$?" != "0" ]; then _debug2 "$_eabresp" _err "Cannot get EAB credentials from ZeroSSL." @@ -4026,7 +4038,7 @@ _regAccount() { _debug3 externalBinding "$externalBinding" fi if [ "$_email" ]; then - email_sg="\"contact\": [\"mailto:$_email\"], " + email_sg="\"contact\": [$(echo "$_email" | _mailto_contacts)], " fi regjson="{$email_sg\"termsOfServiceAgreed\": true$externalBinding}" @@ -4106,7 +4118,7 @@ updateaccount() { _email="$(_getAccountEmail)" if [ "$_email" ]; then - updjson='{"contact": ["mailto:'$_email'"]}' + updjson='{"contact": ['$(echo "$_email" | _mailto_contacts)']}' else updjson='{"contact": []}' fi From cabe432539f0acbdac954c48175d20d259ab9aaf Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 17:39:50 +0800 Subject: [PATCH 585/689] add bash completion for commands and parameters, installed via --install (#307) --- acme.sh | 11 ++ acme.sh.completion | 341 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 acme.sh.completion diff --git a/acme.sh b/acme.sh index 931ab750..4735b830 100755 --- a/acme.sh +++ b/acme.sh @@ -7236,6 +7236,10 @@ _installalias() { _sed_i "/^export LE_CONFIG_HOME/d" "$_envfile" fi _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" ]; then + #the completion file does nothing when sourced by a non-bash shell + _setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\"" + fi _profile="$(_detect_profile)" if [ "$_profile" ]; then @@ -7353,6 +7357,11 @@ install() { _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" + if [ -f "$PROJECT_ENTRY.completion" ]; then + cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/" + _debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion" + fi + if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then _installalias "$_c_home" fi @@ -7430,6 +7439,7 @@ uninstall() { _uninstallalias rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" + rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" _info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\". You can remove them by yourself." } @@ -7739,6 +7749,7 @@ Parameters: --config-home Specifies the home dir to save all the configurations. --useragent Specifies the user agent string. it will be saved for future use too. -m, --email Specifies the account email, only valid for the '--install' and '--update-account' command. + Multiple emails can be given as a comma-separated list: 'a@example.com,b@example.com' --accountkey Specifies the account key path, only valid for the '--install' command. --days Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days. Negative values could be used to specify a number of days relative to the expiration date of the certificate. diff --git a/acme.sh.completion b/acme.sh.completion new file mode 100644 index 00000000..26cb88da --- /dev/null +++ b/acme.sh.completion @@ -0,0 +1,341 @@ +# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh +# +# "acme.sh --install" copies this file to the acme.sh home dir and wires +# it into acme.sh.env, so the completion is loaded automatically in new +# bash sessions after installation. +# +# To use it without installing acme.sh, source it from ~/.bashrc, or copy +# it to /usr/share/bash-completion/completions/acme.sh +# +# Zsh users can load it with: +# autoload -U +X bashcompinit && bashcompinit +# . /path/to/acme.sh.completion + +# This file may also be sourced by non-bash shells via acme.sh.env, +# so silently do nothing if the "complete" builtin is not available. +if ! command -v complete >/dev/null 2>&1; then + return 0 2>/dev/null || exit 0 +fi + +# Add each word of $1 that starts with $cur to COMPREPLY. +# The words are read line by line, so that candidates like a wildcard +# domain "*.example.com" are never glob-expanded against the cwd. +_acme_sh_add_matches() { + local _word + while read -r _word; do + [ -n "$_word" ] || continue + case "$_word" in + "$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;; + esac + done </dev/null 2>&1; then + compopt -o filenames 2>/dev/null + fi + return 0 +} + +_acme_sh_dirs() { + local _dir + while IFS= read -r _dir; do + [ -n "$_dir" ] || continue + COMPREPLY=("${COMPREPLY[@]}" "$_dir") + done </dev/null 2>&1; then + compopt -o filenames 2>/dev/null + fi + return 0 +} + +# Complete the domains that already have a cert: every directory in the +# config home that contains a ".conf" file ("_ecc" suffix stripped). +_acme_sh_domains() { + local _dir _name _domains="" + [ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null + for _dir in "$_acme_conf_home"/*/; do + [ -d "$_dir" ] || continue + _name="${_dir%/}" + _name="${_name##*/}" + _name="${_name%_ecc}" + if [ -f "${_dir}${_name}.conf" ]; then + case " $_domains " in + *" $_name "*) ;; + *) _domains="$_domains $_name" ;; + esac + fi + done + _acme_sh_add_matches "$_domains" +} + +# Complete hook names from a subfolder of the acme.sh home dir. +# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty. +_acme_sh_hooks() { + local _file _hooks="" + [ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null + for _file in "$_acme_home/$1/$2"*.sh; do + [ -f "$_file" ] || continue + _file="${_file##*/}" + _hooks="$_hooks ${_file%.sh}" + done + _acme_sh_add_matches "$_hooks" +} + +_acme_sh_completion() { + local cur prev _acme_home _acme_conf_home + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="" + if [ "$COMP_CWORD" -gt 0 ]; then + prev="${COMP_WORDS[COMP_CWORD - 1]}" + fi + _acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}" + _acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}" + + # The first argument is the command. + if [ "$COMP_CWORD" -eq 1 ]; then + _acme_sh_add_matches " + --help + --version + --install + --install-online + --uninstall + --upgrade + --issue + --deploy + --sign-csr + --show-csr + --install-cert + --renew + --renew-all + --revoke + --remove + --list + --list-profiles + --info + --to-pkcs12 + --to-pkcs8 + --create-account-key + --create-domain-key + --create-csr + --deactivate + --update-account + --register-account + --deactivate-account + --make-dns-persist-value + --install-cronjob + --uninstall-cronjob + --cron + --set-notify + --set-default-ca + --set-default-chain + " + return 0 + fi + + # Complete the value of the previous option. + case "$prev" in + -d | --domain | --challenge-alias | --domain-alias) + _acme_sh_domains + return 0 + ;; + --dns) + # The dns hook argument is optional, keep completing options if the + # current word already looks like one. + case "$cur" in + -*) ;; + *) + _acme_sh_hooks "dnsapi" "dns_" + return 0 + ;; + esac + ;; + --deploy-hook) + _acme_sh_hooks "deploy" "" + return 0 + ;; + --notify-hook) + _acme_sh_hooks "notify" "" + return 0 + ;; + --server) + _acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis" + return 0 + ;; + -k | --keylength | -ak | --accountkeylength) + _acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521" + return 0 + ;; + --debug) + # Optional argument. + case "$cur" in + -*) ;; + *) + _acme_sh_add_matches "0 1 2 3" + return 0 + ;; + esac + ;; + --log) + # Optional argument. + case "$cur" in + -*) ;; + *) + _acme_sh_files + return 0 + ;; + esac + ;; + --nginx) + # Optional argument. + case "$cur" in + -*) ;; + *) + _acme_sh_files + return 0 + ;; + esac + ;; + --auto-upgrade | --always-force-new-domain-key) + # Optional argument. + case "$cur" in + -*) ;; + *) + _acme_sh_add_matches "0 1" + return 0 + ;; + esac + ;; + --log-level) + _acme_sh_add_matches "1 2" + return 0 + ;; + --syslog) + _acme_sh_add_matches "0 3 6 7" + return 0 + ;; + --notify-level) + _acme_sh_add_matches "0 1 2 3" + return 0 + ;; + --notify-mode) + _acme_sh_add_matches "0 1" + return 0 + ;; + --revoke-reason) + _acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10" + return 0 + ;; + --cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin) + _acme_sh_files + return 0 + ;; + -w | --webroot | --home | --cert-home | --config-home | --ca-path) + _acme_sh_dirs + return 0 + ;; + -m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days) + # These options take a free-form value, offer nothing. + return 0 + ;; + esac + + # Complete the parameters. + _acme_sh_add_matches " + --accountconf + --accountkey + --accountkeylength + --alpn + --always-force-new-domain-key + --apache + --auto-upgrade + --branch + --ca-bundle + --ca-file + --ca-path + --cert-file + --cert-home + --cert-profile + --challenge-alias + --config-home + --csr + --days + --debug + --deploy-hook + --dns + --dns-persist + --dns-persist-ca-name + --dns-persist-days + --dns-persist-wildcard + --dnssleep + --domain + --domain-alias + --eab-hmac-key + --eab-kid + --ecc + --email + --extended-key-usage + --force + --force-color + --fullchain-file + --home + --httpport + --insecure + --key-file + --keylength + --listen-v4 + --listen-v6 + --listraw + --local-address + --log + --log-level + --nginx + --no-color + --no-cron + --no-profile + --notify-hook + --notify-level + --notify-mode + --notify-source + --ocsp-must-staple + --openssl-bin + --output-insecure + --password + --post-hook + --pre-hook + --preferred-chain + --reloadcmd + --renew-hook + --revoke-reason + --server + --staging + --standalone + --stateless + --stop-renew-on-error + --syslog + --tlsport + --treat-skip-as-success + --use-wget + --useragent + --valid-from + --valid-to + --webroot + --yes-I-know-dns-manual-mode-enough-go-ahead-please + " + return 0 +} + +complete -F _acme_sh_completion acme.sh From 7b6d96387c7eae778ab3e630950d14f8a434785e Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 17:48:33 +0800 Subject: [PATCH 586/689] migrate the legacy ACMEDNS_UPDATE_URL from the account conf (#3899) --- dnsapi/dns_acmedns.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dnsapi/dns_acmedns.sh b/dnsapi/dns_acmedns.sh index b109a4e5..a21f8ef0 100755 --- a/dnsapi/dns_acmedns.sh +++ b/dnsapi/dns_acmedns.sh @@ -37,6 +37,16 @@ dns_acmedns_add() { ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}" ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}" + #for compatibility: old versions stored ACMEDNS_UPDATE_URL in the account + #conf (issue 3899). Do not clear it here: it must stay available for the + #other domains that have not migrated to their domain conf yet. + if [ -z "$ACMEDNS_BASE_URL" ]; then + _acmedns_update_url="$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)" + if [ "$_acmedns_update_url" ]; then + ACMEDNS_BASE_URL="$(echo "$_acmedns_update_url" | sed 's#/update$##')" + fi + fi + if [ "$ACMEDNS_BASE_URL" = "" ]; then ACMEDNS_BASE_URL="https://auth.acme-dns.io" fi From d3af3315da2a8d9a0080bc62c348e728db7feefc Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 18:02:34 +0800 Subject: [PATCH 587/689] dnsapi/dns_edgedns: use the system clock for the request timestamp (#3973) --- dnsapi/dns_edgedns.sh | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/dnsapi/dns_edgedns.sh b/dnsapi/dns_edgedns.sh index e88a1483..9ff1cc06 100755 --- a/dnsapi/dns_edgedns.sh +++ b/dnsapi/dns_edgedns.sh @@ -363,17 +363,12 @@ _edgedns_rest() { _edgedns_eg_timestamp() { _debug "Generating signature Timestamp" - _debug3 "Retriving ntp time" - _timeheaders="$(_get "https://www.ntp.org" "onlyheader")" - _debug3 "_timeheaders" "$_timeheaders" - _ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")" - _debug3 "_ntpdate" "$_ntpdate" - _ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')" - _debug3 "_NTPDATE" "$_ntpdate" - _ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")" - _debug3 "_ntptime" "$_ntptime" - _eg_timestamp=$(date -u "+%Y%m%dT") - _eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")" + #Akamai accepts a clock skew of +/-30s, so use the system clock directly. + #The previous code fetched the Date header from www.ntp.org, which is not + #a reliable time source (it served a wrong time for hours, issue 3973), + #cost an extra https round-trip for every API request, and combined the + #remote time of day with the LOCAL date, breaking around UTC midnight. + _eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")" _debug "_eg_timestamp" "$_eg_timestamp" } From 2a175f97e87890e47f87eb09198155cc5a94a668 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 5 Jul 2026 18:04:24 +0800 Subject: [PATCH 588/689] toPkcs8: support --password and re-export the pkcs8 file on renewal (#4134) --- acme.sh | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/acme.sh b/acme.sh index 4735b830..f09436b9 100755 --- a/acme.sh +++ b/acme.sh @@ -1547,6 +1547,22 @@ _toPkcs() { } +_toPkcs8() { + _cpkcs8="$1" + _ckey="$2" + pkcs8Password="$3" + + if [ "$pkcs8Password" ]; then + ${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -v2 aes256 -passout "pass:$pkcs8Password" -in "$_ckey" -out "$_cpkcs8" + else + ${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$_ckey" -out "$_cpkcs8" + fi + if [ "$?" = "0" ]; then + _savedomainconf "Le_PKCS8Password" "$pkcs8Password" "base64" + fi + +} + #domain [password] [isEcc] toPkcs() { domain="$1" @@ -1568,20 +1584,21 @@ toPkcs() { } -#domain [isEcc] +#domain [password] [isEcc] toPkcs8() { domain="$1" + pkcs8Password="$2" if [ -z "$domain" ]; then - _usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain [--ecc]" + _usage "Usage: $PROJECT_ENTRY --to-pkcs8 --domain [--password ] [--ecc]" return 1 fi - _isEcc="$2" + _isEcc="$3" _initpath "$domain" "$_isEcc" - ${ACME_OPENSSL_BIN:-openssl} pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$CERT_KEY_PATH" -out "$CERT_PKCS8_PATH" + _toPkcs8 "$CERT_PKCS8_PATH" "$CERT_KEY_PATH" "$pkcs8Password" if [ "$?" = "0" ]; then _info "Success, $CERT_PKCS8_PATH" @@ -5889,6 +5906,12 @@ $_authorizations_map" _toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword" fi + #convert to pkcs8 + Le_PKCS8Password="$(_readdomainconf Le_PKCS8Password)" + if [ "$Le_PKCS8Password" ]; then + _toPkcs8 "$CERT_PKCS8_PATH" "$CERT_KEY_PATH" "$Le_PKCS8Password" + fi + if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then _savedomainconf "Le_RealCertPath" "$_real_cert" _savedomainconf "Le_RealCACertPath" "$_real_ca" @@ -7801,7 +7824,7 @@ Parameters: --revoke-reason <0-10> The reason for revocation, can be used in conjunction with the '--revoke' command. See: $_REVOKE_WIKI - --password Add a password to exported pfx file. Use with --to-pkcs12. + --password Add a password to the exported pfx or pkcs8 file. Use with '--to-pkcs12' or '--to-pkcs8'. " @@ -8818,7 +8841,7 @@ _process() { toPkcs "$_domain" "$_password" "$_ecc" ;; toPkcs8) - toPkcs8 "$_domain" "$_ecc" + toPkcs8 "$_domain" "$_password" "$_ecc" ;; createAccountKey) createAccountKey "$_accountkeylength" From 8f3c1701f396887bf242e912dcbe3a50419af8c1 Mon Sep 17 00:00:00 2001 From: LaoDC Date: Sun, 5 Jul 2026 22:30:26 +0700 Subject: [PATCH 589/689] Add LaoDC DNS API (dns_laodc) (#6974) * Added LaoDC API Module * Cleaned up debug and info revised get subdomain to filter by TXT records. * Added commet to _get_root * Removed PATCH logic of updating acme records as this doesn't work for wildcard DNS. Revised rm() function to do explicit record matching. * Revised _get_root() to handle different scenarios. Fixed _laodc_api() function to check if query failed to run. added basic json sanitation to handle \ and " in $value unset _H2 _H3 after call as per request from copilot. * fixed indentation of case statement block * fixed condition checking. _get_root should start at 1 so full fqdn can be tested $? was being reference after export command failing response checks removed export txtvalue fixed docs link and issues link * Verify key for both add and rm * fixed dns alias condition check validate key returns 1 if failed. --------- Co-authored-by: neil Co-authored-by: LaoDC --- dnsapi/dns_laodc.sh | 197 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 dnsapi/dns_laodc.sh diff --git a/dnsapi/dns_laodc.sh b/dnsapi/dns_laodc.sh new file mode 100644 index 00000000..9f2103b3 --- /dev/null +++ b/dnsapi/dns_laodc.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_laodc_info='LaoDC DNS API Server +Site: laodc.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_laodc +Options: + LaoDC_Key API Key +Issues: github.com/acmesh-official/acme.sh/issues/6973 +Author: @laodc +' + +# Usage: +# export LaoDC_Key="your-api-key" +# acme.sh --issue --dns dns_laodc -d example.la -d *.example.la --dnssleep 120 +# +# The credentials will be saved in ~/.acme.sh/account.conf + +LAODC_VER="0.1.2" +LAODC_API_ENDPOINT="https://dns.laodc.com/v1" + +######## Public functions ##################### + +# Usage: dns_laodc_add _acme-challenge.example.la ZPXvna6tBhq7XQMH7_t2WC2sg0F-BdmtmmpUJiK6Ho +dns_laodc_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Using LaoDC DNS API" + + _laodc_validate_key || return 1 + + _debug "Checking root zone exists for [$fulldomain]" + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + _debug _domain_hash "$domain_hash" + + _info "Adding acme record" + if _laodc_api "POST" "$domain_hash" "$_sub_domain" "$txtvalue"; then + if [ "$_code" = "201" ]; then + _info "Added, OK" + return 0 + else + _err "Add TXT record error, invalid code. Code: $_code" + return 1 + fi + fi + + _err "Add TXT record error." + return 1 +} + +dns_laodc_rm() { + fulldomain=$1 + txtvalue=$2 + + _laodc_validate_key || return 1 + + _debug "Checking root zone exists for [$fulldomain]" + if ! _get_root "$fulldomain"; then + _err "Invalid domain" + return 1 + fi + + domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") + _debug _root_domain "$_domain" + _debug _sub_domain "$_sub_domain" + _debug _domain_hash "$domain_hash" + + _info "Deleting acme record" + if _laodc_api "DELETE" "$domain_hash" "$_sub_domain" "$txtvalue"; then + if [ "$_code" = "204" ]; then + _info "Deleted, OK" + return 0 + else + _err "Delete TXT record error, invalid code. Code: $_code" + return 1 + fi + fi + + _err "Delete TXT record error." + return 1 +} + +#################### Private functions below ################################## +# _acme-challenge.www.domain.com +# returns +# _domain=domain.com +# _sub_domain=www +_get_root() { + fqdn=$1 + p=1 + i=1 + + while true; do + h=$(printf "%s" "$fqdn" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + return 1 # not valid domain + fi + + # Check API if domain exists + if _laodc_api "GET" "$h"; then + if [ "$_code" = "200" ]; then + _domain="$h" + + # DNS alias mode - @ is alias for fqdn + _sub_domain=$(printf "%s" "$fqdn" | cut -d . -f 1-"$p") + if [ "$i" = "1" ]; then + _sub_domain="@" + fi + + return 0 + fi + fi + + p="$i" + i=$(_math "$i" + 1) + done + + return 1 +} + +_laodc_validate_key() { + LaoDC_Key="${LaoDC_Key:-$(_readaccountconf_mutable LaoDC_Key)}" + + if [ -z "$LaoDC_Key" ]; then + LaoDC_Key="" + _err "You didn't specify a LaoDC API Key yet." + _err "Please export LaoDC_Key and try again." + return 1 + fi + + # Save the api key to the account conf file. + _saveaccountconf_mutable LaoDC_Key "$LaoDC_Key" +} + +_laodc_api() { + method=$1 + domain=$2 + subdomain=$3 + value=$4 + + export _H1="Content-Type: application/json" + export _H2="User-Agent: acme.sh/$VER laodc-dns-acme-sh/$LAODC_VER" + export _H3="Authorization: Bearer $LaoDC_Key" + + case $method in + GET) + if [ -n "$subdomain" ]; then + response="$(_get "$LAODC_API_ENDPOINT/$domain/$subdomain?type=TXT")" + else + response="$(_get "$LAODC_API_ENDPOINT/$domain")" + fi + ;; + POST) + # Sanitize value input + value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') + data="{ \"type\": \"TXT\", \"value\": \"$value\", \"ttl\": \"60\" }" + response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "POST" "application/json")" + ;; + DELETE) + # Sanitize value input + value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g') + data="{ \"type\": \"TXT\", \"value\": \"$value\" }" + response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "DELETE" "application/json")" + ;; + esac + + _ret=$? + + # Unset immediately after request to prevent leaks + export _H1= + export _H2= + export _H3= + + if [ "$_ret" != "0" ]; then + _err "Error $domain" + return 1 + fi + + responseHeaders="$(cat "$HTTP_HEADER")" + + if echo "$responseHeaders" | grep -i "Content-Type: *application/json" >/dev/null 2>&1; then + response="$(echo "$response" | _json_decode | _normalizeJson)" + fi + + _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" + + _debug "http response code $_code" + _debug response "$response" + return 0 +} From 58423df3e82b181946c1d57f821f20405715c52d Mon Sep 17 00:00:00 2001 From: PM Extra Date: Sun, 5 Jul 2026 23:33:57 +0800 Subject: [PATCH 590/689] retry failed install and deploy on renew (#7083) * retry failed install and deploy on renew * fix notify level for renew retry failures --- acme.sh | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/acme.sh b/acme.sh index f09436b9..5b8f9566 100755 --- a/acme.sh +++ b/acme.sh @@ -6052,6 +6052,31 @@ renew() { fi if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then + _renew_retry_fixed="" + res="0" + _ensure_install "$Le_Domain" + res="$?" + if [ "$Le_DeployHook" ] && [ "$res" = "0" ]; then + _ensure_deploy "$Le_Domain" + res="$?" + fi + if [ "$res" != "0" ]; then + if [ -z "$_ACME_IN_RENEWALL" ]; then + if [ $_set_level -ge $NOTIFY_LEVEL_ERROR ]; then + _send_notify "Renew $Le_Domain error" "There is an error." "$NOTIFY_HOOK" 1 + fi + fi + return 1 + fi + if [ "$_renew_retry_fixed" ]; then + _info "Install/deploy retry succeeded, no renewal is needed." + if [ -z "$_ACME_IN_RENEWALL" ]; then + if [ $_set_level -ge $NOTIFY_LEVEL_RENEW ]; then + _send_notify "Renew $Le_Domain success" "Good, the cert install/deploy retry succeeded." "$NOTIFY_HOOK" 0 + fi + fi + return 0 + fi _info "Skipping. Next renewal time is: $(__green "$Le_NextRenewTimeStr")" _info "Add '$(__red '--force')' to force renewal." if [ -z "$_ACME_IN_RENEWALL" ]; then @@ -6485,6 +6510,45 @@ _deploy() { _info "$(__green Success)" fi done + + _deploy_success_time="$(_time)" + _savedomainconf "Le_DeploySuccessTime" "$_deploy_success_time" + _savedomainconf "Le_DeploySuccessTimeStr" "$(_time2str "$_deploy_success_time")" +} + +_ensure_deploy() { + _d="$1" + if [ -z "$Le_DeployHook" ]; then + return 0 + fi + if [ -z "$Le_CertCreateTime" ]; then + return 0 + fi + + _deploy_success_time="$(_readdomainconf Le_DeploySuccessTime)" + if [ -z "$_deploy_success_time" ]; then + _debug "Le_DeploySuccessTime is empty, skip deploy retry check." + return 0 + fi + case "$_deploy_success_time$Le_CertCreateTime" in + *[!0-9]*) + _debug "Le_DeploySuccessTime or Le_CertCreateTime is not a number, skip deploy retry check." + return 0 + ;; + esac + + if [ "$_deploy_success_time" -lt "$Le_CertCreateTime" ]; then + _info "The cert was created after the last successful deploy, retrying deploy hooks." + if _deploy "$_d" "$Le_DeployHook"; then + _info "Deploy retry succeeded." + _renew_retry_fixed=1 + return 0 + fi + _err "Deploy retry failed." + return 1 + fi + + return 0 } #domain hooks @@ -6644,9 +6708,54 @@ _installcert() { _info "$(__green "Reload successful")" else _err "Reload error for: $_main_domain" + return 1 fi fi + _installcert_success_time="$(_time)" + _savedomainconf "Le_InstallCertSuccessTime" "$_installcert_success_time" + _savedomainconf "Le_InstallCertSuccessTimeStr" "$(_time2str "$_installcert_success_time")" +} + +_ensure_install() { + _d="$1" + if [ -z "$Le_CertCreateTime" ]; then + return 0 + fi + + _real_cert="$(_readdomainconf Le_RealCertPath)" + _real_key="$(_readdomainconf Le_RealKeyPath)" + _real_ca="$(_readdomainconf Le_RealCACertPath)" + _reload_cmd="$(_readdomainconf Le_ReloadCmd)" + _real_fullchain="$(_readdomainconf Le_RealFullChainPath)" + if [ -z "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then + return 0 + fi + + _installcert_success_time="$(_readdomainconf Le_InstallCertSuccessTime)" + if [ -z "$_installcert_success_time" ]; then + _debug "Le_InstallCertSuccessTime is empty, skip install retry check." + return 0 + fi + case "$_installcert_success_time$Le_CertCreateTime" in + *[!0-9]*) + _debug "Le_InstallCertSuccessTime or Le_CertCreateTime is not a number, skip install retry check." + return 0 + ;; + esac + + if [ "$_installcert_success_time" -lt "$Le_CertCreateTime" ]; then + _info "The cert was created after the last successful install, retrying install cert." + if _installcert "$_d" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd"; then + _info "Install cert retry succeeded." + _renew_retry_fixed=1 + return 0 + fi + _err "Install cert retry failed." + return 1 + fi + + return 0 } __read_password() { From 1f778e6ef1c08d089413da8777359ef9ec87f9d3 Mon Sep 17 00:00:00 2001 From: Oliver Mueller Date: Mon, 6 Jul 2026 04:11:58 +0200 Subject: [PATCH 591/689] deploy/ssh: return non-zero when a server deployment fails (#6795) ssh_deploy() ignored the result of _ssh_deploy and always returned success, so a failed transfer to one (or all) of the servers in DEPLOY_SSH_SERVER was silently swallowed. Track the return code across the loop and return non-zero if any server failed, letting the caller handle notification. Co-authored-by: Claude Opus 4.8 (1M context) --- deploy/ssh.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 848380a5..82b0382c 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -170,10 +170,16 @@ ssh_deploy() { _info "Required commands batched and sent in single call to remote host" fi + _returnCode=0 _deploy_ssh_servers="$DEPLOY_SSH_SERVER" for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do - _ssh_deploy + if ! _ssh_deploy; then + # in case of an error, remember it, but keep going for the remaining servers + _returnCode=1 + fi done + + return $_returnCode } _ssh_deploy() { From ff9b969bdb04065f0ae6afae0449bbe5f02d3d9d Mon Sep 17 00:00:00 2001 From: "Simon V." <218359733+sim0n-v@users.noreply.github.com> Date: Mon, 6 Jul 2026 04:22:36 +0200 Subject: [PATCH 592/689] Add support for Account Key Rollover (#7080) * add wiki * feat: add support for account key rollover * Place --update-account-key next to --update-account * fix shfmt * fix shfmt * fix shfmt * Fix from review * fix shfmt * fix from review * fix review --------- Co-authored-by: neil --- acme.sh | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/acme.sh b/acme.sh index 5b8f9566..4f1c0336 100755 --- a/acme.sh +++ b/acme.sh @@ -4154,6 +4154,93 @@ updateaccount() { fi } +#Implement account key rollover +updateaccountkey() { + _length="$1" + _initpath + + if [ ! -f "$ACCOUNT_KEY_PATH" ]; then + _err "Account key not found at: $ACCOUNT_KEY_PATH" + return 1 + fi + ACCOUNT_KEY_PATH_NEW="$ACCOUNT_KEY_PATH.new" + + _accUri=$(_readcaconf "ACCOUNT_URL") + _debug _accUri "$_accUri" + + if [ -z "$_accUri" ]; then + _err "The account URL is empty, please run '--update-account' first to update the account info, then try again." + return 1 + fi + if ! _calcjwk "$ACCOUNT_KEY_PATH"; then + return 1 + fi + _inner_payload="{\"account\": \"$_accUri\", \"oldKey\": $jwk}" + + _initAPI + if [ -z "$ACME_KEY_CHANGE" ]; then + _err "Server does not expose keyChange url." + return 1 + fi + + _url="$ACME_KEY_CHANGE" + if _createkey "$_length" "$ACCOUNT_KEY_PATH_NEW"; then + _info "New account key creation OK." + else + _err "New account key creation error." + return 1 + fi + + if ! _calcjwk "$ACCOUNT_KEY_PATH_NEW"; then + rm -f "$ACCOUNT_KEY_PATH_NEW" + return 1 + fi + _inner_protected="{\"url\": \"${_url}$JWK_HEADERPLACE_PART2, \"jwk\": $jwk"'}' + _inner_protected64="$(printf "%s" "$_inner_protected" | _base64 | _url_replace)" + _inner_payload64="$(printf "%s" "$_inner_payload" | _base64 | _url_replace)" + if ! _inner_sig_t="$(printf "%s" "$_inner_protected64.$_inner_payload64" | _sign "$ACCOUNT_KEY_PATH_NEW" "sha256")"; then + _err "Sign request failed." + rm -f "$ACCOUNT_KEY_PATH_NEW" + return 1 + fi + _debug3 _inner_sig_t "$_inner_sig_t" + + _inner_sig="$(printf "%s" "$_inner_sig_t" | _url_replace)" + _debug3 _inner_sig "$_inner_sig" + + _body="{\"protected\": \"$_inner_protected64\", \"payload\": \"$_inner_payload64\", \"signature\": \"$_inner_sig\"}" + + if ! _send_signed_request "$_url" "$_body" "" "$ACCOUNT_KEY_PATH"; then + _err "Error rotating account key: $response." + rm -f "$ACCOUNT_KEY_PATH_NEW" + return 1 + fi + + if [ "$code" = '200' ]; then + echo "$response" >"$ACCOUNT_JSON_PATH" + mv -f "$ACCOUNT_KEY_PATH_NEW" "$ACCOUNT_KEY_PATH" + _info "Account key rotation success for $_accUri." + elif [ "$code" = "409" ]; then + _err "An existing account is using the new key" + rm -f "$ACCOUNT_KEY_PATH_NEW" + return 1 + else + _err "Account key rollover error: $response" + rm -f "$ACCOUNT_KEY_PATH_NEW" + return 1 + fi + + __CACHED_JWK_KEY_FILE="" + _calcjwk "$ACCOUNT_KEY_PATH" + + ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)" + _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" + + CA_KEY_HASH="$(__calcAccountKeyHash)" + _debug "Calc CA_KEY_HASH" "$CA_KEY_HASH" + _savecaconf CA_KEY_HASH "$CA_KEY_HASH" +} + #Implement deactivate account deactivateaccount() { _initpath @@ -7786,6 +7873,7 @@ Commands: -ccr, --create-csr Create CSR, professional use. --create-domain-key Create an domain private key, professional use. --update-account Update account info. + --update-account-key Rotate account key. --register-account Register account key. --deactivate-account Deactivate the account. --make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation @@ -8338,6 +8426,9 @@ _process() { --update-account | --updateaccount) _CMD="updateaccount" ;; + --update-account-key | --updateaccountkey) + _CMD="updateaccountkey" + ;; --register-account | --registeraccount) _CMD="registeraccount" ;; @@ -8931,6 +9022,9 @@ _process() { updateaccount) updateaccount ;; + updateaccountkey) + updateaccountkey "$_accountkeylength" + ;; deactivateaccount) deactivateaccount ;; From ca8ab7f8b5bdaa43b2ecb1cf4bc7d4af6bba1743 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 13:35:27 +0800 Subject: [PATCH 593/689] add wiki-guard workflow: auto-restore wiki pages deleted or renamed by non-maintainer --- .github/workflows/wiki-guard.yml | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/wiki-guard.yml diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml new file mode 100644 index 00000000..356ff58a --- /dev/null +++ b/.github/workflows/wiki-guard.yml @@ -0,0 +1,95 @@ +name: Restore Wiki Pages Deleted by Others + +# The gollum event only fires on page create/update, never on deletion, +# so deletions can only be caught by polling the wiki git history. + +on: + schedule: + - cron: "*/10 * * * *" + gollum: + workflow_dispatch: + +permissions: + contents: write + issues: write + +concurrency: + group: wiki-guard + cancel-in-progress: false + +jobs: + restore: + runs-on: ubuntu-latest + steps: + - name: Checkout wiki repository + uses: actions/checkout@v7 + with: + repository: ${{ github.repository }}.wiki + path: wiki + fetch-depth: 0 + + - name: Restore pages deleted by non-maintainer + id: restore + run: | + cd wiki + git config core.quotePath false + + # Any author email under this domain is the maintainer and may delete pages. + OWNER_DOMAIN="neilpang.com" + + : > ../restored.txt + + # Paths deleted within the recent window (rolling; the cron runs + # every 10 minutes, so 7 days gives ample overlap without + # resurrecting old deletions the maintainer already accepted). + # --no-renames makes a rename count as a deletion of the old path, + # so a page renamed by a non-maintainer is restored under its + # original name (the new name stays -- creating pages is allowed). + git log --since="7 days ago" --diff-filter=D --no-renames --name-only --format= \ + | sort -u \ + | while IFS= read -r f; do + if [ -z "$f" ] || [ -e "$f" ]; then + continue + fi + # The most recent commit (in full history) that deleted this path. + del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")" + if [ -z "$del" ]; then + continue + fi + ae="$(git show -s --format=%ae "$del" | tr 'A-Z' 'a-z')" + case "$ae" in + *@"$OWNER_DOMAIN") + continue + ;; + esac + an="$(git show -s --format=%an "$del")" + echo "Restoring: $f (deleted in $del by $an <$ae>)" + git checkout "$del^" -- "$f" + printf '%s\n' "- \`$f\` deleted in $del by $an <$ae>" >> ../restored.txt + done + + if [ -s ../restored.txt ]; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "Restore pages deleted by non-maintainer" + git push origin HEAD || { git pull --rebase && git push origin HEAD; } + { + echo "The following wiki pages were deleted or renamed by someone other than the maintainer and have been restored automatically:" + echo "" + cat ../restored.txt + echo "" + echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki" + } > ../restore-msg.txt + echo "restored=true" >> "$GITHUB_OUTPUT" + else + echo "No unauthorized deletions found." + echo "restored=false" >> "$GITHUB_OUTPUT" + fi + + - name: Create issue to notify Neilpang + if: steps.restore.outputs.restored == 'true' + uses: peter-evans/create-issue-from-file@v6 + with: + title: "Wiki pages restored after unauthorized deletion" + content-filepath: ./restore-msg.txt + assignees: Neilpang From 8585d9f4a7bc4a135df9e6f995388fadf76b3622 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 14:23:51 +0800 Subject: [PATCH 594/689] wiki-monitor: skip notification for the maintainer's own wiki changes --- .github/workflows/wiki-monitor.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 7e5d7ca3..349ef3f3 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -9,13 +9,14 @@ jobs: if: github.actor != 'neilpang' steps: - name: Checkout wiki repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: repository: ${{ github.repository }}.wiki path: wiki fetch-depth: 0 - name: Generate wiki change message + id: msg run: | actor="${{ github.actor }}" sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH") @@ -27,6 +28,17 @@ jobs: now="$(date '+%Y-%m-%d %H:%M:%S')" cd wiki + # Skip notification when the change was authored by the + # maintainer himself (any author email under neilpang.com), + # e.g. a direct git push to the wiki repository. + author_email=$(git show -s --format=%ae "$page_sha" 2>/dev/null | tr 'A-Z' 'a-z') + case "$author_email" in + *@neilpang.com) + echo "Change authored by maintainer ($author_email); skipping notification." + echo "notify=false" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1) if [ -n "$prev_sha" ]; then git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff @@ -44,13 +56,15 @@ jobs: echo "[Click here to Revert](${page_url}/_history)" echo "" echo "----" - echo "### diff:" + echo "### diff:" echo '```diff' cat wiki.diff echo '```' } > wiki-change-msg.txt + echo "notify=true" >> "$GITHUB_OUTPUT" - name: Create issue to notify Neilpang + if: steps.msg.outputs.notify == 'true' uses: peter-evans/create-issue-from-file@v6 with: title: "Wiki edited" From dc1b06006f244f75b4ab9f446191f4b8df3139dc Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 14:27:00 +0800 Subject: [PATCH 595/689] issue.yml: assign and label "Report bugs to" tracking issues instead of posting the upgrade boilerplate --- .github/workflows/issue.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index c659fce5..37e95eba 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -10,10 +10,27 @@ jobs: - uses: actions/github-script@v9 with: script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, + const issue = context.payload.issue; + if (issue.title.startsWith("Report bugs to")) { + // Tracking issue for a third-party dns/deploy/notify api: + // no upgrade boilerplate; assign it to the opener and label it. + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + assignees: [issue.user.login] + }); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + labels: ["3rd party api"] + }); + return; + } + await github.rest.issues.createComment({ + issue_number: issue.number, owner: context.repo.owner, repo: context.repo.repo, body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you." - }) \ No newline at end of file From 2b5a19d34a363597be753261f43b584448599df8 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 15:11:35 +0800 Subject: [PATCH 596/689] wiki-guard: trust repo/org members with write access in all rule checks --- .github/workflows/issue.yml | 46 +++++ .github/workflows/wiki-guard.yml | 293 +++++++++++++++++++++++++++---- 2 files changed, 301 insertions(+), 38 deletions(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 37e95eba..bd6dc9e8 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -2,6 +2,12 @@ name: "Update issues" on: issues: types: [opened] + pull_request_target: + types: [opened] + +permissions: + issues: write + pull-requests: write jobs: comment: @@ -10,6 +16,46 @@ jobs: - uses: actions/github-script@v9 with: script: | + const item = context.payload.issue || context.payload.pull_request; + + // Close on sight anything opened by a user on the wiki Blacklist + // page (maintained by the Wiki Guard workflow). + let blacklist = []; + try { + const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`); + if (res.ok) { + blacklist = (await res.text()).split("\n") + .filter(l => l.startsWith("- ")) + .map(l => l.slice(2).trim().toLowerCase()) + .filter(Boolean); + } + } catch (e) { + core.warning(`Failed to fetch the blacklist: ${e}`); + } + if (blacklist.includes(item.user.login.toLowerCase())) { + if (context.payload.pull_request) { + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: item.number, + state: "closed" + }); + } else { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: item.number, + state: "closed", + state_reason: "not_planned" + }); + } + return; + } + + if (context.payload.pull_request) { + return; + } + const issue = context.payload.issue; if (issue.title.startsWith("Report bugs to")) { // Tracking issue for a third-party dns/deploy/notify api: diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml index 356ff58a..a0199441 100644 --- a/.github/workflows/wiki-guard.yml +++ b/.github/workflows/wiki-guard.yml @@ -1,7 +1,19 @@ -name: Restore Wiki Pages Deleted by Others +name: Wiki Guard +# Rules enforced here: +# - Only the maintainer and write-access members may delete or rename wiki +# pages. Anyone else doing so gets blacklisted and the page restored to +# its last good revision. +# - Only the maintainer and write-access members may edit the Blacklist +# wiki page. Anyone else touching it gets blacklisted and the page +# reverted. +# - Any wiki change made by a blacklisted identity is reverted. +# A "good" revision is one authored by the maintainer, by this bot, or by +# a non-blacklisted user -- restoring from the deleted commit's parent is +# NOT safe, because vandals replace a page before destroying it and the +# parent would launder their version into a bot commit. # The gollum event only fires on page create/update, never on deletion, -# so deletions can only be caught by polling the wiki git history. +# so violations are caught by polling the wiki git history. on: schedule: @@ -18,7 +30,7 @@ concurrency: cancel-in-progress: false jobs: - restore: + guard: runs-on: ubuntu-latest steps: - name: Checkout wiki repository @@ -28,68 +40,273 @@ jobs: path: wiki fetch-depth: 0 - - name: Restore pages deleted by non-maintainer - id: restore + - name: Enforce wiki rules + id: guard + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + # Logins with write (push) access to the repository, including + # organization members -- they may delete/rename pages and edit + # the blacklist just like the maintainer. If the API call fails, + # the list stays empty and enforcement falls back to + # maintainer-only, which is the safe direction. + gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \ + -q '.[] | select(.permissions.push) | .login' 2>/dev/null \ + | tr 'A-Z' 'a-z' | sort -u > writers.txt || true cd wiki git config core.quotePath false - # Any author email under this domain is the maintainer and may delete pages. + # Any author email under this domain is the maintainer. OWNER_DOMAIN="neilpang.com" + # Our own enforcement commits. + BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com" + BL_PAGE="Blacklist.md" + # Rolling window; the cron runs every 10 minutes, so 7 days gives + # ample overlap without re-judging old changes the maintainer + # already accepted. + WINDOW="7 days ago" - : > ../restored.txt + : > ../actions.txt + : > ../bl_new.txt - # Paths deleted within the recent window (rolling; the cron runs - # every 10 minutes, so 7 days gives ample overlap without - # resurrecting old deletions the maintainer already accepted). - # --no-renames makes a rename count as a deletion of the old path, - # so a page renamed by a non-maintainer is restored under its - # original name (the new name stays -- creating pages is allowed). - git log --since="7 days ago" --diff-filter=D --no-renames --name-only --format= \ + is_owner() { + case "$1" in + *@"$OWNER_DOMAIN") return 0 ;; + esac + return 1 + } + + is_bot() { + [ "$1" = "$BOT_EMAIL" ] + } + + author_email() { + git show -s --format=%ae "$1" | tr 'A-Z' 'a-z' + } + + # Identity of a commit author: the GitHub login when the email is a + # users.noreply.github.com address, otherwise the email itself. + identity_of() { + case "$1" in + *+*@users.noreply.github.com) + printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//' + ;; + *@users.noreply.github.com) + printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//' + ;; + *) + printf '%s\n' "$1" + ;; + esac + } + + is_blacklisted() { + grep -Fxq "$1" ../bl_all.txt + } + + # Trusted committers: the maintainer (by email domain), this bot, + # and anyone whose GitHub login has write access to the repo. + is_trusted() { + if is_owner "$1" || is_bot "$1"; then + return 0 + fi + grep -Fxq "$(identity_of "$1")" ../writers.txt + } + + # Newest commit on file $1 authored by a non-blacklisted user. + last_good_for() { + for g in $(git log --format=%H --no-renames -- "$1"); do + gae="$(author_email "$g")" + if is_trusted "$gae"; then + printf '%s\n' "$g" + return 0 + fi + gid="$(identity_of "$gae")" + if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then + printf '%s\n' "$g" + return 0 + fi + done + return 0 + } + + if [ -e "$BL_PAGE" ]; then + page_existed=1 + else + page_existed="" + fi + + # ---- 1. Last good version of the blacklist page: the newest + # revision authored by the maintainer or by this bot. Everything + # else on that page is tampering and is discarded. + bl_good_commit="" + for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do + ae="$(author_email "$c")" + if is_trusted "$ae"; then + bl_good_commit="$c" + break + fi + done + if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then + git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt + else + { + echo "# Blacklist" + echo "" + echo "Users listed below violated the wiki rules (deleted or renamed" + echo "pages, or tampered with this page). Their new issues and pull" + echo "requests are closed on sight and their wiki edits are reverted" + echo "automatically. Only the maintainer and write-access members" + echo "may edit this page." + echo "" + echo "To pardon a user while their violation is still inside the" + echo "scan window, replace their entry with: pardon: username" + echo "" + } > ../bl_page.txt + fi + sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt + sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt + + bl_add() { + if grep -Fxq "$1" ../bl_pardon.txt; then + return 0 + fi + if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then + printf '%s\n' "$1" >> ../bl_new.txt + printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt + fi + } + + # ---- 2. Blacklist everyone who deleted or renamed a page. + # --no-renames makes a rename count as a deletion of the old path. + for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do + ae="$(author_email "$c")" + if is_trusted "$ae"; then + continue + fi + an="$(git show -s --format=%an "$c")" + bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)" + done + + # ---- 3. Blacklist everyone else who touched the blacklist page. + # The revert of their tampering falls out of steps 5 and 6. + for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do + ae="$(author_email "$c")" + if is_trusted "$ae"; then + continue + fi + an="$(git show -s --format=%an "$c")" + bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)" + done + + sort -u ../bl_new.txt > ../bl_new_u.txt + cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt + + # ---- 4. Restore pages that are currently missing because a + # non-maintainer deleted them, using the last good revision. + git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \ | sort -u \ | while IFS= read -r f; do - if [ -z "$f" ] || [ -e "$f" ]; then + if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then continue fi - # The most recent commit (in full history) that deleted this path. del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")" if [ -z "$del" ]; then continue fi - ae="$(git show -s --format=%ae "$del" | tr 'A-Z' 'a-z')" - case "$ae" in - *@"$OWNER_DOMAIN") - continue - ;; - esac - an="$(git show -s --format=%an "$del")" - echo "Restoring: $f (deleted in $del by $an <$ae>)" - git checkout "$del^" -- "$f" - printf '%s\n' "- \`$f\` deleted in $del by $an <$ae>" >> ../restored.txt + ae="$(author_email "$del")" + if is_trusted "$ae"; then + continue + fi + good="$(last_good_for "$f")" + if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then + git checkout "$good" -- "$f" + printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt + fi done - if [ -s ../restored.txt ]; then + # ---- 5. Revert every recent change made by a blacklisted + # identity: each touched file goes back to its newest revision + # authored by a non-blacklisted user; a file that has no such + # revision (they created it) is removed. + if [ -s ../bl_all.txt ]; then + for c in $(git log --since="$WINDOW" --format=%H --no-renames); do + ae="$(author_email "$c")" + if is_trusted "$ae"; then + continue + fi + id="$(identity_of "$ae")" + if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then + continue + fi + git show --name-only --no-renames --format= "$c" \ + | while IFS= read -r f; do + if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then + continue + fi + good="$(last_good_for "$f")" + if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then + want="$(git rev-parse "$good:$f")" + have="$(git hash-object -- "$f" 2>/dev/null || echo missing)" + if [ "$want" != "$have" ]; then + git checkout "$good" -- "$f" + printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt + fi + elif [ -e "$f" ]; then + git rm -q -- "$f" + printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt + fi + done + done + fi + + # ---- 6. Regenerate the blacklist page: the last good text plus + # any newly blacklisted identities. This both reverts tampering + # and records new violators; manual edits by the maintainer are + # preserved as the new good text. + cp ../bl_page.txt ../bl_page_new.txt + if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then + echo >> ../bl_page_new.txt + fi + while IFS= read -r id; do + if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then + printf -- '- %s\n' "$id" >> ../bl_page_new.txt + fi + done < ../bl_new_u.txt + if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then + cp ../bl_page_new.txt "$BL_PAGE" + git add -- "$BL_PAGE" + if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then + printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt + fi + fi + + # ---- 7. Commit, push, notify. + if [ -n "$(git status --porcelain)" ]; then git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git commit -m "Restore pages deleted by non-maintainer" + git config user.email "$BOT_EMAIL" + git commit -m "wiki-guard: restore pages and enforce blacklist" git push origin HEAD || { git pull --rebase && git push origin HEAD; } + fi + if [ -s ../actions.txt ]; then { - echo "The following wiki pages were deleted or renamed by someone other than the maintainer and have been restored automatically:" + echo "The wiki guard handled the following rule violations:" echo "" - cat ../restored.txt + cat ../actions.txt echo "" + echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist" echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki" - } > ../restore-msg.txt - echo "restored=true" >> "$GITHUB_OUTPUT" + } > ../guard-msg.txt + echo "acted=true" >> "$GITHUB_OUTPUT" else - echo "No unauthorized deletions found." - echo "restored=false" >> "$GITHUB_OUTPUT" + echo "No rule violations found." + echo "acted=false" >> "$GITHUB_OUTPUT" fi - name: Create issue to notify Neilpang - if: steps.restore.outputs.restored == 'true' + if: steps.guard.outputs.acted == 'true' uses: peter-evans/create-issue-from-file@v6 with: - title: "Wiki pages restored after unauthorized deletion" - content-filepath: ./restore-msg.txt + title: "Wiki guard: rule violations handled" + content-filepath: ./guard-msg.txt assignees: Neilpang From e4eaa590639d8590f0fe3016f67bf7f85dbc511e Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 15:23:45 +0800 Subject: [PATCH 597/689] wiki-guard: log the number of write-access members loaded --- .github/workflows/wiki-guard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml index a0199441..45033554 100644 --- a/.github/workflows/wiki-guard.yml +++ b/.github/workflows/wiki-guard.yml @@ -53,6 +53,7 @@ jobs: gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \ -q '.[] | select(.permissions.push) | .login' 2>/dev/null \ | tr 'A-Z' 'a-z' | sort -u > writers.txt || true + echo "write-access members loaded: $(wc -l < writers.txt)" cd wiki git config core.quotePath false From 919492df1347127f14f92e4732d9b0d862a1a0ba Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 16:27:26 +0800 Subject: [PATCH 598/689] wiki-guard: use WIKI_GUARD_TOKEN (PAT with read:org) to enumerate org write members --- .github/workflows/wiki-guard.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml index 45033554..6c392d37 100644 --- a/.github/workflows/wiki-guard.yml +++ b/.github/workflows/wiki-guard.yml @@ -43,7 +43,10 @@ jobs: - name: Enforce wiki rules id: guard env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # WIKI_GUARD_TOKEN: a PAT with read:org, needed to enumerate + # members whose write access comes via the organization -- the + # repo-scoped GITHUB_TOKEN only sees direct collaborators. + GH_TOKEN: ${{ secrets.WIKI_GUARD_TOKEN || secrets.GITHUB_TOKEN }} run: | # Logins with write (push) access to the repository, including # organization members -- they may delete/rename pages and edit @@ -54,6 +57,7 @@ jobs: -q '.[] | select(.permissions.push) | .login' 2>/dev/null \ | tr 'A-Z' 'a-z' | sort -u > writers.txt || true echo "write-access members loaded: $(wc -l < writers.txt)" + cat writers.txt cd wiki git config core.quotePath false From 73df21abc4d81e2e98fd5d67a8284bbee54368fc Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 16:30:41 +0800 Subject: [PATCH 599/689] clean --- .github/workflows/wiki-guard.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml index 6c392d37..3c21f492 100644 --- a/.github/workflows/wiki-guard.yml +++ b/.github/workflows/wiki-guard.yml @@ -57,7 +57,6 @@ jobs: -q '.[] | select(.permissions.push) | .login' 2>/dev/null \ | tr 'A-Z' 'a-z' | sort -u > writers.txt || true echo "write-access members loaded: $(wc -l < writers.txt)" - cat writers.txt cd wiki git config core.quotePath false From f2b37b32ff6feec7228fbfbbe988056e6f26dac2 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 16:36:16 +0800 Subject: [PATCH 600/689] add more events --- .github/workflows/wiki-guard.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml index 3c21f492..709fb8ea 100644 --- a/.github/workflows/wiki-guard.yml +++ b/.github/workflows/wiki-guard.yml @@ -19,6 +19,12 @@ on: schedule: - cron: "*/10 * * * *" gollum: + # Piggyback on frequent repo activity, because the cron schedule is + # best-effort and often delayed well beyond its interval. + issues: + types: [opened] + issue_comment: + types: [created] workflow_dispatch: permissions: From cc64a732308bb0f5cf5c573a0539f3db1ba4f4b0 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 6 Jul 2026 17:39:56 +0800 Subject: [PATCH 601/689] one-click revert and ban --- .github/workflows/blacklist-command.yml | 113 ++++++++++++++++++++++++ .github/workflows/issue.yml | 4 + .github/workflows/revert-command.yml | 109 +++++++++++++++++++++++ .github/workflows/wiki-monitor.yml | 4 +- 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/blacklist-command.yml create mode 100644 .github/workflows/revert-command.yml diff --git a/.github/workflows/blacklist-command.yml b/.github/workflows/blacklist-command.yml new file mode 100644 index 00000000..4d0ff5f3 --- /dev/null +++ b/.github/workflows/blacklist-command.yml @@ -0,0 +1,113 @@ +name: Blacklist Command + +# An issue titled "blacklist: " opened by the maintainer +# or a write-access member adds that identity to the Blacklist wiki page +# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification +# embeds a prefilled link that opens such an issue in one click. + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +# Share the wiki-guard concurrency group so we never push to the wiki +# at the same time as the guard. +concurrency: + group: wiki-guard + cancel-in-progress: false + +jobs: + blacklist: + if: startsWith(github.event.issue.title, 'blacklist:') + runs-on: ubuntu-latest + steps: + - name: Check authorization + id: auth + run: | + assoc="${{ github.event.issue.author_association }}" + case "$assoc" in + OWNER|MEMBER|COLLABORATOR) + echo "ok=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "issue author is not authorized ($assoc); ignoring" + echo "ok=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Checkout wiki repository + if: steps.auth.outputs.ok == 'true' + uses: actions/checkout@v7 + with: + repository: ${{ github.repository }}.wiki + path: wiki + + - name: Add the identity to the blacklist page + if: steps.auth.outputs.ok == 'true' + id: add + env: + TITLE: ${{ github.event.issue.title }} + run: | + target="$(printf '%s' "$TITLE" \ + | sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \ + | tr 'A-Z' 'a-z')" + case "$target" in + ''|*[!a-z0-9._+@-]*) + echo "invalid target: '$target'" + echo "result=invalid" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac + echo "target=$target" >> "$GITHUB_OUTPUT" + cd wiki + if [ ! -e Blacklist.md ]; then + echo "result=nopage" >> "$GITHUB_OUTPUT" + exit 0 + fi + if grep -Fxiq -- "- $target" Blacklist.md; then + echo "result=already" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ -n "$(tail -c1 Blacklist.md)" ]; then + echo >> Blacklist.md + fi + printf -- '- %s\n' "$target" >> Blacklist.md + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Blacklist.md + git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})" + git push origin HEAD || { git pull --rebase && git push origin HEAD; } + echo "result=added" >> "$GITHUB_OUTPUT" + + - name: Reply and close + if: steps.auth.outputs.ok == 'true' + uses: actions/github-script@v9 + env: + RESULT: ${{ steps.add.outputs.result }} + TARGET: ${{ steps.add.outputs.target }} + with: + script: | + const result = process.env.RESULT; + const target = process.env.TARGET; + const messages = { + added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`, + already: `\`${target}\` is already on the blacklist.`, + invalid: "Could not parse a valid login or email from the issue title.", + nopage: "The Blacklist wiki page does not exist." + }; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: messages[result] || "No action taken." + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: "closed", + state_reason: result === "added" ? "completed" : "not_planned" + }); diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index bd6dc9e8..a72c18ab 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -57,6 +57,10 @@ jobs: } const issue = context.payload.issue; + if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) { + // Handled by the Blacklist / Revert Command workflows. + return; + } if (issue.title.startsWith("Report bugs to")) { // Tracking issue for a third-party dns/deploy/notify api: // no upgrade boilerplate; assign it to the opener and label it. diff --git a/.github/workflows/revert-command.yml b/.github/workflows/revert-command.yml new file mode 100644 index 00000000..7a792bf4 --- /dev/null +++ b/.github/workflows/revert-command.yml @@ -0,0 +1,109 @@ +name: Revert Command + +# An issue titled "revert: " opened by the maintainer or +# a write-access member reverts that commit in the wiki repository and +# closes the issue. The wiki-monitor notification embeds a prefilled link +# that opens such an issue in one click. + +on: + issues: + types: [opened] + +permissions: + contents: write + issues: write + +# Share the wiki-guard concurrency group so we never push to the wiki +# at the same time as the guard. +concurrency: + group: wiki-guard + cancel-in-progress: false + +jobs: + revert: + if: startsWith(github.event.issue.title, 'revert:') + runs-on: ubuntu-latest + steps: + - name: Check authorization + id: auth + run: | + assoc="${{ github.event.issue.author_association }}" + case "$assoc" in + OWNER|MEMBER|COLLABORATOR) + echo "ok=true" >> "$GITHUB_OUTPUT" + ;; + *) + echo "issue author is not authorized ($assoc); ignoring" + echo "ok=false" >> "$GITHUB_OUTPUT" + ;; + esac + + - name: Checkout wiki repository + if: steps.auth.outputs.ok == 'true' + uses: actions/checkout@v7 + with: + repository: ${{ github.repository }}.wiki + path: wiki + fetch-depth: 0 + + - name: Revert the wiki commit + if: steps.auth.outputs.ok == 'true' + id: revert + env: + TITLE: ${{ github.event.issue.title }} + run: | + target="$(printf '%s' "$TITLE" \ + | sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \ + | tr 'A-Z' 'a-z')" + case "$target" in + *[!0-9a-f]*|"") + echo "invalid commit sha: '$target'" + echo "result=invalid" >> "$GITHUB_OUTPUT" + exit 0 + ;; + esac + echo "target=$target" >> "$GITHUB_OUTPUT" + cd wiki + if ! git cat-file -e "$target^{commit}" 2>/dev/null; then + echo "result=notfound" >> "$GITHUB_OUTPUT" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + if git revert --no-edit "$target"; then + git push origin HEAD || { git pull --rebase && git push origin HEAD; } + echo "result=reverted" >> "$GITHUB_OUTPUT" + else + git revert --abort || true + echo "result=conflict" >> "$GITHUB_OUTPUT" + fi + + - name: Reply and close + if: steps.auth.outputs.ok == 'true' + uses: actions/github-script@v9 + env: + RESULT: ${{ steps.revert.outputs.result }} + TARGET: ${{ steps.revert.outputs.target }} + with: + script: | + const result = process.env.RESULT; + const target = process.env.TARGET; + const messages = { + reverted: `Wiki commit \`${target}\` has been reverted.`, + conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`, + notfound: `Commit \`${target}\` was not found in the wiki repository.`, + invalid: "Could not parse a commit sha from the issue title." + }; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: messages[result] || "No action taken." + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + state: "closed", + state_reason: result === "reverted" ? "completed" : "not_planned" + }); diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 349ef3f3..89bb1f3a 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -53,7 +53,9 @@ jobs: echo "Time: $now" echo "Page: [$page_name]($page_url) (Action: $page_action)" echo "Comment: $page_summary" - echo "[Click here to Revert](${page_url}/_history)" + echo "[Click here to Revert](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)" + echo "" + echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)" echo "" echo "----" echo "### diff:" From b1b539695f8e6337421bb65a5a76afb758c8c2ff Mon Sep 17 00:00:00 2001 From: Marvo2011 Date: Mon, 6 Jul 2026 15:30:13 +0200 Subject: [PATCH 602/689] Merge pull request #7026 from Marvo2011/dev Update SelfHost DNS provider --- dnsapi/dns_selfhost.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_selfhost.sh b/dnsapi/dns_selfhost.sh index 40cc0210..782a5d5f 100644 --- a/dnsapi/dns_selfhost.sh +++ b/dnsapi/dns_selfhost.sh @@ -7,6 +7,7 @@ Options: SELFHOSTDNS_USERNAME Username SELFHOSTDNS_PASSWORD Password SELFHOSTDNS_MAP Subdomain name + SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://account.selfhost.de/cgi-bin/api.pl" Issues: github.com/acmesh-official/acme.sh/issues/4291 Author: Marvin Edeler ' @@ -18,9 +19,11 @@ dns_selfhost_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txt" - SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl" + DEFAULT_SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl" # Get values, but don't save until we successfully validated + SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}" + SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}" SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}" SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}" # These values are domain dependent, so read them from there @@ -84,6 +87,11 @@ dns_selfhost_add() { fi fi + # Save api url if different from default + if [ "$DEFAULT_SELFHOSTDNS_UPDATE_URL" != "$SELFHOSTDNS_UPDATE_URL" ]; then + _saveaccountconf_mutable SELFHOSTDNS_UPDATE_URL "$SELFHOSTDNS_UPDATE_URL" + fi + # Now that we know the values are good, save them _saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME" _saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD" From ca118be754d59ecdfcc5f298d156dcd7fa93a1c3 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 10:02:45 +0800 Subject: [PATCH 603/689] issue.yml: match tracking issue title variants --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index a72c18ab..f72a1f06 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -61,7 +61,7 @@ jobs: // Handled by the Blacklist / Revert Command workflows. return; } - if (issue.title.startsWith("Report bugs to")) { + if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) { // Tracking issue for a third-party dns/deploy/notify api: // no upgrade boilerplate; assign it to the opener and label it. await github.rest.issues.addAssignees({ From cf3eab95ee85fb38fa8e62e1f4c9b35eee14c789 Mon Sep 17 00:00:00 2001 From: lwohn-creo Date: Fri, 10 Jul 2026 04:07:12 +0200 Subject: [PATCH 604/689] Add creoline API as DNS provider (#7100) * New Banner Updated README to include responsive images for dark and light modes. * acme-sh-creoline-as-dns-provider * acme-sh-creoline-as-dns-provider - Review changes implemented according code review * acme-sh-creoline-as-dns-provider - Review changes implemented according second code review, minding --cron * acme-sh-creoline-as-dns-provider - Remove debug code * acme-sh-creoline-as-dns-provider - shfmt formatting according Code of conduct --------- Co-authored-by: neil Co-authored-by: ZeroSSL-Andreas Co-authored-by: Steven Kauschke --- dnsapi/dns_creoline.sh | 181 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 dnsapi/dns_creoline.sh diff --git a/dnsapi/dns_creoline.sh b/dnsapi/dns_creoline.sh new file mode 100644 index 00000000..9d04af4f --- /dev/null +++ b/dnsapi/dns_creoline.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_creoline_info='creoline +Site: https://www.creoline.com/de +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_creoline +Help: https://help.creoline.com +Options: + creolineApiToken + creolineApiSecret +Issues: github.com/acmesh-official/acme.sh/issues/7103 +' + +creolineApi="https://api.creoline.com/v1" + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPB8" +dns_creoline_add() { + fulldomain=$1 + txtvalue=$2 + + creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}" + creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}" + + if [ -z "$creolineApiToken" ] || [ -z "$creolineApiSecret" ]; then + _err "Error required creoline API Token or creoline API Secret not specified." + _err "Please set it with the Command 'export creolineApiToken=' and 'export creolineApiSecret='." + return 1 + else + _saveaccountconf_mutable creolineApiToken "$creolineApiToken" + _saveaccountconf_mutable creolineApiSecret "$creolineApiSecret" + fi + + _debug "Detecting the root dns zone." + if ! _get_root "$fulldomain"; then + _err "Error on detecting the root dns zone." + return 1 + fi + + _info "Adding record" + if _creoline_rest POST "dns/zone/$_domain/record" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"record\":\"$txtvalue\",\"ttl\":\"60\"}"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 +} + +#fulldomain txtvalue +dns_creoline_rm() { + fulldomain=$1 + txtvalue=$2 + + creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}" + creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}" + + _debug "Detecting the root dns zone." + if ! _get_root "$fulldomain"; then + _err "Error on detecting the root dns zone." + return 1 + fi + + _info "Getting earlier created txt record." + if ! _creoline_rest GET "dns/zone/$_domain/record/type/TXT/record/$txtvalue"; then + if _contains "$response" "errors" || _contains "$response" "message"; then + _err "Error on getting earlier created txt record." + return 1 + fi + _err "Error on getting earlier created txt record." + return 1 + fi + + record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _debug "record_id" "$record_id" + + if [ -z "$record_id" ]; then + _err "Error on deleting earlier created txt record. No record id found in response." + return 1 + fi + + _info "Deleting earlier created txt record." + if ! _creoline_rest DELETE "dns/zone/$_domain/record/$record_id"; then + if _contains "$response" "errors" || _contains "$response" "message"; then + _err "Error on deleting earlier created txt record." + return 1 + fi + _err "Error on deleting earlier created txt record." + return 1 + fi + + _info "Deleted, OK" + return 0 +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + if ! _creoline_rest GET "dns/zone/root/$domain"; then + return 1 + fi + + _sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _debug _sub_domain "$_sub_domain" + + _domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _debug _domain "$_domain" + + if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then + return 1 + fi +} + +_creoline_rest() { + method=$1 + uri="$2" + data="$3" + timestamp=$(_time) + canonical_request="${timestamp}.${creolineApi}/${uri}" + signature_hash=$(printf "%s" "$canonical_request" | _hmac sha256 "$(printf "%s" "$creolineApiSecret" | _hex_dump | tr -d " ")" hex) + + _debug method "$method" + _debug uri "$uri" + _debug data "$data" + + _debug2 timestamp "$timestamp" + _debug2 canonical_request "$canonical_request" + _debug2 signature_hash "$signature_hash" + + token_trimmed=$(echo "$creolineApiToken" | tr -d '"') + hmac_trimmed=$(echo "$signature_hash" | tr -d '"') + + export _H1="Content-Type: application/json" + + if [ "$token_trimmed" ]; then + export _H2="X-Api-Token: $token_trimmed" + fi + + if [ "$hmac_trimmed" ]; then + export _H3="X-Creoline-Api-Signature: $hmac_trimmed" + fi + + if [ "$timestamp" ]; then + export _H4="X-Creoline-Api-Timestamp: $timestamp" + fi + + if [ "$method" != "GET" ]; then + response="$(_post "$data" "$creolineApi/$uri" "" "$method")" + else + response="$(_get "$creolineApi/$uri")" + fi + + if [ "$?" != "0" ]; then + _err "error $uri" + return 1 + fi + + _debug response "$response" + + if _contains "$response" "errors"; then + error=$(echo "$response" | _egrep_o "\"errors\":[[]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | tr -d "[") + _err "Error: $error" + _err "URI:$uri" + return 1 + elif _contains "$response" "message"; then + message=$(echo "$response" | _egrep_o "\"message\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \") + _err "Error: $message" + _err "URI:$uri" + return 1 + fi + + return 0 +} From 1324dcd4720274ac328165a54a06f0055b4e93e4 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:10:44 -0700 Subject: [PATCH 605/689] Docker: update crontab used (#7111) Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 55a9cc67..229e4830 100644 --- a/Dockerfile +++ b/Dockerfile @@ -81,8 +81,8 @@ if [ \"\$1\" = \"daemon\" ]; then \n \ echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \ time=\$(date -u \"+%s\") \n \ random_minute=\$((\$time % 60)) \n \ - random_hour=\$((\$time / 60 % 24)) \n \ - echo \"\$random_minute \$random_hour * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \ + random_hour=\$((\$time / 60 % 6)) \n \ + echo \"\$random_minute \$random_hour,\$((\$random_hour + 6)),\$((\$random_hour + 12)),\$((\$random_hour + 18)) * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \ fi \n \ echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \ exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \ From bed15ba8447e93c3052442900955894d79d8daf7 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 10:23:45 +0800 Subject: [PATCH 606/689] dns_freedns.sh: use grep -E, BRE \| alternation is a GNU extension OpenBSD grep treats \| in a BRE as a literal | character, so _freedns_domain_id never matched any row and every domain lookup failed with "Domain not found". Switch to ERE with -E, keeping the parens escaped so the (.*) suffix branch still requires literal parentheses and does not widen the match (e.g. searching example.com must not match example.company). Reported-by: @katiekloss @boretom Ref: https://github.com/acmesh-official/acme.sh/issues/2305 --- dnsapi/dns_freedns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_freedns.sh b/dnsapi/dns_freedns.sh index 13d9f68b..8ea86c24 100755 --- a/dnsapi/dns_freedns.sh +++ b/dnsapi/dns_freedns.sh @@ -305,7 +305,7 @@ _freedns_domain_id() { fi domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' | - grep "$search_domain\|$search_domain(.*)" | + grep -E "$search_domain|$search_domain\(.*\)" | sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' | cut -d = -f 2)" # The above beauty extracts domain ID from the html page... From 83b52e0cd77e3fd189e1508dde9da08b9f50b94f Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 10:38:52 +0800 Subject: [PATCH 607/689] notify/smtp.sh: add --crlf so curl sends CRLF line endings Postfix with smtpd_forbid_bare_newline (default hardening since 3.9, after SMTP smuggling) rejects the message with "521 5.5.2 Error: bare received". RFC 5321 requires CRLF. The python sender is unaffected (smtplib already emits CRLF). fix https://github.com/acmesh-official/acme.sh/issues/7104 --- notify/smtp.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/notify/smtp.sh b/notify/smtp.sh index f5ebebca..a7318692 100644 --- a/notify/smtp.sh +++ b/notify/smtp.sh @@ -200,6 +200,7 @@ _smtp_send_curl() { set -- "$@" \ --upload-file - \ + --crlf \ --mail-from "$SMTP_FROM" \ --max-time "$SMTP_TIMEOUT" From 45c0ad4112557599d5dd1725c7c6048efa563fd1 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 11:05:40 +0800 Subject: [PATCH 608/689] Add _cleardeployconf to clear deploy hook keys from domain conf Mirrors _clearaccountconf_mutable: clears the SAVED_ prefixed key and the legacy unprefixed key. Replaces the local copy in synology_dsm.sh and the direct _cleardomainconf call in multideploy.sh. Closes #4722. Thanks to @sg1888. --- acme.sh | 7 +++++++ deploy/multideploy.sh | 2 +- deploy/synology_dsm.sh | 5 ----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/acme.sh b/acme.sh index 4f1c0336..b072c6cf 100755 --- a/acme.sh +++ b/acme.sh @@ -2594,6 +2594,13 @@ _savedeployconf() { _cleardomainconf "$1" } +#key +_cleardeployconf() { + _cleardomainconf "SAVED_$1" + #remove later + _cleardomainconf "$1" +} + #key _getdeployconf() { _rac_key="$1" diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index ef920f64..375668ec 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -210,7 +210,7 @@ _clear_envs() { echo "$env_pairs" | while IFS='=' read -r _key _value; do _debug3 "Deleting key" "$_key" - _cleardomainconf "SAVED_$_key" + _cleardeployconf "$_key" unset -v "$_key" done } diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 502bc59b..202e8188 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -424,11 +424,6 @@ _temp_admin_cleanup() { fi } -#_cleardeployconf key -_cleardeployconf() { - _cleardomainconf "SAVED_$1" -} - # key _check2cleardeployconfexp() { _key="$1" From 534a1714dcbb79699407a4f9e0f2137ca92b1eb7 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 11:20:08 +0800 Subject: [PATCH 609/689] dns_me.sh: use LC_ALL=C so the request date header is always English LC_ALL in the environment overrides both LC_TIME and LANG, so LANG=C alone still produced localized day/month names on non-English systems and DNS Made Easy rejected the request date header. An LC_ALL=C command prefix beats every locale variable (same pattern as dns_oci.sh). Fixes #4272. Closes #4271. Thanks to @Nickinthebox. --- dnsapi/dns_me.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_me.sh b/dnsapi/dns_me.sh index 163fe8db..0966c5f1 100644 --- a/dnsapi/dns_me.sh +++ b/dnsapi/dns_me.sh @@ -140,7 +140,7 @@ _me_rest() { data="$3" _debug "$ep" - cdate=$(LANG=C date -u +"%a, %d %b %Y %T %Z") + cdate=$(LC_ALL=C date -u +"%a, %d %b %Y %T %Z") hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex) export _H1="x-dnsme-apiKey: $ME_Key" From 90b4795bb1a75712258b523d7bcec9fdd3e2c635 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 11:22:20 +0800 Subject: [PATCH 610/689] issue: strip the trailing dot of a fully-qualified alias domain A trailing dot in --domain-alias/--challenge-alias was passed through to the dnsapi hook verbatim. Providers with exact-match record-name lookups (e.g. Cloudflare's name= filter) then never find the record, so rm never deletes it and relic TXT records accumulate on every issue. Stripping in issue() also fixes certs with a dotted alias already saved in domain.conf. fix https://github.com/acmesh-official/acme.sh/issues/4636 --- acme.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/acme.sh b/acme.sh index b072c6cf..84b38068 100755 --- a/acme.sh +++ b/acme.sh @@ -5350,6 +5350,8 @@ $_authorizations_map" fi _d_alias="$(_getfield "$_challenge_alias" "$_alias_index")" test "$_d_alias" = "$NO_VALUE" && _d_alias="" + # strip the trailing dot of a fully-qualified alias domain + _d_alias="${_d_alias%.}" _alias_index="$(_math "$_alias_index" + 1)" _debug "_d_alias" "$_d_alias" if [ "$_d_alias" ]; then From fa763db1051e15978aa11b5e8721e7c82ae3314e Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 12:05:28 +0800 Subject: [PATCH 611/689] dns_pleskxml.sh: use grep -F when matching interpolated values fulldomain/txtvalue/root_domain_name were interpolated into grep regex patterns; match them as fixed strings instead. from https://github.com/acmesh-official/acme.sh/pull/7031 --- dnsapi/dns_pleskxml.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_pleskxml.sh b/dnsapi/dns_pleskxml.sh index 465bcc60..176f329d 100644 --- a/dnsapi/dns_pleskxml.sh +++ b/dnsapi/dns_pleskxml.sh @@ -151,8 +151,8 @@ dns_pleskxml_rm() { # Extracting the id of the TXT record for the full domain (NOT case-sensitive) and corresponding value recid="$( _value "$reclist" | - grep -i "${fulldomain}." | - grep "${txtvalue}" | + grep -Fi "${fulldomain}." | + grep -F "${txtvalue}" | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/' )" @@ -419,7 +419,7 @@ _pleskxml_get_root_domain() { _debug "Checking if '$root_domain_name' is managed by the Plesk server..." - root_domain_id="$(_value "$output" | grep "$root_domain_name" | _head_n 1 | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/')" + root_domain_id="$(_value "$output" | grep -F "$root_domain_name" | _head_n 1 | sed 's/^.*\([0-9]\{1,\}\)<\/id>.*$/\1/')" if [ -n "$root_domain_id" ]; then # Found a match From 2af543a358a1ac1cdb316d8d551db6bd41a99550 Mon Sep 17 00:00:00 2001 From: Jan Pieper Date: Fri, 10 Jul 2026 08:42:57 +0200 Subject: [PATCH 612/689] Fix typo (#6924) --- deploy/synology_dsm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 202e8188..e19024c7 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -72,7 +72,7 @@ synology_dsm_deploy() { if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then - _err "Missing required tools to creat temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead." + _err "Missing required tools to create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead." _err "Notice: temp admin user authorization method only supports local deployment on DSM." return 1 fi From 660a5e322c24c4975bb416a52de288474ae4aabf Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 18:37:13 +0800 Subject: [PATCH 613/689] deploy/synology_dsm.sh: use grep -Eo '[0-9]+' when extracting error codes grep -o '[0-9]*' can match the empty string; GNU grep skips empty matches but BSD greps handle them differently, breaking the 2FA login flow on OpenBSD. Force a non-empty match at all three sites. from https://github.com/acmesh-official/acme.sh/pull/6725 --- deploy/synology_dsm.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index e19024c7..75497671 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -234,7 +234,7 @@ synology_dsm_deploy() { fi fi - error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*') + error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+') _debug2 error_code "$error_code" # Account has 2FA-OTP enabled, since error 403 reported. # https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf @@ -269,7 +269,7 @@ synology_dsm_deploy() { _secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID" fi fi - error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*') + error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+') _debug2 error_code "$error_code" fi @@ -336,7 +336,7 @@ synology_dsm_deploy() { id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p") _debug2 id "$id" - error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*') + error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+') _debug2 error_code "$error_code" if [ -n "$error_code" ]; then if [ "$error_code" -eq 105 ]; then From 2058a77d83c0211e5a5b09cc2924eae6a944b3c3 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 19:37:36 +0800 Subject: [PATCH 614/689] acme.sh: fix variable name typo _excapedAlgnames -> _escapedAltnames from https://github.com/acmesh-official/acme.sh/pull/6547 --- acme.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/acme.sh b/acme.sh index 84b38068..6495a90e 100755 --- a/acme.sh +++ b/acme.sh @@ -1443,13 +1443,13 @@ _readSubjectAltNamesFromCSR() { _debug _dnsAltnames "$_dnsAltnames" # escape the wildcard '*' so it is not taken as a regex operator by grep/sed below - _excapedAlgnames="$(echo "$_dnsAltnames" | tr '*' '#')" - _debug _excapedAlgnames "$_excapedAlgnames" + _escapedAltnames="$(echo "$_dnsAltnames" | tr '*' '#')" + _debug _escapedAltnames "$_escapedAltnames" _escapedSubject="$(echo "$_csrsubj" | tr '*' '#')" _debug _escapedSubject "$_escapedSubject" - if _contains "$_excapedAlgnames," "DNS:$_escapedSubject,"; then + if _contains "$_escapedAltnames," "DNS:$_escapedSubject,"; then _debug "AltNames contains subject" - _dnsAltnames="$(echo "$_excapedAlgnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")" + _dnsAltnames="$(echo "$_escapedAltnames," | sed "s/DNS:$_escapedSubject,//g" | tr '#' '*' | sed "s/,\$//g")" _debug _dnsAltnames "$_dnsAltnames" else _debug "AltNames doesn't contain subject" From 98c30912fb5a58f3a50b6263ec0e30574c5ef8c1 Mon Sep 17 00:00:00 2001 From: "Andrew V." Date: Fri, 10 Jul 2026 15:36:42 +0300 Subject: [PATCH 615/689] 2024-12-24 - Ensure that $PDNS_Url has no trailing slash ('/'). (#6171) --- dnsapi/dns_pdns.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh index ec19ad25..847a1af1 100755 --- a/dnsapi/dns_pdns.sh +++ b/dnsapi/dns_pdns.sh @@ -50,6 +50,9 @@ dns_pdns_add() { PDNS_Ttl="$DEFAULT_PDNS_TTL" fi + # Ensure PDNS_Url has no trailing slash ('/') + PDNS_Url="${PDNS_Url%/}" + #save the api addr and key to the account conf file. _saveaccountconf_mutable PDNS_Url "$PDNS_Url" _saveaccountconf_mutable PDNS_ServerId "$PDNS_ServerId" From 58cd667d6573262eb1061b51a18b21f2721e4fb0 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 10 Jul 2026 20:47:20 +0800 Subject: [PATCH 616/689] dns_njalla.sh: accept string record ids when removing records The Njalla API returns record ids as JSON strings now; the numeric-only pattern matched nothing, so the removal never found the record id. Match both quoted and bare ids. from https://github.com/acmesh-official/acme.sh/pull/5121 --- dnsapi/dns_njalla.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_njalla.sh b/dnsapi/dns_njalla.sh index c410447d..6ce51380 100644 --- a/dnsapi/dns_njalla.sh +++ b/dnsapi/dns_njalla.sh @@ -98,7 +98,7 @@ dns_njalla_rm() { echo "$records" | while read -r record; do record_name=$(echo "$record" | _egrep_o "\"name\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \") record_content=$(echo "$record" | _egrep_o "\"content\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \") - record_id=$(echo "$record" | _egrep_o "\"id\":\s?[0-9]+" | cut -d : -f 2 | tr -d " " | tr -d \") + record_id=$(echo "$record" | _egrep_o "\"id\":\s?\"?[^\",}]*" | cut -d : -f 2 | tr -d " " | tr -d \") if [ "$_sub_domain" = "$record_name" ]; then if [ "$txtvalue" = "$record_content" ]; then _debug "record_id" "$record_id" From ebde8345ae154e621087341eecd691b26bbc555b Mon Sep 17 00:00:00 2001 From: CV Date: Fri, 10 Jul 2026 14:55:23 +0200 Subject: [PATCH 617/689] dns_ispconfig.sh client_id not numeric at ispconfig v 3.2.7p1 (#4999) Getting client_id failed due to incorrect extraction! At least in version 3.2.7p1 and probably later the plugin is not working any more properly. The result of ```curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")"``` is something like this ```Result of _ISPC_ClientGetID: '[Tue Jan 23 11:44:57 CET 2024] Retrying post {"code":"ok","message":"","response":3}[Tue Jan 23 11:44:57 CET 2024] _hcode 0'```. The parsing code does not work properly and leaves a non numeric value such as ```Client ID: '3[Tue Jan 23 11'```. --- dnsapi/dns_ispconfig.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_ispconfig.sh b/dnsapi/dns_ispconfig.sh index edc789e1..bd6bfb28 100755 --- a/dnsapi/dns_ispconfig.sh +++ b/dnsapi/dns_ispconfig.sh @@ -136,7 +136,7 @@ _ISPC_getZoneInfo() { curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")" _debug "Calling _ISPC_ClientGetID: '${curData}' '${ISPC_Api}?client_get_id'" _debug "Result of _ISPC_ClientGetID: '$curResult'" - client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}') + client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d '[' -f 1 | tr -d '{}') _debug "Client ID: '${client_id}'" case "${client_id}" in '' | *[!0-9]*) From 50e5e771d56a5989e0bc24df3b422059df63669b Mon Sep 17 00:00:00 2001 From: Roman Lumetsberger Date: Fri, 10 Jul 2026 13:02:06 +0000 Subject: [PATCH 618/689] Feature: Support other shells then sh (#4877) * Add support for DEPLOY_SSH_REMOTE_SHELL * allow to configure quoting of remote cmd string * shell check and shellfmt fixes --- deploy/ssh.sh | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 82b0382c..0bf3ee48 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -25,7 +25,8 @@ # export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value # export DEPLOY_SSH_USE_SCP="" yes or no, default to no # export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q" -# +# export DEPLOY_SSH_REMOTE_SHELL="" # defaults to sh -c +# export DEPLOY_SSH_REMOTE_CMD_QUOTE="" # yes or no, defaults to yes ######## Public functions ##################### #domain keyfile certfile cafile fullchain @@ -71,6 +72,24 @@ ssh_deploy() { fi _savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD" + # REMOTE_SHELL is optional. If not provided then use sh + _migratedeployconf Le_Deploy_ssh_remote_shell DEPLOY_SSH_REMOTE_SHELL + _getdeployconf DEPLOY_SSH_REMOTE_SHELL + _debug2 DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL" + if [ -z "$DEPLOY_SSH_REMOTE_SHELL" ]; then + DEPLOY_SSH_REMOTE_SHELL="sh -c" + fi + _savedeployconf DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL" + + # REMOTE_CMD_QUOTE is optional. If not provided then yes + _migratedeployconf Le_Deploy_ssh_remote_cmd_quote DEPLOY_SSH_REMOTE_CMD_QUOTE + _getdeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE + _debug2 DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE" + if [ -z "$DEPLOY_SSH_REMOTE_CMD_QUOTE" ]; then + DEPLOY_SSH_REMOTE_CMD_QUOTE="yes" + fi + _savedeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE" + # BACKUP is optional. If not provided then default to previously saved value or yes. _migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP _getdeployconf DEPLOY_SSH_BACKUP @@ -434,9 +453,13 @@ _ssh_remote_cmd() { _secure_debug "Remote commands to execute: $_cmd" _info "Submitting sequence of commands to remote server by $_ssh_cmd" - # quotations in bash cmd below intended. Squash travis spellcheck error - # shellcheck disable=SC2029 - $_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'" + if [ "$DEPLOY_SSH_REMOTE_CMD_QUOTE" = "yes" ]; then + # quotations in bash cmd below intended. Squash travis spellcheck error + # shellcheck disable=SC2029 + $_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "'$_cmd'" + else + $_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "$_cmd" + fi _err_code="$?" if [ "$_err_code" != "0" ]; then From fef90e15e131b8fa2f47dad471aa37dcdba4f772 Mon Sep 17 00:00:00 2001 From: Mike Lei Date: Fri, 10 Jul 2026 21:56:54 +0800 Subject: [PATCH 619/689] Fix name.com DNS API for processing IDNs (#4381) --- dnsapi/dns_namecom.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_namecom.sh b/dnsapi/dns_namecom.sh index 1ba6a6e5..bd7da0c2 100755 --- a/dnsapi/dns_namecom.sh +++ b/dnsapi/dns_namecom.sh @@ -15,7 +15,7 @@ Namecom_API="https://api.name.com/v4" #Usage: dns_namecom_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_namecom_add() { - fulldomain=$1 + fulldomain=$(_idn "$1") txtvalue=$2 Namecom_Username="${Namecom_Username:-$(_readaccountconf_mutable Namecom_Username)}" @@ -68,7 +68,7 @@ dns_namecom_add() { #Usage: fulldomain txtvalue #Remove the txt record after validation. dns_namecom_rm() { - fulldomain=$1 + fulldomain=$(_idn "$1") txtvalue=$2 Namecom_Username="${Namecom_Username:-$(_readaccountconf_mutable Namecom_Username)}" From 2e4acba1055ea7b82e79f4f0ac2c7264796f7b00 Mon Sep 17 00:00:00 2001 From: Sasha Reid Date: Sat, 11 Jul 2026 04:04:49 +0200 Subject: [PATCH 620/689] Microwavenby dns hostinger (#6843) * [Microwavenby--dns_hostinger] Adding initial dns support for Hostinger.com * [Microwavenby--dns_hostinger] Creating a commit now that workflows are enabled * [Microwavenby--dns_hostinger] Correcting shellcheck. Why is this not automatic? * [Microwavenby-dns-hostinger] Responding to comments from Neil * [dns-hostinger] SHfmt and Shellcheck * [dns-hostinger] Writing non-greedy-ish regexes. correcting copypasta --- dnsapi/dns_hostinger.sh | 196 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100755 dnsapi/dns_hostinger.sh diff --git a/dnsapi/dns_hostinger.sh b/dnsapi/dns_hostinger.sh new file mode 100755 index 00000000..665c65da --- /dev/null +++ b/dnsapi/dns_hostinger.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_hostinger_info='Hostinger +Site: Hostinger.com +Domains: hostinger.nl +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hostinger +Options: + HOSTINGER_Token API Key +Issues: https://github.com/acmesh-official/acme.sh/issues/6831 +Author: Sasha Reid +' + +HOSTINGER_Api="https://developers.hostinger.com/api/dns/v1/zones" + +######## Public functions ##################### + +#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_hostinger_add() { + fulldomain=$1 + txtvalue=$2 + + HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}" + + if [ -z "$HOSTINGER_Token" ]; then + HOSTINGER_Token="" + _err "You didn't specify a Hostinger API Key yet." + _err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication" + return 1 + fi + _saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_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" + + _debug "Getting existing records" + _hostinger_rest GET "${_domain}" + + if [ -z "$response" ]; then + _err "Error" + 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 record" + if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [{\"content\":\"$txtvalue\"}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":false}"; then + if _contains "$response" "Request accepted"; then + _info "Added, OK" + return 0 + elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" || + _contains "$response" 'DNS:4008'; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + _err "Add txt record error." + return 1 + +} + +#fulldomain txtvalue +dns_hostinger_rm() { + fulldomain=$1 + txtvalue=$2 + + HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}" + + if [ -z "$HOSTINGER_Token" ]; then + HOSTINGER_Token="" + _err "You didn't specify a Hostinger API Key yet." + _err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication" + return 1 + fi + _saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_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" + + _debug "Getting existing records" + _hostinger_rest GET "${_domain}" + + if [ -z "$response" ]; then + _err "Error" + return 1 + fi + + if _contains "$response" "\"name\":\"$_sub_domain\""; then + # Match the record, and make certain it is a TXT record for the domain not another type. Then remove our target record from the list + remaining_records=$(echo "$response" | _normalizeJson | _egrep_o '{"name":"'"$_sub_domain"'","records":\[[^]]+\],"ttl":[0-9]+,"type":"TXT"\}' | _egrep_o "\[.*\]" | sed -E 's#\{"content":"\\"'"$txtvalue"'\\"","is_disabled":false\},?##g') + if [ "$remaining_records" != "[]" ]; then + remaining_json=$(echo "$remaining_records" | _egrep_o '"content":"\\"[^}]+\\""' | sed -E 's/^(.*)$/{\1},/g' | tr -d '\n' | sed 's/,$//') + # We need to set the remaining records back to Hostinger, as we can't partially delete + _info "Removing $txtvalue from $_sub_domain by setting records to ${remaining_json}" + if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [${remaining_json}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":true}"; then + if _contains "$response" "Request accepted"; then + _info "Updated remaining records, OK" + return 0 + elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" || + _contains "$response" 'DNS:4008'; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + return 1 + fi + fi + # Otherwise delete the TXT record that matches the subdomain + else + if ! _hostinger_rest DELETE "$_domain" "{\"filters\":[{\"name\":\"$_sub_domain\",\"type\":\"TXT\"}]}"; then + _err "Delete record error." + return 1 + fi + fi + echo "$response" | grep "Request accepted" >/dev/null + else + _info "Don't need to remove." + fi + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=1 + p=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 + + _hostinger_rest GET "$h" + if _contains "$response" "records"; then + if [ "$response" = "[]" ]; then + _debug "Valid subdomains are not the root" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + return 0 + fi + fi + + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_hostinger_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + token_trimmed=$(echo "$HOSTINGER_Token" | tr -d '"') + + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $token_trimmed" + + if [ "$m" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$HOSTINGER_Api/$ep" "" "$m")" + else + response="$(_get "$HOSTINGER_Api/$ep")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} From 9366c2e065d1ef006d18fe97965eb85b3a8f3a59 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 11 Jul 2026 11:42:32 +0800 Subject: [PATCH 621/689] dns_cpanel: resolve the most specific zone in _get_root With both domain.tld and sub.domain.tld zones on the account, the first endswith hit could pick the parent zone while cPanel stores the record in the most specific one, so the cleanup never found the record and left an orphaned _acme-challenge TXT entry. Pick the longest matching zone with an exact literal suffix match (_endswith treats the needle as a regex, letting xdomain.tld wrongly match zone domain.tld). https://github.com/acmesh-official/acme.sh/issues/6807 --- dnsapi/dns_cpanel.sh | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/dnsapi/dns_cpanel.sh b/dnsapi/dns_cpanel.sh index a6991403..3868c679 100755 --- a/dnsapi/dns_cpanel.sh +++ b/dnsapi/dns_cpanel.sh @@ -38,7 +38,7 @@ dns_cpanel_add() { fi # adding entry _info "Adding the entry" - stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//") + stripped_fulldomain="${fulldomain%.$_domain}" _debug "Adding $stripped_fulldomain to $_domain zone" _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1" if _successful_update; then return 0; fi @@ -128,13 +128,27 @@ _get_root() { _err "Primary domain list not found!" return 1 fi - for _domain in $_domains; do - _debug "Checking if $fulldomain ends with $_domain" - if (_endswith "$fulldomain" "$_domain"); then - _debug "Root domain: $_domain" - return 0 - fi + # Pick the LONGEST matching zone, dot-anchored: with both domain.tld and + # sub.domain.tld zones on the account, cPanel stores the record in the + # most specific zone, so add and rm must both resolve to that one. + _domain="" + for d in $_domains; do + _debug "Checking if $fulldomain ends with $d" + # case with quoted patterns gives an exact literal suffix match; + # _endswith treats the needle as a regex, so its dots would let + # xdomain.tld wrongly match zone domain.tld + case "$fulldomain" in + "$d" | *".$d") + if [ "${#d}" -gt "${#_domain}" ]; then + _domain="$d" + fi + ;; + esac done + if [ -n "$_domain" ]; then + _debug "Root domain: $_domain" + return 0 + fi return 1 } From 2215f1b988dd544f186b9ae05d0a7061bf92b80f Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 11 Jul 2026 11:56:58 +0800 Subject: [PATCH 622/689] notify: clear inherited _H1.._H5 before running each notify hook The dns/deploy hooks export _H1.._H5 in the main process, and the notify hooks run in a subshell that inherits them. A hook that does not overwrite every slot (ntfy without NTFY_TOKEN, slack, telegram, etc.) sent the stale headers with its request, leaking another service's Authorization credentials to the notify endpoint. https://github.com/acmesh-official/acme.sh/issues/6801 --- acme.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/acme.sh b/acme.sh index 6495a90e..9ec92585 100755 --- a/acme.sh +++ b/acme.sh @@ -7765,6 +7765,14 @@ _send_notify() { continue fi if ! ( + # The dns/deploy hooks export _H1.._H5 in the main process, so the + # values are inherited here. Clear them: a stale Authorization header + # from another service must not leak into the notify request. + export _H1="" + export _H2="" + export _H3="" + export _H4="" + export _H5="" if ! . "$_n_hook_file"; then _err "Error loading file $_n_hook_file. Please check your API file and try again." return 1 From 04e0422526fc5723c3ce4fe88c4e39e9af8c7e2d Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 09:30:46 +0800 Subject: [PATCH 623/689] precheck: log the socat recommendation with _info instead of _err Missing socat only matters for standalone mode; the text even says the warning can be ignored. Printing it to stderr made every --upgrade in a cron noisy for DNS-only users who redirect stdout. https://github.com/acmesh-official/acme.sh/issues/6525 --- acme.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 9ec92585..04002193 100755 --- a/acme.sh +++ b/acme.sh @@ -7420,9 +7420,9 @@ _precheck() { fi if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then - _err "It is recommended to install socat or python first." - _err "We use socat or python for the standalone server, which is used for standalone mode." - _err "If you don't want to use standalone mode, you may ignore this warning." + _info "It is recommended to install socat or python first." + _info "We use socat or python for the standalone server, which is used for standalone mode." + _info "If you don't want to use standalone mode, you may ignore this warning." fi return 0 From 22a5ae3cebe762017baeb3e14f4b3c71d3db6a13 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 09:34:59 +0800 Subject: [PATCH 624/689] dns_cloudns: include the server response in the login error https://github.com/acmesh-official/acme.sh/issues/6520 --- dnsapi/dns_cloudns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_cloudns.sh b/dnsapi/dns_cloudns.sh index 23a219da..2c543271 100755 --- a/dnsapi/dns_cloudns.sh +++ b/dnsapi/dns_cloudns.sh @@ -135,7 +135,7 @@ _dns_cloudns_init_check() { _dns_cloudns_http_api_call "dns/login.json" "" if ! _contains "$response" "\"status\":\"Success\""; then - _err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials." + _err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Server response: $response" return 1 fi From 76811857a09fe7d63036c05555630ff9a1901f13 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 09:35:22 +0800 Subject: [PATCH 625/689] dns_cpanel: quote inner expansion in suffix strip (SC2295) --- dnsapi/dns_cpanel.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_cpanel.sh b/dnsapi/dns_cpanel.sh index 3868c679..6939c3f1 100755 --- a/dnsapi/dns_cpanel.sh +++ b/dnsapi/dns_cpanel.sh @@ -38,7 +38,7 @@ dns_cpanel_add() { fi # adding entry _info "Adding the entry" - stripped_fulldomain="${fulldomain%.$_domain}" + stripped_fulldomain="${fulldomain%."$_domain"}" _debug "Adding $stripped_fulldomain to $_domain zone" _myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1" if _successful_update; then return 0; fi From 7d0283ca2cced9d6eb9985e36de96d15e902b556 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 10:26:40 +0800 Subject: [PATCH 626/689] dns_zonomi: make the API endpoint configurable via ZM_Api RimuHosting (which owns zonomi) exposes the identical API at https://rimuhosting.com/dns/dyndns.jsp; an overridable endpoint serves both providers with one hook. The default stays zonomi.com and is not written to the account conf. https://github.com/acmesh-official/acme.sh/issues/6475 --- dnsapi/dns_zonomi.sh | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_zonomi.sh b/dnsapi/dns_zonomi.sh index ee817381..c783a221 100644 --- a/dnsapi/dns_zonomi.sh +++ b/dnsapi/dns_zonomi.sh @@ -5,9 +5,11 @@ Site: zonomi.com Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_zonomi Options: ZM_Key API Key +OptionsAlt: + ZM_Api API endpoint. Default: "https://zonomi.com/app/dns/dyndns.jsp". For RimuHosting use "https://rimuhosting.com/dns/dyndns.jsp". ' -ZM_Api="https://zonomi.com/app/dns/dyndns.jsp" +ZM_Api_Default="https://zonomi.com/app/dns/dyndns.jsp" ######## Public functions ##################### @@ -28,6 +30,8 @@ dns_zonomi_add() { #save the api key to the account conf file. _saveaccountconf_mutable ZM_Key "$ZM_Key" + _zm_init_api + _info "Get existing txt records for $fulldomain" if ! _zm_request "action=QUERY&name=$fulldomain"; then _err "error" @@ -64,11 +68,27 @@ dns_zonomi_rm() { return 1 fi + _zm_init_api + _zm_request "action=DELETE&type=TXT&name=$fulldomain" } #################### Private functions below ################################## + +# resolve the API endpoint: zonomi by default, overridable for providers +# sharing the same API on another host (e.g. RimuHosting) +_zm_init_api() { + ZM_Api="${ZM_Api:-$(_readaccountconf_mutable ZM_Api)}" + if [ -z "$ZM_Api" ]; then + ZM_Api="$ZM_Api_Default" + fi + _debug2 ZM_Api "$ZM_Api" + if [ "$ZM_Api" != "$ZM_Api_Default" ]; then + _saveaccountconf_mutable ZM_Api "$ZM_Api" + fi +} + #qstr _zm_request() { qstr="$1" From a49f8c1992c26adb6c33a5e7425601297709472f Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 10:49:49 +0800 Subject: [PATCH 627/689] mydevil: replace BSD-only cut -w with tr + plain cut cut -w (split on whitespace) is a FreeBSD extension unknown to GNU coreutils; squeeze blanks into tabs with tr first so the field extraction is POSIX. https://github.com/acmesh-official/acme.sh/issues/6452 --- deploy/mydevil.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/mydevil.sh b/deploy/mydevil.sh index bd9868aa..8954f822 100755 --- a/deploy/mydevil.sh +++ b/deploy/mydevil.sh @@ -54,6 +54,8 @@ mydevil_deploy() { # Usage: ip=$(mydevil_get_ip domain.com) # echo $ip mydevil_get_ip() { - devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1 + # tr squeezes runs of blanks into one tab so plain cut works everywhere; + # cut -w is BSD-only and unknown to GNU coreutils + devil dns list "$1" | tr -s ' \t' '\t' | cut -s -f 3,7 | grep "^A$(printf '\t')" | cut -s -f 2 || return 1 return 0 } From e94631de44e08a826c569e149ed45f7edc70e720 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 11:20:53 +0800 Subject: [PATCH 628/689] dns_pdns: probe zones with the server-side name filter in _get_root The unfiltered GET /zones lists every zone on the server; with large installations (100k zones) root-zone detection took minutes per domain. Probe each walk-up candidate with ?zone= instead (exact match per the PowerDNS API docs); servers that ignore the parameter return the full list, which the existing check still handles. https://github.com/acmesh-official/acme.sh/issues/6382 --- dnsapi/dns_pdns.sh | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/dnsapi/dns_pdns.sh b/dnsapi/dns_pdns.sh index 847a1af1..72a58af0 100755 --- a/dnsapi/dns_pdns.sh +++ b/dnsapi/dns_pdns.sh @@ -189,19 +189,23 @@ _get_root() { domain=$1 i=1 - if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then - _zones_response=$(echo "$response" | _normalizeJson) - fi - while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - if _contains "$_zones_response" "\"name\":\"$h.\""; then - _domain="$h." - if [ -z "$h" ]; then - _domain="=2E" + # Probe each candidate zone with the server-side name filter instead of + # listing every zone: with large installations (100k zones) the + # unfiltered list takes minutes. Servers that ignore the parameter + # return the full list, which the check below still handles. + # https://doc.powerdns.com/authoritative/http-api/zone.html + if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones?zone=$h."; then + _zones_response=$(echo "$response" | _normalizeJson) + if _contains "$_zones_response" "\"name\":\"$h.\""; then + _domain="$h." + if [ -z "$h" ]; then + _domain="=2E" + fi + return 0 fi - return 0 fi if [ -z "$h" ]; then From 44c045b0569b2ec3a7b3b65451a88eaba5f05394 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 11:36:45 +0800 Subject: [PATCH 629/689] notify: add the customscript hook The wiki has documented "21. Set notification for customscript" since 2022 but the implementation (#4193) was never merged, so following the wiki failed with "Cannot find the hook file". Same interface as documented: the script gets subject, content and status code as three arguments. Unlike #4193, the target script is invoked directly instead of through eval -- the subject/content contain domain names and CA messages, and eval would allow command injection through them. https://github.com/acmesh-official/acme.sh/issues/6377 --- notify/customscript.sh | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 notify/customscript.sh diff --git a/notify/customscript.sh b/notify/customscript.sh new file mode 100644 index 00000000..ba8b07cb --- /dev/null +++ b/notify/customscript.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +# Support calling a custom script for notifications +# +# export CUSTOMSCRIPT_PATH="/usr/local/bin/acme-notification.sh" +# +# The script is called with three arguments: +# $1 subject +# $2 content +# $3 status code (0: success, 1: error, 2: skipped) + +customscript_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_subject" "$_subject" + _debug "_content" "$_content" + _debug "_statusCode" "$_statusCode" + + CUSTOMSCRIPT_PATH="${CUSTOMSCRIPT_PATH:-$(_readaccountconf_mutable CUSTOMSCRIPT_PATH)}" + if [ -z "$CUSTOMSCRIPT_PATH" ]; then + _err "You didn't specify the custom script path CUSTOMSCRIPT_PATH yet." + return 1 + fi + if ! _exists "$CUSTOMSCRIPT_PATH"; then + _err "The custom script $CUSTOMSCRIPT_PATH does not exist or is not executable." + return 1 + fi + _saveaccountconf_mutable CUSTOMSCRIPT_PATH "$CUSTOMSCRIPT_PATH" + + # Invoke directly, never through eval: the subject and content contain + # domain names and CA messages, eval would allow command injection. + _customscript_result="$("$CUSTOMSCRIPT_PATH" "$_subject" "$_content" "$_statusCode" 2>&1)" + _customscript_rc="$?" + _debug2 "_customscript_result" "$_customscript_result" + + if [ "$_customscript_rc" != "0" ]; then + _err "custom script execution error ($_customscript_rc): $_customscript_result" + return 1 + fi + + _info "custom script executed successfully." + return 0 +} From c0d62eb9342e592523b1ee98090f1150dd76cc32 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 11:43:07 +0800 Subject: [PATCH 630/689] wiki-guard: skip on forks (no .wiki repo to check out) --- .github/workflows/wiki-guard.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/wiki-guard.yml b/.github/workflows/wiki-guard.yml index 709fb8ea..dbe3dd8d 100644 --- a/.github/workflows/wiki-guard.yml +++ b/.github/workflows/wiki-guard.yml @@ -37,6 +37,9 @@ concurrency: jobs: guard: + # Forks have no .wiki repository, so the checkout below would + # fail there -- run only in the upstream repository. + if: github.repository == 'acmesh-official/acme.sh' runs-on: ubuntu-latest steps: - name: Checkout wiki repository From a9590c5bd7d9cdd32b8dda68bb00002feb43ca2a Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 11:49:53 +0800 Subject: [PATCH 631/689] workflows: run issue/wiki automation only in the upstream repo --- .github/workflows/blacklist-command.yml | 3 ++- .github/workflows/revert-command.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/blacklist-command.yml b/.github/workflows/blacklist-command.yml index 4d0ff5f3..35e53677 100644 --- a/.github/workflows/blacklist-command.yml +++ b/.github/workflows/blacklist-command.yml @@ -21,7 +21,8 @@ concurrency: jobs: blacklist: - if: startsWith(github.event.issue.title, 'blacklist:') + # Upstream only: forks have no .wiki repository to push to. + if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'blacklist:') runs-on: ubuntu-latest steps: - name: Check authorization diff --git a/.github/workflows/revert-command.yml b/.github/workflows/revert-command.yml index 7a792bf4..03161bbb 100644 --- a/.github/workflows/revert-command.yml +++ b/.github/workflows/revert-command.yml @@ -21,7 +21,8 @@ concurrency: jobs: revert: - if: startsWith(github.event.issue.title, 'revert:') + # Upstream only: forks have no .wiki repository to push to. + if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'revert:') runs-on: ubuntu-latest steps: - name: Check authorization From 897e2197433090c24bf55266d35a48987f1a60b0 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 12:12:18 +0800 Subject: [PATCH 632/689] issue: never schedule the default renewal past the cert expiry The default schedule was a fixed CertCreateTime + RenewalDays - 1 day, which passes notAfter entirely for short-lived certs (internal CAs today, the CA/B SC-081 47-day maximum later) and leaves an expired cert in place for weeks. Extract the arithmetic into _calc_next_renew_time and cap it at one day before expiry (one hour for lifetimes of 24h or less, mirroring --valid-to scheduling). CAs with ARI are unaffected -- the ARI window still overrides afterwards. https://github.com/acmesh-official/acme.sh/issues/6305 --- acme.sh | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 04002193..00ec7ef4 100755 --- a/acme.sh +++ b/acme.sh @@ -1968,6 +1968,33 @@ _utc_date() { date -u "+%Y-%m-%d %H:%M:%S" } +#Usage: _calc_next_renew_time createtime renewaldays [endtime] +#Prints createtime + renewaldays*86400 - 86400, capped so it never passes +#the certificate expiry: with short-lived certs (internal CAs, upcoming +#CA/B SC-081 47-day maximum) a fixed RenewalDays would otherwise schedule +#the renewal after notAfter and leave an expired cert in place. +#The cap is one day before endtime, or one hour before for certs whose +#lifetime is 24 hours or less, mirroring the --valid-to scheduling. +_calc_next_renew_time() { + _cnrt_create="$1" + _cnrt_days="$2" + _cnrt_end="$3" + _cnrt_next=$(_math "$_cnrt_create" + "$_cnrt_days" \* 24 \* 60 \* 60 - 86400) + if [ -z "$_cnrt_end" ]; then + printf "%s" "$_cnrt_next" + return 0 + fi + if [ "$(_math "$_cnrt_end" - "$_cnrt_create")" -gt 86400 ]; then + _cnrt_cap=$(_math "$_cnrt_end" - 86400) + else + _cnrt_cap=$(_math "$_cnrt_end" - 3600) + fi + if [ "$_cnrt_next" -gt "$_cnrt_cap" ]; then + _cnrt_next="$_cnrt_cap" + fi + printf "%s" "$_cnrt_next" +} + _mktemp() { if _exists mktemp; then if mktemp 2>/dev/null; then @@ -5946,8 +5973,12 @@ $_authorizations_map" Le_NextRenewTime=$(_math "$_endtime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") else - Le_NextRenewTime=$(_math "$Le_CertCreateTime" + "$Le_RenewalDays" \* 24 \* 60 \* 60) - Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) + _endtime_for_cap="" + _enddate_value=$(_enddate "$CERT_PATH") + if [ "$?" = "0" ] && [ "$_enddate_value" ]; then + _endtime_for_cap=$(_ssldate2time "$_enddate_value") + fi + Le_NextRenewTime=$(_calc_next_renew_time "$Le_CertCreateTime" "$Le_RenewalDays" "$_endtime_for_cap") Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") fi From 099d88e6a99714f0c8cd87d5eeaaed9e2c871d9f Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 12:22:28 +0800 Subject: [PATCH 633/689] dns_knot: KNOT_KEY takes the TSIG key data, not a file path The knsupdate "key" statement wants "[alg:]name secret"; the info block wrongly described the option as a file path. https://github.com/acmesh-official/acme.sh/issues/6293 --- dnsapi/dns_knot.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_knot.sh b/dnsapi/dns_knot.sh index 5636804a..b5ba32f0 100644 --- a/dnsapi/dns_knot.sh +++ b/dnsapi/dns_knot.sh @@ -5,7 +5,7 @@ Site: www.knot-dns.cz/docs/2.5/html/man_knsupdate.html Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_knot Options: KNOT_SERVER Server hostname. Default: "localhost". - KNOT_KEY File path to TSIG key + KNOT_KEY TSIG key data, not a file path. knsupdate "key" statement format: "[alg:]name secret". E.g. "hmac-sha256:acme_key BASE64SECRET=" ' # See also dns_nsupdate.sh From eacf0d6a873798753dbdfee48347b7b917f373ba Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 12:32:54 +0800 Subject: [PATCH 634/689] issue bot: tell reporters to redact secrets before posting logs Debug logs occasionally contain private keys or tokens (issue 6267); the code-side leak in the haproxy hook was fixed by #6268, this adds the missing warning to the auto-comment that asks for logs. --- .github/workflows/issue.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index f72a1f06..00e9ddc5 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -82,5 +82,5 @@ jobs: issue_number: issue.number, owner: context.repo.owner, repo: context.repo.repo, - body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you." + body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords." }) \ No newline at end of file From 2c51ac1c277b691658f57605d83664731fc77170 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 12:45:10 +0800 Subject: [PATCH 635/689] dns_dynu: include the server response in the authentication error Same as the dns_cloudns change: a bare "Authentication failed" hides the actual API error and makes reports undiagnosable. --- dnsapi/dns_dynu.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_dynu.sh b/dnsapi/dns_dynu.sh index 1d1fc311..612756c5 100644 --- a/dnsapi/dns_dynu.sh +++ b/dnsapi/dns_dynu.sh @@ -214,11 +214,11 @@ _dynu_authentication() { response="$(_get "$Dynu_EndPoint/oauth2/token")" if [ "$?" != "0" ]; then - _err "Authentication failed." + _err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token" return 1 fi if _contains "$response" "Authentication Exception"; then - _err "Authentication failed." + _err "Authentication failed. Server response: $response" return 1 fi if _contains "$response" "access_token"; then From 3989eef5e26d9eb84c9abd0a35d7d3ad955f4e0d Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 13:00:12 +0800 Subject: [PATCH 636/689] cpanel_uapi: don't spill a redirection error when the key file is absent With --signcsr the private key never exists in the cert home, so every renewal printed ".../domain.key: No such file or directory" from the shell redirection. Skip the key read in that case; the install_ssl call already ran with an empty key there and cPanel keeps the installed one. https://github.com/acmesh-official/acme.sh/issues/6228 --- deploy/cpanel_uapi.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deploy/cpanel_uapi.sh b/deploy/cpanel_uapi.sh index 16b622bb..156044eb 100644 --- a/deploy/cpanel_uapi.sh +++ b/deploy/cpanel_uapi.sh @@ -52,7 +52,15 @@ cpanel_uapi_deploy() { # read cert and key files and urlencode both _cert=$(_url_encode <"$_ccert") - _key=$(_url_encode <"$_ckey") + # with --signcsr the private key was never handed to acme.sh, so the key + # file does not exist; skip it instead of spilling a shell redirection + # error on every renewal (cPanel keeps using the already-installed key) + if [ -f "$_ckey" ]; then + _key=$(_url_encode <"$_ckey") + else + _debug "Key file $_ckey does not exist (csr mode), not sending a key." + _key="" + fi _debug2 _cert "$_cert" _debug2 _key "$_key" From ebb5cc4981ac38994b124441ef38b961ef565f27 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 16:04:44 +0800 Subject: [PATCH 637/689] deploy/synology_dsm.sh: remove bashisms, keep the hook POSIX sh The hook is sourced by acme.sh, so the bash shebang never takes effect: under dash, `[ x == y ]` fails with "unexpected operator", the 403 branch never triggers and 2FA-OTP login is skipped. Replace `==` with `=` and use the standard sh shebang. --- deploy/synology_dsm.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 75497671..d05e503a 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env sh ################################################################################ # ACME.sh 3rd party deploy plugin for Synology DSM @@ -238,7 +238,7 @@ synology_dsm_deploy() { _debug2 error_code "$error_code" # Account has 2FA-OTP enabled, since error 403 reported. # https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf - if [ "$error_code" == "403" ]; then + if [ "$error_code" = "403" ]; then if [ -z "$SYNO_DEVICE_NAME" ]; then printf "Enter device name or leave empty for default (CertRenewal): " read -r SYNO_DEVICE_NAME @@ -274,22 +274,22 @@ synology_dsm_deploy() { fi if [ -n "$error_code" ]; then - if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then + if [ "$error_code" = "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then _cleardeployconf SYNO_DEVICE_ID _err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window." - elif [ "$error_code" == "404" ]; then + elif [ "$error_code" = "404" ]; then _err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window." - elif [ "$error_code" == "406" ]; then + elif [ "$error_code" = "406" ]; then if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then _err "Failed with unexcepted error, please report this by providing full log with '--debug 3'." else _err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue." fi - elif [ "$error_code" == "400" ]; then + elif [ "$error_code" = "400" ]; then _err "Failed to authenticate, no such account or incorrect password." - elif [ "$error_code" == "401" ]; then + elif [ "$error_code" = "401" ]; then _err "Failed to authenticate with a non-existent account." - elif [ "$error_code" == "408" ] || [ "$error_code" == "409" ] || [ "$error_code" == "410" ]; then + elif [ "$error_code" = "408" ] || [ "$error_code" = "409" ] || [ "$error_code" = "410" ]; then _err "Failed to authenticate, the account password has expired or must be changed." else _err "Failed to authenticate with error: $error_code." From 237f2d9c3b3a54bd1cf7e5ce118de7ec4f9fc8ae Mon Sep 17 00:00:00 2001 From: Achmad Alif Nasrulloh Date: Fri, 10 Jul 2026 18:06:13 +0700 Subject: [PATCH 638/689] Add notify waha support --- notify/waha.sh | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 notify/waha.sh diff --git a/notify/waha.sh b/notify/waha.sh new file mode 100755 index 00000000..022d8916 --- /dev/null +++ b/notify/waha.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env sh + +#Support WAHA (WhatsApp HTTP API) - free, self-hosted WhatsApp API +#https://waha.devlike.pro/ + +#Required: +#WAHA_URL="http://localhost:3000" +#WAHA_CHAT_ID="1234567890@c.us" + +#Optional: +#WAHA_API_KEY="" +#WAHA_SESSION="default" + +waha_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_subject" "$_subject" + _debug "_content" "$_content" + _debug "_statusCode" "$_statusCode" + + WAHA_URL="${WAHA_URL:-$(_readaccountconf_mutable WAHA_URL)}" + if [ -z "$WAHA_URL" ]; then + WAHA_URL="" + _err "You didn't specify the WAHA server url WAHA_URL yet." + _err "Example: export WAHA_URL=\"http://localhost:3000\"" + return 1 + fi + _saveaccountconf_mutable WAHA_URL "$WAHA_URL" + + WAHA_CHAT_ID="${WAHA_CHAT_ID:-$(_readaccountconf_mutable WAHA_CHAT_ID)}" + if [ -z "$WAHA_CHAT_ID" ]; then + WAHA_CHAT_ID="" + _err "You didn't specify the WhatsApp chat id WAHA_CHAT_ID yet." + _err "Example: export WAHA_CHAT_ID=\"1234567890@c.us\"" + return 1 + fi + _saveaccountconf_mutable WAHA_CHAT_ID "$WAHA_CHAT_ID" + + WAHA_API_KEY="${WAHA_API_KEY:-$(_readaccountconf_mutable WAHA_API_KEY)}" + if [ "$WAHA_API_KEY" ]; then + _saveaccountconf_mutable WAHA_API_KEY "$WAHA_API_KEY" + fi + + WAHA_SESSION="${WAHA_SESSION:-$(_readaccountconf_mutable WAHA_SESSION)}" + if [ -z "$WAHA_SESSION" ]; then + WAHA_SESSION="default" + else + _saveaccountconf_mutable WAHA_SESSION "$WAHA_SESSION" + fi + + _content=$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode) + + _data="{\"chatId\": \"$WAHA_CHAT_ID\", " + _data="$_data\"text\": \"$_content\", " + _data="$_data\"session\": \"$WAHA_SESSION\"}" + + _debug "_data" "$_data" + + export _H1="Content-Type: application/json" + if [ "$WAHA_API_KEY" ]; then + export _H2="X-Api-Key: $WAHA_API_KEY" + fi + + _waha_url="${WAHA_URL}/api/sendText" + response="$(_post "$_data" "$_waha_url" "" "POST" "application/json")" + + if [ "$?" = "0" ] && _contains "$response" "id"; then + _info "waha send success." + return 0 + fi + _err "waha send error." + _err "$response" + return 1 +} From d45b6fe8e6b84a85e1a9bdb13ee6bbc6d647dc16 Mon Sep 17 00:00:00 2001 From: Achmad Alif Nasrulloh Date: Sat, 11 Jul 2026 18:42:53 +0700 Subject: [PATCH 639/689] fix(notify): waha: tighten response check --- notify/waha.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify/waha.sh b/notify/waha.sh index 022d8916..989f57ac 100755 --- a/notify/waha.sh +++ b/notify/waha.sh @@ -65,7 +65,7 @@ waha_send() { _waha_url="${WAHA_URL}/api/sendText" response="$(_post "$_data" "$_waha_url" "" "POST" "application/json")" - if [ "$?" = "0" ] && _contains "$response" "id"; then + if [ "$?" = "0" ] && _contains "$response" "\"id\""; then _info "waha send success." return 0 fi From f5e7e6b2259e415b22e0098b5aec3a84ee681478 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:10:21 +0700 Subject: [PATCH 640/689] Merge pull request #7114 from achmadalifn4/master Add notify waha support --- notify/waha.sh | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 notify/waha.sh diff --git a/notify/waha.sh b/notify/waha.sh new file mode 100755 index 00000000..989f57ac --- /dev/null +++ b/notify/waha.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env sh + +#Support WAHA (WhatsApp HTTP API) - free, self-hosted WhatsApp API +#https://waha.devlike.pro/ + +#Required: +#WAHA_URL="http://localhost:3000" +#WAHA_CHAT_ID="1234567890@c.us" + +#Optional: +#WAHA_API_KEY="" +#WAHA_SESSION="default" + +waha_send() { + _subject="$1" + _content="$2" + _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _debug "_subject" "$_subject" + _debug "_content" "$_content" + _debug "_statusCode" "$_statusCode" + + WAHA_URL="${WAHA_URL:-$(_readaccountconf_mutable WAHA_URL)}" + if [ -z "$WAHA_URL" ]; then + WAHA_URL="" + _err "You didn't specify the WAHA server url WAHA_URL yet." + _err "Example: export WAHA_URL=\"http://localhost:3000\"" + return 1 + fi + _saveaccountconf_mutable WAHA_URL "$WAHA_URL" + + WAHA_CHAT_ID="${WAHA_CHAT_ID:-$(_readaccountconf_mutable WAHA_CHAT_ID)}" + if [ -z "$WAHA_CHAT_ID" ]; then + WAHA_CHAT_ID="" + _err "You didn't specify the WhatsApp chat id WAHA_CHAT_ID yet." + _err "Example: export WAHA_CHAT_ID=\"1234567890@c.us\"" + return 1 + fi + _saveaccountconf_mutable WAHA_CHAT_ID "$WAHA_CHAT_ID" + + WAHA_API_KEY="${WAHA_API_KEY:-$(_readaccountconf_mutable WAHA_API_KEY)}" + if [ "$WAHA_API_KEY" ]; then + _saveaccountconf_mutable WAHA_API_KEY "$WAHA_API_KEY" + fi + + WAHA_SESSION="${WAHA_SESSION:-$(_readaccountconf_mutable WAHA_SESSION)}" + if [ -z "$WAHA_SESSION" ]; then + WAHA_SESSION="default" + else + _saveaccountconf_mutable WAHA_SESSION "$WAHA_SESSION" + fi + + _content=$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode) + + _data="{\"chatId\": \"$WAHA_CHAT_ID\", " + _data="$_data\"text\": \"$_content\", " + _data="$_data\"session\": \"$WAHA_SESSION\"}" + + _debug "_data" "$_data" + + export _H1="Content-Type: application/json" + if [ "$WAHA_API_KEY" ]; then + export _H2="X-Api-Key: $WAHA_API_KEY" + fi + + _waha_url="${WAHA_URL}/api/sendText" + response="$(_post "$_data" "$_waha_url" "" "POST" "application/json")" + + if [ "$?" = "0" ] && _contains "$response" "\"id\""; then + _info "waha send success." + return 0 + fi + _err "waha send error." + _err "$response" + return 1 +} From e828b285ad0c10fb811c7c77059ba233df372139 Mon Sep 17 00:00:00 2001 From: matthias-matze Date: Sun, 12 Jul 2026 10:13:42 +0200 Subject: [PATCH 641/689] Matthiasvpfr patch 1 (#7108) * Add files via upload * New Banner Updated README to include responsive images for dark and light modes. * Add files via upload Remove usage of jq and curl should be compliant with acme.sh api dev guide * Add files via upload * Add files via upload change CR LF to LF * Add files via upload missing CR * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload correct auth * Add files via upload better manage group_id * Add files via upload manage wrong / missing domain * Add files via upload strip domain to manage subdomain requests * Add files via upload fix wrong id selection in get root * Add files via upload fix parsing of IDs * Add files via upload correct get_root to better handle unexisting domaines (acmetest) * Add files via upload correct token incorrect in auth * Add files via upload manage case web api reply is empty * Add files via upload try to resolve error when adding unexisting subdmain txt entry * Add files via upload correct domain parsing * Add files via upload revert changes when adding record (name) change rm to better handle complex urls * Add files via upload correct rm function to better manage records deletion * Add files via upload ensure auth variable arent lost during calls * Add files via upload try to keep autj variables accross executions * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload fix stripping subdomains * Add files via upload * Add files via upload keep auth token instead of requesting it each time * Add files via upload debug * Add files via upload * Add files via upload * Add files via upload better manage record deletion to avoid orphans added some debug and checks * Add files via upload paginated api support for rm function * Add files via upload * Add files via upload delay to manage api 404 * Add files via upload enhance parsing of records in rm * Add files via upload fix incorrect record detection in rm * Add files via upload better manage filters on api to limit errors * Add files via upload try to handle 404 when requesting API too frequently * Add files via upload * Add files via upload sleep during auth * Add files via upload manage 404 errors in get_root * Add files via upload correct variable overide translate comments prefix all variables * Add files via upload correct variable * Add files via upload typo * Add files via upload * Add files via upload * Add files via upload * Add files via upload correct variable naming (_comlaude prefix missing) correct rm return code when non existing record typo * Add files via upload log an info instead of an error if no dns record found in RM function. --------- Co-authored-by: neil Co-authored-by: Matthiasvpfr Co-authored-by: ZeroSSL-Andreas --- dnsapi/dns_comlaude.sh | 248 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 dnsapi/dns_comlaude.sh diff --git a/dnsapi/dns_comlaude.sh b/dnsapi/dns_comlaude.sh new file mode 100644 index 00000000..2aa2dba9 --- /dev/null +++ b/dnsapi/dns_comlaude.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 +dns_comlaude_info='comlaude.com +Site: comlaude.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude +Options: + COMLAUDE_USERNAME User account + COMLAUDE_PASSWORD User password + COMLAUDE_API_KEY generated API key + COMLAUDE_GROUP_ID Group ID in comlaude user profile + Get it from the https://www.comlaude.com +Issues: github.com/acmesh-official/acme.sh/issues/7112 +' +# ===== CONFIG ===== +COMLAUDE_API="https://api.comlaude.com" + +########## AUTH ########## + +_comlaude_auth() { + _debug "Checking cached ComLaude token" + + # Try to get token from account.conf + if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then + COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)" + COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)" + fi + + _now=$(_time) + if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then + _debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})" + return 0 + fi + + _info "ComLaude auth..." + _comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}" + _comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")" + + if ! _contains "$_comlaude_response" "access_token"; then + _err "Auth failed: $_comlaude_response" + return 1 + fi + + COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4) + # store expiracy from api reply l'API ("expires_in" in seconds) + _comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2) + [ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info + + COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew + + _saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN" + _saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY" + + return 0 +} + +########## DOMAIN RESOLUTION ########## + +_comlaude_get_root() { + COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" + if [ -z "$COMLAUDE_GROUP_ID" ]; then + _err "Missing COMLAUDE_GROUP_ID" + return 1 + fi + + _comlaude_input_domain="$1" + _comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}" + case "$_comlaude_input_domain" in + \*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;; + esac + + _debug "Normalized domain: $_comlaude_input_domain" + + _comlaude_i=1 + while true; do + _comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-") + [ -z "$_comlaude_d" ] && { + _debug "No matching domain found for $_comlaude_input_domain" + return 1 + } + + # don't test unnecessary levels + # registered domain : TLD only (no dot after cut). + case "$_comlaude_d" in + *.*) : ;; + *) + _debug "Skipping bare TLD candidate: $_comlaude_d" + _comlaude_i=$((_comlaude_i + 1)) + continue + ;; + esac + + _debug "Checking domain: $_comlaude_d" + + _comlaude_retry=0 + _comlaude_max_retry=3 # to avoid network errors + _comlaude_DOM_ID="" + _comlaude_Z_ID="" + + while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + _debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone" + _comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")" + _H1="" + + _debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response" + + # If empty -> true network issue, we retry + if [ -z "$_comlaude_response" ]; then + _comlaude_retry=$((_comlaude_retry + 1)) + [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2 + continue + fi + + # 404 -> domain not found in that level. no retry : continue + if echo "$_comlaude_response" | grep -q '"status_code":404'; then + _debug "404 for $_comlaude_d, moving to next level (not retrying)" + break + fi + + # Domain missing (200 reply, data empty) -> continue + if echo "$_comlaude_response" | grep -q '"data":\[\]'; then + _debug "Empty data for $_comlaude_d, moving to next level" + break + fi + + # Extraction via _egrep_o + _comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')" + _comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')" + + if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then + break + fi + + # 200 reply but malformed data / noid -> retry transport + _comlaude_retry=$((_comlaude_retry + 1)) + [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2 + done + + _debug "_comlaude_DOM_ID=$_comlaude_DOM_ID" + _debug "_comlaude_Z_ID=$_comlaude_Z_ID" + + if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then + _comlaude_domain="$_comlaude_d" + _comlaude_domain_id="$_comlaude_DOM_ID" + _comlaude_zone_id="$_comlaude_Z_ID" + return 0 + fi + + _comlaude_i=$((_comlaude_i + 1)) + done +} +########## ADD TXT ########## + +dns_comlaude_add() { + fulldomain="$1" + txtvalue="$2" + + COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}" + COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}" + COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}" + COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" + + if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then + _err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)." + return 1 + fi + + # Backup variable after validation + _saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME" + _saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD" + _saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY" + _saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID" + + _info "Adding TXT: $fulldomain" + _comlaude_auth || return 1 + _comlaude_get_root "$fulldomain" || return 1 + + _debug "Root: $_comlaude_domain" + + _comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}" + + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + export _H2="Content-Type: application/json" + + _comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")" + + _H1="" + _H2="" + if ! echo "$_comlaude_response" | grep -q '"id"'; then + _err "Failed to create TXT" + _debug "$_comlaude_response" + return 1 + fi + + return 0 +} + +########## REMOVE TXT ########## + +dns_comlaude_rm() { + fulldomain="$1" + txtvalue="$2" + + COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}" + COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}" + COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}" + COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" + + _info "Removing TXT: $fulldomain" + + _comlaude_auth || return 1 + _comlaude_get_root "$fulldomain" || return 1 + + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + _comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)" + _comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)" + _comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value" + _comlaude_response="$(_get "$_comlaude_url")" + _H1="" + + _debug "Filtered records response: $_comlaude_response" + + # first "id" top-level of reply (record itself, + # always on first position of each data[] object) + _comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')" + + if [ -z "$_comlaude_record_id" ]; then + _info "No matching TXT record found to delete for $fulldomain / $txtvalue" + return 0 + fi + + _debug "Deleting record $_comlaude_record_id" + + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + _comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id" + _comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")" + _H1="" + + if echo "$_comlaude_del_resp" | grep -q '"error"'; then + _err "Delete failed for $_comlaude_record_id" + _debug "$_comlaude_del_resp" + return 1 + fi + + _info "Deleted record $_comlaude_record_id" + return 0 +} From cd486cfbb9d88a4ca4d173ba90f012d41e7a0564 Mon Sep 17 00:00:00 2001 From: Steven Qiu Date: Sun, 12 Jul 2026 16:15:59 +0800 Subject: [PATCH 642/689] Add Baidu Cloud CDN deploy hook (#6951) * add Baidu Cloud CDN deploy hook Code generated by GitHub Copilot with Claude Sonnet 4.6. Tested with local environment by human. * inline functions Code generated by OpenAI Codex with GPT-5.5 Sol. Tested with local environment by human. --- deploy/baidu_cdn.sh | 222 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 deploy/baidu_cdn.sh diff --git a/deploy/baidu_cdn.sh b/deploy/baidu_cdn.sh new file mode 100644 index 00000000..7fe31f9b --- /dev/null +++ b/deploy/baidu_cdn.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034,SC2154 + +# Deploy hook: Baidu Cloud CDN +# +# Code generated by GitHub Copilot with Claude Sonnet 4.6 and OpenAI Codex with GPT-5.6 Sol +# +# API Doc: https://cloud.baidu.com/doc/CDN/s/Zkna2r57w +# +# Uses the same credential variables as dnsapi/dns_baidu.sh: +# export Baidu_AK="your-access-key-id" +# export Baidu_SK="your-secret-access-key" +# +# To deploy to a CDN domain different from the certificate CN +# (e.g. wildcard or multi-domain certs): +# export DEPLOY_BAIDU_CDN_DOMAIN="cdn.example.com" +# +# Multiple CDN domains sharing the same certificate: +# export DEPLOY_BAIDU_CDN_DOMAIN="cdn1.example.com cdn2.example.com" + +BAIDU_CDN_HOST="cdn.baidubce.com" +_BAIDU_CDN_BCE_AUTH_RESULT="" + +baidu_cdn_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 ! _baidu_cdn_load_credentials; then + return 1 + fi + + _getdeployconf DEPLOY_BAIDU_CDN_DOMAIN + if [ "$DEPLOY_BAIDU_CDN_DOMAIN" ]; then + _savedeployconf DEPLOY_BAIDU_CDN_DOMAIN "$DEPLOY_BAIDU_CDN_DOMAIN" + else + DEPLOY_BAIDU_CDN_DOMAIN="$_cdomain" + fi + + # Build JSON "domains" array from space-separated domain list + _domains_json="" + for _d in $DEPLOY_BAIDU_CDN_DOMAIN; do + _d_e="$(_baidu_cdn_json_escape "$_d")" + if [ -z "$_domains_json" ]; then + _domains_json="\"${_d_e}\"" + else + _domains_json="${_domains_json},\"${_d_e}\"" + fi + done + + # Build a valid cert name: must start with a letter, allow [A-Za-z0-9-/.], max 65 chars + _cert_name="$(printf "%s" "$_cdomain" | sed 's/\*\./wildcard./g;s/[^A-Za-z0-9./]/-/g' | cut -c 1-65)" + case "$_cert_name" in + [A-Za-z]*) ;; + *) _cert_name="c${_cert_name}" ;; + esac + + # PEM content is already Base64 inside the -----BEGIN/END----- wrappers. + # The API expects the raw PEM as a JSON string, so newlines must be escaped as \n. + _cert_pem="$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')" + _key_pem="$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')" + + _debug2 _cert_name "$_cert_name" + _debug2 _domains_json "[$_domains_json]" + + # Build JSON payload + _payload="{\"domains\":[${_domains_json}],\"certificate\":{\"certName\":\"${_cert_name}\",\"certServerData\":\"${_cert_pem}\",\"certPrivateData\":\"${_key_pem}\"}}" + + # Generate BCE v1 authorization header (query string included in canonical request) + _cdn_path="/v2/domain/certificate" + _cdn_query="action=put" + _ts="$(_utc_date | sed 's/ /T/')Z" + _content_type="application/json; charset=utf-8" + _payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)" + + if ! _baidu_cdn_bce_auth "POST" "$_cdn_path" "$_cdn_query" "$BAIDU_CDN_HOST" "$_ts" "3600" "$_content_type" "$_payload_hash"; then + _err "Failed to sign request" + return 1 + fi + + _H1="Authorization: $_BAIDU_CDN_BCE_AUTH_RESULT" + _H2="x-bce-date: $_ts" + _H3="x-bce-content-sha256: $_payload_hash" + _H4="Host: $BAIDU_CDN_HOST" + _H5="" + + _url="https://${BAIDU_CDN_HOST}${_cdn_path}?${_cdn_query}" + response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")" + if [ "$?" != "0" ]; then + _err "Failed to call Baidu Cloud CDN API" + return 1 + fi + + _debug2 response "$response" + + if _contains "$response" "\"certId\""; then + _info "Certificate deployed to Baidu Cloud CDN for: $DEPLOY_BAIDU_CDN_DOMAIN" + return 0 + fi + + _err "Failed to deploy certificate to Baidu Cloud CDN: $response" + return 1 +} + +# BCE v1 signing with canonical query string support. +# The CDN endpoint uses ?action=put so it must be included in the canonical request. +_baidu_cdn_bce_auth() { + _method="$1" + _uri="$2" + _query="$3" + _host="$4" + _ts="$5" + _expire="$6" + _ct="$7" + _payload_hash="$8" + + _BAIDU_CDN_BCE_AUTH_RESULT="" + + _auth_prefix="bce-auth-v1/${Baidu_AK}/${_ts}/${_expire}" + _signed_headers="content-type;host;x-bce-content-sha256;x-bce-date" + _canonical_uri="$(_baidu_cdn_bce_encode_path "$_uri")" + + _host_e="$(printf "%s" "$_host" | _url_encode upper-hex)" + _date_e="$(printf "%s" "$_ts" | _url_encode upper-hex)" + _ct_e="$(printf "%s" "$_ct" | _url_encode upper-hex)" + _hash_e="$(printf "%s" "$_payload_hash" | _url_encode upper-hex)" + + _canonical_headers="content-type:${_ct_e} +host:${_host_e} +x-bce-content-sha256:${_hash_e} +x-bce-date:${_date_e}" + + _canonical_request="${_method} +${_canonical_uri} +${_query} +${_canonical_headers}" + + _sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")" + _signing_key="$(_baidu_cdn_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")" + _signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")" + _signature="$(_baidu_cdn_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")" + + _BAIDU_CDN_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}" +} + +_baidu_cdn_load_credentials() { + Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}" + Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}" + + Baidu_AK="$(_baidu_cdn_trim_ws "$Baidu_AK")" + Baidu_SK="$(_baidu_cdn_trim_ws "$Baidu_SK")" + + if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then + _err "Baidu_AK and Baidu_SK are required" + return 1 + fi + + _saveaccountconf_mutable Baidu_AK "$Baidu_AK" + _saveaccountconf_mutable Baidu_SK "$Baidu_SK" + + return 0 +} + +_baidu_cdn_bce_encode_path() { + _p="$1" + _out="" + if [ "${_p#"/"}" != "$_p" ]; then + _out="/" + fi + + _rest="${_p#/}" + while [ -n "$_rest" ]; do + _seg="${_rest%%/*}" + if [ "$_seg" ]; then + if [ -z "$_out" ] || [ "$_out" = "/" ]; then + _out="${_out}$(printf "%s" "$_seg" | _url_encode upper-hex)" + else + _out="${_out}/$(printf "%s" "$_seg" | _url_encode upper-hex)" + fi + fi + if [ "${_rest#*/}" = "$_rest" ]; then + break + fi + _rest="${_rest#*/}" + done + + if [ -z "$_out" ]; then + _out="/" + fi + printf "%s" "$_out" +} + +_baidu_cdn_trim_ws() { + printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//' +} + +_baidu_cdn_json_escape() { + _s="$1" + _s="$(printf "%s" "$_s" | tr -d '\r\n')" + printf "%s" "$_s" | + sed 's/\\/\\\\/g; s/ /\\t/g' | + _baidu_cdn_json_encode +} + +_baidu_cdn_json_encode() { + _j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")" + printf "%s" "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n" +} + +_baidu_cdn_hmac_sha256_hexkey() { + _key_hex="$1" + _msg="$2" + printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex +} From dd6540ce46a52f94d4ec37fa4e46cf5e717e3ad2 Mon Sep 17 00:00:00 2001 From: amk1969 <32023825+amk1969@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:21:00 +0200 Subject: [PATCH 643/689] dns_ovh specific permission for record removal (#6386) Co-authored-by: amk --- dnsapi/dns_ovh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_ovh.sh b/dnsapi/dns_ovh.sh index 9f2cd23f..df2b184d 100755 --- a/dnsapi/dns_ovh.sh +++ b/dnsapi/dns_ovh.sh @@ -224,7 +224,7 @@ _ovh_authentication() { _H3="" _H4="" - _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}' + _ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "GET","path": "/domain/zone/*/record/*"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}' response="$(_post "$_ovhdata" "$OVH_API/auth/credential")" _debug3 response "$response" From 5a8c685fd3e39b958cad84004ba495467b4f53ca Mon Sep 17 00:00:00 2001 From: Kat Crichton-Seager Date: Sun, 12 Jul 2026 09:37:21 +0100 Subject: [PATCH 644/689] fix: dnsexit api rejects a TTL of zero, changed to 1 (minute) (#7107) --- dnsapi/dns_dnsexit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_dnsexit.sh b/dnsapi/dns_dnsexit.sh index 6b10891c..b92482cf 100644 --- a/dnsapi/dns_dnsexit.sh +++ b/dnsapi/dns_dnsexit.sh @@ -25,7 +25,7 @@ dns_dnsexit_add() { return 1 fi - _dnsexit_zone_op add ',"ttl":0,"overwrite":false' + _dnsexit_zone_op add ',"ttl":1,"overwrite":false' } #Usage: fulldomain txtvalue From 447dc3c7e7e8fa45337980e2ad13e0185c1be4ff Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 17:21:03 +0800 Subject: [PATCH 645/689] dns_ali: convert IDN domains to punycode The Aliyun API only accepts punycode domain names, and a raw UTF-8 domain also breaks the request signature. Same _idn pattern as dns_namecom. https://github.com/acmesh-official/acme.sh/issues/4733 --- dnsapi/dns_ali.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index 62e54e0c..b8ca9169 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -18,7 +18,9 @@ Ali_DNS_API="https://alidns.aliyuncs.com/" #Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_ali_add() { - fulldomain=$1 + # the API only accepts punycode for IDN domains, and a raw UTF-8 domain + # also breaks the request signature (issue 4733) + fulldomain=$(_idn "$1") txtvalue=$2 _prepare_ali_credentials || return 1 @@ -33,7 +35,7 @@ dns_ali_add() { } dns_ali_rm() { - fulldomain=$1 + fulldomain=$(_idn "$1") txtvalue=$2 Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}" Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}" From 15a1067f1b0bc36c803c11e80ca965cf0e3e15d0 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 17:34:37 +0800 Subject: [PATCH 646/689] update-account: persist the new email into the CA conf "--update-account -m new@example.com" updated the contact on the CA but never saved it locally, so CA_EMAIL kept showing the old address on every later run. Save it in the success path like _regAccount does. https://github.com/acmesh-official/acme.sh/issues/4673 --- acme.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acme.sh b/acme.sh index 00ec7ef4..85dccda6 100755 --- a/acme.sh +++ b/acme.sh @@ -4179,6 +4179,12 @@ updateaccount() { if [ "$code" = '200' ]; then echo "$response" >"$ACCOUNT_JSON_PATH" _info "Account update success for $_accUri." + # persist the effective mailbox like _regAccount does; otherwise + # "--update-account -m new@..." updates the CA but the local conf + # keeps showing the old address (issue 4673) + if [ "$_email" ]; then + _savecaconf "CA_EMAIL" "$_email" + fi ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)" _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" From b9ce911eb16de4648670ad7947a8f565e24479d8 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 19:21:40 +0800 Subject: [PATCH 647/689] installcronjob: never wipe existing cron jobs when crontab -l fails Piping a failed 'crontab -l' straight back into 'crontab -' replaced the whole crontab with just the acme.sh entry when the listing failed while jobs existed (seen on cPanel/CloudLinux jailshell). Capture the listing first and refuse to write unless the failure is the normal "no crontab for user" case. https://github.com/acmesh-official/acme.sh/issues/3079 --- acme.sh | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/acme.sh b/acme.sh index 85dccda6..0b0f3ce9 100755 --- a/acme.sh +++ b/acme.sh @@ -7003,15 +7003,35 @@ installcronjob() { return 1 fi _info "Installing cron job" - if ! $_CRONTAB -l 2>/dev/null | grep "$PROJECT_ENTRY --cron"; then + _cron_entry="$random_minute $random_hour,$(_math "$random_hour" + 6),$(_math "$random_hour" + 12),$(_math "$random_hour" + 18) * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" + _cron_entries="$($_CRONTAB -l 2>/dev/null)" + if [ "$?" != "0" ]; then + #when the user has no crontab yet, crontab -l also exits non-zero; + #only that case may proceed with an empty list. Any other listing + #failure must abort: piping an incomplete list back into 'crontab -' + #would wipe the user's existing cron jobs (issue 3079) + _cron_list_err="$($_CRONTAB -l 2>&1 >/dev/null)" + if echo "$_cron_list_err" | grep -i "no crontab\|no fcrontab\|can't open" >/dev/null; then + _cron_entries="" + else + _err "Can not list the current cron jobs: $_cron_list_err" + _err "Refusing to install the cron job, that could wipe your existing cron jobs." + _err "Please add this cron job manually:" + _err "$_cron_entry" + return 1 + fi + fi + if ! echo "$_cron_entries" | grep "$PROJECT_ENTRY --cron"; then if _exists uname && uname -a | grep SunOS >/dev/null; then _CRONTAB_STDIN="$_CRONTAB --" else _CRONTAB_STDIN="$_CRONTAB -" fi - $_CRONTAB -l 2>/dev/null | { - cat - echo "$random_minute $random_hour,$(_math $random_hour + 6),$(_math $random_hour + 12),$(_math $random_hour + 18) * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null" + { + if [ "$_cron_entries" ]; then + echo "$_cron_entries" + fi + echo "$_cron_entry" } | $_CRONTAB_STDIN fi if [ "$?" != "0" ]; then From a6766d41868ce4aa991860a93f077eb8d460b9a9 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 19:21:48 +0800 Subject: [PATCH 648/689] dns_dnsimple: use mutable conf storage so a newly exported token wins The legacy plain _saveaccountconf copy in account.conf is sourced at startup and silently overrides a newly exported DNSimple_OAUTH_TOKEN, so rotated tokens never took effect. fixes https://github.com/acmesh-official/acme.sh/issues/3422 --- dnsapi/dns_dnsimple.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_dnsimple.sh b/dnsapi/dns_dnsimple.sh index e262239a..257549b4 100644 --- a/dnsapi/dns_dnsimple.sh +++ b/dnsapi/dns_dnsimple.sh @@ -18,6 +18,7 @@ dns_dnsimple_add() { fulldomain=$1 txtvalue=$2 + DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" if [ -z "$DNSimple_OAUTH_TOKEN" ]; then DNSimple_OAUTH_TOKEN="" _err "You have not set the dnsimple oauth token yet." @@ -26,7 +27,7 @@ dns_dnsimple_add() { fi # save the oauth token for later - _saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN" + _saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN" if ! _get_account_id; then _err "failed to retrieve account id" @@ -57,6 +58,12 @@ dns_dnsimple_add() { dns_dnsimple_rm() { fulldomain=$1 + DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" + if [ -z "$DNSimple_OAUTH_TOKEN" ]; then + _err "You have not set the dnsimple oauth token yet." + return 1 + fi + if ! _get_account_id; then _err "failed to retrieve account id" return 1 @@ -123,9 +130,9 @@ _get_root() { # returns _account_id _get_account_id() { - DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf DNSimple_ACCOUNT_ID)}" + DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf_mutable DNSimple_ACCOUNT_ID)}" if [ "$DNSimple_ACCOUNT_ID" ]; then - _saveaccountconf DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID" + _saveaccountconf_mutable DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID" _account_id="$DNSimple_ACCOUNT_ID" _debug _account_id "$_account_id" return 0 From 3fddea296207de9754727ebb5349ec50b0e8b176 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 19:21:56 +0800 Subject: [PATCH 649/689] dns_dynu: use mutable conf storage so newly exported credentials win Same stale-account.conf override as dns_dnsimple: the sourced plain copy shadowed newly exported Dynu_ClientId/Dynu_Secret. https://github.com/acmesh-official/acme.sh/issues/3216 --- dnsapi/dns_dynu.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_dynu.sh b/dnsapi/dns_dynu.sh index 612756c5..3ac5c2f1 100644 --- a/dnsapi/dns_dynu.sh +++ b/dnsapi/dns_dynu.sh @@ -23,6 +23,8 @@ dns_dynu_add() { fulldomain=$1 txtvalue=$2 + Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}" + Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}" if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then Dynu_ClientId="" Dynu_Secret="" @@ -32,8 +34,8 @@ dns_dynu_add() { fi #save the client id and secret to the account conf file. - _saveaccountconf Dynu_ClientId "$Dynu_ClientId" - _saveaccountconf Dynu_Secret "$Dynu_Secret" + _saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId" + _saveaccountconf_mutable Dynu_Secret "$Dynu_Secret" if [ -z "$Dynu_Token" ]; then _info "Getting Dynu token." @@ -69,6 +71,8 @@ dns_dynu_rm() { fulldomain=$1 txtvalue=$2 + Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}" + Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}" if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then Dynu_ClientId="" Dynu_Secret="" @@ -78,8 +82,8 @@ dns_dynu_rm() { fi #save the client id and secret to the account conf file. - _saveaccountconf Dynu_ClientId "$Dynu_ClientId" - _saveaccountconf Dynu_Secret "$Dynu_Secret" + _saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId" + _saveaccountconf_mutable Dynu_Secret "$Dynu_Secret" if [ -z "$Dynu_Token" ]; then _info "Getting Dynu token." From 5e33e9f5f170a49a269ddcff9c50ab226ae88d4f Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 19:22:03 +0800 Subject: [PATCH 650/689] dns_knot: add KNOT_ZONE for delegated subdomain zones The zone cannot be derived from the record name when the Knot server is only authoritative for a delegated subdomain; let the user name it explicitly, like NSUPDATE_ZONE. fixes https://github.com/acmesh-official/acme.sh/issues/2881 --- dnsapi/dns_knot.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dnsapi/dns_knot.sh b/dnsapi/dns_knot.sh index b5ba32f0..2b6d8ef4 100644 --- a/dnsapi/dns_knot.sh +++ b/dnsapi/dns_knot.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_knot Options: KNOT_SERVER Server hostname. Default: "localhost". KNOT_KEY TSIG key data, not a file path. knsupdate "key" statement format: "[alg:]name secret". E.g. "hmac-sha256:acme_key BASE64SECRET=" + KNOT_ZONE Zone name. Optional, set it when the challenge record lives in a delegated subdomain zone. Default: the parent domain of the challenge record. ' # See also dns_nsupdate.sh @@ -21,6 +22,9 @@ dns_knot_add() { # save the dns server and key to the account.conf file. _saveaccountconf KNOT_SERVER "${KNOT_SERVER}" _saveaccountconf KNOT_KEY "${KNOT_KEY}" + if [ -n "${KNOT_ZONE}" ]; then + _saveaccountconf KNOT_ZONE "${KNOT_ZONE}" + fi if ! _get_root "$fulldomain"; then _err "Domain does not exist." @@ -84,6 +88,13 @@ EOF # _domain=domain.com _get_root() { domain=$1 + # a delegated subdomain zone cannot be derived from the record name; + # let the user name the zone explicitly (issue 2881) + if [ -n "${KNOT_ZONE}" ]; then + _domain="${KNOT_ZONE%.}" + _debug "Using KNOT_ZONE zone" "${_domain}" + return 0 + fi i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" i=$(_math "$i" - 1) From d621d6952aa1c815658bacd0aa2af44b4136d708 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 20:18:36 +0800 Subject: [PATCH 651/689] fix --- acme.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 0b0f3ce9..ecea7bc2 100755 --- a/acme.sh +++ b/acme.sh @@ -7011,7 +7011,9 @@ installcronjob() { #failure must abort: piping an incomplete list back into 'crontab -' #would wipe the user's existing cron jobs (issue 3079) _cron_list_err="$($_CRONTAB -l 2>&1 >/dev/null)" - if echo "$_cron_list_err" | grep -i "no crontab\|no fcrontab\|can't open" >/dev/null; then + #multiple -e instead of \| : BRE alternation is a GNU extension that + #BSD grep does not support + if echo "$_cron_list_err" | grep -i -e "no crontab" -e "no fcrontab" -e "can't open" >/dev/null; then _cron_entries="" else _err "Can not list the current cron jobs: $_cron_list_err" From 17964cfd6ef177988e07bd34a9f0c0388cd597db Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 12 Jul 2026 22:28:56 +0800 Subject: [PATCH 652/689] installcronjob: Solaris grep takes only one -e, use separate greps The "no crontab" whitelist used multiple -e patterns, but Solaris /usr/bin/grep honors only a single -e, so a fresh install was refused there. Use one plain grep per message pattern, which every grep implementation supports (caught by le_test_installcronjob_no_wipe on the Solaris CI). --- acme.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index ecea7bc2..4615543b 100755 --- a/acme.sh +++ b/acme.sh @@ -7011,9 +7011,11 @@ installcronjob() { #failure must abort: piping an incomplete list back into 'crontab -' #would wipe the user's existing cron jobs (issue 3079) _cron_list_err="$($_CRONTAB -l 2>&1 >/dev/null)" - #multiple -e instead of \| : BRE alternation is a GNU extension that - #BSD grep does not support - if echo "$_cron_list_err" | grep -i -e "no crontab" -e "no fcrontab" -e "can't open" >/dev/null; then + #separate greps: BRE alternation \| is a GNU extension and Solaris + #grep takes only a single -e pattern + if echo "$_cron_list_err" | grep -i "no crontab" >/dev/null || + echo "$_cron_list_err" | grep -i "no fcrontab" >/dev/null || + echo "$_cron_list_err" | grep -i "can't open" >/dev/null; then _cron_entries="" else _err "Can not list the current cron jobs: $_cron_list_err" From 020123d812e15fcce07d174e1ad74637c7c56c75 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 13 Jul 2026 08:30:43 +0800 Subject: [PATCH 653/689] dns_infomaniak: log zones response and fail early in _get_zone The sed in _get_zone passed the raw JSON through when the response contained no [{"fqdn":", so an API error turned the zone into "{" and the failure only surfaced later as POST /2/zones/{/records "method_not_found". Log the response at debug2, error out on non-success results, and parse fqdn position-independently. https://github.com/acmesh-official/acme.sh/issues/6851 --- dnsapi/dns_infomaniak.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index 43ade8ce..6cc8a4a0 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -182,7 +182,11 @@ dns_infomaniak_rm() { _get_zone() { domain="$1" # Whatever the domain is, you can get the fqdn with the following. - # shellcheck disable=SC1004 - response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/') - echo "${response%%\"*}" + response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones") + _debug2 "_get_zone response" "$response" + if ! _contains "$response" '"result":"success"'; then + _err "cannot get zones for ${domain}, response: ${response}" + return 1 + fi + echo "$response" | _egrep_o '"fqdn" *: *"[^"]*"' | _head_n 1 | cut -d '"' -f 4 } From 1a54307dbf3e8a1bebb3ddf3bf4efcf964b7557c Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 13 Jul 2026 10:43:08 +0800 Subject: [PATCH 654/689] account: keep restored ACCOUNT_URL in the EAB-already-bound path When re-registering an already-bound EAB account (HTTP 400 "not awaiting external account binding"), the else branch restored ACCOUNT_URL from ca.conf but the following unconditional `export ACCOUNT_URL="$_accUri"` clobbered it with an empty _accUri (never set on that path), so later signed requests failed with "A Key ID MUST be specified" / "account URL is empty". Assign the restored value to _accUri so the shared export uses it. https://github.com/acmesh-official/acme.sh/issues/3382 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 4615543b..b981b54e 100755 --- a/acme.sh +++ b/acme.sh @@ -4125,7 +4125,7 @@ _regAccount() { fi _savecaconf "ACCOUNT_URL" "$_accUri" else - ACCOUNT_URL="$(_readcaconf ACCOUNT_URL)" + _accUri="$(_readcaconf ACCOUNT_URL)" fi export ACCOUNT_URL="$_accUri" From 4558a8aa98d444db2dbbffdef38ab478b4229d09 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 13 Jul 2026 11:59:35 +0800 Subject: [PATCH 655/689] challenge: use portable [{] literal-brace in _egrep_o patterns (#968) _egrep_o falls back to a BRE sed expression on shells without egrep -o (Solaris, DD-WRT busybox). A bare "\{" there is a BRE interval operator and aborts ("sed: command garbled" / "Invalid content of \{\}"), so the challenge-status-invalid path extracted an empty error object and the CA's failure reason was lost. Replace the escaped braces with "[{]"/"[}]" bracket expressions, unambiguous literal braces in both BRE and ERE, at all four call sites (challenge type/error extraction and profiles). --- acme.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/acme.sh b/acme.sh index b981b54e..b53601ee 100755 --- a/acme.sh +++ b/acme.sh @@ -5304,7 +5304,7 @@ $_authorizations_map" fi # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 - entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^{]*"type":"'$vtype'"[^}]*')" _debug entry "$entry" if [ -z "$keyauthorization" -a -z "$entry" ]; then @@ -5636,7 +5636,7 @@ $_authorizations_map" status=$(echo "$response" | _egrep_o '"status":"[^"]*' | cut -d : -f 2 | tr -d '"') _debug2 status "$status" if _contains "$status" "invalid"; then - error="$(echo "$response" | _egrep_o '"error":\{[^\}]*')" + error="$(echo "$response" | _egrep_o '"error":[{][^}]*')" _debug2 error "$error" errordetail="$(echo "$error" | _egrep_o '"detail": *"[^"]*' | cut -d '"' -f 4)" _debug2 errordetail "$errordetail" @@ -6581,7 +6581,7 @@ list_profiles() { fi normalized_response=$(echo "$response" | _normalizeJson) - profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *\{[^\}]*\}') + profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *[{][^}]*[}]') if [ -z "$profiles_json" ]; then _info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint." @@ -7264,7 +7264,7 @@ _deactivate() { _debug "Trigger validation." vtype="$(_getIdType "$_d_domain")" # Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018 - entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" + entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^{]*"type":"'$vtype'"[^}]*')" _debug entry "$entry" if [ -z "$entry" ]; then _err "$d: Cannot get domain token" From 1a746d98b8d282435450446aae424df53616c39a Mon Sep 17 00:00:00 2001 From: NotAnotherHelloWorld Date: Tue, 14 Jul 2026 03:02:34 +0200 Subject: [PATCH 656/689] Deploy certificate to FortiGate firewall using API (#6236) * Deploy certificate to FortiGate firewall using API * Refactor FortiGate deployment functions Prefix private functions and working variables and use a timestamped certificate name. * Replace grep -o for POSIX compatibility --- deploy/fortigate.sh | 175 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 deploy/fortigate.sh diff --git a/deploy/fortigate.sh b/deploy/fortigate.sh new file mode 100644 index 00000000..f00ca1cb --- /dev/null +++ b/deploy/fortigate.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env sh +# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate. +# +# FortiGate's native ACME integration does not support wildcard certificates or domain validation, +# and is not supported if you have a custom management web port (eg. DNAT web traffic). +# +# REQUIRED: +# export FGT_HOST="fortigate_hostname-or-ip" +# export FGT_TOKEN="fortigate_api_token" +# +# OPTIONAL: +# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set) +# +# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script. +# `--insecure` is required on first run if not already using a valid SSL certificate on firewall. + +# Function to parse a FortiGate API response +_fortigate_parse_response() { + _fortigate_response="$1" + _fortigate_func="$2" + _fortigate_status=$(echo "$_fortigate_response" | _egrep_o '"status":[ ]*"[^"]*"' | cut -d '"' -f 4) + + if [ "$_fortigate_status" != "success" ]; then + _err "[$_fortigate_func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot." + return 1 + fi + + _debug "[$_fortigate_func] Operation successful." + return 0 +} + +# Function to deploy a base64-encoded certificate to the firewall +_fortigate_deployer() { + _fortigate_cert_base64=$(_base64 <"$_fortigate_cfullchain" | tr -d '\n') + _fortigate_key_base64=$(_base64 <"$_fortigate_ckey" | tr -d '\n') + _fortigate_payload=$( + cat < Date: Tue, 14 Jul 2026 22:58:47 +1000 Subject: [PATCH 657/689] fix(dns_oci): read ~/.oci/config before cached account.conf values (#7124) The OCI DNS plugin cached the tenancy, user, region and signing key into acme.sh's account.conf at issuance and then, on subsequent runs, read those cached values before consulting ~/.oci/config. A value cached at issuance therefore permanently shadowed the config file, so editing ~/.oci/config afterwards (most visibly rotating the API signing key) had no effect and renewals kept using stale credentials, failing authentication. Resolve each field in the order: explicit environment variable, then ~/.oci/config when it exists, then the cached account.conf value as a fallback for env-only installs that have no config file. The signing key likewise prefers the key_file resolved from the environment or ~/.oci/config over any cached key. Signed-off-by: Avi Miller --- dnsapi/dns_oci.sh | 53 +++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/dnsapi/dns_oci.sh b/dnsapi/dns_oci.sh index c76a4565..e1aa3dd9 100644 --- a/dnsapi/dns_oci.sh +++ b/dnsapi/dns_oci.sh @@ -115,12 +115,15 @@ _oci_config() { _clearaccountconf_mutable OCI_CLI_PROFILE fi - OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readaccountconf_mutable OCI_CLI_TENANCY)}" + if [ -z "$OCI_CLI_TENANCY" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_TENANCY=$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE") + fi + if [ -z "$OCI_CLI_TENANCY" ]; then + OCI_CLI_TENANCY=$(_readaccountconf_mutable OCI_CLI_TENANCY) + fi if [ "$OCI_CLI_TENANCY" ]; then _saveaccountconf_mutable OCI_CLI_TENANCY "$OCI_CLI_TENANCY" - elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then - _debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE" - OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE")}" fi if [ -z "$OCI_CLI_TENANCY" ]; then @@ -128,41 +131,47 @@ _oci_config() { return 1 fi - OCI_CLI_USER="${OCI_CLI_USER:-$(_readaccountconf_mutable OCI_CLI_USER)}" + if [ -z "$OCI_CLI_USER" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_USER=$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE") + fi + if [ -z "$OCI_CLI_USER" ]; then + OCI_CLI_USER=$(_readaccountconf_mutable OCI_CLI_USER) + fi if [ "$OCI_CLI_USER" ]; then _saveaccountconf_mutable OCI_CLI_USER "$OCI_CLI_USER" - elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then - _debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE" - OCI_CLI_USER="${OCI_CLI_USER:-$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE")}" fi if [ -z "$OCI_CLI_USER" ]; then _err "Error: unable to read OCI_CLI_USER from config file or environment variable." return 1 fi - OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readaccountconf_mutable OCI_CLI_REGION)}" + if [ -z "$OCI_CLI_REGION" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then + _debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE" + OCI_CLI_REGION=$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE") + fi + if [ -z "$OCI_CLI_REGION" ]; then + OCI_CLI_REGION=$(_readaccountconf_mutable OCI_CLI_REGION) + fi if [ "$OCI_CLI_REGION" ]; then _saveaccountconf_mutable OCI_CLI_REGION "$OCI_CLI_REGION" - elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then - _debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE" - OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE")}" fi if [ -z "$OCI_CLI_REGION" ]; then _err "Error: unable to read OCI_CLI_REGION from config file or environment variable." return 1 fi - OCI_CLI_KEY="${OCI_CLI_KEY:-$(_readaccountconf_mutable OCI_CLI_KEY)}" - if [ -z "$OCI_CLI_KEY" ]; then - _clearaccountconf_mutable OCI_CLI_KEY - OCI_CLI_KEY_FILE="${OCI_CLI_KEY_FILE:-$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE")}" - if [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then - _debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE" - OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE") - _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" - fi - else + if [ -z "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then + OCI_CLI_KEY_FILE=$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE") + fi + if [ "$OCI_CLI_KEY" ]; then _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" + elif [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then + _debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE" + OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE") + _saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY" + else + OCI_CLI_KEY=$(_readaccountconf_mutable OCI_CLI_KEY) fi if [ -z "$OCI_CLI_KEY_FILE" ] && [ -z "$OCI_CLI_KEY" ]; then From 1dbabf0da95c8803012b3d006ed7094962ac9761 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 14 Jul 2026 21:12:43 +0800 Subject: [PATCH 658/689] valid-to: allow a negative --days to set the renewal margin A negative --days is anchored to the expiry, so it composes with a relative --valid-to: "--valid-to +30d --days -7" renews 7 days before the expiry instead of the hardcoded 1 day. A positive --days and any --days with a fixed-date --valid-to are still rejected. https://github.com/acmesh-official/acme.sh/issues/6570 --- acme.sh | 55 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/acme.sh b/acme.sh index b53601ee..3a73cd77 100755 --- a/acme.sh +++ b/acme.sh @@ -1995,6 +1995,26 @@ _calc_next_renew_time() { printf "%s" "$_cnrt_next" } +#Usage: _calc_validto_renew_time notaftertime renewaldays now +#Prints the next renew time for a cert issued with a relative --valid-to. +#A negative renewaldays is anchored to the expiry: notaftertime + +#renewaldays*86400. Otherwise the cert renews one day before the expiry, +#or one hour before for certs whose lifetime is 24 hours or less. +_calc_validto_renew_time() { + _cvrt_end="$1" + _cvrt_days="$2" + _cvrt_now="$3" + if [ "$_cvrt_days" ] && [ "$_cvrt_days" -lt 0 ]; then + _math "$_cvrt_end" + "$_cvrt_days" \* 24 \* 60 \* 60 + return 0 + fi + if [ "$(_math "$_cvrt_end" - "$_cvrt_now")" -gt 86400 ]; then + _math "$_cvrt_end" - 86400 + else + _math "$_cvrt_end" - 3600 + fi +} + _mktemp() { if _exists mktemp; then if mktemp 2>/dev/null; then @@ -5950,19 +5970,8 @@ $_authorizations_map" _info "It cannot be renewed automatically" _info "See: $_VALIDITY_WIKI" else - _now=$(_time) - _debug2 "_now" "$_now" - _lifetime=$(_math $Le_NextRenewTime - $_now) - _debug2 "_lifetime" "$_lifetime" - if [ $_lifetime -gt 86400 ]; then - #if lifetime is logner than one day, it will renew one day before - Le_NextRenewTime=$(_math $Le_NextRenewTime - 86400) - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - else - #if lifetime is less than 24 hours, it will renew one hour before - Le_NextRenewTime=$(_math $Le_NextRenewTime - 3600) - Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") - fi + Le_NextRenewTime=$(_calc_validto_renew_time "$Le_NextRenewTime" "$Le_RenewalDays" "$(_time)") + Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime") fi elif [ "$Le_RenewalDays" -lt "0" ]; then _enddate_value=$(_enddate "$CERT_PATH") @@ -8050,6 +8059,7 @@ Parameters: Multiple emails can be given as a comma-separated list: 'a@example.com,b@example.com' --accountkey Specifies the account key path, only valid for the '--install' command. --days Specifies the days to renew the cert when using '--issue' command. The default value is $DEFAULT_RENEW days. + A negative value renews that many days before the cert expiry. Negative values could be used to specify a number of days relative to the expiration date of the certificate. --httpport Specifies the standalone listening port. Only valid if the server is behind a reverse proxy or load balancer. --tlsport Specifies the standalone tls listening port. Only valid if the server is behind a reverse proxy or load balancer. @@ -9044,13 +9054,20 @@ _process() { _debug2 LE_WORKING_DIR "$LE_WORKING_DIR" - # --days and --valid-to are mutually exclusive by design: --valid-to pins - # the cert lifetime and the renewal time follows the expiry, so a - # creation-based --days schedule can not apply. + # --valid-to pins the cert lifetime, so a creation-anchored (positive) + # --days schedule can not apply and is rejected. A negative --days is + # anchored to the expiry and composes with a relative --valid-to: the + # cert renews that many days before the expiry. if [ "$_days" ] && [ "$_valid_to" ]; then - _err "--days can not be used together with --valid-to." - _err "With --valid-to, the renewal time is derived from the expiry time automatically." - return 1 + if ! _startswith "$_valid_to" "+"; then + _err "--days can not be used together with a fixed-date --valid-to: such a cert can not be renewed automatically." + return 1 + fi + if ! _startswith "$_days" "-"; then + _err "A positive --days can not be used together with --valid-to, the renewal time is derived from the expiry time." + _err "Use a negative --days to renew that many days before the expiry, or omit --days to renew 1 day before the expiry." + return 1 + fi fi if [ "$DEBUG" ]; then From 3faf65c46d7c35fde3f0a35f64092f1d9b0f6967 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 15 Jul 2026 20:41:35 +0800 Subject: [PATCH 659/689] fix https://github.com/acmesh-official/acme.sh/issues/1940#issuecomment-4971257867 --- acme.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 3a73cd77..a5f53469 100755 --- a/acme.sh +++ b/acme.sh @@ -5838,7 +5838,7 @@ $_authorizations_map" return 1 fi - echo "$response" >"$CERT_PATH" + echo "$response" | _strip_blank_lines >"$CERT_PATH" _split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH" if [ -z "$_preferred_chain" ]; then _preferred_chain=$(_readcaconf DEFAULT_PREFERRED_CHAIN) @@ -5865,7 +5865,7 @@ $_authorizations_map" _relcert="$CERT_PATH.alt" _relfullchain="$CERT_FULLCHAIN_PATH.alt" _relca="$CA_CERT_PATH.alt" - echo "$response" >"$_relcert" + echo "$response" | _strip_blank_lines >"$_relcert" _split_cert_chain "$_relcert" "$_relfullchain" "$_relca" if [ "$DEBUG" ]; then _debug "rel chain issuers: " "$(_get_chain_issuers "$_relfullchain")" @@ -6072,6 +6072,15 @@ $_authorizations_map" } #in_out_cert out_fullchain out_ca +#Reads a PEM chain from stdin, prints it without the blank lines. +#Some CAs (Let's Encrypt) separate the certificates of a chain with a blank +#line, others (ZeroSSL) don't. The blank lines are valid PEM (RFC 7468), but +#some devices and APIs reject them, so the certs are stored back to back. +#https://github.com/acmesh-official/acme.sh/issues/1940 +_strip_blank_lines() { + sed '/^[[:space:]]*$/d' +} + _split_cert_chain() { _certf="$1" _fullchainf="$2" From a82cf763cfe8a69f8329f4f0d2cfc4b5a8cb6240 Mon Sep 17 00:00:00 2001 From: Achmad Alif Nasrulloh Date: Thu, 16 Jul 2026 11:19:07 +0700 Subject: [PATCH 660/689] fix(notify): remove duplicate Content-Type header in waha hook --- notify/waha.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/notify/waha.sh b/notify/waha.sh index 989f57ac..573295e9 100755 --- a/notify/waha.sh +++ b/notify/waha.sh @@ -57,9 +57,8 @@ waha_send() { _debug "_data" "$_data" - export _H1="Content-Type: application/json" if [ "$WAHA_API_KEY" ]; then - export _H2="X-Api-Key: $WAHA_API_KEY" + export _H1="X-Api-Key: $WAHA_API_KEY" fi _waha_url="${WAHA_URL}/api/sendText" From 6d559ae69f6b18bd1b4e0087dafba695acbbd933 Mon Sep 17 00:00:00 2001 From: ACHMAD ALIF NASRULLOH <106044706+achmadalifn4@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:29:39 +0700 Subject: [PATCH 661/689] Add newline at end of waha.sh Fix missing newline at end of file. --- notify/waha.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify/waha.sh b/notify/waha.sh index bbb1dc44..573295e9 100755 --- a/notify/waha.sh +++ b/notify/waha.sh @@ -71,4 +71,4 @@ waha_send() { _err "waha send error." _err "$response" return 1 -} \ No newline at end of file +} From 745a42f1936cff1d9853ae7ee9d6e12c1f98d344 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 16 Jul 2026 20:09:10 +0800 Subject: [PATCH 662/689] minor --- .github/workflows/issue.yml | 45 ++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml index 00e9ddc5..a25cd4ef 100644 --- a/.github/workflows/issue.yml +++ b/.github/workflows/issue.yml @@ -2,6 +2,8 @@ name: "Update issues" on: issues: types: [opened] + issue_comment: + types: [created] pull_request_target: types: [opened] @@ -32,6 +34,32 @@ jobs: } catch (e) { core.warning(`Failed to fetch the blacklist: ${e}`); } + // A comment on a closed tracking issue reopens it (the standard + // closing note promises this). Bots, blacklisted users and the + // maintainer's own comments don't reopen. + if (context.eventName === "issue_comment") { + const issue = context.payload.issue; + const commenter = context.payload.comment.user; + if (issue.pull_request || issue.state !== "closed") { + return; + } + if (!/^report\s+(bugs?|issues?)\b/i.test(issue.title)) { + return; + } + if (commenter.type === "Bot" || + commenter.login.toLowerCase() === "neilpang" || + blacklist.includes(commenter.login.toLowerCase())) { + return; + } + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: "open" + }); + return; + } + if (blacklist.includes(item.user.login.toLowerCase())) { if (context.payload.pull_request) { await github.rest.pulls.update({ @@ -63,7 +91,9 @@ jobs: } if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) { // Tracking issue for a third-party dns/deploy/notify api: - // no upgrade boilerplate; assign it to the opener and label it. + // no upgrade boilerplate; assign it to the opener, label it, + // then close it right away to keep the issue list clean. Any + // later comment reopens it (see the issue_comment handler). await github.rest.issues.addAssignees({ owner: context.repo.owner, repo: context.repo.repo, @@ -76,6 +106,19 @@ jobs: issue_number: issue.number, labels: ["3rd party api"] }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: "Closing this tracking issue for now to keep the issue list clean. It remains the place to report problems with this provider -- if you hit a bug, comment here and the issue will be reopened." + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: "closed", + state_reason: "completed" + }); return; } await github.rest.issues.createComment({ From a836e747d1aea2d64e393bb5046471325e6dfa3a Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 17 Jul 2026 10:10:07 +0800 Subject: [PATCH 663/689] start 3.1.5 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index a5f53469..5fa6882a 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.4 +VER=3.1.5 PROJECT_NAME="acme.sh" From 59a97d7f8b765a9ff0a27bc408b939903b46e551 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 17 Jul 2026 12:38:14 +0800 Subject: [PATCH 664/689] fix bug for solaris. dnsapi/deploy: remove POSIX character classes from sed/grep patterns Solaris /usr/bin/sed and /usr/bin/grep parse [[:space:]] etc. as a literal bracket set and silently mis-match. Replace with [ ]* for JSON matching, a printf-tab bracket for user-input trimming, and [0-9] for digits; also drop GNU-only sed -r/-E in rage4, selfhost and selectel, and reuse _strip_blank_lines in byteplus_alb. --- .github/workflows/vtag.yml | 32 ++++++++++++++++++++++++++++++++ acme.sh | 5 ++++- deploy/byteplus_alb.sh | 4 ++-- dnsapi/dns_bhosted.sh | 6 +++--- dnsapi/dns_creoline.sh | 8 ++++---- dnsapi/dns_czechia.sh | 13 ++++++++----- dnsapi/dns_hostup.sh | 8 ++++---- dnsapi/dns_infoblox_uddi.sh | 4 ++-- dnsapi/dns_poweradmin.sh | 2 +- dnsapi/dns_rage4.sh | 2 +- dnsapi/dns_selectel.sh | 2 +- dnsapi/dns_selfhost.sh | 7 +++++-- dnsapi/dns_udr.sh | 4 ++-- dnsapi/dns_yandex360.sh | 4 ++-- 14 files changed, 71 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/vtag.yml diff --git a/.github/workflows/vtag.yml b/.github/workflows/vtag.yml new file mode 100644 index 00000000..e9f7e8df --- /dev/null +++ b/.github/workflows/vtag.yml @@ -0,0 +1,32 @@ +name: Mirror version tag + +# Historical release tags are plain version numbers ("3.1.3") and cannot be +# renamed. When a plain version tag is pushed (including the tag created by +# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3") +# pointing to the same object, so both forms exist. +# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and +# refs created with GITHUB_TOKEN do not fire workflows anyway. + +on: + push: + tags: + - '[0-9]*' + +permissions: + contents: write + +jobs: + vtag: + if: github.repository == 'acmesh-official/acme.sh' + runs-on: ubuntu-latest + steps: + - name: Create the v-prefixed tag + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh api "repos/${{ github.repository }}/git/ref/tags/v${{ github.ref_name }}" >/dev/null 2>&1; then + echo "Tag v${{ github.ref_name }} already exists, nothing to do." + exit 0 + fi + gh api "repos/${{ github.repository }}/git/refs" -f ref="refs/tags/v${{ github.ref_name }}" -f sha="${{ github.sha }}" + echo "Created tag v${{ github.ref_name }} -> ${{ github.sha }}" diff --git a/acme.sh b/acme.sh index 5fa6882a..753b1159 100755 --- a/acme.sh +++ b/acme.sh @@ -6078,7 +6078,10 @@ $_authorizations_map" #some devices and APIs reject them, so the certs are stored back to back. #https://github.com/acmesh-official/acme.sh/issues/1940 _strip_blank_lines() { - sed '/^[[:space:]]*$/d' + #spell out space and tab: Solaris sed treats [[:space:]] as a literal + #bracket set and silently stops matching the blank lines + _sbl_tab="$(printf '\t')" + sed "/^[ $_sbl_tab]*\$/d" } _split_cert_chain() { diff --git a/deploy/byteplus_alb.sh b/deploy/byteplus_alb.sh index 8443bb99..394b431f 100644 --- a/deploy/byteplus_alb.sh +++ b/deploy/byteplus_alb.sh @@ -163,8 +163,8 @@ byteplus_alb_deploy() { # ── 3. Read cert and key ───────────────────────────────────────────────────── # BytePlus requires NO blank lines between PEM blocks in the certificate chain - _public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r') - _private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r') + _public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r') + _private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r') if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then _err "Failed to read certificate or key file." diff --git a/dnsapi/dns_bhosted.sh b/dnsapi/dns_bhosted.sh index 1493c60a..46ddd5dd 100644 --- a/dnsapi/dns_bhosted.sh +++ b/dnsapi/dns_bhosted.sh @@ -323,21 +323,21 @@ _bhosted_extract_id() { fi # JSON: "id":12345 - _id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + _id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')" if [ -n "$_id" ]; then printf "%s" "$_id" return 0 fi # key=value: id=12345 - _id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + _id="$(printf "%s" "$_resp" | _egrep_o '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')" if [ -n "$_id" ]; then printf "%s" "$_id" return 0 fi # "record id 12345" / "recordid 12345" - _id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + _id="$(printf "%s" "$_resp" | _egrep_o '(record[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')" if [ -n "$_id" ]; then printf "%s" "$_id" return 0 diff --git a/dnsapi/dns_creoline.sh b/dnsapi/dns_creoline.sh index 9d04af4f..f4d76f8e 100644 --- a/dnsapi/dns_creoline.sh +++ b/dnsapi/dns_creoline.sh @@ -75,7 +75,7 @@ dns_creoline_rm() { return 1 fi - record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + record_id=$(echo "$response" | _egrep_o "\"id\"[ ]*:[ ]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") _debug "record_id" "$record_id" if [ -z "$record_id" ]; then @@ -108,10 +108,10 @@ _get_root() { return 1 fi - _sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") _debug _sub_domain "$_sub_domain" - _domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") + _domain=$(echo "$response" | _egrep_o "\"domain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ") _debug _domain "$_domain" if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then @@ -171,7 +171,7 @@ _creoline_rest() { _err "URI:$uri" return 1 elif _contains "$response" "message"; then - message=$(echo "$response" | _egrep_o "\"message\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \") + message=$(echo "$response" | _egrep_o "\"message\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \") _err "Error: $message" _err "URI:$uri" return 1 diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index e2ffcf50..6ad60442 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -30,8 +30,9 @@ dns_czechia_add() { return 1 fi - _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') - _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + _czechia_tab="$(printf '\t')" + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//") + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//") if [ -z "$_cz" ] || [ -z "$_tk" ]; then _err "Missing zone or CZ_AuthorizationToken." @@ -108,8 +109,9 @@ dns_czechia_rm() { return 1 fi - _cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') - _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + _czechia_tab="$(printf '\t')" + _cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//") + _tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//") if [ -z "$_cz" ] || [ -z "$_tk" ]; then _err "Missing zone or CZ_AuthorizationToken." @@ -180,12 +182,13 @@ _czechia_load_conf() { } _czechia_pick_zone() { + _czechia_pz_tab="$(printf '\t')" _fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//') _best_zone="" _zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g') for _z in $_zones_space; do - _clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//') + _clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//") [ -z "$_clean_z" ] && continue case "$_fd" in diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 73189189..a3d9174a 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -441,18 +441,18 @@ _hostup_json_extract() { input="${2:-$line}" # First try to extract quoted values (strings) - quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | _head_n 1)" + quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*\"[^\"]*\"" | _head_n 1)" if [ -n "$quoted_match" ]; then printf "%s" "$quoted_match" | cut -d : -f2- | - sed 's/^[[:space:]]*"//' | - sed 's/"[[:space:]]*$//' | + sed 's/^[ ]*"//' | + sed 's/"[ ]*$//' | sed 's/\\"/"/g' return 0 fi # Fallback for unquoted values (e.g., numeric IDs) - unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | _head_n 1)" + unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*[^,}]*" | _head_n 1)" if [ -n "$unquoted_match" ]; then printf "%s" "$unquoted_match" | cut -d : -f2- | diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 4b15088a..902cc700 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -117,7 +117,7 @@ dns_infoblox_uddi_rm() { return 0 fi - record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + record_id=$(echo "$response" | _egrep_o '"id":[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4) _debug "record_id" "$record_id" if [ -z "$record_id" ]; then @@ -178,7 +178,7 @@ _get_root() { # 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) + zone_id=$(echo "$response" | _egrep_o '"id":[ ]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4) if [ -n "$zone_id" ]; then # Found the zone _domain="$h" diff --git a/dnsapi/dns_poweradmin.sh b/dnsapi/dns_poweradmin.sh index db31fa4f..a4c81835 100644 --- a/dnsapi/dns_poweradmin.sh +++ b/dnsapi/dns_poweradmin.sh @@ -227,7 +227,7 @@ _poweradmin_rest() { return 1 fi - if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then + if printf '%s' "$response" | grep -q '"success"[ ]*:[ ]*false'; then _err "API reported failure on $method $ep" _debug "Response: $response" return 1 diff --git a/dnsapi/dns_rage4.sh b/dnsapi/dns_rage4.sh index c27fbc5f..b9abff17 100755 --- a/dnsapi/dns_rage4.sh +++ b/dnsapi/dns_rage4.sh @@ -71,7 +71,7 @@ dns_rage4_rm() { _debug "Getting txt records" _rage4_rest "getrecords/?id=${_domain_id}" - _record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\1/p') + _record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -n 's/.*"id":\([0-9][0-9]*\),.*/\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 diff --git a/dnsapi/dns_selectel.sh b/dnsapi/dns_selectel.sh index 565f541b..8ba9a4fb 100644 --- a/dnsapi/dns_selectel.sh +++ b/dnsapi/dns_selectel.sh @@ -368,7 +368,7 @@ _get_auth_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") + _token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | cut -d ':' -f 2- | tr -d ' \t\r') _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" diff --git a/dnsapi/dns_selfhost.sh b/dnsapi/dns_selfhost.sh index 782a5d5f..25130146 100644 --- a/dnsapi/dns_selfhost.sh +++ b/dnsapi/dns_selfhost.sh @@ -42,7 +42,10 @@ dns_selfhost_add() { # only match full domains (at the beginning of the string or with a leading whitespace), # e.g. don't match mytest.example.com or sub.test.example.com for test.example.com # if the domain is defined multiple times only the last occurance will be matched - mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p") + # prepend a space to each line so "start of line" and "after whitespace" + # can both be matched as "after a space/tab" (portable BRE, no ERE (^|..)) + _selfhost_tab="$(printf '\t')" + mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*:\{0,1\}[0-9]*\).*/\1/p") _debug2 mapEntry "$mapEntry" if test -z "$mapEntry"; then _err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID" @@ -54,7 +57,7 @@ dns_selfhost_add() { rid2=$(echo "$mapEntry" | cut -d: -f3) # read last used rid domain - lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain:[[:digit:]]+)(.*)/\2/p") + lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p") _debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry" lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2) diff --git a/dnsapi/dns_udr.sh b/dnsapi/dns_udr.sh index 656a0557..dbc959d6 100644 --- a/dnsapi/dns_udr.sh +++ b/dnsapi/dns_udr.sh @@ -145,8 +145,8 @@ _udr_rest() { _debug data "${data}" response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")" - _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') - _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') + _code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | tr -d ' \t\r') + _description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | tr -d '\r' | sed -e 's/^[ ]*//' -e 's/[ ]*$//') _debug response_code "$_code" _debug response_description "$_description" diff --git a/dnsapi/dns_yandex360.sh b/dnsapi/dns_yandex360.sh index 18d01361..98841d6b 100644 --- a/dnsapi/dns_yandex360.sh +++ b/dnsapi/dns_yandex360.sh @@ -149,7 +149,7 @@ _check_variables() { org_response="$(echo "$org_response" | _normalizeJson)" YANDEX360_ORG_ID=$( echo "$org_response" | - _egrep_o '"id":[[:space:]]*[0-9]+' | + _egrep_o '"id":[ ]*[0-9]+' | cut -d':' -f2 ) _debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID" @@ -216,7 +216,7 @@ _get_token() { interval=$( echo "$response" | - _egrep_o '"interval":[[:space:]]*[0-9]+' | + _egrep_o '"interval":[ ]*[0-9]+' | cut -d':' -f2 ) _debug 'Polling interval' "$interval" From 7fa301821911ca93bbf82ad77edef577ac07785e Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 17 Jul 2026 22:03:00 +0800 Subject: [PATCH 665/689] feat: add ACME_PACKAGED for distro-packaged installs When ACME_PACKAGED is set (e.g. exported by a distro package wrapper): - --install does not copy the script or the hooks into LE_WORKING_DIR; the cron job and the shell alias point to the packaged script instead - --upgrade, --install-online and the cron AUTO_UPGRADE path refuse and point to the system package manager - --uninstall does not remove the packaged files https://github.com/acmesh-official/acme.sh/issues/7135 --- acme.sh | 101 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 33 deletions(-) diff --git a/acme.sh b/acme.sh index 753b1159..274af3ac 100755 --- a/acme.sh +++ b/acme.sh @@ -7528,6 +7528,15 @@ _installalias() { _c_home="$1" _initpath + _alias_bin="$LE_WORKING_DIR/$PROJECT_ENTRY" + if [ ! -f "$_alias_bin" ]; then + #ACME_PACKAGED install: no copy in LE_WORKING_DIR, alias the current script + _script="$(_readlink "$_SCRIPT_")" + if [ -f "$_script" ]; then + _alias_bin="$_script" + fi + fi + _envfile="$LE_WORKING_DIR/$PROJECT_ENTRY.env" if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile" @@ -7545,7 +7554,7 @@ _installalias() { else _sed_i "/^export LE_CONFIG_HOME/d" "$_envfile" fi - _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$_alias_bin$_c_entry\"" if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" ]; then #the completion file does nothing when sourced by a non-bash shell _setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\"" @@ -7572,7 +7581,7 @@ _installalias() { else _sed_i "/^setenv LE_CONFIG_HOME/d" "$_cshfile" fi - _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$_alias_bin$_c_entry\"" _setopt "$_csh_profile" "source \"$_cshfile\"" fi @@ -7584,7 +7593,7 @@ _installalias() { if [ "$_c_home" ]; then _setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\"" fi - _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\"" + _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$_alias_bin$_c_entry\"" _setopt "$_tcsh_profile" "source \"$_cshfile\"" fi @@ -7658,30 +7667,38 @@ install() { chmod 700 "$LE_CONFIG_HOME" fi - cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY" + if [ "$ACME_PACKAGED" ]; then + #the script and its hooks are managed by a system package manager, + #do not copy them into LE_WORKING_DIR. https://github.com/acmesh-official/acme.sh/issues/7135 + _info "ACME_PACKAGED is set, skipping the script copy." + else + cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY" - if [ "$?" != "0" ]; then - _err "Installation failed, cannot copy $PROJECT_ENTRY" - return 1 - fi + if [ "$?" != "0" ]; then + _err "Installation failed, cannot copy $PROJECT_ENTRY" + return 1 + fi - _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" + _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY" - if [ -f "$PROJECT_ENTRY.completion" ]; then - cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/" - _debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion" + if [ -f "$PROJECT_ENTRY.completion" ]; then + cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/" + _debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion" + fi fi if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then _installalias "$_c_home" fi - for subf in $_SUB_FOLDERS; do - if [ -d "$subf" ]; then - mkdir -p "$LE_WORKING_DIR/$subf" - cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/ - fi - done + if [ -z "$ACME_PACKAGED" ]; then + for subf in $_SUB_FOLDERS; do + if [ -d "$subf" ]; then + mkdir -p "$LE_WORKING_DIR/$subf" + cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/ + fi + done + fi if [ ! -f "$ACCOUNT_CONF_PATH" ]; then _initconf @@ -7709,7 +7726,7 @@ install() { installcronjob "$_c_home" fi - if [ -z "$NO_DETECT_SH" ]; then + if [ -z "$NO_DETECT_SH" ] && [ -z "$ACME_PACKAGED" ]; then #Modify shebang if _exists bash; then _bash_path="$(bash -c "command -v bash 2>/dev/null")" @@ -7734,7 +7751,9 @@ install() { if [ "$_accountemail" ]; then _saveaccountconf "ACCOUNT_EMAIL" "$_accountemail" fi - _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)" + if [ -z "$ACME_PACKAGED" ]; then + _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)" + fi _info OK } @@ -7748,8 +7767,12 @@ uninstall() { _uninstallalias - rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" - rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" + if [ -z "$ACME_PACKAGED" ]; then + #don't remove the script when it is managed by a system package manager, + #LE_WORKING_DIR may point to the packaged files + rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY" + rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" + fi _info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\". You can remove them by yourself." } @@ -7785,20 +7808,24 @@ cron() { _initpath _info "$(__green "===Starting cron===")" if [ "$AUTO_UPGRADE" = "1" ]; then - export LE_WORKING_DIR - ( - if ! upgrade; then - _err "Cron: Upgrade failed!" - return 1 + if [ "$ACME_PACKAGED" ]; then + _info "ACME_PACKAGED is set, skipping the auto upgrade." + else + export LE_WORKING_DIR + ( + if ! upgrade; then + _err "Cron: Upgrade failed!" + return 1 + fi + ) + . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null + + if [ -t 1 ]; then + __INTERACTIVE="1" fi - ) - . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null - if [ -t 1 ]; then - __INTERACTIVE="1" + _info "Automatically upgraded to: $VER" fi - - _info "Automatically upgraded to: $VER" fi _TREAT_SKIP_AS_SUCCESS="1" renewAll @@ -8128,6 +8155,10 @@ Parameters: } installOnline() { + if [ "$ACME_PACKAGED" ]; then + _err "ACME_PACKAGED is set: acme.sh is managed by the system package manager, please use it to upgrade." + return 1 + fi _info "Installing from online archive." _branch="$BRANCH" @@ -8185,6 +8216,10 @@ _getUpgradeHash() { } upgrade() { + if [ "$ACME_PACKAGED" ]; then + _err "ACME_PACKAGED is set: acme.sh is managed by the system package manager, please use it to upgrade." + exit 1 + fi if ( _initpath [ -z "$FORCE" ] && [ "$(_getUpgradeHash)" = "$(_readaccountconf "UPGRADE_HASH")" ] && _info "Already up to date!" && exit 0 From 97c5aca136c37830ffcd116435aa1eb9b2d80aa1 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 18 Jul 2026 09:32:26 +0800 Subject: [PATCH 666/689] add cache-after-prepare: true --- .github/workflows/DNS.yml | 11 +++++++++++ .github/workflows/DragonFlyBSD.yml | 1 + .github/workflows/FreeBSD.yml | 1 + .github/workflows/GhostBSD.yml | 1 + .github/workflows/Haiku.yml | 1 + .github/workflows/MidnightBSD.yml | 1 + .github/workflows/NetBSD.yml | 1 + .github/workflows/Omnios.yml | 1 + .github/workflows/OpenBSD.yml | 1 + .github/workflows/OpenIndiana.yml | 1 + .github/workflows/Solaris.yml | 1 + .github/workflows/Tribblix.yml | 1 + 12 files changed, 22 insertions(+) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 5417068f..f7d5cb98 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -237,6 +237,7 @@ jobs: - uses: vmactions/freebsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg install -y socat curl usesh: true @@ -295,6 +296,7 @@ jobs: - uses: vmactions/ghostbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg install -y socat curl usesh: true @@ -351,6 +353,7 @@ jobs: - uses: vmactions/openbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: pkg_add socat curl libiconv usesh: true @@ -407,6 +410,7 @@ jobs: - uses: vmactions/netbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | /usr/sbin/pkg_add curl socat @@ -464,6 +468,7 @@ jobs: - uses: vmactions/dragonflybsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: | pkg install -y libnghttp2 @@ -525,6 +530,7 @@ jobs: - uses: vmactions/midnightbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' prepare: mport install socat curl || true usesh: true @@ -582,6 +588,7 @@ jobs: - uses: vmactions/solaris-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: | @@ -641,6 +648,7 @@ jobs: - uses: vmactions/omnios-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: pkg install socat @@ -697,6 +705,7 @@ jobs: - uses: vmactions/openindiana-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: pkg install socat @@ -753,6 +762,7 @@ jobs: - uses: vmactions/tribblix-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: nfs prepare: zap install socat @@ -809,6 +819,7 @@ jobs: - uses: vmactions/haiku-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' sync: rsync copyback: false diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index c8cbc985..20c61dcc 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -58,6 +58,7 @@ jobs: - uses: vmactions/dragonflybsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 50fcab32..88ef0a6e 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -64,6 +64,7 @@ jobs: - uses: vmactions/freebsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml index c77fdf2e..04510c15 100644 --- a/.github/workflows/GhostBSD.yml +++ b/.github/workflows/GhostBSD.yml @@ -66,6 +66,7 @@ jobs: - uses: vmactions/ghostbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 9884ebeb..b133dd18 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -65,6 +65,7 @@ jobs: - uses: vmactions/haiku-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/MidnightBSD.yml b/.github/workflows/MidnightBSD.yml index 15024833..ce499e4e 100644 --- a/.github/workflows/MidnightBSD.yml +++ b/.github/workflows/MidnightBSD.yml @@ -58,6 +58,7 @@ jobs: - uses: vmactions/midnightbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 16d0ae2d..6695f71e 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -58,6 +58,7 @@ jobs: - uses: vmactions/netbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index eb486b35..aabb168b 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -64,6 +64,7 @@ jobs: - uses: vmactions/omnios-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index 4fdb76c5..46318163 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -64,6 +64,7 @@ jobs: - uses: vmactions/openbsd-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index b5061ba7..e3119f8e 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -64,6 +64,7 @@ jobs: - uses: vmactions/openindiana-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 3393269a..30e4e291 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -64,6 +64,7 @@ jobs: - uses: vmactions/solaris-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml index cd43e0e3..68e61dc8 100644 --- a/.github/workflows/Tribblix.yml +++ b/.github/workflows/Tribblix.yml @@ -64,6 +64,7 @@ jobs: - uses: vmactions/tribblix-vm@v1 with: debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' nat: | "8080": "80" From 6feb1df83cb2d1e628c0b751cf63a645ec5266c5 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 19 Jul 2026 10:20:58 +0800 Subject: [PATCH 667/689] fix cpanel_uapi: pass --user to DomainInfo list_domains when run as root The auto mode sitelist query was missing the --user branch that the install_ssl calls already have, so deploy always failed under root. fix https://github.com/acmesh-official/acme.sh/issues/7139 --- deploy/cpanel_uapi.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deploy/cpanel_uapi.sh b/deploy/cpanel_uapi.sh index 156044eb..02ef6b3e 100644 --- a/deploy/cpanel_uapi.sh +++ b/deploy/cpanel_uapi.sh @@ -87,7 +87,11 @@ cpanel_uapi_deploy() { # Auto mode if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then # call API for site config - _response=$(uapi DomainInfo list_domains) + if [ -n "$_uapi_user" ]; then + _response=$(uapi --user="$_uapi_user" DomainInfo list_domains) + else + _response=$(uapi DomainInfo list_domains) + fi # exit if error in response if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then _err "Error in deploying certificate - cannot retrieve sitelist:" From 4c8a143086549d77d21fcec4fce0bb7d97b60821 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 20 Jul 2026 10:02:54 +0800 Subject: [PATCH 668/689] fix proxmoxve/proxmoxbs deploy: fail on non-2xx API response The success check only grepped "message" from the response body, but PVE/PBS auth failures return HTTP 401 with an empty body, so wrong or unauthorized API tokens were reported as "Certificate successfully deployed". Also _retval captured the exit code of the message pipeline instead of _post. Check the HTTP status line from $HTTP_HEADER and capture _post's exit code directly. fix https://github.com/acmesh-official/acme.sh/issues/7141 --- deploy/proxmoxbs.sh | 27 +++++++++++++++++---------- deploy/proxmoxve.sh | 27 +++++++++++++++++---------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh index 179b0369..30599a44 100644 --- a/deploy/proxmoxbs.sh +++ b/deploy/proxmoxbs.sh @@ -116,17 +116,24 @@ HEREDOC export HTTPS_INSECURE=1 export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}" response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") + _retval=$? + # The API errors out with a non-2xx HTTP status and an empty body, + # so the status line is checked too, not only the response body. + _status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug2 "HTTP status" "$_status_code" response="$(echo "$response" | _json_decode | _normalizeJson)" message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"') - _retval=$? - if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then - _debug3 response "$response" - _info "Certificate successfully deployed" - return 0 - else - _err "Certificate deployment failed: $message" - _debug "Response" "$response" - return 1 - fi + case "$_status_code" in + 2[0-9][0-9]) + if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then + _debug3 response "$response" + _info "Certificate successfully deployed" + return 0 + fi + ;; + esac + _err "Certificate deployment failed (HTTP status $_status_code). $message" + _debug "Response" "$response" + return 1 } diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh index b6298ee7..fd8d69d8 100644 --- a/deploy/proxmoxve.sh +++ b/deploy/proxmoxve.sh @@ -128,17 +128,24 @@ HEREDOC export HTTPS_INSECURE=1 export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}" response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") + _retval=$? + # The API errors out with a non-2xx HTTP status and an empty body, + # so the status line is checked too, not only the response body. + _status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug2 "HTTP status" "$_status_code" response="$(echo "$response" | _json_decode | _normalizeJson)" message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"') - _retval=$? - if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then - _debug3 response "$response" - _info "Certificate successfully deployed" - return 0 - else - _err "Certificate deployment failed: $message" - _debug "Response" "$response" - return 1 - fi + case "$_status_code" in + 2[0-9][0-9]) + if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then + _debug3 response "$response" + _info "Certificate successfully deployed" + return 0 + fi + ;; + esac + _err "Certificate deployment failed (HTTP status $_status_code). $message" + _debug "Response" "$response" + return 1 } From 1774d838cabca07d50e55b7e736c83e3d1228ecf Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 22 Jul 2026 20:42:14 +0800 Subject: [PATCH 669/689] add GNU hurd --- .github/workflows/DNS.yml | 91 +++++++++++++++++++++++++++++++------- .github/workflows/Hurd.yml | 76 +++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/Hurd.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index f7d5cb98..09b65a96 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -66,7 +66,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file @@ -114,7 +114,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install tools run: | brew untap aws/tap || true @@ -167,7 +167,7 @@ jobs: - name: Set git to use LF run: | git config --global core.autocrlf false - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Install cygwin base packages with chocolatey run: | choco config get cacheLocation @@ -231,7 +231,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/freebsd-vm@v1 @@ -290,7 +290,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/ghostbsd-vm@v1 @@ -347,7 +347,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 @@ -404,7 +404,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/netbsd-vm@v1 @@ -462,7 +462,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/dragonflybsd-vm@v1 @@ -524,7 +524,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/midnightbsd-vm@v1 @@ -582,7 +582,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/solaris-vm@v1 @@ -642,7 +642,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/omnios-vm@v1 @@ -699,7 +699,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openindiana-vm@v1 @@ -756,7 +756,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/tribblix-vm@v1 @@ -813,7 +813,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/haiku-vm@v1 @@ -826,7 +826,68 @@ jobs: prepare: | mkdir -p /boot/home/.cache pkgman install -y cronie - + + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + + + Hurd: + runs-on: ubuntu-latest + needs: Haiku + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v7 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/hurd-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + sync: rsync + copyback: false + usesh: true + prepare: | + apt-get update -y + apt-get install -y curl cron run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/.github/workflows/Hurd.yml b/.github/workflows/Hurd.yml new file mode 100644 index 00000000..fee80d29 --- /dev/null +++ b/.github/workflows/Hurd.yml @@ -0,0 +1,76 @@ +name: Hurd +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Hurd.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Hurd.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + Hurd: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + steps: + - uses: actions/checkout@v7 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/hurd-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN' + nat: | + "8080": "80" + # Do NOT install socat: socat's SYSTEM: address is broken on GNU Hurd + # (the child shell output goes to socat's stdout instead of the socket, + # so clients get an empty reply). Without socat, acme.sh standalone + # mode falls back to its python3 server, which works on Hurd. + prepare: | + apt-get update -y + apt-get install -y curl cron + usesh: true + sync: rsync + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/README.md b/README.md index b93a8a50..23c8b3e0 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ OpenIndiana Tribblix Haiku + Hurd

@@ -130,6 +131,7 @@ |25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS |26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix |27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD +|28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From 24d573b6d3f0ee409c145b0ed26813efaae4c63c Mon Sep 17 00:00:00 2001 From: Radu Date: Thu, 23 Jul 2026 06:50:20 +0300 Subject: [PATCH 670/689] Merge pull request #7140 from radumalica/dns_hestiacp feat: add dnsapi for HestiaCP --- dnsapi/dns_hestiacp.sh | 198 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 dnsapi/dns_hestiacp.sh diff --git a/dnsapi/dns_hestiacp.sh b/dnsapi/dns_hestiacp.sh new file mode 100644 index 00000000..13ee6caf --- /dev/null +++ b/dnsapi/dns_hestiacp.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_hestiacp_info='HestiaCP Server API +Site: hestiacp.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hestiacp +Options: + HESTIA_HOST Panel URL. E.g. "https://panel.example.com:8083" + HESTIA_ACCESS API access key + HESTIA_SECRET API secret key + HESTIA_USER Username owning the DNS zones. Default "admin". Optional. +Issues: github.com/acmesh-official/acme.sh/issues/6251 +Author: Radu Malica +' + +######## Public functions ##################### + +# Usage: dns_hestiacp_add fulldomain txtvalue +dns_hestiacp_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _hestia_init; then + return 1 + fi + + _debug "Detecting the root zone for $fulldomain" + if ! _hestia_get_root "$fulldomain"; then + _err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER" + return 1 + fi + _debug _hestia_domain "$_hestia_domain" + _debug _hestia_sub "$_hestia_sub" + + # _hestia_get_root left the zone record listing in _hestia_response + if _hestia_find_records "$_hestia_sub" "TXT" | grep -F -- "$txtvalue" >/dev/null; then + _info "The TXT record already exists, skipping" + return 0 + fi + + _info "Adding TXT record for $fulldomain" + if ! _hestia_rest "v-add-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_sub" "TXT" "$txtvalue" "" "" "yes" "600"; then + _err "Error adding TXT record: $_hestia_response" + return 1 + fi + _info "TXT record added successfully" + return 0 +} + +# Usage: dns_hestiacp_rm fulldomain txtvalue +dns_hestiacp_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _hestia_init; then + return 1 + fi + + _debug "Detecting the root zone for $fulldomain" + if ! _hestia_get_root "$fulldomain"; then + _err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER" + return 1 + fi + _debug _hestia_domain "$_hestia_domain" + _debug _hestia_sub "$_hestia_sub" + + _hestia_removed=0 + _hestia_failed=0 + while IFS='|' read -r _hestia_id _hestia_value || [ -n "$_hestia_id" ]; do + if [ -z "$_hestia_id" ]; then + continue + fi + if ! _contains "$_hestia_value" "$txtvalue"; then + continue + fi + _info "Deleting TXT record $_hestia_id" + if ! _hestia_rest "v-delete-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_id" "yes"; then + _err "Error deleting TXT record $_hestia_id: $_hestia_response" + _hestia_failed=$(_math "$_hestia_failed" + 1) + continue + fi + _hestia_removed=$(_math "$_hestia_removed" + 1) + done < Date: Fri, 24 Jul 2026 07:55:17 +0200 Subject: [PATCH 671/689] feat: added realtoxmedia dnsapi (#7156) --- dnsapi/dns_rltx.sh | 145 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 dnsapi/dns_rltx.sh diff --git a/dnsapi/dns_rltx.sh b/dnsapi/dns_rltx.sh new file mode 100644 index 00000000..065ac177 --- /dev/null +++ b/dnsapi/dns_rltx.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_rltx_info='Realtox Media Cloudpanel DNS API +Site: realtoxmedia.de +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_rltx +Options: + RLTX_Key API Key + RLTX_OrganizationID Organization ID +' + +######## Public functions ##################### + +#Usage: dns_rltx_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_rltx_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Using Realtox Media Cloudpanel DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _rltx_init; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Could not find matching DNS zone for $fulldomain" + return 1 + fi + + _debug _domain_id "$_domain_id" + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}" + if ! _rltx_rest POST "domains/$_domain_id/dns/acme-txt" "$data"; then + _err "Add TXT record request failed" + return 1 + fi + if _contains "$response" '"status":"added"'; then + _info "Added TXT record, OK" + return 0 + fi + _err "Add TXT record failed: $response" + return 1 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_rltx_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Using Realtox Media Cloudpanel DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _rltx_init; then + return 1 + fi + + if ! _get_root "$fulldomain"; then + _err "Could not find matching DNS zone for $fulldomain" + return 1 + fi + + _debug _domain_id "$_domain_id" + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}" + if ! _rltx_rest DELETE "domains/$_domain_id/dns/acme-txt" "$data"; then + _err "Remove TXT record request failed" + return 1 + fi + if _contains "$response" '"status":"removed"'; then + _info "Removed TXT record, OK" + return 0 + fi + _err "Remove TXT record failed: $response" + return 1 +} + +#################### Private functions below ################################## + +_rltx_init() { + RLTX_Key="${RLTX_Key:-$(_readaccountconf_mutable RLTX_Key)}" + RLTX_OrganizationID="${RLTX_OrganizationID:-$(_readaccountconf_mutable RLTX_OrganizationID)}" + + if [ -z "$RLTX_Key" ] || [ -z "$RLTX_OrganizationID" ]; then + RLTX_Key="" + RLTX_OrganizationID="" + _err "Please specify RLTX_Key and RLTX_OrganizationID." + _err "You can export them and retry: export RLTX_Key=... RLTX_OrganizationID=..." + return 1 + fi + + _saveaccountconf_mutable RLTX_Key "$RLTX_Key" + _saveaccountconf_mutable RLTX_OrganizationID "$RLTX_OrganizationID" +} + +_get_root() { + domain=$1 + fqdn_encoded="$(printf "%s" "$domain" | _url_encode)" + if ! _rltx_rest GET "domains/dns/acme-zone?fqdn=$fqdn_encoded"; then + return 1 + fi + if ! _contains "$response" '"domain_id":"'; then + return 1 + fi + + _domain_id="$(printf "%s" "$response" | _egrep_o '"domain_id":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)" + _domain="$(printf "%s" "$response" | _egrep_o '"zone":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)" + _sub_domain="$(printf "%s" "$response" | _egrep_o '"record_name":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)" + + if [ -z "$_domain_id" ] || [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then + return 1 + fi + return 0 +} + +_rltx_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="X-API-Key: $RLTX_Key" + export _H2="X-Organization-ID: $RLTX_OrganizationID" + export _H3="Content-Type: application/json" + + if [ "$m" = "GET" ]; then + response="$(_get "https://api.ccp.realtoxmedia.de/api/$ep")" + else + _debug2 data "$data" + response="$(_post "$data" "https://api.ccp.realtoxmedia.de/api/$ep" "" "$m")" + fi + + if [ "$?" != "0" ]; then + _err "Realtox Media Cloudpanel API request failed: $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} From 698f6c73298e76c65ad7e726e462b50af1c5cf28 Mon Sep 17 00:00:00 2001 From: Qhilm <3350433+Qhilm@users.noreply.github.com> Date: Sat, 25 Jul 2026 07:03:56 +0200 Subject: [PATCH 672/689] Feat: Shelly deploy hook for firmware 2.0.0+ (#7145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Shelly Gen3+ deploy hook with RFC 7616 HTTP Digest auth Adds deploy/shelly.sh for deploying Let's Encrypt HTTPS server certificates to Shelly Gen3+ devices (Gen4 tested) via JSON-RPC over HTTP. - RFC 7616 SHA-256 HTTP Digest authentication (Authorization header) - Uploads fullchain.pem and private key via Shelly.PutHTTPServerCert / PutHTTPServerKey - Auto-reboot support (SHELLY_REBOOT to disable) - Auth auto-detection: no password = no auth, password = Digest - Nonce counter (nc) increments per request per RFC 7616 - Tested against Shelly 2PM Gen4 (firmware 2.0.0) Also adds deploy/test_shelly.sh for self-testing the hook logic without a real device (mocked _post). * fix: address review feedback on shelly deploy hook - Fix _secure_debug calls to use two arguments (label + value) - Remove bash-only $RANDOM cnonce fallback; openssl always available - Parse $HTTP_HEADER directly instead of raw curl re-request - Detect auth via HTTP 401 status line, not empty response body - Route reboot through _shelly_rpc to rebuild auth header with correct nc - Remove export HTTPS_INSECURE=1 (no-op for http://, leaks to other hooks) - Clear _H1 before returning from shelly_deploy - Prefix all helper variables with _shelly_ to avoid namespace collisions - Delete deploy/test_shelly.sh (deploy/ files become hook names) - Fix missing trailing newline * fix: validate shelly JSON-RPC responses are valid JSON Non-JSON responses like HTTP 429 'Too Many Requests' would pass the empty-response and '"error"' checks and be reported as success. Now reject any response that doesn't start with '{' and contain '"id"'. * fix: add 1s delay between shelly cert/key clear and upload calls The Shelly device has a race condition where uploading data immediately after clearing the existing cert/key returns -103 'Missing required argument data!'. A 1-second delay fixes this. * fix: remove clear-before-upload in shelly deploy hook Shelly auto-removes all three TLS files (cert, key, CA bundle) when any single one is cleared. The old sequence clear-cert → upload-cert → clear-key → upload-key resulted in the key clear wiping the newly uploaded cert, leaving only the key at boot time. The mbedtls pk_check_pair then silently skipped the HTTPS listener. Fix: just upload directly (overwrite in place). No clearing needed. * Fix ShellCheck SC2090 and shfmt in shelly deploy hook SC2090: false positive on export _H1 (used quoted in _post) shfmt: no space after "<" in _json_encode redirects * moved two lines to cover the whole if block --------- Co-authored-by: neil Co-authored-by: cysimons --- deploy/shelly.sh | 280 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 deploy/shelly.sh diff --git a/deploy/shelly.sh b/deploy/shelly.sh new file mode 100644 index 00000000..dbdab346 --- /dev/null +++ b/deploy/shelly.sh @@ -0,0 +1,280 @@ +#!/usr/bin/env sh + +# Here is a script to deploy cert to a Shelly Gen3+ device. +# Deploy the HTTPS server certificate to a Shelly device on the local network. +# +# ```sh +# export SHELLY_HOST=192.168.1.100 +# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device +# acme.sh --deploy -d shelly.example.com --deploy-hook shelly +# ``` +# +# Environment variables: +# SHELLY_HOST (required) IP or hostname of the Shelly device +# SHELLY_PASSWORD (optional) Admin password for digest authentication. +# Omit if auth is disabled on the device. +# SHELLY_USER (optional) Username for auth. Default: admin +# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot. +# Default: 1 (reboot after upload) +# +# Requirements: +# - Shelly Gen3+ device (Gen4 recommended) +# - Firmware 2.0.0+ for HTTPS server certificate support +# - curl or wget +# - openssl (for SHA-256 digest and random cnonce) +# +# The device must be reachable via HTTP on the local network. +# The hook uploads the fullchain.pem and private key, +# then reboots the device to apply the new certificate. +# +# Authentication uses standard RFC 7616 HTTP Digest (SHA-256) since +# firmware 2.0.0. The JSON-RPC auth object is not used for HTTP transport. +# +# returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +shelly_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" + + _getdeployconf SHELLY_HOST + _getdeployconf SHELLY_PASSWORD + _getdeployconf SHELLY_USER + _getdeployconf SHELLY_REBOOT + + _debug SHELLY_HOST "$SHELLY_HOST" + _debug SHELLY_USER "$SHELLY_USER" + _secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD" + _debug SHELLY_REBOOT "$SHELLY_REBOOT" + + if [ -z "$SHELLY_HOST" ]; then + _err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device." + return 1 + fi + + SHELLY_USER="${SHELLY_USER:-admin}" + SHELLY_REBOOT="${SHELLY_REBOOT:-1}" + + _savedeployconf SHELLY_HOST "$SHELLY_HOST" + _savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD" + _savedeployconf SHELLY_USER "$SHELLY_USER" + _savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT" + + # --- Auth handshake (only if password is set) --- + _shelly_auth_header="" + if [ -n "$SHELLY_PASSWORD" ]; then + _info "Authenticating to Shelly device at $SHELLY_HOST" + if ! _shelly_handshake; then + _err "Authentication handshake failed. Check SHELLY_PASSWORD and device accessibility." + return 1 + fi + _info "Authentication successful" + fi + + # --- Upload certificate --- + _info "Uploading certificate to Shelly device at $SHELLY_HOST" + if ! _shelly_upload_cert; then + _err "Certificate upload failed" + return 1 + fi + + # --- Upload key --- + _info "Uploading private key to Shelly device" + if ! _shelly_upload_key; then + _err "Private key upload failed" + return 1 + fi + + _info "Certificate and key uploaded successfully" + + # --- Reboot --- + if [ "$SHELLY_REBOOT" != "0" ]; then + _info "Rebooting Shelly device to apply certificate" + # Reboot may close the connection before sending a response + _shelly_rpc "Shelly.Reboot" '{}' || _debug "Reboot may have closed connection (expected)" + _info "Reboot command sent. Device will restart shortly." + else + _info "Skipping reboot (SHELLY_REBOOT=0). Certificate will apply on next restart." + fi + + # Clear auth header so it does not leak to other hooks + export _H1="" + + return 0 +} + +# --- Helper functions --- + +# Perform RFC 7616 HTTP Digest auth handshake. +# Sets _shelly_auth_header on success (the Authorization header value). +_shelly_handshake() { + _inithttp + + _debug "Probing device for auth challenge" + + # Use a protected method (Shelly.GetStatus) to trigger 401. + # Shelly.GetDeviceInfo is excluded from auth and would miss the challenge. + _post '{"id":1,"method":"Shelly.GetStatus"}' \ + "http://${SHELLY_HOST}/rpc" "" "" "application/json" + + # Detect auth from HTTP status line rather than response body + if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then + # No auth challenge — device accepted the request without credentials + _debug "Device responded without auth challenge. Proceeding without auth." + return 0 + fi + + _shelly_realm="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*realm="//;s/".*//')" + _shelly_nonce="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*nonce="//;s/".*//')" + _shelly_qop="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*qop="//;s/".*//')" + + if [ -z "$_shelly_nonce" ]; then + _err "Failed to extract nonce from WWW-Authenticate header. Is SHELLY_PASSWORD correct?" + return 1 + fi + + _shelly_qop="${_shelly_qop:-auth}" + + _debug "Shelly realm: $_shelly_realm" + _debug "Shelly qop: $_shelly_qop" + _secure_debug "Shelly nonce" "$_shelly_nonce" + + # ha1 = SHA256(username:realm:password) + _shelly_ha1="$(printf '%s' "${SHELLY_USER}:${_shelly_realm}:${SHELLY_PASSWORD}" | _digest sha256 hex)" + _secure_debug "Shelly ha1" "$_shelly_ha1" + + # Generate client nonce (openssl is required for _digest, so always available) + _shelly_cnonce="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 8 2>/dev/null)" + _debug "Shelly cnonce: $_shelly_cnonce" + + # Build the digest Authorization header value (stored for reuse) + _shelly_nc=1 + _shelly_build_auth_header + + return 0 +} + +# Check whether the HTTP response headers contain a digest auth challenge. +# Returns 0 (true) if a 401 with WWW-Authenticate is present. +_shelly_has_auth_challenge() { + _shelly_headers_file="$1" + _shelly_status="$(grep -i '^HTTP/' "$_shelly_headers_file" | _tail_n 1 | awk '{print $2}')" + [ "$_shelly_status" = "401" ] && grep -qi '^WWW-Authenticate:' "$_shelly_headers_file" +} + +# Build or rebuild the RFC 7616 Authorization header. +# Uses: _shelly_ha1, _shelly_nonce, _shelly_cnonce, _shelly_qop, _shelly_realm, _shelly_nc +# Sets: _shelly_auth_header +_shelly_build_auth_header() { + _shelly_nc_hex="$(printf '%08x' "$_shelly_nc")" + + # ha2 = SHA256(POST:/rpc) + _shelly_ha2="$(printf '%s' "POST:/rpc" | _digest sha256 hex)" + + # response = SHA256(ha1:nonce:nc:cnonce:qop:ha2) + _shelly_digest_response="$(printf '%s' "${_shelly_ha1}:${_shelly_nonce}:${_shelly_nc_hex}:${_shelly_cnonce}:${_shelly_qop}:${_shelly_ha2}" | _digest sha256 hex)" + + # Build the Authorization header value (without the "Authorization: " prefix) + _shelly_auth_header="Digest username=\"${SHELLY_USER}\", realm=\"${_shelly_realm}\", nonce=\"${_shelly_nonce}\", uri=\"/rpc\", qop=${_shelly_qop}, nc=${_shelly_nc_hex}, cnonce=\"${_shelly_cnonce}\", response=\"${_shelly_digest_response}\", algorithm=SHA-256" + + _secure_debug "Authorization header" "$_shelly_auth_header" +} + +# Make a Shelly JSON-RPC call. +# Usage: _shelly_rpc +# Returns 0 on success, 1 on error. +_shelly_rpc() { + _shelly_method="$1" + _shelly_params="$2" + + _shelly_body='{"id":1,"method":"'"$_shelly_method"'","params":'"$_shelly_params"'}' + + _debug "RPC method: $_shelly_method" + _debug2 "RPC body: $_shelly_body" + + # shellcheck disable=SC2090 + if [ -n "$_shelly_auth_header" ]; then + export _H1="Authorization: $_shelly_auth_header" + else + export _H1="" + fi + + _post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json" + _shelly_ret=$? + + if [ "$_shelly_ret" != "0" ]; then + _err "HTTP request failed for $_shelly_method (curl/wget error $_shelly_ret)" + return 1 + fi + + # Empty response means something went wrong (auth required but not provided, etc.) + if [ -z "$response" ]; then + _err "Empty response from Shelly device. If authentication is enabled on the device, set SHELLY_PASSWORD." + return 1 + fi + + # Validate response looks like a Shelly JSON-RPC response. + # Catches non-JSON responses such as HTTP 429 "Too Many Requests" which + # would otherwise pass the empty and "error" checks below. + if ! _startswith "$response" '{' || ! _contains "$response" '"id"'; then + _err "Invalid response from Shelly device: $response" + return 1 + fi + + # Check for JSON-RPC error in response + if _contains "$response" '"error"'; then + _err "RPC error from Shelly: $response" + return 1 + fi + + _debug "RPC response: $response" + + # Increment nonce counter and rebuild auth header for next request + if [ -n "$_shelly_auth_header" ]; then + _shelly_nc=$((_shelly_nc + 1)) + _shelly_build_auth_header + fi + + return 0 +} + +# Upload the certificate to the device. +# Note: We do NOT clear the existing certificate first, because the Shelly +# auto-removes all three files (cert, key, CA) when any one is cleared. +# Uploading overwrites in place — no clearing needed. +_shelly_upload_cert() { + _shelly_cert_data="$(_json_encode <"$_cfullchain")" + + _debug "Uploading certificate" + if ! _shelly_rpc "Shelly.PutHTTPServerCert" '{"data":"'"$_shelly_cert_data"'"}'; then + _err "Failed to upload certificate to device" + return 1 + fi + + return 0 +} + +# Upload the private key to the device. +# Note: Do not clear first — see _shelly_upload_cert for rationale. +_shelly_upload_key() { + _shelly_key_data="$(_json_encode <"$_ckey")" + + _debug "Uploading key" + if ! _shelly_rpc "Shelly.PutHTTPServerKey" '{"data":"'"$_shelly_key_data"'"}'; then + _err "Failed to upload key to device" + return 1 + fi + + return 0 +} From 830782fd1da6b20f25017f9778af6fe1cc80d7a5 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 25 Jul 2026 13:33:54 +0800 Subject: [PATCH 673/689] fix dns_yc: avoid empty-matchable _egrep_o pattern that hangs OmniOS OmniOS native egrep -o infinite-loops emitting empty lines when the pattern can match the empty string, so `_egrep_o "[^:]*$"` never lets the pipeline finish and dns_yc hangs until the CI timeout. Require at least one character instead. `+` is not usable because the sed fallback in _egrep_o parses BRE. --- dnsapi/dns_yc.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_yc.sh b/dnsapi/dns_yc.sh index 36c49ce4..13a0d1f6 100644 --- a/dnsapi/dns_yc.sh +++ b/dnsapi/dns_yc.sh @@ -126,7 +126,7 @@ dns_yc_rm() { _debug "Getting txt records" if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then - exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$") + exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:][^:]*$") _debug exists_txtvalue "$exists_txtvalue" else _err "Error: $response" @@ -194,7 +194,7 @@ _get_root() { return 1 fi if _contains "$response" "\"zone\": \"$h\""; then - _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"') + _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"') _debug _domain_id "$_domain_id" if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") @@ -264,7 +264,7 @@ _yc_login() { _iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")" _debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)" - YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')" + YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')" _debug3 YC_Token return 0 From 7c12deb7ef2e8c5fdcfced93aaa5501ae30e4d35 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 25 Jul 2026 16:00:03 +0800 Subject: [PATCH 674/689] fix: grep -A is not portable, breaks ARI on Solaris Solaris /usr/bin/grep has no -A ("illegal option -- A"), so _getAKI printed an error to stderr on every cron renewal and returned empty. The empty AKI silently corrupts the RFC 9773 ARI certID, so ARI is never available and renewal falls back to the fixed schedule. Split the pipeline into a testable stdin filter _extractAKI and select the value line with a portable sed range instead. Same fix for the two hooks that still used grep -A: dns_world4you.sh (also replaces the GNU-only "\s" in the same expression) and deploy/keyhelp.sh (the -A 2 window could truncate the div range that follows it, so it is just dropped). https://github.com/acmesh-official/acme.sh/issues/7159 --- acme.sh | 10 +++++++++- deploy/keyhelp.sh | 4 ++-- dnsapi/dns_world4you.sh | 15 +++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/acme.sh b/acme.sh index 274af3ac..17244133 100755 --- a/acme.sh +++ b/acme.sh @@ -7369,10 +7369,18 @@ deactivate() { done } +#reads the output of "openssl x509 -text" from stdin, prints the hex AKI +#the value is on the line right after the extension header; "grep -A" is not +#portable (Solaris /usr/bin/grep: "illegal option -- A"), so select from the +#header to EOF and keep the second line of that range +_extractAKI() { + sed -n '/X509v3 Authority Key Identifier/,$p' | _head_n 2 | _tail_n 1 | tr -d ': ' | sed "s/keyid//" +} + #cert _getAKI() { _cert="$1" - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep -A 1 "X509v3 Authority Key Identifier" | _tail_n 1 | tr -d ': ' | sed "s/keyid//" + ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | _extractAKI } #cert diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh index 97f9c21c..f66d27ce 100644 --- a/deploy/keyhelp.sh +++ b/deploy/keyhelp.sh @@ -83,7 +83,7 @@ keyhelp_deploy() { _request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca" _H1="Cookie: $_cookie" _response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST") - _message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/

/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') + _message=$(echo "$_response" | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') _info "_message" "$_message" if [ -z "$_message" ]; then _err "Fail to upload certificate." @@ -118,7 +118,7 @@ keyhelp_deploy() { _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/ *$//') + _message=$(echo "$_response" | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') _info "_message" "$_message" if [ -z "$_message" ]; then _err "Fail to apply certificate." diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index f59715ac..0a1cda6b 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -61,7 +61,7 @@ dns_world4you_add() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') + msg=$(_w4y_alert_msg "$res") if [ "$msg" = '' ]; then _err "Unable to add record: Unknown error" echo "$ret" >'error-01.html' @@ -125,7 +125,7 @@ dns_world4you_rm() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') + msg=$(_w4y_alert_msg "$res") if [ "$msg" = '' ]; then _err "Unable to remove record: Unknown error" echo "$ret" >'error-01.html' @@ -145,6 +145,17 @@ dns_world4you_rm() { ################ Private functions ################ +# Usage: _w4y_alert_msg +# Extracts the error text out of the alert box of a DNS page. +# "grep -A" is not portable (Solaris /usr/bin/grep: "illegal option -- A"), +# so select from the alert to EOF and keep the same number of lines. +# "\s" is a GNU sed extension, use an explicit space/tab bracket instead. +_w4y_alert_msg() { + _w4y_tab=$(printf '\t') + echo "$1" | sed -n '/alert-notification/,$p' | _head_n 21 | + grep 'class="weak-title">[^<]' | sed "s/<[^>]*>//g;s/^[ $_w4y_tab]*//" +} + # Usage: _login _login() { WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}" From 057c94089595b9565bc350ac9ceef73d7e3b9784 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 26 Jul 2026 15:01:41 +0800 Subject: [PATCH 675/689] add OpenEuler --- .github/workflows/DNS.yml | 59 +++++++++++++++++++++++++++ .github/workflows/OpenEuler.yml | 70 +++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 131 insertions(+) create mode 100644 .github/workflows/OpenEuler.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 09b65a96..84a17470 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -914,3 +914,62 @@ jobs: + OpenEuler: + runs-on: ubuntu-latest + needs: Hurd + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v7 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/openeuler-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + sync: rsync + copyback: false + usesh: true + prepare: dnf install -y curl socat cronie tar gzip + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + + diff --git a/.github/workflows/OpenEuler.yml b/.github/workflows/OpenEuler.yml new file mode 100644 index 00000000..2b4bd0ab --- /dev/null +++ b/.github/workflows/OpenEuler.yml @@ -0,0 +1,70 @@ +name: OpenEuler +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/OpenEuler.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/OpenEuler.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + OpenEuler: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + steps: + - uses: actions/checkout@v7 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/openeuler-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + cache-after-prepare: true + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN' + nat: | + "8080": "80" + prepare: dnf install -y curl socat cronie tar gzip + usesh: true + sync: rsync + copyback: false + run: | + cd ../acmetest \ + && ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/README.md b/README.md index 23c8b3e0..90280e94 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Tribblix Haiku Hurd + OpenEuler

@@ -132,6 +133,7 @@ |26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix |27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD |28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd +|29|[![OpenEuler](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler > 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest) From 05654436229074ff015a6539476eb51dd8b8609c Mon Sep 17 00:00:00 2001 From: Goncharenko Alexander <61147910+gasRU76@users.noreply.github.com> Date: Tue, 28 Jul 2026 05:04:20 +0300 Subject: [PATCH 676/689] dns_yc: fix TXT record removal failing with "Unknown key file format" (#7150) * dns_yc: restore YC_SA_Key_File in dns_yc_rm before signing the JWT dns_yc_rm() never rebuilt YC_SA_Key_File from YC_SA_Key_File_PEM_b64 / YC_SA_Key_File_Path like dns_yc_add() does. Per the DNS API dev guide, add()/rm() run in separate subshells, so rm() must repeat add()'s setup steps rather than rely on variables set during add(). Without it, when _yc_login() needs a fresh JWT during removal (the IAM token from the add phase isn't available), it signs with an empty/unset key path, and openssl fails with "Unknown key file format". The resulting auth failure then surfaces misleadingly as "invalid domain" in _get_root, and the TXT record is never deleted. Verified against a real Yandex Cloud account/zone with --staging: before the fix, removal failed with the same errors reported in the issue; after adding the missing key-restoration block, add + remove both succeed and the TXT record is actually deleted. * dns_yc: preserve other TXT values when removing one at the same name dns_yc_rm previously sent the full current data array (all existing TXT values at the name) to the deletions API, wiping out the whole rrset instead of only the value being removed. This breaks wildcard + base domain issuance, where both share the same _acme-challenge name with two different values: removing the first one deleted both, leaving nothing for the second removal to find. * dns_yc: read persisted config from domain conf before account conf YC_Zone_ID, YC_Folder_ID, YC_SA_ID, YC_SA_Key_ID (zone-ID mode) and YC_SA_Key_File_PEM_b64/Path were always saved via _savedomainconf (domain.conf), but only ever read back via _readaccountconf_mutable (account.conf). Once the env vars were unset, none of these could be recovered from the saved config, so dns_yc_add/dns_yc_rm failed with "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File." even though the values had been persisted correctly on the prior run. * dns_yc: replace grep -Fxv/sed with a portable loop in dns_yc_rm Solaris's /usr/bin/grep supports neither -F nor -x, so _remaining_txtvalue was always empty there and the preserve-other- values logic silently fell back to deleting the whole rrset (with a grep usage error on stderr on every rm). The sed trailing-comma strip had a matching issue on Solaris, whose sed drops an unterminated last line. CI didn't catch this because the fallback path also returns "done: true". Use a plain for-loop with word splitting instead. * dns_yc: use upsertRecordSets.deletions to remove a single TXT value updateRecordSets has no "merges" field (only deletions/additions), so the previous preserve-other-values logic silently did nothing -- the TXT record was never actually removed, a regression from before that change (which at least deleted the whole rrset). CI didn't catch it because _clearupdns runs dns_yc_rm in a subshell and ignores its exit code. upsertRecordSets.deletions removes only the specified value from the rrset directly, so the getRecordSet read and the remaining-value recomputation are no longer needed at all. Verified against a real zone (base + wildcard domain sharing one _acme-challenge name): adding both values then removing one leaves the other in place, and removing the second cleans up fully. * dns_yc: don't delete the user's own key file in YC_SA_Key_File_Path mode _yc_login unconditionally rm'd $YC_SA_Key_File after signing. That's fine for the PEM_b64 path, where it's a decoded temp file, but in YC_SA_Key_File_Path mode it's the user's own persistent key file -- the first successful login permanently deleted it, so every subsequent dns_yc_rm/renewal hit "Unknown key file format" (the exact symptom this PR is about, just from a different cause). Track whether the key file is our own temp copy and only delete it in that case. Verified with a stubbed _yc_login: a temp-mode key gets removed after login, a path-mode key survives. * dns_yc: clear both domain and account conf on invalid config The failure branch in dns_yc_add only ever called _clearaccountconf, but YC_Zone_ID/YC_Folder_ID/YC_SA_Key_File_PEM_b64/Path are persisted via _savedomainconf, and YC_SA_ID/YC_SA_Key_ID may have been saved via _saveaccountconf_mutable (Folder_ID mode, which stores under a SAVED_ prefix read back by _readaccountconf_mutable). Clearing only one store left stale values behind in whichever one wasn't touched. Verified by seeding both domain.conf and account.conf with leftover values, then triggering this branch and confirming both config files end up empty. --- dnsapi/dns_yc.sh | 62 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/dnsapi/dns_yc.sh b/dnsapi/dns_yc.sh index 13a0d1f6..cacb5977 100644 --- a/dnsapi/dns_yc.sh +++ b/dnsapi/dns_yc.sh @@ -22,21 +22,32 @@ dns_yc_add() { fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name txtvalue=$2 + # YC_SA_Key_File_PEM_b64/Path are always persisted to the domain conf below, + # so they must be recovered from there first (account conf is only a + # fallback for the YC_Folder_ID case, see the SA_ID/SA_Key_ID save below). + YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}" YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}" + YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}" YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}" if [ "$YC_SA_Key_File_PEM_b64" ]; then echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key YC_SA_Key_File="private.key" + _yc_key_is_temp=1 _savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64" else YC_SA_Key_File="$YC_SA_Key_File_Path" + _yc_key_is_temp="" _savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path" fi + YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}" YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}" + YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}" YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}" + YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}" YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}" + YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}" YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}" if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then @@ -65,11 +76,21 @@ dns_yc_add() { return 1 fi else + # Clear both possible stores -- YC_Zone_ID/YC_Folder_ID/key material are + # persisted to the domain conf, while YC_SA_ID/YC_SA_Key_ID may have been + # saved account-wide (Folder_ID mode), so a plain _clearaccountconf alone + # would leave stale values behind in whichever store wasn't touched. + _cleardomainconf YC_Zone_ID _clearaccountconf YC_Zone_ID + _cleardomainconf YC_Folder_ID _clearaccountconf YC_Folder_ID - _clearaccountconf YC_SA_ID - _clearaccountconf YC_SA_Key_ID + _cleardomainconf YC_SA_ID + _clearaccountconf_mutable YC_SA_ID + _cleardomainconf YC_SA_Key_ID + _clearaccountconf_mutable YC_SA_Key_ID + _cleardomainconf YC_SA_Key_File_PEM_b64 _clearaccountconf YC_SA_Key_File_PEM_b64 + _cleardomainconf YC_SA_Key_File_Path _clearaccountconf YC_SA_Key_File_Path _err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File." return 1 @@ -110,11 +131,30 @@ dns_yc_rm() { fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name txtvalue=$2 + YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}" YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}" + YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}" YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}" + YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}" YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}" + YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}" YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}" + # See dns_yc_add() for why domain conf is checked before account conf. + YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}" + YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}" + YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}" + YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}" + + if [ "$YC_SA_Key_File_PEM_b64" ]; then + echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key + YC_SA_Key_File="private.key" + _yc_key_is_temp=1 + else + YC_SA_Key_File="$YC_SA_Key_File_Path" + _yc_key_is_temp="" + fi + _debug "First detect the root zone" if ! _get_root "$fulldomain"; then _err "invalid domain" @@ -124,16 +164,10 @@ dns_yc_rm() { _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" - _debug "Getting txt records" - if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then - exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:][^:]*$") - _debug exists_txtvalue "$exists_txtvalue" - else - _err "Error: $response" - return 1 - fi - - if _yc_rest POST "zones/$_domain_id:updateRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":$exists_txtvalue}]}"; then + # upsertRecordSets.deletions removes only the given value from the rrset, + # leaving any other values at the same name (e.g. base + wildcard domain) + # intact -- no need to read the current data set and recompute it. + if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then if _contains "$response" "\"done\": true"; then _info "Delete, OK" return 0 @@ -255,7 +289,9 @@ _yc_login() { _signature=$(printf "%s.%s" "$header" "$payload" | _sign "$YC_SA_Key_File" "sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _url_replace) _debug2 _signature "$_signature" - rm -rf "$YC_SA_Key_File" + if [ "$_yc_key_is_temp" ]; then + rm -f "$YC_SA_Key_File" + fi _jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature") _debug2 _jwt "$_jwt" From bf90b845b2d673adcf2b132826eb975fe41e70f2 Mon Sep 17 00:00:00 2001 From: Joel Samson Date: Sun, 2 Aug 2026 09:34:41 -0400 Subject: [PATCH 677/689] Refactor dns_freemyip.sh for enhanced compatibility (#7166) * Refactor dns_freemyip.sh for clarity and compatibility Updated dns_freemyip.sh for better readability and compatibility with ASUSWRT-Merlin. Improved error handling and response logging. * Update author information in dns_freemyip.sh * replace both loops with POSIX shell counters replace both loops with POSIX shell counters * Typo Typo * Fix error message for freemyip API request failure Remove existing token leak. Not my regression. * Refactor retry logic and improve error handling * Remove unnecessary blank lines in dns_freemyip.sh * Clean up dns_freemyip.sh by removing blank lines Removed unnecessary blank lines in the script to improve readability. --- dnsapi/dns_freemyip.sh | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/dnsapi/dns_freemyip.sh b/dnsapi/dns_freemyip.sh index d598a657..18d8e7f9 100644 --- a/dnsapi/dns_freemyip.sh +++ b/dnsapi/dns_freemyip.sh @@ -6,7 +6,7 @@ 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 +Author: Recolic Keghart , @Giova96, ExtremeFiretop ' FREEMYIP_DNS_API="https://freemyip.com/update?" @@ -68,22 +68,30 @@ dns_freemyip_rm() { return $? } -################ Private functions below ################ +################ Private functions below ################ _get_root() { _fmi_d="$1" - echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev + echo "$_fmi_d" | sed 's/.*\.\([^.]*\.[^.]*\.[^.]*\)$/\1/' } # 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 + _fmi_i=1 + while [ "$_fmi_i" -le 8 ]; do + _debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..." + _fmi_response="$(_get "$_fmi_url")" + printf '%s\n' "$_fmi_response" >&2 + + if _contains "$_fmi_response" "OK"; then + return 0 + fi + _sleep 1 # DO NOT send the request too fast + _fmi_i=$((_fmi_i + 1)) done - _err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'" + _err "Failed to request freemyip API. Server does not say 'OK'" return 1 } @@ -93,13 +101,16 @@ _is_root_domain_published() { _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..." + _fmi_i=1 + while [ "$_fmi_i" -le 3 ]; do + _debug "'$_webroot' ns lookup, retry $_fmi_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 + _fmi_i=$((_fmi_i + 1)) done return 1 } From ea5e70564d94ec3aa4a06c72b33e458f0c8fcb91 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 2 Aug 2026 21:47:51 +0800 Subject: [PATCH 678/689] Fix --make-dns-persist-value printing a wildcard TXT record name For -d '*.example.com' the printed record name kept the literal '*' label (_validation-persist.*.example.com). The CA never queries that name, so issuance fails with "No TXT record found for DNS-PERSIST-01 challenge". Per draft-ietf-acme-dns-persist-01 sec 4 and 10.2 the record is published at the base domain's Validation Domain Name; the wildcard scope comes from 'policy=wildcard' in the record value (sec 5.1), not from a '*' label in the record name. Strip the leading "*." in a new _dns_persist_txt_name helper, and imply --dns-persist-wildcard for a wildcard -d, since without policy=wildcard the printed record can never authorize the wildcard. Fixes #7168 --- acme.sh | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index 17244133..9601d43d 100755 --- a/acme.sh +++ b/acme.sh @@ -4355,6 +4355,24 @@ deactivateaccount() { fi } +#domain +#Print the Validation Domain Name where the persistent TXT record must be +#published: the "_validation-persist" label prepended to the domain being +#validated (draft-ietf-acme-dns-persist-01 sec 4). +#A wildcard identifier is validated by the record at its base domain, so the +#leading "*." label is dropped: the wildcard scope comes from 'policy=wildcard' +#in the record value, not from a "*" label in the record name (sec 5.1, 10.2). +_dns_persist_txt_name() { + _dpt_domain="$1" + if _startswith "$_dpt_domain" "*."; then + _dpt_domain="$(echo "$_dpt_domain" | sed 's/^\*\.//')" + fi + if [ -z "$_dpt_domain" ]; then + return 1 + fi + echo "_validation-persist.$_dpt_domain" +} + #domain wildcard ca_name days #Print the TXT record(s) the user must add to enable persistent DNS validation #per draft-ietf-acme-dns-persist-01. @@ -4369,6 +4387,20 @@ makednspersistvalue() { return 1 fi + _txt_name="$(_dns_persist_txt_name "$_mdpv_domain")" + if [ -z "$_txt_name" ]; then + _err "Invalid domain: $_mdpv_domain" + return 1 + fi + _debug _txt_name "$_txt_name" + + #A wildcard identifier can only be issued if the record carries + #'policy=wildcard', so don't print a record that is guaranteed to fail. + if _startswith "$_mdpv_domain" "*." && [ "$_mdpv_wildcard" != "1" ]; then + _info "$_mdpv_domain is a wildcard domain, adding 'policy=wildcard' automatically." + _mdpv_wildcard="1" + fi + if [ -n "$_mdpv_days" ]; then case "$_mdpv_days" in '' | *[!0-9]*) @@ -4400,8 +4432,6 @@ makednspersistvalue() { fi _debug "Account URL" "$_accUri" - _txt_name="_validation-persist.$_mdpv_domain" - _txt_suffix="; accounturi=$_accUri" if [ "$_mdpv_wildcard" = "1" ]; then _txt_suffix="$_txt_suffix; policy=wildcard" @@ -8075,7 +8105,9 @@ Parameters: --dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the generated TXT record so the issuer is also authorized for wildcards - and subdomains (draft-ietf-acme-dns-persist-01). + and subdomains (draft-ietf-acme-dns-persist-01). It is implied when + the domain given to -d is a wildcard (e.g. '*.example.com'); the + record itself is always published at the base domain. --dns-persist-ca-name Used with '--make-dns-persist-value'. Use the given CA identity domain (e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If omitted, the identities are read from the ACME directory's From b4925052dd10f6b407fba5c5f550ebe829356d28 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 2 Aug 2026 21:55:19 +0800 Subject: [PATCH 679/689] Fix dns_cyon cleanup failing on FreeBSD _cyon_delete_txt relied on `printf "%b"` to convert a sed-injected literal `\n` into a real newline, but `%b` also processes the `\"` escapes that the JSON response is full of. glibc/bash/dash keep the backslash of such an undefined escape, FreeBSD's printf (sh builtin and /usr/bin/printf alike) drops it -- so `data-hash=\"..\"` became `data-hash=".."`, the extraction regex matched nothing, _dns_entries stayed empty and no TXT record was ever deleted. Drop the newline injection and use _egrep_o, which already yields one match per line, then parse each line with sed. Also feed the read loop a newline-terminated list: `printf "%s"` left the last line unterminated, so `read` returned non-zero at EOF and the loop skipped the final entry on every platform. Verified identical output on FreeBSD 14.3, Linux/bash and Linux/dash. Fixes #7169 --- dnsapi/dns_cyon.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index d4b6b6e8..6677b32f 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -285,15 +285,15 @@ _cyon_delete_txt() { list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async" - list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')" + list_txt_response="$(_get "${list_txt_url}")" _debug list_txt_response "${list_txt_response}" if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi # Find and delete all acme challenge entries for the $fulldomain. - _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')" + _dns_entries="$(printf "%s\n" "${list_txt_response}" | _egrep_o 'data-hash=\\"[^"]*\\" data-identifier=\\"[^"]*\\"' | sed 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\"/\1 \2/')" - printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do + printf "%s\n" "${_dns_entries}" | while read -r _hash _identifier; do dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)" dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)" From 5e6c263211d05d75434f10e61ac479b06ff30a1f Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 5 Aug 2026 19:53:52 +0800 Subject: [PATCH 680/689] Fix empty finalize URL when resuming a saved DNS-manual order The decision to resume a pending order is keyed on Le_Vlist, but the decision to keep Le_OrderFinalize/Le_LinkOrder was keyed on the webroot being exactly "dns". Any other webroot with a saved Le_Vlist skipped newOrder and then finalized against an empty URL. Key both on Le_Vlist, and always clear Le_LinkCert, which is per-run state that is never read back from the saved domain conf. Fixes #7177 --- acme.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/acme.sh b/acme.sh index 9601d43d..5b59ece4 100755 --- a/acme.sh +++ b/acme.sh @@ -4949,11 +4949,18 @@ issue() { if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" mkdir -p "$DOMAIN_PATH" - elif ! _hasfield "$_web_roots" "$W_DNS"; then + elif [ -z "$Le_Vlist" ]; then + # Whether the saved order is resumed is decided by Le_Vlist below, so key + # this on Le_Vlist too. With no pending order to resume a new one is + # created, and a stale order link from the previous issuance must not be + # reused. https://github.com/acmesh-official/acme.sh/issues/3635 Le_OrderFinalize="" Le_LinkOrder="" - Le_LinkCert="" fi + # Per-run state only: it is set after finalize and never read back from the + # saved domain conf. Carrying it over would make a run that gives up while + # the order is still 'processing' download the previous certificate again. + Le_LinkCert="" if _hasfield "$_web_roots" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then _err "$_DNS_MANUAL_ERROR" From 603a126a7cd557d001dfc903ddd0189c914262e6 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 5 Aug 2026 19:56:13 +0800 Subject: [PATCH 681/689] Fix synology_dsm logging out after the temp admin is already deleted _temp_admin_cleanup ran before _logout, so the logout request carried the session id of an account synouser had already removed and DSM kept the orphaned entry in Connected Users. Swap the order in both terminal branches, and add the missing _logout to the two post-login error paths (CRT list failure, certificate not found without SYNO_CREATE). _logout overwrites the global $response, so the upload-failure branch prints its error message before calling it. Reported by @Bertl75 in #7174 --- deploy/synology_dsm.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index d05e503a..336980a5 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -344,6 +344,7 @@ synology_dsm_deploy() { else _err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more." fi + _logout _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" return 1 fi @@ -354,6 +355,7 @@ synology_dsm_deploy() { if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then _err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set." + _logout _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" return 1 fi @@ -389,13 +391,13 @@ synology_dsm_deploy() { else _info "Restart HTTP services not necessary." fi - _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" _logout + _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" return 0 else - _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" _err "Unable to update certificate, got error response: $response." _logout + _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" return 1 fi } @@ -403,6 +405,8 @@ synology_dsm_deploy() { #################### Private functions below ################################## _logout() { # Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables) + # Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out. + # Note: this overwrites $response, so print any error message that needs it before calling. response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid") _debug3 response "$response" } From 9aad4dcbd5fb22f4c97ee72963af58ee25481d1b Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 5 Aug 2026 23:01:54 +0800 Subject: [PATCH 682/689] Fix multideploy MULTIDEPLOY_FILENAME conf read and allow an absolute path _getdeployconf assigns and exports the variable, it does not print the value, so wrapping it in a command substitution ran it in a subshell and always yielded an empty string. A MULTIDEPLOY_FILENAME saved by an earlier run was therefore never restored on renewal and the hook silently fell back to multideploy.yml. Call it the same way every other deploy hook does. Also treat a MULTIDEPLOY_FILENAME starting with '/' as an absolute path instead of always resolving it under DOMAIN_PATH, so one deploy file can live outside the certificate directory and be shared by all domains. Names without a leading '/' keep resolving under DOMAIN_PATH as before. --- deploy/multideploy.sh | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh index 375668ec..4a8c9dc9 100644 --- a/deploy/multideploy.sh +++ b/deploy/multideploy.sh @@ -10,6 +10,10 @@ # Usage (shown values are the examples): # 1. Set optional environment variables # - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set" +# A name without a leading '/' is looked up in the certificate directory +# of the domain. An absolute path is used as is, so a single deploy file +# can be shared by all domains, e.g. +# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml" # # 2. Run command: # acme.sh --deploy --deploy-hook multideploy -d example.com @@ -49,7 +53,7 @@ multideploy_deploy() { _debug _cfullchain "$_cfullchain" _debug _cpfx "$_cpfx" - MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" + _getdeployconf MULTIDEPLOY_FILENAME if [ -z "$MULTIDEPLOY_FILENAME" ]; then MULTIDEPLOY_FILENAME="multideploy.yml" _info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'." @@ -75,7 +79,8 @@ multideploy_deploy() { # This function preprocesses the deploy file by checking if 'yq' is installed, # verifying the existence of the deploy file, and ensuring only one deploy file is present. # Arguments: -# $@ - Posible deploy file names. +# $@ - Posible deploy file names. A name starting with '/' is treated as an +# absolute path, any other name is relative to the domain directory. # Usage: # _preprocess_deployfile "" "?" _preprocess_deployfile() { @@ -87,15 +92,21 @@ _preprocess_deployfile() { _debug3 "yq is installed." # Check if deploy file exists + found_file="" for file in "$@"; do - _debug3 "Checking file" "$DOMAIN_PATH/$file" - if [ -f "$DOMAIN_PATH/$file" ]; then + if _startswith "$file" "/"; then + _multideploy_path="$file" + else + _multideploy_path="$DOMAIN_PATH/$file" + fi + _debug3 "Checking file" "$_multideploy_path" + if [ -f "$_multideploy_path" ]; then _debug3 "File found" if [ -n "$found_file" ]; then _err "Multiple deploy files found. Please keep only one deploy file." return 1 fi - found_file="$file" + found_file="$_multideploy_path" else _debug3 "File not found" fi @@ -105,12 +116,12 @@ _preprocess_deployfile() { _err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." return 1 fi - if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then - _err "Deploy file is not valid: $DOMAIN_PATH/$found_file" + if ! _check_deployfile "$found_file"; then + _err "Deploy file is not valid: $found_file" return 1 fi - echo "$DOMAIN_PATH/$found_file" + echo "$found_file" } # Description: From f67be78ff429df2ebd7c447cfefd463804ddb4b6 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 6 Aug 2026 19:36:26 +0800 Subject: [PATCH 683/689] Fix dns_namecheap ignoring IsOurDNS when matching the root zone _get_root_by_getList() matched the candidate suffix as an unanchored substring of the whole domains.getList response and never looked at the IsOurDNS attribute. A domain parked on Namecheap's webhosting DNS is listed with IsOurDNS="false", yet it was still accepted as the root zone, so _get_root() returned success and the domains.dns.getHosts probe that would have found the real zone never ran. Every following getHosts call was then refused with error 2030288 "not using proper DNS servers" and the challenge failed with "invalid tld". Match the exact entry instead and require IsOurDNS="true", so a subdomain delegated to Namecheap BasicDNS/FreeDNS under a parent that is not on Namecheap DNS now resolves to its own zone. Matching the entry exactly also drops the old substring/regex match, in which the dots of a domain matched any character. Fixes #7178 --- dnsapi/dns_namecheap.sh | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh index cca59735..7035640b 100755 --- a/dnsapi/dns_namecheap.sh +++ b/dnsapi/dns_namecheap.sh @@ -104,6 +104,9 @@ _get_root_by_getList() { return 1 fi + _namecheap_domain_list=$(echo "$response" | _egrep_o ']*') + _debug2 domain_list "$_namecheap_domain_list" + i=2 p=1 @@ -120,7 +123,7 @@ _get_root_by_getList() { return 1 fi - if ! _contains "$response" "$h"; then + if ! _namecheap_is_our_dns "$h"; then _debug "$h not found" else _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") @@ -133,6 +136,29 @@ _get_root_by_getList() { return 1 } +#Usage: _namecheap_is_our_dns +#Succeeds only when domains.getList listed exactly AND that entry is +#served by Namecheap's own DNS. A domain parked on Namecheap's webhosting DNS +#is listed with IsOurDNS="false", and every dns.getHosts/setHosts call against +#it is refused with error 2030288 "not using proper DNS servers". Accepting +#such a domain as the root zone hides a subdomain that IS delegated to +#Namecheap DNS and that the getHosts probe below would have found. +#https://github.com/acmesh-official/acme.sh/issues/7178 +_namecheap_is_our_dns() { + _namecheap_entry=$(echo "$_namecheap_domain_list" | grep -F " Name=\"$1\"" | _head_n 1) + if [ -z "$_namecheap_entry" ]; then + return 1 + fi + + _namecheap_ourdns=$(echo "$_namecheap_entry" | _egrep_o ' IsOurDNS="[^"]*' | cut -d '"' -f 2) + _debug2 "$1 IsOurDNS" "$_namecheap_ourdns" + + if [ "$_namecheap_ourdns" = "true" ]; then + return 0 + fi + return 1 +} + _get_root_by_getHosts() { i=100 p=99 From f1cbba05f67e82f921990d385add7db17224d56c Mon Sep 17 00:00:00 2001 From: Alexey Morozov <223741939+morozov-alexey@users.noreply.github.com> Date: Fri, 7 Aug 2026 03:22:28 +0200 Subject: [PATCH 684/689] feat: added nexdns dnsapi (#7170) * feat: added nexdns dnsapi Adds a DNS-01 hook for NexDNS, an authoritative DNS service with a REST API. dns_nexdns_add walks the label list to find the zone that owns the challenge name and creates the TXT record in it. dns_nexdns_rm lists the TXT records at that name, picks the one carrying exactly this challenge value and deletes it by id, so a wildcard and its base domain do not remove each other's record. A 429 is waited out and the request retried, in the shape dns_hetznercloud.sh and dns_bunny.sh already use. * dns_nexdns: cap the rate-limit wait, judge success by status, add the tracking issue --- dnsapi/dns_nexdns.sh | 244 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100755 dnsapi/dns_nexdns.sh diff --git a/dnsapi/dns_nexdns.sh b/dnsapi/dns_nexdns.sh new file mode 100755 index 00000000..e4447c0e --- /dev/null +++ b/dnsapi/dns_nexdns.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_nexdns_info='NexDNS +Site: nexdns.tech +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_nexdns +Options: + NEXDNS_Token API token. Can be created at https://nexdns.tech/settings/api-keys + NEXDNS_Api API base url. Default "https://api.nexdns.tech/v1". Optional. +Issues: github.com/acmesh-official/acme.sh/issues/7179 +Author: NexDNS +' + +NEXDNS_Api_Default="https://api.nexdns.tech/v1" + +######## Public functions ##################### + +#Usage: dns_nexdns_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nexdns_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _nexdns_init; then + return 1 + fi + + _saveaccountconf_mutable NEXDNS_Token "$NEXDNS_Token" + if [ "$NEXDNS_Api" != "$NEXDNS_Api_Default" ]; then + _saveaccountconf_mutable NEXDNS_Api "$NEXDNS_Api" + else + _clearaccountconf_mutable NEXDNS_Api + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Cannot find the zone of $fulldomain in this NexDNS account." + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _domain_id "$_domain_id" + + _info "Adding the TXT record for $fulldomain" + if ! _nexdns_rest POST "zones/$_domain_id/records" "{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}"; then + return 1 + fi + + _info "The TXT record has been added." + return 0 +} + +#Usage: dns_nexdns_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_nexdns_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _nexdns_init; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Cannot find the zone of $fulldomain in this NexDNS account." + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug _domain_id "$_domain_id" + + _info "Removing the TXT record for $fulldomain" + if ! _nexdns_rest GET "zones/$_domain_id/records?type=TXT&name=$_sub_domain"; then + return 1 + fi + + #All the challenge records share one name and one type, so the value is the + #only thing that tells them apart. A certificate covering example.com and + #*.example.com puts two of them at the same name at the same time. + _record_id="$(echo "$response" | tr '{' "\n" | grep -- "$txtvalue" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" + _debug _record_id "$_record_id" + + if [ -z "$_record_id" ]; then + _info "The TXT record is already gone, nothing to remove." + return 0 + fi + + if ! _nexdns_rest DELETE "zones/$_domain_id/records/$_record_id"; then + return 1 + fi + + _info "The TXT record has been removed." + return 0 +} + +#################### Private functions below ################################## + +#Reads the token and the api url, and applies the default url. +_nexdns_init() { + NEXDNS_Token="${NEXDNS_Token:-$(_readaccountconf_mutable NEXDNS_Token)}" + NEXDNS_Api="${NEXDNS_Api:-$(_readaccountconf_mutable NEXDNS_Api)}" + + if [ -z "$NEXDNS_Token" ]; then + _err "You have not set NEXDNS_Token yet." + _err "Create one at https://nexdns.tech/settings/api-keys, on a plan that includes API access, then:" + _err "export NEXDNS_Token=\"your-api-token\"" + return 1 + fi + + if [ -z "$NEXDNS_Api" ]; then + NEXDNS_Api="$NEXDNS_Api_Default" + fi + #A trailing slash would make every request path begin with a double slash. + NEXDNS_Api="$(echo "$NEXDNS_Api" | sed 's|/*$||')" + _debug NEXDNS_Api "$NEXDNS_Api" + + return 0 +} + +#_acme-challenge.www.example.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=example.com +# _domain_id=Zm9vYmFy +_get_root() { + domain=$1 + i=1 + p=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 ! _nexdns_rest GET "zones?search=$h&per_page=100"; then + return 1 + fi + + #search matches on a substring, so the page can also hold zones that merely + #contain h. Take the id of the one whose name is exactly h. + _domain_id="$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done +} + +#Usage: _nexdns_rest GET|POST|DELETE path [body] [attempt] +_nexdns_rest() { + m=$1 + ep=$2 + data=$3 + attempt=${4:-1} + _debug "$ep" + + export _H1="Authorization: Bearer $NEXDNS_Token" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + if [ "$m" = "GET" ]; then + response="$(_get "$NEXDNS_Api/$ep")" + else + _debug2 data "$data" + response="$(_post "$data" "$NEXDNS_Api/$ep" "" "$m" "application/json")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + + #A single certificate costs a handful of requests, but a renewal sweep over + #many of them meets the account's per-minute budget, and that run is + #unattended. Retry-After is treated as a floor: an api may report the time one + #token needs at an average rate and name a second when nothing frees for a + #minute, so the wait grows on its own across attempts. + if [ "$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" = "429" ]; then + if [ "$attempt" -ge 4 ]; then + _err "$m $ep failed: rate limited, and the wait budget is spent" + return 1 + fi + + _retry_after="$(grep -i "^Retry-After" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d : -f 2 | tr -d " \r\n")" + _backoff="$(_math "$attempt" \* 15)" + #The header may also carry an http date. Anything but a plain count of + #seconds falls through to the backoff rather than being parsed: guessing + #wrong about a date is worse than waiting a known interval, and comparing a + #date numerically would abort the hook outright. + case "$_retry_after" in + "" | *[!0-9]*) _retry_after="$_backoff" ;; + *) + if [ "$_retry_after" -lt "$_backoff" ]; then + _retry_after="$_backoff" + fi + ;; + esac + + #A wait longer than this is a refusal rather than a schedule, and sleeping + #it out would hold the hook for the length of the window. Hand the run back + #instead, so the next cron pass picks it up. + if [ "$_retry_after" -gt 120 ]; then + _err "$m $ep failed: rate limited for ${_retry_after}s, longer than this hook will wait" + return 1 + fi + + _info "Rate limited by the NexDNS API; retrying in $_retry_after seconds." + _sleep "$_retry_after" + + _nexdns_rest "$m" "$ep" "$data" "$(_math "$attempt" + 1)" + return $? + fi + + #Whitespace between a key and its value would defeat every match made on the + #body, here and in the callers. + response="$(echo "$response" | _normalizeJson)" + _debug2 response "$response" + + #The status line decides success, not the body: a delete answers 204 with no + #body at all, and a record whose own content contains "error": would otherwise + #turn a stored value into a reported failure. The body is read only for the + #message once the status says the request was rejected. + _code="$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug2 _code "$_code" + case "$_code" in + "" | 2*) + return 0 + ;; + esac + + #A rejected request carries {"error":{"code":..,"message":..}}, so say what the + #api says went wrong. + _message="$(echo "$response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" + if [ -z "$_message" ]; then + _message="status $_code" + fi + _err "$m $ep failed: $_message" + + return 1 +} From 05367d3598b43618f7e89f160f5d570f2ab1da39 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 8 Aug 2026 13:09:18 +0800 Subject: [PATCH 685/689] Listen on both IPv4 and IPv6 in standalone mode by default socat binds a single family unless told which one: up to 1.7.x the default IP version for TCP-LISTEN is 4, and 1.8.0 made it "no preference", which resolves to whatever getaddrinfo and bindv6only happen to give. So an order carrying both an IPv4 and an IPv6 identifier could never pass both http-01 challenges. Bind one socket per family instead, with ipv6only on the IPv6 one so the two do not collide. IPv4-mapped IPv6 addresses are not a portable alternative, OpenBSD does not support them at all. The IPv6 listener is best effort, a host without IPv6 still gets the IPv4 one. The python fallback does the same. --listen-v4 and --listen-v6 keep forcing a single family, and passing both now means both. Le_Listen_V4 and Le_Listen_V6 were mutually exclusive in the domain conf, which silently dropped one of them on renewal, and _starttlsserver let -4 win when both were set. Fixes #7185 --- acme.sh | 115 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/acme.sh b/acme.sh index 5b59ece4..4d4f6cbe 100755 --- a/acme.sh +++ b/acme.sh @@ -2715,6 +2715,21 @@ _clearcaconf() { _clear_conf "$CA_CONF" "$1" } +#Starts a socat listener in the background, the pid is set to _socat_pid. +#It uses the content, _content_len, _NC and _SOCAT_ERR of _startserver. +#options +_startsocat() { + _socat_opts="$1" + _debug "_NC" "$_NC $_socat_opts" + $_NC $_socat_opts SYSTEM:"sleep 1; \ +echo 'HTTP/1.0 200 OK'; \ +echo 'Content-Length\: $_content_len'; \ +echo ''; \ +printf '%s' '$content';" 2>>"$_SOCAT_ERR" & + _socat_pid="$!" + _debug "_socat_pid" "$_socat_pid" +} + # content localaddress _startserver() { content="$1" @@ -2728,16 +2743,24 @@ _startserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" + _serverproc_v6="" if _exists "socat"; then _NC="socat" - if [ "$Le_Listen_V6" ]; then + SOCAT_OPTIONS6="" + if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then _NC="$_NC -6" SOCAT_OPTIONS=TCP6-LISTEN - elif [ "$Le_Listen_V4" ]; then + elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then _NC="$_NC -4" SOCAT_OPTIONS=TCP4-LISTEN - else + elif [ "$ncaddr" ]; then + #a single local address belongs to a single family, let socat pick it SOCAT_OPTIONS=TCP-LISTEN + else + #listen on both ipv4 and ipv6, with one socket for each family: + #ipv4-mapped ipv6 addresses are not available everywhere. + SOCAT_OPTIONS=TCP4-LISTEN + SOCAT_OPTIONS6=TCP6-LISTEN fi if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then @@ -2745,6 +2768,10 @@ _startserver() { fi SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork + if [ "$SOCAT_OPTIONS6" ]; then + #ipv6only keeps this socket from colliding with the ipv4 one + SOCAT_OPTIONS6=$SOCAT_OPTIONS6:$Le_HTTPPort,crlf,reuseaddr,fork,ipv6only=1 + fi #Adding bind to local-address if [ "$ncaddr" ]; then @@ -2753,14 +2780,14 @@ _startserver() { _content_len="$(printf "%s" "$content" | wc -c)" _debug _content_len "$_content_len" - _debug "_NC" "$_NC $SOCAT_OPTIONS" export _SOCAT_ERR="$(_mktemp)" - $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \ -echo 'HTTP/1.0 200 OK'; \ -echo 'Content-Length\: $_content_len'; \ -echo ''; \ -printf '%s' '$content';" 2>"$_SOCAT_ERR" & - serverproc="$!" + _startsocat "$SOCAT_OPTIONS" + serverproc="$_socat_pid" + if [ "$SOCAT_OPTIONS6" ]; then + #best effort, the host may have no ipv6 support at all + _startsocat "$SOCAT_OPTIONS6" + _serverproc_v6="$_socat_pid" + fi else _PYTHON="" if _exists "python3"; then @@ -2772,21 +2799,40 @@ printf '%s' '$content';" 2>"$_SOCAT_ERR" & fi if [ "$_PYTHON" ]; then _debug "Using python: $_PYTHON" - _AF="socket.AF_INET" - _BIND_ADDR="0.0.0.0" - if [ "$Le_Listen_V6" ]; then - _AF="socket.AF_INET6" + #a comma separated list of addresses to listen on, one socket for each + _BIND_ADDR="0.0.0.0,::" + if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then _BIND_ADDR="::" + elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then + _BIND_ADDR="0.0.0.0" fi if [ "$ncaddr" ]; then _BIND_ADDR="$ncaddr" fi + _debug "_BIND_ADDR" "$_BIND_ADDR" export _SOCAT_ERR="$(_mktemp)" - $_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3]; + $_PYTHON -c "import socket,sys,select +res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3] +ads=sys.argv[2].split(',') +ls=[] +for ad in ads: + try: + sk=socket.socket(socket.AF_INET6 if ':' in ad else socket.AF_INET,socket.SOCK_STREAM) + sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) + if ':' in ad and len(ads)>1: + sk.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1) + sk.bind((ad,int(sys.argv[1]))) + sk.listen(5) + ls.append(sk) + except Exception: + sys.stderr.write(str(sys.exc_info()[1])+'\n') +if not ls: + sys.exit(1) while True: - c,a=s.accept() - c.sendall(res.encode() if hasattr(res, 'encode') else res) - c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & + for sk in select.select(ls,[],[])[0]: + c,a=sk.accept() + c.sendall(res.encode() if hasattr(res, 'encode') else res) + c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" & serverproc="$!" _NC="$_PYTHON" else @@ -2809,6 +2855,11 @@ while True: _stopserver() { pid="$1" _debug "pid" "$pid" + if [ "$_serverproc_v6" ]; then + _debug "_serverproc_v6" "$_serverproc_v6" + kill $_serverproc_v6 >/dev/null 2>&1 + _serverproc_v6="" + fi if [ -z "$pid" ]; then rm -f "$_SOCAT_ERR" return @@ -2882,9 +2933,11 @@ _starttlsserver() { _debug Le_Listen_V4 "$Le_Listen_V4" _debug Le_Listen_V6 "$Le_Listen_V6" - if [ "$Le_Listen_V4" ]; then + #openssl s_server binds a single socket, so both options together can only + #mean: do not force a family, same as when neither of them is given. + if [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then __S_OPENSSL="$__S_OPENSSL -4" - elif [ "$Le_Listen_V6" ]; then + elif [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then __S_OPENSSL="$__S_OPENSSL -6" fi @@ -5986,12 +6039,17 @@ $_authorizations_map" _clearaccountconf "HTTPS_INSECURE" fi - if [ "$Le_Listen_V4" ]; then - _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" - _cleardomainconf Le_Listen_V6 - elif [ "$Le_Listen_V6" ]; then - _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" - _cleardomainconf Le_Listen_V4 + if [ "$Le_Listen_V4" ] || [ "$Le_Listen_V6" ]; then + if [ "$Le_Listen_V4" ]; then + _savedomainconf "Le_Listen_V4" "$Le_Listen_V4" + else + _cleardomainconf Le_Listen_V4 + fi + if [ "$Le_Listen_V6" ]; then + _savedomainconf "Le_Listen_V6" "$Le_Listen_V6" + else + _cleardomainconf Le_Listen_V6 + fi fi if [ "$Le_ForceNewDomainKey" = "1" ]; then @@ -8172,8 +8230,9 @@ Parameters: --ocsp, --ocsp-must-staple Generate OCSP-Must-Staple extension. --always-force-new-domain-key Generate new domain key on renewal. Otherwise, the domain key is not changed by default. --auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted. - --listen-v4 Force standalone/tls server to listen at ipv4. - --listen-v6 Force standalone/tls server to listen at ipv6. + --listen-v4 Force standalone/tls server to listen at ipv4 only. + By default the standalone server listens on both ipv4 and ipv6. + --listen-v6 Force standalone/tls server to listen at ipv6 only. --request-v4 Force client requests to use ipv4 to connect to the CA server. --request-v6 Force client requests to use ipv6 to connect to the CA server. --openssl-bin Specifies a custom openssl bin location. From 4a3bc2c9193360bc1a8fbf7e0d7974c8e3d0a2f1 Mon Sep 17 00:00:00 2001 From: neil Date: Mon, 10 Aug 2026 10:00:26 +0800 Subject: [PATCH 686/689] Fix dns_netcup reporting a bogus 4013 instead of the real zone error The zone lookup walked the challenge name from the right and ended up asking netcup for the full "_acme-challenge." as a zone name. That can never be a zone, so netcup answered 4013 "Validation Error", which replaced the real 5028 "The zone could not be found" as the error shown to the user. Stop one label short of the full name, and fail explicitly when no zone matched, reporting the last API response plus what to check. Before, a run where every candidate returned 5028 fell through to logout and returned success. --- dnsapi/dns_netcup.sh | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_netcup.sh b/dnsapi/dns_netcup.sh index 8609adf6..3b291854 100644 --- a/dnsapi/dns_netcup.sh +++ b/dnsapi/dns_netcup.sh @@ -33,9 +33,11 @@ dns_netcup_add() { exit=$(echo "$fulldomain" | tr -dc '.' | wc -c) exit=$(_math "$exit" + 1) i=$exit + _nc_last=$(_nc_lastlevel "$i") + _nc_found="" while - [ "$exit" -gt 0 ] + [ "$exit" -ge "$_nc_last" ] do tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") if [ "$(_math "$i" - "$exit")" -eq 0 ]; then @@ -51,12 +53,18 @@ dns_netcup_add() { _err "$msg" return 1 else + _nc_found=1 break fi fi fi exit=$(_math "$exit" - 1) done + if [ -z "$_nc_found" ]; then + _err "$msg" + _nc_nozone "$fulldomain" + return 1 + fi logout } @@ -70,9 +78,11 @@ dns_netcup_rm() { exit=$(_math "$exit" + 1) i=$exit rec="" + _nc_last=$(_nc_lastlevel "$i") + _nc_found="" while - [ "$exit" -gt 0 ] + [ "$exit" -ge "$_nc_last" ] do tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit") if [ "$(_math "$i" - "$exit")" -eq 0 ]; then @@ -89,12 +99,18 @@ dns_netcup_rm() { _err "$msg" return 1 else + _nc_found=1 break fi fi fi exit=$(_math "$exit" - 1) done + if [ -z "$_nc_found" ]; then + _err "$msg" + _nc_nozone "$fulldomain" + return 1 + fi ida=0000 idv=0001 @@ -125,6 +141,27 @@ dns_netcup_rm() { logout } +# The zone is looked up by walking the challenge name from the right, one +# label at a time. The leftmost label is the challenge prefix, so the full +# name itself can never be a zone: asking netcup for it only returns 4013 +# "Validation Error", which would then mask the real 5028 "zone could not be +# found". Stop one label short, unless the name is too short to have a +# challenge prefix at all (manual invocation). +# levels +_nc_lastlevel() { + if [ "$1" -ge 3 ]; then + echo 2 + else + echo 1 + fi +} + +# fulldomain +_nc_nozone() { + _err "No DNS zone for $1 was found at netcup." + _err "Check that the domain belongs to the account of the configured NC_CID and that its DNS is hosted at netcup." +} + _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) From 2e2782f0d87b620acb1b17935ab658b4b904788c Mon Sep 17 00:00:00 2001 From: Zhiwei Liang Date: Wed, 12 Aug 2026 10:47:58 -0400 Subject: [PATCH 687/689] Remove deprecated Linode API v3 DNS plugin (#7054) Signed-off-by: Zhiwei Liang --- dnsapi/dns_linode.sh | 189 ------------------------------------------- 1 file changed, 189 deletions(-) delete mode 100755 dnsapi/dns_linode.sh diff --git a/dnsapi/dns_linode.sh b/dnsapi/dns_linode.sh deleted file mode 100755 index d74d1fc8..00000000 --- a/dnsapi/dns_linode.sh +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_linode_info='Linode.com (Old) - Deprecated. Use dns_linode_v4 -Site: Linode.com -Options: - LINODE_API_KEY API Key -Author: Philipp Grosswiler -' - -LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action=" - -######## Public functions ##################### - -#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -dns_linode_add() { - fulldomain="${1}" - txtvalue="${2}" - - if ! _Linode_API; then - return 1 - fi - - _info "Using Linode" - _debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Domain does not exist." - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue" - - if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then - _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) - _debug _resource_id "$_resource_id" - - if [ -z "$_resource_id" ]; then - _err "Error adding the domain resource." - return 1 - fi - - _info "Domain resource successfully added." - return 0 - fi - - return 1 -} - -#Usage: dns_linode_rm _acme-challenge.www.domain.com -dns_linode_rm() { - fulldomain="${1}" - - if ! _Linode_API; then - return 1 - fi - - _info "Using Linode" - _debug "Calling: dns_linode_rm() '${fulldomain}'" - - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "Domain does not exist." - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _parameters="&DomainID=$_domain_id" - - if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then - response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" - - resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")" - if [ "$resource" ]; then - _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) - if [ "$_resource_id" ]; then - _debug _resource_id "$_resource_id" - - _parameters="&DomainID=$_domain_id&ResourceID=$_resource_id" - - if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then - _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) - _debug _resource_id "$_resource_id" - - if [ -z "$_resource_id" ]; then - _err "Error deleting the domain resource." - return 1 - fi - - _info "Domain resource successfully deleted." - return 0 - fi - fi - - return 1 - fi - - return 0 - fi - - return 1 -} - -#################### Private functions below ################################## - -_Linode_API() { - if [ -z "$LINODE_API_KEY" ]; then - LINODE_API_KEY="" - - _err "You didn't specify the Linode API key yet." - _err "Please create your key and try again." - - return 1 - fi - - _saveaccountconf LINODE_API_KEY "$LINODE_API_KEY" -} - -#################### Private functions below ################################## -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -# _domain_id=12345 -_get_root() { - domain=$1 - i=2 - p=1 - - if _rest GET "domain.list"; then - response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")" - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug h "$h" - if [ -z "$h" ]; then - #not valid - return 1 - fi - - hostedzone="$(echo "$response" | _egrep_o "{.*\"DOMAIN\":\s*\"$h\".*}")" - if [ "$hostedzone" ]; then - _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"DOMAINID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 - fi - return 1 - fi - p=$i - i=$(_math "$i" + 1) - done - fi - return 1 -} - -#method method action data -_rest() { - mtd="$1" - ep="$2" - data="$3" - - _debug mtd "$mtd" - _debug ep "$ep" - - export _H1="Accept: application/json" - export _H2="Content-Type: application/json" - - if [ "$mtd" != "GET" ]; then - # both POST and DELETE. - _debug data "$data" - response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")" - else - response="$(_get "$LINODE_API_URL$ep$data")" - fi - - if [ "$?" != "0" ]; then - _err "error $ep" - return 1 - fi - _debug2 response "$response" - return 0 -} From a89ba9c2e5ca269047adf585a371bd7bd3e25587 Mon Sep 17 00:00:00 2001 From: XuChao Date: Thu, 13 Aug 2026 14:52:48 +0800 Subject: [PATCH 688/689] add deploy hook support for ikuai (#6456) * add deploy hook support for ikuai * fix shellcheck warn and shfmt the code * 1.fix config load 2.use _secre_debug2 to log password 3.use fullchain to deploy 4.fix hardcode id 5.change shebang * fix miss ; after cookie * 1.fix shfmt ; 2.fix IKUAI_CERT_ID conf load; 3.correct IKUAI_CERT_ID description * fix some log msg * fix shfmt --- deploy/ikuai.sh | 114 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 deploy/ikuai.sh diff --git a/deploy/ikuai.sh b/deploy/ikuai.sh new file mode 100644 index 00000000..fa0926dc --- /dev/null +++ b/deploy/ikuai.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env sh + +# Here is a script to deploy cert to ikuai using curl +# +# it requires following environment variables: +# +# IKUAI_SCHEME="http" - http or https , defaults to "http" +# IKUAI_HOSTNAME="localhost" - host , defaults to "192.168.9.1" +# IKUAI_PORT="80" - port , defaults to "80" +# IKUAI_USERNAME="admin" - username , defaults to "admin" +# IKUAI_PASSWORD="yourPassword" - password +# IKUAI_CERT_ID=1 - ikuai cert id , defaults to 1, and only 1 is supported for now !!! +# +#returns 0 means success, otherwise error. +# +######## Public functions ##################### +# +#domain keyfile certfile cafile fullchain +ikuai_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" + + # Get deploy conf + _getdeployconf IKUAI_SCHEME + _getdeployconf IKUAI_HOSTNAME + _getdeployconf IKUAI_PORT + _getdeployconf IKUAI_USERNAME + _getdeployconf IKUAI_PASSWORD + _getdeployconf IKUAI_CERT_ID + + # Use default if not provided + [ -n "$IKUAI_SCHEME" ] || IKUAI_SCHEME="http" + [ -n "$IKUAI_HOSTNAME" ] || IKUAI_HOSTNAME="192.168.9.1" + [ -n "$IKUAI_PORT" ] || IKUAI_PORT=80 + [ -n "$IKUAI_USERNAME" ] || IKUAI_USERNAME="admin" + [ -n "$IKUAI_CERT_ID" ] || IKUAI_CERT_ID=1 + + if [ -z "$IKUAI_PASSWORD" ]; then + _err "please define IKUAI_PASSWORD." + return 1 + fi + + _debug2 IKUAI_SCHEME "$IKUAI_SCHEME" + _debug2 IKUAI_HOSTNAME "$IKUAI_HOSTNAME" + _debug2 IKUAI_PORT "$IKUAI_PORT" + _debug2 IKUAI_USERNAME "$IKUAI_USERNAME" + _secure_debug2 IKUAI_PASSWORD "$IKUAI_PASSWORD" + + _info "Login to ikuai ..." + _ikuai_url="$IKUAI_SCHEME://$IKUAI_HOSTNAME:$IKUAI_PORT" + _pass_md5="$(printf "%s" "$IKUAI_PASSWORD" | _digest md5 hex | _lower_case)" + _pass_salt="$(printf "salt_11%s" "$IKUAI_PASSWORD" | _base64)" + _debug2 _ikuai_url "$_ikuai_url" + + _login_req="{\"username\":\"$IKUAI_USERNAME\",\"passwd\":\"$_pass_md5\",\"pass\":\"$_pass_salt\",\"remember_password\":\"\"}" + _response=$(_post "$_login_req" "$_ikuai_url/Action/login" "" "POST" "application/json") + + _err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)" + # check ErrMsg + if [ "$_err_msg" != "Success" ]; then + _err "Failed to login to ikuai: $_err_msg" + return 1 + fi + # check cookie + _cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | sed 's/;.*//')" + if [ -z "$_cookie" ]; then + _err "Fail to get the cookie." + return 1 + fi + + # Set cookie header + _H1="Cookie: $_cookie; username=$IKUAI_USERNAME; login=1" + + _info "Deploy the cert to ikuai ... " + + # Should replace \n to @ ," " to # + _cert_content_single_line="$(tr <"$_cfullchain" '\n' '@' | tr ' ' '#')" + _key_content_single_line="$(tr <"$_ckey" '\n' '@' | tr ' ' '#')" + + _debug2 _cert_content_single_line "$_cert_content_single_line" + _secure_debug2 _key_content_single_line "$_key_content_single_line" + + _key_manager_req="{\"func_name\":\"key_manager\",\"action\":\"save\",\"param\":{\"ca\":\"$_cert_content_single_line\",\"key\":\"$_key_content_single_line\",\"id\":$IKUAI_CERT_ID,\"enabled\":\"yes\",\"comment\":\"\"}}" + _response=$(_post "$_key_manager_req" "$_ikuai_url/Action/call" "" "POST" "application/json") + + _err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)" + # check ErrMsg + if [ "$_err_msg" != "Success" ]; then + _err "Failed to deploy the cert to ikuai: $_err_msg" + return 1 + fi + + _info "Save the deploy config ... " + # Save the config + _savedeployconf IKUAI_SCHEME "$IKUAI_SCHEME" + _savedeployconf IKUAI_HOSTNAME "$IKUAI_HOSTNAME" + _savedeployconf IKUAI_PORT "$IKUAI_PORT" + _savedeployconf IKUAI_USERNAME "$IKUAI_USERNAME" + _savedeployconf IKUAI_PASSWORD "$IKUAI_PASSWORD" + _savedeployconf IKUAI_CERT_ID "$IKUAI_CERT_ID" + + _info "Successfully deployed certificate to ikuai. Enjoy! :>" + + return 0 +} From 41bdd4cd0e9bc908fa57dcfb9f8afc3c2658af40 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 12 Aug 2026 23:57:44 -0700 Subject: [PATCH 689/689] Add UniFi OS Server deploy hook (#7184) * Add UniFi OS Server deploy hook Uses UniFi OS Server's local REST API (login, list, upload, activate, remove superseded) since it stores certificates in its own Postgres database rather than flat config files, unlike the Cloud Key/UDM hardware covered by the existing unifi deploy hook. Tested against real instances on both macOS and Ubuntu 26.04 (self-hosted, remote). * Address review: portable sed/grep, scoped HTTPS_INSECURE, fingerprint matching - Replace GNU-only \n in sed replacement with a portable literal newline (matches dnsapi/dns_cpanel_uapi.sh, dnsapi/dns_glesys.sh); pipe the list response through _normalizeJson first for consistent formatting. - Use grep -F for the domain-name match instead of an unescaped BRE -- a wildcard cert name (*.example.com) broke the regex. - Drop \W (undocumented, GNU-only) from the cookie lookup in favor of an anchored `^Set-Cookie: *NAME=` match. - Scope HTTPS_INSECURE=1 inside the hook (matches deploy/proxmoxve.sh, deploy/fritzbox.sh) instead of requiring the caller to export it for the whole acme.sh run, which would also disable verification for the connection to the ACME CA. - On a duplicate-certificate response, match the existing entry by fingerprint instead of taking the first name match -- with more than one stale entry for a domain, the wrong one could get activated. - Check the list endpoint's response code before proceeding. - Save username/password with the "base64" flag (matches deploy/synology_dsm.sh) since _save_conf wraps values in unescaped single quotes. * Rework certificate handling: unique names per upload, drop cleanup Testing against a real UniFi OS Server showed the server enforces name uniqueness independently of fingerprint uniqueness, and that activation is exclusive server-wide regardless of name/domain. A unique name per upload avoids the name-collision path entirely (previously only handled as a retry-of-identical-content edge case), and removes the need for the post-hoc cleanup loop, which risked deleting the wrong entry. Co-Authored-By: Claude Sonnet 5 * Shorten generated certificate name to Unix epoch seconds Real-hardware testing showed the UniFi OS Server certificate list's name column is fixed-width and doesn't wrap, so a full human-readable timestamp overlaps the Expires column and makes both unreadable. Epoch seconds are still short enough to fit while remaining unique. Co-Authored-By: Claude Sonnet 5 * Add scoped cleanup of old certificate entries, use _time helper Per review: dropping cleanup entirely went further than the original bug required, and left old entries (each holding a private key) accumulating indefinitely. Since every upload now gets a name unique to its domain and run, cleanup can safely target only entries whose name starts with that domain -- entries this hook itself created -- excluding the one just activated. Also swaps date +%s for the core _time helper, and rewrote the design comments to make them clearer and match the current behavior instead of the pre-redesign one. Co-Authored-By: Claude Sonnet 5 --------- Co-authored-by: Claude Sonnet 5 --- deploy/unifios.sh | 307 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 deploy/unifios.sh diff --git a/deploy/unifios.sh b/deploy/unifios.sh new file mode 100644 index 00000000..82b65c69 --- /dev/null +++ b/deploy/unifios.sh @@ -0,0 +1,307 @@ +#!/usr/bin/env sh +# Deploy hook for UniFi OS Server (self-hosted). +# +# Supports: +# - UniFi OS Server on macOS +# - UniFi OS Server on Linux +# - UniFi OS Server on Windows should also work (runs under WSL2), but +# has not been tested. +# +# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local). +# +# This is a different product from the Cloud Key / UDM hardware and +# self-hosted Unifi Controller covered by the `unifi` deploy hook above +# (that hook already covers Cloud Key running UnifiOS v2.0.0+/Gen2/2+) -- +# this hook targets the separately-installed, self-hosted "UniFi OS Server" +# application instead, which stores certificates in its own Postgres +# database via a REST API rather than a Java keystore, so the `unifi` +# hook's approach does not apply here. +# +# UniFi OS Server exposes a REST API on its management port (default +# 11443) that its own web UI uses for certificate management: +# POST /api/auth/login - session login (cookie + JWT) +# GET /api/userCertificates - list uploaded certificates +# POST /api/userCertificates - upload a new certificate +# DELETE /api/userCertificates/{id} - remove a certificate +# PUT /api/userCertificates/{id}/status - activate/deactivate a certificate +# +# This was reverse-engineered from the browser's Network tab while using the +# real GUI upload/activate/delete flow -- it is undocumented but is the same +# code path the UI uses, so it's far more robust than editing settings.yaml, +# http/local-certs.conf, or the underlying Postgres user_certificates table +# directly (all of which are also touched by this API, but only as a result +# of the app's own internal logic, which handles cert parsing, active-cert +# bookkeeping, and nginx config regeneration correctly on its own). +# +# Auth: POST /api/auth/login returns a `TOKEN` cookie containing a JWT whose +# payload has a `csrfToken` claim. That value must be echoed back as the +# `x-csrf-token` header on every subsequent state-changing request (a classic +# double-submit CSRF pattern). No other cookies were found to be necessary. +# +# Uses core acme.sh helpers throughout (_post/_get, _json_encode, +# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or +# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all +# honored the same as every other hook. The management API's cert is +# self-signed (it's a management-only port, not meant for public exposure), +# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see +# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS +# verification for the rest of the acme.sh run, e.g. the connection to the +# ACME CA. +# +# Design: This hook does not save a certificate ID between renewals. Each +# upload gets a name unique to that run: the domain name plus a timestamp. +# This name never collides with an entry from a previous deploy. This is +# true even if that entry is still active. The hook uploads and activates +# the new certificate before it removes any old entries. If a failure +# occurs during this process, the server still has a valid, active +# certificate. The hook removes old entries only after activation is +# complete. It removes only entries whose name starts with the domain name, +# because this is the hook's own naming convention. As a result, this step +# can only affect entries that this hook created for this domain. It can +# never affect a certificate that a user uploaded manually, and it can +# never affect a self-signed certificate. +# +# Settings: +# DEPLOY_UNIFIOS_HOST - base URL of the management API +# (default: "https://localhost:11443") +# DEPLOY_UNIFIOS_USERNAME - UniFi OS Server admin username (required) +# DEPLOY_UNIFIOS_PASSWORD - UniFi OS Server admin password (required) +# +# Example: +# export DEPLOY_UNIFIOS_USERNAME="acmeuser" +# export DEPLOY_UNIFIOS_PASSWORD="xxxxx" +# acme.sh --deploy -d example.com --deploy-hook unifios +# +# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/7182 + +_uos_response_code() { + # tr strips the trailing newline along with form feeds; re-terminate + # before the second _egrep_o, whose sed fallback (used wherever egrep -o + # is unavailable) drops an unterminated final line on some platforms. + _uos_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")" + printf '%s\n' "$_uos_code" | _egrep_o "^[0-9][0-9]*" +} + +_uos_response_cookie() { + # $1 = cookie name + grep <"$HTTP_HEADER" -i "^Set-Cookie: *$1=" | _tail_n 1 | _egrep_o "$1=[^;]*" | _head_n 1 +} + +unifios_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" + + # Scoped to this hook's own subshell -- does not affect the rest of the + # acme.sh run (e.g. the connection to the ACME CA). + export HTTPS_INSECURE=1 + + _getdeployconf DEPLOY_UNIFIOS_HOST + DEPLOY_UNIFIOS_HOST="${DEPLOY_UNIFIOS_HOST:-https://localhost:11443}" + _savedeployconf DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST" + _debug DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST" + + _getdeployconf DEPLOY_UNIFIOS_USERNAME + _getdeployconf DEPLOY_UNIFIOS_PASSWORD + + if [ -z "$DEPLOY_UNIFIOS_USERNAME" ] || [ -z "$DEPLOY_UNIFIOS_PASSWORD" ]; then + _err "DEPLOY_UNIFIOS_USERNAME and DEPLOY_UNIFIOS_PASSWORD must be set." + return 1 + fi + _debug DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME" + _secure_debug DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD" + + _info "Logging in to UniFi OS Server API at $DEPLOY_UNIFIOS_HOST..." + + # _json_encode always appends a trailing "\n" escape, even to input with + # no trailing newline (it normalizes via `echo`, unconditionally adding + # one). That's harmless for the key/cert file content below, which + # legitimately ends in a real newline anyway, but wrong for these plain + # strings -- strip the spurious escape it leaves behind. + _uos_user_json="$(printf '%s' "$DEPLOY_UNIFIOS_USERNAME" | _json_encode)" + _uos_user_json="${_uos_user_json%\\n}" + _uos_pass_json="$(printf '%s' "$DEPLOY_UNIFIOS_PASSWORD" | _json_encode)" + _uos_pass_json="${_uos_pass_json%\\n}" + _login_body="{\"username\":\"$_uos_user_json\",\"password\":\"$_uos_pass_json\",\"token\":\"\",\"rememberMe\":false}" + + _login_json="$(_post "$_login_body" "$DEPLOY_UNIFIOS_HOST/api/auth/login" "" "POST" "application/json")" + _login_code="$(_uos_response_code)" + + if [ "$_login_code" != "200" ]; then + _err "Login failed (HTTP $_login_code)." + _err "Response: $_login_json" + return 1 + fi + + # Credentials are proven correct now -- save them, rather than only at the + # very end, so a later step failing doesn't discard a working login. + # base64-encoded: _save_conf wraps values in single quotes with no + # escaping, so a literal "'" in the password would otherwise corrupt the + # domain conf (see deploy/synology_dsm.sh for the same pattern). + _savedeployconf DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME" "base64" + _savedeployconf DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD" "base64" + + _uos_token="$(_uos_response_cookie TOKEN)" + if [ -z "$_uos_token" ]; then + _err "Login succeeded but no TOKEN cookie was returned." + return 1 + fi + + _H1="Cookie: $_uos_token" + export _H1 + + _uos_jwt_payload="$(echo "$_uos_token" | cut -d '=' -f 2- | cut -d '.' -f 2)" + _uos_csrf="$(_durl_replace_base64 "$_uos_jwt_payload" | _dbase64 | _egrep_o '"csrfToken":"[^"]*"' | cut -d '"' -f 4)" + if [ -z "$_uos_csrf" ]; then + _err "Could not extract csrfToken from session token." + return 1 + fi + + _H2="x-csrf-token: $_uos_csrf" + export _H2 + + _info "Uploading new certificate..." + # "name" is a purely cosmetic label -- the server never validates it + # against the certificate's actual CN/SAN, and accepts arbitrary text + # including spaces (confirmed: a cert for example.com served correctly + # after being uploaded under the unrelated name "totally unrelated label"). + # The only constraint that matters here is uniqueness: the server rejects + # a second entry with a name it already has, so a bare domain name would + # collide with the previous deploy's entry on every renewal after the + # first. A full human-readable timestamp would make that obvious in the + # UI, but the certificate list's name column is fixed-width and doesn't + # wrap (confirmed against the real UI: a long name overlaps the Expires + # column and makes both unreadable), so keep the suffix short instead -- + # Unix epoch seconds are still unique enough for this purpose. + _uos_name="$_cdomain $(_time)" + _uos_key_json="$(_json_encode <"$_ckey")" + _uos_cert_json="$(_json_encode <"$_cfullchain")" + _create_body="{\"name\":\"$_uos_name\",\"key\":\"$_uos_key_json\",\"cert\":\"$_uos_cert_json\"}" + + _create_json="$(_post "$_create_body" "$DEPLOY_UNIFIOS_HOST/api/userCertificates" "" "POST" "application/json")" + _create_code="$(_uos_response_code)" + + if [ "$_create_code" = "201" ]; then + _new_id="$(echo "$_create_json" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" + if [ -z "$_new_id" ]; then + _err "Could not determine new certificate ID from upload response." + return 1 + fi + elif [ "$_create_code" = "400" ] && echo "$_create_json" | grep -q "USER_CERTIFICATE_DUPLICATE"; then + # HTTP 400 alone just means "bad request" -- it's the USER_CERTIFICATE_DUPLICATE + # code in the response body, checked above, that actually confirms this. + # The name above is unique to this run, so a duplicate here can only be + # the server's other uniqueness constraint: this exact certificate (by + # fingerprint) already exists as some other entry -- most likely a retry + # after a prior run already uploaded it (a real renewal always produces a + # new fingerprint, so this shouldn't happen in normal cron use). The + # response body doesn't include the existing entry's id, so look it up + # by fingerprint instead. + # The API's own fingerprint field is SHA-1 (20 bytes), not SHA-256 -- + # confirmed against a real response, e.g. + # "fingerprint":"FC:02:50:9C:3B:3F:B7:79:9D:CA:4D:7C:AC:92:E7:D5:EA:F1:3A:29" + # (20 colon-separated groups). _fingerprint (core helper) strips the + # colons that field has, so re-insert them rather than stripping the + # JSON's own colons, which would also remove the ones separating every + # key from its value. + _uos_fingerprint="$(_fingerprint "$_cfullchain" sha1)" + if [ -z "$_uos_fingerprint" ]; then + _err "Could not compute the certificate's fingerprint." + return 1 + fi + _uos_fingerprint="$(echo "$_uos_fingerprint" | sed 's/\(..\)/\1:/g; s/:$//')" + + _list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")" + _list_code="$(_uos_response_code)" + if [ "$_list_code" != "200" ]; then + _err "Failed to list existing certificates (HTTP $_list_code)." + _err "Response: $_list_json" + return 1 + fi + # _normalizeJson collapses the response to one predictable line (no stray + # whitespace around colons, no embedded CR/LF the server might emit) but + # also strips the trailing newline entirely -- re-terminate before the + # split below, since some sed implementations drop an unterminated final + # line rather than processing it. + _list_json="$(echo "$_list_json" | _normalizeJson)" + # A literal embedded newline (not the two-character "\n", which GNU sed + # treats as a newline in the replacement but POSIX doesn't define and BSD + # sed emits literally) splits it one JSON object per line so grep can + # match a single certificate entry at a time. + _list_json="$( + printf '%s\n' "$_list_json" | sed 's/},{/},\ +{/g' + )" + _new_id="$(echo "$_list_json" | grep -F "\"fingerprint\":\"$_uos_fingerprint\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)" + if [ -z "$_new_id" ]; then + _err "Certificate upload rejected as a duplicate (server reported USER_CERTIFICATE_DUPLICATE), but no existing entry matching this fingerprint was found." + _err "Response: $_create_json" + return 1 + fi + # Reusing the existing entry rather than deleting it and re-uploading + # under today's name+timestamp: the served content is identical either + # way, so replacing it would only cost an extra delete+create round trip + # for no functional benefit. The tradeoff is cosmetic -- this entry keeps + # whatever name it was given whenever it was originally uploaded, so it + # won't reflect today's date in the UI. + _info "Certificate already present as entry $_new_id; reusing it." + else + _err "Certificate upload failed (HTTP $_create_code)." + _err "Response: $_create_json" + return 1 + fi + + _info "Activating certificate $_new_id..." + _activate_json="$(_post '{"active":true}' "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_new_id/status" "" "PUT" "application/json")" + _activate_code="$(_uos_response_code)" + + if [ "$_activate_code" != "200" ]; then + _err "Failed to activate new certificate (HTTP $_activate_code)." + _err "Response: $_activate_json" + return 1 + fi + + # UniFi OS Server activation is exclusive server-wide. Tests against the + # real API confirm this: activation of one entry deactivates whichever + # other entry was active before, no matter its name or domain. As a + # result, the server serves the certificate that this hook just activated. + # This certificate is already live. If the removal of old entries below + # fails, the hook logs the failure. The deploy does not fail because of + # this. + _info "Checking for old certificate entries to remove..." + _list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")" + _list_code="$(_uos_response_code)" + if [ "$_list_code" != "200" ]; then + _err "Failed to list certificates for cleanup (HTTP $_list_code) -- leaving old entries in place." + else + _list_json="$(echo "$_list_json" | _normalizeJson)" + _list_json="$( + printf '%s\n' "$_list_json" | sed 's/},{/},\ +{/g' + )" + # The pattern below matches the domain name followed by a space. If the + # space is missing, the pattern can also match a different domain that + # starts with the same text as this domain. + _old_ids="$(echo "$_list_json" | grep -F "\"name\":\"$_cdomain " | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4 | grep -v "^$_new_id$")" + for _old_id in $_old_ids; do + _info "Removing old certificate entry $_old_id..." + _del_json="$(_post "" "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_old_id" "" "DELETE")" + _del_code="$(_uos_response_code)" + if [ "$_del_code" != "204" ] && [ "$_del_code" != "200" ]; then + _err "Failed to delete old certificate $_old_id (HTTP $_del_code) -- leaving it in place." + _err "Response: $_del_json" + fi + done + fi + + _info "UniFi OS Server certificate deployed and activated successfully." + return 0 +}