فهرست منبع

Merge branch 'dev' into patch-1

Ciaran Walsh 2 ماه پیش
والد
کامیت
49866b6cf4
100فایلهای تغییر یافته به همراه4384 افزوده شده و 1186 حذف شده
  1. 2 1
      .github/workflows/DNS.yml
  2. 1 1
      .github/workflows/DragonFlyBSD.yml
  3. 2 2
      .github/workflows/FreeBSD.yml
  4. 2 2
      .github/workflows/Linux.yml
  5. 1 1
      .github/workflows/MacOS.yml
  6. 1 1
      .github/workflows/NetBSD.yml
  7. 2 2
      .github/workflows/Omnios.yml
  8. 2 2
      .github/workflows/OpenBSD.yml
  9. 1 1
      .github/workflows/PebbleStrict.yml
  10. 2 2
      .github/workflows/Solaris.yml
  11. 2 2
      .github/workflows/Ubuntu.yml
  12. 1 1
      .github/workflows/Windows.yml
  13. 15 2
      .github/workflows/dockerhub.yml
  14. 8 5
      .github/workflows/pr_dns.yml
  15. 1 1
      .github/workflows/pr_notify.yml
  16. 60 0
      .github/workflows/wiki-monitor.yml
  17. 8 4
      Dockerfile
  18. 174 136
      acme.sh
  19. 88 0
      deploy/ali_cdn.sh
  20. 88 0
      deploy/ali_dcdn.sh
  21. 13 0
      deploy/docker.sh
  22. 0 1
      deploy/exim4.sh
  23. 132 9
      deploy/haproxy.sh
  24. 98 0
      deploy/kemplm.sh
  25. 19 0
      deploy/panos.sh
  26. 120 0
      deploy/proxmoxbs.sh
  27. 6 5
      deploy/routeros.sh
  28. 200 0
      deploy/ruckus.sh
  29. 77 34
      deploy/strongswan.sh
  30. 319 103
      deploy/synology_dsm.sh
  31. 113 64
      deploy/truenas.sh
  32. 325 0
      deploy/truenas_ws.sh
  33. 118 27
      deploy/unifi.sh
  34. 88 18
      deploy/vault.sh
  35. 1 1
      deploy/vsftpd.sh
  36. 500 0
      deploy/zyxel_gs1900.sh
  37. 15 20
      dnsapi/dns_1984hosting.sh
  38. 14 14
      dnsapi/dns_acmedns.sh
  39. 13 5
      dnsapi/dns_acmeproxy.sh
  40. 117 52
      dnsapi/dns_active24.sh
  41. 11 10
      dnsapi/dns_ad.sh
  42. 85 68
      dnsapi/dns_ali.sh
  43. 185 0
      dnsapi/dns_alviy.sh
  44. 11 9
      dnsapi/dns_anx.sh
  45. 10 13
      dnsapi/dns_artfiles.sh
  46. 12 7
      dnsapi/dns_arvan.sh
  47. 13 7
      dnsapi/dns_aurora.sh
  48. 13 14
      dnsapi/dns_autodns.sh
  49. 36 19
      dnsapi/dns_aws.sh
  50. 11 7
      dnsapi/dns_azion.sh
  51. 121 71
      dnsapi/dns_azure.sh
  52. 281 0
      dnsapi/dns_beget.sh
  53. 10 11
      dnsapi/dns_bookmyname.sh
  54. 13 16
      dnsapi/dns_bunny.sh
  55. 14 11
      dnsapi/dns_cf.sh
  56. 11 6
      dnsapi/dns_clouddns.sh
  57. 14 10
      dnsapi/dns_cloudns.sh
  58. 12 5
      dnsapi/dns_cn.sh
  59. 13 2
      dnsapi/dns_conoha.sh
  60. 12 6
      dnsapi/dns_constellix.sh
  61. 13 13
      dnsapi/dns_cpanel.sh
  62. 12 6
      dnsapi/dns_curanet.sh
  63. 24 23
      dnsapi/dns_cyon.sh
  64. 12 29
      dnsapi/dns_da.sh
  65. 9 12
      dnsapi/dns_ddnss.sh
  66. 11 9
      dnsapi/dns_desec.sh
  67. 11 14
      dnsapi/dns_df.sh
  68. 11 15
      dnsapi/dns_dgon.sh
  69. 12 9
      dnsapi/dns_dnsexit.sh
  70. 10 11
      dnsapi/dns_dnshome.sh
  71. 10 10
      dnsapi/dns_dnsimple.sh
  72. 10 7
      dnsapi/dns_dnsservices.sh
  73. 12 10
      dnsapi/dns_doapi.sh
  74. 11 2
      dnsapi/dns_domeneshop.sh
  75. 10 8
      dnsapi/dns_dp.sh
  76. 10 8
      dnsapi/dns_dpi.sh
  77. 9 5
      dnsapi/dns_dreamhost.sh
  78. 8 10
      dnsapi/dns_duckdns.sh
  79. 11 5
      dnsapi/dns_durabledns.sh
  80. 12 13
      dnsapi/dns_dyn.sh
  81. 13 12
      dnsapi/dns_dynu.sh
  82. 17 11
      dnsapi/dns_dynv6.sh
  83. 13 10
      dnsapi/dns_easydns.sh
  84. 163 0
      dnsapi/dns_edgecenter.sh
  85. 14 11
      dnsapi/dns_edgedns.sh
  86. 12 16
      dnsapi/dns_euserv.sh
  87. 10 2
      dnsapi/dns_exoscale.sh
  88. 23 16
      dnsapi/dns_fornex.sh
  89. 10 9
      dnsapi/dns_freedns.sh
  90. 105 0
      dnsapi/dns_freemyip.sh
  91. 12 9
      dnsapi/dns_gandi_livedns.sh
  92. 8 2
      dnsapi/dns_gcloud.sh
  93. 11 7
      dnsapi/dns_gcore.sh
  94. 10 10
      dnsapi/dns_gd.sh
  95. 9 9
      dnsapi/dns_geoscaling.sh
  96. 11 6
      dnsapi/dns_googledomains.sh
  97. 11 12
      dnsapi/dns_he.sh
  98. 45 0
      dnsapi/dns_he_ddns.sh
  99. 11 7
      dnsapi/dns_hetzner.sh
  100. 11 7
      dnsapi/dns_hexonet.sh

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

@@ -1,5 +1,6 @@
 name: DNS
 on:
+  workflow_dispatch:
   push:
     paths:
       - 'dnsapi/*.sh'
@@ -280,7 +281,7 @@ jobs:
     - uses: vmactions/openbsd-vm@v1
       with:
         envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
-        prepare: pkg_add socat curl
+        prepare: pkg_add socat curl libiconv
         usesh: true
         copyback: false
         run: |

+ 1 - 1
.github/workflows/DragonFlyBSD.yml

@@ -29,7 +29,7 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
          #  CA: "ZeroSSL RSA Domain Secure Site CA"

+ 2 - 2
.github/workflows/FreeBSD.yml

@@ -29,12 +29,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          - TEST_ACME_Server: "LetsEncrypt.org_test"
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
            ACME_USE_WGET: 1
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"

+ 2 - 2
.github/workflows/Linux.yml

@@ -26,11 +26,11 @@ jobs:
   Linux:
     strategy:
       matrix:
-        os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "centos:7", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3"]
+        os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3"]
     runs-on: ubuntu-latest
     env:
       TEST_LOCAL: 1
-      TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+      TEST_PREFERRED_CHAIN: (STAGING)
       TEST_ACME_Server: "LetsEncrypt.org_test"
     steps:
     - uses: actions/checkout@v4

+ 1 - 1
.github/workflows/MacOS.yml

@@ -29,7 +29,7 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
          #  CA: "ZeroSSL RSA Domain Secure Site CA"

+ 1 - 1
.github/workflows/NetBSD.yml

@@ -29,7 +29,7 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
          #  CA: "ZeroSSL RSA Domain Secure Site CA"

+ 2 - 2
.github/workflows/Omnios.yml

@@ -29,12 +29,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          - TEST_ACME_Server: "LetsEncrypt.org_test"
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
            ACME_USE_WGET: 1
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"

+ 2 - 2
.github/workflows/OpenBSD.yml

@@ -29,12 +29,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          - TEST_ACME_Server: "LetsEncrypt.org_test"
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
            ACME_USE_WGET: 1
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"

+ 1 - 1
.github/workflows/PebbleStrict.yml

@@ -37,7 +37,7 @@ jobs:
     - name: Install tools
       run: sudo apt-get install -y socat
     - name: Run Pebble
-      run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker-compose up -d
+      run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
     - name: Set up Pebble
       run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
     - name: Clone acmetest

+ 2 - 2
.github/workflows/Solaris.yml

@@ -29,12 +29,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          - TEST_ACME_Server: "LetsEncrypt.org_test"
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
            ACME_USE_WGET: 1
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"

+ 2 - 2
.github/workflows/Ubuntu.yml

@@ -29,12 +29,12 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          - TEST_ACME_Server: "LetsEncrypt.org_test"
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
            ACME_USE_WGET: 1
          - TEST_ACME_Server: "ZeroSSL.com"
            CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"

+ 1 - 1
.github/workflows/Windows.yml

@@ -29,7 +29,7 @@ jobs:
            CA_ECDSA: ""
            CA: ""
            CA_EMAIL: ""
-           TEST_PREFERRED_CHAIN: (STAGING) Pretend Pear X1
+           TEST_PREFERRED_CHAIN: (STAGING)
          #- TEST_ACME_Server: "ZeroSSL.com"
          #  CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
          #  CA: "ZeroSSL RSA Domain Secure Site CA"

+ 15 - 2
.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:
@@ -42,8 +44,15 @@ jobs:
     steps:
       - name: checkout code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
       - name: Set up QEMU
         uses: docker/setup-qemu-action@v2
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/[email protected]
+        with:
+          images: ${DOCKER_IMAGE}
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v2
       - name: login to docker hub
@@ -51,8 +60,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 +73,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 .

+ 8 - 5
.github/workflows/pr_dns.yml

@@ -20,11 +20,14 @@ jobs:
               owner: context.repo.owner,
               repo: context.repo.repo,
               body: `**Welcome**
-                First thing: don't send PR to the master branch, please send to the dev branch instead.
-                Please make sure you've read our [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test).
-                Then reply on this message, otherwise, your code will not be reviewed or merged.
-                We look forward to reviewing your Pull request shortly ✨
-                注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
+                    READ ME !!!!!
+                    Read me !!!!!!
+                    First thing: don't send PR to the master branch, please send to the dev branch instead.
+                    Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide).
+                    You MUST pass the [DNS-API-Test](../wiki/DNS-API-Test).
+                    Then reply on this message, otherwise, your code will not be reviewed or merged.
+                    Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
+                    注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
                 `
             })
 

+ 1 - 1
.github/workflows/pr_notify.yml

@@ -1,4 +1,4 @@
-name: Check dns api
+name: Check notify api
 
 on:
   pull_request_target:

+ 60 - 0
.github/workflows/wiki-monitor.yml

@@ -0,0 +1,60 @@
+name: Notify via Issue on Wiki Edit
+
+on:
+  gollum:
+
+jobs:
+  notify:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout wiki repository
+        uses: actions/checkout@v4
+        with:
+          repository: ${{ github.repository }}.wiki
+          path: wiki
+
+      - name: Generate wiki change message
+        run: |
+            actor="${{ github.actor }}"
+            sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
+            page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH")
+            page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH")
+            page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH")
+            page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH")
+            now="$(date '+%Y-%m-%d %H:%M:%S')"
+
+            cd wiki
+            prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
+            if [ -n "$prev_sha" ]; then
+                git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
+            else
+                echo "(no diff)" > ../wiki.diff
+            fi
+            cd ..
+            {
+            echo "Wiki edited"
+            echo -n "User: "
+            echo "[$actor]($sender_url)"
+            echo "Time: $now"
+            echo "Page: [$page_name]($page_url) (Action: $page_action)"
+            echo ""
+            echo "----"
+            echo "###  diff:"
+            echo '```diff'
+            cat wiki.diff
+            echo '```'
+            } > wiki-change-msg.txt
+
+      - name: Create issue to notify Neilpang
+        uses: peter-evans/create-issue-from-file@v5
+        with:
+          title: "Wiki edited"
+          content-filepath: ./wiki-change-msg.txt
+          assignees: Neilpang
+        env:
+          TZ: Asia/Shanghai
+
+
+
+
+

+ 8 - 4
Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.17
+FROM alpine:3.21
 
 RUN apk --no-cache add -f \
   openssl \
@@ -15,14 +15,18 @@ RUN apk --no-cache add -f \
   jq \
   cronie
 
-ENV LE_CONFIG_HOME /acme.sh
+ENV LE_CONFIG_HOME=/acme.sh
 
 ARG AUTO_UPGRADE=1
 
-ENV AUTO_UPGRADE $AUTO_UPGRADE
+ENV AUTO_UPGRADE=$AUTO_UPGRADE
 
 #Install
-COPY ./ /install_acme.sh/
+COPY ./acme.sh /install_acme.sh/acme.sh
+COPY ./deploy /install_acme.sh/deploy
+COPY ./dnsapi /install_acme.sh/dnsapi
+COPY ./notify /install_acme.sh/notify
+
 RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/
 
 

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 174 - 136
acme.sh


+ 88 - 0
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'
+}

+ 88 - 0
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'
+}

+ 13 - 0
deploy/docker.sh

@@ -18,6 +18,7 @@ docker_deploy() {
   _ccert="$3"
   _cca="$4"
   _cfullchain="$5"
+  _cpfx="$6"
   _debug _cdomain "$_cdomain"
   _getdeployconf DEPLOY_DOCKER_CONTAINER_LABEL
   _debug2 DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL"
@@ -88,6 +89,12 @@ docker_deploy() {
     _savedeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"
   fi
 
+  _getdeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE
+  _debug2 DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
+  if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
+    _savedeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
+  fi
+
   _getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD
   _debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
   if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
@@ -125,6 +132,12 @@ docker_deploy() {
     fi
   fi
 
+  if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
+    if ! _docker_cp "$_cid" "$_cpfx" "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"; then
+      return 1
+    fi
+  fi
+
   if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
     _info "Reloading: $DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
     if ! _docker_exec "$_cid" "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"; then

+ 0 - 1
deploy/exim4.sh

@@ -109,6 +109,5 @@ exim4_deploy() {
     fi
     return 1
   fi
-  return 0
 
 }

+ 132 - 9
deploy/haproxy.sh

@@ -36,6 +36,19 @@
 # Note: This functionality requires HAProxy was compiled against
 # a version of OpenSSL that supports this.
 #
+# export DEPLOY_HAPROXY_HOT_UPDATE="yes"
+# export DEPLOY_HAPROXY_STATS_SOCKET="UNIX:/run/haproxy/admin.sock"
+#
+# OPTIONAL: Deploy the certificate over the HAProxy stats socket without
+# needing to reload HAProxy. Default is "no".
+#
+# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
+# address format.
+#
+# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
+#
+# OPTIONAL: To use the master CLI with DEPLOY_HAPROXY_HOT_UPDATE="yes" instead
+# of a stats socket, use this variable.
 
 ########  Public functions #####################
 
@@ -46,6 +59,7 @@ haproxy_deploy() {
   _ccert="$3"
   _cca="$4"
   _cfullchain="$5"
+  _cmdpfx=""
 
   # Some defaults
   DEPLOY_HAPROXY_PEM_PATH_DEFAULT="/etc/haproxy"
@@ -53,6 +67,8 @@ haproxy_deploy() {
   DEPLOY_HAPROXY_BUNDLE_DEFAULT="no"
   DEPLOY_HAPROXY_ISSUER_DEFAULT="no"
   DEPLOY_HAPROXY_RELOAD_DEFAULT="true"
+  DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT="no"
+  DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT="UNIX:/run/haproxy/admin.sock"
 
   _debug _cdomain "${_cdomain}"
   _debug _ckey "${_ckey}"
@@ -86,6 +102,11 @@ haproxy_deploy() {
     _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}"
   elif [ -z "${Le_Deploy_haproxy_pem_name}" ]; then
     Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
+    # We better not have '*' as the first character
+    if [ "${Le_Deploy_haproxy_pem_name%%"${Le_Deploy_haproxy_pem_name#?}"}" = '*' ]; then
+      # removes the first characters and add a _ instead
+      Le_Deploy_haproxy_pem_name="_${Le_Deploy_haproxy_pem_name#?}"
+    fi
   fi
 
   # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
@@ -118,6 +139,36 @@ haproxy_deploy() {
     Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
   fi
 
+  # HOT_UPDATE is optional. If not provided then assume "${DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_HOT_UPDATE
+  _debug2 DEPLOY_HAPROXY_HOT_UPDATE "${DEPLOY_HAPROXY_HOT_UPDATE}"
+  if [ -n "${DEPLOY_HAPROXY_HOT_UPDATE}" ]; then
+    Le_Deploy_haproxy_hot_update="${DEPLOY_HAPROXY_HOT_UPDATE}"
+    _savedomainconf Le_Deploy_haproxy_hot_update "${Le_Deploy_haproxy_hot_update}"
+  elif [ -z "${Le_Deploy_haproxy_hot_update}" ]; then
+    Le_Deploy_haproxy_hot_update="${DEPLOY_HAPROXY_HOT_UPDATE_DEFAULT}"
+  fi
+
+  # STATS_SOCKET is optional. If not provided then assume "${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}"
+  _getdeployconf DEPLOY_HAPROXY_STATS_SOCKET
+  _debug2 DEPLOY_HAPROXY_STATS_SOCKET "${DEPLOY_HAPROXY_STATS_SOCKET}"
+  if [ -n "${DEPLOY_HAPROXY_STATS_SOCKET}" ]; then
+    Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET}"
+    _savedomainconf Le_Deploy_haproxy_stats_socket "${Le_Deploy_haproxy_stats_socket}"
+  elif [ -z "${Le_Deploy_haproxy_stats_socket}" ]; then
+    Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_STATS_SOCKET_DEFAULT}"
+  fi
+
+  # MASTER_CLI is optional. No defaults are used. When the master CLI is used,
+  # all commands are sent with a prefix.
+  _getdeployconf DEPLOY_HAPROXY_MASTER_CLI
+  _debug2 DEPLOY_HAPROXY_MASTER_CLI "${DEPLOY_HAPROXY_MASTER_CLI}"
+  if [ -n "${DEPLOY_HAPROXY_MASTER_CLI}" ]; then
+    Le_Deploy_haproxy_stats_socket="${DEPLOY_HAPROXY_MASTER_CLI}"
+    _savedomainconf Le_Deploy_haproxy_stats_socket "${Le_Deploy_haproxy_stats_socket}"
+    _cmdpfx="@1 " # command prefix used for master CLI only.
+  fi
+
   # Set the suffix depending if we are creating a bundle or not
   if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
     _info "Bundle creation requested"
@@ -142,12 +193,13 @@ haproxy_deploy() {
   _issuer="${_pem}.issuer"
   _ocsp="${_pem}.ocsp"
   _reload="${Le_Deploy_haproxy_reload}"
+  _statssock="${Le_Deploy_haproxy_stats_socket}"
 
   _info "Deploying PEM file"
   # Create a temporary PEM file
   _temppem="$(_mktemp)"
   _debug _temppem "${_temppem}"
-  cat "${_ccert}" "${_cca}" "${_ckey}" >"${_temppem}"
+  cat "${_ccert}" "${_cca}" "${_ckey}" | grep . >"${_temppem}"
   _ret="$?"
 
   # Check that we could create the temporary file
@@ -265,15 +317,86 @@ haproxy_deploy() {
     fi
   fi
 
-  # Reload HAProxy
-  _debug _reload "${_reload}"
-  eval "${_reload}"
-  _ret=$?
-  if [ "${_ret}" != "0" ]; then
-    _err "Error code ${_ret} during reload"
-    return ${_ret}
+  if [ "${Le_Deploy_haproxy_hot_update}" = "yes" ]; then
+    # set the socket name for messages
+    if [ -n "${_cmdpfx}" ]; then
+      _socketname="master CLI"
+    else
+      _socketname="stats socket"
+    fi
+
+    # Update certificate over HAProxy stats socket or master CLI.
+    if _exists socat; then
+      # look for the certificate on the stats socket, to chose between updating or creating one
+      _socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
+      _debug _socat_cert_cmd "${_socat_cert_cmd}"
+      eval "${_socat_cert_cmd}"
+      _ret=$?
+      if [ "${_ret}" != "0" ]; then
+        _newcert="1"
+        _info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
+        # certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
+        _socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
+        _debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
+        eval "${_socat_crtlist_show_cmd}"
+        _ret=$?
+        if [ "${_ret}" != "0" ]; then
+          _err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
+          return "${_ret}"
+        fi
+        # create a new certificate
+        _socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
+        _debug _socat_new_cmd "${_socat_new_cmd}"
+        eval "${_socat_new_cmd}"
+        _ret=$?
+        if [ "${_ret}" != "0" ]; then
+          _err "Couldn't create '${_pem}' in haproxy"
+          return "${_ret}"
+        fi
+      else
+        _info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
+      fi
+      _socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
+      _secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
+      eval "${_socat_cert_set_cmd}"
+      _ret=$?
+      if [ "${_ret}" != "0" ]; then
+        _err "Can't update '${_pem}' in haproxy"
+        return "${_ret}"
+      fi
+      _socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
+      _debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
+      eval "${_socat_cert_commit_cmd}"
+      _ret=$?
+      if [ "${_ret}" != "0" ]; then
+        _err "Can't commit '${_pem}' in haproxy"
+        return ${_ret}
+      fi
+      if [ "${_newcert}" = "1" ]; then
+        # if this is a new certificate, it needs to be inserted into the crt-list`
+        _socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
+        _debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
+        eval "${_socat_cert_add_cmd}"
+        _ret=$?
+        if [ "${_ret}" != "0" ]; then
+          _err "Can't update '${_pem}' in haproxy"
+          return "${_ret}"
+        fi
+      fi
+    else
+      _err "'socat' is not available, couldn't update over ${_socketname}"
+    fi
   else
-    _info "Reload successful"
+    # Reload HAProxy
+    _debug _reload "${_reload}"
+    eval "${_reload}"
+    _ret=$?
+    if [ "${_ret}" != "0" ]; then
+      _err "Error code ${_ret} during reload"
+      return ${_ret}
+    else
+      _info "Reload successful"
+    fi
   fi
 
   return 0

+ 98 - 0
deploy/kemplm.sh

@@ -0,0 +1,98 @@
+#!/usr/bin/env sh
+
+#Here is a script to deploy cert to a Kemp Loadmaster.
+
+#returns 0 means success, otherwise error.
+
+#DEPLOY_KEMP_TOKEN="token"
+#DEPLOY_KEMP_URL="https://kemplm.example.com"
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+kemplm_deploy() {
+  _domain="$1"
+  _key_file="$2"
+  _cert_file="$3"
+  _ca_file="$4"
+  _fullchain_file="$5"
+
+  _debug _domain "$_domain"
+  _debug _key_file "$_key_file"
+  _debug _cert_file "$_cert_file"
+  _debug _ca_file "$_ca_file"
+  _debug _fullchain_file "$_fullchain_file"
+
+  if ! _exists jq; then
+    _err "jq not found"
+    return 1
+  fi
+
+  # Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename
+  _kemp_domain=$(echo "${_domain}" | sed 's/\*\.//')
+  _debug _kemp_domain "$_kemp_domain"
+
+  # Read config from saved values or env
+  _getdeployconf DEPLOY_KEMP_TOKEN
+  _getdeployconf DEPLOY_KEMP_URL
+
+  _debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
+  _secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
+
+  if [ -z "$DEPLOY_KEMP_TOKEN" ]; then
+    _err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN."
+    return 1
+  fi
+  if [ -z "$DEPLOY_KEMP_URL" ]; then
+    _err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL."
+    return 1
+  fi
+
+  # Save current values
+  _savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
+  _savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
+
+  # Check if certificate is already installed
+  _info "Check if certificate is already present"
+  _list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}"
+  _debug3 _list_request "${_list_request}"
+  _kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}")
+  _debug2 _kemp_cert_count "${_kemp_cert_count}"
+
+  _kemp_replace_cert=1
+  if [ "${_kemp_cert_count}" -eq 0 ]; then
+    _kemp_replace_cert=0
+    _info "Certificate does not exist on Kemp Loadmaster"
+  else
+    _info "Certificate already exists on Kemp Loadmaster"
+  fi
+  _debug _kemp_replace_cert "${_kemp_replace_cert}"
+
+  # Upload new certificate to Kemp Loadmaster
+  _kemp_upload_cert=$(_mktemp)
+  cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}"
+
+  _info "Uploading certificate to Kemp Loadmaster"
+  _add_data=$(cat "${_kemp_upload_cert}")
+  _add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}"
+  _debug3 _add_request "${_add_request}"
+  _kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2")
+  _retval=$?
+  _debug2 _kemp_post_result "${_kemp_post_result}"
+  if [ "${_retval}" -eq 0 ]; then
+    _kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status')
+    _kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message')
+    if [ "${_kemp_post_status}" = "ok" ]; then
+      _info "Upload successful"
+    else
+      _err "Upload failed: ${_kemp_post_message}"
+    fi
+  else
+    _err "Upload failed"
+    _retval=1
+  fi
+
+  rm "${_kemp_upload_cert}"
+
+  return $_retval
+}

+ 19 - 0
deploy/panos.sh

@@ -12,6 +12,9 @@
 #     export PANOS_USER=""    #User *MUST* have Commit and Import Permissions in XML API for Admin Role
 #     export PANOS_PASS=""
 #
+# OPTIONAL
+#    export PANOS_TEMPLATE="" #Template Name of panorama managed devices
+#
 # The script will automatically generate a new API key if
 # no key is found, or if a saved key has expired or is invalid.
 
@@ -78,6 +81,9 @@ deployer() {
       content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
       content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
       content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
+      if [ "$_panos_template" ]; then
+        content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
+      fi
     fi
     if [ "$type" = 'key' ]; then
       panos_url="${panos_url}?type=import"
@@ -87,6 +93,9 @@ deployer() {
       content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
       content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456"
       content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
+      if [ "$_panos_template" ]; then
+        content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
+      fi
     fi
     #Close multipart
     content="$content${nl}--$delim--${nl}${nl}"
@@ -173,10 +182,20 @@ panos_deploy() {
     unset _panos_key
   fi
 
+  # PANOS_TEMPLATE
+  if [ "$PANOS_TEMPLATE" ]; then
+    _debug "Detected ENV variable PANOS_TEMPLATE. Saving to file."
+    _savedeployconf PANOS_TEMPLATE "$PANOS_TEMPLATE" 1
+  else
+    _debug "Attempting to load variable PANOS_TEMPLATE from file."
+    _getdeployconf PANOS_TEMPLATE
+  fi
+
   #Store variables
   _panos_host=$PANOS_HOST
   _panos_user=$PANOS_USER
   _panos_pass=$PANOS_PASS
+  _panos_template=$PANOS_TEMPLATE
 
   #Test API Key if found.  If the key is invalid, the variable _panos_key will be unset.
   if [ "$_panos_host" ] && [ "$_panos_key" ]; then

+ 120 - 0
deploy/proxmoxbs.sh

@@ -0,0 +1,120 @@
+#!/usr/bin/env sh
+
+# Deploy certificates to a proxmox backup server using the API.
+#
+# Environment variables that can be set are:
+# `DEPLOY_PROXMOXBS_SERVER`: The hostname of the proxmox backup server. Defaults to
+#                            _cdomain.
+# `DEPLOY_PROXMOXBS_SERVER_PORT`: The port number the management interface is on.
+#                                 Defaults to 8007.
+# `DEPLOY_PROXMOXBS_USER`: The user we'll connect as. Defaults to root.
+# `DEPLOY_PROXMOXBS_USER_REALM`: The authentication realm the user authenticates
+#                                with. Defaults to pam.
+# `DEPLOY_PROXMOXBS_API_TOKEN_NAME`: The name of the API token created for the
+#                                    user account. Defaults to acme.
+# `DEPLOY_PROXMOXBS_API_TOKEN_KEY`: The API token. Required.
+
+proxmoxbs_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug2 _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  # "Sane" defaults.
+  _getdeployconf DEPLOY_PROXMOXBS_SERVER
+  if [ -z "$DEPLOY_PROXMOXBS_SERVER" ]; then
+    _target_hostname="$_cdomain"
+  else
+    _target_hostname="$DEPLOY_PROXMOXBS_SERVER"
+    _savedeployconf DEPLOY_PROXMOXBS_SERVER "$DEPLOY_PROXMOXBS_SERVER"
+  fi
+  _debug2 DEPLOY_PROXMOXBS_SERVER "$_target_hostname"
+
+  _getdeployconf DEPLOY_PROXMOXBS_SERVER_PORT
+  if [ -z "$DEPLOY_PROXMOXBS_SERVER_PORT" ]; then
+    _target_port="8007"
+  else
+    _target_port="$DEPLOY_PROXMOXBS_SERVER_PORT"
+    _savedeployconf DEPLOY_PROXMOXBS_SERVER_PORT "$DEPLOY_PROXMOXBS_SERVER_PORT"
+  fi
+  _debug2 DEPLOY_PROXMOXBS_SERVER_PORT "$_target_port"
+
+  # Complete URL.
+  _target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/localhost/certificates/custom"
+  _debug TARGET_URL "$_target_url"
+
+  # More "sane" defaults.
+  _getdeployconf DEPLOY_PROXMOXBS_USER
+  if [ -z "$DEPLOY_PROXMOXBS_USER" ]; then
+    _proxmoxbs_user="root"
+  else
+    _proxmoxbs_user="$DEPLOY_PROXMOXBS_USER"
+    _savedeployconf DEPLOY_PROXMOXBS_USER "$DEPLOY_PROXMOXBS_USER"
+  fi
+  _debug2 DEPLOY_PROXMOXBS_USER "$_proxmoxbs_user"
+
+  _getdeployconf DEPLOY_PROXMOXBS_USER_REALM
+  if [ -z "$DEPLOY_PROXMOXBS_USER_REALM" ]; then
+    _proxmoxbs_user_realm="pam"
+  else
+    _proxmoxbs_user_realm="$DEPLOY_PROXMOXBS_USER_REALM"
+    _savedeployconf DEPLOY_PROXMOXBS_USER_REALM "$DEPLOY_PROXMOXBS_USER_REALM"
+  fi
+  _debug2 DEPLOY_PROXMOXBS_USER_REALM "$_proxmoxbs_user_realm"
+
+  _getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME
+  if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" ]; then
+    _proxmoxbs_api_token_name="acme"
+  else
+    _proxmoxbs_api_token_name="$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
+    _savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME "$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
+  fi
+  _debug2 DEPLOY_PROXMOXBS_API_TOKEN_NAME "$_proxmoxbs_api_token_name"
+
+  # This is required.
+  _getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY
+  if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" ]; then
+    _err "API key not provided."
+    return 1
+  else
+    _proxmoxbs_api_token_key="$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
+    _savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY "$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
+  fi
+  _debug2 DEPLOY_PROXMOXBS_API_TOKEN_KEY "$_proxmoxbs_api_token_key"
+
+  # PBS API Token header value. Used in "Authorization: PBSAPIToken".
+  _proxmoxbs_header_api_token="${_proxmoxbs_user}@${_proxmoxbs_user_realm}!${_proxmoxbs_api_token_name}:${_proxmoxbs_api_token_key}"
+  _debug2 "Auth Header" "$_proxmoxbs_header_api_token"
+
+  # Ugly. I hate putting heredocs inside functions because heredocs don't
+  # account for whitespace correctly but it _does_ work and is several times
+  # cleaner than anything else I had here.
+  #
+  # This dumps the json payload to a variable that should be passable to the
+  # _psot function.
+  _json_payload=$(
+    cat <<HEREDOC
+{
+  "certificates": "$(tr '\n' ':' <"$_cfullchain" | sed 's/:/\\n/g')",
+  "key": "$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')",
+  "node":"localhost",
+  "restart":true,
+  "force":true
+}
+HEREDOC
+  )
+  _debug2 Payload "$_json_payload"
+
+  _info "Push certificates to server"
+  export HTTPS_INSECURE=1
+  export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
+  _post "$_json_payload" "$_target_url" "" POST "application/json"
+
+}

+ 6 - 5
deploy/routeros.sh

@@ -137,17 +137,18 @@ routeros_deploy() {
     return $_err_code
   fi
 
-  DEPLOY_SCRIPT_CMD="/system script add name=\"LECertDeploy-$_cdomain\" owner=$ROUTER_OS_USERNAME \
+  DEPLOY_SCRIPT_CMD=":do {/system script remove \"LECertDeploy-$_cdomain\" } on-error={ }; \
+/system script add name=\"LECertDeploy-$_cdomain\" owner=$ROUTER_OS_USERNAME \
 comment=\"generated by routeros deploy script in acme.sh\" \
 source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
 \n/certificate remove [ find name=$_cdomain.cer_1 ];\
 \n/certificate remove [ find name=$_cdomain.cer_2 ];\
 \ndelay 1;\
-\n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\
-\n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\
+\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
+\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
 \ndelay 1;\
-\n/file remove $_cdomain.cer;\
-\n/file remove $_cdomain.key;\
+\n:do {/file remove $_cdomain.cer; } on-error={ }\
+\n:do {/file remove $_cdomain.key; } on-error={ }\
 \ndelay 2;\
 \n/ip service set www-ssl certificate=$_cdomain.cer_0;\
 \n$ROUTER_OS_ADDITIONAL_SERVICES;\

+ 200 - 0
deploy/ruckus.sh

@@ -0,0 +1,200 @@
+#!/usr/bin/env sh
+
+# Here is a script to deploy cert to Ruckus ZoneDirector / Unleashed.
+#
+# Public domain, 2024, Tony Rielly <https://github.com/ms264556>
+#
+# ```sh
+# acme.sh --deploy -d ruckus.example.com --deploy-hook ruckus
+# ```
+#
+# Then you need to set the environment variables for the
+# deploy script to work.
+#
+# ```sh
+# export RUCKUS_HOST=myruckus.example.com
+# export RUCKUS_USER=myruckususername
+# export RUCKUS_PASS=myruckuspassword
+#
+# acme.sh --deploy -d myruckus.example.com --deploy-hook ruckus
+# ```
+#
+# returns 0 means success, otherwise error.
+
+########  Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+ruckus_deploy() {
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+  _err_code=0
+
+  _debug _cdomain "$_cdomain"
+  _debug _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  _getdeployconf RUCKUS_HOST
+  _getdeployconf RUCKUS_USER
+  _getdeployconf RUCKUS_PASS
+
+  if [ -z "$RUCKUS_HOST" ]; then
+    _debug "Using _cdomain as RUCKUS_HOST, please set if not correct."
+    RUCKUS_HOST="$_cdomain"
+  fi
+
+  if [ -z "$RUCKUS_USER" ]; then
+    _err "Need to set the env variable RUCKUS_USER"
+    return 1
+  fi
+
+  if [ -z "$RUCKUS_PASS" ]; then
+    _err "Need to set the env variable RUCKUS_PASS"
+    return 1
+  fi
+
+  _savedeployconf RUCKUS_HOST "$RUCKUS_HOST"
+  _savedeployconf RUCKUS_USER "$RUCKUS_USER"
+  _savedeployconf RUCKUS_PASS "$RUCKUS_PASS"
+
+  _debug RUCKUS_HOST "$RUCKUS_HOST"
+  _debug RUCKUS_USER "$RUCKUS_USER"
+  _secure_debug RUCKUS_PASS "$RUCKUS_PASS"
+
+  export ACME_HTTP_NO_REDIRECTS=1
+
+  _info "Discovering the login URL"
+  _get "https://$RUCKUS_HOST" >/dev/null
+  _login_url="$(_response_header 'Location')"
+  if [ -n "$_login_url" ]; then
+    _login_path=$(echo "$_login_url" | sed 's|https\?://[^/]\+||')
+    if [ -z "$_login_path" ]; then
+      # redirect was to a different host
+      _err "Connection failed: redirected to a different host. Configure Unleashed with a Preferred Master or Management Interface."
+      return 1
+    fi
+  fi
+
+  if [ -z "${_login_url}" ]; then
+    _err "Connection failed: couldn't find login page."
+    return 1
+  fi
+
+  _base_url=$(dirname "$_login_url")
+  _login_page=$(basename "$_login_url")
+
+  if [ "$_login_page" = "index.html" ]; then
+    _err "Connection temporarily unavailable: Unleashed Rebuilding."
+    return 1
+  fi
+
+  if [ "$_login_page" = "wizard.jsp" ]; then
+    _err "Connection failed: Setup Wizard not complete."
+    return 1
+  fi
+
+  _info "Login"
+  _username_encoded="$(printf "%s" "$RUCKUS_USER" | _url_encode)"
+  _password_encoded="$(printf "%s" "$RUCKUS_PASS" | _url_encode)"
+  _login_query="$(printf "%s" "username=${_username_encoded}&password=${_password_encoded}&ok=Log+In")"
+  _post "$_login_query" "$_login_url" >/dev/null
+
+  _login_code="$(_response_code)"
+  if [ "$_login_code" = "200" ]; then
+    _err "Login failed: incorrect credentials."
+    return 1
+  fi
+
+  _info "Collect Session Cookie"
+  _H1="Cookie: $(_response_cookie)"
+  export _H1
+  _info "Collect CSRF Token"
+  _H2="X-CSRF-Token: $(_response_header 'HTTP_X_CSRF_TOKEN')"
+  export _H2
+
+  if _isRSA "$_ckey" >/dev/null 2>&1; then
+    _debug "Using RSA certificate."
+  else
+    _info "Verifying ECC certificate support."
+
+    _ul_version="$(_get_unleashed_version)"
+    if [ -z "$_ul_version" ]; then
+      _err "Your controller doesn't support ECC certificates. Please deploy an RSA certificate."
+      return 1
+    fi
+
+    _ul_version_major="$(echo "$_ul_version" | cut -d . -f 1)"
+    _ul_version_minor="$(echo "$_ul_version" | cut -d . -f 2)"
+    if [ "$_ul_version_major" -lt "200" ]; then
+      _err "ZoneDirector doesn't support ECC certificates. Please deploy an RSA certificate."
+      return 1
+    elif [ "$_ul_version_minor" -lt "13" ]; then
+      _err "Unleashed $_ul_version_major.$_ul_version_minor doesn't support ECC certificates. Please deploy an RSA certificate or upgrade to Unleashed 200.13+."
+      return 1
+    fi
+
+    _debug "ECC certificates OK for Unleashed $_ul_version_major.$_ul_version_minor."
+  fi
+
+  _info "Uploading certificate"
+  _post_upload "uploadcert" "$_cfullchain"
+
+  _info "Uploading private key"
+  _post_upload "uploadprivatekey" "$_ckey"
+
+  _info "Replacing certificate"
+  _replace_cert_ajax='<ajax-request action="docmd" comp="system" updater="rid.0.5" xcmd="replace-cert" checkAbility="6" timeout="-1"><xcmd cmd="replace-cert" cn="'$RUCKUS_HOST'"/></ajax-request>'
+  _post "$_replace_cert_ajax" "$_base_url/_cmdstat.jsp" >/dev/null
+
+  _info "Rebooting"
+  _cert_reboot_ajax='<ajax-request action="docmd" comp="worker" updater="rid.0.5" xcmd="cert-reboot" checkAbility="6"><xcmd cmd="cert-reboot" action="undefined"/></ajax-request>'
+  _post "$_cert_reboot_ajax" "$_base_url/_cmdstat.jsp" >/dev/null
+
+  return 0
+}
+
+_response_code() {
+  _egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n" | _egrep_o "^[0-9]*"
+}
+
+_response_header() {
+  grep <"$HTTP_HEADER" -i "^$1:" | cut -d ':' -f 2- | tr -d "\r\n\t "
+}
+
+_response_cookie() {
+  _response_header 'Set-Cookie' | sed 's/;.*//'
+}
+
+_get_unleashed_version() {
+  _post '<ajax-request action="getstat" comp="system"><sysinfo/></ajax-request>' "$_base_url/_cmdstat.jsp" | _egrep_o "version-num=\"[^\"]*\"" | cut -d '"' -f 2
+}
+
+_post_upload() {
+  _post_action="$1"
+  _post_file="$2"
+
+  _post_boundary="----FormBoundary$(date "+%s%N")"
+
+  _post_data="$({
+    printf -- "--%s\r\n" "$_post_boundary"
+    printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action"
+    printf -- "Content-Type: application/octet-stream\r\n\r\n"
+    printf -- "%s\r\n" "$(cat "$_post_file")"
+
+    printf -- "--%s\r\n" "$_post_boundary"
+    printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
+    printf -- "%s\r\n" "$_post_action"
+
+    printf -- "--%s\r\n" "$_post_boundary"
+    printf -- "Content-Disposition: form-data; name=\"callback\"\r\n\r\n"
+    printf -- "%s\r\n" "uploader_$_post_action"
+
+    printf -- "--%s--\r\n\r\n" "$_post_boundary"
+  })"
+
+  _post "$_post_data" "$_base_url/_upload.jsp?request_type=xhr" "" "" "multipart/form-data; boundary=$_post_boundary" >/dev/null
+}

+ 77 - 34
deploy/strongswan.sh

@@ -10,46 +10,89 @@
 
 #domain keyfile certfile cafile fullchain
 strongswan_deploy() {
-  _cdomain="$1"
-  _ckey="$2"
-  _ccert="$3"
-  _cca="$4"
-  _cfullchain="$5"
-
+  _cdomain="${1}"
+  _ckey="${2}"
+  _ccert="${3}"
+  _cca="${4}"
+  _cfullchain="${5}"
   _info "Using strongswan"
-
-  if [ -x /usr/sbin/ipsec ]; then
-    _ipsec=/usr/sbin/ipsec
-  elif [ -x /usr/sbin/strongswan ]; then
-    _ipsec=/usr/sbin/strongswan
-  elif [ -x /usr/local/sbin/ipsec ]; then
-    _ipsec=/usr/local/sbin/ipsec
-  else
+  if _exists ipsec; then
+    _ipsec=ipsec
+  elif _exists strongswan; then
+    _ipsec=strongswan
+  fi
+  if _exists swanctl; then
+    _swanctl=swanctl
+  fi
+  # For legacy stroke mode
+  if [ -n "${_ipsec}" ]; then
+    _info "${_ipsec} command detected"
+    _confdir=$(${_ipsec} --confdir)
+    if [ -z "${_confdir}" ]; then
+      _err "no strongswan --confdir is detected"
+      return 1
+    fi
+    _info _confdir "${_confdir}"
+    __deploy_cert "$@" "stroke" "${_confdir}"
+    ${_ipsec} reload
+  fi
+  # For modern vici mode
+  if [ -n "${_swanctl}" ]; then
+    _info "${_swanctl} command detected"
+    for _dir in /usr/local/etc/swanctl /etc/swanctl /etc/strongswan/swanctl; do
+      if [ -d ${_dir} ]; then
+        _confdir=${_dir}
+        _info _confdir "${_confdir}"
+        break
+      fi
+    done
+    if [ -z "${_confdir}" ]; then
+      _err "no swanctl config dir is found"
+      return 1
+    fi
+    __deploy_cert "$@" "vici" "${_confdir}"
+    ${_swanctl} --load-creds
+  fi
+  if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then
     _err "no strongswan or ipsec command is detected"
+    _err "no swanctl is detected"
     return 1
   fi
+}
 
-  _info _ipsec "$_ipsec"
+####################  Private functions below ##################################
 
-  _confdir=$($_ipsec --confdir)
-  if [ $? -ne 0 ] || [ -z "$_confdir" ]; then
-    _err "no strongswan --confdir is detected"
+__deploy_cert() {
+  _cdomain="${1}"
+  _ckey="${2}"
+  _ccert="${3}"
+  _cca="${4}"
+  _cfullchain="${5}"
+  _swan_mode="${6}"
+  _confdir="${7}"
+  _debug _cdomain "${_cdomain}"
+  _debug _ckey "${_ckey}"
+  _debug _ccert "${_ccert}"
+  _debug _cca "${_cca}"
+  _debug _cfullchain "${_cfullchain}"
+  _debug _swan_mode "${_swan_mode}"
+  _debug _confdir "${_confdir}"
+  if [ "${_swan_mode}" = "vici" ]; then
+    _dir_private="private"
+    _dir_cert="x509"
+    _dir_ca="x509ca"
+  elif [ "${_swan_mode}" = "stroke" ]; then
+    _dir_private="ipsec.d/private"
+    _dir_cert="ipsec.d/certs"
+    _dir_ca="ipsec.d/cacerts"
+  else
+    _err "unknown StrongSwan mode ${_swan_mode}"
     return 1
   fi
-
-  _info _confdir "$_confdir"
-
-  _debug _cdomain "$_cdomain"
-  _debug _ckey "$_ckey"
-  _debug _ccert "$_ccert"
-  _debug _cca "$_cca"
-  _debug _cfullchain "$_cfullchain"
-
-  cat "$_ckey" >"${_confdir}/ipsec.d/private/$(basename "$_ckey")"
-  cat "$_ccert" >"${_confdir}/ipsec.d/certs/$(basename "$_ccert")"
-  cat "$_cca" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cca")"
-  cat "$_cfullchain" >"${_confdir}/ipsec.d/cacerts/$(basename "$_cfullchain")"
-
-  $_ipsec reload
-
+  cat "${_ckey}" >"${_confdir}/${_dir_private}/$(basename "${_ckey}")"
+  cat "${_ccert}" >"${_confdir}/${_dir_cert}/$(basename "${_ccert}")"
+  cat "${_cca}" >"${_confdir}/${_dir_ca}/$(basename "${_cca}")"
+  if [ "${_swan_mode}" = "stroke" ]; then
+    cat "${_cfullchain}" >"${_confdir}/${_dir_ca}/$(basename "${_cfullchain}")"
+  fi
 }

+ 319 - 103
deploy/synology_dsm.sh

@@ -8,20 +8,38 @@
 # Updated: 2023-07-03
 # Issues:  https://github.com/acmesh-official/acme.sh/issues/2727
 ################################################################################
-# Usage:
-# 1. export SYNO_Username="adminUser"
-# 2. export SYNO_Password="adminPassword"
-# Optional exports (shown values are the defaults):
-# - export SYNO_Certificate="" to replace a specific certificate via description
-# - export SYNO_Scheme="http"
-# - export SYNO_Hostname="localhost"
-# - export SYNO_Port="5000"
-# - export SYNO_Device_Name="CertRenewal" - required for skipping 2FA-OTP
-# - export SYNO_Device_ID=""              - required for skipping 2FA-OTP
-# 3. acme.sh --deploy --deploy-hook synology_dsm -d example.com
+# Usage (shown values are the examples):
+# 1. Set required environment variables:
+# - use automatically created temp admin user to authenticate
+#   export SYNO_USE_TEMP_ADMIN=1
+# - or provide your own admin user credential to authenticate
+#   1. export SYNO_USERNAME="adminUser"
+#   2. export SYNO_PASSWORD="adminPassword"
+# 2. Set optional environment variables
+# - common optional variables
+#   - export SYNO_SCHEME="http"         - defaults to "http"
+#   - export SYNO_HOSTNAME="localhost"  - defaults to "localhost"
+#   - export SYNO_PORT="5000"           - defaults to "5000"
+#   - export SYNO_CREATE=1 - to allow creating the cert if it doesn't exist
+#   - export SYNO_CERTIFICATE="" - to replace a specific cert by its
+#                                    description
+# - temp admin optional variables
+#   - export SYNO_LOCAL_HOSTNAME=1   - if set to 1, force to treat hostname is
+#                                      targeting current local machine (since
+#                                      this method only locally supported)
+# - exsiting admin 2FA-OTP optional variables
+#   - export SYNO_OTP_CODE="XXXXXX" - if set, script won't require to
+#                                     interactive input the OTP code
+#   - export SYNO_DEVICE_NAME="CertRenewal" - if set, script won't require to
+#                                             interactive input the device name
+#   - export SYNO_DEVICE_ID=""      - (deprecated, auth with OTP code instead)
+#                                     required for omitting 2FA-OTP
+# 3. Run command:
+# acme.sh --deploy --deploy-hook synology_dsm -d example.com
 ################################################################################
 # Dependencies:
-# - jq & curl
+# - curl
+# - synouser & synogroup & synosetkeyvalue (Required for SYNO_USE_TEMP_ADMIN=1)
 ################################################################################
 # Return value:
 # 0 means success, otherwise error.
@@ -37,59 +55,89 @@ synology_dsm_deploy() {
 
   _debug _cdomain "$_cdomain"
 
-  # Get username & password, but don't save until we authenticated successfully
-  _getdeployconf SYNO_Username
-  _getdeployconf SYNO_Password
-  _getdeployconf SYNO_Create
-  _getdeployconf SYNO_DID
-  _getdeployconf SYNO_TOTP_SECRET
-  _getdeployconf SYNO_Device_Name
-  _getdeployconf SYNO_Device_ID
-  if [ -z "${SYNO_Username:-}" ] || [ -z "${SYNO_Password:-}" ]; then
-    _err "SYNO_Username & SYNO_Password must be set"
-    return 1
+  # Get username and password, but don't save until we authenticated successfully
+  _migratedeployconf SYNO_Username SYNO_USERNAME
+  _migratedeployconf SYNO_Password SYNO_PASSWORD
+  _migratedeployconf SYNO_Device_ID SYNO_DEVICE_ID
+  _migratedeployconf SYNO_Device_Name SYNO_DEVICE_NAME
+  _getdeployconf SYNO_USERNAME
+  _getdeployconf SYNO_PASSWORD
+  _getdeployconf SYNO_DEVICE_ID
+  _getdeployconf SYNO_DEVICE_NAME
+
+  # Prepare to use temp admin if SYNO_USE_TEMP_ADMIN is set
+  _getdeployconf SYNO_USE_TEMP_ADMIN
+  _check2cleardeployconfexp SYNO_USE_TEMP_ADMIN
+  _debug2 SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
+
+  if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
+    if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then
+      _err "Missing required tools to creat temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
+      _err "Notice: temp admin user authorization method only supports local deployment on DSM."
+      return 1
+    fi
+    if synouser --help 2>&1 | grep -q 'Permission denied'; then
+      _err "For creating temp admin user, the deploy script must be run as root."
+      return 1
+    fi
+
+    [ -n "$SYNO_USERNAME" ] || _savedeployconf SYNO_USERNAME ""
+    [ -n "$SYNO_PASSWORD" ] || _savedeployconf SYNO_PASSWORD ""
+
+    _debug "Setting temp admin user credential..."
+    SYNO_USERNAME=sc-acmesh-tmp
+    SYNO_PASSWORD=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 16)
+    # Set 2FA-OTP settings to empty consider they won't be needed.
+    SYNO_DEVICE_ID=
+    SYNO_DEVICE_NAME=
+    SYNO_OTP_CODE=
+  else
+    _debug2 SYNO_USERNAME "$SYNO_USERNAME"
+    _secure_debug2 SYNO_PASSWORD "$SYNO_PASSWORD"
+    _debug2 SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
+    _secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
   fi
-  if [ -n "${SYNO_Device_Name:-}" ] && [ -z "${SYNO_Device_ID:-}" ]; then
-    _err "SYNO_Device_Name set, but SYNO_Device_ID is empty"
+
+  if [ -z "$SYNO_USERNAME" ] || [ -z "$SYNO_PASSWORD" ]; then
+    _err "You must set either SYNO_USE_TEMP_ADMIN, or set both SYNO_USERNAME and SYNO_PASSWORD."
     return 1
   fi
-  _debug2 SYNO_Username "$SYNO_Username"
-  _secure_debug2 SYNO_Password "$SYNO_Password"
-  _debug2 SYNO_Create "$SYNO_Create"
-  _debug2 SYNO_Device_Name "$SYNO_Device_Name"
-  _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID"
-
-  # Optional scheme, hostname & port for Synology DSM
-  _getdeployconf SYNO_Scheme
-  _getdeployconf SYNO_Hostname
-  _getdeployconf SYNO_Port
-
-  # Default values for scheme, hostname & port
-  # Defaulting to localhost & http, because it's localhost…
-  [ -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"
-  _debug2 SYNO_Scheme "$SYNO_Scheme"
-  _debug2 SYNO_Hostname "$SYNO_Hostname"
-  _debug2 SYNO_Port "$SYNO_Port"
+
+  # Optional scheme, hostname and port for Synology DSM
+  _migratedeployconf SYNO_Scheme SYNO_SCHEME
+  _migratedeployconf SYNO_Hostname SYNO_HOSTNAME
+  _migratedeployconf SYNO_Port SYNO_PORT
+  _getdeployconf SYNO_SCHEME
+  _getdeployconf SYNO_HOSTNAME
+  _getdeployconf SYNO_PORT
+
+  # 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
+  _savedeployconf SYNO_SCHEME "$SYNO_SCHEME"
+  _savedeployconf SYNO_HOSTNAME "$SYNO_HOSTNAME"
+  _savedeployconf SYNO_PORT "$SYNO_PORT"
+  _debug2 SYNO_SCHEME "$SYNO_SCHEME"
+  _debug2 SYNO_HOSTNAME "$SYNO_HOSTNAME"
+  _debug2 SYNO_PORT "$SYNO_PORT"
 
   # Get the certificate description, but don't save it until we verify it's real
-  _getdeployconf SYNO_Certificate
-  _debug SYNO_Certificate "${SYNO_Certificate:-}"
+  _migratedeployconf SYNO_Certificate SYNO_CERTIFICATE "base64"
+  _getdeployconf SYNO_CERTIFICATE
+  _check2cleardeployconfexp SYNO_CERTIFICATE
+  _debug SYNO_CERTIFICATE "${SYNO_CERTIFICATE:-}"
 
   # shellcheck disable=SC1003 # We are not trying to escape a single quote
-  if printf "%s" "$SYNO_Certificate" | grep '\\'; then
+  if printf "%s" "$SYNO_CERTIFICATE" | grep '\\'; then
     _err "Do not use a backslash (\) in your certificate description"
     return 1
   fi
 
-  _base_url="$SYNO_Scheme://$SYNO_Hostname:$SYNO_Port"
+  _debug "Getting API version..."
+  _base_url="$SYNO_SCHEME://$SYNO_HOSTNAME:$SYNO_PORT"
   _debug _base_url "$_base_url"
-
-  _debug "Getting API version"
   response=$(_get "$_base_url/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query&query=SYNO.API.Auth")
   api_path=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"path" *: *"\([^"]*\)".*/\1/p')
   api_version=$(echo "$response" | grep "SYNO.API.Auth" | sed -n 's/.*"maxVersion" *: *\([0-9]*\).*/\1/p')
@@ -97,63 +145,167 @@ synology_dsm_deploy() {
   _debug3 api_path "$api_path"
   _debug3 api_version "$api_version"
 
-  # Login, get the session ID & SynoToken from JSON
-  _info "Logging into $SYNO_Hostname:$SYNO_Port"
-  encoded_username="$(printf "%s" "$SYNO_Username" | _url_encode)"
-  encoded_password="$(printf "%s" "$SYNO_Password" | _url_encode)"
+  # Login, get the session ID and SynoToken from JSON
+  _info "Logging into $SYNO_HOSTNAME:$SYNO_PORT..."
+  encoded_username="$(printf "%s" "$SYNO_USERNAME" | _url_encode)"
+  encoded_password="$(printf "%s" "$SYNO_PASSWORD" | _url_encode)"
+
+  # ## START ## - DEPRECATED, for backward compatibility
+  _getdeployconf SYNO_TOTP_SECRET
 
-  otp_code=""
-  # START - DEPRECATED, only kept for legacy compatibility reasons
   if [ -n "$SYNO_TOTP_SECRET" ]; then
     _info "WARNING: Usage of SYNO_TOTP_SECRET is deprecated!"
     _info "         See synology_dsm.sh script or ACME.sh Wiki page for details:"
     _info "         https://github.com/acmesh-official/acme.sh/wiki/Synology-NAS-Guide"
-    DEPRECATED_otp_code=""
-    if _exists oathtool; then
-      DEPRECATED_otp_code="$(oathtool --base32 --totp "${SYNO_TOTP_SECRET}" 2>/dev/null)"
-    else
+    if ! _exists oathtool; then
       _err "oathtool could not be found, install oathtool to use SYNO_TOTP_SECRET"
       return 1
     fi
+    DEPRECATED_otp_code="$(oathtool --base32 --totp "$SYNO_TOTP_SECRET" 2>/dev/null)"
 
-    if [ -n "$SYNO_DID" ]; then
-      _H1="Cookie: did=$SYNO_DID"
+    if [ -z "$SYNO_DEVICE_ID" ]; then
+      _getdeployconf SYNO_DID
+      [ -n "$SYNO_DID" ] || SYNO_DEVICE_ID="$SYNO_DID"
+    fi
+    if [ -n "$SYNO_DEVICE_ID" ]; then
+      _H1="Cookie: did=$SYNO_DEVICE_ID"
       export _H1
       _debug3 H1 "${_H1}"
     fi
 
-    response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DID" "$_base_url/webapi/auth.cgi?enable_syno_token=yes")
+    response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DEVICE_ID" "$_base_url/webapi/$api_path?enable_syno_token=yes")
     _debug3 response "$response"
-  # END - DEPRECATED, only kept for legacy compatibility reasons
-  # Get device ID if still empty first, otherwise log in right away
-  elif [ -z "${SYNO_Device_ID:-}" ]; then
-    printf "Enter OTP code for user '%s': " "$SYNO_Username"
-    read -r otp_code
-    if [ -z "${SYNO_Device_Name:-}" ]; then
+  # ## END ## - DEPRECATED, for backward compatibility
+  # If SYNO_DEVICE_ID or SYNO_OTP_CODE is set, we treat current account enabled 2FA-OTP.
+  # Notice that if SYNO_USE_TEMP_ADMIN=1, both variables will be unset
+  else
+    if [ -n "$SYNO_DEVICE_ID" ] || [ -n "$SYNO_OTP_CODE" ]; then
+      response='{"error":{"code":403}}'
+    # Assume the current account disabled 2FA-OTP, try to log in right away.
+    else
+      if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
+        _getdeployconf SYNO_LOCAL_HOSTNAME
+        _debug SYNO_LOCAL_HOSTNAME "${SYNO_LOCAL_HOSTNAME:-}"
+        if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then
+          if [ "$SYNO_LOCAL_HOSTNAME" != "1" ]; then
+            _err "SYNO_USE_TEMP_ADMIN=1 only support local deployment, though if you are sure that the hostname $SYNO_HOSTNAME is targeting to your **current local machine**, execute 'export SYNO_LOCAL_HOSTNAME=1' then rerun."
+            return 1
+          fi
+        fi
+        _debug "Creating temp admin user in Synology DSM..."
+        if synogroup --help | grep -q '\-\-memberadd '; then
+          _temp_admin_create "$SYNO_USERNAME" "$SYNO_PASSWORD"
+          synogroup --memberadd administrators "$SYNO_USERNAME" >/dev/null
+        elif synogroup --help | grep -q '\-\-member '; then
+          # For supporting DSM 6.x which only has `--member` parameter.
+          cur_admins=$(synogroup --get administrators | awk -F '[][]' '/Group Members/,0{if(NF>1)printf "%s ", $2}')
+          if [ -n "$cur_admins" ]; then
+            _temp_admin_create "$SYNO_USERNAME" "$SYNO_PASSWORD"
+            _secure_debug3 admin_users "$cur_admins$SYNO_USERNAME"
+            # shellcheck disable=SC2086
+            synogroup --member administrators $cur_admins $SYNO_USERNAME >/dev/null
+          else
+            _err "The tool synogroup may be broken, please set SYNO_USERNAME and SYNO_PASSWORD instead."
+            return 1
+          fi
+        else
+          _err "Unsupported synogroup tool detected, please set SYNO_USERNAME and SYNO_PASSWORD instead."
+          return 1
+        fi
+        # havig a workaround to temporary disable enforce 2FA-OTP, will restore
+        # it soon (after a single request), though if any accident occurs like
+        # unexpected interruption, this setting can be easily reverted manually.
+        otp_enforce_option=$(synogetkeyvalue /etc/synoinfo.conf otp_enforce_option)
+        if [ -n "$otp_enforce_option" ] && [ "${otp_enforce_option:-"none"}" != "none" ]; then
+          synosetkeyvalue /etc/synoinfo.conf otp_enforce_option none
+          _info "Enforcing 2FA-OTP has been disabled to complete temp admin authentication."
+          _info "Notice: it will be restored soon, if not, you can restore it manually via Control Panel."
+          _info "previous_otp_enforce_option" "$otp_enforce_option"
+        else
+          otp_enforce_option=""
+        fi
+      fi
+      response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes")
+      if [ -n "$SYNO_USE_TEMP_ADMIN" ] && [ -n "$otp_enforce_option" ]; then
+        synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option"
+        _info "Restored previous enforce 2FA-OTP option."
+      fi
+      _debug3 response "$response"
+    fi
+  fi
+
+  error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
+  _debug2 error_code "$error_code"
+  # Account has 2FA-OTP enabled, since error 403 reported.
+  # https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
+  if [ "$error_code" == "403" ]; then
+    if [ -z "$SYNO_DEVICE_NAME" ]; then
       printf "Enter device name or leave empty for default (CertRenewal): "
-      read -r SYNO_Device_Name
-      [ -n "${SYNO_Device_Name}" ] || SYNO_Device_Name="CertRenewal"
+      read -r SYNO_DEVICE_NAME
+      [ -n "$SYNO_DEVICE_NAME" ] || SYNO_DEVICE_NAME="CertRenewal"
     fi
 
-    response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&otp_code=$otp_code&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_Device_Name")
-    _secure_debug3 response "$response"
+    if [ -n "$SYNO_DEVICE_ID" ]; then
+      # Omit OTP code with SYNO_DEVICE_ID.
+      response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_DEVICE_NAME&device_id=$SYNO_DEVICE_ID")
+      _secure_debug3 response "$response"
+    else
+      # Require the OTP code if still unset.
+      if [ -z "$SYNO_OTP_CODE" ]; then
+        printf "Enter OTP code for user '%s': " "$SYNO_USERNAME"
+        read -r SYNO_OTP_CODE
+      fi
+      _secure_debug SYNO_OTP_CODE "${SYNO_OTP_CODE:-}"
 
-    id_property='device_id'
-    [ "${api_version}" -gt '6' ] || id_property='did'
-    SYNO_Device_ID=$(echo "$response" | grep "$id_property" | sed -n 's/.*"'$id_property'" *: *"\([^"]*\).*/\1/p')
-    _secure_debug2 SYNO_Device_ID "$SYNO_Device_ID"
-  else
-    response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_Device_Name&device_id=$SYNO_Device_ID")
-    _debug3 response "$response"
+      if [ -z "$SYNO_OTP_CODE" ]; then
+        response='{"error":{"code":404}}'
+      else
+        response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_DEVICE_NAME&otp_code=$SYNO_OTP_CODE")
+        _secure_debug3 response "$response"
+
+        id_property='device_id'
+        [ "${api_version}" -gt '6' ] || id_property='did'
+        SYNO_DEVICE_ID=$(echo "$response" | grep "$id_property" | sed -n 's/.*"'$id_property'" *: *"\([^"]*\).*/\1/p')
+        _secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
+      fi
+    fi
+    error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
+    _debug2 error_code "$error_code"
+  fi
+
+  if [ -n "$error_code" ]; then
+    if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
+      _cleardeployconf SYNO_DEVICE_ID
+      _err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window."
+    elif [ "$error_code" == "404" ]; then
+      _err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
+    elif [ "$error_code" == "406" ]; then
+      if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
+        _err "Failed with unexcepted error, please report this by providing full log with '--debug 3'."
+      else
+        _err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue."
+      fi
+    elif [ "$error_code" == "400" ]; then
+      _err "Failed to authenticate, no such account or incorrect password."
+    elif [ "$error_code" == "401" ]; then
+      _err "Failed to authenticate with a non-existent account."
+    elif [ "$error_code" == "408" ] || [ "$error_code" == "409" ] || [ "$error_code" == "410" ]; then
+      _err "Failed to authenticate, the account password has expired or must be changed."
+    else
+      _err "Failed to authenticate with error: $error_code."
+    fi
+    _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
+    return 1
   fi
 
   sid=$(echo "$response" | grep "sid" | sed -n 's/.*"sid" *: *"\([^"]*\).*/\1/p')
   token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p')
   _debug "Session ID" "$sid"
   _debug SynoToken "$token"
-  if [ -z "$SYNO_DID" ] && [ -z "$SYNO_Device_ID" ] || [ -z "$sid" ] || [ -z "$token" ]; then
-    _err "Unable to authenticate to $_base_url - check your username & password."
-    _err "If two-factor authentication is enabled for the user, set SYNO_Device_ID."
+  if [ -z "$sid" ] || [ -z "$token" ]; then
+    # Still can't get necessary info even got no errors, may Synology have API updated?
+    _err "Unable to authenticate to $_base_url, you may report this by providing full log with '--debug 3'."
+    _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
     return 1
   fi
 
@@ -161,36 +313,62 @@ synology_dsm_deploy() {
   export _H1
   _debug2 H1 "${_H1}"
 
-  # Now that we know the username & password are good, save them
-  _savedeployconf SYNO_Username "$SYNO_Username"
-  _savedeployconf SYNO_Password "$SYNO_Password"
-  _savedeployconf SYNO_Device_Name "$SYNO_Device_Name"
-  _savedeployconf SYNO_Device_ID "$SYNO_Device_ID"
+  # Now that we know the username and password are good, save them if not in temp admin mode.
+  if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
+    _cleardeployconf SYNO_USERNAME
+    _cleardeployconf SYNO_PASSWORD
+    _cleardeployconf SYNO_DEVICE_ID
+    _cleardeployconf SYNO_DEVICE_NAME
+    _savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
+    _savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
+  else
+    _savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
+    _savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
+    _savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
+    _savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
+  fi
 
-  _info "Getting certificates in Synology DSM"
+  _info "Getting certificates in Synology DSM..."
   response=$(_post "api=SYNO.Core.Certificate.CRT&method=list&version=1&_sid=$sid" "$_base_url/webapi/entry.cgi")
   _debug3 response "$response"
-  escaped_certificate="$(printf "%s" "$SYNO_Certificate" | sed 's/\([].*^$[]\)/\\\1/g;s/"/\\\\"/g')"
+  escaped_certificate="$(printf "%s" "$SYNO_CERTIFICATE" | sed 's/\([].*^$[]\)/\\\1/g;s/"/\\\\"/g')"
   _debug escaped_certificate "$escaped_certificate"
   id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p")
   _debug2 id "$id"
 
-  if [ -z "$id" ] && [ -z "${SYNO_Create:-}" ]; then
-    _err "Unable to find certificate: $SYNO_Certificate & \$SYNO_Create is not set"
+  error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
+  _debug2 error_code "$error_code"
+  if [ -n "$error_code" ]; then
+    if [ "$error_code" -eq 105 ]; then
+      _err "Current user is not administrator and does not have sufficient permission for deploying."
+    else
+      _err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
+    fi
+    _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
+    return 1
+  fi
+
+  _migratedeployconf SYNO_Create SYNO_CREATE
+  _getdeployconf SYNO_CREATE
+  _debug2 SYNO_CREATE "$SYNO_CREATE"
+
+  if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
+    _err "Unable to find certificate: $SYNO_CERTIFICATE and $SYNO_CREATE is not set."
+    _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
     return 1
   fi
 
   # We've verified this certificate description is a thing, so save it
-  _savedeployconf SYNO_Certificate "$SYNO_Certificate" "base64"
+  _savedeployconf SYNO_CERTIFICATE "$SYNO_CERTIFICATE" "base64"
 
-  _info "Generate form POST request"
+  _info "Generating form POST request..."
   nl="\0015\0012"
   delim="--------------------------$(_utc_date | tr -d -- '-: ')"
   content="--$delim${nl}Content-Disposition: form-data; name=\"key\"; filename=\"$(basename "$_ckey")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")\0012"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"cert\"; filename=\"$(basename "$_ccert")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ccert")\0012"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"inter_cert\"; filename=\"$(basename "$_cca")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cca")\0012"
   content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"id\"${nl}${nl}$id"
-  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_Certificate}"
+  content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"desc\"${nl}${nl}${SYNO_CERTIFICATE}"
   if echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\([^{]*\).*/\1/p" | grep -- 'is_default":true' >/dev/null; then
     _debug2 default "This is the default certificate"
     content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"as_default\"${nl}${nl}true"
@@ -201,21 +379,22 @@ synology_dsm_deploy() {
   content="$(printf "%b_" "$content")"
   content="${content%_}" # protect trailing \n
 
-  _info "Upload certificate to the Synology DSM"
+  _info "Upload certificate to the Synology DSM."
   response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}")
   _debug3 response "$response"
 
   if ! echo "$response" | grep '"error":' >/dev/null; then
     if echo "$response" | grep '"restart_httpd":true' >/dev/null; then
-      _info "Restarting HTTP services succeeded"
+      _info "Restart HTTP services succeeded."
     else
-      _info "Restarting HTTP services failed"
+      _info "Restart HTTP services failed."
     fi
-
+    _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
     _logout
     return 0
   else
-    _err "Unable to update certificate, error code $response"
+    _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
+    _err "Unable to update certificate, got error response: $response."
     _logout
     return 1
   fi
@@ -227,3 +406,40 @@ _logout() {
   response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
   _debug3 response "$response"
 }
+
+_temp_admin_create() {
+  _username="$1"
+  _password="$2"
+  synouser --del "$_username" >/dev/null 2>/dev/null
+  synouser --add "$_username" "$_password" "" 0 "" 0 >/dev/null
+}
+
+_temp_admin_cleanup() {
+  _flag=$1
+  _username=$2
+
+  if [ -n "${_flag}" ]; then
+    _debug "Cleanuping temp admin info..."
+    synouser --del "$_username" >/dev/null
+  fi
+}
+
+#_cleardeployconf   key
+_cleardeployconf() {
+  _cleardomainconf "SAVED_$1"
+}
+
+# key
+_check2cleardeployconfexp() {
+  _key="$1"
+  _clear_key="CLEAR_$_key"
+  # Clear saved settings if explicitly requested
+  if [ -n "$(eval echo \$"$_clear_key")" ]; then
+    _debug2 "$_key: value cleared from config, exported value will be ignored."
+    _cleardeployconf "$_key"
+    eval "$_key"=
+    export "$_key"=
+    eval SAVED_"$_key"=
+    export SAVED_"$_key"=
+  fi
+}

+ 113 - 64
deploy/truenas.sh

@@ -9,7 +9,7 @@
 #
 # Following environment variables must be set:
 #
-# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
+# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
 #
 # The following environmental variables may be set if you don't like their
 # default values:
@@ -64,6 +64,20 @@ truenas_deploy() {
   _response=$(_get "$_api_url/system/state")
   _info "TrueNAS system state: $_response."
 
+  _info "Getting TrueNAS version"
+  _response=$(_get "$_api_url/system/version")
+
+  if echo "$_response" | grep -q "SCALE"; then
+    _truenas_os=$(echo "$_response" | cut -d '-' -f 2)
+    _truenas_version=$(echo "$_response" | cut -d '-' -f 3 | tr -d '"' | cut -d '.' -f 1,2)
+  else
+    _truenas_os="unknown"
+    _truenas_version="unknown"
+  fi
+
+  _info "Detected TrueNAS system os: $_truenas_os"
+  _info "Detected TrueNAS system version: $_truenas_version"
+
   if [ -z "$_response" ]; then
     _err "Unable to authenticate to $_api_url."
     _err 'Check your connection settings are correct, e.g.'
@@ -115,27 +129,106 @@ truenas_deploy() {
 
   _debug3 _activate_result "$_activate_result"
 
-  _info "Checking if WebDAV certificate is the same as the TrueNAS web UI"
-  _webdav_list=$(_get "$_api_url/webdav")
-  _webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,')
-
-  if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then
-    _info "Updating the WebDAV certificate"
-    _debug _webdav_cert_id "$_webdav_cert_id"
-    _webdav_data="{\"certssl\": \"${_cert_id}\"}"
-    _activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")"
-    _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
-    if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then
-      _info "WebDAV certificate updated successfully"
-    else
-      _err "Unable to set WebDAV certificate"
-      _debug3 _activate_webdav_cert "$_activate_webdav_cert"
+  _truenas_version_23_10="23.10"
+  _truenas_version_24_10="24.10"
+
+  _check_version=$(printf "%s\n%s" "$_truenas_version_23_10" "$_truenas_version" | sort -V | head -n 1)
+  if [ "$_truenas_os" != "SCALE" ] || [ "$_check_version" != "$_truenas_version_23_10" ]; then
+    _info "Checking if WebDAV certificate is the same as the TrueNAS web UI"
+    _webdav_list=$(_get "$_api_url/webdav")
+    _webdav_cert_id=$(echo "$_webdav_list" | grep '"certssl":' | tr -d -- '"certsl: ,')
+
+    if [ "$_webdav_cert_id" = "$_active_cert_id" ]; then
+      _info "Updating the WebDAV certificate"
+      _debug _webdav_cert_id "$_webdav_cert_id"
+      _webdav_data="{\"certssl\": \"${_cert_id}\"}"
+      _activate_webdav_cert="$(_post "$_webdav_data" "$_api_url/webdav" "" "PUT" "application/json")"
+      _webdav_new_cert_id=$(echo "$_activate_webdav_cert" | _json_decode | grep '"certssl":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
+      if [ "$_webdav_new_cert_id" -eq "$_cert_id" ]; then
+        _info "WebDAV certificate updated successfully"
+      else
+        _err "Unable to set WebDAV certificate"
+        _debug3 _activate_webdav_cert "$_activate_webdav_cert"
+        _debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
+        return 1
+      fi
       _debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
-      return 1
+    else
+      _info "WebDAV certificate is not configured or is not the same as TrueNAS web UI"
+    fi
+
+    _info "Checking if S3 certificate is the same as the TrueNAS web UI"
+    _s3_list=$(_get "$_api_url/s3")
+    _s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,')
+
+    if [ "$_s3_cert_id" = "$_active_cert_id" ]; then
+      _info "Updating the S3 certificate"
+      _debug _s3_cert_id "$_s3_cert_id"
+      _s3_data="{\"certificate\": \"${_cert_id}\"}"
+      _activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")"
+      _s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
+      if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then
+        _info "S3 certificate updated successfully"
+      else
+        _err "Unable to set S3 certificate"
+        _debug3 _activate_s3_cert "$_activate_s3_cert"
+        _debug3 _s3_new_cert_id "$_s3_new_cert_id"
+        return 1
+      fi
+      _debug3 _activate_s3_cert "$_activate_s3_cert"
+    else
+      _info "S3 certificate is not configured or is not the same as TrueNAS web UI"
+    fi
+  fi
+
+  if [ "$_truenas_os" = "SCALE" ]; then
+    _check_version=$(printf "%s\n%s" "$_truenas_version_24_10" "$_truenas_version" | sort -V | head -n 1)
+    if [ "$_check_version" != "$_truenas_version_24_10" ]; then
+      _info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
+      if _exists jq; then
+        _info "Query all chart release"
+        _release_list=$(_get "$_api_url/chart/release")
+        _related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique")
+        _release_length=$(printf "%s" "$_related_name_list" | jq -r "length")
+        _info "Found $_release_length related chart release in list: $_related_name_list"
+        for i in $(seq 0 $((_release_length - 1))); do
+          _release_name=$(echo "$_related_name_list" | jq -r ".[$i]")
+          _info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name"
+          #Read the chart release configuration
+          _chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")")
+          #Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress
+          _updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert  ) |= $_cert_id | .config.ingress ")
+          _update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")"
+          _debug3 _update_chart_result "$_update_chart_result"
+        done
+      else
+        _info "Tool 'jq' does not exists, skip chart release checking"
+      fi
+    else
+      _info "Checking if any app is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
+      if _exists jq; then
+        _info "Query all apps"
+        _app_list=$(_get "$_api_url/app")
+        _app_id_list=$(printf "%s" "$_app_list" | jq -r '.[].name')
+        _app_length=$(echo "$_app_id_list" | wc -l)
+        _info "Found $_app_length apps"
+        _info "Checking for each app if an update is needed"
+        for i in $(seq 1 "$_app_length"); do
+          _app_id=$(echo "$_app_id_list" | sed -n "${i}p")
+          _app_config="$(_post "\"$_app_id\"" "$_api_url/app/config" "" "POST" "application/json")"
+          # Check if the app use the same certificate TrueNAS web UI
+          _app_active_cert_config=$(echo "$_app_config" | tr -d '\000-\037' | _json_decode | jq -r ".ix_certificates[\"$_active_cert_id\"]")
+          if [ "$_app_active_cert_config" != "null" ]; then
+            _info "Updating certificate from $_active_cert_id to $_cert_id for app: $_app_id"
+            #Replace the old certificate id with the new one in path
+            _update_app_result="$(_post "{\"values\" : { \"network\": { \"certificate_id\": $_cert_id } } }" "$_api_url/app/id/$_app_id" "" "PUT" "application/json")"
+            _debug3 _update_app_result "$_update_app_result"
+          fi
+        done
+      else
+        _info "Tool 'jq' does not exists, skip app checking"
+      fi
     fi
-    _debug3 _webdav_new_cert_id "$_webdav_new_cert_id"
-  else
-    _info "WebDAV certificate is not configured or is not the same as TrueNAS web UI"
   fi
 
   _info "Checking if FTP certificate is the same as the TrueNAS web UI"
@@ -161,50 +254,6 @@ truenas_deploy() {
     _info "FTP certificate is not configured or is not the same as TrueNAS web UI"
   fi
 
-  _info "Checking if S3 certificate is the same as the TrueNAS web UI"
-  _s3_list=$(_get "$_api_url/s3")
-  _s3_cert_id=$(echo "$_s3_list" | grep '"certificate":' | tr -d -- '"certifa:_ ,')
-
-  if [ "$_s3_cert_id" = "$_active_cert_id" ]; then
-    _info "Updating the S3 certificate"
-    _debug _s3_cert_id "$_s3_cert_id"
-    _s3_data="{\"certificate\": \"${_cert_id}\"}"
-    _activate_s3_cert="$(_post "$_s3_data" "$_api_url/s3" "" "PUT" "application/json")"
-    _s3_new_cert_id=$(echo "$_activate_s3_cert" | _json_decode | grep '"certificate":' | sed -n 's/.*: \([0-9]\{1,\}\),\{0,1\}$/\1/p')
-    if [ "$_s3_new_cert_id" -eq "$_cert_id" ]; then
-      _info "S3 certificate updated successfully"
-    else
-      _err "Unable to set S3 certificate"
-      _debug3 _activate_s3_cert "$_activate_s3_cert"
-      _debug3 _s3_new_cert_id "$_s3_new_cert_id"
-      return 1
-    fi
-    _debug3 _activate_s3_cert "$_activate_s3_cert"
-  else
-    _info "S3 certificate is not configured or is not the same as TrueNAS web UI"
-  fi
-
-  _info "Checking if any chart release Apps is using the same certificate as TrueNAS web UI. Tool 'jq' is required"
-  if _exists jq; then
-    _info "Query all chart release"
-    _release_list=$(_get "$_api_url/chart/release")
-    _related_name_list=$(printf "%s" "$_release_list" | jq -r "[.[] | {name,certId: .config.ingress?.main.tls[]?.scaleCert} | select(.certId==$_active_cert_id) | .name ] | unique")
-    _release_length=$(printf "%s" "$_related_name_list" | jq -r "length")
-    _info "Found $_release_length related chart release in list: $_related_name_list"
-    for i in $(seq 0 $((_release_length - 1))); do
-      _release_name=$(echo "$_related_name_list" | jq -r ".[$i]")
-      _info "Updating certificate from $_active_cert_id to $_cert_id for chart release: $_release_name"
-      #Read the chart release configuration
-      _chart_config=$(printf "%s" "$_release_list" | jq -r ".[] | select(.name==\"$_release_name\")")
-      #Replace the old certificate id with the new one in path .config.ingress.main.tls[].scaleCert. Then update .config.ingress
-      _updated_chart_config=$(printf "%s" "$_chart_config" | jq "(.config.ingress?.main.tls[]? | select(.scaleCert==$_active_cert_id) | .scaleCert  ) |= $_cert_id | .config.ingress ")
-      _update_chart_result="$(_post "{\"values\" : { \"ingress\" : $_updated_chart_config } }" "$_api_url/chart/release/id/$_release_name" "" "PUT" "application/json")"
-      _debug3 _update_chart_result "$_update_chart_result"
-    done
-  else
-    _info "Tool 'jq' does not exists, skip chart release checking"
-  fi
-
   _info "Deleting old certificate"
   _delete_result="$(_post "" "$_api_url/certificate/id/$_active_cert_id" "" "DELETE" "application/json")"
 

+ 325 - 0
deploy/truenas_ws.sh

@@ -0,0 +1,325 @@
+#!/usr/bin/env sh
+
+# TrueNAS deploy script for SCALE/CORE using websocket
+# It is recommend to use a wildcard certificate
+#
+# Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html
+#
+# Tested with TrueNAS Scale - Electric Eel 24.10
+# Changes certificate in the following services:
+#  - Web UI
+#  - FTP
+#  - iX Apps
+#
+# The following environment variables must be set:
+# ------------------------------------------------
+#
+# # API KEY
+# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
+# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
+#
+
+### Private functions
+
+# Call websocket method
+# Usage:
+#   _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'")
+#   _info "$_ws_response"
+#
+# Output:
+#   {"z": 9}
+#
+# Arguments:
+#   $@ - midclt arguments for call
+#
+# Returns:
+#   JSON/JOBID
+_ws_call() {
+  _debug "_ws_call arg1" "$1"
+  _debug "_ws_call arg2" "$2"
+  _debug "_ws_call arg3" "$3"
+  if [ $# -eq 3 ]; then
+    _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
+  fi
+  if [ $# -eq 2 ]; then
+    _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
+  fi
+  if [ $# -eq 1 ]; then
+    _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
+  fi
+  _debug "_ws_response" "$_ws_response"
+  printf "%s" "$_ws_response"
+  return 0
+}
+
+# Upload certificate with webclient api
+_ws_upload_cert() {
+
+  /usr/bin/env python - <<EOF
+
+import sys
+
+from truenas_api_client import Client
+with Client() as c:
+
+  ### Login with API key
+  print("I:Trying to upload new certificate...")
+  ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
+  if ret:
+    ### upload certificate
+    with open('$1', 'r') as file:
+      fullchain = file.read()
+    with open('$2', 'r') as file:
+      privatekey = file.read()
+    ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True)
+    print("R:" + str(ret["id"]))
+    sys.exit(0)
+  else:
+    print("R:0")
+    print("E:_ws_upload_cert error!")
+    sys.exit(7)
+
+EOF
+
+  return $?
+
+}
+
+# Check argument is a number
+# Usage:
+#
+# Output:
+#   n/a
+#
+# Arguments:
+#   $1 - Anything
+#
+# Returns:
+#   0: true
+#   1: false
+_ws_check_jobid() {
+  case "$1" in
+  [0-9]*)
+    return 0
+    ;;
+  esac
+  return 1
+}
+
+# Wait for job to finish and return result as JSON
+# Usage:
+#   _ws_result=$(_ws_get_job_result "$_ws_jobid")
+#   _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
+#
+# Output:
+#   JSON result of the job
+#
+# Arguments:
+#   $1 - JobID
+#
+# Returns:
+#   n/a
+_ws_get_job_result() {
+  while true; do
+    sleep 2
+    _ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
+    if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then
+      _ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
+      _debug "_ws_result" "$_ws_result"
+      printf "%s" "$_ws_result"
+      _ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
+      if [ "$_ws_error" != "null" ]; then
+        _err "Job $1 failed:"
+        _err "$_ws_error"
+        return 7
+      fi
+      break
+    fi
+  done
+  return 0
+}
+
+########################
+### Public functions ###
+########################
+
+# truenas_ws_deploy
+#
+# Deploy new certificate to TrueNAS services
+#
+# Arguments
+#  1: Domain
+#  2: Key-File
+#  3: Certificate-File
+#  4: CA-File
+#  5: FullChain-File
+# Returns:
+#  0: Success
+#  1: Missing API Key
+#  2: TrueNAS not ready
+#  3: Not a JobID
+#  4: FTP cert error
+#  5: WebUI cert error
+#  6: Job error
+#  7: WS call error
+#
+truenas_ws_deploy() {
+  _domain="$1"
+  _file_key="$2"
+  _file_cert="$3"
+  _file_ca="$4"
+  _file_fullchain="$5"
+  _debug _domain "$_domain"
+  _debug _file_key "$_file_key"
+  _debug _file_cert "$_file_cert"
+  _debug _file_ca "$_file_ca"
+  _debug _file_fullchain "$_file_fullchain"
+
+  ########## Environment check
+
+  _info "Checking environment variables..."
+  _getdeployconf DEPLOY_TRUENAS_APIKEY
+  # Check API Key
+  if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
+    _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
+    return 1
+  fi
+  _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
+  _info "Environment variables: OK"
+
+  ########## Health check
+
+  _info "Checking TrueNAS health..."
+  _ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
+  _ws_ret=$?
+  if [ $_ws_ret -gt 0 ]; then
+    _err "Error calling system.ready:"
+    _err "$_ws_response"
+    return $_ws_ret
+  fi
+
+  if [ "$_ws_response" != "TRUE" ]; then
+    _err "TrueNAS is not ready."
+    _err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
+    _err "Verify API key."
+    return 2
+  fi
+  _savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
+  _info "TrueNAS health: OK"
+
+  ########## System info
+
+  _info "Gather system info..."
+  _ws_response=$(_ws_call "system.info")
+  _truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
+  _info "TrueNAS version: $_truenas_version"
+
+  ########## Gather current certificate
+
+  _info "Gather current WebUI certificate..."
+  _ws_response="$(_ws_call "system.general.config")"
+  _ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
+  _ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
+  _info "Current WebUI certificate ID: $_ui_certificate_id"
+  _info "Current WebUI certificate name: $_ui_certificate_name"
+
+  ########## Upload new certificate
+
+  _info "Upload new certificate..."
+  _certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
+  _info "New WebUI certificate name: $_certname"
+  _debug _certname "$_certname"
+  _ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
+
+  echo "$_ws_out" | while IFS= read -r LINE; do
+    case "$LINE" in
+    I:*)
+      _info "${LINE#I:}"
+      ;;
+    D:*)
+      _debug "${LINE#D:}"
+      ;;
+    E*)
+      _err "${LINE#E:}"
+      ;;
+    *) ;;
+
+    esac
+  done
+
+  _new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
+
+  _info "New certificate ID: $_new_certid"
+
+  ########## FTP
+
+  _info "Replace FTP certificate..."
+  _ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
+  _ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
+  if [ "$_ftp_certid" != "$_new_certid" ]; then
+    _err "Cannot set FTP certificate."
+    _debug "_ws_response" "$_ws_response"
+    return 4
+  fi
+
+  ########## ix Apps (SCALE only)
+
+  _info "Replace app certificates..."
+  _ws_response=$(_ws_call "app.query")
+  for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
+    _info "Checking app $_app_name..."
+    _ws_response=$(_ws_call "app.config" "$_app_name")
+    if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
+      _info "App has certificate option, setup new certificate..."
+      _info "App will be redeployed after updating the certificate."
+      _ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
+      _debug "_ws_jobid" "$_ws_jobid"
+      if ! _ws_check_jobid "$_ws_jobid"; then
+        _err "No JobID returned from websocket method."
+        return 3
+      fi
+      _ws_result=$(_ws_get_job_result "$_ws_jobid")
+      _ws_ret=$?
+      if [ $_ws_ret -gt 0 ]; then
+        return $_ws_ret
+      fi
+      _debug "_ws_result" "$_ws_result"
+      _info "App certificate replaced."
+    else
+      _info "App has no certificate option, skipping..."
+    fi
+  done
+
+  ########## WebUI
+
+  _info "Replace WebUI certificate..."
+  _ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
+  _changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
+  if [ "$_changed_certid" != "$_new_certid" ]; then
+    _err "WebUI certificate change error.."
+    return 5
+  else
+    _info "WebUI certificate replaced."
+  fi
+  _info "Restarting WebUI..."
+  _ws_response=$(_ws_call "system.general.ui_restart")
+  _info "Waiting for UI restart..."
+  sleep 6
+
+  ########## Certificates
+
+  _info "Deleting old certificate..."
+  _ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
+  if ! _ws_check_jobid "$_ws_jobid"; then
+    _err "No JobID returned from websocket method."
+    return 3
+  fi
+  _ws_result=$(_ws_get_job_result "$_ws_jobid")
+  _ws_ret=$?
+  if [ $_ws_ret -gt 0 ]; then
+    return $_ws_ret
+  fi
+
+  _info "Have a nice day...bye!"
+
+}

+ 118 - 27
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.
@@ -21,7 +30,9 @@
 # Keystore password (built into Unifi Controller, not a user-set password):
 #DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
 # Command to restart Unifi Controller:
-#DEPLOY_UNIFI_RELOAD="service unifi restart"
+# DEPLOY_UNIFI_RELOAD="systemctl restart unifi"
+# System Properties file location for controller
+#DEPLOY_UNIFI_SYSTEM_PROPERTIES="/usr/lib/unifi/data/system.properties"
 #
 # Settings for Unifi Cloud Key Gen1 (nginx admin pages):
 # Directory where cloudkey.crt and cloudkey.key live:
@@ -34,7 +45,7 @@
 # Directory where unifi-core.crt and unifi-core.key live:
 #DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/"
 # Command to restart unifi-core:
-#DEPLOY_UNIFI_RELOAD="systemctl restart unifi-core"
+# DEPLOY_UNIFI_OS_RELOAD="systemctl restart unifi-core"
 #
 # At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR,
 # or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs.
@@ -60,12 +71,16 @@ unifi_deploy() {
   _getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR
   _getdeployconf DEPLOY_UNIFI_CORE_CONFIG
   _getdeployconf DEPLOY_UNIFI_RELOAD
+  _getdeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES
+  _getdeployconf DEPLOY_UNIFI_OS_RELOAD
 
   _debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
   _debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
   _debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
   _debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
   _debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+  _debug2 DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
+  _debug2 DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
 
   # Space-separated list of environments detected and installed:
   _services_updated=""
@@ -74,14 +89,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 +109,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 +117,77 @@ 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
+
+    # correct file ownership according to the directory, the keystore is placed in
+    _unifi_keystore_dir=$(dirname "${_unifi_keystore}")
+    _unifi_keystore_dir_owner=$(find "${_unifi_keystore_dir}" -maxdepth 0 -printf '%u\n')
+    _unifi_keystore_owner=$(find "${_unifi_keystore}" -maxdepth 0 -printf '%u\n')
+    if ! [ "${_unifi_keystore_owner}" = "${_unifi_keystore_dir_owner}" ]; then
+      _debug "Changing keystore owner to ${_unifi_keystore_dir_owner}"
+      chown "$_unifi_keystore_dir_owner" "${_unifi_keystore}" >/dev/null 2>&1 # fail quietly if we're not running as root
     fi
 
-    if systemctl -q is-active unifi; then
-      _reload_cmd="${_reload_cmd:+$_reload_cmd && }service unifi restart"
+    # Update unifi service for certificate cipher compatibility
+    _unifi_system_properties="${DEPLOY_UNIFI_SYSTEM_PROPERTIES:-/usr/lib/unifi/data/system.properties}"
+    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
+      if [ -f "$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties" ]; then
+        _unifi_system_properties="$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties"
+      else
+        _unifi_system_properties="/usr/lib/unifi/data/system.properties"
+      fi
+      if [ -f "${_unifi_system_properties}" ]; then
+        cp -f "${_unifi_system_properties}" "${_unifi_system_properties}"_original
+        _info "Updating system configuration for cipher compatibility."
+        _info "Saved original system config to ${_unifi_system_properties}_original"
+        sed -i '/unifi\.https\.ciphers/d' "${_unifi_system_properties}"
+        echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>"${_unifi_system_properties}"
+        sed -i '/unifi\.https\.sslEnabledProtocols/d' "${_unifi_system_properties}"
+        echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>"${_unifi_system_properties}"
+        _info "System configuration updated."
+      fi
+    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 OS (e.g. unifi-core service), otherwise stop for later restart.
+    _unifi_reload="${DEPLOY_UNIFI_RELOAD:-systemctl restart unifi}"
+    if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
+    else
+      _info "Stopping Unifi Controller for later restart."
+      _unifi_stop=$(echo "${_unifi_reload}" | sed -e 's/restart/stop/')
+      $_unifi_stop
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
+      _info "Unifi Controller stopped."
     fi
     _services_updated="${_services_updated} unifi"
     _info "Install Unifi Controller certificate success!"
@@ -134,13 +207,24 @@ unifi_deploy() {
       return 1
     fi
     # Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks.
-    # Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was
-    # updated above), but if not, we don't know how to handle this installation:
-    if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
-      _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
-      return 1
+    # It appears that unifi won't start if this is a symlink, so we'll copy it instead.
+
+    # if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
+    #   _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
+    #   return 1
+    # fi
+
+    _info "Updating ${_cloudkey_certdir}/unifi.keystore.jks"
+    if [ -e "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
+      if [ -L "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
+        rm -f "${_cloudkey_certdir}/unifi.keystore.jks"
+      else
+        mv "${_cloudkey_certdir}/unifi.keystore.jks" "${_cloudkey_certdir}/unifi.keystore.jks_original"
+      fi
     fi
 
+    cp "${_unifi_keystore}" "${_cloudkey_certdir}/unifi.keystore.jks"
+
     cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt"
     cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key"
     (cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks)
@@ -165,12 +249,17 @@ 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"
 
-    if systemctl -q is-active unifi-core; then
-      _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core"
-    fi
+    _unifi_os_reload="${DEPLOY_UNIFI_OS_RELOAD:-systemctl restart unifi-core}"
+    _reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_os_reload"
+
     _info "Install UnifiOS certificate success!"
     _services_updated="${_services_updated} unifi-core"
   elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then
@@ -209,6 +298,8 @@ unifi_deploy() {
   _savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
   _savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
   _savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+  _savedeployconf DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
+  _savedeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
 
   return 0
 }

+ 88 - 18
deploy/vault.sh

@@ -70,20 +70,25 @@ vault_deploy() {
 
   # JSON does not allow multiline strings.
   # So replacing new-lines with "\n" here
-  _ckey=$(sed -z 's/\n/\\n/g' <"$2")
-  _ccert=$(sed -z 's/\n/\\n/g' <"$3")
-  _cca=$(sed -z 's/\n/\\n/g' <"$4")
-  _cfullchain=$(sed -z 's/\n/\\n/g' <"$5")
+  _ckey=$(sed -e ':a' -e N -e '$ ! ba' -e 's/\n/\\n/g' <"$2")
+  _ccert=$(sed -e ':a' -e N -e '$ ! ba' -e 's/\n/\\n/g' <"$3")
+  _cca=$(sed -e ':a' -e N -e '$ ! ba' -e 's/\n/\\n/g' <"$4")
+  _cfullchain=$(sed -e ':a' -e N -e '$ ! ba' -e 's/\n/\\n/g' <"$5")
 
   export _H1="X-Vault-Token: $VAULT_TOKEN"
 
   if [ -n "$VAULT_RENEW_TOKEN" ]; then
     URL="$VAULT_ADDR/v1/auth/token/renew-self"
     _info "Renew the Vault token to default TTL"
-    if ! _post "" "$URL" >/dev/null; then
+    _response=$(_post "" "$URL")
+    if [ "$?" != "0" ]; then
       _err "Failed to renew the Vault token"
       return 1
     fi
+    if echo "$_response" | grep -q '"errors":\['; then
+      _err "Failed to renew the Vault token: $_response"
+      return 1
+    fi
   fi
 
   URL="$VAULT_ADDR/v1/$VAULT_PREFIX/$_cdomain"
@@ -91,29 +96,85 @@ vault_deploy() {
   if [ -n "$VAULT_FABIO_MODE" ]; then
     _info "Writing certificate and key to $URL in Fabio mode"
     if [ -n "$VAULT_KV_V2" ]; then
-      _post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL" >/dev/null || return 1
+      _response=$(_post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error: $_response"
+        return 1
+      fi
     else
-      _post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL" >/dev/null || return 1
+      _response=$(_post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error: $_response"
+        return 1
+      fi
     fi
   else
     if [ -n "$VAULT_KV_V2" ]; then
       _info "Writing certificate to $URL/cert.pem"
-      _post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem" >/dev/null || return 1
+      _response=$(_post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing cert.pem: $_response"
+        return 1
+      fi
+
       _info "Writing key to $URL/cert.key"
-      _post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key" >/dev/null || return 1
+      _response=$(_post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing cert.key: $_response"
+        return 1
+      fi
+
       _info "Writing CA certificate to $URL/ca.pem"
-      _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem" >/dev/null || return 1
+      _response=$(_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing ca.pem: $_response"
+        return 1
+      fi
+
       _info "Writing full-chain certificate to $URL/fullchain.pem"
-      _post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem" >/dev/null || return 1
+      _response=$(_post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing fullchain.pem: $_response"
+        return 1
+      fi
     else
       _info "Writing certificate to $URL/cert.pem"
-      _post "{\"value\": \"$_ccert\"}" "$URL/cert.pem" >/dev/null || return 1
+      _response=$(_post "{\"value\": \"$_ccert\"}" "$URL/cert.pem")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing cert.pem: $_response"
+        return 1
+      fi
+
       _info "Writing key to $URL/cert.key"
-      _post "{\"value\": \"$_ckey\"}" "$URL/cert.key" >/dev/null || return 1
+      _response=$(_post "{\"value\": \"$_ckey\"}" "$URL/cert.key")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing cert.key: $_response"
+        return 1
+      fi
+
       _info "Writing CA certificate to $URL/ca.pem"
-      _post "{\"value\": \"$_cca\"}" "$URL/ca.pem" >/dev/null || return 1
+      _response=$(_post "{\"value\": \"$_cca\"}" "$URL/ca.pem")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing ca.pem: $_response"
+        return 1
+      fi
+
       _info "Writing full-chain certificate to $URL/fullchain.pem"
-      _post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem" >/dev/null || return 1
+      _response=$(_post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem")
+      if [ "$?" != "0" ]; then return 1; fi
+      if echo "$_response" | grep -q '"errors":\['; then
+        _err "Vault error writing fullchain.pem: $_response"
+        return 1
+      fi
     fi
 
     # To make it compatible with the wrong ca path `chain.pem` which was used in former versions
@@ -121,11 +182,20 @@ vault_deploy() {
       _err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning"
       _info "Updating CA certificate to $URL/chain.pem for backward compatibility"
       if [ -n "$VAULT_KV_V2" ]; then
-        _post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem" >/dev/null || return 1
+        _response=$(_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem")
+        if [ "$?" != "0" ]; then return 1; fi
+        if echo "$_response" | grep -q '"errors":\['; then
+          _err "Vault error writing chain.pem: $_response"
+          return 1
+        fi
       else
-        _post "{\"value\": \"$_cca\"}" "$URL/chain.pem" >/dev/null || return 1
+        _response=$(_post "{\"value\": \"$_cca\"}" "$URL/chain.pem")
+        if [ "$?" != "0" ]; then return 1; fi
+        if echo "$_response" | grep -q '"errors":\['; then
+          _err "Vault error writing chain.pem: $_response"
+          return 1
+        fi
       fi
     fi
   fi
-
 }

+ 1 - 1
deploy/vsftpd.sh

@@ -106,5 +106,5 @@ vsftpd_deploy() {
     fi
     return 1
   fi
-  return 0
+
 }

+ 500 - 0
deploy/zyxel_gs1900.sh

@@ -0,0 +1,500 @@
+#!/usr/bin/env sh
+
+# Deploy certificates to Zyxel GS1900 series switches
+#
+# This script uses the https web administration interface in order
+# to upload updated certificates to Zyxel GS1900 series switches.
+# Only a few models have been tested but untested switches from the
+# same model line may work as well. If you test and confirm a switch
+# as working please submit a pull request updating this compatibility
+# list!
+#
+# Known Issues:
+#   1. This is a consumer grade switch and is a bit underpowered
+#      the longer the RSA key size the slower your switch web UI
+#      will be. RSA 2048 will work, RSA 4096 will work but you may
+#      experience performance problems.
+#   2. You must use RSA certificates. The switch will reject EC-256
+#      and EC-384 certificates in firmware 2.80
+#      See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/
+#
+# Current GS1900 Switch Compatibility:
+#   GS1900-8    - Working as of firmware V2.80
+#   GS1900-8HP  - Untested
+#   GS1900-10HP - Untested
+#   GS1900-16   - Untested
+#   GS1900-24   - Untested
+#   GS1900-24E  - Working as of firmware V2.80
+#   GS1900-24EP - Untested
+#   GS1900-24HP - Untested
+#   GS1900-48   - Untested
+#   GS1900-48HP - Untested
+#
+# Prerequisite Setup Steps:
+#   1. Install at least firmware V2.80 on your switch
+#   2. Enable HTTPS web management on your switch
+#
+# Usage:
+#   1. Ensure the switch has firmware V2.80 or later.
+#   2. Ensure the switch has HTTPS management enabled.
+#   3. Set the appropriate environment variables for your environment.
+#
+#      DEPLOY_ZYXEL_SWITCH          - The switch hostname. (Default: _cdomain)
+#      DEPLOY_ZYXEL_SWITCH_USER     - The webadmin user. (Default: admin)
+#      DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch.
+#      DEPLOY_ZYXEL_SWITCH_REBOOT   - If "1" reboot after update. (Default: "0")
+#
+#   4. Run the deployment plugin:
+#      acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com
+#
+# returns 0 means success, otherwise error.
+
+#domain keyfile certfile cafile fullchain
+zyxel_gs1900_deploy() {
+  _zyxel_gs1900_minimum_firmware_version="v2.80"
+
+  _cdomain="$1"
+  _ckey="$2"
+  _ccert="$3"
+  _cca="$4"
+  _cfullchain="$5"
+
+  _debug _cdomain "$_cdomain"
+  _debug2 _ckey "$_ckey"
+  _debug _ccert "$_ccert"
+  _debug _cca "$_cca"
+  _debug _cfullchain "$_cfullchain"
+
+  _getdeployconf DEPLOY_ZYXEL_SWITCH
+  _getdeployconf DEPLOY_ZYXEL_SWITCH_USER
+  _getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
+  _getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then
+    DEPLOY_ZYXEL_SWITCH="$_cdomain"
+  fi
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then
+    DEPLOY_ZYXEL_SWITCH_USER="admin"
+  fi
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then
+    DEPLOY_ZYXEL_SWITCH_PASSWORD="1234"
+  fi
+
+  if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then
+    DEPLOY_ZYXEL_SWITCH_REBOOT="0"
+  fi
+
+  _savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
+  _savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
+  _savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
+  _savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
+
+  _debug DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
+  _debug DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
+  _secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
+  _debug DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
+
+  _zyxel_switch_base_uri="https://${DEPLOY_ZYXEL_SWITCH}"
+
+  _info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}."
+  _zyxel_gs1900_deployment_precheck || return $?
+
+  _zyxel_gs1900_should_update
+  if [ "$?" != "0" ]; then
+    _info "The switch already has our certificate installed. No update required."
+    return 0
+  else
+    _info "The switch does not yet have our certificate installed."
+  fi
+
+  _info "Logging into the switch web interface."
+  _zyxel_gs1900_login || return $?
+
+  _info "Validating the switch is compatible with this deployment process."
+  _zyxel_gs1900_validate_device_compatibility || return $?
+
+  _info "Uploading the certificate."
+  _zyxel_gs1900_upload_certificate || return $?
+
+  if [ "$DEPLOY_ZYXEL_SWITCH_REBOOT" = "1" ]; then
+    _info "Rebooting the switch."
+    _zyxel_gs1900_trigger_reboot || return $?
+  fi
+
+  return 0
+}
+
+_zyxel_gs1900_deployment_precheck() {
+  # Initialize the keylength if it isn't already
+  if [ -z "$Le_Keylength" ]; then
+    Le_Keylength=""
+  fi
+
+  if _isEccKey "$Le_Keylength"; then
+    _info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!"
+    _info "You can continue, but your switch may reject your key."
+  elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then
+    _info "Warning: Your RSA key length is greater than 2048!"
+    _info "You can continue, but you may experience performance issues in the web administration interface."
+  fi
+
+  # Check the server for some common failure modes prior to authentication and certificate upload in order to avoid
+  # sending a certificate when we may not want to.
+  test_login_response=$(_post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" 2>&1)
+  test_login_page_exitcode="$?"
+  _debug3 "Test Login Response: ${test_login_response}"
+  if [ "$test_login_page_exitcode" -ne "0" ]; then
+    if { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then
+      _err "The SSL certificate at $_zyxel_switch_base_uri could not be validated."
+      _err "Please double check your hostname, port, and that you are actually connecting to your switch."
+      _err "If the problem persists then please ensure that the certificate is not self-signed, has not"
+      _err "expired, and matches the switch hostname. If you expect validation to fail then you can disable"
+      _err "certificate validation by running with --insecure."
+      return 1
+    elif [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then
+      _debug3 "Intentionally ignore curl exit code 56 in our precheck"
+    else
+      _err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri."
+      return 1
+    fi
+  fi
+}
+
+_zyxel_gs1900_login() {
+  # Login to the switch and set the appropriate auth cookie in _H1
+  username_encoded=$(printf "%s" "$DEPLOY_ZYXEL_SWITCH_USER" | _url_encode)
+  password_encoded=$(_zyxel_gs1900_password_obfuscate "$DEPLOY_ZYXEL_SWITCH_PASSWORD" | _url_encode)
+
+  login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
+  auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
+  if [ "$auth_response" != "OK" ]; then
+    _err "Login failed due to invalid credentials."
+    _err "Please double check the configured username and password and try again."
+    return 1
+  fi
+
+  sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';')
+  _secure_debug2 "sessionid" "$sessionid"
+
+  export _H1="Cookie: $sessionid"
+  _secure_debug2 "_H1" "$_H1"
+
+  return 0
+}
+
+_zyxel_gs1900_validate_device_compatibility() {
+  # Check the switches model and firmware version and throw errors
+  # if this script isn't compatible.
+  device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n')
+
+  model_name=$(_zyxel_gs1900_get_model "$device_info_html")
+  _debug2 "model_name" "$model_name"
+  if [ -z "$model_name" ]; then
+    _err "Could not find the switch model name."
+    _err "Please re-run with --debug and report a bug."
+    return $?
+  fi
+
+  if ! expr "$model_name" : "GS1900-" >/dev/null; then
+    _err "Switch is an unsupported model: $model_name"
+    return 1
+  fi
+
+  firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html")
+  _debug2 "firmware_version" "$firmware_version"
+  if [ -z "$firmware_version" ]; then
+    _err "Could not find the switch firmware version."
+    _err "Please re-run with --debug and report a bug."
+    return $?
+  fi
+
+  _debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version"
+  minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version")
+  _debug2 "minimum_major_version" "$minimum_major_version"
+  minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version")
+  _debug2 "minimum_minor_version" "$minimum_minor_version"
+
+  _debug2 "firmware_version" "$firmware_version"
+  firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version")
+  _debug2 "firmware_major_version" "$firmware_major_version"
+  firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version")
+  _debug2 "firmware_minor_version" "$firmware_minor_version"
+
+  _ret=0
+  if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then
+    _ret=1
+  elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then
+    _ret=1
+  fi
+
+  if [ "$_ret" != "0" ]; then
+    _err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version."
+  fi
+
+  return $?
+}
+
+_zyxel_gs1900_should_update() {
+  # Get the remote certificate serial number
+  _remote_cert=$(${ACME_OPENSSL_BIN:-openssl} s_client -showcerts -connect "${DEPLOY_ZYXEL_SWITCH}:443" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p')
+  _debug3 "_remote_cert" "$_remote_cert"
+
+  _remote_cert_serial=$(printf "%s" "${_remote_cert}" | ${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial)
+  _debug2 "_remote_cert_serial" "$_remote_cert_serial"
+
+  # Get our certificate serial number
+  _our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial <"${_ccert}")
+  _debug2 "_our_cert_serial" "$_our_cert_serial"
+
+  [ "${_remote_cert_serial}" != "${_our_cert_serial}" ]
+}
+
+_zyxel_gs1900_upload_certificate() {
+  # Generate a PKCS12 certificate with a temporary password since the web interface
+  # requires a password be present. Then upload that certificate.
+  temp_cert_password=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 64)
+  _secure_debug2 "temp_cert_password" "$temp_cert_password"
+
+  temp_pkcs12="$(_mktemp)"
+  _debug2 "temp_pkcs12" "$temp_pkcs12"
+  _toPkcs "$temp_pkcs12" "$_ckey" "$_ccert" "$_cca" "$temp_cert_password"
+  if [ "$?" != "0" ]; then
+    _err "Failed to generate a pkcs12 certificate."
+    _err "Please re-run with --debug and report a bug."
+
+    # ensure the temporary certificate file is cleaned up
+    [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
+
+    return $?
+  fi
+
+  # Load the upload page
+  upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n')
+
+  # Get the first instance of XSSID from the upload page
+  form_xss_value=$(printf "%s" "$upload_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' | head -n 1)
+  _secure_debug2 "form_xss_value" "$form_xss_value"
+
+  _info "Generating the certificate upload request"
+  upload_post_request="$(_mktemp)"
+  upload_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
+
+  {
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"XSSID\"\r\n\r\n%s\r\n" "${form_xss_value}"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"http_file\"; filename=\"temp_pkcs12.pfx\"\r\n"
+    printf "Content-Type: application/pkcs12\r\n\r\n"
+    cat "${temp_pkcs12}"
+    printf "\r\n"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"pwd\"\r\n\r\n%s\r\n" "${temp_cert_password}"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"cmd\"\r\n\r\n%s\r\n" "31"
+    printf -- "--%s\r\n" "${upload_post_boundary}"
+    printf "Content-Disposition: form-data; name=\"sysSubmit\"\r\n\r\n%s\r\n" "Import"
+    printf -- "--%s--\r\n" "${upload_post_boundary}"
+  } >"${upload_post_request}"
+
+  _info "Upload certificate to the switch"
+
+  # Unfortunately we cannot rely upon the switch response across switch models
+  # to return a consistent body return - so we cannot inspect the result of this
+  # upload to determine success.
+  upload_response=$(_zyxel_upload_pkcs12 "${upload_post_request}" "${upload_post_boundary}" 2>&1)
+  _debug3 "Upload response: ${upload_response}"
+  rm "${upload_post_request}"
+
+  # Pause for a few seconds to give the switch a chance to process the certificate
+  # For some reason I've found this to be necessary on my GS1900-24E
+  _debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate."
+  sleep "4"
+
+  # Check to see whether or not our update was successful
+  _ret=0
+  _zyxel_gs1900_should_update
+  if [ "$?" != "0" ]; then
+    _info "The certificate was updated successfully"
+  else
+    _ret=1
+    _err "The certificate upload does not appear to have worked."
+    _err "The remote certificate does not match the certificate we tried to upload."
+    _err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug."
+  fi
+
+  # ensure the temporary files are cleaned up
+  [ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
+
+  return $_ret
+}
+
+# make the certificate upload request using either
+# --data binary with @ for file access in CURL
+# or using --post-file for wget to ensure we upload
+# the pkcs12 without getting tripped up on null bytes
+#
+# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker]
+_zyxel_upload_pkcs12() {
+  bodyfilename="$1"
+  multipartformmarker="$2"
+  _post_url="${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi"
+  httpmethod="POST"
+  _postContentType="multipart/form-data; boundary=${multipartformmarker}"
+
+  if [ -z "$httpmethod" ]; then
+    httpmethod="POST"
+  fi
+  _debug $httpmethod
+  _debug "_post_url" "$_post_url"
+  _debug2 "bodyfilename" "$bodyfilename"
+  _debug2 "_postContentType" "$_postContentType"
+
+  _inithttp
+
+  if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then
+    _CURL="$_ACME_CURL"
+    if [ "$HTTPS_INSECURE" ]; then
+      _CURL="$_CURL --insecure  "
+    fi
+    if [ "$httpmethod" = "HEAD" ]; then
+      _CURL="$_CURL -I  "
+    fi
+    _debug "_CURL" "$_CURL"
+
+    response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data-binary "@${bodyfilename}" "$_post_url")"
+
+    _ret="$?"
+    if [ "$_ret" != "0" ]; then
+      _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
+      if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
+        _err "Here is the curl dump log:"
+        _err "$(cat "$_CURL_DUMP")"
+      fi
+    fi
+  elif [ "$_ACME_WGET" ]; then
+    _WGET="$_ACME_WGET"
+    if [ "$HTTPS_INSECURE" ]; then
+      _WGET="$_WGET --no-check-certificate "
+    fi
+    _debug "_WGET" "$_WGET"
+
+    response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-file="${bodyfilename}" "$_post_url" 2>"$HTTP_HEADER")"
+
+    _ret="$?"
+    if [ "$_ret" = "8" ]; then
+      _ret=0
+      _debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later."
+    fi
+    if [ "$_ret" != "0" ]; then
+      _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret"
+    fi
+    if _contains "$_WGET" " -d "; then
+      # Demultiplex wget debug output
+      cat "$HTTP_HEADER" >&2
+      _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
+    fi
+    # remove leading whitespaces from header to match curl format
+    _sed_i 's/^  //g' "$HTTP_HEADER"
+  else
+    _ret="$?"
+    _err "Neither curl nor wget have been found, cannot make $httpmethod request."
+  fi
+  _debug "_ret" "$_ret"
+  printf "%s" "$response"
+  return $_ret
+}
+
+_zyxel_gs1900_trigger_reboot() {
+  # Trigger a reboot via the management reboot page in the web ui
+  reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n')
+  reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g')
+  _secure_debug2 "reboot_xss_value" "$reboot_xss_value"
+
+  reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded")
+  reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...")
+
+  if [ -z "$reboot_message" ]; then
+    _err "Failed to trigger switch reboot!"
+    return 1
+  fi
+
+  return 0
+}
+
+# password
+_zyxel_gs1900_password_obfuscate() {
+  # Return the password obfuscated via the same method used by the
+  # switch's web UI login process
+  echo "$1" | awk '{
+    encoded = "";
+    password = $1;
+    allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    len = length($1);
+    pwi = length($1);
+
+    for (i=1; i <= (321 - pwi); i++)
+    {
+      if (0 == i % 5 && pwi > 0)
+      {
+        encoded = (encoded)(substr(password, pwi--, 1));
+      }
+      else if (i == 123)
+      {
+        if (len < 10)
+        {
+          encoded = (encoded)(0);
+        }
+        else
+        {
+          encoded = (encoded)(int(len / 10));
+        }
+      }
+      else if (i == 289)
+      {
+        encoded = (encoded)(len % 10)
+      }
+      else
+      {
+        encoded = (encoded)(substr(allowed, int(rand() * length(allowed)), 1))
+      }
+    }
+    printf("%s", encoded);
+  }'
+}
+
+# html label
+_zyxel_html_table_lookup() {
+  # Look up a value in the html representing the status page of the switch
+  # when provided with the html of the page and the label (i.e. "Model Name:")
+  html="$1"
+  label=$(printf "%s" "$2" | tr -d ' ')
+  lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's/<tr>/\n<tr>/g' | sed 's/<td[^>]*>/<td>/g' | tr -d ' ' | grep -i "$label" | sed "s/<tr><td>$label<\/td><td>\([^<]\{1,\}\)<\/td><\/tr>/\1/i")
+  printf "%s" "$lookup_result"
+  return 0
+}
+
+# html
+_zyxel_gs1900_get_model() {
+  html="$1"
+  model_name=$(_zyxel_html_table_lookup "$html" "Model Name:")
+  printf "%s" "$model_name"
+}
+
+# html
+_zyxel_gs1900_get_firmware_version() {
+  html="$1"
+  firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+")
+  printf "%s" "$firmware_version"
+}
+
+# version_number
+_zyxel_gs1900_parse_major_version() {
+  printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
+}
+
+# version_number
+_zyxel_gs1900_parse_minor_version() {
+  printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
+}

+ 15 - 20
dnsapi/dns_1984hosting.sh

@@ -1,22 +1,18 @@
 #!/usr/bin/env sh
-# This file name is "dns_1984hosting.sh"
-# So, here must be a method dns_1984hosting_add()
-# Which will be called by acme.sh to add the txt record to your api system.
-# returns 0 means success, otherwise error.
-
-# Author: Adrian Fedoreanu
-# Report Bugs here: https://github.com/acmesh-official/acme.sh
-# or here... https://github.com/acmesh-official/acme.sh/issues/2851
+# shellcheck disable=SC2034
+dns_1984hosting_info='1984.hosting
+Domains: 1984.is
+Site: 1984.hosting
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting
+Options:
+ One984HOSTING_Username Username
+ One984HOSTING_Password Password
+Issues: github.com/acmesh-official/acme.sh/issues/2851
+Author: Adrian Fedoreanu
+'
 
 ######## Public functions #####################
 
-# Export 1984HOSTING username and password in following variables
-#
-#  One984HOSTING_Username=username
-#  One984HOSTING_Password=password
-#
-# username/password and csrftoken/sessionid cookies are saved in ~/.acme.sh/account.conf
-
 # Usage: dns_1984hosting_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 # Add a text record.
 dns_1984hosting_add() {
@@ -132,7 +128,7 @@ _1984hosting_login() {
 
   _get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
   csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
-  sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+  sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
 
   if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
     _err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@@ -149,7 +145,7 @@ _1984hosting_login() {
   _debug2 response "$response"
 
   if _contains "$response" '"loggedin": true'; then
-    One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
+    One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
     One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
     export One984HOSTING_SESSIONID_COOKIE
     export One984HOSTING_CSRFTOKEN_COOKIE
@@ -215,8 +211,8 @@ _get_root() {
       return 1
     fi
 
-    _authget "https://1984.hosting/domains/soacheck/?zone=$h&nameserver=ns0.1984.is."
-    if _contains "$_response" "serial" && ! _contains "$_response" "null"; then
+    _authget "https://1984.hosting/domains/zonestatus/$h/?cached=no"
+    if _contains "$_response" '"ok": true'; then
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain="$h"
       return 0
@@ -250,7 +246,6 @@ _authget() {
 }
 
 # Truncate huge HTML response
-# Echo: Argument list too long
 _htmlget() {
   export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
   _response=$(_get "$1" | grep "$2")

+ 14 - 14
dnsapi/dns_acmedns.sh

@@ -1,18 +1,18 @@
 #!/usr/bin/env sh
-#
-#Author: Wolfgang Ebner
-#Author: Sven Neubuaer
-#Report Bugs here: https://github.com/dampfklon/acme.sh
-#
-# Usage:
-# export ACMEDNS_BASE_URL="https://auth.acme-dns.io"
-#
-# You can optionally define an already existing account:
-#
-# export ACMEDNS_USERNAME="<username>"
-# export ACMEDNS_PASSWORD="<password>"
-# export ACMEDNS_SUBDOMAIN="<subdomain>"
-#
+# shellcheck disable=SC2034
+dns_acmedns_info='acme-dns Server API
+ The acme-dns is a limited DNS server with RESTful API to handle ACME DNS challenges.
+Site: github.com/joohoi/acme-dns
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_acmedns
+Options:
+ ACMEDNS_USERNAME Username. Optional.
+ ACMEDNS_PASSWORD Password. Optional.
+ ACMEDNS_SUBDOMAIN Subdomain. Optional.
+ ACMEDNS_BASE_URL API endpoint. Default: "https://auth.acme-dns.io".
+Issues: github.com/dampfklon/acme.sh
+Author: Wolfgang Ebner, Sven Neubuaer
+'
+
 ########  Public functions #####################
 
 #Usage: dns_acmedns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"

+ 13 - 5
dnsapi/dns_acmeproxy.sh

@@ -1,9 +1,17 @@
 #!/usr/bin/env sh
-
-## Acmeproxy DNS provider to be used with acmeproxy (https://github.com/mdbraber/acmeproxy)
-## API integration by Maarten den Braber
-##
-## Report any bugs via https://github.com/mdbraber/acme.sh
+# shellcheck disable=SC2034
+dns_acmeproxy_info='AcmeProxy Server API
+ AcmeProxy can be used to as a single host in your network to request certificates through a DNS API.
+ Clients can connect with the one AcmeProxy host so you do not need to store DNS API credentials on every single host.
+Site: github.com/mdbraber/acmeproxy
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_acmeproxy
+Options:
+ ACMEPROXY_ENDPOINT API Endpoint
+ ACMEPROXY_USERNAME Username
+ ACMEPROXY_PASSWORD Password
+Issues: github.com/acmesh-official/acme.sh/issues/2251
+Author: Maarten den Braber
+'
 
 dns_acmeproxy_add() {
   fulldomain="${1}"

+ 117 - 52
dnsapi/dns_active24.sh

@@ -1,10 +1,17 @@
 #!/usr/bin/env sh
-
-#ACTIVE24_Token="sdfsdfsdfljlbjkljlkjsdfoiwje"
-
-ACTIVE24_Api="https://api.active24.com"
-
-########  Public functions #####################
+# shellcheck disable=SC2034
+dns_active24_info='Active24.cz
+Site: Active24.cz
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_active24
+Options:
+ Active24_ApiKey API Key. Called "Identifier" in the Active24 Admin
+ Active24_ApiSecret API Secret. Called "Secret key" in the Active24 Admin
+Issues: github.com/acmesh-official/acme.sh/issues/2059
+'
+
+Active24_Api="https://rest.active24.cz"
+# export Active24_ApiKey=ak48l3h7-ak5d-qn4t-p8gc-b6fs8c3l
+# export Active24_ApiSecret=ajvkeo3y82ndsu2smvxy3o36496dcascksldncsq
 
 # Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 # Used to add txt record
@@ -15,8 +22,8 @@ dns_active24_add() {
   _active24_init
 
   _info "Adding txt record"
-  if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then
-    if _contains "$response" "errors"; then
+  if _active24_rest POST "/v2/service/$_service_id/dns/record" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":300}"; then
+    if _contains "$response" "error"; then
       _err "Add txt record error."
       return 1
     else
@@ -24,6 +31,7 @@ dns_active24_add() {
       return 0
     fi
   fi
+
   _err "Add txt record error."
   return 1
 }
@@ -37,19 +45,25 @@ dns_active24_rm() {
   _active24_init
 
   _debug "Getting txt records"
-  _active24_rest GET "dns/$_domain/records/v1"
+  # The API needs to send data in body in order the filter to work
+  # TODO: web can also add content $txtvalue to filter and then get the id from response
+  _active24_rest GET "/v2/service/$_service_id/dns/record" "{\"page\":1,\"descending\":true,\"sortBy\":\"name\",\"rowsPerPage\":100,\"totalRecords\":0,\"filters\":{\"type\":[\"TXT\"],\"name\":\"${_sub_domain}\"}}"
+  #_active24_rest GET "/v2/service/$_service_id/dns/record?rowsPerPage=100"
 
-  if _contains "$response" "errors"; then
+  if _contains "$response" "error"; then
     _err "Error"
     return 1
   fi
 
-  hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-)
+  # Note: it might never be more than one record actually, NEEDS more INVESTIGATION
+  record_ids=$(printf "%s" "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
+  _debug2 record_ids "$record_ids"
 
-  for hash_id in $hash_ids; do
-    _debug "Removing hash_id" "$hash_id"
-    if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then
-      if _contains "$response" "errors"; then
+  for redord_id in $record_ids; do
+    _debug "Removing record_id" "$redord_id"
+    _debug "txtvalue" "$txtvalue"
+    if _active24_rest DELETE "/v2/service/$_service_id/dns/record/$redord_id" ""; then
+      if _contains "$response" "error"; then
         _err "Unable to remove txt record."
         return 1
       else
@@ -63,23 +77,17 @@ dns_active24_rm() {
   return 1
 }
 
-####################  Private functions below ##################################
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
 _get_root() {
   domain=$1
+  i=1
+  p=1
 
-  if ! _active24_rest GET "dns/domains/v1"; then
+  if ! _active24_rest GET "/v1/user/self/service"; then
     return 1
   fi
 
-  i=2
-  p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug "h" "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -87,7 +95,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"$h\"" >/dev/null; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi
@@ -97,45 +105,102 @@ _get_root() {
   return 1
 }
 
-_active24_rest() {
-  m=$1
-  ep="$2"
-  data="$3"
-  _debug "$ep"
-
-  export _H1="Authorization: Bearer $ACTIVE24_Token"
-
-  if [ "$m" != "GET" ]; then
-    _debug "data" "$data"
-    response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")"
-  else
-    response="$(_get "$ACTIVE24_Api/$ep")"
+_active24_init() {
+  Active24_ApiKey="${Active24_ApiKey:-$(_readaccountconf_mutable Active24_ApiKey)}"
+  Active24_ApiSecret="${Active24_ApiSecret:-$(_readaccountconf_mutable Active24_ApiSecret)}"
+  #Active24_ServiceId="${Active24_ServiceId:-$(_readaccountconf_mutable Active24_ServiceId)}"
+
+  if [ -z "$Active24_ApiKey" ] || [ -z "$Active24_ApiSecret" ]; then
+    Active24_ApiKey=""
+    Active24_ApiSecret=""
+    _err "You don't specify Active24 api key and ApiSecret yet."
+    _err "Please create your key and try again."
+    return 1
   fi
 
-  if [ "$?" != "0" ]; then
-    _err "error $ep"
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable Active24_ApiKey "$Active24_ApiKey"
+  _saveaccountconf_mutable Active24_ApiSecret "$Active24_ApiSecret"
+
+  _debug "A24 API CHECK"
+  if ! _active24_rest GET "/v2/check"; then
+    _err "A24 API check failed with: $response"
     return 1
   fi
-  _debug2 response "$response"
-  return 0
-}
 
-_active24_init() {
-  ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}"
-  if [ -z "$ACTIVE24_Token" ]; then
-    ACTIVE24_Token=""
-    _err "You didn't specify a Active24 api token yet."
-    _err "Please create the token and try again."
+  if ! echo "$response" | tr -d " " | grep \"verified\":true >/dev/null; then
+    _err "A24 API check failed with: $response"
     return 1
   fi
 
-  _saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_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"
+  _active24_get_service_id "$_domain"
+  _debug _service_id "$_service_id"
+}
+
+_active24_get_service_id() {
+  _d=$1
+  if ! _active24_rest GET "/v1/user/self/zone/${_d}"; then
+    return 1
+  else
+    response=$(echo "$response" | _json_decode)
+    _service_id=$(echo "$response" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
+  fi
+}
+
+_active24_rest() {
+  m=$1
+  ep_qs=$2 # with query string
+  # ep=$2
+  ep=$(printf "%s" "$ep_qs" | cut -d '?' -f1) # no query string
+  data="$3"
+
+  _debug "A24 $ep"
+  _debug "A24 $Active24_ApiKey"
+  _debug "A24 $Active24_ApiSecret"
+
+  timestamp=$(_time)
+  datez=$(date -u +"%Y%m%dT%H%M%SZ")
+  canonicalRequest="${m} ${ep} ${timestamp}"
+  signature=$(printf "%s" "$canonicalRequest" | _hmac sha1 "$(printf "%s" "$Active24_ApiSecret" | _hex_dump | tr -d " ")" hex)
+  authorization64="$(printf "%s:%s" "$Active24_ApiKey" "$signature" | _base64)"
+
+  export _H1="Date: ${datez}"
+  export _H2="Accept: application/json"
+  export _H3="Content-Type: application/json"
+  export _H4="Authorization: Basic ${authorization64}"
+
+  _debug2 H1 "$_H1"
+  _debug2 H2 "$_H2"
+  _debug2 H3 "$_H3"
+  _debug2 H4 "$_H4"
+
+  # _sleep 1
+
+  if [ "$m" != "GET" ]; then
+    _debug2 "${m} $Active24_Api${ep_qs}"
+    _debug "data" "$data"
+    response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
+  else
+    if [ -z "$data" ]; then
+      _debug2 "GET $Active24_Api${ep_qs}"
+      response="$(_get "$Active24_Api${ep_qs}")"
+    else
+      _debug2 "GET $Active24_Api${ep_qs} with data: ${data}"
+      response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
+    fi
+  fi
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
 }

+ 11 - 10
dnsapi/dns_ad.sh

@@ -1,12 +1,13 @@
 #!/usr/bin/env sh
-
-#
-#AD_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
-
-#This is the Alwaysdata api wrapper for acme.sh
-#
-#Author: Paul Koppen
-#Report Bugs here: https://github.com/wpk-/acme.sh
+# shellcheck disable=SC2034
+dns_ad_info='AlwaysData.com
+Site: AlwaysData.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_ad
+Options:
+ AD_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/pull/503
+Author: Paul Koppen
+'
 
 AD_API_URL="https://$AD_API_KEY:@api.alwaysdata.com/v1"
 
@@ -94,7 +95,7 @@ _get_root() {
   if _ad_rest GET "domain/"; then
     response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')"
     while true; do
-      h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+      h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
       _debug h "$h"
       if [ -z "$h" ]; then
         #not valid
@@ -105,7 +106,7 @@ _get_root() {
       if [ "$hostedzone" ]; then
         _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"id\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
         if [ "$_domain_id" ]; then
-          _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+          _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
           _domain=$h
           return 0
         fi

+ 85 - 68
dnsapi/dns_ali.sh

@@ -1,27 +1,27 @@
 #!/usr/bin/env sh
-
-Ali_API="https://alidns.aliyuncs.com/"
-
-#Ali_Key="LTqIA87hOKdjevsf5"
-#Ali_Secret="0p5EYueFNq501xnCPzKNbx6K51qPH2"
+# shellcheck disable=SC2034
+dns_ali_info='AlibabaCloud.com
+Domains: Aliyun.com
+Site: AlibabaCloud.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_ali
+Options:
+ Ali_Key API Key
+ Ali_Secret API Secret
+'
+
+# 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
@@ -46,14 +46,74 @@ 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 </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"
+}
+
+####################  Private functions below  ####################
 
 _get_root() {
   domain=$1
-  i=2
+  i=1
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       #not valid
       return 1
@@ -65,7 +125,7 @@ _get_root() {
     fi
 
     if _contains "$response" "PageNumber"; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _debug _sub_domain "$_sub_domain"
       _domain="$h"
       _debug _domain "$_domain"
@@ -77,52 +137,10 @@ _get_root() {
   return 1
 }
 
-_ali_rest() {
-  signature=$(printf "%s" "GET&%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 ! response="$(_get "$url")"; then
-    _err "Error <$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 </dev/urandom | _digest "sha256" hex | cut -c 1-31
-  #Not so good...
-  date +"%s%N" | sed 's/%N//g'
-}
-
 _check_exist_query() {
   _qdomain="$1"
   _qsubdomain="$2"
+  endpoint=$Ali_DNS_API
   query=''
   query=$query'AccessKeyId='$Ali_Key
   query=$query'&Action=DescribeDomainRecords'
@@ -138,6 +156,7 @@ _check_exist_query() {
 }
 
 _add_record_query() {
+  endpoint=$Ali_DNS_API
   query=''
   query=$query'AccessKeyId='$Ali_Key
   query=$query'&Action=AddDomainRecord'
@@ -154,6 +173,7 @@ _add_record_query() {
 }
 
 _delete_record_query() {
+  endpoint=$Ali_DNS_API
   query=''
   query=$query'AccessKeyId='$Ali_Key
   query=$query'&Action=DeleteDomainRecord'
@@ -167,6 +187,7 @@ _delete_record_query() {
 }
 
 _describe_records_query() {
+  endpoint=$Ali_DNS_API
   query=''
   query=$query'AccessKeyId='$Ali_Key
   query=$query'&Action=DescribeDomainRecords'
@@ -197,7 +218,3 @@ _clean() {
   fi
 
 }
-
-_timestamp() {
-  date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
-}

+ 185 - 0
dnsapi/dns_alviy.sh

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_alviy_info='Alviy.com
+Site: Alviy.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_alviy
+Options:
+ Alviy_token API token. Get it from the https://cloud.alviy.com/token
+Issues: github.com/acmesh-official/acme.sh/issues/5115
+'
+
+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
+}

+ 11 - 9
dnsapi/dns_anx.sh

@@ -1,9 +1,12 @@
 #!/usr/bin/env sh
-
-# Anexia CloudDNS acme.sh hook
-# Author: MA
-
-#ANX_Token="xxxx"
+# shellcheck disable=SC2034
+dns_anx_info='Anexia.com CloudDNS
+Site: Anexia.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_anx
+Options:
+ ANX_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/3238
+'
 
 ANX_API='https://engine.anexia-it.com/api/clouddns/v1'
 
@@ -127,18 +130,17 @@ _get_root() {
   i=1
   p=1
 
-  _anx_rest GET "zone.json"
-
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
       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)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi

+ 10 - 13
dnsapi/dns_artfiles.sh

@@ -1,17 +1,14 @@
 #!/usr/bin/env sh
-
-################################################################################
-# ACME.sh 3rd party DNS API plugin for ArtFiles.de
-################################################################################
-# Author:   Martin Arndt, https://troublezone.net/
-# Released: 2022-02-27
-# Issues:   https://github.com/acmesh-official/acme.sh/issues/4718
-################################################################################
-# Usage:
-# 1. export AF_API_USERNAME='api12345678'
-# 2. export AF_API_PASSWORD='apiPassword'
-# 3. acme.sh --issue -d example.com --dns dns_artfiles
-################################################################################
+# shellcheck disable=SC2034
+dns_artfiles_info='ArtFiles.de
+Site: ArtFiles.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_artfiles
+Options:
+ AF_API_USERNAME API Username
+ AF_API_PASSWORD API Password
+Issues: github.com/acmesh-official/acme.sh/issues/4718
+Author: Martin Arndt <https://troublezone.net/>
+'
 
 ########## API configuration ###################################################
 

+ 12 - 7
dnsapi/dns_arvan.sh

@@ -1,11 +1,16 @@
 #!/usr/bin/env sh
-
-# Arvan_Token="Apikey xxxx"
+# shellcheck disable=SC2034
+dns_arvan_info='ArvanCloud.ir
+Site: ArvanCloud.ir
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_arvan
+Options:
+ Arvan_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/2796
+Author: Vahid Fardi
+'
 
 ARVAN_API_URL="https://napi.arvancloud.ir/cdn/4.0/domains"
-# Author: Vahid Fardi
-# Report Bugs here: https://github.com/Neilpang/acme.sh
-#
+
 ########  Public functions #####################
 
 #Usage: dns_arvan_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -102,7 +107,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -115,7 +120,7 @@ _get_root() {
     if _contains "$response" "\"domain\":\"$h\""; then
       _domain_id=$(echo "$response" | cut -d : -f 3 | cut -d , -f 1 | tr -d \")
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 13 - 7
dnsapi/dns_aurora.sh

@@ -1,9 +1,15 @@
 #!/usr/bin/env sh
-
-#
-#AURORA_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
-#
-#AURORA_Secret="sdfsdfsdfljlbjkljlkjsdfoiwje"
+# shellcheck disable=SC2034
+dns_aurora_info='versio.nl AuroraDNS
+Domains: pcextreme.nl
+Site: versio.nl
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_aurora
+Options:
+ AURORA_Key API Key
+ AURORA_Secret API Secret
+Issues: github.com/acmesh-official/acme.sh/issues/3459
+Author: Jasper Zonneveld
+'
 
 AURORA_Api="https://api.auroradns.eu"
 
@@ -111,7 +117,7 @@ _get_root() {
   p=1
 
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -126,7 +132,7 @@ _get_root() {
       _domain_id=$(echo "$response" | _normalizeJson | tr -d "{}" | tr "," "\n" | grep "\"id\": *\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
       _debug _domain_id "$_domain_id"
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 13 - 14
dnsapi/dns_autodns.sh

@@ -1,16 +1,15 @@
 #!/usr/bin/env sh
-# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
-
-# This is the InternetX autoDNS xml api wrapper for acme.sh
-# Author: [email protected]
-# Created: 2018-01-14
-#
-#     export AUTODNS_USER="username"
-#     export AUTODNS_PASSWORD="password"
-#     export AUTODNS_CONTEXT="context"
-#
-# Usage:
-#     acme.sh --issue --dns dns_autodns -d example.com
+# shellcheck disable=SC2034
+dns_autodns_info='InternetX autoDNS
+ InternetX autoDNS XML API
+Site: InternetX.com/autodns/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_autodns
+Options:
+ AUTODNS_USER Username
+ AUTODNS_PASSWORD Password
+ AUTODNS_CONTEXT Context
+Author: <[email protected]>
+'
 
 AUTODNS_API="https://gateway.autodns.com"
 
@@ -111,7 +110,7 @@ _get_autodns_zone() {
   p=1
 
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
 
     if [ -z "$h" ]; then
@@ -129,7 +128,7 @@ _get_autodns_zone() {
     if _contains "$autodns_response" "<summary>1</summary>" >/dev/null; then
       _zone="$(echo "$autodns_response" | _egrep_o '<name>[^<]*</name>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
       _system_ns="$(echo "$autodns_response" | _egrep_o '<system_ns>[^<]*</system_ns>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       return 0
     fi
 

+ 36 - 19
dnsapi/dns_aws.sh

@@ -1,13 +1,15 @@
 #!/usr/bin/env sh
-
-#
-#AWS_ACCESS_KEY_ID="sdfsdfsdfljlbjkljlkjsdfoiwje"
-#
-#AWS_SECRET_ACCESS_KEY="xxxxxxx"
-
-#This is the Amazon Route53 api wrapper for acme.sh
-#All `_sleep` commands are included to avoid Route53 throttling, see
-#https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
+# shellcheck disable=SC2034
+dns_aws_info='Amazon AWS Route53 domain API
+Site: docs.aws.amazon.com/route53/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_aws
+Options:
+ AWS_ACCESS_KEY_ID API Key ID
+ AWS_SECRET_ACCESS_KEY API Secret
+'
+
+# All `_sleep` commands are included to avoid Route53 throttling, see
+# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
 
 AWS_HOST="route53.amazonaws.com"
 AWS_URL="https://$AWS_HOST"
@@ -145,7 +147,6 @@ dns_aws_rm() {
   fi
   _sleep 1
   return 1
-
 }
 
 ####################  Private functions below ##################################
@@ -157,7 +158,7 @@ _get_root() {
 
   # iterate over names (a.b.c.d -> b.c.d -> c.d -> d)
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100 | sed 's/\./\\./g')
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g')
     _debug "Checking domain: $h"
     if [ -z "$h" ]; then
       _error "invalid domain"
@@ -173,7 +174,7 @@ _get_root() {
         if [ "$hostedzone" ]; then
           _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "<Id>.*<.Id>" | head -n 1 | _egrep_o ">.*<" | tr -d "<>")
           if [ "$_domain_id" ]; then
-            _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+            _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
             _domain=$h
             return 0
           fi
@@ -207,24 +208,40 @@ _use_container_role() {
 }
 
 _use_instance_role() {
-  _url="http://169.254.169.254/latest/meta-data/iam/security-credentials/"
-  _debug "_url" "$_url"
-  if ! _get "$_url" true 1 | _head_n 1 | grep -Fq 200; then
+  _instance_role_name_url="http://169.254.169.254/latest/meta-data/iam/security-credentials/"
+
+  if _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 401; then
+    _debug "Using IMDSv2"
+    _token_url="http://169.254.169.254/latest/api/token"
+    export _H1="X-aws-ec2-metadata-token-ttl-seconds: 21600"
+    _token="$(_post "" "$_token_url" "" "PUT")"
+    _secure_debug3 "_token" "$_token"
+    if [ -z "$_token" ]; then
+      _debug "Unable to fetch IMDSv2 token from instance metadata"
+      return 1
+    fi
+    export _H1="X-aws-ec2-metadata-token: $_token"
+  fi
+
+  if ! _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 200; then
     _debug "Unable to fetch IAM role from instance metadata"
     return 1
   fi
-  _aws_role=$(_get "$_url" "" 1)
-  _debug "_aws_role" "$_aws_role"
-  _use_metadata "$_url$_aws_role"
+
+  _instance_role_name=$(_get "$_instance_role_name_url" "" 1)
+  _debug "_instance_role_name" "$_instance_role_name"
+  _use_metadata "$_instance_role_name_url$_instance_role_name" "$_token"
+
 }
 
 _use_metadata() {
+  export _H1="X-aws-ec2-metadata-token: $2"
   _aws_creds="$(
     _get "$1" "" 1 |
       _normalizeJson |
       tr '{,}' '\n' |
       while read -r _line; do
-        _key="$(echo "${_line%%:*}" | tr -d '"')"
+        _key="$(echo "${_line%%:*}" | tr -d '\"')"
         _value="${_line#*:}"
         _debug3 "_key" "$_key"
         _secure_debug3 "_value" "$_value"

+ 11 - 7
dnsapi/dns_azion.sh

@@ -1,9 +1,13 @@
 #!/usr/bin/env sh
-
-#
-#AZION_Email=""
-#AZION_Password=""
-#
+# shellcheck disable=SC2034
+dns_azion_info='Azion.om
+Site: Azion.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_azion
+Options:
+ AZION_Email Email
+ AZION_Password Password
+Issues: github.com/acmesh-official/acme.sh/issues/3555
+'
 
 AZION_Api="https://api.azionapi.net"
 
@@ -96,7 +100,7 @@ _get_root() {
   fi
 
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       # not valid
@@ -107,7 +111,7 @@ _get_root() {
       _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"domain\":\"$h\"" | _egrep_o "\"id\":[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d \")
       _debug _domain_id "$_domain_id"
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 121 - 71
dnsapi/dns_azure.sh

@@ -1,13 +1,25 @@
 #!/usr/bin/env sh
-
-WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS"
+# shellcheck disable=SC2034
+dns_azure_info='Azure
+Site: Azure.microsoft.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_azure
+Options:
+ AZUREDNS_SUBSCRIPTIONID Subscription ID
+ AZUREDNS_TENANTID Tenant ID
+ AZUREDNS_APPID App ID. App ID of the service principal
+ AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
+ AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
+ AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
+'
+
+wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
 
 ########  Public functions #####################
 
 # Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 # Used to add txt record
 #
-# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/createorupdate
+# Ref: https://learn.microsoft.com/en-us/rest/api/dns/record-sets/create-or-update?view=rest-dns-2018-05-01&tabs=HTTP
 #
 
 dns_azure_add() {
@@ -20,6 +32,7 @@ dns_azure_add() {
     AZUREDNS_TENANTID=""
     AZUREDNS_APPID=""
     AZUREDNS_CLIENTSECRET=""
+    AZUREDNS_BEARERTOKEN=""
     _err "You didn't specify the Azure Subscription ID"
     return 1
   fi
@@ -34,37 +47,45 @@ dns_azure_add() {
     _saveaccountconf_mutable AZUREDNS_TENANTID ""
     _saveaccountconf_mutable AZUREDNS_APPID ""
     _saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
+    _saveaccountconf_mutable AZUREDNS_BEARERTOKEN ""
   else
-    _info "You didn't ask to use Azure managed identity, checking service principal credentials"
+    _info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
     AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
     AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
     AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
+    AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
+    if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
+      if [ -z "$AZUREDNS_TENANTID" ]; then
+        AZUREDNS_SUBSCRIPTIONID=""
+        AZUREDNS_TENANTID=""
+        AZUREDNS_APPID=""
+        AZUREDNS_CLIENTSECRET=""
+        AZUREDNS_BEARERTOKEN=""
+        _err "You didn't specify the Azure Tenant ID "
+        return 1
+      fi
 
-    if [ -z "$AZUREDNS_TENANTID" ]; then
-      AZUREDNS_SUBSCRIPTIONID=""
-      AZUREDNS_TENANTID=""
-      AZUREDNS_APPID=""
-      AZUREDNS_CLIENTSECRET=""
-      _err "You didn't specify the Azure Tenant ID "
-      return 1
-    fi
-
-    if [ -z "$AZUREDNS_APPID" ]; then
-      AZUREDNS_SUBSCRIPTIONID=""
-      AZUREDNS_TENANTID=""
-      AZUREDNS_APPID=""
-      AZUREDNS_CLIENTSECRET=""
-      _err "You didn't specify the Azure App ID"
-      return 1
-    fi
+      if [ -z "$AZUREDNS_APPID" ]; then
+        AZUREDNS_SUBSCRIPTIONID=""
+        AZUREDNS_TENANTID=""
+        AZUREDNS_APPID=""
+        AZUREDNS_CLIENTSECRET=""
+        AZUREDNS_BEARERTOKEN=""
+        _err "You didn't specify the Azure App ID"
+        return 1
+      fi
 
-    if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
-      AZUREDNS_SUBSCRIPTIONID=""
-      AZUREDNS_TENANTID=""
-      AZUREDNS_APPID=""
-      AZUREDNS_CLIENTSECRET=""
-      _err "You didn't specify the Azure Client Secret"
-      return 1
+      if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
+        AZUREDNS_SUBSCRIPTIONID=""
+        AZUREDNS_TENANTID=""
+        AZUREDNS_APPID=""
+        AZUREDNS_CLIENTSECRET=""
+        AZUREDNS_BEARERTOKEN=""
+        _err "You didn't specify the Azure Client Secret"
+        return 1
+      fi
+    else
+      _info "Using provided bearer token"
     fi
 
     #save account details to account conf file, don't opt in for azure manages identity check.
@@ -72,9 +93,14 @@ dns_azure_add() {
     _saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
     _saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
     _saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
+    _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN"
   fi
 
-  accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+  if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
+    accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+  else
+    accesstoken=$(echo "$AZUREDNS_BEARERTOKEN" | sed "s/Bearer //g")
+  fi
 
   if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
     _err "invalid domain"
@@ -124,7 +150,7 @@ dns_azure_add() {
 # Usage: fulldomain txtvalue
 # Used to remove the txt record after validation
 #
-# Ref: https://docs.microsoft.com/en-us/rest/api/dns/recordsets/delete
+# Ref: https://learn.microsoft.com/en-us/rest/api/dns/record-sets/delete?view=rest-dns-2018-05-01&tabs=HTTP
 #
 dns_azure_rm() {
   fulldomain=$1
@@ -136,6 +162,7 @@ dns_azure_rm() {
     AZUREDNS_TENANTID=""
     AZUREDNS_APPID=""
     AZUREDNS_CLIENTSECRET=""
+    AZUREDNS_BEARERTOKEN=""
     _err "You didn't specify the Azure Subscription ID "
     return 1
   fi
@@ -144,40 +171,51 @@ dns_azure_rm() {
   if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
     _info "Using Azure managed identity"
   else
-    _info "You didn't ask to use Azure managed identity, checking service principal credentials"
+    _info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
     AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
     AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
     AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
+    AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
+    if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
+      if [ -z "$AZUREDNS_TENANTID" ]; then
+        AZUREDNS_SUBSCRIPTIONID=""
+        AZUREDNS_TENANTID=""
+        AZUREDNS_APPID=""
+        AZUREDNS_CLIENTSECRET=""
+        AZUREDNS_BEARERTOKEN=""
+        _err "You didn't specify the Azure Tenant ID "
+        return 1
+      fi
 
-    if [ -z "$AZUREDNS_TENANTID" ]; then
-      AZUREDNS_SUBSCRIPTIONID=""
-      AZUREDNS_TENANTID=""
-      AZUREDNS_APPID=""
-      AZUREDNS_CLIENTSECRET=""
-      _err "You didn't specify the Azure Tenant ID "
-      return 1
-    fi
-
-    if [ -z "$AZUREDNS_APPID" ]; then
-      AZUREDNS_SUBSCRIPTIONID=""
-      AZUREDNS_TENANTID=""
-      AZUREDNS_APPID=""
-      AZUREDNS_CLIENTSECRET=""
-      _err "You didn't specify the Azure App ID"
-      return 1
-    fi
+      if [ -z "$AZUREDNS_APPID" ]; then
+        AZUREDNS_SUBSCRIPTIONID=""
+        AZUREDNS_TENANTID=""
+        AZUREDNS_APPID=""
+        AZUREDNS_CLIENTSECRET=""
+        AZUREDNS_BEARERTOKEN=""
+        _err "You didn't specify the Azure App ID"
+        return 1
+      fi
 
-    if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
-      AZUREDNS_SUBSCRIPTIONID=""
-      AZUREDNS_TENANTID=""
-      AZUREDNS_APPID=""
-      AZUREDNS_CLIENTSECRET=""
-      _err "You didn't specify the Azure Client Secret"
-      return 1
+      if [ -z "$AZUREDNS_CLIENTSECRET" ]; then
+        AZUREDNS_SUBSCRIPTIONID=""
+        AZUREDNS_TENANTID=""
+        AZUREDNS_APPID=""
+        AZUREDNS_CLIENTSECRET=""
+        AZUREDNS_BEARERTOKEN=""
+        _err "You didn't specify the Azure Client Secret"
+        return 1
+      fi
+    else
+      _info "Using provided bearer token"
     fi
   fi
 
-  accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+  if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
+    accesstoken=$(_azure_getaccess_token "$AZUREDNS_MANAGEDIDENTITY" "$AZUREDNS_TENANTID" "$AZUREDNS_APPID" "$AZUREDNS_CLIENTSECRET")
+  else
+    accesstoken=$(echo "$AZUREDNS_BEARERTOKEN" | sed "s/Bearer //g")
+  fi
 
   if ! _get_root "$fulldomain" "$AZUREDNS_SUBSCRIPTIONID" "$accesstoken"; then
     _err "invalid domain"
@@ -256,10 +294,10 @@ _azure_rest() {
     if [ "$_code" = "401" ]; then
       # we have an invalid access token set to expired
       _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "0"
-      _err "access denied make sure your Azure settings are correct. See $WIKI"
+      _err "Access denied. Invalid access token. Make sure your Azure settings are correct. See: $wiki"
       return 1
     fi
-    # See https://docs.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes
+    # See https://learn.microsoft.com/en-us/azure/architecture/best-practices/retry-service-specific#general-rest-and-retry-guidelines for retryable HTTP codes
     if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "408" ] || [ "$_code" = "500" ] || [ "$_code" = "503" ] || [ "$_code" = "504" ]; then
       _request_retry_times="$(_math "$_request_retry_times" + 1)"
       _info "REST call error $_code retrying $ep in $_request_retry_times s"
@@ -277,14 +315,14 @@ _azure_rest() {
   return 0
 }
 
-## Ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-service-to-service#request-an-access-token
+## Ref: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#request-an-access-token
 _azure_getaccess_token() {
   managedIdentity=$1
   tenantID=$2
   clientID=$3
   clientSecret=$4
 
-  accesstoken="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
+  accesstoken="${AZUREDNS_ACCESSTOKEN:-$(_readaccountconf_mutable AZUREDNS_ACCESSTOKEN)}"
   expires_on="${AZUREDNS_TOKENVALIDTO:-$(_readaccountconf_mutable AZUREDNS_TOKENVALIDTO)}"
 
   # can we reuse the bearer token?
@@ -301,9 +339,18 @@ _azure_getaccess_token() {
   _debug "getting new bearer token"
 
   if [ "$managedIdentity" = true ]; then
-    # https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
-    export _H1="Metadata: true"
-    response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)"
+    # https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
+    if [ -n "$IDENTITY_ENDPOINT" ]; then
+      # Some Azure environments may set IDENTITY_ENDPOINT (formerly MSI_ENDPOINT) to have an alternative metadata endpoint
+      url="$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/"
+      headers="X-IDENTITY-HEADER: $IDENTITY_HEADER"
+    else
+      url="http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
+      headers="Metadata: true"
+    fi
+
+    export _H1="$headers"
+    response="$(_get "$url")"
     response="$(echo "$response" | _normalizeJson)"
     accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
     expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
@@ -321,14 +368,14 @@ _azure_getaccess_token() {
   fi
 
   if [ -z "$accesstoken" ]; then
-    _err "no acccess token received. Check your Azure settings see $WIKI"
+    _err "No acccess token received. Check your Azure settings. See: $wiki"
     return 1
   fi
   if [ "$_ret" != "0" ]; then
     _err "error $response"
     return 1
   fi
-  _saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$accesstoken"
+  _saveaccountconf_mutable AZUREDNS_ACCESSTOKEN "$accesstoken"
   _saveaccountconf_mutable AZUREDNS_TOKENVALIDTO "$expires_on"
   printf "%s" "$accesstoken"
   return 0
@@ -341,15 +388,18 @@ _get_root() {
   i=1
   p=1
 
-  ## Ref: https://docs.microsoft.com/en-us/rest/api/dns/zones/list
-  ## returns up to 100 zones in one response therefore handling more results is not not implemented
-  ## (ZoneListResult with  continuation token for the next page of results)
-  ## Per https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits#dns-limits you are limited to 100 Zone/subscriptions anyways
+  ## Ref: https://learn.microsoft.com/en-us/rest/api/dns/zones/list?view=rest-dns-2018-05-01&tabs=HTTP
+  ## returns up to 100 zones in one response. Handling more results is not implemented
+  ## (ZoneListResult with continuation token for the next page of results)
+  ##
+  ## TODO: handle more than 100 results, as per:
+  ## https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-dns-limits
+  ## The new limit is 250 Public DNS zones per subscription, while the old limit was only 100
   ##
   _azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken"
   # Find matching domain name in Json response
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug2 "Checking domain: $h"
     if [ -z "$h" ]; then
       #not valid
@@ -364,7 +414,7 @@ _get_root() {
           #create the record at the domain apex (@) if only the domain name was provided as --domain-alias
           _sub_domain="@"
         else
-          _sub_domain=$(echo "$domain" | cut -d . -f 1-$p)
+          _sub_domain=$(echo "$domain" | cut -d . -f 1-"$p")
         fi
         _domain=$h
         return 0

+ 281 - 0
dnsapi/dns_beget.sh

@@ -0,0 +1,281 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_beget_info='Beget.com
+Site: Beget.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_beget
+Options:
+ BEGET_User API user
+ BEGET_Password API password
+Issues: github.com/acmesh-official/acme.sh/issues/6200
+Author: ARNik <[email protected]>
+'
+
+Beget_Api="https://api.beget.com/api"
+
+####################  Public functions ####################
+
+# Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_beget_add() {
+  fulldomain=$1
+  txtvalue=$2
+  _debug "dns_beget_add() $fulldomain $txtvalue"
+  fulldomain=$(echo "$fulldomain" | _lower_case)
+
+  Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
+  Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
+
+  if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then
+    Beget_Username=""
+    Beget_Password=""
+    _err "You must export variables: Beget_Username, and Beget_Password"
+    return 1
+  fi
+
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable Beget_Username "$Beget_Username"
+  _saveaccountconf_mutable Beget_Password "$Beget_Password"
+
+  _info "Prepare subdomain."
+  if ! _prepare_subdomain "$fulldomain"; then
+    _err "Can't prepare subdomain."
+    return 1
+  fi
+
+  _info "Get domain records"
+  data="{\"fqdn\":\"$fulldomain\"}"
+  res=$(_api_call "$Beget_Api/dns/getData" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't get domain records."
+    return 1
+  fi
+
+  _info "Add new TXT record"
+  data="{\"fqdn\":\"$fulldomain\",\"records\":{"
+  data=${data}$(_parce_records "$res" "A")
+  data=${data}$(_parce_records "$res" "AAAA")
+  data=${data}$(_parce_records "$res" "CAA")
+  data=${data}$(_parce_records "$res" "MX")
+  data=${data}$(_parce_records "$res" "SRV")
+  data=${data}$(_parce_records "$res" "TXT")
+  data=$(echo "$data" | sed 's/,$//')
+  data=${data}'}}'
+
+  str=$(_txt_to_dns_json "$txtvalue")
+  data=$(_add_record "$data" "TXT" "$str")
+
+  res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't change domain records."
+    return 1
+  fi
+
+  return 0
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_beget_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  _debug "dns_beget_rm() $fulldomain $txtvalue"
+  fulldomain=$(echo "$fulldomain" | _lower_case)
+
+  Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
+  Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
+
+  _info "Get current domain records"
+  data="{\"fqdn\":\"$fulldomain\"}"
+  res=$(_api_call "$Beget_Api/dns/getData" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't get domain records."
+    return 1
+  fi
+
+  _info "Remove TXT record"
+  data="{\"fqdn\":\"$fulldomain\",\"records\":{"
+  data=${data}$(_parce_records "$res" "A")
+  data=${data}$(_parce_records "$res" "AAAA")
+  data=${data}$(_parce_records "$res" "CAA")
+  data=${data}$(_parce_records "$res" "MX")
+  data=${data}$(_parce_records "$res" "SRV")
+  data=${data}$(_parce_records "$res" "TXT")
+  data=$(echo "$data" | sed 's/,$//')
+  data=${data}'}}'
+
+  str=$(_txt_to_dns_json "$txtvalue")
+  data=$(_rm_record "$data" "$str")
+
+  res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't change domain records."
+    return 1
+  fi
+
+  return 0
+}
+
+####################  Private functions below ####################
+
+# Create subdomain if needed
+# Usage: _prepare_subdomain [fulldomain]
+_prepare_subdomain() {
+  fulldomain=$1
+
+  _info "Detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _domain_id "$_domain_id"
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  if [ -z "$_sub_domain" ]; then
+    _debug "$fulldomain is a root domain."
+    return 0
+  fi
+
+  _info "Get subdomain list"
+  res=$(_api_call "$Beget_Api/domain/getSubdomainList")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't get subdomain list."
+    return 1
+  fi
+
+  if _contains "$res" "\"fqdn\":\"$fulldomain\""; then
+    _debug "Subdomain $fulldomain already exist."
+    return 0
+  fi
+
+  _info "Subdomain $fulldomain does not exist. Let's create one."
+  data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}"
+  res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't create subdomain."
+    return 1
+  fi
+
+  _debug "Cleanup subdomen records"
+  data="{\"fqdn\":\"$fulldomain\",\"records\":{}}"
+  res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _debug "Can't cleanup $fulldomain records."
+  fi
+
+  data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}"
+  res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
+  if ! _is_api_reply_ok "$res"; then
+    _debug "Can't cleanup www.$fulldomain records."
+  fi
+
+  return 0
+}
+
+# Usage: _get_root _acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=32436365
+_get_root() {
+  fulldomain=$1
+  i=1
+  p=1
+
+  _debug "Get domain list"
+  res=$(_api_call "$Beget_Api/domain/getList")
+  if ! _is_api_reply_ok "$res"; then
+    _err "Can't get domain list."
+    return 1
+  fi
+
+  while true; do
+    h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
+    _debug h "$h"
+
+    if [ -z "$h" ]; then
+      return 1
+    fi
+
+    if _contains "$res" "$h"; then
+      _domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2)
+      if [ "$_domain_id" ]; then
+        if [ "$h" != "$fulldomain" ]; then
+          _sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p")
+        else
+          _sub_domain=""
+        fi
+        _domain=$h
+        return 0
+      fi
+      return 1
+    fi
+    p="$i"
+    i=$(_math "$i" + 1)
+  done
+  return 1
+}
+
+# Parce DNS records from json string
+# Usage: _parce_records [j_str] [record_name]
+_parce_records() {
+  j_str=$1
+  record_name=$2
+  res="\"$record_name\":["
+  res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1)
+  res=${res}"],"
+  echo "$res"
+}
+
+# Usage: _add_record [data] [record_name] [record_data]
+_add_record() {
+  data=$1
+  record_name=$2
+  record_data=$3
+  echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/"
+}
+
+# Usage: _rm_record [data] [record_data]
+_rm_record() {
+  data=$1
+  record_data=$2
+  echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" |
+    sed "s/{,/{/g" | sed "s/,}/}/g" |
+    sed "s/\[,/\[/g" | sed "s/,\]/\]/g"
+}
+
+_txt_to_dns_json() {
+  echo "{\"ttl\":600,\"txtdata\":\"$1\"}"
+}
+
+# Usage: _api_call [api_url] [input_data]
+_api_call() {
+  api_url="$1"
+  input_data="$2"
+
+  _debug "_api_call $api_url"
+  _debug "Request: $input_data"
+
+  # res=$(curl -s -L -D ./http.header \
+  # "$api_url" \
+  # --data-urlencode login=$Beget_Username \
+  # --data-urlencode passwd=$Beget_Password \
+  # --data-urlencode input_format=json \
+  # --data-urlencode output_format=json \
+  # --data-urlencode "input_data=$input_data")
+
+  url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json"
+  if [ -n "$input_data" ]; then
+    url=${url}"&input_data="
+    url=${url}$(echo "$input_data" | _url_encode)
+  fi
+  res=$(_get "$url")
+
+  _debug "Reply: $res"
+  echo "$res"
+}
+
+# Usage: _is_api_reply_ok [api_reply]
+_is_api_reply_ok() {
+  _contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$'
+}

+ 10 - 11
dnsapi/dns_bookmyname.sh

@@ -1,18 +1,17 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_bookmyname_info='BookMyName.com
+Site: BookMyName.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bookmyname
+Options:
+ BOOKMYNAME_USERNAME Username
+ BOOKMYNAME_PASSWORD Password
+Issues: github.com/acmesh-official/acme.sh/issues/3209
+Author: @Neilpang
+'
 
-#Here is a sample custom api script.
-#This file name is "dns_bookmyname.sh"
-#So, here must be a method   dns_bookmyname_add()
-#Which will be called by acme.sh to add the txt record to your api system.
-#returns 0 means success, otherwise error.
-#
-#Author: Neilpang
-#Report Bugs here: https://github.com/acmesh-official/acme.sh
-#
 ########  Public functions #####################
 
-# Please Read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
-
 # BookMyName urls:
 # https://BOOKMYNAME_USERNAME:[email protected]/dyndns/?hostname=_acme-challenge.domain.tld&type=txt&ttl=300&do=add&value="XXXXXXXX"'
 # https://BOOKMYNAME_USERNAME:[email protected]/dyndns/?hostname=_acme-challenge.domain.tld&type=txt&ttl=300&do=remove&value="XXXXXXXX"'

+ 13 - 16
dnsapi/dns_bunny.sh

@@ -1,16 +1,13 @@
 #!/usr/bin/env sh
-
-## Will be called by acme.sh to add the TXT record via the Bunny DNS API.
-## returns 0 means success, otherwise error.
-
-## Author: nosilver4u <nosilver4u at ewww.io>
-## GitHub: https://github.com/nosilver4u/acme.sh
-
-##
-## Environment Variables Required:
-##
-## BUNNY_API_KEY="75310dc4-ca77-9ac3-9a19-f6355db573b49ce92ae1-2655-3ebd-61ac-3a3ae34834cc"
-##
+# shellcheck disable=SC2034
+dns_bunny_info='Bunny.net
+Site: Bunny.net/dns/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bunny
+Options:
+ BUNNY_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/issues/4296
+Author: <[email protected]>
+'
 
 #####################  Public functions  #####################
 
@@ -199,7 +196,7 @@ _get_base_domain() {
     _debug2 domain_list "$domain_list"
 
     i=1
-    while [ $i -gt 0 ]; do
+    while [ "$i" -gt 0 ]; do
       ## get next longest domain
       _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
       ## check we got something back from our cut (or are we at the end)
@@ -211,7 +208,7 @@ _get_base_domain() {
       ## check if it exists
       if [ -n "$found" ]; then
         ## exists - exit loop returning the parts
-        sub_point=$(_math $i - 1)
+        sub_point=$(_math "$i" - 1)
         _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
         _domain_id="$(echo "$found" | _egrep_o "Id\"\s*\:\s*\"*[0-9]+" | _egrep_o "[0-9]+")"
         _debug _domain_id "$_domain_id"
@@ -221,11 +218,11 @@ _get_base_domain() {
         return 0
       fi
       ## increment cut point $i
-      i=$(_math $i + 1)
+      i=$(_math "$i" + 1)
     done
 
     if [ -z "$found" ]; then
-      page=$(_math $page + 1)
+      page=$(_math "$page" + 1)
       nextpage="https://api.bunny.net/dnszone?page=$page"
       ## Find the next page if we don't have a match.
       hasnextpage="$(echo "$domain_list" | _egrep_o "\"HasMoreItems\"\s*:\s*true")"

+ 14 - 11
dnsapi/dns_cf.sh

@@ -1,13 +1,16 @@
 #!/usr/bin/env sh
-
-#
-#CF_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
-#
-#CF_Email="[email protected]"
-
-#CF_Token="xxxx"
-#CF_Account_ID="xxxx"
-#CF_Zone_ID="xxxx"
+# shellcheck disable=SC2034
+dns_cf_info='CloudFlare
+Site: CloudFlare.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cf
+Options:
+ CF_Key API Key
+ CF_Email Your account email
+OptionsAlt:
+ CF_Token API Token
+ CF_Account_ID Account ID
+ CF_Zone_ID Zone ID. Optional.
+'
 
 CF_Api="https://api.cloudflare.com/client/v4"
 
@@ -183,7 +186,7 @@ _get_root() {
   fi
 
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -203,7 +206,7 @@ _get_root() {
     if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_count":1'; then
       _domain_id=$(echo "$response" | _egrep_o "\[.\"id\": *\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ")
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 11 - 6
dnsapi/dns_clouddns.sh

@@ -1,10 +1,15 @@
 #!/usr/bin/env sh
-
-# Author: Radek Sprta <[email protected]>
-
-#CLOUDDNS_EMAIL=XXXXX
-#CLOUDDNS_PASSWORD="YYYYYYYYY"
-#CLOUDDNS_CLIENT_ID=XXXXX
+# shellcheck disable=SC2034
+dns_clouddns_info='vshosting.cz CloudDNS
+Site: github.com/vshosting/clouddns
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_clouddns
+Options:
+ CLOUDDNS_EMAIL Email
+ CLOUDDNS_PASSWORD Password
+ CLOUDDNS_CLIENT_ID Client ID
+Issues: github.com/acmesh-official/acme.sh/issues/2699
+Author: Radek Sprta <[email protected]>
+'
 
 CLOUDDNS_API='https://admin.vshosting.cloud/clouddns'
 CLOUDDNS_LOGIN_API='https://admin.vshosting.cloud/api/public/auth/login'

+ 14 - 10
dnsapi/dns_cloudns.sh

@@ -1,12 +1,15 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_cloudns_info='ClouDNS.net
+Site: ClouDNS.net
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cloudns
+Options:
+ CLOUDNS_AUTH_ID Regular auth ID
+ CLOUDNS_SUB_AUTH_ID Sub auth ID
+ CLOUDNS_AUTH_PASSWORD Auth Password
+Author: Boyan Peychev <[email protected]>
+'
 
-# Author: Boyan Peychev <boyan at cloudns dot net>
-# Repository: https://github.com/ClouDNS/acme.sh/
-# Editor: I Komang Suryadana
-
-#CLOUDNS_AUTH_ID=XXXXX
-#CLOUDNS_SUB_AUTH_ID=XXXXX
-#CLOUDNS_AUTH_PASSWORD="YYYYYYYYY"
 CLOUDNS_API="https://api.cloudns.net"
 DOMAIN_TYPE=
 DOMAIN_MASTER=
@@ -161,7 +164,7 @@ _dns_cloudns_get_zone_info() {
 _dns_cloudns_get_zone_name() {
   i=2
   while true; do
-    zoneForCheck=$(printf "%s" "$1" | cut -d . -f $i-100)
+    zoneForCheck=$(printf "%s" "$1" | cut -d . -f "$i"-100)
 
     if [ -z "$zoneForCheck" ]; then
       return 1
@@ -194,10 +197,11 @@ _dns_cloudns_http_api_call() {
     auth_user="auth-id=$CLOUDNS_AUTH_ID"
   fi
 
+  encoded_password=$(echo "$CLOUDNS_AUTH_PASSWORD" | tr -d "\n\r" | _url_encode)
   if [ -z "$2" ]; then
-    data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD"
+    data="$auth_user&auth-password=$encoded_password"
   else
-    data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
+    data="$auth_user&auth-password=$encoded_password&$2"
   fi
 
   response="$(_get "$CLOUDNS_API/$method?$data")"

+ 12 - 5
dnsapi/dns_cn.sh

@@ -1,7 +1,14 @@
 #!/usr/bin/env sh
-
-# DNS API for acme.sh for Core-Networks (https://beta.api.core-networks.de/doc/).
-# created by 5ll and francis
+# shellcheck disable=SC2034
+dns_cn_info='Core-Networks.de
+Site: beta.api.Core-Networks.de/doc/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cn
+Options:
+ CN_User User
+ CN_Password Password
+Issues: github.com/acmesh-official/acme.sh/issues/2142
+Author: 5ll, francis
+'
 
 CN_API="https://beta.api.core-networks.de"
 
@@ -124,7 +131,7 @@ _cn_get_root() {
   p=1
   while true; do
 
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     _debug _H1 "${_H1}"
 
@@ -142,7 +149,7 @@ _cn_get_root() {
     fi
 
     if _contains "$_cn_zonelist" "\"name\":\"$h\"" >/dev/null; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     else

+ 13 - 2
dnsapi/dns_conoha.sh

@@ -1,4 +1,15 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_conoha_info='ConoHa.jp
+Domains: ConoHa.io
+Site: ConoHa.jp
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_conoha
+Options:
+ CONOHA_Username Username
+ CONOHA_Password Password
+ CONOHA_TenantId TenantId
+ CONOHA_IdentityServiceApi Identity Service API. E.g. "https://identity.xxxx.conoha.io/v2.0"
+'
 
 CONOHA_DNS_EP_PREFIX_REGEXP="https://dns-service\."
 
@@ -226,7 +237,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100).
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100).
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -240,7 +251,7 @@ _get_root() {
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
       _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | head -n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 12 - 6
dnsapi/dns_constellix.sh

@@ -1,10 +1,16 @@
 #!/usr/bin/env sh
-
-# Author: Wout Decre <[email protected]>
+# shellcheck disable=SC2034
+dns_constellix_info='Constellix.com
+Site: Constellix.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_constellix
+Options:
+ CONSTELLIX_Key API Key
+ CONSTELLIX_Secret API Secret
+Issues: github.com/acmesh-official/acme.sh/issues/2724
+Author: Wout Decre <[email protected]>
+'
 
 CONSTELLIX_Api="https://api.dns.constellix.com/v1"
-#CONSTELLIX_Key="XXX"
-#CONSTELLIX_Secret="XXX"
 
 ########  Public functions #####################
 
@@ -116,7 +122,7 @@ _get_root() {
   p=1
   _debug "Detecting root zone"
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       return 1
     fi
@@ -128,7 +134,7 @@ _get_root() {
     if _contains "$response" "\"name\":\"$h\""; then
       _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[0-9]*" | cut -d ':' -f 2)
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d '.' -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d '.' -f 1-"$p")
         _domain="$h"
 
         _debug _domain_id "$_domain_id"

+ 13 - 13
dnsapi/dns_cpanel.sh

@@ -1,18 +1,18 @@
 #!/usr/bin/env sh
-#
-#Author: Bjarne Saltbaek
-#Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/3732
-#
-#
+# shellcheck disable=SC2034
+dns_cpanel_info='cPanel Server API
+ Manage DNS via cPanel Dashboard.
+Site: cPanel.net
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_cpanel
+Options:
+ cPanel_Username Username
+ cPanel_Apitoken API Token
+ cPanel_Hostname Server URL. E.g. "https://hostname:port"
+Issues: github.com/acmesh-official/acme.sh/issues/3732
+Author: Bjarne Saltbaek
+'
+
 ########  Public functions #####################
-#
-# Export CPANEL username,api token and hostname in the following variables
-#
-# cPanel_Username=username
-# cPanel_Apitoken=apitoken
-# cPanel_Hostname=hostname
-#
-# Usage: add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 
 # Used to add txt record
 dns_cpanel_add() {

+ 12 - 6
dnsapi/dns_curanet.sh

@@ -1,9 +1,15 @@
 #!/usr/bin/env sh
-
-#Script to use with curanet.dk, scannet.dk, wannafind.dk, dandomain.dk DNS management.
-#Requires api credentials with scope: dns
-#Author: Peter L. Hansen <[email protected]>
-#Version 1.0
+# shellcheck disable=SC2034
+dns_curanet_info='Curanet.dk
+Domains: scannet.dk wannafind.dk dandomain.dk
+Site: Curanet.dk
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_curanet
+Options:
+ CURANET_AUTHCLIENTID Auth ClientID. Requires scope dns
+ CURANET_AUTHSECRET Auth Secret
+Issues: github.com/acmesh-official/acme.sh/issues/3933
+Author: Peter L. Hansen <[email protected]>
+'
 
 CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains"
 CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token"
@@ -136,7 +142,7 @@ _get_root() {
   i=1
 
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid

+ 24 - 23
dnsapi/dns_cyon.sh

@@ -1,21 +1,15 @@
 #!/usr/bin/env sh
-
-########
-# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/acmesh-official/acme.sh)
-#
-# Usage: acme.sh --issue --dns dns_cyon -d www.domain.com
-#
-# Dependencies:
-# -------------
-# - oathtool (When using 2 Factor Authentication)
-#
-# Issues:
-# -------
-# Any issues / questions / suggestions can be posted here:
-# https://github.com/noplanman/cyon-api/issues
-#
-# Author: Armando Lüscher <[email protected]>
-########
+# shellcheck disable=SC2034
+dns_cyon_info='cyon.ch
+Site: cyon.ch
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cyon
+Options:
+ CY_Username Username
+ CY_Password API Token
+ CY_OTP_Secret OTP token. Only required if using 2FA
+Issues: github.com/noplanman/cyon-api/issues
+Author: Armando Lüscher <[email protected]>
+'
 
 dns_cyon_add() {
   _cyon_load_credentials &&
@@ -221,10 +215,8 @@ _cyon_change_domain_env() {
 
   if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi
 
-  domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)"
-
   # Bail if domain environment change fails.
-  if [ "${domain_env_success}" != "true" ]; then
+  if [ "$(printf "%s" "${domain_env_response}" | _cyon_get_environment_change_status)" != "true" ]; then
     _err "    $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)"
     _err ""
     return 1
@@ -238,7 +230,7 @@ _cyon_add_txt() {
   _info "  - Adding DNS TXT entry..."
 
   add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async"
-  add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}"
+  add_txt_data="name=${fulldomain_idn}.&ttl=900&type=TXT&dnscontent=${txtvalue}"
 
   add_txt_response="$(_post "$add_txt_data" "$add_txt_url")"
   _debug add_txt_response "${add_txt_response}"
@@ -247,9 +239,10 @@ _cyon_add_txt() {
 
   add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)"
   add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)"
+  add_txt_validation="$(printf "%s" "${add_txt_response}" | _cyon_get_validation_status)"
 
   # Bail if adding TXT entry fails.
-  if [ "${add_txt_status}" != "true" ]; then
+  if [ "${add_txt_status}" != "true" ] || [ "${add_txt_validation}" != "true" ]; then
     _err "    ${add_txt_message}"
     _err ""
     return 1
@@ -311,13 +304,21 @@ _cyon_get_response_message() {
 }
 
 _cyon_get_response_status() {
-  _egrep_o '"status":\w*' | cut -d : -f 2
+  _egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2
+}
+
+_cyon_get_validation_status() {
+  _egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2
 }
 
 _cyon_get_response_success() {
   _egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"'
 }
 
+_cyon_get_environment_change_status() {
+  _egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2
+}
+
 _cyon_check_if_2fa_missed() {
   # Did we miss the 2FA?
   if test "${1#*multi_factor_form}" != "${1}"; then

+ 12 - 29
dnsapi/dns_da.sh

@@ -1,31 +1,14 @@
 #!/usr/bin/env sh
-# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
-# vim: et ts=2 sw=2
-#
-# DirectAdmin 1.41.0 API
-# The DirectAdmin interface has it's own Let's encrypt functionality, but this
-# script can be used to generate certificates for names which are not hosted on
-# DirectAdmin
-#
-# User must provide login data and URL to DirectAdmin incl. port.
-# You can create login key, by using the Login Keys function
-# ( https://da.example.com:8443/CMD_LOGIN_KEYS ), which only has access to
-# - CMD_API_DNS_CONTROL
-# - CMD_API_SHOW_DOMAINS
-#
-# See also https://www.directadmin.com/api.php and
-# https://www.directadmin.com/features.php?id=1298
-#
-# Report bugs to https://github.com/TigerP/acme.sh/issues
-#
-# Values to export:
-# export DA_Api="https://remoteUser:[email protected]:8443"
-# export DA_Api_Insecure=1
-#
-# Set DA_Api_Insecure to 1 for insecure and 0 for secure -> difference is
-# whether ssl cert is checked for validity (0) or whether it is just accepted
-# (1)
-#
+# shellcheck disable=SC2034
+dns_da_info='DirectAdmin Server API
+Site: DirectAdmin.com/api.php
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da
+Options:
+ DA_Api API Server URL. E.g. "https://remoteUser:[email protected]:8443"
+ DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
+Issues: github.com/TigerP/acme.sh/issues
+'
+
 ########  Public functions #####################
 
 # Usage: dns_myapi_add  _acme-challenge.www.example.com  "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -78,7 +61,7 @@ _get_root() {
   # response will contain "list[]=example.com&list[]=example.org"
   _da_api CMD_API_SHOW_DOMAINS "" "${domain}"
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       # not valid
@@ -86,7 +69,7 @@ _get_root() {
       return 1
     fi
     if _contains "$response" "$h" >/dev/null; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi

+ 9 - 12
dnsapi/dns_ddnss.sh

@@ -1,16 +1,13 @@
 #!/usr/bin/env sh
-
-#Created by RaidenII, to use DuckDNS's API to add/remove text records
-#modified by helbgd @ 03/13/2018 to support ddnss.de
-#modified by mod242 @ 04/24/2018 to support different ddnss domains
-#Please note: the Wildcard Feature must be turned on for the Host record
-#and the checkbox for TXT needs to be enabled
-
-# Pass credentials before "acme.sh --issue --dns dns_ddnss ..."
-# --
-# export DDNSS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
-# --
-#
+# shellcheck disable=SC2034
+dns_ddnss_info='DDNSS.de
+Site: DDNSS.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_ddnss
+Options:
+ DDNSS_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/2230
+Author: @helbgd, @mod242
+'
 
 DDNSS_DNS_API="https://ddnss.de/upd.php"
 

+ 11 - 9
dnsapi/dns_desec.sh

@@ -1,11 +1,13 @@
 #!/usr/bin/env sh
-#
-# deSEC.io Domain API
-#
-# Author: Zheng Qian
-#
-# deSEC API doc
-# https://desec.readthedocs.io/en/latest/
+# shellcheck disable=SC2034
+dns_desec_info='deSEC.io
+Site: desec.readthedocs.io/en/latest/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec
+Options:
+ DDNSS_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/2180
+Author: Zheng Qian
+'
 
 REST_API="https://desec.io/api/v1/domains"
 
@@ -174,7 +176,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -186,7 +188,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi

+ 11 - 14
dnsapi/dns_df.sh

@@ -1,18 +1,15 @@
 #!/usr/bin/env sh
-
-########################################################################
-# https://dyndnsfree.de hook script for acme.sh
-#
-# Environment variables:
-#
-#  - $DF_user      (your dyndnsfree.de username)
-#  - $DF_password  (your dyndnsfree.de password)
-#
-# Author: Thilo Gass <[email protected]>
-# Git repo: https://github.com/ThiloGa/acme.sh
-
-#-- dns_df_add() - Add TXT record --------------------------------------
-# Usage: dns_df_add _acme-challenge.subdomain.domain.com "XyZ123..."
+# shellcheck disable=SC2034
+dns_df_info='DynDnsFree.de
+Domains: dynup.de
+Site: DynDnsFree.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_df
+Options:
+ DF_user Username
+ DF_password Password
+Issues: github.com/acmesh-official/acme.sh/issues/2897
+Author: Thilo Gass <[email protected]>
+'
 
 dyndnsfree_api="https://dynup.de/acme.php"
 

+ 11 - 15
dnsapi/dns_dgon.sh

@@ -1,16 +1,12 @@
 #!/usr/bin/env sh
-
-## Will be called by acme.sh to add the txt record to your api system.
-## returns 0 means success, otherwise error.
-
-## Author: thewer <github at thewer.com>
-## GitHub: https://github.com/gitwer/acme.sh
-
-##
-## Environment Variables Required:
-##
-## DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc"
-##
+# shellcheck disable=SC2034
+dns_dgon_info='DigitalOcean.com
+Site: DigitalOcean.com/help/api/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dgon
+Options:
+ DO_API_KEY API Key
+Author: <[email protected]>
+'
 
 #####################  Public functions  #####################
 
@@ -207,7 +203,7 @@ _get_base_domain() {
     _debug2 domain_list "$domain_list"
 
     i=1
-    while [ $i -gt 0 ]; do
+    while [ "$i" -gt 0 ]; do
       ## get next longest domain
       _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM")
       ## check we got something back from our cut (or are we at the end)
@@ -219,14 +215,14 @@ _get_base_domain() {
       ## check if it exists
       if [ -n "$found" ]; then
         ## exists - exit loop returning the parts
-        sub_point=$(_math $i - 1)
+        sub_point=$(_math "$i" - 1)
         _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point")
         _debug _domain "$_domain"
         _debug _sub_domain "$_sub_domain"
         return 0
       fi
       ## increment cut point $i
-      i=$(_math $i + 1)
+      i=$(_math "$i" + 1)
     done
 
     if [ -z "$found" ]; then

+ 12 - 9
dnsapi/dns_dnsexit.sh

@@ -1,13 +1,16 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_dnsexit_info='DNSExit.com
+Site: DNSExit.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit
+Options:
+ DNSEXIT_API_KEY API Key
+ DNSEXIT_AUTH_USER Username
+ DNSEXIT_AUTH_PASS Password
+Issues: github.com/acmesh-official/acme.sh/issues/4719
+Author: Samuel Jimenez
+'
 
-#use dns-01 at DNSExit.com
-
-#Author: Samuel Jimenez
-#Report Bugs here: https://github.com/acmesh-official/acme.sh
-
-#DNSEXIT_API_KEY=ABCDEFGHIJ0123456789abcdefghij
-#[email protected]
-#DNSEXIT_AUTH_PASS=aStrongPassword
 DNSEXIT_API_URL="https://api.dnsexit.com/dns/"
 DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp"
 
@@ -81,7 +84,7 @@ _get_root() {
   domain=$1
   i=1
   while true; do
-    _domain=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$_domain"
     if [ -z "$_domain" ]; then
       return 1

+ 10 - 11
dnsapi/dns_dnshome.sh

@@ -1,15 +1,14 @@
 #!/usr/bin/env sh
-
-# dnsHome.de API for acme.sh
-#
-# This Script adds the necessary TXT record to a Subdomain
-#
-# Author dnsHome.de (https://github.com/dnsHome-de)
-#
-# Report Bugs to https://github.com/acmesh-official/acme.sh/issues/3819
-#
-# export DNSHOME_Subdomain=""
-# export DNSHOME_SubdomainPassword=""
+# shellcheck disable=SC2034
+dns_dnshome_info='dnsHome.de
+Site: dnsHome.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnshome
+Options:
+ DNSHOME_Subdomain Subdomain
+ DNSHOME_SubdomainPassword Subdomain Password
+Issues: github.com/acmesh-official/acme.sh/issues/3819
+Author: @dnsHome-de
+'
 
 # Usage: add subdomain.ddnsdomain.tld "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 # Used to add txt record

+ 10 - 10
dnsapi/dns_dnsimple.sh

@@ -1,12 +1,12 @@
 #!/usr/bin/env sh
-
-# DNSimple domain api
-# https://github.com/pho3nixf1re/acme.sh/issues
-#
-# This is your oauth token which can be acquired on the account page. Please
-# note that this must be an _account_ token and not a _user_ token.
-# https://dnsimple.com/a/<your account id>/account/access_tokens
-# DNSimple_OAUTH_TOKEN="sdfsdfsdfljlbjkljlkjsdfoiwje"
+# shellcheck disable=SC2034
+dns_dnsimple_info='DNSimple.com
+Site: DNSimple.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
+Options:
+ DNSimple_OAUTH_TOKEN OAuth Token
+Issues: github.com/pho3nixf1re/acme.sh/issues
+'
 
 DNSimple_API="https://api.dnsimple.com/v2"
 
@@ -92,7 +92,7 @@ _get_root() {
   i=2
   previous=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       # not valid
       return 1
@@ -105,7 +105,7 @@ _get_root() {
     if _contains "$response" 'not found'; then
       _debug "$h not found"
     else
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$previous)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
       _domain="$h"
 
       _debug _domain "$_domain"

+ 10 - 7
dnsapi/dns_dnsservices.sh

@@ -1,12 +1,15 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_dnsservices_info='DNS.Services
+Site: DNS.Services
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsservices
+Options:
+ DnsServices_Username Username
+ DnsServices_Password Password
+Issues: github.com/acmesh-official/acme.sh/issues/4152
+Author: Bjarke Bruun <[email protected]>
+'
 
-#This file name is "dns_dnsservices.sh"
-#Script for Danish DNS registra and DNS hosting provider https://dns.services
-
-#Author: Bjarke Bruun <[email protected]>
-#Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/4152
-
-# Global variable to connect to the DNS.Services API
 DNSServices_API=https://dns.services/api
 
 ########  Public functions #####################

+ 12 - 10
dnsapi/dns_doapi.sh

@@ -1,14 +1,16 @@
 #!/usr/bin/env sh
-
-# Official Let's Encrypt API for do.de / Domain-Offensive
-#
-# This is different from the dns_do adapter, because dns_do is only usable for enterprise customers
-# This API is also available to private customers/individuals
-#
-# Provide the required LetsEncrypt token like this:
-# DO_LETOKEN="FmD408PdqT1E269gUK57"
-
-DO_API="https://www.do.de/api/letsencrypt"
+# shellcheck disable=SC2034
+dns_doapi_info='Domain-Offensive do.de
+ Official LetsEncrypt API for do.de / Domain-Offensive.
+ This API is also available to private customers/individuals.
+Site: do.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_doapi
+Options:
+ DO_LETOKEN LetsEncrypt Token
+Issues: github.com/acmesh-official/acme.sh/issues/2057
+'
+
+DO_API="https://my.do.de/api/letsencrypt"
 
 ########  Public functions #####################
 

+ 11 - 2
dnsapi/dns_domeneshop.sh

@@ -1,4 +1,13 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_domeneshop_info='DomeneShop.no
+Site: DomeneShop.no
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_domeneshop
+Options:
+ DOMENESHOP_Token Token
+ DOMENESHOP_Secret Secret
+Issues: github.com/acmesh-official/acme.sh/issues/2457
+'
 
 DOMENESHOP_Api_Endpoint="https://api.domeneshop.no/v0"
 
@@ -84,7 +93,7 @@ _get_domainid() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug "h" "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -93,7 +102,7 @@ _get_domainid() {
 
     if _contains "$response" "\"$h\"" >/dev/null; then
       # We have found the domain name.
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       _domainid=$(printf "%s" "$response" | _egrep_o "[^{]*\"domain\":\"$_domain\"[^}]*" | _egrep_o "\"id\":[0-9]+" | cut -d : -f 2)
       return 0

+ 10 - 8
dnsapi/dns_dp.sh

@@ -1,10 +1,12 @@
 #!/usr/bin/env sh
-
-# Dnspod.cn Domain api
-#
-#DP_Id="1234"
-#
-#DP_Key="sADDsdasdgdsf"
+# shellcheck disable=SC2034
+dns_dp_info='DNSPod.cn
+Site: DNSPod.cn
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dp
+Options:
+ DP_Id Id
+ DP_Key Key
+'
 
 REST_API="https://dnsapi.cn"
 
@@ -107,7 +109,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       #not valid
       return 1
@@ -121,7 +123,7 @@ _get_root() {
       _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
       _debug _domain_id "$_domain_id"
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _debug _sub_domain "$_sub_domain"
         _domain="$h"
         _debug _domain "$_domain"

+ 10 - 8
dnsapi/dns_dpi.sh

@@ -1,10 +1,12 @@
 #!/usr/bin/env sh
-
-# Dnspod.com Domain api
-#
-#DPI_Id="1234"
-#
-#DPI_Key="sADDsdasdgdsf"
+# shellcheck disable=SC2034
+dns_dpi_info='DNSPod.com
+Site: DNSPod.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dpi
+Options:
+ DPI_Id Id
+ DPI_Key Key
+'
 
 REST_API="https://api.dnspod.com"
 
@@ -107,7 +109,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       #not valid
       return 1
@@ -121,7 +123,7 @@ _get_root() {
       _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
       _debug _domain_id "$_domain_id"
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _debug _sub_domain "$_sub_domain"
         _domain="$h"
         _debug _domain "$_domain"

+ 9 - 5
dnsapi/dns_dreamhost.sh

@@ -1,10 +1,14 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_dreamhost_info='DreamHost.com
+Site: DreamHost.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dreamhost
+Options:
+ DH_API_KEY API Key
+Issues: github.com/RhinoLance/acme.sh
+Author: RhinoLance
+'
 
-#Author: RhinoLance
-#Report Bugs here: https://github.com/RhinoLance/acme.sh
-#
-
-#define the api endpoint
 DH_API_ENDPOINT="https://api.dreamhost.com/"
 querystring=""
 

+ 8 - 10
dnsapi/dns_duckdns.sh

@@ -1,14 +1,12 @@
 #!/usr/bin/env sh
-
-#Created by RaidenII, to use DuckDNS's API to add/remove text records
-#06/27/2017
-
-# Pass credentials before "acme.sh --issue --dns dns_duckdns ..."
-# --
-# export DuckDNS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
-# --
-#
-# Due to the fact that DuckDNS uses StartSSL as cert provider, --insecure may need to be used with acme.sh
+# shellcheck disable=SC2034
+dns_duckdns_info='DuckDNS.org
+Site: www.DuckDNS.org
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns
+Options:
+ DuckDNS_Token API Token
+Author: @RaidenII
+'
 
 DuckDNS_API="https://www.duckdns.org/update"
 

+ 11 - 5
dnsapi/dns_durabledns.sh

@@ -1,7 +1,13 @@
 #!/usr/bin/env sh
-
-#DD_API_User="xxxxx"
-#DD_API_Key="xxxxxx"
+# shellcheck disable=SC2034
+dns_durabledns_info='DurableDNS.com
+Site: DurableDNS.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_durabledns
+Options:
+ DD_API_User API User
+ DD_API_Key API Key
+Issues: github.com/acmesh-official/acme.sh/issues/2281
+'
 
 _DD_BASE="https://durabledns.com/services/dns"
 
@@ -104,7 +110,7 @@ _get_root() {
   i=1
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -112,7 +118,7 @@ _get_root() {
     fi
 
     if _contains "$response" ">$h.</origin>"; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi

+ 12 - 13
dnsapi/dns_dyn.sh

@@ -1,10 +1,16 @@
 #!/usr/bin/env sh
-#
-# Dyn.com Domain API
-#
-# Author: Gerd Naschenweng
-# https://github.com/magicdude4eva
-#
+# shellcheck disable=SC2034
+dns_dyn_info='Dyn.com
+Domains: dynect.net
+Site: Dyn.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dyn
+Options:
+ DYN_Customer Customer
+ DYN_Username API Username
+ DYN_Password Secret
+Author: Gerd Naschenweng <@magicdude4eva>
+'
+
 # Dyn Managed DNS API
 # https://help.dyn.com/dns-api-knowledge-base/
 #
@@ -20,13 +26,6 @@
 # ZoneRemoveNode
 # ZonePublish
 # --
-#
-# Pass credentials before "acme.sh --issue --dns dns_dyn ..."
-# --
-# export DYN_Customer="customer"
-# export DYN_Username="apiuser"
-# export DYN_Password="secret"
-# --
 
 DYN_API="https://api.dynect.net/REST"
 

+ 13 - 12
dnsapi/dns_dynu.sh

@@ -1,20 +1,21 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_dynu_info='Dynu.com
+Site: Dynu.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dynu
+Options:
+ Dynu_ClientId Client ID
+ Dynu_Secret Secret
+Issues: github.com/shar0119/acme.sh
+Author: Dynu Systems Inc
+'
 
-#Client ID
-#Dynu_ClientId="0b71cae7-a099-4f6b-8ddf-94571cdb760d"
-#
-#Secret
-#Dynu_Secret="aCUEY4BDCV45KI8CSIC3sp2LKQ9"
-#
 #Token
 Dynu_Token=""
 #
 #Endpoint
 Dynu_EndPoint="https://api.dynu.com/v2"
-#
-#Author: Dynu Systems, Inc.
-#Report Bugs here: https://github.com/shar0119/acme.sh
-#
+
 ########  Public functions #####################
 
 #Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -125,7 +126,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -139,7 +140,7 @@ _get_root() {
     if _contains "$response" "\"domainName\":\"$h\"" >/dev/null; then
       dnsId=$(printf "%s" "$response" | tr -d "{}" | cut -d , -f 2 | cut -d : -f 2)
       _domain_name=$h
-      _node=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _node=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       return 0
     fi
     p=$i

+ 17 - 11
dnsapi/dns_dynv6.sh

@@ -1,16 +1,23 @@
 #!/usr/bin/env sh
-#Author StefanAbl
-#Usage specify a private keyfile to use with dynv6 'export KEY="path/to/keyfile"'
-#or use the HTTP REST API by by specifying a token 'export DYNV6_TOKEN="value"
-#if no keyfile is specified, you will be asked if you want to create one in /home/$USER/.ssh/dynv6 and /home/$USER/.ssh/dynv6.pub
+# shellcheck disable=SC2034
+dns_dynv6_info='DynV6.com
+Site: DynV6.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dynv6
+Options:
+ DYNV6_TOKEN REST API token. Get from https://DynV6.com/keys
+OptionsAlt:
+ KEY Path to SSH private key file. E.g. "/root/.ssh/dynv6"
+Issues: github.com/acmesh-official/acme.sh/issues/2702
+Author: @StefanAbl
+'
 
 dynv6_api="https://dynv6.com/api/v2"
 ########  Public functions #####################
 # Please Read this guide first: https://github.com/Neilpang/acme.sh/wiki/DNS-API-Dev-Guide
 #Usage: dns_dynv6_add  _acme-challenge.www.domain.com  "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_dynv6_add() {
-  fulldomain=$1
-  txtvalue=$2
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue="$2"
   _info "Using dynv6 api"
   _debug fulldomain "$fulldomain"
   _debug txtvalue "$txtvalue"
@@ -36,15 +43,14 @@ dns_dynv6_add() {
       _err "Something went wrong! it does not seem like the record was added successfully"
       return 1
     fi
-    return 1
   fi
-  return 1
+
 }
 #Usage: fulldomain txtvalue
 #Remove the txt record after validation.
 dns_dynv6_rm() {
-  fulldomain=$1
-  txtvalue=$2
+  fulldomain="$(echo "$1" | _lower_case)"
+  txtvalue="$2"
   _info "Using dynv6 API"
   _debug fulldomain "$fulldomain"
   _debug txtvalue "$txtvalue"
@@ -199,7 +205,7 @@ _get_zone_id() {
     return 1
   fi
 
-  zone_id="$(echo "$response" | tr '}' '\n' | grep "$selected" | tr ',' '\n' | grep id | tr -d '"')"
+  zone_id="$(echo "$response" | tr '}' '\n' | grep "$selected" | tr ',' '\n' | grep '"id":' | tr -d '"')"
   _zone_id="${zone_id#id:}"
   _debug "zone id: $_zone_id"
 }

+ 13 - 10
dnsapi/dns_easydns.sh

@@ -1,14 +1,17 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_easydns_info='easyDNS.net
+Site: easyDNS.net
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_easydns
+Options:
+ EASYDNS_Token API Token
+ EASYDNS_Key API Key
+Issues: github.com/acmesh-official/acme.sh/issues/2647
+Author: @Neilpang, wurzelpanzer <[email protected]>
+'
 
-#######################################################
-#
-# easyDNS REST API for acme.sh by Neilpang based on dns_cf.sh
-#
 # API Documentation: https://sandbox.rest.easydns.net:3001/
-#
-# Author: wurzelpanzer [[email protected]]
-# Report Bugs here: https://github.com/acmesh-official/acme.sh/issues/2647
-#
+
 ####################  Public functions #################
 
 #EASYDNS_Key="xxxxxxxxxxxxxxxxxxxxxxxx"
@@ -118,7 +121,7 @@ _get_root() {
   i=1
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -130,7 +133,7 @@ _get_root() {
     fi
 
     if _contains "$response" "\"status\":200"; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi

+ 163 - 0
dnsapi/dns_edgecenter.sh

@@ -0,0 +1,163 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_edgecenter_info='EdgeCenter.ru
+Site: EdgeCenter.ru
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter
+Options:
+ EDGECENTER_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/issues/6313
+Author: Konstantin Ruchev <[email protected]>
+'
+
+EDGECENTER_API="https://api.edgecenter.ru"
+DOMAIN_TYPE=
+DOMAIN_MASTER=
+
+########  Public functions #####################
+
+#Usage: dns_edgecenter_add   _acme-challenge.www.domain.com   "TXT_RECORD_VALUE"
+dns_edgecenter_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Using EdgeCenter DNS API"
+
+  if ! _dns_edgecenter_init_check; then
+    return 1
+  fi
+
+  _debug "Detecting root zone for $fulldomain"
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
+  subdomain="${fulldomain%."$_zone"}"
+  subdomain=${subdomain%.}
+
+  _debug "Zone: $_zone"
+  _debug "Subdomain: $subdomain"
+  _debug "TXT value: $txtvalue"
+
+  payload='{"resource_records": [ { "content": ["'"$txtvalue"'"] } ], "ttl": 60 }'
+  _dns_edgecenter_http_api_call "post" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$payload"
+
+  if _contains "$response" '"error":"rrset is already exists"'; then
+    _debug "RRSet exists, merging values"
+    _dns_edgecenter_http_api_call "get" "dns/v2/zones/$_zone/$subdomain.$_zone/txt"
+    current="$response"
+    newlist=""
+    for v in $(echo "$current" | sed -n 's/.*"content":\["\([^"]*\)"\].*/\1/p'); do
+      newlist="$newlist {\"content\":[\"$v\"]},"
+    done
+    newlist="$newlist{\"content\":[\"$txtvalue\"]}"
+    putdata="{\"resource_records\":[${newlist}]}
+"
+    _dns_edgecenter_http_api_call "put" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$putdata"
+    _info "Updated existing RRSet with new TXT value."
+    return 0
+  fi
+
+  if _contains "$response" '"exception":'; then
+    _err "Record cannot be added."
+    return 1
+  fi
+
+  _info "TXT record added successfully."
+  return 0
+}
+
+#Usage: dns_edgecenter_rm   _acme-challenge.www.domain.com   "TXT_RECORD_VALUE"
+dns_edgecenter_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Removing TXT record for $fulldomain"
+
+  if ! _dns_edgecenter_init_check; then
+    return 1
+  fi
+
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+
+  subdomain="${fulldomain%."$_zone"}"
+  subdomain=${subdomain%.}
+
+  _dns_edgecenter_http_api_call "delete" "dns/v2/zones/$_zone/$subdomain.$_zone/txt"
+
+  if [ -z "$response" ]; then
+    _info "TXT record deleted successfully."
+  else
+    _info "TXT record may not have been deleted: $response"
+  fi
+  return 0
+}
+
+####################  Private functions below ##################################
+
+_dns_edgecenter_init_check() {
+  EDGECENTER_API_KEY="${EDGECENTER_API_KEY:-$(_readaccountconf_mutable EDGECENTER_API_KEY)}"
+  if [ -z "$EDGECENTER_API_KEY" ]; then
+    _err "EDGECENTER_API_KEY was not exported."
+    return 1
+  fi
+
+  _saveaccountconf_mutable EDGECENTER_API_KEY "$EDGECENTER_API_KEY"
+  export _H1="Authorization: APIKey $EDGECENTER_API_KEY"
+
+  _dns_edgecenter_http_api_call "get" "dns/v2/clients/me/features"
+  if ! _contains "$response" '"id":'; then
+    _err "Invalid API key."
+    return 1
+  fi
+  return 0
+}
+
+_get_root() {
+  domain="$1"
+  i=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-)
+    if [ -z "$h" ]; then
+      return 1
+    fi
+    _dns_edgecenter_http_api_call "get" "dns/v2/zones/$h"
+    if ! _contains "$response" 'zone is not found'; then
+      _zone="$h"
+      return 0
+    fi
+    i=$((i + 1))
+  done
+  return 1
+}
+
+_dns_edgecenter_http_api_call() {
+  mtd="$1"
+  endpoint="$2"
+  data="$3"
+
+  export _H1="Authorization: APIKey $EDGECENTER_API_KEY"
+
+  case "$mtd" in
+  get)
+    response="$(_get "$EDGECENTER_API/$endpoint")"
+    ;;
+  post)
+    response="$(_post "$data" "$EDGECENTER_API/$endpoint")"
+    ;;
+  delete)
+    response="$(_post "" "$EDGECENTER_API/$endpoint" "" "DELETE")"
+    ;;
+  put)
+    response="$(_post "$data" "$EDGECENTER_API/$endpoint" "" "PUT")"
+    ;;
+  *)
+    _err "Unknown HTTP method $mtd"
+    return 1
+    ;;
+  esac
+
+  _debug "HTTP $mtd response: $response"
+  return 0
+}

+ 14 - 11
dnsapi/dns_edgedns.sh

@@ -1,4 +1,15 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_edgedns_info='Akamai.com Edge DNS
+Site: techdocs.Akamai.com/edge-dns/reference/edge-dns-api
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgedns
+Options: Specify individual credentials
+ AKAMAI_HOST Host
+ AKAMAI_ACCESS_TOKEN Access token
+ AKAMAI_CLIENT_TOKEN Client token
+ AKAMAI_CLIENT_SECRET Client secret
+Issues: github.com/acmesh-official/acme.sh/issues/3157
+'
 
 # Akamai Edge DNS v2  API
 # User must provide Open Edgegrid API credentials to the EdgeDNS installation. The remote user in EdgeDNS must have CRUD access to
@@ -6,18 +17,10 @@
 
 # Report bugs to https://control.akamai.com/apps/support-ui/#/contact-support
 
-# Values to export:
-# --EITHER--
 # *** TBD. NOT IMPLEMENTED YET ***
-# specify Edgegrid credentials file and section
-# AKAMAI_EDGERC=<full file path>
-# AKAMAI_EDGERC_SECTION="default"
-## --OR--
-# specify indiviual credentials
-# export AKAMAI_HOST = <host>
-# export AKAMAI_ACCESS_TOKEN = <access token>
-# export AKAMAI_CLIENT_TOKEN = <client token>
-# export AKAMAI_CLIENT_SECRET = <client secret>
+# Specify Edgegrid credentials file and section.
+# AKAMAI_EDGERC Edge RC. Full file path
+# AKAMAI_EDGERC_SECTION Edge RC Section. E.g. "default"
 
 ACME_EDGEDNS_VERSION="0.1.0"
 

+ 12 - 16
dnsapi/dns_euserv.sh

@@ -1,18 +1,14 @@
 #!/usr/bin/env sh
-
-#This is the euserv.eu api wrapper for acme.sh
-#
-#Author: Michael Brueckner
-#Report Bugs: https://www.github.com/initit/acme.sh  or  [email protected]
-
-#
-#EUSERV_Username="username"
-#
-#EUSERV_Password="password"
-#
-# Dependencies:
-# -------------
-# - none -
+# shellcheck disable=SC2034
+dns_euserv_info='EUserv.com
+Domains: EUserv.eu
+Site: EUserv.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_euserv
+Options:
+ EUSERV_Username Username
+ EUSERV_Password Password
+Author: Michael Brueckner
+'
 
 EUSERV_Api="https://api.euserv.net"
 
@@ -155,7 +151,7 @@ _get_root() {
   response="$_euserv_domain_orders"
 
   while true; do
-    h=$(echo "$domain" | cut -d . -f $i-100)
+    h=$(echo "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -163,7 +159,7 @@ _get_root() {
     fi
 
     if _contains "$response" "$h"; then
-      _sub_domain=$(echo "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(echo "$domain" | cut -d . -f 1-"$p")
       _domain="$h"
       if ! _euserv_get_domain_id "$_domain"; then
         _err "invalid domain"

+ 10 - 2
dnsapi/dns_exoscale.sh

@@ -1,4 +1,12 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_exoscale_info='Exoscale.com
+Site: Exoscale.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_exoscale
+Options:
+ EXOSCALE_API_KEY API Key
+ EXOSCALE_SECRET_KEY API Secret key
+'
 
 EXOSCALE_API=https://api.exoscale.com/dns/v1
 
@@ -111,7 +119,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -122,7 +130,7 @@ _get_root() {
       _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
       _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_token" ] && [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 23 - 16
dnsapi/dns_fornex.sh

@@ -1,8 +1,15 @@
 #!/usr/bin/env sh
-
-#Author: Timur Umarov <[email protected]>
-
-FORNEX_API_URL="https://fornex.com/api/dns/v0.1"
+# shellcheck disable=SC2034
+dns_fornex_info='Fornex.com
+Site: Fornex.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_fornex
+Options:
+ FORNEX_API_KEY API Key
+Issues: github.com/acmesh-official/acme.sh/issues/3998
+Author: Timur Umarov <[email protected]>
+'
+
+FORNEX_API_URL="https://fornex.com/api"
 
 ########  Public functions #####################
 
@@ -23,12 +30,10 @@ dns_fornex_add() {
   fi
 
   _info "Adding record"
-  if _rest POST "$_domain/entry_set/add/" "host=$fulldomain&type=TXT&value=$txtvalue&apikey=$FORNEX_API_KEY"; then
+  if _rest POST "dns/domain/$_domain/entry_set/" "{\"host\" : \"${fulldomain}\" , \"type\" : \"TXT\" , \"value\" : \"${txtvalue}\" , \"ttl\" : null}"; then
     _debug _response "$response"
-    if _contains "$response" '"ok": true' || _contains "$response" 'Такая запись уже существует.'; then
-      _info "Added, OK"
-      return 0
-    fi
+    _info "Added, OK"
+    return 0
   fi
   _err "Add txt record error."
   return 1
@@ -51,21 +56,21 @@ dns_fornex_rm() {
   fi
 
   _debug "Getting txt records"
-  _rest GET "$_domain/entry_set.json?apikey=$FORNEX_API_KEY"
+  _rest GET "dns/domain/$_domain/entry_set?type=TXT&q=$fulldomain"
 
   if ! _contains "$response" "$txtvalue"; then
     _err "Txt record not found"
     return 1
   fi
 
-  _record_id="$(echo "$response" | _egrep_o "{[^{]*\"value\"*:*\"$txtvalue\"[^}]*}" | sed -n -e 's#.*"id": \([0-9]*\).*#\1#p')"
+  _record_id="$(echo "$response" | _egrep_o "\{[^\{]*\"value\"*:*\"$txtvalue\"[^\}]*\}" | sed -n -e 's#.*"id":\([0-9]*\).*#\1#p')"
   _debug "_record_id" "$_record_id"
   if [ -z "$_record_id" ]; then
     _err "can not find _record_id"
     return 1
   fi
 
-  if ! _rest POST "$_domain/entry_set/$_record_id/delete/" "apikey=$FORNEX_API_KEY"; then
+  if ! _rest DELETE "dns/domain/$_domain/entry_set/$_record_id/"; then
     _err "Delete record error."
     return 1
   fi
@@ -83,18 +88,18 @@ _get_root() {
 
   i=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
       return 1
     fi
 
-    if ! _rest GET "domain_list.json?q=$h&apikey=$FORNEX_API_KEY"; then
+    if ! _rest GET "dns/domain/"; then
       return 1
     fi
 
-    if _contains "$response" "\"$h\"" >/dev/null; then
+    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
       _domain=$h
       return 0
     else
@@ -127,7 +132,9 @@ _rest() {
   data="$3"
   _debug "$ep"
 
-  export _H1="Accept: application/json"
+  export _H1="Authorization: Api-Key $FORNEX_API_KEY"
+  export _H2="Content-Type: application/json"
+  export _H3="Accept: application/json"
 
   if [ "$m" != "GET" ]; then
     _debug data "$data"

+ 10 - 9
dnsapi/dns_freedns.sh

@@ -1,14 +1,15 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_freedns_info='FreeDNS
+Site: FreeDNS.afraid.org
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_freedns
+Options:
+ FREEDNS_User Username
+ FREEDNS_Password Password
+Issues: github.com/acmesh-official/acme.sh/issues/2305
+Author: David Kerr <@dkerr64>
+'
 
-#This file name is "dns_freedns.sh"
-#So, here must be a method dns_freedns_add()
-#Which will be called by acme.sh to add the txt record to your api system.
-#returns 0 means success, otherwise error.
-#
-#Author: David Kerr
-#Report Bugs here: https://github.com/dkerr64/acme.sh
-#or here... https://github.com/acmesh-official/acme.sh/issues/2305
-#
 ########  Public functions #####################
 
 # Export FreeDNS userid and password in following variables...

+ 105 - 0
dnsapi/dns_freemyip.sh

@@ -0,0 +1,105 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_freemyip_info='FreeMyIP.com
+Site: FreeMyIP.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
+Options:
+ FREEMYIP_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/6247
+Author: Recolic Keghart <[email protected]>, @Giova96
+'
+
+FREEMYIP_DNS_API="https://freemyip.com/update?"
+
+################ Public functions ################
+
+#Usage: dns_freemyip_add    fulldomain    txtvalue
+dns_freemyip_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Add TXT record $txtvalue for $fulldomain using freemyip.com api"
+
+  FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}"
+  if [ -z "$FREEMYIP_Token" ]; then
+    FREEMYIP_Token=""
+    _err "You don't specify FREEMYIP_Token yet."
+    _err "Please specify your token and try again."
+    return 1
+  fi
+
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token"
+
+  if _is_root_domain_published "$fulldomain"; then
+    _err "freemyip API don't allow you to set multiple TXT record for the same subdomain!"
+    _err "You must apply certificate for only one domain at a time!"
+    _err "===="
+    _err "For example, aaa.yourdomain.freemyip.com and bbb.yourdomain.freemyip.com and yourdomain.freemyip.com ALWAYS share the same TXT record. They will overwrite each other if you apply multiple domain at the same time."
+    _debug "If you are testing this workflow in github pipeline or acmetest, please set TEST_DNS_NO_SUBDOMAIN=1 and TEST_DNS_NO_WILDCARD=1"
+    return 1
+  fi
+
+  # txtvalue must be url-encoded. But it's not necessary for acme txt value.
+  _freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=$txtvalue" 2>&1
+  return $?
+}
+
+#Usage: dns_freemyip_rm    fulldomain    txtvalue
+dns_freemyip_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  _info "Delete TXT record $txtvalue for $fulldomain using freemyip.com api"
+
+  FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}"
+  if [ -z "$FREEMYIP_Token" ]; then
+    FREEMYIP_Token=""
+    _err "You don't specify FREEMYIP_Token yet."
+    _err "Please specify your token and try again."
+    return 1
+  fi
+
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token"
+
+  # Leave the TXT record as empty or "null" to delete the record.
+  _freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=" 2>&1
+  return $?
+}
+
+################ Private functions below  ################
+_get_root() {
+  _fmi_d="$1"
+
+  echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
+}
+
+# There is random failure while calling freemyip API too fast. This function automatically retry until success.
+_freemyip_get_until_ok() {
+  _fmi_url="$1"
+  for i in $(seq 1 8); do
+    _debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
+    _get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
+    _sleep 1 # DO NOT send the request too fast
+  done
+  _err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
+  return 1
+}
+
+# Verify in public dns if domain is already there.
+_is_root_domain_published() {
+  _fmi_d="$1"
+  _webroot="$(_get_root "$_fmi_d")"
+
+  _info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
+  for i in $(seq 1 3); do
+    _debug "'$_webroot' ns lookup, retry $i/3..."
+    if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
+      _debug "'$_webroot' already has a TXT record published!"
+      return 0
+    fi
+    _sleep 10 # Give it some time to propagate the TXT record
+  done
+  return 1
+}

+ 12 - 9
dnsapi/dns_gandi_livedns.sh

@@ -1,16 +1,19 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_gandi_livedns_info='Gandi.net LiveDNS
+Site: Gandi.net/domain/dns
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gandi_livedns
+Options:
+ GANDI_LIVEDNS_KEY API Key
+Issues: github.com/fcrozat/acme.sh
+Author: Frédéric Crozat <[email protected]>, Dominik Röttsches <[email protected]>
+'
 
 # Gandi LiveDNS v5 API
 # https://api.gandi.net/docs/livedns/
 # https://api.gandi.net/docs/authentication/ for token + apikey (deprecated) authentication
 # currently under beta
-#
-# Requires GANDI API KEY set in GANDI_LIVEDNS_KEY set as environment variable
-#
-#Author: Frédéric Crozat <[email protected]>
-#        Dominik Röttsches <[email protected]>
-#Report Bugs here: https://github.com/fcrozat/acme.sh
-#
+
 ########  Public functions #####################
 
 GANDI_LIVEDNS_API="https://api.gandi.net/v5/livedns"
@@ -92,7 +95,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -109,7 +112,7 @@ _get_root() {
     elif _contains "$response" '"code": 404'; then
       _debug "$h not found"
     else
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain="$h"
       return 0
     fi

+ 8 - 2
dnsapi/dns_gcloud.sh

@@ -1,6 +1,12 @@
 #!/usr/bin/env sh
-
-# Author: Janos Lenart <[email protected]>
+# shellcheck disable=SC2034
+dns_gcloud_info='Google Cloud DNS
+Site: Cloud.Google.com/dns
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gcloud
+Options:
+ CLOUDSDK_ACTIVE_CONFIG_NAME Active config name. E.g. "default"
+Author: Janos Lenart <[email protected]>
+'
 
 ########  Public functions #####################
 

+ 11 - 7
dnsapi/dns_gcore.sh

@@ -1,8 +1,12 @@
 #!/usr/bin/env sh
-
-#
-#GCORE_Key='773$7b7adaf2a2b32bfb1b83787b4ff32a67eb178e3ada1af733e47b1411f2461f7f4fa7ed7138e2772a46124377bad7384b3bb8d87748f87b3f23db4b8bbe41b2bb'
-#
+# shellcheck disable=SC2034
+dns_gcore_info='Gcore.com
+Site: Gcore.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gcore
+Options:
+ GCORE_Key API Key
+Issues: github.com/acmesh-official/acme.sh/issues/4460
+'
 
 GCORE_Api="https://api.gcore.com/dns/v2"
 GCORE_Doc="https://api.gcore.com/docs/dns"
@@ -24,7 +28,7 @@ dns_gcore_add() {
   fi
 
   #save the api key to the account conf file.
-  _saveaccountconf_mutable GCORE_Key "$GCORE_Key"
+  _saveaccountconf_mutable GCORE_Key "$GCORE_Key" "base64"
 
   _debug "First detect the zone name"
   if ! _get_root "$fulldomain"; then
@@ -134,7 +138,7 @@ _get_root() {
   p=1
 
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -148,7 +152,7 @@ _get_root() {
     if _contains "$response" "\"name\":\"$h\""; then
       _zone_name=$h
       if [ "$_zone_name" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         return 0
       fi

+ 10 - 10
dnsapi/dns_gd.sh

@@ -1,12 +1,12 @@
 #!/usr/bin/env sh
-
-#Godaddy domain api
-# Get API key and secret from https://developer.godaddy.com/
-#
-# GD_Key="sdfsdfsdfljlbjkljlkjsdfoiwje"
-# GD_Secret="asdfsdfsfsdfsdfdfsdf"
-#
-# Ex.: acme.sh --issue --staging --dns dns_gd -d "*.s.example.com" -d "s.example.com"
+# shellcheck disable=SC2034
+dns_gd_info='GoDaddy.com
+Site: GoDaddy.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_gd
+Options:
+ GD_Key API Key
+ GD_Secret API Secret
+'
 
 GD_Api="https://api.godaddy.com/v1"
 
@@ -148,7 +148,7 @@ _get_root() {
   i=2
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       #not valid
       return 1
@@ -161,7 +161,7 @@ _get_root() {
     if _contains "$response" '"code":"NOT_FOUND"'; then
       _debug "$h not found"
     else
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain="$h"
       return 0
     fi

+ 9 - 9
dnsapi/dns_geoscaling.sh

@@ -1,12 +1,12 @@
 #!/usr/bin/env sh
-
-########################################################################
-# Geoscaling hook script for acme.sh
-#
-# Environment variables:
-#
-#  - $GEOSCALING_Username  (your Geoscaling username - this is usually NOT an amail address)
-#  - $GEOSCALING_Password  (your Geoscaling password)
+# shellcheck disable=SC2034
+dns_geoscaling_info='GeoScaling.com
+Site: GeoScaling.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_geoscaling
+Options:
+ GEOSCALING_Username Username. This is usually NOT an email address
+ GEOSCALING_Password Password
+'
 
 #-- dns_geoscaling_add() - Add TXT record --------------------------------------
 # Usage: dns_geoscaling_add _acme-challenge.subdomain.domain.com "XyZ123..."
@@ -202,7 +202,7 @@ find_zone() {
   # Walk through all possible zone names
   strip_counter=1
   while true; do
-    attempted_zone=$(echo "${domain}" | cut -d . -f ${strip_counter}-)
+    attempted_zone=$(echo "${domain}" | cut -d . -f "${strip_counter}"-)
 
     # All possible zone names have been tried
     if [ -z "${attempted_zone}" ]; then

+ 11 - 6
dnsapi/dns_googledomains.sh

@@ -1,10 +1,15 @@
 #!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_googledomains_info='Google Domains
+Site: Domains.Google.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_googledomains
+Options:
+ GOOGLEDOMAINS_ACCESS_TOKEN API Access Token
+ GOOGLEDOMAINS_ZONE Zone
+Issues: github.com/acmesh-official/acme.sh/issues/4545
+Author: Alex Leigh <[email protected]>
+'
 
-# Author: Alex Leigh <leigh at alexleigh dot me>
-# Created: 2023-03-02
-
-#GOOGLEDOMAINS_ACCESS_TOKEN="xxxx"
-#GOOGLEDOMAINS_ZONE="xxxx"
 GOOGLEDOMAINS_API="https://acmedns.googleapis.com/v1/acmeChallengeSets"
 
 ######## Public functions ########
@@ -127,7 +132,7 @@ _dns_googledomains_get_zone() {
 
   i=2
   while true; do
-    curr=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    curr=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug curr "$curr"
 
     if [ -z "$curr" ]; then

+ 11 - 12
dnsapi/dns_he.sh

@@ -1,15 +1,14 @@
 #!/usr/bin/env sh
-
-########################################################################
-# Hurricane Electric hook script for acme.sh
-#
-# Environment variables:
-#
-#  - $HE_Username  (your dns.he.net username)
-#  - $HE_Password  (your dns.he.net password)
-#
-# Author: Ondrej Simek <[email protected]>
-# Git repo: https://github.com/angel333/acme.sh
+# shellcheck disable=SC2034
+dns_he_info='Hurricane Electric HE.net
+Site: dns.he.net
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_he
+Options:
+ HE_Username Username
+ HE_Password Password
+Issues: github.com/angel333/acme.sh/issues/
+Author: Ondrej Simek <[email protected]>
+'
 
 #-- dns_he_add() - Add TXT record --------------------------------------
 # Usage: dns_he_add _acme-challenge.subdomain.domain.com "XyZ123..."
@@ -144,7 +143,7 @@ _find_zone() {
   # Walk through all possible zone names
   _strip_counter=1
   while true; do
-    _attempted_zone=$(echo "$_domain" | cut -d . -f ${_strip_counter}-)
+    _attempted_zone=$(echo "$_domain" | cut -d . -f "${_strip_counter}"-)
 
     # All possible zone names have been tried
     if [ -z "$_attempted_zone" ]; then

+ 45 - 0
dnsapi/dns_he_ddns.sh

@@ -0,0 +1,45 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_he_ddns_info='Hurricane Electric HE.net DDNS
+Site: dns.he.net
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns
+Options:
+ HE_DDNS_KEY The DDNS key
+Issues: https://github.com/acmesh-official/acme.sh/issues/5238
+Author: Markku Leiniö
+'
+
+HE_DDNS_URL="https://dyn.dns.he.net/nic/update"
+
+########  Public functions #####################
+
+#Usage: dns_he_ddns_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_he_ddns_add() {
+  fulldomain=$1
+  txtvalue=$2
+  HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}"
+  if [ -z "$HE_DDNS_KEY" ]; then
+    HE_DDNS_KEY=""
+    _err "You didn't specify a DDNS key for accessing the TXT record in HE API."
+    return 1
+  fi
+  #Save the DDNS key to the account conf file.
+  _saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY"
+
+  _info "Using Hurricane Electric DDNS API"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")"
+  _info "Response: $response"
+  _contains "$response" "good" && return 0 || return 1
+}
+
+# dns_he_ddns_rm() is not doing anything because the API call always updates the
+# contents of the existing record (that the API key gives access to).
+
+dns_he_ddns_rm() {
+  fulldomain=$1
+  _debug "Delete TXT record called for '${fulldomain}', not doing anything."
+  return 0
+}

+ 11 - 7
dnsapi/dns_hetzner.sh

@@ -1,8 +1,12 @@
 #!/usr/bin/env sh
-
-#
-#HETZNER_Token="sdfsdfsdfljlbjkljlkjsdfoiwje"
-#
+# shellcheck disable=SC2034
+dns_hetzner_info='Hetzner.com
+Site: Hetzner.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
+Options:
+ HETZNER_Token API Token
+Issues: github.com/acmesh-official/acme.sh/issues/2943
+'
 
 HETZNER_Api="https://dns.hetzner.com/api/v1"
 
@@ -177,7 +181,7 @@ _get_root() {
 
   _debug "Trying to get zone id by domain name for '$domain_without_acme'."
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     if [ -z "$h" ]; then
       #not valid
       return 1
@@ -189,7 +193,7 @@ _get_root() {
     if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
       _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_id" ]; then
-        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
         _domain=$h
         HETZNER_Zone_ID=$_domain_id
         _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
@@ -208,7 +212,7 @@ _get_root() {
 _response_has_error() {
   unset _response_error
 
-  err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')"
+  err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
 
   if [ -n "$err_part" ]; then
     err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)

+ 11 - 7
dnsapi/dns_hexonet.sh

@@ -1,9 +1,13 @@
 #!/usr/bin/env sh
-
-#
-# Hexonet_Login="username!roleId"
-#
-# Hexonet_Password="rolePassword"
+# shellcheck disable=SC2034
+dns_hexonet_info='Hexonet.com
+Site: Hexonet.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hexonet
+Options:
+ Hexonet_Login Login. E.g. "username!roleId"
+ Hexonet_Password Role Password
+Issues: github.com/acmesh-official/acme.sh/issues/2389
+'
 
 Hexonet_Api="https://coreapi.1api.net/api/call.cgi"
 
@@ -119,7 +123,7 @@ _get_root() {
   i=1
   p=1
   while true; do
-    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
     _debug h "$h"
     if [ -z "$h" ]; then
       #not valid
@@ -131,7 +135,7 @@ _get_root() {
     fi
 
     if _contains "$response" "CODE=200"; then
-      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
       _domain=$h
       return 0
     fi

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است