dns_bhosted.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. #!/usr/bin/env sh
  2. # shellcheck disable=SC2034
  3. dns_bhosted_info='bHosted.nl DNS API
  4. Site: bHosted.nl
  5. Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bhosted
  6. Options:
  7. BHOSTED_Username API username
  8. BHOSTED_Password API password (MD5 hash like bHosted web services example)
  9. BHOSTED_TTL TTL for TXT record (default: 300)
  10. BHOSTED_SLD Optional override (useful for multi-part TLDs like co.uk)
  11. BHOSTED_TLD Optional override (useful for multi-part TLDs like co.uk)
  12. Notes:
  13. - Plugin uses addrecord + delrecord for DNS-01 challenge
  14. - Record ID is retrieved from addrecord XML response and cached for cleanup
  15. '
  16. BHOSTED_API_ROOT="https://webservices.bhosted.com/dns"
  17. ############ Public functions #####################
  18. # Usage: dns_bhosted_add _acme-challenge.www.example.com "txt-value"
  19. dns_bhosted_add() {
  20. fulldomain="$1"
  21. txtvalue="$2"
  22. _debug "fulldomain" "$fulldomain"
  23. _debug "txtvalue" "$txtvalue"
  24. _bhosted_load_credentials || return 1
  25. _bhosted_get_root "$fulldomain" || return 1
  26. _info "Adding TXT record: ${_bhosted_name}.${_domain}"
  27. BHOSTED_TTL="${BHOSTED_TTL:-$(_readaccountconf_mutable BHOSTED_TTL)}"
  28. BHOSTED_TTL="${BHOSTED_TTL:-300}"
  29. _saveaccountconf_mutable BHOSTED_TTL "$BHOSTED_TTL"
  30. _bhosted_api_add_txt "$_bhosted_sld" "$_bhosted_tld" "$_bhosted_name" "$txtvalue" "$BHOSTED_TTL" || return 1
  31. # Extract and cache record id in-memory for cleanup in this run
  32. _rec_id="$(_bhosted_extract_id "$response")"
  33. if [ -n "$_rec_id" ]; then
  34. _hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")"
  35. _debug "_hash" "$_hash"
  36. _debug "_rec_id" "$_rec_id"
  37. _bhosted_mem_set_id "$_hash" "$_rec_id"
  38. else
  39. _err "TXT record added but no record id found in response."
  40. _err "Cleanup may fail unless bHosted addrecord returns <id>...</id>."
  41. _debug2 "add response" "$response"
  42. return 1
  43. fi
  44. return 0
  45. }
  46. # Usage: dns_bhosted_rm _acme-challenge.www.example.com "txt-value"
  47. dns_bhosted_rm() {
  48. fulldomain="$1"
  49. txtvalue="$2"
  50. _debug "fulldomain" "$fulldomain"
  51. _debug "txtvalue" "$txtvalue"
  52. _bhosted_load_credentials || return 1
  53. _bhosted_get_root "$fulldomain" || return 1
  54. _hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")"
  55. _rec_id="$(_bhosted_mem_get_id "$_hash")"
  56. if [ -z "$_rec_id" ]; then
  57. _err "No cached bHosted record id found for cleanup."
  58. _err "Please delete TXT manually in bHosted DNS for: ${_bhosted_name}.${_domain}"
  59. return 1
  60. fi
  61. _info "Removing TXT record id=${_rec_id}: ${_bhosted_name}.${_domain}"
  62. _bhosted_api_del_record "$_bhosted_sld" "$_bhosted_tld" "$_rec_id" || return 1
  63. return 0
  64. }
  65. ######## Private functions #####################
  66. _bhosted_load_credentials() {
  67. BHOSTED_Username="${BHOSTED_Username:-$(_readaccountconf_mutable BHOSTED_Username)}"
  68. BHOSTED_Password="${BHOSTED_Password:-$(_readaccountconf_mutable BHOSTED_Password)}"
  69. if [ -z "$BHOSTED_Username" ] || [ -z "$BHOSTED_Password" ]; then
  70. BHOSTED_Username=""
  71. BHOSTED_Password=""
  72. _err "You didn't specify bHosted credentials."
  73. _err "Please export BHOSTED_Username and BHOSTED_Password (MD5 hash)."
  74. return 1
  75. fi
  76. _saveaccountconf_mutable BHOSTED_Username "$BHOSTED_Username"
  77. _saveaccountconf_mutable BHOSTED_Password "$BHOSTED_Password"
  78. return 0
  79. }
  80. # Determine root zone and host part
  81. # Supports simple domains automatically (example.com, example.nl)
  82. # For multi-part TLDs (example.co.uk), set:
  83. # BHOSTED_SLD=example
  84. # BHOSTED_TLD=co.uk
  85. _bhosted_get_root() {
  86. domain="$1"
  87. BHOSTED_SLD="${BHOSTED_SLD:-$(_readdomainconf BHOSTED_SLD)}"
  88. BHOSTED_TLD="${BHOSTED_TLD:-$(_readdomainconf BHOSTED_TLD)}"
  89. if [ -n "$BHOSTED_SLD" ] && [ -n "$BHOSTED_TLD" ]; then
  90. _savedomainconf BHOSTED_SLD "$BHOSTED_SLD"
  91. _savedomainconf BHOSTED_TLD "$BHOSTED_TLD"
  92. _domain="${BHOSTED_SLD}.${BHOSTED_TLD}"
  93. case "$domain" in
  94. *."$_domain") ;;
  95. "$_domain") ;;
  96. *)
  97. _err "BHOSTED_SLD/BHOSTED_TLD do not match requested domain: $domain"
  98. return 1
  99. ;;
  100. esac
  101. _bhosted_sld="$BHOSTED_SLD"
  102. _bhosted_tld="$BHOSTED_TLD"
  103. _bhosted_name="${domain%."$_domain"}"
  104. if [ "$_bhosted_name" = "$domain" ]; then
  105. _bhosted_name=""
  106. fi
  107. [ -n "$_bhosted_name" ] || _bhosted_name="@"
  108. _debug "_domain" "$_domain"
  109. _debug "_bhosted_sld" "$_bhosted_sld"
  110. _debug "_bhosted_tld" "$_bhosted_tld"
  111. _debug "_bhosted_name" "$_bhosted_name"
  112. return 0
  113. fi
  114. # Auto-parse: assume last label = tld, label before = sld
  115. # Works for .nl / .com / .org etc.
  116. _bhosted_tld="$(printf "%s" "$domain" | awk -F. '{print $NF}')"
  117. _bhosted_sld="$(printf "%s" "$domain" | awk -F. '{print $(NF-1)}')"
  118. if [ -z "$_bhosted_sld" ] || [ -z "$_bhosted_tld" ]; then
  119. _err "Could not parse SLD/TLD from domain: $domain"
  120. return 1
  121. fi
  122. _domain="${_bhosted_sld}.${_bhosted_tld}"
  123. _bhosted_name="${domain%."$_domain"}"
  124. if [ "$_bhosted_name" = "$domain" ]; then
  125. _bhosted_name=""
  126. fi
  127. [ -n "$_bhosted_name" ] || _bhosted_name="@"
  128. _debug "_domain" "$_domain"
  129. _debug "_bhosted_sld" "$_bhosted_sld"
  130. _debug "_bhosted_tld" "$_bhosted_tld"
  131. _debug "_bhosted_name" "$_bhosted_name"
  132. return 0
  133. }
  134. _bhosted_api_add_txt() {
  135. _sld="$1"
  136. _tld="$2"
  137. _name="$3"
  138. _content="$4"
  139. _ttl="$5"
  140. _u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)"
  141. _u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)"
  142. _u_sld="$(printf "%s" "$_sld" | _url_encode)"
  143. _u_tld="$(printf "%s" "$_tld" | _url_encode)"
  144. _u_name="$(printf "%s" "$_name" | _url_encode)"
  145. _u_content="$(printf "%s" "$_content" | _url_encode)"
  146. _u_ttl="$(printf "%s" "$_ttl" | _url_encode)"
  147. _data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&type=TXT&name=${_u_name}&content=${_u_content}&ttl=${_u_ttl}"
  148. _debug "bHosted add endpoint" "${BHOSTED_API_ROOT}/addrecord"
  149. response="$(_post "$_data" "${BHOSTED_API_ROOT}/addrecord")"
  150. _ret="$?"
  151. _debug2 "bHosted add response" "$response"
  152. if [ "$_ret" != "0" ]; then
  153. _err "bHosted addrecord request failed"
  154. return 1
  155. fi
  156. if _bhosted_response_has_error "$response"; then
  157. _err "bHosted addrecord returned an error"
  158. _debug2 "response" "$response"
  159. return 1
  160. fi
  161. return 0
  162. }
  163. _bhosted_api_del_record() {
  164. _sld="$1"
  165. _tld="$2"
  166. _id="$3"
  167. _u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)"
  168. _u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)"
  169. _u_sld="$(printf "%s" "$_sld" | _url_encode)"
  170. _u_tld="$(printf "%s" "$_tld" | _url_encode)"
  171. _u_id="$(printf "%s" "$_id" | _url_encode)"
  172. _url="${BHOSTED_API_ROOT}/delrecord"
  173. _data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&id=${_u_id}"
  174. _debug "bHosted delete endpoint" "$_url"
  175. response="$(_post "$_data" "$_url")"
  176. _ret="$?"
  177. _debug2 "bHosted delete response" "$response"
  178. if [ "$_ret" != "0" ]; then
  179. _err "bHosted delrecord request failed"
  180. return 1
  181. fi
  182. if _bhosted_response_has_error "$response"; then
  183. _err "bHosted delrecord returned an error"
  184. _debug2 "response" "$response"
  185. return 1
  186. fi
  187. return 0
  188. }
  189. # Extract XML tag value from response, e.g. <id>12345</id>
  190. _bhosted_xml_value() {
  191. _tag="$1"
  192. _resp="$2"
  193. # Flatten response to simplify parsing
  194. _flat="$(printf "%s" "$_resp" | tr -d '\r\n\t')"
  195. printf "%s" "$_flat" | sed -n "s:.*<${_tag}>\\([^<]*\\)</${_tag}>.*:\\1:p" | _head_n 1
  196. }
  197. # Return code convention:
  198. # return 0 => response HAS error
  199. # return 1 => response has NO error (success)
  200. _bhosted_response_has_error() {
  201. _resp="$1"
  202. # Empty response = error
  203. if [ -z "$_resp" ]; then
  204. _debug "Empty API response"
  205. return 0
  206. fi
  207. # Prefer explicit bHosted XML response fields
  208. if _contains "$_resp" "<response>"; then
  209. _errors="$(_bhosted_xml_value "errors" "$_resp")"
  210. _done="$(_bhosted_xml_value "done" "$_resp")"
  211. _subcommand="$(_bhosted_xml_value "subcommand" "$_resp")"
  212. _id="$(_bhosted_xml_value "id" "$_resp")"
  213. _debug "bHosted XML subcommand" "$_subcommand"
  214. _debug "bHosted XML id" "$_id"
  215. _debug "bHosted XML errors" "$_errors"
  216. _debug "bHosted XML done" "$_done"
  217. # Success according to provided format
  218. if [ "$_errors" = "0" ] && [ "$_done" = "true" ]; then
  219. return 1
  220. fi
  221. _debug "bHosted XML indicates failure"
  222. return 0
  223. fi
  224. # Fallback for unexpected/non-XML responses
  225. _resp_lc="$(_lower_case "$_resp")"
  226. if _contains "$_resp_lc" "error"; then
  227. _debug "Detected 'error' in response"
  228. return 0
  229. fi
  230. if _contains "$_resp_lc" "fout"; then
  231. _debug "Detected 'fout' in response"
  232. return 0
  233. fi
  234. if _contains "$_resp_lc" "invalid"; then
  235. _debug "Detected 'invalid' in response"
  236. return 0
  237. fi
  238. if _contains "$_resp_lc" "failed"; then
  239. _debug "Detected 'failed' in response"
  240. return 0
  241. fi
  242. if _contains "$_resp_lc" "denied"; then
  243. _debug "Detected 'denied' in response"
  244. return 0
  245. fi
  246. # If no explicit error markers found, assume success
  247. return 1
  248. }
  249. # Extract record id from response
  250. # Supports bHosted XML first, then generic fallbacks
  251. _bhosted_extract_id() {
  252. _resp="$1"
  253. # bHosted XML: <id>12345</id>
  254. _id="$(_bhosted_xml_value "id" "$_resp" | tr -cd '0-9')"
  255. if [ -n "$_id" ]; then
  256. printf "%s" "$_id"
  257. return 0
  258. fi
  259. # JSON: "id":12345
  260. _id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
  261. if [ -n "$_id" ]; then
  262. printf "%s" "$_id"
  263. return 0
  264. fi
  265. # key=value: id=12345
  266. _id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
  267. if [ -n "$_id" ]; then
  268. printf "%s" "$_id"
  269. return 0
  270. fi
  271. # "record id 12345" / "recordid 12345"
  272. _id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
  273. if [ -n "$_id" ]; then
  274. printf "%s" "$_id"
  275. return 0
  276. fi
  277. return 1
  278. }
  279. # Create a unique config key for cached record ids
  280. _bhosted_cache_hash() {
  281. _fd="$1"
  282. _tv="$2"
  283. # md5 hex of fulldomain|txtvalue
  284. printf "%s|%s" "$_fd" "$_tv" | _digest md5 hex
  285. }
  286. _bhosted_cache_key() {
  287. _hash="$1"
  288. printf "%s" "BHOSTED_TXT_ID_${_hash}"
  289. }
  290. _bhosted_mem_set_id() {
  291. _hash="$1"
  292. _id="$2"
  293. _key="$(_bhosted_cache_key "$_hash")"
  294. _savedomainconf "$_key" "$_id"
  295. }
  296. _bhosted_mem_get_id() {
  297. _hash="$1"
  298. _key="$(_bhosted_cache_key "$_hash")"
  299. _readdomainconf "$_key"
  300. }