From 010bd1111a7f44c02f6d8415a1ab915cd145226d Mon Sep 17 00:00:00 2001 From: Tom Sommer Date: Sun, 24 May 2026 22:20:40 +0200 Subject: [PATCH 01/18] Improve Simply.com API (#6933) Improve Simply.com API (#6933) --- dnsapi/dns_simply.sh | 92 +++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh index e0ad16e2..74e891ad 100644 --- a/dnsapi/dns_simply.sh +++ b/dnsapi/dns_simply.sh @@ -8,11 +8,7 @@ Options: SIMPLY_ApiKey API Key ' -#SIMPLY_Api="https://api.simply.com/2/" -SIMPLY_Api_Default="https://api.simply.com/2" - -#This is used for determining success of REST call -SIMPLY_SUCCESS_CODE='"status":200' +SIMPLY_Api="https://api.simply.com/2" ######## Public functions ##################### #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -72,7 +68,16 @@ dns_simply_rm() { return 1 fi - records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') + case "$_simply_http_code" in + 2*) ;; + *) + _err "Failed to fetch DNS records (HTTP $_simply_http_code)" + _err "$response" + return 1 + ;; + esac + + records=$(echo "$response" | tr '{' "\n" | grep -E 'record_id|type|data|name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ') nr_of_deleted_records=0 _info "Fetching txt record" @@ -95,7 +100,7 @@ dns_simply_rm() { if [ "$record_id" -gt 0 ]; then - if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then + if ! _simply_delete_record "$_domain" "$record_id"; then _err "Record with id $record_id could not be deleted" return 1 fi @@ -122,14 +127,9 @@ dns_simply_rm() { #################### Private functions below ################################## _simply_load_config() { - SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}" SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}" SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}" - if [ -z "$SIMPLY_Api" ]; then - SIMPLY_Api="$SIMPLY_Api_Default" - fi - if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then SIMPLY_AccountName="" SIMPLY_ApiKey="" @@ -144,9 +144,6 @@ _simply_load_config() { } _simply_save_config() { - if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then - _saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api" - fi _saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName" _saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey" } @@ -163,26 +160,39 @@ _simply_get_all_records() { _get_root() { domain=$1 + + if ! _simply_rest GET "my/products/"; then + return 1 + fi + + case "$_simply_http_code" in + 2*) ;; + *) + _err "Failed to fetch product list (HTTP $_simply_http_code)" + _err "$response" + return 1 + ;; + esac + i=2 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) if [ -z "$h" ]; then - #not valid return 1 fi - if ! _simply_rest GET "my/products/$h/dns/"; then - return 1 - fi + _domain=$(printf "%s" "$response" | tr '}' '\n' | + grep -F -e "\"object\":\"$h\"" -e "\"name\":\"$h\"" -e "\"name_idn\":\"$h\"" | + sed -n 's/.*"object":"\([^"]*\)".*/\1/p' | + _head_n 1) - if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then - _debug "$h not found" - else + if [ -n "$_domain" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain="$h" return 0 fi + + _debug "No Simply.com product found for $h" p="$i" i=$(_math "$i" + 1) done @@ -194,39 +204,44 @@ _simply_add_record() { sub_domain=$2 txtval=$3 - data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}" + data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 120}" if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then - _err "Adding record not successfull!" + _err "Adding record not successful!" return 1 fi - if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then - _err "Call to API not sucessfull, see below message for more details" + case "$_simply_http_code" in + 2*) ;; + *) + _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details" _err "$response" return 1 - fi + ;; + esac return 0 } _simply_delete_record() { domain=$1 - sub_domain=$2 - record_id=$3 + record_id=$2 _debug record_id "Delete record with id $record_id" if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then - _err "Deleting record not successfull!" + _err "Deleting record not successful!" return 1 fi - if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then - _err "Call to API not sucessfull, see below message for more details" + case "$_simply_http_code" in + 2*) ;; + *) + _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details" _err "$response" return 1 - fi + ;; + esac return 0 } @@ -248,17 +263,24 @@ _simply_rest() { export _H2="Content-Type: application/json" + : >"$HTTP_HEADER" + if [ "$m" != "GET" ]; then response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")" else response="$(_get "$SIMPLY_Api/$ep")" fi - if [ "$?" != "0" ]; then + _ret="$?" + unset _H1 _H2 + + if [ "$_ret" != "0" ]; then _err "error $ep" return 1 fi + _simply_http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d' ' -f2 | tr -d '\r\n')" + response="$(echo "$response" | _normalizeJson)" _debug2 response "$response" From ce07759cede1fddc70d303200137bc157e04e2fd Mon Sep 17 00:00:00 2001 From: Markus Ebner Date: Sun, 24 May 2026 22:24:00 +0200 Subject: [PATCH 02/18] [dnsapi] add IP-Projects dns hook (#6959) --- dnsapi/dns_ipprojects.sh | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 dnsapi/dns_ipprojects.sh diff --git a/dnsapi/dns_ipprojects.sh b/dnsapi/dns_ipprojects.sh new file mode 100644 index 00000000..dadd05f0 --- /dev/null +++ b/dnsapi/dns_ipprojects.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_ipprojects_info='IP-Projects DNS +Site: ip-projects.de/ +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects +Options: + IPP_Apikey API Key +Issues: github.com/acmesh-official/acme.sh/issues/6958 +Author: Markus Ebner +' + +IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}" +IPP_API="https://api.ip-projects.de/v1/dns/acme" + +######## Public functions ######## + +dns_ipprojects_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Using IP-Projects DNS API to add record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _IPP_load_credentials; then + return 1 + fi + + _IPP_api_request "add" "$fulldomain" "$txtvalue" +} + +dns_ipprojects_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Using IP-Projects DNS API to remove record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _IPP_load_credentials; then + return 1 + fi + + _IPP_api_request "remove" "$fulldomain" "$txtvalue" +} + +######## Private helpers ######## + +_IPP_load_credentials() { + IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}" + + if [ -z "$IPP_Apikey" ]; then + _err "You must export IPP_Apikey" + _err "e.g.: export IPP_Apikey=\"your_api_key\"" + return 1 + fi + + _saveaccountconf_mutable IPP_Apikey "$IPP_Apikey" + return 0 +} + +_IPP_api_request() { + action="$1" + domain="$2" + value="$3" + + url="$IPP_API/$action" + + data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}" + _debug url "$url" + _debug data "$data" + export _H1="X-API-Key: $IPP_Apikey" + + response="$(_post "$data" "$url" "" "POST" "application/json")" + ret="$?" + _ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') + + _debug response "$response" + + if [ "$ret" != "0" ]; then + _err "HTTP request failed" + return 1 + fi + + if [ "$_ipprojects_last_http_code" != "200" ]; then + _err "API returned an error [code: ${_ipprojects_last_http_code}]" + return 1 + fi + + return 0 +} From d9ce7fefa1f3106b39101b7864980f90d2616d9a Mon Sep 17 00:00:00 2001 From: "Simon V." <218359733+sim0n-v@users.noreply.github.com> Date: Sun, 24 May 2026 22:25:52 +0200 Subject: [PATCH 03/18] ARI - Add support for switching ACME Server during renewal (#6983) * ARI - Add support for switching ACME Server during renewal https://github.com/acmesh-official/acme.sh/issues/6964 * Restore old condition while adding malformed --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index aa9d34a0..f67749c0 100755 --- a/acme.sh +++ b/acme.sh @@ -4895,7 +4895,7 @@ issue() { # (Let's Encrypt) may also reject with a malformed error if the prior cert # was issued by a different issuer / different CA. Retry without "replaces" # whenever the failure mentions ARI or the replaces field. - if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then + if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then _err "Error creating new order." From 206f4494ac5977c359e33382c23575deab5c8cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20N=C3=A6ss?= <102598494+InvisibleDuck@users.noreply.github.com> Date: Sun, 24 May 2026 22:34:56 +0200 Subject: [PATCH 04/18] Add Poweradmin DNS API plugin (dns_poweradmin) (#6943) * Add Poweradmin DNS API plugin (dns_poweradmin) --- dnsapi/dns_poweradmin.sh | 238 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 dnsapi/dns_poweradmin.sh diff --git a/dnsapi/dns_poweradmin.sh b/dnsapi/dns_poweradmin.sh new file mode 100644 index 00000000..db31fa4f --- /dev/null +++ b/dnsapi/dns_poweradmin.sh @@ -0,0 +1,238 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 + +# Credits to the authors of dnsapi/dns_pdns.sh as this reuses much of that code. + +dns_poweradmin_info='Poweradmin API +Site: https://www.poweradmin.org/ +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_poweradmin +Options: +POWERADMIN_URL API URL (with scheme). E.g. "https://poweradmin.example.com" or "http://192.168.0.10:8080" +POWERADMIN_API_KEY API Token "pwa_xxxx" +POWERADMIN_API_VERSION Optionally override Poweradmin API version. +Issues: https://github.com/acmesh-official/acme.sh/issues/6912 +Author: Jakob Næss +' + +######## Public functions #################### + +# Usage: dns_poweradmin_add _acme-challenge.www.domain.com "123456789ABCDEF" +# fulldomain +# txtvalue +dns_poweradmin_add() { + fulldomain=$1 + txtvalue=$2 + + POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}" + POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}" + + if [ -z "$POWERADMIN_URL" ]; then + POWERADMIN_URL="" + _err "You didn't specify Poweradmin URL." + _err "Please set POWERADMIN_URL and try again." + return 1 + fi + + if [ -z "$POWERADMIN_API_KEY" ]; then + POWERADMIN_API_KEY="" + _err "You didn't specify Poweradmin token." + _err "Please set POWERADMIN_API_KEY and try again." + return 1 + fi + + # Save the api addr, key, and version to the account conf file. + _saveaccountconf_mutable POWERADMIN_URL "$POWERADMIN_URL" + _saveaccountconf_mutable POWERADMIN_API_KEY "$POWERADMIN_API_KEY" + _saveaccountconf_mutable POWERADMIN_API_VERSION "$POWERADMIN_API_VERSION" + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain "$_domain" + _debug _zone_id "$_zone_id" + + if ! _set_record "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +# Usage: dns_poweradmin_rm _acme-challenge.www.domain.com "123456789ABCDEF" +# fulldomain +# txtvalue +dns_poweradmin_rm() { + fulldomain=$1 + txtvalue=$2 + + POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}" + POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}" + POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}" + + _debug "Detect root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _domain "$_domain" + _debug _zone_id "$_zone_id" + + if ! _rm_record "$fulldomain" "$txtvalue"; then + return 1 + fi + + return 0 +} + +######## Private functions below ##################### + +_set_record() { + _info "Adding TXT record" + full=$1 + new_challenge=$2 + + data='{"name":"'$full'","type":"TXT","content":"'$new_challenge'","ttl":60}' + + if ! _poweradmin_rest "POST" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records" "$data" "application/json"; then + _err "Failed to add TXT record" + return 1 + fi + + return 0 +} + +_rm_record() { + _info "Remove TXT record" + full=$1 + txtvalue=$2 + + if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then + _err "Failed to retrieve records" + return 1 + fi + + # The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]} + _txt_record_obj=$( + printf '%s\n' "$response" | + sed 's/^.*"data":\[//; s/\],"message":.*$//' | + awk '{ gsub(/},{/, "}\n{"); print }' | + grep -F "\"name\":\"$full\"" | + grep -F "\"type\":\"TXT\"" | + grep -F "\"content\":\"$txtvalue\"" | + _head_n 1 + ) + + if [ -z "$_txt_record_obj" ]; then + _info "TXT record not found for $full with content $txtvalue" + return 0 + fi + + record_id=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' | _head_n 1) + record_type=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p' | _head_n 1) + record_name=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | _head_n 1) + record_content=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | _head_n 1) + + _debug2 "_txt_record_obj=$_txt_record_obj" + _debug2 "record id: $record_id" + _debug2 "record type: $record_type" + _debug2 "record name: $record_name" + _debug2 "record content: $record_content" + + if [ "$record_type" != "TXT" ]; then + _err "Refusing to delete non-TXT record id=$record_id type=$record_type name=$full" + return 1 + fi + + if ! _poweradmin_rest "DELETE" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records/$record_id"; then + _err "Failed to delete TXT record" + return 1 + fi + + _info "Record deleted successfully" + return 0 +} + +# _acme-challenge.www.domain.com +# returns +# _domain=domain.com +# _zone_id=220 +_get_root() { + domain=$1 + i=1 + + if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones"; then + _err "Failed to retrieve zones" + return 1 + fi + + _zones_response="$response" + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + + if [ -z "$h" ]; then + _debug "Root domain not found for $domain" + return 1 + fi + + zone_obj=$( + printf '%s' "$_zones_response" | + sed 's/},{/}\n{/g' | + grep -F "\"name\":\"$h\"" | + _head_n 1 + ) + + if [ -n "$zone_obj" ]; then + _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2) + _domain="$h" + _debug "Found zone: $_domain with id: $_zone_id" + return 0 + fi + + i=$(_math "$i" + 1) + done +} + +_poweradmin_rest() { + method=$1 + ep=$2 + data=$3 + ct=$4 + + export _H1="X-API-Key: $POWERADMIN_API_KEY" + + if [ "$method" = "GET" ]; then + response="$(_get "$POWERADMIN_URL$ep")" + else + _debug "API call: $method $ep" + _debug "Content-Type: $ct" + _debug "Payload: $data" + response="$(_post "$data" "$POWERADMIN_URL$ep" "" "$method" "$ct")" + fi + + # Clear _H1 variable + unset -v _H1 + + if [ "$?" != "0" ]; then + _err "API error on $method $ep" + _debug "Response: $response" + return 1 + fi + + if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then + _err "API reported failure on $method $ep" + _debug "Response: $response" + return 1 + fi + + _debug2 "API Response: $response" + return 0 +} From b7e9214e2d65b1099f6cd0dfc321ffa7b06159b0 Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 8 May 2026 20:44:48 +0200 Subject: [PATCH 05/18] minor --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index f67749c0..6ab2d8d8 100755 --- a/acme.sh +++ b/acme.sh @@ -5785,7 +5785,7 @@ renew() { _debug "_renewServer" "$_renewServer" _initpath "$Le_Domain" "$_isEcc" - + _info "Renew: $Le_Domain" _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _info "$(__green "Renewing: '$Le_Domain'")" if [ ! -f "$DOMAIN_CONF" ]; then From 5713c1d39d3dd89f6b6698aaf26181ea9bb8c382 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 30 May 2026 11:48:06 +0200 Subject: [PATCH 06/18] remove dns_hetzner.sh https://github.com/acmesh-official/acme.sh/issues/6990#issuecomment-4576551997 --- dnsapi/dns_hetzner.sh | 256 ------------------------------------------ 1 file changed, 256 deletions(-) delete mode 100755 dnsapi/dns_hetzner.sh diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh deleted file mode 100755 index f1bddc61..00000000 --- a/dnsapi/dns_hetzner.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env sh -# shellcheck disable=SC2034 -dns_hetzner_info='Hetzner.com -Site: Hetzner.com -Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner -Options: - HETZNER_Token API Token -Issues: github.com/acmesh-official/acme.sh/issues/2943 -' - -HETZNER_Api="https://dns.hetzner.com/api/v1" - -######## Public functions ##################### - -# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" -# Used to add txt record -# Ref: https://dns.hetzner.com/api-docs/ -dns_hetzner_add() { - full_domain=$1 - txt_value=$2 - - HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}" - - if [ -z "$HETZNER_Token" ]; then - HETZNER_Token="" - _err "You didn't specify a Hetzner api token." - _err "You can get yours from here https://dns.hetzner.com/settings/api-token." - return 1 - fi - - #save the api key and email to the account conf file. - _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token" - - _debug "First detect the root zone" - - if ! _get_root "$full_domain"; then - _err "Invalid domain" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting TXT records" - if ! _find_record "$_sub_domain" "$txt_value"; then - return 1 - fi - - if [ -z "$_record_id" ]; then - _info "Adding record" - if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then - if _contains "$response" "$txt_value"; then - _info "Record added, OK" - _sleep 2 - return 0 - fi - fi - _err "Add txt record error${_response_error}" - return 1 - else - _info "Found record id: $_record_id." - _info "Record found, do nothing." - return 0 - # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same - #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then - # if _contains "$response" "$txt_value"; then - # _info "Modified, OK" - # return 0 - # fi - #fi - #_err "Add txt record error (modify)." - #return 1 - fi -} - -# Usage: full_domain txt_value -# Used to remove the txt record after validation -dns_hetzner_rm() { - full_domain=$1 - txt_value=$2 - - HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}" - - _debug "First detect the root zone" - if ! _get_root "$full_domain"; then - _err "Invalid domain" - return 1 - fi - _debug _domain_id "$_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting TXT records" - if ! _find_record "$_sub_domain" "$txt_value"; then - return 1 - fi - - if [ -z "$_record_id" ]; then - _info "Remove not needed. Record not found." - else - if ! _hetzner_rest DELETE "records/$_record_id"; then - _err "Delete record error${_response_error}" - return 1 - fi - _sleep 2 - _info "Record deleted" - fi -} - -#################### Private functions below ################################## -#returns -# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error -_find_record() { - unset _record_id - _record_name=$1 - _record_value=$2 - - if [ -z "$_record_value" ]; then - _record_value='[^"]*' - fi - - _debug "Getting all records" - _hetzner_rest GET "records?zone_id=${_domain_id}" - - if _response_has_error; then - _err "Error${_response_error}" - return 1 - else - _record_id=$( - echo "$response" | - grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" | - grep "\"value\":\"$_record_value\"" | - while read -r record; do - # test for type and - if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then - echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \" - break - fi - done - ) - fi -} - -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -# _domain_id=sdjkglgdfewsdfg -_get_root() { - domain=$1 - i=1 - p=1 - - domain_without_acme=$(echo "$domain" | cut -d . -f 2-) - domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g') - - _debug "Reading zone_id for '$domain_without_acme' from config..." - HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name") - if [ "$HETZNER_Zone_ID" ]; then - _debug "Found, using: $HETZNER_Zone_ID" - if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then - _debug "Zone with id '$HETZNER_Zone_ID' does not exist." - _cleardomainconf "$domain_param_name" - unset HETZNER_Zone_ID - else - if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then - _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1) - if [ "$_domain" ]; then - _cut_length=$((${#domain} - ${#_domain} - 1)) - _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length") - _domain_id="$HETZNER_Zone_ID" - return 0 - else - return 1 - fi - else - return 1 - fi - fi - fi - - _debug "Trying to get zone id by domain name for '$domain_without_acme'." - while true; do - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - if [ -z "$h" ]; then - #not valid - return 1 - fi - _debug h "$h" - - _hetzner_rest GET "zones?name=$h" - - if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then - _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") - if [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - HETZNER_Zone_ID=$_domain_id - _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID" - return 0 - fi - return 1 - fi - p=$i - i=$(_math "$i" + 1) - done - return 1 -} - -#returns -# _response_error -_response_has_error() { - unset _response_error - - err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')" - - if [ -n "$err_part" ]; then - err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2) - err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \") - - if [ -n "$err_code" ] && [ -n "$err_message" ]; then - _response_error=" - message: ${err_message}, code: ${err_code}" - return 0 - fi - fi - - return 1 -} - -#returns -# response -_hetzner_rest() { - m=$1 - ep="$2" - data="$3" - _debug "$ep" - - key_trimmed=$(echo "$HETZNER_Token" | tr -d \") - - export _H1="Content-TType: application/json" - export _H2="Auth-API-Token: $key_trimmed" - - if [ "$m" != "GET" ]; then - _debug data "$data" - response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")" - else - response="$(_get "$HETZNER_Api/$ep")" - fi - - if [ "$?" != "0" ] || _response_has_error; then - _debug "Error$_response_error" - return 1 - fi - _debug2 response "$response" - return 0 -} From dfbe2c5bff139a89b7a782d365870aee8b9eaa50 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 4 Jun 2026 20:15:12 +0100 Subject: [PATCH 07/18] fix _getAKI() on OpenBSD (#7007) The order of the arguments does matter for OpenBSD's grep (bug or feature). --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 6ab2d8d8..00192868 100755 --- a/acme.sh +++ b/acme.sh @@ -6865,7 +6865,7 @@ deactivate() { #cert _getAKI() { _cert="$1" - ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ': ' | sed "s/keyid//" + ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep -A 1 "X509v3 Authority Key Identifier" | _tail_n 1 | tr -d ': ' | sed "s/keyid//" } #cert From c7c903fba3188ac2c9063a9fa9b14a2f991bbe25 Mon Sep 17 00:00:00 2001 From: terafin Date: Thu, 4 Jun 2026 12:25:38 -0700 Subject: [PATCH 08/18] ci: add GitHub Container Registry (ghcr.io) publishing (#7005) --- .github/workflows/dockerhub.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 0e7ba748..d10c17a8 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -41,6 +41,9 @@ jobs: runs-on: ubuntu-latest needs: CheckToken if: "contains(needs.CheckToken.outputs.hasToken, 'true')" + permissions: + contents: read + packages: write steps: - name: checkout code uses: actions/checkout@v6 @@ -58,6 +61,9 @@ jobs: - name: login to docker hub run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + - name: login to ghcr + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin - name: build and push the image run: | if [[ $GITHUB_REF == refs/tags/* ]]; then @@ -73,6 +79,8 @@ jobs: fi fi + echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV" + DOCKER_LABELS=() while read -r label; do DOCKER_LABELS+=(--label "${label}") @@ -84,3 +92,9 @@ jobs: --output "type=image,push=true" \ --build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \ --platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x . + - name: mirror the image to ghcr (best-effort) + run: | + docker buildx imagetools create \ + --tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \ + ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \ + || echo "::warning::GHCR mirror failed; Docker Hub publish unaffected" From 2e4e5d7955530932673e446f4be41e32e2204c6b Mon Sep 17 00:00:00 2001 From: neil Date: Thu, 4 Jun 2026 22:34:24 +0200 Subject: [PATCH 09/18] add tribblix --- .github/workflows/DNS.yml | 58 ++++++++++++++++++++++++- .github/workflows/Tribblix.yml | 79 ++++++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/Tribblix.yml diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 00d180b9..232c9b0f 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -661,9 +661,65 @@ jobs: - Haiku: + Tribblix: runs-on: ubuntu-latest needs: OpenIndiana + env: + TEST_DNS : ${{ secrets.TEST_DNS }} + TestingDomain: ${{ secrets.TestingDomain }} + TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }} + TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }} + TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }} + CASE: le_test_dnsapi + TEST_LOCAL: 1 + DEBUG: ${{ secrets.DEBUG }} + http_proxy: ${{ secrets.http_proxy }} + https_proxy: ${{ secrets.https_proxy }} + HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Tribblix doesn't accept the expired ISRG X1 root + TokenName1: ${{ secrets.TokenName1}} + TokenName2: ${{ secrets.TokenName2}} + TokenName3: ${{ secrets.TokenName3}} + TokenName4: ${{ secrets.TokenName4}} + TokenName5: ${{ secrets.TokenName5}} + steps: + - uses: actions/checkout@v6 + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/tribblix-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}' + sync: nfs + prepare: zap install socat + run: | + if [ "${{ secrets.TokenName1}}" ] ; then + export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" + fi + if [ "${{ secrets.TokenName2}}" ] ; then + export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}" + fi + if [ "${{ secrets.TokenName3}}" ] ; then + export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}" + fi + if [ "${{ secrets.TokenName4}}" ] ; then + export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}" + fi + if [ "${{ secrets.TokenName5}}" ] ; then + export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}" + fi + cd ../acmetest + ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" + + + + Haiku: + runs-on: ubuntu-latest + needs: Tribblix env: TEST_DNS : ${{ secrets.TEST_DNS }} TestingDomain: ${{ secrets.TestingDomain }} diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml new file mode 100644 index 00000000..cd43e0e3 --- /dev/null +++ b/.github/workflows/Tribblix.yml @@ -0,0 +1,79 @@ +name: Tribblix +on: + push: + branches: + - '*' + paths: + - '*.sh' + - '.github/workflows/Tribblix.yml' + + pull_request: + branches: + - dev + paths: + - '*.sh' + - '.github/workflows/Tribblix.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + + +jobs: + Tribblix: + strategy: + matrix: + include: + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + - TEST_ACME_Server: "LetsEncrypt.org_test" + CA_ECDSA: "" + CA: "" + CA_EMAIL: "" + TEST_PREFERRED_CHAIN: (STAGING) + ACME_USE_WGET: 1 + #- TEST_ACME_Server: "ZeroSSL.com" + # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2" + # CA: "ZeroSSL RSA DV SSL CA 2" + # CA_EMAIL: "githubtest@acme.sh" + # TEST_PREFERRED_CHAIN: "" + runs-on: ubuntu-latest + env: + TEST_LOCAL: 1 + TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} + CA_ECDSA: ${{ matrix.CA_ECDSA }} + CA: ${{ matrix.CA }} + CA_EMAIL: ${{ matrix.CA_EMAIL }} + TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} + ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} + steps: + - uses: actions/checkout@v6 + - uses: anyvm-org/cf-tunnel@v0 + id: tunnel + with: + protocol: http + port: 8080 + - name: Set envs + run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV + - name: Clone acmetest + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - uses: vmactions/tribblix-vm@v1 + with: + debug-on-error: ${{ vars.DEBUG_ON_ERROR }} + envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' + nat: | + "8080": "80" + prepare: zap install socat curl wget + sync: nfs + run: | + cd ../acmetest \ + && ./letest.sh + - name: DebugOnError + if: ${{ failure() }} + run: | + echo "See how to debug in VM:" + echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" diff --git a/README.md b/README.md index ba5d3591..22700b4c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ MidnightBSD Omnios OpenIndiana + Tribblix Haiku

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