Browse Source
Add dns_cpanel_uapi.sh
Add dns_cpanel_uapi.sh
This script adds support to update DNS using a cPanel account with UAPI (https://api.docs.cpanel.net/cpanel/introduction). This specifically addresses the case where a cPanel account has 2fa enabled and the existing dns_cpanel.sh script is unable to log in cause of that.pull/6878/head
committed by
neil
1 changed files with 239 additions and 0 deletions
@ -0,0 +1,239 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_cpanel_uapi_info='cPanel UAPI |
|||
Manage DNS via cPanel UAPI. Works with API tokens and Two-Factor Authentication. |
|||
Site: cpanel.net |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_cpanel_uapi |
|||
Options: |
|||
cPanel_Username Username |
|||
cPanel_Apitoken API Token |
|||
cPanel_Hostname Server URL. E.g. "https://hostname:port" |
|||
Issues: github.com/acmesh-official/acme.sh/issues/XXXX |
|||
Author: Adam Bodnar |
|||
' |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
# Used to add txt record |
|||
dns_cpanel_uapi_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_info "Adding TXT record via cPanel UAPI" |
|||
_debug fulldomain "$fulldomain" |
|||
_debug txtvalue "$txtvalue" |
|||
|
|||
if ! _cpanel_uapi_login; then |
|||
_err "cPanel UAPI login failed for user $cPanel_Username." |
|||
return 1 |
|||
fi |
|||
|
|||
_debug "Detecting root zone" |
|||
if ! _cpanel_uapi_get_root "$fulldomain"; then |
|||
_err "No matching root domain for $fulldomain found" |
|||
return 1 |
|||
fi |
|||
|
|||
# Build the record name relative to the zone |
|||
_escaped_domain=$(echo "$_domain" | sed 's/\./\\./g') |
|||
_record_name=$(echo "$fulldomain" | sed "s/\.${_escaped_domain}$//") |
|||
_debug "Record name: $_record_name in zone $_domain" |
|||
|
|||
# Get the current SOA serial (required by mass_edit_zone) |
|||
if ! _cpanel_uapi_get_serial "$_domain"; then |
|||
_err "Failed to get zone serial for $_domain" |
|||
return 1 |
|||
fi |
|||
_debug "Zone serial: $_serial" |
|||
|
|||
# URL-encode the JSON add parameter |
|||
_add_json="%7B%22dname%22%3A%22${_record_name}%22%2C%22ttl%22%3A14400%2C%22record_type%22%3A%22TXT%22%2C%22data%22%3A%5B%22${txtvalue}%22%5D%7D" |
|||
_debug "add_json (encoded): $_add_json" |
|||
|
|||
_cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&add=${_add_json}" |
|||
_debug "_result: $_result" |
|||
|
|||
if echo "$_result" | grep -q '"status":1'; then |
|||
_info "TXT record added successfully" |
|||
return 0 |
|||
fi |
|||
_err "Failed to add TXT record." |
|||
_err "Response: $_result" |
|||
return 1 |
|||
} |
|||
|
|||
# Used to remove the txt record after validation |
|||
dns_cpanel_uapi_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_info "Removing TXT record via cPanel UAPI" |
|||
_debug fulldomain "$fulldomain" |
|||
_debug txtvalue "$txtvalue" |
|||
|
|||
if ! _cpanel_uapi_login; then |
|||
_err "cPanel UAPI login failed for user $cPanel_Username." |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _cpanel_uapi_get_root "$fulldomain"; then |
|||
_err "No matching root domain for $fulldomain found" |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _cpanel_uapi_findentry "$fulldomain" "$txtvalue"; then |
|||
_info "Entry doesn't exist, nothing to delete" |
|||
return 0 |
|||
fi |
|||
|
|||
_debug "Deleting record with line_index=$_line_index" |
|||
if ! _cpanel_uapi_get_serial "$_domain"; then |
|||
_err "Failed to get zone serial for $_domain" |
|||
return 1 |
|||
fi |
|||
_cpanel_uapi_request "execute/DNS/mass_edit_zone?zone=${_domain}&serial=${_serial}&remove=${_line_index}" |
|||
_debug "_result: $_result" |
|||
|
|||
if echo "$_result" | grep -q '"status":1'; then |
|||
_info "TXT record removed successfully" |
|||
return 0 |
|||
fi |
|||
_err "Failed to remove TXT record." |
|||
_err "Response: $_result" |
|||
return 1 |
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
|
|||
_cpanel_uapi_checkcredentials() { |
|||
cPanel_Username="${cPanel_Username:-$(_readaccountconf_mutable cPanel_Username)}" |
|||
cPanel_Apitoken="${cPanel_Apitoken:-$(_readaccountconf_mutable cPanel_Apitoken)}" |
|||
cPanel_Hostname="${cPanel_Hostname:-$(_readaccountconf_mutable cPanel_Hostname)}" |
|||
|
|||
if [ -z "$cPanel_Username" ] || [ -z "$cPanel_Apitoken" ] || [ -z "$cPanel_Hostname" ]; then |
|||
cPanel_Username="" |
|||
cPanel_Apitoken="" |
|||
cPanel_Hostname="" |
|||
_err "You haven't specified cPanel_Username, cPanel_Apitoken, and cPanel_Hostname." |
|||
return 1 |
|||
fi |
|||
|
|||
# Remove trailing slash from hostname if present |
|||
cPanel_Hostname=$(echo "$cPanel_Hostname" | sed 's|/$||') |
|||
|
|||
_saveaccountconf_mutable cPanel_Username "$cPanel_Username" |
|||
_saveaccountconf_mutable cPanel_Apitoken "$cPanel_Apitoken" |
|||
_saveaccountconf_mutable cPanel_Hostname "$cPanel_Hostname" |
|||
return 0 |
|||
} |
|||
|
|||
_cpanel_uapi_login() { |
|||
if ! _cpanel_uapi_checkcredentials; then return 1; fi |
|||
|
|||
_cpanel_uapi_request "execute/DomainInfo/list_domains" |
|||
if echo "$_result" | grep -q '"status":1'; then |
|||
_debug "UAPI login check successful" |
|||
return 0 |
|||
fi |
|||
_err "UAPI login check failed. Is the API token correct?" |
|||
_debug "Response: $_result" |
|||
return 1 |
|||
} |
|||
|
|||
_cpanel_uapi_request() { |
|||
export _H1="Authorization: cpanel $cPanel_Username:$cPanel_Apitoken" |
|||
_result=$(_get "$cPanel_Hostname/$1") |
|||
} |
|||
|
|||
_cpanel_uapi_get_root() { |
|||
_cpanel_uapi_request "execute/DomainInfo/list_domains" |
|||
_debug "DomainInfo response length: $(echo "$_result" | wc -c)" |
|||
|
|||
# Extract main_domain |
|||
_main_domain=$(echo "$_result" | _egrep_o '"main_domain"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"main_domain"[[:space:]]*:[[:space:]]*"//;s/"//') |
|||
_debug "main_domain: $_main_domain" |
|||
|
|||
# Extract addon_domains (array of strings) |
|||
_addon_domains=$(echo "$_result" | _egrep_o '"addon_domains"[[:space:]]*:[[:space:]]*\[[^]]*\]' | _egrep_o '"[a-zA-Z0-9._-]+"' | sed 's/"//g' | grep -v 'addon_domains') |
|||
_debug "addon_domains: $_addon_domains" |
|||
|
|||
# Build list of all domains to check |
|||
_all_domains="$_main_domain $_addon_domains" |
|||
_debug "All domains: $_all_domains" |
|||
|
|||
# Find the matching root domain (prefer longest match) |
|||
_best_match="" |
|||
_best_len=0 |
|||
for _check_domain in $_all_domains; do |
|||
if [ -z "$_check_domain" ]; then continue; fi |
|||
if _endswith "$fulldomain" "$_check_domain"; then |
|||
_len=$(printf '%s' "$_check_domain" | wc -c) |
|||
if [ "$_len" -gt "$_best_len" ]; then |
|||
_best_match="$_check_domain" |
|||
_best_len="$_len" |
|||
fi |
|||
fi |
|||
done |
|||
|
|||
if [ -n "$_best_match" ]; then |
|||
_domain="$_best_match" |
|||
_debug "Root domain: $_domain" |
|||
return 0 |
|||
fi |
|||
return 1 |
|||
} |
|||
|
|||
_cpanel_uapi_get_serial() { |
|||
_zone="$1" |
|||
_cpanel_uapi_request "execute/DNS/parse_zone?zone=${_zone}" |
|||
|
|||
# The SOA record has record_type "SOA" and data_b64 array where index 2 is the serial (base64 encoded) |
|||
# Extract the SOA record, find the serial in data_b64 |
|||
_soa_line=$(echo "$_result" | sed 's/},{/},\n{/g' | grep '"record_type":"SOA"' | head -1) |
|||
_debug "SOA line: $_soa_line" |
|||
|
|||
if [ -z "$_soa_line" ]; then |
|||
_err "SOA record not found for zone $_zone" |
|||
return 1 |
|||
fi |
|||
|
|||
# Extract the third element from data_b64 array (serial is index 2, 0-based) |
|||
# data_b64 format: ["ns","admin","SERIAL","refresh","retry","expire","minimum"] |
|||
_serial_b64=$(echo "$_soa_line" | _egrep_o '"data_b64":\[[^]]*\]' | sed 's/"data_b64":\[//;s/\]//' | sed 's/"//g' | cut -d',' -f3) |
|||
_debug "serial_b64: $_serial_b64" |
|||
|
|||
if [ -z "$_serial_b64" ]; then |
|||
_err "Could not extract serial from SOA record" |
|||
return 1 |
|||
fi |
|||
|
|||
_serial=$(printf '%s' "$_serial_b64" | base64 -d 2>/dev/null || echo "$_serial_b64" | openssl base64 -d 2>/dev/null) |
|||
_debug "Decoded serial: $_serial" |
|||
|
|||
if [ -z "$_serial" ]; then |
|||
_err "Failed to decode serial" |
|||
return 1 |
|||
fi |
|||
return 0 |
|||
} |
|||
|
|||
_cpanel_uapi_findentry() { |
|||
_debug "Finding TXT entry for $fulldomain with value $txtvalue" |
|||
|
|||
_cpanel_uapi_request "execute/DNS/parse_zone?zone=${_domain}" |
|||
_debug "parse_zone result length: $(echo "$_result" | wc -c)" |
|||
|
|||
# Base64-encode the txtvalue to match against data_b64 in the response |
|||
_b64_txtvalue=$(printf '%s' "$txtvalue" | base64 | tr -d '\n') |
|||
_debug "b64_txtvalue: $_b64_txtvalue" |
|||
|
|||
# Split records onto separate lines, find matching TXT record by base64 value |
|||
_line_index=$(echo "$_result" | sed 's/},{/},\n{/g' | grep '"record_type":"TXT"' | grep -F "$_b64_txtvalue" | _egrep_o '"line_index":[0-9]+' | head -1 | cut -d: -f2) |
|||
_debug "line_index: $_line_index" |
|||
|
|||
if [ -n "$_line_index" ]; then |
|||
_debug "Entry found with line_index=$_line_index" |
|||
return 0 |
|||
fi |
|||
return 1 |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue