Files
acme.sh/dnsapi/dns_opusdns.sh
Kilian Ries 163eb1acb9 Simplify zone detection with API check
Instead of fetching all zones and matching, iterate through domain
parts and check each against the API until a valid zone is found.
Same approach as GoDaddy DNS plugin.

Example: _acme-challenge.test.example.com
- Try: test.example.com → 404
- Try: example.com → 200 ✓ → zone found!

Script reduced from 304 to 255 lines.
2026-01-22 18:11:01 +01:00

256 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_opusdns_info='OpusDNS.com
Site: OpusDNS.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_opusdns
Options:
OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys
OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional.
OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional.
Issues: github.com/acmesh-official/acme.sh/issues/XXXX
Author: OpusDNS Team <https://github.com/opusdns>
'
OPUSDNS_API_Endpoint_Default="https://api.opusdns.com"
OPUSDNS_TTL_Default=60
######## Public functions ###########
# Add DNS TXT record
# Usage: dns_opusdns_add _acme-challenge.example.com "token_value"
dns_opusdns_add() {
fulldomain=$1
txtvalue=$2
_info "Using OpusDNS DNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
# Load and validate credentials
OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}"
if [ -z "$OPUSDNS_API_Key" ]; then
_err "OPUSDNS_API_Key not set. Please set it and try again."
_err "You can create an API key at your OpusDNS dashboard."
return 1
fi
# Save credentials for future use
_saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key"
# Load optional configuration
OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}"
if [ -z "$OPUSDNS_API_Endpoint" ]; then
OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default"
fi
_saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint"
OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}"
if [ -z "$OPUSDNS_TTL" ]; then
OPUSDNS_TTL="$OPUSDNS_TTL_Default"
fi
_saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL"
_debug "API Endpoint: $OPUSDNS_API_Endpoint"
_debug "TTL: $OPUSDNS_TTL"
# Detect zone from FQDN
if ! _get_zone "$fulldomain"; then
_err "Failed to detect zone for domain: $fulldomain"
return 1
fi
_info "Detected zone: $_zone"
_debug "Record name: $_record_name"
# Add the TXT record
if ! _opusdns_add_record "$_zone" "$_record_name" "$txtvalue"; then
_err "Failed to add TXT record"
return 1
fi
_info "TXT record added successfully"
return 0
}
# Remove DNS TXT record
# Usage: dns_opusdns_rm _acme-challenge.example.com "token_value"
dns_opusdns_rm() {
fulldomain=$1
txtvalue=$2
_info "Removing OpusDNS DNS record"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
# Load credentials
OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}"
OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}"
OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}"
if [ -z "$OPUSDNS_API_Endpoint" ]; then
OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default"
fi
if [ -z "$OPUSDNS_TTL" ]; then
OPUSDNS_TTL="$OPUSDNS_TTL_Default"
fi
if [ -z "$OPUSDNS_API_Key" ]; then
_err "OPUSDNS_API_Key not found"
return 1
fi
# Detect zone from FQDN
if ! _get_zone "$fulldomain"; then
_err "Failed to detect zone for domain: $fulldomain"
# Don't fail cleanup - best effort
return 0
fi
_info "Detected zone: $_zone"
_debug "Record name: $_record_name"
# Remove the TXT record (need to pass txtvalue)
if ! _opusdns_remove_record "$_zone" "$_record_name" "$txtvalue"; then
_err "Warning: Failed to remove TXT record (this is usually not critical)"
# Don't fail cleanup - best effort
return 0
fi
_info "TXT record removed successfully"
return 0
}
######## Private functions ###########
# Detect zone from FQDN by checking against OpusDNS API
# Iterates through domain parts until a valid zone is found
# Sets global variables: _zone, _record_name
_get_zone() {
domain=$1
_debug "Detecting zone for: $domain"
# Remove trailing dot if present
domain=$(echo "$domain" | sed 's/\.$//')
export _H1="X-Api-Key: $OPUSDNS_API_Key"
# Start from position 2 (skip first part like _acme-challenge)
i=2
p=1
while true; do
# Extract potential zone (domain parts from position i onwards)
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug "Trying zone: $h"
if [ -z "$h" ]; then
# No more parts to try
_err "Could not find a valid zone for: $domain"
return 1
fi
# Check if this zone exists in OpusDNS
response=$(_get "$OPUSDNS_API_Endpoint/v1/dns/$h")
if _contains "$response" '"name"'; then
# Zone found
_record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_zone="$h"
_debug "Found zone: $_zone"
_debug "Record name: $_record_name"
return 0
fi
_debug "$h not found, trying next"
p="$i"
i=$(_math "$i" + 1)
done
return 1
}
if [ -z "$_record_name" ]; then
_record_name="@"
fi
return 0
}
# Add TXT record using OpusDNS API
_opusdns_add_record() {
zone=$1
record_name=$2
txtvalue=$3
_debug "Adding TXT record: $record_name.$zone = $txtvalue"
# Escape all JSON special characters in txtvalue
# Order matters: escape backslashes first, then other characters
escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g')
# Build JSON payload
# Note: TXT records need quotes around the value in rdata
json_payload="{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}"
_debug2 "JSON payload: $json_payload"
# Send PATCH request
export _H1="X-Api-Key: $OPUSDNS_API_Key"
export _H2="Content-Type: application/json"
response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH")
status=$?
_debug2 "API Response: $response"
if [ $status -ne 0 ]; then
_err "Failed to add TXT record"
_err "API Response: $response"
return 1
fi
# Check for error in response (OpusDNS returns JSON error even on failure)
# Use anchored pattern to avoid matching field names like "error_count"
if echo "$response" | grep -q '"error":'; then
_err "API returned error: $response"
return 1
fi
return 0
}
# Remove TXT record using OpusDNS API
_opusdns_remove_record() {
zone=$1
record_name=$2
txtvalue=$3
_debug "Removing TXT record: $record_name.$zone = $txtvalue"
# Escape all JSON special characters in txtvalue (same as add)
escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g')
# Build JSON payload for removal - needs complete record specification
json_payload="{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}"
_debug2 "JSON payload: $json_payload"
# Send PATCH request
export _H1="X-Api-Key: $OPUSDNS_API_Key"
export _H2="Content-Type: application/json"
response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH")
status=$?
_debug2 "API Response: $response"
if [ $status -ne 0 ]; then
_err "Failed to remove TXT record"
_err "API Response: $response"
return 1
fi
return 0
}