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