From fb78be0a295384f3682f8dad1b922dc4d0810d07 Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Mon, 26 Jan 2026 11:43:26 +0100 Subject: [PATCH 1/4] Create dns_czechia.sh This PR adds a DNS API plugin for CZECHIA.COM / RegZone (ZONER a.s.). - Supports ACME DNS-01 TXT record management - Uses official REST API (Swagger) - Credentials are stored in account.conf for non-interactive renewals - IP whitelisting is not required for DNS TXT changes used for SSL automation (per official REST API terms) Tested with: - acme.sh v3.x - Zones: zoner-test.eu --- dnsapi/dns_czechia.sh | 239 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 dnsapi/dns_czechia.sh diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh new file mode 100644 index 00000000..0f6ec8c7 --- /dev/null +++ b/dnsapi/dns_czechia.sh @@ -0,0 +1,239 @@ +#!/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} +# +# Required env: +# CZ_AuthorizationToken (saved to account.conf for automatic renewals) +# CZ_Zone (default apex zone), e.g. example.com +# - for multi-domain SAN, use CZ_Zones (see below) +# +# Optional env (multi-zone): +# CZ_Zones list of zones separated by comma/space, e.g. "example.com,example.net" +# For DNS-01 SAN, the plugin picks the longest matching zone suffix per-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) + + +dns_czechia_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Czechia DNS add TXT for $fulldomain" + _czechia_load_conf || return 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 "Czechia zone: $zone" + _info "Czechia API URL: $url" + _info "Czechia hostName: $host" + + _czechia_api_request "POST" "$url" "$body" +} + + +dns_czechia_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Czechia DNS remove TXT for $fulldomain" + _czechia_load_conf || return 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 "Czechia zone: $zone" + _info "Czechia API URL: $url" + _info "Czechia hostName: $host" + + _czechia_api_request "DELETE" "$url" "$body" +} + + +_czechia_load_conf() { + # token must be available for automatic renewals (read from env or account.conf) + CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}" + if [ -z "$CZ_AuthorizationToken" ]; then + CZ_AuthorizationToken="" + _err "CZ_AuthorizationToken is missing." + _err "Export it first: export CZ_AuthorizationToken=\"...\"" + return 1 + fi + _saveaccountconf_mutable CZ_AuthorizationToken "$CZ_AuthorizationToken" + + # other settings can be env or saved + CZ_Zone="${CZ_Zone:-$(_readaccountconf_mutable CZ_Zone)}" + 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)}" + + # at least one zone source must be provided + if [ -z "$CZ_Zone" ] && [ -z "$CZ_Zones" ]; then + _err "CZ_Zone or CZ_Zones is required (apex zone), e.g. example.com or \"example.com,example.net\"" + 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" + + # normalize + CZ_Zone="$(printf "%s" "$CZ_Zone" | tr '[:upper:]' '[:lower:]' | sed 's/\.$//')" + CZ_Zones="$(_czechia_norm_zonelist "$CZ_Zones")" + CZ_API_BASE="$(printf "%s" "$CZ_API_BASE" | sed 's:/*$::')" + + # persist non-secret config + _saveaccountconf_mutable CZ_Zone "$CZ_Zone" + _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" + + return 0 +} + + +_czechia_norm_zonelist() { + # Normalize comma/space separated list to a single comma-separated list + # - lowercased + # - trimmed + # - trailing dots removed + # - empty entries dropped + in="$1" + [ -z "$in" ] && return 0 + printf "%s" "$in" \ + | tr '[:upper:]' '[:lower:]' \ + | tr ' ' ',' \ + | tr -s ',' \ + | sed 's/[\t\r\n]//g; s/\.$//; s/^,//; s/,$//; s/,,*/,/g' +} + + +_czechia_pick_zone() { + fulldomain="$1" + fd="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]' | sed 's/\.$//')" + + best="" + bestlen=0 + + # 1) CZ_Zone as default (only if it matches) + if [ -n "$CZ_Zone" ]; then + z="$CZ_Zone" + case "$fd" in + "$z"|*".$z") + best="$z" + bestlen=${#z} + ;; + esac + fi + + # 2) CZ_Zones list (longest matching suffix wins) + if [ -n "$CZ_Zones" ]; then + 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 + ;; + esac + done + IFS="$oldifs" + fi + + if [ -z "$best" ]; then + _err "No matching zone for '$fd'. Set CZ_Zone or CZ_Zones to include the apex zone for this domain." + return 1 + fi + + echo "$best" + return 0 +} + + +_czechia_rel_host() { + fulldomain="$1" + zone="$2" + + fd="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]' | sed 's/\.$//')" + z="$(printf "%s" "$zone" | tr '[:upper:]' '[:lower:]' | sed 's/\.$//')" + + if [ "$fd" = "$z" ]; then + echo "@" + return 0 + fi + + suffix=".$z" + case "$fd" in + *"$suffix") + rel="${fd%$suffix}" + [ -z "$rel" ] && rel="@" + echo "$rel" + return 0 + ;; + esac + + _err "fulldomain '$fd' is not under zone '$z'" + return 1 +} + + +_czechia_build_body() { + host="$1" + txt="$2" + txt_escaped="$(_czechia_json_escape "$txt")" + echo "{\"hostName\":\"$host\",\"text\":\"$txt_escaped\",\"ttl\":$CZ_TTL,\"publishZone\":$CZ_PublishZone}" +} + + +_czechia_json_escape() { + echo "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + + +_czechia_api_request() { + method="$1" + url="$2" + body="$3" + + export _H1="authorizationToken: $CZ_AuthorizationToken" + export _H2="Content-Type: application/json" + + _info "Czechia request: $method $url" + _debug2 "Czechia body: $body" + + # _post() can do POST/PUT/DELETE; see DNS-API-Dev-Guide + resp="$(_post "$body" "$url" "" "$method" "application/json")" + post_ret="$?" + + if [ "$post_ret" -ne 0 ]; then + _err "Czechia API call failed (ret=$post_ret). Response: ${resp:-}" + return 1 + fi + + _debug2 "Czechia response: ${resp:-}" + return 0 +} From 7b433e58e873a8440bdb77942bed0b61bdb03dcd Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Thu, 29 Jan 2026 21:16:42 +0100 Subject: [PATCH 2/4] Update dns_czechia.sh Fix shellcheck warnings in dns_czechia plugin --- dnsapi/dns_czechia.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index 0f6ec8c7..905b22a0 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -154,7 +154,7 @@ _czechia_pick_zone() { [ -z "$z" ] && continue case "$fd" in "$z"|*".$z") - if [ ${#z} -gt $bestlen ]; then + if [ "${#z}" -gt "$bestlen" ]; then best="$z" bestlen=${#z} fi @@ -189,7 +189,7 @@ _czechia_rel_host() { suffix=".$z" case "$fd" in *"$suffix") - rel="${fd%$suffix}" + rel="${fd%"$suffix"}" [ -z "$rel" ] && rel="@" echo "$rel" return 0 From 4b0a106bed69250331d8bed6bdc5bff342882520 Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Thu, 29 Jan 2026 21:23:03 +0100 Subject: [PATCH 3/4] Update dns_czechia.sh corrections --- dnsapi/dns_czechia.sh | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index 905b22a0..d7a1ea3c 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -119,11 +119,11 @@ _czechia_norm_zonelist() { # - empty entries dropped in="$1" [ -z "$in" ] && return 0 - printf "%s" "$in" \ - | tr '[:upper:]' '[:lower:]' \ - | tr ' ' ',' \ - | tr -s ',' \ - | sed 's/[\t\r\n]//g; s/\.$//; s/^,//; s/,$//; s/,,*/,/g' + printf "%s" "$in" | + tr '[:upper:]' '[:lower:]' | + tr ' ' ',' | + tr -s ',' | + sed 's/[\t\r\n]//g; s/\.$//; s/^,//; s/,$//; s/,,*/,/g' } @@ -137,12 +137,12 @@ _czechia_pick_zone() { # 1) CZ_Zone as default (only if it matches) if [ -n "$CZ_Zone" ]; then z="$CZ_Zone" - case "$fd" in - "$z"|*".$z") - best="$z" - bestlen=${#z} - ;; - esac + case "$fd" in + "$z" | *".$z") + best="$z" + bestlen=${#z} + ;; +esac fi # 2) CZ_Zones list (longest matching suffix wins) @@ -153,13 +153,13 @@ _czechia_pick_zone() { 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 - ;; - esac + "$z" | *".$z") + if [ "${#z}" -gt "$bestlen" ]; then + best="$z" + bestlen=${#z} + fi + ;; +esac done IFS="$oldifs" fi From dba4ebdd80767a5a1f73ca6cce2de28d6d2ce3c2 Mon Sep 17 00:00:00 2001 From: CZECHIA-COM Date: Mon, 2 Feb 2026 06:24:29 +0100 Subject: [PATCH 4/4] Update dns_czechia.sh format dns_czechia.sh --- dnsapi/dns_czechia.sh | 57 ++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh index d7a1ea3c..7867f14b 100644 --- a/dnsapi/dns_czechia.sh +++ b/dnsapi/dns_czechia.sh @@ -23,7 +23,6 @@ # CZ_API_BASE (default https://api.czechia.com) # CZ_CURL_TIMEOUT (default 30) - dns_czechia_add() { fulldomain="$1" txtvalue="$2" @@ -43,7 +42,6 @@ dns_czechia_add() { _czechia_api_request "POST" "$url" "$body" } - dns_czechia_rm() { fulldomain="$1" txtvalue="$2" @@ -63,7 +61,6 @@ dns_czechia_rm() { _czechia_api_request "DELETE" "$url" "$body" } - _czechia_load_conf() { # token must be available for automatic renewals (read from env or account.conf) CZ_AuthorizationToken="${CZ_AuthorizationToken:-$(_readaccountconf_mutable CZ_AuthorizationToken)}" @@ -110,7 +107,6 @@ _czechia_load_conf() { return 0 } - _czechia_norm_zonelist() { # Normalize comma/space separated list to a single comma-separated list # - lowercased @@ -119,14 +115,13 @@ _czechia_norm_zonelist() { # - empty entries dropped in="$1" [ -z "$in" ] && return 0 - printf "%s" "$in" | - tr '[:upper:]' '[:lower:]' | - tr ' ' ',' | - tr -s ',' | - sed 's/[\t\r\n]//g; s/\.$//; s/^,//; s/,$//; s/,,*/,/g' + printf "%s" "$in" | + tr '[:upper:]' '[:lower:]' | + tr ' ' ',' | + tr -s ',' | + sed 's/[\t\r\n]//g; s/\.$//; s/^,//; s/,$//; s/,,*/,/g' } - _czechia_pick_zone() { fulldomain="$1" fd="$(printf "%s" "$fulldomain" | tr '[:upper:]' '[:lower:]' | sed 's/\.$//')" @@ -137,12 +132,12 @@ _czechia_pick_zone() { # 1) CZ_Zone as default (only if it matches) if [ -n "$CZ_Zone" ]; then z="$CZ_Zone" - case "$fd" in - "$z" | *".$z") - best="$z" - bestlen=${#z} - ;; -esac + case "$fd" in + "$z" | *".$z") + best="$z" + bestlen=${#z} + ;; + esac fi # 2) CZ_Zones list (longest matching suffix wins) @@ -153,13 +148,13 @@ esac 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 - ;; -esac + "$z" | *".$z") + if [ "${#z}" -gt "$bestlen" ]; then + best="$z" + bestlen=${#z} + fi + ;; + esac done IFS="$oldifs" fi @@ -173,7 +168,6 @@ esac return 0 } - _czechia_rel_host() { fulldomain="$1" zone="$2" @@ -188,19 +182,18 @@ _czechia_rel_host() { suffix=".$z" case "$fd" in - *"$suffix") - rel="${fd%"$suffix"}" - [ -z "$rel" ] && rel="@" - echo "$rel" - return 0 - ;; + *"$suffix") + rel="${fd%"$suffix"}" + [ -z "$rel" ] && rel="@" + echo "$rel" + return 0 + ;; esac _err "fulldomain '$fd' is not under zone '$z'" return 1 } - _czechia_build_body() { host="$1" txt="$2" @@ -208,12 +201,10 @@ _czechia_build_body() { echo "{\"hostName\":\"$host\",\"text\":\"$txt_escaped\",\"ttl\":$CZ_TTL,\"publishZone\":$CZ_PublishZone}" } - _czechia_json_escape() { echo "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } - _czechia_api_request() { method="$1" url="$2"