Browse Source

Merge pull request #5284 from acmesh-official/dev

sync
neil 1 year ago
parent
commit
fb27261568

+ 1 - 0
.github/workflows/DNS.yml

@@ -1,5 +1,6 @@
 name: DNS
 on:
+  workflow_dispatch:
   push:
     paths:
       - 'dnsapi/*.sh'

+ 157 - 0
deploy/ali_cdn.sh

@@ -0,0 +1,157 @@
+#!/usr/bin/env sh
+
+# Script to create certificate to Alibaba Cloud CDN
+#
+# This deployment required following variables
+# export Ali_Key="ALIACCESSKEY"
+# export Ali_Secret="ALISECRETKEY"
+# export DEPLOY_ALI_CDN_DOMAIN="cdn.example.com"
+# If you have more than one domain, just
+# export DEPLOY_ALI_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
+#
+# The credentials are shared with all domains, also shared with dns_ali api
+
+Ali_API="https://cdn.aliyuncs.com/"
+
+ali_cdn_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
+  Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
+  if [ -z "$Ali_Key" ] || [ -z "$Ali_Secret" ]; then
+    Ali_Key=""
+    Ali_Secret=""
+    _err "You don't specify aliyun api key and secret yet."
+    return 1
+  fi
+
+  #save the api key and secret to the account conf file.
+  _saveaccountconf_mutable Ali_Key "$Ali_Key"
+  _saveaccountconf_mutable Ali_Secret "$Ali_Secret"
+
+  _getdeployconf DEPLOY_ALI_CDN_DOMAIN
+  if [ "$DEPLOY_ALI_CDN_DOMAIN" ]; then
+    _savedeployconf DEPLOY_ALI_CDN_DOMAIN "$DEPLOY_ALI_CDN_DOMAIN"
+  else
+    DEPLOY_ALI_CDN_DOMAIN="$_cdomain"
+  fi
+
+  # read cert and key files and urlencode both
+  _cert=$(_url_encode_upper <"$_cfullchain")
+  _key=$(_url_encode_upper <"$_ckey")
+
+  _debug2 _cert "$_cert"
+  _debug2 _key "$_key"
+
+  ## update domain ssl config
+  for domain in $DEPLOY_ALI_CDN_DOMAIN; do
+    _set_cdn_domain_ssl_certificate_query "$domain" "$_cert" "$_key"
+    if _ali_rest "Set CDN domain SSL certificate for $domain" "" POST; then
+      _info "Domain $domain certificate has been deployed successfully"
+    fi
+  done
+
+  return 0
+}
+
+####################  Private functions below ##################################
+
+# act ign mtd
+_ali_rest() {
+  act="$1"
+  ign="$2"
+  mtd="$3"
+
+  signature=$(printf "%s" "$mtd&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
+  signature=$(_ali_urlencode "$signature")
+  url="$Ali_API?$query&Signature=$signature"
+
+  if [ "$mtd" = "GET" ]; then
+    response="$(_get "$url")"
+  else
+    # post payload is not supported yet because of signature
+    response="$(_post "" "$url")"
+  fi
+
+  _ret="$?"
+  _debug2 response "$response"
+  if [ "$_ret" != "0" ]; then
+    _err "Error <$act>"
+    return 1
+  fi
+
+  if [ -z "$ign" ]; then
+    message="$(echo "$response" | _egrep_o "\"Message\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")"
+    if [ "$message" ]; then
+      _err "$message"
+      return 1
+    fi
+  fi
+}
+
+_ali_urlencode() {
+  _str="$1"
+  _str_len=${#_str}
+  _u_i=1
+  while [ "$_u_i" -le "$_str_len" ]; do
+    _str_c="$(printf "%s" "$_str" | cut -c "$_u_i")"
+    case $_str_c in [a-zA-Z0-9.~_-])
+      printf "%s" "$_str_c"
+      ;;
+    *)
+      printf "%%%02X" "'$_str_c"
+      ;;
+    esac
+    _u_i="$(_math "$_u_i" + 1)"
+  done
+}
+
+_ali_nonce() {
+  #_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
+  #Not so good...
+  date +"%s%N" | sed 's/%N//g'
+}
+
+_timestamp() {
+  date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
+}
+
+# stdin stdout
+_url_encode_upper() {
+  encoded=$(_url_encode)
+
+  for match in $(echo "$encoded" | _egrep_o '%..' | sort -u); do
+    upper=$(echo "$match" | _upper_case)
+    encoded=$(echo "$encoded" | sed "s/$match/$upper/g")
+  done
+
+  echo "$encoded"
+}
+
+# domain pub pri
+_set_cdn_domain_ssl_certificate_query() {
+  query=''
+  query=$query'AccessKeyId='$Ali_Key
+  query=$query'&Action=SetCdnDomainSSLCertificate'
+  query=$query'&CertType=upload'
+  query=$query'&DomainName='$1
+  query=$query'&Format=json'
+  query=$query'&SSLPri='$3
+  query=$query'&SSLProtocol=on'
+  query=$query'&SSLPub='$2
+  query=$query'&SignatureMethod=HMAC-SHA1'
+  query=$query"&SignatureNonce=$(_ali_nonce)"
+  query=$query'&SignatureVersion=1.0'
+  query=$query'&Timestamp='$(_timestamp)
+  query=$query'&Version=2018-05-10'
+}

+ 184 - 0
dnsapi/dns_alviy.sh

@@ -0,0 +1,184 @@
+#!/usr/bin/env sh
+# Alviy domain api
+#
+# Get API key and secret from https://cloud.alviy.com/token
+#
+# Alviy_token="some-secret-key"
+#
+# Ex.: acme.sh --issue --staging --dns dns_alviy -d "*.s.example.com" -d "s.example.com"
+
+Alviy_Api="https://cloud.alviy.com/api/v1"
+
+########  Public functions #####################
+
+#Usage: dns_alviy_add  _acme-challenge.www.domain.com   "content"
+dns_alviy_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  Alviy_token="${Alviy_token:-$(_readaccountconf_mutable Alviy_token)}"
+  if [ -z "$Alviy_token" ]; then
+    Alviy_token=""
+    _err "Please specify Alviy token."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable Alviy_token "$Alviy_token"
+
+  _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 existing records"
+  if _alviy_txt_exists "$_domain" "$fulldomain" "$txtvalue"; then
+    _info "This record already exists, skipping"
+    return 0
+  fi
+
+  _add_data="{\"content\":\"$txtvalue\",\"type\":\"TXT\"}"
+  _debug2 _add_data "$_add_data"
+  _info "Adding record"
+  if _alviy_rest POST "zone/$_domain/domain/$fulldomain/" "$_add_data"; then
+    _debug "Checking updated records of '${fulldomain}'"
+
+    if ! _alviy_txt_exists "$_domain" "$fulldomain" "$txtvalue"; then
+      _err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
+      return 1
+    fi
+
+  else
+    _err "Add txt record error, value '${txtvalue}' for '${fulldomain}' was not set."
+    return 1
+  fi
+
+  _sleep 10
+  _info "Added TXT record '${txtvalue}' for '${fulldomain}'."
+  return 0
+}
+
+#fulldomain
+dns_alviy_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  Alviy_token="${Alviy_token:-$(_readaccountconf_mutable Alviy_token)}"
+
+  _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"
+
+  if ! _alviy_txt_exists "$_domain" "$fulldomain" "$txtvalue"; then
+    _info "The record does not exist, skip"
+    return 0
+  fi
+
+  _add_data=""
+  uuid=$(echo "$response" | tr "{" "\n" | grep "$txtvalue" | tr "," "\n" | grep uuid | cut -d \" -f4)
+  # delete record
+  _debug "Delete TXT record for '${fulldomain}'"
+  if ! _alviy_rest DELETE "zone/$_domain/record/$uuid" "{\"confirm\":1}"; then
+    _err "Cannot delete empty TXT record for '$fulldomain'"
+    return 1
+  fi
+  _info "The record '$fulldomain'='$txtvalue' deleted"
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  i=3
+  a="init"
+  while [ -n "$a" ]; do
+    a=$(printf "%s" "$domain" | cut -d . -f $i-)
+    i=$((i + 1))
+  done
+  n=$((i - 3))
+  h=$(printf "%s" "$domain" | cut -d . -f $n-)
+  if [ -z "$h" ]; then
+    #not valid
+    _alviy_rest GET "zone/$domain/"
+    _debug "can't get host from $domain"
+    return 1
+  fi
+
+  if ! _alviy_rest GET "zone/$h/"; then
+    return 1
+  fi
+
+  if _contains "$response" '"code":"NOT_FOUND"'; then
+    _debug "$h not found"
+  else
+    s=$((n - 1))
+    _sub_domain=$(printf "%s" "$domain" | cut -d . -f -$s)
+    _domain="$h"
+    return 0
+  fi
+  return 1
+}
+
+_alviy_txt_exists() {
+  zone=$1
+  domain=$2
+  content_data=$3
+  _debug "Getting existing records"
+
+  if ! _alviy_rest GET "zone/$zone/domain/$domain/TXT/"; then
+    _info "The record does not exist"
+    return 1
+  fi
+
+  if ! _contains "$response" "$3"; then
+    _info "The record has other value"
+    return 1
+  fi
+  # GOOD code return - TRUE function
+  return 0
+}
+
+_alviy_rest() {
+  method=$1
+  path="$2"
+  content_data="$3"
+  _debug "$path"
+
+  export _H1="Authorization: Bearer $Alviy_token"
+  export _H2="Content-Type: application/json"
+
+  if [ "$content_data" ] || [ "$method" = "DELETE" ]; then
+    _debug "data ($method): " "$content_data"
+    response="$(_post "$content_data" "$Alviy_Api/$path" "" "$method")"
+  else
+    response="$(_get "$Alviy_Api/$path")"
+  fi
+  _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
+  if [ "$_code" = "401" ]; then
+    _err "It seems that your api key or secret is not correct."
+    return 1
+  fi
+
+  if [ "$_code" != "200" ]; then
+    _err "API call error ($method): $path Response code $_code"
+  fi
+  if [ "$?" != "0" ]; then
+    _err "error on rest call ($method): $path. Response:"
+    _err "$response"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}

+ 1 - 2
dnsapi/dns_anx.sh

@@ -130,8 +130,6 @@ _get_root() {
   i=1
   p=1
 
-  _anx_rest GET "zone.json"
-
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
     _debug h "$h"
@@ -140,6 +138,7 @@ _get_root() {
       return 1
     fi
 
+    _anx_rest GET "zone.json/${h}"
     if _contains "$response" "\"name\":\"$h\""; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
       _domain=$h

+ 2 - 2
dnsapi/dns_huaweicloud.sh

@@ -210,7 +210,7 @@ _get_recordset_id() {
   _zoneid=$3
   export _H1="X-Auth-Token: ${_token}"
 
-  response=$(_get "${dns_api}/v2/zones/${_zoneid}/recordsets?name=${_domain}")
+  response=$(_get "${dns_api}/v2/zones/${_zoneid}/recordsets?name=${_domain}&status=ACTIVE")
   if _contains "${response}" '"id"'; then
     _id="$(echo "${response}" | _egrep_o "\"id\": *\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | tr -d " ")"
     printf "%s" "${_id}"
@@ -227,7 +227,7 @@ _add_record() {
 
   # Get Existing Records
   export _H1="X-Auth-Token: ${_token}"
-  response=$(_get "${dns_api}/v2/zones/${zoneid}/recordsets?name=${_domain}")
+  response=$(_get "${dns_api}/v2/zones/${zoneid}/recordsets?name=${_domain}&status=ACTIVE")
 
   _debug2 "${response}"
   _exist_record=$(echo "${response}" | _egrep_o '"records":[^]]*' | sed 's/\"records\"\:\[//g')

+ 5 - 4
dnsapi/dns_miab.sh

@@ -16,8 +16,9 @@ Author: Darven Dissek, William Gertz
 #Usage: dns_miab_add  _acme-challenge.www.domain.com  "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_miab_add() {
   fulldomain=$1
-  txtvalue=$2
-  _info "Using miab challange add"
+  # Added "value=" and "&ttl=300" to accomodate the new TXT record format used by the MIAB/PMIAB API
+  txtvalue="value=$2&ttl=300"
+  _info "Using miab challenge add"
   _debug fulldomain "$fulldomain"
   _debug txtvalue "$txtvalue"
 
@@ -26,7 +27,7 @@ dns_miab_add() {
     return 1
   fi
 
-  #check domain and seperate into doamin and host
+  #check domain and seperate into domain and host
   if ! _get_root "$fulldomain"; then
     _err "Cannot find any part of ${fulldomain} is hosted on ${MIAB_Server}"
     return 1
@@ -55,7 +56,7 @@ dns_miab_rm() {
   fulldomain=$1
   txtvalue=$2
 
-  _info "Using miab challage delete"
+  _info "Using miab challenge delete"
   _debug fulldomain "$fulldomain"
   _debug txtvalue "$txtvalue"
 

+ 8 - 4
dnsapi/dns_nsupdate.sh

@@ -20,6 +20,7 @@ dns_nsupdate_add() {
   NSUPDATE_SERVER_PORT="${NSUPDATE_SERVER_PORT:-$(_readaccountconf_mutable NSUPDATE_SERVER_PORT)}"
   NSUPDATE_KEY="${NSUPDATE_KEY:-$(_readaccountconf_mutable NSUPDATE_KEY)}"
   NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}"
+  NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}"
 
   _checkKeyFile || return 1
 
@@ -28,21 +29,23 @@ dns_nsupdate_add() {
   _saveaccountconf_mutable NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}"
   _saveaccountconf_mutable NSUPDATE_KEY "${NSUPDATE_KEY}"
   _saveaccountconf_mutable NSUPDATE_ZONE "${NSUPDATE_ZONE}"
+  _saveaccountconf_mutable NSUPDATE_OPT "${NSUPDATE_OPT}"
 
   [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
   [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53
+  [ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT=""
 
   _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\""
   [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
   [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
   if [ -z "${NSUPDATE_ZONE}" ]; then
-    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug "${NSUPDATE_OPT}" <<EOF
 server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
 update add ${fulldomain}. 60 in txt "${txtvalue}"
 send
 EOF
   else
-    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug "${NSUPDATE_OPT}" <<EOF
 server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
 zone ${NSUPDATE_ZONE}.
 update add ${fulldomain}. 60 in txt "${txtvalue}"
@@ -65,6 +68,7 @@ dns_nsupdate_rm() {
   NSUPDATE_SERVER_PORT="${NSUPDATE_SERVER_PORT:-$(_readaccountconf_mutable NSUPDATE_SERVER_PORT)}"
   NSUPDATE_KEY="${NSUPDATE_KEY:-$(_readaccountconf_mutable NSUPDATE_KEY)}"
   NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}"
+  NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}"
 
   _checkKeyFile || return 1
   [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost"
@@ -73,13 +77,13 @@ dns_nsupdate_rm() {
   [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d"
   [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D"
   if [ -z "${NSUPDATE_ZONE}" ]; then
-    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug "${NSUPDATE_OPT}" <<EOF
 server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
 update delete ${fulldomain}. txt
 send
 EOF
   else
-    nsupdate -k "${NSUPDATE_KEY}" $nsdebug <<EOF
+    nsupdate -k "${NSUPDATE_KEY}" $nsdebug "${NSUPDATE_OPT}" <<EOF
 server ${NSUPDATE_SERVER}  ${NSUPDATE_SERVER_PORT}
 zone ${NSUPDATE_ZONE}.
 update delete ${fulldomain}. txt

+ 405 - 0
dnsapi/dns_timeweb.sh

@@ -0,0 +1,405 @@
+#!/usr/bin/env sh
+
+# acme.sh DNS API for Timeweb Cloud provider (https://timeweb.cloud).
+#
+# Author: https://github.com/nikolaypronchev.
+#
+# Prerequisites:
+# Timeweb Cloud API JWT token. Obtain one from the Timeweb Cloud control panel
+# ("API and Terraform" section: https://timeweb.cloud/my/api-keys). The JWT token
+# must be provided to this script in one of two ways:
+# 1.  As the "TW_Token" variable, for example: "export TW_Token=eyJhbG...zUxMiIs";
+# 2.  As a "TW_Token" config entry in acme.sh account config file
+#     (usually located at ~/.acme.sh/account.conf by default).
+
+TW_Api="https://api.timeweb.cloud/api/v1"
+
+################  Public functions ################
+
+# Adds an ACME DNS-01 challenge DNS TXT record via the Timeweb Cloud API.
+#
+# Param1: The ACME DNS-01 challenge FQDN.
+# Param2: The value of the ACME DNS-01 challenge TXT record.
+#
+# Example: dns_timeweb_add "_acme-challenge.sub.domain.com" "D-52Wm...4uYM"
+dns_timeweb_add() {
+  _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_add\" started."
+
+  _timeweb_set_acme_fqdn "$1" || return 1
+  _timeweb_set_acme_txt "$2" || return 1
+  _timeweb_check_token || return 1
+  _timeweb_split_acme_fqdn || return 1
+  _timeweb_dns_txt_add || return 1
+
+  _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_add\" finished."
+}
+
+# Removes a DNS TXT record via the Timeweb Cloud API.
+#
+# Param1: The ACME DNS-01 challenge FQDN.
+# Param2: The value of the ACME DNS-01 challenge TXT record.
+#
+# Example: dns_timeweb_rm "_acme-challenge.sub.domain.com" "D-52Wm...4uYM"
+dns_timeweb_rm() {
+  _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_rm\" started."
+
+  _timeweb_set_acme_fqdn "$1" || return 1
+  _timeweb_set_acme_txt "$2" || return 1
+  _timeweb_check_token || return 1
+  _timeweb_split_acme_fqdn || return 1
+  _timeweb_get_dns_txt || return 1
+  _timeweb_dns_txt_remove || return 1
+
+  _debug "$(__green "Timeweb DNS API"): \"dns_timeweb_rm\" finished."
+}
+
+################  Private functions ################
+
+# Checks and sets the ACME DNS-01 challenge FQDN.
+#
+# Param1: The ACME DNS-01 challenge FQDN.
+#
+# Example: _timeweb_set_acme_fqdn "_acme-challenge.sub.domain.com"
+#
+# Sets the "Acme_Fqdn" variable (_acme-challenge.sub.domain.com)
+_timeweb_set_acme_fqdn() {
+  Acme_Fqdn=$1
+  _debug "Setting ACME DNS-01 challenge FQDN \"$Acme_Fqdn\"."
+  [ -z "$Acme_Fqdn" ] && {
+    _err "ACME DNS-01 challenge FQDN is empty."
+    return 1
+  }
+  return 0
+}
+
+# Checks and sets the value of the ACME DNS-01 challenge TXT record.
+#
+# Param1: Value of the ACME DNS-01 challenge TXT record.
+#
+# Example: _timeweb_set_acme_txt "D-52Wm...4uYM"
+#
+# Sets the "Acme_Txt" variable to the provided value (D-52Wm...4uYM)
+_timeweb_set_acme_txt() {
+  Acme_Txt=$1
+  _debug "Setting the value of the ACME DNS-01 challenge TXT record to \"$Acme_Txt\"."
+  [ -z "$Acme_Txt" ] && {
+    _err "ACME DNS-01 challenge TXT record value is empty."
+    return 1
+  }
+  return 0
+}
+
+# Checks if the Timeweb Cloud API JWT token is present (refer to the script description).
+# Adds or updates the token in the acme.sh account configuration.
+_timeweb_check_token() {
+  _debug "Checking for the presence of the Timeweb Cloud API JWT token."
+
+  TW_Token="${TW_Token:-$(_readaccountconf_mutable TW_Token)}"
+
+  [ -z "$TW_Token" ] && {
+    _err "Timeweb Cloud API JWT token was not found."
+    return 1
+  }
+
+  _saveaccountconf_mutable TW_Token "$TW_Token"
+}
+
+# Divides the ACME DNS-01 challenge FQDN into its main domain and subdomain components.
+_timeweb_split_acme_fqdn() {
+  _debug "Trying to divide \"$Acme_Fqdn\" into its main domain and subdomain components."
+
+  TW_Page_Limit=100
+  TW_Page_Offset=0
+
+  while [ -z "$TW_Domains_Total" ] ||
+    [ "$((TW_Domains_Total + TW_Page_Limit))" -gt "$((TW_Page_Offset + TW_Page_Limit))" ]; do
+
+    _timeweb_list_domains "$TW_Page_Limit" "$TW_Page_Offset" || return 1
+
+    # Remove the 'subdomains' subarray to prevent confusion with FQDNs.
+
+    TW_Domains=$(
+      echo "$TW_Domains" |
+        sed 's/"subdomains":\[[^]]*]//g'
+    )
+
+    [ -z "$TW_Domains" ] && {
+      _err "Failed to parse the list of domains."
+      return 1
+    }
+
+    while
+      TW_Domain=$(
+        echo "$TW_Domains" |
+          sed -n 's/.*{[^{]*"fqdn":"\([^"]*\)"[^}]*}.*/\1/p'
+      )
+
+      [ -n "$TW_Domain" ] && {
+        _timeweb_is_main_domain "$TW_Domain" && return 0
+
+        TW_Domains=$(
+          echo "$TW_Domains" |
+            sed 's/{\([^{]*"fqdn":"'"$TW_Domain"'"[^}]*\)}//'
+        )
+        continue
+      }
+    do :; done
+
+    TW_Page_Offset=$(_math "$TW_Page_Offset" + "$TW_Page_Limit")
+  done
+
+  _err "Failed to divide \"$Acme_Fqdn\" into its main domain and subdomain components."
+  return 1
+}
+
+# Searches for a previously added DNS TXT record.
+#
+# Sets the "TW_Dns_Txt_Id" variable.
+_timeweb_get_dns_txt() {
+  _debug "Trying to locate a DNS TXT record with the value \"$Acme_Txt\"."
+
+  TW_Page_Limit=100
+  TW_Page_Offset=0
+
+  while [ -z "$TW_Dns_Records_Total" ] ||
+    [ "$((TW_Dns_Records_Total + TW_Page_Limit))" -gt "$((TW_Page_Offset + TW_Page_Limit))" ]; do
+    _timeweb_list_dns_records "$TW_Page_Limit" "$TW_Page_Offset" || return 1
+
+    while
+      Dns_Record=$(
+        echo "$TW_Dns_Records" |
+          sed -n 's/.*{\([^{]*{[^{]*'"$Acme_Txt"'[^}]*}[^}]*\)}.*/\1/p'
+      )
+
+      [ -n "$Dns_Record" ] && {
+        _timeweb_is_added_txt "$Dns_Record" && return 0
+
+        TW_Dns_Records=$(
+          echo "$TW_Dns_Records" |
+            sed 's/{\([^{]*{[^{]*'"$Acme_Txt"'[^}]*}[^}]*\)}//'
+        )
+        continue
+      }
+    do :; done
+
+    TW_Page_Offset=$(_math "$TW_Page_Offset" + "$TW_Page_Limit")
+  done
+
+  _err "DNS TXT record was not found."
+  return 1
+}
+
+# Lists domains via the Timeweb Cloud API.
+#
+# Param 1: Limit for listed domains.
+# Param 2: Offset for domains list.
+#
+# Sets the "TW_Domains" variable.
+# Sets the "TW_Domains_Total" variable.
+_timeweb_list_domains() {
+  _debug "Listing domains via Timeweb Cloud API. Limit: $1, offset: $2."
+
+  export _H1="Authorization: Bearer $TW_Token"
+
+  if ! TW_Domains=$(_get "$TW_Api/domains?limit=$1&offset=$2"); then
+    _err "The request to the Timeweb Cloud API failed."
+    return 1
+  fi
+
+  [ -z "$TW_Domains" ] && {
+    _err "Empty response from the Timeweb Cloud API."
+    return 1
+  }
+
+  TW_Domains_Total=$(
+    echo "$TW_Domains" |
+      sed 's/.*"meta":{"total":\([0-9]*\)[^0-9].*/\1/'
+  )
+
+  [ -z "$TW_Domains_Total" ] && {
+    _err "Failed to extract the total count of domains."
+    return 1
+  }
+
+  [ "$TW_Domains_Total" -eq "0" ] && {
+    _err "Domains are missing."
+    return 1
+  }
+
+  _debug "Total count of domains in the Timeweb Cloud account: $TW_Domains_Total."
+}
+
+# Lists domain DNS records via the Timeweb Cloud API.
+#
+# Param 1: Limit for listed DNS records.
+# Param 2: Offset for DNS records list.
+#
+# Sets the "TW_Dns_Records" variable.
+# Sets the "TW_Dns_Records_Total" variable.
+_timeweb_list_dns_records() {
+  _debug "Listing domain DNS records via the Timeweb Cloud API. Limit: $1, offset: $2."
+
+  export _H1="Authorization: Bearer $TW_Token"
+
+  if ! TW_Dns_Records=$(_get "$TW_Api/domains/$TW_Main_Domain/dns-records?limit=$1&offset=$2"); then
+    _err "The request to the Timeweb Cloud API failed."
+    return 1
+  fi
+
+  [ -z "$TW_Dns_Records" ] && {
+    _err "Empty response from the Timeweb Cloud API."
+    return 1
+  }
+
+  TW_Dns_Records_Total=$(
+    echo "$TW_Dns_Records" |
+      sed 's/.*"meta":{"total":\([0-9]*\)[^0-9].*/\1/'
+  )
+
+  [ -z "$TW_Dns_Records_Total" ] && {
+    _err "Failed to extract the total count of DNS records."
+    return 1
+  }
+
+  [ "$TW_Dns_Records_Total" -eq "0" ] && {
+    _err "DNS records are missing."
+    return 1
+  }
+
+  _debug "Total count of DNS records: $TW_Dns_Records_Total."
+}
+
+# Verifies whether the domain is the primary domain for the ACME DNS-01 challenge FQDN.
+# The requirement is that the provided domain is the top-level domain
+# for the ACME DNS-01 challenge FQDN.
+#
+# Param 1: Domain object returned by Timeweb Cloud API.
+#
+# Sets the "TW_Main_Domain" variable (e.g. "_acme-challenge.s1.domain.co.uk" → "domain.co.uk").
+# Sets the "TW_Subdomains" variable (e.g. "_acme-challenge.s1.domain.co.uk" → "_acme-challenge.s1").
+_timeweb_is_main_domain() {
+  _debug "Checking if \"$1\" is the main domain of the ACME DNS-01 challenge FQDN."
+
+  [ -z "$1" ] && {
+    _debug "Failed to extract FQDN. Skipping domain."
+    return 1
+  }
+
+  ! echo ".$Acme_Fqdn" | grep -qi "\.$1$" && {
+    _debug "Domain does not match the ACME DNS-01 challenge FQDN. Skipping domain."
+    return 1
+  }
+
+  TW_Main_Domain=$1
+  TW_Subdomains=$(
+    echo "$Acme_Fqdn" |
+      sed "s/\.*.\{${#1}\}$//"
+  )
+
+  _debug "Matched domain. ACME DNS-01 challenge FQDN  split as [$TW_Subdomains].[$TW_Main_Domain]."
+  return 0
+}
+
+# Verifies whether a DNS record was previously added based on the following criteria:
+# - The value matches the ACME DNS-01 challenge TXT record value;
+# - The record type is TXT;
+# - The subdomain matches the ACME DNS-01 challenge FQDN.
+#
+# Param 1: DNS record object returned by Timeweb Cloud API.
+#
+# Sets the "TW_Dns_Txt_Id" variable.
+_timeweb_is_added_txt() {
+  _debug "Checking if \"$1\" is a previously added DNS TXT record."
+
+  echo "$1" | grep -qv '"type":"TXT"' && {
+    _debug "Not a TXT record. Skipping the record."
+    return 1
+  }
+
+  if [ -n "$TW_Subdomains" ]; then
+    echo "$1" | grep -qvi "\"subdomain\":\"$TW_Subdomains\"" && {
+      _debug "Subdomains do not match. Skipping the record."
+      return 1
+    }
+  else
+    echo "$1" | grep -q '"subdomain\":"..*"' && {
+      _debug "Subdomains do not match. Skipping the record."
+      return 1
+    }
+  fi
+
+  TW_Dns_Txt_Id=$(
+    echo "$1" |
+      sed 's/.*"id":\([0-9]*\)[^0-9].*/\1/'
+  )
+
+  [ -z "$TW_Dns_Txt_Id" ] && {
+    _debug "Failed to extract the DNS record ID. Skipping the record."
+    return 1
+  }
+
+  _debug "Matching DNS TXT record ID is \"$TW_Dns_Txt_Id\"."
+  return 0
+}
+
+# Adds a DNS TXT record via the Timeweb Cloud API.
+_timeweb_dns_txt_add() {
+  _debug "Adding a new DNS TXT record via the Timeweb Cloud API."
+
+  export _H1="Authorization: Bearer $TW_Token"
+  export _H2="Content-Type: application/json"
+
+  if ! TW_Response=$(
+    _post "{
+      \"subdomain\":\"$TW_Subdomains\",
+      \"type\":\"TXT\",
+      \"value\":\"$Acme_Txt\"
+    }" \
+      "$TW_Api/domains/$TW_Main_Domain/dns-records"
+  ); then
+    _err "The request to the Timeweb Cloud API failed."
+    return 1
+  fi
+
+  [ -z "$TW_Response" ] && {
+    _err "An unexpected empty response was received from the Timeweb Cloud API."
+    return 1
+  }
+
+  TW_Dns_Txt_Id=$(
+    echo "$TW_Response" |
+      sed 's/.*"id":\([0-9]*\)[^0-9].*/\1/'
+  )
+
+  [ -z "$TW_Dns_Txt_Id" ] && {
+    _err "Failed to extract the DNS TXT Record ID."
+    return 1
+  }
+
+  _debug "DNS TXT record has been added. ID: \"$TW_Dns_Txt_Id\"."
+}
+
+# Removes a DNS record via the Timeweb Cloud API.
+_timeweb_dns_txt_remove() {
+  _debug "Removing DNS record via the Timeweb Cloud API."
+
+  export _H1="Authorization: Bearer $TW_Token"
+
+  if ! TW_Response=$(
+    _post \
+      "" \
+      "$TW_Api/domains/$TW_Main_Domain/dns-records/$TW_Dns_Txt_Id" \
+      "" \
+      "DELETE"
+  ); then
+    _err "The request to the Timeweb Cloud API failed."
+    return 1
+  fi
+
+  [ -n "$TW_Response" ] && {
+    _err "Received an unexpected response body from the Timeweb Cloud API."
+    return 1
+  }
+
+  _debug "DNS TXT record with ID \"$TW_Dns_Txt_Id\" has been removed."
+}

+ 0 - 121
dnsapi/dns_yandex.sh

@@ -1,121 +0,0 @@
-#!/usr/bin/env sh
-# shellcheck disable=SC2034
-dns_yandex_info='Yandex Domains
-Site: tech.Yandex.com/domain/
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_yandex
-Options:
- PDD_Token API Token
-Issues: github.com/non7top/acme.sh/issues
-Author: <[email protected]>
-'
-
-########  Public functions #####################
-
-#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_yandex_add() {
-  fulldomain="${1}"
-  txtvalue="${2}"
-  _debug "Calling: dns_yandex_add() '${fulldomain}' '${txtvalue}'"
-
-  _PDD_credentials || return 1
-
-  _PDD_get_domain || return 1
-  _debug "Found suitable domain: $domain"
-
-  _PDD_get_record_ids || return 1
-  _debug "Record_ids: $record_ids"
-
-  if [ -n "$record_ids" ]; then
-    _info "All existing $subdomain records from $domain will be removed at the very end."
-  fi
-
-  data="domain=${domain}&type=TXT&subdomain=${subdomain}&ttl=300&content=${txtvalue}"
-  uri="https://pddimp.yandex.ru/api2/admin/dns/add"
-  result="$(_post "${data}" "${uri}" | _normalizeJson)"
-  _debug "Result: $result"
-
-  if ! _contains "$result" '"success":"ok"'; then
-    if _contains "$result" '"success":"error"' && _contains "$result" '"error":"record_exists"'; then
-      _info "Record already exists."
-    else
-      _err "Can't add $subdomain to $domain."
-      return 1
-    fi
-  fi
-}
-
-#Usage: dns_myapi_rm   _acme-challenge.www.domain.com
-dns_yandex_rm() {
-  fulldomain="${1}"
-  _debug "Calling: dns_yandex_rm() '${fulldomain}'"
-
-  _PDD_credentials || return 1
-
-  _PDD_get_domain "$fulldomain" || return 1
-  _debug "Found suitable domain: $domain"
-
-  _PDD_get_record_ids "${domain}" "${subdomain}" || return 1
-  _debug "Record_ids: $record_ids"
-
-  for record_id in $record_ids; do
-    data="domain=${domain}&record_id=${record_id}"
-    uri="https://pddimp.yandex.ru/api2/admin/dns/del"
-    result="$(_post "${data}" "${uri}" | _normalizeJson)"
-    _debug "Result: $result"
-
-    if ! _contains "$result" '"success":"ok"'; then
-      _info "Can't remove $subdomain from $domain."
-    fi
-  done
-}
-
-####################  Private functions below ##################################
-
-_PDD_get_domain() {
-  subdomain_start=1
-  while true; do
-    domain_start=$(_math $subdomain_start + 1)
-    domain=$(echo "$fulldomain" | cut -d . -f "$domain_start"-)
-    subdomain=$(echo "$fulldomain" | cut -d . -f -"$subdomain_start")
-
-    _debug "Checking domain $domain"
-    if [ -z "$domain" ]; then
-      return 1
-    fi
-
-    uri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=$domain"
-    result="$(_get "${uri}" | _normalizeJson)"
-    _debug "Result: $result"
-
-    if _contains "$result" '"success":"ok"'; then
-      return 0
-    fi
-    subdomain_start=$(_math $subdomain_start + 1)
-  done
-}
-
-_PDD_credentials() {
-  if [ -z "${PDD_Token}" ]; then
-    PDD_Token=""
-    _err "You need to export PDD_Token=xxxxxxxxxxxxxxxxx."
-    _err "You can get it at https://pddimp.yandex.ru/api2/admin/get_token."
-    return 1
-  else
-    _saveaccountconf PDD_Token "${PDD_Token}"
-  fi
-  export _H1="PddToken: $PDD_Token"
-}
-
-_PDD_get_record_ids() {
-  _debug "Check existing records for $subdomain"
-
-  uri="https://pddimp.yandex.ru/api2/admin/dns/list?domain=${domain}"
-  result="$(_get "${uri}" | _normalizeJson)"
-  _debug "Result: $result"
-
-  if ! _contains "$result" '"success":"ok"'; then
-    return 1
-  fi
-
-  record_ids=$(echo "$result" | _egrep_o "{[^{]*\"subdomain\":\"${subdomain}\"[^}]*}" | sed -n -e 's#.*"record_id": \([0-9]*\).*#\1#p')
-}

+ 352 - 0
dnsapi/dns_yandex360.sh

@@ -0,0 +1,352 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_yandex360_info='Yandex 360 for Business DNS API.
+Yandex 360 for Business is a digital environment for effective collaboration.
+Site: https://360.yandex.com/
+Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_yandex360
+Options:
+ YANDEX360_CLIENT_ID OAuth 2.0 ClientID
+ YANDEX360_CLIENT_SECRET OAuth 2.0 Client secret
+OptionsAlt:
+ YANDEX360_ORG_ID Organization ID. Optional.
+ YANDEX360_ACCESS_TOKEN OAuth 2.0 Access token. Optional.
+Issues: https://github.com/acmesh-official/acme.sh/issues/5213
+Author: <[email protected]>
+'
+
+YANDEX360_API_BASE='https://api360.yandex.net/directory/v1'
+YANDEX360_OAUTH_BASE='https://oauth.yandex.ru'
+
+########  Public functions #####################
+
+dns_yandex360_add() {
+  fulldomain="$(_idn "$1")"
+  txtvalue=$2
+  _info 'Using Yandex 360 DNS API'
+
+  if ! _check_variables; then
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
+  sub_domain=$(echo "$fulldomain" | sed "s/\.$root_domain$//")
+
+  _debug 'Adding Yandex 360 DNS record for subdomain' "$sub_domain"
+  dns_api_url="${YANDEX360_API_BASE}/org/${YANDEX360_ORG_ID}/domains/${root_domain}/dns"
+  data='{"name":"'"$sub_domain"'","type":"TXT","ttl":60,"text":"'"$txtvalue"'"}'
+
+  response="$(_post "$data" "$dns_api_url" '' 'POST' 'application/json')"
+
+  if _contains "$response" 'recordId'; then
+    return 0
+  else
+    _debug 'Response' "$response"
+    return 1
+  fi
+}
+
+dns_yandex360_rm() {
+  fulldomain="$(_idn "$1")"
+  txtvalue=$2
+  _info 'Using Yandex 360 DNS API'
+
+  if ! _check_variables; then
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
+  _debug 'Retrieving 100 records from Yandex 360 DNS'
+  dns_api_url="${YANDEX360_API_BASE}/org/${YANDEX360_ORG_ID}/domains/${root_domain}/dns?perPage=100"
+  response="$(_get "$dns_api_url" '' '')"
+
+  if ! _contains "$response" "$txtvalue"; then
+    _info 'DNS record not found. Nothing to remove.'
+    _debug 'Response' "$response"
+    return 1
+  fi
+
+  response="$(echo "$response" | _normalizeJson)"
+
+  record_id=$(
+    echo "$response" |
+      _egrep_o '\{[^}]*'"${txtvalue}"'[^}]*\}' |
+      _egrep_o '"recordId":[0-9]*' |
+      cut -d':' -f2
+  )
+
+  if [ -z "$record_id" ]; then
+    _err 'Unable to get record ID to remove'
+    return 1
+  fi
+
+  _debug 'Removing DNS record' "$record_id"
+  delete_url="${YANDEX360_API_BASE}/org/${YANDEX360_ORG_ID}/domains/${root_domain}/dns/${record_id}"
+
+  response="$(_post '' "$delete_url" '' 'DELETE')"
+
+  if _contains "$response" '{}'; then
+    return 0
+  else
+    _debug 'Response' "$response"
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################
+
+_check_variables() {
+  YANDEX360_CLIENT_ID="${YANDEX360_CLIENT_ID:-$(_readaccountconf_mutable YANDEX360_CLIENT_ID)}"
+  YANDEX360_CLIENT_SECRET="${YANDEX360_CLIENT_SECRET:-$(_readaccountconf_mutable YANDEX360_CLIENT_SECRET)}"
+  YANDEX360_ORG_ID="${YANDEX360_ORG_ID:-$(_readaccountconf_mutable YANDEX360_ORG_ID)}"
+  YANDEX360_ACCESS_TOKEN="${YANDEX360_ACCESS_TOKEN:-$(_readaccountconf_mutable YANDEX360_ACCESS_TOKEN)}"
+  YANDEX360_REFRESH_TOKEN="${YANDEX360_REFRESH_TOKEN:-$(_readaccountconf_mutable YANDEX360_REFRESH_TOKEN)}"
+
+  if [ -n "$YANDEX360_ACCESS_TOKEN" ]; then
+    _info '========================================='
+    _info '              ATTENTION'
+    _info '========================================='
+    _info 'A manually provided Yandex 360 access token has been detected, which is not recommended.'
+    _info 'Please note that this token is valid for a limited time after issuance.'
+    _info 'It is recommended to obtain the token interactively using acme.sh for one-time setup.'
+    _info 'Subsequent token renewals will be handled automatically.'
+    _info 'For more details, please visit: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_yandex360'
+    _info '========================================='
+
+    _saveaccountconf_mutable YANDEX360_ACCESS_TOKEN "$YANDEX360_ACCESS_TOKEN"
+    export _H1="Authorization: OAuth $YANDEX360_ACCESS_TOKEN"
+
+  elif [ -z "$YANDEX360_CLIENT_ID" ] || [ -z "$YANDEX360_CLIENT_SECRET" ]; then
+    _err '========================================='
+    _err '                 ERROR'
+    _err '========================================='
+    _err 'The required environment variables YANDEX360_CLIENT_ID and YANDEX360_CLIENT_SECRET are not set.'
+    _err 'Alternatively, you can set YANDEX360_ACCESS_TOKEN environment variable.'
+    _err 'For more details, please visit: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_yandex360'
+    _err '========================================='
+    return 1
+
+  else
+    _saveaccountconf_mutable YANDEX360_CLIENT_ID "$YANDEX360_CLIENT_ID"
+    _saveaccountconf_mutable YANDEX360_CLIENT_SECRET "$YANDEX360_CLIENT_SECRET"
+
+    if [ -n "$YANDEX360_REFRESH_TOKEN" ]; then
+      _debug 'Refresh token found. Attempting to refresh access token.'
+    fi
+
+    _refresh_token || _get_token || return 1
+  fi
+
+  if [ -z "$YANDEX360_ORG_ID" ]; then
+    org_response="$(_get "${YANDEX360_API_BASE}/org" '' '')"
+
+    if _contains "$org_response" '"organizations"'; then
+      org_response="$(echo "$org_response" | _normalizeJson)"
+      YANDEX360_ORG_ID=$(
+        echo "$org_response" |
+          _egrep_o '"id":[[:space:]]*[0-9]+' |
+          cut -d':' -f2
+      )
+      _debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID"
+    else
+      _err '========================================='
+      _err '                 ERROR'
+      _err '========================================='
+      _err "Failed to retrieve YANDEX360_ORG_ID automatically."
+      _err 'For more details, please visit: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_yandex360'
+      _err '========================================='
+      _debug 'Response' "$org_response"
+      return 1
+    fi
+  fi
+
+  return 0
+}
+
+_get_token() {
+  _info "$(__red '=========================================')"
+  _info "$(__red '                 NOTICE')"
+  _info "$(__red '=========================================')"
+  _info "$(__red 'Before using the Yandex 360 API, you need to complete an authorization procedure.')"
+  _info "$(__red 'The initial access token is obtained interactively and is a one-time operation.')"
+  _info "$(__red 'Subsequent API requests will be handled automatically.')"
+  _info "$(__red '=========================================')"
+
+  _info 'Initiating device authorization flow'
+  device_code_url="${YANDEX360_OAUTH_BASE}/device/code"
+
+  hostname=$(uname -n)
+  data="client_id=$YANDEX360_CLIENT_ID&device_id=acme.sh ${hostname}&device_name=acme.sh ${hostname}"
+
+  response="$(_post "$data" "$device_code_url" '' 'POST')"
+
+  if ! _contains "$response" 'device_code'; then
+    _err 'Failed to get device code'
+    _debug 'Response' "$response"
+    return 1
+  fi
+
+  response="$(echo "$response" | _normalizeJson)"
+
+  device_code=$(
+    echo "$response" |
+      _egrep_o '"device_code":"[^"]*"' |
+      cut -d'"' -f4
+  )
+  _debug 'Device code' "$device_code"
+
+  user_code=$(
+    echo "$response" |
+      _egrep_o '"user_code":"[^"]*"' |
+      cut -d'"' -f4
+  )
+  _debug 'User code' "$user_code"
+
+  verification_url=$(
+    echo "$response" |
+      _egrep_o '"verification_url":"[^"]*"' |
+      cut -d'"' -f4
+  )
+  _debug 'Verification URL' "$verification_url"
+
+  interval=$(
+    echo "$response" |
+      _egrep_o '"interval":[[:space:]]*[0-9]+' |
+      cut -d':' -f2
+  )
+  _debug 'Polling interval' "$interval"
+
+  _info "$(__red 'Please visit '"$verification_url"' and log in as an organization administrator')"
+  _info "$(__red 'Once logged in, enter the code: '"$user_code"' on the page from the previous step')"
+  _info "$(__red 'Waiting for authorization...')"
+
+  _debug 'Polling for token'
+  token_url="${YANDEX360_OAUTH_BASE}/token"
+
+  while true; do
+    data="grant_type=device_code&code=$device_code&client_id=$YANDEX360_CLIENT_ID&client_secret=$YANDEX360_CLIENT_SECRET"
+
+    response="$(_post "$data" "$token_url" '' 'POST')"
+
+    if _contains "$response" 'access_token'; then
+      response="$(echo "$response" | _normalizeJson)"
+      YANDEX360_ACCESS_TOKEN=$(
+        echo "$response" |
+          _egrep_o '"access_token":"[^"]*"' |
+          cut -d'"' -f4
+      )
+      YANDEX360_REFRESH_TOKEN=$(
+        echo "$response" |
+          _egrep_o '"refresh_token":"[^"]*"' |
+          cut -d'"' -f4
+      )
+
+      _secure_debug 'Obtained access token' "$YANDEX360_ACCESS_TOKEN"
+      _secure_debug 'Obtained refresh token' "$YANDEX360_REFRESH_TOKEN"
+
+      _saveaccountconf_mutable YANDEX360_REFRESH_TOKEN "$YANDEX360_REFRESH_TOKEN"
+
+      export _H1="Authorization: OAuth $YANDEX360_ACCESS_TOKEN"
+
+      _info 'Access token obtained successfully'
+      return 0
+    elif _contains "$response" 'authorization_pending'; then
+      _debug 'Response' "$response"
+      _debug "Authorization pending. Waiting $interval seconds before next attempt."
+      _sleep "$interval"
+    else
+      _debug 'Response' "$response"
+      _err 'Failed to get access token'
+      return 1
+    fi
+  done
+}
+
+_refresh_token() {
+  token_url="${YANDEX360_OAUTH_BASE}/token"
+
+  data="grant_type=refresh_token&refresh_token=$YANDEX360_REFRESH_TOKEN&client_id=$YANDEX360_CLIENT_ID&client_secret=$YANDEX360_CLIENT_SECRET"
+
+  response="$(_post "$data" "$token_url" '' 'POST')"
+
+  if _contains "$response" 'access_token'; then
+    response="$(echo "$response" | _normalizeJson)"
+    YANDEX360_ACCESS_TOKEN=$(
+      echo "$response" |
+        _egrep_o '"access_token":"[^"]*"' |
+        cut -d'"' -f4
+    )
+    YANDEX360_REFRESH_TOKEN=$(
+      echo "$response" |
+        _egrep_o '"refresh_token":"[^"]*"' |
+        cut -d'"' -f4
+    )
+
+    _secure_debug 'Received access token' "$YANDEX360_ACCESS_TOKEN"
+    _secure_debug 'Received refresh token' "$YANDEX360_REFRESH_TOKEN"
+
+    _saveaccountconf_mutable YANDEX360_REFRESH_TOKEN "$YANDEX360_REFRESH_TOKEN"
+
+    export _H1="Authorization: OAuth $YANDEX360_ACCESS_TOKEN"
+
+    _info 'Access token refreshed successfully'
+    return 0
+  else
+    _info 'Failed to refresh token. Will attempt to obtain a new one.'
+    _debug 'Response' "$response"
+    return 1
+  fi
+}
+
+_get_root() {
+  domain="$1"
+
+  for org_id in $YANDEX360_ORG_ID; do
+    _debug 'Checking organization ID' "$org_id"
+    domains_api_url="${YANDEX360_API_BASE}/org/${org_id}/domains"
+
+    domains_response="$(_get "$domains_api_url" '' '')"
+
+    if ! _contains "$domains_response" '"domains"'; then
+      _debug 'No domains found for organization' "$org_id"
+      _debug 'Response' "$domains_response"
+      continue
+    fi
+
+    domains_response="$(echo "$domains_response" | _normalizeJson)"
+    domain_names=$(
+      echo "$domains_response" |
+        _egrep_o '"name":"[^"]*"' |
+        cut -d'"' -f4
+    )
+
+    for d in $domain_names; do
+      d="$(_idn "$d")"
+      _debug 'Checking domain' "$d"
+
+      if _endswith "$domain" "$d"; then
+        root_domain="$d"
+        break
+      fi
+    done
+
+    if [ -n "$root_domain" ]; then
+      _debug "Root domain found: $root_domain in organization $org_id"
+
+      YANDEX360_ORG_ID="$org_id"
+      _saveaccountconf_mutable YANDEX360_ORG_ID "$YANDEX360_ORG_ID"
+
+      return 0
+    fi
+  done
+
+  if [ -z "$root_domain" ]; then
+    _err "Could not find a matching root domain for $domain in any organization"
+    return 1
+  fi
+}

+ 32 - 37
notify/teams.sh

@@ -3,10 +3,6 @@
 #Support Microsoft Teams webhooks
 
 #TEAMS_WEBHOOK_URL=""
-#TEAMS_THEME_COLOR=""
-#TEAMS_SUCCESS_COLOR=""
-#TEAMS_ERROR_COLOR=""
-#TEAMS_SKIP_COLOR=""
 
 teams_send() {
   _subject="$1"
@@ -14,9 +10,9 @@ teams_send() {
   _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
   _debug "_statusCode" "$_statusCode"
 
-  _color_success="2cbe4e" # green
-  _color_danger="cb2431"  # red
-  _color_muted="586069"   # gray
+  _color_success="Good"
+  _color_danger="Attention"
+  _color_muted="Accent"
 
   TEAMS_WEBHOOK_URL="${TEAMS_WEBHOOK_URL:-$(_readaccountconf_mutable TEAMS_WEBHOOK_URL)}"
   if [ -z "$TEAMS_WEBHOOK_URL" ]; then
@@ -26,26 +22,6 @@ teams_send() {
   fi
   _saveaccountconf_mutable TEAMS_WEBHOOK_URL "$TEAMS_WEBHOOK_URL"
 
-  TEAMS_THEME_COLOR="${TEAMS_THEME_COLOR:-$(_readaccountconf_mutable TEAMS_THEME_COLOR)}"
-  if [ -n "$TEAMS_THEME_COLOR" ]; then
-    _saveaccountconf_mutable TEAMS_THEME_COLOR "$TEAMS_THEME_COLOR"
-  fi
-
-  TEAMS_SUCCESS_COLOR="${TEAMS_SUCCESS_COLOR:-$(_readaccountconf_mutable TEAMS_SUCCESS_COLOR)}"
-  if [ -n "$TEAMS_SUCCESS_COLOR" ]; then
-    _saveaccountconf_mutable TEAMS_SUCCESS_COLOR "$TEAMS_SUCCESS_COLOR"
-  fi
-
-  TEAMS_ERROR_COLOR="${TEAMS_ERROR_COLOR:-$(_readaccountconf_mutable TEAMS_ERROR_COLOR)}"
-  if [ -n "$TEAMS_ERROR_COLOR" ]; then
-    _saveaccountconf_mutable TEAMS_ERROR_COLOR "$TEAMS_ERROR_COLOR"
-  fi
-
-  TEAMS_SKIP_COLOR="${TEAMS_SKIP_COLOR:-$(_readaccountconf_mutable TEAMS_SKIP_COLOR)}"
-  if [ -n "$TEAMS_SKIP_COLOR" ]; then
-    _saveaccountconf_mutable TEAMS_SKIP_COLOR "$TEAMS_SKIP_COLOR"
-  fi
-
   export _H1="Content-Type: application/json"
 
   _subject=$(echo "$_subject" | _json_encode)
@@ -63,16 +39,35 @@ teams_send() {
     ;;
   esac
 
-  _color=$(echo "$_color" | tr -cd 'a-fA-F0-9')
-  if [ -z "$_color" ]; then
-    _color=$(echo "${TEAMS_THEME_COLOR:-$_color_muted}" | tr -cd 'a-fA-F0-9')
-  fi
-
-  _data="{\"title\": \"$_subject\","
-  if [ -n "$_color" ]; then
-    _data="$_data\"themeColor\": \"$_color\", "
-  fi
-  _data="$_data\"text\": \"$_content\"}"
+  _data="{
+    \"type\": \"message\",
+    \"attachments\": [
+        {
+            \"contentType\": \"application/vnd.microsoft.card.adaptive\",
+            \"contentUrl\": null,
+            \"content\": {
+                \"schema\": \"http://adaptivecards.io/schemas/adaptive-card.json\",
+                \"type\": \"AdaptiveCard\",
+                \"version\": \"1.2\",
+                \"body\": [
+                    {
+                        \"type\": \"TextBlock\",
+                        \"size\": \"large\",
+                        \"weight\": \"bolder\",
+                        \"wrap\": true,
+                        \"color\": \"$_color\",
+                        \"text\": \"$_subject\"
+                    },
+                    {
+                        \"type\": \"TextBlock\",
+                        \"text\": \"$_content\",
+                        \"wrap\": true
+                    }
+                ]
+            }
+        }
+    ]
+}"
 
   if response=$(_post "$_data" "$TEAMS_WEBHOOK_URL"); then
     if ! _contains "$response" error; then