From e828b285ad0c10fb811c7c77059ba233df372139 Mon Sep 17 00:00:00 2001 From: matthias-matze Date: Sun, 12 Jul 2026 10:13:42 +0200 Subject: [PATCH] Matthiasvpfr patch 1 (#7108) * Add files via upload * New Banner Updated README to include responsive images for dark and light modes. * Add files via upload Remove usage of jq and curl should be compliant with acme.sh api dev guide * Add files via upload * Add files via upload change CR LF to LF * Add files via upload missing CR * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload correct auth * Add files via upload better manage group_id * Add files via upload manage wrong / missing domain * Add files via upload strip domain to manage subdomain requests * Add files via upload fix wrong id selection in get root * Add files via upload fix parsing of IDs * Add files via upload correct get_root to better handle unexisting domaines (acmetest) * Add files via upload correct token incorrect in auth * Add files via upload manage case web api reply is empty * Add files via upload try to resolve error when adding unexisting subdmain txt entry * Add files via upload correct domain parsing * Add files via upload revert changes when adding record (name) change rm to better handle complex urls * Add files via upload correct rm function to better manage records deletion * Add files via upload ensure auth variable arent lost during calls * Add files via upload try to keep autj variables accross executions * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload * Add files via upload fix stripping subdomains * Add files via upload * Add files via upload keep auth token instead of requesting it each time * Add files via upload debug * Add files via upload * Add files via upload * Add files via upload better manage record deletion to avoid orphans added some debug and checks * Add files via upload paginated api support for rm function * Add files via upload * Add files via upload delay to manage api 404 * Add files via upload enhance parsing of records in rm * Add files via upload fix incorrect record detection in rm * Add files via upload better manage filters on api to limit errors * Add files via upload try to handle 404 when requesting API too frequently * Add files via upload * Add files via upload sleep during auth * Add files via upload manage 404 errors in get_root * Add files via upload correct variable overide translate comments prefix all variables * Add files via upload correct variable * Add files via upload typo * Add files via upload * Add files via upload * Add files via upload * Add files via upload correct variable naming (_comlaude prefix missing) correct rm return code when non existing record typo * Add files via upload log an info instead of an error if no dns record found in RM function. --------- Co-authored-by: neil Co-authored-by: Matthiasvpfr Co-authored-by: ZeroSSL-Andreas --- dnsapi/dns_comlaude.sh | 248 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 dnsapi/dns_comlaude.sh diff --git a/dnsapi/dns_comlaude.sh b/dnsapi/dns_comlaude.sh new file mode 100644 index 00000000..2aa2dba9 --- /dev/null +++ b/dnsapi/dns_comlaude.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 +dns_comlaude_info='comlaude.com +Site: comlaude.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude +Options: + COMLAUDE_USERNAME User account + COMLAUDE_PASSWORD User password + COMLAUDE_API_KEY generated API key + COMLAUDE_GROUP_ID Group ID in comlaude user profile + Get it from the https://www.comlaude.com +Issues: github.com/acmesh-official/acme.sh/issues/7112 +' +# ===== CONFIG ===== +COMLAUDE_API="https://api.comlaude.com" + +########## AUTH ########## + +_comlaude_auth() { + _debug "Checking cached ComLaude token" + + # Try to get token from account.conf + if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then + COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)" + COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)" + fi + + _now=$(_time) + if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then + _debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})" + return 0 + fi + + _info "ComLaude auth..." + _comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}" + _comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")" + + if ! _contains "$_comlaude_response" "access_token"; then + _err "Auth failed: $_comlaude_response" + return 1 + fi + + COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4) + # store expiracy from api reply l'API ("expires_in" in seconds) + _comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2) + [ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info + + COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew + + _saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN" + _saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY" + + return 0 +} + +########## DOMAIN RESOLUTION ########## + +_comlaude_get_root() { + COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" + if [ -z "$COMLAUDE_GROUP_ID" ]; then + _err "Missing COMLAUDE_GROUP_ID" + return 1 + fi + + _comlaude_input_domain="$1" + _comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}" + case "$_comlaude_input_domain" in + \*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;; + esac + + _debug "Normalized domain: $_comlaude_input_domain" + + _comlaude_i=1 + while true; do + _comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-") + [ -z "$_comlaude_d" ] && { + _debug "No matching domain found for $_comlaude_input_domain" + return 1 + } + + # don't test unnecessary levels + # registered domain : TLD only (no dot after cut). + case "$_comlaude_d" in + *.*) : ;; + *) + _debug "Skipping bare TLD candidate: $_comlaude_d" + _comlaude_i=$((_comlaude_i + 1)) + continue + ;; + esac + + _debug "Checking domain: $_comlaude_d" + + _comlaude_retry=0 + _comlaude_max_retry=3 # to avoid network errors + _comlaude_DOM_ID="" + _comlaude_Z_ID="" + + while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + _debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone" + _comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")" + _H1="" + + _debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response" + + # If empty -> true network issue, we retry + if [ -z "$_comlaude_response" ]; then + _comlaude_retry=$((_comlaude_retry + 1)) + [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2 + continue + fi + + # 404 -> domain not found in that level. no retry : continue + if echo "$_comlaude_response" | grep -q '"status_code":404'; then + _debug "404 for $_comlaude_d, moving to next level (not retrying)" + break + fi + + # Domain missing (200 reply, data empty) -> continue + if echo "$_comlaude_response" | grep -q '"data":\[\]'; then + _debug "Empty data for $_comlaude_d, moving to next level" + break + fi + + # Extraction via _egrep_o + _comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')" + _comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')" + + if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then + break + fi + + # 200 reply but malformed data / noid -> retry transport + _comlaude_retry=$((_comlaude_retry + 1)) + [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2 + done + + _debug "_comlaude_DOM_ID=$_comlaude_DOM_ID" + _debug "_comlaude_Z_ID=$_comlaude_Z_ID" + + if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then + _comlaude_domain="$_comlaude_d" + _comlaude_domain_id="$_comlaude_DOM_ID" + _comlaude_zone_id="$_comlaude_Z_ID" + return 0 + fi + + _comlaude_i=$((_comlaude_i + 1)) + done +} +########## ADD TXT ########## + +dns_comlaude_add() { + fulldomain="$1" + txtvalue="$2" + + COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}" + COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}" + COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}" + COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" + + if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then + _err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)." + return 1 + fi + + # Backup variable after validation + _saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME" + _saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD" + _saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY" + _saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID" + + _info "Adding TXT: $fulldomain" + _comlaude_auth || return 1 + _comlaude_get_root "$fulldomain" || return 1 + + _debug "Root: $_comlaude_domain" + + _comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}" + + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + export _H2="Content-Type: application/json" + + _comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")" + + _H1="" + _H2="" + if ! echo "$_comlaude_response" | grep -q '"id"'; then + _err "Failed to create TXT" + _debug "$_comlaude_response" + return 1 + fi + + return 0 +} + +########## REMOVE TXT ########## + +dns_comlaude_rm() { + fulldomain="$1" + txtvalue="$2" + + COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}" + COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}" + COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}" + COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}" + + _info "Removing TXT: $fulldomain" + + _comlaude_auth || return 1 + _comlaude_get_root "$fulldomain" || return 1 + + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + _comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)" + _comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)" + _comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value" + _comlaude_response="$(_get "$_comlaude_url")" + _H1="" + + _debug "Filtered records response: $_comlaude_response" + + # first "id" top-level of reply (record itself, + # always on first position of each data[] object) + _comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')" + + if [ -z "$_comlaude_record_id" ]; then + _info "No matching TXT record found to delete for $fulldomain / $txtvalue" + return 0 + fi + + _debug "Deleting record $_comlaude_record_id" + + export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN" + _comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id" + _comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")" + _H1="" + + if echo "$_comlaude_del_resp" | grep -q '"error"'; then + _err "Delete failed for $_comlaude_record_id" + _debug "$_comlaude_del_resp" + return 1 + fi + + _info "Deleted record $_comlaude_record_id" + return 0 +}