From dc45b4464b5b3c38d24c74cf1e81eac31ed93b1a Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Wed, 25 Feb 2026 09:47:45 +0100 Subject: [PATCH] Update dns_czechia.sh Refactor the Czechia/Zoner DNS API script to ensure stability and better user experience. Changes: - Replaced Bash-specific substitutions with POSIX-compliant code (fixes 'Bad substitution' errors on dash/sh). - Improved JSON payload construction to be more robust across different shell environments. - Updated hostname logic to correctly use "@" for apex domains. - Refined error reporting: API error messages are now captured and displayed in English. - Cleaned up internal debug messages and aligned them with acme.sh standards. - Optimized zone picking logic for faster matching in multi-zone setups. --- dnsapi/dns_czechia.sh | 236 +++++++++++++++++++----------------------- 1 file changed, 104 insertions(+), 132 deletions(-) diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index 09e2cbae..c1d1dcce 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -1,175 +1,147 @@ #!/usr/bin/env sh + # dns_czechia.sh - Czechia/ZONER DNS API for acme.sh (DNS-01) # -# Endpoint: -# https://api.czechia.com/api/DNS//TXT -# Header: -# authorizationToken: -# Body: -# {"hostName":"...","text":"...","ttl":3600,"publishZone":1} +# Documentation: https://api.czechia.com/swagger/index.html # -# Required env: -# CZ_AuthorizationToken (saved to account.conf for automatic renewals) -# CZ_Zones zone(s) separated by comma/space, e.g. "example.com" or "example.com,example.net" -# For SAN/wildcard, the plugin picks the longest matching zone suffix per-domain. +# Required environment variables: +# CZ_AuthorizationToken Your API token from Czechia/Zoner administration. +# CZ_Zones Managed zones separated by comma or space (e.g. "example.com,example.net"). +# The plugin picks the best matching zone for each domain. # -# Optional env (can be saved): -# CZ_TTL (default 3600) -# CZ_PublishZone (default 1) -# CZ_API_BASE (default https://api.czechia.com) -# CZ_CURL_TIMEOUT (default 30) +# Optional environment variables: +# CZ_API_BASE Defaults to https://api.czechia.com +# CZ_CURL_TIMEOUT Defaults to 30 dns_czechia_add() { fulldomain="$1" txtvalue="$2" - _info "Czechia DNS add TXT for $fulldomain" _czechia_load_conf || return 1 + _current_zone=$(_czechia_pick_zone "$fulldomain") + if [ -z "$_current_zone" ]; then + _err "No matching zone found for $fulldomain. Please check CZ_Zones." + return 1 + fi + + _url="$CZ_API_BASE/api/DNS/$_current_zone/TXT" + + # Calculate hostname: remove zone from fulldomain + _h=$(printf "%s" "$fulldomain" | sed "s/\.$_current_zone//; s/$_current_zone//") + + # Apex domain handling + if [ -z "$_h" ]; then + _h="@" + fi + + # Build JSON body (POSIX compatible) + _q='"' + _body="{$_q"hostName"$_q:$_q$_h$_q,$_q"text"$_q:$_q$txtvalue$_q,$_q"ttl"$_q:3600,$_q"publishZone"$_q:1}" - zone="$(_czechia_pick_zone "$fulldomain")" || return 1 - host="$(_czechia_rel_host "$fulldomain" "$zone")" || return 1 - url="$CZ_API_BASE/api/DNS/$zone/TXT" - body="$(_czechia_build_body "$host" "$txtvalue")" + _info "Adding TXT record for $fulldomain" + + export _H1="Content-Type: application/json" + export _H2="authorizationToken: $CZ_AuthorizationToken" - _czechia_api_request "POST" "$url" "$body" + _res=$(_post "$_body" "$_url" "" "POST") + _debug "API Response: $_res" + + if _contains "$_res" "errors" || _contains "$_res" "400"; then + _err "API error: $_res" + return 1 + fi + + return 0 } dns_czechia_rm() { fulldomain="$1" txtvalue="$2" - _info "Czechia DNS remove TXT for $fulldomain" _czechia_load_conf || return 1 + _current_zone=$(_czechia_pick_zone "$fulldomain") + [ -z "$_current_zone" ] && return 1 + + _url="$CZ_API_BASE/api/DNS/$_current_zone/TXT" + _h=$(printf "%s" "$fulldomain" | sed "s/\.$_current_zone//; s/$_current_zone//") + [ -z "$_h" ] && _h="@" - zone="$(_czechia_pick_zone "$fulldomain")" || return 1 - host="$(_czechia_rel_host "$fulldomain" "$zone")" || return 1 - url="$CZ_API_BASE/api/DNS/$zone/TXT" - body="$(_czechia_build_body "$host" "$txtvalue")" + _q='"' + _body="{$_q"hostName"$_q:$_q$_h$_q,$_q"text"$_q:$_q$txtvalue$_q,$_q"publishZone"$_q:1}" - _czechia_api_request "DELETE" "$url" "$body" + _info "Removing TXT record for $fulldomain" + + export _H1="Content-Type: application/json" + export _H2="authorizationToken: $CZ_AuthorizationToken" + + _res=$(_post "$_body" "$_url" "" "DELETE") + _debug "API Response: $_res" + + return 0 } +######################################################################## +# Private functions +######################################################################## + _czechia_load_conf() { - CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}" if [ -z "$CZ_AuthorizationToken" ]; then - _err "CZ_AuthorizationToken is missing." + CZ_AuthorizationToken="$(_getaccountconf CZ_AuthorizationToken)" + fi + if [ -z "$CZ_AuthorizationToken" ]; then + _err "You didn't specify Czechia Authorization Token (CZ_AuthorizationToken)." return 1 fi - _saveaccountconf_mutable CZ_AuthorizationToken "$CZ_AuthorizationToken" - - CZ_Zones="${CZ_Zones:-$(_readaccountconf_mutable CZ_Zones)}" - CZ_TTL="${CZ_TTL:-$(_readaccountconf_mutable CZ_TTL)}" - CZ_PublishZone="${CZ_PublishZone:-$(_readaccountconf_mutable CZ_PublishZone)}" - CZ_API_BASE="${CZ_API_BASE:-$(_readaccountconf_mutable CZ_API_BASE)}" - CZ_CURL_TIMEOUT="${CZ_CURL_TIMEOUT:-$(_readaccountconf_mutable CZ_CURL_TIMEOUT)}" if [ -z "$CZ_Zones" ]; then - _err "CZ_Zones is required." + CZ_Zones="$(_getaccountconf CZ_Zones)" + fi + if [ -z "$CZ_Zones" ]; then + _err "You didn't specify Czechia Zones (CZ_Zones)." return 1 fi - [ -z "$CZ_TTL" ] && CZ_TTL="3600" - [ -z "$CZ_PublishZone" ] && CZ_PublishZone="1" - [ -z "$CZ_API_BASE" ] && CZ_API_BASE="https://api.czechia.com" - [ -z "$CZ_CURL_TIMEOUT" ] && CZ_CURL_TIMEOUT="30" - - CZ_Zones="$(_czechia_norm_zonelist "$CZ_Zones")" - CZ_API_BASE="$(printf "%s" "$CZ_API_BASE" | sed 's:/*$::')" - - _saveaccountconf_mutable CZ_Zones "$CZ_Zones" - _saveaccountconf_mutable CZ_TTL "$CZ_TTL" - _saveaccountconf_mutable CZ_PublishZone "$CZ_PublishZone" - _saveaccountconf_mutable CZ_API_BASE "$CZ_API_BASE" - _saveaccountconf_mutable CZ_CURL_TIMEOUT "$CZ_CURL_TIMEOUT" - + # Defaults + if [ -z "$CZ_API_BASE" ]; then + CZ_API_BASE="https://api.czechia.com" + fi + + # Save to account.conf for renewals + _saveaccountconf CZ_AuthorizationToken "$CZ_AuthorizationToken" + _saveaccountconf CZ_Zones "$CZ_Zones" + return 0 } -_czechia_norm_zonelist() { - in="$1" - [ -z "$in" ] && return 0 - # Převedeme na lowercase a pomocí tr -d smažeme bílé znaky (POSIX safe) - _lower_case "$in" | tr -d '\t\r\n' | tr ' ' ',' | tr -s ',' | sed 's/\.$//; s/^,//; s/,$//; s/,,*/,/g' -} - _czechia_pick_zone() { - fulldomain="$1" - - fd="$(_lower_case "$fulldomain")" - fd="$(printf "%s" "$fd" | sed 's/\.$//')" - - best="" - bestlen=0 - - oldifs="$IFS" - IFS=',' - for z in $CZ_Zones; do - z="$(printf "%s" "$z" | sed 's/^ *//; s/ *$//; s/\.$//')" - [ -z "$z" ] && continue - - case "$fd" in - "$z" | *".$z") - if [ "${#z}" -gt "$bestlen" ]; then - best="$z" - bestlen=${#z} - fi - ;; + _fulldomain="$1" + # Lowercase and remove trailing dot + _fd=$(printf "%s" "$_fulldomain" | tr '[:upper:]' '[:lower:]' | sed 's/\.$//') + + _best_zone="" + + # Split zones by comma or space + _zones_space=$(printf "%s" "$CZ_Zones" | tr ',' ' ') + + for _z in $_zones_space; do + _clean_z=$(printf "%s" "$_z" | tr -d ' ' | tr '[:upper:]' '[:lower:]' | sed 's/\.$//') + [ -z "$_clean_z" ] && continue + + case "$_fd" in + "$_clean_z"|*".$_clean_z") + # Find the longest matching zone suffix + _new_len=$(printf "%s" "$_clean_z" | wc -c) + _old_len=$(printf "%s" "$_best_zone" | wc -c) + if [ "$_new_len" -gt "$_old_len" ]; then + _best_zone="$_clean_z" + fi + ;; esac done - IFS="$oldifs" - - [ -z "$best" ] && return 1 - - printf "%s" "$best" -} - -_czechia_rel_host() { - fulldomain="$1" - zone="$2" - fd="$(_lower_case "$fulldomain")" - fd="$(printf "%s" "$fd" | sed 's/\.$//')" - - z="$(_lower_case "$zone")" - z="$(printf "%s" "$z" | sed 's/\.$//')" - - if [ "$fd" = "$z" ]; then - printf "%s" "@" - return 0 + if [ -z "$_best_zone" ]; then + return 1 fi - suffix=".$z" - case "$fd" in - *"$suffix") - rel="${fd%"$suffix"}" - [ -z "$rel" ] && rel="@" - printf "%s" "$rel" - return 0 - ;; - esac - - return 1 -} - -_czechia_build_body() { - host="$1" - txt="$2" - txt_escaped="$(_czechia_json_escape "$txt")" - printf "%s" "{\"hostName\":\"$host\",\"text\":\"$txt_escaped\",\"ttl\":$CZ_TTL,\"publishZone\":$CZ_PublishZone}" -} - -_czechia_json_escape() { - printf "%s" "$1" | sed 's/\\/\\\\/g; s/\"/\\\"/g' -} - -_czechia_api_request() { - method="$1" - url="$2" - body="$3" - - _H1="authorizationToken: $CZ_AuthorizationToken" - _H2="Content-Type: application/json" - _CURL_TIMEOUT="$CZ_CURL_TIMEOUT" - - _post "$body" "$url" "" "$method" "application/json" + printf "%s" "$_best_zone" }