diff --git a/dnsapi/dns_czechia.sh b/dnsapi/dns_czechia.sh new file mode 100644 index 00000000..7867f14b --- /dev/null +++ b/dnsapi/dns_czechia.sh @@ -0,0 +1,230 @@ +#!/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 +}