Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 90 additions & 241 deletions dnsapi/dns_beget.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,270 +12,119 @@ Author: ARNik <[email protected]>

Beget_Api="https://api.beget.com/api"

#################### Public functions ####################

# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
dns_beget_add() {
fulldomain=$1
txtvalue=$2
_debug "dns_beget_add() $fulldomain $txtvalue"
fulldomain=$(echo "$fulldomain" | _lower_case)

Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"

if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then
Beget_Username=""
Beget_Password=""
_err "You must export variables: Beget_Username, and Beget_Password"
return 1
fi

#save the credentials to the account conf file.
_saveaccountconf_mutable Beget_Username "$Beget_Username"
_saveaccountconf_mutable Beget_Password "$Beget_Password"

_info "Prepare subdomain."
if ! _prepare_subdomain "$fulldomain"; then
_err "Can't prepare subdomain."
return 1
fi

_info "Get domain records"
data="{\"fqdn\":\"$fulldomain\"}"
res=$(_api_call "$Beget_Api/dns/getData" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't get domain records."
return 1
fi

_info "Add new TXT record"
data="{\"fqdn\":\"$fulldomain\",\"records\":{"
data=${data}$(_parce_records "$res" "A")
data=${data}$(_parce_records "$res" "AAAA")
data=${data}$(_parce_records "$res" "CAA")
data=${data}$(_parce_records "$res" "MX")
data=${data}$(_parce_records "$res" "SRV")
data=${data}$(_parce_records "$res" "TXT")
data=$(echo "$data" | sed 's/,$//')
data=${data}'}}'

str=$(_txt_to_dns_json "$txtvalue")
data=$(_add_record "$data" "TXT" "$str")

res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't change domain records."
return 1
fi
# API call function
_api_call() {
api_url="$1"
input_data="$2"
url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json"
[ -n "$input_data" ] && url="${url}&input_data=$(echo -n "$input_data" | jq -s -R -r @uri)"

return 0
echo "[DEBUG] _api_call url=$url"
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug output exposes sensitive credentials (username and password) in the URL. This could leak credentials to logs. Consider masking credentials in debug output or using a different logging approach.

Suggested change
echo "[DEBUG] _api_call url=$url"
# Mask credentials in debug output
masked_url="$api_url?login=***&passwd=***&input_format=json&output_format=json"
[ -n "$input_data" ] && masked_url="${masked_url}&input_data=$(echo -n "$input_data" | jq -s -R -r @uri)"
echo "[DEBUG] _api_call url=$masked_url"

Copilot uses AI. Check for mistakes.
curl -s "$url"
}

# Usage: fulldomain txtvalue
# Used to remove the txt record after validation
dns_beget_rm() {
# Add TXT record (supports multiple additions without overwriting)
dns_beget_add() {
fulldomain=$1
txtvalue=$2
_debug "dns_beget_rm() $fulldomain $txtvalue"
fulldomain=$(echo "$fulldomain" | _lower_case)

Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"

_info "Get current domain records"
data="{\"fqdn\":\"$fulldomain\"}"
res=$(_api_call "$Beget_Api/dns/getData" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't get domain records."
return 1
fi

_info "Remove TXT record"
data="{\"fqdn\":\"$fulldomain\",\"records\":{"
data=${data}$(_parce_records "$res" "A")
data=${data}$(_parce_records "$res" "AAAA")
data=${data}$(_parce_records "$res" "CAA")
data=${data}$(_parce_records "$res" "MX")
data=${data}$(_parce_records "$res" "SRV")
data=${data}$(_parce_records "$res" "TXT")
data=$(echo "$data" | sed 's/,$//')
data=${data}'}}'
echo "[DEBUG] Starting dns_beget_add"
echo "[DEBUG] fulldomain=$fulldomain"
echo "[DEBUG] txtvalue=$txtvalue"

str=$(_txt_to_dns_json "$txtvalue")
data=$(_rm_record "$data" "$str")
Beget_Username="${Beget_Username:?Please set Beget_Username}"
Beget_Password="${Beget_Password:?Please set Beget_Password}"
Comment on lines +35 to +36
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The credential handling pattern has changed from the original implementation which used _readaccountconf_mutable and _saveaccountconf_mutable functions. This breaks the existing credential management pattern used throughout the acme.sh project.

Suggested change
Beget_Username="${Beget_Username:?Please set Beget_Username}"
Beget_Password="${Beget_Password:?Please set Beget_Password}"
# Credential management: read from account config if not set
if [ -z "$Beget_Username" ]; then
Beget_Username="$(_readaccountconf_mutable BEGET_User)"
fi
if [ -z "$Beget_Password" ]; then
Beget_Password="$(_readaccountconf_mutable BEGET_Password)"
fi
if [ -z "$Beget_Username" ]; then
_err "Please set BEGET_User."
return 1
fi
if [ -z "$Beget_Password" ]; then
_err "Please set BEGET_Password."
return 1
fi
# Save credentials to account config for future use
_saveaccountconf_mutable BEGET_User "$Beget_Username"
_saveaccountconf_mutable BEGET_Password "$Beget_Password"

Copilot uses AI. Check for mistakes.

res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't change domain records."
return 1
fi

return 0
}

#################### Private functions below ####################
fulldomain=$(echo "$fulldomain" | tr '[:upper:]' '[:lower:]')
echo "[DEBUG] fulldomain (lowercase)=$fulldomain"

# Create subdomain if needed
# Usage: _prepare_subdomain [fulldomain]
_prepare_subdomain() {
fulldomain=$1

_info "Detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
# Get current DNS records
res=$(_api_call "$Beget_Api/dns/getData" "{\"fqdn\":\"$fulldomain\"}") || {
echo "[ERROR] API getData did not return a response"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"

if [ -z "$_sub_domain" ]; then
_debug "$fulldomain is a root domain."
return 0
fi
}
Comment on lines +42 to +45
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || operator will trigger on any non-zero exit code from _api_call, but _api_call uses curl -s which returns the response regardless of HTTP status. This condition may incorrectly trigger when the API returns a valid response with an error status.

Copilot uses AI. Check for mistakes.
echo "[DEBUG] API getData response: $res"

status=$(echo "$res" | jq -r '.answer.status' 2>/dev/null || echo "error")

if [ "$status" = "success" ]; then
old_txts=$(echo "$res" | jq -c '.answer.result.records.TXT // []')
echo "[DEBUG] Existing TXT records from API: $old_txts"
else
echo "[WARN] Beget API error (status=$status). Try fallback with dig polling."

old_txts="[]"
i=1
while [ $i -le 6 ]; do # 6 раз по 20 секунд = максимум 120
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment contains text in Russian (Cyrillic characters). Comments should be in English for consistency with the codebase.

Suggested change
while [ $i -le 6 ]; do # 6 раз по 20 секунд = максимум 120
while [ $i -le 6 ]; do # 6 times 20 seconds = maximum 120 seconds

Copilot uses AI. Check for mistakes.
dig_txts=$(dig TXT +short "${fulldomain}" \
@ns1.beget.com @ns2.beget.com | sed 's/^"//;s/"$//' | jq -R . | jq -s .)

if [ "$dig_txts" != "[]" ]; then
old_txts="$dig_txts"
echo "[DEBUG] dig found TXT records on attempt $i: $old_txts"
break
else
echo "[DEBUG] dig attempt $i: no TXT yet, waiting 20s..."
if [ $i -gt 3 ]; then
sleep 40
else
sleep 20
fi
fi

_info "Get subdomain list"
res=$(_api_call "$Beget_Api/domain/getSubdomainList")
if ! _is_api_reply_ok "$res"; then
_err "Can't get subdomain list."
return 1
fi
i=$((i+1))
done

if _contains "$res" "\"fqdn\":\"$fulldomain\""; then
_debug "Subdomain $fulldomain already exist."
return 0
if [ "$old_txts" = "[]" ]; then
echo "[DEBUG] dig found no TXT records after 120s. old_txts empty."
fi
fi

# Prepare new TXT record
new_txt="{\"priority\":10,\"value\":\"$txtvalue\"}"
echo "[DEBUG] New TXT record: $new_txt"

# Merge with existing TXT records
if [ "$old_txts" = "[]" ]; then
txt_records="[$new_txt]"
else
old_objs=$(jq -c --argjson p 10 '[.[] | {priority: ($p|tonumber), value: .}]' <<< "$old_txts")
txt_records=$(jq -c --argjson new "$new_txt" '. + [$new]' <<< "$old_objs")
fi
echo "[DEBUG] Final TXT records set: $txt_records"

data="{\"fqdn\":\"$fulldomain\",\"records\":{\"TXT\":$txt_records}}"
echo "[DEBUG] Sending data to changeRecords: $data"

_info "Subdomain $fulldomain does not exist. Let's create one."
data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}"
res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't create subdomain."
_api_call "$Beget_Api/dns/changeRecords" "$data" || {
echo "[ERROR] Error calling changeRecords"
return 1
fi

_debug "Cleanup subdomen records"
data="{\"fqdn\":\"$fulldomain\",\"records\":{}}"
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_debug "Can't cleanup $fulldomain records."
fi

data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}"
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_debug "Can't cleanup www.$fulldomain records."
fi
}
Comment on lines +99 to +102
Copy link

Copilot AI Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above - the || operator will trigger on any non-zero exit code from _api_call, but the function doesn't validate HTTP status codes or API response status, potentially masking actual API errors.

Copilot uses AI. Check for mistakes.

return 0
echo "[INFO] TXT record successfully added for $fulldomain"
}

# Usage: _get_root _acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=32436365
_get_root() {
# Remove all _acme-challenge TXT records
dns_beget_rm() {
fulldomain=$1
i=1
p=1

_debug "Get domain list"
res=$(_api_call "$Beget_Api/domain/getList")
if ! _is_api_reply_ok "$res"; then
_err "Can't get domain list."
return 1
fi
echo "[DEBUG] Starting dns_beget_rm"
echo "[DEBUG] fulldomain=$fulldomain"

while true; do
h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
_debug h "$h"
Beget_Username="${Beget_Username:?Please set Beget_Username}"
Beget_Password="${Beget_Password:?Please set Beget_Password}"

if [ -z "$h" ]; then
return 1
fi
fulldomain=$(echo "$fulldomain" | tr '[:upper:]' '[:lower:]')
echo "[DEBUG] fulldomain (lowercase)=$fulldomain"

if _contains "$res" "$h"; then
_domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2)
if [ "$_domain_id" ]; then
if [ "$h" != "$fulldomain" ]; then
_sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p")
else
_sub_domain=""
fi
_domain=$h
return 0
fi
return 1
fi
p="$i"
i=$(_math "$i" + 1)
done
return 1
}
# Remove all TXT records by sending empty array
data="{\"fqdn\":\"$fulldomain\",\"records\":{\"TXT\": []}}"
echo "[DEBUG] Sending data to changeRecords: $data"

# Parce DNS records from json string
# Usage: _parce_records [j_str] [record_name]
_parce_records() {
j_str=$1
record_name=$2
res="\"$record_name\":["
res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1)
res=${res}"],"
echo "$res"
}

# Usage: _add_record [data] [record_name] [record_data]
_add_record() {
data=$1
record_name=$2
record_data=$3
echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/"
}

# Usage: _rm_record [data] [record_data]
_rm_record() {
data=$1
record_data=$2
echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" |
sed "s/{,/{/g" | sed "s/,}/}/g" |
sed "s/\[,/\[/g" | sed "s/,\]/\]/g"
}

_txt_to_dns_json() {
echo "{\"ttl\":600,\"txtdata\":\"$1\"}"
}

# Usage: _api_call [api_url] [input_data]
_api_call() {
api_url="$1"
input_data="$2"

_debug "_api_call $api_url"
_debug "Request: $input_data"

# res=$(curl -s -L -D ./http.header \
# "$api_url" \
# --data-urlencode login=$Beget_Username \
# --data-urlencode passwd=$Beget_Password \
# --data-urlencode input_format=json \
# --data-urlencode output_format=json \
# --data-urlencode "input_data=$input_data")

url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json"
if [ -n "$input_data" ]; then
url=${url}"&input_data="
url=${url}$(echo "$input_data" | _url_encode)
fi
res=$(_get "$url")

_debug "Reply: $res"
echo "$res"
}
_api_call "$Beget_Api/dns/changeRecords" "$data" || {
echo "[ERROR] Error calling changeRecords"
return 1
}

# Usage: _is_api_reply_ok [api_reply]
_is_api_reply_ok() {
_contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$'
echo "[INFO] All _acme-challenge TXT records removed for $fulldomain"
}