Compare commits

..
1 Commits
73 changed files with 578 additions and 6096 deletions
+14 -258
View File
@@ -66,7 +66,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file
@@ -114,7 +114,7 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install tools
run: |
brew untap aws/tap || true
@@ -167,7 +167,7 @@ jobs:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
@@ -231,13 +231,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg install -y socat curl
usesh: true
@@ -290,13 +289,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg install -y socat curl
usesh: true
@@ -347,13 +345,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg_add socat curl libiconv
usesh: true
@@ -404,13 +401,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
/usr/sbin/pkg_add curl socat
@@ -462,13 +458,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
pkg install -y libnghttp2
@@ -524,13 +519,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: mport install socat curl || true
usesh: true
@@ -582,13 +576,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: |
@@ -642,13 +635,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
@@ -699,13 +691,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
@@ -756,13 +747,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: zap install socat
@@ -813,13 +803,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
@@ -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"
-1
View File
@@ -58,7 +58,6 @@ jobs:
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-1
View File
@@ -64,7 +64,6 @@ jobs:
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-1
View File
@@ -66,7 +66,6 @@ jobs:
- uses: vmactions/ghostbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-1
View File
@@ -65,7 +65,6 @@ jobs:
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-76
View File
@@ -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"
-76
View File
@@ -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"
-1
View File
@@ -58,7 +58,6 @@ jobs:
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-1
View File
@@ -58,7 +58,6 @@ jobs:
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-86
View File
@@ -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"
-1
View File
@@ -64,7 +64,6 @@ jobs:
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-1
View File
@@ -64,7 +64,6 @@ jobs:
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-78
View File
@@ -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"
-1
View File
@@ -64,7 +64,6 @@ jobs:
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
+1 -9
View File
@@ -31,21 +31,13 @@ jobs:
Le_HTTPPort: 5002
TEST_LOCAL: 1
TEST_CA: "Pebble Intermediate CA"
TEST_DNS_MANUAL: 1
steps:
- uses: actions/checkout@v6
- name: Install tools
run: sudo apt-get install -y socat
- name: Run Pebble
run: |
cd ..
curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml
# 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
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
- name: Set up Pebble
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
- name: Clone acmetest
-1
View File
@@ -64,7 +64,6 @@ jobs:
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
-1
View File
@@ -64,7 +64,6 @@ jobs:
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
+3 -45
View File
@@ -10,16 +10,9 @@ on:
- '**.sh'
- "Dockerfile"
- '.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:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
@@ -47,13 +40,13 @@ jobs:
build:
runs-on: ubuntu-latest
needs: CheckToken
if: "github.event_name != 'schedule' && contains(needs.CheckToken.outputs.hasToken, 'true')"
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
permissions:
contents: read
packages: write
steps:
- name: checkout code
uses: actions/checkout@v7
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU
@@ -105,38 +98,3 @@ jobs:
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|| 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
-67
View File
@@ -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."
-33
View File
@@ -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/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/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 align="center">
@@ -134,10 +130,6 @@
|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|29|[![OpenEuler](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
|30|[![HardenedBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml)|HardenedBSD
|31|[![OPNsense](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml)|OPNsense
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
@@ -231,31 +223,6 @@ Cron entry example:
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
+158 -846
View File
File diff suppressed because it is too large Load Diff
-22
View File
@@ -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
+2 -2
View File
@@ -163,8 +163,8 @@ byteplus_alb_deploy() {
# ── 3. Read cert and key ─────────────────────────────────────────────────────
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
_public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
_public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r')
_private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r')
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
_err "Failed to read certificate or key file."
+1 -5
View File
@@ -87,11 +87,7 @@ cpanel_uapi_deploy() {
# Auto mode
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
# call API for site config
if [ -n "$_uapi_user" ]; then
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
else
_response=$(uapi DomainInfo list_domains)
fi
_response=$(uapi DomainInfo list_domains)
# exit if error in response
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate - cannot retrieve sitelist:"
+2 -2
View File
@@ -74,9 +74,9 @@ fritzbox_deploy() {
_info "Log in to the FRITZ!Box"
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
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
_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
_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
+4 -1
View File
@@ -173,7 +173,10 @@ haproxy_deploy() {
# Set the suffix depending if we are creating a bundle or not
if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
_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
_info "ECC key type detected"
_suffix=".ecdsa"
-114
View File
@@ -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
}
-253
View File
@@ -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
View File
@@ -83,7 +83,7 @@ keyhelp_deploy() {
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
_H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
_message=$(echo "$_response" | 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"
if [ -z "$_message" ]; then
_err "Fail to upload certificate."
@@ -118,7 +118,7 @@ keyhelp_deploy() {
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
_message=$(echo "$_response" | 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"
if [ -z "$_message" ]; then
_err "Fail to apply certificate."
+4 -1
View File
@@ -121,7 +121,10 @@ lighttpd_deploy() {
# Set the suffix depending if we are creating a bundle or not
if [ "${Le_Deploy_lighttpd_bundle}" = "yes" ]; then
_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
_info "ECC key type detected"
_suffix=".ecdsa"
+8 -19
View File
@@ -10,10 +10,6 @@
# Usage (shown values are the examples):
# 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
# A name without a leading '/' is looked up in the certificate directory
# of the domain. An absolute path is used as is, so a single deploy file
# can be shared by all domains, e.g.
# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml"
#
# 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com
@@ -53,7 +49,7 @@ multideploy_deploy() {
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf MULTIDEPLOY_FILENAME
MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}"
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
@@ -79,8 +75,7 @@ multideploy_deploy() {
# This function preprocesses the deploy file by checking if 'yq' is installed,
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments:
# $@ - Posible deploy file names. A name starting with '/' is treated as an
# absolute path, any other name is relative to the domain directory.
# $@ - Posible deploy file names.
# Usage:
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
_preprocess_deployfile() {
@@ -92,21 +87,15 @@ _preprocess_deployfile() {
_debug3 "yq is installed."
# Check if deploy file exists
found_file=""
for file in "$@"; do
if _startswith "$file" "/"; then
_multideploy_path="$file"
else
_multideploy_path="$DOMAIN_PATH/$file"
fi
_debug3 "Checking file" "$_multideploy_path"
if [ -f "$_multideploy_path" ]; then
_debug3 "Checking file" "$DOMAIN_PATH/$file"
if [ -f "$DOMAIN_PATH/$file" ]; then
_debug3 "File found"
if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file."
return 1
fi
found_file="$_multideploy_path"
found_file="$file"
else
_debug3 "File not found"
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."
return 1
fi
if ! _check_deployfile "$found_file"; then
_err "Deploy file is not valid: $found_file"
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file"
return 1
fi
echo "$found_file"
echo "$DOMAIN_PATH/$found_file"
}
# Description:
+10 -17
View File
@@ -116,24 +116,17 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
_retval=$?
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed: $message"
_debug "Response" "$response"
return 1
fi
}
+10 -17
View File
@@ -128,24 +128,17 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
_retval=$?
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed: $message"
_debug "Response" "$response"
return 1
fi
}
+2 -2
View File
@@ -175,7 +175,7 @@ _get_unleashed_version() {
_post_upload() {
_post_action="$1"
_post_upfile="$2"
_post_file="$2"
_post_boundary="----FormBoundary$(date "+%s%N")"
@@ -183,7 +183,7 @@ _post_upload() {
printf -- "--%s\r\n" "$_post_boundary"
printf -- "Content-Disposition: form-data; name=\"u\"; filename=\"%s\"\r\n" "$_post_action"
printf -- "Content-Type: application/octet-stream\r\n\r\n"
printf -- "%s\r\n" "$(cat "$_post_upfile")"
printf -- "%s\r\n" "$(cat "$_post_file")"
printf -- "--%s\r\n" "$_post_boundary"
printf -- "Content-Disposition: form-data; name=\"action\"\r\n\r\n"
-280
View File
@@ -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
View File
@@ -232,6 +232,8 @@ _ssh_deploy() {
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"
# 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 directories erased after 180 days."
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 [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# 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 ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
@@ -282,7 +284,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
_pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# 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 ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
@@ -323,7 +325,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
_pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# 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 ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
@@ -368,8 +370,8 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
_pipe=">>"
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
# 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;"
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
_cmdstr="$_cmdstr cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null;"
if [ "$DEPLOY_SSH_FULLCHAIN" = "yes" ]; then
if ! _ssh_remote_cmd "$_cmdstr"; then
return $_err_code
fi
+2 -6
View File
@@ -344,7 +344,6 @@ synology_dsm_deploy() {
else
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@@ -355,7 +354,6 @@ synology_dsm_deploy() {
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@@ -391,13 +389,13 @@ synology_dsm_deploy() {
else
_info "Restart HTTP services not necessary."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_logout
return 0
else
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_err "Unable to update certificate, got error response: $response."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
}
@@ -405,8 +403,6 @@ synology_dsm_deploy() {
#################### Private functions below ##################################
_logout() {
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
# Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out.
# Note: this overwrites $response, so print any error message that needs it before calling.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
_debug3 response "$response"
}
-518
View File
@@ -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
View File
@@ -43,15 +43,15 @@ _ws_call() {
_debug "_ws_call arg1" "$1"
_debug "_ws_call arg2" "$2"
_debug "_ws_call arg3" "$3"
# TrueNAS 26.0.0-BETA3 and later need a --plain option for midclt call, detect if it is available
_midclt_plain=""
case "$(midclt --help 2>/dev/null)" in
*--plain*) _midclt_plain="--plain" ;;
esac
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" $_midclt_plain call "$@")
if [ $# -eq 3 ]; then
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
fi
if [ $# -eq 2 ]; then
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
fi
if [ $# -eq 1 ]; then
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
fi
_debug "_ws_response" "$_ws_response"
printf "%s" "$_ws_response"
return 0
@@ -256,8 +256,8 @@ truenas_ws_deploy() {
_info "Gather current WebUI certificate..."
_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_name=$(printf "%s" "$_ws_response" | jq -r 'if (.ui_certificate | type) == "object" then .ui_certificate.name else .ui_certificate_name end')
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
_info "Current WebUI certificate ID: $_ui_certificate_id"
_info "Current WebUI certificate name: $_ui_certificate_name"
@@ -332,7 +332,7 @@ truenas_ws_deploy() {
_info "Replace WebUI certificate..."
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '.ui_certificate | if type == "object" then .id else . end')
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
if [ "$_changed_certid" != "$_new_certid" ]; then
_err "WebUI certificate change error.."
return 5
-341
View File
@@ -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
View File
@@ -9,8 +9,7 @@ Options:
AZUREDNS_APPID App ID. App ID of the service principal
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Not saved, provide it on every run. Optional.
AZUREDNS_PRIVATEZONE Use Azure Private DNS Zones instead of Public DNS Zones. "true"/"false"
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
'
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
@@ -40,12 +39,6 @@ dns_azure_add() {
#save subscription id to account conf file.
_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)}"
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
_info "Using Azure managed identity"
@@ -54,15 +47,13 @@ dns_azure_add() {
_saveaccountconf_mutable AZUREDNS_TENANTID ""
_saveaccountconf_mutable AZUREDNS_APPID ""
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN ""
else
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
#AZUREDNS_BEARERTOKEN is short-lived, so it is taken from the environment only and never
#read from or saved to the account conf. Versions up to 3.0.9 cached their internal access
#token under the same name, which must not be replayed as a user token (#7218).
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
if [ -z "$AZUREDNS_TENANTID" ]; then
AZUREDNS_SUBSCRIPTIONID=""
@@ -102,7 +93,7 @@ dns_azure_add() {
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN"
fi
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
@@ -119,9 +110,7 @@ dns_azure_add() {
_debug _sub_domain "$_sub_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=$_azure_api_version"
acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01"
_debug "$acmeRecordURI"
# Get existing TXT record
_azure_rest GET "$acmeRecordURI" "" "$accesstoken"
@@ -147,7 +136,7 @@ dns_azure_add() {
fi
fi
# 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"
if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
_info "validation value added"
@@ -178,8 +167,6 @@ dns_azure_rm() {
return 1
fi
AZUREDNS_PRIVATEZONE="${AZUREDNS_PRIVATEZONE:-$(_readaccountconf_mutable AZUREDNS_PRIVATEZONE)}"
AZUREDNS_MANAGEDIDENTITY="${AZUREDNS_MANAGEDIDENTITY:-$(_readaccountconf_mutable AZUREDNS_MANAGEDIDENTITY)}"
if [ "$AZUREDNS_MANAGEDIDENTITY" = true ]; then
_info "Using Azure managed identity"
@@ -188,7 +175,7 @@ dns_azure_rm() {
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
#AZUREDNS_BEARERTOKEN 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_TENANTID" ]; then
AZUREDNS_SUBSCRIPTIONID=""
@@ -238,11 +225,8 @@ dns_azure_rm() {
_debug _sub_domain "$_sub_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=$_azure_api_version"
acmeRecordURI="https://management.azure.com$(printf '%s' "$_domain_id" | sed 's/\\//g')/TXT/$_sub_domain?api-version=2017-09-01"
_debug "$acmeRecordURI"
# Get existing TXT record
_azure_rest GET "$acmeRecordURI" "" "$accesstoken"
timestamp="$(_time)"
@@ -266,7 +250,7 @@ dns_azure_rm() {
fi
else
# 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"
if [ "$_code" = "200" ] || [ "$_code" = '201' ]; then
_info "validation value removed"
@@ -397,21 +381,6 @@ _azure_getaccess_token() {
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() {
domain=$1
subscriptionId=$2
@@ -419,8 +388,6 @@ _get_root() {
i=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
## returns up to 100 zones in one response. Handling more results is not implemented
## (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
## 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
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
-348
View File
@@ -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
}
+3 -3
View File
@@ -323,21 +323,21 @@ _bhosted_extract_id() {
fi
# 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
printf "%s" "$_id"
return 0
fi
# 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
printf "%s" "$_id"
return 0
fi
# "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
printf "%s" "$_id"
return 0
+4 -4
View File
@@ -75,7 +75,7 @@ dns_creoline_rm() {
return 1
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"
if [ -z "$record_id" ]; then
@@ -108,10 +108,10 @@ _get_root() {
return 1
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"
_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"
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
@@ -171,7 +171,7 @@ _creoline_rest() {
_err "URI:$uri"
return 1
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 "URI:$uri"
return 1
+3 -3
View File
@@ -285,15 +285,15 @@ _cyon_delete_txt() {
list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
list_txt_response="$(_get "${list_txt_url}")"
list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
_debug list_txt_response "${list_txt_response}"
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
# Find and delete all acme challenge entries for the $fulldomain.
_dns_entries="$(printf "%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_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"
+5 -8
View File
@@ -30,9 +30,8 @@ dns_czechia_add() {
return 1
fi
_czechia_tab="$(printf '\t')"
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken."
@@ -109,9 +108,8 @@ dns_czechia_rm() {
return 1
fi
_czechia_tab="$(printf '\t')"
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken."
@@ -182,13 +180,12 @@ _czechia_load_conf() {
}
_czechia_pick_zone() {
_czechia_pz_tab="$(printf '\t')"
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
_best_zone=""
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
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
case "$_fd" in
-243
View File
@@ -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" ""
}
-6
View File
@@ -150,9 +150,6 @@ _dns_dynv6_add_http() {
fi
_get_zone_name "$_zone_id"
record=${fulldomain%%."$_zone_name"}
if [ "$fulldomain" = "$_zone_name" ]; then
record=""
fi
_set_record TXT "$record" "$txtvalue"
if _contains "$response" "$txtvalue"; then
_info "Successfully added record"
@@ -171,9 +168,6 @@ _dns_dynv6_rm_http() {
fi
_get_zone_name "$_zone_id"
record=${fulldomain%%."$_zone_name"}
if [ "$fulldomain" = "$_zone_name" ]; then
record=""
fi
_get_record_id "$_zone_id" "$record" "$txtvalue"
_del_record "$_zone_id" "$_record_id"
if [ -z "$response" ]; then
+15 -17
View File
@@ -75,11 +75,6 @@ dns_easydns_rm() {
EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}"
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"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
@@ -96,21 +91,24 @@ dns_easydns_rm() {
return 1
fi
record_id=$(printf "%s\n" "$response" | tr '{' '\n' | grep "\"rdata\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
_debug count "$count"
if [ "$count" = "0" ]; then
_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
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
_err "Delete record error."
return 1
fi
_contains "$response" "\"status\":200"
}
#################### Private functions below ##################################
+9 -20
View File
@@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
Options:
FREEMYIP_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/6247
Author: Recolic Keghart <root@recolic.net>, @Giova96, ExtremeFiretop
Author: Recolic Keghart <root@recolic.net>, @Giova96
'
FREEMYIP_DNS_API="https://freemyip.com/update?"
@@ -68,30 +68,22 @@ dns_freemyip_rm() {
return $?
}
################ Private functions below ################
################ Private functions below ################
_get_root() {
_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.
_freemyip_get_until_ok() {
_fmi_url="$1"
_fmi_i=1
while [ "$_fmi_i" -le 8 ]; do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..."
_fmi_response="$(_get "$_fmi_url")"
printf '%s\n' "$_fmi_response" >&2
if _contains "$_fmi_response" "OK"; then
return 0
fi
for i in $(seq 1 8); do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
_sleep 1 # DO NOT send the request too fast
_fmi_i=$((_fmi_i + 1))
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
}
@@ -101,16 +93,13 @@ _is_root_domain_published() {
_webroot="$(_get_root "$_fmi_d")"
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
_fmi_i=1
while [ "$_fmi_i" -le 3 ]; do
_debug "'$_webroot' ns lookup, retry $_fmi_i/3..."
for i in $(seq 1 3); do
_debug "'$_webroot' ns lookup, retry $i/3..."
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
_debug "'$_webroot' already has a TXT record published!"
return 0
fi
_sleep 10 # Give it some time to propagate the TXT record
_fmi_i=$((_fmi_i + 1))
done
return 1
}
-198
View File
@@ -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
}
+4 -4
View File
@@ -441,18 +441,18 @@ _hostup_json_extract() {
input="${2:-$line}"
# 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
printf "%s" "$quoted_match" |
cut -d : -f2- |
sed 's/^[ ]*"//' |
sed 's/"[ ]*$//' |
sed 's/^[[:space:]]*"//' |
sed 's/"[[:space:]]*$//' |
sed 's/\\"/"/g'
return 0
fi
# 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
printf "%s" "$unquoted_match" |
cut -d : -f2- |
-227
View File
@@ -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
}
+2 -2
View File
@@ -117,7 +117,7 @@ dns_infoblox_uddi_rm() {
return 0
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"
if [ -z "$record_id" ]; then
@@ -178,7 +178,7 @@ _get_root() {
# Check if response contains results (even if empty)
if _contains "$response" '"results"'; then
# 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
# Found the zone
_domain="$h"
+40 -48
View File
@@ -7,23 +7,22 @@ Options:
JD_ACCESS_KEY_ID Access key ID
JD_ACCESS_KEY_SECRET Access key secret
JD_REGION Region. E.g. "cn-north-1"
Issues: github.com/acmesh-official/acme.sh/issues/7202
Author: @skysaint
Issues: github.com/acmesh-official/acme.sh/issues/2388
'
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
_JD_PROD="domainservice"
_JD_PROD="clouddnsservice"
_JD_API="jdcloud-api.com"
_JD_API_VERSION="v2"
_JD_API_VERSION="v1"
_JD_DEFAULT_REGION="cn-north-1"
_JD_HOST="$_JD_PROD.$_JD_API"
######## 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() {
fulldomain=$1
txtvalue=$2
@@ -59,14 +58,24 @@ dns_jd_add() {
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
#_debug "Getting describeViewTree"
#_debug "Getting getViewTree"
_debug "Adding records"
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1}}"
#_addrr='{"req":{"hostRecord":"_acme-challenge","hostValue":"XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs","ttl":300,"type":"TXT","viewValue":-1}}'
if jd_rest POST "domain/$_domain_id/ResourceRecord" "" "$_addrr"; then
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1},\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
#_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/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."
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
if [ "$_srid" ]; then
_rid="$_srid,$_rid"
fi
_savedomainconf "JD_CLOUD_RIDS" "$_rid"
return 0
fi
@@ -88,7 +97,14 @@ dns_jd_rm() {
_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"
if ! _get_root "$fulldomain"; then
@@ -99,37 +115,16 @@ dns_jd_rm() {
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
# List records, filter by hostRecord and use a large pageSize so it isn't missed on record-heavy zones.
if ! jd_rest GET "domain/$_domain_id/ResourceRecord" "pageSize=50&search=$_sub_domain"; then
_err "Failed to list resource records"
return 1
fi
_cleardomainconf JD_CLOUD_RIDS
# Match record by hostRecord + type TXT + hostValue
_record_id=""
_matched="$(echo "$response" | tr '{' '\n' | grep "\"hostRecord\":\"$_sub_domain\"" | grep "\"type\":\"TXT\"" | grep "\"hostValue\":\"$txtvalue\"")"
_debug2 _matched "$_matched"
_aws_tmpl_xml="{\"ids\":[$_srid],\"action\":\"del\",\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
if [ -z "$_matched" ]; then
_info "TXT record not found, nothing to remove."
return 0
fi
_record_id="$(echo "$_matched" | tr ',' '\n' | grep "\"id\":" | cut -d : -f 2 | tr -d '"' | _head_n 1)"
_debug _record_id "$_record_id"
if [ -z "$_record_id" ]; then
_info "Could not extract record id from response, nothing to remove."
return 0
fi
if jd_rest DELETE "domain/$_domain_id/ResourceRecord/$_record_id"; then
if jd_rest POST "domain/$_domain_id/RROperate" "" "$_aws_tmpl_xml" && _contains "$response" "\"code\":\"OK\""; then
_info "TXT record deleted successfully."
return 0
fi
_err "Failed to delete TXT record."
return 1
}
#################### Private functions below ##################################
@@ -139,14 +134,13 @@ _get_root() {
i=1
p=1
if ! jd_rest GET "domain"; then
_err "error get domain list"
return 1
fi
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug2 "Checking domain: $h"
if ! jd_rest GET "domain"; then
_err "error get domain list"
return 1
fi
if [ -z "$h" ]; then
#not valid
_err "Invalid domain"
@@ -174,8 +168,6 @@ _get_root() {
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
jd_rest() {
mtd="$1"
@@ -228,7 +220,7 @@ jd_rest() {
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash"
_debug2 CanonicalRequest "$CanonicalRequest"
HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)"
HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)"
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
Algorithm="JDCLOUD2-HMAC-SHA256"
@@ -254,19 +246,19 @@ jd_rest() {
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
_secure_debug2 kSecretH "$kSecretH"
kDateH="$(printf '%s' "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)"
kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)"
_debug2 kDateH "$kDateH"
kRegionH="$(printf '%s' "$Region" | _hmac "$Hash" "$kDateH" hex)"
kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)"
_debug2 kRegionH "$kRegionH"
kServiceH="$(printf '%s' "$Service" | _hmac "$Hash" "$kRegionH" hex)"
kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)"
_debug2 kServiceH "$kServiceH"
kSigningH="$(printf '%s' "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
kSigningH="$(printf "%s" "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
_debug2 kSigningH "$kSigningH"
signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)"
signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)"
_debug2 signature "$signature"
Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
+189
View File
@@ -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
}
-121
View File
@@ -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
View File
@@ -104,9 +104,6 @@ _get_root_by_getList() {
return 1
fi
_namecheap_domain_list=$(echo "$response" | _egrep_o '<Domain [^>]*')
_debug2 domain_list "$_namecheap_domain_list"
i=2
p=1
@@ -123,7 +120,7 @@ _get_root_by_getList() {
return 1
fi
if ! _namecheap_is_our_dns "$h"; then
if ! _contains "$response" "$h"; then
_debug "$h not found"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
@@ -136,29 +133,6 @@ _get_root_by_getList() {
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() {
i=100
p=99
+22 -377
View File
@@ -5,324 +5,37 @@ Domains: netcup.de netcup.net
Site: netcup.eu/
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_netcup
Options:
NC_Apikey API Key. The new netcup REST API key (64 characters) or the legacy CCP API key
NC_Apipw API Password. Only used for the legacy CCP API
NC_CID Customer Number. Only used for the legacy CCP API
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.
NC_Apikey API Key
NC_Apipw API Password
NC_CID Customer Number
Author: linux-insideDE
'
NC_Apikey="${NC_Apikey:-$(_readaccountconf_mutable NC_Apikey)}"
NC_Apipw="${NC_Apipw:-$(_readaccountconf_mutable NC_Apipw)}"
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"
_nc_endrest="https://api.netcup.com/v1"
client=""
dns_netcup_add() {
fulldomain=$1
txtvalue=$2
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if ! _nc_check_credentials; then
_debug NC_Apikey "$NC_Apikey"
_login
if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then
_err "No Credentials given"
return 1
fi
_saveaccountconf_mutable NC_Apikey "$NC_Apikey"
if [ -n "$NC_Apipw" ]; then
_saveaccountconf_mutable NC_Apipw "$NC_Apipw"
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() {
_saveaccountconf_mutable NC_Apipw "$NC_Apipw"
_saveaccountconf_mutable NC_CID "$NC_CID"
fulldomain=$1
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=""
exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
exit=$(_math "$exit" + 1)
i=$exit
_nc_last=$(_nc_lastlevel "$i")
_nc_found=""
while
[ "$exit" -ge "$_nc_last" ]
[ "$exit" -gt 0 ]
do
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
@@ -331,45 +44,35 @@ _nc_legacy_add() {
domain="$tmp.$domain"
fi
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"
if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
_err "$msg"
return 1
else
_nc_found=1
break
fi
fi
fi
exit=$(_math "$exit" - 1)
done
if [ -z "$_nc_found" ]; then
_err "$msg"
_nc_nozone "$fulldomain"
return 1
fi
_nc_legacy_logout
logout
}
_nc_legacy_rm() {
dns_netcup_rm() {
_login
fulldomain=$1
txtvalue=$2
if ! _nc_legacy_login; then
return 1
fi
domain=""
exit=$(echo "$fulldomain" | tr -dc '.' | wc -c)
exit=$(_math "$exit" + 1)
i=$exit
rec=""
_nc_last=$(_nc_lastlevel "$i")
_nc_found=""
while
[ "$exit" -ge "$_nc_last" ]
[ "$exit" -gt 0 ]
do
tmp=$(echo "$fulldomain" | cut -d'.' -f"$exit")
if [ "$(_math "$i" - "$exit")" -eq 0 ]; then
@@ -378,7 +81,7 @@ _nc_legacy_rm() {
domain="$tmp.$domain"
fi
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')
_debug "$msg"
if [ "$(_getfield "$msg" "5" | sed 's/"statuscode"://g')" != 5028 ]; then
@@ -386,18 +89,12 @@ _nc_legacy_rm() {
_err "$msg"
return 1
else
_nc_found=1
break
fi
fi
fi
exit=$(_math "$exit" - 1)
done
if [ -z "$_nc_found" ]; then
_err "$msg"
_nc_nozone "$fulldomain"
return 1
fi
ida=0000
idv=0001
@@ -419,24 +116,17 @@ _nc_legacy_rm() {
i=0
fi
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"
if [ "$(_getfield "$msg" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
_err "$msg"
return 1
fi
_nc_legacy_logout
logout
}
_nc_legacy_login() {
# never send the REST API Bearer header (or auth headers of another
# 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")
_login() {
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
_debug "$tmp"
if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
@@ -444,56 +134,11 @@ _nc_legacy_login() {
return 1
fi
}
_nc_legacy_logout() {
tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$_nc_apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
logout() {
tmp=$(_post "{\"action\": \"logout\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apisessionid\": \"$sid\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
_debug "$tmp"
if [ "$(_getfield "$tmp" "4" | sed s/\"status\":\"//g | sed s/\"//g)" != "success" ]; then
_err "$tmp"
return 1
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."
}
-244
View File
@@ -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
}
-229
View File
@@ -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
}
-444
View File
@@ -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
}
+1 -1
View File
@@ -227,7 +227,7 @@ _poweradmin_rest() {
return 1
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"
_debug "Response: $response"
return 1
+1 -1
View File
@@ -71,7 +71,7 @@ dns_rage4_rm() {
_debug "Getting txt records"
_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
_err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'."
return 1
-145
View File
@@ -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
}
+1 -1
View File
@@ -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}\"}}}}}"
export _H1="Content-Type: application/json"
_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)
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"
+2 -5
View File
@@ -42,10 +42,7 @@ dns_selfhost_add() {
# 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
# 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"
# 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")
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed -n -E "s/(^|^.*[[:space:]])($fulldomain)(:[[:digit:]]+)([:]?[[:digit:]]*)(.*)/\2\3\4/p")
_debug2 mapEntry "$mapEntry"
if test -z "$mapEntry"; then
_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)
# 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"
lastUsedRidForDomain=$(echo "$lastUsedRidForDomainEntry" | cut -d: -f2)
+2 -2
View File
@@ -145,8 +145,8 @@ _udr_rest() {
_debug data "${data}"
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')
_description=$(echo "$response" | _egrep_o "description = .*" | _head_n 1 | cut -d = -f 2 | tr -d '\r' | sed -e 's/^[ ]*//' -e 's/[ ]*$//')
_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 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
_debug response_code "$_code"
_debug response_description "$_description"
+2 -13
View File
@@ -61,7 +61,7 @@ dns_world4you_add() {
if _contains "$res" "successfully"; then
return 0
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
_err "Unable to add record: Unknown error"
echo "$ret" >'error-01.html'
@@ -125,7 +125,7 @@ dns_world4you_rm() {
if _contains "$res" "successfully"; then
return 0
else
msg=$(_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
_err "Unable to remove record: Unknown error"
echo "$ret" >'error-01.html'
@@ -145,17 +145,6 @@ dns_world4you_rm() {
################ 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
_login() {
WORLD4YOU_USERNAME="${WORLD4YOU_USERNAME:-$(_readaccountconf_mutable WORLD4YOU_USERNAME)}"
+2 -2
View File
@@ -149,7 +149,7 @@ _check_variables() {
org_response="$(echo "$org_response" | _normalizeJson)"
YANDEX360_ORG_ID=$(
echo "$org_response" |
_egrep_o '"id":[ ]*[0-9]+' |
_egrep_o '"id":[[:space:]]*[0-9]+' |
cut -d':' -f2
)
_debug 'Automatically retrieved YANDEX360_ORG_ID' "$YANDEX360_ORG_ID"
@@ -216,7 +216,7 @@ _get_token() {
interval=$(
echo "$response" |
_egrep_o '"interval":[ ]*[0-9]+' |
_egrep_o '"interval":[[:space:]]*[0-9]+' |
cut -d':' -f2
)
_debug 'Polling interval' "$interval"
+15 -51
View File
@@ -22,32 +22,21 @@ dns_yc_add() {
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
txtvalue=$2
# YC_SA_Key_File_PEM_b64/Path are always persisted to the domain conf below,
# so they must be recovered from there first (account conf is only a
# fallback for the YC_Folder_ID case, see the SA_ID/SA_Key_ID save below).
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
if [ "$YC_SA_Key_File_PEM_b64" ]; then
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
YC_SA_Key_File="private.key"
_yc_key_is_temp=1
_savedomainconf YC_SA_Key_File_PEM_b64 "$YC_SA_Key_File_PEM_b64"
else
YC_SA_Key_File="$YC_SA_Key_File_Path"
_yc_key_is_temp=""
_savedomainconf YC_SA_Key_File_Path "$YC_SA_Key_File_Path"
fi
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
if [ "$YC_SA_ID" ] && [ "$YC_SA_Key_ID" ] && [ "$YC_SA_Key_File" ]; then
@@ -76,21 +65,11 @@ dns_yc_add() {
return 1
fi
else
# Clear both possible stores -- YC_Zone_ID/YC_Folder_ID/key material are
# persisted to the domain conf, while YC_SA_ID/YC_SA_Key_ID may have been
# saved account-wide (Folder_ID mode), so a plain _clearaccountconf alone
# would leave stale values behind in whichever store wasn't touched.
_cleardomainconf YC_Zone_ID
_clearaccountconf YC_Zone_ID
_cleardomainconf YC_Folder_ID
_clearaccountconf YC_Folder_ID
_cleardomainconf YC_SA_ID
_clearaccountconf_mutable YC_SA_ID
_cleardomainconf YC_SA_Key_ID
_clearaccountconf_mutable YC_SA_Key_ID
_cleardomainconf YC_SA_Key_File_PEM_b64
_clearaccountconf YC_SA_ID
_clearaccountconf YC_SA_Key_ID
_clearaccountconf YC_SA_Key_File_PEM_b64
_cleardomainconf YC_SA_Key_File_Path
_clearaccountconf YC_SA_Key_File_Path
_err "You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
return 1
@@ -131,30 +110,11 @@ dns_yc_rm() {
fulldomain="$(echo "$1". | _lower_case)" # Add dot at end of domain name
txtvalue=$2
YC_Zone_ID="${YC_Zone_ID:-$(_readdomainconf YC_Zone_ID)}"
YC_Zone_ID="${YC_Zone_ID:-$(_readaccountconf_mutable YC_Zone_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readdomainconf YC_Folder_ID)}"
YC_Folder_ID="${YC_Folder_ID:-$(_readaccountconf_mutable YC_Folder_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readdomainconf YC_SA_ID)}"
YC_SA_ID="${YC_SA_ID:-$(_readaccountconf_mutable YC_SA_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readdomainconf YC_SA_Key_ID)}"
YC_SA_Key_ID="${YC_SA_Key_ID:-$(_readaccountconf_mutable YC_SA_Key_ID)}"
# See dns_yc_add() for why domain conf is checked before account conf.
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readdomainconf YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_PEM_b64="${YC_SA_Key_File_PEM_b64:-$(_readaccountconf_mutable YC_SA_Key_File_PEM_b64)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readdomainconf YC_SA_Key_File_Path)}"
YC_SA_Key_File_Path="${YC_SA_Key_File_Path:-$(_readaccountconf_mutable YC_SA_Key_File_Path)}"
if [ "$YC_SA_Key_File_PEM_b64" ]; then
echo "$YC_SA_Key_File_PEM_b64" | _dbase64 >private.key
YC_SA_Key_File="private.key"
_yc_key_is_temp=1
else
YC_SA_Key_File="$YC_SA_Key_File_Path"
_yc_key_is_temp=""
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
@@ -164,10 +124,16 @@ dns_yc_rm() {
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
# upsertRecordSets.deletions removes only the given value from the rrset,
# leaving any other values at the same name (e.g. base + wildcard domain)
# intact -- no need to read the current data set and recompute it.
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
_debug "Getting txt records"
if _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
exists_txtvalue=$(echo "$response" | _normalizeJson | _egrep_o "\"data\".*\][^,]*" | _egrep_o "[^:]*$")
_debug exists_txtvalue "$exists_txtvalue"
else
_err "Error: $response"
return 1
fi
if _yc_rest POST "zones/$_domain_id:updateRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":$exists_txtvalue}]}"; then
if _contains "$response" "\"done\": true"; then
_info "Delete, OK"
return 0
@@ -228,7 +194,7 @@ _get_root() {
return 1
fi
if _contains "$response" "\"zone\": \"$h\""; then
_domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')
_domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')
_debug _domain_id "$_domain_id"
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
@@ -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)
_debug2 _signature "$_signature"
if [ "$_yc_key_is_temp" ]; then
rm -f "$YC_SA_Key_File"
fi
rm -rf "$YC_SA_Key_File"
_jwt=$(printf "{\"jwt\": \"%s.%s.%s\"}" "$header" "$payload" "$_signature")
_debug2 _jwt "$_jwt"
@@ -300,7 +264,7 @@ _yc_login() {
_iam_response="$(_post "$_jwt" "https://iam.api.cloud.yandex.net/iam/v1/tokens" "" "POST")"
_debug3 _iam_response "$(echo "$_iam_response" | _normalizeJson)"
YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')"
YC_Token="$(echo "$_iam_response" | _normalizeJson | _egrep_o "\"iamToken\"[^,]*" | _egrep_o "[^:]*$" | tr -d '"')"
_debug3 YC_Token
return 0
-53
View File
@@ -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
View File
@@ -57,8 +57,9 @@ waha_send() {
_debug "_data" "$_data"
export _H1="Content-Type: application/json"
if [ "$WAHA_API_KEY" ]; then
export _H1="X-Api-Key: $WAHA_API_KEY"
export _H2="X-Api-Key: $WAHA_API_KEY"
fi
_waha_url="${WAHA_URL}/api/sendText"