#!/usr/bin/env sh ## Will be called by acme.sh to add the TXT record via the Bunny DNS API. ## returns 0 means success, otherwise error. ## Author: nosilver4u ## GitHub: https://github.com/nosilver4u/acme.sh ## ## Environment Variables Required: ## ## BUNNY_API_KEY="75310dc4-ca77-9ac3-9a19-f6355db573b49ce92ae1-2655-3ebd-61ac-3a3ae34834cc" ## ##################### Public functions ##################### ## Create the text record for validation. ## Usage: fulldomain txtvalue ## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs" dns_bunny_add() { fulldomain="$(echo "$1" | _lower_case)" txtvalue=$2 BUNNY_API_KEY="${BUNNY_API_KEY:-$(_readaccountconf_mutable BUNNY_API_KEY)}" # Check if API Key is set if [ -z "$BUNNY_API_KEY" ]; then BUNNY_API_KEY="" _err "You did not specify Bunny.net API key." _err "Please export BUNNY_API_KEY and try again." return 1 fi _info "Using Bunny.net dns validation - add record" _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" ## save the env vars (key and domain split location) for later automated use _saveaccountconf_mutable BUNNY_API_KEY "$BUNNY_API_KEY" ## split the domain for Bunny API if ! _get_base_domain "$fulldomain"; then _err "domain not found in your account for addition" return 1 fi _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" _debug _domain_id "$_domain_id" ## Set the header with our post type and auth key export _H1="Accept: application/json" export _H2="AccessKey: $BUNNY_API_KEY" export _H3="Content-Type: application/json" PURL="https://api.bunny.net/dnszone/$_domain_id/records" PBODY='{"Id":'$_domain_id',"Type":3,"Name":"'$_sub_domain'","Value":"'$txtvalue'","ttl":120}' _debug PURL "$PURL" _debug PBODY "$PBODY" ## the create request - POST ## args: BODY, URL, [need64, httpmethod] response="$(_post "$PBODY" "$PURL" "" "PUT")" ## check response if [ "$?" != "0" ]; then _err "error in response: $response" return 1 fi _debug2 response "$response" ## finished correctly return 0 } ## Remove the txt record after validation. ## Usage: fulldomain txtvalue ## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs" dns_bunny_rm() { fulldomain="$(echo "$1" | _lower_case)" txtvalue=$2 BUNNY_API_KEY="${BUNNY_API_KEY:-$(_readaccountconf_mutable BUNNY_API_KEY)}" # Check if API Key Exists if [ -z "$BUNNY_API_KEY" ]; then BUNNY_API_KEY="" _err "You did not specify Bunny.net API key." _err "Please export BUNNY_API_KEY and try again." return 1 fi _info "Using Bunny.net dns validation - remove record" _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" ## split the domain for Bunny API if ! _get_base_domain "$fulldomain"; then _err "Domain not found in your account for TXT record removal" return 1 fi _debug _sub_domain "$_sub_domain" _debug _domain "$_domain" _debug _domain_id "$_domain_id" ## Set the header with our post type and key auth key export _H1="Accept: application/json" export _H2="AccessKey: $BUNNY_API_KEY" ## get URL for the list of DNS records GURL="https://api.bunny.net/dnszone/$_domain_id" ## 1) Get the domain/zone records ## the fetch request - GET ## args: URL, [onlyheader, timeout] domain_list="$(_get "$GURL")" ## check response if [ "$?" != "0" ]; then _err "error in domain_list response: $domain_list" return 1 fi _debug2 domain_list "$domain_list" ## 2) search through records ## check for what we are looking for: "Type":3,"Value":"$txtvalue","Name":"$_sub_domain" record="$(echo "$domain_list" | _egrep_o "\"Id\"\s*\:\s*\"*[0-9]+\"*,\s*\"Type\"[^}]*\"Value\"\s*\:\s*\"$txtvalue\"[^}]*\"Name\"\s*\:\s*\"$_sub_domain\"")" if [ -n "$record" ]; then ## We found records rec_ids="$(echo "$record" | _egrep_o "Id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")" _debug rec_ids "$rec_ids" if [ -n "$rec_ids" ]; then echo "$rec_ids" | while IFS= read -r rec_id; do ## delete the record ## delete URL for removing the one we dont want DURL="https://api.bunny.net/dnszone/$_domain_id/records/$rec_id" ## the removal request - DELETE ## args: BODY, URL, [need64, httpmethod] response="$(_post "" "$DURL" "" "DELETE")" ## check response (sort of) if [ "$?" != "0" ]; then _err "error in remove response: $response" return 1 fi _debug2 response "$response" done fi fi ## finished correctly return 0 } ##################### Private functions below ##################### ## Split the domain provided into the "base domain" and the "start prefix". ## This function searches for the longest subdomain in your account ## for the full domain given and splits it into the base domain (zone) ## and the prefix/record to be added/removed ## USAGE: fulldomain ## EG: "_acme-challenge.two.three.four.domain.com" ## returns ## _sub_domain="_acme-challenge.two" ## _domain="three.four.domain.com" *IF* zone "three.four.domain.com" exists ## _domain_id=234 ## if only "domain.com" exists it will return ## _sub_domain="_acme-challenge.two.three.four" ## _domain="domain.com" ## _domain_id=234 _get_base_domain() { # args fulldomain="$(echo "$1" | _lower_case)" _debug fulldomain "$fulldomain" # domain max legal length = 253 MAX_DOM=255 page=1 ## get a list of domains for the account to check thru ## Set the headers export _H1="Accept: application/json" export _H2="AccessKey: $BUNNY_API_KEY" _debug BUNNY_API_KEY "$BUNNY_API_KEY" ## get URL for the list of domains ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}} DOMURL="https://api.bunny.net/dnszone" ## while we dont have a matching domain we keep going while [ -z "$found" ]; do ## get the domain list (current page) domain_list="$(_get "$DOMURL")" ## check response if [ "$?" != "0" ]; then _err "error in domain_list response: $domain_list" return 1 fi _debug2 domain_list "$domain_list" ## for each shortening of our $fulldomain, check if it exists in the $domain_list ## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge" i=2 while [ $i -gt 0 ]; do ## get next longest domain _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM") ## check we got something back from our cut (or are we at the end) if [ -z "$_domain" ]; then break fi ## we got part of a domain back - grep it out found="$(echo "$domain_list" | _egrep_o "\"Id\"\s*:\s*\"*[0-9]+\"*,\s*\"Domain\"\s*\:\s*\"$_domain\"")" ## check if it exists if [ -n "$found" ]; then ## exists - exit loop returning the parts sub_point=$(_math $i - 1) _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point") _domain_id="$(echo "$found" | _egrep_o "Id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")" _debug _domain_id "$_domain_id" _debug _domain "$_domain" _debug _sub_domain "$_sub_domain" found="" return 0 fi ## increment cut point $i i=$(_math $i + 1) done if [ -z "$found" ]; then page=$(_math $page + 1) nextpage="https://api.bunny.net/dnszone?page=$page" ## Find the next page if we don't have a match. hasnextpage="$(echo "$domain_list" | _egrep_o "\"HasMoreItems\"\s*:\s*true")" if [ -z "$hasnextpage" ]; then _err "No record and no nextpage in Bunny.net domain search." found="" return 1 fi _debug2 nextpage "$nextpage" DOMURL="$nextpage" fi done ## We went through the entire domain zone list and didn't find one that matched. ## If we ever get here, something is broken in the code... _err "Domain not found in Bunny.net account, but we should never get here!" found="" return 1 }