diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index cd21b65a..0a121fd7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,6 +58,7 @@ Never use raw shell commands when acme.sh provides a wrapper function. This is t | Instead of | Use | |---|---| | `tr '[:upper:]' '[:lower:]'` | `_lower_case()` | +| `tr '[:lower:]' '[:upper:]'` | `_upper_case()` | | `head -n 1` | `_head_n 1` | | `openssl dgst` / `openssl` | `_digest()` / `_hmac()` | | `date` | `_utc_date()` with `sed`/`tr` | @@ -68,6 +69,14 @@ Never use raw shell commands when acme.sh provides a wrapper function. This is t | `grep -E` / `grep -Po` | `_egrep_o()` | | `printf` | `echo` | | `idn` command | `_idn()` / `_is_idn()` | +| `mktemp` | `_mktemp()` | +| `[:space:]` | ` ` | +| `[:alnum:]` | `A-Za-z0-9` | +| `[:alpha:]` | `A-Za-z` | +| `[:digit:]` | `0-9` | +| `awk` | `cut` / `sed` / `while read` loops | + + When fixing a pattern issue, fix **all instances** in the file, not just the one highlighted. diff --git a/acme.sh b/acme.sh index b57228d3..e780f61d 100755 --- a/acme.sh +++ b/acme.sh @@ -65,7 +65,7 @@ ID_TYPE_IP="ip" LOCAL_ANY_ADDRESS="0.0.0.0" -DEFAULT_RENEW=30 +DEFAULT_RENEW="${DEFAULT_RENEW:-30}" NO_VALUE="no" @@ -1599,6 +1599,7 @@ createCSR() { domain="$1" domainlist="$2" _isEcc="$3" + _csreku="$4" _initpath "$domain" "$_isEcc" @@ -1612,7 +1613,7 @@ createCSR() { _err "Please create it first." return 1 fi - _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" + _createcsr "$domain" "$domainlist" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_csreku" } @@ -5324,6 +5325,11 @@ $_authorizations_map" _link_cert_retry="$(_math $_link_cert_retry + 1)" done + # cover case where the final poll returned 'valid' + if [ -z "$Le_LinkCert" ] && _contains "$response" "\"status\":\"valid\""; then + Le_LinkCert="$(echo "$response" | _egrep_o '"certificate" *: *"[^"]*"' | cut -d '"' -f 4)" + fi + if [ -z "$Le_LinkCert" ]; then _err "Signing failed. Could not get Le_LinkCert, and stopped retrying after reaching the retry limit." _err "$response" @@ -5667,7 +5673,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - for di in "${CERT_HOME}"/*[.:]*/; do + for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" @@ -8254,7 +8260,7 @@ _process() { createDomainKey "$_domain" "$_keylength" ;; createCSR) - createCSR "$_domain" "$_altdomains" "$_ecc" + createCSR "$_domain" "$_altdomains" "$_ecc" "$_extended_key_usage" ;; setnotify) setnotify "$_notify_hook" "$_notify_level" "$_notify_mode" "$_notify_source" diff --git a/deploy/panos.sh b/deploy/panos.sh index 019d8c62..00badffc 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -68,8 +68,8 @@ deployer() { # Get Version Info to test key content="type=version&key=$_panos_key" ## Exclude all scopes for the empty commit - #_exclude_scope="excludeexcludeexclude" - #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scopeacmekeytest" + #_exclude_scope="excludedexcluded" + #content="type=commit&action=partial&key=$_panos_key&cmd=$_exclude_scope$_panos_user" fi # Generate API Key @@ -128,10 +128,9 @@ deployer() { #Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution! if [ "$FORCE" ]; then _debug "Force switch detected. Committing ALL changes to the firewall." - cmd=$(printf "%s" "$_panos_user" | _url_encode) + cmd=$(printf "%s" "$_panos_user" | _url_encode) else - _exclude_scope="excludeexclude" - cmd=$(printf "%s" "$_exclude_scope$_panos_user" | _url_encode) + cmd=$(printf "%s" "$_panos_user" | _url_encode) fi content="type=commit&action=partial&key=$_panos_key&cmd=$cmd" fi diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index f6cc2cd0..e28a4036 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -387,7 +387,7 @@ synology_dsm_deploy() { if echo "$response" | grep '"restart_httpd":true' >/dev/null; then _info "Restart HTTP services succeeded." else - _info "Restart HTTP services failed." + _info "Restart HTTP services not necessary." fi _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME" _logout diff --git a/dnsapi/dns_baidu.sh b/dnsapi/dns_baidu.sh new file mode 100644 index 00000000..8651deab --- /dev/null +++ b/dnsapi/dns_baidu.sh @@ -0,0 +1,548 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 + +# Global variables for returning results (avoid stdout pollution from logging) +_BAIDU_FIND_RESULT="" +_BAIDU_BCE_AUTH_RESULT="" + +: "${BAIDU_LOG_LEVEL:=2}" + +_baidu_log_ts() { + date +} + +_baidu_log_ge() { + _want="$1" + [ "${BAIDU_LOG_LEVEL:-0}" -ge "$_want" ] +} + +_baidu_log() { + _lvl="$1" + _tag="$2" + _msg="$3" + if [ "$_lvl" = "0" ] || _baidu_log_ge "$_lvl"; then + printf -- "[%s] %s %s\n" "$(_baidu_log_ts)" "$_tag" "$_msg" + fi +} + +_baidu_err() { + _baidu_log 0 "baidu_bcd.err" "$1" + return 1 +} + +_baidu_info() { + _baidu_log 1 "baidu_bcd.info" "$1" + return 0 +} + +_baidu_debug() { + _baidu_log 2 "$1" "$2" + return 0 +} + +dns_baidu_info='Baidu Cloud BCD DNS +Site: cloud.baidu.com +Docs: https://cloud.baidu.com/doc/BCD/ +Signature: https://cloud.baidu.com/doc/Reference/s/njwvz1yfu +Options: + Baidu_AK AccessKeyId + Baidu_SK SecretAccessKey +OptionsAlt: + Baidu_BCD_Host API host, default: bcd.baidubce.com + Baidu_BCD_Version API version number, default: 1 + Baidu_BCD_Expire Signature expiration seconds, default: 3600 + Baidu_View Resolve view, default: DEFAULT + Baidu_TTL Resolve ttl seconds, default: 300 + Baidu_RM_Max Max records to delete in one run, default: 20 +' + +BAIDU_BCD_DEFAULT_HOST="bcd.baidubce.com" + +# --- Public API --- +dns_baidu_add() { + fulldomain=$(_idn "$1") + txtvalue=$2 + + if ! _baidu_prepare_record "$fulldomain"; then + _baidu_err "baidu_prepare_record failed for add: $fulldomain" + return 1 + fi + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name" + return 1 + fi + _existing_ids="$_BAIDU_FIND_RESULT" + if [ "$_existing_ids" ]; then + _baidu_info "txt exists, skip add: $_record_domain.$_zone_name" + return 0 + fi + + _ttl="${Baidu_TTL:-300}" + _ttl="$(_baidu_trim_ws "$_ttl")" + case "$_ttl" in + "" | *[!0-9]*) + _ttl="300" + ;; + esac + _view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")" + txtvalue="$(_baidu_trim_ws "$txtvalue")" + _record_domain="$(_baidu_trim_ws "$_record_domain")" + _zone_name="$(_baidu_trim_ws "$_zone_name")" + + _body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")" + + if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then + _baidu_err "baidu_bcd_post failed: add record" + return 1 + fi + + if _baidu_is_api_error "$response"; then + _baidu_err "$response" + return 1 + fi + + return 0 +} + +dns_baidu_rm() { + fulldomain=$(_idn "$1") + txtvalue=$2 + + if ! _baidu_prepare_record "$fulldomain"; then + _baidu_err "baidu_prepare_record failed for delete: $fulldomain" + return 1 + fi + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name" + return 1 + fi + _ids="$_BAIDU_FIND_RESULT" + if [ -z "$_ids" ]; then + _baidu_info "no matching txt to delete: $_record_domain.$_zone_name" + return 0 + fi + + _rm_max="${Baidu_RM_Max:-20}" + _rm_max="$(_baidu_trim_ws "$_rm_max")" + case "$_rm_max" in + "" | *[!0-9]*) + _rm_max="20" + ;; + esac + _rm_cnt="$(printf "%s\n" "$_ids" | sed '/^$/d' | wc -l | tr -d ' ')" + if [ "$_rm_cnt" ] && [ "$_rm_cnt" -gt "$_rm_max" ]; then + _baidu_err "Refusing to delete $_rm_cnt records (limit: $_rm_max)" + return 1 + fi + + for _rid in $_ids; do + _body="$(_baidu_payload_delete "$_zone_name" "$_rid")" + if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then + _baidu_err "baidu_bcd_post failed: delete recordId=$_rid" + return 1 + fi + if _baidu_is_api_error "$response"; then + _baidu_err "$response" + return 1 + fi + done + + if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then + _baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name" + return 1 + fi + _left_ids="$_BAIDU_FIND_RESULT" + if [ -z "$_left_ids" ]; then + return 0 + fi + if [ -n "$_left_ids" ]; then + _baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records" + return 1 + fi + + return 0 +} + +# --- Config / Record Context --- +_baidu_load_credentials() { + Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}" + Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}" + + Baidu_AK="$(_baidu_trim_ws "$Baidu_AK")" + Baidu_SK="$(_baidu_trim_ws "$Baidu_SK")" + + if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then + _baidu_err "Baidu_AK and Baidu_SK are required" + return 1 + fi + + _saveaccountconf_mutable Baidu_AK "$Baidu_AK" + _saveaccountconf_mutable Baidu_SK "$Baidu_SK" + + BAIDU_BCD_HOST="${Baidu_BCD_Host:-$BAIDU_BCD_DEFAULT_HOST}" + BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}" + + return 0 +} + +_baidu_prepare_record() { + _fulldomain="$1" + if ! _baidu_load_credentials; then + _baidu_err "baidu_load_credentials failed" + return 1 + fi + if ! _baidu_get_root "$_fulldomain"; then + _baidu_err "Could not find zone for $_fulldomain" + return 1 + fi + _record_domain="$_sub_domain" + _zone_name="$_domain" + return 0 +} + +# --- Zone / Records --- +_baidu_get_root() { + domain=$1 + i=1 + p=1 + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + if [ -z "$h" ]; then + _baidu_err "invalid domain: $domain" + return 1 + fi + + if ! _baidu_bcd_post "/domain/resolve/list" "$(_baidu_payload_list "$h" 1 1)"; then + _baidu_err "baidu_bcd_post failed: list zones" + return 1 + fi + if ! _baidu_is_api_error "$response" && (_contains "$response" "\"totalCount\"" || _contains "$response" "\"result\""); then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _domain=$h + if [ "$_sub_domain" = "$_domain" ]; then + _sub_domain="@" + fi + _baidu_info "zone matched: $_domain (host: $_sub_domain)" + return 0 + fi + + p=$i + i=$(_math "$i" + 1) + done +} + +_baidu_find_record_ids() { + _zone_name="$1" + _record_domain="$2" + _rdtype="$3" + _rdata="$4" + + # Reset global result variable + _BAIDU_FIND_RESULT="" + + _zone_name_e="$(_baidu_json_escape "$_zone_name")" + _record_domain_e="$(_baidu_json_escape "$_record_domain")" + _rdtype_e="$(_baidu_json_escape "$_rdtype")" + _rdata_e="$(_baidu_json_escape "$_rdata")" + + _page=1 + _page_size=100 + _ids="" + + _max_page="" + while true; do + if ! _baidu_bcd_post "/domain/resolve/list" "$(_baidu_payload_list "$_zone_name" "$_page" "$_page_size")"; then + _baidu_err "baidu_bcd_post failed: list records" + return 1 + fi + + if _baidu_is_api_error "$response"; then + _baidu_err "baidu_bcd error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")" + return 1 + fi + + _normalized="$( + printf "%s" "$response" | _normalizeJson + )" + + if [ -z "$_max_page" ]; then + _total="$(_baidu_parse_totalcount "$_normalized")" + _max_page="$(_baidu_calc_max_page "$_total" "$_page_size")" + fi + + _records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g') + while IFS= read -r _line; do + _id="$(_baidu_match_record_id "$_line" "$_record_domain_e" "$_rdtype_e" "$_rdata_e")" + if [ "$_id" ]; then + _ids="$_ids $_id" + fi + done <...." + _debug2 "add response" "$response" + return 1 + fi + + return 0 +} + +# Usage: dns_bhosted_rm _acme-challenge.www.example.com "txt-value" +dns_bhosted_rm() { + fulldomain="$1" + txtvalue="$2" + + _debug "fulldomain" "$fulldomain" + _debug "txtvalue" "$txtvalue" + + _bhosted_load_credentials || return 1 + _bhosted_get_root "$fulldomain" || return 1 + + _hash="$(_bhosted_cache_hash "$fulldomain" "$txtvalue")" + _rec_id="$(_bhosted_mem_get_id "$_hash")" + + if [ -z "$_rec_id" ]; then + _err "No cached bHosted record id found for cleanup." + _err "Please delete TXT manually in bHosted DNS for: ${_bhosted_name}.${_domain}" + return 1 + fi + + _info "Removing TXT record id=${_rec_id}: ${_bhosted_name}.${_domain}" + _bhosted_api_del_record "$_bhosted_sld" "$_bhosted_tld" "$_rec_id" || return 1 + + return 0 +} + +######## Private functions ##################### + +_bhosted_load_credentials() { + BHOSTED_Username="${BHOSTED_Username:-$(_readaccountconf_mutable BHOSTED_Username)}" + BHOSTED_Password="${BHOSTED_Password:-$(_readaccountconf_mutable BHOSTED_Password)}" + + if [ -z "$BHOSTED_Username" ] || [ -z "$BHOSTED_Password" ]; then + BHOSTED_Username="" + BHOSTED_Password="" + _err "You didn't specify bHosted credentials." + _err "Please export BHOSTED_Username and BHOSTED_Password (MD5 hash)." + return 1 + fi + + _saveaccountconf_mutable BHOSTED_Username "$BHOSTED_Username" + _saveaccountconf_mutable BHOSTED_Password "$BHOSTED_Password" + + return 0 +} + +# Determine root zone and host part +# Supports simple domains automatically (example.com, example.nl) +# For multi-part TLDs (example.co.uk), set: +# BHOSTED_SLD=example +# BHOSTED_TLD=co.uk +_bhosted_get_root() { + domain="$1" + + BHOSTED_SLD="${BHOSTED_SLD:-$(_readdomainconf BHOSTED_SLD)}" + BHOSTED_TLD="${BHOSTED_TLD:-$(_readdomainconf BHOSTED_TLD)}" + + if [ -n "$BHOSTED_SLD" ] && [ -n "$BHOSTED_TLD" ]; then + _savedomainconf BHOSTED_SLD "$BHOSTED_SLD" + _savedomainconf BHOSTED_TLD "$BHOSTED_TLD" + + _domain="${BHOSTED_SLD}.${BHOSTED_TLD}" + case "$domain" in + *."$_domain") ;; + "$_domain") ;; + *) + _err "BHOSTED_SLD/BHOSTED_TLD do not match requested domain: $domain" + return 1 + ;; + esac + + _bhosted_sld="$BHOSTED_SLD" + _bhosted_tld="$BHOSTED_TLD" + _bhosted_name="${domain%."$_domain"}" + if [ "$_bhosted_name" = "$domain" ]; then + _bhosted_name="" + fi + + [ -n "$_bhosted_name" ] || _bhosted_name="@" + + _debug "_domain" "$_domain" + _debug "_bhosted_sld" "$_bhosted_sld" + _debug "_bhosted_tld" "$_bhosted_tld" + _debug "_bhosted_name" "$_bhosted_name" + return 0 + fi + + # Auto-parse: assume last label = tld, label before = sld + # Works for .nl / .com / .org etc. + _bhosted_tld="$(printf "%s" "$domain" | awk -F. '{print $NF}')" + _bhosted_sld="$(printf "%s" "$domain" | awk -F. '{print $(NF-1)}')" + + if [ -z "$_bhosted_sld" ] || [ -z "$_bhosted_tld" ]; then + _err "Could not parse SLD/TLD from domain: $domain" + return 1 + fi + + _domain="${_bhosted_sld}.${_bhosted_tld}" + _bhosted_name="${domain%."$_domain"}" + if [ "$_bhosted_name" = "$domain" ]; then + _bhosted_name="" + fi + + [ -n "$_bhosted_name" ] || _bhosted_name="@" + + _debug "_domain" "$_domain" + _debug "_bhosted_sld" "$_bhosted_sld" + _debug "_bhosted_tld" "$_bhosted_tld" + _debug "_bhosted_name" "$_bhosted_name" + + return 0 +} + +_bhosted_api_add_txt() { + _sld="$1" + _tld="$2" + _name="$3" + _content="$4" + _ttl="$5" + + _u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)" + _u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)" + _u_sld="$(printf "%s" "$_sld" | _url_encode)" + _u_tld="$(printf "%s" "$_tld" | _url_encode)" + _u_name="$(printf "%s" "$_name" | _url_encode)" + _u_content="$(printf "%s" "$_content" | _url_encode)" + _u_ttl="$(printf "%s" "$_ttl" | _url_encode)" + + _data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&type=TXT&name=${_u_name}&content=${_u_content}&ttl=${_u_ttl}" + + _debug "bHosted add endpoint" "${BHOSTED_API_ROOT}/addrecord" + response="$(_post "$_data" "${BHOSTED_API_ROOT}/addrecord")" + _ret="$?" + + _debug2 "bHosted add response" "$response" + + if [ "$_ret" != "0" ]; then + _err "bHosted addrecord request failed" + return 1 + fi + + if _bhosted_response_has_error "$response"; then + _err "bHosted addrecord returned an error" + _debug2 "response" "$response" + return 1 + fi + + return 0 +} + +_bhosted_api_del_record() { + _sld="$1" + _tld="$2" + _id="$3" + + _u_user="$(printf "%s" "$BHOSTED_Username" | _url_encode)" + _u_pass="$(printf "%s" "$BHOSTED_Password" | _url_encode)" + _u_sld="$(printf "%s" "$_sld" | _url_encode)" + _u_tld="$(printf "%s" "$_tld" | _url_encode)" + _u_id="$(printf "%s" "$_id" | _url_encode)" + + _url="${BHOSTED_API_ROOT}/delrecord" + _data="user=${_u_user}&password=${_u_pass}&tld=${_u_tld}&sld=${_u_sld}&id=${_u_id}" + + _debug "bHosted delete endpoint" "$_url" + response="$(_post "$_data" "$_url")" + _ret="$?" + + _debug2 "bHosted delete response" "$response" + + if [ "$_ret" != "0" ]; then + _err "bHosted delrecord request failed" + return 1 + fi + + if _bhosted_response_has_error "$response"; then + _err "bHosted delrecord returned an error" + _debug2 "response" "$response" + return 1 + fi + + return 0 +} + +# Extract XML tag value from response, e.g. 12345 +_bhosted_xml_value() { + _tag="$1" + _resp="$2" + + # Flatten response to simplify parsing + _flat="$(printf "%s" "$_resp" | tr -d '\r\n\t')" + printf "%s" "$_flat" | sed -n "s:.*<${_tag}>\\([^<]*\\).*:\\1:p" | _head_n 1 +} + +# Return code convention: +# return 0 => response HAS error +# return 1 => response has NO error (success) +_bhosted_response_has_error() { + _resp="$1" + + # Empty response = error + if [ -z "$_resp" ]; then + _debug "Empty API response" + return 0 + fi + + # Prefer explicit bHosted XML response fields + if _contains "$_resp" ""; then + _errors="$(_bhosted_xml_value "errors" "$_resp")" + _done="$(_bhosted_xml_value "done" "$_resp")" + _subcommand="$(_bhosted_xml_value "subcommand" "$_resp")" + _id="$(_bhosted_xml_value "id" "$_resp")" + + _debug "bHosted XML subcommand" "$_subcommand" + _debug "bHosted XML id" "$_id" + _debug "bHosted XML errors" "$_errors" + _debug "bHosted XML done" "$_done" + + # Success according to provided format + if [ "$_errors" = "0" ] && [ "$_done" = "true" ]; then + return 1 + fi + + _debug "bHosted XML indicates failure" + return 0 + fi + + # Fallback for unexpected/non-XML responses + _resp_lc="$(_lower_case "$_resp")" + + if _contains "$_resp_lc" "error"; then + _debug "Detected 'error' in response" + return 0 + fi + if _contains "$_resp_lc" "fout"; then + _debug "Detected 'fout' in response" + return 0 + fi + if _contains "$_resp_lc" "invalid"; then + _debug "Detected 'invalid' in response" + return 0 + fi + if _contains "$_resp_lc" "failed"; then + _debug "Detected 'failed' in response" + return 0 + fi + if _contains "$_resp_lc" "denied"; then + _debug "Detected 'denied' in response" + return 0 + fi + + # If no explicit error markers found, assume success + return 1 +} + +# Extract record id from response +# Supports bHosted XML first, then generic fallbacks +_bhosted_extract_id() { + _resp="$1" + + # bHosted XML: 12345 + _id="$(_bhosted_xml_value "id" "$_resp" | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + # JSON: "id":12345 + _id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + # key=value: id=12345 + _id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + # "record id 12345" / "recordid 12345" + _id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')" + if [ -n "$_id" ]; then + printf "%s" "$_id" + return 0 + fi + + return 1 +} + +# Create a unique config key for cached record ids +_bhosted_cache_hash() { + _fd="$1" + _tv="$2" + # md5 hex of fulldomain|txtvalue + printf "%s|%s" "$_fd" "$_tv" | _digest md5 hex +} + +_bhosted_cache_key() { + _hash="$1" + printf "%s" "BHOSTED_TXT_ID_${_hash}" +} + +_bhosted_mem_set_id() { + _hash="$1" + _id="$2" + _key="$(_bhosted_cache_key "$_hash")" + _savedomainconf "$_key" "$_id" +} + +_bhosted_mem_get_id() { + _hash="$1" + _key="$(_bhosted_cache_key "$_hash")" + _readdomainconf "$_key" +} diff --git a/dnsapi/dns_gname.sh b/dnsapi/dns_gname.sh new file mode 100644 index 00000000..886b3dc5 --- /dev/null +++ b/dnsapi/dns_gname.sh @@ -0,0 +1,303 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_gname_info='GNAME +Site: www.gname.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gname +Options: + GNAME_APPID Your APPID + GNAME_APPKEY Your APPKEY + GNAME_TTL DNS resolution record TTL value, default 120. +Issues: github.com/acmesh-official/acme.sh/issues/6874 +Author: GNDevProd +' + +GNAME_TLD_Api="https://www.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) + #Compatible with gname API RFC 1738 standard URL encoding + txtvalue=$(printf '%s' "$txtvalue" | sed 's/%20/+/g') + + GNAME_APPID="${GNAME_APPID:-$(_readaccountconf_mutable GNAME_APPID)}" + GNAME_APPKEY="${GNAME_APPKEY:-$(_readaccountconf_mutable GNAME_APPKEY)}" + GNAME_TTL="${GNAME_TTL:-$(_readaccountconf_mutable GNAME_TTL)}" + GNAME_TTL="${GNAME_TTL:-120}" + + 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 https://www.gname.com/domain/api." + return 1 + fi + + _saveaccountconf_mutable GNAME_APPID "$GNAME_APPID" + _saveaccountconf_mutable GNAME_APPKEY "$GNAME_APPKEY" + _saveaccountconf_mutable GNAME_TTL "$GNAME_TTL" + + 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&exist=1&gntime=$gntime&jlz=$txtvalue&lang=us&lx=TXT&mx=0&ttl=$GNAME_TTL&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 + _err "Failed to add DNS record via Gname API." + return 1 + 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 https://www.gname.com/domain/api." + 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" + + if ! record_id=$(_get_record_id "$ext_domain" "$final_hostname" "$txtvalue"); then + _err "Error occurred during record lookup. Skipping deletion to avoid errors." + return 1 + fi + + if [ -z "$record_id" ]; then + _info "DNS record not found, skip removing." + return 0 + 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 + _err "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' | grep "^{") + matched_rows=$(echo "$records" | grep -Fi "\"zjt\":\"$target_zjt\"") + + if [ -z "$matched_rows" ]; then + _debug "No records found for host: $target_zjt" + return 0 + fi + + exact_row=$(echo "$matched_rows" | grep -F "\"jxz\":\"$target_jxz\"" | _head_n 1) + dns_record_id="" + if [ -n "$exact_row" ]; then + dns_record_id=$(echo "$exact_row" | _egrep_o "\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d '"') + fi + + if [ -n "$dns_record_id" ]; then + _debug "Successfully found exact record ID: $dns_record_id" + printf "%s" "$dns_record_id" + return 0 + fi + + _debug "Can not find exact DNS record match for: $target_zjt" + return 0 +} + +# 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")" + + http_err_code=$? + if [ "$http_err_code" != "0" ]; then + _err "POST API $url request failed:$http_err_code" + return 1 + fi + + normalized_response="$(echo "$post_response" | _normalizeJson)" + if [ -z "$normalized_response" ]; then + _err "Failed to normalize JSON response for [$uri]" + return 1 + fi + + ret_code=$(echo "$normalized_response" | sed 's/.*"code":\([-0-9]*\).*/\1/') + + if [ "$ret_code" = "1" ]; then + return 0 + fi + + if [ "$uri" = "/api/resolution/add" ]; then + if _contains "$normalized_response" "the same host records and record values"; then + _info "DNS record already exists, treat as success." + return 0 + fi + fi + + ret_msg=$(echo "$normalized_response" | sed 's/.*"msg":"\([^"]*\)".*/\1/') + _err "POST API $url error: [$ret_code] $ret_msg" + _debug "Full response: $normalized_response" + return 1 +} + +# 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 after retrieval; cannot extract domain" + 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" | _egrep_o "\." | wc -l) + + if [ "$dot_count" -eq 0 ]; then + _err "Invalid domain format: $host (missing dot)" + return 1 + fi + + 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" + else + _tld="${host##*.}" + _tmp="${host%.*}" + _main="${_tmp##*.}" + ext_domain="$_main.$_tld" + fi + + if [ "$host" = "$ext_domain" ]; then + ext_hostname="" + else + ext_hostname="${host%."$ext_domain"}" + fi + + fi + _debug "ext_hostname:$ext_hostname" + _debug "ext_domain:$ext_domain" + return 0 +} + +# 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 + + normalized_response="$(echo "$response" | _normalizeJson)" + if [ -z "$normalized_response" ]; then + _err "Failed to normalize JSON response for domain suffix list" + return 1 + fi + + if ! _contains "$normalized_response" "\"code\":1"; then + _err "Failed to retrieve list of domain name suffixes; code is not 1" + return 1 + fi + + echo "$normalized_response" + return 0 +} + +# Generate API authentication signature +_gntoken() { + data_to_sign="$1" + full_data="${data_to_sign}${GNAME_APPKEY}" + hash=$(printf "%s" "$full_data" | _digest md5 hex | tr -d ' ') + hash_upper=$(echo "$hash" | _upper_case) + printf "%s" "$hash_upper" +} diff --git a/dnsapi/dns_sitehost.sh b/dnsapi/dns_sitehost.sh new file mode 100755 index 00000000..94a0ee93 --- /dev/null +++ b/dnsapi/dns_sitehost.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_sitehost_info='SiteHost +Site: sitehost.nz +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sitehost +Options: + SITEHOST_API_KEY API Key + SITEHOST_CLIENT_ID Client ID. The numeric client ID for your SiteHost account. +Issues: github.com/acmesh-official/acme.sh/issues/6892 +Author: Jordan Russell +' + +SITEHOST_API="https://api.sitehost.nz/1.5" + +######## Public functions ##################### + +# Usage: dns_sitehost_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_sitehost_add() { + fulldomain=$1 + txtvalue=$2 + + if ! _sitehost_load_creds; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + # SiteHost expects the full record name as the name parameter + _info "Adding TXT record for ${fulldomain}" + if _sitehost_rest POST "dns/add_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&type=TXT&name=$(printf '%s' "${fulldomain}" | _url_encode)&content=$(printf '%s' "${txtvalue}" | _url_encode)"; then + if _contains "$response" '"status":true'; then + _info "TXT record added successfully." + return 0 + fi + fi + + _err "Could not add TXT record for ${fulldomain}" + _err "$response" + return 1 +} + +# Usage: dns_sitehost_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +# Remove the txt record after validation. +dns_sitehost_rm() { + fulldomain=$1 + txtvalue=$2 + + if ! _sitehost_load_creds; then + return 1 + fi + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _debug "Getting TXT records for ${_domain}" + if ! _sitehost_rest GET "dns/list_records.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)"; then + _err "Could not list DNS records" + _err "$response" + return 1 + fi + + if ! _contains "$response" '"status":true'; then + _err "Error listing DNS records" + _err "$response" + return 1 + fi + + # Extract record ID matching our fulldomain, type TXT, and txtvalue + # Response format: {"return":[{"id":"123","name":"...","type":"TXT","content":"..."},...]} + # SiteHost returns flat single-line JSON objects in the records array + # Escape regex metacharacters in values before grep matching + _fulldomain_grep="$(printf "%s" "$fulldomain" | sed 's/[][\\.^$*]/\\&/g')" + _txtvalue_grep="$(printf "%s" "$txtvalue" | sed 's/[][\\.^$*]/\\&/g')" + # Use field-specific matching to avoid false positives from substring matches + _record_id="$(echo "$response" | _egrep_o '\{[^}]*\}' | grep '"name" *: *"'"${_fulldomain_grep}"'"' | grep '"type" *: *"TXT"' | grep '"content" *: *"'"${_txtvalue_grep}"'"' | _head_n 1 | _egrep_o '"id" *: *"?[0-9]+"?' | _egrep_o '[0-9]+')" + + if [ -z "$_record_id" ]; then + _info "TXT record not found, nothing to remove." + return 0 + fi + + _debug _record_id "$_record_id" + + _info "Deleting TXT record ${_record_id} for ${fulldomain}" + if _sitehost_rest POST "dns/delete_record.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&domain=$(printf '%s' "${_domain}" | _url_encode)&record_id=$(printf '%s' "${_record_id}" | _url_encode)"; then + if _contains "$response" '"status":true'; then + _info "TXT record deleted successfully." + return 0 + fi + fi + + _err "Could not delete TXT record for ${fulldomain}" + _err "$response" + return 1 +} + +#################### Private functions below ################################## + +_sitehost_load_creds() { + SITEHOST_API_KEY="${SITEHOST_API_KEY:-$(_readaccountconf_mutable SITEHOST_API_KEY)}" + SITEHOST_CLIENT_ID="${SITEHOST_CLIENT_ID:-$(_readaccountconf_mutable SITEHOST_CLIENT_ID)}" + + if [ -z "$SITEHOST_API_KEY" ] || [ -z "$SITEHOST_CLIENT_ID" ]; then + SITEHOST_API_KEY="" + SITEHOST_CLIENT_ID="" + _err "You didn't specify SITEHOST_API_KEY and/or SITEHOST_CLIENT_ID." + _err "Please export them and try again." + return 1 + fi + + _saveaccountconf_mutable SITEHOST_API_KEY "$SITEHOST_API_KEY" + _saveaccountconf_mutable SITEHOST_CLIENT_ID "$SITEHOST_CLIENT_ID" + return 0 +} + +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + + _debug "Getting domain list" + + # Fetch ALL pages of domains first so we can match the most specific zone + # (a more specific zone on a later page must take precedence over a broader match) + _all_domains="" + _page=1 + + while true; do + if ! _sitehost_rest GET "dns/list_domains.json" "client_id=$(printf '%s' "${SITEHOST_CLIENT_ID}" | _url_encode)&filters%5Bpage_number%5D=${_page}"; then + _err "Could not list domains" + return 1 + fi + + if ! _contains "$response" '"status":true'; then + _err "Error listing domains" + _err "$response" + return 1 + fi + + _all_domains="${_all_domains} ${response}" + + _total_pages=$(echo "$response" | _egrep_o '"total_pages" *: *[0-9]+' | _egrep_o '[0-9]+') + if [ -z "$_total_pages" ] || [ "$_page" -ge "$_total_pages" ]; then + break + fi + + _page=$(_math "$_page" + 1) + done + + # Try each subdomain level, most specific first + _i=1 + _p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "${_i}"-100) + _debug h "$h" + if [ -z "$h" ]; then + return 1 + fi + + if echo "$_all_domains" | grep -F "\"${h}\"" >/dev/null 2>&1; then + if [ "$_i" = "1" ]; then + # DNS alias mode - fulldomain is the zone itself + _sub_domain="" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"${_p}") + fi + _domain="${h}" + return 0 + fi + + _p="${_i}" + _i=$(_math "$_i" + 1) + done + + return 1 +} + +# Usage: _sitehost_rest method endpoint data +_sitehost_rest() { + m="$1" + ep="$2" + data="$3" + url="${SITEHOST_API}/${ep}" + + _debug url "$url" + + _apikey="$(printf "%s" "${SITEHOST_API_KEY}" | _url_encode)" + + if [ "$m" = "GET" ]; then + response="$(_get "${url}?apikey=${_apikey}&${data}")" + else + _debug2 data "$data" + response="$(_post "apikey=${_apikey}&${data}" "$url")" + fi + + if [ "$?" != "0" ]; then + _err "error ${ep}" + return 1 + fi + + response="$(printf '%s' "$response" | tr -d '\r')" + + _debug2 response "$response" + return 0 +} diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh index dc295330..f59715ac 100644 --- a/dnsapi/dns_world4you.sh +++ b/dnsapi/dns_world4you.sh @@ -61,7 +61,7 @@ dns_world4you_add() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') + msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to add record: Unknown error" echo "$ret" >'error-01.html' @@ -110,7 +110,7 @@ dns_world4you_rm() { return 3 fi - recordid=$(printf "TXT:%s.:\"%s\"" "$fqdn" "$value" | _base64) + recordid=$(echo "$form" | grep 'data-records="' | sed 's/.*"\([^"]*\)".*/\1/;s/"/"/g;s/},{/}\n{/g' | grep '"type":"TXT"' | grep "\"name\":\"$fqdn\"" | grep "\"value\":\"$value\"" | sed 's/^.*"id":"\([^"]*\)".*$/\1/') _debug recordid "$recordid" _resethttp @@ -125,7 +125,7 @@ dns_world4you_rm() { if _contains "$res" "successfully"; then return 0 else - msg=$(echo "$res" | grep -A 15 'data-type="danger"' | grep "]*>[^<]" | sed 's/<[^>]*>//g' | sed 's/^\s*//g') + msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g') if [ "$msg" = '' ]; then _err "Unable to remove record: Unknown error" echo "$ret" >'error-01.html'