diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 727ba315..c1406d91 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -1,5 +1,6 @@ name: DNS on: + workflow_dispatch: push: paths: - 'dnsapi/*.sh' diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index ea446d84..435fd6b5 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -15,6 +15,8 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + DOCKER_IMAGE: neilpang/acme.sh jobs: CheckToken: @@ -44,6 +46,11 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v2 + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: ${DOCKER_IMAGE} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: login to docker hub @@ -51,8 +58,6 @@ jobs: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - name: build and push the image run: | - DOCKER_IMAGE=neilpang/acme.sh - if [[ $GITHUB_REF == refs/tags/* ]]; then DOCKER_IMAGE_TAG=${GITHUB_REF#refs/tags/} fi @@ -66,8 +71,14 @@ jobs: fi fi + DOCKER_LABELS=() + while read -r label; do + DOCKER_LABELS+=(--label "${label}") + done <<<"${DOCKER_METADATA_OUTPUT_LABELS}" + docker buildx build \ --tag ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \ + "${DOCKER_LABELS[@]}" \ --output "type=image,push=true" \ --build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \ --platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x . diff --git a/acme.sh b/acme.sh index 4cf407f2..e39a146b 100755 --- a/acme.sh +++ b/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=3.0.8 +VER=3.1.0 PROJECT_NAME="acme.sh" @@ -672,8 +672,10 @@ _hex_dump() { #0 1 2 3 4 5 6 7 8 9 - _ . ~ #30 31 32 33 34 35 36 37 38 39 2d 5f 2e 7e +#_url_encode [upper-hex] the encoded hex will be upper-case if the argument upper-hex is followed #stdin stdout _url_encode() { + _upper_hex=$1 _hex_str=$(_hex_dump) _debug3 "_url_encode" _debug3 "_hex_str" "$_hex_str" @@ -883,6 +885,9 @@ _url_encode() { ;; #other hex *) + if [ "$_upper_hex" = "upper-hex" ]; then + _hex_code=$(printf "%s" "$_hex_code" | _upper_case) + fi printf '%%%s' "$_hex_code" ;; esac @@ -2361,7 +2366,7 @@ _clear_conf() { _sdkey="$2" if [ "$_c_c_f" ]; then _conf_data="$(cat "$_c_c_f")" - echo "$_conf_data" | sed "s/^$_sdkey *=.*$//" >"$_c_c_f" + echo "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f" else _err "Config file is empty, cannot clear" fi @@ -5111,6 +5116,19 @@ $_authorizations_map" _on_issue_err "$_post_hook" "$vlist" return 1 fi + _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r') + _sleep_overload_retry_sec=$_retryafter + if [ "$_sleep_overload_retry_sec" ]; then + if [ $_sleep_overload_retry_sec -le 600 ]; then + _sleep $_sleep_overload_retry_sec + else + _info "The retryafter=$_retryafter value is too large (> 600), will not retry anymore." + _clearupwebbroot "$_currentRoot" "$removelevel" "$token" + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi + fi done done diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh new file mode 100644 index 00000000..70a2e532 --- /dev/null +++ b/deploy/ali_cdn.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034,SC2154 + +# Script to create certificate to Alibaba Cloud CDN +# +# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#33-deploy-your-certificate-to-cdn-or-dcdn-of-alibaba-cloud-aliyun +# +# This deployment required following variables +# export Ali_Key="ALIACCESSKEY" +# export Ali_Secret="ALISECRETKEY" +# The credentials are shared with all the Alibaba Cloud deploy hooks and dnsapi +# +# To specify the CDN domain that is different from the certificate CN, usually used for multi-domain or wildcard certificates +# export DEPLOY_ALI_CDN_DOMAIN="cdn.example.com" +# If you have multiple CDN domains using the same certificate, just +# export DEPLOY_ALI_CDN_DOMAIN="cdn1.example.com cdn2.example.com" +# +# For DCDN, see ali_dcdn deploy hook + +Ali_CDN_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" + + # Load dnsapi/dns_ali.sh to reduce the duplicated codes + # https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276 + dnsapi_ali="$(_findHook "$_cdomain" "$_SUB_FOLDER_DNSAPI" dns_ali)" + # shellcheck source=/dev/null + if ! . "$dnsapi_ali"; then + _err "Error loading file $dnsapi_ali. Please check your API file and try again." + return 1 + fi + + _prepare_ali_credentials || return 1 + + _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-hex <"$_cfullchain") + _key=$(_url_encode upper-hex <"$_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 +} + +# domain pub pri +_set_cdn_domain_ssl_certificate_query() { + endpoint=$Ali_CDN_API + 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' +} diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh new file mode 100644 index 00000000..14ac500a --- /dev/null +++ b/deploy/ali_dcdn.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034,SC2154 + +# Script to create certificate to Alibaba Cloud DCDN +# +# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#33-deploy-your-certificate-to-cdn-or-dcdn-of-alibaba-cloud-aliyun +# +# This deployment required following variables +# export Ali_Key="ALIACCESSKEY" +# export Ali_Secret="ALISECRETKEY" +# The credentials are shared with all the Alibaba Cloud deploy hooks and dnsapi +# +# To specify the DCDN domain that is different from the certificate CN, usually used for multi-domain or wildcard certificates +# export DEPLOY_ALI_DCDN_DOMAIN="dcdn.example.com" +# If you have multiple CDN domains using the same certificate, just +# export DEPLOY_ALI_DCDN_DOMAIN="dcdn1.example.com dcdn2.example.com" +# +# For regular CDN, see ali_cdn deploy hook + +Ali_DCDN_API="https://dcdn.aliyuncs.com/" + +ali_dcdn_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" + + # Load dnsapi/dns_ali.sh to reduce the duplicated codes + # https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276 + dnsapi_ali="$(_findHook "$_cdomain" "$_SUB_FOLDER_DNSAPI" dns_ali)" + # shellcheck source=/dev/null + if ! . "$dnsapi_ali"; then + _err "Error loading file $dnsapi_ali. Please check your API file and try again." + return 1 + fi + + _prepare_ali_credentials || return 1 + + _getdeployconf DEPLOY_ALI_DCDN_DOMAIN + if [ "$DEPLOY_ALI_DCDN_DOMAIN" ]; then + _savedeployconf DEPLOY_ALI_DCDN_DOMAIN "$DEPLOY_ALI_DCDN_DOMAIN" + else + DEPLOY_ALI_DCDN_DOMAIN="$_cdomain" + fi + + # read cert and key files and urlencode both + _cert=$(_url_encode upper-hex <"$_cfullchain") + _key=$(_url_encode upper-hex <"$_ckey") + + _debug2 _cert "$_cert" + _debug2 _key "$_key" + + ## update domain ssl config + for domain in $DEPLOY_ALI_DCDN_DOMAIN; do + _set_dcdn_domain_ssl_certificate_query "$domain" "$_cert" "$_key" + if _ali_rest "Set DCDN domain SSL certificate for $domain" "" POST; then + _info "Domain $domain certificate has been deployed successfully" + fi + done + + return 0 +} + +# domain pub pri +_set_dcdn_domain_ssl_certificate_query() { + endpoint=$Ali_DCDN_API + query='' + query=$query'AccessKeyId='$Ali_Key + query=$query'&Action=SetDcdnDomainSSLCertificate' + 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-01-15' +} diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh index 3ddb8de1..0d01e199 100644 --- a/deploy/synology_dsm.sh +++ b/deploy/synology_dsm.sh @@ -113,9 +113,9 @@ synology_dsm_deploy() { # Default values for scheme, hostname and port # Defaulting to localhost and http, because it's localhost… - [ -n "$SYNO_SCHEME" ] || SYNO_SCHEME="http" - [ -n "$SYNO_HOSTNAME" ] || SYNO_HOSTNAME="localhost" - [ -n "$SYNO_PORT" ] || SYNO_PORT="5000" + [ -n "$SYNO_SCHEME" ] || SYNO_SCHEME=http + [ -n "$SYNO_HOSTNAME" ] || SYNO_HOSTNAME=localhost + [ -n "$SYNO_PORT" ] || SYNO_PORT=5000 _savedeployconf SYNO_SCHEME "$SYNO_SCHEME" _savedeployconf SYNO_HOSTNAME "$SYNO_HOSTNAME" _savedeployconf SYNO_PORT "$SYNO_PORT" diff --git a/deploy/unifi.sh b/deploy/unifi.sh index a864135e..4d8c058e 100644 --- a/deploy/unifi.sh +++ b/deploy/unifi.sh @@ -5,6 +5,15 @@ # - self-hosted Unifi Controller # - Unifi Cloud Key (Gen1/2/2+) # - Unifi Cloud Key running UnifiOS (v2.0.0+, Gen2/2+ only) +# - Unifi Dream Machine +# This has not been tested on other "all-in-one" devices such as +# UDM Pro or Unifi Express. +# +# OS Version v2.0.0+ +# Network Application version 7.0.0+ +# OS version ~3.1 removed java and keytool from the UnifiOS. +# Using PKCS12 format keystore appears to work fine. +# # Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3359 #returns 0 means success, otherwise error. @@ -74,14 +83,16 @@ unifi_deploy() { _reload_cmd="" # Unifi Controller environment (self hosted or any Cloud Key) -- - # auto-detect by file /usr/lib/unifi/data/keystore: + # auto-detect by file /usr/lib/unifi/data/keystore _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-/usr/lib/unifi/data/keystore}" if [ -f "$_unifi_keystore" ]; then - _info "Installing certificate for Unifi Controller (Java keystore)" _debug _unifi_keystore "$_unifi_keystore" if ! _exists keytool; then - _err "keytool not found" - return 1 + _do_keytool=0 + _info "Installing certificate for Unifi Controller (PKCS12 keystore)." + else + _do_keytool=1 + _info "Installing certificate for Unifi Controller (Java keystore)" fi if [ ! -w "$_unifi_keystore" ]; then _err "The file $_unifi_keystore is not writable, please change the permission." @@ -92,6 +103,7 @@ unifi_deploy() { _debug "Generate import pkcs12" _import_pkcs12="$(_mktemp)" + _debug "_toPkcs $_import_pkcs12 $_ckey $_ccert $_cca $_unifi_keypass unifi root" _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root # shellcheck disable=SC2181 if [ "$?" != "0" ]; then @@ -99,22 +111,57 @@ unifi_deploy() { return 1 fi - _debug "Import into keystore: $_unifi_keystore" - if keytool -importkeystore \ - -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \ - -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \ - -alias unifi -noprompt; then - _debug "Import keystore success!" - rm "$_import_pkcs12" + # Save the existing keystore in case something goes wrong. + mv -f "${_unifi_keystore}" "${_unifi_keystore}"_original + _info "Previous keystore saved to ${_unifi_keystore}_original." + + if [ "$_do_keytool" -eq 1 ]; then + _debug "Import into keystore: $_unifi_keystore" + if keytool -importkeystore \ + -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \ + -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \ + -alias unifi -noprompt; then + _debug "Import keystore success!" + else + _err "Error importing into Unifi Java keystore." + _err "Please re-run with --debug and report a bug." + _info "Restoring original keystore." + mv -f "${_unifi_keystore}"_original "${_unifi_keystore}" + rm "$_import_pkcs12" + return 1 + fi else - _err "Error importing into Unifi Java keystore." - _err "Please re-run with --debug and report a bug." - rm "$_import_pkcs12" - return 1 + _debug "Copying new keystore to $_unifi_keystore" + cp -f "$_import_pkcs12" "$_unifi_keystore" + fi + + # Update unifi service for certificate cipher compatibility + if ${ACME_OPENSSL_BIN:-openssl} pkcs12 \ + -in "$_import_pkcs12" \ + -password pass:aircontrolenterprise \ + -nokeys | ${ACME_OPENSSL_BIN:-openssl} x509 -text \ + -noout | grep -i "signature" | grep -iq ecdsa >/dev/null 2>&1; then + cp -f /usr/lib/unifi/data/system.properties /usr/lib/unifi/data/system.properties_original + _info "Updating system configuration for cipher compatibility." + _info "Saved original system config to /usr/lib/unifi/data/system.properties_original" + sed -i '/unifi\.https\.ciphers/d' /usr/lib/unifi/data/system.properties + echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>/usr/lib/unifi/data/system.properties + sed -i '/unifi\.https\.sslEnabledProtocols/d' /usr/lib/unifi/data/system.properties + echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>/usr/lib/unifi/data/system.properties + _info "System configuration updated." fi + rm "$_import_pkcs12" + + # Restarting unifi-core will bring up unifi, doing it out of order results in + # a certificate error, and breaks wifiman. + # Restart if we aren't doing unifi-core, otherwise stop for later restart. if systemctl -q is-active unifi; then - _reload_cmd="${_reload_cmd:+$_reload_cmd && }service unifi restart" + if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then + _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi" + else + _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl stop unifi" + fi fi _services_updated="${_services_updated} unifi" _info "Install Unifi Controller certificate success!" @@ -165,6 +212,11 @@ unifi_deploy() { return 1 fi + # Save the existing certs in case something goes wrong. + cp -f "${_unifi_core_config}"/unifi-core.crt "${_unifi_core_config}"/unifi-core_original.crt + cp -f "${_unifi_core_config}"/unifi-core.key "${_unifi_core_config}"/unifi-core_original.key + _info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt/key." + cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt" cat "$_ckey" >"${_unifi_core_config}/unifi-core.key" diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh index 9bdfc20b..0f1626f5 100755 --- a/dnsapi/dns_ali.sh +++ b/dnsapi/dns_ali.sh @@ -9,25 +9,19 @@ Options: Ali_Secret API Secret ' -Ali_API="https://alidns.aliyuncs.com/" +# NOTICE: +# This file is referenced by Alibaba Cloud Services deploy hooks +# https://github.com/acmesh-official/acme.sh/pull/5205#issuecomment-2357867276 +# Be careful when modifying this file, especially when making breaking changes for common functions + +Ali_DNS_API="https://alidns.aliyuncs.com/" #Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" dns_ali_add() { fulldomain=$1 txtvalue=$2 - 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" + _prepare_ali_credentials || return 1 _debug "First detect the root zone" if ! _get_root "$fulldomain"; then @@ -52,7 +46,67 @@ dns_ali_rm() { _clean } -#################### Private functions below ################################## +#################### Alibaba Cloud common functions below #################### + +_prepare_ali_credentials() { + 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" +} + +# act ign mtd +_ali_rest() { + act="$1" + ign="$2" + mtd="${3:-GET}" + + signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64) + signature=$(printf "%s" "$signature" | _url_encode upper-hex) + url="$endpoint?Signature=$signature" + + if [ "$mtd" = "GET" ]; then + url="$url&$query" + response="$(_get "$url")" + else + response="$(_post "$query" "$url" "" "$mtd" "application/x-www-form-urlencoded")" + 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_nonce() { + #_head_n 1 " - return 1 - fi - - _debug2 response "$response" - if [ -z "$2" ]; 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 /\>/g") + _s=$(echo "$_s" | sed 's/"/\"/g') + printf -- %s "$_s" +} + _inwx_login() { if _inwx_check_cookie; then @@ -170,6 +179,8 @@ _inwx_login() { return 0 fi + XML_PASS=$(_htmlEscape "$INWX_Password") + xml_content=$(printf ' account.login @@ -193,7 +204,7 @@ _inwx_login() { - ' "$INWX_User" "$INWX_Password") + ' "$INWX_User" "$XML_PASS") response="$(_post "$xml_content" "$INWX_Api" "" "POST")" diff --git a/dnsapi/dns_ispconfig.sh b/dnsapi/dns_ispconfig.sh index 7fd0d99c..edc789e1 100755 --- a/dnsapi/dns_ispconfig.sh +++ b/dnsapi/dns_ispconfig.sh @@ -14,6 +14,8 @@ Options: # User must provide login data and URL to the ISPConfig installation incl. port. # The remote user in ISPConfig must have access to: # - DNS txt Functions +# - DNS zone functions +# - Client functions ######## Public functions ##################### diff --git a/dnsapi/dns_miab.sh b/dnsapi/dns_miab.sh index ec9867db..9416c8ce 100644 --- a/dnsapi/dns_miab.sh +++ b/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" diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 9df6262e..d5dbbcbc 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/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,25 @@ 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 <.*\(\(.*\)'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(.*<\/type>\).*\(.*<\/value>\).*\(.*<\/prio>\).*\(.*<\/ttl>\)\).*<\/item>.*/\2<\/name>\3\4\5\6<\/item>/p')" fi - if [ -z "$(echo "$new_item" | _egrep_o ".*(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA|NS)<\/type>.*")" ]; then + if [ -z "$(echo "$new_item" | _egrep_o ".*(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA)<\/type>.*")" ]; then _debug "not an allowed record type, skipping" "$new_item" continue fi @@ -152,7 +152,7 @@ dns_openprovider_rm() { new_item="$(echo "$item" | sed -n 's/.*.*\(\(.*\)'"$_domain_name"'\.'"$_domain_extension"'<\/name>.*\(.*<\/type>\).*\(.*<\/value>\).*\(.*<\/prio>\).*\(.*<\/ttl>\)\).*<\/item>.*/\2<\/name>\3\4\5\6<\/item>/p')" fi - if [ -z "$(echo "$new_item" | _egrep_o ".*(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA|NS)<\/type>.*")" ]; then + if [ -z "$(echo "$new_item" | _egrep_o ".*(A|AAAA|CNAME|MX|SPF|SRV|TXT|TLSA|SSHFP|CAA)<\/type>.*")" ]; then _debug "not an allowed record type, skipping" "$new_item" continue fi diff --git a/dnsapi/dns_timeweb.sh b/dnsapi/dns_timeweb.sh new file mode 100644 index 00000000..9860872c --- /dev/null +++ b/dnsapi/dns_timeweb.sh @@ -0,0 +1,406 @@ +#!/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 + TW_Domains_Returned="" + + while [ -z "$TW_Domains_Returned" ] || [ "$TW_Domains_Returned" -ge "$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 + TW_Dns_Records_Returned="" + + while [ -z "$TW_Dns_Records_Returned" ] || [ "$TW_Dns_Records_Returned" -ge "$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_Returned" 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_Returned=$( + echo "$TW_Domains" | + sed 's/.*"meta":{"total":\([0-9]*\)[^0-9].*/\1/' + ) + + [ -z "$TW_Domains_Returned" ] && { + _err "Failed to extract the total count of domains." + return 1 + } + + [ "$TW_Domains_Returned" -eq "0" ] && { + _err "Domains are missing." + return 1 + } + + _debug "Domains returned by Timeweb Cloud API: $TW_Domains_Returned." +} + +# 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_Returned" 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_Returned=$( + echo "$TW_Dns_Records" | + sed 's/.*"meta":{"total":\([0-9]*\)[^0-9].*/\1/' + ) + + [ -z "$TW_Dns_Records_Returned" ] && { + _err "Failed to extract the total count of DNS records." + return 1 + } + + [ "$TW_Dns_Records_Returned" -eq "0" ] && { + _err "DNS records are missing." + return 1 + } + + _debug "DNS records returned by Timeweb Cloud API: $TW_Dns_Records_Returned." +} + +# 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." +} diff --git a/dnsapi/dns_yandex.sh b/dnsapi/dns_yandex.sh deleted file mode 100755 index d780459f..00000000 --- a/dnsapi/dns_yandex.sh +++ /dev/null @@ -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: -' - -######## 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') -} diff --git a/dnsapi/dns_yandex360.sh b/dnsapi/dns_yandex360.sh new file mode 100644 index 00000000..c6b6053d --- /dev/null +++ b/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: +' + +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 +} diff --git a/notify/bark.sh b/notify/bark.sh index bbd5bf34..fd9ebd76 100644 --- a/notify/bark.sh +++ b/notify/bark.sh @@ -1,32 +1,40 @@ #!/usr/bin/env sh -#Support iOS Bark Notification +# Support iOS Bark Notification -#BARK_API_URL="https://api.day.app/xxxx" -#BARK_SOUND="yyyy" -#BARK_GROUP="zzzz" +# Every parameter explained: https://github.com/Finb/bark-server/blob/master/docs/API_V2.md#push -# subject content statusCode +# BARK_API_URL="https://api.day.app/xxxx" (required) +# BARK_GROUP="ACME" (optional) +# BARK_SOUND="alarm" (optional) +# BARK_LEVEL="active" (optional) +# BARK_BADGE=0 (optional) +# BARK_AUTOMATICALLYCOPY="1" (optional) +# BARK_COPY="My clipboard Content" (optional) +# BARK_ICON="https://example.com/icon.png" (optional) +# BARK_ISARCHIVE="1" (optional) +# BARK_URL="https://example.com" (optional) + +# subject content statusCode bark_send() { _subject="$1" _content="$2" - _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped + _statusCode="$3" # 0: success, 1: error, 2: skipped _debug "_subject" "$_subject" _debug "_content" "$_content" _debug "_statusCode" "$_statusCode" + _content=$(echo "$_content" | _url_encode) + _subject=$(echo "$_subject" | _url_encode) + BARK_API_URL="${BARK_API_URL:-$(_readaccountconf_mutable BARK_API_URL)}" if [ -z "$BARK_API_URL" ]; then - BARK_API_URL="" _err "You didn't specify a Bark API URL BARK_API_URL yet." _err "You can download Bark from App Store and get yours." return 1 fi _saveaccountconf_mutable BARK_API_URL "$BARK_API_URL" - BARK_SOUND="${BARK_SOUND:-$(_readaccountconf_mutable BARK_SOUND)}" - _saveaccountconf_mutable BARK_SOUND "$BARK_SOUND" - BARK_GROUP="${BARK_GROUP:-$(_readaccountconf_mutable BARK_GROUP)}" if [ -z "$BARK_GROUP" ]; then BARK_GROUP="ACME" @@ -35,10 +43,79 @@ bark_send() { _saveaccountconf_mutable BARK_GROUP "$BARK_GROUP" fi - _content=$(echo "$_content" | _url_encode) - _subject=$(echo "$_subject" | _url_encode) + BARK_SOUND="${BARK_SOUND:-$(_readaccountconf_mutable BARK_SOUND)}" + if [ -n "$BARK_SOUND" ]; then + _saveaccountconf_mutable BARK_SOUND "$BARK_SOUND" + fi + + BARK_LEVEL="${BARK_LEVEL:-$(_readaccountconf_mutable BARK_LEVEL)}" + if [ -n "$BARK_LEVEL" ]; then + _saveaccountconf_mutable BARK_LEVEL "$BARK_LEVEL" + fi + + BARK_BADGE="${BARK_BADGE:-$(_readaccountconf_mutable BARK_BADGE)}" + if [ -n "$BARK_BADGE" ]; then + _saveaccountconf_mutable BARK_BADGE "$BARK_BADGE" + fi + + BARK_AUTOMATICALLYCOPY="${BARK_AUTOMATICALLYCOPY:-$(_readaccountconf_mutable BARK_AUTOMATICALLYCOPY)}" + if [ -n "$BARK_AUTOMATICALLYCOPY" ]; then + _saveaccountconf_mutable BARK_AUTOMATICALLYCOPY "$BARK_AUTOMATICALLYCOPY" + fi + + BARK_COPY="${BARK_COPY:-$(_readaccountconf_mutable BARK_COPY)}" + if [ -n "$BARK_COPY" ]; then + _saveaccountconf_mutable BARK_COPY "$BARK_COPY" + fi + + BARK_ICON="${BARK_ICON:-$(_readaccountconf_mutable BARK_ICON)}" + if [ -n "$BARK_ICON" ]; then + _saveaccountconf_mutable BARK_ICON "$BARK_ICON" + fi + + BARK_ISARCHIVE="${BARK_ISARCHIVE:-$(_readaccountconf_mutable BARK_ISARCHIVE)}" + if [ -n "$BARK_ISARCHIVE" ]; then + _saveaccountconf_mutable BARK_ISARCHIVE "$BARK_ISARCHIVE" + fi + + BARK_URL="${BARK_URL:-$(_readaccountconf_mutable BARK_URL)}" + if [ -n "$BARK_URL" ]; then + _saveaccountconf_mutable BARK_URL "$BARK_URL" + fi + + _params="" + + if [ -n "$BARK_SOUND" ]; then + _params="$_params&sound=$BARK_SOUND" + fi + if [ -n "$BARK_GROUP" ]; then + _params="$_params&group=$BARK_GROUP" + fi + if [ -n "$BARK_LEVEL" ]; then + _params="$_params&level=$BARK_LEVEL" + fi + if [ -n "$BARK_BADGE" ]; then + _params="$_params&badge=$BARK_BADGE" + fi + if [ -n "$BARK_AUTOMATICALLYCOPY" ]; then + _params="$_params&automaticallyCopy=$BARK_AUTOMATICALLYCOPY" + fi + if [ -n "$BARK_COPY" ]; then + _params="$_params©=$BARK_COPY" + fi + if [ -n "$BARK_ICON" ]; then + _params="$_params&icon=$BARK_ICON" + fi + if [ -n "$BARK_ISARCHIVE" ]; then + _params="$_params&isArchive=$BARK_ISARCHIVE" + fi + if [ -n "$BARK_URL" ]; then + _params="$_params&url=$BARK_URL" + fi + + _params=$(echo "$_params" | sed 's/^&//') # remove leading '&' if exists - response="$(_get "$BARK_API_URL/$_subject/$_content?sound=$BARK_SOUND&group=$BARK_GROUP")" + response="$(_get "$BARK_API_URL/$_subject/$_content?$_params")" if [ "$?" = "0" ] && _contains "$response" "success"; then _info "Bark API fired success." diff --git a/notify/teams.sh b/notify/teams.sh old mode 100644 new mode 100755 index 1bc5ed08..5e8fe6c0 --- a/notify/teams.sh +++ b/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