From 8aea731bd4f7c44615cb2af70c519a96b63fbb35 Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Mon, 16 Mar 2026 13:17:18 +0100 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] [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 08/22] 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 09/22] 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 cf9c70a6c797076b45f16d73baa6f090cc66ecb0 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 4 Apr 2026 21:16:40 +0800 Subject: [PATCH 10/22] 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 11/22] 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 12/22] 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 13/22] 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 14/22] 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 15/22] [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 16/22] 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 17/22] 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 18/22] 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 19/22] 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 20/22] 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 539b46adc9fa865299f59cfc29530f51fdc5d344 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 14 Apr 2026 21:19:25 +0800 Subject: [PATCH 21/22] 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 22/22] 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