From ef49a9fd23ad2e5dca5a23d41afb870e8b7f9926 Mon Sep 17 00:00:00 2001 From: Florian Heigl Date: Fri, 3 Apr 2026 05:38:21 +0200 Subject: [PATCH 01/11] 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 d66264e741e9dba207a5a9158580dacb340dc14e Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Mon, 16 Mar 2026 13:17:18 +0100 Subject: [PATCH 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 07/11] 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 08/11] [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 09/11] 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 10/11] 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 11/11] 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.