Compare commits
+14
-258
@@ -66,7 +66,7 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
- uses: actions/checkout@v6
|
||||||
- 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@v7
|
- uses: actions/checkout@v6
|
||||||
- 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,13 +231,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -290,13 +289,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -347,13 +345,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -404,13 +401,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -462,13 +458,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -524,13 +519,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -582,13 +576,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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: |
|
||||||
@@ -642,13 +635,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -699,13 +691,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -756,13 +747,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -813,13 +803,12 @@ jobs:
|
|||||||
TokenName4: ${{ secrets.TokenName4}}
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
TokenName5: ${{ secrets.TokenName5}}
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
- 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
|
||||||
@@ -853,236 +842,3 @@ 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"
|
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ 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"
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ 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"
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ 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"
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -58,7 +58,6 @@ 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"
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ 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"
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -31,21 +31,13 @@ jobs:
|
|||||||
Le_HTTPPort: 5002
|
Le_HTTPPort: 5002
|
||||||
TEST_LOCAL: 1
|
TEST_LOCAL: 1
|
||||||
TEST_CA: "Pebble Intermediate CA"
|
TEST_CA: "Pebble Intermediate CA"
|
||||||
TEST_DNS_MANUAL: 1
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: sudo apt-get install -y socat
|
run: sudo apt-get install -y socat
|
||||||
- name: Run Pebble
|
- name: Run Pebble
|
||||||
run: |
|
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||||
cd ..
|
|
||||||
curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml
|
|
||||||
# Pebble reuses a valid authorization in a new order 50% of the time
|
|
||||||
# by default, which makes the dns manual mode case a coin flip: a
|
|
||||||
# reused authorization leaves nothing for the TXT record to answer.
|
|
||||||
printf 'services:\n pebble:\n environment:\n PEBBLE_AUTHZREUSE: "0"\n' >docker-compose.override.yml
|
|
||||||
docker compose up -d
|
|
||||||
- name: Set up Pebble
|
- name: Set up Pebble
|
||||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||||
- name: Clone acmetest
|
- name: Clone acmetest
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ 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"
|
||||||
|
|||||||
@@ -10,16 +10,9 @@ 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.event_name }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -47,13 +40,13 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: CheckToken
|
needs: CheckToken
|
||||||
if: "github.event_name != 'schedule' && contains(needs.CheckToken.outputs.hasToken, 'true')"
|
if: "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@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@@ -105,38 +98,3 @@ 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}"
|
|
||||||
# A tag cut before this job existed carries a workflow file with no
|
|
||||||
# workflow_dispatch trigger; the API rejects the dispatch with 422.
|
|
||||||
# That is expected (nothing to rebuild there), so only a different
|
|
||||||
# error fails the job.
|
|
||||||
if ! out="$(gh workflow run dockerhub.yml --repo "${GITHUB_REPOSITORY}" --ref "${tag}" 2>&1)"; then
|
|
||||||
echo "$out"
|
|
||||||
case "$out" in
|
|
||||||
*"does not have 'workflow_dispatch' trigger"*)
|
|
||||||
echo "::warning::${tag} predates the dispatch trigger; skipping the rebuild"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
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.
|
|
||||||
# The job mirrors first and then fails when the pushed tag is lightweight,
|
|
||||||
# which is what the release form produces: that tag can never carry a
|
|
||||||
# signature, so the failure has to be loud.
|
|
||||||
|
|
||||||
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: |
|
|
||||||
# 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.
|
|
||||||
_ref="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq '.object.sha + " " + .object.type')"
|
|
||||||
sha="${_ref%% *}"
|
|
||||||
objtype="${_ref##* }"
|
|
||||||
if [ -z "$sha" ] || [ "$sha" = "null" ]; then
|
|
||||||
echo "Could not resolve refs/tags/$TAG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
|
|
||||||
echo "Tag v$TAG already exists, nothing to do."
|
|
||||||
else
|
|
||||||
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
|
|
||||||
echo "Created tag v$TAG -> $sha"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check that the tag is signable
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
TAG: ${{ github.ref_name }}
|
|
||||||
run: |
|
|
||||||
# A release published from the GitHub UI creates the tag server-side
|
|
||||||
# as a lightweight ref, which points straight at a commit and can
|
|
||||||
# never carry a signature (3.1.5 shipped that way, see issue 7273).
|
|
||||||
# The tag has to be created locally with "git tag -s" and pushed
|
|
||||||
# BEFORE the release is published, then selected on the release form.
|
|
||||||
objtype="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.type)"
|
|
||||||
if [ "$objtype" != "tag" ]; then
|
|
||||||
echo "::error::refs/tags/$TAG points at a $objtype, not a tag object. It is a lightweight tag and carries no signature. Recreate it locally with 'git tag -s $TAG' and push it before publishing the release."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "refs/tags/$TAG is a tag object."
|
|
||||||
@@ -36,10 +36,6 @@
|
|||||||
<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">
|
||||||
@@ -134,10 +130,6 @@
|
|||||||
|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)
|
||||||
@@ -231,31 +223,6 @@ Cron entry example:
|
|||||||
acme.sh -h
|
acme.sh -h
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🔏 Verify a Release
|
|
||||||
|
|
||||||
Release tags from `3.1.6` 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.6
|
|
||||||
```
|
|
||||||
|
|
||||||
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.6/ 3.1.6 > acme.sh-3.1.6.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ Tags up to `3.1.5` are unsigned.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2️⃣ Issue a Certificate
|
### 2️⃣ Issue a Certificate
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# 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.
|
|
||||||
#
|
|
||||||
# Signing starts at 3.1.6; tags up to 3.1.5 are unsigned.
|
|
||||||
#
|
|
||||||
# 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.6
|
|
||||||
#
|
|
||||||
# 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.6/ 3.1.6 > acme.sh-3.1.6.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=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
|
_public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r')
|
||||||
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
|
_private_key=$(sed '/^[[:space:]]*$/d' "$_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,11 +87,7 @@ 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 DomainInfo list_domains)
|
||||||
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
|
|
||||||
else
|
|
||||||
_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:"
|
||||||
|
|||||||
+2
-2
@@ -74,9 +74,9 @@ fritzbox_deploy() {
|
|||||||
_info "Log in to the FRITZ!Box"
|
_info "Log in to the FRITZ!Box"
|
||||||
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
|
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
|
||||||
if _exists iconv; then
|
if _exists iconv; then
|
||||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF-16LE | _digest md5 hex)"
|
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)"
|
||||||
elif _exists uconv; then
|
elif _exists uconv; then
|
||||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF-16LE | _digest md5 hex)"
|
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)"
|
||||||
else
|
else
|
||||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
|
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+4
-1
@@ -173,7 +173,10 @@ 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"
|
||||||
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
# Initialise $Le_Keylength if its not already set
|
||||||
|
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
@@ -1,114 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
# Script to deploy a certificate to a JetKVM (https://jetkvm.com) KVM-over-IP
|
|
||||||
# device over SSH. See also:
|
|
||||||
# https://github.com/acmesh-official/acme.sh/wiki/deployhooks
|
|
||||||
#
|
|
||||||
# JetKVM only supports key-based SSH authentication (root@<device>, password
|
|
||||||
# logins are disabled) once "Developer Mode" is enabled and a public key is
|
|
||||||
# pasted into its web UI (Settings > Advanced). SSH keys must already be
|
|
||||||
# exchanged and a passwordless login confirmed working (e.g. `ssh
|
|
||||||
# root@jetkvm.example.com true`) before using this hook.
|
|
||||||
#
|
|
||||||
# JetKVM's minimal userspace does not ship an scp binary or SFTP server, so
|
|
||||||
# unlike deploy/ssh.sh this hook has no "use scp" option: it always writes
|
|
||||||
# the certificate and key by piping a small POSIX shell script to the
|
|
||||||
# remote "sh" over stdin (only depends on "sh", "cat", "chmod", "mkdir",
|
|
||||||
# "mv" and "rm" on the device side). The remote path, filenames and file
|
|
||||||
# permissions are firmware constants on this single-purpose, single-root
|
|
||||||
# appliance, so they are not configurable here.
|
|
||||||
#
|
|
||||||
# JetKVM's "Custom" TLS mode (device web UI: Settings > Network > HTTPS
|
|
||||||
# Mode, must already be set to "Custom" before this hook's uploads take
|
|
||||||
# effect) reads the certificate/key from that fixed location and does not
|
|
||||||
# hot-reload: a device reboot is required to pick up a new certificate.
|
|
||||||
# This hook's restart command therefore defaults to "reboot" -- a blank
|
|
||||||
# DEPLOY_JETKVM_RESTART_CMD is treated the same as unset (falls back to
|
|
||||||
# "reboot") rather than silently skipping it, since a renewed certificate
|
|
||||||
# that's never actually applied defeats the point of automating this; set
|
|
||||||
# it to the literal value "none" to opt out and apply/verify manually.
|
|
||||||
# The restart command is run detached on the device (nohup ... &) so this
|
|
||||||
# hook's ssh call can return before the reboot itself lands, rather than
|
|
||||||
# racing the connection teardown.
|
|
||||||
#
|
|
||||||
# The certificate and key are staged under fixed temporary names on the
|
|
||||||
# device and only renamed into their final names (an atomic "mv", on the
|
|
||||||
# same filesystem) once both have been fully written and chmod'ed. This
|
|
||||||
# keeps a dropped connection or a failed write from ever leaving the
|
|
||||||
# device with a truncated or mismatched certificate/key pair for its own
|
|
||||||
# HTTPS listener, and a "trap ... EXIT" in the generated script removes
|
|
||||||
# any leftover staged file however that script exits.
|
|
||||||
#
|
|
||||||
# Before writing anything, this hook also checks that the device's HTTPS
|
|
||||||
# Mode is already "Custom" -- uploading a certificate that mode won't
|
|
||||||
# even serve would otherwise be a silent no-op. There is currently no
|
|
||||||
# documented/headless way to read this back (JetKVM's own JSON-RPC
|
|
||||||
# getTLSState/setTLSState calls require an authenticated WebRTC session,
|
|
||||||
# see https://github.com/jetkvm/kvm/issues/1240 and the still-open
|
|
||||||
# https://github.com/jetkvm/kvm/pull/1515), so this greps the device's
|
|
||||||
# own config file instead: JetKVM's firmware (see web_tls.go / config.go
|
|
||||||
# in https://github.com/jetkvm/kvm) persists the mode as the plain-JSON
|
|
||||||
# field "tls_mode" (values "", "self-signed", or "custom") in
|
|
||||||
# /userdata/kvm_config.json.
|
|
||||||
#
|
|
||||||
# None of the above (storage path, filenames, config file, reboot-to-apply
|
|
||||||
# behavior) is part of JetKVM's stable/documented API; it was confirmed
|
|
||||||
# against real JetKVM hardware, but is worth a spot-check after a JetKVM
|
|
||||||
# firmware upgrade -- set DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE=no to skip the
|
|
||||||
# HTTPS-mode check entirely if a future firmware version changes that
|
|
||||||
# file's format out from under it.
|
|
||||||
#
|
|
||||||
# The following variables exported from environment will be used. If not
|
|
||||||
# set then values previously saved in the domain.conf file are used. All
|
|
||||||
# of them are optional.
|
|
||||||
#
|
|
||||||
# export DEPLOY_JETKVM_USER="root" # defaults to "root"
|
|
||||||
# export DEPLOY_JETKVM_HOST="jetkvm.example.com" # defaults to the cert's domain
|
|
||||||
# export DEPLOY_JETKVM_PORT="22" # defaults to 22
|
|
||||||
# export DEPLOY_JETKVM_SSH_CMD="ssh -T" # defaults to "ssh -T"
|
|
||||||
# export DEPLOY_JETKVM_RESTART_CMD="reboot" # defaults to "reboot"; set to "none" to skip it
|
|
||||||
# export DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE="yes" # defaults to "yes" (verify tls_mode=custom before upload); set to "no" to skip
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# ```sh
|
|
||||||
# export DEPLOY_JETKVM_HOST="192.168.1.50"
|
|
||||||
# acme.sh --deploy -d jetkvm.example.com --deploy-hook jetkvm
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# returns 0 means success, otherwise error.
|
|
||||||
|
|
||||||
######## Public functions #####################
|
|
||||||
|
|
||||||
#domain keyfile certfile cafile fullchain
|
|
||||||
jetkvm_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"
|
|
||||||
|
|
||||||
if [ ! -s "$_ckey" ] || [ ! -s "$_cfullchain" ]; then
|
|
||||||
_err "JetKVM deploy needs both a private key and a fullchain certificate (not available, e.g., after --signcsr)."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_getdeployconf DEPLOY_JETKVM_USER
|
|
||||||
if [ -z "$DEPLOY_JETKVM_USER" ]; then
|
|
||||||
DEPLOY_JETKVM_USER="root"
|
|
||||||
fi
|
|
||||||
_savedeployconf DEPLOY_JETKVM_USER "$DEPLOY_JETKVM_USER"
|
|
||||||
|
|
||||||
_getdeployconf DEPLOY_JETKVM_HOST
|
|
||||||
if [ -z "$DEPLOY_JETKVM_HOST" ]; then
|
|
||||||
_debug "Using _cdomain as DEPLOY_JETKVM_HOST, please set if not correct."
|
|
||||||
DEPLOY_JETKVM_HOST="$_cdomain"
|
|
||||||
fi
|
|
||||||
_savedeployconf DEPLOY_JETKVM_HOST "$DEPLOY_JETKVM_HOST"
|
|
||||||
|
|
||||||
_getdeployconf DEPLOY_JETKVM_PORT
|
|
||||||
if [ -z "$DEPLOY_JETKVM_PORT" ]; then
|
|
||||||
DEPLOY_JETKVM_PORT="22"
|
|
||||||
fi
|
|
||||||
_savedeployconf DEPLOY_JETKVM_PORT "$DEPLOY_JETKVM_PORT"
|
|
||||||
|
|
||||||
_getdeployconf DEPLOY_JETKVM_SSH_CMD
|
|
||||||
if [ -z "$DEPLOY_JETKVM_SSH_CMD" ]; then
|
|
||||||
DEPLOY_JETKVM_SSH_CMD="ssh -T"
|
|
||||||
fi
|
|
||||||
_savedeployconf DEPLOY_JETKVM_SSH_CMD "$DEPLOY_JETKVM_SSH_CMD" "base64"
|
|
||||||
|
|
||||||
_getdeployconf DEPLOY_JETKVM_RESTART_CMD
|
|
||||||
if [ -z "$DEPLOY_JETKVM_RESTART_CMD" ]; then
|
|
||||||
DEPLOY_JETKVM_RESTART_CMD="reboot"
|
|
||||||
fi
|
|
||||||
_savedeployconf DEPLOY_JETKVM_RESTART_CMD "$DEPLOY_JETKVM_RESTART_CMD" "base64"
|
|
||||||
|
|
||||||
_getdeployconf DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE
|
|
||||||
if [ -z "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" ]; then
|
|
||||||
DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE="yes"
|
|
||||||
fi
|
|
||||||
_savedeployconf DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE"
|
|
||||||
|
|
||||||
_info "Deploying certificate to JetKVM device $DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST:$DEPLOY_JETKVM_PORT"
|
|
||||||
|
|
||||||
# Firmware constants on a single-purpose, single-root appliance -- not
|
|
||||||
# user configuration. If JetKVM ever moves these, that's a hook update,
|
|
||||||
# not a setting (a saved-per-domain override would just as easily hide
|
|
||||||
# the fix from anyone already using this hook).
|
|
||||||
_jetkvm_remote_path="/userdata/jetkvm/tls"
|
|
||||||
_jetkvm_cert_name="user-defined.crt"
|
|
||||||
_jetkvm_key_name="user-defined.key"
|
|
||||||
_jetkvm_config_file="/userdata/kvm_config.json"
|
|
||||||
_jetkvm_mode_exitcode=3
|
|
||||||
_jetkvm_config_missing_exitcode=4
|
|
||||||
|
|
||||||
_jetkvm_run_id="$$.$(_time)"
|
|
||||||
_jetkvm_cert_marker="ACME_JETKVM_CERT_$_jetkvm_run_id"
|
|
||||||
_jetkvm_key_marker="ACME_JETKVM_KEY_$_jetkvm_run_id"
|
|
||||||
_jetkvm_cert_tmp="$_jetkvm_remote_path/.$_jetkvm_cert_name.tmp"
|
|
||||||
_jetkvm_key_tmp="$_jetkvm_remote_path/.$_jetkvm_key_name.tmp"
|
|
||||||
_jetkvm_cert_target="$_jetkvm_remote_path/$_jetkvm_cert_name"
|
|
||||||
_jetkvm_key_target="$_jetkvm_remote_path/$_jetkvm_key_name"
|
|
||||||
|
|
||||||
# Command substitution strips all trailing newlines, so the printf below
|
|
||||||
# always emits the content with exactly one trailing newline before the
|
|
||||||
# heredoc terminator -- regardless of whether the source file already
|
|
||||||
# ended with one -- so the terminator is guaranteed to start its own line.
|
|
||||||
_jetkvm_cert_content="$(cat "$_cfullchain")"
|
|
||||||
_jetkvm_key_content="$(cat "$_ckey")"
|
|
||||||
|
|
||||||
_jetkvm_upload_script="$(
|
|
||||||
echo "#!/bin/sh"
|
|
||||||
echo "set -e"
|
|
||||||
echo "umask 077"
|
|
||||||
printf "trap \"rm -f '%s' '%s'\" EXIT\n" "$_jetkvm_cert_tmp" "$_jetkvm_key_tmp"
|
|
||||||
if [ "$DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE" != "no" ]; then
|
|
||||||
# Uploading a certificate that HTTPS Mode won't even serve would
|
|
||||||
# otherwise fail silently -- see the header comment for why this
|
|
||||||
# greps the device's own config file rather than querying it
|
|
||||||
# through a documented API (there isn't one for reading this
|
|
||||||
# headlessly yet). The config file is checked for readability
|
|
||||||
# separately so a missing/renamed file isn't misreported as
|
|
||||||
# HTTPS Mode being wrong.
|
|
||||||
printf "if [ ! -r '%s' ]; then exit %s; fi\n" "$_jetkvm_config_file" "$_jetkvm_config_missing_exitcode"
|
|
||||||
printf 'if ! grep -q '\''"tls_mode" *: *"custom"'\'' '\''%s'\''; then exit %s; fi\n' "$_jetkvm_config_file" "$_jetkvm_mode_exitcode"
|
|
||||||
fi
|
|
||||||
printf "mkdir -p '%s'\n" "$_jetkvm_remote_path"
|
|
||||||
printf "cat > '%s' <<'%s'\n" "$_jetkvm_cert_tmp" "$_jetkvm_cert_marker"
|
|
||||||
printf '%s\n' "$_jetkvm_cert_content"
|
|
||||||
echo "$_jetkvm_cert_marker"
|
|
||||||
printf "chmod 0644 '%s'\n" "$_jetkvm_cert_tmp"
|
|
||||||
printf "cat > '%s' <<'%s'\n" "$_jetkvm_key_tmp" "$_jetkvm_key_marker"
|
|
||||||
printf '%s\n' "$_jetkvm_key_content"
|
|
||||||
echo "$_jetkvm_key_marker"
|
|
||||||
printf "chmod 0600 '%s'\n" "$_jetkvm_key_tmp"
|
|
||||||
printf "mv '%s' '%s'\n" "$_jetkvm_cert_tmp" "$_jetkvm_cert_target"
|
|
||||||
printf "mv '%s' '%s'\n" "$_jetkvm_key_tmp" "$_jetkvm_key_target"
|
|
||||||
)"
|
|
||||||
|
|
||||||
_secure_debug "Generated upload script" "$_jetkvm_upload_script"
|
|
||||||
|
|
||||||
_info "Connecting to JetKVM device $DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST:$DEPLOY_JETKVM_PORT to deploy certificate"
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
printf '%s\n' "$_jetkvm_upload_script" | $DEPLOY_JETKVM_SSH_CMD -p "$DEPLOY_JETKVM_PORT" "$DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST" sh
|
|
||||||
_ret=$?
|
|
||||||
|
|
||||||
if [ "$_ret" = "$_jetkvm_config_missing_exitcode" ]; then
|
|
||||||
_err "JetKVM config file ($_jetkvm_config_file) was not found or not readable on the device -- this hook's assumptions may be out of date after a firmware upgrade. Certificate was NOT uploaded."
|
|
||||||
return "$_ret"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_ret" = "$_jetkvm_mode_exitcode" ]; then
|
|
||||||
_err "JetKVM HTTPS Mode is not set to \"Custom\" (checked \"tls_mode\" in $_jetkvm_config_file on the device). Set it in the device's web UI (Settings > Network > HTTPS Mode) before this hook can take effect. Certificate was NOT uploaded."
|
|
||||||
return "$_ret"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_ret" != "0" ]; then
|
|
||||||
_err "Error code $_ret returned uploading certificate to JetKVM device"
|
|
||||||
return "$_ret"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "Certificate and key uploaded to $_jetkvm_remote_path on the device"
|
|
||||||
|
|
||||||
if [ "$DEPLOY_JETKVM_RESTART_CMD" = "none" ]; then
|
|
||||||
_info "Certificate successfully deployed to JetKVM device. DEPLOY_JETKVM_RESTART_CMD=none, skipping restart command."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run the restart command detached (nohup ... &) so this ssh call
|
|
||||||
# returns as soon as it's launched, before the device actually reboots,
|
|
||||||
# rather than racing the connection teardown -- observed, against real
|
|
||||||
# hardware, that a reboot racing the SSH session's own exit can make
|
|
||||||
# ssh itself exit anywhere from a clean 0 to a connection-reset 255.
|
|
||||||
# Since the restart command then runs as an unwaited background job on
|
|
||||||
# the device, this ssh call reports success as soon as that job is
|
|
||||||
# launched -- it does NOT confirm nohup, sh, or the restart command
|
|
||||||
# itself actually exist or succeed (measured: a nonexistent restart
|
|
||||||
# command, and even a missing nohup binary, both still return 0 here).
|
|
||||||
# Only an outright SSH connection failure (unreachable host, auth
|
|
||||||
# failure, etc.) is caught below. "sleep" runs on the device's own
|
|
||||||
# shell, not acme.sh's, so acme.sh's _sleep wrapper does not apply.
|
|
||||||
_info "Running post-upload command on JetKVM device: $DEPLOY_JETKVM_RESTART_CMD"
|
|
||||||
# Escape any single quotes in the (user-configurable, free-text)
|
|
||||||
# restart command before nesting it inside the outer 'sleep N; ...'
|
|
||||||
# single-quoted string -- otherwise a value like "sh -c 'sync; reboot'"
|
|
||||||
# breaks the quoting and only part of it ends up inside the detached
|
|
||||||
# background job.
|
|
||||||
_jetkvm_restart_cmd_escaped=$(printf '%s' "$DEPLOY_JETKVM_RESTART_CMD" | sed "s/'/'\\\\''/g")
|
|
||||||
_jetkvm_detached_cmd="nohup sh -c 'sleep 2; $_jetkvm_restart_cmd_escaped' >/dev/null 2>&1 &"
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
if ! $DEPLOY_JETKVM_SSH_CMD -p "$DEPLOY_JETKVM_PORT" "$DEPLOY_JETKVM_USER@$DEPLOY_JETKVM_HOST" "$_jetkvm_detached_cmd"; then
|
|
||||||
_err "Certificate was uploaded, but connecting to the JetKVM device to launch the restart command failed."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "Certificate deployed to JetKVM device; it will restart shortly to apply it."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
+2
-2
@@ -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" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
_message=$(echo "$_response" | grep -A 2 'message-body' | 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" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
|
_message=$(echo "$_response" | grep -A 2 'message-body' | 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."
|
||||||
|
|||||||
+4
-1
@@ -121,7 +121,10 @@ 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"
|
||||||
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
# Initialise $Le_Keylength if its not already set
|
||||||
|
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"
|
||||||
|
|||||||
+8
-19
@@ -10,10 +10,6 @@
|
|||||||
# 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
|
||||||
@@ -53,7 +49,7 @@ multideploy_deploy() {
|
|||||||
_debug _cfullchain "$_cfullchain"
|
_debug _cfullchain "$_cfullchain"
|
||||||
_debug _cpfx "$_cpfx"
|
_debug _cpfx "$_cpfx"
|
||||||
|
|
||||||
_getdeployconf MULTIDEPLOY_FILENAME
|
MULTIDEPLOY_FILENAME="${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'."
|
||||||
@@ -79,8 +75,7 @@ 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. A name starting with '/' is treated as an
|
# $@ - Posible deploy file names.
|
||||||
# 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() {
|
||||||
@@ -92,21 +87,15 @@ _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
|
||||||
if _startswith "$file" "/"; then
|
_debug3 "Checking file" "$DOMAIN_PATH/$file"
|
||||||
_multideploy_path="$file"
|
if [ -f "$DOMAIN_PATH/$file" ]; then
|
||||||
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="$_multideploy_path"
|
found_file="$file"
|
||||||
else
|
else
|
||||||
_debug3 "File not found"
|
_debug3 "File not found"
|
||||||
fi
|
fi
|
||||||
@@ -116,12 +105,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 "$found_file"; then
|
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then
|
||||||
_err "Deploy file is not valid: $found_file"
|
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$found_file"
|
echo "$DOMAIN_PATH/$found_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Description:
|
# Description:
|
||||||
|
|||||||
+10
-17
@@ -116,24 +116,17 @@ 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 '"')
|
||||||
case "$_status_code" in
|
_retval=$?
|
||||||
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"
|
||||||
;;
|
_debug "Response" "$response"
|
||||||
esac
|
return 1
|
||||||
_err "Certificate deployment failed (HTTP status $_status_code). $message"
|
fi
|
||||||
_debug "Response" "$response"
|
|
||||||
return 1
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-17
@@ -128,24 +128,17 @@ 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 '"')
|
||||||
case "$_status_code" in
|
_retval=$?
|
||||||
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"
|
||||||
;;
|
_debug "Response" "$response"
|
||||||
esac
|
return 1
|
||||||
_err "Certificate deployment failed (HTTP status $_status_code). $message"
|
fi
|
||||||
_debug "Response" "$response"
|
|
||||||
return 1
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -175,7 +175,7 @@ _get_unleashed_version() {
|
|||||||
|
|
||||||
_post_upload() {
|
_post_upload() {
|
||||||
_post_action="$1"
|
_post_action="$1"
|
||||||
_post_upfile="$2"
|
_post_file="$2"
|
||||||
|
|
||||||
_post_boundary="----FormBoundary$(date "+%s%N")"
|
_post_boundary="----FormBoundary$(date "+%s%N")"
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ _post_upload() {
|
|||||||
printf -- "--%s\r\n" "$_post_boundary"
|
printf -- "--%s\r\n" "$_post_boundary"
|
||||||
printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action"
|
printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action"
|
||||||
printf -- "Content-Type: application/octet-stream\r\n\r\n"
|
printf -- "Content-Type: application/octet-stream\r\n\r\n"
|
||||||
printf -- "%s\r\n" "$(cat "$_post_upfile")"
|
printf -- "%s\r\n" "$(cat "$_post_file")"
|
||||||
|
|
||||||
printf -- "--%s\r\n" "$_post_boundary"
|
printf -- "--%s\r\n" "$_post_boundary"
|
||||||
printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
|
printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
+7
-5
@@ -232,6 +232,8 @@ _ssh_deploy() {
|
|||||||
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
|
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
|
||||||
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
|
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
|
||||||
# Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
|
# Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
|
||||||
|
# Create our backup directory for overwritten cert files.
|
||||||
|
_cmdstr="mkdir -p $_backupdir; $_cmdstr"
|
||||||
_info "Backup of old certificate files will be placed in remote directory $_backupdir"
|
_info "Backup of old certificate files will be placed in remote directory $_backupdir"
|
||||||
_info "Backup directories erased after 180 days."
|
_info "Backup directories erased after 180 days."
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
@@ -245,7 +247,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
|
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
|
||||||
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi;"
|
_cmdstr="$_cmdstr cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null;"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -282,7 +284,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
_pipe=">>"
|
_pipe=">>"
|
||||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi;"
|
_cmdstr="$_cmdstr cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null;"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -323,7 +325,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
_pipe=">>"
|
_pipe=">>"
|
||||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi;"
|
_cmdstr="$_cmdstr cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null;"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -368,8 +370,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
_pipe=">>"
|
_pipe=">>"
|
||||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi;"
|
_cmdstr="$_cmdstr cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null;"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_FULLCHAIN" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -344,7 +344,6 @@ 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
|
||||||
@@ -355,7 +354,6 @@ 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
|
||||||
@@ -391,13 +389,13 @@ synology_dsm_deploy() {
|
|||||||
else
|
else
|
||||||
_info "Restart HTTP services not necessary."
|
_info "Restart HTTP services not necessary."
|
||||||
fi
|
fi
|
||||||
_logout
|
|
||||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||||
|
_logout
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -405,8 +403,6 @@ 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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,518 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
# TrueNAS deploy script for SCALE/CORE using websocket (websocat binary)
|
|
||||||
# It is recommend to use a wildcard certificate
|
|
||||||
#
|
|
||||||
# Tested with TrueNAS SCALE 25.10 (API "wss://host/api/current", JSON-RPC 2.0).
|
|
||||||
#
|
|
||||||
# Unlike "truenas_ws" hook, this script does NOT use midclt, the truenas_api_client Python package.
|
|
||||||
# It only depends on:
|
|
||||||
# - jq
|
|
||||||
# - websocat (a static binary you deploy)
|
|
||||||
#
|
|
||||||
# Why: avoids installing a Python environment / TrueNAS package on remote machine just to push a certificate.
|
|
||||||
#
|
|
||||||
# IMPORTANT: This script is written in pure POSIX sh (no coproc, no bash arrays).
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Environment variables
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
#
|
|
||||||
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
|
|
||||||
#
|
|
||||||
# Required:
|
|
||||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
|
|
||||||
#
|
|
||||||
# Optional:
|
|
||||||
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>" (required on first run)
|
|
||||||
# export DEPLOY_TRUENAS_PROTOCOL="ws" # ws or wss (default: ws)
|
|
||||||
# export DEPLOY_TRUENAS_PORT="80" # 80, 443, 8443 (default: 80)
|
|
||||||
# NOTE: defaults are intentionally "ws"/80, not "wss"/443: a freshly
|
|
||||||
# installed TrueNAS serves its Web UI over plain HTTP on port 80 out
|
|
||||||
# of the box, and port 443 is not listening until HTTPS is configured.
|
|
||||||
# Port 80 stays reachable even after HTTPS is enabled, so this keeps
|
|
||||||
# the hook working on first run without extra setup.
|
|
||||||
# export DEPLOY_TRUENAS_UPDATE_FTP="no" # yes or no (default: no) also updates the FTP certificate
|
|
||||||
# export DEPLOY_TRUENAS_UPDATE_APPS="no" # yes or no (default: no) also updates the certificate for any
|
|
||||||
# iX app exposing a "certificate_id" option.
|
|
||||||
# WARNING: this redeploys (restarts) every matching app.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
########################
|
|
||||||
### Public functions ###
|
|
||||||
########################
|
|
||||||
|
|
||||||
# truenas_websocat_deploy
|
|
||||||
#
|
|
||||||
# Deploy new certificate to TrueNAS services with websocat binary
|
|
||||||
#
|
|
||||||
# Arguments
|
|
||||||
# 1: Domain
|
|
||||||
# 2: Key-File
|
|
||||||
# 3: Certificate-File
|
|
||||||
# 4: CA-File
|
|
||||||
# 5: FullChain-File
|
|
||||||
# Returns:
|
|
||||||
# 0: Success
|
|
||||||
# 1: Missing or invalid API Key
|
|
||||||
# 2: TrueNAS not ready (health check failed)
|
|
||||||
# 3: (reserved)
|
|
||||||
# 4: FTP & iX App cert error
|
|
||||||
# 5: WebUI cert error
|
|
||||||
# 6: Certificate creation job error
|
|
||||||
# 7: Websocat / transport call error (socket write/read failed)
|
|
||||||
# 8: Missing binary or invalid configuration
|
|
||||||
# 9: TrueNAS API returned an explicit error (JSON-RPC .error field)
|
|
||||||
|
|
||||||
truenas_websocat_deploy() {
|
|
||||||
_jq_bin=$(command -v jq 2>/dev/null)
|
|
||||||
if [ -z "$_jq_bin" ]; then
|
|
||||||
_err "jq binary not found in PATH. Install it using your system's package manager."
|
|
||||||
return 8
|
|
||||||
fi
|
|
||||||
_websocat_bin=$(command -v websocat 2>/dev/null)
|
|
||||||
if [ -z "$_websocat_bin" ]; then
|
|
||||||
_err "websocat binary not found in PATH. Install it using your system's package manager, or download a static binary from https://github.com/vi/websocat/releases."
|
|
||||||
return 8
|
|
||||||
fi
|
|
||||||
|
|
||||||
_domain="$1"
|
|
||||||
_file_key="$2"
|
|
||||||
_file_cert="$3"
|
|
||||||
_file_cca="$4"
|
|
||||||
_file_fullchain="$5"
|
|
||||||
_debug _domain "$_domain"
|
|
||||||
_debug _file_key "$_file_key"
|
|
||||||
_debug _file_cert "$_file_cert"
|
|
||||||
_debug _file_ca "$_file_cca"
|
|
||||||
_debug _file_fullchain "$_file_fullchain"
|
|
||||||
|
|
||||||
if [ ! -x "$_jq_bin" ]; then
|
|
||||||
_err "Binary not found or not executable: $_jq_bin"
|
|
||||||
return 8
|
|
||||||
fi
|
|
||||||
if [ ! -x "$_websocat_bin" ]; then
|
|
||||||
_err "Binary not found or not executable: $_websocat_bin"
|
|
||||||
return 8
|
|
||||||
fi
|
|
||||||
|
|
||||||
### ---- Configuration ----
|
|
||||||
|
|
||||||
_info "Checking environment variables..."
|
|
||||||
_getdeployconf DEPLOY_TRUENAS_APIKEY
|
|
||||||
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
|
|
||||||
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
|
|
||||||
_getdeployconf DEPLOY_TRUENAS_PORT
|
|
||||||
_getdeployconf DEPLOY_TRUENAS_UPDATE_FTP
|
|
||||||
_getdeployconf DEPLOY_TRUENAS_UPDATE_APPS
|
|
||||||
|
|
||||||
# Check API Key
|
|
||||||
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
|
|
||||||
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# Check Hostname, default to localhost if not set
|
|
||||||
if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then
|
|
||||||
_info "TrueNAS hostname not set. Using 'localhost'."
|
|
||||||
DEPLOY_TRUENAS_HOSTNAME="localhost"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check protocol, default to ws if not set: a freshly installed TrueNAS serves its Web UI over plain HTTP, so wss/443 is not available out of the box.
|
|
||||||
# Use DEPLOY_TRUENAS_PROTOCOL="wss" once HTTPS is configured, since the payload otherwise carries the API key and private key in plain text.
|
|
||||||
if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then
|
|
||||||
_info "TrueNAS protocol not set. Using 'ws'."
|
|
||||||
DEPLOY_TRUENAS_PROTOCOL="ws"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check port, default to 80 if not set (see protocol comment above)
|
|
||||||
if [ -z "$DEPLOY_TRUENAS_PORT" ]; then
|
|
||||||
_info "TrueNAS port not set. Using '80'."
|
|
||||||
DEPLOY_TRUENAS_PORT="80"
|
|
||||||
fi
|
|
||||||
case "$DEPLOY_TRUENAS_PORT" in
|
|
||||||
'' | *[!0-9]*)
|
|
||||||
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
|
|
||||||
return 8
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
_truenas_websocat_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/api/current"
|
|
||||||
|
|
||||||
# Check FTP update, default to no if not set
|
|
||||||
if [ -z "$DEPLOY_TRUENAS_UPDATE_FTP" ]; then
|
|
||||||
_info "Certificate update for FTP is not set. Using 'no'."
|
|
||||||
DEPLOY_TRUENAS_UPDATE_FTP="no"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Apps update, default to no if not set
|
|
||||||
if [ -z "$DEPLOY_TRUENAS_UPDATE_APPS" ]; then
|
|
||||||
_info "Certificate update for Apps is not set. Using 'no'."
|
|
||||||
DEPLOY_TRUENAS_UPDATE_APPS="no"
|
|
||||||
fi
|
|
||||||
|
|
||||||
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
|
||||||
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
|
||||||
_debug2 DEPLOY_TRUENAS_UPDATE_FTP "$DEPLOY_TRUENAS_UPDATE_FTP"
|
|
||||||
_debug2 DEPLOY_TRUENAS_UPDATE_APPS "$DEPLOY_TRUENAS_UPDATE_APPS"
|
|
||||||
_debug _truenas_websocat_uri "$_truenas_websocat_uri"
|
|
||||||
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
|
|
||||||
_info "Environment variables: OK"
|
|
||||||
|
|
||||||
### ---- Persistent WebSocket connection (FIFOs, sh/dash compatible) ----
|
|
||||||
#
|
|
||||||
# Authentication is tied to the WebSocket connection:
|
|
||||||
# the SAME connection must stay open from login until the end, otherwise every subsequent call comes back unauthenticated.
|
|
||||||
# We use two FIFOs + `exec` to talk to a background websocat process, without relying on bash-only extensions.
|
|
||||||
|
|
||||||
_websocat_tmpdir=$(mktemp -d /tmp/truenas_websocat.XXXXXX) || {
|
|
||||||
_err "mktemp failed"
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
_websocat_fifo_in="${_websocat_tmpdir}/in"
|
|
||||||
_websocat_fifo_out="${_websocat_tmpdir}/out"
|
|
||||||
mkfifo "$_websocat_fifo_in" "$_websocat_fifo_out" || {
|
|
||||||
_err "mkfifo failed"
|
|
||||||
rm -rf "$_websocat_tmpdir"
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
|
|
||||||
"$_websocat_bin" -n -k "$_truenas_websocat_uri" <"$_websocat_fifo_in" >"$_websocat_fifo_out" 2>"${_websocat_tmpdir}/err.log" &
|
|
||||||
_websocat_pid=$!
|
|
||||||
|
|
||||||
# Opening "in" for read+write avoids a deadlock if websocat hasn't opened the fifo for reading yet at the time we write to it.
|
|
||||||
exec 3<>"$_websocat_fifo_in"
|
|
||||||
exec 4<"$_websocat_fifo_out"
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
if ! kill -0 "$_websocat_pid" 2>/dev/null; then
|
|
||||||
_err "websocat exited prematurely."
|
|
||||||
_err "$(cat "${_websocat_tmpdir}/err.log" 2>/dev/null)"
|
|
||||||
exec 3>&- 4<&-
|
|
||||||
rm -rf "$_websocat_tmpdir"
|
|
||||||
return 3
|
|
||||||
fi
|
|
||||||
|
|
||||||
_websocat_req_counter=0
|
|
||||||
|
|
||||||
_truenas_websocat_cleanup() {
|
|
||||||
exec 3>&- 2>/dev/null
|
|
||||||
exec 4<&- 2>/dev/null
|
|
||||||
[ -n "$_websocat_pid" ] && kill "$_websocat_pid" 2>/dev/null
|
|
||||||
rm -rf "$_websocat_tmpdir" 2>/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
# _truenas_websocat_rpc_call <method> <json_params>
|
|
||||||
# Does NOT log the payload/response: some calls (certificate.create, core.get_jobs) contain the certificate and private key in plain text, which would massively bloat the logs.
|
|
||||||
_truenas_websocat_rpc_call() {
|
|
||||||
_truenas_websocat_method="$1"
|
|
||||||
_truenas_websocat_params="$2"
|
|
||||||
_websocat_req_counter=$((_websocat_req_counter + 1))
|
|
||||||
_req_id="$_websocat_req_counter"
|
|
||||||
|
|
||||||
_truenas_websocat_payload=$("$_jq_bin" -c -n \
|
|
||||||
--arg jsonrpc "2.0" \
|
|
||||||
--arg id "$_req_id" \
|
|
||||||
--arg method "$_truenas_websocat_method" \
|
|
||||||
--argjson params "$_truenas_websocat_params" \
|
|
||||||
'{jsonrpc: $jsonrpc, id: $id, method: $method, params: $params}')
|
|
||||||
|
|
||||||
printf '%s\n' "$_truenas_websocat_payload" >&3 || {
|
|
||||||
_err "Socket write failed (method: $_truenas_websocat_method)"
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
IFS= read -r _truenas_websocat_response <&4 || {
|
|
||||||
_err "Socket read failed (method: $_truenas_websocat_method)"
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
|
|
||||||
printf '%s' "$_truenas_websocat_response"
|
|
||||||
}
|
|
||||||
|
|
||||||
# _truenas_websocat_rpc_has_error <response> <label> -> returns 0 (and prints) if an error was found, 1 otherwise
|
|
||||||
_truenas_websocat_rpc_has_error() {
|
|
||||||
_msg=$(printf '%s' "$1" | "$_jq_bin" -r '.error.message // empty' 2>/dev/null)
|
|
||||||
if [ -n "$_msg" ]; then
|
|
||||||
_err "RPC error ($2): $_msg"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Polls once per second and gives up after _TRUENAS_WEBSOCAT_JOB_TIMEOUT seconds (default 60s)
|
|
||||||
# if the job never leaves RUNNING/WAITING, so a stuck TrueNAS job cannot hang the deploy hook forever.
|
|
||||||
# NOTE: this same timeout also bounds app.update jobs when DEPLOY_TRUENAS_UPDATE_APPS=yes (iX App redeploy)
|
|
||||||
# raise _TRUENAS_WEBSOCAT_JOB_TIMEOUT if an app takes longer than that to redeploy (e.g. image pull, migrations).
|
|
||||||
|
|
||||||
_truenas_websocat_wait_for_job() {
|
|
||||||
_jobid="$1"
|
|
||||||
_truenas_websocat_job_elapsed=0
|
|
||||||
_truenas_websocat_job_timeout="${_TRUENAS_WEBSOCAT_JOB_TIMEOUT:-60}"
|
|
||||||
while true; do
|
|
||||||
if [ "$_truenas_websocat_job_elapsed" -ge "$_truenas_websocat_job_timeout" ]; then
|
|
||||||
_err "Job $_jobid: timed out after ${_truenas_websocat_job_timeout}s."
|
|
||||||
return 6
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
_truenas_websocat_job_elapsed=$((_truenas_websocat_job_elapsed + 1))
|
|
||||||
_job_resp=$(_truenas_websocat_rpc_call "core.get_jobs" "[[[\"id\",\"=\",${_jobid}]]]") || return 6
|
|
||||||
if _truenas_websocat_rpc_has_error "$_job_resp" "core.get_jobs"; then return 6; fi
|
|
||||||
_state=$(printf '%s' "$_job_resp" | "$_jq_bin" -r '.result[0].state // empty')
|
|
||||||
case "$_state" in
|
|
||||||
SUCCESS)
|
|
||||||
printf '%s' "$_job_resp" | "$_jq_bin" -c '.result[0].result'
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
FAILED | ABORTED)
|
|
||||||
_err "Job $_jobid failed: $(printf '%s' "$_job_resp" | "$_jq_bin" -c '.result[0].error')"
|
|
||||||
return 6
|
|
||||||
;;
|
|
||||||
"")
|
|
||||||
_err "Job $_jobid: unexpected response."
|
|
||||||
return 6
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
### ---- 1. Health check ----
|
|
||||||
|
|
||||||
_info "Testing connection to TrueNAS WebSocket at $_truenas_websocat_uri..."
|
|
||||||
_ping_resp=$(_truenas_websocat_rpc_call "core.ping" "[]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_ping_resp" "core.ping"; then
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 9
|
|
||||||
fi
|
|
||||||
if [ "$(printf '%s' "$_ping_resp" | "$_jq_bin" -r '.result // empty')" != "pong" ]; then
|
|
||||||
_err "Health check failed (no pong received)."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
_info "Health check OK (pong received)."
|
|
||||||
|
|
||||||
### ---- 2. Authentication & Check ----
|
|
||||||
|
|
||||||
_info "Authenticating with API Key..."
|
|
||||||
_key_params=$("$_jq_bin" -c -n --arg k "$DEPLOY_TRUENAS_APIKEY" '[$k]')
|
|
||||||
_auth_resp=$(_truenas_websocat_rpc_call "auth.login_with_api_key" "$_key_params") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_auth_resp" "auth.login_with_api_key"; then
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 9
|
|
||||||
fi
|
|
||||||
if [ "$(printf '%s' "$_auth_resp" | "$_jq_bin" -r '.result // empty')" != "true" ]; then
|
|
||||||
_err "Authentication failed (invalid API key?)."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_info "Connected to TrueNAS ($_truenas_websocat_uri)."
|
|
||||||
|
|
||||||
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
|
|
||||||
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
|
||||||
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
|
||||||
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
|
|
||||||
_savedeployconf DEPLOY_TRUENAS_UPDATE_FTP "$DEPLOY_TRUENAS_UPDATE_FTP"
|
|
||||||
_savedeployconf DEPLOY_TRUENAS_UPDATE_APPS "$DEPLOY_TRUENAS_UPDATE_APPS"
|
|
||||||
|
|
||||||
_info "Checking TrueNAS system version..."
|
|
||||||
_ver_resp=$(_truenas_websocat_rpc_call "system.info" "[]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_ver_resp" "system.info"; then
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 9
|
|
||||||
fi
|
|
||||||
_sys_version=$(printf '%s' "$_ver_resp" | "$_jq_bin" -r '.result.version // .result // "Unknown"')
|
|
||||||
_info "TrueNAS System Version: $_sys_version"
|
|
||||||
|
|
||||||
### ---- 3. Read certificate files ----
|
|
||||||
|
|
||||||
_info "Reading certificate files for $_domain..."
|
|
||||||
if [ ! -f "$_file_fullchain" ] || [ ! -f "$_file_key" ]; then
|
|
||||||
_err "Certificate or key file not found."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 5
|
|
||||||
fi
|
|
||||||
_cert_content=$("$_jq_bin" -sR . "$_file_fullchain")
|
|
||||||
_key_content=$("$_jq_bin" -sR . "$_file_key")
|
|
||||||
|
|
||||||
_safe_domain=$(echo "$_domain" | tr '*.' '_')
|
|
||||||
_cert_name="acme_${_safe_domain}_$(date +%Y%m%d_%H%M%S)"
|
|
||||||
_debug _certname "$_cert_name"
|
|
||||||
|
|
||||||
### ---- 4. Current Web UI certificate ----
|
|
||||||
|
|
||||||
_info "Retrieving current Web UI configuration..."
|
|
||||||
_config_resp=$(_truenas_websocat_rpc_call "system.general.config" "[]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_config_resp" "system.general.config"; then
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 9
|
|
||||||
fi
|
|
||||||
_old_cert_id=$(printf '%s' "$_config_resp" | "$_jq_bin" -r '.result.ui_certificate.id // .result.ui_certificate // empty')
|
|
||||||
_info "Current Web UI Certificate ID: ${_old_cert_id:-None}"
|
|
||||||
|
|
||||||
### ---- 5. Import the new certificate (asynchronous job) ----
|
|
||||||
|
|
||||||
_info "Importing new certificate '$_cert_name'..."
|
|
||||||
_create_params=$("$_jq_bin" -n \
|
|
||||||
--arg name "$_cert_name" \
|
|
||||||
--argjson cert "$_cert_content" \
|
|
||||||
--argjson key "$_key_content" \
|
|
||||||
'[{create_type: "CERTIFICATE_CREATE_IMPORTED", name: $name, certificate: $cert, privatekey: $key}]')
|
|
||||||
|
|
||||||
_new_cert_resp=$(_truenas_websocat_rpc_call "certificate.create" "$_create_params") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_new_cert_resp" "certificate.create"; then
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 9
|
|
||||||
fi
|
|
||||||
_new_cert_jobid=$(printf '%s' "$_new_cert_resp" | "$_jq_bin" -r '.result // empty')
|
|
||||||
|
|
||||||
case "$_new_cert_jobid" in
|
|
||||||
'' | *[!0-9]*)
|
|
||||||
_err "Unexpected response from certificate.create (expected a job ID)."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 6
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
_info "Import job certificate started (job ID: $_new_cert_jobid), waiting..."
|
|
||||||
_job_result=$(_truenas_websocat_wait_for_job "$_new_cert_jobid") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 6
|
|
||||||
}
|
|
||||||
_new_cert_id=$(printf '%s' "$_job_result" | "$_jq_bin" -r '.id // empty')
|
|
||||||
|
|
||||||
if [ -z "$_new_cert_id" ]; then
|
|
||||||
_err "Could not retrieve the imported certificate's ID."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 6
|
|
||||||
fi
|
|
||||||
_info "Certificate imported: '$_cert_name' (ID $_new_cert_id)."
|
|
||||||
|
|
||||||
### ---- 6. Assign to the Web UI ----
|
|
||||||
|
|
||||||
_info "Assigning certificate ID $_new_cert_id to Web UI..."
|
|
||||||
_update_resp=$(_truenas_websocat_rpc_call "system.general.update" "[{\"ui_certificate\": ${_new_cert_id}}]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 7
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_update_resp" "system.general.update"; then
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 9
|
|
||||||
fi
|
|
||||||
_assigned_id=$(printf '%s' "$_update_resp" | "$_jq_bin" -r '.result.ui_certificate.id // .result.ui_certificate // empty')
|
|
||||||
|
|
||||||
if [ "$_assigned_id" != "$_new_cert_id" ]; then
|
|
||||||
_err "Failed to assign the certificate to the Web UI."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 5
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "Restarting TrueNAS Web UI service..."
|
|
||||||
_restart_resp=$(_truenas_websocat_rpc_call "system.general.ui_restart" "[]")
|
|
||||||
_truenas_websocat_rpc_has_error "$_restart_resp" "system.general.ui_restart"
|
|
||||||
_info "Web UI certificate updated and UI restarted."
|
|
||||||
# Need TrueNas to sleep before perform other actions
|
|
||||||
_info "Waiting for Web UI restart."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
### ---- 7. FTP (optional) ----
|
|
||||||
|
|
||||||
if [ "$DEPLOY_TRUENAS_UPDATE_FTP" = "yes" ]; then
|
|
||||||
_info "Sending certicate for FTP service..."
|
|
||||||
_ftp_resp=$(_truenas_websocat_rpc_call "ftp.update" "[{\"ssltls_certificate\": ${_new_cert_id}}]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_ftp_resp" "ftp.update"; then
|
|
||||||
_err "Failed to update the FTP certificate."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
else
|
|
||||||
_ftp_certid=$(printf '%s' "$_ftp_resp" | "$_jq_bin" -r '.result.ssltls_certificate // empty')
|
|
||||||
if [ "$_ftp_certid" = "$_new_cert_id" ]; then
|
|
||||||
_info "FTP certificate updated."
|
|
||||||
else
|
|
||||||
_err "FTP certificate: unexpected response."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
### ---- 8. iX Apps (optional - redeploys every matching app) ----
|
|
||||||
|
|
||||||
if [ "$DEPLOY_TRUENAS_UPDATE_APPS" = "yes" ]; then
|
|
||||||
_info "Sending certicate for iX Apps..."
|
|
||||||
_apps_resp=$(_truenas_websocat_rpc_call "app.query" "[]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
if _truenas_websocat_rpc_has_error "$_apps_resp" "app.query"; then
|
|
||||||
_err "Could not list apps."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
else
|
|
||||||
for _app_name in $(printf '%s' "$_apps_resp" | "$_jq_bin" -r '.result[].name'); do
|
|
||||||
_app_cfg=$(_truenas_websocat_rpc_call "app.config" "[\"${_app_name}\"]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
_has_cert_opt=$(printf '%s' "$_app_cfg" | "$_jq_bin" -r '.result.network // {} | has("certificate_id")' 2>/dev/null)
|
|
||||||
if [ "$_has_cert_opt" = "true" ]; then
|
|
||||||
_info "Updating certificate for app '$_app_name' (this will redeploy it)..."
|
|
||||||
_app_update_resp=$(_truenas_websocat_rpc_call "app.update" "[\"${_app_name}\", {\"values\": {\"network\": {\"certificate_id\": ${_new_cert_id}}}}]") || {
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
_app_jobid=$(printf '%s' "$_app_update_resp" | "$_jq_bin" -r '.result // empty')
|
|
||||||
case "$_app_jobid" in
|
|
||||||
'' | *[!0-9]*)
|
|
||||||
_err "App '$_app_name': no job ID returned."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if ! _truenas_websocat_wait_for_job "$_app_jobid" >/dev/null; then
|
|
||||||
_err "App '$_app_name': update not confirmed."
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
return 4
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
### ---- 9. Delete the old certificate (non blocking) ----
|
|
||||||
|
|
||||||
if [ -n "$_old_cert_id" ] && [ "$_old_cert_id" != "$_new_cert_id" ] && [ "$_old_cert_id" != "null" ]; then
|
|
||||||
_del_resp=$(_truenas_websocat_rpc_call "certificate.delete" "[${_old_cert_id}]")
|
|
||||||
if ! _truenas_websocat_rpc_has_error "$_del_resp" "certificate.delete"; then
|
|
||||||
_del_jobid=$(printf '%s' "$_del_resp" | "$_jq_bin" -r '.result // empty')
|
|
||||||
case "$_del_jobid" in
|
|
||||||
'' | *[!0-9]*) : ;;
|
|
||||||
*)
|
|
||||||
_truenas_websocat_wait_for_job "$_del_jobid" >/dev/null || _info "Old certificate: deletion not confirmed ."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
_truenas_websocat_cleanup
|
|
||||||
_info "TrueNAS deployment completed successfully."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
+12
-12
@@ -43,15 +43,15 @@ _ws_call() {
|
|||||||
_debug "_ws_call arg1" "$1"
|
_debug "_ws_call arg1" "$1"
|
||||||
_debug "_ws_call arg2" "$2"
|
_debug "_ws_call arg2" "$2"
|
||||||
_debug "_ws_call arg3" "$3"
|
_debug "_ws_call arg3" "$3"
|
||||||
|
if [ $# -eq 3 ]; then
|
||||||
# TrueNAS 26.0.0-BETA3 and later need a --plain option for midclt call, detect if it is available
|
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
|
||||||
_midclt_plain=""
|
fi
|
||||||
case "$(midclt --help 2>/dev/null)" in
|
if [ $# -eq 2 ]; then
|
||||||
*--plain*) _midclt_plain="--plain" ;;
|
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
|
||||||
esac
|
fi
|
||||||
|
if [ $# -eq 1 ]; then
|
||||||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" $_midclt_plain call "$@")
|
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
|
||||||
|
fi
|
||||||
_debug "_ws_response" "$_ws_response"
|
_debug "_ws_response" "$_ws_response"
|
||||||
printf "%s" "$_ws_response"
|
printf "%s" "$_ws_response"
|
||||||
return 0
|
return 0
|
||||||
@@ -256,8 +256,8 @@ truenas_ws_deploy() {
|
|||||||
|
|
||||||
_info "Gather current WebUI certificate..."
|
_info "Gather current WebUI certificate..."
|
||||||
_ws_response="$(_ws_call "system.general.config")"
|
_ws_response="$(_ws_call "system.general.config")"
|
||||||
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '.ui_certificate | if type == "object" then .id else . end')
|
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
|
||||||
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r 'if (.ui_certificate | type) == "object" then .ui_certificate.name else .ui_certificate_name end')
|
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
|
||||||
_info "Current WebUI certificate ID: $_ui_certificate_id"
|
_info "Current WebUI certificate ID: $_ui_certificate_id"
|
||||||
_info "Current WebUI certificate name: $_ui_certificate_name"
|
_info "Current WebUI certificate name: $_ui_certificate_name"
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ truenas_ws_deploy() {
|
|||||||
|
|
||||||
_info "Replace WebUI certificate..."
|
_info "Replace WebUI certificate..."
|
||||||
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
|
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
|
||||||
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '.ui_certificate | if type == "object" then .id else . end')
|
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
|
||||||
if [ "$_changed_certid" != "$_new_certid" ]; then
|
if [ "$_changed_certid" != "$_new_certid" ]; then
|
||||||
_err "WebUI certificate change error.."
|
_err "WebUI certificate change error.."
|
||||||
return 5
|
return 5
|
||||||
|
|||||||
@@ -1,341 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
+10
-43
@@ -9,8 +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. Not saved, provide it on every run. Optional.
|
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
|
||||||
AZUREDNS_PRIVATEZONE Use Azure Private DNS Zones instead of Public DNS Zones. "true"/"false"
|
|
||||||
'
|
'
|
||||||
|
|
||||||
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
|
||||||
@@ -40,12 +39,6 @@ dns_azure_add() {
|
|||||||
#save subscription id to account conf file.
|
#save subscription id to account conf file.
|
||||||
_saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID"
|
_saveaccountconf_mutable AZUREDNS_SUBSCRIPTIONID "$AZUREDNS_SUBSCRIPTIONID"
|
||||||
|
|
||||||
AZUREDNS_PRIVATEZONE="${AZUREDNS_PRIVATEZONE:-$(_readaccountconf_mutable AZUREDNS_PRIVATEZONE)}"
|
|
||||||
if [ -n "$AZUREDNS_PRIVATEZONE" ]; then
|
|
||||||
#save public/private dns to account conf file.
|
|
||||||
_saveaccountconf_mutable AZUREDNS_PRIVATEZONE "$AZUREDNS_PRIVATEZONE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
|
AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
|
||||||
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
|
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
|
||||||
_info "Using Azure managed identity"
|
_info "Using Azure managed identity"
|
||||||
@@ -54,15 +47,13 @@ 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 ""
|
||||||
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
|
_saveaccountconf_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 is short-lived, so it is taken from the environment only and never
|
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
||||||
#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=""
|
||||||
@@ -102,7 +93,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"
|
||||||
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
|
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
@@ -119,9 +110,7 @@ dns_azure_add() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
_azure_set_zone_vars
|
acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01"
|
||||||
|
|
||||||
acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=$_azure_api_version"
|
|
||||||
_debug "$acmeRecordURI"
|
_debug "$acmeRecordURI"
|
||||||
# Get existing TXT record
|
# Get existing TXT record
|
||||||
_azure_rest GET "$acmeRecordURI" "" "$accesstoken"
|
_azure_rest GET "$acmeRecordURI" "" "$accesstoken"
|
||||||
@@ -147,7 +136,7 @@ dns_azure_add() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Add the txtvalue TXT Record
|
# Add the txtvalue TXT Record
|
||||||
body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"$_azure_ttl_key\":10, \"$_azure_txt_key\":[$values]}}"
|
body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}"
|
||||||
_azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
|
_azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
|
||||||
if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
|
if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
|
||||||
_info "validation value added"
|
_info "validation value added"
|
||||||
@@ -178,8 +167,6 @@ dns_azure_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
AZUREDNS_PRIVATEZONE="${AZUREDNS_PRIVATEZONE:-$(_readaccountconf_mutable AZUREDNS_PRIVATEZONE)}"
|
|
||||||
|
|
||||||
AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
|
AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
|
||||||
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
|
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
|
||||||
_info "Using Azure managed identity"
|
_info "Using Azure managed identity"
|
||||||
@@ -188,7 +175,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 comes from the environment only, see the note in dns_azure_add
|
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||||
AZUREDNS_SUBSCRIPTIONID=""
|
AZUREDNS_SUBSCRIPTIONID=""
|
||||||
@@ -238,11 +225,8 @@ dns_azure_rm() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
_azure_set_zone_vars
|
acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01"
|
||||||
|
|
||||||
acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=$_azure_api_version"
|
|
||||||
_debug "$acmeRecordURI"
|
_debug "$acmeRecordURI"
|
||||||
|
|
||||||
# Get existing TXT record
|
# Get existing TXT record
|
||||||
_azure_rest GET "$acmeRecordURI" "" "$accesstoken"
|
_azure_rest GET "$acmeRecordURI" "" "$accesstoken"
|
||||||
timestamp="$(_time)"
|
timestamp="$(_time)"
|
||||||
@@ -266,7 +250,7 @@ dns_azure_rm() {
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Remove only txtvalue from the TXT Record
|
# Remove only txtvalue from the TXT Record
|
||||||
body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"$_azure_ttl_key\":10, \"$_azure_txt_key\":[$values]}}"
|
body="{\"properties\":{\"metadata\":{\"acmetscheck\":\"$timestamp\"},\"TTL\":10, \"TXTRecords\":[$values]}}"
|
||||||
_azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
|
_azure_rest PUT "$acmeRecordURI" "$body" "$accesstoken"
|
||||||
if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
|
if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
|
||||||
_info "validation value removed"
|
_info "validation value removed"
|
||||||
@@ -397,21 +381,6 @@ _azure_getaccess_token() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
_azure_set_zone_vars() {
|
|
||||||
if [ "$AZUREDNS_PRIVATEZONE" = "true" ]; then
|
|
||||||
_azure_zone_type="privateDnsZones"
|
|
||||||
_azure_api_version="2024-06-01"
|
|
||||||
_azure_ttl_key="ttl"
|
|
||||||
_azure_txt_key="txtRecords"
|
|
||||||
_debug "Querying private DNS zone"
|
|
||||||
else
|
|
||||||
_azure_zone_type="dnszones"
|
|
||||||
_azure_api_version="2017-09-01"
|
|
||||||
_azure_ttl_key="TTL"
|
|
||||||
_azure_txt_key="TXTRecords"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_get_root() {
|
_get_root() {
|
||||||
domain=$1
|
domain=$1
|
||||||
subscriptionId=$2
|
subscriptionId=$2
|
||||||
@@ -419,8 +388,6 @@ _get_root() {
|
|||||||
i=1
|
i=1
|
||||||
p=1
|
p=1
|
||||||
|
|
||||||
_azure_set_zone_vars
|
|
||||||
|
|
||||||
## Ref: https://learn.microsoft.com/en-us/rest/api/dns/zones/list?view=rest-dns-2018-05-01&tabs=HTTP
|
## Ref: https://learn.microsoft.com/en-us/rest/api/dns/zones/list?view=rest-dns-2018-05-01&tabs=HTTP
|
||||||
## returns up to 100 zones in one response. Handling more results is not implemented
|
## returns up to 100 zones in one response. Handling more results is not implemented
|
||||||
## (ZoneListResult with continuation token for the next page of results)
|
## (ZoneListResult with continuation token for the next page of results)
|
||||||
@@ -429,7 +396,7 @@ _get_root() {
|
|||||||
## https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-dns-limits
|
## https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-dns-limits
|
||||||
## The new limit is 250 Public DNS zones per subscription, while the old limit was only 100
|
## The new limit is 250 Public DNS zones per subscription, while the old limit was only 100
|
||||||
##
|
##
|
||||||
_azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/$_azure_zone_type?\$top=500&api-version=$_azure_api_version" "" "$accesstoken"
|
_azure_rest GET "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Network/dnszones?\$top=500&api-version=2017-09-01" "" "$accesstoken"
|
||||||
# Find matching domain name in Json response
|
# Find matching domain name in Json response
|
||||||
while true; do
|
while true; do
|
||||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||||
|
|||||||
@@ -1,348 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
dns_bergdns_info='bergdns.at
|
|
||||||
Site: bergdns.at
|
|
||||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bergdns
|
|
||||||
Options:
|
|
||||||
BERGDNS_API_KEY API key, as issued in the account UI. Needs read (to find the zone and the record) and write over the challenge names.
|
|
||||||
BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1".
|
|
||||||
BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60".
|
|
||||||
BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "60". "0" does not wait.
|
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/7261
|
|
||||||
Author: Kenny Kropp <https://github.com/kekropp>
|
|
||||||
'
|
|
||||||
|
|
||||||
_BERGDNS_DEFAULT_URL='https://bergdns.at/v1'
|
|
||||||
_BERGDNS_DEFAULT_TTL='60'
|
|
||||||
_BERGDNS_DEFAULT_WAIT='60'
|
|
||||||
|
|
||||||
######## Public functions ####################################################
|
|
||||||
|
|
||||||
# Usage: dns_bergdns_add _acme-challenge.www.example.com "token"
|
|
||||||
# The value is added to the RRset, so a domain and its wildcard can be
|
|
||||||
# validated at the same time.
|
|
||||||
dns_bergdns_add() {
|
|
||||||
fulldomain=$(echo "$1" | _lower_case)
|
|
||||||
txtvalue=$2
|
|
||||||
|
|
||||||
_bergdns_init || return 1
|
|
||||||
_bergdns_find_zone "$fulldomain" || return 1
|
|
||||||
_bergdns_find_rrset "$fulldomain" || return 1
|
|
||||||
|
|
||||||
_info "Adding TXT $fulldomain in zone $_bergdns_zone_name"
|
|
||||||
if [ -z "$_bergdns_rrset_id" ]; then
|
|
||||||
if _bergdns_rest POST "zones/$_bergdns_zone_id/rrsets" \
|
|
||||||
"{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":$BERGDNS_TTL,\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
|
|
||||||
_bergdns_rrset_id=$(echo "$response" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
|
||||||
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
|
|
||||||
_bergdns_wait "$fulldomain"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ "$_bergdns_code" != "rrset_exists" ]; then
|
|
||||||
_err "bergdns: could not add the challenge record: $_bergdns_error"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# another run created it since the lookup above
|
|
||||||
_bergdns_find_rrset "$fulldomain" || return 1
|
|
||||||
if [ -z "$_bergdns_rrset_id" ]; then
|
|
||||||
_err "bergdns: could not find the challenge record at $fulldomain"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! _bergdns_rest POST \
|
|
||||||
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
|
|
||||||
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
|
|
||||||
_err "bergdns: could not add the challenge record: $_bergdns_error"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_bergdns_wait "$fulldomain"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: dns_bergdns_rm _acme-challenge.www.example.com "token"
|
|
||||||
# Only this value is removed. Removing the last value deletes the RRset, and
|
|
||||||
# removing a value that does not exist is not an error.
|
|
||||||
dns_bergdns_rm() {
|
|
||||||
fulldomain=$(echo "$1" | _lower_case)
|
|
||||||
txtvalue=$2
|
|
||||||
|
|
||||||
_bergdns_init || return 1
|
|
||||||
_bergdns_find_zone "$fulldomain" || return 1
|
|
||||||
_bergdns_find_rrset "$fulldomain" || return 1
|
|
||||||
|
|
||||||
if [ -z "$_bergdns_rrset_id" ]; then
|
|
||||||
_info "bergdns: no TXT records at $fulldomain, nothing to remove"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "Removing TXT $fulldomain from zone $_bergdns_zone_name"
|
|
||||||
if ! _bergdns_rest DELETE \
|
|
||||||
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
|
|
||||||
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
|
|
||||||
# Any flavour of not-found is a cleanup that has already happened: the
|
|
||||||
# RRset was removed by a previous run, or by the other half of a
|
|
||||||
# domain-and-wildcard pair taking the last value with it.
|
|
||||||
if [ "$_bergdns_status" = "404" ]; then
|
|
||||||
_info "bergdns: $fulldomain holds no such record any more, nothing to remove"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
_err "bergdns: could not remove the challenge record: $_bergdns_error"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
######## Private functions ###################################################
|
|
||||||
|
|
||||||
_bergdns_init() {
|
|
||||||
BERGDNS_API_KEY="${BERGDNS_API_KEY:-$(_readaccountconf_mutable BERGDNS_API_KEY)}"
|
|
||||||
BERGDNS_API_URL="${BERGDNS_API_URL:-$(_readaccountconf_mutable BERGDNS_API_URL)}"
|
|
||||||
BERGDNS_TTL="${BERGDNS_TTL:-$(_readaccountconf_mutable BERGDNS_TTL)}"
|
|
||||||
BERGDNS_PROPAGATION_TIMEOUT="${BERGDNS_PROPAGATION_TIMEOUT:-$(_readaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT)}"
|
|
||||||
|
|
||||||
if [ -z "$BERGDNS_API_KEY" ]; then
|
|
||||||
BERGDNS_API_KEY=""
|
|
||||||
_clearaccountconf_mutable BERGDNS_API_KEY
|
|
||||||
_err "You have not set BERGDNS_API_KEY. Create a key in the bergdns UI and export it:"
|
|
||||||
_err " export BERGDNS_API_KEY=\"bgd_...\""
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "$BERGDNS_API_URL" ] || BERGDNS_API_URL="$_BERGDNS_DEFAULT_URL"
|
|
||||||
[ -n "$BERGDNS_TTL" ] || BERGDNS_TTL="$_BERGDNS_DEFAULT_TTL"
|
|
||||||
[ -n "$BERGDNS_PROPAGATION_TIMEOUT" ] || BERGDNS_PROPAGATION_TIMEOUT="$_BERGDNS_DEFAULT_WAIT"
|
|
||||||
|
|
||||||
# strip trailing slashes
|
|
||||||
BERGDNS_API_URL=$(echo "$BERGDNS_API_URL" | sed 's#/*$##')
|
|
||||||
|
|
||||||
# The TTL is interpolated into the request body and the timeout is counted
|
|
||||||
# down in arithmetic, so a stray value from the environment or from an old
|
|
||||||
# account.conf has to be caught here rather than become malformed JSON and
|
|
||||||
# an opaque 400.
|
|
||||||
case "$BERGDNS_TTL" in
|
|
||||||
*[!0-9]* | '')
|
|
||||||
_err "bergdns: BERGDNS_TTL must be a number of seconds, not \"$BERGDNS_TTL\"."
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
case "$BERGDNS_PROPAGATION_TIMEOUT" in
|
|
||||||
*[!0-9]* | '')
|
|
||||||
_err "bergdns: BERGDNS_PROPAGATION_TIMEOUT must be a number of seconds, not \"$BERGDNS_PROPAGATION_TIMEOUT\"."
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
_saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY"
|
|
||||||
# Only what the user actually chose is written back, and a value equal to
|
|
||||||
# the default clears any older setting. Persisting a default would pin the
|
|
||||||
# install to today's value, and a later change to the shipped one -- a move
|
|
||||||
# of the API base above all -- would never reach it; leaving an old setting
|
|
||||||
# in place would mean the environment could never put one back to default.
|
|
||||||
if [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ]; then
|
|
||||||
_clearaccountconf_mutable BERGDNS_API_URL
|
|
||||||
else
|
|
||||||
_saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL"
|
|
||||||
fi
|
|
||||||
if [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ]; then
|
|
||||||
_clearaccountconf_mutable BERGDNS_TTL
|
|
||||||
else
|
|
||||||
_saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL"
|
|
||||||
fi
|
|
||||||
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ]; then
|
|
||||||
_clearaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT
|
|
||||||
else
|
|
||||||
_saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _bergdns_find_zone _acme-challenge.www.example.com
|
|
||||||
# Sets _bergdns_zone_id and _bergdns_zone_name.
|
|
||||||
# Zones are addressed by an id, not by name, so the zone list is fetched once
|
|
||||||
# and the longest matching zone name wins.
|
|
||||||
_bergdns_find_zone() {
|
|
||||||
_bergdns_fqdn=$1
|
|
||||||
_bergdns_zone_id=""
|
|
||||||
_bergdns_zone_name=""
|
|
||||||
|
|
||||||
if ! _bergdns_rest GET "zones"; then
|
|
||||||
_err "bergdns: could not list zones: $_bergdns_error"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# one zone object per line, so id and name stay together
|
|
||||||
_bergdns_zone_lines=$(echo "$response" | tr '{' '\n')
|
|
||||||
|
|
||||||
_bergdns_cand="$_bergdns_fqdn"
|
|
||||||
while [ -n "$_bergdns_cand" ]; do
|
|
||||||
_bergdns_line=$(echo "$_bergdns_zone_lines" | _bergdns_select "$_bergdns_cand" | _head_n 1)
|
|
||||||
if [ -n "$_bergdns_line" ]; then
|
|
||||||
_bergdns_zone_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
|
||||||
_bergdns_zone_name="$_bergdns_cand"
|
|
||||||
_debug _bergdns_zone_id "$_bergdns_zone_id"
|
|
||||||
_debug _bergdns_zone_name "$_bergdns_zone_name"
|
|
||||||
[ -n "$_bergdns_zone_id" ] && return 0
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
case "$_bergdns_cand" in
|
|
||||||
*.*) _bergdns_cand=${_bergdns_cand#*.} ;;
|
|
||||||
*) _bergdns_cand="" ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
_err "bergdns: no zone in this account holds $_bergdns_fqdn."
|
|
||||||
_err "bergdns: the key must be able to read the zone as well as write the record."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _bergdns_find_rrset _acme-challenge.www.example.com
|
|
||||||
# Sets _bergdns_rrset_id to the id of the TXT RRset at that name, or to an
|
|
||||||
# empty string if there is none. Records are addressed by id, so the zone's
|
|
||||||
# RRsets are listed to find it.
|
|
||||||
_bergdns_find_rrset() {
|
|
||||||
_bergdns_fqdn=$1
|
|
||||||
_bergdns_rrset_id=""
|
|
||||||
|
|
||||||
if ! _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets"; then
|
|
||||||
_err "bergdns: could not list the records of $_bergdns_zone_name: $_bergdns_error"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_bergdns_line=$(echo "$response" | tr '{' '\n' | _bergdns_select "$_bergdns_fqdn" TXT | _head_n 1)
|
|
||||||
if [ -n "$_bergdns_line" ]; then
|
|
||||||
_bergdns_rrset_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
|
||||||
fi
|
|
||||||
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: ... | _bergdns_select name [type]
|
|
||||||
# Reads one JSON object per line and prints those whose "name" is name and,
|
|
||||||
# when a type is given, whose "type" is that type. The two fields are matched
|
|
||||||
# one at a time, so neither the order the server writes its keys in nor
|
|
||||||
# anything sitting between them changes the answer.
|
|
||||||
#
|
|
||||||
# The comparison is a shell case, which is literal by construction: grep -F
|
|
||||||
# does not exist on Solaris, and _contains and _startswith would read the name
|
|
||||||
# as a regular expression. Each pattern anchors on the start of the object or
|
|
||||||
# on the comma before the key, so a key that merely ends in "name" cannot
|
|
||||||
# match.
|
|
||||||
_bergdns_select() {
|
|
||||||
_bergdns_sel_name=$1
|
|
||||||
_bergdns_sel_type=$2
|
|
||||||
while IFS= read -r _bergdns_sel_line || [ -n "$_bergdns_sel_line" ]; do
|
|
||||||
case "$_bergdns_sel_line" in
|
|
||||||
'"name":"'"$_bergdns_sel_name"'"'* | *',"name":"'"$_bergdns_sel_name"'"'*) ;;
|
|
||||||
*) continue ;;
|
|
||||||
esac
|
|
||||||
if [ -n "$_bergdns_sel_type" ]; then
|
|
||||||
case "$_bergdns_sel_line" in
|
|
||||||
'"type":"'"$_bergdns_sel_type"'"'* | *',"type":"'"$_bergdns_sel_type"'"'*) ;;
|
|
||||||
*) continue ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
printf '%s\n' "$_bergdns_sel_line"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _bergdns_wait _acme-challenge.www.example.com
|
|
||||||
# Polls the propagation endpoint until all bergdns nameservers serve the
|
|
||||||
# record. A timeout is logged but does not fail the issuance.
|
|
||||||
#
|
|
||||||
# This covers the zone transfer from the primary to the secondaries, which
|
|
||||||
# takes seconds; the resolver side is acme.sh's own _check_dns_entries, which
|
|
||||||
# runs after every record has been added and has a timeout of its own.
|
|
||||||
_bergdns_wait() {
|
|
||||||
_bergdns_fqdn=$1
|
|
||||||
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "0" ] || [ -z "$_bergdns_rrset_id" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
_bergdns_waited=0
|
|
||||||
while [ "$_bergdns_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do
|
|
||||||
if _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/propagation"; then
|
|
||||||
case "$response" in
|
|
||||||
*'"propagated":true'*)
|
|
||||||
_info "bergdns: $_bergdns_fqdn is served by every secondary after ${_bergdns_waited}s"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
elif [ "$_bergdns_code" = "propagation_unavailable" ]; then
|
|
||||||
# propagation checks are not configured on this server
|
|
||||||
_info "bergdns: this deployment does not offer propagation checks; not waiting"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
case "$_bergdns_status" in
|
|
||||||
429) ;; # rate limited, worth another go
|
|
||||||
4*)
|
|
||||||
# The check is refused rather than pending, and waiting will not
|
|
||||||
# change that. _check_dns_entries still has to pass, so this is not
|
|
||||||
# the place to fail the issuance.
|
|
||||||
_info "bergdns: the propagation check is unavailable ($_bergdns_error); not waiting"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
_sleep 5
|
|
||||||
_bergdns_waited=$((_bergdns_waited + 5))
|
|
||||||
done
|
|
||||||
|
|
||||||
_info "bergdns: $_bergdns_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _bergdns_rest method endpoint [body]
|
|
||||||
# Sets response and _bergdns_status. On failure also sets _bergdns_error and,
|
|
||||||
# where the API itself answered, _bergdns_code.
|
|
||||||
_bergdns_rest() {
|
|
||||||
_bergdns_method=$1
|
|
||||||
_bergdns_endpoint=$2
|
|
||||||
_bergdns_body=$3
|
|
||||||
_bergdns_error=""
|
|
||||||
_bergdns_code=""
|
|
||||||
_bergdns_status=""
|
|
||||||
|
|
||||||
export _H1="Authorization: Bearer $BERGDNS_API_KEY"
|
|
||||||
export _H2="Accept: application/json"
|
|
||||||
|
|
||||||
_bergdns_url="$BERGDNS_API_URL/$_bergdns_endpoint"
|
|
||||||
_debug _bergdns_url "$_bergdns_url"
|
|
||||||
|
|
||||||
# drop the headers of the previous request, so that a request which never
|
|
||||||
# reaches the server cannot be read as carrying its status
|
|
||||||
if [ -f "$HTTP_HEADER" ]; then
|
|
||||||
: >"$HTTP_HEADER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_bergdns_method" = "GET" ]; then
|
|
||||||
response="$(_get "$_bergdns_url")"
|
|
||||||
else
|
|
||||||
_debug2 _bergdns_body "$_bergdns_body"
|
|
||||||
response="$(_post "$_bergdns_body" "$_bergdns_url" "" "$_bergdns_method" "application/json")"
|
|
||||||
fi
|
|
||||||
_bergdns_ret="$?"
|
|
||||||
_debug2 response "$response"
|
|
||||||
|
|
||||||
if [ "$_bergdns_ret" != "0" ]; then
|
|
||||||
_bergdns_error="the request to $_bergdns_url could not be made"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_bergdns_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
|
||||||
_debug _bergdns_status "$_bergdns_status"
|
|
||||||
|
|
||||||
# The HTTP status decides. Errors from the API itself are RFC 9457
|
|
||||||
# problem+json and carry a "detail" to show and a stable "code" to branch on,
|
|
||||||
# but a request that never gets that far -- bergdns.at answers from behind a
|
|
||||||
# reverse proxy, whose 502 and 504 are HTML -- has neither, and reading the
|
|
||||||
# body alone would take those for success.
|
|
||||||
case "$_bergdns_status" in
|
|
||||||
2*) return 0 ;;
|
|
||||||
esac
|
|
||||||
_bergdns_error=$(echo "$response" | _egrep_o '"detail":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
|
||||||
_bergdns_code=$(echo "$response" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
|
||||||
[ -n "$_bergdns_error" ] || _bergdns_error="$_bergdns_url answered HTTP ${_bergdns_status:-(none)}"
|
|
||||||
_debug _bergdns_code "$_bergdns_code"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
@@ -323,21 +323,21 @@ _bhosted_extract_id() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# JSON: "id":12345
|
# JSON: "id":12345
|
||||||
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[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 '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[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[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
_id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*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\"[ ]*:[ ]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[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\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | 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\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
_domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | 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\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
message=$(echo "$response" | _egrep_o "\"message\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
||||||
_err "Error: $message"
|
_err "Error: $message"
|
||||||
_err "URI:$uri"
|
_err "URI:$uri"
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
+3
-3
@@ -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}")"
|
list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
|
||||||
_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 "%s\n" "${list_txt_response}" | _egrep_o 'data-hash=\\"[^"]*\\" data-identifier=\\"[^"]*\\"' | sed 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\"/\1 \2/')"
|
_dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')"
|
||||||
|
|
||||||
printf "%s\n" "${_dns_entries}" | while read -r _hash _identifier; do
|
printf "%s" "${_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,9 +30,8 @@ dns_czechia_add() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_czechia_tab="$(printf '\t')"
|
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
||||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
|
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||||
_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."
|
||||||
@@ -109,9 +108,8 @@ dns_czechia_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_czechia_tab="$(printf '\t')"
|
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
||||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
|
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||||
_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."
|
||||||
@@ -182,13 +180,12 @@ _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/[ $_czechia_pz_tab]//g; s/\.\$//")
|
_clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
||||||
[ -z "$_clean_z" ] && continue
|
[ -z "$_clean_z" ] && continue
|
||||||
|
|
||||||
case "$_fd" in
|
case "$_fd" in
|
||||||
|
|||||||
@@ -1,243 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
dns_dnsmint_info='DNSMint.com
|
|
||||||
DNSMint mints hostnames on domains it operates and serves from its own
|
|
||||||
authoritative nameservers, so records are published through its API rather
|
|
||||||
than a zone you run.
|
|
||||||
Site: dnsmint.com
|
|
||||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsmint
|
|
||||||
Options:
|
|
||||||
DNSMINT_API_KEY API key. dns01:write is enough to issue certificates.
|
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/7251
|
|
||||||
Author: DNSMint
|
|
||||||
'
|
|
||||||
|
|
||||||
DNSMint_Api="${DNSMint_Api:-https://dnsmint.com/api}"
|
|
||||||
|
|
||||||
######## Public functions #####################
|
|
||||||
|
|
||||||
#Usage: dns_dnsmint_add _acme-challenge.q7k4m2.example.dev "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
||||||
dns_dnsmint_add() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
|
|
||||||
_info "Using DNSMint"
|
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _dnsmint_key; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# An ACME challenge goes to the DNS-01 endpoint, which derives the hostname
|
|
||||||
# itself and needs only dns01:write. Any other name is an ordinary record
|
|
||||||
# under a hostname, which is a different endpoint and a wider scope.
|
|
||||||
if _startswith "$fulldomain" "_acme-challenge."; then
|
|
||||||
if _dnsmint_challenge present "$fulldomain" "$txtvalue"; then
|
|
||||||
_info "Added, OK"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if _dnsmint_record_add "$fulldomain" "$txtvalue"; then
|
|
||||||
_info "Added, OK"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#Usage: dns_dnsmint_rm _acme-challenge.q7k4m2.example.dev "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
||||||
dns_dnsmint_rm() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
|
|
||||||
_info "Using DNSMint"
|
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _dnsmint_key; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if _startswith "$fulldomain" "_acme-challenge."; then
|
|
||||||
if _dnsmint_challenge cleanup "$fulldomain" "$txtvalue"; then
|
|
||||||
_info "Removed, OK"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if _dnsmint_record_rm "$fulldomain" "$txtvalue"; then
|
|
||||||
_info "Removed, OK"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
|
||||||
|
|
||||||
_dnsmint_key() {
|
|
||||||
DNSMINT_API_KEY="${DNSMINT_API_KEY:-$(_readaccountconf_mutable DNSMINT_API_KEY)}"
|
|
||||||
if [ -z "$DNSMINT_API_KEY" ]; then
|
|
||||||
DNSMINT_API_KEY=""
|
|
||||||
_err "You did not specify DNSMINT_API_KEY yet."
|
|
||||||
_err "Create a key with the dns01:write scope at https://dnsmint.com/dashboard"
|
|
||||||
_err "e.g."
|
|
||||||
_err "export DNSMINT_API_KEY=dnsm_xxxxxxxxxxxx_xxxxxxxx"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_saveaccountconf_mutable DNSMINT_API_KEY "$DNSMINT_API_KEY"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_dnsmint_headers() {
|
|
||||||
export _H1="Authorization: Bearer $DNSMINT_API_KEY"
|
|
||||||
export _H2="Accept: application/json"
|
|
||||||
export _H3="Content-Type: application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
# One request. Sets $response and $_code; returns non-zero on a transport error.
|
|
||||||
_dnsmint_rest() {
|
|
||||||
_m="$1"
|
|
||||||
_ep="$2"
|
|
||||||
_data="$3"
|
|
||||||
|
|
||||||
_dnsmint_headers
|
|
||||||
if [ "$_m" = "GET" ]; then
|
|
||||||
response="$(_get "$DNSMint_Api$_ep")"
|
|
||||||
else
|
|
||||||
_secure_debug2 _data "$_data"
|
|
||||||
response="$(_post "$_data" "$DNSMint_Api$_ep" "" "$_m")"
|
|
||||||
fi
|
|
||||||
_ret="$?"
|
|
||||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
|
||||||
_debug "http response code $_code"
|
|
||||||
_debug2 response "$response"
|
|
||||||
if [ "$_ret" != "0" ]; then
|
|
||||||
_err "error $_ep"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
case "$_code" in
|
|
||||||
2*) return 0 ;;
|
|
||||||
*)
|
|
||||||
# The API says why in the body - a key narrowed to another hostname, a
|
|
||||||
# name that is not live - and that is more use than the status alone.
|
|
||||||
_err "error $_ep: HTTP $_code $response"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# The DNS-01 endpoint. It derives the hostname from the challenge name, so
|
|
||||||
# there is no zone to look up and no record id to track: the value published
|
|
||||||
# is the value removed.
|
|
||||||
_dnsmint_challenge() {
|
|
||||||
_action="$1"
|
|
||||||
_fqdn="$2"
|
|
||||||
_value="$3"
|
|
||||||
_dnsmint_rest POST "/httpreq/$_action" "{\"fqdn\":\"$_fqdn\",\"value\":\"$_value\"}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Everything below here is for names that are not ACME challenges. A record
|
|
||||||
# under a hostname is addressed by the hostname's id and a name relative to
|
|
||||||
# it, so the hostname has to be found first.
|
|
||||||
_dnsmint_host() {
|
|
||||||
_name="$1"
|
|
||||||
_host_id=""
|
|
||||||
_host_sub=""
|
|
||||||
|
|
||||||
if ! _dnsmint_rest GET "/v1/hostnames?limit=500"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for _h in $(echo "$response" | _egrep_o '"hostname":"[^"]*"' | cut -d'"' -f4); do
|
|
||||||
case "$_name" in
|
|
||||||
*".$_h")
|
|
||||||
# Longest suffix wins, so a.b.example.dev prefers b.example.dev over
|
|
||||||
# example.dev when both are hostnames on the account.
|
|
||||||
if [ "${#_h}" -gt "${#_host_sub}" ]; then
|
|
||||||
_host_sub="$_h"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$_host_sub" ]; then
|
|
||||||
_err "$_name is not under a hostname on this account"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# The id sits next to the hostname in the same object.
|
|
||||||
_host_id="$(echo "$response" | _egrep_o "\"id\":\"[^\"]*\",\"hostname\":\"$_host_sub\"" | cut -d'"' -f4)"
|
|
||||||
if [ -z "$_host_id" ]; then
|
|
||||||
_err "could not read the id for $_host_sub"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_record_name="${_name%".$_host_sub"}"
|
|
||||||
_debug _host_sub "$_host_sub"
|
|
||||||
_debug _record_name "$_record_name"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_dnsmint_record_add() {
|
|
||||||
_name="$1"
|
|
||||||
_value="$2"
|
|
||||||
|
|
||||||
if ! _dnsmint_host "$_name"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_dnsmint_rest POST "/v1/hostnames/$_host_id/records" \
|
|
||||||
"{\"name\":\"$_record_name\",\"type\":\"TXT\",\"text\":\"$_value\"}"
|
|
||||||
}
|
|
||||||
|
|
||||||
_dnsmint_record_rm() {
|
|
||||||
_name="$1"
|
|
||||||
_value="$2"
|
|
||||||
|
|
||||||
if ! _dnsmint_host "$_name"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! _dnsmint_rest GET "/v1/hostnames/$_host_id/records"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Records come back as {"id":...,"name":"<fqdn>","type":"TXT","ttl":...,
|
|
||||||
# "data":{...,"text":["<value>"]}}. Match on the value so a name holding
|
|
||||||
# several TXT records loses only the one that was added.
|
|
||||||
#
|
|
||||||
# The replacement carries a literal newline: "\n" there is a GNU extension
|
|
||||||
# and BSD sed inserts the letter n, which would leave the whole reply on one
|
|
||||||
# line and match the first record under the hostname whatever its value.
|
|
||||||
#
|
|
||||||
# echo rather than printf "%s": the reply arrives with no trailing newline,
|
|
||||||
# and Solaris /usr/bin/sed discards an incomplete final line. Here that line
|
|
||||||
# is the entire reply, so every removal would report the record already gone.
|
|
||||||
_records="$(
|
|
||||||
echo "$response" | sed 's/},{/}\
|
|
||||||
{/g'
|
|
||||||
)"
|
|
||||||
|
|
||||||
_rid=""
|
|
||||||
while IFS= read -r _line; do
|
|
||||||
case "$_line" in
|
|
||||||
*"\"$_value\""*)
|
|
||||||
# _head_n 1 because a record object may carry a nested id under "data",
|
|
||||||
# and two lines in _rid would break the DELETE URL.
|
|
||||||
_rid="$(echo "$_line" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4 | _head_n 1)"
|
|
||||||
if [ -n "$_rid" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <<EOF
|
|
||||||
$_records
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ -z "$_rid" ]; then
|
|
||||||
_info "Record already gone, nothing to remove"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
_dnsmint_rest DELETE "/v1/hostnames/$_host_id/records/$_rid" ""
|
|
||||||
}
|
|
||||||
@@ -150,9 +150,6 @@ _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"
|
||||||
@@ -171,9 +168,6 @@ _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
|
||||||
|
|||||||
+15
-17
@@ -75,11 +75,6 @@ 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"
|
||||||
@@ -96,21 +91,24 @@ dns_easydns_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
record_id=$(printf "%s\n" "$response" | tr '{' '\n' | grep "\"rdata\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
|
count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
|
||||||
_debug "record_id" "$record_id"
|
_debug count "$count"
|
||||||
|
if [ "$count" = "0" ]; then
|
||||||
if [ -z "$record_id" ]; then
|
|
||||||
_info "Don't need to remove."
|
_info "Don't need to remove."
|
||||||
return 0
|
else
|
||||||
|
record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
|
||||||
|
_debug "record_id" "$record_id"
|
||||||
|
if [ -z "$record_id" ]; then
|
||||||
|
_err "Can not get record id to remove."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
|
||||||
|
_err "Delete record error."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
_contains "$response" "\"status\":200"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
|
|
||||||
_err "Delete record error."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_contains "$response" "\"status\":200"
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
#################### Private functions below ##################################
|
||||||
|
|||||||
+9
-20
@@ -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, ExtremeFiretop
|
Author: Recolic Keghart <root@recolic.net>, @Giova96
|
||||||
'
|
'
|
||||||
|
|
||||||
FREEMYIP_DNS_API="https://freemyip.com/update?"
|
FREEMYIP_DNS_API="https://freemyip.com/update?"
|
||||||
@@ -68,30 +68,22 @@ dns_freemyip_rm() {
|
|||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
################ Private functions below ################
|
################ Private functions below ################
|
||||||
_get_root() {
|
_get_root() {
|
||||||
_fmi_d="$1"
|
_fmi_d="$1"
|
||||||
|
|
||||||
echo "$_fmi_d" | sed 's/.*\.\([^.]*\.[^.]*\.[^.]*\)$/\1/'
|
echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
|
||||||
}
|
}
|
||||||
|
|
||||||
# There is random failure while calling freemyip API too fast. This function automatically retry until success.
|
# 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"
|
||||||
_fmi_i=1
|
for i in $(seq 1 8); do
|
||||||
while [ "$_fmi_i" -le 8 ]; do
|
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
|
||||||
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..."
|
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
|
||||||
_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. Server does not say 'OK'"
|
_err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,16 +93,13 @@ _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"
|
||||||
_fmi_i=1
|
for i in $(seq 1 3); do
|
||||||
while [ "$_fmi_i" -le 3 ]; do
|
_debug "'$_webroot' ns lookup, retry $i/3..."
|
||||||
_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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
#!/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\"[ ]*:[ ]*\"[^\"]*\"" | _head_n 1)"
|
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | _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/^[ ]*"//' |
|
sed 's/^[[:space:]]*"//' |
|
||||||
sed 's/"[ ]*$//' |
|
sed 's/"[[:space:]]*$//' |
|
||||||
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\"[ ]*:[ ]*[^,}]*" | _head_n 1)"
|
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | _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- |
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
#!/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="limit=1&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=$(echo "${_hw_domain}." | _lower_case)
|
|
||||||
_hw_returned_name=$(echo "$_hw_returned_name" | _lower_case)
|
|
||||||
if [ "$_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":[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _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":[ ]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"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"
|
||||||
|
|||||||
+40
-48
@@ -7,23 +7,22 @@ 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/7202
|
Issues: github.com/acmesh-official/acme.sh/issues/2388
|
||||||
Author: @skysaint
|
|
||||||
'
|
'
|
||||||
|
|
||||||
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
|
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
|
||||||
|
|
||||||
_JD_PROD="domainservice"
|
_JD_PROD="clouddnsservice"
|
||||||
_JD_API="jdcloud-api.com"
|
_JD_API="jdcloud-api.com"
|
||||||
|
|
||||||
_JD_API_VERSION="v2"
|
_JD_API_VERSION="v1"
|
||||||
_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_jd_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
dns_jd_add() {
|
dns_jd_add() {
|
||||||
fulldomain=$1
|
fulldomain=$1
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
@@ -59,14 +58,24 @@ dns_jd_add() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
#_debug "Getting describeViewTree"
|
#_debug "Getting getViewTree"
|
||||||
|
|
||||||
_debug "Adding records"
|
_debug "Adding records"
|
||||||
|
|
||||||
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1}}"
|
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1},\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
|
||||||
#_addrr='{"req":{"hostRecord":"_acme-challenge","hostValue":"XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs","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"}'
|
||||||
if jd_rest POST "domain/$_domain_id/ResourceRecord" "" "$_addrr"; then
|
if jd_rest POST "domain/$_domain_id/RRAdd" "" "$_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
|
||||||
|
|
||||||
@@ -88,7 +97,14 @@ dns_jd_rm() {
|
|||||||
|
|
||||||
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
|
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
|
||||||
|
|
||||||
_info "Removing TXT record for $fulldomain"
|
_info "Getting existing records 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
|
||||||
@@ -99,37 +115,16 @@ dns_jd_rm() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
# List records, filter by hostRecord and use a large pageSize so it isn't missed on record-heavy zones.
|
_cleardomainconf JD_CLOUD_RIDS
|
||||||
if ! jd_rest GET "domain/$_domain_id/ResourceRecord" "pageSize=50&search=$_sub_domain"; then
|
|
||||||
_err "Failed to list resource records"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Match record by hostRecord + type TXT + hostValue
|
_aws_tmpl_xml="{\"ids\":[$_srid],\"action\":\"del\",\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
|
||||||
_record_id=""
|
|
||||||
_matched="$(echo "$response" | tr '{' '\n' | grep "\"hostRecord\":\"$_sub_domain\"" | grep "\"type\":\"TXT\"" | grep "\"hostValue\":\"$txtvalue\"")"
|
|
||||||
_debug2 _matched "$_matched"
|
|
||||||
|
|
||||||
if [ -z "$_matched" ]; then
|
if jd_rest POST "domain/$_domain_id/RROperate" "" "$_aws_tmpl_xml" && _contains "$response" "\"code\":\"OK\""; 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
|
||||||
|
|
||||||
_err "Failed to delete TXT record."
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
#################### Private functions below ##################################
|
||||||
@@ -139,14 +134,13 @@ _get_root() {
|
|||||||
i=1
|
i=1
|
||||||
p=1
|
p=1
|
||||||
|
|
||||||
if ! jd_rest GET "domain"; then
|
|
||||||
_err "error get domain list"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||||
_debug2 "Checking domain: $h"
|
_debug2 "Checking domain: $h"
|
||||||
|
if ! jd_rest GET "domain"; then
|
||||||
|
_err "error get domain list"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
if [ -z "$h" ]; then
|
if [ -z "$h" ]; then
|
||||||
#not valid
|
#not valid
|
||||||
_err "Invalid domain"
|
_err "Invalid domain"
|
||||||
@@ -174,8 +168,6 @@ _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"
|
||||||
@@ -228,7 +220,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 '%b' "$CanonicalRequest" | _digest "$Hash" hex)"
|
HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)"
|
||||||
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
||||||
|
|
||||||
Algorithm="JDCLOUD2-HMAC-SHA256"
|
Algorithm="JDCLOUD2-HMAC-SHA256"
|
||||||
@@ -254,19 +246,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 '%s' "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)"
|
kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)"
|
||||||
_debug2 kDateH "$kDateH"
|
_debug2 kDateH "$kDateH"
|
||||||
|
|
||||||
kRegionH="$(printf '%s' "$Region" | _hmac "$Hash" "$kDateH" hex)"
|
kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)"
|
||||||
_debug2 kRegionH "$kRegionH"
|
_debug2 kRegionH "$kRegionH"
|
||||||
|
|
||||||
kServiceH="$(printf '%s' "$Service" | _hmac "$Hash" "$kRegionH" hex)"
|
kServiceH="$(printf "$Service%s" | _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 '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)"
|
signature="$(printf "$StringToSign%s" | _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"
|
||||||
|
|||||||
Executable
+189
@@ -0,0 +1,189 @@
|
|||||||
|
#!/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
|
||||||
|
}
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
+1
-27
@@ -104,9 +104,6 @@ _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
|
||||||
|
|
||||||
@@ -123,7 +120,7 @@ _get_root_by_getList() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! _namecheap_is_our_dns "$h"; then
|
if ! _contains "$response" "$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")
|
||||||
@@ -136,29 +133,6 @@ _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
|
||||||
|
|||||||
+22
-377
@@ -5,324 +5,37 @@ Domains: netcup.de netcup.net
|
|||||||
Site: netcup.eu/
|
Site: netcup.eu/
|
||||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_netcup
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_netcup
|
||||||
Options:
|
Options:
|
||||||
NC_Apikey API Key. The new netcup REST API key (64 characters) or the legacy CCP API key
|
NC_Apikey API Key
|
||||||
NC_Apipw API Password. Only used for the legacy CCP API
|
NC_Apipw API Password
|
||||||
NC_CID Customer Number. Only used for the legacy CCP API
|
NC_CID Customer Number
|
||||||
NC_Apikey_Legacy Legacy CCP API Key. Only used for domains not manageable via the REST API when NC_Apikey holds a new netcup REST API key. Optional.
|
|
||||||
Author: linux-insideDE
|
Author: linux-insideDE
|
||||||
'
|
'
|
||||||
|
|
||||||
NC_Apikey="${NC_Apikey:-$(_readaccountconf_mutable NC_Apikey)}"
|
NC_Apikey="${NC_Apikey:-$(_readaccountconf_mutable NC_Apikey)}"
|
||||||
NC_Apipw="${NC_Apipw:-$(_readaccountconf_mutable NC_Apipw)}"
|
NC_Apipw="${NC_Apipw:-$(_readaccountconf_mutable NC_Apipw)}"
|
||||||
NC_CID="${NC_CID:-$(_readaccountconf_mutable NC_CID)}"
|
NC_CID="${NC_CID:-$(_readaccountconf_mutable NC_CID)}"
|
||||||
NC_Apikey_Legacy="${NC_Apikey_Legacy:-$(_readaccountconf_mutable NC_Apikey_Legacy)}"
|
|
||||||
end="https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
|
end="https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON"
|
||||||
_nc_endrest="https://api.netcup.com/v1"
|
|
||||||
client=""
|
client=""
|
||||||
|
|
||||||
dns_netcup_add() {
|
dns_netcup_add() {
|
||||||
fulldomain=$1
|
_debug NC_Apikey "$NC_Apikey"
|
||||||
txtvalue=$2
|
_login
|
||||||
_debug fulldomain "$fulldomain"
|
if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then
|
||||||
_debug txtvalue "$txtvalue"
|
_err "No Credentials given"
|
||||||
|
|
||||||
if ! _nc_check_credentials; then
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
_saveaccountconf_mutable NC_Apikey "$NC_Apikey"
|
_saveaccountconf_mutable NC_Apikey "$NC_Apikey"
|
||||||
if [ -n "$NC_Apipw" ]; then
|
_saveaccountconf_mutable NC_Apipw "$NC_Apipw"
|
||||||
_saveaccountconf_mutable NC_Apipw "$NC_Apipw"
|
_saveaccountconf_mutable NC_CID "$NC_CID"
|
||||||
fi
|
|
||||||
if [ -n "$NC_CID" ]; then
|
|
||||||
_saveaccountconf_mutable NC_CID "$NC_CID"
|
|
||||||
fi
|
|
||||||
if [ -n "$NC_Apikey_Legacy" ]; then
|
|
||||||
_saveaccountconf_mutable NC_Apikey_Legacy "$NC_Apikey_Legacy"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if _nc_is_rest_key; then
|
|
||||||
_nc_rest_add "$fulldomain" "$txtvalue"
|
|
||||||
else
|
|
||||||
_nc_apikey="$NC_Apikey"
|
|
||||||
_nc_legacy_add "$fulldomain" "$txtvalue"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
dns_netcup_rm() {
|
|
||||||
fulldomain=$1
|
fulldomain=$1
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _nc_check_credentials; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if _nc_is_rest_key; then
|
|
||||||
_nc_rest_rm "$fulldomain" "$txtvalue"
|
|
||||||
else
|
|
||||||
_nc_apikey="$NC_Apikey"
|
|
||||||
_nc_legacy_rm "$fulldomain" "$txtvalue"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
#################### New netcup REST API (api.netcup.com) ####################
|
|
||||||
|
|
||||||
_nc_rest_add() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
|
|
||||||
if ! _nc_rest_get_domain "$fulldomain"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug _domain_id "$_domain_id"
|
|
||||||
_debug _dns_managed "$_dns_managed"
|
|
||||||
|
|
||||||
if [ "$_dns_managed" = "false" ]; then
|
|
||||||
_nc_rest_use_legacy "$_domain" || return 1
|
|
||||||
_nc_legacy_add "$fulldomain" "$txtvalue"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$_dns_managed" != "true" ]; then
|
|
||||||
_err "Unable to read isDnsManaged for $_domain from the netcup REST API response: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$fulldomain" in
|
|
||||||
_acme-challenge.*) ;;
|
|
||||||
acmetestXyzRandomName.*)
|
|
||||||
# The synthetic record of the DNS-API-Test, which expects add and
|
|
||||||
# rm to succeed. The REST API can only manage _acme-challenge
|
|
||||||
# records, so skip it. Real records are never treated as a no-op.
|
|
||||||
_info "Skipping the DNS-API-Test record $fulldomain, the netcup REST API can only manage _acme-challenge records."
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# e.g. a challenge alias given in the "=" form without the prefix
|
|
||||||
if [ -n "$NC_Apikey_Legacy" ] && [ -n "$NC_Apipw" ] && [ -n "$NC_CID" ]; then
|
|
||||||
_debug "The netcup REST API can only create _acme-challenge records, using the legacy CCP API for $fulldomain"
|
|
||||||
_nc_apikey="$NC_Apikey_Legacy"
|
|
||||||
_nc_legacy_add "$fulldomain" "$txtvalue"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
_err "The netcup REST API can only create _acme-challenge records, unable to create $fulldomain."
|
|
||||||
_err "Set NC_Apikey_Legacy, NC_Apipw and NC_CID to manage it via the legacy CCP API."
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
_nc_rest_get_scope "$fulldomain" "$_domain"
|
|
||||||
_debug _scope "$_scope"
|
|
||||||
|
|
||||||
if ! _nc_rest POST "domain/$_domain_id/acme/challenge" "{\"scope\": \"$_scope\", \"value\": \"$txtvalue\"}" ||
|
|
||||||
! _contains "$response" '"success": *true'; then
|
|
||||||
_err "Unable to add the challenge record: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# The challenge record is added to the zone right away, but deploying
|
|
||||||
# the zone to the nameservers happens in the background, so poll until
|
|
||||||
# the record has actually been deployed (up to about 60 seconds).
|
|
||||||
_nc_tries=0
|
|
||||||
while true; do
|
|
||||||
if _nc_rest GET "domain/$_domain_id/acme/challenge/$_scope/$txtvalue" &&
|
|
||||||
_contains "$response" '"status": *"deployed"'; then
|
|
||||||
_info "The challenge record has been deployed"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
_nc_tries=$(_math "$_nc_tries" + 1)
|
|
||||||
if [ "$_nc_tries" -ge 12 ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
_debug "The challenge record has not been deployed yet, waiting 5 more seconds"
|
|
||||||
_sleep 5
|
|
||||||
done
|
|
||||||
_info "The challenge record has still not been deployed after 60 seconds, continuing anyway"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
_nc_rest_rm() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
|
|
||||||
if ! _nc_rest_get_domain "$fulldomain"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_dns_managed" = "false" ]; then
|
|
||||||
_nc_rest_use_legacy "$_domain" || return 1
|
|
||||||
_nc_legacy_rm "$fulldomain" "$txtvalue"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
if [ "$_dns_managed" != "true" ]; then
|
|
||||||
_err "Unable to read isDnsManaged for $_domain from the netcup REST API response: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$fulldomain" in
|
|
||||||
_acme-challenge.*) ;;
|
|
||||||
acmetestXyzRandomName.*)
|
|
||||||
# See _nc_rest_add.
|
|
||||||
_info "Skipping the DNS-API-Test record $fulldomain, the netcup REST API can only manage _acme-challenge records."
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [ -n "$NC_Apikey_Legacy" ] && [ -n "$NC_Apipw" ] && [ -n "$NC_CID" ]; then
|
|
||||||
_debug "The netcup REST API can only remove _acme-challenge records, using the legacy CCP API for $fulldomain"
|
|
||||||
_nc_apikey="$NC_Apikey_Legacy"
|
|
||||||
_nc_legacy_rm "$fulldomain" "$txtvalue"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
_err "The netcup REST API can only remove _acme-challenge records, unable to remove $fulldomain."
|
|
||||||
_err "Set NC_Apikey_Legacy, NC_Apipw and NC_CID to manage it via the legacy CCP API."
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
_nc_rest_get_scope "$fulldomain" "$_domain"
|
|
||||||
|
|
||||||
if ! _nc_rest DELETE "domain/$_domain_id/acme/challenge/$_scope/$txtvalue"; then
|
|
||||||
_err "Unable to remove the challenge record: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_nc_status=$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
|
|
||||||
_debug _nc_status "$_nc_status"
|
|
||||||
case "$_nc_status" in
|
|
||||||
204)
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
404)
|
|
||||||
_info "The challenge record was not found, nothing to remove"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
_err "Unable to remove the challenge record: $response"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# fulldomain
|
|
||||||
# Sets _domain_id, _domain and _dns_managed of the domain the record
|
|
||||||
# belongs to, walking up the name, longest match first. For a challenge
|
|
||||||
# record the leftmost label is the prefix and can never be a zone, so
|
|
||||||
# the walk starts one label in. Other names (e.g. a challenge alias in
|
|
||||||
# the "=" form) may be a zone apex themselves.
|
|
||||||
_nc_rest_get_domain() {
|
|
||||||
case "$1" in
|
|
||||||
_acme-challenge.*) i=2 ;;
|
|
||||||
*) i=1 ;;
|
|
||||||
esac
|
|
||||||
while true; do
|
|
||||||
h=$(printf "%s" "$1" | cut -d . -f "$i"-100)
|
|
||||||
if [ -z "$h" ]; then
|
|
||||||
_nc_nozone "$1"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug h "$h"
|
|
||||||
if ! _nc_rest GET "domain?fqdn=$h"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if _contains "$response" '"success": *true'; then
|
|
||||||
if _contains "$response" '"fqdn"'; then
|
|
||||||
# split the response so that first/last match cannot differ
|
|
||||||
# between the egrep and sed implementations of _egrep_o
|
|
||||||
_domain_id=$(printf "%s" "$response" | tr '{,' '\n' | _egrep_o '"id": *[0-9][0-9]*' | _head_n 1 | tr -dc '0-9')
|
|
||||||
_dns_managed=$(printf "%s" "$response" | tr '{,' '\n' | _egrep_o '"isDnsManaged": *[a-z][a-z]*' | _head_n 1 | sed 's/.*: *//')
|
|
||||||
_domain="$h"
|
|
||||||
if [ -n "$_domain_id" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
_err "Unable to parse the domain id from the netcup REST API response: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# an empty result, $h is not a domain of this account: walk on
|
|
||||||
elif _contains "$response" '"code": *"resourceDoesNotExist"'; then
|
|
||||||
# the API reports a domain that is not in this account with
|
|
||||||
# success:false and this error code: walk on
|
|
||||||
_debug "$h is not a domain of this account"
|
|
||||||
else
|
|
||||||
# e.g. an invalid API key; do not walk on, it would end in a
|
|
||||||
# misleading "no zone found" error
|
|
||||||
_err "The netcup REST API request failed: $response"
|
|
||||||
_err "Note: NC_Apikey was detected as a netcup REST API key because it is 64 characters long."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
i=$(_math "$i" + 1)
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# fulldomain domain
|
|
||||||
# Sets _scope to the host part of the challenge relative to the domain.
|
|
||||||
# The REST API prepends _acme-challenge. to the scope itself, so the
|
|
||||||
# prefix is stripped from the record name (the callers guarantee it is
|
|
||||||
# present).
|
|
||||||
_nc_rest_get_scope() {
|
|
||||||
_scope="${1#_acme-challenge.}"
|
|
||||||
if [ "$_scope" = "$2" ]; then
|
|
||||||
_scope="@"
|
|
||||||
else
|
|
||||||
_scope="${_scope%".$2"}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# domain
|
|
||||||
# Selects the legacy credentials for a domain whose DNS cannot be
|
|
||||||
# managed via the new netcup REST API.
|
|
||||||
_nc_rest_use_legacy() {
|
|
||||||
_debug "The DNS of $1 cannot be managed via the REST API, using the legacy CCP API"
|
|
||||||
if [ -z "$NC_Apikey_Legacy" ] || [ -z "$NC_Apipw" ] || [ -z "$NC_CID" ]; then
|
|
||||||
_err "The DNS of the domain $1 cannot be managed via the new netcup REST API."
|
|
||||||
_err "Set NC_Apikey_Legacy, NC_Apipw and NC_CID to your legacy CCP API credentials to manage it."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_nc_apikey="$NC_Apikey_Legacy"
|
|
||||||
}
|
|
||||||
|
|
||||||
# method endpoint [data]
|
|
||||||
# The response is returned in the global variable $response.
|
|
||||||
_nc_rest() {
|
|
||||||
m=$1
|
|
||||||
ep=$2
|
|
||||||
data=$3
|
|
||||||
_debug2 "REST $m $ep"
|
|
||||||
|
|
||||||
export _H1="Authorization: Bearer $NC_Apikey"
|
|
||||||
# blank the remaining header slots so that auth headers of another
|
|
||||||
# dns hook cannot ride into the netcup REST API in a multi-provider
|
|
||||||
# issuance
|
|
||||||
export _H2=""
|
|
||||||
export _H3=""
|
|
||||||
export _H4=""
|
|
||||||
export _H5=""
|
|
||||||
if [ "$m" = "GET" ]; then
|
|
||||||
response=$(_get "$_nc_endrest/$ep")
|
|
||||||
else
|
|
||||||
_debug2 data "$data"
|
|
||||||
response=$(_post "$data" "$_nc_endrest/$ep" "" "$m" "application/json")
|
|
||||||
fi
|
|
||||||
_nc_ret="$?"
|
|
||||||
_debug2 response "$response"
|
|
||||||
return "$_nc_ret"
|
|
||||||
}
|
|
||||||
|
|
||||||
#################### Legacy CCP API (ccp.netcup.net) ####################
|
|
||||||
|
|
||||||
_nc_legacy_add() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
if ! _nc_legacy_login; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
domain=""
|
domain=""
|
||||||
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" -ge "$_nc_last" ]
|
[ "$exit" -gt 0 ]
|
||||||
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
|
||||||
@@ -331,45 +44,35 @@ _nc_legacy_add() {
|
|||||||
domain="$tmp.$domain"
|
domain="$tmp.$domain"
|
||||||
fi
|
fi
|
||||||
if [ "$(_math "$i" - "$exit")" -ge 1 ]; then
|
if [ "$(_math "$i" - "$exit")" -ge 1 ]; then
|
||||||
msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"false\", \"state\": \"yes\"} ]}}}" "$end" "" "POST")
|
msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"false\", \"state\": \"yes\"} ]}}}" "$end" "" "POST")
|
||||||
_debug "$msg"
|
_debug "$msg"
|
||||||
if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
|
if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
|
||||||
if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
||||||
_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
|
logout
|
||||||
_err "$msg"
|
|
||||||
_nc_nozone "$fulldomain"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_nc_legacy_logout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_nc_legacy_rm() {
|
dns_netcup_rm() {
|
||||||
|
_login
|
||||||
fulldomain=$1
|
fulldomain=$1
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
if ! _nc_legacy_login; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
domain=""
|
domain=""
|
||||||
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
|
||||||
rec=""
|
rec=""
|
||||||
_nc_last=$(_nc_lastlevel "$i")
|
|
||||||
_nc_found=""
|
|
||||||
|
|
||||||
while
|
while
|
||||||
[ "$exit" -ge "$_nc_last" ]
|
[ "$exit" -gt 0 ]
|
||||||
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
|
||||||
@@ -378,7 +81,7 @@ _nc_legacy_rm() {
|
|||||||
domain="$tmp.$domain"
|
domain="$tmp.$domain"
|
||||||
fi
|
fi
|
||||||
if [ "$(_math "$i" - "$exit")" -ge 1 ]; then
|
if [ "$(_math "$i" - "$exit")" -ge 1 ]; then
|
||||||
msg=$(_post "{\"action\": \"infoDnsRecords\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\", \"domainname\": \"$domain\"}}" "$end" "" "POST")
|
msg=$(_post "{\"action\": \"infoDnsRecords\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\", \"domainname\": \"$domain\"}}" "$end" "" "POST")
|
||||||
rec=$(echo "$msg" | sed 's/\[//g' | sed 's/\]//g' | sed 's/{\"serverrequestid\".*\"dnsrecords\"://g' | sed 's/},{/};{/g' | sed 's/{//g' | sed 's/}//g')
|
rec=$(echo "$msg" | sed 's/\[//g' | sed 's/\]//g' | sed 's/{\"serverrequestid\".*\"dnsrecords\"://g' | sed 's/},{/};{/g' | sed 's/{//g' | sed 's/}//g')
|
||||||
_debug "$msg"
|
_debug "$msg"
|
||||||
if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
|
if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
|
||||||
@@ -386,18 +89,12 @@ _nc_legacy_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
|
||||||
@@ -419,24 +116,17 @@ _nc_legacy_rm() {
|
|||||||
i=0
|
i=0
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"$ids\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"TRUE\", \"state\": \"yes\"} ]}}}" "$end" "" "POST")
|
msg=$(_post "{\"action\": \"updateDnsRecords\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\",\"clientrequestid\": \"$client\" , \"domainname\": \"$domain\", \"dnsrecordset\": { \"dnsrecords\": [ {\"id\": \"$ids\", \"hostname\": \"$fulldomain.\", \"type\": \"TXT\", \"priority\": \"\", \"destination\": \"$txtvalue\", \"deleterecord\": \"TRUE\", \"state\": \"yes\"} ]}}}" "$end" "" "POST")
|
||||||
_debug "$msg"
|
_debug "$msg"
|
||||||
if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
||||||
_err "$msg"
|
_err "$msg"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
_nc_legacy_logout
|
logout
|
||||||
}
|
}
|
||||||
|
|
||||||
_nc_legacy_login() {
|
_login() {
|
||||||
# never send the REST API Bearer header (or auth headers of another
|
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
|
||||||
# dns hook) to the legacy CCP API
|
|
||||||
export _H1=""
|
|
||||||
export _H2=""
|
|
||||||
export _H3=""
|
|
||||||
export _H4=""
|
|
||||||
export _H5=""
|
|
||||||
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)
|
||||||
_debug "$tmp"
|
_debug "$tmp"
|
||||||
if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
||||||
@@ -444,56 +134,11 @@ _nc_legacy_login() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
logout() {
|
||||||
_nc_legacy_logout() {
|
tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
|
||||||
tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
|
|
||||||
_debug "$tmp"
|
_debug "$tmp"
|
||||||
if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
|
||||||
_err "$tmp"
|
_err "$tmp"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
#################### Shared helpers ####################
|
|
||||||
|
|
||||||
_nc_check_credentials() {
|
|
||||||
if [ -z "$NC_Apikey" ]; then
|
|
||||||
_err "No Credentials given"
|
|
||||||
_err "Set NC_Apikey to your netcup REST API key (64 characters) or your legacy CCP API key."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! _nc_is_rest_key; then
|
|
||||||
if [ -z "$NC_Apipw" ] || [ -z "$NC_CID" ]; then
|
|
||||||
_err "No Credentials given"
|
|
||||||
_err "The legacy CCP API needs NC_Apikey, NC_Apipw and NC_CID."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# New netcup REST API keys are 64 characters long, legacy CCP API keys
|
|
||||||
# are 50, so the key length selects the API.
|
|
||||||
_nc_is_rest_key() {
|
|
||||||
[ "${#NC_Apikey}" -eq 64 ]
|
|
||||||
}
|
|
||||||
|
|
||||||
# The legacy zone lookup walks 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 credentials and that its DNS is hosted at netcup."
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,244 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
dns_opteamax_info='Opteamax.de
|
|
||||||
Site: opteamax.de
|
|
||||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_opteamax
|
|
||||||
Options:
|
|
||||||
OPTEAMAX_Token API token. Create it in the customer panel under "API-Tokens"; it starts with "oxt_".
|
|
||||||
OPTEAMAX_Api API endpoint. Default "https://api.opteam.ax/api/v2". Optional.
|
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/7245
|
|
||||||
Author: Jens Ott <jo@opteamax.de>
|
|
||||||
'
|
|
||||||
|
|
||||||
OPTEAMAX_Api_Default="https://api.opteam.ax/api/v2"
|
|
||||||
|
|
||||||
######## Public functions #####################
|
|
||||||
|
|
||||||
#Usage: dns_opteamax_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
||||||
dns_opteamax_add() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _opteamax_init; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
|
||||||
if ! _get_root "$fulldomain"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug _domain "$_domain"
|
|
||||||
_debug _domain_id "$_domain_id"
|
|
||||||
|
|
||||||
_info "Adding the TXT record"
|
|
||||||
_opteamax_body="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"ttl\":300}"
|
|
||||||
if ! _opteamax_rest POST "/dns/domains/$_domain_id/records/" "$_opteamax_body"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! _contains "$response" "data_id"; then
|
|
||||||
_err "Could not add the TXT record: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "TXT record added"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#Usage: dns_opteamax_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
||||||
dns_opteamax_rm() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _opteamax_init; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
|
||||||
if ! _get_root "$fulldomain"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug _domain "$_domain"
|
|
||||||
_debug _domain_id "$_domain_id"
|
|
||||||
|
|
||||||
_debug "Getting the record id"
|
|
||||||
if ! _opteamax_rest GET "/dns/domains/$_domain_id/records/"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Only this challenge's record may go: a wildcard certificate puts two TXT
|
|
||||||
# values on the same name, and acme.sh removes them one call at a time.
|
|
||||||
# PowerDNS hands TXT content back in wire format, so the value arrives inside
|
|
||||||
# escaped quotes ("content": "\"<value>\""); matching from the field name up
|
|
||||||
# to the next field keeps the value pinned to the content field without
|
|
||||||
# spelling out those backslashes. _head_n 1 guarantees a single id even if an
|
|
||||||
# aborted earlier run left the same value behind twice.
|
|
||||||
_record_id=$(
|
|
||||||
echo "$response" | _opteamax_split |
|
|
||||||
grep '"type": *"TXT"' |
|
|
||||||
grep "\"name\": *\"$(_opteamax_re "$fulldomain")\.\"" |
|
|
||||||
grep "\"content\":[^,]*$txtvalue" |
|
|
||||||
_egrep_o '"data_id": *"[^"]*"' |
|
|
||||||
sed 's/.*"data_id": *"//;s/"$//' |
|
|
||||||
_head_n 1
|
|
||||||
)
|
|
||||||
_debug _record_id "$_record_id"
|
|
||||||
|
|
||||||
if [ -z "$_record_id" ]; then
|
|
||||||
_info "No such TXT record, nothing to remove."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "Removing the TXT record"
|
|
||||||
if ! _opteamax_rest DELETE "/dns/domains/$_domain_id/records/$_record_id/"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_info "TXT record removed"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
|
||||||
|
|
||||||
# Read and check the credentials, and remember them for the renewal.
|
|
||||||
_opteamax_init() {
|
|
||||||
OPTEAMAX_Token="${OPTEAMAX_Token:-$(_readaccountconf_mutable OPTEAMAX_Token)}"
|
|
||||||
OPTEAMAX_Api="${OPTEAMAX_Api:-$(_readaccountconf_mutable OPTEAMAX_Api)}"
|
|
||||||
|
|
||||||
if [ -z "$OPTEAMAX_Token" ]; then
|
|
||||||
_err "You have not set OPTEAMAX_Token yet."
|
|
||||||
_err "Create an API token in the customer panel under \"API-Tokens\" and export it:"
|
|
||||||
_err "export OPTEAMAX_Token=\"oxt_...\""
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$OPTEAMAX_Api" ]; then
|
|
||||||
OPTEAMAX_Api="$OPTEAMAX_Api_Default"
|
|
||||||
else
|
|
||||||
# A trailing slash would make every request path a double slash, which
|
|
||||||
# Django answers with a redirect that drops the request body.
|
|
||||||
OPTEAMAX_Api=$(echo "$OPTEAMAX_Api" | sed 's|/*$||')
|
|
||||||
_saveaccountconf_mutable OPTEAMAX_Api "$OPTEAMAX_Api"
|
|
||||||
fi
|
|
||||||
_saveaccountconf_mutable OPTEAMAX_Token "$OPTEAMAX_Token"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#_acme-challenge.www.domain.com
|
|
||||||
#returns
|
|
||||||
# _domain=domain.com
|
|
||||||
# _domain_id=1234
|
|
||||||
_get_root() {
|
|
||||||
domain=$1
|
|
||||||
|
|
||||||
# One request for the account's zones, then the name is walked up against
|
|
||||||
# them locally: the longest match wins, so a delegated subzone beats its
|
|
||||||
# parent.
|
|
||||||
if ! _opteamax_rest GET "/dns/domains/"; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_zones=$(echo "$response" | _opteamax_split)
|
|
||||||
|
|
||||||
i=1
|
|
||||||
while true; do
|
|
||||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
|
||||||
if [ -z "$h" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
|
|
||||||
_domain_id=$(
|
|
||||||
echo "$_zones" |
|
|
||||||
grep "\"domain\": *\"$(_opteamax_re "$h")\.\{0,1\}\"" |
|
|
||||||
_egrep_o '"domain_id": *[0-9]*' |
|
|
||||||
tr -d ' ' | cut -d : -f 2 | _head_n 1
|
|
||||||
)
|
|
||||||
if [ "$_domain_id" ]; then
|
|
||||||
_domain="$h"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
i=$(_math "$i" + 1)
|
|
||||||
done
|
|
||||||
|
|
||||||
# Only reached when the walk ran out of labels -- a failed request returns
|
|
||||||
# above, so this really does mean the account holds no zone for the name.
|
|
||||||
_err "Could not find a zone for $domain in your Opteamax account."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Put one JSON object per line so a record's id can be read off the same line
|
|
||||||
# as its name and content.
|
|
||||||
_opteamax_split() {
|
|
||||||
sed 's/}, *{/}#{/g' | tr '#' '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Escape a domain name for use in a grep pattern: the dots are literal.
|
|
||||||
_opteamax_re() {
|
|
||||||
echo "$1" | sed 's/\./\\./g'
|
|
||||||
}
|
|
||||||
|
|
||||||
# method endpoint [body]
|
|
||||||
_opteamax_rest() {
|
|
||||||
m="$1"
|
|
||||||
ep="$2"
|
|
||||||
data="$3"
|
|
||||||
_debug "$ep"
|
|
||||||
|
|
||||||
export _H1="Authorization: Bearer $OPTEAMAX_Token"
|
|
||||||
export _H2="Content-Type: application/json"
|
|
||||||
export _H3="Accept: application/json"
|
|
||||||
|
|
||||||
if [ "$m" = "GET" ]; then
|
|
||||||
response="$(_get "$OPTEAMAX_Api$ep")"
|
|
||||||
else
|
|
||||||
_debug data "$data"
|
|
||||||
response="$(_post "$data" "$OPTEAMAX_Api$ep" "" "$m")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
|
||||||
_err "Error talking to $OPTEAMAX_Api$ep"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug2 response "$response"
|
|
||||||
|
|
||||||
# A proxy or gateway error comes back as an HTML page with a 5xx status, and
|
|
||||||
# the http helpers only report transport failures -- so without this check the
|
|
||||||
# caller parses an error page as data and reports something misleading, such
|
|
||||||
# as the zone not existing.
|
|
||||||
case "$response" in
|
|
||||||
"{"* | "["*) ;;
|
|
||||||
*)
|
|
||||||
_err "Unexpected response from $OPTEAMAX_Api$ep (not JSON):"
|
|
||||||
_err "$(echo "$response" | _head_n 3)"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# The API answers an authentication or permission problem with a JSON body
|
|
||||||
# and a 4xx status; the http helpers only report transport errors, so the
|
|
||||||
# body is what tells us the call was refused.
|
|
||||||
if _contains "$response" '"detail"'; then
|
|
||||||
_err "The API refused the request: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
@@ -1,444 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
dns_optidata_info='Optidata Cloud
|
|
||||||
Site: console.optidata.com
|
|
||||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_optidata
|
|
||||||
Options:
|
|
||||||
OPTIDATA_Token DNS API key. Create a key of kind DNS in the Optidata Console (API Keys); it starts with "ocs_".
|
|
||||||
OPTIDATA_Api API base URL. Default "https://console.optidata.com".
|
|
||||||
OPTIDATA_Location Location code or UUID of the zone. Only needed when the zone lives outside the account default location and the lookup cannot tell. Optional.
|
|
||||||
OPTIDATA_Zone_ID Zone ID. Pins the zone and skips the zone lookup. Optional.
|
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/7241
|
|
||||||
Author: Eduardo Langner <https://github.com/optidatacloud>
|
|
||||||
'
|
|
||||||
|
|
||||||
# Port of dnsapi/dns_cf.sh (CloudFlare) to the Optidata Cloud DNS API served by
|
|
||||||
# ocs-backend. Routes used, all under $OPTIDATA_Api/api/v1 and authenticated
|
|
||||||
# with the x-api-key header:
|
|
||||||
#
|
|
||||||
# GET dns-zones?name=<record fqdn> resolve the zone containing the name
|
|
||||||
# GET dns-zones/<zone_id> read one zone (OPTIDATA_Zone_ID)
|
|
||||||
# POST dns-zones/<zone_id>/recordsets create / upsert the TXT record set
|
|
||||||
# DELETE dns-zones/<zone_id>/recordsets?name&type&value remove one TXT value
|
|
||||||
#
|
|
||||||
# Every response is wrapped in {"success":true,"data":...}; errors are flat
|
|
||||||
# {"status_code":<n>,"message":"...","error":"..."}.
|
|
||||||
|
|
||||||
OPTIDATA_DEFAULT_API="https://console.optidata.com"
|
|
||||||
OPTIDATA_TTL=120
|
|
||||||
OPTIDATA_MAX_ATTEMPTS=3
|
|
||||||
|
|
||||||
######## Public functions #####################
|
|
||||||
|
|
||||||
#Usage: dns_optidata_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
||||||
dns_optidata_add() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
_info "Using Optidata Cloud DNS API"
|
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _optidata_load_config; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_optidata_save_config
|
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
|
||||||
if ! _get_root "$fulldomain"; then
|
|
||||||
_err "invalid domain"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug _domain_id "$_domain_id"
|
|
||||||
_debug _sub_domain "$_sub_domain"
|
|
||||||
_debug _domain "$_domain"
|
|
||||||
|
|
||||||
_info "Adding TXT record $fulldomain"
|
|
||||||
# A record set is unique per (name, type) and the apex and wildcard
|
|
||||||
# challenges share the same name, so the second value has to be merged into
|
|
||||||
# the existing set: that is what upsert does. require_active_zone makes the
|
|
||||||
# API fail now (409) instead of queueing a record that would only be
|
|
||||||
# published after delegation, long after the ACME server gave up.
|
|
||||||
_body="{\"type\":\"TXT\",\"name\":\"$(_optidata_json_escape "$fulldomain")\",\"records\":[\"$(_optidata_json_escape "$txtvalue")\"],\"ttl\":$OPTIDATA_TTL,\"upsert\":true,\"require_active_zone\":true}"
|
|
||||||
if _optidata_rest POST "dns-zones/$_domain_id/recordsets$(_optidata_query "")" "$_body"; then
|
|
||||||
# The API echoes the whole record set back. The value is base64url
|
|
||||||
# ([A-Za-z0-9_-]), so it needs no JSON escaping and a case pattern matches
|
|
||||||
# it without depending on a grep that supports -F (Solaris grep does not).
|
|
||||||
case "$response" in
|
|
||||||
*"$txtvalue"*)
|
|
||||||
_info "Added, OK"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
_err "The API accepted the record but the value is missing from the record set: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_optidata_report_error "Add txt record error."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#Usage: dns_optidata_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
|
||||||
dns_optidata_rm() {
|
|
||||||
fulldomain=$1
|
|
||||||
txtvalue=$2
|
|
||||||
_info "Using Optidata Cloud DNS API"
|
|
||||||
_debug fulldomain "$fulldomain"
|
|
||||||
_debug txtvalue "$txtvalue"
|
|
||||||
|
|
||||||
if ! _optidata_load_config; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
|
||||||
if ! _get_root "$fulldomain"; then
|
|
||||||
_err "invalid domain"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_debug _domain_id "$_domain_id"
|
|
||||||
_debug _sub_domain "$_sub_domain"
|
|
||||||
_debug _domain "$_domain"
|
|
||||||
|
|
||||||
_info "Removing TXT record $fulldomain"
|
|
||||||
# Deleting by value keeps the other challenge value (wildcard + apex) in
|
|
||||||
# place; the API drops the whole record set once its last value goes away.
|
|
||||||
_q="name=$(printf "%s" "$fulldomain" | _url_encode)&type=TXT&value=$(printf "%s" "$txtvalue" | _url_encode)"
|
|
||||||
if _optidata_rest DELETE "dns-zones/$_domain_id/recordsets$(_optidata_query "$_q")"; then
|
|
||||||
if printf "%s\n" "$response" | tr -d " " | grep '"deleted":true' >/dev/null; then
|
|
||||||
_info "Removed, OK"
|
|
||||||
else
|
|
||||||
_info "Record value not found, nothing to remove."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
_optidata_report_error "Delete txt record error."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
|
||||||
|
|
||||||
# Reads the settings from the environment or from the saved acme.sh config and
|
|
||||||
# validates them. Shared by add and rm, which run in separate subshells.
|
|
||||||
_optidata_load_config() {
|
|
||||||
OPTIDATA_Token="${OPTIDATA_Token:-$(_readdomainconf OPTIDATA_Token)}"
|
|
||||||
OPTIDATA_Token="${OPTIDATA_Token:-$(_readaccountconf_mutable OPTIDATA_Token)}"
|
|
||||||
OPTIDATA_Api="${OPTIDATA_Api:-$(_readaccountconf_mutable OPTIDATA_Api)}"
|
|
||||||
OPTIDATA_Location="${OPTIDATA_Location:-$(_readdomainconf OPTIDATA_Location)}"
|
|
||||||
OPTIDATA_Location="${OPTIDATA_Location:-$(_readaccountconf_mutable OPTIDATA_Location)}"
|
|
||||||
OPTIDATA_Zone_ID="${OPTIDATA_Zone_ID:-$(_readdomainconf OPTIDATA_Zone_ID)}"
|
|
||||||
OPTIDATA_Zone_ID="${OPTIDATA_Zone_ID:-$(_readaccountconf_mutable OPTIDATA_Zone_ID)}"
|
|
||||||
|
|
||||||
# Keys are pasted with quotes or blanks often enough to be worth cleaning.
|
|
||||||
OPTIDATA_Token="$(printf "%s" "$OPTIDATA_Token" | tr -d '" ')"
|
|
||||||
if [ -z "$OPTIDATA_Token" ]; then
|
|
||||||
OPTIDATA_Token=""
|
|
||||||
_err "You did not specify OPTIDATA_Token yet."
|
|
||||||
_err "Create a DNS API key in the Optidata Console (API Keys) and export it:"
|
|
||||||
_err "export OPTIDATA_Token=ocs_xxxxxxxxxxxxxxxx"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! _startswith "$OPTIDATA_Token" "ocs_"; then
|
|
||||||
OPTIDATA_Token=""
|
|
||||||
_err 'OPTIDATA_Token must be an Optidata API key: it starts with "ocs_". Did you copy the entire key?'
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
OPTIDATA_Api="${OPTIDATA_Api:-$OPTIDATA_DEFAULT_API}"
|
|
||||||
OPTIDATA_Api="$(printf "%s\n" "$OPTIDATA_Api" | sed 's:/*$::')"
|
|
||||||
case "$OPTIDATA_Api" in
|
|
||||||
http://* | https://*) ;;
|
|
||||||
*)
|
|
||||||
_err "OPTIDATA_Api must be an http(s) URL, e.g. $OPTIDATA_DEFAULT_API"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
_debug OPTIDATA_Api "$OPTIDATA_Api"
|
|
||||||
_debug OPTIDATA_Location "$OPTIDATA_Location"
|
|
||||||
_debug OPTIDATA_Zone_ID "$OPTIDATA_Zone_ID"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Persists the settings so renewals work without the environment, following
|
|
||||||
# dns_cf.sh: with a pinned zone the key lives in the domain config (so a
|
|
||||||
# zone-restricted key can be used per certificate), otherwise in the account
|
|
||||||
# config.
|
|
||||||
_optidata_save_config() {
|
|
||||||
if [ "$OPTIDATA_Zone_ID" ]; then
|
|
||||||
_savedomainconf OPTIDATA_Token "$OPTIDATA_Token"
|
|
||||||
_savedomainconf OPTIDATA_Zone_ID "$OPTIDATA_Zone_ID"
|
|
||||||
if [ "$OPTIDATA_Location" ]; then
|
|
||||||
_savedomainconf OPTIDATA_Location "$OPTIDATA_Location"
|
|
||||||
else
|
|
||||||
_cleardomainconf OPTIDATA_Location
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
_saveaccountconf_mutable OPTIDATA_Token "$OPTIDATA_Token"
|
|
||||||
if [ "$OPTIDATA_Location" ]; then
|
|
||||||
_saveaccountconf_mutable OPTIDATA_Location "$OPTIDATA_Location"
|
|
||||||
else
|
|
||||||
_clearaccountconf_mutable OPTIDATA_Location
|
|
||||||
fi
|
|
||||||
_clearaccountconf_mutable OPTIDATA_Zone_ID
|
|
||||||
_clearaccountconf OPTIDATA_Zone_ID
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$OPTIDATA_Api" != "$OPTIDATA_DEFAULT_API" ]; then
|
|
||||||
_saveaccountconf_mutable OPTIDATA_Api "$OPTIDATA_Api"
|
|
||||||
else
|
|
||||||
_clearaccountconf_mutable OPTIDATA_Api
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
#_acme-challenge.www.domain.com
|
|
||||||
#returns
|
|
||||||
# _sub_domain=_acme-challenge.www
|
|
||||||
# _domain=domain.com
|
|
||||||
# _domain_id=a86dba58-0043-4cc6-a1bb-69d5e86f3ca3
|
|
||||||
# _zone_location=3f2b46f2-4f14-44e2-8e21-1b6c17f2a9d1 (empty when the API does not report one)
|
|
||||||
_get_root() {
|
|
||||||
domain=$1
|
|
||||||
_domain_lc="$(printf "%s\n" "$domain" | _lower_case | sed 's/\.$//')"
|
|
||||||
_domain=""
|
|
||||||
_domain_id=""
|
|
||||||
_sub_domain=""
|
|
||||||
_zone_location=""
|
|
||||||
_zone_status=""
|
|
||||||
|
|
||||||
if [ "$OPTIDATA_Zone_ID" ]; then
|
|
||||||
_debug "Using the pinned zone" "$OPTIDATA_Zone_ID"
|
|
||||||
if ! _optidata_rest GET "dns-zones/$OPTIDATA_Zone_ID$(_optidata_query "")"; then
|
|
||||||
_optidata_report_error "Can not read zone $OPTIDATA_Zone_ID."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_zone_json="$response"
|
|
||||||
else
|
|
||||||
# The API returns every zone that contains the name, most specific first.
|
|
||||||
if ! _optidata_rest GET "dns-zones?name=$(printf "%s" "$_domain_lc" | _url_encode)"; then
|
|
||||||
_optidata_report_error "Zone lookup for $domain failed."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if printf "%s\n" "$response" | tr -d " " | grep '"data":\[\]' >/dev/null; then
|
|
||||||
_err "No Optidata DNS zone contains $domain."
|
|
||||||
_err "Check that the zone exists in this account and that the API key is allowed to access it."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
# Pick the longest zone that is a suffix of the name ourselves as well, so
|
|
||||||
# an API that ignores the name filter still resolves the right zone.
|
|
||||||
_zone_json="$(_optidata_pick_zone "$response" "$_domain_lc")"
|
|
||||||
if [ -z "$_zone_json" ]; then
|
|
||||||
_err "No Optidata DNS zone contains $domain: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
_domain_id="$(_optidata_json_string "$_zone_json" id)"
|
|
||||||
_domain="$(_optidata_json_string "$_zone_json" zone_name)"
|
|
||||||
if [ -z "$_domain" ]; then
|
|
||||||
_domain="$(_optidata_json_string "$_zone_json" name)"
|
|
||||||
fi
|
|
||||||
_domain="$(printf "%s\n" "$_domain" | _lower_case | sed 's/\.$//')"
|
|
||||||
_zone_location="$(_optidata_json_string "$_zone_json" location)"
|
|
||||||
_zone_status="$(_optidata_json_string "$_zone_json" status)"
|
|
||||||
_debug _zone_location "$_zone_location"
|
|
||||||
_debug _zone_status "$_zone_status"
|
|
||||||
|
|
||||||
if [ -z "$_domain_id" ] || [ -z "$_domain" ]; then
|
|
||||||
_err "Could not read the zone id and name from the API response: $response"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_domain_lc" = "$_domain" ]; then
|
|
||||||
_sub_domain=""
|
|
||||||
else
|
|
||||||
case "$_domain_lc" in
|
|
||||||
*".$_domain")
|
|
||||||
_sub_domain="${_domain_lc%".$_domain"}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
_err "Zone $_domain ($_domain_id) does not contain $domain."
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$_zone_status" ] && [ "$_zone_status" != "ACTIVE" ]; then
|
|
||||||
_info "Zone $_domain has status $_zone_status; records are only published once the zone is ACTIVE (delegated to the Optidata name servers)."
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _optidata_pick_zone '<list response>' '<lowercase fqdn>'
|
|
||||||
# Prints the JSON of the zone with the longest name that is the fqdn itself or
|
|
||||||
# one of its parents. Zones are flat objects, so splitting on "},{" is safe.
|
|
||||||
_optidata_pick_zone() {
|
|
||||||
_pz_json="$1"
|
|
||||||
_pz_name="$2"
|
|
||||||
_pz_objects="$(printf "%s\n" "$_pz_json" | sed 's/}, *{/}\
|
|
||||||
{/g')"
|
|
||||||
_pz_count="$(printf "%s\n" "$_pz_objects" | wc -l | tr -d " ")"
|
|
||||||
_pz_best=""
|
|
||||||
_pz_best_len=0
|
|
||||||
_pz_i=1
|
|
||||||
while [ "$_pz_i" -le "$_pz_count" ]; do
|
|
||||||
_pz_obj="$(printf "%s\n" "$_pz_objects" | sed -n "${_pz_i}p")"
|
|
||||||
_pz_zone="$(_optidata_json_string "$_pz_obj" zone_name)"
|
|
||||||
if [ -z "$_pz_zone" ]; then
|
|
||||||
_pz_zone="$(_optidata_json_string "$_pz_obj" name)"
|
|
||||||
fi
|
|
||||||
_pz_zone="$(printf "%s\n" "$_pz_zone" | _lower_case | sed 's/\.$//')"
|
|
||||||
if [ "$_pz_zone" ]; then
|
|
||||||
case "$_pz_name" in
|
|
||||||
"$_pz_zone" | *".$_pz_zone")
|
|
||||||
if [ "${#_pz_zone}" -gt "$_pz_best_len" ]; then
|
|
||||||
_pz_best="$_pz_obj"
|
|
||||||
_pz_best_len="${#_pz_zone}"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
_pz_i=$(_math "$_pz_i" + 1)
|
|
||||||
done
|
|
||||||
printf "%s" "$_pz_best"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _optidata_json_string '<json>' key
|
|
||||||
# Prints the string value of the first "key" in the JSON, nothing when the key
|
|
||||||
# is absent or not a string (e.g. "location":null).
|
|
||||||
_optidata_json_string() {
|
|
||||||
printf "%s\n" "$1" | _egrep_o "\"$2\": *\"[^\"]*\"" | _head_n 1 | sed 's/^"[^"]*": *"//; s/"$//'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Escapes a value for use inside a JSON string literal.
|
|
||||||
_optidata_json_escape() {
|
|
||||||
printf "%s\n" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _optidata_query '<query without the leading ?>'
|
|
||||||
# Appends the location (explicit OPTIDATA_Location, else the one reported by
|
|
||||||
# the zone lookup) and prints the query string with its leading "?", or
|
|
||||||
# nothing when there is nothing to send.
|
|
||||||
_optidata_query() {
|
|
||||||
_oq="$1"
|
|
||||||
_oq_loc="${OPTIDATA_Location:-$_zone_location}"
|
|
||||||
if [ "$_oq_loc" ]; then
|
|
||||||
if [ "$_oq" ]; then
|
|
||||||
_oq="$_oq&location=$(printf "%s" "$_oq_loc" | _url_encode)"
|
|
||||||
else
|
|
||||||
_oq="location=$(printf "%s" "$_oq_loc" | _url_encode)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ "$_oq" ]; then
|
|
||||||
printf "?%s" "$_oq"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _optidata_rest METHOD 'endpoint under /api/v1' [json body]
|
|
||||||
# Sets $response to the normalized JSON body. Returns 0 on a success envelope;
|
|
||||||
# otherwise sets $_optidata_status / $_optidata_message and returns 1. Rate
|
|
||||||
# limits and upstream hiccups (429, 502-504) are retried a few times.
|
|
||||||
_optidata_rest() {
|
|
||||||
_m=$1
|
|
||||||
_ep=$2
|
|
||||||
_data=$3
|
|
||||||
_debug "$_m $_ep"
|
|
||||||
|
|
||||||
# Hooks share one shell and _get/_post always send _H1 to _H5, so the unused
|
|
||||||
# slots have to be cleared: otherwise a previous provider's header (an auth
|
|
||||||
# header, for instance) is sent to the Optidata endpoint.
|
|
||||||
export _H1="Accept: application/json"
|
|
||||||
export _H2="Content-Type: application/json"
|
|
||||||
export _H3="x-api-key: $OPTIDATA_Token"
|
|
||||||
export _H4=""
|
|
||||||
export _H5=""
|
|
||||||
|
|
||||||
_url="$OPTIDATA_Api/api/v1/$_ep"
|
|
||||||
_optidata_status=""
|
|
||||||
_optidata_message=""
|
|
||||||
_attempt=1
|
|
||||||
while true; do
|
|
||||||
# A failed request leaves the previous status line in the header file, which
|
|
||||||
# would then be read as this request's response code.
|
|
||||||
if [ -z "$HTTP_HEADER" ]; then
|
|
||||||
_err "HTTP header file is not initialized"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
: >"$HTTP_HEADER" || return 1
|
|
||||||
if [ "$_m" = "GET" ]; then
|
|
||||||
response="$(_get "$_url")"
|
|
||||||
else
|
|
||||||
_debug2 data "$_data"
|
|
||||||
response="$(_post "$_data" "$_url" "" "$_m")"
|
|
||||||
fi
|
|
||||||
_ret="$?"
|
|
||||||
if [ "$_ret" != "0" ]; then
|
|
||||||
_err "Request to $_url failed. Is OPTIDATA_Api correct and reachable?"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')"
|
|
||||||
_debug "http response code" "$_code"
|
|
||||||
response="$(printf "%s\n" "$response" | _normalizeJson)"
|
|
||||||
_debug2 response "$response"
|
|
||||||
|
|
||||||
if printf "%s\n" "$response" | tr -d " " | grep '"success":true' >/dev/null; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
_optidata_status="$(printf "%s\n" "$response" | _egrep_o '"status_code": *[0-9]*' | _head_n 1 | cut -d : -f 2 | tr -d " ")"
|
|
||||||
if [ -z "$_optidata_status" ]; then
|
|
||||||
_optidata_status="$_code"
|
|
||||||
fi
|
|
||||||
_optidata_message="$(_optidata_json_string "$response" message)"
|
|
||||||
if [ -z "$_optidata_message" ]; then
|
|
||||||
# Validation errors carry an array of messages.
|
|
||||||
_optidata_message="$(printf "%s\n" "$response" | _egrep_o '"message": *\[[^]]*\]' | _head_n 1 | sed 's/^"message": *\[//; s/\]$//' | tr -d '"')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$_optidata_status" in
|
|
||||||
429 | 502 | 503 | 504)
|
|
||||||
if [ "$_attempt" -lt "$OPTIDATA_MAX_ATTEMPTS" ]; then
|
|
||||||
_wait="$(grep -i "^Retry-After:" "$HTTP_HEADER" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r\n')"
|
|
||||||
case "$_wait" in
|
|
||||||
'' | *[!0-9]*) _wait=5 ;;
|
|
||||||
esac
|
|
||||||
if [ "$_wait" -gt 60 ]; then
|
|
||||||
_wait=60
|
|
||||||
fi
|
|
||||||
_info "Optidata API answered HTTP $_optidata_status; retrying in ${_wait}s (attempt $_attempt of $OPTIDATA_MAX_ATTEMPTS)."
|
|
||||||
_sleep "$_wait"
|
|
||||||
_attempt=$(_math "$_attempt" + 1)
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
return 1
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: _optidata_report_error 'what failed'
|
|
||||||
# Logs the API error captured by _optidata_rest plus a hint for the usual causes.
|
|
||||||
_optidata_report_error() {
|
|
||||||
_err "$1"
|
|
||||||
if [ "$_optidata_message" ]; then
|
|
||||||
_err "Optidata API answered HTTP ${_optidata_status:-?}: $_optidata_message"
|
|
||||||
elif [ "$_optidata_status" ]; then
|
|
||||||
_err "Optidata API answered HTTP $_optidata_status: $response"
|
|
||||||
fi
|
|
||||||
case "$_optidata_status" in
|
|
||||||
401)
|
|
||||||
_err "Check OPTIDATA_Token: it must be a valid, enabled Optidata API key (it starts with ocs_). Did you copy the entire key?"
|
|
||||||
;;
|
|
||||||
402)
|
|
||||||
_err "The account is blocked for billing reasons. Check the payment method in the Optidata Console."
|
|
||||||
;;
|
|
||||||
403)
|
|
||||||
_err "The key must be a DNS API key with the dns_zones scope, the permissions zones_read, records_create, records_update and records_delete, and access to this zone."
|
|
||||||
;;
|
|
||||||
404)
|
|
||||||
_err "The zone was not found. If it lives outside the account default location, set OPTIDATA_Location to its location code or UUID."
|
|
||||||
;;
|
|
||||||
409)
|
|
||||||
_err "The zone is not delegated to the Optidata name servers yet. Point the domain NS records to them and retry once the zone status is ACTIVE."
|
|
||||||
;;
|
|
||||||
429)
|
|
||||||
_err "The Optidata API rate limit was reached. Retry in a minute."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
@@ -227,7 +227,7 @@ _poweradmin_rest() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf '%s' "$response" | grep -q '"success"[ ]*:[ ]*false'; then
|
if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*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
|
||||||
|
|||||||
+1
-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 -n 's/.*"id":\([0-9][0-9]*\),.*/\1/p')
|
_record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\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
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
#!/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" | cut -d ':' -f 2- | tr -d ' \t\r')
|
_token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p")
|
||||||
_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,10 +42,7 @@ 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
|
||||||
# prepend a space to each line so "start of line" and "after whitespace"
|
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p")
|
||||||
# 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"
|
||||||
@@ -57,7 +54,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 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p")
|
lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain:[[:digit:]]+)(.*)/\2/p")
|
||||||
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
|
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
|
||||||
lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2)
|
lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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 | tr -d ' \t\r')
|
_code=$(echo "$response" | _egrep_o "code = ([0-9]+)" | _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/[ ]*$//')
|
_description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
||||||
|
|
||||||
_debug response_code "$_code"
|
_debug response_code "$_code"
|
||||||
_debug response_description "$_description"
|
_debug response_description "$_description"
|
||||||
|
|||||||
+2
-13
@@ -61,7 +61,7 @@ dns_world4you_add() {
|
|||||||
if _contains "$res" "successfully"; then
|
if _contains "$res" "successfully"; then
|
||||||
return 0
|
return 0
|
||||||
else
|
else
|
||||||
msg=$(_w4y_alert_msg "$res")
|
msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g')
|
||||||
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=$(_w4y_alert_msg "$res")
|
msg=$(echo "$res" | grep -A 20 'alert-notification' | grep 'class="weak-title">[^<]' | sed 's/<[^>]*>//g;s/^\s*//g')
|
||||||
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,17 +145,6 @@ 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":[ ]*[0-9]+' |
|
_egrep_o '"id":[[:space:]]*[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":[ ]*[0-9]+' |
|
_egrep_o '"interval":[[:space:]]*[0-9]+' |
|
||||||
cut -d':' -f2
|
cut -d':' -f2
|
||||||
)
|
)
|
||||||
_debug 'Polling interval' "$interval"
|
_debug 'Polling interval' "$interval"
|
||||||
|
|||||||
+15
-51
@@ -22,32 +22,21 @@ 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
|
||||||
@@ -76,21 +65,11 @@ 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
|
||||||
_cleardomainconf YC_SA_ID
|
_clearaccountconf YC_SA_ID
|
||||||
_clearaccountconf_mutable YC_SA_ID
|
_clearaccountconf YC_SA_Key_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
|
||||||
@@ -131,30 +110,11 @@ 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"
|
||||||
@@ -164,10 +124,16 @@ dns_yc_rm() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
# upsertRecordSets.deletions removes only the given value from the rrset,
|
_debug "Getting txt records"
|
||||||
# leaving any other values at the same name (e.g. base + wildcard domain)
|
if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
|
||||||
# intact -- no need to read the current data set and recompute it.
|
exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$")
|
||||||
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
_debug exists_txtvalue "$exists_txtvalue"
|
||||||
|
else
|
||||||
|
_err "Error: $response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _yc_rest POST "zones/$_domain_id:updateRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":$exists_txtvalue}]}"; then
|
||||||
if _contains "$response" "\"done\": true"; then
|
if _contains "$response" "\"done\": true"; then
|
||||||
_info "Delete, OK"
|
_info "Delete, OK"
|
||||||
return 0
|
return 0
|
||||||
@@ -228,7 +194,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")
|
||||||
@@ -289,9 +255,7 @@ _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"
|
||||||
|
|
||||||
if [ "$_yc_key_is_temp" ]; then
|
rm -rf "$YC_SA_Key_File"
|
||||||
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"
|
||||||
@@ -300,7 +264,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
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
#Support Healthchecks.io (hosted or self-hosted)
|
|
||||||
#https://healthchecks.io/docs/http_api/
|
|
||||||
|
|
||||||
#Required:
|
|
||||||
#HEALTHCHECKS_URL="https://hc-ping.com/your-uuid"
|
|
||||||
|
|
||||||
#Use with --notify-level 3, so a ping is sent on every cron run, even when
|
|
||||||
#all certs are skipped. Otherwise Healthchecks reports the check as down.
|
|
||||||
|
|
||||||
healthchecks_send() {
|
|
||||||
_subject="$1"
|
|
||||||
_content="$2"
|
|
||||||
_statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
|
|
||||||
_debug "_subject" "$_subject"
|
|
||||||
_debug "_content" "$_content"
|
|
||||||
_debug "_statusCode" "$_statusCode"
|
|
||||||
|
|
||||||
HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-$(_readaccountconf_mutable HEALTHCHECKS_URL)}"
|
|
||||||
if [ -z "$HEALTHCHECKS_URL" ]; then
|
|
||||||
HEALTHCHECKS_URL=""
|
|
||||||
_err "You didn't specify the Healthchecks.io ping url HEALTHCHECKS_URL yet."
|
|
||||||
_err "Example: export HEALTHCHECKS_URL=\"https://hc-ping.com/your-uuid\""
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_saveaccountconf_mutable HEALTHCHECKS_URL "$HEALTHCHECKS_URL"
|
|
||||||
|
|
||||||
_hc_url="${HEALTHCHECKS_URL%/}"
|
|
||||||
case "$_statusCode" in
|
|
||||||
0 | 2) ;;
|
|
||||||
1)
|
|
||||||
_hc_url="$_hc_url/fail"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
_hc_url="$_hc_url/log"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
_data="$_subject
|
|
||||||
|
|
||||||
$_content"
|
|
||||||
|
|
||||||
response="$(_post "$_data" "$_hc_url" "" "POST" "text/plain")"
|
|
||||||
|
|
||||||
if [ "$?" = "0" ] && [ "$response" = "OK" ]; then
|
|
||||||
_info "healthchecks ping success."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
_err "healthchecks ping error."
|
|
||||||
_err "$response"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
+2
-1
@@ -57,8 +57,9 @@ 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 _H1="X-Api-Key: $WAHA_API_KEY"
|
export _H2="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