diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml
index 5417068f..84a17470 100644
--- a/.github/workflows/DNS.yml
+++ b/.github/workflows/DNS.yml
@@ -66,7 +66,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file
@@ -114,7 +114,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Install tools
run: |
brew untap aws/tap || true
@@ -167,7 +167,7 @@ jobs:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
@@ -231,12 +231,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
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 install -y socat curl
usesh: true
@@ -289,12 +290,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
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 install -y socat curl
usesh: true
@@ -345,12 +347,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
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 libiconv
usesh: true
@@ -401,12 +404,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
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: |
/usr/sbin/pkg_add curl socat
@@ -458,12 +462,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
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 install -y libnghttp2
@@ -519,12 +524,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
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: mport install socat curl || true
usesh: true
@@ -576,12 +582,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: |
@@ -635,12 +642,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
@@ -691,12 +699,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
@@ -747,12 +756,13 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: zap install socat
@@ -803,19 +813,140 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
prepare: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
-
+
+ run: |
+ if [ "${{ secrets.TokenName1}}" ] ; then
+ export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
+ fi
+ if [ "${{ secrets.TokenName2}}" ] ; then
+ export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
+ fi
+ if [ "${{ secrets.TokenName3}}" ] ; then
+ export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
+ fi
+ if [ "${{ secrets.TokenName4}}" ] ; then
+ export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
+ fi
+ if [ "${{ secrets.TokenName5}}" ] ; then
+ export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
+ fi
+ cd ../acmetest
+ ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
+
+
+
+ Hurd:
+ runs-on: ubuntu-latest
+ needs: Haiku
+ env:
+ TEST_DNS : ${{ secrets.TEST_DNS }}
+ TestingDomain: ${{ secrets.TestingDomain }}
+ TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
+ TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
+ TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
+ CASE: le_test_dnsapi
+ TEST_LOCAL: 1
+ DEBUG: ${{ secrets.DEBUG }}
+ http_proxy: ${{ secrets.http_proxy }}
+ https_proxy: ${{ secrets.https_proxy }}
+ HTTPS_INSECURE: 1 # always set to 1 to ignore https error
+ TokenName1: ${{ secrets.TokenName1}}
+ TokenName2: ${{ secrets.TokenName2}}
+ TokenName3: ${{ secrets.TokenName3}}
+ TokenName4: ${{ secrets.TokenName4}}
+ TokenName5: ${{ secrets.TokenName5}}
+ steps:
+ - uses: actions/checkout@v7
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/hurd-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
+ envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
+ sync: rsync
+ copyback: false
+ usesh: true
+ prepare: |
+ apt-get update -y
+ apt-get install -y curl cron
+ run: |
+ if [ "${{ secrets.TokenName1}}" ] ; then
+ export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
+ fi
+ if [ "${{ secrets.TokenName2}}" ] ; then
+ export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
+ fi
+ if [ "${{ secrets.TokenName3}}" ] ; then
+ export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
+ fi
+ if [ "${{ secrets.TokenName4}}" ] ; then
+ export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
+ fi
+ if [ "${{ secrets.TokenName5}}" ] ; then
+ export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
+ fi
+ cd ../acmetest
+ ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
+
+
+
+ OpenEuler:
+ runs-on: ubuntu-latest
+ needs: Hurd
+ env:
+ TEST_DNS : ${{ secrets.TEST_DNS }}
+ TestingDomain: ${{ secrets.TestingDomain }}
+ TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
+ TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
+ TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
+ CASE: le_test_dnsapi
+ TEST_LOCAL: 1
+ DEBUG: ${{ secrets.DEBUG }}
+ http_proxy: ${{ secrets.http_proxy }}
+ https_proxy: ${{ secrets.https_proxy }}
+ HTTPS_INSECURE: 1 # always set to 1 to ignore https error
+ TokenName1: ${{ secrets.TokenName1}}
+ TokenName2: ${{ secrets.TokenName2}}
+ TokenName3: ${{ secrets.TokenName3}}
+ TokenName4: ${{ secrets.TokenName4}}
+ TokenName5: ${{ secrets.TokenName5}}
+ steps:
+ - uses: actions/checkout@v7
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/openeuler-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
+ envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
+ sync: rsync
+ copyback: false
+ usesh: true
+ prepare: dnf install -y curl socat cronie tar gzip
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml
index c8cbc985..20c61dcc 100644
--- a/.github/workflows/DragonFlyBSD.yml
+++ b/.github/workflows/DragonFlyBSD.yml
@@ -58,6 +58,7 @@ jobs:
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml
index 50fcab32..88ef0a6e 100644
--- a/.github/workflows/FreeBSD.yml
+++ b/.github/workflows/FreeBSD.yml
@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml
index c77fdf2e..04510c15 100644
--- a/.github/workflows/GhostBSD.yml
+++ b/.github/workflows/GhostBSD.yml
@@ -66,6 +66,7 @@ jobs:
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml
index 9884ebeb..b133dd18 100644
--- a/.github/workflows/Haiku.yml
+++ b/.github/workflows/Haiku.yml
@@ -65,6 +65,7 @@ jobs:
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/Hurd.yml b/.github/workflows/Hurd.yml
new file mode 100644
index 00000000..fee80d29
--- /dev/null
+++ b/.github/workflows/Hurd.yml
@@ -0,0 +1,76 @@
+name: Hurd
+on:
+ push:
+ branches:
+ - '*'
+ paths:
+ - '*.sh'
+ - '.github/workflows/Hurd.yml'
+
+ pull_request:
+ branches:
+ - dev
+ paths:
+ - '*.sh'
+ - '.github/workflows/Hurd.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+
+
+jobs:
+ Hurd:
+ strategy:
+ matrix:
+ include:
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ runs-on: ubuntu-latest
+ env:
+ TEST_LOCAL: 1
+ TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
+ CA_ECDSA: ${{ matrix.CA_ECDSA }}
+ CA: ${{ matrix.CA }}
+ CA_EMAIL: ${{ matrix.CA_EMAIL }}
+ TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
+ steps:
+ - uses: actions/checkout@v7
+ - uses: anyvm-org/cf-tunnel@v0
+ id: tunnel
+ with:
+ protocol: http
+ port: 8080
+ - name: Set envs
+ run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/hurd-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
+ envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
+ nat: |
+ "8080": "80"
+ # Do NOT install socat: socat's SYSTEM: address is broken on GNU Hurd
+ # (the child shell output goes to socat's stdout instead of the socket,
+ # so clients get an empty reply). Without socat, acme.sh standalone
+ # mode falls back to its python3 server, which works on Hurd.
+ prepare: |
+ apt-get update -y
+ apt-get install -y curl cron
+ usesh: true
+ sync: rsync
+ copyback: false
+ run: |
+ cd ../acmetest \
+ && ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
diff --git a/.github/workflows/MidnightBSD.yml b/.github/workflows/MidnightBSD.yml
index 15024833..ce499e4e 100644
--- a/.github/workflows/MidnightBSD.yml
+++ b/.github/workflows/MidnightBSD.yml
@@ -58,6 +58,7 @@ jobs:
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml
index 16d0ae2d..6695f71e 100644
--- a/.github/workflows/NetBSD.yml
+++ b/.github/workflows/NetBSD.yml
@@ -58,6 +58,7 @@ jobs:
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml
index eb486b35..aabb168b 100644
--- a/.github/workflows/Omnios.yml
+++ b/.github/workflows/Omnios.yml
@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml
index 4fdb76c5..46318163 100644
--- a/.github/workflows/OpenBSD.yml
+++ b/.github/workflows/OpenBSD.yml
@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/OpenEuler.yml b/.github/workflows/OpenEuler.yml
new file mode 100644
index 00000000..2b4bd0ab
--- /dev/null
+++ b/.github/workflows/OpenEuler.yml
@@ -0,0 +1,70 @@
+name: OpenEuler
+on:
+ push:
+ branches:
+ - '*'
+ paths:
+ - '*.sh'
+ - '.github/workflows/OpenEuler.yml'
+
+ pull_request:
+ branches:
+ - dev
+ paths:
+ - '*.sh'
+ - '.github/workflows/OpenEuler.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+
+
+jobs:
+ OpenEuler:
+ strategy:
+ matrix:
+ include:
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ runs-on: ubuntu-latest
+ env:
+ TEST_LOCAL: 1
+ TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
+ CA_ECDSA: ${{ matrix.CA_ECDSA }}
+ CA: ${{ matrix.CA }}
+ CA_EMAIL: ${{ matrix.CA_EMAIL }}
+ TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
+ steps:
+ - uses: actions/checkout@v7
+ - uses: anyvm-org/cf-tunnel@v0
+ id: tunnel
+ with:
+ protocol: http
+ port: 8080
+ - name: Set envs
+ run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/openeuler-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
+ envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
+ nat: |
+ "8080": "80"
+ prepare: dnf install -y curl socat cronie tar gzip
+ usesh: true
+ sync: rsync
+ copyback: false
+ run: |
+ cd ../acmetest \
+ && ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml
index b5061ba7..e3119f8e 100644
--- a/.github/workflows/OpenIndiana.yml
+++ b/.github/workflows/OpenIndiana.yml
@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml
index 3393269a..30e4e291 100644
--- a/.github/workflows/Solaris.yml
+++ b/.github/workflows/Solaris.yml
@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml
index cd43e0e3..68e61dc8 100644
--- a/.github/workflows/Tribblix.yml
+++ b/.github/workflows/Tribblix.yml
@@ -64,6 +64,7 @@ jobs:
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
diff --git a/README.md b/README.md
index b93a8a50..90280e94 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,8 @@
+
+
@@ -130,6 +132,8 @@
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
+|28|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
+|29|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
> ๐งช Check our [testing project](https://github.com/acmesh-official/acmetest)
diff --git a/acme.sh b/acme.sh
index 753b1159..4d4f6cbe 100755
--- a/acme.sh
+++ b/acme.sh
@@ -2715,6 +2715,21 @@ _clearcaconf() {
_clear_conf "$CA_CONF" "$1"
}
+#Starts a socat listener in the background, the pid is set to _socat_pid.
+#It uses the content, _content_len, _NC and _SOCAT_ERR of _startserver.
+#options
+_startsocat() {
+ _socat_opts="$1"
+ _debug "_NC" "$_NC $_socat_opts"
+ $_NC $_socat_opts SYSTEM:"sleep 1; \
+echo 'HTTP/1.0 200 OK'; \
+echo 'Content-Length\: $_content_len'; \
+echo ''; \
+printf '%s' '$content';" 2>>"$_SOCAT_ERR" &
+ _socat_pid="$!"
+ _debug "_socat_pid" "$_socat_pid"
+}
+
# content localaddress
_startserver() {
content="$1"
@@ -2728,16 +2743,24 @@ _startserver() {
_debug Le_Listen_V4 "$Le_Listen_V4"
_debug Le_Listen_V6 "$Le_Listen_V6"
+ _serverproc_v6=""
if _exists "socat"; then
_NC="socat"
- if [ "$Le_Listen_V6" ]; then
+ SOCAT_OPTIONS6=""
+ if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then
_NC="$_NC -6"
SOCAT_OPTIONS=TCP6-LISTEN
- elif [ "$Le_Listen_V4" ]; then
+ elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then
_NC="$_NC -4"
SOCAT_OPTIONS=TCP4-LISTEN
- else
+ elif [ "$ncaddr" ]; then
+ #a single local address belongs to a single family, let socat pick it
SOCAT_OPTIONS=TCP-LISTEN
+ else
+ #listen on both ipv4 and ipv6, with one socket for each family:
+ #ipv4-mapped ipv6 addresses are not available everywhere.
+ SOCAT_OPTIONS=TCP4-LISTEN
+ SOCAT_OPTIONS6=TCP6-LISTEN
fi
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
@@ -2745,6 +2768,10 @@ _startserver() {
fi
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
+ if [ "$SOCAT_OPTIONS6" ]; then
+ #ipv6only keeps this socket from colliding with the ipv4 one
+ SOCAT_OPTIONS6=$SOCAT_OPTIONS6:$Le_HTTPPort,crlf,reuseaddr,fork,ipv6only=1
+ fi
#Adding bind to local-address
if [ "$ncaddr" ]; then
@@ -2753,14 +2780,14 @@ _startserver() {
_content_len="$(printf "%s" "$content" | wc -c)"
_debug _content_len "$_content_len"
- _debug "_NC" "$_NC $SOCAT_OPTIONS"
export _SOCAT_ERR="$(_mktemp)"
- $_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
-echo 'HTTP/1.0 200 OK'; \
-echo 'Content-Length\: $_content_len'; \
-echo ''; \
-printf '%s' '$content';" 2>"$_SOCAT_ERR" &
- serverproc="$!"
+ _startsocat "$SOCAT_OPTIONS"
+ serverproc="$_socat_pid"
+ if [ "$SOCAT_OPTIONS6" ]; then
+ #best effort, the host may have no ipv6 support at all
+ _startsocat "$SOCAT_OPTIONS6"
+ _serverproc_v6="$_socat_pid"
+ fi
else
_PYTHON=""
if _exists "python3"; then
@@ -2772,21 +2799,40 @@ printf '%s' '$content';" 2>"$_SOCAT_ERR" &
fi
if [ "$_PYTHON" ]; then
_debug "Using python: $_PYTHON"
- _AF="socket.AF_INET"
- _BIND_ADDR="0.0.0.0"
- if [ "$Le_Listen_V6" ]; then
- _AF="socket.AF_INET6"
+ #a comma separated list of addresses to listen on, one socket for each
+ _BIND_ADDR="0.0.0.0,::"
+ if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then
_BIND_ADDR="::"
+ elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then
+ _BIND_ADDR="0.0.0.0"
fi
if [ "$ncaddr" ]; then
_BIND_ADDR="$ncaddr"
fi
+ _debug "_BIND_ADDR" "$_BIND_ADDR"
export _SOCAT_ERR="$(_mktemp)"
- $_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3];
+ $_PYTHON -c "import socket,sys,select
+res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3]
+ads=sys.argv[2].split(',')
+ls=[]
+for ad in ads:
+ try:
+ sk=socket.socket(socket.AF_INET6 if ':' in ad else socket.AF_INET,socket.SOCK_STREAM)
+ sk.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
+ if ':' in ad and len(ads)>1:
+ sk.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1)
+ sk.bind((ad,int(sys.argv[1])))
+ sk.listen(5)
+ ls.append(sk)
+ except Exception:
+ sys.stderr.write(str(sys.exc_info()[1])+'\n')
+if not ls:
+ sys.exit(1)
while True:
- c,a=s.accept()
- c.sendall(res.encode() if hasattr(res, 'encode') else res)
- c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
+ for sk in select.select(ls,[],[])[0]:
+ c,a=sk.accept()
+ c.sendall(res.encode() if hasattr(res, 'encode') else res)
+ c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
serverproc="$!"
_NC="$_PYTHON"
else
@@ -2809,6 +2855,11 @@ while True:
_stopserver() {
pid="$1"
_debug "pid" "$pid"
+ if [ "$_serverproc_v6" ]; then
+ _debug "_serverproc_v6" "$_serverproc_v6"
+ kill $_serverproc_v6 >/dev/null 2>&1
+ _serverproc_v6=""
+ fi
if [ -z "$pid" ]; then
rm -f "$_SOCAT_ERR"
return
@@ -2882,9 +2933,11 @@ _starttlsserver() {
_debug Le_Listen_V4 "$Le_Listen_V4"
_debug Le_Listen_V6 "$Le_Listen_V6"
- if [ "$Le_Listen_V4" ]; then
+ #openssl s_server binds a single socket, so both options together can only
+ #mean: do not force a family, same as when neither of them is given.
+ if [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then
__S_OPENSSL="$__S_OPENSSL -4"
- elif [ "$Le_Listen_V6" ]; then
+ elif [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then
__S_OPENSSL="$__S_OPENSSL -6"
fi
@@ -4355,6 +4408,24 @@ deactivateaccount() {
fi
}
+#domain
+#Print the Validation Domain Name where the persistent TXT record must be
+#published: the "_validation-persist" label prepended to the domain being
+#validated (draft-ietf-acme-dns-persist-01 sec 4).
+#A wildcard identifier is validated by the record at its base domain, so the
+#leading "*." label is dropped: the wildcard scope comes from 'policy=wildcard'
+#in the record value, not from a "*" label in the record name (sec 5.1, 10.2).
+_dns_persist_txt_name() {
+ _dpt_domain="$1"
+ if _startswith "$_dpt_domain" "*."; then
+ _dpt_domain="$(echo "$_dpt_domain" | sed 's/^\*\.//')"
+ fi
+ if [ -z "$_dpt_domain" ]; then
+ return 1
+ fi
+ echo "_validation-persist.$_dpt_domain"
+}
+
#domain wildcard ca_name days
#Print the TXT record(s) the user must add to enable persistent DNS validation
#per draft-ietf-acme-dns-persist-01.
@@ -4369,6 +4440,20 @@ makednspersistvalue() {
return 1
fi
+ _txt_name="$(_dns_persist_txt_name "$_mdpv_domain")"
+ if [ -z "$_txt_name" ]; then
+ _err "Invalid domain: $_mdpv_domain"
+ return 1
+ fi
+ _debug _txt_name "$_txt_name"
+
+ #A wildcard identifier can only be issued if the record carries
+ #'policy=wildcard', so don't print a record that is guaranteed to fail.
+ if _startswith "$_mdpv_domain" "*." && [ "$_mdpv_wildcard" != "1" ]; then
+ _info "$_mdpv_domain is a wildcard domain, adding 'policy=wildcard' automatically."
+ _mdpv_wildcard="1"
+ fi
+
if [ -n "$_mdpv_days" ]; then
case "$_mdpv_days" in
'' | *[!0-9]*)
@@ -4400,8 +4485,6 @@ makednspersistvalue() {
fi
_debug "Account URL" "$_accUri"
- _txt_name="_validation-persist.$_mdpv_domain"
-
_txt_suffix="; accounturi=$_accUri"
if [ "$_mdpv_wildcard" = "1" ]; then
_txt_suffix="$_txt_suffix; policy=wildcard"
@@ -4919,11 +5002,18 @@ issue() {
if [ -z "$_ACME_IS_RENEW" ]; then
_initpath "$_main_domain" "$_key_length"
mkdir -p "$DOMAIN_PATH"
- elif ! _hasfield "$_web_roots" "$W_DNS"; then
+ elif [ -z "$Le_Vlist" ]; then
+ # Whether the saved order is resumed is decided by Le_Vlist below, so key
+ # this on Le_Vlist too. With no pending order to resume a new one is
+ # created, and a stale order link from the previous issuance must not be
+ # reused. https://github.com/acmesh-official/acme.sh/issues/3635
Le_OrderFinalize=""
Le_LinkOrder=""
- Le_LinkCert=""
fi
+ # Per-run state only: it is set after finalize and never read back from the
+ # saved domain conf. Carrying it over would make a run that gives up while
+ # the order is still 'processing' download the previous certificate again.
+ Le_LinkCert=""
if _hasfield "$_web_roots" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then
_err "$_DNS_MANUAL_ERROR"
@@ -5949,12 +6039,17 @@ $_authorizations_map"
_clearaccountconf "HTTPS_INSECURE"
fi
- if [ "$Le_Listen_V4" ]; then
- _savedomainconf "Le_Listen_V4" "$Le_Listen_V4"
- _cleardomainconf Le_Listen_V6
- elif [ "$Le_Listen_V6" ]; then
- _savedomainconf "Le_Listen_V6" "$Le_Listen_V6"
- _cleardomainconf Le_Listen_V4
+ if [ "$Le_Listen_V4" ] || [ "$Le_Listen_V6" ]; then
+ if [ "$Le_Listen_V4" ]; then
+ _savedomainconf "Le_Listen_V4" "$Le_Listen_V4"
+ else
+ _cleardomainconf Le_Listen_V4
+ fi
+ if [ "$Le_Listen_V6" ]; then
+ _savedomainconf "Le_Listen_V6" "$Le_Listen_V6"
+ else
+ _cleardomainconf Le_Listen_V6
+ fi
fi
if [ "$Le_ForceNewDomainKey" = "1" ]; then
@@ -7369,10 +7464,18 @@ deactivate() {
done
}
+#reads the output of "openssl x509 -text" from stdin, prints the hex AKI
+#the value is on the line right after the extension header; "grep -A" is not
+#portable (Solaris /usr/bin/grep: "illegal option -- A"), so select from the
+#header to EOF and keep the second line of that range
+_extractAKI() {
+ sed -n '/X509v3 Authority Key Identifier/,$p' | _head_n 2 | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
+}
+
#cert
_getAKI() {
_cert="$1"
- ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep -A 1 "X509v3 Authority Key Identifier" | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
+ ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | _extractAKI
}
#cert
@@ -7528,6 +7631,15 @@ _installalias() {
_c_home="$1"
_initpath
+ _alias_bin="$LE_WORKING_DIR/$PROJECT_ENTRY"
+ if [ ! -f "$_alias_bin" ]; then
+ #ACME_PACKAGED install: no copy in LE_WORKING_DIR, alias the current script
+ _script="$(_readlink "$_SCRIPT_")"
+ if [ -f "$_script" ]; then
+ _alias_bin="$_script"
+ fi
+ fi
+
_envfile="$LE_WORKING_DIR/$PROJECT_ENTRY.env"
if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then
echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile"
@@ -7545,7 +7657,7 @@ _installalias() {
else
_sed_i "/^export LE_CONFIG_HOME/d" "$_envfile"
fi
- _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\""
+ _setopt "$_envfile" "alias $PROJECT_ENTRY" "=" "\"$_alias_bin$_c_entry\""
if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" ]; then
#the completion file does nothing when sourced by a non-bash shell
_setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\""
@@ -7572,7 +7684,7 @@ _installalias() {
else
_sed_i "/^setenv LE_CONFIG_HOME/d" "$_cshfile"
fi
- _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\""
+ _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$_alias_bin$_c_entry\""
_setopt "$_csh_profile" "source \"$_cshfile\""
fi
@@ -7584,7 +7696,7 @@ _installalias() {
if [ "$_c_home" ]; then
_setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\""
fi
- _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$LE_WORKING_DIR/$PROJECT_ENTRY$_c_entry\""
+ _setopt "$_cshfile" "alias $PROJECT_ENTRY" " " "\"$_alias_bin$_c_entry\""
_setopt "$_tcsh_profile" "source \"$_cshfile\""
fi
@@ -7658,30 +7770,38 @@ install() {
chmod 700 "$LE_CONFIG_HOME"
fi
- cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY"
+ if [ "$ACME_PACKAGED" ]; then
+ #the script and its hooks are managed by a system package manager,
+ #do not copy them into LE_WORKING_DIR. https://github.com/acmesh-official/acme.sh/issues/7135
+ _info "ACME_PACKAGED is set, skipping the script copy."
+ else
+ cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY"
- if [ "$?" != "0" ]; then
- _err "Installation failed, cannot copy $PROJECT_ENTRY"
- return 1
- fi
+ if [ "$?" != "0" ]; then
+ _err "Installation failed, cannot copy $PROJECT_ENTRY"
+ return 1
+ fi
- _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY"
+ _info "Installed to $LE_WORKING_DIR/$PROJECT_ENTRY"
- if [ -f "$PROJECT_ENTRY.completion" ]; then
- cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/"
- _debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion"
+ if [ -f "$PROJECT_ENTRY.completion" ]; then
+ cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/"
+ _debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion"
+ fi
fi
if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then
_installalias "$_c_home"
fi
- for subf in $_SUB_FOLDERS; do
- if [ -d "$subf" ]; then
- mkdir -p "$LE_WORKING_DIR/$subf"
- cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/
- fi
- done
+ if [ -z "$ACME_PACKAGED" ]; then
+ for subf in $_SUB_FOLDERS; do
+ if [ -d "$subf" ]; then
+ mkdir -p "$LE_WORKING_DIR/$subf"
+ cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/
+ fi
+ done
+ fi
if [ ! -f "$ACCOUNT_CONF_PATH" ]; then
_initconf
@@ -7709,7 +7829,7 @@ install() {
installcronjob "$_c_home"
fi
- if [ -z "$NO_DETECT_SH" ]; then
+ if [ -z "$NO_DETECT_SH" ] && [ -z "$ACME_PACKAGED" ]; then
#Modify shebang
if _exists bash; then
_bash_path="$(bash -c "command -v bash 2>/dev/null")"
@@ -7734,7 +7854,9 @@ install() {
if [ "$_accountemail" ]; then
_saveaccountconf "ACCOUNT_EMAIL" "$_accountemail"
fi
- _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)"
+ if [ -z "$ACME_PACKAGED" ]; then
+ _saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)"
+ fi
_info OK
}
@@ -7748,8 +7870,12 @@ uninstall() {
_uninstallalias
- rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY"
- rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion"
+ if [ -z "$ACME_PACKAGED" ]; then
+ #don't remove the script when it is managed by a system package manager,
+ #LE_WORKING_DIR may point to the packaged files
+ rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY"
+ rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion"
+ fi
_info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\". You can remove them by yourself."
}
@@ -7785,20 +7911,24 @@ cron() {
_initpath
_info "$(__green "===Starting cron===")"
if [ "$AUTO_UPGRADE" = "1" ]; then
- export LE_WORKING_DIR
- (
- if ! upgrade; then
- _err "Cron: Upgrade failed!"
- return 1
+ if [ "$ACME_PACKAGED" ]; then
+ _info "ACME_PACKAGED is set, skipping the auto upgrade."
+ else
+ export LE_WORKING_DIR
+ (
+ if ! upgrade; then
+ _err "Cron: Upgrade failed!"
+ return 1
+ fi
+ )
+ . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null
+
+ if [ -t 1 ]; then
+ __INTERACTIVE="1"
fi
- )
- . "$LE_WORKING_DIR/$PROJECT_ENTRY" >/dev/null
- if [ -t 1 ]; then
- __INTERACTIVE="1"
+ _info "Automatically upgraded to: $VER"
fi
-
- _info "Automatically upgraded to: $VER"
fi
_TREAT_SKIP_AS_SUCCESS="1"
renewAll
@@ -8040,7 +8170,9 @@ Parameters:
--dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the
generated TXT record so the issuer is also authorized for wildcards
- and subdomains (draft-ietf-acme-dns-persist-01).
+ and subdomains (draft-ietf-acme-dns-persist-01). It is implied when
+ the domain given to -d is a wildcard (e.g. '*.example.com'); the
+ record itself is always published at the base domain.
--dns-persist-ca-name Used with '--make-dns-persist-value'. Use the given CA identity domain
(e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If
omitted, the identities are read from the ACME directory's
@@ -8098,8 +8230,9 @@ Parameters:
--ocsp, --ocsp-must-staple Generate OCSP-Must-Staple extension.
--always-force-new-domain-key Generate new domain key on renewal. Otherwise, the domain key is not changed by default.
--auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted.
- --listen-v4 Force standalone/tls server to listen at ipv4.
- --listen-v6 Force standalone/tls server to listen at ipv6.
+ --listen-v4 Force standalone/tls server to listen at ipv4 only.
+ By default the standalone server listens on both ipv4 and ipv6.
+ --listen-v6 Force standalone/tls server to listen at ipv6 only.
--request-v4 Force client requests to use ipv4 to connect to the CA server.
--request-v6 Force client requests to use ipv6 to connect to the CA server.
--openssl-bin Specifies a custom openssl bin location.
@@ -8128,6 +8261,10 @@ Parameters:
}
installOnline() {
+ if [ "$ACME_PACKAGED" ]; then
+ _err "ACME_PACKAGED is set: acme.sh is managed by the system package manager, please use it to upgrade."
+ return 1
+ fi
_info "Installing from online archive."
_branch="$BRANCH"
@@ -8185,6 +8322,10 @@ _getUpgradeHash() {
}
upgrade() {
+ if [ "$ACME_PACKAGED" ]; then
+ _err "ACME_PACKAGED is set: acme.sh is managed by the system package manager, please use it to upgrade."
+ exit 1
+ fi
if (
_initpath
[ -z "$FORCE" ] && [ "$(_getUpgradeHash)" = "$(_readaccountconf "UPGRADE_HASH")" ] && _info "Already up to date!" && exit 0
diff --git a/deploy/cpanel_uapi.sh b/deploy/cpanel_uapi.sh
index 156044eb..02ef6b3e 100644
--- a/deploy/cpanel_uapi.sh
+++ b/deploy/cpanel_uapi.sh
@@ -87,7 +87,11 @@ cpanel_uapi_deploy() {
# Auto mode
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
# call API for site config
- _response=$(uapi DomainInfo list_domains)
+ if [ -n "$_uapi_user" ]; then
+ _response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
+ else
+ _response=$(uapi DomainInfo list_domains)
+ fi
# exit if error in response
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate - cannot retrieve sitelist:"
diff --git a/deploy/ikuai.sh b/deploy/ikuai.sh
new file mode 100644
index 00000000..fa0926dc
--- /dev/null
+++ b/deploy/ikuai.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env sh
+
+# Here is a script to deploy cert to ikuai using curl
+#
+# it requires following environment variables:
+#
+# IKUAI_SCHEME="http" - http or https , defaults to "http"
+# IKUAI_HOSTNAME="localhost" - host , defaults to "192.168.9.1"
+# IKUAI_PORT="80" - port , defaults to "80"
+# IKUAI_USERNAME="admin" - username , defaults to "admin"
+# IKUAI_PASSWORD="yourPassword" - password
+# IKUAI_CERT_ID=1 - ikuai cert id , defaults to 1, and only 1 is supported for now !!!
+#
+#returns 0 means success, otherwise error.
+#
+######## Public functions #####################
+#
+#domain keyfile certfile cafile fullchain
+ikuai_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"
+
+ # Get deploy conf
+ _getdeployconf IKUAI_SCHEME
+ _getdeployconf IKUAI_HOSTNAME
+ _getdeployconf IKUAI_PORT
+ _getdeployconf IKUAI_USERNAME
+ _getdeployconf IKUAI_PASSWORD
+ _getdeployconf IKUAI_CERT_ID
+
+ # Use default if not provided
+ [ -n "$IKUAI_SCHEME" ] || IKUAI_SCHEME="http"
+ [ -n "$IKUAI_HOSTNAME" ] || IKUAI_HOSTNAME="192.168.9.1"
+ [ -n "$IKUAI_PORT" ] || IKUAI_PORT=80
+ [ -n "$IKUAI_USERNAME" ] || IKUAI_USERNAME="admin"
+ [ -n "$IKUAI_CERT_ID" ] || IKUAI_CERT_ID=1
+
+ if [ -z "$IKUAI_PASSWORD" ]; then
+ _err "please define IKUAI_PASSWORD."
+ return 1
+ fi
+
+ _debug2 IKUAI_SCHEME "$IKUAI_SCHEME"
+ _debug2 IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
+ _debug2 IKUAI_PORT "$IKUAI_PORT"
+ _debug2 IKUAI_USERNAME "$IKUAI_USERNAME"
+ _secure_debug2 IKUAI_PASSWORD "$IKUAI_PASSWORD"
+
+ _info "Login to ikuai ..."
+ _ikuai_url="$IKUAI_SCHEME://$IKUAI_HOSTNAME:$IKUAI_PORT"
+ _pass_md5="$(printf "%s" "$IKUAI_PASSWORD" | _digest md5 hex | _lower_case)"
+ _pass_salt="$(printf "salt_11%s" "$IKUAI_PASSWORD" | _base64)"
+ _debug2 _ikuai_url "$_ikuai_url"
+
+ _login_req="{\"username\":\"$IKUAI_USERNAME\",\"passwd\":\"$_pass_md5\",\"pass\":\"$_pass_salt\",\"remember_password\":\"\"}"
+ _response=$(_post "$_login_req" "$_ikuai_url/Action/login" "" "POST" "application/json")
+
+ _err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
+ # check ErrMsg
+ if [ "$_err_msg" != "Success" ]; then
+ _err "Failed to login to ikuai: $_err_msg"
+ return 1
+ fi
+ # check cookie
+ _cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | sed 's/;.*//')"
+ if [ -z "$_cookie" ]; then
+ _err "Fail to get the cookie."
+ return 1
+ fi
+
+ # Set cookie header
+ _H1="Cookie: $_cookie; username=$IKUAI_USERNAME; login=1"
+
+ _info "Deploy the cert to ikuai ... "
+
+ # Should replace \n to @ ," " to #
+ _cert_content_single_line="$(tr <"$_cfullchain" '\n' '@' | tr ' ' '#')"
+ _key_content_single_line="$(tr <"$_ckey" '\n' '@' | tr ' ' '#')"
+
+ _debug2 _cert_content_single_line "$_cert_content_single_line"
+ _secure_debug2 _key_content_single_line "$_key_content_single_line"
+
+ _key_manager_req="{\"func_name\":\"key_manager\",\"action\":\"save\",\"param\":{\"ca\":\"$_cert_content_single_line\",\"key\":\"$_key_content_single_line\",\"id\":$IKUAI_CERT_ID,\"enabled\":\"yes\",\"comment\":\"\"}}"
+ _response=$(_post "$_key_manager_req" "$_ikuai_url/Action/call" "" "POST" "application/json")
+
+ _err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
+ # check ErrMsg
+ if [ "$_err_msg" != "Success" ]; then
+ _err "Failed to deploy the cert to ikuai: $_err_msg"
+ return 1
+ fi
+
+ _info "Save the deploy config ... "
+ # Save the config
+ _savedeployconf IKUAI_SCHEME "$IKUAI_SCHEME"
+ _savedeployconf IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
+ _savedeployconf IKUAI_PORT "$IKUAI_PORT"
+ _savedeployconf IKUAI_USERNAME "$IKUAI_USERNAME"
+ _savedeployconf IKUAI_PASSWORD "$IKUAI_PASSWORD"
+ _savedeployconf IKUAI_CERT_ID "$IKUAI_CERT_ID"
+
+ _info "Successfully deployed certificate to ikuai. Enjoy! :>"
+
+ return 0
+}
diff --git a/deploy/keyhelp.sh b/deploy/keyhelp.sh
index 97f9c21c..f66d27ce 100644
--- a/deploy/keyhelp.sh
+++ b/deploy/keyhelp.sh
@@ -83,7 +83,7 @@ keyhelp_deploy() {
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
_H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
- _message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '//,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
+ _message=$(echo "$_response" | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to upload certificate."
@@ -118,7 +118,7 @@ keyhelp_deploy() {
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
- _message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
+ _message=$(echo "$_response" | sed -n '/
/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to apply certificate."
diff --git a/deploy/multideploy.sh b/deploy/multideploy.sh
index 375668ec..4a8c9dc9 100644
--- a/deploy/multideploy.sh
+++ b/deploy/multideploy.sh
@@ -10,6 +10,10 @@
# Usage (shown values are the examples):
# 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
+# A name without a leading '/' is looked up in the certificate directory
+# of the domain. An absolute path is used as is, so a single deploy file
+# can be shared by all domains, e.g.
+# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml"
#
# 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com
@@ -49,7 +53,7 @@ multideploy_deploy() {
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
- MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}"
+ _getdeployconf MULTIDEPLOY_FILENAME
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
@@ -75,7 +79,8 @@ multideploy_deploy() {
# This function preprocesses the deploy file by checking if 'yq' is installed,
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments:
-# $@ - Posible deploy file names.
+# $@ - Posible deploy file names. A name starting with '/' is treated as an
+# absolute path, any other name is relative to the domain directory.
# Usage:
# _preprocess_deployfile "
" "?"
_preprocess_deployfile() {
@@ -87,15 +92,21 @@ _preprocess_deployfile() {
_debug3 "yq is installed."
# Check if deploy file exists
+ found_file=""
for file in "$@"; do
- _debug3 "Checking file" "$DOMAIN_PATH/$file"
- if [ -f "$DOMAIN_PATH/$file" ]; then
+ if _startswith "$file" "/"; then
+ _multideploy_path="$file"
+ else
+ _multideploy_path="$DOMAIN_PATH/$file"
+ fi
+ _debug3 "Checking file" "$_multideploy_path"
+ if [ -f "$_multideploy_path" ]; then
_debug3 "File found"
if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file."
return 1
fi
- found_file="$file"
+ found_file="$_multideploy_path"
else
_debug3 "File not found"
fi
@@ -105,12 +116,12 @@ _preprocess_deployfile() {
_err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one."
return 1
fi
- if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then
- _err "Deploy file is not valid: $DOMAIN_PATH/$found_file"
+ if ! _check_deployfile "$found_file"; then
+ _err "Deploy file is not valid: $found_file"
return 1
fi
- echo "$DOMAIN_PATH/$found_file"
+ echo "$found_file"
}
# Description:
diff --git a/deploy/proxmoxbs.sh b/deploy/proxmoxbs.sh
index 179b0369..30599a44 100644
--- a/deploy/proxmoxbs.sh
+++ b/deploy/proxmoxbs.sh
@@ -116,17 +116,24 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
+ _retval=$?
+ # The API errors out with a non-2xx HTTP status and an empty body,
+ # so the status line is checked too, not only the response body.
+ _status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ _debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
- _retval=$?
- if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
- _debug3 response "$response"
- _info "Certificate successfully deployed"
- return 0
- else
- _err "Certificate deployment failed: $message"
- _debug "Response" "$response"
- return 1
- fi
+ case "$_status_code" in
+ 2[0-9][0-9])
+ if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
+ _debug3 response "$response"
+ _info "Certificate successfully deployed"
+ return 0
+ fi
+ ;;
+ esac
+ _err "Certificate deployment failed (HTTP status $_status_code). $message"
+ _debug "Response" "$response"
+ return 1
}
diff --git a/deploy/proxmoxve.sh b/deploy/proxmoxve.sh
index b6298ee7..fd8d69d8 100644
--- a/deploy/proxmoxve.sh
+++ b/deploy/proxmoxve.sh
@@ -128,17 +128,24 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
+ _retval=$?
+ # The API errors out with a non-2xx HTTP status and an empty body,
+ # so the status line is checked too, not only the response body.
+ _status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ _debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
- _retval=$?
- if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
- _debug3 response "$response"
- _info "Certificate successfully deployed"
- return 0
- else
- _err "Certificate deployment failed: $message"
- _debug "Response" "$response"
- return 1
- fi
+ case "$_status_code" in
+ 2[0-9][0-9])
+ if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
+ _debug3 response "$response"
+ _info "Certificate successfully deployed"
+ return 0
+ fi
+ ;;
+ esac
+ _err "Certificate deployment failed (HTTP status $_status_code). $message"
+ _debug "Response" "$response"
+ return 1
}
diff --git a/deploy/shelly.sh b/deploy/shelly.sh
new file mode 100644
index 00000000..dbdab346
--- /dev/null
+++ b/deploy/shelly.sh
@@ -0,0 +1,280 @@
+#!/usr/bin/env sh
+
+# Here is a script to deploy cert to a Shelly Gen3+ device.
+# Deploy the HTTPS server certificate to a Shelly device on the local network.
+#
+# ```sh
+# export SHELLY_HOST=192.168.1.100
+# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
+# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
+# ```
+#
+# Environment variables:
+# SHELLY_HOST (required) IP or hostname of the Shelly device
+# SHELLY_PASSWORD (optional) Admin password for digest authentication.
+# Omit if auth is disabled on the device.
+# SHELLY_USER (optional) Username for auth. Default: admin
+# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
+# Default: 1 (reboot after upload)
+#
+# Requirements:
+# - Shelly Gen3+ device (Gen4 recommended)
+# - Firmware 2.0.0+ for HTTPS server certificate support
+# - curl or wget
+# - openssl (for SHA-256 digest and random cnonce)
+#
+# The device must be reachable via HTTP on the local network.
+# The hook uploads the fullchain.pem and private key,
+# then reboots the device to apply the new certificate.
+#
+# Authentication uses standard RFC 7616 HTTP Digest (SHA-256) since
+# firmware 2.0.0. The JSON-RPC auth object is not used for HTTP transport.
+#
+# returns 0 means success, otherwise error.
+
+######## Public functions #####################
+
+#domain keyfile certfile cafile fullchain
+shelly_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"
+
+ _getdeployconf SHELLY_HOST
+ _getdeployconf SHELLY_PASSWORD
+ _getdeployconf SHELLY_USER
+ _getdeployconf SHELLY_REBOOT
+
+ _debug SHELLY_HOST "$SHELLY_HOST"
+ _debug SHELLY_USER "$SHELLY_USER"
+ _secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
+ _debug SHELLY_REBOOT "$SHELLY_REBOOT"
+
+ if [ -z "$SHELLY_HOST" ]; then
+ _err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
+ return 1
+ fi
+
+ SHELLY_USER="${SHELLY_USER:-admin}"
+ SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
+
+ _savedeployconf SHELLY_HOST "$SHELLY_HOST"
+ _savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
+ _savedeployconf SHELLY_USER "$SHELLY_USER"
+ _savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
+
+ # --- Auth handshake (only if password is set) ---
+ _shelly_auth_header=""
+ if [ -n "$SHELLY_PASSWORD" ]; then
+ _info "Authenticating to Shelly device at $SHELLY_HOST"
+ if ! _shelly_handshake; then
+ _err "Authentication handshake failed. Check SHELLY_PASSWORD and device accessibility."
+ return 1
+ fi
+ _info "Authentication successful"
+ fi
+
+ # --- Upload certificate ---
+ _info "Uploading certificate to Shelly device at $SHELLY_HOST"
+ if ! _shelly_upload_cert; then
+ _err "Certificate upload failed"
+ return 1
+ fi
+
+ # --- Upload key ---
+ _info "Uploading private key to Shelly device"
+ if ! _shelly_upload_key; then
+ _err "Private key upload failed"
+ return 1
+ fi
+
+ _info "Certificate and key uploaded successfully"
+
+ # --- Reboot ---
+ if [ "$SHELLY_REBOOT" != "0" ]; then
+ _info "Rebooting Shelly device to apply certificate"
+ # Reboot may close the connection before sending a response
+ _shelly_rpc "Shelly.Reboot" '{}' || _debug "Reboot may have closed connection (expected)"
+ _info "Reboot command sent. Device will restart shortly."
+ else
+ _info "Skipping reboot (SHELLY_REBOOT=0). Certificate will apply on next restart."
+ fi
+
+ # Clear auth header so it does not leak to other hooks
+ export _H1=""
+
+ return 0
+}
+
+# --- Helper functions ---
+
+# Perform RFC 7616 HTTP Digest auth handshake.
+# Sets _shelly_auth_header on success (the Authorization header value).
+_shelly_handshake() {
+ _inithttp
+
+ _debug "Probing device for auth challenge"
+
+ # Use a protected method (Shelly.GetStatus) to trigger 401.
+ # Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
+ _post '{"id":1,"method":"Shelly.GetStatus"}' \
+ "http://${SHELLY_HOST}/rpc" "" "" "application/json"
+
+ # Detect auth from HTTP status line rather than response body
+ if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
+ # No auth challenge โ device accepted the request without credentials
+ _debug "Device responded without auth challenge. Proceeding without auth."
+ return 0
+ fi
+
+ _shelly_realm="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*realm="//;s/".*//')"
+ _shelly_nonce="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*nonce="//;s/".*//')"
+ _shelly_qop="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*qop="//;s/".*//')"
+
+ if [ -z "$_shelly_nonce" ]; then
+ _err "Failed to extract nonce from WWW-Authenticate header. Is SHELLY_PASSWORD correct?"
+ return 1
+ fi
+
+ _shelly_qop="${_shelly_qop:-auth}"
+
+ _debug "Shelly realm: $_shelly_realm"
+ _debug "Shelly qop: $_shelly_qop"
+ _secure_debug "Shelly nonce" "$_shelly_nonce"
+
+ # ha1 = SHA256(username:realm:password)
+ _shelly_ha1="$(printf '%s' "${SHELLY_USER}:${_shelly_realm}:${SHELLY_PASSWORD}" | _digest sha256 hex)"
+ _secure_debug "Shelly ha1" "$_shelly_ha1"
+
+ # Generate client nonce (openssl is required for _digest, so always available)
+ _shelly_cnonce="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 8 2>/dev/null)"
+ _debug "Shelly cnonce: $_shelly_cnonce"
+
+ # Build the digest Authorization header value (stored for reuse)
+ _shelly_nc=1
+ _shelly_build_auth_header
+
+ return 0
+}
+
+# Check whether the HTTP response headers contain a digest auth challenge.
+# Returns 0 (true) if a 401 with WWW-Authenticate is present.
+_shelly_has_auth_challenge() {
+ _shelly_headers_file="$1"
+ _shelly_status="$(grep -i '^HTTP/' "$_shelly_headers_file" | _tail_n 1 | awk '{print $2}')"
+ [ "$_shelly_status" = "401" ] && grep -qi '^WWW-Authenticate:' "$_shelly_headers_file"
+}
+
+# Build or rebuild the RFC 7616 Authorization header.
+# Uses: _shelly_ha1, _shelly_nonce, _shelly_cnonce, _shelly_qop, _shelly_realm, _shelly_nc
+# Sets: _shelly_auth_header
+_shelly_build_auth_header() {
+ _shelly_nc_hex="$(printf '%08x' "$_shelly_nc")"
+
+ # ha2 = SHA256(POST:/rpc)
+ _shelly_ha2="$(printf '%s' "POST:/rpc" | _digest sha256 hex)"
+
+ # response = SHA256(ha1:nonce:nc:cnonce:qop:ha2)
+ _shelly_digest_response="$(printf '%s' "${_shelly_ha1}:${_shelly_nonce}:${_shelly_nc_hex}:${_shelly_cnonce}:${_shelly_qop}:${_shelly_ha2}" | _digest sha256 hex)"
+
+ # Build the Authorization header value (without the "Authorization: " prefix)
+ _shelly_auth_header="Digest username=\"${SHELLY_USER}\", realm=\"${_shelly_realm}\", nonce=\"${_shelly_nonce}\", uri=\"/rpc\", qop=${_shelly_qop}, nc=${_shelly_nc_hex}, cnonce=\"${_shelly_cnonce}\", response=\"${_shelly_digest_response}\", algorithm=SHA-256"
+
+ _secure_debug "Authorization header" "$_shelly_auth_header"
+}
+
+# Make a Shelly JSON-RPC call.
+# Usage: _shelly_rpc
+# Returns 0 on success, 1 on error.
+_shelly_rpc() {
+ _shelly_method="$1"
+ _shelly_params="$2"
+
+ _shelly_body='{"id":1,"method":"'"$_shelly_method"'","params":'"$_shelly_params"'}'
+
+ _debug "RPC method: $_shelly_method"
+ _debug2 "RPC body: $_shelly_body"
+
+ # shellcheck disable=SC2090
+ if [ -n "$_shelly_auth_header" ]; then
+ export _H1="Authorization: $_shelly_auth_header"
+ else
+ export _H1=""
+ fi
+
+ _post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
+ _shelly_ret=$?
+
+ if [ "$_shelly_ret" != "0" ]; then
+ _err "HTTP request failed for $_shelly_method (curl/wget error $_shelly_ret)"
+ return 1
+ fi
+
+ # Empty response means something went wrong (auth required but not provided, etc.)
+ if [ -z "$response" ]; then
+ _err "Empty response from Shelly device. If authentication is enabled on the device, set SHELLY_PASSWORD."
+ return 1
+ fi
+
+ # Validate response looks like a Shelly JSON-RPC response.
+ # Catches non-JSON responses such as HTTP 429 "Too Many Requests" which
+ # would otherwise pass the empty and "error" checks below.
+ if ! _startswith "$response" '{' || ! _contains "$response" '"id"'; then
+ _err "Invalid response from Shelly device: $response"
+ return 1
+ fi
+
+ # Check for JSON-RPC error in response
+ if _contains "$response" '"error"'; then
+ _err "RPC error from Shelly: $response"
+ return 1
+ fi
+
+ _debug "RPC response: $response"
+
+ # Increment nonce counter and rebuild auth header for next request
+ if [ -n "$_shelly_auth_header" ]; then
+ _shelly_nc=$((_shelly_nc + 1))
+ _shelly_build_auth_header
+ fi
+
+ return 0
+}
+
+# Upload the certificate to the device.
+# Note: We do NOT clear the existing certificate first, because the Shelly
+# auto-removes all three files (cert, key, CA) when any one is cleared.
+# Uploading overwrites in place โ no clearing needed.
+_shelly_upload_cert() {
+ _shelly_cert_data="$(_json_encode <"$_cfullchain")"
+
+ _debug "Uploading certificate"
+ if ! _shelly_rpc "Shelly.PutHTTPServerCert" '{"data":"'"$_shelly_cert_data"'"}'; then
+ _err "Failed to upload certificate to device"
+ return 1
+ fi
+
+ return 0
+}
+
+# Upload the private key to the device.
+# Note: Do not clear first โ see _shelly_upload_cert for rationale.
+_shelly_upload_key() {
+ _shelly_key_data="$(_json_encode <"$_ckey")"
+
+ _debug "Uploading key"
+ if ! _shelly_rpc "Shelly.PutHTTPServerKey" '{"data":"'"$_shelly_key_data"'"}'; then
+ _err "Failed to upload key to device"
+ return 1
+ fi
+
+ return 0
+}
diff --git a/deploy/synology_dsm.sh b/deploy/synology_dsm.sh
index d05e503a..336980a5 100644
--- a/deploy/synology_dsm.sh
+++ b/deploy/synology_dsm.sh
@@ -344,6 +344,7 @@ synology_dsm_deploy() {
else
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
fi
+ _logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@@ -354,6 +355,7 @@ synology_dsm_deploy() {
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
+ _logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@@ -389,13 +391,13 @@ synology_dsm_deploy() {
else
_info "Restart HTTP services not necessary."
fi
- _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_logout
+ _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 0
else
- _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_err "Unable to update certificate, got error response: $response."
_logout
+ _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
}
@@ -403,6 +405,8 @@ synology_dsm_deploy() {
#################### Private functions below ##################################
_logout() {
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
+ # Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out.
+ # Note: this overwrites $response, so print any error message that needs it before calling.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
_debug3 response "$response"
}
diff --git a/deploy/unifios.sh b/deploy/unifios.sh
new file mode 100644
index 00000000..82b65c69
--- /dev/null
+++ b/deploy/unifios.sh
@@ -0,0 +1,307 @@
+#!/usr/bin/env sh
+# Deploy hook for UniFi OS Server (self-hosted).
+#
+# Supports:
+# - UniFi OS Server on macOS
+# - UniFi OS Server on Linux
+# - UniFi OS Server on Windows should also work (runs under WSL2), but
+# has not been tested.
+#
+# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
+#
+# This is a different product from the Cloud Key / UDM hardware and
+# self-hosted Unifi Controller covered by the `unifi` deploy hook above
+# (that hook already covers Cloud Key running UnifiOS v2.0.0+/Gen2/2+) --
+# this hook targets the separately-installed, self-hosted "UniFi OS Server"
+# application instead, which stores certificates in its own Postgres
+# database via a REST API rather than a Java keystore, so the `unifi`
+# hook's approach does not apply here.
+#
+# UniFi OS Server exposes a REST API on its management port (default
+# 11443) that its own web UI uses for certificate management:
+# POST /api/auth/login - session login (cookie + JWT)
+# GET /api/userCertificates - list uploaded certificates
+# POST /api/userCertificates - upload a new certificate
+# DELETE /api/userCertificates/{id} - remove a certificate
+# PUT /api/userCertificates/{id}/status - activate/deactivate a certificate
+#
+# This was reverse-engineered from the browser's Network tab while using the
+# real GUI upload/activate/delete flow -- it is undocumented but is the same
+# code path the UI uses, so it's far more robust than editing settings.yaml,
+# http/local-certs.conf, or the underlying Postgres user_certificates table
+# directly (all of which are also touched by this API, but only as a result
+# of the app's own internal logic, which handles cert parsing, active-cert
+# bookkeeping, and nginx config regeneration correctly on its own).
+#
+# Auth: POST /api/auth/login returns a `TOKEN` cookie containing a JWT whose
+# payload has a `csrfToken` claim. That value must be echoed back as the
+# `x-csrf-token` header on every subsequent state-changing request (a classic
+# double-submit CSRF pattern). No other cookies were found to be necessary.
+#
+# Uses core acme.sh helpers throughout (_post/_get, _json_encode,
+# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or
+# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all
+# honored the same as every other hook. The management API's cert is
+# self-signed (it's a management-only port, not meant for public exposure),
+# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see
+# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS
+# verification for the rest of the acme.sh run, e.g. the connection to the
+# ACME CA.
+#
+# Design: This hook does not save a certificate ID between renewals. Each
+# upload gets a name unique to that run: the domain name plus a timestamp.
+# This name never collides with an entry from a previous deploy. This is
+# true even if that entry is still active. The hook uploads and activates
+# the new certificate before it removes any old entries. If a failure
+# occurs during this process, the server still has a valid, active
+# certificate. The hook removes old entries only after activation is
+# complete. It removes only entries whose name starts with the domain name,
+# because this is the hook's own naming convention. As a result, this step
+# can only affect entries that this hook created for this domain. It can
+# never affect a certificate that a user uploaded manually, and it can
+# never affect a self-signed certificate.
+#
+# Settings:
+# DEPLOY_UNIFIOS_HOST - base URL of the management API
+# (default: "https://localhost:11443")
+# DEPLOY_UNIFIOS_USERNAME - UniFi OS Server admin username (required)
+# DEPLOY_UNIFIOS_PASSWORD - UniFi OS Server admin password (required)
+#
+# Example:
+# export DEPLOY_UNIFIOS_USERNAME="acmeuser"
+# export DEPLOY_UNIFIOS_PASSWORD="xxxxx"
+# acme.sh --deploy -d example.com --deploy-hook unifios
+#
+# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/7182
+
+_uos_response_code() {
+ # tr strips the trailing newline along with form feeds; re-terminate
+ # before the second _egrep_o, whose sed fallback (used wherever egrep -o
+ # is unavailable) drops an unterminated final line on some platforms.
+ _uos_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")"
+ printf '%s\n' "$_uos_code" | _egrep_o "^[0-9][0-9]*"
+}
+
+_uos_response_cookie() {
+ # $1 = cookie name
+ grep <"$HTTP_HEADER" -i "^Set-Cookie: *$1=" | _tail_n 1 | _egrep_o "$1=[^;]*" | _head_n 1
+}
+
+unifios_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"
+
+ # Scoped to this hook's own subshell -- does not affect the rest of the
+ # acme.sh run (e.g. the connection to the ACME CA).
+ export HTTPS_INSECURE=1
+
+ _getdeployconf DEPLOY_UNIFIOS_HOST
+ DEPLOY_UNIFIOS_HOST="${DEPLOY_UNIFIOS_HOST:-https://localhost:11443}"
+ _savedeployconf DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
+ _debug DEPLOY_UNIFIOS_HOST "$DEPLOY_UNIFIOS_HOST"
+
+ _getdeployconf DEPLOY_UNIFIOS_USERNAME
+ _getdeployconf DEPLOY_UNIFIOS_PASSWORD
+
+ if [ -z "$DEPLOY_UNIFIOS_USERNAME" ] || [ -z "$DEPLOY_UNIFIOS_PASSWORD" ]; then
+ _err "DEPLOY_UNIFIOS_USERNAME and DEPLOY_UNIFIOS_PASSWORD must be set."
+ return 1
+ fi
+ _debug DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME"
+ _secure_debug DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD"
+
+ _info "Logging in to UniFi OS Server API at $DEPLOY_UNIFIOS_HOST..."
+
+ # _json_encode always appends a trailing "\n" escape, even to input with
+ # no trailing newline (it normalizes via `echo`, unconditionally adding
+ # one). That's harmless for the key/cert file content below, which
+ # legitimately ends in a real newline anyway, but wrong for these plain
+ # strings -- strip the spurious escape it leaves behind.
+ _uos_user_json="$(printf '%s' "$DEPLOY_UNIFIOS_USERNAME" | _json_encode)"
+ _uos_user_json="${_uos_user_json%\\n}"
+ _uos_pass_json="$(printf '%s' "$DEPLOY_UNIFIOS_PASSWORD" | _json_encode)"
+ _uos_pass_json="${_uos_pass_json%\\n}"
+ _login_body="{\"username\":\"$_uos_user_json\",\"password\":\"$_uos_pass_json\",\"token\":\"\",\"rememberMe\":false}"
+
+ _login_json="$(_post "$_login_body" "$DEPLOY_UNIFIOS_HOST/api/auth/login" "" "POST" "application/json")"
+ _login_code="$(_uos_response_code)"
+
+ if [ "$_login_code" != "200" ]; then
+ _err "Login failed (HTTP $_login_code)."
+ _err "Response: $_login_json"
+ return 1
+ fi
+
+ # Credentials are proven correct now -- save them, rather than only at the
+ # very end, so a later step failing doesn't discard a working login.
+ # base64-encoded: _save_conf wraps values in single quotes with no
+ # escaping, so a literal "'" in the password would otherwise corrupt the
+ # domain conf (see deploy/synology_dsm.sh for the same pattern).
+ _savedeployconf DEPLOY_UNIFIOS_USERNAME "$DEPLOY_UNIFIOS_USERNAME" "base64"
+ _savedeployconf DEPLOY_UNIFIOS_PASSWORD "$DEPLOY_UNIFIOS_PASSWORD" "base64"
+
+ _uos_token="$(_uos_response_cookie TOKEN)"
+ if [ -z "$_uos_token" ]; then
+ _err "Login succeeded but no TOKEN cookie was returned."
+ return 1
+ fi
+
+ _H1="Cookie: $_uos_token"
+ export _H1
+
+ _uos_jwt_payload="$(echo "$_uos_token" | cut -d '=' -f 2- | cut -d '.' -f 2)"
+ _uos_csrf="$(_durl_replace_base64 "$_uos_jwt_payload" | _dbase64 | _egrep_o '"csrfToken":"[^"]*"' | cut -d '"' -f 4)"
+ if [ -z "$_uos_csrf" ]; then
+ _err "Could not extract csrfToken from session token."
+ return 1
+ fi
+
+ _H2="x-csrf-token: $_uos_csrf"
+ export _H2
+
+ _info "Uploading new certificate..."
+ # "name" is a purely cosmetic label -- the server never validates it
+ # against the certificate's actual CN/SAN, and accepts arbitrary text
+ # including spaces (confirmed: a cert for example.com served correctly
+ # after being uploaded under the unrelated name "totally unrelated label").
+ # The only constraint that matters here is uniqueness: the server rejects
+ # a second entry with a name it already has, so a bare domain name would
+ # collide with the previous deploy's entry on every renewal after the
+ # first. A full human-readable timestamp would make that obvious in the
+ # UI, but the certificate list's name column is fixed-width and doesn't
+ # wrap (confirmed against the real UI: a long name overlaps the Expires
+ # column and makes both unreadable), so keep the suffix short instead --
+ # Unix epoch seconds are still unique enough for this purpose.
+ _uos_name="$_cdomain $(_time)"
+ _uos_key_json="$(_json_encode <"$_ckey")"
+ _uos_cert_json="$(_json_encode <"$_cfullchain")"
+ _create_body="{\"name\":\"$_uos_name\",\"key\":\"$_uos_key_json\",\"cert\":\"$_uos_cert_json\"}"
+
+ _create_json="$(_post "$_create_body" "$DEPLOY_UNIFIOS_HOST/api/userCertificates" "" "POST" "application/json")"
+ _create_code="$(_uos_response_code)"
+
+ if [ "$_create_code" = "201" ]; then
+ _new_id="$(echo "$_create_json" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
+ if [ -z "$_new_id" ]; then
+ _err "Could not determine new certificate ID from upload response."
+ return 1
+ fi
+ elif [ "$_create_code" = "400" ] && echo "$_create_json" | grep -q "USER_CERTIFICATE_DUPLICATE"; then
+ # HTTP 400 alone just means "bad request" -- it's the USER_CERTIFICATE_DUPLICATE
+ # code in the response body, checked above, that actually confirms this.
+ # The name above is unique to this run, so a duplicate here can only be
+ # the server's other uniqueness constraint: this exact certificate (by
+ # fingerprint) already exists as some other entry -- most likely a retry
+ # after a prior run already uploaded it (a real renewal always produces a
+ # new fingerprint, so this shouldn't happen in normal cron use). The
+ # response body doesn't include the existing entry's id, so look it up
+ # by fingerprint instead.
+ # The API's own fingerprint field is SHA-1 (20 bytes), not SHA-256 --
+ # confirmed against a real response, e.g.
+ # "fingerprint":"FC:02:50:9C:3B:3F:B7:79:9D:CA:4D:7C:AC:92:E7:D5:EA:F1:3A:29"
+ # (20 colon-separated groups). _fingerprint (core helper) strips the
+ # colons that field has, so re-insert them rather than stripping the
+ # JSON's own colons, which would also remove the ones separating every
+ # key from its value.
+ _uos_fingerprint="$(_fingerprint "$_cfullchain" sha1)"
+ if [ -z "$_uos_fingerprint" ]; then
+ _err "Could not compute the certificate's fingerprint."
+ return 1
+ fi
+ _uos_fingerprint="$(echo "$_uos_fingerprint" | sed 's/\(..\)/\1:/g; s/:$//')"
+
+ _list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
+ _list_code="$(_uos_response_code)"
+ if [ "$_list_code" != "200" ]; then
+ _err "Failed to list existing certificates (HTTP $_list_code)."
+ _err "Response: $_list_json"
+ return 1
+ fi
+ # _normalizeJson collapses the response to one predictable line (no stray
+ # whitespace around colons, no embedded CR/LF the server might emit) but
+ # also strips the trailing newline entirely -- re-terminate before the
+ # split below, since some sed implementations drop an unterminated final
+ # line rather than processing it.
+ _list_json="$(echo "$_list_json" | _normalizeJson)"
+ # A literal embedded newline (not the two-character "\n", which GNU sed
+ # treats as a newline in the replacement but POSIX doesn't define and BSD
+ # sed emits literally) splits it one JSON object per line so grep can
+ # match a single certificate entry at a time.
+ _list_json="$(
+ printf '%s\n' "$_list_json" | sed 's/},{/},\
+{/g'
+ )"
+ _new_id="$(echo "$_list_json" | grep -F "\"fingerprint\":\"$_uos_fingerprint\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
+ if [ -z "$_new_id" ]; then
+ _err "Certificate upload rejected as a duplicate (server reported USER_CERTIFICATE_DUPLICATE), but no existing entry matching this fingerprint was found."
+ _err "Response: $_create_json"
+ return 1
+ fi
+ # Reusing the existing entry rather than deleting it and re-uploading
+ # under today's name+timestamp: the served content is identical either
+ # way, so replacing it would only cost an extra delete+create round trip
+ # for no functional benefit. The tradeoff is cosmetic -- this entry keeps
+ # whatever name it was given whenever it was originally uploaded, so it
+ # won't reflect today's date in the UI.
+ _info "Certificate already present as entry $_new_id; reusing it."
+ else
+ _err "Certificate upload failed (HTTP $_create_code)."
+ _err "Response: $_create_json"
+ return 1
+ fi
+
+ _info "Activating certificate $_new_id..."
+ _activate_json="$(_post '{"active":true}' "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_new_id/status" "" "PUT" "application/json")"
+ _activate_code="$(_uos_response_code)"
+
+ if [ "$_activate_code" != "200" ]; then
+ _err "Failed to activate new certificate (HTTP $_activate_code)."
+ _err "Response: $_activate_json"
+ return 1
+ fi
+
+ # UniFi OS Server activation is exclusive server-wide. Tests against the
+ # real API confirm this: activation of one entry deactivates whichever
+ # other entry was active before, no matter its name or domain. As a
+ # result, the server serves the certificate that this hook just activated.
+ # This certificate is already live. If the removal of old entries below
+ # fails, the hook logs the failure. The deploy does not fail because of
+ # this.
+ _info "Checking for old certificate entries to remove..."
+ _list_json="$(_get "$DEPLOY_UNIFIOS_HOST/api/userCertificates")"
+ _list_code="$(_uos_response_code)"
+ if [ "$_list_code" != "200" ]; then
+ _err "Failed to list certificates for cleanup (HTTP $_list_code) -- leaving old entries in place."
+ else
+ _list_json="$(echo "$_list_json" | _normalizeJson)"
+ _list_json="$(
+ printf '%s\n' "$_list_json" | sed 's/},{/},\
+{/g'
+ )"
+ # The pattern below matches the domain name followed by a space. If the
+ # space is missing, the pattern can also match a different domain that
+ # starts with the same text as this domain.
+ _old_ids="$(echo "$_list_json" | grep -F "\"name\":\"$_cdomain " | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4 | grep -v "^$_new_id$")"
+ for _old_id in $_old_ids; do
+ _info "Removing old certificate entry $_old_id..."
+ _del_json="$(_post "" "$DEPLOY_UNIFIOS_HOST/api/userCertificates/$_old_id" "" "DELETE")"
+ _del_code="$(_uos_response_code)"
+ if [ "$_del_code" != "204" ] && [ "$_del_code" != "200" ]; then
+ _err "Failed to delete old certificate $_old_id (HTTP $_del_code) -- leaving it in place."
+ _err "Response: $_del_json"
+ fi
+ done
+ fi
+
+ _info "UniFi OS Server certificate deployed and activated successfully."
+ return 0
+}
diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh
index d4b6b6e8..6677b32f 100644
--- a/dnsapi/dns_cyon.sh
+++ b/dnsapi/dns_cyon.sh
@@ -285,15 +285,15 @@ _cyon_delete_txt() {
list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
- list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
+ list_txt_response="$(_get "${list_txt_url}")"
_debug list_txt_response "${list_txt_response}"
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
# Find and delete all acme challenge entries for the $fulldomain.
- _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')"
+ _dns_entries="$(printf "%s\n" "${list_txt_response}" | _egrep_o 'data-hash=\\"[^"]*\\" data-identifier=\\"[^"]*\\"' | sed 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\"/\1 \2/')"
- printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do
+ printf "%s\n" "${_dns_entries}" | while read -r _hash _identifier; do
dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
diff --git a/dnsapi/dns_freemyip.sh b/dnsapi/dns_freemyip.sh
index d598a657..18d8e7f9 100644
--- a/dnsapi/dns_freemyip.sh
+++ b/dnsapi/dns_freemyip.sh
@@ -6,7 +6,7 @@ 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 , @Giova96
+Author: Recolic Keghart , @Giova96, ExtremeFiretop
'
FREEMYIP_DNS_API="https://freemyip.com/update?"
@@ -68,22 +68,30 @@ dns_freemyip_rm() {
return $?
}
-################ Private functions below ################
+################ Private functions below ################
_get_root() {
_fmi_d="$1"
- echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
+ echo "$_fmi_d" | sed 's/.*\.\([^.]*\.[^.]*\.[^.]*\)$/\1/'
}
# 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
+ _fmi_i=1
+ while [ "$_fmi_i" -le 8 ]; do
+ _debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..."
+ _fmi_response="$(_get "$_fmi_url")"
+ printf '%s\n' "$_fmi_response" >&2
+
+ if _contains "$_fmi_response" "OK"; then
+ return 0
+ fi
+
_sleep 1 # DO NOT send the request too fast
+ _fmi_i=$((_fmi_i + 1))
done
- _err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
+ _err "Failed to request freemyip API. Server does not say 'OK'"
return 1
}
@@ -93,13 +101,16 @@ _is_root_domain_published() {
_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..."
+ _fmi_i=1
+ while [ "$_fmi_i" -le 3 ]; do
+ _debug "'$_webroot' ns lookup, retry $_fmi_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
+ _fmi_i=$((_fmi_i + 1))
done
return 1
}
diff --git a/dnsapi/dns_hestiacp.sh b/dnsapi/dns_hestiacp.sh
new file mode 100644
index 00000000..13ee6caf
--- /dev/null
+++ b/dnsapi/dns_hestiacp.sh
@@ -0,0 +1,198 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_hestiacp_info='HestiaCP Server API
+Site: hestiacp.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hestiacp
+Options:
+ HESTIA_HOST Panel URL. E.g. "https://panel.example.com:8083"
+ HESTIA_ACCESS API access key
+ HESTIA_SECRET API secret key
+ HESTIA_USER Username owning the DNS zones. Default "admin". Optional.
+Issues: github.com/acmesh-official/acme.sh/issues/6251
+Author: Radu Malica
+'
+
+######## Public functions #####################
+
+# Usage: dns_hestiacp_add fulldomain txtvalue
+dns_hestiacp_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if ! _hestia_init; then
+ return 1
+ fi
+
+ _debug "Detecting the root zone for $fulldomain"
+ if ! _hestia_get_root "$fulldomain"; then
+ _err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER"
+ return 1
+ fi
+ _debug _hestia_domain "$_hestia_domain"
+ _debug _hestia_sub "$_hestia_sub"
+
+ # _hestia_get_root left the zone record listing in _hestia_response
+ if _hestia_find_records "$_hestia_sub" "TXT" | grep -F -- "$txtvalue" >/dev/null; then
+ _info "The TXT record already exists, skipping"
+ return 0
+ fi
+
+ _info "Adding TXT record for $fulldomain"
+ if ! _hestia_rest "v-add-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_sub" "TXT" "$txtvalue" "" "" "yes" "600"; then
+ _err "Error adding TXT record: $_hestia_response"
+ return 1
+ fi
+ _info "TXT record added successfully"
+ return 0
+}
+
+# Usage: dns_hestiacp_rm fulldomain txtvalue
+dns_hestiacp_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if ! _hestia_init; then
+ return 1
+ fi
+
+ _debug "Detecting the root zone for $fulldomain"
+ if ! _hestia_get_root "$fulldomain"; then
+ _err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER"
+ return 1
+ fi
+ _debug _hestia_domain "$_hestia_domain"
+ _debug _hestia_sub "$_hestia_sub"
+
+ _hestia_removed=0
+ _hestia_failed=0
+ while IFS='|' read -r _hestia_id _hestia_value || [ -n "$_hestia_id" ]; do
+ if [ -z "$_hestia_id" ]; then
+ continue
+ fi
+ if ! _contains "$_hestia_value" "$txtvalue"; then
+ continue
+ fi
+ _info "Deleting TXT record $_hestia_id"
+ if ! _hestia_rest "v-delete-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_id" "yes"; then
+ _err "Error deleting TXT record $_hestia_id: $_hestia_response"
+ _hestia_failed=$(_math "$_hestia_failed" + 1)
+ continue
+ fi
+ _hestia_removed=$(_math "$_hestia_removed" + 1)
+ done <
-'
-
-LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action="
-
-######## Public functions #####################
-
-#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-dns_linode_add() {
- fulldomain="${1}"
- txtvalue="${2}"
-
- if ! _Linode_API; then
- return 1
- fi
-
- _info "Using Linode"
- _debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'"
-
- _debug "First detect the root zone"
- if ! _get_root "$fulldomain"; then
- _err "Domain does not exist."
- return 1
- fi
- _debug _domain_id "$_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue"
-
- if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then
- _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
- _debug _resource_id "$_resource_id"
-
- if [ -z "$_resource_id" ]; then
- _err "Error adding the domain resource."
- return 1
- fi
-
- _info "Domain resource successfully added."
- return 0
- fi
-
- return 1
-}
-
-#Usage: dns_linode_rm _acme-challenge.www.domain.com
-dns_linode_rm() {
- fulldomain="${1}"
-
- if ! _Linode_API; then
- return 1
- fi
-
- _info "Using Linode"
- _debug "Calling: dns_linode_rm() '${fulldomain}'"
-
- _debug "First detect the root zone"
- if ! _get_root "$fulldomain"; then
- _err "Domain does not exist."
- return 1
- fi
- _debug _domain_id "$_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _parameters="&DomainID=$_domain_id"
-
- if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then
- response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
-
- resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")"
- if [ "$resource" ]; then
- _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
- if [ "$_resource_id" ]; then
- _debug _resource_id "$_resource_id"
-
- _parameters="&DomainID=$_domain_id&ResourceID=$_resource_id"
-
- if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then
- _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
- _debug _resource_id "$_resource_id"
-
- if [ -z "$_resource_id" ]; then
- _err "Error deleting the domain resource."
- return 1
- fi
-
- _info "Domain resource successfully deleted."
- return 0
- fi
- fi
-
- return 1
- fi
-
- return 0
- fi
-
- return 1
-}
-
-#################### Private functions below ##################################
-
-_Linode_API() {
- if [ -z "$LINODE_API_KEY" ]; then
- LINODE_API_KEY=""
-
- _err "You didn't specify the Linode API key yet."
- _err "Please create your key and try again."
-
- return 1
- fi
-
- _saveaccountconf LINODE_API_KEY "$LINODE_API_KEY"
-}
-
-#################### Private functions below ##################################
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=12345
-_get_root() {
- domain=$1
- i=2
- p=1
-
- if _rest GET "domain.list"; then
- response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
- while true; do
- h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
- _debug h "$h"
- if [ -z "$h" ]; then
- #not valid
- return 1
- fi
-
- hostedzone="$(echo "$response" | _egrep_o "{.*\"DOMAIN\":\s*\"$h\".*}")"
- if [ "$hostedzone" ]; then
- _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"DOMAINID\":\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")
- _domain=$h
- return 0
- fi
- return 1
- fi
- p=$i
- i=$(_math "$i" + 1)
- done
- fi
- return 1
-}
-
-#method method action data
-_rest() {
- mtd="$1"
- ep="$2"
- data="$3"
-
- _debug mtd "$mtd"
- _debug ep "$ep"
-
- export _H1="Accept: application/json"
- export _H2="Content-Type: application/json"
-
- if [ "$mtd" != "GET" ]; then
- # both POST and DELETE.
- _debug data "$data"
- response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")"
- else
- response="$(_get "$LINODE_API_URL$ep$data")"
- fi
-
- if [ "$?" != "0" ]; then
- _err "error $ep"
- return 1
- fi
- _debug2 response "$response"
- return 0
-}
diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh
index cca59735..7035640b 100755
--- a/dnsapi/dns_namecheap.sh
+++ b/dnsapi/dns_namecheap.sh
@@ -104,6 +104,9 @@ _get_root_by_getList() {
return 1
fi
+ _namecheap_domain_list=$(echo "$response" | _egrep_o ']*')
+ _debug2 domain_list "$_namecheap_domain_list"
+
i=2
p=1
@@ -120,7 +123,7 @@ _get_root_by_getList() {
return 1
fi
- if ! _contains "$response" "$h"; then
+ if ! _namecheap_is_our_dns "$h"; then
_debug "$h not found"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
@@ -133,6 +136,29 @@ _get_root_by_getList() {
return 1
}
+#Usage: _namecheap_is_our_dns
+#Succeeds only when domains.getList listed exactly AND that entry is
+#served by Namecheap's own DNS. A domain parked on Namecheap's webhosting DNS
+#is listed with IsOurDNS="false", and every dns.getHosts/setHosts call against
+#it is refused with error 2030288 "not using proper DNS servers". Accepting
+#such a domain as the root zone hides a subdomain that IS delegated to
+#Namecheap DNS and that the getHosts probe below would have found.
+#https://github.com/acmesh-official/acme.sh/issues/7178
+_namecheap_is_our_dns() {
+ _namecheap_entry=$(echo "$_namecheap_domain_list" | grep -F " Name=\"$1\"" | _head_n 1)
+ if [ -z "$_namecheap_entry" ]; then
+ return 1
+ fi
+
+ _namecheap_ourdns=$(echo "$_namecheap_entry" | _egrep_o ' IsOurDNS="[^"]*' | cut -d '"' -f 2)
+ _debug2 "$1 IsOurDNS" "$_namecheap_ourdns"
+
+ if [ "$_namecheap_ourdns" = "true" ]; then
+ return 0
+ fi
+ return 1
+}
+
_get_root_by_getHosts() {
i=100
p=99
diff --git a/dnsapi/dns_netcup.sh b/dnsapi/dns_netcup.sh
index 8609adf6..3b291854 100644
--- a/dnsapi/dns_netcup.sh
+++ b/dnsapi/dns_netcup.sh
@@ -33,9 +33,11 @@ dns_netcup_add() {
exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
exit=$(_math "$exit" + 1)
i=$exit
+ _nc_last=$(_nc_lastlevel "$i")
+ _nc_found=""
while
- [ "$exit" -gt 0 ]
+ [ "$exit" -ge "$_nc_last" ]
do
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
@@ -51,12 +53,18 @@ dns_netcup_add() {
_err "$msg"
return 1
else
+ _nc_found=1
break
fi
fi
fi
exit=$(_math "$exit" - 1)
done
+ if [ -z "$_nc_found" ]; then
+ _err "$msg"
+ _nc_nozone "$fulldomain"
+ return 1
+ fi
logout
}
@@ -70,9 +78,11 @@ dns_netcup_rm() {
exit=$(_math "$exit" + 1)
i=$exit
rec=""
+ _nc_last=$(_nc_lastlevel "$i")
+ _nc_found=""
while
- [ "$exit" -gt 0 ]
+ [ "$exit" -ge "$_nc_last" ]
do
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
@@ -89,12 +99,18 @@ dns_netcup_rm() {
_err "$msg"
return 1
else
+ _nc_found=1
break
fi
fi
fi
exit=$(_math "$exit" - 1)
done
+ if [ -z "$_nc_found" ]; then
+ _err "$msg"
+ _nc_nozone "$fulldomain"
+ return 1
+ fi
ida=0000
idv=0001
@@ -125,6 +141,27 @@ dns_netcup_rm() {
logout
}
+# The zone is looked up by walking the challenge name from the right, one
+# label at a time. The leftmost label is the challenge prefix, so the full
+# name itself can never be a zone: asking netcup for it only returns 4013
+# "Validation Error", which would then mask the real 5028 "zone could not be
+# found". Stop one label short, unless the name is too short to have a
+# challenge prefix at all (manual invocation).
+# levels
+_nc_lastlevel() {
+ if [ "$1" -ge 3 ]; then
+ echo 2
+ else
+ echo 1
+ fi
+}
+
+# fulldomain
+_nc_nozone() {
+ _err "No DNS zone for $1 was found at netcup."
+ _err "Check that the domain belongs to the account of the configured NC_CID and that its DNS is hosted at netcup."
+}
+
_login() {
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
diff --git a/dnsapi/dns_nexdns.sh b/dnsapi/dns_nexdns.sh
new file mode 100755
index 00000000..e4447c0e
--- /dev/null
+++ b/dnsapi/dns_nexdns.sh
@@ -0,0 +1,244 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_nexdns_info='NexDNS
+Site: nexdns.tech
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_nexdns
+Options:
+ NEXDNS_Token API token. Can be created at https://nexdns.tech/settings/api-keys
+ NEXDNS_Api API base url. Default "https://api.nexdns.tech/v1". Optional.
+Issues: github.com/acmesh-official/acme.sh/issues/7179
+Author: NexDNS
+'
+
+NEXDNS_Api_Default="https://api.nexdns.tech/v1"
+
+######## Public functions #####################
+
+#Usage: dns_nexdns_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_nexdns_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if ! _nexdns_init; then
+ return 1
+ fi
+
+ _saveaccountconf_mutable NEXDNS_Token "$NEXDNS_Token"
+ if [ "$NEXDNS_Api" != "$NEXDNS_Api_Default" ]; then
+ _saveaccountconf_mutable NEXDNS_Api "$NEXDNS_Api"
+ else
+ _clearaccountconf_mutable NEXDNS_Api
+ fi
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "Cannot find the zone of $fulldomain in this NexDNS account."
+ return 1
+ fi
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+ _debug _domain_id "$_domain_id"
+
+ _info "Adding the TXT record for $fulldomain"
+ if ! _nexdns_rest POST "zones/$_domain_id/records" "{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
+ return 1
+ fi
+
+ _info "The TXT record has been added."
+ return 0
+}
+
+#Usage: dns_nexdns_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_nexdns_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ if ! _nexdns_init; then
+ return 1
+ fi
+
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "Cannot find the zone of $fulldomain in this NexDNS account."
+ return 1
+ fi
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+ _debug _domain_id "$_domain_id"
+
+ _info "Removing the TXT record for $fulldomain"
+ if ! _nexdns_rest GET "zones/$_domain_id/records?type=TXT&name=$_sub_domain"; then
+ return 1
+ fi
+
+ #All the challenge records share one name and one type, so the value is the
+ #only thing that tells them apart. A certificate covering example.com and
+ #*.example.com puts two of them at the same name at the same time.
+ _record_id="$(echo "$response" | tr '{' "\n" | grep -- "$txtvalue" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
+ _debug _record_id "$_record_id"
+
+ if [ -z "$_record_id" ]; then
+ _info "The TXT record is already gone, nothing to remove."
+ return 0
+ fi
+
+ if ! _nexdns_rest DELETE "zones/$_domain_id/records/$_record_id"; then
+ return 1
+ fi
+
+ _info "The TXT record has been removed."
+ return 0
+}
+
+#################### Private functions below ##################################
+
+#Reads the token and the api url, and applies the default url.
+_nexdns_init() {
+ NEXDNS_Token="${NEXDNS_Token:-$(_readaccountconf_mutable NEXDNS_Token)}"
+ NEXDNS_Api="${NEXDNS_Api:-$(_readaccountconf_mutable NEXDNS_Api)}"
+
+ if [ -z "$NEXDNS_Token" ]; then
+ _err "You have not set NEXDNS_Token yet."
+ _err "Create one at https://nexdns.tech/settings/api-keys, on a plan that includes API access, then:"
+ _err "export NEXDNS_Token=\"your-api-token\""
+ return 1
+ fi
+
+ if [ -z "$NEXDNS_Api" ]; then
+ NEXDNS_Api="$NEXDNS_Api_Default"
+ fi
+ #A trailing slash would make every request path begin with a double slash.
+ NEXDNS_Api="$(echo "$NEXDNS_Api" | sed 's|/*$||')"
+ _debug NEXDNS_Api "$NEXDNS_Api"
+
+ return 0
+}
+
+#_acme-challenge.www.example.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=example.com
+# _domain_id=Zm9vYmFy
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug h "$h"
+ if [ -z "$h" ]; then
+ #not valid
+ return 1
+ fi
+
+ if ! _nexdns_rest GET "zones?search=$h&per_page=100"; then
+ return 1
+ fi
+
+ #search matches on a substring, so the page can also hold zones that merely
+ #contain h. Take the id of the one whose name is exactly h.
+ _domain_id="$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
+ if [ "$_domain_id" ]; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ return 0
+ fi
+
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+}
+
+#Usage: _nexdns_rest GET|POST|DELETE path [body] [attempt]
+_nexdns_rest() {
+ m=$1
+ ep=$2
+ data=$3
+ attempt=${4:-1}
+ _debug "$ep"
+
+ export _H1="Authorization: Bearer $NEXDNS_Token"
+ export _H2="Content-Type: application/json"
+ export _H3="Accept: application/json"
+
+ if [ "$m" = "GET" ]; then
+ response="$(_get "$NEXDNS_Api/$ep")"
+ else
+ _debug2 data "$data"
+ response="$(_post "$data" "$NEXDNS_Api/$ep" "" "$m" "application/json")"
+ fi
+
+ if [ "$?" != "0" ]; then
+ _err "error $ep"
+ return 1
+ fi
+
+ #A single certificate costs a handful of requests, but a renewal sweep over
+ #many of them meets the account's per-minute budget, and that run is
+ #unattended. Retry-After is treated as a floor: an api may report the time one
+ #token needs at an average rate and name a second when nothing frees for a
+ #minute, so the wait grows on its own across attempts.
+ if [ "$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" = "429" ]; then
+ if [ "$attempt" -ge 4 ]; then
+ _err "$m $ep failed: rate limited, and the wait budget is spent"
+ return 1
+ fi
+
+ _retry_after="$(grep -i "^Retry-After" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d : -f 2 | tr -d " \r\n")"
+ _backoff="$(_math "$attempt" \* 15)"
+ #The header may also carry an http date. Anything but a plain count of
+ #seconds falls through to the backoff rather than being parsed: guessing
+ #wrong about a date is worse than waiting a known interval, and comparing a
+ #date numerically would abort the hook outright.
+ case "$_retry_after" in
+ "" | *[!0-9]*) _retry_after="$_backoff" ;;
+ *)
+ if [ "$_retry_after" -lt "$_backoff" ]; then
+ _retry_after="$_backoff"
+ fi
+ ;;
+ esac
+
+ #A wait longer than this is a refusal rather than a schedule, and sleeping
+ #it out would hold the hook for the length of the window. Hand the run back
+ #instead, so the next cron pass picks it up.
+ if [ "$_retry_after" -gt 120 ]; then
+ _err "$m $ep failed: rate limited for ${_retry_after}s, longer than this hook will wait"
+ return 1
+ fi
+
+ _info "Rate limited by the NexDNS API; retrying in $_retry_after seconds."
+ _sleep "$_retry_after"
+
+ _nexdns_rest "$m" "$ep" "$data" "$(_math "$attempt" + 1)"
+ return $?
+ fi
+
+ #Whitespace between a key and its value would defeat every match made on the
+ #body, here and in the callers.
+ response="$(echo "$response" | _normalizeJson)"
+ _debug2 response "$response"
+
+ #The status line decides success, not the body: a delete answers 204 with no
+ #body at all, and a record whose own content contains "error": would otherwise
+ #turn a stored value into a reported failure. The body is read only for the
+ #message once the status says the request was rejected.
+ _code="$(grep "^HTTP" "$HTTP_HEADER" 2>/dev/null | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ _debug2 _code "$_code"
+ case "$_code" in
+ "" | 2*)
+ return 0
+ ;;
+ esac
+
+ #A rejected request carries {"error":{"code":..,"message":..}}, so say what the
+ #api says went wrong.
+ _message="$(echo "$response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
+ if [ -z "$_message" ]; then
+ _message="status $_code"
+ fi
+ _err "$m $ep failed: $_message"
+
+ return 1
+}
diff --git a/dnsapi/dns_rltx.sh b/dnsapi/dns_rltx.sh
new file mode 100644
index 00000000..065ac177
--- /dev/null
+++ b/dnsapi/dns_rltx.sh
@@ -0,0 +1,145 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_rltx_info='Realtox Media Cloudpanel DNS API
+Site: realtoxmedia.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_rltx
+Options:
+ RLTX_Key API Key
+ RLTX_OrganizationID Organization ID
+'
+
+######## Public functions #####################
+
+#Usage: dns_rltx_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_rltx_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _info "Using Realtox Media Cloudpanel DNS API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _rltx_init; then
+ return 1
+ fi
+
+ if ! _get_root "$fulldomain"; then
+ _err "Could not find matching DNS zone for $fulldomain"
+ return 1
+ fi
+
+ _debug _domain_id "$_domain_id"
+ _debug _domain "$_domain"
+ _debug _sub_domain "$_sub_domain"
+
+ data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}"
+ if ! _rltx_rest POST "domains/$_domain_id/dns/acme-txt" "$data"; then
+ _err "Add TXT record request failed"
+ return 1
+ fi
+ if _contains "$response" '"status":"added"'; then
+ _info "Added TXT record, OK"
+ return 0
+ fi
+ _err "Add TXT record failed: $response"
+ return 1
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_rltx_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _info "Using Realtox Media Cloudpanel DNS API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _rltx_init; then
+ return 1
+ fi
+
+ if ! _get_root "$fulldomain"; then
+ _err "Could not find matching DNS zone for $fulldomain"
+ return 1
+ fi
+
+ _debug _domain_id "$_domain_id"
+ _debug _domain "$_domain"
+ _debug _sub_domain "$_sub_domain"
+
+ data="{\"name\":\"$_sub_domain\",\"value\":\"$txtvalue\",\"ttl\":120}"
+ if ! _rltx_rest DELETE "domains/$_domain_id/dns/acme-txt" "$data"; then
+ _err "Remove TXT record request failed"
+ return 1
+ fi
+ if _contains "$response" '"status":"removed"'; then
+ _info "Removed TXT record, OK"
+ return 0
+ fi
+ _err "Remove TXT record failed: $response"
+ return 1
+}
+
+#################### Private functions below ##################################
+
+_rltx_init() {
+ RLTX_Key="${RLTX_Key:-$(_readaccountconf_mutable RLTX_Key)}"
+ RLTX_OrganizationID="${RLTX_OrganizationID:-$(_readaccountconf_mutable RLTX_OrganizationID)}"
+
+ if [ -z "$RLTX_Key" ] || [ -z "$RLTX_OrganizationID" ]; then
+ RLTX_Key=""
+ RLTX_OrganizationID=""
+ _err "Please specify RLTX_Key and RLTX_OrganizationID."
+ _err "You can export them and retry: export RLTX_Key=... RLTX_OrganizationID=..."
+ return 1
+ fi
+
+ _saveaccountconf_mutable RLTX_Key "$RLTX_Key"
+ _saveaccountconf_mutable RLTX_OrganizationID "$RLTX_OrganizationID"
+}
+
+_get_root() {
+ domain=$1
+ fqdn_encoded="$(printf "%s" "$domain" | _url_encode)"
+ if ! _rltx_rest GET "domains/dns/acme-zone?fqdn=$fqdn_encoded"; then
+ return 1
+ fi
+ if ! _contains "$response" '"domain_id":"'; then
+ return 1
+ fi
+
+ _domain_id="$(printf "%s" "$response" | _egrep_o '"domain_id":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)"
+ _domain="$(printf "%s" "$response" | _egrep_o '"zone":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)"
+ _sub_domain="$(printf "%s" "$response" | _egrep_o '"record_name":"[^"]*"' | cut -d : -f 2 | tr -d '"' | _head_n 1)"
+
+ if [ -z "$_domain_id" ] || [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
+ return 1
+ fi
+ return 0
+}
+
+_rltx_rest() {
+ m=$1
+ ep="$2"
+ data="$3"
+ _debug "$ep"
+
+ export _H1="X-API-Key: $RLTX_Key"
+ export _H2="X-Organization-ID: $RLTX_OrganizationID"
+ export _H3="Content-Type: application/json"
+
+ if [ "$m" = "GET" ]; then
+ response="$(_get "https://api.ccp.realtoxmedia.de/api/$ep")"
+ else
+ _debug2 data "$data"
+ response="$(_post "$data" "https://api.ccp.realtoxmedia.de/api/$ep" "" "$m")"
+ fi
+
+ if [ "$?" != "0" ]; then
+ _err "Realtox Media Cloudpanel API request failed: $ep"
+ return 1
+ fi
+ _debug2 response "$response"
+ return 0
+}
diff --git a/dnsapi/dns_world4you.sh b/dnsapi/dns_world4you.sh
index f59715ac..0a1cda6b 100644
--- a/dnsapi/dns_world4you.sh
+++ b/dnsapi/dns_world4you.sh
@@ -61,7 +61,7 @@ dns_world4you_add() {
if _contains "$res" "successfully"; then
return 0
else
- msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g')
+ msg=$(_w4y_alert_msg "$res")
if [ "$msg" = '' ]; then
_err "Unable to add record: Unknown error"
echo "$ret" >'error-01.html'
@@ -125,7 +125,7 @@ dns_world4you_rm() {
if _contains "$res" "successfully"; then
return 0
else
- msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g')
+ msg=$(_w4y_alert_msg "$res")
if [ "$msg" = '' ]; then
_err "Unable to remove record: Unknown error"
echo "$ret" >'error-01.html'
@@ -145,6 +145,17 @@ dns_world4you_rm() {
################ Private functions ################
+# Usage: _w4y_alert_msg
+# Extracts the error text out of the alert box of a DNS page.
+# "grep -A" is not portable (Solaris /usr/bin/grep: "illegal option -- A"),
+# so select from the alert to EOF and keep the same number of lines.
+# "\s" is a GNU sed extension, use an explicit space/tab bracket instead.
+_w4y_alert_msg() {
+ _w4y_tab=$(printf '\t')
+ echo "$1" | sed -n '/alert-notification/,$p' | _head_n 21 |
+ grep 'class="weak-title">[^<]' | sed "s/<[^>]*>//g;s/^[ $_w4y_tab]*//"
+}
+
# Usage: _login
_login() {
WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}"
diff --git a/dnsapi/dns_yc.sh b/dnsapi/dns_yc.sh
index 36c49ce4..cacb5977 100644
--- a/dnsapi/dns_yc.sh
+++ b/dnsapi/dns_yc.sh
@@ -22,21 +22,32 @@ dns_yc_add() {
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
txtvalue=$2
+ # YC_SA_Key_File_PEM_b64/Path are always persisted to the domain conf below,
+ # so they must be recovered from there first (account conf is only a
+ # fallback for the YC_Folder_ID case, see the SA_ID/SA_Key_ID save below).
+ YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
+ YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
if [ "$YC_SA_Key_File_PEM_b64" ]; then
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
YC_SA_Key_File="private.key"
+ _yc_key_is_temp=1
_savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64"
else
YC_SA_Key_File="$YC_SA_Key_File_Path"
+ _yc_key_is_temp=""
_savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path"
fi
+ YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
+ YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
+ YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
+ YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then
@@ -65,11 +76,21 @@ dns_yc_add() {
return 1
fi
else
+ # Clear both possible stores -- YC_Zone_ID/YC_Folder_ID/key material are
+ # persisted to the domain conf, while YC_SA_ID/YC_SA_Key_ID may have been
+ # saved account-wide (Folder_ID mode), so a plain _clearaccountconf alone
+ # would leave stale values behind in whichever store wasn't touched.
+ _cleardomainconf YC_Zone_ID
_clearaccountconf YC_Zone_ID
+ _cleardomainconf YC_Folder_ID
_clearaccountconf YC_Folder_ID
- _clearaccountconf YC_SA_ID
- _clearaccountconf YC_SA_Key_ID
+ _cleardomainconf YC_SA_ID
+ _clearaccountconf_mutable YC_SA_ID
+ _cleardomainconf YC_SA_Key_ID
+ _clearaccountconf_mutable YC_SA_Key_ID
+ _cleardomainconf YC_SA_Key_File_PEM_b64
_clearaccountconf YC_SA_Key_File_PEM_b64
+ _cleardomainconf YC_SA_Key_File_Path
_clearaccountconf YC_SA_Key_File_Path
_err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
return 1
@@ -110,11 +131,30 @@ dns_yc_rm() {
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
txtvalue=$2
+ YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
+ YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
+ YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
+ YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
+ # See dns_yc_add() for why domain conf is checked before account conf.
+ YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
+ YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
+ YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
+ YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
+
+ if [ "$YC_SA_Key_File_PEM_b64" ]; then
+ echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
+ YC_SA_Key_File="private.key"
+ _yc_key_is_temp=1
+ else
+ YC_SA_Key_File="$YC_SA_Key_File_Path"
+ _yc_key_is_temp=""
+ fi
+
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
@@ -124,16 +164,10 @@ dns_yc_rm() {
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
- _debug "Getting txt records"
- if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
- exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$")
- _debug exists_txtvalue "$exists_txtvalue"
- else
- _err "Error: $response"
- return 1
- fi
-
- if _yc_rest POST "zones/$_domain_id:updateRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":$exists_txtvalue}]}"; then
+ # upsertRecordSets.deletions removes only the given value from the rrset,
+ # leaving any other values at the same name (e.g. base + wildcard domain)
+ # intact -- no need to read the current data set and recompute it.
+ if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
if _contains "$response" "\"done\": true"; then
_info "Delete, OK"
return 0
@@ -194,7 +228,7 @@ _get_root() {
return 1
fi
if _contains "$response" "\"zone\": \"$h\""; then
- _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')
+ _domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')
_debug _domain_id "$_domain_id"
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
@@ -255,7 +289,9 @@ _yc_login() {
_signature=$(printf "%s.%s" "$header" "$payload" | _sign "$YC_SA_Key_File" "sha256 -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1" | _url_replace)
_debug2 _signature "$_signature"
- rm -rf "$YC_SA_Key_File"
+ if [ "$_yc_key_is_temp" ]; then
+ rm -f "$YC_SA_Key_File"
+ fi
_jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature")
_debug2 _jwt "$_jwt"
@@ -264,7 +300,7 @@ _yc_login() {
_iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")"
_debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)"
- YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')"
+ YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')"
_debug3 YC_Token
return 0
diff --git a/notify/waha.sh b/notify/waha.sh
index 989f57ac..573295e9 100755
--- a/notify/waha.sh
+++ b/notify/waha.sh
@@ -57,9 +57,8 @@ waha_send() {
_debug "_data" "$_data"
- export _H1="Content-Type: application/json"
if [ "$WAHA_API_KEY" ]; then
- export _H2="X-Api-Key: $WAHA_API_KEY"
+ export _H1="X-Api-Key: $WAHA_API_KEY"
fi
_waha_url="${WAHA_URL}/api/sendText"