From 890ab4a7bbfc9e950ba0ae103796e066442c25ee Mon Sep 17 00:00:00 2001 From: Stefan Riegel Date: Sun, 30 Nov 2025 00:48:15 +0100 Subject: [PATCH] Refactor dns_infoblox_uddi.sh: Fix zone detection and add wildcard cert support - Added _get_root() helper function for proper zone detection - Fixed zone ID extraction to match dns/auth_zone/* pattern - Added _infoblox_rest() wrapper for API calls with proper auth - Improved error handling for authentication failures - Added support for wildcard certificates (multiple TXT records) - Filter by exact txtvalue when deleting records - Follow acme.sh best practices and conventions Tested with: - Standard domain certificates - Wildcard certificates (*.domain.com) - Multiple subdomains - Staging and production Let's Encrypt --- dnsapi/dns_infoblox_uddi.sh | 289 +++++++++++++++++++----------------- 1 file changed, 156 insertions(+), 133 deletions(-) diff --git a/dnsapi/dns_infoblox_uddi.sh b/dnsapi/dns_infoblox_uddi.sh index ebf4484c..54cfe47b 100644 --- a/dnsapi/dns_infoblox_uddi.sh +++ b/dnsapi/dns_infoblox_uddi.sh @@ -10,6 +10,8 @@ Issues: github.com/acmesh-official/acme.sh/issues Author: Stefan Riegel ' +Infoblox_UDDI_Api="https://" + ######## Public functions ##################### #Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -38,76 +40,42 @@ dns_infoblox_uddi_add() { export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" - zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - zone_result="$(_get "$zone_url")" - _debug2 "zone_result: $zone_result" - - if [ "$?" != "0" ]; then - _err "Error fetching zones from Infoblox API" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" return 1 fi - - fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') - _debug "Looking for zone matching domain: $fulldomain_no_acme" - - zone_fqdn="" - temp_domain="$fulldomain_no_acme" - - while [ -n "$temp_domain" ]; do - _debug "Checking if '$temp_domain' is a zone..." - if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then - zone_fqdn="$temp_domain" - _debug "Found matching zone: $zone_fqdn" - break - fi - temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') - if ! echo "$temp_domain" | grep -q '\.'; then - break + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting existing txt records" + _infoblox_rest GET "dns/record?_filter=type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'" + + _info "Adding record" + body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" + + if _infoblox_rest POST "dns/record" "$body"; then + if _contains "$response" "$txtvalue"; then + _info "Added, OK" + return 0 + elif _contains "$response" '"error"'; then + # Check if record already exists + if _contains "$response" "already exists" || _contains "$response" "duplicate"; then + _info "Already exists, OK" + return 0 + else + _err "Add txt record error." + _err "Response: $response" + return 1 + fi + else + _info "Added, OK" + return 0 fi - done - - if [ -z "$zone_fqdn" ]; then - _err "Could not determine zone for domain $fulldomain" - _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" - return 1 - fi - - # Fetch exact zone_id for the matched fqdn using server-side filtering - filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" - filter_encoded=$(_url_encode "$filter") - zone_query="$zone_url?_filter=$filter_encoded" - zone_lookup="$(_get "$zone_query")" - _debug2 "zone_lookup: $zone_lookup" - zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - - _debug zone_id "$zone_id" - - if [ -z "$zone_id" ]; then - _err "Could not find zone ID for $zone_fqdn" - _debug "Zone result: $zone_result" - return 1 - fi - - name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//") - name_in_zone=$(echo "$name_in_zone" | sed 's/\.$//') - _debug name_in_zone "$name_in_zone" - - baseurl="https://$Infoblox_Portal/api/ddi/v1/dns/record" - - body="{\"type\":\"TXT\",\"name_in_zone\":\"$name_in_zone\",\"zone\":\"$zone_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" - - result="$(_post "$body" "$baseurl" "" "POST")" - _debug2 result "$result" - - if echo "$result" | grep -q '"id"'; then - record_id=$(echo "$result" | _egrep_o '"id":"[^"]*"' | head -1 | sed 's/"id":"\([^"]*\)"/\1/') - _info "Successfully created TXT record with ID: $record_id" - return 0 - else - _err "Error encountered during record addition" - _err "Response: $result" - return 1 fi + _err "Add txt record error." + return 1 } #Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" @@ -130,92 +98,147 @@ dns_infoblox_uddi_rm() { export _H1="Authorization: Token $Infoblox_UDDI_Key" export _H2="Content-Type: application/json" - zone_url="https://$Infoblox_Portal/api/ddi/v1/dns/auth_zone" - zone_result="$(_get "$zone_url")" - _debug2 "zone_result: $zone_result" - - if [ "$?" != "0" ]; then - _err "Error fetching zones from Infoblox API" + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" return 1 fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" - fulldomain_no_acme=$(echo "$fulldomain" | sed 's/^_acme-challenge\.//') - _debug "Looking for zone matching domain: $fulldomain_no_acme" + _debug "Getting txt records to delete" + # Filter by txtvalue to support wildcard certs (multiple TXT records) + filter="type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'%20and%20rdata.text%20eq%20'$txtvalue'" + _infoblox_rest GET "dns/record?_filter=$filter" - zone_fqdn="" - temp_domain="$fulldomain_no_acme" + if ! _contains "$response" '"results"'; then + _info "Don't need to remove, record not found." + return 0 + fi - while [ -n "$temp_domain" ]; do - _debug "Checking if '$temp_domain' is a zone..." - if echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\"" || echo "$zone_result" | grep -q "\"fqdn\":\"$temp_domain\.\""; then - zone_fqdn="$temp_domain" - _debug "Found matching zone: $zone_fqdn" - break - fi - temp_domain=$(echo "$temp_domain" | sed 's/^[^.]*\.//') - if ! echo "$temp_domain" | grep -q '\.'; then - break - fi - done + record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4) + _debug "record_id" "$record_id" - if [ -z "$zone_fqdn" ]; then - _err "Could not determine zone for domain $fulldomain" - _err "Available zones: $(echo "$zone_result" | _egrep_o '"fqdn":"[^"]*"' | sed 's/"fqdn":"//;s/"//')" - return 1 + if [ -z "$record_id" ]; then + _info "Don't need to remove, record not found." + return 0 fi - # Fetch exact zone_id for the matched fqdn using server-side filtering - filter="fqdn eq '$zone_fqdn.' or fqdn eq '$zone_fqdn'" - filter_encoded=$(_url_encode "$filter") - zone_query="$zone_url?_filter=$filter_encoded" - zone_lookup="$(_get "$zone_query")" - _debug2 "zone_lookup: $zone_lookup" - zone_id=$(echo "$zone_lookup" | _egrep_o '"id":"dns/auth_zone/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - - _debug zone_id "$zone_id" + # Extract UUID from the full record ID (format: dns/record/uuid) + record_uuid=$(echo "$record_id" | sed 's|.*/||') + _debug "record_uuid" "$record_uuid" - if [ -z "$zone_id" ]; then - _err "Could not find zone ID for $zone_fqdn" - _debug "Zone result: $zone_result" + if ! _infoblox_rest DELETE "dns/record/$record_uuid"; then + _err "Delete record error." return 1 fi - name_in_zone=$(echo "$fulldomain" | sed "s/\.$zone_fqdn\$//" | sed 's/\.$//') - _debug name_in_zone "$name_in_zone" - - filter="type eq 'TXT' and name_in_zone eq '$name_in_zone' and zone eq '$zone_id' and rdata.text eq '$txtvalue'" - filter_encoded=$(_url_encode "$filter") - geturl="https://$Infoblox_Portal/api/ddi/v1/dns/record?_filter=$filter_encoded" - - result="$(_get "$geturl")" - _debug2 result "$result" - - if echo "$result" | grep -q '"results":'; then - record_id=$(echo "$result" | _egrep_o '"id":"dns/record/[^\"]*"' | _head_n 1 | sed 's/.*"id":"\([^\"]*\)".*/\1/') - _debug "Found record_id: $record_id" + _info "Removed record successfully" + return 0 +} - if [ -n "$record_id" ]; then - record_uuid=$(echo "$record_id" | sed 's/.*\/\([a-f0-9-]*\)$/\1/') - _debug record_uuid "$record_uuid" +#################### Private functions below ################################## + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=dns/auth_zone/xxxx-xxxx +_get_root() { + domain=$1 + i=1 + p=1 + + # Remove _acme-challenge prefix if present + domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//') + + while true; do + h=$(printf "%s" "$domain_no_acme" | cut -d . -f "$i"-100) + _debug h "$h" + if [ -z "$h" ]; then + # not valid + return 1 + fi - delurl="https://$Infoblox_Portal/api/ddi/v1/dns/record/$record_uuid" - rmResult="$(_post "" "$delurl" "" "DELETE")" + # Query for the zone with both trailing dot and without + filter="fqdn%20eq%20'$h.'%20or%20fqdn%20eq%20'$h'" + if ! _infoblox_rest GET "dns/auth_zone?_filter=$filter"; then + # API error - don't continue if we get auth errors + if _contains "$response" "401" || _contains "$response" "Authorization"; then + _err "Authentication failed. Please check your Infoblox_UDDI_Key." + return 1 + fi + # For other errors, continue to parent domain + p=$i + i=$((i + 1)) + continue + fi - if [ -z "$rmResult" ] || [ "$rmResult" = "{}" ]; then - _info "Successfully deleted the txt record" + # Check if response contains results (even if empty) + if _contains "$response" '"results"'; then + # Extract zone ID - must match the pattern dns/auth_zone/... + zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4) + if [ -n "$zone_id" ]; then + # Found the zone + _domain="$h" + _domain_id="$zone_id" + + # Calculate subdomain + if [ "$_domain" = "$domain" ]; then + _sub_domain="" + else + _cutlength=$((${#domain} - ${#_domain} - 1)) + _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") + fi + return 0 - else - _err "Error occurred during txt record delete" - _err "Response: $rmResult" - return 1 fi - else - _err "Record to delete didn't match an existing record (no matching txtvalue found)" - return 1 fi + + p=$i + i=$((i + 1)) + done + + return 1 +} + +# _infoblox_rest GET "dns/record?_filter=..." +# _infoblox_rest POST "dns/record" "{json body}" +# _infoblox_rest DELETE "dns/record/uuid" +_infoblox_rest() { + method=$1 + ep="$2" + data="$3" + + _debug "$ep" + + # Ensure credentials are available (when called from _get_root) + Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" + Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" + + Infoblox_UDDI_Api="https://$Infoblox_Portal/api/ddi/v1" + export _H1="Authorization: Token $Infoblox_UDDI_Key" + export _H2="Content-Type: application/json" + + # Debug (masked) + _tok_len=$(printf "%s" "$Infoblox_UDDI_Key" | wc -c | tr -d ' \n') + _debug2 "Auth header set" "Token len=${_tok_len} on $Infoblox_Portal" + + if [ "$method" != "GET" ]; then + _debug data "$data" + response="$(_post "$data" "$Infoblox_UDDI_Api/$ep" "" "$method")" else - _err "Record to delete didn't match an existing record (no results found)" - _debug "Response: $result" + response="$(_get "$Infoblox_UDDI_Api/$ep")" + fi + + _ret="$?" + _debug2 response "$response" + + if [ "$_ret" != "0" ]; then + _err "Error: $ep" return 1 fi + + return 0 }