From 81fd2d2126600959c82b8224d7f25097a67a065a Mon Sep 17 00:00:00 2001 From: Lee Date: Tue, 13 Oct 2026 03:43:28 +0200 Subject: [PATCH 1/7] Add Option to Control HTTP Scheme (#7318) * Add Option to Control HTTP Scheme Once certificates are installed renewal should always use HTTPS. HTTP is only required if there have never been any certificates on the device. This also fixes an authentication issue where HTTPS is forced by the device. Before this patch authentication failed for devices which forced HTTPS because the POST request used for request did not follow the HTTP to HTTPS redirect. * Use http as Default Scheme Using https as default for SHELLY_SCHEME breaks existing deployments as they may not have a SHELLY_HOST which is identical to a name in the certificate. SHELLY_SCHEME is now a parameter as any other without the automatic upgrade to https. This will always need to be done manually. --- deploy/shelly.sh | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/deploy/shelly.sh b/deploy/shelly.sh index dbdab346..d87828de 100644 --- a/deploy/shelly.sh +++ b/deploy/shelly.sh @@ -4,18 +4,29 @@ # Deploy the HTTPS server certificate to a Shelly device on the local network. # # ```sh -# export SHELLY_HOST=192.168.1.100 -# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device +# export SHELLY_HOST=shelly.example.com +# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device +# export SHELLY_SCHEME=https # only if devices factory shipped with +# # firmware 2.0.0 or already has https +# # enabled # acme.sh --deploy -d shelly.example.com --deploy-hook shelly # ``` # +# If `SHELLY_SCHEME=https` is used, the first deploy is likely to require +# the additional `--insecure` parameter due to factory signed certificates. +# After the initial deployment make sure to retest deployment without +# `--insecure` simply using `acme.sh --deploy -d shelly.example.com` +# to ensure follow up deployment will work. +# # Environment variables: # SHELLY_HOST (required) IP or hostname of the Shelly device # SHELLY_PASSWORD (optional) Admin password for digest authentication. # Omit if auth is disabled on the device. # SHELLY_USER (optional) Username for auth. Default: admin -# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot. +# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot. # Default: 1 (reboot after upload) +# SHELLY_SCHEME (optional) Scheme used to call the API: http or https +# Default: http # # Requirements: # - Shelly Gen3+ device (Gen4 recommended) @@ -23,7 +34,7 @@ # - curl or wget # - openssl (for SHA-256 digest and random cnonce) # -# The device must be reachable via HTTP on the local network. +# The device must be reachable on the local network. # The hook uploads the fullchain.pem and private key, # then reboots the device to apply the new certificate. # @@ -52,11 +63,13 @@ shelly_deploy() { _getdeployconf SHELLY_PASSWORD _getdeployconf SHELLY_USER _getdeployconf SHELLY_REBOOT + _getdeployconf SHELLY_SCHEME _debug SHELLY_HOST "$SHELLY_HOST" _debug SHELLY_USER "$SHELLY_USER" _secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD" _debug SHELLY_REBOOT "$SHELLY_REBOOT" + _debug SHELLY_SCHEME "$SHELLY_SCHEME" if [ -z "$SHELLY_HOST" ]; then _err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device." @@ -65,11 +78,20 @@ shelly_deploy() { SHELLY_USER="${SHELLY_USER:-admin}" SHELLY_REBOOT="${SHELLY_REBOOT:-1}" + SHELLY_SCHEME="${SHELLY_SCHEME:-http}" + case "$SHELLY_SCHEME" in + http | https) ;; + *) + _err "SHELLY_SCHEME must be http or https" + return 1 + ;; + esac _savedeployconf SHELLY_HOST "$SHELLY_HOST" _savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD" _savedeployconf SHELLY_USER "$SHELLY_USER" _savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT" + _savedeployconf SHELLY_SCHEME "$SHELLY_SCHEME" # --- Auth handshake (only if password is set) --- _shelly_auth_header="" @@ -126,7 +148,7 @@ _shelly_handshake() { # Use a protected method (Shelly.GetStatus) to trigger 401. # Shelly.GetDeviceInfo is excluded from auth and would miss the challenge. _post '{"id":1,"method":"Shelly.GetStatus"}' \ - "http://${SHELLY_HOST}/rpc" "" "" "application/json" + "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json" # Detect auth from HTTP status line rather than response body if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then @@ -210,7 +232,7 @@ _shelly_rpc() { export _H1="" fi - _post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json" + _post "$_shelly_body" "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json" _shelly_ret=$? if [ "$_shelly_ret" != "0" ]; then From f5aeed17523ac9f8c1c6e2c20ed772109b1093d2 Mon Sep 17 00:00:00 2001 From: Kevin Winter Date: Wed, 14 Oct 2026 06:37:08 +0200 Subject: [PATCH 2/7] dns_cf: support IDN domains (#7322) * dns_cf: support IDN domains (punycode record lookup, URL-encoded zone lookup) * dns_cf: convert IDN once at the top of dns_cf_add/dns_cf_rm --- dnsapi/dns_cf.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh index 7b383c43..3342fbef 100755 --- a/dnsapi/dns_cf.sh +++ b/dnsapi/dns_cf.sh @@ -18,7 +18,7 @@ CF_Api="https://api.cloudflare.com/client/v4" #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_cf_add() { - fulldomain=$1 + fulldomain="$(_idn "$1")" txtvalue=$2 CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}" @@ -109,7 +109,7 @@ dns_cf_add() { #fulldomain txtvalue dns_cf_rm() { - fulldomain=$1 + fulldomain="$(_idn "$1")" txtvalue=$2 CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}" From 228a1400948740f9c1e1d9948dc0b74683e3eb3a Mon Sep 17 00:00:00 2001 From: Eyal Kalderon Date: Wed, 14 Oct 2026 00:38:33 -0400 Subject: [PATCH 3/7] Add deploy hook for Redfish-compatible BMCs (#7276) * Add `deploy/redfish.sh` and implement Redfish session log in/out Since `acme.sh` intends to keep third-party dependencies to a minimum, this implementation will not rely on `redfishtool`. * Detect BMC manager and replace existing cert with ACME cert * Reuse `_toPkcs8` defined in `acme.sh` * Return early if unsupported key algorithms or bit lengths are used * Improve robustness and UX a bit * Support opt-in automatic restart of BMC * Improve log output and inline docs some more * Remove `test-redfish-deploy-hook.sh` test harness The business logic has been implemented, so it should be safe to delete this test harness now and just interact with `acme.sh` directly. * Trim `_redfish_` prefixes from local variables Since each deploy hook is executed within its own subshell, there's no risk of polluting the outer `acme.sh` environment nor any other deploy hooks that might follow. * Save deploy configuration changes to disk * Tweak deploy hook log messages a bit * Add `DEPLOY_REDFISH_MANAGER` environment variable * Add function for checking HTTP status code * Add more helper functions, clean up log output a bit more * Return early from non-zero `curl` exit codes * Only upload leaf cert in `--sign-csr` mode * Fix bug with RSA key compatibility error message * Remove `_redfish_load_deploy_conf` helper, use consistent var naming * Add several more helper functions * Check whether server supports Certificate schema >=1.4.0 Redfish servers with a schema version older than this don't support uploading private keys + full certificate chains directly. Instead, the CSR must be generated on the server and signed with `acme.sh --sign-csr` prior to deployment. * Clarify environment variable naming a bit * Don't implicitly opt into `HTTPS_INSECURE` * Feedback: Fix lower-bound RSA key length check * Feedback: Verify file existence with `-f` flag * Feedback: Correctly map `TPM_ECC_CURVE` key length values * Feedback: Replace exact HTTP 204 checks with 2xx instead * Feedback: Use POSIX-compliant alternative to `\+` * Feedback: Get session token from body instead of HTTP header * Feedback: Save PKCS#8 private key directly to variable * Feedback: Drop `jq` dependency, handle missing action info * Fix regression in `_redfish_rest()` error reporting * Simplify construction of expected certificate JSON string Confirmed with a real-world test that the value of `_certificate_str` is unchanged before and after applying this commit. See below: diff --git a/deploy/redfish.sh b/deploy/redfish.sh index e485fd60..3459b71a 100644 --- a/deploy/redfish.sh +++ b/deploy/redfish.sh @@ -131,6 +131,10 @@ redfish_deploy() { printf '%s\n' "${_ckey_pkcs8}" cat "${_cfullchain}" } | _json_encode)" + + _new_certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)" + [ "${_certificate_str}" = "${_new_certificate_str}" ] || return 123 + _certificate_type='PEMchain' else _info "Private key and full certificate chain not available; we must be using a server-generated CSR." * Include link to `redfish-generate-csr.sh` helper script in error message * Feedback: Drop use of `paste` and `expr` * Feedback: Remove stale `_err` invocation * Feedback: Correctly deal with whitespace after colon * Feedback: Handle `\n` in username and password in Dash shell It seems that `echo` in Dash automatically converts `\n` into a line break, causing usernames and/or passwords containing a literal backslash-n to reach the BMC with a newline, breaking login. It seems that `printf '%s'` doesn't have the same issue, so we use those to format `_username` and `_password` instead of `echo`. * Feedback: Link to acme.sh wiki instead of Gist * Feedback: Fix `Certificate` schema version check * Feedback: Better handle missing key algo/length/curve info This commit changes how the deploy hook detects which cryptographic algorithms are supported by the server if not explicitly declared in `KeyPairAlgorithm`. Instead of blindly assuming RSA, we instead check for the presence of `KeyBitLength` and/or `KeyCurveId` fields and try to make an educated guess. If not enough info exists to determine key compatibility, we fail-open instead of failing closed; the server will yell at us anyway if `ReplaceCertificate` doesn't support something. * Feedback: Fix character escaping of username/password * Feedback: Correctly parse inline `KeyBitLength@Redfish.AllowableValues` This metadatum is represented as a JSON array of allowed values, not a numeric range with min/max. Make sure to treat the element in the list as the maximum, and not assume the list is at most 2 elements long. * Feedback: Add "\n" to format string for Solaris compatibility * Feedback: Use POSIX parameter expansion instead of `rev` --- deploy/redfish.sh | 492 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 deploy/redfish.sh diff --git a/deploy/redfish.sh b/deploy/redfish.sh new file mode 100644 index 00000000..ab65b8cd --- /dev/null +++ b/deploy/redfish.sh @@ -0,0 +1,492 @@ +#!/usr/bin/env sh + +# Script to deploy certificates to compatible BMCs (baseboard management +# controllers) using the [Redfish API](https://www.dmtf.org/standards/redfish). +# +# Environment Variables: +# +# ```sh +# DEPLOY_REDFISH_HOST="ipmi.example.com" # Required +# DEPLOY_REDFISH_USERNAME="Administrator" # Required +# DEPLOY_REDFISH_PASSWORD="superuser" # Required +# DEPLOY_REDFISH_USE_BASIC_AUTH=0 # Optional (set to "1" to enable) +# DEPLOY_REDFISH_RESTART=0 # Optional (set to "1" to enable) +# DEPLOY_REDFISH_MANAGER= # Optional (e.g. "/redfish/v1/Managers/Self") +# DEPLOY_REDFISH_CERTIFICATE= # Optional (e.g. "/redfish/v1/CertificateService/Certificates/1") +# ``` +# +# Compatibility: +# +# Compatible with any IPMI/BMC solution supporting Redfish v1.6.1 or newer (or +# Redfish Schema Bundle 2018.3), as defined in the following documents: +# +# +# +# +# +# This script is confirmed to work on the following systems: +# +# | IPMI Solution | BMC Chipset | Firmware Family | Redfish Version | +# |------------------|----------------|------------------|-----------------| +# | ASUS ASMB12-iKVM | ASPEED AST2600 | AMI MegaRAC SP-X | 1.15.1 | +# +# However, this should also work with HPE iLO 5 v2.42 (or newer), Dell iDRAC9 +# v5.0 (or newer), or any ASUS/Lenovo/Supermicro IPMI solution based on AMI +# MegaRAC SP-X v12.x (or newer). + +#domain keyfile certfile cafile fullchain +redfish_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf DEPLOY_REDFISH_HOST + _getdeployconf DEPLOY_REDFISH_USERNAME + _getdeployconf DEPLOY_REDFISH_PASSWORD + _getdeployconf DEPLOY_REDFISH_USE_BASIC_AUTH + _getdeployconf DEPLOY_REDFISH_RESTART + _getdeployconf DEPLOY_REDFISH_MANAGER + _getdeployconf DEPLOY_REDFISH_CERTIFICATE + + if [ -z "${DEPLOY_REDFISH_HOST}" ]; then + _err 'DEPLOY_REDFISH_HOST must be set. Please specify the IP or domain name of the Redfish server.' + return 1 + fi + + if [ -z "${DEPLOY_REDFISH_USERNAME}" ] || [ -z "${DEPLOY_REDFISH_PASSWORD}" ]; then + _err 'DEPLOY_REDFISH_USERNAME and DEPLOY_REDFISH_PASSWORD must be set.' + return 1 + fi + + DEPLOY_REDFISH_USE_BASIC_AUTH="${DEPLOY_REDFISH_USE_BASIC_AUTH:-0}" + DEPLOY_REDFISH_RESTART="${DEPLOY_REDFISH_RESTART:-0}" + + _debug DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}" + _debug DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}" + _secure_debug DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}" + _debug DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}" + + _redfish_log_in "$DEPLOY_REDFISH_USERNAME" "$DEPLOY_REDFISH_PASSWORD" || return 1 + + # Credentials are proven correct now -- save them, rather than only at the + # very end, so a later step failing doesn't discard a working login. + _savedeployconf DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}" + _savedeployconf DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}" + _savedeployconf DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}" 'base64' + _savedeployconf DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}" + + if ! _redfish_get_certificate_service_endpoint; then + _err "Redfish server ${DEPLOY_REDFISH_HOST} doesn't support certificate management API." + return 1 + fi + + _debug DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}" + _debug DEPLOY_REDFISH_MANAGER "${DEPLOY_REDFISH_MANAGER}" + _debug DEPLOY_REDFISH_CERTIFICATE "${DEPLOY_REDFISH_CERTIFICATE}" + + if [ -n "${DEPLOY_REDFISH_MANAGER}" ]; then + _manager_endpoint="${DEPLOY_REDFISH_MANAGER}" + else + _info 'Identifying REST endpoint of primary Redfish manager.' + _redfish_get_primary_manager_endpoint || return 1 + _savedeployconf DEPLOY_REDFISH_MANAGER "${_manager_endpoint}" + fi + + if [ -n "${DEPLOY_REDFISH_CERTIFICATE}" ]; then + _certificate_endpoint="${DEPLOY_REDFISH_CERTIFICATE}" + else + _info "Identifying REST endpoint of primary TLS certificate on: ${_manager_endpoint}" + _redfish_get_manager_certificate_endpoint "${_manager_endpoint}" || return 1 + fi + + if [ -f "${_ckey}" ] && [ -f "${_cfullchain}" ]; then + _info "Uploading private key and full certificate chain to: ${_certificate_endpoint}" + + if ! _redfish_supports_full_certificate_chains; then + _err 'Redfish server does not support uploading full certificate chains!' + _err "Please call 'GenerateCSR' API on server first, pass the CSR to 'acme.sh --sign-csr --csr ', and try again." + _err 'For more information: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#generate-csr-on-server-optional' + return 1 + elif ! _redfish_supports_private_key "${_ckey}" "${_certificate_service_endpoint}"; then + _err "Redfish server does not support this type of private key." + return 1 + fi + + _ckey_pkcs8="$("${ACME_OPENSSL_BIN:-openssl}" pkcs8 -topk8 -nocrypt -in "${_ckey}")" + + # shellcheck disable=SC2181 + if [ "$?" != '0' ] || [ -z "${_ckey_pkcs8}" ]; then + _err 'Failed to convert private key to PKCS#8 format!' + return 1 + fi + + _certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)" + _certificate_type='PEMchain' + else + _info "Private key and full certificate chain not available; we must be using a server-generated CSR." + _info "Uploading only leaf certificate to: ${_certificate_endpoint}" + + _certificate_str="$(_json_encode <"${_ccert}")" + _certificate_type='PEM' + fi + + _body="$(printf '{"CertificateString":"%s","CertificateType":"%s","CertificateUri":{"@odata.id":"%s"}}' "${_certificate_str}" "${_certificate_type}" "${_certificate_endpoint}")" + _redfish_rest POST "${_certificate_service_endpoint}/Actions/CertificateService.ReplaceCertificate" "${_body}" || return 1 + _code="$(_redfish_response_code)" + + if ! _startswith "${_code}" '2'; then + _err "Failed to update Redfish server TLS certificate! (HTTP ${_code})" + _err "Response: ${_response}" + return 1 + fi + + _info 'Successfully updated Redfish server TLS certificate!' + _savedeployconf DEPLOY_REDFISH_CERTIFICATE "${_certificate_endpoint}" + + if [ "${DEPLOY_REDFISH_RESTART}" = '1' ]; then + _info 'Attempting to restart BMC gracefully.' + _redfish_attempt_graceful_restart "${_manager_endpoint}" || return 0 + _info 'Successfully sent graceful restart command to BMC. After restarting, the new TLS certificate will be active.' + else + _info 'Please wait up to 20 seconds to take effect, or restart the BMC manually.' + fi + + _savedeployconf DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}" + + return 0 +} + +_redfish_rest() { + _method="$1" + _endpoint="$2" + _body="${3:-}" + + export _H1='OData-Version: 4.0' + + if [ "${_method}" = 'GET' ]; then + _response="$(_get "https://${DEPLOY_REDFISH_HOST}${_endpoint}")" + else + _response="$(_post "${_body}" "https://${DEPLOY_REDFISH_HOST}${_endpoint}" '' "${_method}" "${_body:+application/json}")" + fi + + _ret="$?" + _response="$(echo "${_response}" | _normalizeJson)" + + if [ "${_ret}" != '0' ]; then + _err "Error while calling ${_method} ${_endpoint}. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot." + fi + + return "${_ret}" +} + +_redfish_response_code() { + _egrep_o <"${HTTP_HEADER}" '^HTTP[^ ]* [0-9]+' | _tail_n 1 | tr -d '\r\n' | cut -d ' ' -f 2 +} + +_redfish_get_odata_id() { + _regex='s/{' + for key in $(echo "$1" | tr '.' '\n'); do + _regex="${_regex}.*\"${key}\":{" + done + sed -n "${_regex}[^}]*\"@odata\\.id\":[ ]*\"\\([^\"]*\\)\".*/\\1/p" +} + +_redfish_get_odata_members() { + sed -n 's/.*"Members":[ ]*\[\([^]]*\)\].*/\1/p' | sed 's/,/\n/g' | sed -n 's/.*{"@odata\.id":"\([^"]*\)"}.*/\1/p' +} + +_redfish_get_odata_count() { + sed -n 's/.*"Members@odata\.count":[ ]*\([0-9][0-9]*\).*/\1/p' +} + +_redfish_log_in() { + _username="$1" + _password="$2" + + _redfish_rest GET '/redfish/v1/' || return 1 + _info "Authenticating with Redfish API: ${DEPLOY_REDFISH_HOST}" + + if [ "${DEPLOY_REDFISH_USE_BASIC_AUTH}" = '1' ]; then + _access_token="$(printf '%s:%s' "${_username}" "${_password}" | _base64)" + export _H2="Authorization: Basic ${_access_token}" + + # Verify credentials are valid + _account_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'AccountService')" + _redfish_rest GET "${_account_service_endpoint}" || return 1 + _code="$(_redfish_response_code)" + + if [ "${_code}" != '200' ]; then + _err "Redfish authentication failed (HTTP ${_code})" + _err "Response: ${_response}" + return 1 + fi + else + # Create new session + _sessions_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Links.Sessions')" + _username="$(printf '%s\n' "${_username}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')" + _password="$(printf '%s\n' "${_password}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')" + _body="$(printf '{"UserName":"%s","Password":"%s"}' "${_username}" "${_password}")" + _redfish_rest POST "${_sessions_endpoint}" "${_body}" || return 1 + _code="$(_redfish_response_code)" + + # Verify authentication succeeded + if [ "${_code}" != '201' ]; then + _err "Redfish authentication failed (HTTP ${_code})" + _err "Response: ${_response}" + if [ "${_code}" = '401' ] && grep -qi '^WWW-Authenticate: Basic.*$' "${HTTP_HEADER}"; then + _err 'NOTE: Server supports Basic auth scheme. Consider setting DEPLOY_REDFISH_USE_BASIC_AUTH=1 and try again.' + fi + return 1 + fi + + _session="$(echo "${_response}" | _redfish_get_odata_id '')" + _auth_token="$(grep -i '^X-Auth-Token: .*$' "${HTTP_HEADER}" | _tail_n 1 | tr -d ' \r\n' | cut -d ':' -f 2)" + export _H2="X-Auth-Token: ${_auth_token}" + fi + + trap '_redfish_log_out "${_session:-}"' EXIT INT +} + +_redfish_log_out() { + _session_endpoint="${1:-}" + + _info "Logging out of Redfish session on: ${DEPLOY_REDFISH_HOST}" + + if [ -n "${_session_endpoint}" ]; then + _redfish_rest DELETE "${_session_endpoint}" || return 1 + _code="$(_redfish_response_code)" + + if ! _startswith "${_code}" '2'; then + _err "Failed to log out of Redfish server (HTTP ${_code})." + _err "Response: ${_response}" + fi + else + _info 'Using basic HTTP auth, no need to log out of Redfish API.' + fi + + export _H1= + export _H2= +} + +_redfish_get_certificate_service_endpoint() { + _redfish_rest GET '/redfish/v1/' || return 1 + _certificate_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'CertificateService')" + [ -n "${_certificate_service_endpoint}" ] +} + +_redfish_supports_full_certificate_chains() { + _redfish_rest GET "/redfish/v1/\$metadata" || return 1 + + _newest_certificate_schema_version="$(echo "${_response}" | + _egrep_o 'Namespace="Certificate\.v[_0-9][_0-9]*"' | + sed -n 's/.*v\([_0-9][_0-9]*\)".*/\1/p' | + sort -n -t _ -k1,1 -k2,2 -k3,3 | _tail_n 1 | tr '_' '.')" + _debug _newest_certificate_schema_version "${_newest_certificate_schema_version}" + + _schema_major=${_newest_certificate_schema_version%%.*} + _schema_patch_minor=${_newest_certificate_schema_version#*.} + _schema_minor=${_schema_patch_minor%%.*} + _schema_patch=${_schema_patch_minor#*.} + + for var in _schema_major _schema_minor _schema_patch; do + _debug "$var" "$(eval echo "\$$var")" + case "$(eval echo "\$$var")" in + '' | *[!0-9]*) return 1 ;; + *) continue ;; + esac + done + + [ "${_schema_major}" -gt '1' ] || { [ "${_schema_major}" -ge '1' ] && [ "${_schema_minor}" -ge '4' ]; } +} + +_redfish_get_action() { + unset _action _action_info + + _redfish_rest GET "$1" || return 1 + _action="$(echo "${_response}" | sed -n "s/{.*\"Actions\":{.*\"#$2\":\({[^}]*}\).*/\1/p")" + _action_info_endpoint="$(echo "${_action}" | sed -n 's/.*"@Redfish\.ActionInfo":"\([^"]*\)".*/\1/p')" + + if [ -n "${_action_info_endpoint}" ]; then + _redfish_rest GET "${_action_info_endpoint}" || return 1 + _code="$(_redfish_response_code)" + + if [ "${_code}" = '200' ]; then + _action_info="${_response}" + fi + fi +} + +_redfish_get_action_allowable_values() { + _parameter="$1" + + _allowable_values="$(echo "${_action}" | + sed -n "s/.*\"${_parameter}@Redfish\.AllowableValues\":[ ]*\[\([^]]*\)\].*/\1/p" | + tr -d '"')" + + if [ -n "${_allowable_values}" ]; then + _debug2 "Found allowable values for '${_parameter}' in '@Redfish.Action': [${_allowable_values}]" + echo "${_allowable_values}" + elif [ -n "${_action_info}" ]; then + _parameter_info="$(echo "${_action_info}" | + sed -n 's/.*"Parameters":[ ]*\[\(\({[^}]*},\{0,1\}\)*\)\].*/\1/p' | _egrep_o '\{[^\}]{1,}\}' | + grep "\"Name\":\"${_parameter}\"")" + + if [ -n "${_parameter_info}" ]; then + _debug2 "Found allowable values for '${_parameter}' in @Redfish.ActionInfo: ${_parameter_info}" + _data_type="$(echo "${_parameter_info}" | sed -n 's/.*"DataType":[ ]*"\([^"]*\)".*/\1/p')" + + case "${_data_type}" in + String) + echo "${_parameter_info}" | sed -n 's/.*"AllowableValues":[ ]*\[\([^]]*\)\].*/\1/p' | tr -d '"' + ;; + Number) + _min_value="$(echo "${_parameter_info}" | sed -n 's/.*"MinimumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')" + _max_value="$(echo "${_parameter_info}" | sed -n 's/.*"MaximumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')" + echo "${_min_value},${_max_value}" + ;; + esac + fi + fi +} + +_redfish_supports_private_key() { + _private_key="$1" + _certificate_service_endpoint="$2" + + if _isRSA "${_private_key}"; then + _key_algo='RSA' + elif _isEcc "${_private_key}"; then + _key_algo='ECDSA' + else + _err 'Private key uses unknown cryptographic algorithm!' + return 1 + fi + + _redfish_get_action "${_certificate_service_endpoint}" 'CertificateService.GenerateCSR' || return 1 + _allowed_key_algos="$(_redfish_get_action_allowable_values 'KeyPairAlgorithm')" + _allowed_key_bit_lengths="$(_redfish_get_action_allowable_values 'KeyBitLength')" + _allowed_key_curve_ids="$(_redfish_get_action_allowable_values 'KeyCurveId')" + + if [ -z "${_allowed_key_algos}" ]; then + _contains "${_response}" '"KeyBitLength"' && _allowed_key_algos='TPM_ALG_RSA' + _contains "${_response}" '"KeyCurveId"' && _allowed_key_algos="${_allowed_key_algos},TPM_ALG_ECDSA" + _allowed_key_algos="${_allowed_key_algos#,}" + + if [ -z "${_allowed_key_algos}" ]; then + _debug 'Failed to determine server crypto requirements, proceeding blindly.' + return 0 + fi + + _debug "GenerateCSR does not specify allowed KeyPairAlgorithm(s), so assuming ${_allowed_key_algos}." + fi + + _debug _key_algo "${_key_algo}" + _debug _allowed_key_algos "${_allowed_key_algos}" + + case "${_key_algo}:${_allowed_key_algos}" in + RSA:*RSA*) + _min_key_length="$(echo "${_allowed_key_bit_lengths}" | cut -d ',' -f 1)" + _max_key_length="${_allowed_key_bit_lengths##*,}" + _debug _min_key_length "${_min_key_length}" + _debug _max_key_length "${_max_key_length}" + + if [ -z "${_min_key_length}" ] && [ -z "${_max_key_length}" ]; then + _debug "Server supports RSA private keys but doesn't specify minimum/maximum bit length, proceeding blindly." + return 0 + fi + + # shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook + if [ "${Le_Keylength}" -lt "${_min_key_length:-${Le_Keylength}}" ] || [ "${Le_Keylength}" -gt "${_max_key_length:-${Le_Keylength}}" ]; then + _err "Unsupported RSA private key length ${Le_Keylength}!" + _err "Please re-run acme.sh with --keylength set to a value between ${_min_key_length} and ${_max_key_length}." + return 1 + fi + ;; + ECDSA:*ECDSA*) + _debug _allowed_key_curve_ids "${_allowed_key_curve_ids}" + + if [ -z "${_allowed_key_curve_ids}" ]; then + _debug "Server supports ECDSA private keys but doesn't specify which curves, proceeding blindly." + return 0 + fi + + # shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook + if ! _contains "${_allowed_key_curve_ids}" "TPM_ECC_NIST_P${Le_Keylength#ec-}"; then + _err "Unsupported ECDSA private key type! Server supports only: ${_allowed_key_curve_ids}" + return 1 + fi + ;; + *) + _err "Unsupported key pair algorithm ${_key_algo}! Server supports only: ${_allowed_key_algos}" + return 1 + ;; + esac +} + +_redfish_get_primary_manager_endpoint() { + _redfish_rest GET '/redfish/v1/' || return 1 + _managers_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Managers')" + _redfish_rest GET "${_managers_endpoint}" || return 1 + _num_managers="$(echo "${_response}" | _redfish_get_odata_count)" + + if [ "${_num_managers}" != '1' ]; then + _all_managers="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')" + _err "Multiple Redfish managers identified (${_all_managers}), but expected exactly one." + _err 'Please specify the correct manager in DEPLOY_REDFISH_MANAGER.' + return 1 + fi + + _manager_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)" + [ -n "${_manager_endpoint}" ] +} + +_redfish_get_manager_certificate_endpoint() { + _manager_endpoint="$1" + + _redfish_rest GET "${_manager_endpoint}" || return 1 + _protocol_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'NetworkProtocol')" + _redfish_rest GET "${_protocol_endpoint}/HTTPS/Certificates" || return 1 + _num_certificates="$(echo "${_response}" | _redfish_get_odata_count)" + + if [ -z "${_num_certificates}" ]; then + _certificate_endpoint="${_protocol_endpoint}/HTTPS/Certificates/1" + return 0 + elif [ "${_num_certificates}" != '1' ]; then + _all_certificates="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')" + _err "Multiple web service HTTPS certificates identified (${_all_certificates}), but expected exactly one." + _err "Please specify which certificate should be replaced in DEPLOY_REDFISH_CERTIFICATE." + return 1 + fi + + _certificate_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)" + [ -n "${_certificate_endpoint}" ] +} + +_redfish_attempt_graceful_restart() { + _manager_endpoint="$1" + + _redfish_get_action "${_manager_endpoint}" 'Manager.Reset' || return 1 + _allowed_reset_types="$(_redfish_get_action_allowable_values 'ResetType')" + + if ! _contains "${_allowed_reset_types}" 'GracefulRestart'; then + _err "BMC doesn't support graceful restarts. Please wait up to 20 seconds to take effect, or restart the BMC manually." + return 1 + fi + + _redfish_rest POST "${_manager_endpoint}/Actions/Manager.Reset" '{"ResetType":"GracefulRestart"}' || return 1 + _code="$(_redfish_response_code)" + + if ! _startswith "${_code}" '2'; then + _err "Failed to gracefully restart BMC! (HTTP ${_code})" + _err "Response: ${_response}" + return 1 + fi +} From d73d68ced48d9c3ff45e18f6bbe78c3c11c31746 Mon Sep 17 00:00:00 2001 From: AmadeusAbion Date: Wed, 14 Oct 2026 06:39:44 +0200 Subject: [PATCH 4/7] Add Abion.com DNS API (#7311) * Add Abion.com DNS API * Update dns_abion.sh * dns_abion: keep existing TXT records on add and rm Continues acmesh-official/acme.sh#6885 by @stelar7 and addresses the review comments there. - add: append to the existing TXT values at the name instead of replacing them, so wildcard + base certificates work - rm: remove only the matching value, keep the TTL and comments of the other records, and clear the name with null when it is empty - only read the TXT records of the target name; the old parser also picked up TXT records from other names in the zone - find the zone with GET zones/ instead of the paginated zone list, and stop with the API error on anything but "Zone not found" - add Docs line and progress messages, fix shfmt Co-authored-by: stelar7 * dns_abion: lowercase the domain so rm finds the record * dns_abion: add tracking issue to info block * dns_abion: align info block with the structural info format --------- Co-authored-by: stelar7 --- dnsapi/dns_abion.sh | 198 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 dnsapi/dns_abion.sh diff --git a/dnsapi/dns_abion.sh b/dnsapi/dns_abion.sh new file mode 100644 index 00000000..acd12229 --- /dev/null +++ b/dnsapi/dns_abion.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_abion_info='Abion.com +Site: abion.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_abion +Options: + ABION_API_KEY API Key. Created in the Abion Core web interface. +Issues: github.com/acmesh-official/acme.sh/issues/7325 +' + +ABION_Api_Endpoint="https://api.abion.com/v1" + +##################### Public functions ##################### + +# Usage: dns_abion_add +# Example: dns_abion_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_abion_add() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + + ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}" + + if [ -z "$ABION_API_KEY" ]; then + ABION_API_KEY="" + _err "You need to specify ABION_API_KEY." + return 1 + fi + + _saveaccountconf_mutable ABION_API_KEY "$ABION_API_KEY" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _abion_get_txt_records; then + return 1 + fi + + if _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then + _info "TXT record already exists" + return 0 + fi + + _records=$(echo "$_txt_records" | tr '\n' ',' | sed 's/,*$//') + if [ -n "$_records" ]; then + _records="$_records," + fi + + _info "Adding record" + if ! _abion_patch_txt "[$_records{\"rdata\":\"$txtvalue\",\"ttl\":60}]"; then + return 1 + fi + + _info "Added, OK" + return 0 +} + +# Usage: dns_abion_rm +dns_abion_rm() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + + ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}" + + if [ -z "$ABION_API_KEY" ]; then + ABION_API_KEY="" + _err "You need to specify ABION_API_KEY." + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + if ! _abion_get_txt_records; then + return 1 + fi + + if ! _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then + _info "TXT record not found, nothing to remove" + return 0 + fi + + _records=$(echo "$_txt_records" | grep -v "\"rdata\":\"$txtvalue\"" | tr '\n' ',' | sed 's/,*$//') + + if [ -n "$_records" ]; then + _records="[$_records]" + else + _records="null" + fi + + _info "Deleting record" + if ! _abion_patch_txt "$_records"; then + return 1 + fi + + _info "Deleted, OK" + return 0 +} + +##################### Private functions ##################### + +_get_root() { + domain=$1 + i=1 + p=0 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 + fi + + if ! _abion_rest GET "zones/$h"; then + return 1 + fi + + if _contains "$response" '"type" *: *"zone"'; then + if [ "$p" = "0" ]; then + _sub_domain="@" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + fi + _domain="$h" + return 0 + fi + + if ! _contains "$response" "Zone not found"; then + _err "$response" + return 1 + fi + + p="$i" + i=$(_math "$i" + 1) + done +} + +_abion_get_txt_records() { + if ! _abion_rest GET "zones/$_domain"; then + return 1 + fi + + _sub_re=$(printf "%s" "$_sub_domain" | sed 's/[.]/\\./g') + _txt_records=$(echo "$response" | _normalizeJson | + sed -n "s/.*\"$_sub_re\":{//p" | + sed 's/\] *}.*/]/' | + _egrep_o '"TXT":\[[^]]*\]' | + sed 's/^"TXT":\[ *//;s/ *\]$//;s/} *, *{/}\ +{/g') + _debug2 _txt_records "$_txt_records" + return 0 +} + +_abion_patch_txt() { + if ! _abion_rest PATCH "zones/$_domain" "{\"data\":{\"type\":\"zone\",\"id\":\"$_domain\",\"attributes\":{\"records\":{\"$_sub_domain\":{\"TXT\":$1}}}}}"; then + return 1 + fi + + if ! _contains "$response" '"type" *: *"zone"'; then + _err "Unexpected API response: $response" + return 1 + fi + + return 0 +} + +_abion_rest() { + method=$1 + endpoint=$2 + data=$3 + + export _H1="X-API-KEY: $ABION_API_KEY" + export _H2="Content-Type: application/json" + + if [ "$method" = "GET" ]; then + response="$(_get "$ABION_Api_Endpoint/$endpoint")" + else + response="$(_post "$data" "$ABION_Api_Endpoint/$endpoint" "" "$method")" + fi + + if [ "$?" != "0" ]; then + _err "error $endpoint" + return 1 + fi + + _debug2 response "$response" + return 0 +} From f5f2242d607a575aef9bb87c2dd18de9969287e8 Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 14 Oct 2026 14:13:21 +0800 Subject: [PATCH 5/7] deploy/ssh: stop the batched remote command at the first failed step In single-call mode (the default) all steps were joined with ";", so a failed write did not stop the batch: the remaining files were still overwritten, DEPLOY_SSH_REMOTE_CMD still ran on the broken file, and the deploy reported success because only the last exit code counted. With a POSIX remote shell, steps are now wrapped in { } and joined with &&, matching multi-call mode. Non-POSIX remote shells (powershell -c, cmd /c) keep the old ";" join byte for byte. See #7328 --- deploy/ssh.sh | 54 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index a76d8097..16ec6482 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -189,6 +189,19 @@ ssh_deploy() { _info "Required commands batched and sent in single call to remote host" fi + # With a POSIX remote shell, steps are joined with && so that the first + # failed step stops the rest (no REMOTE_CMD after a failed write). Other + # shells ("powershell -c", "cmd /c") keep the plain ";" join. + _ssh_shell_name="${DEPLOY_SSH_REMOTE_SHELL%% *}" + case "${_ssh_shell_name##*/}" in + sh | bash | dash | ash | ksh | mksh | zsh) + _ssh_fail_fast="yes" + ;; + *) + _ssh_fail_fast="no" + ;; + esac + _returnCode=0 _deploy_ssh_servers="$DEPLOY_SSH_SERVER" for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do @@ -228,9 +241,9 @@ _ssh_deploy() { _backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')" # run cleanup on the backup directory, erase all older # than 180 days (15552000 seconds). - _cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \ + _ssh_add_step "now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \ do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \ -then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr" +then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done" # Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr" _info "Backup of old certificate files will be placed in remote directory $_backupdir" _info "Backup directories erased after 180 days." @@ -245,7 +258,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d if [ -n "$DEPLOY_SSH_KEYFILE" ]; then if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi;" + _ssh_add_step "if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then return $_err_code @@ -261,10 +274,9 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d return $_err_code fi else - # If file doesn't exist, create it and change its permissions. - _cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;" - # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" + # If file doesn't exist, create it and change its permissions, then + # ssh echo to the file. One step: "test ! -f" fails if the file exists. + _ssh_add_step "test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE; echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE" _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then @@ -282,7 +294,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d _pipe=">>" elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi;" + _ssh_add_step "if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then return $_err_code @@ -304,7 +316,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi else # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;" + _ssh_add_step "echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE" _info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then @@ -323,7 +335,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d _pipe=">>" elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi;" + _ssh_add_step "if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then return $_err_code @@ -348,7 +360,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi else # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;" + _ssh_add_step "echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE" _info "will copy CA file to remote file $DEPLOY_SSH_CAFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then @@ -368,7 +380,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d _pipe=">>" elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then # backup file we are about to overwrite. - _cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi;" + _ssh_add_step "if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then return $_err_code @@ -396,7 +408,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi else # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;" + _ssh_add_step "echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN" _info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then @@ -419,7 +431,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then - _cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;" + _ssh_add_step "$DEPLOY_SSH_REMOTE_CMD" _info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then @@ -439,6 +451,20 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d return 0 } +#step +_ssh_add_step() { + if [ "$_ssh_fail_fast" = "yes" ]; then + if [ -n "$_cmdstr" ]; then + _cmdstr="$_cmdstr && " + fi + # the newline ends the step even if it ends with a comment, ";" or "&" + _cmdstr="$_cmdstr{ $1 +}" + else + _cmdstr="$_cmdstr $1;" + fi +} + #cmd _ssh_remote_cmd() { _cmd="$1" From f659ba2ea23d46af4a3d770c08f3f3d0baf2151f Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 15 Oct 2026 15:03:18 +0800 Subject: [PATCH 6/7] Skip conf writes that change nothing Since #7278 every conf write is a temp file plus a rename, and _setopt and _clear_conf rewrote the conf even when the content stayed the same: _savedeployconf clears a legacy key that is almost never there, and deploy hooks re-save the same values on every run. On storage where file creation and rename are expensive (ZFS on HDD) a multideploy run went from about 23 seconds to over 2 minutes. Return early when the new content equals the old one. A simulated 3-service docker multideploy drops from 96 conf writes to 32. Fixes #7335 --- acme.sh | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/acme.sh b/acme.sh index ccd34b18..e1a35075 100755 --- a/acme.sh +++ b/acme.sh @@ -2914,6 +2914,7 @@ _setopt() { _err "Cannot read $__conf." return 1 fi + __old_text="$__text" #build the new content first and write it once through _write_conf: #redirecting straight into the conf truncates it before anything is #written, so a failing sed (#2426) or a failing write (#7247) left a @@ -2930,6 +2931,13 @@ _setopt() { _debug3 APP __text="$__text${__text:+$__nl}$__opt$__sep$__val$__end" fi + if [ -z "$__sed_err" ] && [ "$__text" = "$__old_text" ]; then + #the value is already saved: callers re-save the same values on every + #run, and each write is a temp file plus a rename, which is slow on + #some storage (#7335) + _debug3 "Unchanged, skip writing $__conf" + return 0 + fi if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then _err "Cannot save '$__opt' to $__conf." return 1 @@ -2959,11 +2967,19 @@ _clear_conf() { _c_c_f="$1" _sdkey="$2" if [ "$_c_c_f" ]; then - _conf_data="$(cat "$_c_c_f")" + _c_c_old="$(cat "$_c_c_f")" #printf, not echo: dash's builtin echo interprets backslash escapes and #would corrupt saved values that contain them on every rewrite - if ! _conf_data="$(printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d")" || - ! _write_conf "$_c_c_f" "$_conf_data"; then + if ! _conf_data="$(printf -- "%s\n" "$_c_c_old" | sed "/^$_sdkey *=.*$/d")"; then + _err "Cannot clear '$_sdkey' in $_c_c_f." + return 1 + fi + #the key is not there, the usual case for the legacy keys cleared on + #every _savedeployconf: nothing to write (#7335) + if [ "$_conf_data" = "$_c_c_old" ]; then + return 0 + fi + if ! _write_conf "$_c_c_f" "$_conf_data"; then _err "Cannot clear '$_sdkey' in $_c_c_f." return 1 fi From 579c994fadc20d5108bccb669597c4a0439f6ced Mon Sep 17 00:00:00 2001 From: Phil Hagen Date: Fri, 16 Oct 2026 04:11:30 -0400 Subject: [PATCH 7/7] Handle DNSimple API response for nonexistent subdomain (#7324) * handle OAuth response * revert comment removal * handle lowercased domain value * expand error detection logic for all non-expected responses * shell-independent lowercasing * revert; domain value already lowercased * add missing double-quotes * use zone records endpoint fallback when zone cannot be identified * add logic missing from previous --------- Co-authored-by: neil Co-authored-by: Mashiro Co-authored-by: MBWhitestone <25477219+MBWhitestone@users.noreply.github.com> Co-authored-by: Pablo Co-authored-by: Claude Sonnet 5 --- dnsapi/dns_dnsimple.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_dnsimple.sh b/dnsapi/dns_dnsimple.sh index 257549b4..983ee9f0 100644 --- a/dnsapi/dns_dnsimple.sh +++ b/dnsapi/dns_dnsimple.sh @@ -15,7 +15,7 @@ DNSimple_API="https://api.dnsimple.com/v2" # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_dnsimple_add() { - fulldomain=$1 + fulldomain=$(echo "$1" | _lower_case) txtvalue=$2 DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" @@ -56,7 +56,7 @@ dns_dnsimple_add() { # fulldomain dns_dnsimple_rm() { - fulldomain=$1 + fulldomain=$(echo "$1" | _lower_case) DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}" if [ -z "$DNSimple_OAUTH_TOKEN" ]; then @@ -110,9 +110,13 @@ _get_root() { return 1 fi - if _contains "$response" 'not found'; then - _debug "$h not found" - else + if ! _contains "$response" "\"name\":\"$h\""; then + if ! _dnsimple_rest GET "$_account_id/zones/$h/records?per_page=1"; then + return 1 + fi + fi + + if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"pagination":'; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous") _domain="$h" @@ -121,6 +125,7 @@ _get_root() { return 0 fi + _debug "$h not found" previous="$i" i=$(_math "$i" + 1)