mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-09-05 20:24:54 +08:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94d2f62626 | ||
|
|
41b6371afa | ||
|
|
f2b0484137 | ||
|
|
943cc0cf25 | ||
|
|
fa8dc180aa | ||
|
|
195cbd5d09 | ||
|
|
7b5a902755 | ||
|
|
4a3f8b5dea | ||
|
|
08ac83459d | ||
|
|
2b7487ba72 | ||
|
|
19efdf269a | ||
|
|
cdca555cad | ||
|
|
cce4a28b99 | ||
|
|
4756183e30 | ||
|
|
6e1deacac0 | ||
|
|
c6cd844986 | ||
|
|
ef7b2d3c2e | ||
|
|
b481ffb81b | ||
|
|
b1a8eb1c95 | ||
|
|
e0c0297ba3 | ||
|
|
1cbd223386 | ||
|
|
5180911929 | ||
|
|
5ff7f0a4e7 | ||
|
|
41bdd4cd0e | ||
|
|
a89ba9c2e5 | ||
|
|
2e2782f0d8 | ||
|
|
4a3bc2c919 | ||
|
|
05367d3598 | ||
|
|
f1cbba05f6 | ||
|
|
f67be78ff4 | ||
|
|
9aad4dcbd5 | ||
|
|
603a126a7c | ||
|
|
5e6c263211 | ||
|
|
b4925052dd | ||
|
|
ea5e70564d | ||
|
|
bf90b845b2 | ||
|
|
0565443622 | ||
|
|
057c940895 | ||
|
|
7c12deb7ef | ||
|
|
830782fd1d | ||
|
|
698f6c7329 | ||
|
|
749fa2487e | ||
|
|
24d573b6d3 | ||
|
|
1774d838ca | ||
|
|
4c8a143086 | ||
|
|
6feb1df83c | ||
|
|
97c5aca136 | ||
|
|
7fa3018219 | ||
|
|
9bfcf2cf1a | ||
|
|
59a97d7f8b | ||
|
|
a836e747d1 | ||
|
|
8f98786d93 | ||
|
|
6d559ae69f | ||
|
|
24501ac0ca | ||
|
|
a82cf763cf | ||
|
|
7cc16cd09a | ||
|
|
d45b6fe8e6 | ||
|
|
237f2d9c3b | ||
|
|
3e64b5e6f5 | ||
|
|
347cc207cd | ||
|
|
1651a5a609 | ||
|
|
0c76c1f211 | ||
|
|
e00d3cfde3 | ||
|
|
4187ec23c1 | ||
|
|
9f0ef7abcd | ||
|
|
0e2659b768 | ||
|
|
f50401a342 | ||
|
|
992886c4eb | ||
|
|
d60c75b4e3 | ||
|
|
3c848adbb7 | ||
|
|
f265c304da |
272
.github/workflows/DNS.yml
vendored
272
.github/workflows/DNS.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- name: Set env file
|
- name: Set env file
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: |
|
run: |
|
||||||
brew untap aws/tap || true
|
brew untap aws/tap || true
|
||||||
@@ -167,7 +167,7 @@ jobs:
|
|||||||
- name: Set git to use LF
|
- name: Set git to use LF
|
||||||
run: |
|
run: |
|
||||||
git config --global core.autocrlf false
|
git config --global core.autocrlf false
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Install cygwin base packages with chocolatey
|
- name: Install cygwin base packages with chocolatey
|
||||||
run: |
|
run: |
|
||||||
choco config get cacheLocation
|
choco config get cacheLocation
|
||||||
@@ -231,12 +231,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/freebsd-vm@v1
|
- uses: vmactions/freebsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
prepare: pkg install -y socat curl
|
||||||
usesh: true
|
usesh: true
|
||||||
@@ -289,12 +290,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/ghostbsd-vm@v1
|
- uses: vmactions/ghostbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
prepare: pkg install -y socat curl
|
||||||
usesh: true
|
usesh: true
|
||||||
@@ -345,12 +347,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/openbsd-vm@v1
|
- uses: vmactions/openbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
prepare: pkg_add socat curl libiconv
|
||||||
usesh: true
|
usesh: true
|
||||||
@@ -401,12 +404,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/netbsd-vm@v1
|
- uses: vmactions/netbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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: |
|
prepare: |
|
||||||
/usr/sbin/pkg_add curl socat
|
/usr/sbin/pkg_add curl socat
|
||||||
@@ -458,12 +462,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/dragonflybsd-vm@v1
|
- uses: vmactions/dragonflybsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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: |
|
prepare: |
|
||||||
pkg install -y libnghttp2
|
pkg install -y libnghttp2
|
||||||
@@ -519,12 +524,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/midnightbsd-vm@v1
|
- uses: vmactions/midnightbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
prepare: mport install socat curl || true
|
||||||
usesh: true
|
usesh: true
|
||||||
@@ -576,12 +582,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/solaris-vm@v1
|
- uses: vmactions/solaris-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
sync: nfs
|
||||||
prepare: |
|
prepare: |
|
||||||
@@ -635,12 +642,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/omnios-vm@v1
|
- uses: vmactions/omnios-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
sync: nfs
|
||||||
prepare: pkg install socat
|
prepare: pkg install socat
|
||||||
@@ -691,12 +699,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/openindiana-vm@v1
|
- uses: vmactions/openindiana-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
sync: nfs
|
||||||
prepare: pkg install socat
|
prepare: pkg install socat
|
||||||
@@ -747,12 +756,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/tribblix-vm@v1
|
- uses: vmactions/tribblix-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
sync: nfs
|
||||||
prepare: zap install socat
|
prepare: zap install socat
|
||||||
@@ -803,12 +813,13 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
- uses: vmactions/haiku-vm@v1
|
- uses: vmactions/haiku-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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}}'
|
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
|
sync: rsync
|
||||||
copyback: false
|
copyback: false
|
||||||
@@ -842,3 +853,236 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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}}"
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
HardenedBSD:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: OpenEuler
|
||||||
|
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 }}
|
||||||
|
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/hardenedbsd-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
|
||||||
|
sync: nfs
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
OPNsense:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: HardenedBSD
|
||||||
|
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 }}
|
||||||
|
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/opnsense-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}}'
|
||||||
|
#The dns-01 cases need no inbound port, so the appliance's web GUI can
|
||||||
|
#keep the 80 port here, unlike the standalone workflow.
|
||||||
|
prepare: pkg install -y socat curl
|
||||||
|
usesh: true
|
||||||
|
sync: nfs
|
||||||
|
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"
|
||||||
|
|||||||
1
.github/workflows/DragonFlyBSD.yml
vendored
1
.github/workflows/DragonFlyBSD.yml
vendored
@@ -58,6 +58,7 @@ jobs:
|
|||||||
- uses: vmactions/dragonflybsd-vm@v1
|
- uses: vmactions/dragonflybsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/FreeBSD.yml
vendored
1
.github/workflows/FreeBSD.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
|||||||
- uses: vmactions/freebsd-vm@v1
|
- uses: vmactions/freebsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/GhostBSD.yml
vendored
1
.github/workflows/GhostBSD.yml
vendored
@@ -66,6 +66,7 @@ jobs:
|
|||||||
- uses: vmactions/ghostbsd-vm@v1
|
- uses: vmactions/ghostbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/Haiku.yml
vendored
1
.github/workflows/Haiku.yml
vendored
@@ -65,6 +65,7 @@ jobs:
|
|||||||
- uses: vmactions/haiku-vm@v1
|
- uses: vmactions/haiku-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
76
.github/workflows/HardenedBSD.yml
vendored
Normal file
76
.github/workflows/HardenedBSD.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
name: HardenedBSD
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/HardenedBSD.yml'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/HardenedBSD.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
HardenedBSD:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||||
|
CA_ECDSA: ""
|
||||||
|
CA: ""
|
||||||
|
CA_EMAIL: ""
|
||||||
|
TEST_PREFERRED_CHAIN: (STAGING)
|
||||||
|
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||||
|
CA_ECDSA: ""
|
||||||
|
CA: ""
|
||||||
|
CA_EMAIL: ""
|
||||||
|
TEST_PREFERRED_CHAIN: (STAGING)
|
||||||
|
ACME_USE_WGET: 1
|
||||||
|
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 }}
|
||||||
|
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
|
||||||
|
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/hardenedbsd-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"
|
||||||
|
prepare: pkg install -y socat curl wget
|
||||||
|
usesh: true
|
||||||
|
sync: nfs
|
||||||
|
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"
|
||||||
76
.github/workflows/Hurd.yml
vendored
Normal file
76
.github/workflows/Hurd.yml
vendored
Normal file
@@ -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"
|
||||||
1
.github/workflows/MidnightBSD.yml
vendored
1
.github/workflows/MidnightBSD.yml
vendored
@@ -58,6 +58,7 @@ jobs:
|
|||||||
- uses: vmactions/midnightbsd-vm@v1
|
- uses: vmactions/midnightbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/NetBSD.yml
vendored
1
.github/workflows/NetBSD.yml
vendored
@@ -58,6 +58,7 @@ jobs:
|
|||||||
- uses: vmactions/netbsd-vm@v1
|
- uses: vmactions/netbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
86
.github/workflows/OPNsense.yml
vendored
Normal file
86
.github/workflows/OPNsense.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: OPNsense
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/OPNsense.yml'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/OPNsense.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
OPNsense:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||||
|
CA_ECDSA: ""
|
||||||
|
CA: ""
|
||||||
|
CA_EMAIL: ""
|
||||||
|
TEST_PREFERRED_CHAIN: (STAGING)
|
||||||
|
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||||
|
CA_ECDSA: ""
|
||||||
|
CA: ""
|
||||||
|
CA_EMAIL: ""
|
||||||
|
TEST_PREFERRED_CHAIN: (STAGING)
|
||||||
|
ACME_USE_WGET: 1
|
||||||
|
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 }}
|
||||||
|
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
|
||||||
|
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/opnsense-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"
|
||||||
|
prepare: pkg install -y socat curl wget
|
||||||
|
usesh: true
|
||||||
|
sync: nfs
|
||||||
|
run: |
|
||||||
|
#OPNsense is a firewall appliance whose web GUI holds the 80 port,
|
||||||
|
#where every --standalone case listens. configd has no "stop"
|
||||||
|
#action for it and the rc script cannot stop it either, so kill it.
|
||||||
|
#This belongs here and not in prepare: prepare runs before the
|
||||||
|
#cache-after-prepare reboot, which would bring the GUI back. And do
|
||||||
|
#NOT free the port by disabling the GUI's http redirect in
|
||||||
|
#config.xml: pf's automatic pass rule for the 80 port is generated
|
||||||
|
#from the web GUI settings, so dropping the redirect also drops the
|
||||||
|
#rule on the next boot, and the inbound challenge is filtered.
|
||||||
|
pkill lighttpd || true
|
||||||
|
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"
|
||||||
1
.github/workflows/Omnios.yml
vendored
1
.github/workflows/Omnios.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
|||||||
- uses: vmactions/omnios-vm@v1
|
- uses: vmactions/omnios-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/OpenBSD.yml
vendored
1
.github/workflows/OpenBSD.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
|||||||
- uses: vmactions/openbsd-vm@v1
|
- uses: vmactions/openbsd-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
78
.github/workflows/OpenEuler.yml
vendored
Normal file
78
.github/workflows/OpenEuler.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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: |
|
||||||
|
# openEuler ships every repo with both a baseurl and a metalink.
|
||||||
|
# The metalink mirror pool is split-brain (dl-cdn.openeuler.openatom.cn
|
||||||
|
# froze at the 2026-08-20 snapshot while repo.openeuler.org moved on),
|
||||||
|
# so dnf takes repomd.xml from the stale mirror and then 404s fetching
|
||||||
|
# the checksummed metadata it names from the fresh ones. Keep only the
|
||||||
|
# vendor baseurl, which is self-consistent.
|
||||||
|
sed -i '/^metalink=/d' /etc/yum.repos.d/*.repo
|
||||||
|
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"
|
||||||
1
.github/workflows/OpenIndiana.yml
vendored
1
.github/workflows/OpenIndiana.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
|||||||
- uses: vmactions/openindiana-vm@v1
|
- uses: vmactions/openindiana-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/Solaris.yml
vendored
1
.github/workflows/Solaris.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
|||||||
- uses: vmactions/solaris-vm@v1
|
- uses: vmactions/solaris-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
1
.github/workflows/Tribblix.yml
vendored
1
.github/workflows/Tribblix.yml
vendored
@@ -64,6 +64,7 @@ jobs:
|
|||||||
- uses: vmactions/tribblix-vm@v1
|
- uses: vmactions/tribblix-vm@v1
|
||||||
with:
|
with:
|
||||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
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'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
|
|||||||
36
.github/workflows/dockerhub.yml
vendored
36
.github/workflows/dockerhub.yml
vendored
@@ -10,9 +10,16 @@ on:
|
|||||||
- '**.sh'
|
- '**.sh'
|
||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
- '.github/workflows/dockerhub.yml'
|
- '.github/workflows/dockerhub.yml'
|
||||||
|
# A dispatch on a tag ref rebuilds that tag's own image; the weekly
|
||||||
|
# schedule (default branch only) dispatches the latest release tag so a
|
||||||
|
# pinned version tag picks up Alpine package security updates (issue 7209).
|
||||||
|
# master never publishes anything but latest.
|
||||||
|
schedule:
|
||||||
|
- cron: '17 3 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -40,13 +47,13 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: CheckToken
|
needs: CheckToken
|
||||||
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
|
if: "github.event_name != 'schedule' && contains(needs.CheckToken.outputs.hasToken, 'true')"
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -98,3 +105,26 @@ jobs:
|
|||||||
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
|
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
|
||||||
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|
||||||
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
|
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
|
||||||
|
|
||||||
|
rebuild:
|
||||||
|
# weekly: dispatch this workflow on the latest release tag so the tag
|
||||||
|
# rebuilds its own image from its own commit and its own workflow file
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'schedule'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: write
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: dispatch a rebuild of the latest release tag
|
||||||
|
run: |
|
||||||
|
tag="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq .tag_name)"
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
echo "::error::cannot resolve the latest release tag"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "dispatching a rebuild of ${tag}"
|
||||||
|
# fails with 422 when the tag's workflow file has no workflow_dispatch
|
||||||
|
# trigger (releases before this job existed); nothing to do then
|
||||||
|
gh workflow run dockerhub.yml --repo "${GITHUB_REPOSITORY}" --ref "${tag}"
|
||||||
|
|||||||
44
.github/workflows/vtag.yml
vendored
Normal file
44
.github/workflows/vtag.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Mirror version tag
|
||||||
|
|
||||||
|
# Historical release tags are plain version numbers ("3.1.3") and cannot be
|
||||||
|
# renamed. When a plain version tag is pushed (including the tag created by
|
||||||
|
# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3")
|
||||||
|
# pointing to the same object, so both forms exist.
|
||||||
|
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
|
||||||
|
# refs created with GITHUB_TOKEN do not fire workflows anyway.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '[0-9]*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
vtag:
|
||||||
|
if: github.repository == 'acmesh-official/acme.sh'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create the v-prefixed tag
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Tag v$TAG already exists, nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Mirror the object the pushed tag actually points at: the commit
|
||||||
|
# for a lightweight tag, the tag object itself for an annotated or
|
||||||
|
# signed one. Pointing the mirror at the commit would strip the
|
||||||
|
# signature, so "git verify-tag v3.1.3" would fail while
|
||||||
|
# "git verify-tag 3.1.3" succeeds.
|
||||||
|
sha="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.sha)"
|
||||||
|
if [ -z "$sha" ] || [ "$sha" = "null" ]; then
|
||||||
|
echo "Could not resolve refs/tags/$TAG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
|
||||||
|
echo "Created tag v$TAG -> $sha"
|
||||||
33
README.md
33
README.md
@@ -36,6 +36,10 @@
|
|||||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
|
||||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg" alt="Tribblix"></a>
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg" alt="Tribblix"></a>
|
||||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
|
||||||
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg" alt="Hurd"></a>
|
||||||
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg" alt="OpenEuler"></a>
|
||||||
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg" alt="HardenedBSD"></a>
|
||||||
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg" alt="OPNsense"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -130,6 +134,10 @@
|
|||||||
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|
|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
|
|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
|
|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
|
||||||
|
|30|[](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml)|HardenedBSD
|
||||||
|
|31|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml)|OPNsense
|
||||||
|
|
||||||
|
|
||||||
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
||||||
@@ -223,6 +231,31 @@ Cron entry example:
|
|||||||
acme.sh -h
|
acme.sh -h
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 🔏 Verify a Release
|
||||||
|
|
||||||
|
Release tags from `3.1.5` on are signed with the maintainer's SSH key. The
|
||||||
|
signing happens on the maintainer's machine, so the private key is never
|
||||||
|
available to CI. The public half is [`allowed_signers`](allowed_signers) in
|
||||||
|
this repository. From a clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config gpg.ssh.allowedSignersFile allowed_signers
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git verify-tag 3.1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
The signature covers the tag object, which pins the commit and therefore the
|
||||||
|
whole tree, so a good signature verifies every file at that release and no
|
||||||
|
separate tarball checksum is needed. Build a tarball from the verified tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2️⃣ Issue a Certificate
|
### 2️⃣ Issue a Certificate
|
||||||
|
|||||||
550
acme.sh
550
acme.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
VER=3.1.4
|
VER=3.1.5
|
||||||
|
|
||||||
PROJECT_NAME="acme.sh"
|
PROJECT_NAME="acme.sh"
|
||||||
|
|
||||||
@@ -1482,39 +1482,57 @@ _readKeyLengthFromCSR() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#port
|
||||||
|
#Reads a netstat or ss listing on stdin, prints the lines that show a socket
|
||||||
|
#listening on port.
|
||||||
|
#Linux and windows print the local address as "addr:port", aix, macos, the
|
||||||
|
#bsds and solaris print it as "addr.port", so both separators must match.
|
||||||
|
#The state is "LISTEN" nearly everywhere, "LISTENING" on windows and lower
|
||||||
|
#case "listen" on haiku, hence the substring match and the -i.
|
||||||
|
_filter_listen_port() {
|
||||||
|
_flp_port="$1"
|
||||||
|
if [ -z "$_flp_port" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
grep -i "LISTEN" | grep "[:.]$_flp_port "
|
||||||
|
}
|
||||||
|
|
||||||
|
#port
|
||||||
_ss() {
|
_ss() {
|
||||||
_port="$1"
|
_port="$1"
|
||||||
|
|
||||||
if _exists "ss"; then
|
if _exists "ss"; then
|
||||||
_debug "Using: ss"
|
_debug "Using: ss"
|
||||||
ss -ntpl 2>/dev/null | grep ":$_port "
|
ss -ntpl 2>/dev/null | _filter_listen_port "$_port"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$(uname)" = "AIX" ]; then
|
#aix, macos and the bsds have no "-p protocol" socket listing that works on
|
||||||
_debug "Using: AIX netstat"
|
#all of them: on netbsd "-p" is "Show statistics about protocol" instead
|
||||||
netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port "
|
#(netstat(1), NetBSD 10.1). Their default display does show "the state of
|
||||||
|
#all sockets" with -a, so use that and keep only the tcp lines.
|
||||||
|
case "$(uname)" in
|
||||||
|
AIX | Darwin | DragonFly | *BSD*)
|
||||||
|
_debug "Using: AIX/BSD netstat"
|
||||||
|
netstat -an | grep "^tcp" | _filter_listen_port "$_port"
|
||||||
return 0
|
return 0
|
||||||
fi
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if _exists "netstat"; then
|
if _exists "netstat"; then
|
||||||
_debug "Using: netstat"
|
_debug "Using: netstat"
|
||||||
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
|
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
|
||||||
#for windows version netstat tool
|
#for windows version netstat tool
|
||||||
netstat -an -p tcp | grep "LISTENING" | grep ":$_port "
|
netstat -an -p tcp | _filter_listen_port "$_port"
|
||||||
else
|
|
||||||
if netstat -help 2>&1 | grep "\-p protocol" >/dev/null; then
|
|
||||||
netstat -an -p tcp | grep LISTEN | grep ":$_port "
|
|
||||||
elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then
|
elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then
|
||||||
#for solaris
|
#for solaris
|
||||||
netstat -an -P tcp | grep "\.$_port " | grep "LISTEN"
|
netstat -an -P tcp | _filter_listen_port "$_port"
|
||||||
elif netstat -help 2>&1 | grep "\-p" >/dev/null; then
|
elif netstat -help 2>&1 | grep "\-p" >/dev/null; then
|
||||||
#for full linux
|
#for full linux
|
||||||
netstat -ntpl | grep ":$_port "
|
netstat -ntpl | _filter_listen_port "$_port"
|
||||||
else
|
else
|
||||||
#for busybox (embedded linux; no pid support)
|
#for busybox (embedded linux; no pid support)
|
||||||
netstat -ntl 2>/dev/null | grep ":$_port "
|
netstat -ntl 2>/dev/null | _filter_listen_port "$_port"
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -2015,6 +2033,32 @@ _calc_validto_renew_time() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Usage: _calc_ari_renew_time aristarttime ariendtime now currenttime pinned
|
||||||
|
#Prints the renew time to take from the CA's ARI suggestedWindow, or nothing
|
||||||
|
#when the window must be ignored. The point inside the window is derived from
|
||||||
|
#the current time rather than its start, so renewals spread out across the
|
||||||
|
#network instead of all firing at the same instant.
|
||||||
|
#A schedule the user pinned with --days or --valid-to only yields to a window
|
||||||
|
#that is EARLIER than what the user asked for: the CA can still pull an urgent
|
||||||
|
#renewal forward, but it can never push a pinned renewal back.
|
||||||
|
_calc_ari_renew_time() {
|
||||||
|
_cart_start="$1"
|
||||||
|
_cart_end="$2"
|
||||||
|
_cart_now="$3"
|
||||||
|
_cart_current="$4"
|
||||||
|
_cart_pinned="$5"
|
||||||
|
if [ -z "$_cart_start" ] || [ -z "$_cart_end" ] || [ "$_cart_end" -le "$_cart_start" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_cart_window=$(_math "$_cart_end" - "$_cart_start")
|
||||||
|
_cart_offset=$(_math "$_cart_now" % "$_cart_window")
|
||||||
|
_cart_next=$(_math "$_cart_start" + "$_cart_offset")
|
||||||
|
if [ "$_cart_pinned" ] && [ "$_cart_current" ] && [ "$_cart_next" -ge "$_cart_current" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf "%s" "$_cart_next"
|
||||||
|
}
|
||||||
|
|
||||||
_mktemp() {
|
_mktemp() {
|
||||||
if _exists mktemp; then
|
if _exists mktemp; then
|
||||||
if mktemp 2>/dev/null; then
|
if mktemp 2>/dev/null; then
|
||||||
@@ -2343,6 +2387,76 @@ _tail_c() {
|
|||||||
tail -c "$1" 2>/dev/null || tail -"$1"c
|
tail -c "$1" 2>/dev/null || tail -"$1"c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#code
|
||||||
|
#Is this status the CA's front end failing rather than its ACME
|
||||||
|
#implementation answering? 502 and 504 mean the proxy could not reach the
|
||||||
|
#backend or gave up waiting for it, 503 that it is overloaded. The body of
|
||||||
|
#those is the proxy's html, not problem+json, so no ACME status can be read
|
||||||
|
#out of it and a caller looking for one abandons an order that is fine.
|
||||||
|
#Anything else, a 500 from the ACME implementation included, is a real
|
||||||
|
#answer and must be passed through to the caller.
|
||||||
|
_is_gateway_error() {
|
||||||
|
case "$1" in
|
||||||
|
502 | 503 | 504) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#response
|
||||||
|
#Does the CA say the order cannot be finalized yet? By the time acme.sh
|
||||||
|
#finalizes, every authorization is valid, and an order becomes ready as
|
||||||
|
#soon as they all are -- so this is the CA's own state lagging, not a
|
||||||
|
#refusal. A struggling CA lags: ZeroSSL answered this on 2026-08-31 forty
|
||||||
|
#seconds after the authorization went valid, in the middle of the 502s it
|
||||||
|
#was serving that morning. Waiting is the answer, not abandoning an order
|
||||||
|
#whose challenges have all passed.
|
||||||
|
_is_order_not_ready() {
|
||||||
|
case "$1" in
|
||||||
|
*acme:error:orderNotReady*) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#response
|
||||||
|
#Does the CA's answer to a revokeCert mean the certificate is revoked? An
|
||||||
|
#empty body is the plain success. urn:ietf:params:acme:error:alreadyRevoked
|
||||||
|
#is "The request specified a certificate to be revoked that has already been
|
||||||
|
#revoked" (RFC 8555 sec 6.7), which is what a user sees when a retry follows
|
||||||
|
#a request the CA did carry out but could not answer: the gateway ate the
|
||||||
|
#reply, not the revocation. Either way the certificate is revoked, which is
|
||||||
|
#what was asked for, so do not report a failure and do not go on to try the
|
||||||
|
#domain key for a certificate that is already gone.
|
||||||
|
#Match the error type, not the bare word: claiming a revocation that did not
|
||||||
|
#happen is far worse than missing one, so a body that merely mentions the
|
||||||
|
#name must not count.
|
||||||
|
_is_revoked_response() {
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case "$1" in
|
||||||
|
*acme:error:alreadyRevoked*) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#attempt
|
||||||
|
#Seconds to wait before retry number <attempt>, for the cases where the CA
|
||||||
|
#gave us no Retry-After to go by. A flat two seconds let the whole twenty
|
||||||
|
#attempt budget burn out in forty eight seconds, which is shorter than the
|
||||||
|
#gateway outages a CA really has: ZeroSSL answered 502 and 504 for over a
|
||||||
|
#minute at a time through August 2026, so every renewal that started during
|
||||||
|
#one of those died instead of waiting it out. Backing off spends the same
|
||||||
|
#twenty attempts over about six minutes, which is still far below the ten
|
||||||
|
#minutes at which a Retry-After is read as the CA refusing outright.
|
||||||
|
_retry_backoff_sec() {
|
||||||
|
case "$1" in
|
||||||
|
1) echo 2 ;;
|
||||||
|
2) echo 5 ;;
|
||||||
|
3) echo 10 ;;
|
||||||
|
*) echo 20 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# url payload needbase64 keyfile
|
# url payload needbase64 keyfile
|
||||||
_send_signed_request() {
|
_send_signed_request() {
|
||||||
url=$1
|
url=$1
|
||||||
@@ -2406,8 +2520,9 @@ _send_signed_request() {
|
|||||||
nonce="$_CACHED_NONCE"
|
nonce="$_CACHED_NONCE"
|
||||||
_debug2 nonce "$nonce"
|
_debug2 nonce "$nonce"
|
||||||
if [ -z "$nonce" ]; then
|
if [ -z "$nonce" ]; then
|
||||||
_info "Could not get nonce, let's try again."
|
_sleep_nonce_sec="$(_retry_backoff_sec "$_request_retry_times")"
|
||||||
_sleep 2
|
_info "Could not get nonce, let's try again. Sleeping for $_sleep_nonce_sec seconds."
|
||||||
|
_sleep "$_sleep_nonce_sec"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -2466,13 +2581,13 @@ _send_signed_request() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
||||||
if [ "$code" = '503' ]; then
|
if _is_gateway_error "$code"; then
|
||||||
_sleep_overload_retry_sec=$_retryafter
|
_sleep_overload_retry_sec=$_retryafter
|
||||||
if [ -z "$_sleep_overload_retry_sec" ]; then
|
if [ -z "$_sleep_overload_retry_sec" ]; then
|
||||||
_sleep_overload_retry_sec=5
|
_sleep_overload_retry_sec="$(_retry_backoff_sec "$_request_retry_times")"
|
||||||
fi
|
fi
|
||||||
if [ $_sleep_overload_retry_sec -le 600 ]; then
|
if [ $_sleep_overload_retry_sec -le 600 ]; then
|
||||||
_info "It seems the CA server is currently overloaded, let's wait and retry. Sleeping for $_sleep_overload_retry_sec seconds."
|
_info "The CA server answered $code, let's wait and retry. Sleeping for $_sleep_overload_retry_sec seconds."
|
||||||
_sleep $_sleep_overload_retry_sec
|
_sleep $_sleep_overload_retry_sec
|
||||||
continue
|
continue
|
||||||
else
|
else
|
||||||
@@ -2499,6 +2614,15 @@ _send_signed_request() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Reads a value from stdin, prints it escaped for use as the replacement text
|
||||||
|
#of a sed s command delimited by '|'. The backslash must go first: a bare one
|
||||||
|
#starts an escape sequence and backslash-digit is a backreference, both make
|
||||||
|
#sed error out. Then '&' (the whole-match reference) and the '|' delimiter.
|
||||||
|
#https://github.com/acmesh-official/acme.sh/issues/7213
|
||||||
|
_sed_escape_rhs() {
|
||||||
|
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g'
|
||||||
|
}
|
||||||
|
|
||||||
#setopt "file" "opt" "=" "value" [";"]
|
#setopt "file" "opt" "=" "value" [";"]
|
||||||
_setopt() {
|
_setopt() {
|
||||||
__conf="$1"
|
__conf="$1"
|
||||||
@@ -2514,34 +2638,50 @@ _setopt() {
|
|||||||
touch "$__conf"
|
touch "$__conf"
|
||||||
chmod 600 "$__conf"
|
chmod 600 "$__conf"
|
||||||
fi
|
fi
|
||||||
|
__nl="
|
||||||
|
"
|
||||||
|
case "$__val" in
|
||||||
|
*"$__nl"*)
|
||||||
|
#the conf format is line based and the file is sourced by the shell, so a
|
||||||
|
#value holding a line break cannot be represented in it (it would also
|
||||||
|
#make the replace sed below fail with an unterminated 's' command)
|
||||||
|
_err "The value of '$__opt' contains a line break, it cannot be saved to $__conf."
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
|
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
|
||||||
echo >>"$__conf"
|
echo >>"$__conf"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
|
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
|
||||||
_debug3 OK
|
_debug3 OK
|
||||||
if _contains "$__val" "&"; then
|
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
|
||||||
__val="$(echo "$__val" | sed 's/&/\\&/g')"
|
|
||||||
fi
|
|
||||||
if _contains "$__val" "|"; then
|
|
||||||
__val="$(echo "$__val" | sed 's/|/\\|/g')"
|
|
||||||
fi
|
|
||||||
text="$(cat "$__conf")"
|
text="$(cat "$__conf")"
|
||||||
printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf"
|
#capture first, write only on success: redirecting sed straight into the
|
||||||
|
#conf file truncates it before sed runs, so a failing sed (e.g. on an
|
||||||
|
#unescaped special character in the value) wiped the whole conf (#2426)
|
||||||
|
if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
|
||||||
|
printf -- "%s\n" "$__text" >"$__conf"
|
||||||
|
else
|
||||||
|
_err "Cannot save '$__opt' to $__conf."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
|
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
|
||||||
if _contains "$__val" "&"; then
|
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
|
||||||
__val="$(echo "$__val" | sed 's/&/\\&/g')"
|
|
||||||
fi
|
|
||||||
if _contains "$__val" "|"; then
|
|
||||||
__val="$(echo "$__val" | sed 's/|/\\|/g')"
|
|
||||||
fi
|
|
||||||
text="$(cat "$__conf")"
|
text="$(cat "$__conf")"
|
||||||
printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf"
|
if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
|
||||||
|
printf -- "%s\n" "$__text" >"$__conf"
|
||||||
|
else
|
||||||
|
_err "Cannot save '$__opt' to $__conf."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
_debug3 APP
|
_debug3 APP
|
||||||
echo "$__opt$__sep$__val$__end" >>"$__conf"
|
#printf, not echo: dash's builtin echo interprets backslash escapes in
|
||||||
|
#the value and would corrupt it
|
||||||
|
printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf"
|
||||||
fi
|
fi
|
||||||
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
|
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
|
||||||
}
|
}
|
||||||
@@ -2569,7 +2709,9 @@ _clear_conf() {
|
|||||||
_sdkey="$2"
|
_sdkey="$2"
|
||||||
if [ "$_c_c_f" ]; then
|
if [ "$_c_c_f" ]; then
|
||||||
_conf_data="$(cat "$_c_c_f")"
|
_conf_data="$(cat "$_c_c_f")"
|
||||||
echo "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
|
#printf, not echo: dash's builtin echo interprets backslash escapes and
|
||||||
|
#would corrupt saved values that contain them on every rewrite
|
||||||
|
printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
|
||||||
else
|
else
|
||||||
_err "Config file is empty, cannot clear"
|
_err "Config file is empty, cannot clear"
|
||||||
fi
|
fi
|
||||||
@@ -2715,6 +2857,21 @@ _clearcaconf() {
|
|||||||
_clear_conf "$CA_CONF" "$1"
|
_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
|
# content localaddress
|
||||||
_startserver() {
|
_startserver() {
|
||||||
content="$1"
|
content="$1"
|
||||||
@@ -2728,16 +2885,24 @@ _startserver() {
|
|||||||
_debug Le_Listen_V4 "$Le_Listen_V4"
|
_debug Le_Listen_V4 "$Le_Listen_V4"
|
||||||
_debug Le_Listen_V6 "$Le_Listen_V6"
|
_debug Le_Listen_V6 "$Le_Listen_V6"
|
||||||
|
|
||||||
|
_serverproc_v6=""
|
||||||
if _exists "socat"; then
|
if _exists "socat"; then
|
||||||
_NC="socat"
|
_NC="socat"
|
||||||
if [ "$Le_Listen_V6" ]; then
|
SOCAT_OPTIONS6=""
|
||||||
|
if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then
|
||||||
_NC="$_NC -6"
|
_NC="$_NC -6"
|
||||||
SOCAT_OPTIONS=TCP6-LISTEN
|
SOCAT_OPTIONS=TCP6-LISTEN
|
||||||
elif [ "$Le_Listen_V4" ]; then
|
elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then
|
||||||
_NC="$_NC -4"
|
_NC="$_NC -4"
|
||||||
SOCAT_OPTIONS=TCP4-LISTEN
|
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
|
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
|
fi
|
||||||
|
|
||||||
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
|
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
|
||||||
@@ -2745,6 +2910,10 @@ _startserver() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
|
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
|
#Adding bind to local-address
|
||||||
if [ "$ncaddr" ]; then
|
if [ "$ncaddr" ]; then
|
||||||
@@ -2753,14 +2922,14 @@ _startserver() {
|
|||||||
|
|
||||||
_content_len="$(printf "%s" "$content" | wc -c)"
|
_content_len="$(printf "%s" "$content" | wc -c)"
|
||||||
_debug _content_len "$_content_len"
|
_debug _content_len "$_content_len"
|
||||||
_debug "_NC" "$_NC $SOCAT_OPTIONS"
|
|
||||||
export _SOCAT_ERR="$(_mktemp)"
|
export _SOCAT_ERR="$(_mktemp)"
|
||||||
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
|
_startsocat "$SOCAT_OPTIONS"
|
||||||
echo 'HTTP/1.0 200 OK'; \
|
serverproc="$_socat_pid"
|
||||||
echo 'Content-Length\: $_content_len'; \
|
if [ "$SOCAT_OPTIONS6" ]; then
|
||||||
echo ''; \
|
#best effort, the host may have no ipv6 support at all
|
||||||
printf '%s' '$content';" 2>"$_SOCAT_ERR" &
|
_startsocat "$SOCAT_OPTIONS6"
|
||||||
serverproc="$!"
|
_serverproc_v6="$_socat_pid"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
_PYTHON=""
|
_PYTHON=""
|
||||||
if _exists "python3"; then
|
if _exists "python3"; then
|
||||||
@@ -2772,19 +2941,38 @@ printf '%s' '$content';" 2>"$_SOCAT_ERR" &
|
|||||||
fi
|
fi
|
||||||
if [ "$_PYTHON" ]; then
|
if [ "$_PYTHON" ]; then
|
||||||
_debug "Using python: $_PYTHON"
|
_debug "Using python: $_PYTHON"
|
||||||
_AF="socket.AF_INET"
|
#a comma separated list of addresses to listen on, one socket for each
|
||||||
_BIND_ADDR="0.0.0.0"
|
_BIND_ADDR="0.0.0.0,::"
|
||||||
if [ "$Le_Listen_V6" ]; then
|
if [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then
|
||||||
_AF="socket.AF_INET6"
|
|
||||||
_BIND_ADDR="::"
|
_BIND_ADDR="::"
|
||||||
|
elif [ "$Le_Listen_V4" ] && [ -z "$Le_Listen_V6" ]; then
|
||||||
|
_BIND_ADDR="0.0.0.0"
|
||||||
fi
|
fi
|
||||||
if [ "$ncaddr" ]; then
|
if [ "$ncaddr" ]; then
|
||||||
_BIND_ADDR="$ncaddr"
|
_BIND_ADDR="$ncaddr"
|
||||||
fi
|
fi
|
||||||
|
_debug "_BIND_ADDR" "$_BIND_ADDR"
|
||||||
export _SOCAT_ERR="$(_mktemp)"
|
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:
|
while True:
|
||||||
c,a=s.accept()
|
for sk in select.select(ls,[],[])[0]:
|
||||||
|
c,a=sk.accept()
|
||||||
c.sendall(res.encode() if hasattr(res, 'encode') else res)
|
c.sendall(res.encode() if hasattr(res, 'encode') else res)
|
||||||
c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
|
c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
|
||||||
serverproc="$!"
|
serverproc="$!"
|
||||||
@@ -2809,6 +2997,11 @@ while True:
|
|||||||
_stopserver() {
|
_stopserver() {
|
||||||
pid="$1"
|
pid="$1"
|
||||||
_debug "pid" "$pid"
|
_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
|
if [ -z "$pid" ]; then
|
||||||
rm -f "$_SOCAT_ERR"
|
rm -f "$_SOCAT_ERR"
|
||||||
return
|
return
|
||||||
@@ -2882,9 +3075,11 @@ _starttlsserver() {
|
|||||||
|
|
||||||
_debug Le_Listen_V4 "$Le_Listen_V4"
|
_debug Le_Listen_V4 "$Le_Listen_V4"
|
||||||
_debug Le_Listen_V6 "$Le_Listen_V6"
|
_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"
|
__S_OPENSSL="$__S_OPENSSL -4"
|
||||||
elif [ "$Le_Listen_V6" ]; then
|
elif [ "$Le_Listen_V6" ] && [ -z "$Le_Listen_V4" ]; then
|
||||||
__S_OPENSSL="$__S_OPENSSL -6"
|
__S_OPENSSL="$__S_OPENSSL -6"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -3858,6 +4053,11 @@ _on_before_issue() {
|
|||||||
if [ -z "$netprc" ]; then
|
if [ -z "$netprc" ]; then
|
||||||
netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS:$_checkport")"
|
netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS:$_checkport")"
|
||||||
fi
|
fi
|
||||||
|
if [ -z "$netprc" ]; then
|
||||||
|
#aix, macos, the bsds and solaris print the wildcard local address as
|
||||||
|
#"*.port", not "0.0.0.0:port", and it blocks $_checkaddr just the same
|
||||||
|
netprc="$(echo "$_netprc" | grep " [*][:.]$_checkport ")"
|
||||||
|
fi
|
||||||
if [ "$netprc" ]; then
|
if [ "$netprc" ]; then
|
||||||
_err "$netprc"
|
_err "$netprc"
|
||||||
_err "tcp port $_checkport is already used by $(echo "$netprc" | cut -d : -f 4)"
|
_err "tcp port $_checkport is already used by $(echo "$netprc" | cut -d : -f 4)"
|
||||||
@@ -4355,6 +4555,24 @@ deactivateaccount() {
|
|||||||
fi
|
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
|
#domain wildcard ca_name days
|
||||||
#Print the TXT record(s) the user must add to enable persistent DNS validation
|
#Print the TXT record(s) the user must add to enable persistent DNS validation
|
||||||
#per draft-ietf-acme-dns-persist-01.
|
#per draft-ietf-acme-dns-persist-01.
|
||||||
@@ -4369,6 +4587,20 @@ makednspersistvalue() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
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
|
if [ -n "$_mdpv_days" ]; then
|
||||||
case "$_mdpv_days" in
|
case "$_mdpv_days" in
|
||||||
'' | *[!0-9]*)
|
'' | *[!0-9]*)
|
||||||
@@ -4400,8 +4632,6 @@ makednspersistvalue() {
|
|||||||
fi
|
fi
|
||||||
_debug "Account URL" "$_accUri"
|
_debug "Account URL" "$_accUri"
|
||||||
|
|
||||||
_txt_name="_validation-persist.$_mdpv_domain"
|
|
||||||
|
|
||||||
_txt_suffix="; accounturi=$_accUri"
|
_txt_suffix="; accounturi=$_accUri"
|
||||||
if [ "$_mdpv_wildcard" = "1" ]; then
|
if [ "$_mdpv_wildcard" = "1" ]; then
|
||||||
_txt_suffix="$_txt_suffix; policy=wildcard"
|
_txt_suffix="$_txt_suffix; policy=wildcard"
|
||||||
@@ -4919,11 +5149,18 @@ issue() {
|
|||||||
if [ -z "$_ACME_IS_RENEW" ]; then
|
if [ -z "$_ACME_IS_RENEW" ]; then
|
||||||
_initpath "$_main_domain" "$_key_length"
|
_initpath "$_main_domain" "$_key_length"
|
||||||
mkdir -p "$DOMAIN_PATH"
|
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_OrderFinalize=""
|
||||||
Le_LinkOrder=""
|
Le_LinkOrder=""
|
||||||
Le_LinkCert=""
|
|
||||||
fi
|
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
|
if _hasfield "$_web_roots" "$W_DNS" && [ -z "$FORCE_DNS_MANUAL" ]; then
|
||||||
_err "$_DNS_MANUAL_ERROR"
|
_err "$_DNS_MANUAL_ERROR"
|
||||||
@@ -5737,11 +5974,25 @@ $_authorizations_map"
|
|||||||
|
|
||||||
_info "Let's finalize the order."
|
_info "Let's finalize the order."
|
||||||
_info "Le_OrderFinalize" "$Le_OrderFinalize"
|
_info "Le_OrderFinalize" "$Le_OrderFinalize"
|
||||||
|
_finalize_retry=0
|
||||||
|
MAX_FINALIZE_RETRY_TIMES=10
|
||||||
|
while [ "$_finalize_retry" -lt "$MAX_FINALIZE_RETRY_TIMES" ]; do
|
||||||
|
_finalize_retry=$(_math "$_finalize_retry" + 1)
|
||||||
if ! _send_signed_request "${Le_OrderFinalize}" "{\"csr\": \"$der\"}"; then
|
if ! _send_signed_request "${Le_OrderFinalize}" "{\"csr\": \"$der\"}"; then
|
||||||
_err "Signing failed."
|
_err "Signing failed."
|
||||||
_on_issue_err "$_post_hook"
|
_on_issue_err "$_post_hook"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
if [ "$code" = "200" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if ! _is_order_not_ready "$response"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
_finalize_wait_sec="$(_retry_backoff_sec "$_finalize_retry")"
|
||||||
|
_info "The order is not ready to be finalized yet, waiting $_finalize_wait_sec seconds. ($_finalize_retry/$MAX_FINALIZE_RETRY_TIMES)"
|
||||||
|
_sleep "$_finalize_wait_sec"
|
||||||
|
done
|
||||||
if [ "$code" != "200" ]; then
|
if [ "$code" != "200" ]; then
|
||||||
_err "Signing failed. Finalize code was not 200."
|
_err "Signing failed. Finalize code was not 200."
|
||||||
_err "$response"
|
_err "$response"
|
||||||
@@ -5925,10 +6176,16 @@ $_authorizations_map"
|
|||||||
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
|
Le_CertCreateTimeStr=$(_time2str "$Le_CertCreateTime")
|
||||||
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
|
_savedomainconf "Le_CertCreateTimeStr" "$Le_CertCreateTimeStr"
|
||||||
|
|
||||||
|
# Le_RenewalDays is only written to the domain conf when the user actually
|
||||||
|
# passed --days; the default schedule is never saved. That is what makes the
|
||||||
|
# presence of the value a reliable "the user pinned this" flag, here and on
|
||||||
|
# every later renewal check.
|
||||||
if [ -z "$Le_RenewalDays" ]; then
|
if [ -z "$Le_RenewalDays" ]; then
|
||||||
Le_RenewalDays="$DEFAULT_RENEW"
|
Le_RenewalDays="$DEFAULT_RENEW"
|
||||||
|
_ari_pinned=""
|
||||||
else
|
else
|
||||||
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
|
_savedomainconf "Le_RenewalDays" "$Le_RenewalDays"
|
||||||
|
_ari_pinned="1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CA_BUNDLE" ]; then
|
if [ "$CA_BUNDLE" ]; then
|
||||||
@@ -5949,13 +6206,18 @@ $_authorizations_map"
|
|||||||
_clearaccountconf "HTTPS_INSECURE"
|
_clearaccountconf "HTTPS_INSECURE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$Le_Listen_V4" ] || [ "$Le_Listen_V6" ]; then
|
||||||
if [ "$Le_Listen_V4" ]; then
|
if [ "$Le_Listen_V4" ]; then
|
||||||
_savedomainconf "Le_Listen_V4" "$Le_Listen_V4"
|
_savedomainconf "Le_Listen_V4" "$Le_Listen_V4"
|
||||||
_cleardomainconf Le_Listen_V6
|
else
|
||||||
elif [ "$Le_Listen_V6" ]; then
|
|
||||||
_savedomainconf "Le_Listen_V6" "$Le_Listen_V6"
|
|
||||||
_cleardomainconf Le_Listen_V4
|
_cleardomainconf Le_Listen_V4
|
||||||
fi
|
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
|
if [ "$Le_ForceNewDomainKey" = "1" ]; then
|
||||||
_savedomainconf "Le_ForceNewDomainKey" "$Le_ForceNewDomainKey"
|
_savedomainconf "Le_ForceNewDomainKey" "$Le_ForceNewDomainKey"
|
||||||
@@ -5997,15 +6259,25 @@ $_authorizations_map"
|
|||||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# RFC 9773 ARI: if the CA exposes renewalInfo, override Le_NextRenewTime
|
# RFC 9773 ARI: if the CA exposes renewalInfo, take Le_NextRenewTime from
|
||||||
# with a time picked at random within the suggestedWindow. This both gives
|
# the suggestedWindow. This gives the CA control over renewal scheduling and
|
||||||
# the CA full control over renewal scheduling and disperses renewals across
|
# disperses renewals across the network so all clients don't hit the CA at
|
||||||
# the network so all clients don't hit the CA at the same instant.
|
# the same instant.
|
||||||
|
# An explicit --days or --valid-to wins over the window, except when the CA
|
||||||
|
# wants the cert renewed EARLIER than the user asked for: an urgent renewal
|
||||||
|
# must still get through. A fixed-date --valid-to opts out entirely, because
|
||||||
|
# there the cert is pinned to an expiry and is not renewed automatically at
|
||||||
|
# all -- letting ARI pull it forward would change that, not just its timing.
|
||||||
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and fall back to
|
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and fall back to
|
||||||
# the legacy time-based renewal calculation.
|
# the legacy time-based renewal calculation.
|
||||||
|
if [ "$_notAfter" ]; then
|
||||||
|
_ari_pinned="1"
|
||||||
|
fi
|
||||||
if [ "$NO_ARI" = "1" ]; then
|
if [ "$NO_ARI" = "1" ]; then
|
||||||
_debug "NO_ARI=1, skipping ARI suggestedWindow override"
|
_debug "NO_ARI=1, skipping ARI suggestedWindow override"
|
||||||
elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ] && [ -z "$_notAfter" ]; then
|
elif [ "$_valid_to" ] && ! _startswith "$_valid_to" "+"; then
|
||||||
|
_debug "Fixed --valid-to, skipping ARI suggestedWindow override"
|
||||||
|
elif [ "$ACME_RENEWAL_INFO" ] && [ -f "$CERT_PATH" ]; then
|
||||||
_ari_resp_new="$(_get_ARI "$CERT_PATH")"
|
_ari_resp_new="$(_get_ARI "$CERT_PATH")"
|
||||||
_debug2 "_ari_resp_new" "$_ari_resp_new"
|
_debug2 "_ari_resp_new" "$_ari_resp_new"
|
||||||
_ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')"
|
_ari_start_new="$(echo "$_ari_resp_new" | _egrep_o '"start" *: *"[^"]*' | sed 's/.*"//')"
|
||||||
@@ -6013,13 +6285,15 @@ $_authorizations_map"
|
|||||||
if [ "$_ari_start_new" ] && [ "$_ari_end_new" ]; then
|
if [ "$_ari_start_new" ] && [ "$_ari_end_new" ]; then
|
||||||
_ari_start_t_new="$(_date2time "$(echo "$_ari_start_new" | sed 's/\.[0-9]*//')")"
|
_ari_start_t_new="$(_date2time "$(echo "$_ari_start_new" | sed 's/\.[0-9]*//')")"
|
||||||
_ari_end_t_new="$(_date2time "$(echo "$_ari_end_new" | sed 's/\.[0-9]*//')")"
|
_ari_end_t_new="$(_date2time "$(echo "$_ari_end_new" | sed 's/\.[0-9]*//')")"
|
||||||
if [ "$_ari_start_t_new" ] && [ "$_ari_end_t_new" ] && [ "$_ari_end_t_new" -gt "$_ari_start_t_new" ]; then
|
_ari_next_new="$(_calc_ari_renew_time "$_ari_start_t_new" "$_ari_end_t_new" "$(_time)" "$Le_NextRenewTime" "$_ari_pinned")"
|
||||||
_ari_window=$(_math "$_ari_end_t_new" - "$_ari_start_t_new")
|
if [ "$_ari_next_new" ]; then
|
||||||
_ari_offset=$(_math "$(_time)" % "$_ari_window")
|
Le_NextRenewTime="$_ari_next_new"
|
||||||
Le_NextRenewTime=$(_math "$_ari_start_t_new" + "$_ari_offset")
|
|
||||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||||
_info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")"
|
_info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")"
|
||||||
_info "Next renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")"
|
_info "Next renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")"
|
||||||
|
elif [ "$_ari_pinned" ]; then
|
||||||
|
_info "ARI suggestedWindow: $(__green "$_ari_start_new") to $(__green "$_ari_end_new")"
|
||||||
|
_info "It is later than the renewal time you asked for, keeping: $(__green "$Le_NextRenewTimeStr")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -6078,7 +6352,10 @@ $_authorizations_map"
|
|||||||
#some devices and APIs reject them, so the certs are stored back to back.
|
#some devices and APIs reject them, so the certs are stored back to back.
|
||||||
#https://github.com/acmesh-official/acme.sh/issues/1940
|
#https://github.com/acmesh-official/acme.sh/issues/1940
|
||||||
_strip_blank_lines() {
|
_strip_blank_lines() {
|
||||||
sed '/^[[:space:]]*$/d'
|
#spell out space and tab: Solaris sed treats [[:space:]] as a literal
|
||||||
|
#bracket set and silently stops matching the blank lines
|
||||||
|
_sbl_tab="$(printf '\t')"
|
||||||
|
sed "/^[ $_sbl_tab]*\$/d"
|
||||||
}
|
}
|
||||||
|
|
||||||
_split_cert_chain() {
|
_split_cert_chain() {
|
||||||
@@ -6159,10 +6436,21 @@ renew() {
|
|||||||
|
|
||||||
# ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check.
|
# ARI (RFC 9773): fetch the CA's suggestedWindow on every renewal check.
|
||||||
# If the window has started, renew now even if Le_NextRenewTime is in the future.
|
# If the window has started, renew now even if Le_NextRenewTime is in the future.
|
||||||
|
# Le_RenewalDays and Le_Valid_To are only in the domain conf when the user
|
||||||
|
# passed --days or --valid-to, so their presence is what pins the schedule.
|
||||||
|
# A pinned schedule still yields to a window that is EARLIER than it, so the
|
||||||
|
# CA can pull an urgent renewal forward. A fixed-date --valid-to opts out
|
||||||
|
# entirely: that cert is not renewed automatically at all.
|
||||||
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only
|
# Set NO_ARI=1 (env, account.conf, or ca.conf) to opt out and use only
|
||||||
# Le_NextRenewTime for the renewal decision.
|
# Le_NextRenewTime for the renewal decision.
|
||||||
|
_ari_pinned=""
|
||||||
|
if [ "$Le_RenewalDays" ] || [ "$Le_Valid_To" ]; then
|
||||||
|
_ari_pinned="1"
|
||||||
|
fi
|
||||||
if [ "$NO_ARI" = "1" ]; then
|
if [ "$NO_ARI" = "1" ]; then
|
||||||
_debug "NO_ARI=1, skipping ARI suggestedWindow check"
|
_debug "NO_ARI=1, skipping ARI suggestedWindow check"
|
||||||
|
elif [ "$Le_Valid_To" ] && ! _startswith "$Le_Valid_To" "+"; then
|
||||||
|
_debug "Fixed --valid-to, skipping ARI suggestedWindow check"
|
||||||
elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
|
elif [ -z "$FORCE" ] && [ -f "$CERT_PATH" ]; then
|
||||||
if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then
|
if _initAPI && [ "$ACME_RENEWAL_INFO" ]; then
|
||||||
_ari_resp="$(_get_ARI "$CERT_PATH")"
|
_ari_resp="$(_get_ARI "$CERT_PATH")"
|
||||||
@@ -6181,16 +6469,19 @@ renew() {
|
|||||||
_debug "Le_NextRenewTime" "$Le_NextRenewTime"
|
_debug "Le_NextRenewTime" "$Le_NextRenewTime"
|
||||||
# Update ARI if needed
|
# Update ARI if needed
|
||||||
if [ "$_ari_start_t" ] && [ "$_ari_end_t" ] && [ "$Le_NextRenewTime" ] && [ "$_ari_end_t" -gt "$_ari_start_t" ] && ([ "$Le_NextRenewTime" -lt "$_ari_start_t" ] || [ "$Le_NextRenewTime" -gt "$_ari_end_t" ]); then
|
if [ "$_ari_start_t" ] && [ "$_ari_end_t" ] && [ "$Le_NextRenewTime" ] && [ "$_ari_end_t" -gt "$_ari_start_t" ] && ([ "$Le_NextRenewTime" -lt "$_ari_start_t" ] || [ "$Le_NextRenewTime" -gt "$_ari_end_t" ]); then
|
||||||
|
_ari_next="$(_calc_ari_renew_time "$_ari_start_t" "$_ari_end_t" "$(_time)" "$Le_NextRenewTime" "$_ari_pinned")"
|
||||||
|
if [ "$_ari_next" ]; then
|
||||||
_ari_old_time_str="$Le_NextRenewTimeStr"
|
_ari_old_time_str="$Le_NextRenewTimeStr"
|
||||||
_info "Current renewal time: $(__green "$_ari_old_time_str")"
|
_info "Current renewal time: $(__green "$_ari_old_time_str")"
|
||||||
_ari_window=$(_math "$_ari_end_t" - "$_ari_start_t")
|
Le_NextRenewTime="$_ari_next"
|
||||||
_ari_offset=$(_math "$(_time)" % "$_ari_window")
|
|
||||||
Le_NextRenewTime=$(_math "$_ari_start_t" + "$_ari_offset")
|
|
||||||
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
Le_NextRenewTimeStr=$(_time2str "$Le_NextRenewTime")
|
||||||
_info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")"
|
_info "ARI suggestedWindow: $(__green "$_ari_start") to $(__green "$_ari_end")"
|
||||||
_info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")"
|
_info "Updating renewal time picked from ARI window: $(__green "$Le_NextRenewTimeStr")"
|
||||||
_savedomainconf Le_NextRenewTime "$Le_NextRenewTime"
|
_savedomainconf Le_NextRenewTime "$Le_NextRenewTime"
|
||||||
_savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr"
|
_savedomainconf Le_NextRenewTimeStr "$Le_NextRenewTimeStr"
|
||||||
|
else
|
||||||
|
_debug "ARI wants a later renewal than --days/--valid-to asked for, keeping $Le_NextRenewTimeStr"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [ "$Le_NextRenewTime" ] && [ "$(_time)" -ge "$Le_NextRenewTime" ]; then
|
if [ "$Le_NextRenewTime" ] && [ "$(_time)" -ge "$Le_NextRenewTime" ]; then
|
||||||
_info "ARI suggested renewal has passed ($(__green "$Le_NextRenewTimeStr")), proceeding with renewal."
|
_info "ARI suggested renewal has passed ($(__green "$Le_NextRenewTimeStr")), proceeding with renewal."
|
||||||
@@ -6974,6 +7265,30 @@ _uninstall_win_taskscheduler() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#binpath
|
||||||
|
#Reads a crontab listing from stdin, prints it without the acme.sh cron
|
||||||
|
#entries that call binpath.
|
||||||
|
_filter_cron_bin() {
|
||||||
|
_fcb_bin="$1"
|
||||||
|
if [ -z "$_fcb_bin" ]; then
|
||||||
|
cat
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
#a case pattern with a quoted variable matches binpath literally, which
|
||||||
|
#grep cannot do portably: Solaris /usr/bin/grep has no -F, and as a regex
|
||||||
|
#the dot of ~/.acme.sh would stand for any character
|
||||||
|
while IFS= read -r _fcb_line || [ -n "$_fcb_line" ]; do
|
||||||
|
case "$_fcb_line" in
|
||||||
|
*"$_fcb_bin --cron"*)
|
||||||
|
_debug3 "Dropping cron entry" "$_fcb_line"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$_fcb_line"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
#confighome
|
#confighome
|
||||||
installcronjob() {
|
installcronjob() {
|
||||||
_c_home="$1"
|
_c_home="$1"
|
||||||
@@ -7043,7 +7358,26 @@ installcronjob() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if ! echo "$_cron_entries" | grep "$PROJECT_ENTRY --cron"; then
|
#An entry that calls LE_WORKING_DIR/PROJECT_ENTRY is dead once that copy is
|
||||||
|
#gone: ACME_PACKAGED installs never write it, and the package manager
|
||||||
|
#removes it when it takes over. The entry below would then keep the install
|
||||||
|
#from adding a working one and cron would fail silently every day, so drop
|
||||||
|
#the stale entries first.
|
||||||
|
_cron_stale=""
|
||||||
|
if [ ! -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ] && [ "$_cron_entries" ]; then
|
||||||
|
_cron_kept="$(echo "$_cron_entries" | _filter_cron_bin "\"$LE_WORKING_DIR\"/$PROJECT_ENTRY")"
|
||||||
|
if [ "$_cron_kept" != "$_cron_entries" ]; then
|
||||||
|
_info "Removing the cron job that calls the missing $LE_WORKING_DIR/$PROJECT_ENTRY"
|
||||||
|
_cron_entries="$_cron_kept"
|
||||||
|
_cron_stale=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
#>/dev/null: grep would print the matching crontab line to the console
|
||||||
|
_cron_add=""
|
||||||
|
if ! echo "$_cron_entries" | grep "$PROJECT_ENTRY --cron" >/dev/null; then
|
||||||
|
_cron_add=1
|
||||||
|
fi
|
||||||
|
if [ "$_cron_add" ] || [ "$_cron_stale" ]; then
|
||||||
if _exists uname && uname -a | grep SunOS >/dev/null; then
|
if _exists uname && uname -a | grep SunOS >/dev/null; then
|
||||||
_CRONTAB_STDIN="$_CRONTAB --"
|
_CRONTAB_STDIN="$_CRONTAB --"
|
||||||
else
|
else
|
||||||
@@ -7053,7 +7387,9 @@ installcronjob() {
|
|||||||
if [ "$_cron_entries" ]; then
|
if [ "$_cron_entries" ]; then
|
||||||
echo "$_cron_entries"
|
echo "$_cron_entries"
|
||||||
fi
|
fi
|
||||||
|
if [ "$_cron_add" ]; then
|
||||||
echo "$_cron_entry"
|
echo "$_cron_entry"
|
||||||
|
fi
|
||||||
} | $_CRONTAB_STDIN
|
} | $_CRONTAB_STDIN
|
||||||
fi
|
fi
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
@@ -7158,7 +7494,7 @@ revoke() {
|
|||||||
|
|
||||||
_info "Trying account key first."
|
_info "Trying account key first."
|
||||||
if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then
|
if _send_signed_request "$uri" "$data" "" "$ACCOUNT_KEY_PATH"; then
|
||||||
if [ -z "$response" ]; then
|
if _is_revoked_response "$response"; then
|
||||||
_info "Successfully revoked."
|
_info "Successfully revoked."
|
||||||
rm -f "$CERT_PATH"
|
rm -f "$CERT_PATH"
|
||||||
cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked"
|
cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked"
|
||||||
@@ -7173,7 +7509,7 @@ revoke() {
|
|||||||
if [ -f "$CERT_KEY_PATH" ]; then
|
if [ -f "$CERT_KEY_PATH" ]; then
|
||||||
_info "Trying domain key."
|
_info "Trying domain key."
|
||||||
if _send_signed_request "$uri" "$data" "" "$CERT_KEY_PATH"; then
|
if _send_signed_request "$uri" "$data" "" "$CERT_KEY_PATH"; then
|
||||||
if [ -z "$response" ]; then
|
if _is_revoked_response "$response"; then
|
||||||
_info "Successfully revoked."
|
_info "Successfully revoked."
|
||||||
rm -f "$CERT_PATH"
|
rm -f "$CERT_PATH"
|
||||||
cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked"
|
cat "$CERT_KEY_PATH" >"$CERT_KEY_PATH.revoked"
|
||||||
@@ -7366,10 +7702,18 @@ deactivate() {
|
|||||||
done
|
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
|
#cert
|
||||||
_getAKI() {
|
_getAKI() {
|
||||||
_cert="$1"
|
_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
|
#cert
|
||||||
@@ -7525,6 +7869,15 @@ _installalias() {
|
|||||||
_c_home="$1"
|
_c_home="$1"
|
||||||
_initpath
|
_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"
|
_envfile="$LE_WORKING_DIR/$PROJECT_ENTRY.env"
|
||||||
if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then
|
if [ "$_upgrading" ] && [ "$_upgrading" = "1" ]; then
|
||||||
echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile"
|
echo "$(cat "$_envfile")" | sed "s|^LE_WORKING_DIR.*$||" >"$_envfile"
|
||||||
@@ -7542,7 +7895,7 @@ _installalias() {
|
|||||||
else
|
else
|
||||||
_sed_i "/^export LE_CONFIG_HOME/d" "$_envfile"
|
_sed_i "/^export LE_CONFIG_HOME/d" "$_envfile"
|
||||||
fi
|
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
|
if [ -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion" ]; then
|
||||||
#the completion file does nothing when sourced by a non-bash shell
|
#the completion file does nothing when sourced by a non-bash shell
|
||||||
_setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\""
|
_setopt "$_envfile" ". \"$LE_WORKING_DIR/$PROJECT_ENTRY.completion\""
|
||||||
@@ -7569,7 +7922,7 @@ _installalias() {
|
|||||||
else
|
else
|
||||||
_sed_i "/^setenv LE_CONFIG_HOME/d" "$_cshfile"
|
_sed_i "/^setenv LE_CONFIG_HOME/d" "$_cshfile"
|
||||||
fi
|
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\""
|
_setopt "$_csh_profile" "source \"$_cshfile\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -7581,7 +7934,7 @@ _installalias() {
|
|||||||
if [ "$_c_home" ]; then
|
if [ "$_c_home" ]; then
|
||||||
_setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\""
|
_setopt "$_cshfile" "setenv LE_CONFIG_HOME" " " "\"$LE_CONFIG_HOME\""
|
||||||
fi
|
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\""
|
_setopt "$_tcsh_profile" "source \"$_cshfile\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -7655,6 +8008,11 @@ install() {
|
|||||||
chmod 700 "$LE_CONFIG_HOME"
|
chmod 700 "$LE_CONFIG_HOME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
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"
|
cp "$PROJECT_ENTRY" "$LE_WORKING_DIR/" && chmod +x "$LE_WORKING_DIR/$PROJECT_ENTRY"
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
@@ -7668,17 +8026,20 @@ install() {
|
|||||||
cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/"
|
cp "$PROJECT_ENTRY.completion" "$LE_WORKING_DIR/"
|
||||||
_debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion"
|
_debug "Installed bash completion to $LE_WORKING_DIR/$PROJECT_ENTRY.completion"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then
|
if [ "$_ACME_IN_CRON" != "1" ] && [ -z "$_noprofile" ]; then
|
||||||
_installalias "$_c_home"
|
_installalias "$_c_home"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ACME_PACKAGED" ]; then
|
||||||
for subf in $_SUB_FOLDERS; do
|
for subf in $_SUB_FOLDERS; do
|
||||||
if [ -d "$subf" ]; then
|
if [ -d "$subf" ]; then
|
||||||
mkdir -p "$LE_WORKING_DIR/$subf"
|
mkdir -p "$LE_WORKING_DIR/$subf"
|
||||||
cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/
|
cp "$subf"/* "$LE_WORKING_DIR"/"$subf"/
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
if [ ! -f "$ACCOUNT_CONF_PATH" ]; then
|
if [ ! -f "$ACCOUNT_CONF_PATH" ]; then
|
||||||
_initconf
|
_initconf
|
||||||
@@ -7706,7 +8067,7 @@ install() {
|
|||||||
installcronjob "$_c_home"
|
installcronjob "$_c_home"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$NO_DETECT_SH" ]; then
|
if [ -z "$NO_DETECT_SH" ] && [ -z "$ACME_PACKAGED" ]; then
|
||||||
#Modify shebang
|
#Modify shebang
|
||||||
if _exists bash; then
|
if _exists bash; then
|
||||||
_bash_path="$(bash -c "command -v bash 2>/dev/null")"
|
_bash_path="$(bash -c "command -v bash 2>/dev/null")"
|
||||||
@@ -7731,7 +8092,9 @@ install() {
|
|||||||
if [ "$_accountemail" ]; then
|
if [ "$_accountemail" ]; then
|
||||||
_saveaccountconf "ACCOUNT_EMAIL" "$_accountemail"
|
_saveaccountconf "ACCOUNT_EMAIL" "$_accountemail"
|
||||||
fi
|
fi
|
||||||
|
if [ -z "$ACME_PACKAGED" ]; then
|
||||||
_saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)"
|
_saveaccountconf "UPGRADE_HASH" "$(_getUpgradeHash)"
|
||||||
|
fi
|
||||||
_info OK
|
_info OK
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7745,8 +8108,12 @@ uninstall() {
|
|||||||
|
|
||||||
_uninstallalias
|
_uninstallalias
|
||||||
|
|
||||||
|
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"
|
||||||
rm -f "$LE_WORKING_DIR/$PROJECT_ENTRY.completion"
|
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."
|
_info "The keys and certs are in \"$(__green "$LE_CONFIG_HOME")\". You can remove them by yourself."
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7782,6 +8149,9 @@ cron() {
|
|||||||
_initpath
|
_initpath
|
||||||
_info "$(__green "===Starting cron===")"
|
_info "$(__green "===Starting cron===")"
|
||||||
if [ "$AUTO_UPGRADE" = "1" ]; then
|
if [ "$AUTO_UPGRADE" = "1" ]; then
|
||||||
|
if [ "$ACME_PACKAGED" ]; then
|
||||||
|
_info "ACME_PACKAGED is set, skipping the auto upgrade."
|
||||||
|
else
|
||||||
export LE_WORKING_DIR
|
export LE_WORKING_DIR
|
||||||
(
|
(
|
||||||
if ! upgrade; then
|
if ! upgrade; then
|
||||||
@@ -7797,6 +8167,7 @@ cron() {
|
|||||||
|
|
||||||
_info "Automatically upgraded to: $VER"
|
_info "Automatically upgraded to: $VER"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
_TREAT_SKIP_AS_SUCCESS="1"
|
_TREAT_SKIP_AS_SUCCESS="1"
|
||||||
renewAll
|
renewAll
|
||||||
_ret="$?"
|
_ret="$?"
|
||||||
@@ -8037,7 +8408,9 @@ Parameters:
|
|||||||
|
|
||||||
--dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the
|
--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
|
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 <name> Used with '--make-dns-persist-value'. Use the given CA identity domain
|
--dns-persist-ca-name <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
|
(e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If
|
||||||
omitted, the identities are read from the ACME directory's
|
omitted, the identities are read from the ACME directory's
|
||||||
@@ -8095,8 +8468,9 @@ Parameters:
|
|||||||
--ocsp, --ocsp-must-staple Generate OCSP-Must-Staple extension.
|
--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.
|
--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.
|
--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-v4 Force standalone/tls server to listen at ipv4 only.
|
||||||
--listen-v6 Force standalone/tls server to listen at ipv6.
|
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-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.
|
--request-v6 Force client requests to use ipv6 to connect to the CA server.
|
||||||
--openssl-bin <file> Specifies a custom openssl bin location.
|
--openssl-bin <file> Specifies a custom openssl bin location.
|
||||||
@@ -8125,6 +8499,10 @@ Parameters:
|
|||||||
}
|
}
|
||||||
|
|
||||||
installOnline() {
|
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."
|
_info "Installing from online archive."
|
||||||
|
|
||||||
_branch="$BRANCH"
|
_branch="$BRANCH"
|
||||||
@@ -8182,6 +8560,10 @@ _getUpgradeHash() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upgrade() {
|
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 (
|
if (
|
||||||
_initpath
|
_initpath
|
||||||
[ -z "$FORCE" ] && [ "$(_getUpgradeHash)" = "$(_readaccountconf "UPGRADE_HASH")" ] && _info "Already up to date!" && exit 0
|
[ -z "$FORCE" ] && [ "$(_getUpgradeHash)" = "$(_readaccountconf "UPGRADE_HASH")" ] && _info "Already up to date!" && exit 0
|
||||||
|
|||||||
20
allowed_signers
Normal file
20
allowed_signers
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# acme.sh release signing key.
|
||||||
|
#
|
||||||
|
# Release tags are signed with this key. Its private half is held by the
|
||||||
|
# maintainer and is never available to CI, so a compromise of the build
|
||||||
|
# pipeline cannot produce a tag that verifies against this file.
|
||||||
|
#
|
||||||
|
# Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE
|
||||||
|
#
|
||||||
|
# To verify a release tag, from a clone of this repository:
|
||||||
|
#
|
||||||
|
# git config gpg.ssh.allowedSignersFile allowed_signers
|
||||||
|
# git verify-tag 3.1.5
|
||||||
|
#
|
||||||
|
# A good signature covers the tag object, which pins the commit, which pins
|
||||||
|
# the whole tree -- so verifying the tag verifies every file at that
|
||||||
|
# release. Build a tarball from the verified tag with:
|
||||||
|
#
|
||||||
|
# git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
|
||||||
|
#
|
||||||
|
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB
|
||||||
@@ -163,8 +163,8 @@ byteplus_alb_deploy() {
|
|||||||
# ── 3. Read cert and key ─────────────────────────────────────────────────────
|
# ── 3. Read cert and key ─────────────────────────────────────────────────────
|
||||||
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
|
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
|
||||||
|
|
||||||
_public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r')
|
_public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
|
||||||
_private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r')
|
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
|
||||||
|
|
||||||
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
|
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
|
||||||
_err "Failed to read certificate or key file."
|
_err "Failed to read certificate or key file."
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ cpanel_uapi_deploy() {
|
|||||||
# Auto mode
|
# Auto mode
|
||||||
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
|
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
|
||||||
# call API for site config
|
# call API for site config
|
||||||
|
if [ -n "$_uapi_user" ]; then
|
||||||
|
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
|
||||||
|
else
|
||||||
_response=$(uapi DomainInfo list_domains)
|
_response=$(uapi DomainInfo list_domains)
|
||||||
|
fi
|
||||||
# exit if error in response
|
# exit if error in response
|
||||||
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
|
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
|
||||||
_err "Error in deploying certificate - cannot retrieve sitelist:"
|
_err "Error in deploying certificate - cannot retrieve sitelist:"
|
||||||
|
|||||||
@@ -173,10 +173,7 @@ haproxy_deploy() {
|
|||||||
# Set the suffix depending if we are creating a bundle or not
|
# Set the suffix depending if we are creating a bundle or not
|
||||||
if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
|
if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
|
||||||
_info "Bundle creation requested"
|
_info "Bundle creation requested"
|
||||||
# Initialise $Le_Keylength if its not already set
|
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||||
if [ -z "${Le_Keylength}" ]; then
|
|
||||||
Le_Keylength=""
|
|
||||||
fi
|
|
||||||
if _isEccKey "${Le_Keylength}"; then
|
if _isEccKey "${Le_Keylength}"; then
|
||||||
_info "ECC key type detected"
|
_info "ECC key type detected"
|
||||||
_suffix=".ecdsa"
|
_suffix=".ecdsa"
|
||||||
|
|||||||
114
deploy/ikuai.sh
Normal file
114
deploy/ikuai.sh
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
_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"
|
_H1="Cookie: $_cookie"
|
||||||
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
|
_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 class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
||||||
_info "_message" "$_message"
|
_info "_message" "$_message"
|
||||||
if [ -z "$_message" ]; then
|
if [ -z "$_message" ]; then
|
||||||
_err "Fail to upload certificate."
|
_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"
|
_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")
|
_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 class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
||||||
_info "_message" "$_message"
|
_info "_message" "$_message"
|
||||||
if [ -z "$_message" ]; then
|
if [ -z "$_message" ]; then
|
||||||
_err "Fail to apply certificate."
|
_err "Fail to apply certificate."
|
||||||
|
|||||||
@@ -121,10 +121,7 @@ lighttpd_deploy() {
|
|||||||
# Set the suffix depending if we are creating a bundle or not
|
# Set the suffix depending if we are creating a bundle or not
|
||||||
if [ "${Le_Deploy_lighttpd_bundle}" = "yes" ]; then
|
if [ "${Le_Deploy_lighttpd_bundle}" = "yes" ]; then
|
||||||
_info "Bundle creation requested"
|
_info "Bundle creation requested"
|
||||||
# Initialise $Le_Keylength if its not already set
|
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||||
if [ -z "${Le_Keylength}" ]; then
|
|
||||||
Le_Keylength=""
|
|
||||||
fi
|
|
||||||
if _isEccKey "${Le_Keylength}"; then
|
if _isEccKey "${Le_Keylength}"; then
|
||||||
_info "ECC key type detected"
|
_info "ECC key type detected"
|
||||||
_suffix=".ecdsa"
|
_suffix=".ecdsa"
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
# Usage (shown values are the examples):
|
# Usage (shown values are the examples):
|
||||||
# 1. Set optional environment variables
|
# 1. Set optional environment variables
|
||||||
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
|
# - 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:
|
# 2. Run command:
|
||||||
# acme.sh --deploy --deploy-hook multideploy -d example.com
|
# acme.sh --deploy --deploy-hook multideploy -d example.com
|
||||||
@@ -49,7 +53,7 @@ multideploy_deploy() {
|
|||||||
_debug _cfullchain "$_cfullchain"
|
_debug _cfullchain "$_cfullchain"
|
||||||
_debug _cpfx "$_cpfx"
|
_debug _cpfx "$_cpfx"
|
||||||
|
|
||||||
MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}"
|
_getdeployconf MULTIDEPLOY_FILENAME
|
||||||
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
|
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
|
||||||
MULTIDEPLOY_FILENAME="multideploy.yml"
|
MULTIDEPLOY_FILENAME="multideploy.yml"
|
||||||
_info "MULTIDEPLOY_FILENAME is not set, so I will use '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,
|
# 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.
|
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
|
||||||
# Arguments:
|
# 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:
|
# Usage:
|
||||||
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
|
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
|
||||||
_preprocess_deployfile() {
|
_preprocess_deployfile() {
|
||||||
@@ -87,15 +92,21 @@ _preprocess_deployfile() {
|
|||||||
_debug3 "yq is installed."
|
_debug3 "yq is installed."
|
||||||
|
|
||||||
# Check if deploy file exists
|
# Check if deploy file exists
|
||||||
|
found_file=""
|
||||||
for file in "$@"; do
|
for file in "$@"; do
|
||||||
_debug3 "Checking file" "$DOMAIN_PATH/$file"
|
if _startswith "$file" "/"; then
|
||||||
if [ -f "$DOMAIN_PATH/$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"
|
_debug3 "File found"
|
||||||
if [ -n "$found_file" ]; then
|
if [ -n "$found_file" ]; then
|
||||||
_err "Multiple deploy files found. Please keep only one deploy file."
|
_err "Multiple deploy files found. Please keep only one deploy file."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
found_file="$file"
|
found_file="$_multideploy_path"
|
||||||
else
|
else
|
||||||
_debug3 "File not found"
|
_debug3 "File not found"
|
||||||
fi
|
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."
|
_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
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then
|
if ! _check_deployfile "$found_file"; then
|
||||||
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file"
|
_err "Deploy file is not valid: $found_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$DOMAIN_PATH/$found_file"
|
echo "$found_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Description:
|
# Description:
|
||||||
|
|||||||
@@ -116,17 +116,24 @@ HEREDOC
|
|||||||
export HTTPS_INSECURE=1
|
export HTTPS_INSECURE=1
|
||||||
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
|
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
|
||||||
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
|
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)"
|
response="$(echo "$response" | _json_decode | _normalizeJson)"
|
||||||
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
||||||
_retval=$?
|
case "$_status_code" in
|
||||||
|
2[0-9][0-9])
|
||||||
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
|
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
|
||||||
_debug3 response "$response"
|
_debug3 response "$response"
|
||||||
_info "Certificate successfully deployed"
|
_info "Certificate successfully deployed"
|
||||||
return 0
|
return 0
|
||||||
else
|
fi
|
||||||
_err "Certificate deployment failed: $message"
|
;;
|
||||||
|
esac
|
||||||
|
_err "Certificate deployment failed (HTTP status $_status_code). $message"
|
||||||
_debug "Response" "$response"
|
_debug "Response" "$response"
|
||||||
return 1
|
return 1
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,17 +128,24 @@ HEREDOC
|
|||||||
export HTTPS_INSECURE=1
|
export HTTPS_INSECURE=1
|
||||||
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
|
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
|
||||||
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
|
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)"
|
response="$(echo "$response" | _json_decode | _normalizeJson)"
|
||||||
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
|
||||||
_retval=$?
|
case "$_status_code" in
|
||||||
|
2[0-9][0-9])
|
||||||
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
|
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
|
||||||
_debug3 response "$response"
|
_debug3 response "$response"
|
||||||
_info "Certificate successfully deployed"
|
_info "Certificate successfully deployed"
|
||||||
return 0
|
return 0
|
||||||
else
|
fi
|
||||||
_err "Certificate deployment failed: $message"
|
;;
|
||||||
|
esac
|
||||||
|
_err "Certificate deployment failed (HTTP status $_status_code). $message"
|
||||||
_debug "Response" "$response"
|
_debug "Response" "$response"
|
||||||
return 1
|
return 1
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
280
deploy/shelly.sh
Normal file
280
deploy/shelly.sh
Normal file
@@ -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 <method> <params_json>
|
||||||
|
# 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
|
||||||
|
}
|
||||||
@@ -344,6 +344,7 @@ synology_dsm_deploy() {
|
|||||||
else
|
else
|
||||||
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
|
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
|
||||||
fi
|
fi
|
||||||
|
_logout
|
||||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -354,6 +355,7 @@ synology_dsm_deploy() {
|
|||||||
|
|
||||||
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
|
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
|
||||||
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
|
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
|
||||||
|
_logout
|
||||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -389,13 +391,13 @@ synology_dsm_deploy() {
|
|||||||
else
|
else
|
||||||
_info "Restart HTTP services not necessary."
|
_info "Restart HTTP services not necessary."
|
||||||
fi
|
fi
|
||||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
|
||||||
_logout
|
_logout
|
||||||
|
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
|
||||||
_err "Unable to update certificate, got error response: $response."
|
_err "Unable to update certificate, got error response: $response."
|
||||||
_logout
|
_logout
|
||||||
|
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -403,6 +405,8 @@ synology_dsm_deploy() {
|
|||||||
#################### Private functions below ##################################
|
#################### Private functions below ##################################
|
||||||
_logout() {
|
_logout() {
|
||||||
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
|
# 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")
|
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
|
||||||
_debug3 response "$response"
|
_debug3 response "$response"
|
||||||
}
|
}
|
||||||
|
|||||||
341
deploy/unifios.sh
Normal file
341
deploy/unifios.sh
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Deploy hook for UniFi OS, via the certificate REST API.
|
||||||
|
#
|
||||||
|
# Works against any UniFi OS whose management UI exposes
|
||||||
|
# /api/userCertificates. Confirmed on:
|
||||||
|
# - UniFi OS Server (the separately-installed, self-hosted application)
|
||||||
|
# on macOS and on Linux. Windows should also work (it runs under
|
||||||
|
# WSL2), but has not been tested.
|
||||||
|
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
|
||||||
|
# - UniFi OS hardware: UDM Pro on UniFi OS 5.1.26, UCG Fiber on
|
||||||
|
# UniFi OS 5.0.16 (user reports, see issues 7184 and 6916).
|
||||||
|
# No lower version bound is claimed -- if the UI has a certificate
|
||||||
|
# manager, this hook should work.
|
||||||
|
#
|
||||||
|
# `unifios` vs `unifi`: the split is the access method, not the product
|
||||||
|
# line. `unifi` writes files / a Java keystore and needs local or SSH
|
||||||
|
# access on the device; this hook drives the same REST API the web UI
|
||||||
|
# uses and works remotely. Use `unifi` where acme.sh runs on the device
|
||||||
|
# itself, this hook where it does not.
|
||||||
|
#
|
||||||
|
# The API is served on the management port, which differs per install:
|
||||||
|
# UniFi OS Server listens on 11443 (hence the default below), while
|
||||||
|
# UniFi OS hardware serves it on 443 -- set DEPLOY_UNIFIOS_HOST to
|
||||||
|
# "https://<host>" there.
|
||||||
|
#
|
||||||
|
# Endpoints used, all as the web UI itself calls them:
|
||||||
|
# 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 may be
|
||||||
|
# self-signed -- it always is on a fresh install, and there is no reliable
|
||||||
|
# way to tell in advance whether an earlier run has already replaced it --
|
||||||
|
# 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", i.e. a UniFi OS Server on the
|
||||||
|
# same machine as acme.sh; set it to "https://<host>" for UniFi OS
|
||||||
|
# hardware or any remote target)
|
||||||
|
# DEPLOY_UNIFIOS_USERNAME - UniFi OS admin username (required)
|
||||||
|
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS 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
|
||||||
|
}
|
||||||
|
|
||||||
|
_uos_split_json() {
|
||||||
|
# $1 = raw JSON list response
|
||||||
|
#
|
||||||
|
# _normalizeJson collapses the response to one line. This removes extra
|
||||||
|
# space around colons. It also removes any CR or LF characters that the
|
||||||
|
# server can add. However, _normalizeJson also removes the newline
|
||||||
|
# character at the end of the line. If the line has no ending newline
|
||||||
|
# character, some sed programs drop the last line of input. This code
|
||||||
|
# adds the newline back before the split below, to prevent that problem.
|
||||||
|
_uos_normalized="$(echo "$1" | _normalizeJson)"
|
||||||
|
# A literal newline character splits the JSON into one object per line.
|
||||||
|
# Grep can then match a single certificate entry at a time. This is not
|
||||||
|
# the two-character "\n" sequence: GNU sed reads "\n" in the replacement
|
||||||
|
# text as a newline character. POSIX does not define this behavior, and
|
||||||
|
# BSD sed prints "\n" as two literal characters, not as a newline.
|
||||||
|
printf '%s\n' "$_uos_normalized" | sed 's/},{/},\
|
||||||
|
{/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
_uos_grep_literal() {
|
||||||
|
# $1 = literal text to find, matched without a regex -- portable to
|
||||||
|
# grep implementations with no -F flag (e.g. Solaris), and avoids "*"
|
||||||
|
# or "." in a domain name being read as a regex metacharacter.
|
||||||
|
while IFS= read -r _uos_line || [ -n "$_uos_line" ]; do
|
||||||
|
case "$_uos_line" in
|
||||||
|
*"$1"*) printf '%s\n' "$_uos_line" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
#
|
||||||
|
# This name includes the key type (rsa or ecdsa), to keep an RSA
|
||||||
|
# deploy and an ECC deploy of the same domain from sharing this
|
||||||
|
# prefix. Without the key type, the cleanup step for each deploy
|
||||||
|
# removes the entry that the other deploy creates. `deploy/haproxy.sh`
|
||||||
|
# and `deploy/lighttpd.sh` use the same `_isEccKey` check, for the same
|
||||||
|
# reason.
|
||||||
|
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||||
|
if _isEccKey "${Le_Keylength}"; then
|
||||||
|
_uos_keytype="ecdsa"
|
||||||
|
else
|
||||||
|
_uos_keytype="rsa"
|
||||||
|
fi
|
||||||
|
_uos_name="$_cdomain $_uos_keytype $(_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
|
||||||
|
_list_json="$(_uos_split_json "$_list_json")"
|
||||||
|
_new_id="$(echo "$_list_json" | _uos_grep_literal "\"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="$(_uos_split_json "$_list_json")"
|
||||||
|
# The pattern below matches the domain name and key type, 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" | _uos_grep_literal "\"name\":\"$_cdomain $_uos_keytype " | _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
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ Options:
|
|||||||
AZUREDNS_APPID App ID. App ID of the service principal
|
AZUREDNS_APPID App ID. App ID of the service principal
|
||||||
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
|
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
|
||||||
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
|
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
|
||||||
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
|
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Not saved, provide it on every run. Optional.
|
||||||
'
|
'
|
||||||
|
|
||||||
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
|
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
|
||||||
@@ -47,13 +47,15 @@ dns_azure_add() {
|
|||||||
_saveaccountconf_mutable AZUREDNS_TENANTID ""
|
_saveaccountconf_mutable AZUREDNS_TENANTID ""
|
||||||
_saveaccountconf_mutable AZUREDNS_APPID ""
|
_saveaccountconf_mutable AZUREDNS_APPID ""
|
||||||
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
|
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
|
||||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN ""
|
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
|
||||||
else
|
else
|
||||||
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
|
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
|
||||||
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
||||||
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
||||||
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
||||||
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
#AZUREDNS_BEARERTOKEN is short-lived, so it is taken from the environment only and never
|
||||||
|
#read from or saved to the account conf. Versions up to 3.0.9 cached their internal access
|
||||||
|
#token under the same name, which must not be replayed as a user token (#7218).
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||||
AZUREDNS_SUBSCRIPTIONID=""
|
AZUREDNS_SUBSCRIPTIONID=""
|
||||||
@@ -93,7 +95,7 @@ dns_azure_add() {
|
|||||||
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
|
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
|
||||||
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
|
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
|
||||||
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
|
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
|
||||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN"
|
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
@@ -175,7 +177,7 @@ dns_azure_rm() {
|
|||||||
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
||||||
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
||||||
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
||||||
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
#AZUREDNS_BEARERTOKEN comes from the environment only, see the note in dns_azure_add
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||||
AZUREDNS_SUBSCRIPTIONID=""
|
AZUREDNS_SUBSCRIPTIONID=""
|
||||||
|
|||||||
@@ -323,21 +323,21 @@ _bhosted_extract_id() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# JSON: "id":12345
|
# JSON: "id":12345
|
||||||
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||||
if [ -n "$_id" ]; then
|
if [ -n "$_id" ]; then
|
||||||
printf "%s" "$_id"
|
printf "%s" "$_id"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# key=value: id=12345
|
# key=value: id=12345
|
||||||
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||||
if [ -n "$_id" ]; then
|
if [ -n "$_id" ]; then
|
||||||
printf "%s" "$_id"
|
printf "%s" "$_id"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# "record id 12345" / "recordid 12345"
|
# "record id 12345" / "recordid 12345"
|
||||||
_id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
_id="$(printf "%s" "$_resp" | _egrep_o '(record[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||||
if [ -n "$_id" ]; then
|
if [ -n "$_id" ]; then
|
||||||
printf "%s" "$_id"
|
printf "%s" "$_id"
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ dns_creoline_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
record_id=$(echo "$response" | _egrep_o "\"id\"[ ]*:[ ]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||||
_debug "record_id" "$record_id"
|
_debug "record_id" "$record_id"
|
||||||
|
|
||||||
if [ -z "$record_id" ]; then
|
if [ -z "$record_id" ]; then
|
||||||
@@ -108,10 +108,10 @@ _get_root() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
|
|
||||||
_domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
_domain=$(echo "$response" | _egrep_o "\"domain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
|
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
|
||||||
@@ -171,7 +171,7 @@ _creoline_rest() {
|
|||||||
_err "URI:$uri"
|
_err "URI:$uri"
|
||||||
return 1
|
return 1
|
||||||
elif _contains "$response" "message"; then
|
elif _contains "$response" "message"; then
|
||||||
message=$(echo "$response" | _egrep_o "\"message\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
message=$(echo "$response" | _egrep_o "\"message\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
||||||
_err "Error: $message"
|
_err "Error: $message"
|
||||||
_err "URI:$uri"
|
_err "URI:$uri"
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -285,15 +285,15 @@ _cyon_delete_txt() {
|
|||||||
|
|
||||||
list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
|
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}"
|
_debug list_txt_response "${list_txt_response}"
|
||||||
|
|
||||||
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
|
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
|
||||||
|
|
||||||
# Find and delete all acme challenge entries for the $fulldomain.
|
# 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_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
|
||||||
dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
|
dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ dns_czechia_add() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
_czechia_tab="$(printf '\t')"
|
||||||
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
|
||||||
|
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
|
||||||
|
|
||||||
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
|
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
|
||||||
_err "Missing zone or CZ_AuthorizationToken."
|
_err "Missing zone or CZ_AuthorizationToken."
|
||||||
@@ -108,8 +109,9 @@ dns_czechia_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
_czechia_tab="$(printf '\t')"
|
||||||
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
|
||||||
|
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
|
||||||
|
|
||||||
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
|
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
|
||||||
_err "Missing zone or CZ_AuthorizationToken."
|
_err "Missing zone or CZ_AuthorizationToken."
|
||||||
@@ -180,12 +182,13 @@ _czechia_load_conf() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_czechia_pick_zone() {
|
_czechia_pick_zone() {
|
||||||
|
_czechia_pz_tab="$(printf '\t')"
|
||||||
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
|
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
|
||||||
_best_zone=""
|
_best_zone=""
|
||||||
|
|
||||||
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
|
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
|
||||||
for _z in $_zones_space; do
|
for _z in $_zones_space; do
|
||||||
_clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
_clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//")
|
||||||
[ -z "$_clean_z" ] && continue
|
[ -z "$_clean_z" ] && continue
|
||||||
|
|
||||||
case "$_fd" in
|
case "$_fd" in
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ _dns_dynv6_add_http() {
|
|||||||
fi
|
fi
|
||||||
_get_zone_name "$_zone_id"
|
_get_zone_name "$_zone_id"
|
||||||
record=${fulldomain%%."$_zone_name"}
|
record=${fulldomain%%."$_zone_name"}
|
||||||
|
if [ "$fulldomain" = "$_zone_name" ]; then
|
||||||
|
record=""
|
||||||
|
fi
|
||||||
_set_record TXT "$record" "$txtvalue"
|
_set_record TXT "$record" "$txtvalue"
|
||||||
if _contains "$response" "$txtvalue"; then
|
if _contains "$response" "$txtvalue"; then
|
||||||
_info "Successfully added record"
|
_info "Successfully added record"
|
||||||
@@ -168,6 +171,9 @@ _dns_dynv6_rm_http() {
|
|||||||
fi
|
fi
|
||||||
_get_zone_name "$_zone_id"
|
_get_zone_name "$_zone_id"
|
||||||
record=${fulldomain%%."$_zone_name"}
|
record=${fulldomain%%."$_zone_name"}
|
||||||
|
if [ "$fulldomain" = "$_zone_name" ]; then
|
||||||
|
record=""
|
||||||
|
fi
|
||||||
_get_record_id "$_zone_id" "$record" "$txtvalue"
|
_get_record_id "$_zone_id" "$record" "$txtvalue"
|
||||||
_del_record "$_zone_id" "$_record_id"
|
_del_record "$_zone_id" "$_record_id"
|
||||||
if [ -z "$response" ]; then
|
if [ -z "$response" ]; then
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ dns_easydns_rm() {
|
|||||||
EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}"
|
EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}"
|
||||||
EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}"
|
EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}"
|
||||||
|
|
||||||
|
if [ -z "$EASYDNS_Token" ] || [ -z "$EASYDNS_Key" ]; then
|
||||||
|
_err "You didn't specify an easydns.net token or api key. Signup at https://cp.easydns.com/manage/security/api/signup.php"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
_debug "First detect the root zone"
|
||||||
if ! _get_root "$fulldomain"; then
|
if ! _get_root "$fulldomain"; then
|
||||||
_err "invalid domain"
|
_err "invalid domain"
|
||||||
@@ -91,23 +96,20 @@ dns_easydns_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
|
record_id=$(printf "%s\n" "$response" | tr '{' '\n' | grep "\"rdata\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
|
||||||
_debug count "$count"
|
|
||||||
if [ "$count" = "0" ]; then
|
|
||||||
_info "Don't need to remove."
|
|
||||||
else
|
|
||||||
record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
|
|
||||||
_debug "record_id" "$record_id"
|
_debug "record_id" "$record_id"
|
||||||
|
|
||||||
if [ -z "$record_id" ]; then
|
if [ -z "$record_id" ]; then
|
||||||
_err "Can not get record id to remove."
|
_info "Don't need to remove."
|
||||||
return 1
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
|
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
|
||||||
_err "Delete record error."
|
_err "Delete record error."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_contains "$response" "\"status\":200"
|
_contains "$response" "\"status\":200"
|
||||||
fi
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
|
|||||||
Options:
|
Options:
|
||||||
FREEMYIP_Token API Token
|
FREEMYIP_Token API Token
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/6247
|
Issues: github.com/acmesh-official/acme.sh/issues/6247
|
||||||
Author: Recolic Keghart <root@recolic.net>, @Giova96
|
Author: Recolic Keghart <root@recolic.net>, @Giova96, ExtremeFiretop
|
||||||
'
|
'
|
||||||
|
|
||||||
FREEMYIP_DNS_API="https://freemyip.com/update?"
|
FREEMYIP_DNS_API="https://freemyip.com/update?"
|
||||||
@@ -72,18 +72,26 @@ dns_freemyip_rm() {
|
|||||||
_get_root() {
|
_get_root() {
|
||||||
_fmi_d="$1"
|
_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.
|
# There is random failure while calling freemyip API too fast. This function automatically retry until success.
|
||||||
_freemyip_get_until_ok() {
|
_freemyip_get_until_ok() {
|
||||||
_fmi_url="$1"
|
_fmi_url="$1"
|
||||||
for i in $(seq 1 8); do
|
_fmi_i=1
|
||||||
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
|
while [ "$_fmi_i" -le 8 ]; do
|
||||||
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
|
_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
|
_sleep 1 # DO NOT send the request too fast
|
||||||
|
_fmi_i=$((_fmi_i + 1))
|
||||||
done
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,13 +101,16 @@ _is_root_domain_published() {
|
|||||||
_webroot="$(_get_root "$_fmi_d")"
|
_webroot="$(_get_root "$_fmi_d")"
|
||||||
|
|
||||||
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
|
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
|
||||||
for i in $(seq 1 3); do
|
_fmi_i=1
|
||||||
_debug "'$_webroot' ns lookup, retry $i/3..."
|
while [ "$_fmi_i" -le 3 ]; do
|
||||||
|
_debug "'$_webroot' ns lookup, retry $_fmi_i/3..."
|
||||||
|
|
||||||
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
|
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
|
||||||
_debug "'$_webroot' already has a TXT record published!"
|
_debug "'$_webroot' already has a TXT record published!"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
_sleep 10 # Give it some time to propagate the TXT record
|
_sleep 10 # Give it some time to propagate the TXT record
|
||||||
|
_fmi_i=$((_fmi_i + 1))
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
198
dnsapi/dns_hestiacp.sh
Normal file
198
dnsapi/dns_hestiacp.sh
Normal file
@@ -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 <radu.malica@gmail.com>
|
||||||
|
'
|
||||||
|
|
||||||
|
######## 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 <<EOF
|
||||||
|
$(_hestia_find_records "$_hestia_sub" "TXT")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ "$_hestia_removed" = "0" ] && [ "$_hestia_failed" = "0" ]; then
|
||||||
|
_info "No matching TXT record found to remove"
|
||||||
|
else
|
||||||
|
_info "Removed $_hestia_removed TXT record(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$_hestia_failed" != "0" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#################### Private functions below ##################################
|
||||||
|
|
||||||
|
_hestia_init() {
|
||||||
|
HESTIA_HOST="${HESTIA_HOST:-$(_readaccountconf_mutable HESTIA_HOST)}"
|
||||||
|
HESTIA_ACCESS="${HESTIA_ACCESS:-$(_readaccountconf_mutable HESTIA_ACCESS)}"
|
||||||
|
HESTIA_SECRET="${HESTIA_SECRET:-$(_readaccountconf_mutable HESTIA_SECRET)}"
|
||||||
|
HESTIA_USER="${HESTIA_USER:-$(_readaccountconf_mutable HESTIA_USER)}"
|
||||||
|
|
||||||
|
if [ -z "$HESTIA_HOST" ] || [ -z "$HESTIA_ACCESS" ] || [ -z "$HESTIA_SECRET" ]; then
|
||||||
|
HESTIA_HOST=""
|
||||||
|
HESTIA_ACCESS=""
|
||||||
|
HESTIA_SECRET=""
|
||||||
|
_err "You must export HESTIA_HOST, HESTIA_ACCESS and HESTIA_SECRET first"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HESTIA_HOST="${HESTIA_HOST%/}"
|
||||||
|
if ! echo "$HESTIA_HOST" | grep -qE '^https?://[^/]+$'; then
|
||||||
|
_err "HESTIA_HOST must be a valid URL (e.g. https://panel.example.com:8083)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$HESTIA_USER" ]; then
|
||||||
|
HESTIA_USER="admin"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_saveaccountconf_mutable HESTIA_HOST "$HESTIA_HOST"
|
||||||
|
_saveaccountconf_mutable HESTIA_ACCESS "$HESTIA_ACCESS"
|
||||||
|
_saveaccountconf_mutable HESTIA_SECRET "$HESTIA_SECRET"
|
||||||
|
_saveaccountconf_mutable HESTIA_USER "$HESTIA_USER"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Walk up the domain labels until the API returns a DNS zone.
|
||||||
|
# Sets _hestia_domain to the zone and _hestia_sub to the record name
|
||||||
|
# relative to the zone. The zone record listing stays in _hestia_response.
|
||||||
|
_hestia_get_root() {
|
||||||
|
_hestia_fqdn="${1%.}"
|
||||||
|
_hestia_i=1
|
||||||
|
while true; do
|
||||||
|
_hestia_h=$(printf "%s" "$_hestia_fqdn" | cut -d . -f "$_hestia_i"-100)
|
||||||
|
_debug2 _hestia_h "$_hestia_h"
|
||||||
|
if [ -z "$_hestia_h" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if _hestia_rest "v-list-dns-records" "$HESTIA_USER" "$_hestia_h" "json"; then
|
||||||
|
_hestia_domain="$_hestia_h"
|
||||||
|
if [ "$_hestia_h" = "$_hestia_fqdn" ]; then
|
||||||
|
_hestia_sub="@"
|
||||||
|
else
|
||||||
|
_hestia_sub=$(printf "%s" "$_hestia_fqdn" | cut -d . -f 1-"$(_math "$_hestia_i" - 1)")
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_hestia_i=$(_math "$_hestia_i" + 1)
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the HestiaCP API. Args: cmd [arg1 arg2 ...]
|
||||||
|
# The response body is stored in _hestia_response.
|
||||||
|
_hestia_rest() {
|
||||||
|
_hestia_cmd=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
_hestia_data="{\"access_key\":\"$HESTIA_ACCESS\",\"secret_key\":\"$HESTIA_SECRET\",\"cmd\":\"$_hestia_cmd\""
|
||||||
|
_hestia_argn=1
|
||||||
|
for _hestia_arg in "$@"; do
|
||||||
|
_hestia_data="$_hestia_data,\"arg$_hestia_argn\":\"$_hestia_arg\""
|
||||||
|
_hestia_argn=$(_math "$_hestia_argn" + 1)
|
||||||
|
done
|
||||||
|
_hestia_data="$_hestia_data}"
|
||||||
|
|
||||||
|
_debug2 "Calling $_hestia_cmd"
|
||||||
|
_hestia_response=$(_post "$_hestia_data" "$HESTIA_HOST/api/" "" "POST" "application/json")
|
||||||
|
_hestia_ret=$?
|
||||||
|
_debug2 _hestia_response "$_hestia_response"
|
||||||
|
if [ "$_hestia_ret" != "0" ]; then
|
||||||
|
_err "Error connecting to the HestiaCP API"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if _contains "$_hestia_response" "Error:"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract records matching name and type from the v-list-dns-records
|
||||||
|
# response in _hestia_response. Prints one "id|value" line per match.
|
||||||
|
_hestia_find_records() {
|
||||||
|
_hestia_fname=$1
|
||||||
|
_hestia_ftype=$2
|
||||||
|
|
||||||
|
echo "$_hestia_response" | tr -d '\n' | sed 's/},/}\
|
||||||
|
/g' | grep -F -- "\"RECORD\": \"$_hestia_fname\"" | grep -F -- "\"TYPE\": \"$_hestia_ftype\"" | while read -r _hestia_line; do
|
||||||
|
_hestia_id=$(echo "$_hestia_line" | _egrep_o '"ID": "[^"]*' | cut -d '"' -f 4)
|
||||||
|
_hestia_value=$(echo "$_hestia_line" | _egrep_o '"VALUE": "[^"]*' | cut -d '"' -f 4)
|
||||||
|
if [ -n "$_hestia_id" ]; then
|
||||||
|
echo "$_hestia_id|$_hestia_value"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
@@ -441,18 +441,18 @@ _hostup_json_extract() {
|
|||||||
input="${2:-$line}"
|
input="${2:-$line}"
|
||||||
|
|
||||||
# First try to extract quoted values (strings)
|
# First try to extract quoted values (strings)
|
||||||
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | _head_n 1)"
|
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*\"[^\"]*\"" | _head_n 1)"
|
||||||
if [ -n "$quoted_match" ]; then
|
if [ -n "$quoted_match" ]; then
|
||||||
printf "%s" "$quoted_match" |
|
printf "%s" "$quoted_match" |
|
||||||
cut -d : -f2- |
|
cut -d : -f2- |
|
||||||
sed 's/^[[:space:]]*"//' |
|
sed 's/^[ ]*"//' |
|
||||||
sed 's/"[[:space:]]*$//' |
|
sed 's/"[ ]*$//' |
|
||||||
sed 's/\\"/"/g'
|
sed 's/\\"/"/g'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fallback for unquoted values (e.g., numeric IDs)
|
# Fallback for unquoted values (e.g., numeric IDs)
|
||||||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | _head_n 1)"
|
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*[^,}]*" | _head_n 1)"
|
||||||
if [ -n "$unquoted_match" ]; then
|
if [ -n "$unquoted_match" ]; then
|
||||||
printf "%s" "$unquoted_match" |
|
printf "%s" "$unquoted_match" |
|
||||||
cut -d : -f2- |
|
cut -d : -f2- |
|
||||||
|
|||||||
226
dnsapi/dns_hw.sh
Normal file
226
dnsapi/dns_hw.sh
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
dns_hw_info='Huawei Cloud DNS
|
||||||
|
Site: HuaweiCloud.com
|
||||||
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hw
|
||||||
|
Options:
|
||||||
|
HW_AK Access Key
|
||||||
|
HW_SK Secret Access Key
|
||||||
|
HW_Region Region. E.g. "cn-north-4". Optional, defaults to "cn-north-4".
|
||||||
|
Issues: github.com/acmesh-official/acme.sh/issues/7221
|
||||||
|
Author: mashirozx
|
||||||
|
'
|
||||||
|
|
||||||
|
dns_hw_add() {
|
||||||
|
fulldomain=$1
|
||||||
|
txtvalue=$2
|
||||||
|
|
||||||
|
if ! _hw_init; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _hw_get_zoneid "$fulldomain"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! _hw_get_recordset "$fulldomain" "$_hw_zoneid"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Huawei Cloud stores each TXT value with its required inner quotes.
|
||||||
|
_hw_txt_record="\"\\\"${txtvalue}\\\"\""
|
||||||
|
case "$_hw_records" in
|
||||||
|
*"$txtvalue"*)
|
||||||
|
_debug "TXT record already exists"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ -z "$_hw_recordid" ]; then
|
||||||
|
_hw_body="{\"name\":\"${fulldomain}.\",\"type\":\"TXT\",\"ttl\":300,\"records\":[${_hw_txt_record}]}"
|
||||||
|
# Create a TXT record set: https://support.huaweicloud.com/api-dns/dns_api_64001.html
|
||||||
|
_hw_rest "POST" "/v2/zones/${_hw_zoneid}/recordsets" "" "$_hw_body" || return 1
|
||||||
|
else
|
||||||
|
_hw_body="{\"name\":\"${fulldomain}.\",\"type\":\"TXT\",\"ttl\":${_hw_recordttl},\"records\":[${_hw_records},${_hw_txt_record}]}"
|
||||||
|
# Update the existing TXT record set: https://support.huaweicloud.com/api-dns/UpdateRecordSets.html
|
||||||
|
_hw_rest "PUT" "/v2/zones/${_hw_zoneid}/recordsets/${_hw_recordid}" "" "$_hw_body" || return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
_saveaccountconf_mutable HW_AK "$HW_AK"
|
||||||
|
_saveaccountconf_mutable HW_SK "$HW_SK"
|
||||||
|
if [ -n "$HW_Region" ]; then
|
||||||
|
_saveaccountconf_mutable HW_Region "$HW_Region"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
dns_hw_rm() {
|
||||||
|
fulldomain=$1
|
||||||
|
txtvalue=$2
|
||||||
|
|
||||||
|
if ! _hw_init; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _hw_get_zoneid "$fulldomain" || ! _hw_get_recordset "$fulldomain" "$_hw_zoneid"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ -z "$_hw_recordid" ]; then
|
||||||
|
_debug "TXT record not found"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep unrelated TXT values that share this record set.
|
||||||
|
_hw_txt_record="\"\\\"${txtvalue}\\\"\""
|
||||||
|
case "$_hw_records" in
|
||||||
|
*"$txtvalue"*) ;;
|
||||||
|
*)
|
||||||
|
_debug "TXT record value not found"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
_hw_sed_txt_record=$(echo "$_hw_txt_record" | sed 's/\\/\\\\/g')
|
||||||
|
_hw_new_records=$(echo "$_hw_records" | sed "s/${_hw_sed_txt_record},//; s/,${_hw_sed_txt_record}//; s/${_hw_sed_txt_record}//")
|
||||||
|
if [ -z "$_hw_new_records" ]; then
|
||||||
|
# Delete an empty TXT record set: https://support.huaweicloud.com/api-dns/dns_api_64005.html
|
||||||
|
_hw_rest "DELETE" "/v2/zones/${_hw_zoneid}/recordsets/${_hw_recordid}" "" "" || return 1
|
||||||
|
else
|
||||||
|
_hw_body="{\"name\":\"${fulldomain}.\",\"type\":\"TXT\",\"ttl\":${_hw_recordttl},\"records\":[${_hw_new_records}]}"
|
||||||
|
# Update the record set after removing this challenge value: https://support.huaweicloud.com/api-dns/UpdateRecordSets.html
|
||||||
|
_hw_rest "PUT" "/v2/zones/${_hw_zoneid}/recordsets/${_hw_recordid}" "" "$_hw_body" || return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_hw_init() {
|
||||||
|
# Credentials from the environment override the persisted account settings.
|
||||||
|
HW_AK="${HW_AK:-$(_readaccountconf_mutable HW_AK)}"
|
||||||
|
HW_SK="${HW_SK:-$(_readaccountconf_mutable HW_SK)}"
|
||||||
|
HW_Region="${HW_Region:-$(_readaccountconf_mutable HW_Region)}"
|
||||||
|
if [ -z "$HW_AK" ] || [ -z "$HW_SK" ]; then
|
||||||
|
_err "You don't specify Huawei Cloud Access Key and Secret Access Key yet."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_hw_region="${HW_Region:-cn-north-4}"
|
||||||
|
_hw_api="https://dns.${_hw_region}.myhuaweicloud.com"
|
||||||
|
_hw_host="dns.${_hw_region}.myhuaweicloud.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
_hw_get_zoneid() {
|
||||||
|
_hw_domain=$1
|
||||||
|
_hw_index=1
|
||||||
|
# Try successively shorter suffixes so delegated zones are supported.
|
||||||
|
while true; do
|
||||||
|
_hw_zone_name=$(echo "$_hw_domain" | cut -d . -f "$_hw_index"-100)
|
||||||
|
if [ -z "$_hw_zone_name" ]; then
|
||||||
|
_err "Could not find Huawei Cloud DNS zone for $_hw_domain"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_hw_query="name=$(printf "%s" "$_hw_zone_name" | _url_encode upper-hex)&search_mode=equal"
|
||||||
|
# List public zones to find the authoritative zone: https://support.huaweicloud.com/api-dns/dns_api_62003.html
|
||||||
|
if ! _hw_rest "GET" "/v2/zones" "$_hw_query" ""; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_hw_zoneid=$(echo "$_hw_response" | _egrep_o '"id"[ ]*:[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||||
|
_hw_returned_name=$(echo "$_hw_response" | _egrep_o '"name"[ ]*:[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||||
|
if [ -n "$_hw_zoneid" ] && [ "$_hw_returned_name" = "${_hw_zone_name}." ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_hw_index=$(_math "$_hw_index" + 1)
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_hw_get_recordset() {
|
||||||
|
_hw_domain=$1
|
||||||
|
_hw_zone=$2
|
||||||
|
_hw_recordid=""
|
||||||
|
_hw_records=""
|
||||||
|
_hw_recordttl=""
|
||||||
|
_hw_query="name=$(printf "%s" "$_hw_domain" | _url_encode upper-hex)&search_mode=equal&type=TXT"
|
||||||
|
# List TXT record sets to locate the existing challenge record: https://support.huaweicloud.com/api-dns/dns_api_64004.html
|
||||||
|
if ! _hw_rest "GET" "/v2/zones/${_hw_zone}/recordsets" "$_hw_query" ""; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_hw_recordid=$(echo "$_hw_response" | _egrep_o '"id"[ ]*:[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||||
|
_hw_returned_name=$(echo "$_hw_response" | _egrep_o '"name"[ ]*:[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||||
|
if [ -z "$_hw_recordid" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_hw_expected_name="$(_lower_case "${_hw_domain}.")"
|
||||||
|
if [ "$(_lower_case "$_hw_returned_name")" != "$_hw_expected_name" ]; then
|
||||||
|
_err "Huawei Cloud DNS returned an unexpected record set for $_hw_domain"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# A DNS record set may contain multiple TXT values for concurrent challenges.
|
||||||
|
_hw_records=$(echo "$_hw_response" | sed 's/.*"records"[ ]*:[ ]*\[//; s/\].*//' | tr -d '\r\n')
|
||||||
|
_hw_recordttl=$(echo "$_hw_response" | _egrep_o '"ttl"[ ]*:[ ]*[0-9]*' | _head_n 1 | cut -d : -f 2 | tr -d ' ')
|
||||||
|
if [ -z "$_hw_recordttl" ]; then
|
||||||
|
_err "Huawei Cloud DNS record set did not include a TTL"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_hw_sha256() {
|
||||||
|
printf "%s" "$1" | _digest sha256 hex
|
||||||
|
}
|
||||||
|
|
||||||
|
_hw_hmac() {
|
||||||
|
_hw_key_hex=$(printf "%s" "$1" | _hex_dump | tr -d ' ')
|
||||||
|
printf "%s" "$2" | _hmac sha256 "$_hw_key_hex" hex
|
||||||
|
}
|
||||||
|
|
||||||
|
_hw_rest() {
|
||||||
|
_hw_method=$1
|
||||||
|
_hw_uri=$2
|
||||||
|
_hw_query=$3
|
||||||
|
_hw_payload=$4
|
||||||
|
_H1=""
|
||||||
|
_H2=""
|
||||||
|
_H3=""
|
||||||
|
_H4=""
|
||||||
|
_H5=""
|
||||||
|
_hw_date=$(_utc_date | tr -d ' :-')
|
||||||
|
_hw_short_date=${_hw_date%??????}
|
||||||
|
_hw_date="${_hw_short_date}T${_hw_date#????????}Z"
|
||||||
|
# Huawei's API gateway signs a trailing slash even when the published URI has none.
|
||||||
|
_hw_canonical_uri="${_hw_uri%/}/"
|
||||||
|
# SDK-HMAC-SHA256 signs the exact canonical request sent to Huawei Cloud.
|
||||||
|
_hw_payload_hash=$(_hw_sha256 "$_hw_payload")
|
||||||
|
_hw_headers="content-type:application/json
|
||||||
|
host:${_hw_host}
|
||||||
|
x-sdk-date:${_hw_date}
|
||||||
|
"
|
||||||
|
_hw_signed_headers="content-type;host;x-sdk-date"
|
||||||
|
_hw_canonical_request="${_hw_method}
|
||||||
|
${_hw_canonical_uri}
|
||||||
|
${_hw_query}
|
||||||
|
${_hw_headers}
|
||||||
|
${_hw_signed_headers}
|
||||||
|
${_hw_payload_hash}"
|
||||||
|
_hw_string_to_sign="SDK-HMAC-SHA256
|
||||||
|
${_hw_date}
|
||||||
|
$(_hw_sha256 "$_hw_canonical_request")"
|
||||||
|
_hw_signature=$(_hw_hmac "$HW_SK" "$_hw_string_to_sign")
|
||||||
|
_H1="Content-Type: application/json"
|
||||||
|
_H2="Host: ${_hw_host}"
|
||||||
|
_H3="X-Sdk-Date: ${_hw_date}"
|
||||||
|
_H4="Authorization: SDK-HMAC-SHA256 Access=${HW_AK}, SignedHeaders=${_hw_signed_headers}, Signature=${_hw_signature}"
|
||||||
|
_hw_url="${_hw_api}${_hw_uri}"
|
||||||
|
if [ -n "$_hw_query" ]; then
|
||||||
|
_hw_url="${_hw_url}?${_hw_query}"
|
||||||
|
fi
|
||||||
|
# _post sends the canonical request with each signed header exactly once.
|
||||||
|
if [ -z "$HTTP_HEADER" ]; then
|
||||||
|
_err "HTTP header file is not initialized"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
: >"$HTTP_HEADER" || return 1
|
||||||
|
if ! _hw_response=$(_post "$_hw_payload" "$_hw_url" "" "$_hw_method"); then
|
||||||
|
_err "Huawei Cloud DNS API request failed"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_hw_code=$(grep '^HTTP' "$HTTP_HEADER" | _tail_n 1 | cut -d ' ' -f 2 | tr -d '\r\n')
|
||||||
|
if ! _startswith "$_hw_code" "2"; then
|
||||||
|
_err "Huawei Cloud DNS API error: HTTP $_hw_code"
|
||||||
|
_debug2 response "$_hw_response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -117,7 +117,7 @@ dns_infoblox_uddi_rm() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
record_id=$(echo "$response" | _egrep_o '"id":[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||||
_debug "record_id" "$record_id"
|
_debug "record_id" "$record_id"
|
||||||
|
|
||||||
if [ -z "$record_id" ]; then
|
if [ -z "$record_id" ]; then
|
||||||
@@ -178,7 +178,7 @@ _get_root() {
|
|||||||
# Check if response contains results (even if empty)
|
# Check if response contains results (even if empty)
|
||||||
if _contains "$response" '"results"'; then
|
if _contains "$response" '"results"'; then
|
||||||
# Extract zone ID - must match the pattern dns/auth_zone/...
|
# Extract zone ID - must match the pattern dns/auth_zone/...
|
||||||
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
zone_id=$(echo "$response" | _egrep_o '"id":[ ]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||||
if [ -n "$zone_id" ]; then
|
if [ -n "$zone_id" ]; then
|
||||||
# Found the zone
|
# Found the zone
|
||||||
_domain="$h"
|
_domain="$h"
|
||||||
|
|||||||
@@ -7,22 +7,23 @@ Options:
|
|||||||
JD_ACCESS_KEY_ID Access key ID
|
JD_ACCESS_KEY_ID Access key ID
|
||||||
JD_ACCESS_KEY_SECRET Access key secret
|
JD_ACCESS_KEY_SECRET Access key secret
|
||||||
JD_REGION Region. E.g. "cn-north-1"
|
JD_REGION Region. E.g. "cn-north-1"
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/2388
|
Issues: github.com/acmesh-official/acme.sh/issues/7202
|
||||||
|
Author: @skysaint
|
||||||
'
|
'
|
||||||
|
|
||||||
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
|
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
|
||||||
|
|
||||||
_JD_PROD="clouddnsservice"
|
_JD_PROD="domainservice"
|
||||||
_JD_API="jdcloud-api.com"
|
_JD_API="jdcloud-api.com"
|
||||||
|
|
||||||
_JD_API_VERSION="v1"
|
_JD_API_VERSION="v2"
|
||||||
_JD_DEFAULT_REGION="cn-north-1"
|
_JD_DEFAULT_REGION="cn-north-1"
|
||||||
|
|
||||||
_JD_HOST="$_JD_PROD.$_JD_API"
|
_JD_HOST="$_JD_PROD.$_JD_API"
|
||||||
|
|
||||||
######## Public functions #####################
|
######## Public functions #####################
|
||||||
|
|
||||||
#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
#Usage: dns_jd_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
dns_jd_add() {
|
dns_jd_add() {
|
||||||
fulldomain=$1
|
fulldomain=$1
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
@@ -58,24 +59,14 @@ dns_jd_add() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
#_debug "Getting getViewTree"
|
#_debug "Getting describeViewTree"
|
||||||
|
|
||||||
_debug "Adding records"
|
_debug "Adding records"
|
||||||
|
|
||||||
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1},\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
|
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1}}"
|
||||||
#_addrr='{"req":{"hostRecord":"xx","hostValue":"\"value4\"","jcloudRes":false,"mxPriority":null,"port":null,"ttl":300,"type":"TXT","weight":null,"viewValue":-1},"regionId":"cn-north-1","domainId":"8824"}'
|
#_addrr='{"req":{"hostRecord":"_acme-challenge","hostValue":"XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs","ttl":300,"type":"TXT","viewValue":-1}}'
|
||||||
if jd_rest POST "domain/$_domain_id/RRAdd" "" "$_addrr"; then
|
if jd_rest POST "domain/$_domain_id/ResourceRecord" "" "$_addrr"; then
|
||||||
_rid="$(echo "$response" | tr '{},' '\n' | grep '"id":' | cut -d : -f 2)"
|
|
||||||
if [ -z "$_rid" ]; then
|
|
||||||
_err "Can not find record id from the result."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_info "TXT record added successfully."
|
_info "TXT record added successfully."
|
||||||
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
|
|
||||||
if [ "$_srid" ]; then
|
|
||||||
_rid="$_srid,$_rid"
|
|
||||||
fi
|
|
||||||
_savedomainconf "JD_CLOUD_RIDS" "$_rid"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -97,14 +88,7 @@ dns_jd_rm() {
|
|||||||
|
|
||||||
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
|
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
|
||||||
|
|
||||||
_info "Getting existing records for $fulldomain"
|
_info "Removing TXT record for $fulldomain"
|
||||||
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
|
|
||||||
_debug _srid "$_srid"
|
|
||||||
|
|
||||||
if [ -z "$_srid" ]; then
|
|
||||||
_err "Not rid skip"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
_debug "First detect the root zone"
|
||||||
if ! _get_root "$fulldomain"; then
|
if ! _get_root "$fulldomain"; then
|
||||||
@@ -115,16 +99,37 @@ dns_jd_rm() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
_cleardomainconf JD_CLOUD_RIDS
|
# List records, filter by hostRecord and use a large pageSize so it isn't missed on record-heavy zones.
|
||||||
|
if ! jd_rest GET "domain/$_domain_id/ResourceRecord" "pageSize=50&search=$_sub_domain"; then
|
||||||
|
_err "Failed to list resource records"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
_aws_tmpl_xml="{\"ids\":[$_srid],\"action\":\"del\",\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
|
# Match record by hostRecord + type TXT + hostValue
|
||||||
|
_record_id=""
|
||||||
|
_matched="$(echo "$response" | tr '{' '\n' | grep "\"hostRecord\":\"$_sub_domain\"" | grep "\"type\":\"TXT\"" | grep "\"hostValue\":\"$txtvalue\"")"
|
||||||
|
_debug2 _matched "$_matched"
|
||||||
|
|
||||||
if jd_rest POST "domain/$_domain_id/RROperate" "" "$_aws_tmpl_xml" && _contains "$response" "\"code\":\"OK\""; then
|
if [ -z "$_matched" ]; then
|
||||||
|
_info "TXT record not found, nothing to remove."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_record_id="$(echo "$_matched" | tr ',' '\n' | grep "\"id\":" | cut -d : -f 2 | tr -d '"' | _head_n 1)"
|
||||||
|
_debug _record_id "$_record_id"
|
||||||
|
|
||||||
|
if [ -z "$_record_id" ]; then
|
||||||
|
_info "Could not extract record id from response, nothing to remove."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if jd_rest DELETE "domain/$_domain_id/ResourceRecord/$_record_id"; then
|
||||||
_info "TXT record deleted successfully."
|
_info "TXT record deleted successfully."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
|
||||||
|
|
||||||
|
_err "Failed to delete TXT record."
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
#################### Private functions below ##################################
|
||||||
@@ -134,13 +139,14 @@ _get_root() {
|
|||||||
i=1
|
i=1
|
||||||
p=1
|
p=1
|
||||||
|
|
||||||
while true; do
|
|
||||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
|
||||||
_debug2 "Checking domain: $h"
|
|
||||||
if ! jd_rest GET "domain"; then
|
if ! jd_rest GET "domain"; then
|
||||||
_err "error get domain list"
|
_err "error get domain list"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||||
|
_debug2 "Checking domain: $h"
|
||||||
if [ -z "$h" ]; then
|
if [ -z "$h" ]; then
|
||||||
#not valid
|
#not valid
|
||||||
_err "Invalid domain"
|
_err "Invalid domain"
|
||||||
@@ -168,6 +174,8 @@ _get_root() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Use '%b' with printf to expand \n escapes in CanonicalRequest and StringToSign.
|
||||||
|
# Use '%s' for plain values that contain no escapes to avoid unintended expansion.
|
||||||
#method uri qstr data
|
#method uri qstr data
|
||||||
jd_rest() {
|
jd_rest() {
|
||||||
mtd="$1"
|
mtd="$1"
|
||||||
@@ -220,7 +228,7 @@ jd_rest() {
|
|||||||
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash"
|
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash"
|
||||||
_debug2 CanonicalRequest "$CanonicalRequest"
|
_debug2 CanonicalRequest "$CanonicalRequest"
|
||||||
|
|
||||||
HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)"
|
HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)"
|
||||||
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
||||||
|
|
||||||
Algorithm="JDCLOUD2-HMAC-SHA256"
|
Algorithm="JDCLOUD2-HMAC-SHA256"
|
||||||
@@ -246,19 +254,19 @@ jd_rest() {
|
|||||||
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
|
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
|
||||||
_secure_debug2 kSecretH "$kSecretH"
|
_secure_debug2 kSecretH "$kSecretH"
|
||||||
|
|
||||||
kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)"
|
kDateH="$(printf '%s' "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)"
|
||||||
_debug2 kDateH "$kDateH"
|
_debug2 kDateH "$kDateH"
|
||||||
|
|
||||||
kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)"
|
kRegionH="$(printf '%s' "$Region" | _hmac "$Hash" "$kDateH" hex)"
|
||||||
_debug2 kRegionH "$kRegionH"
|
_debug2 kRegionH "$kRegionH"
|
||||||
|
|
||||||
kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)"
|
kServiceH="$(printf '%s' "$Service" | _hmac "$Hash" "$kRegionH" hex)"
|
||||||
_debug2 kServiceH "$kServiceH"
|
_debug2 kServiceH "$kServiceH"
|
||||||
|
|
||||||
kSigningH="$(printf "%s" "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
|
kSigningH="$(printf '%s' "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
|
||||||
_debug2 kSigningH "$kSigningH"
|
_debug2 kSigningH "$kSigningH"
|
||||||
|
|
||||||
signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)"
|
signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)"
|
||||||
_debug2 signature "$signature"
|
_debug2 signature "$signature"
|
||||||
|
|
||||||
Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
|
Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
dns_linode_info='Linode.com (Old)
|
|
||||||
Deprecated. Use dns_linode_v4
|
|
||||||
Site: Linode.com
|
|
||||||
Options:
|
|
||||||
LINODE_API_KEY API Key
|
|
||||||
Author: Philipp Grosswiler <philipp.grosswiler@swiss-design.net>
|
|
||||||
'
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
121
dnsapi/dns_myloc.sh
Executable file
121
dnsapi/dns_myloc.sh
Executable file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
dns_myloc_info='myloc.de
|
||||||
|
Site: myloc.de
|
||||||
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_myloc
|
||||||
|
Issues: github.com/acmesh-official/acme.sh/issues/5193
|
||||||
|
Options:
|
||||||
|
MYLOC_token API token
|
||||||
|
'
|
||||||
|
|
||||||
|
# updater for the (experimental) API of myloc.de / webtropia.com
|
||||||
|
# usage: acme.sh --issue -d example.com --dns dns_myloc --dnssleep 60
|
||||||
|
# API documentation at https://apidoc.myloc.de/
|
||||||
|
# As the API does not support quering available zones yet, the zone for a given
|
||||||
|
# fulldomain is searched recursively by removing prefixes one-by-one.
|
||||||
|
|
||||||
|
_myloc_api="https://zkm.myloc.de/api"
|
||||||
|
|
||||||
|
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
|
dns_myloc_add() {
|
||||||
|
_myloc_fulldomain=$1
|
||||||
|
_myloc_txtvalue=$2
|
||||||
|
|
||||||
|
_myloc_token="${MYLOC_token:-$(_readaccountconf_mutable MYLOC_token)}"
|
||||||
|
if [ -z "$_myloc_token" ]; then
|
||||||
|
_err "You didn't specify MYLOC_token"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export _H1="Content-Type: application/json"
|
||||||
|
export _H2="Authorization: Bearer $_myloc_token"
|
||||||
|
|
||||||
|
_myloc_zone="$(_myloc_get_zone "$_myloc_fulldomain")"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# save token if the previous request was successful
|
||||||
|
_saveaccountconf_mutable MYLOC_token "$_myloc_token"
|
||||||
|
|
||||||
|
_info "Adding record"
|
||||||
|
_myloc_record="{\"type\":\"TXT\",\"name\":\"${_myloc_fulldomain}\",\"content\":\"\\\"${_myloc_txtvalue}\\\"\",\"ttl\":60}"
|
||||||
|
_debug "add record request $_myloc_record to ${_myloc_api}/dns/zone/${_myloc_zone}"
|
||||||
|
_myloc_response="$(_post "$_myloc_record" "${_myloc_api}/dns/zone/${_myloc_zone}" "" "PUT")"
|
||||||
|
_myloc_status=$?
|
||||||
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||||
|
_debug "add record response $_code $_myloc_response"
|
||||||
|
if [ $_myloc_status -ne 0 ]; then
|
||||||
|
_err "Add txt record curl error."
|
||||||
|
return 1
|
||||||
|
elif [ "$_code" = "204" ] && [ -z "$_myloc_response" ]; then
|
||||||
|
_info "Add txt record success"
|
||||||
|
return 0
|
||||||
|
elif _contains "$_myloc_response" "error" || _contains "$_myloc_response" "unexpected"; then
|
||||||
|
_err "Add txt record api error."
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
_err "Add txt record unknown response."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#_myloc_fulldomain _myloc_txtvalue
|
||||||
|
dns_myloc_rm() {
|
||||||
|
_myloc_fulldomain=$1
|
||||||
|
_myloc_txtvalue=$2
|
||||||
|
|
||||||
|
_myloc_token="${MYLOC_token:-$(_readaccountconf_mutable MYLOC_token)}"
|
||||||
|
if [ -z "$_myloc_token" ]; then
|
||||||
|
_err "You didn't specify MYLOC_token"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export _H1="Content-Type: application/json"
|
||||||
|
export _H2="Authorization: Bearer $_myloc_token"
|
||||||
|
|
||||||
|
_myloc_zone="$(_myloc_get_zone "$_myloc_fulldomain")"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# save token if the previous request was successful
|
||||||
|
_saveaccountconf_mutable MYLOC_token "$_myloc_token"
|
||||||
|
|
||||||
|
_info "Deleting record for $_myloc_fulldomain"
|
||||||
|
_myloc_record="{\"type\":\"TXT\",\"name\":\"${_myloc_fulldomain}\",\"content\":\"\\\"${_myloc_txtvalue}\\\"\"}"
|
||||||
|
_debug "delete record $_myloc_record"
|
||||||
|
_myloc_response="$(_post "$_myloc_record" "${_myloc_api}/dns/zone/${_myloc_zone}" "" "DELETE")"
|
||||||
|
_myloc_status=$?
|
||||||
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||||
|
_debug "delete response $_code $_myloc_response"
|
||||||
|
if [ $_myloc_status -ne 0 ] || [ "$_code" != "204" ]; then
|
||||||
|
_err "Failed to delete record"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage: _myloc_get_zone "_acme-challenge.sub1.mydomain.com"
|
||||||
|
# Subdomains are walked until a zone is found or TLD is reached
|
||||||
|
_myloc_get_zone() {
|
||||||
|
_myloc_zone=$1
|
||||||
|
|
||||||
|
while [ "${_myloc_zone#*.}" != "$_myloc_zone" ]; do
|
||||||
|
_debug "Get zone trying $_myloc_zone"
|
||||||
|
_myloc_response="$(_get "${_myloc_api}/dns/zone/${_myloc_zone}")"
|
||||||
|
_myloc_status=$?
|
||||||
|
_debug "Get zone response $_myloc_response"
|
||||||
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||||
|
if [ $_myloc_status -eq 0 ] && [ "$_code" = "200" ]; then
|
||||||
|
_debug "Get zone success for $_myloc_zone"
|
||||||
|
echo "${_myloc_zone}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_myloc_zone="${_myloc_zone#*.}"
|
||||||
|
done
|
||||||
|
|
||||||
|
_err "Get zone failed for all candidates"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
@@ -104,6 +104,9 @@ _get_root_by_getList() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_namecheap_domain_list=$(echo "$response" | _egrep_o '<Domain [^>]*')
|
||||||
|
_debug2 domain_list "$_namecheap_domain_list"
|
||||||
|
|
||||||
i=2
|
i=2
|
||||||
p=1
|
p=1
|
||||||
|
|
||||||
@@ -120,7 +123,7 @@ _get_root_by_getList() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! _contains "$response" "$h"; then
|
if ! _namecheap_is_our_dns "$h"; then
|
||||||
_debug "$h not found"
|
_debug "$h not found"
|
||||||
else
|
else
|
||||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||||
@@ -133,6 +136,29 @@ _get_root_by_getList() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Usage: _namecheap_is_our_dns <domain>
|
||||||
|
#Succeeds only when domains.getList listed exactly <domain> 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() {
|
_get_root_by_getHosts() {
|
||||||
i=100
|
i=100
|
||||||
p=99
|
p=99
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ dns_netcup_add() {
|
|||||||
exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
|
exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
|
||||||
exit=$(_math "$exit" + 1)
|
exit=$(_math "$exit" + 1)
|
||||||
i=$exit
|
i=$exit
|
||||||
|
_nc_last=$(_nc_lastlevel "$i")
|
||||||
|
_nc_found=""
|
||||||
|
|
||||||
while
|
while
|
||||||
[ "$exit" -gt 0 ]
|
[ "$exit" -ge "$_nc_last" ]
|
||||||
do
|
do
|
||||||
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
|
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
|
||||||
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
|
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
|
||||||
@@ -51,12 +53,18 @@ dns_netcup_add() {
|
|||||||
_err "$msg"
|
_err "$msg"
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
|
_nc_found=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
exit=$(_math "$exit" - 1)
|
exit=$(_math "$exit" - 1)
|
||||||
done
|
done
|
||||||
|
if [ -z "$_nc_found" ]; then
|
||||||
|
_err "$msg"
|
||||||
|
_nc_nozone "$fulldomain"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
logout
|
logout
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,9 +78,11 @@ dns_netcup_rm() {
|
|||||||
exit=$(_math "$exit" + 1)
|
exit=$(_math "$exit" + 1)
|
||||||
i=$exit
|
i=$exit
|
||||||
rec=""
|
rec=""
|
||||||
|
_nc_last=$(_nc_lastlevel "$i")
|
||||||
|
_nc_found=""
|
||||||
|
|
||||||
while
|
while
|
||||||
[ "$exit" -gt 0 ]
|
[ "$exit" -ge "$_nc_last" ]
|
||||||
do
|
do
|
||||||
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
|
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
|
||||||
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
|
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
|
||||||
@@ -89,12 +99,18 @@ dns_netcup_rm() {
|
|||||||
_err "$msg"
|
_err "$msg"
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
|
_nc_found=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
exit=$(_math "$exit" - 1)
|
exit=$(_math "$exit" - 1)
|
||||||
done
|
done
|
||||||
|
if [ -z "$_nc_found" ]; then
|
||||||
|
_err "$msg"
|
||||||
|
_nc_nozone "$fulldomain"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
ida=0000
|
ida=0000
|
||||||
idv=0001
|
idv=0001
|
||||||
@@ -125,6 +141,27 @@ dns_netcup_rm() {
|
|||||||
logout
|
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() {
|
_login() {
|
||||||
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
|
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)
|
sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
|
||||||
|
|||||||
244
dnsapi/dns_nexdns.sh
Executable file
244
dnsapi/dns_nexdns.sh
Executable file
@@ -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 <https://github.com/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
|
||||||
|
}
|
||||||
@@ -227,7 +227,7 @@ _poweradmin_rest() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then
|
if printf '%s' "$response" | grep -q '"success"[ ]*:[ ]*false'; then
|
||||||
_err "API reported failure on $method $ep"
|
_err "API reported failure on $method $ep"
|
||||||
_debug "Response: $response"
|
_debug "Response: $response"
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ dns_rage4_rm() {
|
|||||||
_debug "Getting txt records"
|
_debug "Getting txt records"
|
||||||
_rage4_rest "getrecords/?id=${_domain_id}"
|
_rage4_rest "getrecords/?id=${_domain_id}"
|
||||||
|
|
||||||
_record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\1/p')
|
_record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -n 's/.*"id":\([0-9][0-9]*\),.*/\1/p')
|
||||||
if [ -z "$_record_id" ]; then
|
if [ -z "$_record_id" ]; then
|
||||||
_err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'."
|
_err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'."
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
145
dnsapi/dns_rltx.sh
Normal file
145
dnsapi/dns_rltx.sh
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -368,7 +368,7 @@ _get_auth_token() {
|
|||||||
_data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}"
|
_data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}"
|
||||||
export _H1="Content-Type: application/json"
|
export _H1="Content-Type: application/json"
|
||||||
_result=$(_post "$_data_auth" "$auth_uri")
|
_result=$(_post "$_data_auth" "$auth_uri")
|
||||||
_token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p")
|
_token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | cut -d ':' -f 2- | tr -d ' \t\r')
|
||||||
_dt_curr=$(date +%s)
|
_dt_curr=$(date +%s)
|
||||||
SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}"
|
SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}"
|
||||||
_saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2"
|
_saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2"
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ dns_selfhost_add() {
|
|||||||
# only match full domains (at the beginning of the string or with a leading whitespace),
|
# only match full domains (at the beginning of the string or with a leading whitespace),
|
||||||
# e.g. don't match mytest.example.com or sub.test.example.com for test.example.com
|
# e.g. don't match mytest.example.com or sub.test.example.com for test.example.com
|
||||||
# if the domain is defined multiple times only the last occurance will be matched
|
# if the domain is defined multiple times only the last occurance will be matched
|
||||||
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p")
|
# prepend a space to each line so "start of line" and "after whitespace"
|
||||||
|
# can both be matched as "after a space/tab" (portable BRE, no ERE (^|..))
|
||||||
|
_selfhost_tab="$(printf '\t')"
|
||||||
|
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*:\{0,1\}[0-9]*\).*/\1/p")
|
||||||
_debug2 mapEntry "$mapEntry"
|
_debug2 mapEntry "$mapEntry"
|
||||||
if test -z "$mapEntry"; then
|
if test -z "$mapEntry"; then
|
||||||
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
|
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
|
||||||
@@ -54,7 +57,7 @@ dns_selfhost_add() {
|
|||||||
rid2=$(echo "$mapEntry" | cut -d: -f3)
|
rid2=$(echo "$mapEntry" | cut -d: -f3)
|
||||||
|
|
||||||
# read last used rid domain
|
# read last used rid domain
|
||||||
lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain:[[:digit:]]+)(.*)/\2/p")
|
lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p")
|
||||||
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
|
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
|
||||||
lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2)
|
lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2)
|
||||||
|
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ _udr_rest() {
|
|||||||
_debug data "${data}"
|
_debug data "${data}"
|
||||||
response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")"
|
response="$(_post "${data}" "${UDR_API}?s_login=${UDR_USER}&s_pw=${UDR_PASS}" "" "POST")"
|
||||||
|
|
||||||
_code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
_code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _head_n 1 | cut -d = -f 2 | tr -d ' \t\r')
|
||||||
_description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
_description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | tr -d '\r' | sed -e 's/^[ ]*//' -e 's/[ ]*$//')
|
||||||
|
|
||||||
_debug response_code "$_code"
|
_debug response_code "$_code"
|
||||||
_debug response_description "$_description"
|
_debug response_description "$_description"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ dns_world4you_add() {
|
|||||||
if _contains "$res" "successfully"; then
|
if _contains "$res" "successfully"; then
|
||||||
return 0
|
return 0
|
||||||
else
|
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
|
if [ "$msg" = '' ]; then
|
||||||
_err "Unable to add record: Unknown error"
|
_err "Unable to add record: Unknown error"
|
||||||
echo "$ret" >'error-01.html'
|
echo "$ret" >'error-01.html'
|
||||||
@@ -125,7 +125,7 @@ dns_world4you_rm() {
|
|||||||
if _contains "$res" "successfully"; then
|
if _contains "$res" "successfully"; then
|
||||||
return 0
|
return 0
|
||||||
else
|
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
|
if [ "$msg" = '' ]; then
|
||||||
_err "Unable to remove record: Unknown error"
|
_err "Unable to remove record: Unknown error"
|
||||||
echo "$ret" >'error-01.html'
|
echo "$ret" >'error-01.html'
|
||||||
@@ -145,6 +145,17 @@ dns_world4you_rm() {
|
|||||||
|
|
||||||
################ Private functions ################
|
################ Private functions ################
|
||||||
|
|
||||||
|
# Usage: _w4y_alert_msg <html>
|
||||||
|
# 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
|
# Usage: _login
|
||||||
_login() {
|
_login() {
|
||||||
WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}"
|
WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ _check_variables() {
|
|||||||
org_response="$(echo "$org_response" | _normalizeJson)"
|
org_response="$(echo "$org_response" | _normalizeJson)"
|
||||||
YANDEX360_ORG_ID=$(
|
YANDEX360_ORG_ID=$(
|
||||||
echo "$org_response" |
|
echo "$org_response" |
|
||||||
_egrep_o '"id":[[:space:]]*[0-9]+' |
|
_egrep_o '"id":[ ]*[0-9]+' |
|
||||||
cut -d':' -f2
|
cut -d':' -f2
|
||||||
)
|
)
|
||||||
_debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID"
|
_debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID"
|
||||||
@@ -216,7 +216,7 @@ _get_token() {
|
|||||||
|
|
||||||
interval=$(
|
interval=$(
|
||||||
echo "$response" |
|
echo "$response" |
|
||||||
_egrep_o '"interval":[[:space:]]*[0-9]+' |
|
_egrep_o '"interval":[ ]*[0-9]+' |
|
||||||
cut -d':' -f2
|
cut -d':' -f2
|
||||||
)
|
)
|
||||||
_debug 'Polling interval' "$interval"
|
_debug 'Polling interval' "$interval"
|
||||||
|
|||||||
@@ -22,21 +22,32 @@ dns_yc_add() {
|
|||||||
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
|
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
|
||||||
txtvalue=$2
|
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_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)}"
|
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
|
if [ "$YC_SA_Key_File_PEM_b64" ]; then
|
||||||
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
|
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
|
||||||
YC_SA_Key_File="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"
|
_savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64"
|
||||||
else
|
else
|
||||||
YC_SA_Key_File="$YC_SA_Key_File_Path"
|
YC_SA_Key_File="$YC_SA_Key_File_Path"
|
||||||
|
_yc_key_is_temp=""
|
||||||
_savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path"
|
_savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
|
||||||
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable 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_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_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)}"
|
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
|
if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then
|
||||||
@@ -65,11 +76,21 @@ dns_yc_add() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
else
|
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
|
_clearaccountconf YC_Zone_ID
|
||||||
|
_cleardomainconf YC_Folder_ID
|
||||||
_clearaccountconf YC_Folder_ID
|
_clearaccountconf YC_Folder_ID
|
||||||
_clearaccountconf YC_SA_ID
|
_cleardomainconf YC_SA_ID
|
||||||
_clearaccountconf YC_SA_Key_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
|
_clearaccountconf YC_SA_Key_File_PEM_b64
|
||||||
|
_cleardomainconf YC_SA_Key_File_Path
|
||||||
_clearaccountconf 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."
|
_err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
|
||||||
return 1
|
return 1
|
||||||
@@ -110,11 +131,30 @@ dns_yc_rm() {
|
|||||||
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
|
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
|
|
||||||
|
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
|
||||||
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable 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_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_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)}"
|
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"
|
_debug "First detect the root zone"
|
||||||
if ! _get_root "$fulldomain"; then
|
if ! _get_root "$fulldomain"; then
|
||||||
_err "invalid domain"
|
_err "invalid domain"
|
||||||
@@ -124,16 +164,10 @@ dns_yc_rm() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
_debug "Getting txt records"
|
# upsertRecordSets.deletions removes only the given value from the rrset,
|
||||||
if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
|
# leaving any other values at the same name (e.g. base + wildcard domain)
|
||||||
exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$")
|
# intact -- no need to read the current data set and recompute it.
|
||||||
_debug exists_txtvalue "$exists_txtvalue"
|
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
||||||
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
|
|
||||||
if _contains "$response" "\"done\": true"; then
|
if _contains "$response" "\"done\": true"; then
|
||||||
_info "Delete, OK"
|
_info "Delete, OK"
|
||||||
return 0
|
return 0
|
||||||
@@ -194,7 +228,7 @@ _get_root() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if _contains "$response" "\"zone\": \"$h\""; then
|
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"
|
_debug _domain_id "$_domain_id"
|
||||||
if [ "$_domain_id" ]; then
|
if [ "$_domain_id" ]; then
|
||||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
_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)
|
_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"
|
_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")
|
_jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature")
|
||||||
_debug2 _jwt "$_jwt"
|
_debug2 _jwt "$_jwt"
|
||||||
@@ -264,7 +300,7 @@ _yc_login() {
|
|||||||
_iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")"
|
_iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")"
|
||||||
_debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)"
|
_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
|
_debug3 YC_Token
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -57,9 +57,8 @@ waha_send() {
|
|||||||
|
|
||||||
_debug "_data" "$_data"
|
_debug "_data" "$_data"
|
||||||
|
|
||||||
export _H1="Content-Type: application/json"
|
|
||||||
if [ "$WAHA_API_KEY" ]; then
|
if [ "$WAHA_API_KEY" ]; then
|
||||||
export _H2="X-Api-Key: $WAHA_API_KEY"
|
export _H1="X-Api-Key: $WAHA_API_KEY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_waha_url="${WAHA_URL}/api/sendText"
|
_waha_url="${WAHA_URL}/api/sendText"
|
||||||
|
|||||||
Reference in New Issue
Block a user