mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-05-18 11:53:56 +08:00
374 lines
10 KiB
Bash
374 lines
10 KiB
Bash
#!/usr/bin/env sh
|
|
|
|
# shellcheck disable=SC2034
|
|
dns_bhosted_info='bHosted.nl DNS API
|
|
Site: bHosted.nl
|
|
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bhosted
|
|
Options:
|
|
BHOSTED_Username API username
|
|
BHOSTED_Password API password (MD5 hash like bHosted web services example)
|
|
BHOSTED_TTL TTL for TXT record (default: 300)
|
|
BHOSTED_SLD Optional override (useful for multi-part TLDs like co.uk)
|
|
BHOSTED_TLD Optional override (useful for multi-part TLDs like co.uk)
|
|
Notes:
|
|
- Plugin uses addrecord + delrecord for DNS-01 challenge
|
|
- Record ID is retrieved from addrecord XML response and cached for cleanup
|
|
'
|
|
|
|
BHOSTED_API_ROOT="https://webservices.bhosted.com/dns"
|
|
|
|
############ Public functions #####################
|
|
|
|
# Usage: dns_bhosted_add _acme-challenge.www.example.com "txt-value"
|
|
dns_bhosted_add() {
|
|
fulldomain="$1"
|
|
txtvalue="$2"
|
|
|
|
_debug "fulldomain" "$fulldomain"
|
|
_debug "txtvalue" "$txtvalue"
|
|
|
|
_bhosted_load_credentials || return 1
|
|
_bhosted_get_root "$fulldomain" || return 1
|
|
|
|
_info "Adding TXT record: ${_bhosted_name}.${_domain}"
|
|
|
|
BHOSTED_TTL="${BHOSTED_TTL:-$(_readaccountconf_mutable BHOSTED_TTL)}"
|
|
BHOSTED_TTL="${BHOSTED_TTL:-300}"
|
|
_saveaccountconf_mutable BHOSTED_TTL "$BHOSTED_TTL"
|
|
|
|
_bhosted_api_add_txt "$_bhosted_sld" "$_bhosted_tld" "$_bhosted_name" "$txtvalue" "$BHOSTED_TTL" || return 1
|
|
|
|
# Extract and cache record id in-memory for cleanup in this run
|
|
_rec_id="$(_bhosted_extract_id "$response")"
|
|
if [ -n "$_rec_id" ]; then
|
|
_hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")"
|
|
_debug "_hash" "$_hash"
|
|
_debug "_rec_id" "$_rec_id"
|
|
_bhosted_mem_set_id "$_hash" "$_rec_id"
|
|
else
|
|
_err "TXT record added but no record id found in response."
|
|
_err "Cleanup may fail unless bHosted addrecord returns <id>...</id>."
|
|
_debug2 "add response" "$response"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Usage: dns_bhosted_rm _acme-challenge.www.example.com "txt-value"
|
|
dns_bhosted_rm() {
|
|
fulldomain="$1"
|
|
txtvalue="$2"
|
|
|
|
_debug "fulldomain" "$fulldomain"
|
|
_debug "txtvalue" "$txtvalue"
|
|
|
|
_bhosted_load_credentials || return 1
|
|
_bhosted_get_root "$fulldomain" || return 1
|
|
|
|
_hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")"
|
|
_rec_id="$(_bhosted_mem_get_id "$_hash")"
|
|
|
|
if [ -z "$_rec_id" ]; then
|
|
_err "No cached bHosted record id found for cleanup."
|
|
_err "Please delete TXT manually in bHosted DNS for: ${_bhosted_name}.${_domain}"
|
|
return 1
|
|
fi
|
|
|
|
_info "Removing TXT record id=${_rec_id}: ${_bhosted_name}.${_domain}"
|
|
_bhosted_api_del_record "$_bhosted_sld" "$_bhosted_tld" "$_rec_id" || return 1
|
|
|
|
return 0
|
|
}
|
|
|
|
######## Private functions #####################
|
|
|
|
_bhosted_load_credentials() {
|
|
BHOSTED_Username="${BHOSTED_Username:-$(_readaccountconf_mutable BHOSTED_Username)}"
|
|
BHOSTED_Password="${BHOSTED_Password:-$(_readaccountconf_mutable BHOSTED_Password)}"
|
|
|
|
if [ -z "$BHOSTED_Username" ] || [ -z "$BHOSTED_Password" ]; then
|
|
BHOSTED_Username=""
|
|
BHOSTED_Password=""
|
|
_err "You didn't specify bHosted credentials."
|
|
_err "Please export BHOSTED_Username and BHOSTED_Password (MD5 hash)."
|
|
return 1
|
|
fi
|
|
|
|
_saveaccountconf_mutable BHOSTED_Username "$BHOSTED_Username"
|
|
_saveaccountconf_mutable BHOSTED_Password "$BHOSTED_Password"
|
|
|
|
return 0
|
|
}
|
|
|
|
# Determine root zone and host part
|
|
# Supports simple domains automatically (example.com, example.nl)
|
|
# For multi-part TLDs (example.co.uk), set:
|
|
# BHOSTED_SLD=example
|
|
# BHOSTED_TLD=co.uk
|
|
_bhosted_get_root() {
|
|
domain="$1"
|
|
|
|
BHOSTED_SLD="${BHOSTED_SLD:-$(_readdomainconf BHOSTED_SLD)}"
|
|
BHOSTED_TLD="${BHOSTED_TLD:-$(_readdomainconf BHOSTED_TLD)}"
|
|
|
|
if [ -n "$BHOSTED_SLD" ] && [ -n "$BHOSTED_TLD" ]; then
|
|
_savedomainconf BHOSTED_SLD "$BHOSTED_SLD"
|
|
_savedomainconf BHOSTED_TLD "$BHOSTED_TLD"
|
|
|
|
_domain="${BHOSTED_SLD}.${BHOSTED_TLD}"
|
|
case "$domain" in
|
|
*."$_domain") ;;
|
|
"$_domain") ;;
|
|
*)
|
|
_err "BHOSTED_SLD/BHOSTED_TLD do not match requested domain: $domain"
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
_bhosted_sld="$BHOSTED_SLD"
|
|
_bhosted_tld="$BHOSTED_TLD"
|
|
_bhosted_name="${domain%."$_domain"}"
|
|
if [ "$_bhosted_name" = "$domain" ]; then
|
|
_bhosted_name=""
|
|
fi
|
|
|
|
[ -n "$_bhosted_name" ] || _bhosted_name="@"
|
|
|
|
_debug "_domain" "$_domain"
|
|
_debug "_bhosted_sld" "$_bhosted_sld"
|
|
_debug "_bhosted_tld" "$_bhosted_tld"
|
|
_debug "_bhosted_name" "$_bhosted_name"
|
|
return 0
|
|
fi
|
|
|
|
# Auto-parse: assume last label = tld, label before = sld
|
|
# Works for .nl / .com / .org etc.
|
|
_bhosted_tld="$(printf "%s" "$domain" | awk -F. '{print $NF}')"
|
|
_bhosted_sld="$(printf "%s" "$domain" | awk -F. '{print $(NF-1)}')"
|
|
|
|
if [ -z "$_bhosted_sld" ] || [ -z "$_bhosted_tld" ]; then
|
|
_err "Could not parse SLD/TLD from domain: $domain"
|
|
return 1
|
|
fi
|
|
|
|
_domain="${_bhosted_sld}.${_bhosted_tld}"
|
|
_bhosted_name="${domain%."$_domain"}"
|
|
if [ "$_bhosted_name" = "$domain" ]; then
|
|
_bhosted_name=""
|
|
fi
|
|
|
|
[ -n "$_bhosted_name" ] || _bhosted_name="@"
|
|
|
|
_debug "_domain" "$_domain"
|
|
_debug "_bhosted_sld" "$_bhosted_sld"
|
|
_debug "_bhosted_tld" "$_bhosted_tld"
|
|
_debug "_bhosted_name" "$_bhosted_name"
|
|
|
|
return 0
|
|
}
|
|
|
|
_bhosted_api_add_txt() {
|
|
_sld="$1"
|
|
_tld="$2"
|
|
_name="$3"
|
|
_content="$4"
|
|
_ttl="$5"
|
|
|
|
_u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)"
|
|
_u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)"
|
|
_u_sld="$(printf "%s" "$_sld" | _url_encode)"
|
|
_u_tld="$(printf "%s" "$_tld" | _url_encode)"
|
|
_u_name="$(printf "%s" "$_name" | _url_encode)"
|
|
_u_content="$(printf "%s" "$_content" | _url_encode)"
|
|
_u_ttl="$(printf "%s" "$_ttl" | _url_encode)"
|
|
|
|
_data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&type=TXT&name=${_u_name}&content=${_u_content}&ttl=${_u_ttl}"
|
|
|
|
_debug "bHosted add endpoint" "${BHOSTED_API_ROOT}/addrecord"
|
|
response="$(_post "$_data" "${BHOSTED_API_ROOT}/addrecord")"
|
|
_ret="$?"
|
|
|
|
_debug2 "bHosted add response" "$response"
|
|
|
|
if [ "$_ret" != "0" ]; then
|
|
_err "bHosted addrecord request failed"
|
|
return 1
|
|
fi
|
|
|
|
if _bhosted_response_has_error "$response"; then
|
|
_err "bHosted addrecord returned an error"
|
|
_debug2 "response" "$response"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
_bhosted_api_del_record() {
|
|
_sld="$1"
|
|
_tld="$2"
|
|
_id="$3"
|
|
|
|
_u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)"
|
|
_u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)"
|
|
_u_sld="$(printf "%s" "$_sld" | _url_encode)"
|
|
_u_tld="$(printf "%s" "$_tld" | _url_encode)"
|
|
_u_id="$(printf "%s" "$_id" | _url_encode)"
|
|
|
|
_url="${BHOSTED_API_ROOT}/delrecord"
|
|
_data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&id=${_u_id}"
|
|
|
|
_debug "bHosted delete endpoint" "$_url"
|
|
response="$(_post "$_data" "$_url")"
|
|
_ret="$?"
|
|
|
|
_debug2 "bHosted delete response" "$response"
|
|
|
|
if [ "$_ret" != "0" ]; then
|
|
_err "bHosted delrecord request failed"
|
|
return 1
|
|
fi
|
|
|
|
if _bhosted_response_has_error "$response"; then
|
|
_err "bHosted delrecord returned an error"
|
|
_debug2 "response" "$response"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Extract XML tag value from response, e.g. <id>12345</id>
|
|
_bhosted_xml_value() {
|
|
_tag="$1"
|
|
_resp="$2"
|
|
|
|
# Flatten response to simplify parsing
|
|
_flat="$(printf "%s" "$_resp" | tr -d '\r\n\t')"
|
|
printf "%s" "$_flat" | sed -n "s:.*<${_tag}>\\([^<]*\\)</${_tag}>.*:\\1:p" | _head_n 1
|
|
}
|
|
|
|
# Return code convention:
|
|
# return 0 => response HAS error
|
|
# return 1 => response has NO error (success)
|
|
_bhosted_response_has_error() {
|
|
_resp="$1"
|
|
|
|
# Empty response = error
|
|
if [ -z "$_resp" ]; then
|
|
_debug "Empty API response"
|
|
return 0
|
|
fi
|
|
|
|
# Prefer explicit bHosted XML response fields
|
|
if _contains "$_resp" "<response>"; then
|
|
_errors="$(_bhosted_xml_value "errors" "$_resp")"
|
|
_done="$(_bhosted_xml_value "done" "$_resp")"
|
|
_subcommand="$(_bhosted_xml_value "subcommand" "$_resp")"
|
|
_id="$(_bhosted_xml_value "id" "$_resp")"
|
|
|
|
_debug "bHosted XML subcommand" "$_subcommand"
|
|
_debug "bHosted XML id" "$_id"
|
|
_debug "bHosted XML errors" "$_errors"
|
|
_debug "bHosted XML done" "$_done"
|
|
|
|
# Success according to provided format
|
|
if [ "$_errors" = "0" ] && [ "$_done" = "true" ]; then
|
|
return 1
|
|
fi
|
|
|
|
_debug "bHosted XML indicates failure"
|
|
return 0
|
|
fi
|
|
|
|
# Fallback for unexpected/non-XML responses
|
|
_resp_lc="$(_lower_case "$_resp")"
|
|
|
|
if _contains "$_resp_lc" "error"; then
|
|
_debug "Detected 'error' in response"
|
|
return 0
|
|
fi
|
|
if _contains "$_resp_lc" "fout"; then
|
|
_debug "Detected 'fout' in response"
|
|
return 0
|
|
fi
|
|
if _contains "$_resp_lc" "invalid"; then
|
|
_debug "Detected 'invalid' in response"
|
|
return 0
|
|
fi
|
|
if _contains "$_resp_lc" "failed"; then
|
|
_debug "Detected 'failed' in response"
|
|
return 0
|
|
fi
|
|
if _contains "$_resp_lc" "denied"; then
|
|
_debug "Detected 'denied' in response"
|
|
return 0
|
|
fi
|
|
|
|
# If no explicit error markers found, assume success
|
|
return 1
|
|
}
|
|
|
|
# Extract record id from response
|
|
# Supports bHosted XML first, then generic fallbacks
|
|
_bhosted_extract_id() {
|
|
_resp="$1"
|
|
|
|
# bHosted XML: <id>12345</id>
|
|
_id="$(_bhosted_xml_value "id" "$_resp" | tr -cd '0-9')"
|
|
if [ -n "$_id" ]; then
|
|
printf "%s" "$_id"
|
|
return 0
|
|
fi
|
|
|
|
# JSON: "id":12345
|
|
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
|
if [ -n "$_id" ]; then
|
|
printf "%s" "$_id"
|
|
return 0
|
|
fi
|
|
|
|
# key=value: id=12345
|
|
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
|
if [ -n "$_id" ]; then
|
|
printf "%s" "$_id"
|
|
return 0
|
|
fi
|
|
|
|
# "record id 12345" / "recordid 12345"
|
|
_id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
|
if [ -n "$_id" ]; then
|
|
printf "%s" "$_id"
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Create a unique config key for cached record ids
|
|
_bhosted_cache_hash() {
|
|
_fd="$1"
|
|
_tv="$2"
|
|
# md5 hex of fulldomain|txtvalue
|
|
printf "%s|%s" "$_fd" "$_tv" | _digest md5 hex
|
|
}
|
|
|
|
_bhosted_cache_key() {
|
|
_hash="$1"
|
|
printf "%s" "BHOSTED_TXT_ID_${_hash}"
|
|
}
|
|
|
|
_bhosted_mem_set_id() {
|
|
_hash="$1"
|
|
_id="$2"
|
|
_key="$(_bhosted_cache_key "$_hash")"
|
|
_savedomainconf "$_key" "$_id"
|
|
}
|
|
|
|
_bhosted_mem_get_id() {
|
|
_hash="$1"
|
|
_key="$(_bhosted_cache_key "$_hash")"
|
|
_readdomainconf "$_key"
|
|
}
|