From 010bd1111a7f44c02f6d8415a1ab915cd145226d Mon Sep 17 00:00:00 2001 From: Tom Sommer Date: Sun, 24 May 2026 22:20:40 +0200 Subject: [PATCH 1/9] 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 2/9] [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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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)