1 changed files with 300 additions and 0 deletions
@ -0,0 +1,300 @@ |
|||||
|
#!/usr/bin/env sh |
||||
|
# shellcheck disable=SC2034 |
||||
|
dns_sotoon_info='Sotoon.ir |
||||
|
Site: Sotoon.ir |
||||
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon |
||||
|
Options: |
||||
|
Sotoon_Token API Token |
||||
|
Sotoon_WorkspaceUUID Workspace UUID |
||||
|
Sotoon_WorkspaceName Workspace Name |
||||
|
Issues: github.com/acmesh-official/acme.sh/issues/6656 |
||||
|
Author: Erfan Gholizade |
||||
|
' |
||||
|
|
||||
|
SOTOON_API_URL="https://api.sotoon.ir/delivery/v2/global" |
||||
|
|
||||
|
######## Public functions ##################### |
||||
|
|
||||
|
#Adding the txt record for validation. |
||||
|
#Usage: dns_sotoon_add fulldomain TXT_record |
||||
|
#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
||||
|
dns_sotoon_add() { |
||||
|
fulldomain=$1 |
||||
|
txtvalue=$2 |
||||
|
_info_sotoon "Using Sotoon" |
||||
|
|
||||
|
Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" |
||||
|
Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" |
||||
|
Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" |
||||
|
|
||||
|
if [ -z "$Sotoon_Token" ]; then |
||||
|
_err_sotoon "You didn't specify \"Sotoon_Token\" token yet." |
||||
|
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens" |
||||
|
return 1 |
||||
|
fi |
||||
|
if [ -z "$Sotoon_WorkspaceUUID" ]; then |
||||
|
_err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet." |
||||
|
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" |
||||
|
return 1 |
||||
|
fi |
||||
|
if [ -z "$Sotoon_WorkspaceName" ]; then |
||||
|
_err_sotoon "You didn't specify \"Sotoon_WorkspaceName\" Workspace Name yet." |
||||
|
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
#save the info to the account conf file. |
||||
|
_saveaccountconf_mutable Sotoon_Token "$Sotoon_Token" |
||||
|
_saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID" |
||||
|
_saveaccountconf_mutable Sotoon_WorkspaceName "$Sotoon_WorkspaceName" |
||||
|
|
||||
|
_debug_sotoon "First detect the root zone" |
||||
|
if ! _get_root "$fulldomain"; then |
||||
|
_err_sotoon "invalid domain" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
_info_sotoon "Adding record" |
||||
|
|
||||
|
_debug_sotoon _domain_id "$_domain_id" |
||||
|
_debug_sotoon _sub_domain "$_sub_domain" |
||||
|
_debug_sotoon _domain "$_domain" |
||||
|
|
||||
|
# First, GET the current domain zone to check for existing TXT records |
||||
|
# This is needed for wildcard certs which require multiple TXT values |
||||
|
_info_sotoon "Checking for existing TXT records" |
||||
|
if ! _sotoon_rest GET "$_domain"; then |
||||
|
_err_sotoon "Failed to get domain zone" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if there are existing TXT records for this subdomain |
||||
|
_existing_txt="" |
||||
|
if _contains "$response" "\"$_sub_domain\""; then |
||||
|
_debug_sotoon "Found existing records for $_sub_domain" |
||||
|
# Extract existing TXT values from the response |
||||
|
# The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}] |
||||
|
_existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") |
||||
|
_debug_sotoon "Existing TXT records: $_existing_txt" |
||||
|
fi |
||||
|
|
||||
|
# Build the new record entry |
||||
|
_new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}" |
||||
|
|
||||
|
# If there are existing records, append to them; otherwise create new array |
||||
|
if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then |
||||
|
# Check if this exact TXT value already exists (avoid duplicates) |
||||
|
if _contains "$_existing_txt" "\"$txtvalue\""; then |
||||
|
_info_sotoon "TXT record already exists, skipping" |
||||
|
return 0 |
||||
|
fi |
||||
|
# Remove the closing bracket and append new record |
||||
|
_combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]" |
||||
|
_debug_sotoon "Combined records: $_combined_records" |
||||
|
else |
||||
|
# No existing records, create new array |
||||
|
_combined_records="[$_new_record]" |
||||
|
fi |
||||
|
|
||||
|
# Prepare the DNS record data in Kubernetes CRD format |
||||
|
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}" |
||||
|
|
||||
|
_debug_sotoon "DNS record payload: $_dns_record" |
||||
|
|
||||
|
# Use PATCH to update/add the record to the domain zone |
||||
|
_info_sotoon "Updating domain zone $_domain with TXT record" |
||||
|
if _sotoon_rest PATCH "$_domain" "$_dns_record"; then |
||||
|
if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then |
||||
|
_info_sotoon "Added, OK" |
||||
|
return 0 |
||||
|
else |
||||
|
_debug_sotoon "Response: $response" |
||||
|
_err_sotoon "Add txt record error." |
||||
|
return 1 |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
_err_sotoon "Add txt record error." |
||||
|
return 1 |
||||
|
} |
||||
|
|
||||
|
#Remove the txt record after validation. |
||||
|
#Usage: dns_sotoon_rm fulldomain TXT_record |
||||
|
#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
||||
|
dns_sotoon_rm() { |
||||
|
fulldomain=$1 |
||||
|
txtvalue=$2 |
||||
|
_info_sotoon "Using Sotoon" |
||||
|
_debug_sotoon fulldomain "$fulldomain" |
||||
|
_debug_sotoon txtvalue "$txtvalue" |
||||
|
|
||||
|
Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" |
||||
|
Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" |
||||
|
Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}" |
||||
|
|
||||
|
_debug_sotoon "First detect the root zone" |
||||
|
if ! _get_root "$fulldomain"; then |
||||
|
_err_sotoon "invalid domain" |
||||
|
return 1 |
||||
|
fi |
||||
|
_debug_sotoon _domain_id "$_domain_id" |
||||
|
_debug_sotoon _sub_domain "$_sub_domain" |
||||
|
_debug_sotoon _domain "$_domain" |
||||
|
|
||||
|
_info_sotoon "Removing TXT record" |
||||
|
|
||||
|
# First, GET the current domain zone to check for existing TXT records |
||||
|
if ! _sotoon_rest GET "$_domain"; then |
||||
|
_err_sotoon "Failed to get domain zone" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
# Check if there are existing TXT records for this subdomain |
||||
|
_existing_txt="" |
||||
|
if _contains "$response" "\"$_sub_domain\""; then |
||||
|
_debug_sotoon "Found existing records for $_sub_domain" |
||||
|
_existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") |
||||
|
_debug_sotoon "Existing TXT records: $_existing_txt" |
||||
|
fi |
||||
|
|
||||
|
# If no existing records, nothing to remove |
||||
|
if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then |
||||
|
_info_sotoon "No TXT records found, nothing to remove" |
||||
|
return 0 |
||||
|
fi |
||||
|
|
||||
|
# Remove the specific TXT value from the array |
||||
|
# This handles the case where there are multiple TXT values (wildcard certs) |
||||
|
_remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g') |
||||
|
_debug_sotoon "Remaining records after removal: $_remaining_records" |
||||
|
|
||||
|
# If no records remain, set to null to remove the subdomain entirely |
||||
|
if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then |
||||
|
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}" |
||||
|
else |
||||
|
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}" |
||||
|
fi |
||||
|
|
||||
|
_debug_sotoon "Remove record payload: $_dns_record" |
||||
|
|
||||
|
# Use PATCH to remove the record from the domain zone |
||||
|
if _sotoon_rest PATCH "$_domain" "$_dns_record"; then |
||||
|
_info_sotoon "Record removed, OK" |
||||
|
return 0 |
||||
|
else |
||||
|
_debug_sotoon "Response: $response" |
||||
|
_err_sotoon "Error removing record" |
||||
|
return 1 |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
#################### Private functions below ################################## |
||||
|
|
||||
|
_get_root() { |
||||
|
domain=$1 |
||||
|
i=2 |
||||
|
p=1 |
||||
|
|
||||
|
_debug_sotoon "Getting root domain for: $domain" |
||||
|
_debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID" |
||||
|
_debug_sotoon "Sotoon WorkspaceName: $Sotoon_WorkspaceName" |
||||
|
|
||||
|
while true; do |
||||
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) |
||||
|
_debug_sotoon "Checking domain part: $h" |
||||
|
|
||||
|
if [ -z "$h" ]; then |
||||
|
#not valid |
||||
|
_err_sotoon "Could not find valid domain" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
_debug_sotoon "Fetching domain zones from Sotoon API" |
||||
|
if ! _sotoon_rest GET ""; then |
||||
|
_err_sotoon "Failed to get domain zones from Sotoon API" |
||||
|
_err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID, and Sotoon_WorkspaceName" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
_debug2_sotoon "API Response: $response" |
||||
|
|
||||
|
# Check if the response contains our domain |
||||
|
# Sotoon API uses Kubernetes CRD format with spec.origin or metadata.name |
||||
|
if _contains "$response" "\"origin\":\"$h\"" || _contains "$response" "\"name\":\"$h\""; then |
||||
|
_debug_sotoon "Found domain: $h" |
||||
|
|
||||
|
# In Kubernetes CRD format, the metadata.name IS the resource identifier |
||||
|
# Extract metadata.name which serves as the domain ID |
||||
|
_domain_id="$h" |
||||
|
|
||||
|
if [ "$_domain_id" ]; then |
||||
|
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") |
||||
|
_domain=$h |
||||
|
_debug_sotoon "Domain ID (metadata.name): $_domain_id" |
||||
|
_debug_sotoon "Sub domain: $_sub_domain" |
||||
|
_debug_sotoon "Domain: $_domain" |
||||
|
return 0 |
||||
|
fi |
||||
|
_err_sotoon "Found domain $h but could not extract domain ID" |
||||
|
return 1 |
||||
|
fi |
||||
|
p=$i |
||||
|
i=$(_math "$i" + 1) |
||||
|
done |
||||
|
return 1 |
||||
|
} |
||||
|
|
||||
|
_sotoon_rest() { |
||||
|
mtd="$1" |
||||
|
resource_id="$2" |
||||
|
data="$3" |
||||
|
|
||||
|
token_trimmed=$(echo "$Sotoon_Token" | tr -d '"') |
||||
|
|
||||
|
# Construct the API endpoint |
||||
|
_api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/namespaces/$Sotoon_WorkspaceName/domainzones" |
||||
|
|
||||
|
if [ -n "$resource_id" ]; then |
||||
|
_api_path="$_api_path/$resource_id" |
||||
|
fi |
||||
|
|
||||
|
_debug_sotoon "API Path: $_api_path" |
||||
|
_debug_sotoon "Method: $mtd" |
||||
|
|
||||
|
# Set authorization header - Sotoon API uses Bearer token |
||||
|
export _H1="Authorization: Bearer $token_trimmed" |
||||
|
|
||||
|
if [ "$mtd" = "GET" ]; then |
||||
|
# GET request |
||||
|
_debug_sotoon "GET" "$_api_path" |
||||
|
response="$(_get "$_api_path")" |
||||
|
elif [ "$mtd" = "PATCH" ]; then |
||||
|
# PATCH Request |
||||
|
export _H2="Content-Type: application/merge-patch+json" |
||||
|
_debug_sotoon data "$data" |
||||
|
response="$(_post "$data" "$_api_path" "" "$mtd")" |
||||
|
else |
||||
|
_err_sotoon "Unknown method: $mtd" |
||||
|
return 1 |
||||
|
fi |
||||
|
|
||||
|
_debug2_sotoon response "$response" |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
#Wrappers for logging |
||||
|
_info_sotoon() { |
||||
|
_info "[Sotoon]" "$@" |
||||
|
} |
||||
|
|
||||
|
_err_sotoon() { |
||||
|
_err "[Sotoon]" "$@" |
||||
|
} |
||||
|
|
||||
|
_debug_sotoon() { |
||||
|
_debug "[Sotoon]" "$@" |
||||
|
} |
||||
|
|
||||
|
_debug2_sotoon() { |
||||
|
_debug2 "[Sotoon]" "$@" |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue