dns_sotoon.sh 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. #!/usr/bin/env sh
  2. # shellcheck disable=SC2034
  3. dns_sotoon_info='Sotoon.ir
  4. Site: Sotoon.ir
  5. Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon
  6. Options:
  7. Sotoon_Token API Token
  8. Sotoon_WorkspaceUUID Workspace UUID
  9. Issues: github.com/acmesh-official/acme.sh/issues/6656
  10. Author: Erfan Gholizade
  11. '
  12. SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/global"
  13. ######## Public functions #####################
  14. #Adding the txt record for validation.
  15. #Usage: dns_sotoon_add fulldomain TXT_record
  16. #Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
  17. dns_sotoon_add() {
  18. fulldomain=$1
  19. txtvalue=$2
  20. _info_sotoon "Using Sotoon"
  21. Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
  22. Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
  23. if [ -z "$Sotoon_Token" ]; then
  24. _err_sotoon "You didn't specify \"Sotoon_Token\" token yet."
  25. _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens"
  26. return 1
  27. fi
  28. if [ -z "$Sotoon_WorkspaceUUID" ]; then
  29. _err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet."
  30. _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces"
  31. return 1
  32. fi
  33. #save the info to the account conf file.
  34. _saveaccountconf_mutable Sotoon_Token "$Sotoon_Token"
  35. _saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID"
  36. _debug_sotoon "First detect the root zone"
  37. if ! _get_root "$fulldomain"; then
  38. _err_sotoon "invalid domain"
  39. return 1
  40. fi
  41. _info_sotoon "Adding record"
  42. _debug_sotoon _domain_id "$_domain_id"
  43. _debug_sotoon _sub_domain "$_sub_domain"
  44. _debug_sotoon _domain "$_domain"
  45. # First, GET the current domain zone to check for existing TXT records
  46. # This is needed for wildcard certs which require multiple TXT values
  47. _info_sotoon "Checking for existing TXT records"
  48. if ! _sotoon_rest GET "$_domain_id"; then
  49. _err_sotoon "Failed to get domain zone"
  50. return 1
  51. fi
  52. # Check if there are existing TXT records for this subdomain
  53. _existing_txt=""
  54. if _contains "$response" "\"$_sub_domain\""; then
  55. _debug_sotoon "Found existing records for $_sub_domain"
  56. # Extract existing TXT values from the response
  57. # The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}]
  58. _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
  59. _debug_sotoon "Existing TXT records: $_existing_txt"
  60. fi
  61. # Build the new record entry
  62. _new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}"
  63. # If there are existing records, append to them; otherwise create new array
  64. if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then
  65. # Check if this exact TXT value already exists (avoid duplicates)
  66. if _contains "$_existing_txt" "\"$txtvalue\""; then
  67. _info_sotoon "TXT record already exists, skipping"
  68. return 0
  69. fi
  70. # Remove the closing bracket and append new record
  71. _combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]"
  72. _debug_sotoon "Combined records: $_combined_records"
  73. else
  74. # No existing records, create new array
  75. _combined_records="[$_new_record]"
  76. fi
  77. # Prepare the DNS record data in Kubernetes CRD format
  78. _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}"
  79. _debug_sotoon "DNS record payload: $_dns_record"
  80. # Use PATCH to update/add the record to the domain zone
  81. _info_sotoon "Updating domain zone $_domain_id with TXT record"
  82. if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
  83. if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then
  84. _info_sotoon "Added, OK"
  85. return 0
  86. else
  87. _debug_sotoon "Response: $response"
  88. _err_sotoon "Add txt record error."
  89. return 1
  90. fi
  91. fi
  92. _err_sotoon "Add txt record error."
  93. return 1
  94. }
  95. #Remove the txt record after validation.
  96. #Usage: dns_sotoon_rm fulldomain TXT_record
  97. #Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
  98. dns_sotoon_rm() {
  99. fulldomain=$1
  100. txtvalue=$2
  101. _info_sotoon "Using Sotoon"
  102. _debug_sotoon fulldomain "$fulldomain"
  103. _debug_sotoon txtvalue "$txtvalue"
  104. Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
  105. Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
  106. _debug_sotoon "First detect the root zone"
  107. if ! _get_root "$fulldomain"; then
  108. _err_sotoon "invalid domain"
  109. return 1
  110. fi
  111. _debug_sotoon _domain_id "$_domain_id"
  112. _debug_sotoon _sub_domain "$_sub_domain"
  113. _debug_sotoon _domain "$_domain"
  114. _info_sotoon "Removing TXT record"
  115. # First, GET the current domain zone to check for existing TXT records
  116. if ! _sotoon_rest GET "$_domain_id"; then
  117. _err_sotoon "Failed to get domain zone"
  118. return 1
  119. fi
  120. # Check if there are existing TXT records for this subdomain
  121. _existing_txt=""
  122. if _contains "$response" "\"$_sub_domain\""; then
  123. _debug_sotoon "Found existing records for $_sub_domain"
  124. _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
  125. _debug_sotoon "Existing TXT records: $_existing_txt"
  126. fi
  127. # If no existing records, nothing to remove
  128. if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then
  129. _info_sotoon "No TXT records found, nothing to remove"
  130. return 0
  131. fi
  132. # Remove the specific TXT value from the array
  133. # This handles the case where there are multiple TXT values (wildcard certs)
  134. _remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g')
  135. _debug_sotoon "Remaining records after removal: $_remaining_records"
  136. # If no records remain, set to null to remove the subdomain entirely
  137. if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then
  138. _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}"
  139. else
  140. _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}"
  141. fi
  142. _debug_sotoon "Remove record payload: $_dns_record"
  143. # Use PATCH to remove the record from the domain zone
  144. if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
  145. _info_sotoon "Record removed, OK"
  146. return 0
  147. else
  148. _debug_sotoon "Response: $response"
  149. _err_sotoon "Error removing record"
  150. return 1
  151. fi
  152. }
  153. #################### Private functions below ##################################
  154. _get_root() {
  155. domain=$1
  156. i=1
  157. p=1
  158. _debug_sotoon "Getting root domain for: $domain"
  159. _debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID"
  160. while true; do
  161. h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
  162. _debug_sotoon "Checking domain part: $h"
  163. if [ -z "$h" ]; then
  164. #not valid
  165. _err_sotoon "Could not find valid domain"
  166. return 1
  167. fi
  168. _debug_sotoon "Fetching domain zones from Sotoon API"
  169. if ! _sotoon_rest GET ""; then
  170. _err_sotoon "Failed to get domain zones from Sotoon API"
  171. _err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID"
  172. return 1
  173. fi
  174. _debug2_sotoon "API Response: $response"
  175. # Check if the response contains our domain
  176. # Sotoon API uses Kubernetes CRD format with spec.origin for domain matching
  177. if _contains "$response" "\"origin\":\"$h\""; then
  178. _debug_sotoon "Found domain by origin: $h"
  179. # In Kubernetes CRD format, the metadata.name is the resource identifier
  180. # The name can be either:
  181. # 1. Same as origin
  182. # 2. Origin with dots replaced by hyphens
  183. # We check both patterns in the response to determine which one exists
  184. # Convert origin to hyphenated version for checking
  185. _h_hyphenated=$(echo "$h" | tr '.' '-')
  186. # Check if the hyphenated name exists in the response
  187. if _contains "$response" "\"name\":\"$_h_hyphenated\""; then
  188. _domain_id="$_h_hyphenated"
  189. _debug_sotoon "Found domain ID (hyphenated): $_domain_id"
  190. # Check if the origin itself is used as name
  191. elif _contains "$response" "\"name\":\"$h\""; then
  192. _domain_id="$h"
  193. _debug_sotoon "Found domain ID (same as origin): $_domain_id"
  194. else
  195. # Fallback: use the hyphenated version (more common)
  196. _domain_id="$_h_hyphenated"
  197. _debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id"
  198. fi
  199. if [ -n "$_domain_id" ]; then
  200. _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
  201. _domain=$h
  202. _debug_sotoon "Domain ID (metadata.name): $_domain_id"
  203. _debug_sotoon "Sub domain: $_sub_domain"
  204. _debug_sotoon "Domain (origin): $_domain"
  205. return 0
  206. fi
  207. _err_sotoon "Found domain $h but could not extract domain ID"
  208. return 1
  209. fi
  210. p=$i
  211. i=$(_math "$i" + 1)
  212. done
  213. return 1
  214. }
  215. _sotoon_rest() {
  216. mtd="$1"
  217. resource_id="$2"
  218. data="$3"
  219. token_trimmed=$(echo "$Sotoon_Token" | tr -d '"')
  220. # Construct the API endpoint
  221. _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/domainzones"
  222. if [ -n "$resource_id" ]; then
  223. _api_path="$_api_path/$resource_id"
  224. fi
  225. _debug_sotoon "API Path: $_api_path"
  226. _debug_sotoon "Method: $mtd"
  227. # Set authorization header - Sotoon API uses Bearer token
  228. export _H1="Authorization: Bearer $token_trimmed"
  229. if [ "$mtd" = "GET" ]; then
  230. # GET request
  231. _debug_sotoon "GET" "$_api_path"
  232. response="$(_get "$_api_path")"
  233. elif [ "$mtd" = "PATCH" ]; then
  234. # PATCH Request
  235. export _H2="Content-Type: application/merge-patch+json"
  236. _debug_sotoon data "$data"
  237. response="$(_post "$data" "$_api_path" "" "$mtd")"
  238. else
  239. _err_sotoon "Unknown method: $mtd"
  240. return 1
  241. fi
  242. _debug2_sotoon response "$response"
  243. return 0
  244. }
  245. #Wrappers for logging
  246. _info_sotoon() {
  247. _info "[Sotoon]" "$@"
  248. }
  249. _err_sotoon() {
  250. _err "[Sotoon]" "$@"
  251. }
  252. _debug_sotoon() {
  253. _debug "[Sotoon]" "$@"
  254. }
  255. _debug2_sotoon() {
  256. _debug2 "[Sotoon]" "$@"
  257. }