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