You need to sign in or sign up before continuing.
Unverified Commit 274393ac authored by stilez's avatar stilez Committed by GitHub
Browse files

Create DNS 01 module for Plesk XML API

parent 1339b942
#!/usr/bin/env sh
## Name: dns_pleskxml.sh
## Created by Stilez.
## Also uses some code from PR#1832 by @romanlum (https://github.com/Neilpang/acme.sh/pull/1832/files)
## This DNS01 method uses the Plesk XML API described at:
## https://docs.plesk.com/en-US/12.5/api-rpc/about-xml-api.28709
## and more specifically: https://docs.plesk.com/en-US/12.5/api-rpc/reference.28784
## Note: a DNS ID with host = empty string is OK for this API, see
## https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798
## For example, to add a TXT record to DNS alias domain "acme-alias.com" would be a valid Plesk action.
## So this API module can handle such a request, if needed.
## The plesk plugin uses the xml api to add and remvoe the dns records. Therefore the url, username
## and password have to be configured by the user before this module is called.
##
## ```
## export pleskxml_uri="https://YOUR_PLESK_URI_HERE:8443/enterprise/control/agent.php"
## (or probably something similar)
## export pleskxml_user="plesk username"
## export pleskxml_pass="plesk password"
## ```
## Ok, let's issue a cert now:
## ```
## acme.sh --issue --dns dns_pleskxml -d example.com -d www.example.com
## ```
##
## The `pleskxml_uri`, `pleskxml_user` and `pleskxml_pass` will be saved in `~/.acme.sh/account.conf` and reused when needed.
#################### INTERNAL VARIABLES + NEWLINE ##################################
pleskxml_init_checks_done=0
# Variable containing bare newline - not a style issue
# shellcheck disable=SC1004
NEWLINE='\
'
#################### API Templates ##################################
pleskxml_tplt_get_domains="<packet><customer><get-domain-list><filter/></get-domain-list></customer></packet>"
# Get a list of domains that PLESK can manage, so we can check root domain + host for acme.sh
# Also used to test credentials and URI.
# No args.
pleskxml_tplt_get_dns_records="<packet><dns><get_rec><filter><site-id>%s</site-id></filter></get_rec></dns></packet>"
# Get all DNS records for a Plesk domain ID.
# ARG = Plesk domain id to query
pleskxml_tplt_add_txt_record="<packet><dns><add_rec><site-id>%s</site-id><type>TXT</type><host>%s</host><value>%s</value></add_rec></dns></packet>"
# Add a TXT record to a domain.
# ARGS = (1) Plesk internal domain ID, (2) "hostname" for the new record, eg '_acme_challenge', (3) TXT record value
pleskxml_tplt_rmv_dns_record="<packet><dns><del_rec><filter><id>%s</id></filter></del_rec></dns></packet>"
# Add a TXT record to a domain.
# ARG = the Plesk internal ID for the dns record to be deleted
#################### Public functions ##################################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_pleskxml_add() {
fulldomain=$1
txtvalue=$2
_info "Entering dns_pleskxml_add() to add TXT record '$2' to domain '$1'..."
# Get credentials if not already checked, and confirm we can log in to Plesk XML API
if ! _credential_check; then
return 1
fi
# Get root and subdomain details, and Plesk domain ID
if ! _pleskxml_get_root_domain "$fulldomain"; then
return 1
fi
_debug 'Credentials OK, and domain identified. Calling Plesk XML API to add TXT record'
# printf using template in a variable - not a style issue
# shellcheck disable=SC2059
request="$( printf "$pleskxml_tplt_add_txt_record" "$root_domain_id" "$sub_domain_name" "$txtvalue" )"
if ! _call_api "$request"; then
return 1
fi
# OK, we should have added a TXT record. Let's check and return success if so.
# All that should be left in the result, is one section, containing <result><status>ok</status><id>NEW_DNS_RECORD_ID</id></result>
results="$( _api_response_split "$pleskxml_prettyprint_result" 'result' '<status>' )"
if ! _value "$results" | grep '<status>ok</status>' | grep -qE '<id>[0-9]+</id>'; then
# Error - doesn't contain expected string. Something's wrong.
_err 'Error when calling Plesk XML API.'
_err 'The result did not contain the expected <id>XXXXX</id> section, or contained other values as well.'
_err 'This is unexpected: something has gone wrong.'
_err 'The full response was:\n' "$pleskxml_prettyprint_result"
return 1
fi
recid="$( _value "$results" | grep -E '<id>[0-9]+</id>' | sed -E 's/^.*<id>([0-9]+)<\/id>.*$/\1/' )"
_info "Success. TXT record appears to be correctly added (Plesk record ID=$recid). Exiting dns_pleskxml_add()."
return 0
}
#Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_pleskxml_rm() {
fulldomain=$1
txtvalue=$2
_info "Entering dns_pleskxml_rm() to remove TXT record '$2' from domain '$1'..."
# Get credentials if not already checked, and confirm we can log in to Plesk XML API
if ! _credential_check; then
return 1
fi
# Get root and subdomain details, and Plesk domain ID
if ! _pleskxml_get_root_domain "$fulldomain"; then
return 1
fi
_debug 'Credentials OK, and domain identified. Calling Plesk XML API to get list of TXT records and their IDs'
# printf using template in a variable - not a style issue
# shellcheck disable=SC2059
request="$( printf "$pleskxml_tplt_get_dns_records" "$root_domain_id" )"
if ! _call_api "$request"; then
return 1
fi
# Reduce output to one line per DNS record, filtered for TXT records with a record ID only (which they should all have)
reclist="$( _api_response_split "$pleskxml_prettyprint_result" 'result' '<status>ok</status>' | \
grep "<site-id>${root_domain_id}</site-id>" | \
grep -E '<id>[0-9]+</id>' | \
grep '<type>TXT</type>' \
)"
if [ -z "$reclist" ]; then
_err "No TXT records found for root domain ${root_domain_name} (Plesk domain ID ${root_domain_id}). Exiting."
return 1
fi
_debug "Got list of DNS TXT records for root domain '$root_domain_name'"':\n'"$reclist"
recid="$( _value "$reclist" | \
grep "<host>$1.</host>" | \
grep "<value>$txtvalue</value>" | \
sed -E 's/(^.*<id>|<\/id>.*$)//g' \
)"
_debug "List of DNS TXT records for host:"'\n'"$( _value "$reclist" | grep "<host>$1.</host>" )"
if ! _value "$recid" | grep -Eq '^[0-9]+$'; then
_err "DNS records for root domain '${root_domain_name}' (Plesk ID ${root_domain_id}) + host '${sub_domain_name}' do not contain the TXT record '${txtvalue}'"
_err "Cannot delete TXT record. Exiting."
return 1
fi
_debug "Found Plesk record ID for target text string '${txtvalue}': ID=${recid}"
_debug 'Calling Plesk XML API to remove TXT record'
# printf using template in a variable - not a style issue
# shellcheck disable=SC2059
request="$( printf "$pleskxml_tplt_rmv_dns_record" "$recid" )"
if ! _call_api "$request"; then
return 1
fi
# OK, we should have removed a TXT record. Let's check and return success if so.
# All that should be left in the result, is one section, containing <result><status>ok</status><id>PLESK_DELETED_DNS_RECORD_ID</id></result>
results="$( _api_response_split "$pleskxml_prettyprint_result" 'result' '<status>' )"
if ! _value "$results" | grep '<status>ok</status>' | grep -qE '<id>[0-9]+</id>'; then
# Error - doesn't contain expected string. Something's wrong.
_err 'Error when calling Plesk XML API.'
_err 'The result did not contain the expected <id>XXXXX</id> section, or contained other values as well.'
_err 'This is unexpected: something has gone wrong.'
_err 'The full response was:\n' "$pleskxml_prettyprint_result"
return 1
fi
_info "Success. TXT record appears to be correctly removed. Exiting dns_pleskxml_rm()."
return 0
}
#################### Private functions below ##################################
# Outputs value of a variable
_value() {
printf '%s' "$1"
}
# Outputs value of a variable (FQDN) and cuts it at 2 delimiters
# $1, $2 = where to cut
# $3 = FQDN
_valuecut() {
printf '%s' "$3" | cut -d . -f "${1}-${2}"
}
# Cleans up an API response, splits it "per item" and greps for a string to validate useful lines
# $1 - result string from API
# $2 - tag to resplit on (usually "result" or "domain")
# $3 - regex to recognise useful return lines
_api_response_split() {
printf '%s' "$1" | \
sed -E 's/(^[[:space:]]+|[[:space:]]+$)//g' | \
tr -d '\n\r' | \
sed -E "s/<\/?$2>/${NEWLINE}/g" | \
grep -E "$3"
}
# Calls Plesk XML API, and checks results for obvious issues
_call_api() {
request="$1"
errtext=''
_debug 'Entered _call_api(). Calling Plesk XML API with request:\n' "'${request}'"
export _H1="HTTP_AUTH_LOGIN: $pleskxml_user"
export _H2="HTTP_AUTH_PASSWD: $pleskxml_pass"
export _H3="content-Type: text/xml"
export _H4="HTTP_PRETTY_PRINT: true"
pleskxml_prettyprint_result="$(_post "${request}" "$pleskxml_uri" "" "POST")"
pleskxml_retcode="$?"
_debug "acme _post() returned retcode=$pleskxml_retcode. Literal response:" '\n' "'${pleskxml_prettyprint_result}'"
# Error handling
# Detect any <status> that isn't "ok". None of the used calls should fail if the API is working correctly.
# Also detect if there simply aren't any status lines (null result?) and report that, as well.
statuslines="$( echo "$pleskxml_prettyprint_result" | grep -E '^[[:space:]]*<status>[^<]*</status>[[:space:]]*$' )"
if _value "$statuslines" | grep -qv '<status>ok</status>'; then
# We have some status lines that aren't "ok". Get the details
errtext="$( \
_value "$pleskxml_prettyprint_result" | \
grep -iE "(<status>|<errcode>|<errtext>)" | \
sed -E 's/(^[[:space:]]+|<\/[a-z]+$)//g' | \
sed -E 's/^<([a-z]+)>/\1: /' \
)"
elif ! _value "$statuslines" | grep -q '<status>ok</status>'; then
# We have no status lines at all. Results are empty
errtext='The Plesk XML API unexpectedly returned an empty set of results for this call.'
fi
if [ "$pleskxml_retcode" -ne 0 ] || [ "$errtext" != "" ]; then
_err "The Plesk XML API call failed."
_err "The return code for the POST request was $pleskxml_retcode (0=success)."
if [ "$errtext" != "" ]; then
_err 'Status and error messages received from the Plesk server:\n' "$errtext"
else
_err "No additional error messages were received back from the Plesk server"
fi
return 1
fi
_debug "Leaving _call_api(). Successful call."
return 0
}
_credential_check() {
# Startup checks (credentials, URI)
_debug "Checking Plesk XML API login credentials and URI..."
if [ "$pleskxml_init_checks_done" -eq 1 ]; then
_debug "Initial checks already done, no need to repeat. Skipped."
return 0
fi
pleskxml_user="${pleskxml_user:-$(_readaccountconf_mutable pleskxml_user)}"
pleskxml_pass="${pleskxml_pass:-$(_readaccountconf_mutable pleskxml_pass)}"
pleskxml_uri="${pleskxml_uri:-$(_readaccountconf_mutable pleskxml_uri)}"
_debug "Credentials - User: '${pleskxml_user}' Passwd: ****** URI: '${pleskxml_uri}'"
if [ -z "$pleskxml_user" ] || [ -z "$pleskxml_pass" ] || [ -z "$pleskxml_uri" ]; then
pleskxml_user=""
pleskxml_pass=""
pleskxml_uri=""
_err "You didn't specify one or more of the Plesk XML API username, password, or URI."
_err "Please create these and try again."
_err "Instructions are in the module source code."
return 1
fi
# Test the API is usable, by trying to read the list of managed domains...
_call_api "$pleskxml_tplt_get_domains"
if [ "$pleskxml_retcode" -ne 0 ]; then
_err '\nFailed to access Plesk XML API.'
_err "Please check your login credentials and Plesk URI, and that the URI is reachable, and try again."
return 1
fi
_saveaccountconf_mutable pleskxml_uri "$pleskxml_uri"
_saveaccountconf_mutable pleskxml_user "$pleskxml_user"
_saveaccountconf_mutable pleskxml_pass "$pleskxml_pass"
_debug "Test login to Plesk XML API successful. Login credentials and URI successfully saved to the acme.sh configuration file for future use."
pleskxml_init_checks_done=1
return 0
}
# For a FQDN, identify the root domain managed by Plesk, its domain ID in Plesk, and the host if any.
_pleskxml_get_root_domain() {
_debug "Identifying DNS root domain for '$1' that is managed by the Plesk account."
# test if the domain is valid for splitting.
if _value "$root_domain_name" | grep -qvE '^[^.]+\.[^.]+\.[^.]'; then
### COMMENTED OUT ALSO FOR SAME REASON
### _err "Invalid domain. The ACME domain must contain at least three parts (aa.bb.tld) to identify a host, domain, and tld for the TXT record."
_err "Invalid domain. The ACME domain must contain at least two parts (aa.bb) to identify a domain and tld for the TXT record."
return 1
fi
_debug "Querying Plesk server for list of managed domains..."
_call_api "$pleskxml_tplt_get_domains"
if [ "$pleskxml_retcode" -ne 0 ]; then
return 1
fi
# Generate a hacked list of domains known to this Plesk account.
# We convert <ascii-name> tags to <name> so it'll flag on a hit with either <name> or <ascii-name> fields,
# for non-Western character sets.
# Output will be one line per known domain, containing 1 or 2 <name> tages and an <id> tag
# We don't actually need to check for type, name, *and* id, but it guarantees only usable lines are returned.
output="$( _api_response_split "$pleskxml_prettyprint_result" 'domain' '<type>domain</type>' | sed -E 's/<(\/?)ascii-name>/<\1name>/g' | grep '<name>' | grep '<id>' )"
_debug 'Domains managed by Plesk server are (ignore the hacked output):\n' "$output"
# loop and test if domain, or any parent domain, is managed by Plesk
# Loop until we don't have any '.' in the sring we're testing as a root domain
root_domain_name="$1"
doneloop=0
while _contains "$root_domain_name" '\.'; do
_debug "Checking if '$root_domain_name' is managed by the Plesk server..."
root_domain_id="$( _value "$output" | grep "<name>$root_domain_name</name>" | _head_n 1 | sed -E 's/^.*<id>([0-9]+)<\/id>.*$/\1/' )"
if [ -n "$root_domain_id" ]; then
# Found a match
# Note that a result with host = empty string is OK for this API, see
# https://docs.plesk.com/en-US/obsidian/api-rpc/about-xml-api/reference/managing-dns/managing-dns-records/adding-dns-record.34798
# See notes at top of this file
sub_domain_name="$( _value "$1" | sed -E "s/\.?${root_domain_name}"'$//' )"
_info "Matched host '$1' to: DOMAIN '${root_domain_name}' (Plesk ID '${root_domain_id}'), HOST '${sub_domain_name}'. Returning."
return 0
fi
# No match, try next parent up (if any)...
if _contains "$root_domain_name" '\.[^.]+\.'; then
_debug "No match, trying next parent up..."
else
_debug "No match,and next parent would be a TLD..."
fi
root_domain_name="$( _valuecut 2 1000 "$root_domain_name" )"
doneloop=1
done
# if we get here, we failed to find a root domain match in the list of domains managed by Plesk.
# if we never ran the loop a first time, $1 wasn't at least a 2 level domain (domain.tld) and wasn't valid anyway
if [ -z $doneloop ]; then
_err "'$1' isn't a valid domain for ACME DNS. Exiting."
else
_err "Cannot find '$1' or any parent domain of it, in Plesk."
_err "Are you sure that this domain is managed by this Plesk server?"
fi
return 1
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment