| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- #!/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"
- }
|