You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

403 lines
11 KiB

  1. #!/usr/bin/env sh
  2. # shellcheck disable=SC2034
  3. dns_timeweb_info='Timeweb.Cloud
  4. Site: Timeweb.Cloud
  5. Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_timeweb
  6. Options:
  7. TW_Token API JWT token. Get it from the control panel at https://timeweb.cloud/my/api-keys
  8. Issues: github.com/acmesh-official/acme.sh/issues/5140
  9. Author: Nikolay Pronchev <https://github.com/nikolaypronchev>
  10. '
  11. TW_Api="https://api.timeweb.cloud/api/v1"
  12. ################ Public functions ################
  13. # Adds an ACME DNS-01 challenge DNS TXT record via the Timeweb Cloud API.
  14. #
  15. # Param1: The ACME DNS-01 challenge FQDN.
  16. # Param2: The value of the ACME DNS-01 challenge TXT record.
  17. #
  18. # Example: dns_timeweb_add "_acme-challenge.sub.domain.com" "D-52Wm...4uYM"
  19. dns_timeweb_add() {
  20. _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_add\" started."
  21. _timeweb_set_acme_fqdn "$1" || return 1
  22. _timeweb_set_acme_txt "$2" || return 1
  23. _timeweb_check_token || return 1
  24. _timeweb_split_acme_fqdn || return 1
  25. _timeweb_dns_txt_add || return 1
  26. _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_add\" finished."
  27. }
  28. # Removes a DNS TXT record via the Timeweb Cloud API.
  29. #
  30. # Param1: The ACME DNS-01 challenge FQDN.
  31. # Param2: The value of the ACME DNS-01 challenge TXT record.
  32. #
  33. # Example: dns_timeweb_rm "_acme-challenge.sub.domain.com" "D-52Wm...4uYM"
  34. dns_timeweb_rm() {
  35. _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_rm\" started."
  36. _timeweb_set_acme_fqdn "$1" || return 1
  37. _timeweb_set_acme_txt "$2" || return 1
  38. _timeweb_check_token || return 1
  39. _timeweb_split_acme_fqdn || return 1
  40. _timeweb_get_dns_txt || return 1
  41. _timeweb_dns_txt_remove || return 1
  42. _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_rm\" finished."
  43. }
  44. ################ Private functions ################
  45. # Checks and sets the ACME DNS-01 challenge FQDN.
  46. #
  47. # Param1: The ACME DNS-01 challenge FQDN.
  48. #
  49. # Example: _timeweb_set_acme_fqdn "_acme-challenge.sub.domain.com"
  50. #
  51. # Sets the "Acme_Fqdn" variable (_acme-challenge.sub.domain.com)
  52. _timeweb_set_acme_fqdn() {
  53. Acme_Fqdn=$1
  54. _debug "Setting ACME DNS-01 challenge FQDN \"$Acme_Fqdn\"."
  55. [ -z "$Acme_Fqdn" ] && {
  56. _err "ACME DNS-01 challenge FQDN is empty."
  57. return 1
  58. }
  59. return 0
  60. }
  61. # Checks and sets the value of the ACME DNS-01 challenge TXT record.
  62. #
  63. # Param1: Value of the ACME DNS-01 challenge TXT record.
  64. #
  65. # Example: _timeweb_set_acme_txt "D-52Wm...4uYM"
  66. #
  67. # Sets the "Acme_Txt" variable to the provided value (D-52Wm...4uYM)
  68. _timeweb_set_acme_txt() {
  69. Acme_Txt=$1
  70. _debug "Setting the value of the ACME DNS-01 challenge TXT record to \"$Acme_Txt\"."
  71. [ -z "$Acme_Txt" ] && {
  72. _err "ACME DNS-01 challenge TXT record value is empty."
  73. return 1
  74. }
  75. return 0
  76. }
  77. # Checks if the Timeweb Cloud API JWT token is present (refer to the script description).
  78. # Adds or updates the token in the acme.sh account configuration.
  79. _timeweb_check_token() {
  80. _debug "Checking for the presence of the Timeweb Cloud API JWT token."
  81. TW_Token="${TW_Token:-$(_readaccountconf_mutable TW_Token)}"
  82. [ -z "$TW_Token" ] && {
  83. _err "Timeweb Cloud API JWT token was not found."
  84. return 1
  85. }
  86. _saveaccountconf_mutable TW_Token "$TW_Token"
  87. }
  88. # Divides the ACME DNS-01 challenge FQDN into its main domain and subdomain components.
  89. _timeweb_split_acme_fqdn() {
  90. _debug "Trying to divide \"$Acme_Fqdn\" into its main domain and subdomain components."
  91. TW_Page_Limit=100
  92. TW_Page_Offset=0
  93. TW_Domains_Returned=""
  94. while [ -z "$TW_Domains_Returned" ] || [ "$TW_Domains_Returned" -ge "$TW_Page_Limit" ]; do
  95. _timeweb_list_domains "$TW_Page_Limit" "$TW_Page_Offset" || return 1
  96. # Remove the 'subdomains' subarray to prevent confusion with FQDNs.
  97. TW_Domains=$(
  98. echo "$TW_Domains" |
  99. sed 's/"subdomains":\[[^]]*]//g'
  100. )
  101. [ -z "$TW_Domains" ] && {
  102. _err "Failed to parse the list of domains."
  103. return 1
  104. }
  105. while
  106. TW_Domain=$(
  107. echo "$TW_Domains" |
  108. sed -n 's/.*{[^{]*"fqdn":"\([^"]*\)"[^}]*}.*/\1/p'
  109. )
  110. [ -n "$TW_Domain" ] && {
  111. _timeweb_is_main_domain "$TW_Domain" && return 0
  112. TW_Domains=$(
  113. echo "$TW_Domains" |
  114. sed 's/{\([^{]*"fqdn":"'"$TW_Domain"'"[^}]*\)}//'
  115. )
  116. continue
  117. }
  118. do :; done
  119. TW_Page_Offset=$(_math "$TW_Page_Offset" + "$TW_Page_Limit")
  120. done
  121. _err "Failed to divide \"$Acme_Fqdn\" into its main domain and subdomain components."
  122. return 1
  123. }
  124. # Searches for a previously added DNS TXT record.
  125. #
  126. # Sets the "TW_Dns_Txt_Id" variable.
  127. _timeweb_get_dns_txt() {
  128. _debug "Trying to locate a DNS TXT record with the value \"$Acme_Txt\"."
  129. TW_Page_Limit=100
  130. TW_Page_Offset=0
  131. TW_Dns_Records_Returned=""
  132. while [ -z "$TW_Dns_Records_Returned" ] || [ "$TW_Dns_Records_Returned" -ge "$TW_Page_Limit" ]; do
  133. _timeweb_list_dns_records "$TW_Page_Limit" "$TW_Page_Offset" || return 1
  134. while
  135. Dns_Record=$(
  136. echo "$TW_Dns_Records" |
  137. sed -n 's/.*{\([^{]*{[^{]*'"$Acme_Txt"'[^}]*}[^}]*\)}.*/\1/p'
  138. )
  139. [ -n "$Dns_Record" ] && {
  140. _timeweb_is_added_txt "$Dns_Record" && return 0
  141. TW_Dns_Records=$(
  142. echo "$TW_Dns_Records" |
  143. sed 's/{\([^{]*{[^{]*'"$Acme_Txt"'[^}]*}[^}]*\)}//'
  144. )
  145. continue
  146. }
  147. do :; done
  148. TW_Page_Offset=$(_math "$TW_Page_Offset" + "$TW_Page_Limit")
  149. done
  150. _err "DNS TXT record was not found."
  151. return 1
  152. }
  153. # Lists domains via the Timeweb Cloud API.
  154. #
  155. # Param 1: Limit for listed domains.
  156. # Param 2: Offset for domains list.
  157. #
  158. # Sets the "TW_Domains" variable.
  159. # Sets the "TW_Domains_Returned" variable.
  160. _timeweb_list_domains() {
  161. _debug "Listing domains via Timeweb Cloud API. Limit: $1, offset: $2."
  162. export _H1="Authorization: Bearer $TW_Token"
  163. if ! TW_Domains=$(_get "$TW_Api/domains?limit=$1&offset=$2"); then
  164. _err "The request to the Timeweb Cloud API failed."
  165. return 1
  166. fi
  167. [ -z "$TW_Domains" ] && {
  168. _err "Empty response from the Timeweb Cloud API."
  169. return 1
  170. }
  171. TW_Domains_Returned=$(
  172. echo "$TW_Domains" |
  173. sed 's/.*"meta":{"total":\([0-9]*\)[^0-9].*/\1/'
  174. )
  175. [ -z "$TW_Domains_Returned" ] && {
  176. _err "Failed to extract the total count of domains."
  177. return 1
  178. }
  179. [ "$TW_Domains_Returned" -eq "0" ] && {
  180. _err "Domains are missing."
  181. return 1
  182. }
  183. _debug "Domains returned by Timeweb Cloud API: $TW_Domains_Returned."
  184. }
  185. # Lists domain DNS records via the Timeweb Cloud API.
  186. #
  187. # Param 1: Limit for listed DNS records.
  188. # Param 2: Offset for DNS records list.
  189. #
  190. # Sets the "TW_Dns_Records" variable.
  191. # Sets the "TW_Dns_Records_Returned" variable.
  192. _timeweb_list_dns_records() {
  193. _debug "Listing domain DNS records via the Timeweb Cloud API. Limit: $1, offset: $2."
  194. export _H1="Authorization: Bearer $TW_Token"
  195. if ! TW_Dns_Records=$(_get "$TW_Api/domains/$TW_Main_Domain/dns-records?limit=$1&offset=$2"); then
  196. _err "The request to the Timeweb Cloud API failed."
  197. return 1
  198. fi
  199. [ -z "$TW_Dns_Records" ] && {
  200. _err "Empty response from the Timeweb Cloud API."
  201. return 1
  202. }
  203. TW_Dns_Records_Returned=$(
  204. echo "$TW_Dns_Records" |
  205. sed 's/.*"meta":{"total":\([0-9]*\)[^0-9].*/\1/'
  206. )
  207. [ -z "$TW_Dns_Records_Returned" ] && {
  208. _err "Failed to extract the total count of DNS records."
  209. return 1
  210. }
  211. [ "$TW_Dns_Records_Returned" -eq "0" ] && {
  212. _err "DNS records are missing."
  213. return 1
  214. }
  215. _debug "DNS records returned by Timeweb Cloud API: $TW_Dns_Records_Returned."
  216. }
  217. # Verifies whether the domain is the primary domain for the ACME DNS-01 challenge FQDN.
  218. # The requirement is that the provided domain is the top-level domain
  219. # for the ACME DNS-01 challenge FQDN.
  220. #
  221. # Param 1: Domain object returned by Timeweb Cloud API.
  222. #
  223. # Sets the "TW_Main_Domain" variable (e.g. "_acme-challenge.s1.domain.co.uk" → "domain.co.uk").
  224. # Sets the "TW_Subdomains" variable (e.g. "_acme-challenge.s1.domain.co.uk" → "_acme-challenge.s1").
  225. _timeweb_is_main_domain() {
  226. _debug "Checking if \"$1\" is the main domain of the ACME DNS-01 challenge FQDN."
  227. [ -z "$1" ] && {
  228. _debug "Failed to extract FQDN. Skipping domain."
  229. return 1
  230. }
  231. ! echo ".$Acme_Fqdn" | grep -qi "\.$1$" && {
  232. _debug "Domain does not match the ACME DNS-01 challenge FQDN. Skipping domain."
  233. return 1
  234. }
  235. TW_Main_Domain=$1
  236. TW_Subdomains=$(
  237. echo "$Acme_Fqdn" |
  238. sed "s/\.*.\{${#1}\}$//"
  239. )
  240. _debug "Matched domain. ACME DNS-01 challenge FQDN split as [$TW_Subdomains].[$TW_Main_Domain]."
  241. return 0
  242. }
  243. # Verifies whether a DNS record was previously added based on the following criteria:
  244. # - The value matches the ACME DNS-01 challenge TXT record value;
  245. # - The record type is TXT;
  246. # - The subdomain matches the ACME DNS-01 challenge FQDN.
  247. #
  248. # Param 1: DNS record object returned by Timeweb Cloud API.
  249. #
  250. # Sets the "TW_Dns_Txt_Id" variable.
  251. _timeweb_is_added_txt() {
  252. _debug "Checking if \"$1\" is a previously added DNS TXT record."
  253. echo "$1" | grep -qv '"type":"TXT"' && {
  254. _debug "Not a TXT record. Skipping the record."
  255. return 1
  256. }
  257. if [ -n "$TW_Subdomains" ]; then
  258. echo "$1" | grep -qvi "\"subdomain\":\"$TW_Subdomains\"" && {
  259. _debug "Subdomains do not match. Skipping the record."
  260. return 1
  261. }
  262. else
  263. echo "$1" | grep -q '"subdomain\":"..*"' && {
  264. _debug "Subdomains do not match. Skipping the record."
  265. return 1
  266. }
  267. fi
  268. TW_Dns_Txt_Id=$(
  269. echo "$1" |
  270. sed 's/.*"id":\([0-9]*\)[^0-9].*/\1/'
  271. )
  272. [ -z "$TW_Dns_Txt_Id" ] && {
  273. _debug "Failed to extract the DNS record ID. Skipping the record."
  274. return 1
  275. }
  276. _debug "Matching DNS TXT record ID is \"$TW_Dns_Txt_Id\"."
  277. return 0
  278. }
  279. # Adds a DNS TXT record via the Timeweb Cloud API.
  280. _timeweb_dns_txt_add() {
  281. _debug "Adding a new DNS TXT record via the Timeweb Cloud API."
  282. export _H1="Authorization: Bearer $TW_Token"
  283. export _H2="Content-Type: application/json"
  284. if ! TW_Response=$(
  285. _post "{
  286. \"subdomain\":\"$TW_Subdomains\",
  287. \"type\":\"TXT\",
  288. \"value\":\"$Acme_Txt\"
  289. }" \
  290. "$TW_Api/domains/$TW_Main_Domain/dns-records"
  291. ); then
  292. _err "The request to the Timeweb Cloud API failed."
  293. return 1
  294. fi
  295. [ -z "$TW_Response" ] && {
  296. _err "An unexpected empty response was received from the Timeweb Cloud API."
  297. return 1
  298. }
  299. TW_Dns_Txt_Id=$(
  300. echo "$TW_Response" |
  301. sed 's/.*"id":\([0-9]*\)[^0-9].*/\1/'
  302. )
  303. [ -z "$TW_Dns_Txt_Id" ] && {
  304. _err "Failed to extract the DNS TXT Record ID."
  305. return 1
  306. }
  307. _debug "DNS TXT record has been added. ID: \"$TW_Dns_Txt_Id\"."
  308. }
  309. # Removes a DNS record via the Timeweb Cloud API.
  310. _timeweb_dns_txt_remove() {
  311. _debug "Removing DNS record via the Timeweb Cloud API."
  312. export _H1="Authorization: Bearer $TW_Token"
  313. if ! TW_Response=$(
  314. _post \
  315. "" \
  316. "$TW_Api/domains/$TW_Main_Domain/dns-records/$TW_Dns_Txt_Id" \
  317. "" \
  318. "DELETE"
  319. ); then
  320. _err "The request to the Timeweb Cloud API failed."
  321. return 1
  322. fi
  323. [ -n "$TW_Response" ] && {
  324. _err "Received an unexpected response body from the Timeweb Cloud API."
  325. return 1
  326. }
  327. _debug "DNS TXT record with ID \"$TW_Dns_Txt_Id\" has been removed."
  328. }