From 649bec10094ebfdbef294d1e9e5db87e29d144af Mon Sep 17 00:00:00 2001 From: Lukas Rettler Date: Mon, 26 May 2025 16:11:48 +0200 Subject: [PATCH] Add Link11 DNS API --- dnsapi/dns_link11.sh | 159 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100755 dnsapi/dns_link11.sh diff --git a/dnsapi/dns_link11.sh b/dnsapi/dns_link11.sh new file mode 100755 index 00000000..e48b7601 --- /dev/null +++ b/dnsapi/dns_link11.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_link11_info='link11.com +Site: link11.com/ +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_link11 +Options: + LINK11_API_KEY xxxx +' + +LINK11_API="https://api.link11.de" +LINK11_SERVICE_KEY="l11securedns" +# Link11 API documentation https://docs.link11.com/using-link11/api/secure-dns +# How to create an API key: https://docs.link11.com/product-guides/secure-dns/interface/api-access + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_link11_add() { + fulldomain=$1 + txtvalue=$2 + + LINK11_API_KEY="${LINK11_API_KEY:-$(_readaccountconf_mutable LINK11_API_KEY)}" + _saveaccountconf_mutable LINK11_API_KEY "$LINK11_API_KEY" + + _info "Using Link11 Secure DNS" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + if [ -z "$LINK11_API_KEY" ]; then + _err "Missing LINK11_API_KEY environment variable." + fi + if ! _exists jq; then + _err "Tool 'jq' is required but not installed." + return 1 + fi + + _debug "First detect the root zone for $fulldomain" + if ! _get_root "$fulldomain"; then + _err "Failed to get root zone for $fulldomain" + return 1 + fi + _debug _zone_id "$_zone_id" + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + # get existing entries for the zone + if ! _link11_rest "list_primary_zone_entries&zone=$_zone_id"; then + _err "Failed to list primary zone entries for $_zone_id" + return 1 + fi + value="$(printf "%s" "$response" | jq -r --arg sub_domain "$_sub_domain" '.data[] | select(.type=="TXT" and .name==$sub_domain) | .value' | _head_n 1)" + if [ -n "$value" ]; then + _debug2 "Entry already exists for $_sub_domain with value $value" + if [ "$value" = "$txtvalue" ]; then + _info "TXT record for $fulldomain already exists with the correct value." + return 0 + else + _debug2 "Updating existing TXT record for $_sub_domain with new value $txtvalue" + entry_id="$(printf "%s" "$response" | jq -r --arg sub_domain "$_sub_domain" '.data[] | select(.type=="TXT" and .name==$sub_domain) | .id' | _head_n 1)" + _link11_rest "update_primary_zone_entry&zone=$_zone_id&entry=$entry_id&value=$txtvalue" + fi + else + _debug2 "No existing entry found for $_sub_domain, adding new TXT record" + _link11_rest "add_primary_zone_entry&zone=$_zone_id&name=$_sub_domain&value=$txtvalue&type=TXT&ttl=60" + fi + + # validate the addition + if ! _link11_rest "list_primary_zone_entries&zone=$_zone_id"; then + _err "Failed to list primary zone entries for $_zone_id" + return 1 + fi + value="$(printf "%s" "$response" | jq -r --arg sub_domain "$_sub_domain" '.data[] | select(.type=="TXT" and .name==$sub_domain) | .value' | _head_n 1)" + if [ -z "$value" ] || [ "$value" != "$txtvalue" ]; then + _debug2 "Expected TXT record value for $_sub_domain is $txtvalue, but got $value" + _err "Failed to add TXT record for $fulldomain" + return 1 + fi + _info "Added TXT record for $fulldomain with value $txtvalue" +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_link11_rm() { + fulldomain=$1 + txtvalue=$2 + _info "Using Link11 Secure DNS" + + _debug "First detect the root zone for $fulldomain" + if ! _get_root "$fulldomain"; then + _err "Failed to get root zone for $fulldomain" + return 1 + fi + _debug _zone_id "$_zone_id" + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + + # get entry id + if ! _link11_rest "list_primary_zone_entries&zone=$_zone_id"; then + _err "Failed to list primary zone entries for $_zone_id" + return 1 + fi + entry_id="$(printf "%s" "$response" | jq -r --arg sub_domain "$_sub_domain" '.data[] | select(.type=="TXT" and .name==$sub_domain) | .id' | _head_n 1)" + if [ -z "$entry_id" ]; then + _info "Nothing to remove, no entry found for $_sub_domain" + return 0 + fi + # remove entry + if ! _link11_rest "delete_primary_zone_entry&zone=$_zone_id&entry=$entry_id"; then + _err "Failed to remove entry for $_sub_domain" + return 1 + fi + _debug2 "Removed entry for $_sub_domain" + return 0 +} + +#################### Private functions below ################################## + +#_acme-challenge.www.domain.com +#returns +# _zone_id=l11securednsprimary1234 +# _domain=domain.com +# _sub_domain=_acme-challenge.www +_get_root() { + domain="$1" + + _link11_rest "list_primary_zones" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug2 h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + _zone_id="$(printf "%s" "$response" | jq -r --arg h "$h" '.data[] | select(.domain==$h) | .id')" + if [ "$_zone_id" ]; then + _sub_domain="$(printf "%s" "$domain" | cut -d . -f 1-"$p")" + _domain=$h + return 0 + fi + p=$i + i=$(_math "$i" + 1) + done + return 1 +} + +_link11_rest() { + parameters="$1" + _debug2 "parameters" "$parameters" + + export _H1="Content-Type: application/json" + export _H2="key: $LINK11_API_KEY" + response="$(_get "$LINK11_API/?apikey=$LINK11_SERVICE_KEY&$parameters")" + # shellcheck disable=SC2181 + if [ "$?" != "0" ] || [ "$(printf "%s" "$response" | jq -r '.status_code')" != "200" ]; then + _err "$response" + return 1 + fi + _debug2 "response" "$response" + return 0 +}