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
276 changes: 276 additions & 0 deletions dnsapi/dns_gname.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_gname_info='GNAME
Site: gname.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gname
Options:
GNAME_APPID Your APPID
GNAME_APPKEY Your APPKEY
OptionsAlt:
'

GNAME_TLD_Api="https://gname.com/request/tlds?lx=all"
GNAME_Api="https://api.gname.com"
GNAME_TLDS_CACHE=""

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

#Usage: add _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CAS3"
dns_gname_add() {
fulldomain=$1
txtvalue=$(printf "%s" "$2" | _url_encode)

GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}"
GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}"

if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then
GNAME_APPID=""
GNAME_APPKEY=""
_err "You have not configured the APPID and APPKEY for the GNAME API."
_err "You can get yours from here http://gname.com/domain/api."
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The credential-help URL in the error message uses http://. Since the API documentation is available over HTTPS (and the PR description references HTTPS), prefer an https:// link to avoid encouraging insecure browsing.

Suggested change
_err "You can get yours from here http://gname.com/domain/api."
_err "You can get yours from here https://gname.com/domain/api."

Copilot uses AI. Check for mistakes.
return 1
fi

_saveaccountconf_mutable GNAME_APPID "$GNAME_APPID"
_saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY"

if ! _extract_domain "$fulldomain"; then
_err "Failed to extract domain. Please check your network or API response."
return 1
fi

gntime=$(date +%s)

#If the hostname is empty, you need to replace it with @.
final_hostname=$(printf "%s" "${ext_hostname:-@}" | _url_encode)

# Parameters need to be sorted by key
body="appid=$GNAME_APPID&gntime=$gntime&jlz=$txtvalue&lang=us&lx=TXT&mx=0&ttl=600&xl=0&ym=$ext_domain&zj=$final_hostname"

_info "Adding TXT record for $ext_domain, host: $final_hostname"

if _post_to_api "/api/resolution/add" "$body"; then
_info "Successfully added DNS record."
return 0
else
if _contains "$post_response" "the same host records and record values"; then
_info "Successfully DNS record already exists."
return 0
fi
_err "Failed to add DNS record via Gname API."
return 1
Comment on lines +56 to +61
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

In the duplicate-record case, _post_to_api will already have emitted an _err log before this branch treats it as success. This results in an error being logged even though dns_gname_add returns 0. Consider handling the “record already exists” response inside _post_to_api (so it returns success without logging _err), or add a way to suppress/error-classify known benign codes/messages.

Copilot uses AI. Check for mistakes.
fi
}

#Usage: remove _acme-challenge.www.domain.com "T1rxqRBosdIK90xWCG3KLZNf6q_0HG9i01zxXp5CASc"
dns_gname_rm() {
fulldomain=$1
txtvalue=$2

GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}"
GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}"

if [ -z "$GNAME_APPID" ] || [ -z "$GNAME_APPKEY" ]; then
GNAME_APPID=""
GNAME_APPKEY=""
_err "You have not configured the APPID and APPKEY for the GNAME API."
_err "You can get yours from here http://gname.com/domain/api."
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

The credential-help URL in the error message uses http://. Prefer https:// here as well to avoid encouraging insecure browsing.

Suggested change
_err "You can get yours from here http://gname.com/domain/api."
_err "You can get yours from here https://gname.com/domain/api."

Copilot uses AI. Check for mistakes.
return 1
fi

_saveaccountconf_mutable GNAME_APPID "$GNAME_APPID"
_saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY"

if ! _extract_domain "$fulldomain"; then
_err "Failed to extract domain. Please check your network or API response."
return 1
fi

final_hostname="${ext_hostname:-@}"

_debug "Query DNS record ID $ext_domain $final_hostname $txtvalue"

record_id=$(_get_record_id "$ext_domain" "$final_hostname" "$txtvalue")

if [ -z "$record_id" ]; then
_err "No DNS record found"
return 1
Comment on lines +96 to +97
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

dns_gname_rm returns an error when no matching record is found. DNS-API rm handlers are typically expected to be idempotent (i.e., treat “already removed” as success) to avoid failing renewals/cleanup flows. Consider changing this branch to log an informational message (e.g., “Don't need to remove”) and return 0.

Suggested change
_err "No DNS record found"
return 1
_info "No DNS record found, nothing to remove"
return 0

Copilot uses AI. Check for mistakes.
fi

_debug "DNS record ID:$record_id"
gntime=$(date +%s)
body="appid=$GNAME_APPID&gntime=$gntime&jxid=$record_id&lang=us&ym=$ext_domain"

if ! _post_to_api "/api/resolution/delete" "$body"; then
_info "DNS record deletion failed"
return 1
fi

_info "DNS record deletion successful"
return 0
}

# Find the DNS record ID by hostname, record type, and record value.
_get_record_id() {
target_ym="$1"
target_zjt="$2"
target_jxz="$3"
target_lx="TXT"

GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}"
GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}"
gntime=$(date +%s)
body="appid=$GNAME_APPID&gntime=$gntime&limit=1000&lx=$target_lx&page=1&ym=$target_ym"

if ! _post_to_api "/api/resolution/list" "$body"; then
_err "Query and parsing records failed"
return 1
fi

clean_response=$(echo "$post_response" | tr -d '\r')
records=$(echo "$clean_response" | sed 's/.*"data":\[//; s/\],"count".*//; s/},/}\n/g')

_debug "Cleaned Formatted Records:\n$records"

jxz_feature=$(printf "%s" "$target_jxz" | cut -c 1-10)

_debug "Searching with host: $target_zjt and feature: $jxz_feature"

matched_row=$(echo "$records" | grep -i "\"zjt\":\"$target_zjt\"" | grep "\"jxz\":\"$jxz_feature")
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

_get_record_id uses grep with a basic-regex pattern that interpolates target_zjt directly. Hostnames like _acme-challenge.www contain dots, which will be treated as regex wildcards and can match unintended rows, potentially deleting the wrong record. Use fixed-string matching (e.g., grep -F) or escape the hostname/value before searching.

Suggested change
matched_row=$(echo "$records" | grep -i "\"zjt\":\"$target_zjt\"" | grep "\"jxz\":\"$jxz_feature")
matched_row=$(echo "$records" | grep -Fi "\"zjt\":\"$target_zjt\"" | grep -F "\"jxz\":\"$jxz_feature")

Copilot uses AI. Check for mistakes.

_debug "Final Matched Row: $matched_row"

if [ -z "$matched_row" ]; then
_err "Still can not find record row. Please check if host $target_zjt is correct."
return 1
fi

dns_record_id=$(echo "$matched_row" | _egrep_o "\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '"')

if [ -n "$dns_record_id" ]; then
_debug "Successfully found record ID: $dns_record_id"
printf "%s" "$dns_record_id"
return 0
fi

return 1
}

# Request GNAME API,post_response: Response content
_post_to_api() {
uri=$1
body=$2
url="$GNAME_Api$uri"
gntoken=$(_gntoken "$body")
body="$body&gntoken=$gntoken"
post_response="$(_post "$body" "$url" "" "POST" "application/x-www-form-urlencoded")"

curl_err_code=$?
if [ "$curl_err_code" != "0" ]; then
_err "POST API $url curl error:$curl_err_code"
return 1
fi

ret_code=$(echo "$post_response" | sed 's/.*"code":\([-0-9]*\).*/\1/')
if [ "$ret_code" = "1" ]; then
return 0
else
ret_msg=$(echo "$post_response" | sed 's/.*"msg":"\([^"]*\)".*/\1/')
_err "POST API $url error: [$ret_code] $ret_msg"
_debug "Full response: $post_response"
return 1
fi
}

# Split the complete domain into a host and a main domain.
# example, www.gname.com can be split into ext_hostname=www,ext_domain=gname.com
_extract_domain() {

host="$1"

# Prioritize reading from the cache and reduce network caching
if [ -z "$GNAME_TLDS_CACHE" ]; then
GNAME_TLDS_CACHE=$(_get_suffixes_json)
fi

if [ -z "$GNAME_TLDS_CACHE" ]; then
_err "The list of domain suffixes is empty 02"
return 1
fi

main_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"main":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ')
sub_part=$(echo "$GNAME_TLDS_CACHE" | sed 's/.*"sub":\[\([^]]*\)\].*/\1/' | tr -d '"' | tr ',' ' ')
suffix_list=$(echo "$main_part $sub_part" | tr -s ' ' | sed 's/^[ ]//;s/[ ]$//')

dot_count=$(echo "$host" | grep -o "\." | wc -l)

if [ "$dot_count" -eq 1 ]; then
ext_hostname=""
ext_domain="$host"

elif [ "$dot_count" -gt 1 ]; then
matched_suffix=""
for suffix in $suffix_list; do
case "$host" in
*".$suffix")
if [ -z "$matched_suffix" ] || [ "${#suffix}" -gt "${#matched_suffix}" ]; then
matched_suffix="$suffix"
fi
;;
esac
done

if [ -n "$matched_suffix" ]; then
prefix="${host%."$matched_suffix"}"
main_name="${prefix##*.}"

ext_domain="$main_name.$matched_suffix"

if [ "$host" = "$ext_domain" ]; then
ext_hostname=""
else
ext_hostname="${host%."$ext_domain"}"
fi

else
ext_domain=$(echo "$host" | awk -F. '{print $(NF-1)"."$NF}')
ext_hostname=$(echo "$host" | rev | cut -d. -f3- | rev)
fi
fi
_debug "ext_hostname:$ext_hostname"
_debug "ext_domain:$ext_domain"
}
Comment on lines +205 to +242
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

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

_extract_domain does not handle inputs with no dots (e.g., an invalid/trimmed fulldomain). In that case none of the branches set ext_domain, but the function still falls through and returns success, which can lead to API calls with an empty ym. Add an explicit validation/else branch that errors and returns 1 when dot_count is 0 (or when ext_domain ends up empty).

Copilot uses AI. Check for mistakes.

# Obtain the list of domain suffixes via API
_get_suffixes_json() {
_debug "GET request URL: $GNAME_TLD_Api Retrieves a list of domain suffixes."

if ! response="$(_get "$GNAME_TLD_Api")"; then
_err "Failed to retrieve list of domain suffixes"
return 1
fi

if [ -z "$response" ]; then
_err "The list of domain suffixes is empty"
return 1
fi

if ! _contains "$response" "\"code\":1"; then
_err "Failed to retrieve list of domain name suffixes; code is not 1"
return 1
fi

echo "$response"
return 0
}

# Generate API authentication signature
_gntoken() {
_debug "String to be signed:$1"
data_to_sign="$1"
full_data="${data_to_sign}${GNAME_APPKEY}"
hash=$(printf "%s" "$full_data" | _digest md5 hex | tr -d ' ')
hash_upper=$(printf "%s" "$hash" | tr '[:lower:]' '[:upper:]')
_debug "Signature value: $hash_upper"
printf "%s" "$hash_upper"
}
Loading