diff --git a/dnsapi/dns_hestiacp.sh b/dnsapi/dns_hestiacp.sh new file mode 100644 index 00000000..8b5eedad --- /dev/null +++ b/dnsapi/dns_hestiacp.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_hestiacp_info='HestiaCP DNS API +Site: https://hestiacp.com +Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hestiacp + +Options: + HESTIA_HOST The HestiaCP panel URL (e.g., https://panel.domain.com:8083) + HESTIA_ACCESS The HestiaCP API access key + HESTIA_SECRET The HestiaCP API secret key + HESTIA_USER Your HestiaCP username (defaults to "admin") + +API Key Setup: + 1. Log in to HestiaCP panel as admin + 2. Go to Server -> Configure -> API + 3. Generate a key pair with "update-dns-records" permission + 4. Copy Host, Access Key, and Secret Key + 5. Login to our HestiaCP server as root, and go to /usr/local/hestia/data/api + 6. The file "update-dns-records" should contain this line in order for this script to work: + ROLE='user' + COMMANDS='v-list-dns-records,v-change-dns-record,v-delete-dns-record,v-add-dns-record' + By default, only v-list-dns-records and v-change-dns-record are enabled. + +NOTES: for wildcard certificates to work, you need to use LetsEncrypt V2 provider, not Alpha ZeroSSL which is default in acme.sh + +Example Usage: + export HESTIA_HOST="https://panel.domain.com:8083" + export HESTIA_ACCESS="your_access_key" + export HESTIA_SECRET="your_secret_key" + export HESTIA_USER="your_username" + acme.sh --issue -d example.com -d *.example.com --dns dns_hestiacp + +Author: Radu Malica https://github.com/radumalica/ +Issues: https://github.com/acmesh-official/acme.sh/issues/6251 +' + +######## Public functions ##################### + +# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_hestiacp_add() { + fulldomain=$1 + txtvalue=$2 + + HESTIA_HOST="${HESTIA_HOST:-$(_readaccountconf_mutable HESTIA_HOST)}" + HESTIA_ACCESS="${HESTIA_ACCESS:-$(_readaccountconf_mutable HESTIA_ACCESS)}" + HESTIA_SECRET="${HESTIA_SECRET:-$(_readaccountconf_mutable HESTIA_SECRET)}" + HESTIA_USER="${HESTIA_USER:-$(_readaccountconf_mutable HESTIA_USER)}" + + if [ -z "$HESTIA_HOST" ] || [ -z "$HESTIA_ACCESS" ] || [ -z "$HESTIA_SECRET" ]; then + _err "Missing required HestiaCP credentials" + return 1 + fi + + # Remove trailing slash if present + HESTIA_HOST="${HESTIA_HOST%/}" + + # Set default user if not provided + [ -z "$HESTIA_USER" ] && HESTIA_USER="admin" + + # Save the credentials to the account conf file + _saveaccountconf_mutable HESTIA_HOST "$HESTIA_HOST" + _saveaccountconf_mutable HESTIA_ACCESS "$HESTIA_ACCESS" + _saveaccountconf_mutable HESTIA_SECRET "$HESTIA_SECRET" + [ "$HESTIA_USER" != "admin" ] && _saveaccountconf_mutable HESTIA_USER "$HESTIA_USER" + + # Validate hostname format + if ! echo "$HESTIA_HOST" | grep -qE '^https?://[^/]+$'; then + _err "HESTIA_HOST must be a valid URL (e.g., https://panel.domain.com:8083)" + return 1 + fi + + # Validate API keys are not obviously wrong + if [ ${#HESTIA_ACCESS} -lt 20 ] || [ ${#HESTIA_SECRET} -lt 20 ]; then + _err "HESTIA_ACCESS and HESTIA_SECRET must be valid API keys" + return 1 + fi + + # Extract domain and subdomain parts + _debug2 "Original domain: $fulldomain" + _domain=$(echo "$fulldomain" | sed -E 's/^[^.]+\.//' | sed -E 's/^\*\.//') + _debug2 "Using domain: $_domain" + + # Get existing TXT records + _info "Getting DNS records for $_domain" + _payload=$(_hestia_api_payload "v-list-dns-records" "$HESTIA_USER" "$_domain" "json") + _debug2 "API payload: $_payload" + response=$(_post "$_payload" "${HESTIA_HOST}/api/" "" "POST" "--connect-timeout 10") + _ret=$? + _debug3 "Raw API response: $response" + _info "API response (ret=$_ret)" + + # Add timeout handling + if _contains "$response" "Operation timed out"; then + _err "API request timed out. Please try again" + return 1 + fi + + if [ $_ret -ne 0 ]; then + _err "Error accessing domain: $_domain" + return 1 + fi + + if _contains "$response" "Error: "; then + _err "API error: $response" + return 1 + fi + + _sub="_acme-challenge" + + if ! _contains "$fulldomain" "$_sub"; then + _err "Invalid domain format - missing $_sub prefix" + return 1 + fi + + # First delete any existing _acme-challenge TXT records + _info "Checking for existing _acme-challenge TXT records" + while IFS=':' read -r id value || [ -n "$id" ]; do + if [ -n "$id" ]; then + if [ "$value" = "$txtvalue" ]; then + _info "Found existing record with correct value, keeping it" + found_exact=1 + else + _info "Removing old _acme-challenge record $id" + if ! _post "$(_hestia_api_payload "v-delete-dns-record" "$HESTIA_USER" "$_domain" "$id" "no")" "${HESTIA_HOST}/api/" "" "POST" "--connect-timeout 10" >/dev/null 2>&1; then + _err "Error deleting old TXT record $id" + return 1 + fi + _debug2 "Successfully removed old record $id with value: $value" + fi + fi + done </dev/null 2>&1; then + _err "Error creating new TXT record" + return 1 + fi + _info "Successfully added new DNS-01 challenge record" + _debug3 "Added TXT record with value: $txtvalue" + _info "Note: Please allow time for DNS propagation" + return 0 +} + +# Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_hestiacp_rm() { + fulldomain=$1 + txtvalue=$2 + + HESTIA_HOST="${HESTIA_HOST:-$(_readaccountconf_mutable HESTIA_HOST)}" + HESTIA_ACCESS="${HESTIA_ACCESS:-$(_readaccountconf_mutable HESTIA_ACCESS)}" + HESTIA_SECRET="${HESTIA_SECRET:-$(_readaccountconf_mutable HESTIA_SECRET)}" + HESTIA_USER="${HESTIA_USER:-$(_readaccountconf_mutable HESTIA_USER)}" + + # Remove trailing slash if present + HESTIA_HOST="${HESTIA_HOST%/}" + + # Set default user if not provided + [ -z "$HESTIA_USER" ] && HESTIA_USER="admin" + + if [ -z "$HESTIA_HOST" ] || [ -z "$HESTIA_ACCESS" ] || [ -z "$HESTIA_SECRET" ]; then + _err "Missing required HestiaCP credentials" + return 1 + fi + + # Validate hostname format + if ! echo "$HESTIA_HOST" | grep -qE '^https?://[^/]+$'; then + _err "HESTIA_HOST must be a valid URL (e.g., https://panel.domain.com:8083)" + return 1 + fi + + # Validate API keys are not obviously wrong + if [ ${#HESTIA_ACCESS} -lt 20 ] || [ ${#HESTIA_SECRET} -lt 20 ]; then + _err "HESTIA_ACCESS and HESTIA_SECRET must be valid API keys (length >= 20)" + return 1 + fi + + # Extract domain parts + _debug2 "Original domain: $fulldomain" + + # Define subdomain constant + _sub="_acme-challenge" + _debug2 "Challenge prefix: $_sub" + + # Validate _acme-challenge prefix + if ! _contains "$fulldomain" "$_sub"; then + _err "Invalid domain format - missing $_sub prefix" + return 1 + fi + + # Everything after _sub. is our domain + _domain=$(echo "$fulldomain" | sed -E 's/^[^.]+\.//' | sed -E 's/^\*\.//') + _debug2 "Using domain: $_domain" + + # Get zone records + _info "Getting DNS records for $_domain" + response=$(_post "$(_hestia_api_payload "v-list-dns-records" "$HESTIA_USER" "$_domain" "json")" "${HESTIA_HOST}/api/" "" "POST" "--connect-timeout 10") + _ret=$? + _debug3 "Raw API response: $response" + _info "API response (ret=$_ret)" + + # Add timeout handling + if _contains "$response" "Operation timed out"; then + _err "API request timed out. Please try again" + return 1 + fi + + # Enhanced response validation + if [ -z "$response" ]; then + _err "Empty response received from API" + return 1 + fi + + if [ $_ret -ne 0 ]; then + _err "Error accessing domain: $_domain" + return 1 + fi + + if _contains "$response" "Error: "; then + _err "API error: $response" + return 1 + fi + + # Delete matching _acme-challenge record + _info "Checking for challenge TXT record" + found=0 + while IFS=':' read -r id value || [ -n "$id" ]; do + if [ -n "$id" ]; then + if [ "$value" = "$txtvalue" ]; then + _info "Deleting challenge record $id" + if ! _post "$(_hestia_api_payload "v-delete-dns-record" "$HESTIA_USER" "$_domain" "$id" "no")" "${HESTIA_HOST}/api/" "" "POST" "--connect-timeout 10" >/dev/null 2>&1; then + _err "Error deleting TXT record $id" + return 1 + fi + _info "Successfully removed DNS-01 challenge record" + found=1 + else + _debug2 "Skipping record $id with different value: $value" + fi + fi + done <