Skip to content

Commit b88ac77

Browse files
committed
Added Sotoon dns api
shfmt sotoon dns api shfmt Sotoon dns api
1 parent 40290ad commit b88ac77

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed

dnsapi/dns_sotoon.sh

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
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

Comments
 (0)