From 3c3ec2c97cde17b1566db80526ceaf504310d002 Mon Sep 17 00:00:00 2001 From: Philipp Klapp Date: Mon, 13 Oct 2025 09:05:59 +0200 Subject: [PATCH 01/28] Script created for Hetzner Cloud --- dnsapi/dns_hetznercloud.sh | 431 +++++++++++++++++++++++++++++++++++++ 1 file changed, 431 insertions(+) create mode 100644 dnsapi/dns_hetznercloud.sh diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh new file mode 100644 index 00000000..e5b5b0d6 --- /dev/null +++ b/dnsapi/dns_hetznercloud.sh @@ -0,0 +1,431 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_hetznercloud_info='Hetzner Cloud DNS +Site: Hetzner.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hetznercloud +Options: + HETZNER_TOKEN API token for the Hetzner Cloud DNS API +Optional: + HETZNER_TTL Custom TTL for new TXT rrsets (default 120) + HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1) +Issues: github.com/acmesh-official/acme.sh/issues +' + +HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1" +HETZNERCLOUD_TTL_DEFAULT=120 + +######## Public functions ##################### + +dns_hetznercloud_add() { + fulldomain="$(_idn "${1}")" + txtvalue="${2}" + + _info "Using Hetzner Cloud DNS API to add record" + + if ! _hetznercloud_init; then + return 1 + fi + + if ! _hetznercloud_prepare_zone "${fulldomain}"; then + _err "Unable to determine Hetzner Cloud zone for ${fulldomain}" + return 1 + fi + + if ! _hetznercloud_get_rrset; then + return 1 + fi + + if [ "${_hetznercloud_last_http_code}" = "200" ]; then + if _hetznercloud_rrset_contains_value "${txtvalue}"; then + _info "TXT record already present; nothing to do." + return 0 + fi + elif [ "${_hetznercloud_last_http_code}" != "404" ]; then + _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}" + return 1 + fi + + add_payload="$(_hetznercloud_build_add_payload "${txtvalue}")" + if [ -z "${add_payload}" ]; then + _err "Failed to build request payload." + return 1 + fi + + if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_add}" "${add_payload}"; then + return 1 + fi + + case "${_hetznercloud_last_http_code}" in + 200 | 201 | 202 | 204) + _info "Hetzner Cloud TXT record added." + return 0 + ;; + 401 | 403) + _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API." + _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}" + return 1 + ;; + 409 | 422) + _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the add_records request" "${_hetznercloud_last_http_code}" + return 1 + ;; + *) + _hetznercloud_log_http_error "Hetzner Cloud DNS add_records request failed" "${_hetznercloud_last_http_code}" + return 1 + ;; + esac +} + +dns_hetznercloud_rm() { + fulldomain="$(_idn "${1}")" + txtvalue="${2}" + + _info "Using Hetzner Cloud DNS API to remove record" + + if ! _hetznercloud_init; then + return 1 + fi + + if ! _hetznercloud_prepare_zone "${fulldomain}"; then + _err "Unable to determine Hetzner Cloud zone for ${fulldomain}" + return 1 + fi + + if ! _hetznercloud_get_rrset; then + return 1 + fi + + if [ "${_hetznercloud_last_http_code}" = "404" ]; then + _info "TXT rrset does not exist; nothing to remove." + return 0 + fi + + if [ "${_hetznercloud_last_http_code}" != "200" ]; then + _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}" + return 1 + fi + + if _hetznercloud_rrset_contains_value "${txtvalue}"; then + remove_payload="$(_hetznercloud_build_remove_payload "${txtvalue}")" + if [ -z "${remove_payload}" ]; then + _err "Failed to build remove_records payload." + return 1 + fi + if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_remove}" "${remove_payload}"; then + return 1 + fi + case "${_hetznercloud_last_http_code}" in + 200 | 201 | 202 | 204) + _info "Hetzner Cloud TXT record removed." + return 0 + ;; + 401 | 403) + _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API." + _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}" + return 1 + ;; + 404) + _info "TXT rrset already absent after remove action." + return 0 + ;; + 409 | 422) + _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the remove_records request" "${_hetznercloud_last_http_code}" + return 1 + ;; + *) + _hetznercloud_log_http_error "Hetzner Cloud DNS remove_records request failed" "${_hetznercloud_last_http_code}" + return 1 + ;; + esac + else + _info "TXT value not present; nothing to remove." + return 0 + fi +} + +#################### Private functions ################################## + +_hetznercloud_init() { + HETZNER_TOKEN="${HETZNER_TOKEN:-$(_readaccountconf_mutable HETZNER_TOKEN)}" + if [ -z "${HETZNER_TOKEN}" ]; then + _err "The environment variable HETZNER_TOKEN must be set for the Hetzner Cloud DNS API." + return 1 + fi + HETZNER_TOKEN=$(echo "${HETZNER_TOKEN}" | tr -d '"') + _saveaccountconf_mutable HETZNER_TOKEN "${HETZNER_TOKEN}" + + HETZNER_API="${HETZNER_API:-$(_readaccountconf_mutable HETZNER_API)}" + if [ -z "${HETZNER_API}" ]; then + HETZNER_API="${HETZNERCLOUD_API_DEFAULT}" + fi + _saveaccountconf_mutable HETZNER_API "${HETZNER_API}" + + HETZNER_TTL="${HETZNER_TTL:-$(_readaccountconf_mutable HETZNER_TTL)}" + if [ -z "${HETZNER_TTL}" ]; then + HETZNER_TTL="${HETZNERCLOUD_TTL_DEFAULT}" + fi + ttl_check=$(printf "%s" "${HETZNER_TTL}" | tr -d '0-9') + if [ -n "${ttl_check}" ]; then + _err "HETZNER_TTL must be an integer value." + return 1 + fi + _saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}" + + return 0 +} + +_hetznercloud_prepare_zone() { + _hetznercloud_zone_id="" + _hetznercloud_zone_name="" + _hetznercloud_zone_name_lc="" + _hetznercloud_rr_name="" + _hetznercloud_rrset_path="" + _hetznercloud_rrset_action_add="" + _hetznercloud_rrset_action_remove="" + fulldomain_lc=$(printf "%s" "${1}" | sed 's/\.$//' | _lower_case) + + i=2 + p=1 + while true; do + candidate=$(printf "%s" "${fulldomain_lc}" | cut -d . -f "${i}"-100) + if [ -z "${candidate}" ]; then + return 1 + fi + + if _hetznercloud_get_zone_by_candidate "${candidate}"; then + zone_name_lc="${_hetznercloud_zone_name_lc}" + if [ "${fulldomain_lc}" = "${zone_name_lc}" ]; then + _hetznercloud_rr_name="@" + else + suffix=".${zone_name_lc}" + if _endswith "${fulldomain_lc}" "${suffix}"; then + _hetznercloud_rr_name="${fulldomain_lc%"${suffix}"}" + else + _hetznercloud_rr_name="${fulldomain_lc}" + fi + fi + _hetznercloud_rrset_path=$(printf "%s" "${_hetznercloud_rr_name}" | _url_encode) + _hetznercloud_rrset_action_add="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/add_records" + _hetznercloud_rrset_action_remove="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/remove_records" + return 0 + fi + p=${i} + i=$(_math "${i}" + 1) + done +} + +_hetznercloud_get_zone_by_candidate() { + candidate="${1}" + zone_key=$(printf "%s" "${candidate}" | sed 's/[^A-Za-z0-9]/_/g') + zone_conf_key="HETZNERCLOUD_ZONE_ID_for_${zone_key}" + + cached_zone_id=$(_readdomainconf "${zone_conf_key}") + if [ -n "${cached_zone_id}" ]; then + if _hetznercloud_api GET "/zones/${cached_zone_id}"; then + if [ "${_hetznercloud_last_http_code}" = "200" ]; then + zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//') + if _hetznercloud_parse_zone_fields "${zone_data}"; then + zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case) + if [ "${zone_name_lc}" = "${candidate}" ]; then + return 0 + fi + fi + elif [ "${_hetznercloud_last_http_code}" = "404" ]; then + _cleardomainconf "${zone_conf_key}" + fi + else + return 1 + fi + fi + + if _hetznercloud_api GET "/zones/${candidate}"; then + if [ "${_hetznercloud_last_http_code}" = "200" ]; then + zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//') + if _hetznercloud_parse_zone_fields "${zone_data}"; then + zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case) + if [ "${zone_name_lc}" = "${candidate}" ]; then + _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}" + return 0 + fi + fi + elif [ "${_hetznercloud_last_http_code}" != "404" ]; then + _hetznercloud_log_http_error "Hetzner Cloud zone lookup failed" "${_hetznercloud_last_http_code}" + return 1 + fi + else + return 1 + fi + + encoded_candidate=$(printf "%s" "${candidate}" | _url_encode) + if ! _hetznercloud_api GET "/zones?name=${encoded_candidate}"; then + return 1 + fi + if [ "${_hetznercloud_last_http_code}" != "200" ]; then + if [ "${_hetznercloud_last_http_code}" = "404" ]; then + return 1 + fi + _hetznercloud_log_http_error "Hetzner Cloud zone search failed" "${_hetznercloud_last_http_code}" + return 1 + fi + + zone_data=$(_hetznercloud_extract_zone_from_list "${response}" "${candidate}") + if [ -z "${zone_data}" ]; then + return 1 + fi + if ! _hetznercloud_parse_zone_fields "${zone_data}"; then + return 1 + fi + _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}" + return 0 +} + +_hetznercloud_parse_zone_fields() { + zone_json="${1}" + if [ -z "${zone_json}" ]; then + return 1 + fi + normalized=$(printf "%s" "${zone_json}" | _normalizeJson) + zone_id=$(printf "%s" "${normalized}" | _egrep_o '"id":[^,}]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "') + zone_name=$(printf "%s" "${normalized}" | _egrep_o '"name":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') + if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then + return 1 + fi + _hetznercloud_zone_id="${zone_id}" + _hetznercloud_zone_name="${zone_name}" + _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | sed 's/\.$//' | _lower_case) + return 0 +} + +_hetznercloud_extract_zone_from_list() { + list_response=$(printf "%s" "${1}" | _normalizeJson) + candidate="${2}" + escaped_candidate=$(_hetznercloud_escape_regex "${candidate}") + printf "%s" "${list_response}" | _egrep_o "{[^{}]*\"name\":\"${escaped_candidate}\"[^{}]*}" | _head_n 1 +} + +_hetznercloud_escape_regex() { + printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/\./\\./g' | sed 's/-/\\-/g' +} + +_hetznercloud_get_rrset() { + if [ -z "${_hetznercloud_zone_id}" ] || [ -z "${_hetznercloud_rrset_path}" ]; then + return 1 + fi + if ! _hetznercloud_api GET "/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT"; then + return 1 + fi + return 0 +} + +_hetznercloud_rrset_contains_value() { + wanted_value="${1}" + normalized=$(printf "%s" "${response}" | _normalizeJson) + escaped_value=$(_hetznercloud_escape_value "${wanted_value}") + search_pattern="\"value\":\"\\\\\"${escaped_value}\\\\\"\"" + if _contains "${normalized}" "${search_pattern}"; then + return 0 + fi + return 1 +} + +_hetznercloud_build_add_payload() { + value="${1}" + escaped_value=$(_hetznercloud_escape_value "${value}") + printf '{"ttl":%s,"records":[{"value":"\\"%s\\""}]}' "${HETZNER_TTL}" "${escaped_value}" +} + +_hetznercloud_build_remove_payload() { + value="${1}" + escaped_value=$(_hetznercloud_escape_value "${value}") + printf '{"records":[{"value":"\\"%s\\""}]}' "${escaped_value}" +} + +_hetznercloud_escape_value() { + printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' +} + +_hetznercloud_error_message() { + if [ -z "${response}" ]; then + return 1 + fi + message=$(printf "%s" "${response}" | _normalizeJson | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') + if [ -n "${message}" ]; then + printf "%s" "${message}" + return 0 + fi + return 1 +} + +_hetznercloud_log_http_error() { + context="${1}" + code="${2}" + message="$(_hetznercloud_error_message)" + if [ -n "${context}" ]; then + if [ -n "${message}" ]; then + _err "${context} (HTTP ${code}): ${message}" + else + _err "${context} (HTTP ${code})" + fi + else + if [ -n "${message}" ]; then + _err "Hetzner Cloud DNS API error (HTTP ${code}): ${message}" + else + _err "Hetzner Cloud DNS API error (HTTP ${code})" + fi + fi +} + +_hetznercloud_api() { + method="${1}" + ep="${2}" + data="${3}" + retried="${4}" + + if [ -z "${method}" ]; then + method="GET" + fi + + if ! _startswith "${ep}" "/"; then + ep="/${ep}" + fi + url="${HETZNER_API}${ep}" + + export _H1="Authorization: Bearer ${HETZNER_TOKEN}" + export _H2="Accept: application/json" + export _H3="" + export _H4="" + export _H5="" + + : >"${HTTP_HEADER}" + + if [ "${method}" = "GET" ]; then + response="$(_get "${url}")" + else + if [ -z "${data}" ]; then + data="{}" + fi + response="$(_post "${data}" "${url}" "" "${method}" "application/json")" + fi + ret="${?}" + + _hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') + + if [ "${ret}" != "0" ]; then + return 1 + fi + + if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then + retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r') + if [ -z "${retry_after}" ]; then + retry_after=1 + fi + _info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds." + _sleep "${retry_after}" + if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then + return 1 + fi + return 0 + fi + + return 0 +} From b65f432ee02b1ad45224e97cf89e5144680b9d41 Mon Sep 17 00:00:00 2001 From: Philipp Klapp Date: Sat, 1 Nov 2025 13:44:11 +0100 Subject: [PATCH 02/28] Normalize Hetzner zone names to punycode --- dnsapi/dns_hetznercloud.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh index e5b5b0d6..e53ac23a 100644 --- a/dnsapi/dns_hetznercloud.sh +++ b/dnsapi/dns_hetznercloud.sh @@ -290,9 +290,15 @@ _hetznercloud_parse_zone_fields() { if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then return 1 fi + zone_name_trimmed=$(printf "%s" "${zone_name}" | sed 's/\.$//') + if zone_name_ascii=$(_idn "${zone_name_trimmed}"); then + zone_name="${zone_name_ascii}" + else + zone_name="${zone_name_trimmed}" + fi _hetznercloud_zone_id="${zone_id}" _hetznercloud_zone_name="${zone_name}" - _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | sed 's/\.$//' | _lower_case) + _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case) return 0 } From 5f8146050464c9f0c6ae69160e672ad3bce388b3 Mon Sep 17 00:00:00 2001 From: Philipp Klapp Date: Sun, 2 Nov 2025 04:07:58 +0100 Subject: [PATCH 03/28] Wait for Hetzner Cloud DNS actions to complete before returning --- dnsapi/dns_hetznercloud.sh | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh index e53ac23a..4a7eea90 100644 --- a/dnsapi/dns_hetznercloud.sh +++ b/dnsapi/dns_hetznercloud.sh @@ -8,11 +8,13 @@ Options: Optional: HETZNER_TTL Custom TTL for new TXT rrsets (default 120) HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1) + HETZNER_MAX_ATTEMPTS Number of 1s polls to wait for async actions (default 120) Issues: github.com/acmesh-official/acme.sh/issues ' HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1" HETZNERCLOUD_TTL_DEFAULT=120 +HETZNER_MAX_ATTEMPTS_DEFAULT=120 ######## Public functions ##################### @@ -57,6 +59,9 @@ dns_hetznercloud_add() { case "${_hetznercloud_last_http_code}" in 200 | 201 | 202 | 204) + if ! _hetznercloud_handle_action_response "TXT record add"; then + return 1 + fi _info "Hetzner Cloud TXT record added." return 0 ;; @@ -116,6 +121,9 @@ dns_hetznercloud_rm() { fi case "${_hetznercloud_last_http_code}" in 200 | 201 | 202 | 204) + if ! _hetznercloud_handle_action_response "TXT record remove"; then + return 1 + fi _info "Hetzner Cloud TXT record removed." return 0 ;; @@ -171,6 +179,17 @@ _hetznercloud_init() { fi _saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}" + HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS:-$(_readaccountconf_mutable HETZNER_MAX_ATTEMPTS)}" + if [ -z "${HETZNER_MAX_ATTEMPTS}" ]; then + HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS_DEFAULT}" + fi + attempts_check=$(printf "%s" "${HETZNER_MAX_ATTEMPTS}" | tr -d '0-9') + if [ -n "${attempts_check}" ]; then + _err "HETZNER_MAX_ATTEMPTS must be an integer value." + return 1 + fi + _saveaccountconf_mutable HETZNER_MAX_ATTEMPTS "${HETZNER_MAX_ATTEMPTS}" + return 0 } @@ -435,3 +454,140 @@ _hetznercloud_api() { return 0 } + +_hetznercloud_handle_action_response() { + context="${1}" + if [ -z "${response}" ]; then + return 0 + fi + + normalized=$(printf "%s" "${response}" | _normalizeJson) + + failed_message="" + if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then + if [ -n "${failed_message}" ]; then + _err "Hetzner Cloud DNS ${context} failed: ${failed_message}" + else + _err "Hetzner Cloud DNS ${context} failed." + fi + return 1 + fi + + action_ids="" + if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then + for action_id in ${action_ids}; do + if [ -z "${action_id}" ]; then + continue + fi + if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then + return 1 + fi + done + fi + + return 0 +} + +_hetznercloud_extract_failed_action_message() { + normalized="${1}" + failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]') + if [ -z "${failed_section}" ]; then + return 1 + fi + if _contains "${failed_section}" '"failed_actions":[]'; then + return 1 + fi + message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') + if [ -n "${message}" ]; then + printf "%s" "${message}" + else + printf "%s" "${failed_section}" + fi + return 0 +} + +_hetznercloud_extract_action_ids() { + normalized="${1}" + actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]') + if [ -z "${actions_section}" ]; then + return 1 + fi + action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ') + action_ids=$(printf "%s" "${action_ids}" | tr -s ' ') + action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//') + if [ -z "${action_ids}" ]; then + return 1 + fi + printf "%s" "${action_ids}" + return 0 +} + +_hetznercloud_wait_for_action() { + action_id="${1}" + context="${2}" + attempts="0" + + while true; do + if ! _hetznercloud_api GET "/actions/${action_id}"; then + return 1 + fi + if [ "${_hetznercloud_last_http_code}" != "200" ]; then + _hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}" + return 1 + fi + + normalized=$(printf "%s" "${response}" | _normalizeJson) + action_status=$(_hetznercloud_action_status_from_normalized "${normalized}") + + if [ -z "${action_status}" ]; then + _err "Hetzner Cloud DNS ${context} action ${action_id} returned no status." + return 1 + fi + + if [ "${action_status}" = "success" ]; then + return 0 + fi + + if [ "${action_status}" = "error" ]; then + if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then + _err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}" + else + _err "Hetzner Cloud DNS ${context} action ${action_id} failed." + fi + return 1 + fi + + attempts=$(_math "${attempts}" + 1) + if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then + _err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts." + return 1 + fi + + _sleep 1 + done +} + +_hetznercloud_action_status_from_normalized() { + normalized="${1}" + status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') + printf "%s" "${status}" +} + +_hetznercloud_action_error_from_normalized() { + normalized="${1}" + error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}') + if [ -z "${error_section}" ]; then + return 1 + fi + message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') + if [ -n "${message}" ]; then + printf "%s" "${message}" + return 0 + fi + code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') + if [ -n "${code}" ]; then + printf "%s" "${code}" + return 0 + fi + return 1 +} From 4a7f35dea7f8a91545976245e89730bc3f3ffeb1 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 15 Nov 2025 11:12:00 +0100 Subject: [PATCH 04/28] remove clientauth https://github.com/acmesh-official/acme.sh/issues/6610 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 8ac9b366..95337cc8 100755 --- a/acme.sh +++ b/acme.sh @@ -1271,7 +1271,7 @@ _createcsr() { _savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse" printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf" else - printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf" + printf "\nextendedKeyUsage=serverAuth\n" >>"$csrconf" fi if [ "$acmeValidationv1" ]; then From 66bad853aec3778303feec693bf9194644e27f36 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 15 Nov 2025 11:27:04 +0100 Subject: [PATCH 05/28] remove ClearLinux --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f7038f59..e6a8966a 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,6 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) |18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux |19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia |10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux -|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux |22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111 |23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT) |24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management) From a6ff1d69248868531daf0390e5747f50cdefd735 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 16 Nov 2025 09:30:41 +0100 Subject: [PATCH 06/28] add logo --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e6a8966a..6953cc71 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![zerossl.com](https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05)](https://zerossl.com/?fromacme.sh) + # An ACME Shell script: acme.sh [![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml) From 0d1f9edf3fb57b901da0c2f21cd593b4f683e2ba Mon Sep 17 00:00:00 2001 From: Joe Bauser Date: Mon, 17 Nov 2025 15:24:40 -0500 Subject: [PATCH 07/28] README.md clarify keylength arg and ECC default Reorder and reword small portions of the keylength documentation and make the ECC cert default explicitly stated in part 2 to avoid confusion. Fixes #6590 --- README.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6953cc71..05656044 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,8 @@ The certs will be placed in `~/.acme.sh/example.com/` The certs will be renewed automatically every **60** days. +The certs will default to ECC certificates. + More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert @@ -359,36 +361,33 @@ Ok, it's done. **Please use dns api mode instead.** -# 10. Issue ECC certificates +# 10. Issue certificates of different key types and lengths (ECC or RSA) + +Just set the `keylength` to a valid, supported, value. + +Valid values for the `keylength` parameter are: -Just set the `keylength` parameter with a prefix `ec-`. +1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)** +2. **ec-384 (secp384r1, "ECDSA P-384")** +3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)** +4. **2048 (RSA2048)** +5. **3072 (RSA3072)** +6. **4096 (RSA4096)** For example: -### Single domain ECC certificate +### Single domain with ECDSA P-384 certificate ```bash -acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256 +acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384 ``` -### SAN multi domain ECC certificate +### SAN multi domain with RSA4096 certificate ```bash -acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength ec-256 +acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096 ``` -Please look at the `keylength` parameter above. - -Valid values are: - -1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)** -2. **ec-384 (secp384r1, "ECDSA P-384")** -3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)** -4. **2048 (RSA2048)** -5. **3072 (RSA3072)** -6. **4096 (RSA4096)** - - # 11. Issue Wildcard certificates It's simple, just give a wildcard domain as the `-d` parameter. From 6715320e78ea1baa4ec1437166f73215b3fbc2a2 Mon Sep 17 00:00:00 2001 From: neil Date: Tue, 18 Nov 2025 21:41:43 +0100 Subject: [PATCH 08/28] fix https://github.com/acmesh-official/acme.sh/issues/6610 https://github.com/acmesh-official/acme.sh/issues/6617#issuecomment-3546341480 --- acme.sh | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/acme.sh b/acme.sh index 95337cc8..00d2d2d5 100755 --- a/acme.sh +++ b/acme.sh @@ -1250,7 +1250,7 @@ _idn() { fi } -#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 +#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage _createcsr() { _debug _createcsr domain="$1" @@ -1259,6 +1259,7 @@ _createcsr() { csr="$4" csrconf="$5" acmeValidationv1="$6" + extusage="$7" _debug2 domain "$domain" _debug2 domainlist "$domainlist" _debug2 csrkey "$csrkey" @@ -1267,11 +1268,10 @@ _createcsr() { printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf" - if [ "$Le_ExtKeyUse" ]; then - _savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse" - printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf" + if [ "$extusage" ]; then + printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf" else - printf "\nextendedKeyUsage=serverAuth\n" >>"$csrconf" + printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf" fi if [ "$acmeValidationv1" ]; then @@ -4445,6 +4445,7 @@ issue() { _valid_from="${16}" _valid_to="${17}" _certificate_profile="${18}" + _extended_key_usage="${19}" if [ -z "$_ACME_IS_RENEW" ]; then _initpath "$_main_domain" "$_key_length" @@ -4589,12 +4590,25 @@ issue() { return 1 fi fi - if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then + _keyusage="$_extended_key_usage" + if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then + if [ -z "$_keyusage" ]; then + #https://github.com/acmesh-official/acme.sh/issues/6610 + #google accepts serverauth only + _keyusage="serverAuth" + fi + fi + if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then _err "Error creating CSR." _clearup _on_issue_err "$_post_hook" return 1 fi + if [ "$_extended_key_usage" ]; then + _savedomainconf "Le_ExtKeyUse" "$_extended_key_usage" + else + _cleardomainconf "Le_ExtKeyUse" + fi fi _savedomainconf "Le_Keylength" "$_key_length" @@ -5553,7 +5567,7 @@ renew() { _cleardomainconf Le_OCSP_Staple fi fi - issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" + issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse" res="$?" if [ "$res" != "0" ]; then return "$res" @@ -7469,6 +7483,7 @@ _process() { _valid_from="" _valid_to="" _certificate_profile="" + _extended_key_usage="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7864,7 +7879,7 @@ _process() { shift ;; --extended-key-usage) - Le_ExtKeyUse="$2" + _extended_key_usage="$2" shift ;; --ocsp-must-staple | --ocsp) @@ -8081,7 +8096,7 @@ _process() { uninstall) uninstall "$_nocron" ;; upgrade) upgrade ;; issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage" ;; deploy) deploy "$_domain" "$_deploy_hook" "$_ecc" From c950b67e4b8e87bd72a6ea762a704883633095c3 Mon Sep 17 00:00:00 2001 From: Mason <36799194+WongIong@users.noreply.github.com> Date: Wed, 19 Nov 2025 23:27:45 +0800 Subject: [PATCH 09/28] Adapting to Cloudflare's new response --- dnsapi/dns_cf.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 736742f3..b2749972 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -92,7 +92,9 @@ dns_cf_add() { if _contains "$response" "$txtvalue"; then _info "Added, OK" return 0 - elif _contains "$response" "The record already exists"; then + elif _contains "$response" "The record already exists" \ + || _contains "$response" "An identical record already exists." \ + || _contains "$response" '"code":81058'; then _info "Already exists, OK" return 0 else From a9f96bf7097f37fe52b1a79b9b41582f6ff37632 Mon Sep 17 00:00:00 2001 From: Mason <36799194+WongIong@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:29:44 +0800 Subject: [PATCH 10/28] Reformat using shfmt --- dnsapi/dns_cf.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index b2749972..7b383c43 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -92,9 +92,9 @@ dns_cf_add() { if _contains "$response" "$txtvalue"; then _info "Added, OK" return 0 - elif _contains "$response" "The record already exists" \ - || _contains "$response" "An identical record already exists." \ - || _contains "$response" '"code":81058'; then + elif _contains "$response" "The record already exists" || + _contains "$response" "An identical record already exists." || + _contains "$response" '"code":81058'; then _info "Already exists, OK" return 0 else From 3d3053f4277b0218faae29a3adf19b13d32709af Mon Sep 17 00:00:00 2001 From: Antoni Company Date: Thu, 20 Nov 2025 10:06:37 +0000 Subject: [PATCH 11/28] feat: Add custom filename for panos --- deploy/panos.sh | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index a9232e79..8f911fba 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -16,6 +16,7 @@ # export PANOS_TEMPLATE="" # Template Name of panorama managed devices # export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically # export PANOS_VSYS="Shared" # name of the vsys to import the certificate +# export PANOS_FILENAME="" # use a custom filename to work around Panorama's 31-character limit # # The script will automatically generate a new API key if # no key is found, or if a saved key has expired or is invalid. @@ -89,7 +90,7 @@ deployer() { if [ "$type" = 'cert' ]; then panos_url="${panos_url}?type=import" content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_filename" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")" @@ -103,11 +104,11 @@ deployer() { if [ "$type" = 'key' ]; then panos_url="${panos_url}?type=import" content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_filename" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_filename.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")" if [ "$_panos_template" ]; then content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template" fi @@ -168,7 +169,6 @@ deployer() { # This is the main function that will call the other functions to deploy everything. panos_deploy() { - _cdomain=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename _ckey="$2" _cfullchain="$5" @@ -242,6 +242,15 @@ panos_deploy() { _getdeployconf PANOS_VSYS fi + # PANOS_FILENAME + if [ "$PANOS_FILENAME" ]; then + _debug "Detected ENV variable PANOS_FILENAME. Saving to file." + _savedeployconf PANOS_FILENAME "$PANOS_FILENAME" 1 + else + _debug "Attempting to load variable PANOS_FILENAME from file." + _getdeployconf PANOS_FILENAME + fi + #Store variables _panos_host=$PANOS_HOST _panos_user=$PANOS_USER @@ -249,6 +258,7 @@ panos_deploy() { _panos_template=$PANOS_TEMPLATE _panos_template_stack=$PANOS_TEMPLATE_STACK _panos_vsys=$PANOS_VSYS + _panos_filename=$PANOS_FILENAME #Test API Key if found. If the key is invalid, the variable _panos_key will be unset. if [ "$_panos_host" ] && [ "$_panos_key" ]; then @@ -267,6 +277,12 @@ panos_deploy() { _err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs." return 1 else + # Use filename based on the first domain on the certificate if no custom filename is set + if [ -z "$_panos_filename" ]; then + _panos_filename=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename + _savedeployconf PANOS_FILENAME "$_panos_filename" 1 + fi + # Generate a new API key if no valid API key is found if [ -z "$_panos_key" ]; then _debug "**** Generating new PANOS API KEY ****" From d2539c3f1aefb9cf0d7d29f488fb37f28b690188 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 20 Nov 2025 21:18:40 +0100 Subject: [PATCH 12/28] fix https://github.com/acmesh-official/acme.sh/issues/6402 --- acme.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/acme.sh b/acme.sh index 00d2d2d5..6578d414 100755 --- a/acme.sh +++ b/acme.sh @@ -5242,6 +5242,16 @@ $_authorizations_map" return 1 fi break + elif _contains "$response" "\"ready\""; then + _info "Order status is 'ready', let's sleep and retry." + _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') + _debug "_retryafter" "$_retryafter" + if [ "$_retryafter" ]; then + _info "Sleeping for $_retryafter seconds then retrying" + _sleep $_retryafter + else + _sleep 2 + fi elif _contains "$response" "\"processing\""; then _info "Order status is 'processing', let's sleep and retry." _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') From 46a2608783ea647042ea2a6d06c1c0e37bd91f05 Mon Sep 17 00:00:00 2001 From: Antoni Company Date: Sat, 22 Nov 2025 09:22:32 +0000 Subject: [PATCH 13/28] fix: Renamed filaname to certname - Changed filename to certname to better reflect the actual issue at hand. - Restored _cdomain variable to its original place for clarity. --- deploy/panos.sh | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index 8f911fba..c54d21fe 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -16,7 +16,7 @@ # export PANOS_TEMPLATE="" # Template Name of panorama managed devices # export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically # export PANOS_VSYS="Shared" # name of the vsys to import the certificate -# export PANOS_FILENAME="" # use a custom filename to work around Panorama's 31-character limit +# export PANOS_CERTNAME="" # use a custom certificate name to work around Panorama's 31-character limit # # The script will automatically generate a new API key if # no key is found, or if a saved key has expired or is invalid. @@ -90,7 +90,7 @@ deployer() { if [ "$type" = 'cert' ]; then panos_url="${panos_url}?type=import" content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_filename" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")" @@ -104,11 +104,11 @@ deployer() { if [ "$type" = 'key' ]; then panos_url="${panos_url}?type=import" content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_filename" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem" content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456" - content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_filename.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")" + content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_certname.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")" if [ "$_panos_template" ]; then content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template" fi @@ -169,6 +169,7 @@ deployer() { # This is the main function that will call the other functions to deploy everything. panos_deploy() { + _cdomain=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename _ckey="$2" _cfullchain="$5" @@ -242,13 +243,13 @@ panos_deploy() { _getdeployconf PANOS_VSYS fi - # PANOS_FILENAME - if [ "$PANOS_FILENAME" ]; then - _debug "Detected ENV variable PANOS_FILENAME. Saving to file." - _savedeployconf PANOS_FILENAME "$PANOS_FILENAME" 1 + # PANOS_CERTNAME + if [ "$PANOS_CERTNAME" ]; then + _debug "Detected ENV variable PANOS_CERTNAME. Saving to file." + _savedeployconf PANOS_CERTNAME "$PANOS_CERTNAME" 1 else - _debug "Attempting to load variable PANOS_FILENAME from file." - _getdeployconf PANOS_FILENAME + _debug "Attempting to load variable PANOS_CERTNAME from file." + _getdeployconf PANOS_CERTNAME fi #Store variables @@ -258,7 +259,7 @@ panos_deploy() { _panos_template=$PANOS_TEMPLATE _panos_template_stack=$PANOS_TEMPLATE_STACK _panos_vsys=$PANOS_VSYS - _panos_filename=$PANOS_FILENAME + _panos_certname=$PANOS_CERTNAME #Test API Key if found. If the key is invalid, the variable _panos_key will be unset. if [ "$_panos_host" ] && [ "$_panos_key" ]; then @@ -277,10 +278,10 @@ panos_deploy() { _err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs." return 1 else - # Use filename based on the first domain on the certificate if no custom filename is set - if [ -z "$_panos_filename" ]; then - _panos_filename=$(echo "$1" | sed 's/*/WILDCARD_/g') #Wildcard Safe Filename - _savedeployconf PANOS_FILENAME "$_panos_filename" 1 + # Use certificate name based on the first domain on the certificate if no custom certificate name is set + if [ -z "$_panos_certname" ]; then + _panos_certname="$_cdomain" + _savedeployconf PANOS_CERTNAME "$_panos_certname" 1 fi # Generate a new API key if no valid API key is found From 9b30bd5a0356c83ad5c02d3040ae8daee4dbdbea Mon Sep 17 00:00:00 2001 From: ZeroSSL-Andreas Date: Tue, 25 Nov 2025 14:41:31 +0100 Subject: [PATCH 14/28] Update README.md --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 05656044..4afd90a8 100644 --- a/README.md +++ b/README.md @@ -523,3 +523,20 @@ Your donation makes **acme.sh** better: 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/) [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list) + +# 21. About this repository + +> [!NOTE] +> This repository is officially maintained by ZeroSSL as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community! +> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com. +> +> All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL. +

+ + + + + ZeroSSL + + +

From 705fbcd570dfec12b9851cdd9b047020c60e5185 Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 27 Nov 2025 22:13:18 +0100 Subject: [PATCH 15/28] fix https://github.com/acmesh-official/acme.sh/issues/6124#issuecomment-3586650156 --- Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index d8f8b265..88edc4a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,8 @@ RUN apk --no-cache add -f \ jq \ cronie +ENV LE_WORKING_DIR=/acmebin + ENV LE_CONFIG_HOME=/acme.sh ARG AUTO_UPGRADE=1 @@ -30,7 +32,7 @@ COPY ./notify /install_acme.sh/notify RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - +RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - RUN for verb in help \ version \ @@ -64,7 +66,7 @@ RUN for verb in help \ set-default-ca \ set-default-chain \ ; do \ - printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \ + printf -- "%b" "#!/usr/bin/env sh\n$LE_WORKING_DIR/acme.sh --${verb} --config-home $LE_CONFIG_HOME \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \ ; done RUN printf "%b" '#!'"/usr/bin/env sh\n \ @@ -72,7 +74,7 @@ if [ \"\$1\" = \"daemon\" ]; then \n \ exec crond -n -s -m off \n \ else \n \ exec -- \"\$@\"\n \ -fi\n" >/entry.sh && chmod +x /entry.sh +fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME VOLUME /acme.sh From c5566eafebaa04edd30074058e2eefba5b5bfc1e Mon Sep 17 00:00:00 2001 From: SunMar Date: Fri, 28 Nov 2025 09:44:50 +0100 Subject: [PATCH 16/28] fix "dns_aws.sh: line 164: _error: command not found" #6443 --- dnsapi/dns_aws.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index c88c9d9c..b76d69c2 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -161,7 +161,7 @@ _get_root() { h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g') _debug "Checking domain: $h" if [ -z "$h" ]; then - _error "invalid domain" + _err "invalid domain" return 1 fi From ac0df6bc885db5f67e2ccebecfacbedd011974f4 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 29 Nov 2025 16:36:14 +0100 Subject: [PATCH 17/28] start 3.1.3 --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 6578d414..da67fa14 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.1.2 +VER=3.1.3 PROJECT_NAME="acme.sh" From 5c6d8aacbeeb4063822064c27bdaf4a144975cb7 Mon Sep 17 00:00:00 2001 From: Stefan Date: Sat, 29 Nov 2025 22:38:02 +0100 Subject: [PATCH 18/28] Add files via upload --- dnsapi/dns_infoblox_uddi.sh | 220 ++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 dnsapi/dns_infoblox_uddi.sh diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh new file mode 100644 index 00000000..545ce41d --- /dev/null +++ b/dnsapi/dns_infoblox_uddi.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_infoblox_uddi_info='Infoblox UDDI +Site: Infoblox.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infoblox_uddi +Options: + Infoblox_UDDI_Key API Key for Infoblox UDDI + Infoblox_Portal URL, e.g. "csp.infoblox.com" or "csp.eu.infoblox.com" +Issues: github.com/acmesh-official/acme.sh/issues +Author: Stefan Riegel +' + +######## Public functions ##################### + +#Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_infoblox_uddi_add() { + fulldomain=$1 + txtvalue=$2 + + Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" + Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" + + _info "Using Infoblox UDDI API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then + Infoblox_UDDI_Key="" + Infoblox_Portal="" + _err "You didn't specify the Infoblox UDDI key or server (Infoblox_UDDI_Key; Infoblox_Portal)." + _err "Please set them via EXPORT Infoblox_UDDI_Key=your_key, EXPORT Infoblox_Portal=csp.infoblox.com and try again." + return 1 + fi + + _saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key" + _saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal" + + export _H1="Authorization: token $Infoblox_UDDI_Key" + export _H2="Content-Type: application/json" + + zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" + _debug "Fetching zones from: $zone_url" + zone_result="$(_get "$zone_url")" + _debug2 "zone_result: $zone_result" + + if [ "$?" != "0" ]; then + _err "Error fetching zones from Infoblox API" + return 1 + fi + + fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') + _debug "Looking for zone matching domain: $fulldomain_no_acme" + + zone_fqdn="" + temp_domain="$fulldomain_no_acme" + + while [ -n "$temp_domain" ]; do + _debug "Checking if '$temp_domain' is a zone..." + if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then + zone_fqdn="$temp_domain" + _debug "Found matching zone: $zone_fqdn" + break + fi + temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') + if ! echo "$temp_domain" | grep -q '\.'; then + break + fi + done + + if [ -z "$zone_fqdn" ]; then + _err "Could not determine zone for domain $fulldomain" + _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" + return 1 + fi + + zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + + _debug "zone_id: $zone_id" + + if [ -z "$zone_id" ]; then + _err "Could not find zone ID for $zone_fqdn" + _debug "Zone result: $zone_result" + return 1 + fi + + _debug "Extracting name_in_zone from fulldomain='$fulldomain' with zone_fqdn='$zone_fqdn'" + name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") + _debug "name_in_zone after removing zone: '$name_in_zone'" + name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') + _debug "name_in_zone final: '$name_in_zone'" + + baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" + + body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" + + _debug "POST URL: $baseurl" + _debug "POST body: $body" + result="$(_post "$body" "$baseurl" "" "POST")" + _debug "POST result: $result" + + if echo "$result" | grep -q '"id"'; then + record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') + _info "Successfully created TXT record with ID: $record_id" + return 0 + else + _err "Error encountered during record addition" + _err "Response: $result" + return 1 + fi +} + +#Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_infoblox_uddi_rm() { + fulldomain=$1 + txtvalue=$2 + + Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" + Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" + + if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then + _err "Credentials not found" + return 1 + fi + + _info "Using Infoblox UDDI API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + export _H1="Authorization: token $Infoblox_UDDI_Key" + export _H2="Content-Type: application/json" + + zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" + _debug "Fetching zones from: $zone_url" + zone_result="$(_get "$zone_url")" + _debug2 "zone_result: $zone_result" + + if [ "$?" != "0" ]; then + _err "Error fetching zones from Infoblox API" + return 1 + fi + + fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') + _debug "Looking for zone matching domain: $fulldomain_no_acme" + + zone_fqdn="" + temp_domain="$fulldomain_no_acme" + + while [ -n "$temp_domain" ]; do + _debug "Checking if '$temp_domain' is a zone..." + if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then + zone_fqdn="$temp_domain" + _debug "Found matching zone: $zone_fqdn" + break + fi + temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') + if ! echo "$temp_domain" | grep -q '\.'; then + break + fi + done + + if [ -z "$zone_fqdn" ]; then + _err "Could not determine zone for domain $fulldomain" + _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" + return 1 + fi + + zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + + _debug "zone_id: $zone_id" + + if [ -z "$zone_id" ]; then + _err "Could not find zone ID for $zone_fqdn" + _debug "Zone result: $zone_result" + return 1 + fi + + name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') + _debug "name_in_zone: $name_in_zone" + + filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id'" + filter_encoded=$(_url_encode "$filter") + geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" + _debug "GET URL: $geturl" + + result="$(_get "$geturl")" + _debug "GET result: $result" + + if echo "$result" | grep -q '"results":'; then + record_count=$(echo "$result" | jq -r '.results | length') + _debug "Found $record_count result(s)" + + record_id=$(echo "$result" | jq -r '.results[] | select(.rdata.text == "'"$txtvalue"'") | .id' | head -1) + + if [ -n "$record_id" ]; then + record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') + _debug "Found record UUID: $record_uuid" + + delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" + _debug "DELETE URL: $delurl" + rmResult="$(_post "" "$delurl" "" "DELETE")" + + if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then + _info "Successfully deleted the txt record" + return 0 + else + _err "Error occurred during txt record delete" + _err "Response: $rmResult" + return 1 + fi + else + _err "Record to delete didn't match an existing record (no matching txtvalue found)" + _debug "Looking for txtvalue: $txtvalue" + return 1 + fi + else + _err "Record to delete didn't match an existing record (no results found)" + _debug "Response: $result" + return 1 + fi +} From 657b7195d6427c0bc2110c609b0a27365361597f Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:06:20 +0100 Subject: [PATCH 20/28] Fix Authorization header format --- dnsapi/dns_infoblox_uddi.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 545ce41d..c49a7f82 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -35,7 +35,7 @@ dns_infoblox_uddi_add() { _saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key" _saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal" - export _H1="Authorization: token $Infoblox_UDDI_Key" + export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" @@ -126,7 +126,7 @@ dns_infoblox_uddi_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - export _H1="Authorization: token $Infoblox_UDDI_Key" + export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" From eeb91de6a369e209558147f49dbe9a98b08a9a0e Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:13:52 +0100 Subject: [PATCH 21/28] Replace jq with shell-based JSON parsing --- dnsapi/dns_infoblox_uddi.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index c49a7f82..674090be 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -73,7 +73,7 @@ dns_infoblox_uddi_add() { return 1 fi - zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) _debug "zone_id: $zone_id" @@ -164,7 +164,7 @@ dns_infoblox_uddi_rm() { return 1 fi - zone_id=$(echo "$zone_result" | jq -r '(.results // .)[] | select(.fqdn == "'"$zone_fqdn"'" or .fqdn == "'"$zone_fqdn"'.") | .id' | head -1) + zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) _debug "zone_id: $zone_id" @@ -186,10 +186,8 @@ dns_infoblox_uddi_rm() { _debug "GET result: $result" if echo "$result" | grep -q '"results":'; then - record_count=$(echo "$result" | jq -r '.results | length') - _debug "Found $record_count result(s)" - - record_id=$(echo "$result" | jq -r '.results[] | select(.rdata.text == "'"$txtvalue"'") | .id' | head -1) + record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^"]*"' | _egrep_o 'dns/record/[^"]*' | _head_n 1) + _debug "Found record_id: $record_id" if [ -n "$record_id" ]; then record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') From ca35e8c1189b2aa75cf022913154c10aaee30732 Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:32:28 +0100 Subject: [PATCH 22/28] Fix zone_id extraction to query correct zone --- dnsapi/dns_infoblox_uddi.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 674090be..8dfeab5f 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -73,7 +73,14 @@ dns_infoblox_uddi_add() { return 1 fi - zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) + # Fetch exact zone_id for the matched fqdn using server-side filtering + filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" + filter_encoded=$(_url_encode "$filter") + zone_query="$zone_url?_filter=$filter_encoded" + _debug "Fetching zone_id with filter: $zone_query" + zone_lookup="$(_get "$zone_query")" + _debug2 "zone_lookup: $zone_lookup" + zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') _debug "zone_id: $zone_id" @@ -164,7 +171,14 @@ dns_infoblox_uddi_rm() { return 1 fi - zone_id=$(echo "$zone_result" | _egrep_o '"id":"dns/auth_zone/[^"]*"' | _egrep_o 'dns/auth_zone/[^"]*' | _head_n 1) + # Fetch exact zone_id for the matched fqdn using server-side filtering + filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" + filter_encoded=$(_url_encode "$filter") + zone_query="$zone_url?_filter=$filter_encoded" + _debug "Fetching zone_id with filter: $zone_query" + zone_lookup="$(_get "$zone_query")" + _debug2 "zone_lookup: $zone_lookup" + zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') _debug "zone_id: $zone_id" @@ -177,7 +191,7 @@ dns_infoblox_uddi_rm() { name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') _debug "name_in_zone: $name_in_zone" - filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id'" + filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" filter_encoded=$(_url_encode "$filter") geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" _debug "GET URL: $geturl" @@ -186,7 +200,7 @@ dns_infoblox_uddi_rm() { _debug "GET result: $result" if echo "$result" | grep -q '"results":'; then - record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^"]*"' | _egrep_o 'dns/record/[^"]*' | _head_n 1) + record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') _debug "Found record_id: $record_id" if [ -n "$record_id" ]; then From 490b9e2d09d998cee6736176e8c7eefa08b6029b Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sat, 29 Nov 2025 23:39:43 +0100 Subject: [PATCH 23/28] Clean up debug statements --- dnsapi/dns_infoblox_uddi.sh | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 8dfeab5f..ebf4484c 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -39,7 +39,6 @@ dns_infoblox_uddi_add() { export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - _debug "Fetching zones from: $zone_url" zone_result="$(_get "$zone_url")" _debug2 "zone_result: $zone_result" @@ -77,12 +76,11 @@ dns_infoblox_uddi_add() { filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" filter_encoded=$(_url_encode "$filter") zone_query="$zone_url?_filter=$filter_encoded" - _debug "Fetching zone_id with filter: $zone_query" zone_lookup="$(_get "$zone_query")" _debug2 "zone_lookup: $zone_lookup" zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "zone_id: $zone_id" + _debug zone_id "$zone_id" if [ -z "$zone_id" ]; then _err "Could not find zone ID for $zone_fqdn" @@ -90,20 +88,16 @@ dns_infoblox_uddi_add() { return 1 fi - _debug "Extracting name_in_zone from fulldomain='$fulldomain' with zone_fqdn='$zone_fqdn'" name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") - _debug "name_in_zone after removing zone: '$name_in_zone'" name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') - _debug "name_in_zone final: '$name_in_zone'" + _debug name_in_zone "$name_in_zone" baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" - _debug "POST URL: $baseurl" - _debug "POST body: $body" result="$(_post "$body" "$baseurl" "" "POST")" - _debug "POST result: $result" + _debug2 result "$result" if echo "$result" | grep -q '"id"'; then record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') @@ -137,7 +131,6 @@ dns_infoblox_uddi_rm() { export _H2="Content-Type: application/json" zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - _debug "Fetching zones from: $zone_url" zone_result="$(_get "$zone_url")" _debug2 "zone_result: $zone_result" @@ -175,12 +168,11 @@ dns_infoblox_uddi_rm() { filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" filter_encoded=$(_url_encode "$filter") zone_query="$zone_url?_filter=$filter_encoded" - _debug "Fetching zone_id with filter: $zone_query" zone_lookup="$(_get "$zone_query")" _debug2 "zone_lookup: $zone_lookup" zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "zone_id: $zone_id" + _debug zone_id "$zone_id" if [ -z "$zone_id" ]; then _err "Could not find zone ID for $zone_fqdn" @@ -189,15 +181,14 @@ dns_infoblox_uddi_rm() { fi name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') - _debug "name_in_zone: $name_in_zone" + _debug name_in_zone "$name_in_zone" filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" filter_encoded=$(_url_encode "$filter") geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" - _debug "GET URL: $geturl" result="$(_get "$geturl")" - _debug "GET result: $result" + _debug2 result "$result" if echo "$result" | grep -q '"results":'; then record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') @@ -205,10 +196,9 @@ dns_infoblox_uddi_rm() { if [ -n "$record_id" ]; then record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') - _debug "Found record UUID: $record_uuid" + _debug record_uuid "$record_uuid" delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" - _debug "DELETE URL: $delurl" rmResult="$(_post "" "$delurl" "" "DELETE")" if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then @@ -221,7 +211,6 @@ dns_infoblox_uddi_rm() { fi else _err "Record to delete didn't match an existing record (no matching txtvalue found)" - _debug "Looking for txtvalue: $txtvalue" return 1 fi else From 890ab4a7bbfc9e950ba0ae103796e066442c25ee Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sun, 30 Nov 2025 00:48:15 +0100 Subject: [PATCH 24/28] Refactor dns_infoblox_uddi.sh: Fix zone detection and add wildcard cert support - Added _get_root() helper function for proper zone detection - Fixed zone ID extraction to match dns/auth_zone/* pattern - Added _infoblox_rest() wrapper for API calls with proper auth - Improved error handling for authentication failures - Added support for wildcard certificates (multiple TXT records) - Filter by exact txtvalue when deleting records - Follow acme.sh best practices and conventions Tested with: - Standard domain certificates - Wildcard certificates (*.domain.com) - Multiple subdomains - Staging and production Let's Encrypt --- dnsapi/dns_infoblox_uddi.sh | 289 +++++++++++++++++++----------------- 1 file changed, 156 insertions(+), 133 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index ebf4484c..54cfe47b 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -10,6 +10,8 @@ Issues: github.com/acmesh-official/acme.sh/issues Author: Stefan Riegel ' +Infoblox_UDDI_Api="https://" + ######## Public functions ##################### #Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -38,76 +40,42 @@ dns_infoblox_uddi_add() { export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" - zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - zone_result="$(_get "$zone_url")" - _debug2 "zone_result: $zone_result" - - if [ "$?" != "0" ]; then - _err "Error fetching zones from Infoblox API" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" return 1 fi - - fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') - _debug "Looking for zone matching domain: $fulldomain_no_acme" - - zone_fqdn="" - temp_domain="$fulldomain_no_acme" - - while [ -n "$temp_domain" ]; do - _debug "Checking if '$temp_domain' is a zone..." - if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then - zone_fqdn="$temp_domain" - _debug "Found matching zone: $zone_fqdn" - break - fi - temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') - if ! echo "$temp_domain" | grep -q '\.'; then - break + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting existing txt records" + _infoblox_rest GET "dns/record?_filter=type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'" + + _info "Adding record" + body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" + + if _infoblox_rest POST "dns/record" "$body"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + elif _contains "$response" '"error"'; then + # Check if record already exists + if _contains "$response" "already exists" || _contains "$response" "duplicate"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + _err "Response: $response" + return 1 + fi + else + _info "Added, OK" + return 0 fi - done - - if [ -z "$zone_fqdn" ]; then - _err "Could not determine zone for domain $fulldomain" - _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" - return 1 - fi - - # Fetch exact zone_id for the matched fqdn using server-side filtering - filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" - filter_encoded=$(_url_encode "$filter") - zone_query="$zone_url?_filter=$filter_encoded" - zone_lookup="$(_get "$zone_query")" - _debug2 "zone_lookup: $zone_lookup" - zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - - _debug zone_id "$zone_id" - - if [ -z "$zone_id" ]; then - _err "Could not find zone ID for $zone_fqdn" - _debug "Zone result: $zone_result" - return 1 - fi - - name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") - name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') - _debug name_in_zone "$name_in_zone" - - baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" - - body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" - - result="$(_post "$body" "$baseurl" "" "POST")" - _debug2 result "$result" - - if echo "$result" | grep -q '"id"'; then - record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') - _info "Successfully created TXT record with ID: $record_id" - return 0 - else - _err "Error encountered during record addition" - _err "Response: $result" - return 1 fi + _err "Add txt record error." + return 1 } #Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -130,92 +98,147 @@ dns_infoblox_uddi_rm() { export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" - zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - zone_result="$(_get "$zone_url")" - _debug2 "zone_result: $zone_result" - - if [ "$?" != "0" ]; then - _err "Error fetching zones from Infoblox API" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" return 1 fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" - fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') - _debug "Looking for zone matching domain: $fulldomain_no_acme" + _debug "Getting txt records to delete" + # Filter by txtvalue to support wildcard certs (multiple TXT records) + filter="type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'%20and%20rdata.text%20eq%20'$txtvalue'" + _infoblox_rest GET "dns/record?_filter=$filter" - zone_fqdn="" - temp_domain="$fulldomain_no_acme" + if ! _contains "$response" '"results"'; then + _info "Don't need to remove, record not found." + return 0 + fi - while [ -n "$temp_domain" ]; do - _debug "Checking if '$temp_domain' is a zone..." - if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then - zone_fqdn="$temp_domain" - _debug "Found matching zone: $zone_fqdn" - break - fi - temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') - if ! echo "$temp_domain" | grep -q '\.'; then - break - fi - done + record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _debug "record_id" "$record_id" - if [ -z "$zone_fqdn" ]; then - _err "Could not determine zone for domain $fulldomain" - _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" - return 1 + if [ -z "$record_id" ]; then + _info "Don't need to remove, record not found." + return 0 fi - # Fetch exact zone_id for the matched fqdn using server-side filtering - filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" - filter_encoded=$(_url_encode "$filter") - zone_query="$zone_url?_filter=$filter_encoded" - zone_lookup="$(_get "$zone_query")" - _debug2 "zone_lookup: $zone_lookup" - zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - - _debug zone_id "$zone_id" + # Extract UUID from the full record ID (format: dns/record/uuid) + record_uuid=$(echo "$record_id" | sed 's|.*/||') + _debug "record_uuid" "$record_uuid" - if [ -z "$zone_id" ]; then - _err "Could not find zone ID for $zone_fqdn" - _debug "Zone result: $zone_result" + if ! _infoblox_rest DELETE "dns/record/$record_uuid"; then + _err "Delete record error." return 1 fi - name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') - _debug name_in_zone "$name_in_zone" - - filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" - filter_encoded=$(_url_encode "$filter") - geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" - - result="$(_get "$geturl")" - _debug2 result "$result" - - if echo "$result" | grep -q '"results":'; then - record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "Found record_id: $record_id" + _info "Removed record successfully" + return 0 +} - if [ -n "$record_id" ]; then - record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') - _debug record_uuid "$record_uuid" +#################### Private functions below ################################## + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=dns/auth_zone/xxxx-xxxx +_get_root() { + domain=$1 + i=1 + p=1 + + # Remove _acme-challenge prefix if present + domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//') + + while true; do + h=$(printf "%s" "$domain_no_acme" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + # not valid + return 1 + fi - delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" - rmResult="$(_post "" "$delurl" "" "DELETE")" + # Query for the zone with both trailing dot and without + filter="fqdn%20eq%20'$h.'%20or%20fqdn%20eq%20'$h'" + if ! _infoblox_rest GET "dns/auth_zone?_filter=$filter"; then + # API error - don't continue if we get auth errors + if _contains "$response" "401" || _contains "$response" "Authorization"; then + _err "Authentication failed. Please check your Infoblox_UDDI_Key." + return 1 + fi + # For other errors, continue to parent domain + p=$i + i=$((i + 1)) + continue + fi - if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then - _info "Successfully deleted the txt record" + # Check if response contains results (even if empty) + if _contains "$response" '"results"'; then + # Extract zone ID - must match the pattern dns/auth_zone/... + zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4) + if [ -n "$zone_id" ]; then + # Found the zone + _domain="$h" + _domain_id="$zone_id" + + # Calculate subdomain + if [ "$_domain" = "$domain" ]; then + _sub_domain="" + else + _cutlength=$((${#domain} - ${#_domain} - 1)) + _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") + fi + return 0 - else - _err "Error occurred during txt record delete" - _err "Response: $rmResult" - return 1 fi - else - _err "Record to delete didn't match an existing record (no matching txtvalue found)" - return 1 fi + + p=$i + i=$((i + 1)) + done + + return 1 +} + +# _infoblox_rest GET "dns/record?_filter=..." +# _infoblox_rest POST "dns/record" "{json body}" +# _infoblox_rest DELETE "dns/record/uuid" +_infoblox_rest() { + method=$1 + ep="$2" + data="$3" + + _debug "$ep" + + # Ensure credentials are available (when called from _get_root) + Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" + Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" + + Infoblox_UDDI_Api="https://$Infoblox_Portal/api/ddi/v1" + export _H1="Authorization: Token $Infoblox_UDDI_Key" + export _H2="Content-Type: application/json" + + # Debug (masked) + _tok_len=$(printf "%s" "$Infoblox_UDDI_Key" | wc -c | tr -d ' \n') + _debug2 "Auth header set" "Token len=${_tok_len} on $Infoblox_Portal" + + if [ "$method" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$Infoblox_UDDI_Api/$ep" "" "$method")" else - _err "Record to delete didn't match an existing record (no results found)" - _debug "Response: $result" + response="$(_get "$Infoblox_UDDI_Api/$ep")" + fi + + _ret="$?" + _debug2 response "$response" + + if [ "$_ret" != "0" ]; then + _err "Error: $ep" return 1 fi + + return 0 } From 36b8ca2bc07d0e46dd65fc8d8365d9ca1797d786 Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sun, 30 Nov 2025 00:49:36 +0100 Subject: [PATCH 25/28] Fix shfmt formatting: Remove trailing whitespace --- dnsapi/dns_infoblox_uddi.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index 54cfe47b..4b15088a 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -183,7 +183,7 @@ _get_root() { # Found the zone _domain="$h" _domain_id="$zone_id" - + # Calculate subdomain if [ "$_domain" = "$domain" ]; then _sub_domain="" @@ -191,7 +191,7 @@ _get_root() { _cutlength=$((${#domain} - ${#_domain} - 1)) _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") fi - + return 0 fi fi From bee01c938a234021d4aa8d6bcb2cb6d421262573 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Dec 2025 21:46:05 +0100 Subject: [PATCH 26/28] add comment --- .github/workflows/wiki-monitor.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index b0332775..a79d70a4 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -22,6 +22,7 @@ jobs: page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") + page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH") now="$(date '+%Y-%m-%d %H:%M:%S')" cd wiki @@ -35,9 +36,11 @@ jobs: { echo "Wiki edited" echo -n "User: " - echo "[$actor]($sender_url)" + echo "@$actor [$actor]($sender_url)" echo "Time: $now" echo "Page: [$page_name]($page_url) (Action: $page_action)" + echo "Comment: $page_summary" + echo "[Click here to Revert](${page_url}/_history)" echo "" echo "----" echo "### diff:" From 45cb36f6d909dc5ed5b5534088aed088b9c3e55f Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 5 Dec 2025 22:31:45 +0100 Subject: [PATCH 27/28] fix https://github.com/acmesh-official/acme.sh/issues/6246#issuecomment-3610998032 --- dnsapi/dns_ali.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index 53a82f91..cbee773e 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -97,9 +97,10 @@ _ali_rest() { } _ali_nonce() { - #_head_n 1 /dev/null && return 0 + fi + printf "%s" "$(date +%s)$$$(date +%N)" | _digest sha256 hex | cut -c 1-32 } _timestamp() { From 3b2c2b16b2472c986f16a00eb24f27456f57a318 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 6 Dec 2025 11:23:28 +0100 Subject: [PATCH 28/28] minor --- dnsapi/dns_ali.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index cbee773e..90196c69 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -103,7 +103,7 @@ _ali_nonce() { printf "%s" "$(date +%s)$$$(date +%N)" | _digest sha256 hex | cut -c 1-32 } -_timestamp() { +_ali_timestamp() { date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ" } @@ -151,7 +151,7 @@ _check_exist_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_timestamp) + query=$query'&Timestamp='$(_ali_timestamp) query=$query'&TypeKeyWord=TXT' query=$query'&Version=2015-01-09' } @@ -167,7 +167,7 @@ _add_record_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_timestamp) + query=$query'&Timestamp='$(_ali_timestamp) query=$query'&Type=TXT' query=$query'&Value='$3 query=$query'&Version=2015-01-09' @@ -183,7 +183,7 @@ _delete_record_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_timestamp) + query=$query'&Timestamp='$(_ali_timestamp) query=$query'&Version=2015-01-09' } @@ -197,7 +197,7 @@ _describe_records_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_timestamp) + query=$query'&Timestamp='$(_ali_timestamp) query=$query'&Version=2015-01-09' }