From 9980ad0fef9634b105c59711dd5f470a4b35f080 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:33:19 +0100 Subject: [PATCH 01/27] add HostUp DNS --- dnsapi/dns_hostup.sh | 473 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 dnsapi/dns_hostup.sh diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh new file mode 100644 index 00000000..4da0dd9d --- /dev/null +++ b/dnsapi/dns_hostup.sh @@ -0,0 +1,473 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034,SC2154 + +dns_hostup_info='HostUp DNS +Site: hostup.se +Docs: https://hostup.se/en/support/api-autentisering/ +Options: + HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes. + HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api). + HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds). + HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection). +Author: HostUp (https://cloud.hostup.se/contact/en) +' + +HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api" +HOSTUP_DEFAULT_TTL=60 + +# Public: add TXT record +# Usage: dns_hostup_add _acme-challenge.example.com "txt-value" +dns_hostup_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Using HostUp DNS API" + + if ! _hostup_init; then + return 1 + fi + + if ! _hostup_detect_zone "$fulldomain"; then + _err "Unable to determine HostUp zone for $fulldomain" + return 1 + fi + + record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")" + record_name="$(_hostup_sanitize_name "$record_name")" + record_value="$(_hostup_json_escape "$txtvalue")" + + ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}" + + _debug "zone_id" "$HOSTUP_ZONE_ID" + _debug "zone_domain" "$HOSTUP_ZONE_DOMAIN" + _debug "record_name" "$record_name" + _debug "ttl" "$ttl" + + request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}" + + if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then + return 1 + fi + + if ! _contains "$_hostup_response" '"success":true'; then + _err "HostUp DNS API: failed to create TXT record for $fulldomain" + _debug2 "_hostup_response" "$_hostup_response" + return 1 + fi + + record_id="$(_hostup_extract_record_id "$_hostup_response")" + if [ -n "$record_id" ]; then + _hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id" + _debug "hostup_saved_record_id" "$record_id" + fi + + _info "Added TXT record for $fulldomain" + return 0 +} + +# Public: remove TXT record +# Usage: dns_hostup_rm _acme-challenge.example.com "txt-value" +dns_hostup_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Using HostUp DNS API" + + if ! _hostup_init; then + return 1 + fi + + if ! _hostup_detect_zone "$fulldomain"; then + _err "Unable to determine HostUp zone for $fulldomain" + return 1 + fi + + record_name_fqdn="$(_hostup_fqdn "$fulldomain")" + record_value="$txtvalue" + + record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")" + if [ -n "$record_id_cached" ]; then + _debug "hostup_record_id_cached" "$record_id_cached" + if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then + _info "Deleted TXT record $record_id_cached" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" + HOSTUP_ZONE_ID="" + return 0 + fi + fi + + if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then + _info "TXT record not found for $record_name_fqdn. Skipping removal." + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" + return 0 + fi + + _debug "Deleting record" "$HOSTUP_RECORD_ID" + + if ! _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$HOSTUP_RECORD_ID"; then + return 1 + fi + + _info "Deleted TXT record $HOSTUP_RECORD_ID" + _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" + HOSTUP_ZONE_ID="" + return 0 +} + +########################## +# Private helper methods # +########################## + +_hostup_init() { + HOSTUP_API_KEY="${HOSTUP_API_KEY:-$(_readaccountconf_mutable HOSTUP_API_KEY)}" + HOSTUP_API_BASE="${HOSTUP_API_BASE:-$(_readaccountconf_mutable HOSTUP_API_BASE)}" + HOSTUP_TTL="${HOSTUP_TTL:-$(_readaccountconf_mutable HOSTUP_TTL)}" + HOSTUP_ZONE_ID="${HOSTUP_ZONE_ID:-$(_readaccountconf_mutable HOSTUP_ZONE_ID)}" + + if [ -z "$HOSTUP_API_BASE" ]; then + HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT" + fi + + if [ -z "$HOSTUP_API_KEY" ]; then + HOSTUP_API_KEY="" + _err "HOSTUP_API_KEY is not set." + _err "Please export your HostUp API key with read:dns and write:dns scopes." + return 1 + fi + + _saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY" + _saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE" + + if [ -n "$HOSTUP_TTL" ]; then + _saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL" + fi + + if [ -n "$HOSTUP_ZONE_ID" ]; then + _saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID" + fi + + return 0 +} + +_hostup_detect_zone() { + fulldomain="$1" + + if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then + return 0 + fi + + HOSTUP_ZONE_DOMAIN="" + _debug "hostup_full_domain" "$fulldomain" + + if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then + # Attempt to fetch domain name for provided zone ID + if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then + return 0 + fi + HOSTUP_ZONE_ID="" + fi + + if ! _hostup_load_zones; then + return 1 + fi + + _domain_candidate="$(printf "%s" "$fulldomain" | tr 'A-Z' 'a-z')" + _debug "hostup_initial_candidate" "$_domain_candidate" + + while [ -n "$_domain_candidate" ]; do + _debug "hostup_zone_candidate" "$_domain_candidate" + if _hostup_lookup_zone "$_domain_candidate"; then + HOSTUP_ZONE_DOMAIN="$_lookup_zone_domain" + HOSTUP_ZONE_ID="$_lookup_zone_id" + return 0 + fi + + if ! printf "%s" "$_domain_candidate" | _contains "."; then + break + fi + + _domain_candidate="${_domain_candidate#*.}" + done + + HOSTUP_ZONE_ID="" + return 1 +} + +_hostup_record_name() { + fulldomain="$1" + zonedomain="$2" + + # Remove trailing dot, if any + fulldomain="${fulldomain%.}" + zonedomain="${zonedomain%.}" + + if [ "$fulldomain" = "$zonedomain" ]; then + printf "%s" "@" + return 0 + fi + + suffix=".$zonedomain" + case "$fulldomain" in + *"$suffix") + printf "%s" "${fulldomain%$suffix}" + ;; + *) + # Domain not within zone, fall back to full host + printf "%s" "$fulldomain" + ;; + esac +} + +_hostup_sanitize_name() { + name="$1" + + if [ -z "$name" ] || [ "$name" = "." ]; then + printf "%s" "@" + return 0 + fi + + # Remove any trailing dot + name="${name%.}" + printf "%s" "$name" +} + +_hostup_fqdn() { + domain="$1" + printf "%s" "${domain%.}" +} + +_hostup_fetch_zone_details() { + zone_id="$1" + + if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then + return 1 + fi + + zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')" + if [ -n "$zonedomain" ]; then + HOSTUP_ZONE_DOMAIN="$zonedomain" + return 0 + fi + + return 1 +} + +_hostup_load_zones() { + if ! _hostup_rest "GET" "/dns/zones" ""; then + return 1 + fi + + HOSTUP_ZONES_CACHE="" + data="$(printf "%s" "$_hostup_response" | tr '{' '\n')" + + while IFS= read -r line; do + case "$line" in + *'"domain_id"'*'"domain"'*) + zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")" + zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")" + if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then + HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id} +" + _debug "hostup_zone_loaded" "$zone_domain|$zone_id" + fi + ;; + esac + done < Date: Mon, 1 Dec 2025 15:48:48 +0100 Subject: [PATCH 03/27] Update dns_hostup.sh bug fix Omnios fial --- dnsapi/dns_hostup.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 4da0dd9d..8d0600f7 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -182,9 +182,10 @@ _hostup_detect_zone() { return 0 fi - if ! printf "%s" "$_domain_candidate" | _contains "."; then - break - fi + case "$_domain_candidate" in + *.*) ;; + *) break ;; + esac _domain_candidate="${_domain_candidate#*.}" done From d97b4477b2dcf2753ea0afbd24eea8487625aed2 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:55:17 +0100 Subject: [PATCH 04/27] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 8d0600f7..3547f006 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -171,7 +171,7 @@ _hostup_detect_zone() { return 1 fi - _domain_candidate="$(printf "%s" "$fulldomain" | tr 'A-Z' 'a-z')" + _domain_candidate="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]')" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -181,10 +181,10 @@ _hostup_detect_zone() { HOSTUP_ZONE_ID="$_lookup_zone_id" return 0 fi - + case "$_domain_candidate" in - *.*) ;; - *) break ;; + *.*) ;; + *) break ;; esac _domain_candidate="${_domain_candidate#*.}" @@ -210,7 +210,7 @@ _hostup_record_name() { suffix=".$zonedomain" case "$fulldomain" in *"$suffix") - printf "%s" "${fulldomain%$suffix}" + printf "%s" "${fulldomain%"$suffix"}" ;; *) # Domain not within zone, fall back to full host @@ -371,7 +371,7 @@ _hostup_record_key() { zone_id="$1" domain="$2" safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" - safe_domain="$(printf "%s" "$domain" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]/_/g')" + safe_domain="$(printf "%s" "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" printf "%s_%s" "$safe_zone" "$safe_domain" } @@ -449,7 +449,7 @@ _hostup_rest() { _debug2 "_hostup_response" "$_hostup_response" case "$http_status" in - 200|201|204) return 0 ;; + 200 | 201 | 204) return 0 ;; 401) _err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions." return 1 From 64a6ea68fa704ce4df35b03cff7c29fe531c38b3 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:58:36 +0100 Subject: [PATCH 05/27] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 3547f006..ca49096f 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -181,7 +181,7 @@ _hostup_detect_zone() { HOSTUP_ZONE_ID="$_lookup_zone_id" return 0 fi - + case "$_domain_candidate" in *.*) ;; *) break ;; From 51b4fa00800ae2aad88a81fe76783d374044318c Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:19:16 +0100 Subject: [PATCH 06/27] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 49 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index ca49096f..347f34d1 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -319,10 +319,14 @@ _hostup_find_record() { records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" while IFS= read -r line; do - case "$line" in + # Normalize line to make TXT value matching reliable + line_clean="$(printf "%s" "$line" | tr -d '\r\n')" + line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')" + + case "$line_clean" in *'"type":"TXT"'*'"name"'*'"value"'*) - name_value="$(printf "%s" "$line" | _hostup_json_extract "name")" - record_value="$(printf "%s" "$line" | _hostup_json_extract "value")" + name_value="$(_hostup_json_extract "name" "$line_clean")" + record_value="$(_hostup_json_extract "value" "$line_value_clean")" _debug "hostup_record_raw" "$record_value" if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then @@ -337,7 +341,7 @@ _hostup_find_record() { _debug "hostup_record_value" "$record_value" if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then - record_id="$(printf "%s" "$line" | _hostup_json_extract "id")" + record_id="$(_hostup_json_extract "id" "$line_clean")" if [ -n "$record_id" ]; then HOSTUP_RECORD_ID="$record_id" return 0 @@ -354,13 +358,30 @@ EOF _hostup_json_extract() { key="$1" - printf "%s" "$line" | - _egrep_o "\"$key\":\"[^\"]*\"" | - head -n1 | - cut -d : -f2- | - sed 's/^"//' | - sed 's/"$//' | - sed 's/\\"/"/g' + input="${2:-$line}" + + # First try to extract quoted values (strings) + quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | head -n1)" + if [ -n "$quoted_match" ]; then + printf "%s" "$quoted_match" | + cut -d : -f2- | + sed 's/^"//' | + sed 's/"$//' | + sed 's/\\"/"/g' + return 0 + fi + + # Fallback for unquoted values (e.g., numeric IDs) + unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | head -n1)" + if [ -n "$unquoted_match" ]; then + printf "%s" "$unquoted_match" | + cut -d : -f2- | + tr -d '", ' | + tr -d '\r\n' + return 0 + fi + + return 1 } _hostup_json_escape() { @@ -398,6 +419,12 @@ _hostup_clear_record_id() { } _hostup_extract_record_id() { + record_id="$(_hostup_json_extract "id" "$1")" + if [ -n "$record_id" ]; then + printf "%s" "$record_id" + return 0 + fi + printf "%s" "$1" | _egrep_o '"id":[0-9]+' | head -n1 | cut -d: -f2 } From 1413aa332bb48d6cb60a4ef7114fe0be57f3e303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Miri=C4=87?= <1009277+imiric@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:12:27 +0100 Subject: [PATCH 07/27] fix: update Exoscale DNS script This updates the Exoscale DNS script to work with v2 of their API. --- dnsapi/dns_exoscale.sh | 226 ++++++++++++++++++++++++----------------- 1 file changed, 132 insertions(+), 94 deletions(-) mode change 100755 => 100644 dnsapi/dns_exoscale.sh diff --git a/dnsapi/dns_exoscale.sh b/dnsapi/dns_exoscale.sh old mode 100755 new mode 100644 index 6898ce38..ddd526a4 --- a/dnsapi/dns_exoscale.sh +++ b/dnsapi/dns_exoscale.sh @@ -8,9 +8,9 @@ Options: EXOSCALE_SECRET_KEY API Secret key ' -EXOSCALE_API=https://api.exoscale.com/dns/v1 +EXOSCALE_API="https://api-ch-gva-2.exoscale.com/v2" -######## Public functions ##################### +######## Public functions ######## # Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" # Used to add txt record @@ -18,159 +18,197 @@ dns_exoscale_add() { fulldomain=$1 txtvalue=$2 - if ! _checkAuth; then + _debug "Using Exoscale DNS v2 API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + if ! _check_auth; then return 1 fi - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" + root_domain_id=$(_get_root_domain_id "$fulldomain") + if [ -z "$root_domain_id" ]; then + _err "Unable to determine root domain ID for $fulldomain" return 1 fi + _debug root_domain_id "$root_domain_id" - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" + # Always get the subdomain part first + sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id") + _debug sub_domain "$sub_domain" - _info "Adding record" - if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then - if _contains "$response" "$txtvalue"; then - _info "Added, OK" - return 0 - fi + # Build the record name properly + if [ -z "$sub_domain" ]; then + record_name="_acme-challenge" + else + record_name="_acme-challenge.$sub_domain" fi - _err "Add txt record error." - return 1 + payload=$(printf '{"name":"%s","type":"TXT","content":"%s","ttl":120}' "$record_name" "$txtvalue") + _debug payload "$payload" + + response=$(_exoscale_rest POST "/dns-domain/${root_domain_id}/record" "$payload") + if _contains "$response" "\"id\""; then + _info "TXT record added successfully." + return 0 + else + _err "Error adding TXT record: $response" + return 1 + fi } -# Usage: fulldomain txtvalue -# Used to remove the txt record after validation dns_exoscale_rm() { fulldomain=$1 - txtvalue=$2 - if ! _checkAuth; then + _debug "Using Exoscale DNS v2 API for removal" + _debug fulldomain "$fulldomain" + + if ! _check_auth; then return 1 fi - _debug "First detect the root zone" - if ! _get_root "$fulldomain"; then - _err "invalid domain" + root_domain_id=$(_get_root_domain_id "$fulldomain") + if [ -z "$root_domain_id" ]; then + _err "Unable to determine root domain ID for $fulldomain" return 1 fi - _debug _sub_domain "$_sub_domain" - _debug _domain "$_domain" - - _debug "Getting txt records" - _exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token" - if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then - _record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \") + record_name="_acme-challenge" + sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id") + if [ -n "$sub_domain" ]; then + record_name="_acme-challenge.$sub_domain" fi - if [ -z "$_record_id" ]; then - _err "Can not get record id to remove." + record_id=$(_find_record_id "$root_domain_id" "$record_name") + if [ -z "$record_id" ]; then + _err "TXT record not found for deletion." return 1 fi - _debug "Deleting record $_record_id" - - if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then - _err "Delete record error." + response=$(_exoscale_rest DELETE "/dns-domain/$root_domain_id/record/$record_id") + if _contains "$response" "\"state\":\"success\""; then + _info "TXT record deleted successfully." + return 0 + else + _err "Error deleting TXT record: $response" return 1 fi - - return 0 } -#################### Private functions below ################################## +######## Private helpers ######## -_checkAuth() { +_check_auth() { EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}" EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}" - if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then - EXOSCALE_API_KEY="" - EXOSCALE_SECRET_KEY="" - _err "You don't specify Exoscale application key and application secret yet." - _err "Please create you key and try again." + _err "EXOSCALE_API_KEY and EXOSCALE_SECRET_KEY must be set." return 1 fi - _saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY" _saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY" - return 0 } -#_acme-challenge.www.domain.com -#returns -# _sub_domain=_acme-challenge.www -# _domain=domain.com -# _domain_id=sdjkglgdfewsdfg -# _domain_token=sdjkglgdfewsdfg -_get_root() { - - if ! _exoscale_rest GET "domains"; then - return 1 - fi - +_get_root_domain_id() { domain=$1 - i=2 - p=1 + i=1 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" "\"name\":\"$h\"" >/dev/null; then - _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \") - _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \") - if [ "$_domain_token" ] && [ "$_domain_id" ]; then - _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _domain=$h - return 0 + candidate=$(printf "%s" "$domain" | cut -d . -f "${i}-100") + [ -z "$candidate" ] && return 1 + _debug "Trying root domain candidate: $candidate" + domains=$(_exoscale_rest GET "/dns-domain") + # Extract from dns-domains array + result=$(echo "$domains" | _egrep_o '"dns-domains":\[.*\]' | _egrep_o '\{"id":"[^"]*","created-at":"[^"]*","unicode-name":"[^"]*"\}' | while read -r item; do + name=$(echo "$item" | _egrep_o '"unicode-name":"[^"]*"' | cut -d'"' -f4) + id=$(echo "$item" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4) + if [ "$name" = "$candidate" ]; then + echo "$id" + break fi - return 1 + done) + if [ -n "$result" ]; then + echo "$result" + return 0 fi - p=$i i=$(_math "$i" + 1) done - return 1 } -# returns response +_get_sub_domain() { + fulldomain=$1 + root_id=$2 + root_info=$(_exoscale_rest GET "/dns-domain/$root_id") + _debug root_info "$root_info" + root_name=$(echo "$root_info" | _egrep_o "\"unicode-name\":\"[^\"]*\"" | cut -d\" -f4) + sub=${fulldomain%%."$root_name"} + + if [ "$sub" = "_acme-challenge" ]; then + echo "" + else + # Remove _acme-challenge. prefix to get the actual subdomain + echo "${sub#_acme-challenge.}" + fi +} + +_find_record_id() { + root_id=$1 + name=$2 + records=$(_exoscale_rest GET "/dns-domain/$root_id/record") + + # Convert search name to lowercase for case-insensitive matching + name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]') + + echo "$records" | _egrep_o '\{[^}]*"name":"[^"]*"[^}]*\}' | while read -r record; do + record_name=$(echo "$record" | _egrep_o '"name":"[^"]*"' | cut -d'"' -f4) + record_name_lower=$(echo "$record_name" | tr '[:upper:]' '[:lower:]') + if [ "$record_name_lower" = "$name_lower" ]; then + echo "$record" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d'"' -f4 + break + fi + done +} + +_exoscale_sign() { + k=$1 + shift + hex_key=$(printf %b "$k" | _hex_dump | tr -d ' ') + printf %s "$@" | _hmac sha256 "$hex_key" +} + _exoscale_rest() { method=$1 - path="$2" - data="$3" - token="$4" - request_url="$EXOSCALE_API/$path" - _debug "$path" + path=$2 + data=$3 - export _H1="Accept: application/json" + url="${EXOSCALE_API}${path}" + expiration=$(_math "$(date +%s)" + 300) # 5m from now - if [ "$token" ]; then - export _H2="X-DNS-Domain-Token: $token" - else - export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY" - fi + # Build the message with the actual body or empty line + message=$(printf "%s %s\n%s\n\n\n%s" "$method" "/v2$path" "$data" "$expiration") + signature=$(_exoscale_sign "$EXOSCALE_SECRET_KEY" "$message" | _base64) + auth="EXO2-HMAC-SHA256 credential=${EXOSCALE_API_KEY},expires=${expiration},signature=${signature}" + + _debug "API request: $method $url" + _debug "Signed message: [$message]" + _debug "Authorization header: [$auth]" + + export _H1="Accept: application/json" + export _H2="Authorization: ${auth}" if [ "$data" ] || [ "$method" = "DELETE" ]; then export _H3="Content-Type: application/json" _debug data "$data" - response="$(_post "$data" "$request_url" "" "$method")" + response="$(_post "$data" "$url" "" "$method")" else - response="$(_get "$request_url" "" "" "$method")" + response="$(_get "$url" "" "" "$method")" fi - if [ "$?" != "0" ]; then - _err "error $request_url" + # shellcheck disable=SC2181 + if [ "$?" -ne 0 ]; then + _err "error $url" return 1 fi _debug2 response "$response" + echo "$response" return 0 } From b4042d5ccb5082643f4e4eb3ac63bf9437205f4a Mon Sep 17 00:00:00 2001 From: as-kholin Date: Tue, 16 Dec 2025 14:51:42 -0500 Subject: [PATCH 08/27] Updated checks for empty parameters to actually trigger, and added a validation check against the omg.lol API to confirm address and apikey are good before proceeding --- dnsapi/dns_omglol.sh | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index df080bcf..ddde8f2d 100644 --- a/dnsapi/dns_omglol.sh +++ b/dnsapi/dns_omglol.sh @@ -35,7 +35,7 @@ dns_omglol_add() { _debug "omg.lol Address" "$OMG_Address" omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain" - if [ ! $? ]; then + if [ 1 = $? ]; then return 1 fi @@ -67,7 +67,7 @@ dns_omglol_rm() { _debug "omg.lol Address" "$OMG_Address" omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain" - if [ ! $? ]; then + if [ 1 = $? ]; then return 1 fi @@ -100,18 +100,48 @@ omg_validate() { fi _endswith "$fulldomain" "omg.lol" - if [ ! $? ]; then + if [ 1 = $? ]; then _err "Domain name requested is not under omg.lol" return 1 fi _endswith "$fulldomain" "$omg_address.omg.lol" - if [ ! $? ]; then + if [ 1 = $? ]; then _err "Domain name is not a subdomain of provided omg.lol address $omg_address" return 1 fi - _debug "Required environment parameters are all present" + omg_testconnect "$omg_apikey" "$omg_address" + if [ 1 = $? ]; then + _err "Authentication to omg.lol for address $omg_address using provided API key failed" + return 1 + fi + + _debug "Required environment parameters are all present and validated" +} + +# Validate that the address and API key are both correct and associated to each other +omg_testconnect() { + omg_apikey=$1 + omg_address=$2 + + _debug2 "Function" "omg_testconnect" + _secure_debug2 "omg.lol API key" "$omg_apikey" + _debug2 "omg.lol Address" "$omg_address" + + export _H1=$(_createAuthHeader "$omg_apikey") + endpoint="https://api.omg.lol/address/$omg_address/info" + _debug2 "Endpoint for validation" "$endpoint" + + response=$(_get "$endpoint" "" 30) + + _jsonResponseCheck "$response" "status_code" 200 + if [ 1 = $? ]; then + _debug2 "Failed to query omg.lol for $omg_address with provided API key" + _secure_debug2 "API Key" "omg_apikey" + _secure_debug3 "Raw response" "$response" + return 1 + fi } # Add (or modify) an entry for a new ACME query From 4a7e5d07209fa3d50ac9ecf3b7df2febef903084 Mon Sep 17 00:00:00 2001 From: as-kholin Date: Tue, 16 Dec 2025 15:00:05 -0500 Subject: [PATCH 09/27] Updating Auth header to satisfy shellcheck --- dnsapi/dns_omglol.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index ddde8f2d..652cfd15 100644 --- a/dnsapi/dns_omglol.sh +++ b/dnsapi/dns_omglol.sh @@ -129,7 +129,8 @@ omg_testconnect() { _secure_debug2 "omg.lol API key" "$omg_apikey" _debug2 "omg.lol Address" "$omg_address" - export _H1=$(_createAuthHeader "$omg_apikey") + authheader="$(_createAuthHeader "$omg_apikey")" + export _H1="$authheader" endpoint="https://api.omg.lol/address/$omg_address/info" _debug2 "Endpoint for validation" "$endpoint" From 85ff92170b1810fe7005c65f240c2b34f0cfe468 Mon Sep 17 00:00:00 2001 From: as-kholin Date: Wed, 17 Dec 2025 17:15:10 -0500 Subject: [PATCH 10/27] Updated comment to be more clear on variable vs. definition --- dnsapi/dns_omglol.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh index 652cfd15..fd38d046 100644 --- a/dnsapi/dns_omglol.sh +++ b/dnsapi/dns_omglol.sh @@ -4,8 +4,8 @@ dns_omglol_info='omg.lol Site: omg.lol Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_omglol Options: - OMG_ApiKey API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account - OMG_Address Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard + OMG_ApiKey - API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account + OMG_Address - Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard Issues: github.com/acmesh-official/acme.sh/issues/5299 Author: @Kholin ' From e3b1bccb6aa5e1cda2a1d65c5bae66cec54edb30 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Thu, 18 Dec 2025 16:00:55 +0000 Subject: [PATCH 11/27] Fixes to support INWX again Fixes #6688 --- dnsapi/dns_inwx.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 808fc3a9..89ab67ee 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -125,7 +125,7 @@ dns_inwx_rm() { if ! printf "%s" "$response" | grep "count" >/dev/null; then _info "Do not need to delete record" else - _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+') + _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+') _info "Deleting record" _inwx_delete_record "$_record_id" fi @@ -324,7 +324,7 @@ _inwx_delete_record() { id - %s + %s @@ -362,7 +362,7 @@ _inwx_update_record() { id - %s + %s From 987882ea37f28b581b44614d93b0a9ab2f9823e4 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 17:49:24 +0100 Subject: [PATCH 12/27] fix delete --- dnsapi/dns_inwx.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 89ab67ee..afe2c465 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -110,11 +110,17 @@ dns_inwx_rm() { %s + + content + + %s + + - ' "$_domain" "$_sub_domain") + ' "$_domain" "$_sub_domain" "$txtvalue") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" if ! _contains "$response" "Command completed successfully"; then From 1c65c04b54b56a24c6f45d122b594c126b6a45c2 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 17:51:13 +0100 Subject: [PATCH 13/27] update dns_inwx_info --- dnsapi/dns_inwx.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index afe2c465..d83e08d6 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx Options: INWX_User Username INWX_Password Password + INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional) ' # Dependencies: From 27ebf09c5c3834011e29d5adf0f12d0f43540107 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:02:18 +0100 Subject: [PATCH 14/27] Improve _htmlEscape function robustness by using printf instead of echo small commit to trigger github actions --- dnsapi/dns_inwx.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index d83e08d6..a7ce9769 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -172,10 +172,10 @@ _inwx_check_cookie() { _htmlEscape() { _s="$1" - _s=$(echo "$_s" | sed "s/&/&/g") - _s=$(echo "$_s" | sed "s//\>/g") - _s=$(echo "$_s" | sed 's/"/\"/g') + _s=$(printf '%s' "$_s" | sed "s/&/&/g") + _s=$(printf '%s' "$_s" | sed "s//\>/g") + _s=$(printf '%s' "$_s" | sed 's/"/\"/g') printf -- %s "$_s" } From a5ad15be0212cefc2ffa99bcd4c611d15576d4ee Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:07:46 +0100 Subject: [PATCH 15/27] Add oathtool to Docker job for 2FA support --- .github/workflows/DNS.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index ccce2ff6..8f7ebd56 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -67,6 +67,8 @@ jobs: TokenName5: ${{ secrets.TokenName5}} steps: - uses: actions/checkout@v4 + - name: Install oathtool + run: sudo apt-get update && sudo apt-get install -y oathtool - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file @@ -116,7 +118,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install tools - run: brew install socat + run: brew install socat oath-toolkit - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest @@ -173,7 +175,7 @@ jobs: shell: cmd - name: Install cygwin additional packages run: | - C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git + C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,oath-toolkit shell: cmd - name: Set ENV shell: cmd @@ -230,7 +232,7 @@ jobs: - uses: vmactions/freebsd-vm@v1 with: 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 + prepare: pkg install -y socat curl oath-toolkit usesh: true copyback: false run: | @@ -281,7 +283,7 @@ jobs: - uses: vmactions/openbsd-vm@v1 with: 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_add socat curl libiconv + prepare: pkg_add socat curl libiconv oath-toolkit usesh: true copyback: false run: | @@ -333,7 +335,7 @@ jobs: with: 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: | - /usr/sbin/pkg_add curl socat + /usr/sbin/pkg_add curl socat oath-toolkit usesh: true copyback: false run: | @@ -385,7 +387,7 @@ jobs: with: 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 curl socat libnghttp2 + pkg install -y curl socat libnghttp2 oath-toolkit usesh: true copyback: false run: | @@ -443,7 +445,7 @@ jobs: copyback: false prepare: | pkgutil -U - pkgutil -y -i socat + pkgutil -y -i socat oathtool run: | pkg set-mediator -v -I default@1.1 openssl export PATH=/usr/gnu/bin:$PATH @@ -494,7 +496,7 @@ jobs: with: 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}}' copyback: false - prepare: pkg install socat + prepare: pkg install socat oath-toolkit run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" From 0e5aab346f7958fdbe35ffa6a133614a59826a3f Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:17:05 +0100 Subject: [PATCH 16/27] Add oathtool to acmetest Docker image for 2FA support --- .github/workflows/DNS.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 8f7ebd56..781c13fb 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -67,10 +67,15 @@ jobs: TokenName5: ${{ secrets.TokenName5}} steps: - uses: actions/checkout@v4 - - name: Install oathtool - run: sudo apt-get update && sudo apt-get install -y oathtool - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ + - name: Ensure oathtool in test container + run: | + cd ../acmetest + # Add oathtool installation to acmetest Dockerfile if it exists + if [ -f Dockerfile ]; then + sed -i 's/tzdata/tzdata oath-toolkit-oathtool/g' Dockerfile + fi - name: Set env file run: | cd ../acmetest From e92d0a74926855b607fdfb948eafe6bf44e0fdfa Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:20:56 +0100 Subject: [PATCH 17/27] Fix: Install oathtool at runtime in test container --- .github/workflows/DNS.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 781c13fb..e6319c09 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -69,13 +69,11 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Ensure oathtool in test container + - name: Install oathtool in test container run: | cd ../acmetest - # Add oathtool installation to acmetest Dockerfile if it exists - if [ -f Dockerfile ]; then - sed -i 's/tzdata/tzdata oath-toolkit-oathtool/g' Dockerfile - fi + # Modify letest.sh to install oathtool at runtime + sed -i '/TEST_LOCAL skip setup/a apt-get update -qq && apt-get install -y -qq oathtool > /dev/null 2>&1 || true' letest.sh - name: Set env file run: | cd ../acmetest From b6523c230110f5477f3d4a45a020e4b99d230e69 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 18:42:28 +0100 Subject: [PATCH 18/27] Install oathtool in container via _setup function --- .github/workflows/DNS.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index e6319c09..64fa85d2 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -69,11 +69,11 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Install oathtool in test container + - name: Patch acmetest to install oathtool in container run: | cd ../acmetest - # Modify letest.sh to install oathtool at runtime - sed -i '/TEST_LOCAL skip setup/a apt-get update -qq && apt-get install -y -qq oathtool > /dev/null 2>&1 || true' letest.sh + # Add oathtool installation before the test runs inside container + sed -i '/^_setup() {$/a \ if command -v apt-get >/dev/null 2>&1; then\n apt-get update -qq && apt-get install -y -qq oathtool >/dev/null 2>&1 || true\n fi' letest.sh - name: Set env file run: | cd ../acmetest From cba0ff832116b2fc322c3dd51401322789a62d4a Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 19:09:23 +0100 Subject: [PATCH 19/27] Add oathtool to ubuntu package list in plat.conf --- .github/workflows/DNS.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 64fa85d2..84cebf94 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -69,11 +69,11 @@ jobs: - uses: actions/checkout@v4 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Patch acmetest to install oathtool in container + - name: Add oathtool to ubuntu package list run: | cd ../acmetest - # Add oathtool installation before the test runs inside container - sed -i '/^_setup() {$/a \ if command -v apt-get >/dev/null 2>&1; then\n apt-get update -qq && apt-get install -y -qq oathtool >/dev/null 2>&1 || true\n fi' letest.sh + # Add oathtool to the ubuntu platform package list + sed -i 's/unzip,openssl,cron,socat,curl,idn,wget/unzip,openssl,cron,socat,curl,idn,wget,oathtool/' plat.conf - name: Set env file run: | cd ../acmetest From 65892453be0cff43cf1b6f996f5cbc2707d540d7 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Fri, 19 Dec 2025 19:23:13 +0100 Subject: [PATCH 20/27] take a fork of acmetest --- .github/workflows/DNS.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 84cebf94..5650c9e8 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -68,12 +68,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Clone acmetest - run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - - name: Add oathtool to ubuntu package list - run: | - cd ../acmetest - # Add oathtool to the ubuntu platform package list - sed -i 's/unzip,openssl,cron,socat,curl,idn,wget/unzip,openssl,cron,socat,curl,idn,wget,oathtool/' plat.conf + run: cd .. && git clone --depth=1 https://github.com/flybyray/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file run: | cd ../acmetest From 1b2630dc0d4030f4507bcfeba60bdc2e102c1410 Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Sat, 20 Dec 2025 13:26:38 +0100 Subject: [PATCH 21/27] stop using oathtool --- .github/workflows/DNS.yml | 2 +- dnsapi/dns_inwx.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 5650c9e8..39d2a121 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -68,7 +68,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Clone acmetest - run: cd .. && git clone --depth=1 https://github.com/flybyray/acmetest.git && cp -r acme.sh acmetest/ + run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file run: | cd ../acmetest diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index a7ce9769..08809bf0 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx Options: INWX_User Username INWX_Password Password - INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional) + INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional requires oathtool) ' # Dependencies: From 5fb42b7339b82d3397b2003dedd3302b439d745c Mon Sep 17 00:00:00 2001 From: Robert Rettig Date: Sat, 20 Dec 2025 14:53:01 +0100 Subject: [PATCH 22/27] remove prior additions which tried to use oathtool in tests --- .github/workflows/DNS.yml | 16 ++++++++-------- dnsapi/dns_inwx.sh | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 39d2a121..ccce2ff6 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -116,7 +116,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install tools - run: brew install socat oath-toolkit + run: brew install socat - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Run acmetest @@ -173,7 +173,7 @@ jobs: shell: cmd - name: Install cygwin additional packages run: | - C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git,oath-toolkit + C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git shell: cmd - name: Set ENV shell: cmd @@ -230,7 +230,7 @@ jobs: - uses: vmactions/freebsd-vm@v1 with: 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 oath-toolkit + prepare: pkg install -y socat curl usesh: true copyback: false run: | @@ -281,7 +281,7 @@ jobs: - uses: vmactions/openbsd-vm@v1 with: 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_add socat curl libiconv oath-toolkit + prepare: pkg_add socat curl libiconv usesh: true copyback: false run: | @@ -333,7 +333,7 @@ jobs: with: 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: | - /usr/sbin/pkg_add curl socat oath-toolkit + /usr/sbin/pkg_add curl socat usesh: true copyback: false run: | @@ -385,7 +385,7 @@ jobs: with: 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 curl socat libnghttp2 oath-toolkit + pkg install -y curl socat libnghttp2 usesh: true copyback: false run: | @@ -443,7 +443,7 @@ jobs: copyback: false prepare: | pkgutil -U - pkgutil -y -i socat oathtool + pkgutil -y -i socat run: | pkg set-mediator -v -I default@1.1 openssl export PATH=/usr/gnu/bin:$PATH @@ -494,7 +494,7 @@ jobs: with: 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}}' copyback: false - prepare: pkg install socat oath-toolkit + prepare: pkg install socat run: | if [ "${{ secrets.TokenName1}}" ] ; then export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}" diff --git a/dnsapi/dns_inwx.sh b/dnsapi/dns_inwx.sh index 08809bf0..2cf91404 100755 --- a/dnsapi/dns_inwx.sh +++ b/dnsapi/dns_inwx.sh @@ -172,10 +172,10 @@ _inwx_check_cookie() { _htmlEscape() { _s="$1" - _s=$(printf '%s' "$_s" | sed "s/&/&/g") - _s=$(printf '%s' "$_s" | sed "s//\>/g") - _s=$(printf '%s' "$_s" | sed 's/"/\"/g') + _s=$(echo "$_s" | sed "s/&/&/g") + _s=$(echo "$_s" | sed "s//\>/g") + _s=$(echo "$_s" | sed 's/"/\"/g') printf -- %s "$_s" } From f85de2b0d3560bab9fb553b53e9564edcad1b01d Mon Sep 17 00:00:00 2001 From: Erfan Gholizade Date: Wed, 3 Dec 2025 18:26:58 +0330 Subject: [PATCH 23/27] Added Sotoon dns api handle the case of metadata 404 in old api domains fix index of domain start fix shfmt Revert "handle the case of metadata 404 in old api domains" This reverts commit 9fe4616664b897c9891271006e7489b10bb818ca. fix 404 on dot ad hyphen fix shfmt --- dnsapi/dns_sotoon.sh | 319 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 dnsapi/dns_sotoon.sh diff --git a/dnsapi/dns_sotoon.sh b/dnsapi/dns_sotoon.sh new file mode 100644 index 00000000..4a0fc034 --- /dev/null +++ b/dnsapi/dns_sotoon.sh @@ -0,0 +1,319 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_sotoon_info='Sotoon.ir +Site: Sotoon.ir +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon +Options: + Sotoon_Token API Token + Sotoon_WorkspaceUUID Workspace UUID + Sotoon_WorkspaceName Workspace Name +Issues: github.com/acmesh-official/acme.sh/issues/6656 +Author: Erfan Gholizade +' + +SOTOON_API_URL="https://api.sotoon.ir/delivery/v2/global" + +######## Public functions ##################### + +#Adding the txt record for validation. +#Usage: dns_sotoon_add fulldomain TXT_record +#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_sotoon_add() { + fulldomain=$1 + txtvalue=$2 + _info_sotoon "Using Sotoon" + + Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" + Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" + Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" + + if [ -z "$Sotoon_Token" ]; then + _err_sotoon "You didn't specify \"Sotoon_Token\" token yet." + _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens" + return 1 + fi + if [ -z "$Sotoon_WorkspaceUUID" ]; then + _err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet." + _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" + return 1 + fi + if [ -z "$Sotoon_WorkspaceName" ]; then + _err_sotoon "You didn't specify \"Sotoon_WorkspaceName\" Workspace Name yet." + _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" + return 1 + fi + + #save the info to the account conf file. + _saveaccountconf_mutable Sotoon_Token "$Sotoon_Token" + _saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID" + _saveaccountconf_mutable Sotoon_WorkspaceName "$Sotoon_WorkspaceName" + + _debug_sotoon "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err_sotoon "invalid domain" + return 1 + fi + + _info_sotoon "Adding record" + + _debug_sotoon _domain_id "$_domain_id" + _debug_sotoon _sub_domain "$_sub_domain" + _debug_sotoon _domain "$_domain" + + # First, GET the current domain zone to check for existing TXT records + # This is needed for wildcard certs which require multiple TXT values + _info_sotoon "Checking for existing TXT records" + if ! _sotoon_rest GET "$_domain_id"; then + _err_sotoon "Failed to get domain zone" + return 1 + fi + + # Check if there are existing TXT records for this subdomain + _existing_txt="" + if _contains "$response" "\"$_sub_domain\""; then + _debug_sotoon "Found existing records for $_sub_domain" + # Extract existing TXT values from the response + # The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}] + _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") + _debug_sotoon "Existing TXT records: $_existing_txt" + fi + + # Build the new record entry + _new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}" + + # If there are existing records, append to them; otherwise create new array + if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then + # Check if this exact TXT value already exists (avoid duplicates) + if _contains "$_existing_txt" "\"$txtvalue\""; then + _info_sotoon "TXT record already exists, skipping" + return 0 + fi + # Remove the closing bracket and append new record + _combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]" + _debug_sotoon "Combined records: $_combined_records" + else + # No existing records, create new array + _combined_records="[$_new_record]" + fi + + # Prepare the DNS record data in Kubernetes CRD format + _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}" + + _debug_sotoon "DNS record payload: $_dns_record" + + # Use PATCH to update/add the record to the domain zone + _info_sotoon "Updating domain zone $_domain_id with TXT record" + if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then + if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then + _info_sotoon "Added, OK" + return 0 + else + _debug_sotoon "Response: $response" + _err_sotoon "Add txt record error." + return 1 + fi + fi + + _err_sotoon "Add txt record error." + return 1 +} + +#Remove the txt record after validation. +#Usage: dns_sotoon_rm fulldomain TXT_record +#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_sotoon_rm() { + fulldomain=$1 + txtvalue=$2 + _info_sotoon "Using Sotoon" + _debug_sotoon fulldomain "$fulldomain" + _debug_sotoon txtvalue "$txtvalue" + + Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" + Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" + Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" + + _debug_sotoon "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err_sotoon "invalid domain" + return 1 + fi + _debug_sotoon _domain_id "$_domain_id" + _debug_sotoon _sub_domain "$_sub_domain" + _debug_sotoon _domain "$_domain" + + _info_sotoon "Removing TXT record" + + # First, GET the current domain zone to check for existing TXT records + if ! _sotoon_rest GET "$_domain_id"; then + _err_sotoon "Failed to get domain zone" + return 1 + fi + + # Check if there are existing TXT records for this subdomain + _existing_txt="" + if _contains "$response" "\"$_sub_domain\""; then + _debug_sotoon "Found existing records for $_sub_domain" + _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") + _debug_sotoon "Existing TXT records: $_existing_txt" + fi + + # If no existing records, nothing to remove + if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then + _info_sotoon "No TXT records found, nothing to remove" + return 0 + fi + + # Remove the specific TXT value from the array + # This handles the case where there are multiple TXT values (wildcard certs) + _remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g') + _debug_sotoon "Remaining records after removal: $_remaining_records" + + # If no records remain, set to null to remove the subdomain entirely + if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then + _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}" + else + _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}" + fi + + _debug_sotoon "Remove record payload: $_dns_record" + + # Use PATCH to remove the record from the domain zone + if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then + _info_sotoon "Record removed, OK" + return 0 + else + _debug_sotoon "Response: $response" + _err_sotoon "Error removing record" + return 1 + fi +} + +#################### Private functions below ################################## + +_get_root() { + domain=$1 + i=1 + p=1 + + _debug_sotoon "Getting root domain for: $domain" + _debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID" + _debug_sotoon "Sotoon WorkspaceName: $Sotoon_WorkspaceName" + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug_sotoon "Checking domain part: $h" + + if [ -z "$h" ]; then + #not valid + _err_sotoon "Could not find valid domain" + return 1 + fi + + _debug_sotoon "Fetching domain zones from Sotoon API" + if ! _sotoon_rest GET ""; then + _err_sotoon "Failed to get domain zones from Sotoon API" + _err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID, and Sotoon_WorkspaceName" + return 1 + fi + + _debug2_sotoon "API Response: $response" + + # Check if the response contains our domain + # Sotoon API uses Kubernetes CRD format with spec.origin for domain matching + if _contains "$response" "\"origin\":\"$h\""; then + _debug_sotoon "Found domain by origin: $h" + + # In Kubernetes CRD format, the metadata.name is the resource identifier + # The name can be either: + # 1. Same as origin + # 2. Origin with dots replaced by hyphens + # We check both patterns in the response to determine which one exists + + # Convert origin to hyphenated version for checking + _h_hyphenated=$(echo "$h" | tr '.' '-') + + # Check if the hyphenated name exists in the response + if _contains "$response" "\"name\":\"$_h_hyphenated\""; then + _domain_id="$_h_hyphenated" + _debug_sotoon "Found domain ID (hyphenated): $_domain_id" + # Check if the origin itself is used as name + elif _contains "$response" "\"name\":\"$h\""; then + _domain_id="$h" + _debug_sotoon "Found domain ID (same as origin): $_domain_id" + else + # Fallback: use the hyphenated version (more common) + _domain_id="$_h_hyphenated" + _debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id" + fi + + if [ -n "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + _debug_sotoon "Domain ID (metadata.name): $_domain_id" + _debug_sotoon "Sub domain: $_sub_domain" + _debug_sotoon "Domain (origin): $_domain" + return 0 + fi + _err_sotoon "Found domain $h but could not extract domain ID" + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_sotoon_rest() { + mtd="$1" + resource_id="$2" + data="$3" + + token_trimmed=$(echo "$Sotoon_Token" | tr -d '"') + + # Construct the API endpoint + _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/namespaces/$Sotoon_WorkspaceName/domainzones" + + if [ -n "$resource_id" ]; then + _api_path="$_api_path/$resource_id" + fi + + _debug_sotoon "API Path: $_api_path" + _debug_sotoon "Method: $mtd" + + # Set authorization header - Sotoon API uses Bearer token + export _H1="Authorization: Bearer $token_trimmed" + + if [ "$mtd" = "GET" ]; then + # GET request + _debug_sotoon "GET" "$_api_path" + response="$(_get "$_api_path")" + elif [ "$mtd" = "PATCH" ]; then + # PATCH Request + export _H2="Content-Type: application/merge-patch+json" + _debug_sotoon data "$data" + response="$(_post "$data" "$_api_path" "" "$mtd")" + else + _err_sotoon "Unknown method: $mtd" + return 1 + fi + + _debug2_sotoon response "$response" + return 0 +} + +#Wrappers for logging +_info_sotoon() { + _info "[Sotoon]" "$@" +} + +_err_sotoon() { + _err "[Sotoon]" "$@" +} + +_debug_sotoon() { + _debug "[Sotoon]" "$@" +} + +_debug2_sotoon() { + _debug2 "[Sotoon]" "$@" +} From 03d8d3bc1b20fe356feb823069424868e149898e Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:29:50 +0100 Subject: [PATCH 24/27] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index 347f34d1..af722297 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -171,7 +171,7 @@ _hostup_detect_zone() { return 1 fi - _domain_candidate="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]')" + _domain_candidate="$(_lower_case "$fulldomain")" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -361,7 +361,7 @@ _hostup_json_extract() { input="${2:-$line}" # First try to extract quoted values (strings) - quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | head -n1)" + quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)" if [ -n "$quoted_match" ]; then printf "%s" "$quoted_match" | cut -d : -f2- | @@ -372,7 +372,7 @@ _hostup_json_extract() { fi # Fallback for unquoted values (e.g., numeric IDs) - unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | head -n1)" + unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)" if [ -n "$unquoted_match" ]; then printf "%s" "$unquoted_match" | cut -d : -f2- | @@ -392,7 +392,7 @@ _hostup_record_key() { zone_id="$1" domain="$2" safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" - safe_domain="$(printf "%s" "$domain" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/_/g')" + safe_domain="$(printf "%s" "$(_lower_case "$domain")" | sed 's/[^a-z0-9]/_/g')" printf "%s_%s" "$safe_zone" "$safe_domain" } @@ -425,7 +425,7 @@ _hostup_extract_record_id() { return 0 fi - printf "%s" "$1" | _egrep_o '"id":[0-9]+' | head -n1 | cut -d: -f2 + printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2 } _hostup_delete_record_by_id() { From e321b3c75c0eabe6341f9d7b5643a9e9ef4019a6 Mon Sep 17 00:00:00 2001 From: hostup <52465293+hostup@users.noreply.github.com> Date: Mon, 22 Dec 2025 04:53:21 +0100 Subject: [PATCH 25/27] Update dns_hostup.sh --- dnsapi/dns_hostup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh index af722297..b3211069 100644 --- a/dnsapi/dns_hostup.sh +++ b/dnsapi/dns_hostup.sh @@ -3,7 +3,7 @@ dns_hostup_info='HostUp DNS Site: hostup.se -Docs: https://hostup.se/en/support/api-autentisering/ +Docs: https://developer.hostup.se/ Options: HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes. HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api). @@ -171,7 +171,7 @@ _hostup_detect_zone() { return 1 fi - _domain_candidate="$(_lower_case "$fulldomain")" + _domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)" _debug "hostup_initial_candidate" "$_domain_candidate" while [ -n "$_domain_candidate" ]; do @@ -392,7 +392,7 @@ _hostup_record_key() { zone_id="$1" domain="$2" safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" - safe_domain="$(printf "%s" "$(_lower_case "$domain")" | sed 's/[^a-z0-9]/_/g')" + safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')" printf "%s_%s" "$safe_zone" "$safe_domain" } From 0eb40c6ce6ce5c0bf17f39893e1b6ea4056ae362 Mon Sep 17 00:00:00 2001 From: DreamSlave <50408721+mq00fc@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:00:22 +0800 Subject: [PATCH 26/27] Update timestamp variable in ali_cdn.sh --- deploy/ali_cdn.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh index 70a2e532..3c28674e 100644 --- a/deploy/ali_cdn.sh +++ b/deploy/ali_cdn.sh @@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_timestamp) + query=$query'&Timestamp='$(_ali_timestamp) query=$query'&Version=2018-05-10' } From fd6a14de8a69193a5367dfaae71857d804a83528 Mon Sep 17 00:00:00 2001 From: DreamSlave <50408721+mq00fc@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:00:31 +0800 Subject: [PATCH 27/27] Update timestamp function in ali_dcdn.sh --- deploy/ali_dcdn.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh index 14ac500a..27d3a726 100644 --- a/deploy/ali_dcdn.sh +++ b/deploy/ali_dcdn.sh @@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_query() { query=$query'&SignatureMethod=HMAC-SHA1' query=$query"&SignatureNonce=$(_ali_nonce)" query=$query'&SignatureVersion=1.0' - query=$query'&Timestamp='$(_timestamp) + query=$query'&Timestamp='$(_ali_timestamp) query=$query'&Version=2018-01-15' }