From e4c0a4f5f59140a926ceb2f2936075f4aa0f1f8d Mon Sep 17 00:00:00 2001 From: Kenny Kropp Date: Tue, 15 Sep 2026 14:56:59 +0200 Subject: [PATCH 1/4] add bergdns.at support --- dnsapi/dns_bergdns.sh | 252 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 dnsapi/dns_bergdns.sh diff --git a/dnsapi/dns_bergdns.sh b/dnsapi/dns_bergdns.sh new file mode 100644 index 00000000..7c3e2864 --- /dev/null +++ b/dnsapi/dns_bergdns.sh @@ -0,0 +1,252 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_bergdns_info='bergdns.at +Site: bergdns.at +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bergdns +Options: + BERGDNS_API_KEY API key, as issued in the account UI. Needs read (to find the zone and the record) and write over the challenge names. + BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1". + BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60". + BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "120". "0" does not wait. +Author: Kenny Kropp +' + +_BERGDNS_DEFAULT_URL='https://bergdns.at/v1' +_BERGDNS_DEFAULT_TTL='60' +_BERGDNS_DEFAULT_WAIT='120' + +######## Public functions #################################################### + +# Usage: dns_bergdns_add _acme-challenge.www.example.com "token" +# The value is added to the RRset, so a domain and its wildcard can be +# validated at the same time. +dns_bergdns_add() { + fulldomain=$(echo "$1" | _lower_case) + txtvalue=$2 + + _bergdns_init || return 1 + _bergdns_find_zone "$fulldomain" || return 1 + _bergdns_find_rrset "$fulldomain" || return 1 + + _info "Adding TXT $fulldomain in zone $_bergdns_zone_name" + if [ -z "$_bergdns_rrset_id" ]; then + if _bergdns_rest POST "zones/$_bergdns_zone_id/rrsets" \ + "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":$BERGDNS_TTL,\"records\":[\"\\\"$txtvalue\\\"\"]}"; then + _bergdns_rrset_id=$(echo "$response" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _bergdns_wait "$fulldomain" + return 0 + fi + if [ "$_bergdns_code" != "rrset_exists" ]; then + _err "bergdns: could not add the challenge record: $_bergdns_error" + return 1 + fi + # another run created it since the lookup above + _bergdns_find_rrset "$fulldomain" || return 1 + if [ -z "$_bergdns_rrset_id" ]; then + _err "bergdns: could not find the challenge record at $fulldomain" + return 1 + fi + fi + + if ! _bergdns_rest POST \ + "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \ + "{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then + _err "bergdns: could not add the challenge record: $_bergdns_error" + return 1 + fi + + _bergdns_wait "$fulldomain" +} + +# Usage: dns_bergdns_rm _acme-challenge.www.example.com "token" +# Only this value is removed. Removing the last value deletes the RRset, and +# removing a value that does not exist is not an error. +dns_bergdns_rm() { + fulldomain=$(echo "$1" | _lower_case) + txtvalue=$2 + + _bergdns_init || return 1 + _bergdns_find_zone "$fulldomain" || return 1 + _bergdns_find_rrset "$fulldomain" || return 1 + + if [ -z "$_bergdns_rrset_id" ]; then + _info "bergdns: no TXT records at $fulldomain, nothing to remove" + return 0 + fi + + _info "Removing TXT $fulldomain from zone $_bergdns_zone_name" + if ! _bergdns_rest DELETE \ + "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \ + "{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then + if [ "$_bergdns_code" = "rrset_not_found" ]; then + return 0 + fi + _err "bergdns: could not remove the challenge record: $_bergdns_error" + return 1 + fi + return 0 +} + +######## Private functions ################################################### + +_bergdns_init() { + BERGDNS_API_KEY="${BERGDNS_API_KEY:-$(_readaccountconf_mutable BERGDNS_API_KEY)}" + BERGDNS_API_URL="${BERGDNS_API_URL:-$(_readaccountconf_mutable BERGDNS_API_URL)}" + BERGDNS_TTL="${BERGDNS_TTL:-$(_readaccountconf_mutable BERGDNS_TTL)}" + BERGDNS_PROPAGATION_TIMEOUT="${BERGDNS_PROPAGATION_TIMEOUT:-$(_readaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT)}" + + if [ -z "$BERGDNS_API_KEY" ]; then + BERGDNS_API_KEY="" + _clearaccountconf_mutable BERGDNS_API_KEY + _err "You have not set BERGDNS_API_KEY. Create a key in the bergdns UI and export it:" + _err " export BERGDNS_API_KEY=\"bgd_...\"" + return 1 + fi + + [ -n "$BERGDNS_API_URL" ] || BERGDNS_API_URL="$_BERGDNS_DEFAULT_URL" + [ -n "$BERGDNS_TTL" ] || BERGDNS_TTL="$_BERGDNS_DEFAULT_TTL" + [ -n "$BERGDNS_PROPAGATION_TIMEOUT" ] || BERGDNS_PROPAGATION_TIMEOUT="$_BERGDNS_DEFAULT_WAIT" + + # strip trailing slashes + BERGDNS_API_URL=$(echo "$BERGDNS_API_URL" | sed 's#/*$##') + + _saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY" + _saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL" + _saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL" + _saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT" + return 0 +} + +# Usage: _bergdns_find_zone _acme-challenge.www.example.com +# Sets _bergdns_zone_id and _bergdns_zone_name. +# Zones are addressed by an id, not by name, so the zone list is fetched once +# and the longest matching zone name wins. +_bergdns_find_zone() { + _fqdn=$1 + _bergdns_zone_id="" + _bergdns_zone_name="" + + if ! _bergdns_rest GET "zones"; then + _err "bergdns: could not list zones: $_bergdns_error" + return 1 + fi + + # one zone object per line, so id and name stay together + _zone_lines=$(echo "$response" | tr '{' '\n' | grep '"name":"') + + _candidate="$_fqdn" + while [ -n "$_candidate" ]; do + _line=$(echo "$_zone_lines" | grep -F "\"name\":\"$_candidate\"," | _head_n 1) + if [ -n "$_line" ]; then + _bergdns_zone_id=$(echo "$_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _bergdns_zone_name="$_candidate" + _debug _bergdns_zone_id "$_bergdns_zone_id" + _debug _bergdns_zone_name "$_bergdns_zone_name" + [ -n "$_bergdns_zone_id" ] && return 0 + break + fi + case "$_candidate" in + *.*) _candidate=${_candidate#*.} ;; + *) _candidate="" ;; + esac + done + + _err "bergdns: no zone in this account holds $_fqdn." + _err "bergdns: the key must be able to read the zone as well as write the record." + return 1 +} + +# Usage: _bergdns_find_rrset _acme-challenge.www.example.com +# Sets _bergdns_rrset_id to the id of the TXT RRset at that name, or to an +# empty string if there is none. Records are addressed by id, so the zone's +# RRsets are listed to find it. +_bergdns_find_rrset() { + _fqdn=$1 + _bergdns_rrset_id="" + + if ! _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets"; then + _err "bergdns: could not list the records of $_bergdns_zone_name: $_bergdns_error" + return 1 + fi + + _line=$(echo "$response" | tr '{' '\n' | grep -F "\"name\":\"$_fqdn\",\"type\":\"TXT\"," | _head_n 1) + if [ -n "$_line" ]; then + _bergdns_rrset_id=$(echo "$_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + fi + _debug _bergdns_rrset_id "$_bergdns_rrset_id" + return 0 +} + +# Usage: _bergdns_wait _acme-challenge.www.example.com +# Polls the propagation endpoint until all bergdns nameservers serve the +# record. A timeout is logged but does not fail the issuance. +_bergdns_wait() { + _fqdn=$1 + if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "0" ] || [ -z "$_bergdns_rrset_id" ]; then + return 0 + fi + + _waited=0 + while [ "$_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do + if _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/propagation"; then + case "$response" in + *'"propagated":true'*) + _info "bergdns: $_fqdn is served by every secondary after ${_waited}s" + return 0 + ;; + esac + elif [ "$_bergdns_code" = "propagation_unavailable" ]; then + # propagation checks are not configured on this server + _info "bergdns: this deployment does not offer propagation checks; not waiting" + return 0 + fi + _sleep 5 + _waited=$((_waited + 5)) + done + + _info "bergdns: $_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway" + return 0 +} + +# Usage: _bergdns_rest method endpoint [body] +# Sets response. On failure also sets _bergdns_error and _bergdns_code. +_bergdns_rest() { + _method=$1 + _endpoint=$2 + _body=$3 + _bergdns_error="" + _bergdns_code="" + + export _H1="Authorization: Bearer $BERGDNS_API_KEY" + export _H2="Accept: application/json" + + _url="$BERGDNS_API_URL/$_endpoint" + _debug _url "$_url" + + if [ "$_method" = "GET" ]; then + response="$(_get "$_url")" + else + _debug2 _body "$_body" + response="$(_post "$_body" "$_url" "" "$_method" "application/json")" + fi + _ret="$?" + _debug2 response "$response" + + if [ "$_ret" != "0" ]; then + _bergdns_error="the request to $_url could not be made" + return 1 + fi + + # Errors are returned as RFC 9457 problem+json, which always has "status" and + # "code". Successful responses have neither, so the body is checked instead + # of the HTTP status code. + case "$response" in + *'"status":'*'"code":"'* | *'"code":"'*'"status":'*) + _bergdns_error=$(echo "$response" | sed -n 's/.*"detail":"\([^"]*\)".*/\1/p') + [ -n "$_bergdns_error" ] || _bergdns_error="$response" + _bergdns_code=$(echo "$response" | sed -n 's/.*"code":"\([^"]*\)".*/\1/p') + return 1 + ;; + esac + return 0 +} From 0ee2720d731b341bde1428a6008b694a89c1c26d Mon Sep 17 00:00:00 2001 From: Kenny Kropp Date: Wed, 16 Sep 2026 08:30:13 +0200 Subject: [PATCH 2/4] add bergdns.at provider info block values --- dnsapi/dns_bergdns.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dnsapi/dns_bergdns.sh b/dnsapi/dns_bergdns.sh index 7c3e2864..cfb9b519 100644 --- a/dnsapi/dns_bergdns.sh +++ b/dnsapi/dns_bergdns.sh @@ -8,6 +8,7 @@ Options: BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1". BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60". BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "120". "0" does not wait. +Issues: github.com/acmesh-official/acme.sh/issues/7261 Author: Kenny Kropp ' From 84d6b5841e431477bcb3b012269e0facd29f0ddd Mon Sep 17 00:00:00 2001 From: Kenny Kropp Date: Sat, 19 Sep 2026 11:29:08 +0200 Subject: [PATCH 3/4] fix pr comment issues --- dnsapi/dns_bergdns.sh | 187 ++++++++++++++++++++++++++++++------------ 1 file changed, 134 insertions(+), 53 deletions(-) diff --git a/dnsapi/dns_bergdns.sh b/dnsapi/dns_bergdns.sh index cfb9b519..8e3caac1 100644 --- a/dnsapi/dns_bergdns.sh +++ b/dnsapi/dns_bergdns.sh @@ -7,14 +7,14 @@ Options: BERGDNS_API_KEY API key, as issued in the account UI. Needs read (to find the zone and the record) and write over the challenge names. BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1". BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60". - BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "120". "0" does not wait. + BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "60". "0" does not wait. Issues: github.com/acmesh-official/acme.sh/issues/7261 Author: Kenny Kropp ' _BERGDNS_DEFAULT_URL='https://bergdns.at/v1' _BERGDNS_DEFAULT_TTL='60' -_BERGDNS_DEFAULT_WAIT='120' +_BERGDNS_DEFAULT_WAIT='60' ######## Public functions #################################################### @@ -34,6 +34,7 @@ dns_bergdns_add() { if _bergdns_rest POST "zones/$_bergdns_zone_id/rrsets" \ "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":$BERGDNS_TTL,\"records\":[\"\\\"$txtvalue\\\"\"]}"; then _bergdns_rrset_id=$(echo "$response" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _debug _bergdns_rrset_id "$_bergdns_rrset_id" _bergdns_wait "$fulldomain" return 0 fi @@ -79,7 +80,11 @@ dns_bergdns_rm() { if ! _bergdns_rest DELETE \ "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \ "{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then - if [ "$_bergdns_code" = "rrset_not_found" ]; then + # Any flavour of not-found is a cleanup that has already happened: the + # RRset was removed by a previous run, or by the other half of a + # domain-and-wildcard pair taking the last value with it. + if [ "$_bergdns_status" = "404" ]; then + _info "bergdns: $fulldomain holds no such record any more, nothing to remove" return 0 fi _err "bergdns: could not remove the challenge record: $_bergdns_error" @@ -111,10 +116,30 @@ _bergdns_init() { # strip trailing slashes BERGDNS_API_URL=$(echo "$BERGDNS_API_URL" | sed 's#/*$##') + # The TTL is interpolated into the request body and the timeout is counted + # down in arithmetic, so a stray value from the environment or from an old + # account.conf has to be caught here rather than become malformed JSON and + # an opaque 400. + case "$BERGDNS_TTL" in + *[!0-9]* | '') + _err "bergdns: BERGDNS_TTL must be a number of seconds, not \"$BERGDNS_TTL\"." + return 1 + ;; + esac + case "$BERGDNS_PROPAGATION_TIMEOUT" in + *[!0-9]* | '') + _err "bergdns: BERGDNS_PROPAGATION_TIMEOUT must be a number of seconds, not \"$BERGDNS_PROPAGATION_TIMEOUT\"." + return 1 + ;; + esac + _saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY" - _saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL" - _saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL" - _saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT" + # Only what the user actually chose is written back. Persisting a default + # would pin the install to today's value, and a later change to the shipped + # one -- a move of the API base above all -- would never reach it. + [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ] || _saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL" + [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ] || _saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL" + [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ] || _saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT" return 0 } @@ -123,7 +148,7 @@ _bergdns_init() { # Zones are addressed by an id, not by name, so the zone list is fetched once # and the longest matching zone name wins. _bergdns_find_zone() { - _fqdn=$1 + _bergdns_fqdn=$1 _bergdns_zone_id="" _bergdns_zone_name="" @@ -133,26 +158,26 @@ _bergdns_find_zone() { fi # one zone object per line, so id and name stay together - _zone_lines=$(echo "$response" | tr '{' '\n' | grep '"name":"') + _bergdns_zone_lines=$(echo "$response" | tr '{' '\n') - _candidate="$_fqdn" - while [ -n "$_candidate" ]; do - _line=$(echo "$_zone_lines" | grep -F "\"name\":\"$_candidate\"," | _head_n 1) - if [ -n "$_line" ]; then - _bergdns_zone_id=$(echo "$_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) - _bergdns_zone_name="$_candidate" + _bergdns_cand="$_bergdns_fqdn" + while [ -n "$_bergdns_cand" ]; do + _bergdns_line=$(echo "$_bergdns_zone_lines" | _bergdns_select "$_bergdns_cand" | _head_n 1) + if [ -n "$_bergdns_line" ]; then + _bergdns_zone_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _bergdns_zone_name="$_bergdns_cand" _debug _bergdns_zone_id "$_bergdns_zone_id" _debug _bergdns_zone_name "$_bergdns_zone_name" [ -n "$_bergdns_zone_id" ] && return 0 break fi - case "$_candidate" in - *.*) _candidate=${_candidate#*.} ;; - *) _candidate="" ;; + case "$_bergdns_cand" in + *.*) _bergdns_cand=${_bergdns_cand#*.} ;; + *) _bergdns_cand="" ;; esac done - _err "bergdns: no zone in this account holds $_fqdn." + _err "bergdns: no zone in this account holds $_bergdns_fqdn." _err "bergdns: the key must be able to read the zone as well as write the record." return 1 } @@ -162,7 +187,7 @@ _bergdns_find_zone() { # empty string if there is none. Records are addressed by id, so the zone's # RRsets are listed to find it. _bergdns_find_rrset() { - _fqdn=$1 + _bergdns_fqdn=$1 _bergdns_rrset_id="" if ! _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets"; then @@ -170,29 +195,62 @@ _bergdns_find_rrset() { return 1 fi - _line=$(echo "$response" | tr '{' '\n' | grep -F "\"name\":\"$_fqdn\",\"type\":\"TXT\"," | _head_n 1) - if [ -n "$_line" ]; then - _bergdns_rrset_id=$(echo "$_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _bergdns_line=$(echo "$response" | tr '{' '\n' | _bergdns_select "$_bergdns_fqdn" TXT | _head_n 1) + if [ -n "$_bergdns_line" ]; then + _bergdns_rrset_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) fi _debug _bergdns_rrset_id "$_bergdns_rrset_id" return 0 } +# Usage: ... | _bergdns_select name [type] +# Reads one JSON object per line and prints those whose "name" is name and, +# when a type is given, whose "type" is that type. The two fields are matched +# one at a time, so neither the order the server writes its keys in nor +# anything sitting between them changes the answer. +# +# The comparison is a shell case, which is literal by construction: grep -F +# does not exist on Solaris, and _contains and _startswith would read the name +# as a regular expression. Each pattern anchors on the start of the object or +# on the comma before the key, so a key that merely ends in "name" cannot +# match. +_bergdns_select() { + _bergdns_sel_name=$1 + _bergdns_sel_type=$2 + while IFS= read -r _bergdns_sel_line || [ -n "$_bergdns_sel_line" ]; do + case "$_bergdns_sel_line" in + '"name":"'"$_bergdns_sel_name"'"'* | *',"name":"'"$_bergdns_sel_name"'"'*) ;; + *) continue ;; + esac + if [ -n "$_bergdns_sel_type" ]; then + case "$_bergdns_sel_line" in + '"type":"'"$_bergdns_sel_type"'"'* | *',"type":"'"$_bergdns_sel_type"'"'*) ;; + *) continue ;; + esac + fi + printf '%s\n' "$_bergdns_sel_line" + done +} + # Usage: _bergdns_wait _acme-challenge.www.example.com # Polls the propagation endpoint until all bergdns nameservers serve the # record. A timeout is logged but does not fail the issuance. +# +# This covers the zone transfer from the primary to the secondaries, which +# takes seconds; the resolver side is acme.sh's own _check_dns_entries, which +# runs after every record has been added and has a timeout of its own. _bergdns_wait() { - _fqdn=$1 + _bergdns_fqdn=$1 if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "0" ] || [ -z "$_bergdns_rrset_id" ]; then return 0 fi - _waited=0 - while [ "$_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do + _bergdns_waited=0 + while [ "$_bergdns_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do if _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/propagation"; then case "$response" in *'"propagated":true'*) - _info "bergdns: $_fqdn is served by every secondary after ${_waited}s" + _info "bergdns: $_bergdns_fqdn is served by every secondary after ${_bergdns_waited}s" return 0 ;; esac @@ -200,54 +258,77 @@ _bergdns_wait() { # propagation checks are not configured on this server _info "bergdns: this deployment does not offer propagation checks; not waiting" return 0 + else + case "$_bergdns_status" in + 429) ;; # rate limited, worth another go + 4*) + # The check is refused rather than pending, and waiting will not + # change that. _check_dns_entries still has to pass, so this is not + # the place to fail the issuance. + _info "bergdns: the propagation check is unavailable ($_bergdns_error); not waiting" + return 0 + ;; + esac fi _sleep 5 - _waited=$((_waited + 5)) + _bergdns_waited=$((_bergdns_waited + 5)) done - _info "bergdns: $_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway" + _info "bergdns: $_bergdns_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway" return 0 } # Usage: _bergdns_rest method endpoint [body] -# Sets response. On failure also sets _bergdns_error and _bergdns_code. +# Sets response and _bergdns_status. On failure also sets _bergdns_error and, +# where the API itself answered, _bergdns_code. _bergdns_rest() { - _method=$1 - _endpoint=$2 - _body=$3 + _bergdns_method=$1 + _bergdns_endpoint=$2 + _bergdns_body=$3 _bergdns_error="" _bergdns_code="" + _bergdns_status="" export _H1="Authorization: Bearer $BERGDNS_API_KEY" export _H2="Accept: application/json" - _url="$BERGDNS_API_URL/$_endpoint" - _debug _url "$_url" + _bergdns_url="$BERGDNS_API_URL/$_bergdns_endpoint" + _debug _bergdns_url "$_bergdns_url" - if [ "$_method" = "GET" ]; then - response="$(_get "$_url")" - else - _debug2 _body "$_body" - response="$(_post "$_body" "$_url" "" "$_method" "application/json")" + # drop the headers of the previous request, so that a request which never + # reaches the server cannot be read as carrying its status + if [ -f "$HTTP_HEADER" ]; then + : >"$HTTP_HEADER" fi - _ret="$?" + + if [ "$_bergdns_method" = "GET" ]; then + response="$(_get "$_bergdns_url")" + else + _debug2 _bergdns_body "$_bergdns_body" + response="$(_post "$_bergdns_body" "$_bergdns_url" "" "$_bergdns_method" "application/json")" + fi + _bergdns_ret="$?" _debug2 response "$response" - if [ "$_ret" != "0" ]; then - _bergdns_error="the request to $_url could not be made" + if [ "$_bergdns_ret" != "0" ]; then + _bergdns_error="the request to $_bergdns_url could not be made" return 1 fi - # Errors are returned as RFC 9457 problem+json, which always has "status" and - # "code". Successful responses have neither, so the body is checked instead - # of the HTTP status code. - case "$response" in - *'"status":'*'"code":"'* | *'"code":"'*'"status":'*) - _bergdns_error=$(echo "$response" | sed -n 's/.*"detail":"\([^"]*\)".*/\1/p') - [ -n "$_bergdns_error" ] || _bergdns_error="$response" - _bergdns_code=$(echo "$response" | sed -n 's/.*"code":"\([^"]*\)".*/\1/p') - return 1 - ;; + _bergdns_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug _bergdns_status "$_bergdns_status" + + # The HTTP status decides. Errors from the API itself are RFC 9457 + # problem+json and carry a "detail" to show and a stable "code" to branch on, + # but a request that never gets that far -- bergdns.at answers from behind a + # reverse proxy, whose 502 and 504 are HTML -- has neither, and reading the + # body alone would take those for success. + case "$_bergdns_status" in + 2*) return 0 ;; esac - return 0 + _bergdns_error=$(echo "$response" | _egrep_o '"detail":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _bergdns_code=$(echo "$response" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + [ -n "$_bergdns_error" ] || _bergdns_error="$_bergdns_url answered HTTP ${_bergdns_status:-(none)}" + _debug _bergdns_code "$_bergdns_code" + return 1 } From d51d853ad43d32b2509af3e2aac4f9a809059f21 Mon Sep 17 00:00:00 2001 From: Kenny Kropp Date: Mon, 21 Sep 2026 11:53:09 +0200 Subject: [PATCH 4/4] fix default value handling --- dnsapi/dns_bergdns.sh | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_bergdns.sh b/dnsapi/dns_bergdns.sh index 8e3caac1..8660b0d5 100644 --- a/dnsapi/dns_bergdns.sh +++ b/dnsapi/dns_bergdns.sh @@ -134,12 +134,26 @@ _bergdns_init() { esac _saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY" - # Only what the user actually chose is written back. Persisting a default - # would pin the install to today's value, and a later change to the shipped - # one -- a move of the API base above all -- would never reach it. - [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ] || _saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL" - [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ] || _saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL" - [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ] || _saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT" + # Only what the user actually chose is written back, and a value equal to + # the default clears any older setting. Persisting a default would pin the + # install to today's value, and a later change to the shipped one -- a move + # of the API base above all -- would never reach it; leaving an old setting + # in place would mean the environment could never put one back to default. + if [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ]; then + _clearaccountconf_mutable BERGDNS_API_URL + else + _saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL" + fi + if [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ]; then + _clearaccountconf_mutable BERGDNS_TTL + else + _saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL" + fi + if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ]; then + _clearaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT + else + _saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT" + fi return 0 }