mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-08-13 12:33:30 +02:00
Compare commits
246 commits
| 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 | ||
|
|
2feb392bd0 | ||
|
|
59a97d7f8b | ||
|
|
a836e747d1 | ||
|
|
3661fd86b6 | ||
|
|
745a42f193 | ||
|
|
8f98786d93 | ||
|
|
6d559ae69f | ||
|
|
24501ac0ca | ||
|
|
a82cf763cf | ||
|
|
3faf65c46d | ||
|
|
7cc16cd09a | ||
|
|
1dbabf0da9 | ||
|
|
2499ee1ab9 | ||
|
|
1a746d98b8 | ||
|
|
4558a8aa98 | ||
|
|
1a54307dbf | ||
|
|
020123d812 | ||
|
|
17964cfd6e | ||
|
|
d621d6952a | ||
|
|
5e33e9f5f1 | ||
|
|
3fddea2962 | ||
|
|
a6766d4186 | ||
|
|
b9ce911eb1 | ||
|
|
15a1067f1b | ||
|
|
447dc3c7e7 | ||
|
|
5a8c685fd3 | ||
|
|
dd6540ce46 | ||
|
|
cd486cfbb9 | ||
|
|
e828b285ad | ||
|
|
f5e7e6b225 | ||
|
|
d45b6fe8e6 | ||
|
|
237f2d9c3b | ||
|
|
ebb5cc4981 | ||
|
|
3989eef5e2 | ||
|
|
2c51ac1c27 | ||
|
|
eacf0d6a87 | ||
|
|
099d88e6a9 | ||
|
|
897e219743 | ||
|
|
a9590c5bd7 | ||
|
|
c0d62eb934 | ||
|
|
44c045b056 | ||
|
|
e94631de44 | ||
|
|
a49f8c1992 | ||
|
|
7d0283ca2c | ||
|
|
3e64b5e6f5 | ||
|
|
76811857a0 | ||
|
|
22a5ae3ceb | ||
|
|
04e0422526 | ||
|
|
2215f1b988 | ||
|
|
9366c2e065 | ||
|
|
2e4acba105 | ||
|
|
fef90e15e1 | ||
|
|
50e5e771d5 | ||
|
|
ebde8345ae | ||
|
|
58cd667d65 | ||
|
|
98c30912fb | ||
|
|
2058a77d83 | ||
|
|
660a5e322c | ||
|
|
2af543a358 | ||
|
|
fa763db105 | ||
|
|
90b4795bb1 | ||
|
|
534a1714dc | ||
|
|
45c0ad4112 | ||
|
|
83b52e0cd7 | ||
|
|
bed15ba844 | ||
|
|
1324dcd472 | ||
|
|
cf3eab95ee | ||
|
|
ca118be754 | ||
|
|
b1b539695f | ||
|
|
347cc207cd | ||
|
|
cc64a73230 | ||
|
|
1651a5a609 | ||
|
|
f2b37b32ff | ||
|
|
73df21abc4 | ||
|
|
0c76c1f211 | ||
|
|
919492df13 | ||
|
|
e00d3cfde3 | ||
|
|
e4eaa59063 | ||
|
|
4187ec23c1 | ||
|
|
2b5a19d34a | ||
|
|
9f0ef7abcd | ||
|
|
dc1b06006f | ||
|
|
8585d9f4a7 | ||
|
|
0e2659b768 | ||
|
|
ca8ab7f8b5 | ||
|
|
f50401a342 | ||
|
|
ff9b969bdb | ||
|
|
1f778e6ef1 | ||
|
|
58423df3e8 | ||
|
|
8f3c1701f3 | ||
|
|
2a175f97e8 | ||
|
|
d3af3315da | ||
|
|
7b6d96387c | ||
|
|
cabe432539 | ||
|
|
1746fbdb25 | ||
|
|
d2b3772631 | ||
|
|
e964157bff | ||
|
|
934711e51d | ||
|
|
7def43481a | ||
|
|
defd64022d | ||
|
|
4256e3532b | ||
|
|
1f94fd7fd5 | ||
|
|
507baff2ef | ||
|
|
504540e67c | ||
|
|
cacafc9c23 | ||
|
|
24895a15c8 | ||
|
|
1cd63e1480 | ||
|
|
31b13caf8b | ||
|
|
1e2cd50fc9 | ||
|
|
524d96a3a8 | ||
|
|
0eb5cc8384 | ||
|
|
77047eb0ef | ||
|
|
4978782fb8 | ||
|
|
9764f67619 | ||
|
|
988afd0f59 | ||
|
|
b92516f79e | ||
|
|
fbf3b41c54 | ||
|
|
ede9a86d46 | ||
|
|
843a7efa7d | ||
|
|
d3e12694b9 | ||
|
|
a105126063 | ||
|
|
917bebd460 | ||
|
|
33704fc274 | ||
|
|
bcbfe25d08 | ||
|
|
bbfb6f50ae | ||
|
|
6df2d9e451 | ||
|
|
1a36823461 | ||
|
|
8f2a476d21 | ||
|
|
e64529ab50 | ||
|
|
7653eaab31 | ||
|
|
6cd0c00a21 | ||
|
|
b4de9e8621 | ||
|
|
0a6abaf8a1 | ||
|
|
f4dc9fd9d1 | ||
|
|
2229330c48 | ||
|
|
0df051577c | ||
|
|
7fb40f0ccf | ||
|
|
780f2ad5dc | ||
|
|
adf69c4e7e | ||
|
|
ac5624536b | ||
|
|
b974bbd6d6 | ||
|
|
0ce8c24736 | ||
|
|
20254cbaf0 | ||
|
|
f4d2db64ef | ||
|
|
61400500e2 | ||
|
|
92a1b47108 | ||
|
|
5038d12d62 | ||
|
|
7e7c0ee984 | ||
|
|
92bd80c07d | ||
|
|
ad99628e50 | ||
|
|
992886c4eb | ||
|
|
eabd23a551 | ||
|
|
1241649501 | ||
|
|
9900adb007 | ||
|
|
01d6d46914 | ||
|
|
f038958192 | ||
|
|
fdf528c26c | ||
|
|
ced8d72808 | ||
|
|
116c05fbff | ||
|
|
42e13fa797 | ||
|
|
ccd2f04c33 | ||
|
|
a50fad865f | ||
|
|
5433ea86c8 | ||
|
|
e52d75d762 | ||
|
|
cb40b47a4e | ||
|
|
b039ff3087 | ||
|
|
c83eed4994 | ||
|
|
81100db2f3 | ||
|
|
a7ccfcf91d | ||
|
|
d0fcafe29b | ||
|
|
ae8ef74c4b | ||
|
|
0925d5c522 | ||
|
|
0d53d29f7e | ||
|
|
c38182897d | ||
|
|
2998106bd1 | ||
|
|
b3579ff18d | ||
|
|
0dc97187e1 | ||
|
|
d60c75b4e3 | ||
|
|
365d2d10f3 | ||
|
|
db098055de | ||
|
|
4575877d48 | ||
|
|
d98fa53f62 | ||
|
|
9b597b3f1b | ||
|
|
58d9c8d7f6 | ||
|
|
a2f046306e | ||
|
|
b463471951 | ||
|
|
0e5f1518aa | ||
|
|
3c848adbb7 | ||
|
|
2e4e5d7955 | ||
|
|
c7c903fba3 | ||
|
|
dfbe2c5bff | ||
|
|
5713c1d39d | ||
|
|
b7e9214e2d | ||
|
|
206f4494ac | ||
|
|
d9ce7fefa1 | ||
|
|
ce07759ced | ||
|
|
010bd1111a | ||
|
|
f265c304da | ||
|
|
85408bfb4d | ||
|
|
7c91ee1fc0 | ||
|
|
1687cbd5b4 | ||
|
|
d2780eb04e | ||
|
|
eaf4b62ba9 | ||
|
|
47378b5630 | ||
|
|
ac75c54ade | ||
|
|
36667ab656 | ||
|
|
7b19070d98 | ||
|
|
7985319687 | ||
|
|
7735cdf3ab | ||
|
|
d35876e885 | ||
|
|
355b121c79 | ||
|
|
3230d00c3d | ||
|
|
fe5490e0ec | ||
|
|
0d77231350 | ||
|
|
8d5a5a0e0d | ||
|
|
cbb8e9068c | ||
|
|
96f87844cd | ||
|
|
8e71268d03 | ||
|
|
10e7e458af |
128 changed files with 10038 additions and 1323 deletions
50
.github/workflows/Apache.yml
vendored
Normal file
50
.github/workflows/Apache.yml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: Apache
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Apache.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Apache.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Apache:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TestingDomain: example.com
|
||||
TEST_ACME_Server: https://localhost:14000/dir
|
||||
HTTPS_INSECURE: 1
|
||||
TEST_LOCAL: 1
|
||||
TEST_CA: "Pebble Intermediate CA"
|
||||
TEST_APACHE: 1
|
||||
CASE: le_test_apache
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat apache2
|
||||
- name: Run Pebble
|
||||
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||
- name: Set up Pebble
|
||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||
- name: Set up Apache
|
||||
# Apache serves on 5002, which is the HTTP-01 validation port in
|
||||
# Pebble's default config; acme.sh appends the challenge Alias to
|
||||
# the main config itself
|
||||
run: |
|
||||
echo "Listen 5002" | sudo tee /etc/apache2/ports.conf
|
||||
sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf
|
||||
sudo apache2ctl configtest
|
||||
sudo systemctl restart apache2
|
||||
curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404"
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
run: cd ../acmetest && sudo --preserve-env ./letest.sh
|
||||
306
.github/workflows/DNS.yml
vendored
306
.github/workflows/DNS.yml
vendored
|
|
@ -26,9 +26,9 @@ jobs:
|
|||
id: step_one
|
||||
run: |
|
||||
if [ "${{secrets.TokenName1}}" ] ; then
|
||||
echo "::set-output name=hasToken::true"
|
||||
echo "hasToken=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::set-output name=hasToken::false"
|
||||
echo "hasToken=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Check the value
|
||||
run: echo ${{ steps.step_one.outputs.hasToken }}
|
||||
|
|
@ -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,9 +114,11 @@ jobs:
|
|||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install tools
|
||||
run: brew install socat
|
||||
run: |
|
||||
brew untap aws/tap || true
|
||||
brew install socat
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
|
|
@ -165,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
|
||||
|
|
@ -176,9 +178,14 @@ jobs:
|
|||
C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git
|
||||
shell: cmd
|
||||
- name: Set ENV
|
||||
shell: cmd
|
||||
shell: bash
|
||||
run: |
|
||||
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV%
|
||||
echo 'PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin' >> "$GITHUB_ENV"
|
||||
# cygwin git sees the runner workspace as owned by another user and
|
||||
# fails with "dubious ownership" (exit 128) in the checkout post step
|
||||
echo 'GIT_CONFIG_COUNT=1' >> "$GITHUB_ENV"
|
||||
echo 'GIT_CONFIG_KEY_0=safe.directory' >> "$GITHUB_ENV"
|
||||
echo 'GIT_CONFIG_VALUE_0=*' >> "$GITHUB_ENV"
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
|
|
@ -224,12 +231,72 @@ 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
|
||||
sync: nfs
|
||||
run: |
|
||||
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||
fi
|
||||
if [ "${{ secrets.TokenName2}}" ] ; then
|
||||
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
|
||||
fi
|
||||
if [ "${{ secrets.TokenName3}}" ] ; then
|
||||
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
|
||||
fi
|
||||
if [ "${{ secrets.TokenName4}}" ] ; then
|
||||
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
|
||||
fi
|
||||
if [ "${{ secrets.TokenName5}}" ] ; then
|
||||
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
|
||||
fi
|
||||
cd ../acmetest
|
||||
./letest.sh
|
||||
- name: DebugOnError
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "See how to debug in VM:"
|
||||
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||
|
||||
|
||||
|
||||
GhostBSD:
|
||||
runs-on: ubuntu-latest
|
||||
needs: FreeBSD
|
||||
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
|
||||
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
|
||||
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
|
||||
CASE: le_test_dnsapi
|
||||
TEST_LOCAL: 1
|
||||
DEBUG: ${{ secrets.DEBUG }}
|
||||
http_proxy: ${{ secrets.http_proxy }}
|
||||
https_proxy: ${{ secrets.https_proxy }}
|
||||
TokenName1: ${{ secrets.TokenName1}}
|
||||
TokenName2: ${{ secrets.TokenName2}}
|
||||
TokenName3: ${{ secrets.TokenName3}}
|
||||
TokenName4: ${{ secrets.TokenName4}}
|
||||
TokenName5: ${{ secrets.TokenName5}}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- uses: vmactions/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
|
||||
|
|
@ -262,7 +329,7 @@ jobs:
|
|||
|
||||
OpenBSD:
|
||||
runs-on: ubuntu-latest
|
||||
needs: FreeBSD
|
||||
needs: GhostBSD
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
|
|
@ -280,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
|
||||
|
|
@ -336,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
|
||||
|
|
@ -393,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
|
||||
|
|
@ -454,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
|
||||
|
|
@ -511,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: |
|
||||
|
|
@ -570,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
|
||||
|
|
@ -626,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
|
||||
|
|
@ -661,7 +735,7 @@ jobs:
|
|||
|
||||
|
||||
|
||||
Haiku:
|
||||
Tribblix:
|
||||
runs-on: ubuntu-latest
|
||||
needs: OpenIndiana
|
||||
env:
|
||||
|
|
@ -675,26 +749,204 @@ jobs:
|
|||
DEBUG: ${{ secrets.DEBUG }}
|
||||
http_proxy: ${{ secrets.http_proxy }}
|
||||
https_proxy: ${{ secrets.https_proxy }}
|
||||
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root
|
||||
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Tribblix doesn't accept the expired ISRG X1 root
|
||||
TokenName1: ${{ secrets.TokenName1}}
|
||||
TokenName2: ${{ secrets.TokenName2}}
|
||||
TokenName3: ${{ secrets.TokenName3}}
|
||||
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
|
||||
- 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: rsync
|
||||
copyback: false
|
||||
prepare: |
|
||||
mkdir -p /boot/home/.cache
|
||||
pkgman install -y cronie
|
||||
|
||||
sync: nfs
|
||||
prepare: zap install socat
|
||||
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"
|
||||
|
||||
|
||||
|
||||
Haiku:
|
||||
runs-on: ubuntu-latest
|
||||
needs: Tribblix
|
||||
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, since OpenIndiana doesn't accept the expired ISRG X1 root
|
||||
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/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"
|
||||
|
|
|
|||
83
.github/workflows/GhostBSD.yml
vendored
Normal file
83
.github/workflows/GhostBSD.yml
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
name: GhostBSD
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/GhostBSD.yml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/GhostBSD.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
GhostBSD:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
CA_ECDSA: ""
|
||||
CA: ""
|
||||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
CA_ECDSA: ""
|
||||
CA: ""
|
||||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_EMAIL: "githubtest@acme.sh"
|
||||
# TEST_PREFERRED_CHAIN: ""
|
||||
runs-on: ubuntu-latest
|
||||
# GhostBSD VM frequently flakes on boot/ssh; don't let it fail the whole run
|
||||
continue-on-error: true
|
||||
env:
|
||||
TEST_LOCAL: 1
|
||||
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
|
||||
CA_ECDSA: ${{ matrix.CA_ECDSA }}
|
||||
CA: ${{ matrix.CA }}
|
||||
CA_EMAIL: ${{ matrix.CA_EMAIL }}
|
||||
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
|
||||
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- 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/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"
|
||||
prepare: pkg install -y socat curl wget
|
||||
usesh: true
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
- name: DebugOnError
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "See how to debug in VM:"
|
||||
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||
1
.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"
|
||||
|
|
|
|||
66
.github/workflows/Nginx.yml
vendored
Normal file
66
.github/workflows/Nginx.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
name: Nginx
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Nginx.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Nginx.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Nginx:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TestingDomain: example.com
|
||||
TEST_ACME_Server: https://localhost:14000/dir
|
||||
HTTPS_INSECURE: 1
|
||||
TEST_LOCAL: 1
|
||||
TEST_CA: "Pebble Intermediate CA"
|
||||
TEST_NGINX: 1
|
||||
CASE: le_test_nginx
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat nginx
|
||||
- name: Run Pebble
|
||||
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||
- name: Set up Pebble
|
||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||
- name: Set up nginx
|
||||
# a backend on 8081 plus a site with an aaPanel/BT style
|
||||
# "location ^~ /" proxy block that shadows plain regex locations
|
||||
# (regression for #6125); the site listens on 5002, which is the
|
||||
# HTTP-01 validation port in Pebble's default config
|
||||
run: |
|
||||
sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF'
|
||||
server {
|
||||
listen 127.0.0.1:8081;
|
||||
location / {
|
||||
default_type text/plain;
|
||||
return 200 "backend";
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 5002 default_server;
|
||||
server_name example.com;
|
||||
location ^~ / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
sudo nginx -t
|
||||
sudo systemctl restart nginx
|
||||
curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
run: cd ../acmetest && sudo --preserve-env ./letest.sh
|
||||
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"
|
||||
|
|
|
|||
80
.github/workflows/Tribblix.yml
vendored
Normal file
80
.github/workflows/Tribblix.yml
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
name: Tribblix
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Tribblix.yml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Tribblix.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
Tribblix:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
CA_ECDSA: ""
|
||||
CA: ""
|
||||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
- TEST_ACME_Server: "LetsEncrypt.org_test"
|
||||
CA_ECDSA: ""
|
||||
CA: ""
|
||||
CA_EMAIL: ""
|
||||
TEST_PREFERRED_CHAIN: (STAGING)
|
||||
ACME_USE_WGET: 1
|
||||
#- TEST_ACME_Server: "ZeroSSL.com"
|
||||
# CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
|
||||
# CA: "ZeroSSL RSA DV SSL CA 2"
|
||||
# CA_EMAIL: "githubtest@acme.sh"
|
||||
# TEST_PREFERRED_CHAIN: ""
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_LOCAL: 1
|
||||
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
|
||||
CA_ECDSA: ${{ matrix.CA_ECDSA }}
|
||||
CA: ${{ matrix.CA }}
|
||||
CA_EMAIL: ${{ matrix.CA_EMAIL }}
|
||||
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
|
||||
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- 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/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"
|
||||
prepare: zap install socat curl wget
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
- name: DebugOnError
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
echo "See how to debug in VM:"
|
||||
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||
114
.github/workflows/blacklist-command.yml
vendored
Normal file
114
.github/workflows/blacklist-command.yml
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
name: Blacklist Command
|
||||
|
||||
# An issue titled "blacklist: <login-or-email>" opened by the maintainer
|
||||
# or a write-access member adds that identity to the Blacklist wiki page
|
||||
# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification
|
||||
# embeds a prefilled link that opens such an issue in one click.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
# Share the wiki-guard concurrency group so we never push to the wiki
|
||||
# at the same time as the guard.
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
blacklist:
|
||||
# Upstream only: forks have no <fork>.wiki repository to push to.
|
||||
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'blacklist:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check authorization
|
||||
id: auth
|
||||
run: |
|
||||
assoc="${{ github.event.issue.author_association }}"
|
||||
case "$assoc" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "issue author is not authorized ($assoc); ignoring"
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout wiki repository
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
|
||||
- name: Add the identity to the blacklist page
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
id: add
|
||||
env:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
target="$(printf '%s' "$TITLE" \
|
||||
| sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \
|
||||
| tr 'A-Z' 'a-z')"
|
||||
case "$target" in
|
||||
''|*[!a-z0-9._+@-]*)
|
||||
echo "invalid target: '$target'"
|
||||
echo "result=invalid" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
cd wiki
|
||||
if [ ! -e Blacklist.md ]; then
|
||||
echo "result=nopage" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if grep -Fxiq -- "- $target" Blacklist.md; then
|
||||
echo "result=already" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$(tail -c1 Blacklist.md)" ]; then
|
||||
echo >> Blacklist.md
|
||||
fi
|
||||
printf -- '- %s\n' "$target" >> Blacklist.md
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Blacklist.md
|
||||
git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
echo "result=added" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Reply and close
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
RESULT: ${{ steps.add.outputs.result }}
|
||||
TARGET: ${{ steps.add.outputs.target }}
|
||||
with:
|
||||
script: |
|
||||
const result = process.env.RESULT;
|
||||
const target = process.env.TARGET;
|
||||
const messages = {
|
||||
added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`,
|
||||
already: `\`${target}\` is already on the blacklist.`,
|
||||
invalid: "Could not parse a valid login or email from the issue title.",
|
||||
nopage: "The Blacklist wiki page does not exist."
|
||||
};
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: messages[result] || "No action taken."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
state_reason: result === "added" ? "completed" : "not_planned"
|
||||
});
|
||||
18
.github/workflows/dockerhub.yml
vendored
18
.github/workflows/dockerhub.yml
vendored
|
|
@ -41,23 +41,29 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: CheckToken
|
||||
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${DOCKER_IMAGE}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: login to docker hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
- name: login to ghcr
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
- name: build and push the image
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
|
|
@ -73,6 +79,8 @@ jobs:
|
|||
fi
|
||||
fi
|
||||
|
||||
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
|
||||
|
||||
DOCKER_LABELS=()
|
||||
while read -r label; do
|
||||
DOCKER_LABELS+=(--label "${label}")
|
||||
|
|
@ -84,3 +92,9 @@ jobs:
|
|||
--output "type=image,push=true" \
|
||||
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
|
||||
- name: mirror the image to ghcr (best-effort)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
|
||||
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|
||||
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
|
||||
|
|
|
|||
120
.github/workflows/issue.yml
vendored
120
.github/workflows/issue.yml
vendored
|
|
@ -2,18 +2,128 @@ name: "Update issues"
|
|||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
const item = context.payload.issue || context.payload.pull_request;
|
||||
|
||||
// Close on sight anything opened by a user on the wiki Blacklist
|
||||
// page (maintained by the Wiki Guard workflow).
|
||||
let blacklist = [];
|
||||
try {
|
||||
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
|
||||
if (res.ok) {
|
||||
blacklist = (await res.text()).split("\n")
|
||||
.filter(l => l.startsWith("- "))
|
||||
.map(l => l.slice(2).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Failed to fetch the blacklist: ${e}`);
|
||||
}
|
||||
// A comment on a closed tracking issue reopens it (the standard
|
||||
// closing note promises this). Bots, blacklisted users and the
|
||||
// maintainer's own comments don't reopen.
|
||||
if (context.eventName === "issue_comment") {
|
||||
const issue = context.payload.issue;
|
||||
const commenter = context.payload.comment.user;
|
||||
if (issue.pull_request || issue.state !== "closed") {
|
||||
return;
|
||||
}
|
||||
if (!/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
|
||||
return;
|
||||
}
|
||||
if (commenter.type === "Bot" ||
|
||||
commenter.login.toLowerCase() === "neilpang" ||
|
||||
blacklist.includes(commenter.login.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "open"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (blacklist.includes(item.user.login.toLowerCase())) {
|
||||
if (context.payload.pull_request) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: item.number,
|
||||
state: "closed"
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.payload.pull_request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = context.payload.issue;
|
||||
if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) {
|
||||
// Handled by the Blacklist / Revert Command workflows.
|
||||
return;
|
||||
}
|
||||
if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
|
||||
// Tracking issue for a third-party dns/deploy/notify api:
|
||||
// no upgrade boilerplate; assign it to the opener, label it,
|
||||
// then close it right away to keep the issue list clean. Any
|
||||
// later comment reopens it (see the issue_comment handler).
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: [issue.user.login]
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["3rd party api"]
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: "Closing this tracking issue for now to keep the issue list clean. It remains the place to report problems with this provider -- if you hit a bug, comment here and the issue will be reopened."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "completed"
|
||||
});
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you."
|
||||
|
||||
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
|
||||
})
|
||||
2
.github/workflows/pr_dns.yml
vendored
2
.github/workflows/pr_dns.yml
vendored
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
|
|
|||
2
.github/workflows/pr_notify.yml
vendored
2
.github/workflows/pr_notify.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
|
|
|||
110
.github/workflows/revert-command.yml
vendored
Normal file
110
.github/workflows/revert-command.yml
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
name: Revert Command
|
||||
|
||||
# An issue titled "revert: <wiki-commit-sha>" opened by the maintainer or
|
||||
# a write-access member reverts that commit in the wiki repository and
|
||||
# closes the issue. The wiki-monitor notification embeds a prefilled link
|
||||
# that opens such an issue in one click.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
# Share the wiki-guard concurrency group so we never push to the wiki
|
||||
# at the same time as the guard.
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
# Upstream only: forks have no <fork>.wiki repository to push to.
|
||||
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'revert:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check authorization
|
||||
id: auth
|
||||
run: |
|
||||
assoc="${{ github.event.issue.author_association }}"
|
||||
case "$assoc" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "issue author is not authorized ($assoc); ignoring"
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout wiki repository
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Revert the wiki commit
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
id: revert
|
||||
env:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
target="$(printf '%s' "$TITLE" \
|
||||
| sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \
|
||||
| tr 'A-Z' 'a-z')"
|
||||
case "$target" in
|
||||
*[!0-9a-f]*|"")
|
||||
echo "invalid commit sha: '$target'"
|
||||
echo "result=invalid" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
cd wiki
|
||||
if ! git cat-file -e "$target^{commit}" 2>/dev/null; then
|
||||
echo "result=notfound" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git revert --no-edit "$target"; then
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
echo "result=reverted" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git revert --abort || true
|
||||
echo "result=conflict" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Reply and close
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
RESULT: ${{ steps.revert.outputs.result }}
|
||||
TARGET: ${{ steps.revert.outputs.target }}
|
||||
with:
|
||||
script: |
|
||||
const result = process.env.RESULT;
|
||||
const target = process.env.TARGET;
|
||||
const messages = {
|
||||
reverted: `Wiki commit \`${target}\` has been reverted.`,
|
||||
conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`,
|
||||
notfound: `Commit \`${target}\` was not found in the wiki repository.`,
|
||||
invalid: "Could not parse a commit sha from the issue title."
|
||||
};
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: messages[result] || "No action taken."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
state_reason: result === "reverted" ? "completed" : "not_planned"
|
||||
});
|
||||
32
.github/workflows/vtag.yml
vendored
Normal file
32
.github/workflows/vtag.yml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Mirror version tag
|
||||
|
||||
# Historical release tags are plain version numbers ("3.1.3") and cannot be
|
||||
# renamed. When a plain version tag is pushed (including the tag created by
|
||||
# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3")
|
||||
# pointing to the same object, so both forms exist.
|
||||
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
|
||||
# refs created with GITHUB_TOKEN do not fire workflows anyway.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
vtag:
|
||||
if: github.repository == 'acmesh-official/acme.sh'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create the v-prefixed tag
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if gh api "repos/${{ github.repository }}/git/ref/tags/v${{ github.ref_name }}" >/dev/null 2>&1; then
|
||||
echo "Tag v${{ github.ref_name }} already exists, nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
gh api "repos/${{ github.repository }}/git/refs" -f ref="refs/tags/v${{ github.ref_name }}" -f sha="${{ github.sha }}"
|
||||
echo "Created tag v${{ github.ref_name }} -> ${{ github.sha }}"
|
||||
325
.github/workflows/wiki-guard.yml
vendored
Normal file
325
.github/workflows/wiki-guard.yml
vendored
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
name: Wiki Guard
|
||||
|
||||
# Rules enforced here:
|
||||
# - Only the maintainer and write-access members may delete or rename wiki
|
||||
# pages. Anyone else doing so gets blacklisted and the page restored to
|
||||
# its last good revision.
|
||||
# - Only the maintainer and write-access members may edit the Blacklist
|
||||
# wiki page. Anyone else touching it gets blacklisted and the page
|
||||
# reverted.
|
||||
# - Any wiki change made by a blacklisted identity is reverted.
|
||||
# A "good" revision is one authored by the maintainer, by this bot, or by
|
||||
# a non-blacklisted user -- restoring from the deleted commit's parent is
|
||||
# NOT safe, because vandals replace a page before destroying it and the
|
||||
# parent would launder their version into a bot commit.
|
||||
# The gollum event only fires on page create/update, never on deletion,
|
||||
# so violations are caught by polling the wiki git history.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/10 * * * *"
|
||||
gollum:
|
||||
# Piggyback on frequent repo activity, because the cron schedule is
|
||||
# best-effort and often delayed well beyond its interval.
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
# Forks have no <fork>.wiki repository, so the checkout below would
|
||||
# fail there -- run only in the upstream repository.
|
||||
if: github.repository == 'acmesh-official/acme.sh'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Enforce wiki rules
|
||||
id: guard
|
||||
env:
|
||||
# WIKI_GUARD_TOKEN: a PAT with read:org, needed to enumerate
|
||||
# members whose write access comes via the organization -- the
|
||||
# repo-scoped GITHUB_TOKEN only sees direct collaborators.
|
||||
GH_TOKEN: ${{ secrets.WIKI_GUARD_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Logins with write (push) access to the repository, including
|
||||
# organization members -- they may delete/rename pages and edit
|
||||
# the blacklist just like the maintainer. If the API call fails,
|
||||
# the list stays empty and enforcement falls back to
|
||||
# maintainer-only, which is the safe direction.
|
||||
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
|
||||
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
|
||||
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
|
||||
echo "write-access members loaded: $(wc -l < writers.txt)"
|
||||
cd wiki
|
||||
git config core.quotePath false
|
||||
|
||||
# Any author email under this domain is the maintainer.
|
||||
OWNER_DOMAIN="neilpang.com"
|
||||
# Our own enforcement commits.
|
||||
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BL_PAGE="Blacklist.md"
|
||||
# Rolling window; the cron runs every 10 minutes, so 7 days gives
|
||||
# ample overlap without re-judging old changes the maintainer
|
||||
# already accepted.
|
||||
WINDOW="7 days ago"
|
||||
|
||||
: > ../actions.txt
|
||||
: > ../bl_new.txt
|
||||
|
||||
is_owner() {
|
||||
case "$1" in
|
||||
*@"$OWNER_DOMAIN") return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_bot() {
|
||||
[ "$1" = "$BOT_EMAIL" ]
|
||||
}
|
||||
|
||||
author_email() {
|
||||
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
|
||||
}
|
||||
|
||||
# Identity of a commit author: the GitHub login when the email is a
|
||||
# users.noreply.github.com address, otherwise the email itself.
|
||||
identity_of() {
|
||||
case "$1" in
|
||||
*+*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_blacklisted() {
|
||||
grep -Fxq "$1" ../bl_all.txt
|
||||
}
|
||||
|
||||
# Trusted committers: the maintainer (by email domain), this bot,
|
||||
# and anyone whose GitHub login has write access to the repo.
|
||||
is_trusted() {
|
||||
if is_owner "$1" || is_bot "$1"; then
|
||||
return 0
|
||||
fi
|
||||
grep -Fxq "$(identity_of "$1")" ../writers.txt
|
||||
}
|
||||
|
||||
# Newest commit on file $1 authored by a non-blacklisted user.
|
||||
last_good_for() {
|
||||
for g in $(git log --format=%H --no-renames -- "$1"); do
|
||||
gae="$(author_email "$g")"
|
||||
if is_trusted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
gid="$(identity_of "$gae")"
|
||||
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -e "$BL_PAGE" ]; then
|
||||
page_existed=1
|
||||
else
|
||||
page_existed=""
|
||||
fi
|
||||
|
||||
# ---- 1. Last good version of the blacklist page: the newest
|
||||
# revision authored by the maintainer or by this bot. Everything
|
||||
# else on that page is tampering and is discarded.
|
||||
bl_good_commit=""
|
||||
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
bl_good_commit="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
|
||||
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
|
||||
else
|
||||
{
|
||||
echo "# Blacklist"
|
||||
echo ""
|
||||
echo "Users listed below violated the wiki rules (deleted or renamed"
|
||||
echo "pages, or tampered with this page). Their new issues and pull"
|
||||
echo "requests are closed on sight and their wiki edits are reverted"
|
||||
echo "automatically. Only the maintainer and write-access members"
|
||||
echo "may edit this page."
|
||||
echo ""
|
||||
echo "To pardon a user while their violation is still inside the"
|
||||
echo "scan window, replace their entry with: pardon: username"
|
||||
echo ""
|
||||
} > ../bl_page.txt
|
||||
fi
|
||||
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
|
||||
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
|
||||
|
||||
bl_add() {
|
||||
if grep -Fxq "$1" ../bl_pardon.txt; then
|
||||
return 0
|
||||
fi
|
||||
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
|
||||
printf '%s\n' "$1" >> ../bl_new.txt
|
||||
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- 2. Blacklist everyone who deleted or renamed a page.
|
||||
# --no-renames makes a rename count as a deletion of the old path.
|
||||
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
# ---- 3. Blacklist everyone else who touched the blacklist page.
|
||||
# The revert of their tampering falls out of steps 5 and 6.
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
sort -u ../bl_new.txt > ../bl_new_u.txt
|
||||
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
|
||||
|
||||
# ---- 4. Restore pages that are currently missing because a
|
||||
# non-maintainer deleted them, using the last good revision.
|
||||
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
|
||||
| sort -u \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
|
||||
continue
|
||||
fi
|
||||
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
|
||||
if [ -z "$del" ]; then
|
||||
continue
|
||||
fi
|
||||
ae="$(author_email "$del")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- 5. Revert every recent change made by a blacklisted
|
||||
# identity: each touched file goes back to its newest revision
|
||||
# authored by a non-blacklisted user; a file that has no such
|
||||
# revision (they created it) is removed.
|
||||
if [ -s ../bl_all.txt ]; then
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
id="$(identity_of "$ae")"
|
||||
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
git show --name-only --no-renames --format= "$c" \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
want="$(git rev-parse "$good:$f")"
|
||||
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
|
||||
if [ "$want" != "$have" ]; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
|
||||
fi
|
||||
elif [ -e "$f" ]; then
|
||||
git rm -q -- "$f"
|
||||
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# ---- 6. Regenerate the blacklist page: the last good text plus
|
||||
# any newly blacklisted identities. This both reverts tampering
|
||||
# and records new violators; manual edits by the maintainer are
|
||||
# preserved as the new good text.
|
||||
cp ../bl_page.txt ../bl_page_new.txt
|
||||
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
|
||||
echo >> ../bl_page_new.txt
|
||||
fi
|
||||
while IFS= read -r id; do
|
||||
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
|
||||
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
|
||||
fi
|
||||
done < ../bl_new_u.txt
|
||||
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
|
||||
cp ../bl_page_new.txt "$BL_PAGE"
|
||||
git add -- "$BL_PAGE"
|
||||
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
|
||||
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 7. Commit, push, notify.
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
git commit -m "wiki-guard: restore pages and enforce blacklist"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
fi
|
||||
if [ -s ../actions.txt ]; then
|
||||
{
|
||||
echo "The wiki guard handled the following rule violations:"
|
||||
echo ""
|
||||
cat ../actions.txt
|
||||
echo ""
|
||||
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
|
||||
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
|
||||
} > ../guard-msg.txt
|
||||
echo "acted=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No rule violations found."
|
||||
echo "acted=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
if: steps.guard.outputs.acted == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki guard: rule violations handled"
|
||||
content-filepath: ./guard-msg.txt
|
||||
assignees: Neilpang
|
||||
24
.github/workflows/wiki-monitor.yml
vendored
24
.github/workflows/wiki-monitor.yml
vendored
|
|
@ -9,13 +9,14 @@ jobs:
|
|||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate wiki change message
|
||||
id: msg
|
||||
run: |
|
||||
actor="${{ github.actor }}"
|
||||
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
|
||||
|
|
@ -27,6 +28,17 @@ jobs:
|
|||
now="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
cd wiki
|
||||
# Skip notification when the change was authored by the
|
||||
# maintainer himself (any author email under neilpang.com),
|
||||
# e.g. a direct git push to the wiki repository.
|
||||
author_email=$(git show -s --format=%ae "$page_sha" 2>/dev/null | tr 'A-Z' 'a-z')
|
||||
case "$author_email" in
|
||||
*@neilpang.com)
|
||||
echo "Change authored by maintainer ($author_email); skipping notification."
|
||||
echo "notify=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
|
||||
if [ -n "$prev_sha" ]; then
|
||||
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
|
||||
|
|
@ -41,17 +53,21 @@ jobs:
|
|||
echo "Time: $now"
|
||||
echo "Page: [$page_name]($page_url) (Action: $page_action)"
|
||||
echo "Comment: $page_summary"
|
||||
echo "[Click here to Revert](${page_url}/_history)"
|
||||
echo "[Click here to Revert](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)"
|
||||
echo ""
|
||||
echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)"
|
||||
echo ""
|
||||
echo "----"
|
||||
echo "### diff:"
|
||||
echo "### diff:"
|
||||
echo '```diff'
|
||||
cat wiki.diff
|
||||
echo '```'
|
||||
} > wiki-change-msg.txt
|
||||
echo "notify=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
uses: peter-evans/create-issue-from-file@v5
|
||||
if: steps.msg.outputs.notify == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki edited"
|
||||
content-filepath: ./wiki-change-msg.txt
|
||||
|
|
|
|||
8
CONTRIBUTING.md
Normal file
8
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Contributing
|
||||
|
||||
1. Do NOT send pull request to `master` branch.
|
||||
Please send to `dev` branch instead.
|
||||
Any PR to `master` branch will NOT be merged.
|
||||
|
||||
2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
|
||||
You will NOT get any review without passing this guide. You also need to fix the CI errors.
|
||||
|
|
@ -81,8 +81,8 @@ if [ \"\$1\" = \"daemon\" ]; then \n \
|
|||
echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \
|
||||
time=\$(date -u \"+%s\") \n \
|
||||
random_minute=\$((\$time % 60)) \n \
|
||||
random_hour=\$((\$time / 60 % 24)) \n \
|
||||
echo \"\$random_minute \$random_hour * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \
|
||||
random_hour=\$((\$time / 60 % 6)) \n \
|
||||
echo \"\$random_minute \$random_hour,\$((\$random_hour + 6)),\$((\$random_hour + 12)),\$((\$random_hour + 18)) * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \
|
||||
fi \n \
|
||||
echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \
|
||||
exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \
|
||||
|
|
|
|||
137
README.md
137
README.md
|
|
@ -1,7 +1,21 @@
|
|||
<p align="center">
|
||||
<a href="https://zerossl.com/?fromacme.sh">
|
||||
<img src="https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05" alt="zerossl.com">
|
||||
</a>
|
||||
<a href="https://zerossl.com?utm_source=acme-sh">
|
||||
<picture>
|
||||
<!-- Dark mode -->
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://github.com/user-attachments/assets/1308516b-e0cc-496d-b5df-e3932423ead6" />
|
||||
<!-- Light mode -->
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043" />
|
||||
<!-- Fallback for environments without media queries -->
|
||||
<img
|
||||
alt="ZeroSSL"
|
||||
src="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043"
|
||||
height="auto" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">🔐 acme.sh</h1>
|
||||
|
|
@ -17,9 +31,13 @@
|
|||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg" alt="Solaris"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg" alt="DragonFlyBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MidnightBSD.yml/badge.svg" alt="MidnightBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg" alt="GhostBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg" alt="Omnios"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg" alt="Tribblix"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/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">
|
||||
|
|
@ -112,6 +130,10 @@
|
|||
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|
||||
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|
||||
|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)
|
||||
|
|
@ -146,6 +168,7 @@
|
|||
| 🌐 DNS mode | Use DNS TXT records |
|
||||
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
|
||||
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
|
||||
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -396,7 +419,53 @@ acme.sh --renew -d example.com
|
|||
|
||||
---
|
||||
|
||||
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
|
||||
### 🔟 Use DNS Persist Mode
|
||||
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
|
||||
|
||||
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
|
||||
|
||||
DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
|
||||
|
||||
#### 🪄 Step 1: Print the TXT record value
|
||||
|
||||
```bash
|
||||
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
|
||||
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
|
||||
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
|
||||
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
|
||||
|
||||
You should get an output like:
|
||||
|
||||
```sh
|
||||
TXT persist domain:_validation-persist.example.com
|
||||
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
|
||||
```
|
||||
|
||||
#### ✍️ Step 2: Add the TXT record to your DNS
|
||||
|
||||
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
|
||||
|
||||
#### 📜 Step 3: Issue the certificate
|
||||
|
||||
```bash
|
||||
acme.sh --issue -d example.com --dns-persist
|
||||
```
|
||||
|
||||
✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
|
||||
|
||||
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA)
|
||||
|
||||
Just set the `keylength` to a valid, supported value.
|
||||
|
||||
|
|
@ -427,7 +496,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com -
|
|||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Wildcard Certificates
|
||||
### 1️⃣2️⃣ Issue Wildcard Certificates
|
||||
|
||||
It's simple! Just give a wildcard domain as the `-d` parameter:
|
||||
|
||||
|
|
@ -439,9 +508,9 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
|
|||
|
||||
---
|
||||
|
||||
### 1️⃣2️⃣ How to Renew Certificates
|
||||
### 1️⃣3️⃣ How to Renew Certificates
|
||||
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below).
|
||||
|
||||
However, you can force a renewal:
|
||||
|
||||
|
|
@ -455,9 +524,43 @@ acme.sh --renew -d example.com --force
|
|||
acme.sh --renew -d example.com --force --ecc
|
||||
```
|
||||
|
||||
#### 📡 ACME Renewal Information (ARI) — RFC 9773
|
||||
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
|
||||
|
||||
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
|
||||
|
||||
| What | When | Why |
|
||||
|------|------|-----|
|
||||
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
|
||||
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
|
||||
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
|
||||
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
|
||||
|
||||
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
|
||||
|
||||
1. `--force` is given
|
||||
2. The CA's **ARI `suggestedWindow` has started**
|
||||
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
|
||||
|
||||
You can see the resulting next renewal time (already ARI-picked when applicable) in:
|
||||
|
||||
```sh
|
||||
acme.sh --info -d example.com
|
||||
# Look for: Le_NextRenewTimeStr=...
|
||||
```
|
||||
|
||||
For the live ARI window the CA is currently advertising, run with `--debug 2`:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
|
||||
```
|
||||
|
||||
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣3️⃣ How to Stop Certificate Renewal
|
||||
### 1️⃣4️⃣ How to Stop Certificate Renewal
|
||||
|
||||
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
|
||||
|
||||
|
|
@ -471,7 +574,7 @@ The cert/key file is not removed from the disk.
|
|||
|
||||
---
|
||||
|
||||
### 1️⃣4️⃣ How to Upgrade acme.sh
|
||||
### 1️⃣5️⃣ How to Upgrade acme.sh
|
||||
|
||||
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
|
||||
|
||||
|
|
@ -495,25 +598,25 @@ acme.sh --upgrade --auto-upgrade 0
|
|||
|
||||
---
|
||||
|
||||
### 1️⃣5️⃣ Issue a Certificate from an Existing CSR
|
||||
### 1️⃣6️⃣ Issue a Certificate from an Existing CSR
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣6️⃣ Send Notifications in Cronjob
|
||||
### 1️⃣7️⃣ Send Notifications in Cronjob
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣7️⃣ Under the Hood
|
||||
### 1️⃣8️⃣ Under the Hood
|
||||
|
||||
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣8️⃣ Acknowledgments
|
||||
### 1️⃣9️⃣ Acknowledgments
|
||||
|
||||
| Project | Link |
|
||||
|---------|------|
|
||||
|
|
@ -530,6 +633,8 @@ This project exists thanks to all the people who contribute.
|
|||
|
||||
<a href="https://github.com/acmesh-official/acme.sh/graphs/contributors"><img src="https://opencollective.com/acmesh/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
### 💰 Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
|
||||
|
|
@ -555,7 +660,7 @@ Support this project with your organization. Your logo will show up here with a
|
|||
|
||||
---
|
||||
|
||||
### 1️⃣9️⃣ License & Others
|
||||
### 2️⃣0️⃣ License & Others
|
||||
|
||||
📄 **License:** GPLv3
|
||||
|
||||
|
|
@ -565,7 +670,7 @@ Support this project with your organization. Your logo will show up here with a
|
|||
|
||||
---
|
||||
|
||||
### 2️⃣0️⃣ Donate
|
||||
### 2️⃣1️⃣ Donate
|
||||
|
||||
> 💝 Your donation makes **acme.sh** better!
|
||||
|
||||
|
|
@ -577,7 +682,7 @@ Support this project with your organization. Your logo will show up here with a
|
|||
|
||||
---
|
||||
|
||||
### 2️⃣1️⃣ About This Repository
|
||||
### 2️⃣2️⃣ About This Repository
|
||||
|
||||
> [!NOTE]
|
||||
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
|
||||
|
|
|
|||
341
acme.sh.completion
Normal file
341
acme.sh.completion
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh
|
||||
#
|
||||
# "acme.sh --install" copies this file to the acme.sh home dir and wires
|
||||
# it into acme.sh.env, so the completion is loaded automatically in new
|
||||
# bash sessions after installation.
|
||||
#
|
||||
# To use it without installing acme.sh, source it from ~/.bashrc, or copy
|
||||
# it to /usr/share/bash-completion/completions/acme.sh
|
||||
#
|
||||
# Zsh users can load it with:
|
||||
# autoload -U +X bashcompinit && bashcompinit
|
||||
# . /path/to/acme.sh.completion
|
||||
|
||||
# This file may also be sourced by non-bash shells via acme.sh.env,
|
||||
# so silently do nothing if the "complete" builtin is not available.
|
||||
if ! command -v complete >/dev/null 2>&1; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# Add each word of $1 that starts with $cur to COMPREPLY.
|
||||
# The words are read line by line, so that candidates like a wildcard
|
||||
# domain "*.example.com" are never glob-expanded against the cwd.
|
||||
_acme_sh_add_matches() {
|
||||
local _word
|
||||
while read -r _word; do
|
||||
[ -n "$_word" ] || continue
|
||||
case "$_word" in
|
||||
"$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$(printf '%s\n' "$1" | tr ' ' '\n')
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
_acme_sh_files() {
|
||||
local _file
|
||||
while IFS= read -r _file; do
|
||||
[ -n "$_file" ] || continue
|
||||
COMPREPLY=("${COMPREPLY[@]}" "$_file")
|
||||
done <<EOF
|
||||
$(compgen -f -- "$cur")
|
||||
EOF
|
||||
if command -v compopt >/dev/null 2>&1; then
|
||||
compopt -o filenames 2>/dev/null
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
_acme_sh_dirs() {
|
||||
local _dir
|
||||
while IFS= read -r _dir; do
|
||||
[ -n "$_dir" ] || continue
|
||||
COMPREPLY=("${COMPREPLY[@]}" "$_dir")
|
||||
done <<EOF
|
||||
$(compgen -d -- "$cur")
|
||||
EOF
|
||||
if command -v compopt >/dev/null 2>&1; then
|
||||
compopt -o filenames 2>/dev/null
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Complete the domains that already have a cert: every directory in the
|
||||
# config home that contains a "<domain>.conf" file ("_ecc" suffix stripped).
|
||||
_acme_sh_domains() {
|
||||
local _dir _name _domains=""
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
|
||||
for _dir in "$_acme_conf_home"/*/; do
|
||||
[ -d "$_dir" ] || continue
|
||||
_name="${_dir%/}"
|
||||
_name="${_name##*/}"
|
||||
_name="${_name%_ecc}"
|
||||
if [ -f "${_dir}${_name}.conf" ]; then
|
||||
case " $_domains " in
|
||||
*" $_name "*) ;;
|
||||
*) _domains="$_domains $_name" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
_acme_sh_add_matches "$_domains"
|
||||
}
|
||||
|
||||
# Complete hook names from a subfolder of the acme.sh home dir.
|
||||
# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty.
|
||||
_acme_sh_hooks() {
|
||||
local _file _hooks=""
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
|
||||
for _file in "$_acme_home/$1/$2"*.sh; do
|
||||
[ -f "$_file" ] || continue
|
||||
_file="${_file##*/}"
|
||||
_hooks="$_hooks ${_file%.sh}"
|
||||
done
|
||||
_acme_sh_add_matches "$_hooks"
|
||||
}
|
||||
|
||||
_acme_sh_completion() {
|
||||
local cur prev _acme_home _acme_conf_home
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev=""
|
||||
if [ "$COMP_CWORD" -gt 0 ]; then
|
||||
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||
fi
|
||||
_acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}"
|
||||
_acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}"
|
||||
|
||||
# The first argument is the command.
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
_acme_sh_add_matches "
|
||||
--help
|
||||
--version
|
||||
--install
|
||||
--install-online
|
||||
--uninstall
|
||||
--upgrade
|
||||
--issue
|
||||
--deploy
|
||||
--sign-csr
|
||||
--show-csr
|
||||
--install-cert
|
||||
--renew
|
||||
--renew-all
|
||||
--revoke
|
||||
--remove
|
||||
--list
|
||||
--list-profiles
|
||||
--info
|
||||
--to-pkcs12
|
||||
--to-pkcs8
|
||||
--create-account-key
|
||||
--create-domain-key
|
||||
--create-csr
|
||||
--deactivate
|
||||
--update-account
|
||||
--register-account
|
||||
--deactivate-account
|
||||
--make-dns-persist-value
|
||||
--install-cronjob
|
||||
--uninstall-cronjob
|
||||
--cron
|
||||
--set-notify
|
||||
--set-default-ca
|
||||
--set-default-chain
|
||||
"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Complete the value of the previous option.
|
||||
case "$prev" in
|
||||
-d | --domain | --challenge-alias | --domain-alias)
|
||||
_acme_sh_domains
|
||||
return 0
|
||||
;;
|
||||
--dns)
|
||||
# The dns hook argument is optional, keep completing options if the
|
||||
# current word already looks like one.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_hooks "dnsapi" "dns_"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--deploy-hook)
|
||||
_acme_sh_hooks "deploy" ""
|
||||
return 0
|
||||
;;
|
||||
--notify-hook)
|
||||
_acme_sh_hooks "notify" ""
|
||||
return 0
|
||||
;;
|
||||
--server)
|
||||
_acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis"
|
||||
return 0
|
||||
;;
|
||||
-k | --keylength | -ak | --accountkeylength)
|
||||
_acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521"
|
||||
return 0
|
||||
;;
|
||||
--debug)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_add_matches "0 1 2 3"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--log)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--nginx)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--auto-upgrade | --always-force-new-domain-key)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_add_matches "0 1"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--log-level)
|
||||
_acme_sh_add_matches "1 2"
|
||||
return 0
|
||||
;;
|
||||
--syslog)
|
||||
_acme_sh_add_matches "0 3 6 7"
|
||||
return 0
|
||||
;;
|
||||
--notify-level)
|
||||
_acme_sh_add_matches "0 1 2 3"
|
||||
return 0
|
||||
;;
|
||||
--notify-mode)
|
||||
_acme_sh_add_matches "0 1"
|
||||
return 0
|
||||
;;
|
||||
--revoke-reason)
|
||||
_acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10"
|
||||
return 0
|
||||
;;
|
||||
--cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
-w | --webroot | --home | --cert-home | --config-home | --ca-path)
|
||||
_acme_sh_dirs
|
||||
return 0
|
||||
;;
|
||||
-m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days)
|
||||
# These options take a free-form value, offer nothing.
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Complete the parameters.
|
||||
_acme_sh_add_matches "
|
||||
--accountconf
|
||||
--accountkey
|
||||
--accountkeylength
|
||||
--alpn
|
||||
--always-force-new-domain-key
|
||||
--apache
|
||||
--auto-upgrade
|
||||
--branch
|
||||
--ca-bundle
|
||||
--ca-file
|
||||
--ca-path
|
||||
--cert-file
|
||||
--cert-home
|
||||
--cert-profile
|
||||
--challenge-alias
|
||||
--config-home
|
||||
--csr
|
||||
--days
|
||||
--debug
|
||||
--deploy-hook
|
||||
--dns
|
||||
--dns-persist
|
||||
--dns-persist-ca-name
|
||||
--dns-persist-days
|
||||
--dns-persist-wildcard
|
||||
--dnssleep
|
||||
--domain
|
||||
--domain-alias
|
||||
--eab-hmac-key
|
||||
--eab-kid
|
||||
--ecc
|
||||
--email
|
||||
--extended-key-usage
|
||||
--force
|
||||
--force-color
|
||||
--fullchain-file
|
||||
--home
|
||||
--httpport
|
||||
--insecure
|
||||
--key-file
|
||||
--keylength
|
||||
--listen-v4
|
||||
--listen-v6
|
||||
--listraw
|
||||
--local-address
|
||||
--log
|
||||
--log-level
|
||||
--nginx
|
||||
--no-color
|
||||
--no-cron
|
||||
--no-profile
|
||||
--notify-hook
|
||||
--notify-level
|
||||
--notify-mode
|
||||
--notify-source
|
||||
--ocsp-must-staple
|
||||
--openssl-bin
|
||||
--output-insecure
|
||||
--password
|
||||
--post-hook
|
||||
--pre-hook
|
||||
--preferred-chain
|
||||
--reloadcmd
|
||||
--renew-hook
|
||||
--revoke-reason
|
||||
--server
|
||||
--staging
|
||||
--standalone
|
||||
--stateless
|
||||
--stop-renew-on-error
|
||||
--syslog
|
||||
--tlsport
|
||||
--treat-skip-as-success
|
||||
--use-wget
|
||||
--useragent
|
||||
--valid-from
|
||||
--valid-to
|
||||
--webroot
|
||||
--yes-I-know-dns-manual-mode-enough-go-ahead-please
|
||||
"
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _acme_sh_completion acme.sh
|
||||
222
deploy/baidu_cdn.sh
Normal file
222
deploy/baidu_cdn.sh
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Deploy hook: Baidu Cloud CDN
|
||||
#
|
||||
# Code generated by GitHub Copilot with Claude Sonnet 4.6 and OpenAI Codex with GPT-5.6 Sol
|
||||
#
|
||||
# API Doc: https://cloud.baidu.com/doc/CDN/s/Zkna2r57w
|
||||
#
|
||||
# Uses the same credential variables as dnsapi/dns_baidu.sh:
|
||||
# export Baidu_AK="your-access-key-id"
|
||||
# export Baidu_SK="your-secret-access-key"
|
||||
#
|
||||
# To deploy to a CDN domain different from the certificate CN
|
||||
# (e.g. wildcard or multi-domain certs):
|
||||
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn.example.com"
|
||||
#
|
||||
# Multiple CDN domains sharing the same certificate:
|
||||
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
|
||||
|
||||
BAIDU_CDN_HOST="cdn.baidubce.com"
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT=""
|
||||
|
||||
baidu_cdn_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
if ! _baidu_cdn_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_BAIDU_CDN_DOMAIN
|
||||
if [ "$DEPLOY_BAIDU_CDN_DOMAIN" ]; then
|
||||
_savedeployconf DEPLOY_BAIDU_CDN_DOMAIN "$DEPLOY_BAIDU_CDN_DOMAIN"
|
||||
else
|
||||
DEPLOY_BAIDU_CDN_DOMAIN="$_cdomain"
|
||||
fi
|
||||
|
||||
# Build JSON "domains" array from space-separated domain list
|
||||
_domains_json=""
|
||||
for _d in $DEPLOY_BAIDU_CDN_DOMAIN; do
|
||||
_d_e="$(_baidu_cdn_json_escape "$_d")"
|
||||
if [ -z "$_domains_json" ]; then
|
||||
_domains_json="\"${_d_e}\""
|
||||
else
|
||||
_domains_json="${_domains_json},\"${_d_e}\""
|
||||
fi
|
||||
done
|
||||
|
||||
# Build a valid cert name: must start with a letter, allow [A-Za-z0-9-/.], max 65 chars
|
||||
_cert_name="$(printf "%s" "$_cdomain" | sed 's/\*\./wildcard./g;s/[^A-Za-z0-9./]/-/g' | cut -c 1-65)"
|
||||
case "$_cert_name" in
|
||||
[A-Za-z]*) ;;
|
||||
*) _cert_name="c${_cert_name}" ;;
|
||||
esac
|
||||
|
||||
# PEM content is already Base64 inside the -----BEGIN/END----- wrappers.
|
||||
# The API expects the raw PEM as a JSON string, so newlines must be escaped as \n.
|
||||
_cert_pem="$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')"
|
||||
_key_pem="$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')"
|
||||
|
||||
_debug2 _cert_name "$_cert_name"
|
||||
_debug2 _domains_json "[$_domains_json]"
|
||||
|
||||
# Build JSON payload
|
||||
_payload="{\"domains\":[${_domains_json}],\"certificate\":{\"certName\":\"${_cert_name}\",\"certServerData\":\"${_cert_pem}\",\"certPrivateData\":\"${_key_pem}\"}}"
|
||||
|
||||
# Generate BCE v1 authorization header (query string included in canonical request)
|
||||
_cdn_path="/v2/domain/certificate"
|
||||
_cdn_query="action=put"
|
||||
_ts="$(_utc_date | sed 's/ /T/')Z"
|
||||
_content_type="application/json; charset=utf-8"
|
||||
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
|
||||
|
||||
if ! _baidu_cdn_bce_auth "POST" "$_cdn_path" "$_cdn_query" "$BAIDU_CDN_HOST" "$_ts" "3600" "$_content_type" "$_payload_hash"; then
|
||||
_err "Failed to sign request"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H1="Authorization: $_BAIDU_CDN_BCE_AUTH_RESULT"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_CDN_HOST"
|
||||
_H5=""
|
||||
|
||||
_url="https://${BAIDU_CDN_HOST}${_cdn_path}?${_cdn_query}"
|
||||
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Failed to call Baidu Cloud CDN API"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
|
||||
if _contains "$response" "\"certId\""; then
|
||||
_info "Certificate deployed to Baidu Cloud CDN for: $DEPLOY_BAIDU_CDN_DOMAIN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to deploy certificate to Baidu Cloud CDN: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# BCE v1 signing with canonical query string support.
|
||||
# The CDN endpoint uses ?action=put so it must be included in the canonical request.
|
||||
_baidu_cdn_bce_auth() {
|
||||
_method="$1"
|
||||
_uri="$2"
|
||||
_query="$3"
|
||||
_host="$4"
|
||||
_ts="$5"
|
||||
_expire="$6"
|
||||
_ct="$7"
|
||||
_payload_hash="$8"
|
||||
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT=""
|
||||
|
||||
_auth_prefix="bce-auth-v1/${Baidu_AK}/${_ts}/${_expire}"
|
||||
_signed_headers="content-type;host;x-bce-content-sha256;x-bce-date"
|
||||
_canonical_uri="$(_baidu_cdn_bce_encode_path "$_uri")"
|
||||
|
||||
_host_e="$(printf "%s" "$_host" | _url_encode upper-hex)"
|
||||
_date_e="$(printf "%s" "$_ts" | _url_encode upper-hex)"
|
||||
_ct_e="$(printf "%s" "$_ct" | _url_encode upper-hex)"
|
||||
_hash_e="$(printf "%s" "$_payload_hash" | _url_encode upper-hex)"
|
||||
|
||||
_canonical_headers="content-type:${_ct_e}
|
||||
host:${_host_e}
|
||||
x-bce-content-sha256:${_hash_e}
|
||||
x-bce-date:${_date_e}"
|
||||
|
||||
_canonical_request="${_method}
|
||||
${_canonical_uri}
|
||||
${_query}
|
||||
${_canonical_headers}"
|
||||
|
||||
_sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")"
|
||||
_signing_key="$(_baidu_cdn_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")"
|
||||
_signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")"
|
||||
_signature="$(_baidu_cdn_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")"
|
||||
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}"
|
||||
}
|
||||
|
||||
_baidu_cdn_load_credentials() {
|
||||
Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}"
|
||||
Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}"
|
||||
|
||||
Baidu_AK="$(_baidu_cdn_trim_ws "$Baidu_AK")"
|
||||
Baidu_SK="$(_baidu_cdn_trim_ws "$Baidu_SK")"
|
||||
|
||||
if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then
|
||||
_err "Baidu_AK and Baidu_SK are required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable Baidu_AK "$Baidu_AK"
|
||||
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_cdn_bce_encode_path() {
|
||||
_p="$1"
|
||||
_out=""
|
||||
if [ "${_p#"/"}" != "$_p" ]; then
|
||||
_out="/"
|
||||
fi
|
||||
|
||||
_rest="${_p#/}"
|
||||
while [ -n "$_rest" ]; do
|
||||
_seg="${_rest%%/*}"
|
||||
if [ "$_seg" ]; then
|
||||
if [ -z "$_out" ] || [ "$_out" = "/" ]; then
|
||||
_out="${_out}$(printf "%s" "$_seg" | _url_encode upper-hex)"
|
||||
else
|
||||
_out="${_out}/$(printf "%s" "$_seg" | _url_encode upper-hex)"
|
||||
fi
|
||||
fi
|
||||
if [ "${_rest#*/}" = "$_rest" ]; then
|
||||
break
|
||||
fi
|
||||
_rest="${_rest#*/}"
|
||||
done
|
||||
|
||||
if [ -z "$_out" ]; then
|
||||
_out="/"
|
||||
fi
|
||||
printf "%s" "$_out"
|
||||
}
|
||||
|
||||
_baidu_cdn_trim_ws() {
|
||||
printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//'
|
||||
}
|
||||
|
||||
_baidu_cdn_json_escape() {
|
||||
_s="$1"
|
||||
_s="$(printf "%s" "$_s" | tr -d '\r\n')"
|
||||
printf "%s" "$_s" |
|
||||
sed 's/\\/\\\\/g; s/ /\\t/g' |
|
||||
_baidu_cdn_json_encode
|
||||
}
|
||||
|
||||
_baidu_cdn_json_encode() {
|
||||
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
|
||||
printf "%s" "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
|
||||
}
|
||||
|
||||
_baidu_cdn_hmac_sha256_hexkey() {
|
||||
_key_hex="$1"
|
||||
_msg="$2"
|
||||
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
|
||||
}
|
||||
|
|
@ -163,8 +163,8 @@ byteplus_alb_deploy() {
|
|||
# ── 3. Read cert and key ─────────────────────────────────────────────────────
|
||||
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
|
||||
|
||||
_public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r')
|
||||
_private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r')
|
||||
_public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
|
||||
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
|
||||
|
||||
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
|
||||
_err "Failed to read certificate or key file."
|
||||
|
|
|
|||
|
|
@ -52,7 +52,15 @@ cpanel_uapi_deploy() {
|
|||
|
||||
# read cert and key files and urlencode both
|
||||
_cert=$(_url_encode <"$_ccert")
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
# with --signcsr the private key was never handed to acme.sh, so the key
|
||||
# file does not exist; skip it instead of spilling a shell redirection
|
||||
# error on every renewal (cPanel keeps using the already-installed key)
|
||||
if [ -f "$_ckey" ]; then
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
else
|
||||
_debug "Key file $_ckey does not exist (csr mode), not sending a key."
|
||||
_key=""
|
||||
fi
|
||||
|
||||
_debug2 _cert "$_cert"
|
||||
_debug2 _key "$_key"
|
||||
|
|
@ -79,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:"
|
||||
|
|
@ -194,7 +206,8 @@ __cpanel_parse_response() {
|
|||
printf("%s%s=%s\n", prefix, $2, $3);
|
||||
}
|
||||
}' |
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' |
|
||||
sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
|
||||
}
|
||||
|
||||
# Load parameter by prefix+name - fallback to default if not set, and save to config
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx"
|
||||
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640"
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000"
|
||||
#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
|
||||
|
|
@ -71,6 +73,18 @@ docker_deploy() {
|
|||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
|
||||
|
|
@ -112,6 +126,20 @@ docker_deploy() {
|
|||
if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
|
||||
_info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
_err "Can not change owner of key file in container"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
|
||||
_info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
_err "Can not change mode of key file in container"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
|
||||
|
|
@ -189,10 +217,22 @@ _docker_exec() {
|
|||
_debug2 cjson "$cjson"
|
||||
execid="$(echo "$cjson" | cut -d '"' -f 4)"
|
||||
_debug execid "$execid"
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
|
||||
#Detach:true is required for podman's docker-compatible API: with
|
||||
#Detach:false it streams the command output on the connection, so the
|
||||
#non-empty response was misread as an error (issue #4977). The real
|
||||
#result is checked via the exec inspect ExitCode below instead.
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")"
|
||||
_debug2 ejson "$ejson"
|
||||
if [ "$ejson" ]; then
|
||||
_err "$ejson"
|
||||
_et=0
|
||||
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
|
||||
while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do
|
||||
sleep 1
|
||||
_et="$(_math "$_et" + 1)"
|
||||
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
|
||||
done
|
||||
_debug2 ijson "$ijson"
|
||||
if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then
|
||||
_err "docker exec error: $ijson"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
|
|
|
|||
175
deploy/fortigate.sh
Normal file
175
deploy/fortigate.sh
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#!/usr/bin/env sh
|
||||
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
|
||||
#
|
||||
# FortiGate's native ACME integration does not support wildcard certificates or domain validation,
|
||||
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
|
||||
#
|
||||
# REQUIRED:
|
||||
# export FGT_HOST="fortigate_hostname-or-ip"
|
||||
# export FGT_TOKEN="fortigate_api_token"
|
||||
#
|
||||
# OPTIONAL:
|
||||
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
|
||||
#
|
||||
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
|
||||
# `--insecure` is required on first run if not already using a valid SSL certificate on firewall.
|
||||
|
||||
# Function to parse a FortiGate API response
|
||||
_fortigate_parse_response() {
|
||||
_fortigate_response="$1"
|
||||
_fortigate_func="$2"
|
||||
_fortigate_status=$(echo "$_fortigate_response" | _egrep_o '"status":[ ]*"[^"]*"' | cut -d '"' -f 4)
|
||||
|
||||
if [ "$_fortigate_status" != "success" ]; then
|
||||
_err "[$_fortigate_func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "[$_fortigate_func] Operation successful."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to deploy a base64-encoded certificate to the firewall
|
||||
_fortigate_deployer() {
|
||||
_fortigate_cert_base64=$(_base64 <"$_fortigate_cfullchain" | tr -d '\n')
|
||||
_fortigate_key_base64=$(_base64 <"$_fortigate_ckey" | tr -d '\n')
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"type": "regular",
|
||||
"scope": "global",
|
||||
"certname": "$_fortigate_cert_name",
|
||||
"key_file_content": "$_fortigate_key_base64",
|
||||
"file_content": "$_fortigate_cert_base64"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
|
||||
_debug "Uploading certificate via URL: $_fortigate_url"
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
|
||||
_debug "FortiGate API Response: $_fortigate_response"
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Deploying certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to upload a CA certificate to the firewall
|
||||
# FortiGate does not automatically extract the CA from the full chain.
|
||||
_fortigate_upload_ca_cert() {
|
||||
_fortigate_ca_base64=$(_base64 <"$_fortigate_cca" | tr -d '\n')
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"import_method": "file",
|
||||
"scope": "global",
|
||||
"file_content": "$_fortigate_ca_base64"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
|
||||
_debug "Uploading CA certificate via URL: $_fortigate_url"
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
|
||||
_debug "FortiGate API CA Response: $_fortigate_response"
|
||||
|
||||
# FortiGate error -328 means that the CA certificate already exists.
|
||||
if echo "$_fortigate_response" | grep -q '"error":[ ]*-328'; then
|
||||
_debug "CA certificate already exists. Skipping CA upload."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Deploying CA certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to activate the new certificate
|
||||
_fortigate_set_active_web_cert() {
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"admin-server-cert": "$_fortigate_cert_name"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
|
||||
_debug "Setting GUI certificate..."
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "PUT" "application/json")
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Assigning active certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to clean up the previously deployed certificate
|
||||
_fortigate_cleanup_previous_certificate() {
|
||||
_getdeployconf FGT_LAST_CERT
|
||||
|
||||
if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_fortigate_cert_name" ]; then
|
||||
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "" "$_fortigate_url" "" "DELETE" "application/json")
|
||||
_debug "Delete certificate API response: $_fortigate_response"
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Delete previous certificate" || return 1
|
||||
else
|
||||
_debug "No previous certificate found."
|
||||
fi
|
||||
}
|
||||
|
||||
# Main deploy-hook function
|
||||
fortigate_deploy() {
|
||||
# Include date and time to ensure unique names.
|
||||
_fortigate_cert_name="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d_%H-%M-%S")"
|
||||
_fortigate_ckey="$2"
|
||||
_fortigate_cca="$4"
|
||||
_fortigate_cfullchain="$5"
|
||||
|
||||
if [ ! -f "$_fortigate_ckey" ] || [ ! -f "$_fortigate_cfullchain" ]; then
|
||||
_err "Valid key and/or certificate not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save required environment variables if set; otherwise load saved values.
|
||||
for _fortigate_var in FGT_HOST FGT_TOKEN FGT_PORT; do
|
||||
if [ -n "$(eval echo "\$$_fortigate_var")" ]; then
|
||||
_debug "Detected ENV variable $_fortigate_var. Saving to file."
|
||||
_savedeployconf "$_fortigate_var" "$(eval echo "\$$_fortigate_var")" 1
|
||||
else
|
||||
_debug "Attempting to load variable $_fortigate_var from file."
|
||||
_getdeployconf "$_fortigate_var"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
|
||||
_err "FGT_HOST and FGT_TOKEN must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
FGT_PORT="${FGT_PORT:-443}"
|
||||
_debug "Using FortiGate port: $FGT_PORT"
|
||||
|
||||
# Upload the new certificate.
|
||||
_fortigate_deployer || return 1
|
||||
|
||||
# Upload the CA certificate.
|
||||
if [ -n "$_fortigate_cca" ] && [ -f "$_fortigate_cca" ]; then
|
||||
_fortigate_upload_ca_cert || return 1
|
||||
else
|
||||
_debug "No CA certificate provided."
|
||||
fi
|
||||
|
||||
# Activate the new certificate.
|
||||
_fortigate_set_active_web_cert || return 1
|
||||
|
||||
# Delete the previously deployed certificate only after successful activation.
|
||||
_fortigate_cleanup_previous_certificate || return 1
|
||||
|
||||
# Save the new certificate name for cleanup during the next deployment.
|
||||
_savedeployconf "FGT_LAST_CERT" "$_fortigate_cert_name" 1
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@ gcore_cdn_deploy() {
|
|||
_request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}"
|
||||
_debug _request "$_request"
|
||||
export _H1="Content-Type:application/json"
|
||||
_response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login")
|
||||
_response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login")
|
||||
_debug _response "$_response"
|
||||
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
|
||||
_debug _regex "$_regex"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@
|
|||
# needing to reload HAProxy. Default is "no".
|
||||
#
|
||||
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
|
||||
# address format.
|
||||
# address format. The certificate can be deployed to a comma separated ',' list
|
||||
# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999")
|
||||
#
|
||||
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
|
||||
#
|
||||
|
|
@ -193,7 +194,6 @@ haproxy_deploy() {
|
|||
_issuer="${_pem}.issuer"
|
||||
_ocsp="${_pem}.ocsp"
|
||||
_reload="${Le_Deploy_haproxy_reload}"
|
||||
_statssock="${Le_Deploy_haproxy_stats_socket}"
|
||||
|
||||
_info "Deploying PEM file"
|
||||
# Create a temporary PEM file
|
||||
|
|
@ -272,12 +272,18 @@ haproxy_deploy() {
|
|||
_cafile_argument=""
|
||||
fi
|
||||
_debug _cafile_argument "${_cafile_argument}"
|
||||
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
|
||||
# OpenSSL 1.1+ expects -header Host=value (one argument), while
|
||||
# LibreSSL keeps the old two-argument form -header Host value at any
|
||||
# version (3.x/4.x), so it must be detected by name, not by number.
|
||||
_openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1)
|
||||
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
|
||||
_debug _openssl_name "${_openssl_name}"
|
||||
_debug _openssl_version "${_openssl_version}"
|
||||
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
|
||||
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
|
||||
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
if [ "${_openssl_name}" = "LibreSSL" ]; then
|
||||
_header_sep=" "
|
||||
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
_header_sep="="
|
||||
else
|
||||
_header_sep=" "
|
||||
|
|
@ -327,62 +333,67 @@ haproxy_deploy() {
|
|||
|
||||
# Update certificate over HAProxy stats socket or master CLI.
|
||||
if _exists socat; then
|
||||
# look for the certificate on the stats socket, to chose between updating or creating one
|
||||
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
|
||||
_debug _socat_cert_cmd "${_socat_cert_cmd}"
|
||||
eval "${_socat_cert_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_newcert="1"
|
||||
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
|
||||
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
|
||||
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
|
||||
eval "${_socat_crtlist_show_cmd}"
|
||||
IFS=','
|
||||
for _statssock in ${Le_Deploy_haproxy_stats_socket}; do
|
||||
# look for the certificate on the stats socket, to choose between updating or creating one
|
||||
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
|
||||
_debug _socat_cert_cmd "${_socat_cert_cmd}"
|
||||
eval "${_socat_cert_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
_newcert="1"
|
||||
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
|
||||
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
|
||||
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
|
||||
eval "${_socat_crtlist_show_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
fi
|
||||
# create a new certificate
|
||||
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
|
||||
_debug _socat_new_cmd "${_socat_new_cmd}"
|
||||
eval "${_socat_new_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't create '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
else
|
||||
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
fi
|
||||
# create a new certificate
|
||||
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
|
||||
_debug _socat_new_cmd "${_socat_new_cmd}"
|
||||
eval "${_socat_new_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't create '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
else
|
||||
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
fi
|
||||
_socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
|
||||
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
|
||||
eval "${_socat_cert_set_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
|
||||
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
|
||||
eval "${_socat_cert_commit_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't commit '${_pem}' in haproxy"
|
||||
return ${_ret}
|
||||
fi
|
||||
if [ "${_newcert}" = "1" ]; then
|
||||
# if this is a new certificate, it needs to be inserted into the crt-list`
|
||||
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
|
||||
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
|
||||
eval "${_socat_cert_add_cmd}"
|
||||
# printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket.
|
||||
# "Transaction updated" is replied instead of "created" when an uncommitted transaction exists.
|
||||
_socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'"
|
||||
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
|
||||
eval "${_socat_cert_set_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
|
||||
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
|
||||
eval "${_socat_cert_commit_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't commit '${_pem}' in haproxy"
|
||||
return ${_ret}
|
||||
fi
|
||||
if [ "${_newcert}" = "1" ]; then
|
||||
# if this is a new certificate, it needs to be inserted into the crt-list`
|
||||
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
|
||||
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
|
||||
eval "${_socat_cert_add_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
_err "'socat' is not available, couldn't update over ${_socketname}"
|
||||
fi
|
||||
|
|
|
|||
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:
|
||||
|
|
@ -210,7 +221,7 @@ _clear_envs() {
|
|||
|
||||
echo "$env_pairs" | while IFS='=' read -r _key _value; do
|
||||
_debug3 "Deleting key" "$_key"
|
||||
_cleardomainconf "SAVED_$_key"
|
||||
_cleardeployconf "$_key"
|
||||
unset -v "$_key"
|
||||
done
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ mydevil_deploy() {
|
|||
# Usage: ip=$(mydevil_get_ip domain.com)
|
||||
# echo $ip
|
||||
mydevil_get_ip() {
|
||||
devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
|
||||
# tr squeezes runs of blanks into one tab so plain cut works everywhere;
|
||||
# cut -w is BSD-only and unknown to GNU coreutils
|
||||
devil dns list "$1" | tr -s ' \t' '\t' | cut -s -f 3,7 | grep "^A$(printf '\t')" | cut -s -f 2 || return 1
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,9 +296,20 @@ panos_deploy() {
|
|||
_err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again"
|
||||
return 1
|
||||
else
|
||||
deployer cert
|
||||
deployer key
|
||||
deployer commit
|
||||
# A commit of a failed import would leave a mismatched cert/key pair
|
||||
# on the firewall and can lock the admin out of the management
|
||||
# interface, see https://github.com/acmesh-official/acme.sh/issues/4716
|
||||
if ! deployer cert; then
|
||||
_err "Cert import failed. Aborting without committing."
|
||||
return 1
|
||||
fi
|
||||
if ! deployer key; then
|
||||
_err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config."
|
||||
return 1
|
||||
fi
|
||||
if ! deployer commit; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$_panos_template_stack" ]; then
|
||||
# try to get job status for 20 times in 30 sec interval
|
||||
i=0
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ routeros_deploy() {
|
|||
_savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
|
||||
_savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
|
||||
_savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64"
|
||||
|
||||
# push key to routeros
|
||||
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
|
||||
|
|
@ -143,6 +143,7 @@ comment=\"generated by routeros deploy script in acme.sh\" \
|
|||
source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_1 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_2 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_3 ];\
|
||||
\ndelay 1;\
|
||||
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
|
||||
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
@ -25,7 +25,8 @@
|
|||
# export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value
|
||||
# export DEPLOY_SSH_USE_SCP="" yes or no, default to no
|
||||
# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q"
|
||||
#
|
||||
# export DEPLOY_SSH_REMOTE_SHELL="" # defaults to sh -c
|
||||
# export DEPLOY_SSH_REMOTE_CMD_QUOTE="" # yes or no, defaults to yes
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
|
|
@ -71,6 +72,24 @@ ssh_deploy() {
|
|||
fi
|
||||
_savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
|
||||
|
||||
# REMOTE_SHELL is optional. If not provided then use sh
|
||||
_migratedeployconf Le_Deploy_ssh_remote_shell DEPLOY_SSH_REMOTE_SHELL
|
||||
_getdeployconf DEPLOY_SSH_REMOTE_SHELL
|
||||
_debug2 DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
|
||||
if [ -z "$DEPLOY_SSH_REMOTE_SHELL" ]; then
|
||||
DEPLOY_SSH_REMOTE_SHELL="sh -c"
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
|
||||
|
||||
# REMOTE_CMD_QUOTE is optional. If not provided then yes
|
||||
_migratedeployconf Le_Deploy_ssh_remote_cmd_quote DEPLOY_SSH_REMOTE_CMD_QUOTE
|
||||
_getdeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE
|
||||
_debug2 DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
|
||||
if [ -z "$DEPLOY_SSH_REMOTE_CMD_QUOTE" ]; then
|
||||
DEPLOY_SSH_REMOTE_CMD_QUOTE="yes"
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
|
||||
|
||||
# BACKUP is optional. If not provided then default to previously saved value or yes.
|
||||
_migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP
|
||||
_getdeployconf DEPLOY_SSH_BACKUP
|
||||
|
|
@ -170,10 +189,16 @@ ssh_deploy() {
|
|||
_info "Required commands batched and sent in single call to remote host"
|
||||
fi
|
||||
|
||||
_returnCode=0
|
||||
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
|
||||
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
|
||||
_ssh_deploy
|
||||
if ! _ssh_deploy; then
|
||||
# in case of an error, remember it, but keep going for the remaining servers
|
||||
_returnCode=1
|
||||
fi
|
||||
done
|
||||
|
||||
return $_returnCode
|
||||
}
|
||||
|
||||
_ssh_deploy() {
|
||||
|
|
@ -428,9 +453,13 @@ _ssh_remote_cmd() {
|
|||
_secure_debug "Remote commands to execute: $_cmd"
|
||||
_info "Submitting sequence of commands to remote server by $_ssh_cmd"
|
||||
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'"
|
||||
if [ "$DEPLOY_SSH_REMOTE_CMD_QUOTE" = "yes" ]; then
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "'$_cmd'"
|
||||
else
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "$_cmd"
|
||||
fi
|
||||
_err_code="$?"
|
||||
|
||||
if [ "$_err_code" != "0" ]; then
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
################################################################################
|
||||
# ACME.sh 3rd party deploy plugin for Synology DSM
|
||||
|
|
@ -72,7 +72,7 @@ synology_dsm_deploy() {
|
|||
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then
|
||||
_err "Missing required tools to creat temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Missing required tools to create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Notice: temp admin user authorization method only supports local deployment on DSM."
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -234,11 +234,11 @@ synology_dsm_deploy() {
|
|||
fi
|
||||
fi
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
# Account has 2FA-OTP enabled, since error 403 reported.
|
||||
# https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
|
||||
if [ "$error_code" == "403" ]; then
|
||||
if [ "$error_code" = "403" ]; then
|
||||
if [ -z "$SYNO_DEVICE_NAME" ]; then
|
||||
printf "Enter device name or leave empty for default (CertRenewal): "
|
||||
read -r SYNO_DEVICE_NAME
|
||||
|
|
@ -269,27 +269,27 @@ synology_dsm_deploy() {
|
|||
_secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
|
||||
fi
|
||||
fi
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
fi
|
||||
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
if [ "$error_code" = "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
_cleardeployconf SYNO_DEVICE_ID
|
||||
_err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window."
|
||||
elif [ "$error_code" == "404" ]; then
|
||||
_err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window."
|
||||
elif [ "$error_code" = "404" ]; then
|
||||
_err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
|
||||
elif [ "$error_code" == "406" ]; then
|
||||
elif [ "$error_code" = "406" ]; then
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
_err "Failed with unexcepted error, please report this by providing full log with '--debug 3'."
|
||||
else
|
||||
_err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue."
|
||||
fi
|
||||
elif [ "$error_code" == "400" ]; then
|
||||
elif [ "$error_code" = "400" ]; then
|
||||
_err "Failed to authenticate, no such account or incorrect password."
|
||||
elif [ "$error_code" == "401" ]; then
|
||||
elif [ "$error_code" = "401" ]; then
|
||||
_err "Failed to authenticate with a non-existent account."
|
||||
elif [ "$error_code" == "408" ] || [ "$error_code" == "409" ] || [ "$error_code" == "410" ]; then
|
||||
elif [ "$error_code" = "408" ] || [ "$error_code" = "409" ] || [ "$error_code" = "410" ]; then
|
||||
_err "Failed to authenticate, the account password has expired or must be changed."
|
||||
else
|
||||
_err "Failed to authenticate with error: $error_code."
|
||||
|
|
@ -322,8 +322,8 @@ synology_dsm_deploy() {
|
|||
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
|
||||
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
|
||||
else
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
|
||||
_savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
|
||||
_savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
|
||||
fi
|
||||
|
|
@ -336,7 +336,7 @@ synology_dsm_deploy() {
|
|||
id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p")
|
||||
_debug2 id "$id"
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" -eq 105 ]; then
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -424,11 +428,6 @@ _temp_admin_cleanup() {
|
|||
fi
|
||||
}
|
||||
|
||||
#_cleardeployconf key
|
||||
_cleardeployconf() {
|
||||
_cleardomainconf "SAVED_$1"
|
||||
}
|
||||
|
||||
# key
|
||||
_check2cleardeployconfexp() {
|
||||
_key="$1"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,12 @@
|
|||
#
|
||||
# # API KEY
|
||||
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
|
||||
# Optional:
|
||||
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>"
|
||||
# export DEPLOY_TRUENAS_PROTOCOL="wss" # ws or wss
|
||||
# export DEPLOY_TRUENAS_PORT="443" # optional, e.g. 80, 443, 8443
|
||||
|
||||
#
|
||||
|
||||
### Private functions
|
||||
|
|
@ -56,7 +61,6 @@ _ws_call() {
|
|||
_ws_upload_cert() {
|
||||
|
||||
/usr/bin/env python - <<EOF
|
||||
|
||||
import sys
|
||||
|
||||
from truenas_api_client import Client
|
||||
|
|
@ -78,7 +82,6 @@ with Client(uri="$_ws_uri") as c:
|
|||
print("R:0")
|
||||
print("E:_ws_upload_cert error!")
|
||||
sys.exit(7)
|
||||
|
||||
EOF
|
||||
|
||||
return $?
|
||||
|
|
@ -181,6 +184,8 @@ truenas_ws_deploy() {
|
|||
_getdeployconf DEPLOY_TRUENAS_APIKEY
|
||||
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
|
||||
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
|
||||
_getdeployconf DEPLOY_TRUENAS_PORT
|
||||
|
||||
# Check API Key
|
||||
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
|
||||
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
|
||||
|
|
@ -196,7 +201,21 @@ truenas_ws_deploy() {
|
|||
_info "TrueNAS protocol not set. Using 'ws'."
|
||||
DEPLOY_TRUENAS_PROTOCOL="ws"
|
||||
fi
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
|
||||
|
||||
# Check port, optional
|
||||
if [ -n "$DEPLOY_TRUENAS_PORT" ]; then
|
||||
case "$DEPLOY_TRUENAS_PORT" in
|
||||
'' | *[!0-9]*)
|
||||
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
|
||||
return 8
|
||||
;;
|
||||
esac
|
||||
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/websocket"
|
||||
else
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
|
||||
fi
|
||||
|
||||
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
||||
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
||||
_debug _ws_uri "$_ws_uri"
|
||||
|
|
@ -216,13 +235,14 @@ truenas_ws_deploy() {
|
|||
|
||||
if [ "$_ws_response" != "TRUE" ]; then
|
||||
_err "TrueNAS is not ready."
|
||||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
|
||||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME, DEPLOY_TRUENAS_PROTOCOL and DEPLOY_TRUENAS_PORT."
|
||||
_err "Verify API key."
|
||||
return 2
|
||||
fi
|
||||
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
|
||||
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
||||
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
||||
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
|
||||
_info "TrueNAS health: OK"
|
||||
|
||||
########## System info
|
||||
|
|
|
|||
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
|
||||
}
|
||||
158
deploy/windows_rdp.sh
Normal file
158
deploy/windows_rdp.sh
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
|
||||
# Desktop listener (RDP-Tcp).
|
||||
#
|
||||
# One ssh invocation does the whole job:
|
||||
# * the PFX is built locally, base64'd, and embedded as a string literal
|
||||
# inside a generated PowerShell script;
|
||||
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
|
||||
# no temp files on the Windows host.
|
||||
#
|
||||
# First run:
|
||||
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
|
||||
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
|
||||
#
|
||||
# Available variables:
|
||||
# DEPLOY_WIN_RDP_HOST required SSH host
|
||||
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
|
||||
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
|
||||
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
|
||||
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
|
||||
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
|
||||
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
|
||||
# Active RDP sessions will drop!
|
||||
|
||||
windows_rdp_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
if ! _exists "ssh"; then
|
||||
_err "ssh is required but was not found in PATH."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- configuration ------------------------------------------------------
|
||||
_getdeployconf DEPLOY_WIN_RDP_HOST
|
||||
_getdeployconf DEPLOY_WIN_RDP_USER
|
||||
_getdeployconf DEPLOY_WIN_RDP_PORT
|
||||
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
|
||||
_getdeployconf DEPLOY_WIN_RDP_LISTENER
|
||||
_getdeployconf DEPLOY_WIN_RDP_RESTART
|
||||
|
||||
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
|
||||
_err "DEPLOY_WIN_RDP_HOST must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
|
||||
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
|
||||
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
|
||||
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
|
||||
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
|
||||
|
||||
_port="${DEPLOY_WIN_RDP_PORT:-22}"
|
||||
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
|
||||
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
|
||||
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
|
||||
else
|
||||
_target="$DEPLOY_WIN_RDP_HOST"
|
||||
fi
|
||||
_pfx_pass="acme"
|
||||
|
||||
# ---- build thumbprint + PFX locally ------------------------------------
|
||||
_thumb="$(_fingerprint "$_ccert" 'sha1')"
|
||||
if [ -z "$_thumb" ]; then
|
||||
_err "Failed to compute certificate thumbprint."
|
||||
return 1
|
||||
fi
|
||||
_debug "Thumbprint: $_thumb"
|
||||
|
||||
_debug "Building PFX at $_pfx_file"
|
||||
_pfx_file="$(_mktemp)"
|
||||
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
|
||||
_err "Failed to build PFX archive."
|
||||
rm -f "$_pfx_file"
|
||||
return 1
|
||||
fi
|
||||
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
|
||||
rm -f "$_pfx_file"
|
||||
|
||||
# ---- build installer script --------------------------------------------
|
||||
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
|
||||
_restart_ps='Restart-Service -Name TermService -Force'
|
||||
else
|
||||
_restart_ps='# New RdP connections will pick up the new cert automatically.'
|
||||
fi
|
||||
|
||||
# Escape every literal `$` with `\$` so the shell does not expand it.
|
||||
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
|
||||
_ps1=$(
|
||||
cat <<PSEOF
|
||||
|
||||
\$ErrorActionPreference = 'Stop'
|
||||
|
||||
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
|
||||
|
||||
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
|
||||
# could run into quite a lot of trouble when importing the certificate including its entire chain
|
||||
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
|
||||
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
|
||||
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
|
||||
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
|
||||
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
|
||||
# the issues.
|
||||
#
|
||||
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
|
||||
# TermService restore the original, self-signed certificate after at least after the second login attempt.
|
||||
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
|
||||
# present in any case - the following command should get you out of trouble:
|
||||
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
|
||||
|
||||
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
|
||||
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
|
||||
|
||||
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
|
||||
\$store.Open('ReadWrite')
|
||||
\$store.AddRange(\$certs)
|
||||
\$store.Close()
|
||||
Write-Host "Installed certs into LocalMachine\\My"
|
||||
|
||||
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
|
||||
if (-not \$ts) { throw "Listener '${_listener}' not found." }
|
||||
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
|
||||
Write-Host "Listener ${_listener} now uses ${_thumb}"
|
||||
|
||||
${_restart_ps}
|
||||
PSEOF
|
||||
)
|
||||
_debug "Powershell script:${_ps1}"
|
||||
|
||||
# ---- run over a single ssh connection ----------------------------------
|
||||
_ssh_opts="-o BatchMode=yes -p $_port"
|
||||
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
|
||||
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
fi
|
||||
|
||||
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
|
||||
# shellcheck disable=SC2086
|
||||
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
|
||||
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
|
||||
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
|
||||
return 0
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting
|
|||
Options:
|
||||
One984HOSTING_Username Username
|
||||
One984HOSTING_Password Password
|
||||
One984HOSTING_TOTP_Secret Base32 TOTP shared secret. Required only if the account has 2FA enabled. Requires oathtool. Used to mint the OTP code automatically at login so cron renewals keep working.
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2851
|
||||
Author: Adrian Fedoreanu
|
||||
'
|
||||
|
|
@ -124,11 +125,28 @@ _1984hosting_login() {
|
|||
_debug "Login to 1984Hosting as user $One984HOSTING_Username."
|
||||
username=$(printf '%s' "$One984HOSTING_Username" | _url_encode)
|
||||
password=$(printf '%s' "$One984HOSTING_Password" | _url_encode)
|
||||
url="https://1984.hosting/api/auth/"
|
||||
|
||||
_get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
|
||||
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
|
||||
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
|
||||
# When 2FA is enabled, mint a fresh TOTP code from the stored shared secret.
|
||||
# Empty otpkey is accepted by the server when 2FA is off.
|
||||
otpkey=""
|
||||
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
|
||||
if ! _exists oathtool; then
|
||||
_err "oathtool is required to use One984HOSTING_TOTP_Secret for 2FA. Please install it."
|
||||
return 1
|
||||
fi
|
||||
otpcode="$(oathtool --base32 --totp "$One984HOSTING_TOTP_Secret" 2>/dev/null)"
|
||||
if [ -z "$otpcode" ]; then
|
||||
_err "Failed to generate TOTP code from One984HOSTING_TOTP_Secret."
|
||||
return 1
|
||||
fi
|
||||
otpkey="$(printf '%s' "$otpcode" | _url_encode)"
|
||||
fi
|
||||
|
||||
# Fetch the login page to obtain CSRF and session cookies.
|
||||
# Note: _get sets the global 'url', so assign the auth URL afterwards.
|
||||
_get "https://1984.hosting/accounts/login/" >/dev/null
|
||||
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
|
||||
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
|
||||
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
|
||||
|
|
@ -140,17 +158,23 @@ _1984hosting_login() {
|
|||
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
||||
response="$(_post "username=$username&password=$password&otpkey=" $url)"
|
||||
url="https://1984.hosting/api/auth/"
|
||||
response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
|
||||
response="$(echo "$response" | _normalizeJson)"
|
||||
_debug2 response "$response"
|
||||
|
||||
if _contains "$response" '"loggedin": true'; then
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
export One984HOSTING_SESSIONID_COOKIE
|
||||
export One984HOSTING_CSRFTOKEN_COOKIE
|
||||
_saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username"
|
||||
_saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password"
|
||||
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
|
||||
_saveaccountconf_mutable One984HOSTING_TOTP_Secret "$One984HOSTING_TOTP_Secret"
|
||||
else
|
||||
_clearaccountconf_mutable One984HOSTING_TOTP_Secret
|
||||
fi
|
||||
_saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
|
||||
_saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
|
||||
return 0
|
||||
|
|
@ -161,6 +185,7 @@ _1984hosting_login() {
|
|||
_check_credentials() {
|
||||
One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}"
|
||||
One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}"
|
||||
One984HOSTING_TOTP_Secret="${One984HOSTING_TOTP_Secret:-$(_readaccountconf_mutable One984HOSTING_TOTP_Secret)}"
|
||||
if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then
|
||||
One984HOSTING_Username=""
|
||||
One984HOSTING_Password=""
|
||||
|
|
@ -225,9 +250,15 @@ _get_root() {
|
|||
|
||||
# Usage: _get_zone_id url domain.com
|
||||
# Returns zone id for domain.com
|
||||
# Memoized per-domain so add/rm don't re-fetch the same zone list within a run.
|
||||
# Keyed on domain (not url) since the url is always the domains listing.
|
||||
_get_zone_id() {
|
||||
url=$1
|
||||
domain=$2
|
||||
if [ "$_zone_id_for" = "$domain" ] && [ -n "$_zone_id" ]; then
|
||||
_debug2 _zone_id "$_zone_id (cached)"
|
||||
return 0
|
||||
fi
|
||||
_htmlget "$url" "$domain"
|
||||
_zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)"
|
||||
_debug2 _zone_id "$_zone_id"
|
||||
|
|
@ -235,6 +266,7 @@ _get_zone_id() {
|
|||
_err "Error getting _zone_id for $2."
|
||||
return 1
|
||||
fi
|
||||
_zone_id_for="$domain"
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -257,9 +289,8 @@ _htmlget() {
|
|||
|
||||
# Add extra headers to request
|
||||
_authpost() {
|
||||
url="https://1984.hosting/domains"
|
||||
_get_zone_id "$url" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
|
||||
_get_zone_id "https://1984.hosting/domains" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
|
||||
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
|
||||
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,16 @@ dns_acmedns_add() {
|
|||
ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}"
|
||||
ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}"
|
||||
|
||||
#for compatibility: old versions stored ACMEDNS_UPDATE_URL in the account
|
||||
#conf (issue 3899). Do not clear it here: it must stay available for the
|
||||
#other domains that have not migrated to their domain conf yet.
|
||||
if [ -z "$ACMEDNS_BASE_URL" ]; then
|
||||
_acmedns_update_url="$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)"
|
||||
if [ "$_acmedns_update_url" ]; then
|
||||
ACMEDNS_BASE_URL="$(echo "$_acmedns_update_url" | sed 's#/update$##')"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ACMEDNS_BASE_URL" = "" ]; then
|
||||
ACMEDNS_BASE_URL="https://auth.acme-dns.io"
|
||||
fi
|
||||
|
|
@ -71,7 +81,7 @@ dns_acmedns_add() {
|
|||
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
|
||||
_debug response "$response"
|
||||
|
||||
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ Ali_DNS_API="https://alidns.aliyuncs.com/"
|
|||
|
||||
#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_ali_add() {
|
||||
fulldomain=$1
|
||||
# the API only accepts punycode for IDN domains, and a raw UTF-8 domain
|
||||
# also breaks the request signature (issue 4733)
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
_prepare_ali_credentials || return 1
|
||||
|
|
@ -33,7 +35,7 @@ dns_ali_add() {
|
|||
}
|
||||
|
||||
dns_ali_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
|
|
@ -69,8 +71,8 @@ _ali_rest() {
|
|||
ign="$2"
|
||||
mtd="${3:-GET}"
|
||||
|
||||
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(printf "%s" "$signature" | _url_encode upper-hex)
|
||||
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _ali_urlencode_upper)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(printf "%s" "$signature" | _ali_urlencode_upper)
|
||||
url="$endpoint?Signature=$signature"
|
||||
|
||||
if [ "$mtd" = "GET" ]; then
|
||||
|
|
@ -96,6 +98,20 @@ _ali_rest() {
|
|||
fi
|
||||
}
|
||||
|
||||
# stdin stdout
|
||||
# The Aliyun signature requires percent-encoding with upper-case hex.
|
||||
# Do not use "_url_encode upper-hex" here: this file is also bundled by
|
||||
# third parties (e.g. Proxmox VE proxmox-acme) whose older copies of the
|
||||
# acme.sh function library ignore the upper-hex argument and output
|
||||
# lower-case hex, which invalidates the signature.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6272
|
||||
_ali_urlencode_upper() {
|
||||
{
|
||||
_url_encode
|
||||
echo
|
||||
} | sed 's/%a/%A/g;s/%b/%B/g;s/%c/%C/g;s/%d/%D/g;s/%e/%E/g;s/%f/%F/g;s/%\(.\)a/%\1A/g;s/%\(.\)b/%\1B/g;s/%\(.\)c/%\1C/g;s/%\(.\)d/%\1D/g;s/%\(.\)e/%\1E/g;s/%\(.\)f/%\1F/g'
|
||||
}
|
||||
|
||||
_ali_nonce() {
|
||||
if [ "$ACME_OPENSSL_BIN" ]; then
|
||||
"$ACME_OPENSSL_BIN" rand -hex 16 2>/dev/null && return 0
|
||||
|
|
|
|||
490
dnsapi/dns_arubabusiness.sh
Normal file
490
dnsapi/dns_arubabusiness.sh
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
dns_arubabusiness_info='ArubaBusiness
|
||||
Site: business.aruba.it
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness
|
||||
Options:
|
||||
AB_Key Your ArubaBusiness API Key
|
||||
AB_User Your account user
|
||||
AB_Pass Your account password
|
||||
'
|
||||
|
||||
#
|
||||
# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type.
|
||||
# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges
|
||||
#
|
||||
|
||||
ARUBABUSINESS_API='https://api.arubabusiness.it'
|
||||
|
||||
######## Public functions ########
|
||||
|
||||
#
|
||||
# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333
|
||||
#
|
||||
# Add a new TXT record whose name and value match the given domain and value
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the name of the TXT record
|
||||
# _txt_value: $2 - the value of the TXT record
|
||||
# _body
|
||||
# dns_details
|
||||
# domain_id
|
||||
# dns_record_id
|
||||
# response
|
||||
#
|
||||
dns_arubabusiness_add() {
|
||||
_full_domain=$1
|
||||
_txt_value=$2
|
||||
|
||||
if ! _ab_authenticate; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_domain_id "$_full_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
|
||||
# This is very unlikely, but allow the process to use the existing record
|
||||
_info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }"
|
||||
|
||||
_debug "Adding TXT record with name: $_full_domain and value: $_txt_value"
|
||||
|
||||
if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then
|
||||
_err "Failed to add TXT record with name: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Sleeping 10 seconds to let ArubaBusiness do its magic"
|
||||
_sleep 10
|
||||
|
||||
# Refresh dns details and check that the record was really added
|
||||
if ! _ab_dns_details "$root_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
|
||||
# This should never happen
|
||||
_err "The TXT record with name: $_full_domain was not set"
|
||||
_err "Please check that the dns records are clean"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Added TXT record with id: $dns_record_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333
|
||||
#
|
||||
# Remove the TXT record whose name and value match the given domain and value
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the name of the TXT record
|
||||
# _txt_value: $2 - the value of the TXT record
|
||||
# dns_details
|
||||
# dns_record_id
|
||||
#
|
||||
dns_arubabusiness_rm() {
|
||||
_full_domain=$1
|
||||
_txt_value=$2
|
||||
|
||||
if ! _ab_authenticate; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_domain_id "$_full_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then
|
||||
_err "Could not retrieve the record id for: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record: $dns_record_id"
|
||||
if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then
|
||||
_err "Failed to delete TXT record: $dns_record_id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted TXT record: $dns_record_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ########
|
||||
|
||||
#
|
||||
# Usage: _ab_domain_id _acme-challenge.www.domain.com
|
||||
#
|
||||
# Split the input domain into subdomain + root domain and get the id of the root domain
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the domain whose root needs to be extracted
|
||||
# _domain_sections
|
||||
# _current_index
|
||||
# _candidate_subdomain
|
||||
# _candidate_domain
|
||||
# sub_domain
|
||||
# root_domain
|
||||
# domain_id
|
||||
# dns_details: a json containing all dns records registered on the root domain
|
||||
#
|
||||
# Example
|
||||
# _get_root _acme-challenge.www.domain.com
|
||||
#
|
||||
# Should return
|
||||
# sub_domain=_acme-challenge.www
|
||||
# root_domain=domain.com
|
||||
# domain_id=123123123123
|
||||
# dns_details="{JSON_CONTENT}"
|
||||
#
|
||||
_ab_domain_id() {
|
||||
_full_domain=$1
|
||||
|
||||
_info "Attempting to retrieve root domain details for: $_full_domain"
|
||||
|
||||
_domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1)
|
||||
|
||||
if [ "$_domain_sections" -lt 1 ]; then
|
||||
_err "Invalid input $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_current_index=1
|
||||
while true; do
|
||||
_candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi)
|
||||
_candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections")
|
||||
|
||||
if ! _ab_dns_details "$_candidate_domain"; then
|
||||
_debug2 "Could not fetch dns details for: $_candidate_domain"
|
||||
_current_index=$(_math "$_current_index" + 1)
|
||||
|
||||
# Fail if there are no candidates left
|
||||
if [ "$_current_index" -gt "$_domain_sections" ]; then
|
||||
_err "Could not determine the root domain for: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
sub_domain="$_candidate_subdomain"
|
||||
root_domain="$_candidate_domain"
|
||||
# Extract the domain id, which is an integer and contains no commas
|
||||
domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')"
|
||||
|
||||
if [ -z "$domain_id" ]; then
|
||||
_err "Could not determine the domain id for: $root_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Retrieved root domain id: $domain_id"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}"
|
||||
#
|
||||
# Extract the record id of the first TXT record whose name and content match the input values
|
||||
#
|
||||
# Variables
|
||||
# _record_name: $1
|
||||
# _txt_value: $2
|
||||
# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain'
|
||||
# _record_ids
|
||||
# _record_names
|
||||
# _record_types
|
||||
# _record_contents
|
||||
# _record_ids_count
|
||||
# _record_names_count
|
||||
# _record_types_count
|
||||
# _record_contents_count
|
||||
# _i
|
||||
# dns_record_id
|
||||
#
|
||||
# Notes
|
||||
# TXT correspond to record type 5
|
||||
# ArubaBusiness appends a terminating dot (.) to the record name
|
||||
# The content field may contain the following character sequence: \"
|
||||
# All record names are always converted to lowercase
|
||||
#
|
||||
_ab_dns_record_id() {
|
||||
_record_name=$1
|
||||
_txt_value=$2
|
||||
_dns_details=$3
|
||||
|
||||
_record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case)
|
||||
|
||||
# Extract the record ids, which are integers and contain no commas, colons or spaces
|
||||
# The first id is skipped because it refers to the domain id
|
||||
_record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
|
||||
|
||||
# Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes
|
||||
# The first name is skipped because it refers to the domain name
|
||||
_record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ')
|
||||
|
||||
# Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces
|
||||
# The first type is skipped because it refers to the domain type
|
||||
_record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
|
||||
|
||||
# Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals
|
||||
# Note: There is no domain related entry here
|
||||
# Note: A " character is appended at the end of each content to make it easier to process the list later
|
||||
_record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#')
|
||||
|
||||
_info "IDS: $_record_ids"
|
||||
_info "NAMES: $_record_names"
|
||||
_info "TYPEs: $_record_types"
|
||||
_info "CONTENTS: $_record_contents"
|
||||
|
||||
_record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l)
|
||||
_record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l)
|
||||
_record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l)
|
||||
_record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l)
|
||||
|
||||
_info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
|
||||
|
||||
if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then
|
||||
_err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value"
|
||||
|
||||
_i=1
|
||||
while [ "$_i" -le "$_record_ids_count" ]; do
|
||||
_current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i")
|
||||
_current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i")
|
||||
_current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i")
|
||||
|
||||
if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then
|
||||
dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i")
|
||||
_info "Found matching record with id: $dns_record_id"
|
||||
return 0
|
||||
else
|
||||
_debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'"
|
||||
fi
|
||||
_i=$(_math "$_i" + 1)
|
||||
done
|
||||
|
||||
_debug2 "No matching record was found in $_dns_details"
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_dns_details domain.com
|
||||
#
|
||||
# Retrieve dns info for the given input domain
|
||||
#
|
||||
# Variables
|
||||
# _domain: $1
|
||||
# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0)
|
||||
# response
|
||||
#
|
||||
_ab_dns_details() {
|
||||
_domain=$1
|
||||
|
||||
if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
dns_details="$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_authenticate
|
||||
#
|
||||
# Read account conf, update domain conf and perform user authentication to acquire an access token
|
||||
#
|
||||
# Variables
|
||||
# AB_Key
|
||||
# AB_User
|
||||
# AB_Pass
|
||||
# AB_Token
|
||||
#
|
||||
_ab_authenticate() {
|
||||
AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}"
|
||||
AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}"
|
||||
AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}"
|
||||
|
||||
if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then
|
||||
AB_Key=""
|
||||
AB_User=""
|
||||
AB_Pass=""
|
||||
_err "Either the ArubaBusiness API key, the user or the password has not been defined yet."
|
||||
_err "Please configure them and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable AB_Key "$AB_Key"
|
||||
_saveaccountconf_mutable AB_User "$AB_User"
|
||||
_saveaccountconf_mutable AB_Pass "$AB_Pass"
|
||||
|
||||
if ! _ab_get_token || [ -z "$AB_Token" ]; then
|
||||
_err "Failed to acquire an access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_get_token
|
||||
#
|
||||
# Try acquiring a temporary access token. The token should have a 24h lifespan
|
||||
#
|
||||
# Variables
|
||||
# _ab_user_enc
|
||||
# _ab_pass_enc
|
||||
# _ab_authdata
|
||||
# AB_User
|
||||
# AB_Pass
|
||||
# AB_Token
|
||||
# response
|
||||
# _H2
|
||||
#
|
||||
_ab_get_token() {
|
||||
_ab_user_enc=$(printf "%s" "$AB_User" | _url_encode)
|
||||
_ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode)
|
||||
_ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc"
|
||||
|
||||
_H2="Content-Type: application/x-www-form-urlencoded"
|
||||
|
||||
if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then
|
||||
_err "Authentication failure"
|
||||
return 1
|
||||
fi
|
||||
|
||||
AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')"
|
||||
|
||||
if [ -z "$AB_Token" ]; then
|
||||
_err "Could not extract access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Acquired access token"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_rest POST "example/endpoint" "password=123"
|
||||
#
|
||||
# Perform a REST request using the given method, endpoint and data
|
||||
#
|
||||
# Variables
|
||||
# _method: $1 - The http method
|
||||
# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API)
|
||||
# _data: $3 - The body of the request (optional)
|
||||
# _key_trimmed
|
||||
# _token_trimmed
|
||||
# _ret_code
|
||||
# AB_Key
|
||||
# AB_Token
|
||||
# ARUBABUSINESS_API
|
||||
# _H1
|
||||
# _H2
|
||||
# _H3
|
||||
# _H4
|
||||
#
|
||||
_ab_rest() {
|
||||
_method=$1
|
||||
_endpoint="$2"
|
||||
_data="$3"
|
||||
|
||||
_key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"')
|
||||
_token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"')
|
||||
|
||||
_H1="Accept: application/json"
|
||||
|
||||
if [ -z "$_H2" ]; then
|
||||
# Default to application/json
|
||||
_H2="Content-Type: application/json"
|
||||
fi
|
||||
|
||||
if [ "$_key_trimmed" ]; then
|
||||
_H3="Authorization-Key: $_key_trimmed"
|
||||
else
|
||||
_err "Missing Api Key"
|
||||
_ab_cleanup_headers
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_token_trimmed" ]; then
|
||||
_H4="Authorization: Bearer $_token_trimmed"
|
||||
else
|
||||
_debug "No access token set"
|
||||
fi
|
||||
|
||||
if [ "$_method" != "GET" ]; then
|
||||
response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")"
|
||||
else
|
||||
response="$(_get "$ARUBABUSINESS_API/$_endpoint")"
|
||||
fi
|
||||
|
||||
_ret_code=$?
|
||||
|
||||
if [ "$_ret_code" = "0" ] && _ab_call_is_success; then
|
||||
# Normalize the json response
|
||||
response="$(printf "%s" "$response" | _normalizeJson)"
|
||||
_ret_code=0
|
||||
else
|
||||
_err "Failed to call endpoint: $_endpoint"
|
||||
_ret_code=1
|
||||
fi
|
||||
|
||||
_ab_cleanup_headers
|
||||
|
||||
return $_ret_code
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_cleanup_headers
|
||||
#
|
||||
# Unset header variables to avoid interfering with other calls
|
||||
#
|
||||
# Variables
|
||||
# _H1
|
||||
# _H2
|
||||
# _H3
|
||||
# _H4
|
||||
#
|
||||
_ab_cleanup_headers() {
|
||||
# Cleanup request headers
|
||||
unset _H1 _H2 _H3 _H4 _H5
|
||||
|
||||
# Cleanup response headers
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
: >"$HTTP_HEADER"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_call_is_success
|
||||
#
|
||||
# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled)
|
||||
#
|
||||
# Variables
|
||||
# _status
|
||||
# _http_status
|
||||
# _success_http_codes
|
||||
# HTTP_HEADER
|
||||
#
|
||||
_ab_call_is_success() {
|
||||
_success_http_codes="200 201 202 204"
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
_http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)
|
||||
for _status in $_success_http_codes; do
|
||||
if [ "$_status" = "$_http_status" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
|
@ -139,12 +139,21 @@ _get_autodns_zone() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# Escape the XML special characters (& < > ' ") so that credentials
|
||||
# containing them do not break the request document (issue 5317).
|
||||
_autodns_xml_encode() {
|
||||
sed "s/&/\&/g;s/</\</g;s/>/\>/g;s/'/\'/g;s/\"/\"/g"
|
||||
}
|
||||
|
||||
_build_request_auth_xml() {
|
||||
_autodns_user_xml="$(printf "%s" "$AUTODNS_USER" | _autodns_xml_encode)"
|
||||
_autodns_password_xml="$(printf "%s" "$AUTODNS_PASSWORD" | _autodns_xml_encode)"
|
||||
_autodns_context_xml="$(printf "%s" "$AUTODNS_CONTEXT" | _autodns_xml_encode)"
|
||||
printf "<auth>
|
||||
<user>%s</user>
|
||||
<password>%s</password>
|
||||
<context>%s</context>
|
||||
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
|
||||
</auth>" "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml"
|
||||
}
|
||||
|
||||
# Arguments:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ Options:
|
|||
# All `_sleep` commands are included to avoid Route53 throttling, see
|
||||
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
|
||||
|
||||
AWS_HOST="route53.amazonaws.com"
|
||||
# Updated from "route53.amazonaws.com"
|
||||
AWS_HOST="route53.global.api.aws"
|
||||
AWS_URL="https://$AWS_HOST"
|
||||
|
||||
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
|
||||
|
|
|
|||
|
|
@ -49,26 +49,95 @@ Options:
|
|||
Baidu_SK SecretAccessKey
|
||||
OptionsAlt:
|
||||
Baidu_BCD_Host API host, default: bcd.baidubce.com
|
||||
Baidu_DNS_Host New DNS API host, default: dns.baidubce.com
|
||||
Baidu_API_Preference Engine preference, default: auto
|
||||
Baidu_BCD_Version API version number, default: 1
|
||||
Baidu_BCD_Expire Signature expiration seconds, default: 3600
|
||||
Baidu_View Resolve view, default: DEFAULT
|
||||
Baidu_Line New DNS line, default: default
|
||||
Baidu_TTL Resolve ttl seconds, default: 300
|
||||
Baidu_RM_Max Max records to delete in one run, default: 20
|
||||
'
|
||||
|
||||
BAIDU_BCD_DEFAULT_HOST="bcd.baidubce.com"
|
||||
BAIDU_DNS_DEFAULT_HOST="dns.baidubce.com"
|
||||
|
||||
# --- Public API ---
|
||||
dns_baidu_add() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _baidu_prepare_record "$fulldomain"; then
|
||||
_baidu_err "baidu_prepare_record failed for add: $fulldomain"
|
||||
if ! _baidu_run_with_fallback "add" "$fulldomain" "$txtvalue"; then
|
||||
_baidu_err "all baidu api engines failed for add: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_baidu_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _baidu_run_with_fallback "rm" "$fulldomain" "$txtvalue"; then
|
||||
_baidu_err "all baidu api engines failed for delete: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_run_with_fallback() {
|
||||
_action="$1"
|
||||
_fulldomain="$2"
|
||||
_txtvalue="$3"
|
||||
|
||||
if ! _baidu_load_credentials; then
|
||||
_baidu_err "baidu_load_credentials failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for _baidu_api_engine in $(_baidu_engine_order); do
|
||||
if ! _baidu_prepare_record "$_fulldomain"; then
|
||||
_baidu_info "prepare failed for engine: $_baidu_api_engine"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$_action" = "add" ]; then
|
||||
if _baidu_add_record "$_txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if _baidu_rm_record "$_txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_baidu_info "engine failed, try next if available: $_baidu_api_engine"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_baidu_engine_order() {
|
||||
_pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")"
|
||||
case "$_pref" in
|
||||
legacy)
|
||||
printf "%s" "legacy new"
|
||||
;;
|
||||
new)
|
||||
printf "%s" "new legacy"
|
||||
;;
|
||||
*)
|
||||
printf "%s" "new legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_baidu_add_record() {
|
||||
_txtvalue="$1"
|
||||
|
||||
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -85,16 +154,28 @@ dns_baidu_add() {
|
|||
_ttl="300"
|
||||
;;
|
||||
esac
|
||||
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
|
||||
txtvalue="$(_baidu_trim_ws "$txtvalue")"
|
||||
|
||||
txtvalue="$(_baidu_trim_ws "$_txtvalue")"
|
||||
_record_domain="$(_baidu_trim_ws "$_record_domain")"
|
||||
_zone_name="$(_baidu_trim_ws "$_zone_name")"
|
||||
|
||||
_body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")"
|
||||
|
||||
if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: add record"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
_line="$(_baidu_trim_ws "${Baidu_Line:-default}")"
|
||||
if [ -z "$_line" ]; then
|
||||
_line="default"
|
||||
fi
|
||||
_body="$(_baidu_payload_add_txt_dns "$_record_domain" "$txtvalue" "$_ttl" "$_line")"
|
||||
if ! _baidu_dns_call "POST" "/v1/dns/zone/${_zone_name}/record" "$_body"; then
|
||||
_baidu_err "baidu_dns_call failed: add record"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
|
||||
_body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")"
|
||||
if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: add record"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if _baidu_is_api_error "$response"; then
|
||||
|
|
@ -105,16 +186,10 @@ dns_baidu_add() {
|
|||
return 0
|
||||
}
|
||||
|
||||
dns_baidu_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
_baidu_rm_record() {
|
||||
_txtvalue="$1"
|
||||
|
||||
if ! _baidu_prepare_record "$fulldomain"; then
|
||||
_baidu_err "baidu_prepare_record failed for delete: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
|
||||
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -138,28 +213,37 @@ dns_baidu_rm() {
|
|||
fi
|
||||
|
||||
for _rid in $_ids; do
|
||||
_body="$(_baidu_payload_delete "$_zone_name" "$_rid")"
|
||||
if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "$response"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
if ! _baidu_dns_call "DELETE" "/v1/dns/zone/${_zone_name}/record/${_rid}" ""; then
|
||||
_baidu_err "baidu_dns_call failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_body="$(_baidu_payload_delete "$_zone_name" "$_rid")"
|
||||
if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "$response"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
_left_ids="$_BAIDU_FIND_RESULT"
|
||||
if [ -z "$_left_ids" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$_left_ids" ]; then
|
||||
_baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "legacy" ]; then
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
_left_ids="$_BAIDU_FIND_RESULT"
|
||||
if [ -z "$_left_ids" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$_left_ids" ]; then
|
||||
_baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
|
|
@ -182,6 +266,7 @@ _baidu_load_credentials() {
|
|||
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
|
||||
|
||||
BAIDU_BCD_HOST="${Baidu_BCD_Host:-$BAIDU_BCD_DEFAULT_HOST}"
|
||||
BAIDU_DNS_HOST="${Baidu_DNS_Host:-$BAIDU_DNS_DEFAULT_HOST}"
|
||||
BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}"
|
||||
|
||||
return 0
|
||||
|
|
@ -189,13 +274,16 @@ _baidu_load_credentials() {
|
|||
|
||||
_baidu_prepare_record() {
|
||||
_fulldomain="$1"
|
||||
if ! _baidu_load_credentials; then
|
||||
_baidu_err "baidu_load_credentials failed"
|
||||
return 1
|
||||
fi
|
||||
if ! _baidu_get_root "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone for $_fulldomain"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
if ! _baidu_get_root_dns "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone by new dns api for $_fulldomain"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if ! _baidu_get_root "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone by legacy bcd api for $_fulldomain"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_record_domain="$_sub_domain"
|
||||
_zone_name="$_domain"
|
||||
|
|
@ -234,6 +322,43 @@ _baidu_get_root() {
|
|||
done
|
||||
}
|
||||
|
||||
_baidu_get_root_dns() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
_baidu_err "invalid domain: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _baidu_dns_call "GET" "/v1/dns/zone/${h}/record" ""; then
|
||||
_baidu_info "baidu_dns_call failed: list zones"
|
||||
elif ! _baidu_is_api_error "$response" && (_contains "$response" "\"records\"" || _contains "$response" "\"maxKeys\""); then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
if [ "$_sub_domain" = "$_domain" ]; then
|
||||
_sub_domain="@"
|
||||
fi
|
||||
_baidu_info "zone matched by dns api: $_domain (host: $_sub_domain)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_baidu_find_record_ids_current() {
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
_baidu_find_record_ids_dns "$@"
|
||||
else
|
||||
_baidu_find_record_ids "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
_baidu_find_record_ids() {
|
||||
_zone_name="$1"
|
||||
_record_domain="$2"
|
||||
|
|
@ -293,6 +418,39 @@ EOF
|
|||
_BAIDU_FIND_RESULT="$_ids"
|
||||
}
|
||||
|
||||
_baidu_find_record_ids_dns() {
|
||||
_zone_name="$1"
|
||||
_record_domain="$2"
|
||||
_rdtype="$3"
|
||||
_rdata="$4"
|
||||
_BAIDU_FIND_RESULT=""
|
||||
|
||||
if ! _baidu_dns_call "GET" "/v1/dns/zone/${_zone_name}/record" ""; then
|
||||
_baidu_err "baidu_dns_call failed: list records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "baidu_dns error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_normalized="$(printf "%s" "$response" | _normalizeJson)"
|
||||
_records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g')
|
||||
_ids=""
|
||||
|
||||
while IFS= read -r _line; do
|
||||
_id="$(_baidu_match_record_id_dns "$_line" "$_record_domain" "$_rdtype" "$_rdata")"
|
||||
if [ "$_id" ]; then
|
||||
_ids="$_ids $_id"
|
||||
fi
|
||||
done <<EOF
|
||||
$_records
|
||||
EOF
|
||||
|
||||
_BAIDU_FIND_RESULT="$_ids"
|
||||
}
|
||||
|
||||
# --- HTTP ---
|
||||
_baidu_bcd_post() {
|
||||
_api_path="$1"
|
||||
|
|
@ -317,18 +475,17 @@ _baidu_bcd_post() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_BCD_HOST"
|
||||
_H5=""
|
||||
|
||||
_url="https://${BAIDU_BCD_HOST}${_uri}"
|
||||
_signed_headers_dbg="$(printf "%s" "$_auth" | cut -d / -f 5)"
|
||||
_baidu_info "POST ${_uri}"
|
||||
_baidu_info "signedHeaders: $_signed_headers_dbg"
|
||||
_baidu_info "payload_sha256: $_payload_hash"
|
||||
_baidu_debug "baidu_bcd.http.payload" "$(_baidu_dbg_trim "$(_baidu_redact_txt "$_payload")")"
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_BCD_HOST"
|
||||
_H5=""
|
||||
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
|
||||
_ret="$?"
|
||||
_baidu_info "ret: $_ret"
|
||||
|
|
@ -344,6 +501,56 @@ _baidu_bcd_post() {
|
|||
return 0
|
||||
}
|
||||
|
||||
_baidu_dns_call() {
|
||||
_method="$1"
|
||||
_uri="$2"
|
||||
_payload="$3"
|
||||
_content_type="application/json"
|
||||
_attempt=1
|
||||
_max_attempts=3
|
||||
|
||||
while [ "$_attempt" -le "$_max_attempts" ]; do
|
||||
_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
|
||||
|
||||
if ! _baidu_bce_auth "$_method" "$_uri" "" "$BAIDU_DNS_HOST" "$_ts" "${Baidu_BCD_Expire:-3600}" "$_content_type" "$_payload_hash"; then
|
||||
_baidu_err "baidu_dns auth failed"
|
||||
return 1
|
||||
fi
|
||||
_auth="$_BAIDU_BCE_AUTH_RESULT"
|
||||
_url="https://${BAIDU_DNS_HOST}${_uri}"
|
||||
|
||||
# Route through acme.sh's _get/_post (they honor _H1.._H5); no raw curl.
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_DNS_HOST"
|
||||
_H5="Content-Type: $_content_type"
|
||||
|
||||
if [ "$_method" = "GET" ]; then
|
||||
response="$(_get "$_url")"
|
||||
elif [ "$_method" = "DELETE" ]; then
|
||||
response="$(_post "" "$_url" "" "DELETE")"
|
||||
else
|
||||
response="$(_post "$_payload" "$_url")"
|
||||
fi
|
||||
_ret="$?"
|
||||
_baidu_info "${_method} ${_uri} ret=${_ret}"
|
||||
|
||||
# Baidu may return a business error (Exception / 平台服务繁忙) inside HTTP 200.
|
||||
if [ "$_ret" = "0" ] && ! _contains "$response" "\"code\":\"Exception\"" && ! _contains "$response" "平台服务繁忙"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$_attempt" -lt "$_max_attempts" ]; then
|
||||
sleep 2
|
||||
fi
|
||||
_attempt=$(_math "$_attempt" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Auth / Signing ---
|
||||
_baidu_bce_auth() {
|
||||
# Signing algorithm (bce-auth-v1):
|
||||
|
|
@ -499,6 +706,14 @@ _baidu_payload_add_txt() {
|
|||
printf "%s" "{\"domain\":\"${_domain}\",\"view\":\"${_view}\",\"rdType\":\"TXT\",\"ttl\":${_ttl},\"rdata\":\"${_rdata}\",\"zoneName\":\"${_zoneName}\"}"
|
||||
}
|
||||
|
||||
_baidu_payload_add_txt_dns() {
|
||||
_rr="$(printf "%s" "$1" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_value="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_ttl="$3"
|
||||
_line="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
printf "%s" "{\"rr\":\"${_rr}\",\"type\":\"TXT\",\"value\":\"${_value}\",\"ttl\":${_ttl},\"line\":\"${_line}\",\"description\":\"acme.sh\"}"
|
||||
}
|
||||
|
||||
_baidu_payload_delete() {
|
||||
_zoneName="$(_baidu_json_escape "$1")"
|
||||
_recordId="$2"
|
||||
|
|
@ -541,6 +756,18 @@ _baidu_match_record_id() {
|
|||
printf "%s" "$_line" | _egrep_o "\"recordId\": *[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d " "
|
||||
}
|
||||
|
||||
_baidu_match_record_id_dns() {
|
||||
_line="$1"
|
||||
_rr="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_type="$(printf "%s" "$3" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_value="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
case "$_line" in
|
||||
*"\"rr\":\"${_rr}\""*"\"type\":\"${_type}\""*"\"value\":\"${_value}\""*)
|
||||
printf "%s" "$_line" | sed -n 's/.*"id":"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' | _head_n 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_baidu_hmac_sha256_hexkey() {
|
||||
_key_hex="$1"
|
||||
_msg="$2"
|
||||
|
|
|
|||
|
|
@ -323,21 +323,21 @@ _bhosted_extract_id() {
|
|||
fi
|
||||
|
||||
# JSON: "id":12345
|
||||
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||
if [ -n "$_id" ]; then
|
||||
printf "%s" "$_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# key=value: id=12345
|
||||
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||
if [ -n "$_id" ]; then
|
||||
printf "%s" "$_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# "record id 12345" / "recordid 12345"
|
||||
_id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||
_id="$(printf "%s" "$_resp" | _egrep_o '(record[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
|
||||
if [ -n "$_id" ]; then
|
||||
printf "%s" "$_id"
|
||||
return 0
|
||||
|
|
|
|||
137
dnsapi/dns_calrissia.sh
Normal file
137
dnsapi/dns_calrissia.sh
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_calrissia_info='Calrissia.be DNS API
|
||||
Site: calrissia.be
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_calrissia
|
||||
Options:
|
||||
CALRISSIA_TOKEN Personal access token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6809
|
||||
Author: Ward Hus
|
||||
'
|
||||
|
||||
CALRISSIA_API="https://my.calrissia.com/api"
|
||||
|
||||
dns_calrissia_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_calrissia_load_token || return 1
|
||||
|
||||
if ! _calrissia_get_root "$fulldomain"; then
|
||||
_err "Unable to find domain in Calrissia account for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
|
||||
_body="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120,\"prio\":0}"
|
||||
_response="$(_calrissia_request POST "/domain/$_domain_id/record" "$_body")"
|
||||
|
||||
if ! _contains "$_response" '"id"'; then
|
||||
_err "Failed to create TXT record: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_calrissia_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_calrissia_load_token || return 1
|
||||
|
||||
if ! _calrissia_get_root "$fulldomain"; then
|
||||
_err "Unable to find domain in Calrissia account for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
|
||||
|
||||
# Look the record up from the API instead of relying on local state.
|
||||
# The record list is embedded in the domain object.
|
||||
_response="$(_calrissia_request GET "/domain/$_domain_id")"
|
||||
_debug2 "Response: $_response"
|
||||
|
||||
# Split the record objects onto separate lines, then match on both the
|
||||
# subdomain name and the TXT value to find the record id to delete.
|
||||
_record_id="$(printf "%s" "$_response" |
|
||||
tr '{}' '\n' |
|
||||
grep "\"name\" *: *\"$_sub_domain\"" |
|
||||
grep "\"content\" *: *\"$txtvalue\"" |
|
||||
_egrep_o '"id" *: *[0-9]+' |
|
||||
_head_n 1 |
|
||||
_egrep_o '[0-9]+')"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "No matching TXT record found for $fulldomain; nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Removing TXT record id=$_record_id from domain id=$_domain_id"
|
||||
if ! _response="$(_calrissia_request DELETE "/domain/$_domain_id/record/$_record_id")" || _contains "$_response" '"error"'; then
|
||||
_err "Failed to remove TXT record: $_response"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
####################
|
||||
# Private helpers #
|
||||
####################
|
||||
|
||||
_calrissia_load_token() {
|
||||
CALRISSIA_TOKEN="${CALRISSIA_TOKEN:-$(_readaccountconf_mutable CALRISSIA_TOKEN)}"
|
||||
if [ -z "$CALRISSIA_TOKEN" ]; then
|
||||
_err "CALRISSIA_TOKEN is not set. Generate one at https://identity.calrissia.com under API Keys."
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable CALRISSIA_TOKEN "$CALRISSIA_TOKEN"
|
||||
}
|
||||
|
||||
# Sets _domain, _domain_id, _sub_domain for a given FQDN.
|
||||
_calrissia_get_root() {
|
||||
_fqdn="$1"
|
||||
|
||||
i=1
|
||||
while true; do
|
||||
_candidate="$(printf "%s" "$_fqdn" | cut -d . -f "$i"-)"
|
||||
[ -z "$_candidate" ] && return 1
|
||||
|
||||
_debug "Trying root domain: $_candidate"
|
||||
_response="$(_calrissia_request GET "/domain?full_domain_name=$_candidate")"
|
||||
_debug2 "Response: $_response"
|
||||
|
||||
_domain_id="$(printf "%s" "$_response" |
|
||||
_egrep_o '"id" *: *[0-9]+' |
|
||||
_head_n 1 |
|
||||
_egrep_o '[0-9]+')"
|
||||
|
||||
if [ -n "$_domain_id" ]; then
|
||||
if [ "$i" = "1" ]; then
|
||||
# The FQDN itself is the zone apex, e.g. a challenge-alias domain.
|
||||
_sub_domain=""
|
||||
else
|
||||
_sub_domain="$(printf "%s" "$_fqdn" | cut -d . -f "1-$((i - 1))")"
|
||||
fi
|
||||
_domain="$_candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
_calrissia_request() {
|
||||
_method="$1"
|
||||
_path="$2"
|
||||
_body="$3"
|
||||
export _H1="Authorization: Bearer $CALRISSIA_TOKEN"
|
||||
export _H2="Accept: application/json"
|
||||
if [ "$_method" = "GET" ]; then
|
||||
_get "$CALRISSIA_API$_path"
|
||||
else
|
||||
_post "$_body" "$CALRISSIA_API$_path" "" "$_method" "application/json"
|
||||
fi
|
||||
}
|
||||
137
dnsapi/dns_cdmon.sh
Normal file
137
dnsapi/dns_cdmon.sh
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
dns_cdmon_info='cdmon
|
||||
Site: www.cdmon.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon
|
||||
Options:
|
||||
CDMON_Key API Key
|
||||
'
|
||||
|
||||
CDMON_Api="https://api-domains.cdmon.services/api-domains"
|
||||
|
||||
######## Public functions #####################
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
dns_cdmon_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
|
||||
|
||||
if [ -z "$CDMON_Key" ]; then
|
||||
CDMON_Key=""
|
||||
_err "You didn't specify your cdmon api key yet."
|
||||
_err "Please create your key and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable CDMON_Key "$CDMON_Key"
|
||||
|
||||
_debug "First, we detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
_info "Adding record"
|
||||
if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then
|
||||
if _contains "$response" "\"status\":\"ok\""; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: fulldomain txtvalue
|
||||
# Used to remove the txt record after validation
|
||||
dns_cdmon_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
|
||||
_debug "First, we detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Removing record"
|
||||
if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then
|
||||
if _contains "$response" "\"status\":\"ok\""; then
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Delete txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Delete txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
if ! _cdmon_rest "domains/list"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
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 _contains "$response" "\"domain\":\"$h\""; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_cdmon_rest() {
|
||||
ep="$1"
|
||||
data="$2"
|
||||
_debug "$ep"
|
||||
|
||||
key_trimmed=$(echo "$CDMON_Key" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="apikey: $key_trimmed"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$CDMON_Api/$ep")"
|
||||
_ret="$?"
|
||||
|
||||
unset _H1 _H2
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ _dns_cloudns_init_check() {
|
|||
_dns_cloudns_http_api_call "dns/login.json" ""
|
||||
|
||||
if ! _contains "$response" "\"status\":\"Success\""; then
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Server response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ CN_API="https://beta.api.core-networks.de"
|
|||
######## Public functions #####################
|
||||
|
||||
dns_cn_add() {
|
||||
fulldomain=$1
|
||||
# Core-Networks API requires punycode for IDN domains
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _cn_login; then
|
||||
|
|
@ -58,7 +59,8 @@ dns_cn_add() {
|
|||
}
|
||||
|
||||
dns_cn_rm() {
|
||||
fulldomain=$1
|
||||
# Core-Networks API requires punycode for IDN domains
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _cn_login; then
|
||||
|
|
|
|||
248
dnsapi/dns_comlaude.sh
Normal file
248
dnsapi/dns_comlaude.sh
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
dns_comlaude_info='comlaude.com
|
||||
Site: comlaude.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude
|
||||
Options:
|
||||
COMLAUDE_USERNAME User account
|
||||
COMLAUDE_PASSWORD User password
|
||||
COMLAUDE_API_KEY generated API key
|
||||
COMLAUDE_GROUP_ID Group ID in comlaude user profile
|
||||
Get it from the https://www.comlaude.com
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7112
|
||||
'
|
||||
# ===== CONFIG =====
|
||||
COMLAUDE_API="https://api.comlaude.com"
|
||||
|
||||
########## AUTH ##########
|
||||
|
||||
_comlaude_auth() {
|
||||
_debug "Checking cached ComLaude token"
|
||||
|
||||
# Try to get token from account.conf
|
||||
if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then
|
||||
COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)"
|
||||
COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)"
|
||||
fi
|
||||
|
||||
_now=$(_time)
|
||||
if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then
|
||||
_debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "ComLaude auth..."
|
||||
_comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}"
|
||||
_comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")"
|
||||
|
||||
if ! _contains "$_comlaude_response" "access_token"; then
|
||||
_err "Auth failed: $_comlaude_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4)
|
||||
# store expiracy from api reply l'API ("expires_in" in seconds)
|
||||
_comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2)
|
||||
[ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info
|
||||
|
||||
COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew
|
||||
|
||||
_saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN"
|
||||
_saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########## DOMAIN RESOLUTION ##########
|
||||
|
||||
_comlaude_get_root() {
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
if [ -z "$COMLAUDE_GROUP_ID" ]; then
|
||||
_err "Missing COMLAUDE_GROUP_ID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_comlaude_input_domain="$1"
|
||||
_comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}"
|
||||
case "$_comlaude_input_domain" in
|
||||
\*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;;
|
||||
esac
|
||||
|
||||
_debug "Normalized domain: $_comlaude_input_domain"
|
||||
|
||||
_comlaude_i=1
|
||||
while true; do
|
||||
_comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-")
|
||||
[ -z "$_comlaude_d" ] && {
|
||||
_debug "No matching domain found for $_comlaude_input_domain"
|
||||
return 1
|
||||
}
|
||||
|
||||
# don't test unnecessary levels
|
||||
# registered domain : TLD only (no dot after cut).
|
||||
case "$_comlaude_d" in
|
||||
*.*) : ;;
|
||||
*)
|
||||
_debug "Skipping bare TLD candidate: $_comlaude_d"
|
||||
_comlaude_i=$((_comlaude_i + 1))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
_debug "Checking domain: $_comlaude_d"
|
||||
|
||||
_comlaude_retry=0
|
||||
_comlaude_max_retry=3 # to avoid network errors
|
||||
_comlaude_DOM_ID=""
|
||||
_comlaude_Z_ID=""
|
||||
|
||||
while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone"
|
||||
_comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")"
|
||||
_H1=""
|
||||
|
||||
_debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response"
|
||||
|
||||
# If empty -> true network issue, we retry
|
||||
if [ -z "$_comlaude_response" ]; then
|
||||
_comlaude_retry=$((_comlaude_retry + 1))
|
||||
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 404 -> domain not found in that level. no retry : continue
|
||||
if echo "$_comlaude_response" | grep -q '"status_code":404'; then
|
||||
_debug "404 for $_comlaude_d, moving to next level (not retrying)"
|
||||
break
|
||||
fi
|
||||
|
||||
# Domain missing (200 reply, data empty) -> continue
|
||||
if echo "$_comlaude_response" | grep -q '"data":\[\]'; then
|
||||
_debug "Empty data for $_comlaude_d, moving to next level"
|
||||
break
|
||||
fi
|
||||
|
||||
# Extraction via _egrep_o
|
||||
_comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')"
|
||||
_comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')"
|
||||
|
||||
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# 200 reply but malformed data / noid -> retry transport
|
||||
_comlaude_retry=$((_comlaude_retry + 1))
|
||||
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
|
||||
done
|
||||
|
||||
_debug "_comlaude_DOM_ID=$_comlaude_DOM_ID"
|
||||
_debug "_comlaude_Z_ID=$_comlaude_Z_ID"
|
||||
|
||||
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
|
||||
_comlaude_domain="$_comlaude_d"
|
||||
_comlaude_domain_id="$_comlaude_DOM_ID"
|
||||
_comlaude_zone_id="$_comlaude_Z_ID"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_comlaude_i=$((_comlaude_i + 1))
|
||||
done
|
||||
}
|
||||
########## ADD TXT ##########
|
||||
|
||||
dns_comlaude_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
|
||||
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
|
||||
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
|
||||
if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then
|
||||
_err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Backup variable after validation
|
||||
_saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME"
|
||||
_saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD"
|
||||
_saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY"
|
||||
_saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID"
|
||||
|
||||
_info "Adding TXT: $fulldomain"
|
||||
_comlaude_auth || return 1
|
||||
_comlaude_get_root "$fulldomain" || return 1
|
||||
|
||||
_debug "Root: $_comlaude_domain"
|
||||
|
||||
_comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}"
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
_comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")"
|
||||
|
||||
_H1=""
|
||||
_H2=""
|
||||
if ! echo "$_comlaude_response" | grep -q '"id"'; then
|
||||
_err "Failed to create TXT"
|
||||
_debug "$_comlaude_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########## REMOVE TXT ##########
|
||||
|
||||
dns_comlaude_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
|
||||
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
|
||||
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
|
||||
_info "Removing TXT: $fulldomain"
|
||||
|
||||
_comlaude_auth || return 1
|
||||
_comlaude_get_root "$fulldomain" || return 1
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)"
|
||||
_comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)"
|
||||
_comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value"
|
||||
_comlaude_response="$(_get "$_comlaude_url")"
|
||||
_H1=""
|
||||
|
||||
_debug "Filtered records response: $_comlaude_response"
|
||||
|
||||
# first "id" top-level of reply (record itself,
|
||||
# always on first position of each data[] object)
|
||||
_comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')"
|
||||
|
||||
if [ -z "$_comlaude_record_id" ]; then
|
||||
_info "No matching TXT record found to delete for $fulldomain / $txtvalue"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting record $_comlaude_record_id"
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id"
|
||||
_comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")"
|
||||
_H1=""
|
||||
|
||||
if echo "$_comlaude_del_resp" | grep -q '"error"'; then
|
||||
_err "Delete failed for $_comlaude_record_id"
|
||||
_debug "$_comlaude_del_resp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted record $_comlaude_record_id"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ dns_cpanel_add() {
|
|||
fi
|
||||
# adding entry
|
||||
_info "Adding the entry"
|
||||
stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//")
|
||||
stripped_fulldomain="${fulldomain%."$_domain"}"
|
||||
_debug "Adding $stripped_fulldomain to $_domain zone"
|
||||
_myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1"
|
||||
if _successful_update; then return 0; fi
|
||||
|
|
@ -128,13 +128,27 @@ _get_root() {
|
|||
_err "Primary domain list not found!"
|
||||
return 1
|
||||
fi
|
||||
for _domain in $_domains; do
|
||||
_debug "Checking if $fulldomain ends with $_domain"
|
||||
if (_endswith "$fulldomain" "$_domain"); then
|
||||
_debug "Root domain: $_domain"
|
||||
return 0
|
||||
fi
|
||||
# Pick the LONGEST matching zone, dot-anchored: with both domain.tld and
|
||||
# sub.domain.tld zones on the account, cPanel stores the record in the
|
||||
# most specific zone, so add and rm must both resolve to that one.
|
||||
_domain=""
|
||||
for d in $_domains; do
|
||||
_debug "Checking if $fulldomain ends with $d"
|
||||
# case with quoted patterns gives an exact literal suffix match;
|
||||
# _endswith treats the needle as a regex, so its dots would let
|
||||
# xdomain.tld wrongly match zone domain.tld
|
||||
case "$fulldomain" in
|
||||
"$d" | *".$d")
|
||||
if [ "${#d}" -gt "${#_domain}" ]; then
|
||||
_domain="$d"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$_domain" ]; then
|
||||
_debug "Root domain: $_domain"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
|
|
|
|||
181
dnsapi/dns_creoline.sh
Normal file
181
dnsapi/dns_creoline.sh
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_creoline_info='creoline
|
||||
Site: https://www.creoline.com/de
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_creoline
|
||||
Help: https://help.creoline.com
|
||||
Options:
|
||||
creolineApiToken
|
||||
creolineApiSecret
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7103
|
||||
'
|
||||
|
||||
creolineApi="https://api.creoline.com/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPB8"
|
||||
dns_creoline_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
|
||||
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
|
||||
|
||||
if [ -z "$creolineApiToken" ] || [ -z "$creolineApiSecret" ]; then
|
||||
_err "Error required creoline API Token or creoline API Secret not specified."
|
||||
_err "Please set it with the Command 'export creolineApiToken=<YourToken>' and 'export creolineApiSecret=<YourSecret>'."
|
||||
return 1
|
||||
else
|
||||
_saveaccountconf_mutable creolineApiToken "$creolineApiToken"
|
||||
_saveaccountconf_mutable creolineApiSecret "$creolineApiSecret"
|
||||
fi
|
||||
|
||||
_debug "Detecting the root dns zone."
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Error on detecting the root dns zone."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Adding record"
|
||||
if _creoline_rest POST "dns/zone/$_domain/record" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"record\":\"$txtvalue\",\"ttl\":\"60\"}"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_creoline_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
|
||||
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
|
||||
|
||||
_debug "Detecting the root dns zone."
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Error on detecting the root dns zone."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Getting earlier created txt record."
|
||||
if ! _creoline_rest GET "dns/zone/$_domain/record/type/TXT/record/$txtvalue"; then
|
||||
if _contains "$response" "errors" || _contains "$response" "message"; then
|
||||
_err "Error on getting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
_err "Error on getting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$response" | _egrep_o "\"id\"[ ]*:[ ]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug "record_id" "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_err "Error on deleting earlier created txt record. No record id found in response."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleting earlier created txt record."
|
||||
if ! _creoline_rest DELETE "dns/zone/$_domain/record/$record_id"; then
|
||||
if _contains "$response" "errors" || _contains "$response" "message"; then
|
||||
_err "Error on deleting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
_err "Error on deleting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
if ! _creoline_rest GET "dns/zone/root/$domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_sub_domain=$(echo "$response" | _egrep_o "\"subDomain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_domain=$(echo "$response" | _egrep_o "\"domain\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_creoline_rest() {
|
||||
method=$1
|
||||
uri="$2"
|
||||
data="$3"
|
||||
timestamp=$(_time)
|
||||
canonical_request="${timestamp}.${creolineApi}/${uri}"
|
||||
signature_hash=$(printf "%s" "$canonical_request" | _hmac sha256 "$(printf "%s" "$creolineApiSecret" | _hex_dump | tr -d " ")" hex)
|
||||
|
||||
_debug method "$method"
|
||||
_debug uri "$uri"
|
||||
_debug data "$data"
|
||||
|
||||
_debug2 timestamp "$timestamp"
|
||||
_debug2 canonical_request "$canonical_request"
|
||||
_debug2 signature_hash "$signature_hash"
|
||||
|
||||
token_trimmed=$(echo "$creolineApiToken" | tr -d '"')
|
||||
hmac_trimmed=$(echo "$signature_hash" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
|
||||
if [ "$token_trimmed" ]; then
|
||||
export _H2="X-Api-Token: $token_trimmed"
|
||||
fi
|
||||
|
||||
if [ "$hmac_trimmed" ]; then
|
||||
export _H3="X-Creoline-Api-Signature: $hmac_trimmed"
|
||||
fi
|
||||
|
||||
if [ "$timestamp" ]; then
|
||||
export _H4="X-Creoline-Api-Timestamp: $timestamp"
|
||||
fi
|
||||
|
||||
if [ "$method" != "GET" ]; then
|
||||
response="$(_post "$data" "$creolineApi/$uri" "" "$method")"
|
||||
else
|
||||
response="$(_get "$creolineApi/$uri")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $uri"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if _contains "$response" "errors"; then
|
||||
error=$(echo "$response" | _egrep_o "\"errors\":[[]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | tr -d "[")
|
||||
_err "Error: $error"
|
||||
_err "URI:$uri"
|
||||
return 1
|
||||
elif _contains "$response" "message"; then
|
||||
message=$(echo "$response" | _egrep_o "\"message\"[ ]*:[ ]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
||||
_err "Error: $message"
|
||||
_err "URI:$uri"
|
||||
return 1
|
||||
fi
|
||||
|
||||
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)"
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ dns_czechia_add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
||||
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||
_czechia_tab="$(printf '\t')"
|
||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
|
||||
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
|
||||
|
||||
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
|
||||
_err "Missing zone or CZ_AuthorizationToken."
|
||||
|
|
@ -76,7 +77,7 @@ dns_czechia_add() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
_nres="$(_normalizeJson "$_res")"
|
||||
_nres="$(printf '%s' "$_res" | _normalizeJson)"
|
||||
if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then
|
||||
_nres="$_res"
|
||||
fi
|
||||
|
|
@ -108,8 +109,9 @@ dns_czechia_rm() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
||||
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||
_czechia_tab="$(printf '\t')"
|
||||
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
|
||||
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
|
||||
|
||||
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
|
||||
_err "Missing zone or CZ_AuthorizationToken."
|
||||
|
|
@ -180,12 +182,13 @@ _czechia_load_conf() {
|
|||
}
|
||||
|
||||
_czechia_pick_zone() {
|
||||
_czechia_pz_tab="$(printf '\t')"
|
||||
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
|
||||
_best_zone=""
|
||||
|
||||
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
|
||||
for _z in $_zones_space; do
|
||||
_clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
|
||||
_clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//")
|
||||
[ -z "$_clean_z" ] && continue
|
||||
|
||||
case "$_fd" in
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API
|
|||
Site: DirectAdmin.com/api.php
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da
|
||||
Options:
|
||||
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443"
|
||||
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40".
|
||||
DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
|
||||
Issues: github.com/TigerP/acme.sh/issues
|
||||
'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ dns_desec_info='deSEC.io
|
|||
Site: desec.readthedocs.io/en/latest/
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec
|
||||
Options:
|
||||
DDNSS_Token API Token
|
||||
DEDYN_TOKEN API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2180
|
||||
Author: Zheng Qian
|
||||
'
|
||||
|
|
@ -39,6 +39,7 @@ dns_desec_add() {
|
|||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_sub_domain=$(echo "$_sub_domain" | _lower_case)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
|
|
@ -48,7 +49,7 @@ dns_desec_add() {
|
|||
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
|
||||
|
||||
if [ "$_code" = "200" ]; then
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
_debug "existing TXT found"
|
||||
_debug oldtxtvalues "$oldtxtvalues"
|
||||
if [ -n "$oldtxtvalues" ]; then
|
||||
|
|
@ -100,7 +101,7 @@ dns_desec_rm() {
|
|||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_sub_domain=$(echo "$_sub_domain" | _lower_case)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
|
|
@ -110,7 +111,7 @@ dns_desec_rm() {
|
|||
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
|
||||
|
||||
if [ "$_code" = "200" ]; then
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
_debug "existing TXT found"
|
||||
_debug oldtxtvalues "$oldtxtvalues"
|
||||
if [ -n "$oldtxtvalues" ]; then
|
||||
|
|
@ -150,6 +151,8 @@ _desec_rest() {
|
|||
if [ "$m" != "GET" ]; then
|
||||
_secure_debug2 data "$data"
|
||||
response="$(_post "$data" "$ep" "" "$m")"
|
||||
_info "Sleeping 1s to respect deSEC write rate limit"
|
||||
_sleep 1
|
||||
else
|
||||
response="$(_get "$ep")"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ Site: DNSExit.com
|
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit
|
||||
Options:
|
||||
DNSEXIT_API_KEY API Key
|
||||
DNSEXIT_AUTH_USER Username
|
||||
DNSEXIT_AUTH_PASS Password
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/4719
|
||||
Author: Samuel Jimenez
|
||||
'
|
||||
|
||||
DNSEXIT_API_URL="https://api.dnsexit.com/dns/"
|
||||
DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp"
|
||||
|
||||
######## Public functions #####################
|
||||
#Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
|
|
@ -28,20 +25,7 @@ dns_dnsexit_add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_debug 'First detect the root zone'
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
_dnsexit_zone_op add ',"ttl":1,"overwrite":false'
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
|
|
@ -58,54 +42,43 @@ dns_dnsexit_rm() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_debug 'First detect the root zone'
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
_dnsexit_zone_op delete ''
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp)
|
||||
# was shut down by DNSExit and now returns 503, and the JSON API offers no
|
||||
# zone-list call. So find the root zone by attempting the actual operation at
|
||||
# each domain level: the API answers "code":0 only when the domain matches a
|
||||
# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914
|
||||
#Usage: _dnsexit_zone_op <add|delete> <extra-json-fields>
|
||||
_dnsexit_zone_op() {
|
||||
_op="$1"
|
||||
_extra="$2"
|
||||
i=1
|
||||
while true; do
|
||||
_domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$_domain"
|
||||
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
|
||||
_debug _domain "$_domain"
|
||||
if [ -z "$_domain" ]; then
|
||||
_err "Could not find the root zone of $fulldomain in your DNSExit account"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug login "$DNSEXIT_AUTH_USER"
|
||||
_debug password "$DNSEXIT_AUTH_PASS"
|
||||
_debug domain "$_domain"
|
||||
_sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")"
|
||||
if [ "$_sub_domain" = "$fulldomain" ]; then
|
||||
_sub_domain=""
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain"
|
||||
|
||||
if _contains "$response" "0=$_domain"; then
|
||||
_sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
|
||||
return 0
|
||||
else
|
||||
_debug "Go to next level of $_domain"
|
||||
if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then
|
||||
if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
fi
|
||||
_debug "Zone $_domain was not accepted, trying the next level" "$response"
|
||||
fi
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_dnsexit_rest() {
|
||||
|
|
@ -136,27 +109,7 @@ _dnsexit_rest() {
|
|||
return 0
|
||||
}
|
||||
|
||||
_dnsexit_http() {
|
||||
m=GET
|
||||
param="$1"
|
||||
_debug param "$param"
|
||||
_debug get "$DNSEXIT_HOSTS_URL?$param"
|
||||
|
||||
response="$(_get "$DNSEXIT_HOSTS_URL?$param")"
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Error $param"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_account_info() {
|
||||
|
||||
DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}"
|
||||
if test -z "$DNSEXIT_API_KEY"; then
|
||||
DNSEXIT_API_KEY=''
|
||||
|
|
@ -166,23 +119,5 @@ get_account_info() {
|
|||
|
||||
_saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY"
|
||||
|
||||
DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}"
|
||||
if test -z "$DNSEXIT_AUTH_USER"; then
|
||||
DNSEXIT_AUTH_USER=""
|
||||
_err 'DNSEXIT_AUTH_USER was not exported'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER"
|
||||
|
||||
DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}"
|
||||
if test -z "$DNSEXIT_AUTH_PASS"; then
|
||||
DNSEXIT_AUTH_PASS=""
|
||||
_err 'DNSEXIT_AUTH_PASS was not exported'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Site: DNSimple.com
|
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
|
||||
Options:
|
||||
DNSimple_OAUTH_TOKEN OAuth Token
|
||||
DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts.
|
||||
Issues: github.com/pho3nixf1re/acme.sh/issues
|
||||
'
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ dns_dnsimple_add() {
|
|||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
DNSimple_OAUTH_TOKEN=""
|
||||
_err "You have not set the dnsimple oauth token yet."
|
||||
|
|
@ -25,10 +27,10 @@ dns_dnsimple_add() {
|
|||
fi
|
||||
|
||||
# save the oauth token for later
|
||||
_saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
|
||||
_saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
|
||||
|
||||
if ! _get_account_id; then
|
||||
_err "failed to retrive account id"
|
||||
_err "failed to retrieve account id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
@ -56,8 +58,14 @@ dns_dnsimple_add() {
|
|||
dns_dnsimple_rm() {
|
||||
fulldomain=$1
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
_err "You have not set the dnsimple oauth token yet."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _get_account_id; then
|
||||
_err "failed to retrive account id"
|
||||
_err "failed to retrieve account id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
@ -122,13 +130,16 @@ _get_root() {
|
|||
|
||||
# returns _account_id
|
||||
_get_account_id() {
|
||||
_debug "retrive account id"
|
||||
if ! _dnsimple_rest GET "whoami"; then
|
||||
return 1
|
||||
DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf_mutable DNSimple_ACCOUNT_ID)}"
|
||||
if [ "$DNSimple_ACCOUNT_ID" ]; then
|
||||
_saveaccountconf_mutable DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID"
|
||||
_account_id="$DNSimple_ACCOUNT_ID"
|
||||
_debug _account_id "$_account_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"account\":null"; then
|
||||
_err "no account associated with this token"
|
||||
_debug "retrieve account id"
|
||||
if ! _dnsimple_rest GET "whoami"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
@ -137,7 +148,25 @@ _get_account_id() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"account\":null"; then
|
||||
# the whoami of a user token (dnsimple_u_*) carries no account,
|
||||
# so list the accounts the token can access instead
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6491
|
||||
if ! _dnsimple_rest GET "accounts"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1)
|
||||
if [ -z "$_account_id" ]; then
|
||||
_err "no account associated with this token"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then
|
||||
_err "The token has access to multiple accounts, please pick one and set it explicitly:"
|
||||
_err "export DNSimple_ACCOUNT_ID=<one of: $(echo "$_account_id" | tr '\n' ' ')>"
|
||||
return 1
|
||||
fi
|
||||
_debug _account_id "$_account_id"
|
||||
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ dns_dynu_add() {
|
|||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
|
||||
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
|
||||
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
|
||||
Dynu_ClientId=""
|
||||
Dynu_Secret=""
|
||||
|
|
@ -32,8 +34,8 @@ dns_dynu_add() {
|
|||
fi
|
||||
|
||||
#save the client id and secret to the account conf file.
|
||||
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf Dynu_Secret "$Dynu_Secret"
|
||||
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
|
||||
|
||||
if [ -z "$Dynu_Token" ]; then
|
||||
_info "Getting Dynu token."
|
||||
|
|
@ -69,6 +71,8 @@ dns_dynu_rm() {
|
|||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
|
||||
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
|
||||
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
|
||||
Dynu_ClientId=""
|
||||
Dynu_Secret=""
|
||||
|
|
@ -78,8 +82,8 @@ dns_dynu_rm() {
|
|||
fi
|
||||
|
||||
#save the client id and secret to the account conf file.
|
||||
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf Dynu_Secret "$Dynu_Secret"
|
||||
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
|
||||
|
||||
if [ -z "$Dynu_Token" ]; then
|
||||
_info "Getting Dynu token."
|
||||
|
|
@ -214,11 +218,11 @@ _dynu_authentication() {
|
|||
|
||||
response="$(_get "$Dynu_EndPoint/oauth2/token")"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Authentication failed."
|
||||
_err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token"
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "Authentication Exception"; then
|
||||
_err "Authentication failed."
|
||||
_err "Authentication failed. Server response: $response"
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "access_token"; then
|
||||
|
|
|
|||
|
|
@ -363,17 +363,12 @@ _edgedns_rest() {
|
|||
|
||||
_edgedns_eg_timestamp() {
|
||||
_debug "Generating signature Timestamp"
|
||||
_debug3 "Retriving ntp time"
|
||||
_timeheaders="$(_get "https://www.ntp.org" "onlyheader")"
|
||||
_debug3 "_timeheaders" "$_timeheaders"
|
||||
_ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")"
|
||||
_debug3 "_ntpdate" "$_ntpdate"
|
||||
_ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')"
|
||||
_debug3 "_NTPDATE" "$_ntpdate"
|
||||
_ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")"
|
||||
_debug3 "_ntptime" "$_ntptime"
|
||||
_eg_timestamp=$(date -u "+%Y%m%dT")
|
||||
_eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")"
|
||||
#Akamai accepts a clock skew of +/-30s, so use the system clock directly.
|
||||
#The previous code fetched the Date header from www.ntp.org, which is not
|
||||
#a reliable time source (it served a wrong time for hours, issue 3973),
|
||||
#cost an extra https round-trip for every API request, and combined the
|
||||
#remote time of day with the LOCAL date, breaking around UTC midnight.
|
||||
_eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")"
|
||||
_debug "_eg_timestamp" "$_eg_timestamp"
|
||||
}
|
||||
|
||||
|
|
|
|||
267
dnsapi/dns_eurodns.sh
Normal file
267
dnsapi/dns_eurodns.sh
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_eurodns_info='EuroDNS
|
||||
Site: eurodns.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns
|
||||
Options:
|
||||
EURODNS_APP_ID Application ID
|
||||
EURODNS_API_KEY API Key
|
||||
EURODNS_TTL TTL. Default: "600".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues
|
||||
Author: Nicolas Santorelli
|
||||
'
|
||||
|
||||
#
|
||||
# EuroDNS DNS API
|
||||
#
|
||||
# EuroDNS API documentation:
|
||||
# https://docapi.eurodns.com
|
||||
#
|
||||
# Usage:
|
||||
# export EURODNS_APP_ID="your-app-id"
|
||||
# export EURODNS_API_KEY="your-api-key"
|
||||
# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com
|
||||
#
|
||||
# The credentials will be saved in ~/.acme.sh/account.conf
|
||||
#
|
||||
# Optional:
|
||||
# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL
|
||||
# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS)
|
||||
#
|
||||
|
||||
EURODNS_API_DEFAULT="https://rest-api.eurodns.com"
|
||||
EURODNS_TTL_DEFAULT=600
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_eurodns_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
_err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID"
|
||||
_saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY"
|
||||
if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL"
|
||||
fi
|
||||
if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL"
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Adding TXT record"
|
||||
if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Added TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to add TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
dns_eurodns_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Removing TXT record"
|
||||
if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Removed TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to remove TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_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
|
||||
return 1
|
||||
fi
|
||||
|
||||
_eurodns_rest GET "dns-zones/$h"
|
||||
if [ "$?" != "0" ]; then
|
||||
if [ "$_code" = "404" ]; then
|
||||
_debug "Zone $h not found, continuing..."
|
||||
else
|
||||
_err "API error looking up zone $h"
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
continue
|
||||
fi
|
||||
|
||||
if _contains "$response" '"name"'; then
|
||||
if [ "$i" = "1" ]; then
|
||||
_sub_domain="@"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_add_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]'
|
||||
|
||||
_debug "Adding TXT record via API"
|
||||
if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Failed to add TXT record"
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_rm_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
_debug "Getting current zone data for $domain"
|
||||
|
||||
if ! _eurodns_rest GET "dns-zones/$domain"; then
|
||||
_err "Failed to get zone data"
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_data=$(echo "$response" | _normalizeJson)
|
||||
_debug2 zone_data "$zone_data"
|
||||
|
||||
# Find the record ID matching our TXT record
|
||||
record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1)
|
||||
_debug record_id "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_info "TXT record not found or already removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record $record_id"
|
||||
if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then
|
||||
_err "Failed to delete TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _eurodns_rest METHOD ENDPOINT [DATA]
|
||||
_eurodns_rest() {
|
||||
method=$1
|
||||
endpoint=$2
|
||||
data="$3"
|
||||
|
||||
export _H1="X-APP-ID: $EURODNS_APP_ID"
|
||||
export _H2="X-API-KEY: $EURODNS_API_KEY"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
url="$EURODNS_API_URL/$endpoint"
|
||||
|
||||
_debug2 url "$url"
|
||||
_debug2 method "$method"
|
||||
_debug2 data "$data"
|
||||
|
||||
: >"$HTTP_HEADER"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
unset _H1 _H2 _H3
|
||||
_debug2 response "$response"
|
||||
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
_debug2 _code "$_code"
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error calling API: $endpoint"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then
|
||||
if [ "$_code" != "404" ]; then
|
||||
_err "API error (HTTP $_code): $response"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
110
dnsapi/dns_firestorm.sh
Normal file
110
dnsapi/dns_firestorm.sh
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_firestorm_info='Firestorm.ch
|
||||
Site: firestorm.ch
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm
|
||||
Options:
|
||||
FST_Key Customer ID
|
||||
FST_Secret API Secret
|
||||
FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6839
|
||||
Author: FireStorm GmbH
|
||||
'
|
||||
|
||||
FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
_err "Get your API credentials at https://admin.firestorm.ch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
_saveaccountconf_mutable FST_Key "$FST_Key"
|
||||
_saveaccountconf_mutable FST_Secret "$FST_Secret"
|
||||
if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable FST_Url "$FST_Url"
|
||||
else
|
||||
_clearaccountconf_mutable FST_Url
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
_debug "Subdomain" "$subdomain"
|
||||
_debug "TXT value" "$txtvalue"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "update" "$body")"
|
||||
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "TXT record added successfully"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to add TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Removing TXT record for $fulldomain"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "remove" "$body")"
|
||||
|
||||
if _contains "$response" "removed"; then
|
||||
_info "TXT record removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to remove TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# Escape special characters for safe JSON string interpolation
|
||||
_json_safe() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
_firestorm_api() {
|
||||
action=$1
|
||||
data=$2
|
||||
|
||||
export _H1="X-Api-User: $FST_Key"
|
||||
export _H2="X-Api-Key: $FST_Secret"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
_post "$data" "$FST_Url/$action" "" "POST"
|
||||
}
|
||||
|
|
@ -305,7 +305,7 @@ _freedns_domain_id() {
|
|||
fi
|
||||
|
||||
domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' |
|
||||
grep "<td>$search_domain</td>\|<td>$search_domain(.*)</td>" |
|
||||
grep -E "<td>$search_domain</td>|<td>$search_domain\(.*\)</td>" |
|
||||
sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' |
|
||||
cut -d = -f 2)"
|
||||
# The above beauty extracts domain ID from the html page...
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,12 @@ dns_gd_add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$response" "$txtvalue"; then
|
||||
if _contains "$response" "UNKNOWN_DOMAIN"; then
|
||||
# GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even
|
||||
# though the PUT above succeeded; skip the local readback check and let
|
||||
# acme.sh's own DNS propagation check verify the record was published.
|
||||
_info "GoDaddy API won't allow reading the record back; skipping local verification."
|
||||
elif ! _contains "$response" "$txtvalue"; then
|
||||
_err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -145,8 +150,8 @@ dns_gd_rm() {
|
|||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=2
|
||||
p=1
|
||||
i=1
|
||||
p=0
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
|
|
@ -154,17 +159,41 @@ _get_root() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
if ! _gd_rest GET "domains/$h"; then
|
||||
return 1
|
||||
# The record name is whatever precedes the candidate zone. Do not assume
|
||||
# _acme-challenge here: with DNS alias mode it can be any name, and the
|
||||
# record may even sit at the zone apex (name "@").
|
||||
if [ "$p" = "0" ]; then
|
||||
_probe_sub="@"
|
||||
else
|
||||
_probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
|
||||
if _contains "$response" '"code":"NOT_FOUND"'; then
|
||||
_debug "$h not found"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
# Probe with the records endpoint instead of "GET domains/$h": since
|
||||
# 2024-05 GoDaddy rejects the domain details call for accounts with
|
||||
# fewer than 10 domains, while record-level calls keep working.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/4487
|
||||
if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then
|
||||
return 1
|
||||
fi
|
||||
if _startswith "$response" '\['; then
|
||||
_sub_domain="$_probe_sub"
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone
|
||||
# even though writes succeed (see issue #6517); fall back to the domain
|
||||
# details call for them.
|
||||
if ! _gd_rest GET "domains/$h"; then
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" '"domainId"'; then
|
||||
_sub_domain="$_probe_sub"
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "$h not found"
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
|
|
|||
263
dnsapi/dns_glesys.sh
Normal file
263
dnsapi/dns_glesys.sh
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_glesys_info='Glesys
|
||||
Site: Glesys.se
|
||||
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys
|
||||
Options:
|
||||
GLESYS_API_KEY Generated API key.
|
||||
GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345).
|
||||
GLESYS_API API endpoint. Default "https://api.glesys.com/domain".
|
||||
GLESYS_TTL TXT record TTL. Default 120.
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/7057
|
||||
Author: Toni Karppi
|
||||
'
|
||||
|
||||
GLESYS_API_DEFAULT="https://api.glesys.com/domain"
|
||||
GLESYS_TTL_DEFAULT="120"
|
||||
|
||||
######## Public functions #####################################################
|
||||
|
||||
# Usage:
|
||||
# dns_glesys_add _acme-challenge.www.example.com "txt-value"
|
||||
dns_glesys_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
_glesys_init || return 1
|
||||
|
||||
if ! _glesys_get_root "$fulldomain"; then
|
||||
_err "Could not find root zone for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
host_value="${_sub_domain:-@}"
|
||||
_debug _host_value "$host_value"
|
||||
|
||||
data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}"
|
||||
|
||||
_debug2 data "$data"
|
||||
|
||||
if ! _glesys_rest POST "/addrecord" "$data"; then
|
||||
_err "Failed to send HTTP request to add TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
_err "GleSYS API responded with an unexpected status when attempting to add TXT record"
|
||||
_debug2 "API response" "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record added"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage:
|
||||
# dns_glesys_rm _acme-challenge.www.example.com "txt-value"
|
||||
dns_glesys_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
_glesys_init || return 1
|
||||
|
||||
if ! _glesys_get_root "$fulldomain"; then
|
||||
_err "Could not find root zone for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _glesys_find_record_id "$txtvalue"; then
|
||||
_info "TXT record not present, skip removal"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then
|
||||
_err "Failed to send HTTP request to remove TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
_err "GleSYS API responded with unexpected status when attempting to remove TXT record"
|
||||
_debug2 "API response" "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record removed"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ####################################################
|
||||
|
||||
_glesys_find_record_id() {
|
||||
txtvalue="$1"
|
||||
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if [ -z "$txtvalue" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_record_id=""
|
||||
|
||||
_debug "Looking for TXT record with value" "$txtvalue"
|
||||
|
||||
if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then
|
||||
_err "Failed to list DNS records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
records="$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
sed 's/},{/}\
|
||||
{/g'
|
||||
)"
|
||||
|
||||
_debug2 records "$records"
|
||||
|
||||
expected_data="\"data\":\"$txtvalue\""
|
||||
|
||||
_record_id="$(
|
||||
printf "%s\n" "$records" |
|
||||
while IFS= read -r record; do
|
||||
printf "%s" "$record" | grep -q '"type":"TXT"' || continue
|
||||
printf "%s" "$record" | grep -Fq "$expected_data" || continue
|
||||
|
||||
printf "%s" "$record" |
|
||||
grep -E -o '"recordid":"?[0-9]+' |
|
||||
grep -E -o '[0-9]+$'
|
||||
|
||||
break
|
||||
done
|
||||
)"
|
||||
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Finds:
|
||||
# _domain example.com
|
||||
# _sub_domain _acme-challenge.www
|
||||
_glesys_get_root() {
|
||||
domain="$1"
|
||||
i=1
|
||||
|
||||
while true; do
|
||||
h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)"
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _glesys_rest GET "/listrecords?domainname=$h"; then
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" = "200" ]; then
|
||||
cut_len="$((${#domain} - ${#h} - 1))"
|
||||
_domain="$h"
|
||||
_sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
i="$((i + 1))"
|
||||
done
|
||||
}
|
||||
|
||||
_glesys_init() {
|
||||
[ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT"
|
||||
[ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT"
|
||||
|
||||
_debug GLESYS_API "$GLESYS_API"
|
||||
_debug GLESYS_TTL "$GLESYS_TTL"
|
||||
|
||||
GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}"
|
||||
GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}"
|
||||
|
||||
if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then
|
||||
_err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_secure_debug GLESYS_API_KEY "$GLESYS_API_KEY"
|
||||
_secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
|
||||
|
||||
_glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)"
|
||||
_secure_debug2 _glesys_basic_auth "$_glesys_basic_auth"
|
||||
|
||||
_saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY"
|
||||
_saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_glesys_rest() {
|
||||
method="$1"
|
||||
path="$2"
|
||||
data="$3"
|
||||
|
||||
export _H1="Authorization: Basic $_glesys_basic_auth"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
|
||||
url="$GLESYS_API$path"
|
||||
_debug "$method $url"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
ret="$?"
|
||||
_debug2 response "$response"
|
||||
_debug ret "$ret"
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
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,256 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_hetzner_info='Hetzner.com
|
||||
Site: Hetzner.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
|
||||
Options:
|
||||
HETZNER_Token API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2943
|
||||
'
|
||||
|
||||
HETZNER_Api="https://dns.hetzner.com/api/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
# Ref: https://dns.hetzner.com/api-docs/
|
||||
dns_hetzner_add() {
|
||||
full_domain=$1
|
||||
txt_value=$2
|
||||
|
||||
HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
|
||||
|
||||
if [ -z "$HETZNER_Token" ]; then
|
||||
HETZNER_Token=""
|
||||
_err "You didn't specify a Hetzner api token."
|
||||
_err "You can get yours from here https://dns.hetzner.com/settings/api-token."
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save the api key and email to the account conf file.
|
||||
_saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
|
||||
if ! _get_root "$full_domain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting TXT records"
|
||||
if ! _find_record "$_sub_domain" "$txt_value"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "Adding record"
|
||||
if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
|
||||
if _contains "$response" "$txt_value"; then
|
||||
_info "Record added, OK"
|
||||
_sleep 2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error${_response_error}"
|
||||
return 1
|
||||
else
|
||||
_info "Found record id: $_record_id."
|
||||
_info "Record found, do nothing."
|
||||
return 0
|
||||
# we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
|
||||
#if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
|
||||
# if _contains "$response" "$txt_value"; then
|
||||
# _info "Modified, OK"
|
||||
# return 0
|
||||
# fi
|
||||
#fi
|
||||
#_err "Add txt record error (modify)."
|
||||
#return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage: full_domain txt_value
|
||||
# Used to remove the txt record after validation
|
||||
dns_hetzner_rm() {
|
||||
full_domain=$1
|
||||
txt_value=$2
|
||||
|
||||
HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$full_domain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting TXT records"
|
||||
if ! _find_record "$_sub_domain" "$txt_value"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "Remove not needed. Record not found."
|
||||
else
|
||||
if ! _hetzner_rest DELETE "records/$_record_id"; then
|
||||
_err "Delete record error${_response_error}"
|
||||
return 1
|
||||
fi
|
||||
_sleep 2
|
||||
_info "Record deleted"
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#returns
|
||||
# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error
|
||||
_find_record() {
|
||||
unset _record_id
|
||||
_record_name=$1
|
||||
_record_value=$2
|
||||
|
||||
if [ -z "$_record_value" ]; then
|
||||
_record_value='[^"]*'
|
||||
fi
|
||||
|
||||
_debug "Getting all records"
|
||||
_hetzner_rest GET "records?zone_id=${_domain_id}"
|
||||
|
||||
if _response_has_error; then
|
||||
_err "Error${_response_error}"
|
||||
return 1
|
||||
else
|
||||
_record_id=$(
|
||||
echo "$response" |
|
||||
grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" |
|
||||
grep "\"value\":\"$_record_value\"" |
|
||||
while read -r record; do
|
||||
# test for type and
|
||||
if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
|
||||
echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
|
||||
break
|
||||
fi
|
||||
done
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
# _domain_id=sdjkglgdfewsdfg
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
|
||||
domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
|
||||
|
||||
_debug "Reading zone_id for '$domain_without_acme' from config..."
|
||||
HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
|
||||
if [ "$HETZNER_Zone_ID" ]; then
|
||||
_debug "Found, using: $HETZNER_Zone_ID"
|
||||
if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
|
||||
_debug "Zone with id '$HETZNER_Zone_ID' does not exist."
|
||||
_cleardomainconf "$domain_param_name"
|
||||
unset HETZNER_Zone_ID
|
||||
else
|
||||
if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
|
||||
_domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
|
||||
if [ "$_domain" ]; then
|
||||
_cut_length=$((${#domain} - ${#_domain} - 1))
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
|
||||
_domain_id="$HETZNER_Zone_ID"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
_debug "Trying to get zone id by domain name for '$domain_without_acme'."
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
_debug h "$h"
|
||||
|
||||
_hetzner_rest GET "zones?name=$h"
|
||||
|
||||
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
|
||||
_domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _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
|
||||
HETZNER_Zone_ID=$_domain_id
|
||||
_savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
#returns
|
||||
# _response_error
|
||||
_response_has_error() {
|
||||
unset _response_error
|
||||
|
||||
err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
|
||||
|
||||
if [ -n "$err_part" ]; then
|
||||
err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
|
||||
err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
|
||||
|
||||
if [ -n "$err_code" ] && [ -n "$err_message" ]; then
|
||||
_response_error=" - message: ${err_message}, code: ${err_code}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
#returns
|
||||
# response
|
||||
_hetzner_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
|
||||
|
||||
export _H1="Content-TType: application/json"
|
||||
export _H2="Auth-API-Token: $key_trimmed"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$HETZNER_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ] || _response_has_error; then
|
||||
_debug "Error$_response_error"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -40,6 +40,11 @@ _hostingde_apiKey() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# The endpoint is the base URL only; the api path is appended below.
|
||||
# hosting.de's own docs show the full api URL, so strip it if pasted in.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6896
|
||||
HOSTINGDE_ENDPOINT="$(echo "$HOSTINGDE_ENDPOINT" | sed 's|/api/dns/v1/json||; s|/*$||')"
|
||||
|
||||
_saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY"
|
||||
_saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT"
|
||||
}
|
||||
|
|
|
|||
196
dnsapi/dns_hostinger.sh
Executable file
196
dnsapi/dns_hostinger.sh
Executable file
|
|
@ -0,0 +1,196 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_hostinger_info='Hostinger
|
||||
Site: Hostinger.com
|
||||
Domains: hostinger.nl
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hostinger
|
||||
Options:
|
||||
HOSTINGER_Token API Key
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/6831
|
||||
Author: Sasha Reid <github@sasha.hackl.es>
|
||||
'
|
||||
|
||||
HOSTINGER_Api="https://developers.hostinger.com/api/dns/v1/zones"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_hostinger_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}"
|
||||
|
||||
if [ -z "$HOSTINGER_Token" ]; then
|
||||
HOSTINGER_Token=""
|
||||
_err "You didn't specify a Hostinger API Key yet."
|
||||
_err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting existing records"
|
||||
_hostinger_rest GET "${_domain}"
|
||||
|
||||
if [ -z "$response" ]; then
|
||||
_err "Error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
|
||||
# we can not use updating anymore.
|
||||
# count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
|
||||
# _debug count "$count"
|
||||
# if [ "$count" = "0" ]; then
|
||||
_info "Adding record"
|
||||
if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [{\"content\":\"$txtvalue\"}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":false}"; then
|
||||
if _contains "$response" "Request accepted"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" ||
|
||||
_contains "$response" 'DNS:4008'; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_hostinger_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}"
|
||||
|
||||
if [ -z "$HOSTINGER_Token" ]; then
|
||||
HOSTINGER_Token=""
|
||||
_err "You didn't specify a Hostinger API Key yet."
|
||||
_err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting existing records"
|
||||
_hostinger_rest GET "${_domain}"
|
||||
|
||||
if [ -z "$response" ]; then
|
||||
_err "Error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"name\":\"$_sub_domain\""; then
|
||||
# Match the record, and make certain it is a TXT record for the domain not another type. Then remove our target record from the list
|
||||
remaining_records=$(echo "$response" | _normalizeJson | _egrep_o '{"name":"'"$_sub_domain"'","records":\[[^]]+\],"ttl":[0-9]+,"type":"TXT"\}' | _egrep_o "\[.*\]" | sed -E 's#\{"content":"\\"'"$txtvalue"'\\"","is_disabled":false\},?##g')
|
||||
if [ "$remaining_records" != "[]" ]; then
|
||||
remaining_json=$(echo "$remaining_records" | _egrep_o '"content":"\\"[^}]+\\""' | sed -E 's/^(.*)$/{\1},/g' | tr -d '\n' | sed 's/,$//')
|
||||
# We need to set the remaining records back to Hostinger, as we can't partially delete
|
||||
_info "Removing $txtvalue from $_sub_domain by setting records to ${remaining_json}"
|
||||
if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [${remaining_json}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":true}"; then
|
||||
if _contains "$response" "Request accepted"; then
|
||||
_info "Updated remaining records, OK"
|
||||
return 0
|
||||
elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" ||
|
||||
_contains "$response" 'DNS:4008'; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
# Otherwise delete the TXT record that matches the subdomain
|
||||
else
|
||||
if ! _hostinger_rest DELETE "$_domain" "{\"filters\":[{\"name\":\"$_sub_domain\",\"type\":\"TXT\"}]}"; then
|
||||
_err "Delete record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
echo "$response" | grep "Request accepted" >/dev/null
|
||||
else
|
||||
_info "Don't need to remove."
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_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
|
||||
|
||||
_hostinger_rest GET "$h"
|
||||
if _contains "$response" "records"; then
|
||||
if [ "$response" = "[]" ]; then
|
||||
_debug "Valid subdomains are not the root"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostinger_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
token_trimmed=$(echo "$HOSTINGER_Token" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="Authorization: Bearer $token_trimmed"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$HOSTINGER_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$HOSTINGER_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -6,13 +6,13 @@ Site: hostup.se
|
|||
Docs: https://developer.hostup.se/
|
||||
Options:
|
||||
HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes.
|
||||
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
|
||||
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api/v2).
|
||||
HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
|
||||
HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection).
|
||||
HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection.
|
||||
Author: HostUp (https://cloud.hostup.se/contact/en)
|
||||
'
|
||||
|
||||
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api"
|
||||
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2"
|
||||
HOSTUP_DEFAULT_TTL=60
|
||||
|
||||
# Public: add TXT record
|
||||
|
|
@ -20,6 +20,7 @@ HOSTUP_DEFAULT_TTL=60
|
|||
dns_hostup_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
hostup_add_txtvalue="$2"
|
||||
|
||||
_info "Using HostUp DNS API"
|
||||
|
||||
|
|
@ -34,31 +35,34 @@ dns_hostup_add() {
|
|||
|
||||
record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")"
|
||||
record_name="$(_hostup_sanitize_name "$record_name")"
|
||||
record_value="$(_hostup_json_escape "$txtvalue")"
|
||||
hostup_add_record_value="$(_hostup_json_escape "$hostup_add_txtvalue")"
|
||||
|
||||
ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
|
||||
raw_ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
|
||||
ttl="$(_hostup_normalize_ttl "$raw_ttl")"
|
||||
if [ -z "$ttl" ]; then
|
||||
_err "HOSTUP_TTL must be a whole number between 60 and 86400 seconds."
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$HOSTUP_TTL" ]; then
|
||||
HOSTUP_TTL="$ttl"
|
||||
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
|
||||
fi
|
||||
|
||||
_debug "zone_id" "$HOSTUP_ZONE_ID"
|
||||
_debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
|
||||
_debug "record_name" "$record_name"
|
||||
_debug "ttl" "$ttl"
|
||||
|
||||
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}"
|
||||
|
||||
if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
|
||||
return 1
|
||||
record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
|
||||
if _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$hostup_add_txtvalue"; then
|
||||
_info "TXT record already exists for $fulldomain"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! _contains "$_hostup_response" '"success":true'; then
|
||||
_err "HostUp DNS API: failed to create TXT record for $fulldomain"
|
||||
_debug2 "_hostup_response" "$_hostup_response"
|
||||
return 1
|
||||
fi
|
||||
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$hostup_add_record_value\",\"ttl\":$ttl}"
|
||||
|
||||
record_id="$(_hostup_extract_record_id "$_hostup_response")"
|
||||
if [ -n "$record_id" ]; then
|
||||
_hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id"
|
||||
_debug "hostup_saved_record_id" "$record_id"
|
||||
if ! _hostup_rest "POST" "/dns-zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Added TXT record for $fulldomain"
|
||||
|
|
@ -85,20 +89,9 @@ dns_hostup_rm() {
|
|||
record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
|
||||
record_value="$txtvalue"
|
||||
|
||||
record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")"
|
||||
if [ -n "$record_id_cached" ]; then
|
||||
_debug "hostup_record_id_cached" "$record_id_cached"
|
||||
if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then
|
||||
_info "Deleted TXT record $record_id_cached"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
|
||||
HOSTUP_ZONE_ID=""
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then
|
||||
_info "TXT record not found for $record_name_fqdn. Skipping removal."
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value"
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -109,7 +102,7 @@ dns_hostup_rm() {
|
|||
fi
|
||||
|
||||
_info "Deleted TXT record $HOSTUP_RECORD_ID"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value"
|
||||
HOSTUP_ZONE_ID=""
|
||||
return 0
|
||||
}
|
||||
|
|
@ -127,21 +120,18 @@ _hostup_init() {
|
|||
if [ -z "$HOSTUP_API_BASE" ]; then
|
||||
HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT"
|
||||
fi
|
||||
HOSTUP_API_BASE="$(_hostup_normalize_api_base "$HOSTUP_API_BASE")"
|
||||
|
||||
if [ -z "$HOSTUP_API_KEY" ]; then
|
||||
HOSTUP_API_KEY=""
|
||||
_err "HOSTUP_API_KEY is not set."
|
||||
_err "Please export your HostUp API key with read:dns and write:dns scopes."
|
||||
_err "Please export your HostUp API key with read:dns, write:dns, and read:domains scopes."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY"
|
||||
_saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE"
|
||||
|
||||
if [ -n "$HOSTUP_TTL" ]; then
|
||||
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
|
||||
fi
|
||||
|
||||
if [ -n "$HOSTUP_ZONE_ID" ]; then
|
||||
_saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID"
|
||||
fi
|
||||
|
|
@ -149,11 +139,80 @@ _hostup_init() {
|
|||
return 0
|
||||
}
|
||||
|
||||
_hostup_normalize_api_base() {
|
||||
api_base="${1%/}"
|
||||
|
||||
case "$api_base" in
|
||||
*/api/v2)
|
||||
printf "%s" "$api_base"
|
||||
;;
|
||||
*/api)
|
||||
printf "%s/v2" "$api_base"
|
||||
;;
|
||||
*)
|
||||
printf "%s" "$api_base"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_hostup_normalize_ttl() {
|
||||
ttl_value="$1"
|
||||
|
||||
case "$ttl_value" in
|
||||
"" | *[!0-9]*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
while [ "${ttl_value#0}" != "$ttl_value" ]; do
|
||||
ttl_value="${ttl_value#0}"
|
||||
done
|
||||
[ -z "$ttl_value" ] && ttl_value=0
|
||||
|
||||
case "$ttl_value" in
|
||||
??????*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ttl_value" -lt 60 ] || [ "$ttl_value" -gt 86400 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "%s" "$ttl_value"
|
||||
}
|
||||
|
||||
_hostup_domain_in_zone() {
|
||||
host="$(printf "%s" "${1%.}" | _lower_case)"
|
||||
zone="$(printf "%s" "${2%.}" | _lower_case)"
|
||||
|
||||
if [ -z "$host" ] || [ -z "$zone" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$host" = "$zone" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$host" in
|
||||
*."$zone")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_detect_zone() {
|
||||
fulldomain="$1"
|
||||
|
||||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then
|
||||
return 0
|
||||
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_cached_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
|
||||
HOSTUP_ZONE_ID=""
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
fi
|
||||
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
|
|
@ -162,16 +221,16 @@ _hostup_detect_zone() {
|
|||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then
|
||||
# Attempt to fetch domain name for provided zone ID
|
||||
if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then
|
||||
return 0
|
||||
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_forced_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
|
||||
fi
|
||||
HOSTUP_ZONE_ID=""
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
fi
|
||||
|
||||
if ! _hostup_load_zones; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)"
|
||||
_domain_candidate="$(printf "%s" "${fulldomain%.}" | _lower_case)"
|
||||
_debug "hostup_initial_candidate" "$_domain_candidate"
|
||||
|
||||
while [ -n "$_domain_candidate" ]; do
|
||||
|
|
@ -240,11 +299,11 @@ _hostup_fqdn() {
|
|||
_hostup_fetch_zone_details() {
|
||||
zone_id="$1"
|
||||
|
||||
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
|
||||
if ! _hostup_rest "GET" "/dns-zones/$zone_id/records" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')"
|
||||
zonedomain="$(_hostup_json_extract "name" "$_hostup_response")"
|
||||
if [ -n "$zonedomain" ]; then
|
||||
HOSTUP_ZONE_DOMAIN="$zonedomain"
|
||||
return 0
|
||||
|
|
@ -254,7 +313,7 @@ _hostup_fetch_zone_details() {
|
|||
}
|
||||
|
||||
_hostup_load_zones() {
|
||||
if ! _hostup_rest "GET" "/dns/zones" ""; then
|
||||
if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
|
@ -263,9 +322,9 @@ _hostup_load_zones() {
|
|||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*'"domain_id"'*'"domain"'*)
|
||||
zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")"
|
||||
zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")"
|
||||
*'"id"'*'"name"'*)
|
||||
zone_id="$(_hostup_json_extract "id" "$line")"
|
||||
zone_domain="$(_hostup_json_extract "name" "$line")"
|
||||
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
|
||||
HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id}
|
||||
"
|
||||
|
|
@ -290,9 +349,30 @@ _hostup_lookup_zone() {
|
|||
_lookup_zone_id=""
|
||||
_lookup_zone_domain=""
|
||||
|
||||
encoded_domain="$(printf "%s" "$lookup_domain" | _url_encode)"
|
||||
if _hostup_rest "GET" "/dns-zones?name=$encoded_domain&limit=1" ""; then
|
||||
zone_id="$(_hostup_json_extract "id" "$_hostup_response")"
|
||||
zone_domain="$(_hostup_json_extract "name" "$_hostup_response")"
|
||||
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
|
||||
zone_domain_lower="$(printf "%s" "$zone_domain" | _lower_case)"
|
||||
if [ "$zone_domain_lower" = "$lookup_domain" ]; then
|
||||
_lookup_zone_domain="$zone_domain"
|
||||
_lookup_zone_id="$zone_id"
|
||||
HOSTUP_ZONE_DOMAIN="$zone_domain"
|
||||
HOSTUP_ZONE_ID="$zone_id"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$HOSTUP_ZONES_CACHE" ] && ! _hostup_load_zones; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS='|' read -r domain zone_id; do
|
||||
[ -z "$domain" ] && continue
|
||||
if [ "$domain" = "$lookup_domain" ]; then
|
||||
domain_lower="$(printf "%s" "$domain" | _lower_case)"
|
||||
if [ "$domain_lower" = "$lookup_domain" ]; then
|
||||
_lookup_zone_domain="$domain"
|
||||
_lookup_zone_id="$zone_id"
|
||||
HOSTUP_ZONE_DOMAIN="$domain"
|
||||
|
|
@ -307,50 +387,50 @@ EOF
|
|||
}
|
||||
|
||||
_hostup_find_record() {
|
||||
zone_id="$1"
|
||||
fqdn="$2"
|
||||
txtvalue="$3"
|
||||
_hostup_find_zone_id="$1"
|
||||
_hostup_find_fqdn="$2"
|
||||
_hostup_find_txtvalue="$3"
|
||||
|
||||
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
|
||||
_hostup_find_encoded_name="$(printf "%s" "$_hostup_find_fqdn" | _url_encode)"
|
||||
if ! _hostup_rest "GET" "/dns-zones/$_hostup_find_zone_id/records?type=TXT&name=$_hostup_find_encoded_name" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
HOSTUP_RECORD_ID=""
|
||||
records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
|
||||
_hostup_find_records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
|
||||
|
||||
while IFS= read -r line; do
|
||||
while IFS= read -r _hostup_find_line; do
|
||||
# Normalize line to make TXT value matching reliable
|
||||
line_clean="$(printf "%s" "$line" | tr -d '\r\n')"
|
||||
line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')"
|
||||
_hostup_find_line_clean="$(printf "%s" "$_hostup_find_line" | tr -d '\r\n')"
|
||||
_hostup_find_line_value_clean="$(printf "%s" "$_hostup_find_line_clean" | sed 's/\\"//g')"
|
||||
|
||||
case "$line_clean" in
|
||||
*'"type":"TXT"'*'"name"'*'"value"'*)
|
||||
name_value="$(_hostup_json_extract "name" "$line_clean")"
|
||||
record_value="$(_hostup_json_extract "value" "$line_value_clean")"
|
||||
_hostup_find_record_type="$(_hostup_json_extract "type" "$_hostup_find_line_clean")"
|
||||
[ "$_hostup_find_record_type" != "TXT" ] && continue
|
||||
|
||||
_debug "hostup_record_raw" "$record_value"
|
||||
if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then
|
||||
record_value="${record_value#\"}"
|
||||
record_value="${record_value%\"}"
|
||||
fi
|
||||
if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then
|
||||
record_value="${record_value#\'}"
|
||||
record_value="${record_value%\'}"
|
||||
fi
|
||||
record_value="$(printf "%s" "$record_value" | tr -d '\r\n')"
|
||||
_debug "hostup_record_value" "$record_value"
|
||||
_hostup_find_name_value="$(_hostup_json_extract "name" "$_hostup_find_line_clean")"
|
||||
_hostup_find_record_value="$(_hostup_json_extract "value" "$_hostup_find_line_value_clean")"
|
||||
|
||||
if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then
|
||||
record_id="$(_hostup_json_extract "id" "$line_clean")"
|
||||
if [ -n "$record_id" ]; then
|
||||
HOSTUP_RECORD_ID="$record_id"
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_record_raw" "$_hostup_find_record_value"
|
||||
if [ "${_hostup_find_record_value#\"}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\"}" != "$_hostup_find_record_value" ]; then
|
||||
_hostup_find_record_value="${_hostup_find_record_value#\"}"
|
||||
_hostup_find_record_value="${_hostup_find_record_value%\"}"
|
||||
fi
|
||||
if [ "${_hostup_find_record_value#\'}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\'}" != "$_hostup_find_record_value" ]; then
|
||||
_hostup_find_record_value="${_hostup_find_record_value#\'}"
|
||||
_hostup_find_record_value="${_hostup_find_record_value%\'}"
|
||||
fi
|
||||
_hostup_find_record_value="$(printf "%s" "$_hostup_find_record_value" | tr -d '\r\n')"
|
||||
_debug "hostup_record_value" "$_hostup_find_record_value"
|
||||
|
||||
if [ "$_hostup_find_name_value" = "$_hostup_find_fqdn" ] && [ "$_hostup_find_record_value" = "$_hostup_find_txtvalue" ]; then
|
||||
_hostup_find_record_id="$(_hostup_json_extract "id" "$_hostup_find_line_clean")"
|
||||
if [ -n "$_hostup_find_record_id" ]; then
|
||||
HOSTUP_RECORD_ID="$_hostup_find_record_id"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done <<EOF
|
||||
$records
|
||||
$_hostup_find_records
|
||||
EOF
|
||||
|
||||
return 1
|
||||
|
|
@ -361,22 +441,22 @@ _hostup_json_extract() {
|
|||
input="${2:-$line}"
|
||||
|
||||
# First try to extract quoted values (strings)
|
||||
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)"
|
||||
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*\"[^\"]*\"" | _head_n 1)"
|
||||
if [ -n "$quoted_match" ]; then
|
||||
printf "%s" "$quoted_match" |
|
||||
cut -d : -f2- |
|
||||
sed 's/^"//' |
|
||||
sed 's/"$//' |
|
||||
sed 's/^[ ]*"//' |
|
||||
sed 's/"[ ]*$//' |
|
||||
sed 's/\\"/"/g'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback for unquoted values (e.g., numeric IDs)
|
||||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)"
|
||||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[ ]*:[ ]*[^,}]*" | _head_n 1)"
|
||||
if [ -n "$unquoted_match" ]; then
|
||||
printf "%s" "$unquoted_match" |
|
||||
cut -d : -f2- |
|
||||
tr -d '", ' |
|
||||
tr -d '", ' |
|
||||
tr -d '\r\n'
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -391,58 +471,56 @@ _hostup_json_escape() {
|
|||
_hostup_record_key() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
txtvalue="$3"
|
||||
safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')"
|
||||
safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')"
|
||||
if [ -n "$txtvalue" ]; then
|
||||
safe_value="$(printf "%s" "$txtvalue" | sed 's/[^A-Za-z0-9]/_/g')"
|
||||
printf "%s_%s_%s" "$safe_zone" "$safe_domain" "$safe_value"
|
||||
return 0
|
||||
fi
|
||||
printf "%s_%s" "$safe_zone" "$safe_domain"
|
||||
}
|
||||
|
||||
_hostup_save_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
record_id="$3"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
_saveaccountconf_mutable "HOSTUP_RECORD_$key" "$record_id"
|
||||
}
|
||||
|
||||
_hostup_get_saved_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
_readaccountconf_mutable "HOSTUP_RECORD_$key"
|
||||
}
|
||||
|
||||
_hostup_clear_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
txtvalue="$3"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain" "$txtvalue")"
|
||||
_clearaccountconf_mutable "HOSTUP_RECORD_$key"
|
||||
}
|
||||
|
||||
_hostup_extract_record_id() {
|
||||
record_id="$(_hostup_json_extract "id" "$1")"
|
||||
if [ -n "$record_id" ]; then
|
||||
printf "%s" "$record_id"
|
||||
return 0
|
||||
legacy_key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
if [ "$legacy_key" != "$key" ]; then
|
||||
_clearaccountconf_mutable "HOSTUP_RECORD_$legacy_key"
|
||||
fi
|
||||
|
||||
printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2
|
||||
}
|
||||
|
||||
_hostup_delete_record_by_id() {
|
||||
zone_id="$1"
|
||||
record_id="$2"
|
||||
|
||||
if ! _hostup_rest "DELETE" "/dns/zones/$zone_id/records/$record_id" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$_hostup_response" '"success":true'; then
|
||||
if ! _hostup_rest "DELETE" "/dns-zones/$zone_id/records/$record_id" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_hostup_problem_error() {
|
||||
problem_code="$(_hostup_json_extract "code" "$_hostup_response")"
|
||||
problem_detail="$(_hostup_json_extract "detail" "$_hostup_response")"
|
||||
|
||||
if [ -n "$problem_detail" ]; then
|
||||
if [ -n "$problem_code" ]; then
|
||||
_err "HostUp API error ($problem_code): $problem_detail"
|
||||
else
|
||||
_err "HostUp API error: $problem_detail"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_rest() {
|
||||
method="$1"
|
||||
route="$2"
|
||||
|
|
@ -451,8 +529,7 @@ _hostup_rest() {
|
|||
_hostup_response=""
|
||||
|
||||
export _H1="Authorization: Bearer $HOSTUP_API_KEY"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
export _H2="Accept: application/json"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
_hostup_response="$(_get "$HOSTUP_API_BASE$route")"
|
||||
|
|
@ -464,7 +541,6 @@ _hostup_rest() {
|
|||
|
||||
unset _H1
|
||||
unset _H2
|
||||
unset _H3
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
_err "HTTP request failed for $route"
|
||||
|
|
@ -478,23 +554,23 @@ _hostup_rest() {
|
|||
case "$http_status" in
|
||||
200 | 201 | 204) return 0 ;;
|
||||
401)
|
||||
_err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
|
||||
_hostup_problem_error || _err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
|
||||
return 1
|
||||
;;
|
||||
403)
|
||||
_err "HostUp API returned 403 Forbidden. The API key lacks required DNS scopes."
|
||||
_hostup_problem_error || _err "HostUp API returned 403 Forbidden. The API key lacks required DNS/domain scopes."
|
||||
return 1
|
||||
;;
|
||||
404)
|
||||
_err "HostUp API returned 404 Not Found for $route"
|
||||
_hostup_problem_error || _err "HostUp API returned 404 Not Found for $route"
|
||||
return 1
|
||||
;;
|
||||
429)
|
||||
_err "HostUp API rate limit exceeded. Please retry later."
|
||||
_hostup_problem_error || _err "HostUp API rate limit exceeded. Please retry later."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
_err "HostUp API request failed with status $http_status"
|
||||
_hostup_problem_error || _err "HostUp API request failed with status $http_status"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ Options:
|
|||
HUAWEICLOUD_Username Username
|
||||
HUAWEICLOUD_Password Password
|
||||
HUAWEICLOUD_DomainName DomainName
|
||||
HUAWEICLOUD_Region Region. E.g. "cn-north-4". Optional, defaults to "ap-southeast-1".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/3265
|
||||
'
|
||||
|
||||
iam_api="https://iam.myhuaweicloud.com"
|
||||
dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ dns_huaweicloud_add() {
|
|||
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
|
||||
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
|
||||
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
|
||||
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
|
||||
|
||||
# Check information
|
||||
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
|
||||
|
|
@ -37,8 +38,11 @@ dns_huaweicloud_add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
|
||||
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
|
||||
|
||||
unset token # Clear token
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
|
||||
if [ -z "${token}" ]; then # Check token
|
||||
_err "dns_api(dns_huaweicloud): Error getting token."
|
||||
return 1
|
||||
|
|
@ -65,6 +69,9 @@ dns_huaweicloud_add() {
|
|||
_saveaccountconf_mutable HUAWEICLOUD_Username "${HUAWEICLOUD_Username}"
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Password "${HUAWEICLOUD_Password}"
|
||||
_saveaccountconf_mutable HUAWEICLOUD_DomainName "${HUAWEICLOUD_DomainName}"
|
||||
if [ -n "${HUAWEICLOUD_Region}" ]; then
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Region "${HUAWEICLOUD_Region}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +88,7 @@ dns_huaweicloud_rm() {
|
|||
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
|
||||
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
|
||||
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
|
||||
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
|
||||
|
||||
# Check information
|
||||
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
|
||||
|
|
@ -88,8 +96,11 @@ dns_huaweicloud_rm() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
|
||||
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
|
||||
|
||||
unset token # Clear token
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
|
||||
if [ -z "${token}" ]; then # Check token
|
||||
_err "dns_api(dns_huaweicloud): Error getting token."
|
||||
return 1
|
||||
|
|
@ -298,6 +309,7 @@ _get_token() {
|
|||
_username=$1
|
||||
_password=$2
|
||||
_domain_name=$3
|
||||
_region_name=$4
|
||||
|
||||
_debug "Getting Token"
|
||||
body="{
|
||||
|
|
@ -318,7 +330,7 @@ _get_token() {
|
|||
},
|
||||
\"scope\": {
|
||||
\"project\": {
|
||||
\"name\": \"ap-southeast-1\"
|
||||
\"name\": \"${_region_name}\"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ dns_infoblox_uddi_rm() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
record_id=$(echo "$response" | _egrep_o '"id":[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
_debug "record_id" "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
|
|
@ -178,7 +178,7 @@ _get_root() {
|
|||
# Check if response contains results (even if empty)
|
||||
if _contains "$response" '"results"'; then
|
||||
# Extract zone ID - must match the pattern dns/auth_zone/...
|
||||
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
zone_id=$(echo "$response" | _egrep_o '"id":[ ]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
if [ -n "$zone_id" ]; then
|
||||
# Found the zone
|
||||
_domain="$h"
|
||||
|
|
|
|||
|
|
@ -85,12 +85,10 @@ dns_infomaniak_add() {
|
|||
|
||||
# API call
|
||||
response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records")
|
||||
if [ -n "$response" ]; then
|
||||
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
|
||||
_info "Record added"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
if _contains "$response" '"result":"success"'; then
|
||||
_info "Record added"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
_err "Could not create record."
|
||||
_debug "Response: $response"
|
||||
|
|
@ -131,7 +129,7 @@ dns_infomaniak_rm() {
|
|||
fi
|
||||
|
||||
export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN"
|
||||
export _H2="ContentType: application/json"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
|
@ -169,11 +167,10 @@ dns_infomaniak_rm() {
|
|||
|
||||
# API call
|
||||
response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE)
|
||||
if [ -n "$response" ]; then
|
||||
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
|
||||
_info "Record deleted"
|
||||
return 0
|
||||
fi
|
||||
if _contains "$response" '"result":"success"'; then
|
||||
_info "Record deleted"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
_err "Could not delete record."
|
||||
_debug "Response: $response"
|
||||
|
|
@ -185,7 +182,11 @@ dns_infomaniak_rm() {
|
|||
_get_zone() {
|
||||
domain="$1"
|
||||
# Whatever the domain is, you can get the fqdn with the following.
|
||||
# shellcheck disable=SC1004
|
||||
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/')
|
||||
echo "${response%%\"*}"
|
||||
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones")
|
||||
_debug2 "_get_zone response" "$response"
|
||||
if ! _contains "$response" '"result":"success"'; then
|
||||
_err "cannot get zones for ${domain}, response: ${response}"
|
||||
return 1
|
||||
fi
|
||||
echo "$response" | _egrep_o '"fqdn" *: *"[^"]*"' | _head_n 1 | cut -d '"' -f 4
|
||||
}
|
||||
|
|
|
|||
|
|
@ -307,11 +307,32 @@ _get_root() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "$h"; then
|
||||
# Anchor the match to the XML tag and escape dots so $h is compared
|
||||
# literally: _contains uses grep, which treats "$h" as a regex, and a
|
||||
# bare "g.berlight.de" would match "<string>berlight.de" (the 'g' from
|
||||
# "<string>" plus '.' matching '>'). See issue #5129.
|
||||
_hregex=$(printf "%s" "$h" | sed 's/\./\\./g')
|
||||
if _contains "$response" "<string>$_hregex</string>"; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
# IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode,
|
||||
# encode each zone name via _idn() and compare -- no python dependency.
|
||||
if _contains "$h" "xn--"; then
|
||||
_zone_unicode=$(printf "%s" "$response" | _egrep_o '<string>[^<]*' |
|
||||
sed 's/<[^>]*>//g' | while IFS= read -r _z; do
|
||||
if [ "$(_idn "$_z")" = "$h" ]; then
|
||||
printf "%s" "$_z"
|
||||
break
|
||||
fi
|
||||
done)
|
||||
if [ -n "$_zone_unicode" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$_zone_unicode"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ IONOS_TXT_TTL=60 # minimum accepted by API
|
|||
IONOS_TXT_PRIO=10
|
||||
|
||||
dns_ionos_add() {
|
||||
fulldomain=$1
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
if ! _ionos_init; then
|
||||
|
|
@ -34,7 +34,7 @@ dns_ionos_add() {
|
|||
}
|
||||
|
||||
dns_ionos_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
if ! _ionos_init; then
|
||||
|
|
@ -146,7 +146,7 @@ _ionos_rest() {
|
|||
|
||||
if [ "$method" != "GET" ]; then
|
||||
export _H2="Accept: application/json"
|
||||
export _H3="Content-Type: application/json"
|
||||
export _H3=
|
||||
|
||||
_response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")"
|
||||
else
|
||||
|
|
|
|||
91
dnsapi/dns_ipprojects.sh
Normal file
91
dnsapi/dns_ipprojects.sh
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_ipprojects_info='IP-Projects DNS
|
||||
Site: ip-projects.de/
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects
|
||||
Options:
|
||||
IPP_Apikey API Key
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6958
|
||||
Author: Markus Ebner
|
||||
'
|
||||
|
||||
IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
|
||||
IPP_API="https://api.ip-projects.de/v1/dns/acme"
|
||||
|
||||
######## Public functions ########
|
||||
|
||||
dns_ipprojects_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using IP-Projects DNS API to add record"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _IPP_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_IPP_api_request "add" "$fulldomain" "$txtvalue"
|
||||
}
|
||||
|
||||
dns_ipprojects_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using IP-Projects DNS API to remove record"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _IPP_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_IPP_api_request "remove" "$fulldomain" "$txtvalue"
|
||||
}
|
||||
|
||||
######## Private helpers ########
|
||||
|
||||
_IPP_load_credentials() {
|
||||
IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
|
||||
|
||||
if [ -z "$IPP_Apikey" ]; then
|
||||
_err "You must export IPP_Apikey"
|
||||
_err "e.g.: export IPP_Apikey=\"your_api_key\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable IPP_Apikey "$IPP_Apikey"
|
||||
return 0
|
||||
}
|
||||
|
||||
_IPP_api_request() {
|
||||
action="$1"
|
||||
domain="$2"
|
||||
value="$3"
|
||||
|
||||
url="$IPP_API/$action"
|
||||
|
||||
data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}"
|
||||
_debug url "$url"
|
||||
_debug data "$data"
|
||||
export _H1="X-API-Key: $IPP_Apikey"
|
||||
|
||||
response="$(_post "$data" "$url" "" "POST" "application/json")"
|
||||
ret="$?"
|
||||
_ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
_err "HTTP request failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_ipprojects_last_http_code" != "200" ]; then
|
||||
_err "API returned an error [code: ${_ipprojects_last_http_code}]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ _ISPC_getZoneInfo() {
|
|||
curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")"
|
||||
_debug "Calling _ISPC_ClientGetID: '${curData}' '${ISPC_Api}?client_get_id'"
|
||||
_debug "Result of _ISPC_ClientGetID: '$curResult'"
|
||||
client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}')
|
||||
client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d '[' -f 1 | tr -d '{}')
|
||||
_debug "Client ID: '${client_id}'"
|
||||
case "${client_id}" in
|
||||
'' | *[!0-9]*)
|
||||
|
|
|
|||
|
|
@ -35,9 +35,28 @@ dns_joker_add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Joker's /nic/replace overwrites all TXT records at the label on every call,
|
||||
# and the API is not readable, so accumulate the values locally (keyed by the
|
||||
# full record name) and re-send the whole set each time. This is required so a
|
||||
# wildcard cert (base + *.domain both validating under the same
|
||||
# _acme-challenge label) does not overwrite its own first challenge value.
|
||||
_joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_')
|
||||
_joker_values=$(_readdomainconf "$_joker_conf_key")
|
||||
if [ -z "$_joker_values" ]; then
|
||||
_joker_values="$txtvalue"
|
||||
elif ! _contains " $_joker_values " " $txtvalue "; then
|
||||
_joker_values="$_joker_values $txtvalue"
|
||||
fi
|
||||
|
||||
_joker_value_params=""
|
||||
for _joker_v in $_joker_values; do
|
||||
_joker_value_params="$_joker_value_params&value=$_joker_v"
|
||||
done
|
||||
|
||||
_info "Adding TXT record"
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value=$txtvalue"; then
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then
|
||||
if _startswith "$response" "OK"; then
|
||||
_savedomainconf "$_joker_conf_key" "$_joker_values"
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -59,10 +78,36 @@ dns_joker_rm() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
# Remove only this value from the accumulated set and replace the label with
|
||||
# whatever remains (an empty value clears the label's TXT records entirely).
|
||||
_joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_')
|
||||
_joker_values=$(_readdomainconf "$_joker_conf_key")
|
||||
_joker_remaining=""
|
||||
for _joker_v in $_joker_values; do
|
||||
if [ "$_joker_v" != "$txtvalue" ]; then
|
||||
_joker_remaining="$_joker_remaining $_joker_v"
|
||||
fi
|
||||
done
|
||||
_joker_remaining=$(printf "%s" "$_joker_remaining" | sed 's/^ *//')
|
||||
|
||||
_joker_value_params=""
|
||||
for _joker_v in $_joker_remaining; do
|
||||
_joker_value_params="$_joker_value_params&value=$_joker_v"
|
||||
done
|
||||
if [ -z "$_joker_value_params" ]; then
|
||||
_joker_value_params="&value="
|
||||
fi
|
||||
|
||||
_info "Removing TXT record"
|
||||
# TXT record is removed by setting its value to empty.
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value="; then
|
||||
# TXT record is removed by replacing the label with the remaining values
|
||||
# (or an empty value, which clears all TXT records at the label).
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then
|
||||
if _startswith "$response" "OK"; then
|
||||
if [ -z "$_joker_remaining" ]; then
|
||||
_cleardomainconf "$_joker_conf_key"
|
||||
else
|
||||
_savedomainconf "$_joker_conf_key" "$_joker_remaining"
|
||||
fi
|
||||
_info "Removed, OK"
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ Site: www.knot-dns.cz/docs/2.5/html/man_knsupdate.html
|
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_knot
|
||||
Options:
|
||||
KNOT_SERVER Server hostname. Default: "localhost".
|
||||
KNOT_KEY File path to TSIG key
|
||||
KNOT_KEY TSIG key data, not a file path. knsupdate "key" statement format: "[alg:]name secret". E.g. "hmac-sha256:acme_key BASE64SECRET="
|
||||
KNOT_ZONE Zone name. Optional, set it when the challenge record lives in a delegated subdomain zone. Default: the parent domain of the challenge record.
|
||||
'
|
||||
|
||||
# See also dns_nsupdate.sh
|
||||
|
|
@ -21,6 +22,9 @@ dns_knot_add() {
|
|||
# save the dns server and key to the account.conf file.
|
||||
_saveaccountconf KNOT_SERVER "${KNOT_SERVER}"
|
||||
_saveaccountconf KNOT_KEY "${KNOT_KEY}"
|
||||
if [ -n "${KNOT_ZONE}" ]; then
|
||||
_saveaccountconf KNOT_ZONE "${KNOT_ZONE}"
|
||||
fi
|
||||
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Domain does not exist."
|
||||
|
|
@ -84,6 +88,13 @@ EOF
|
|||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
# a delegated subdomain zone cannot be derived from the record name;
|
||||
# let the user name the zone explicitly (issue 2881)
|
||||
if [ -n "${KNOT_ZONE}" ]; then
|
||||
_domain="${KNOT_ZONE%.}"
|
||||
_debug "Using KNOT_ZONE zone" "${_domain}"
|
||||
return 0
|
||||
fi
|
||||
i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
|
||||
i=$(_math "$i" - 1)
|
||||
|
||||
|
|
|
|||
197
dnsapi/dns_laodc.sh
Normal file
197
dnsapi/dns_laodc.sh
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_laodc_info='LaoDC DNS API Server
|
||||
Site: laodc.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_laodc
|
||||
Options:
|
||||
LaoDC_Key API Key
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6973
|
||||
Author: @laodc
|
||||
'
|
||||
|
||||
# Usage:
|
||||
# export LaoDC_Key="your-api-key"
|
||||
# acme.sh --issue --dns dns_laodc -d example.la -d *.example.la --dnssleep 120
|
||||
#
|
||||
# The credentials will be saved in ~/.acme.sh/account.conf
|
||||
|
||||
LAODC_VER="0.1.2"
|
||||
LAODC_API_ENDPOINT="https://dns.laodc.com/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_laodc_add _acme-challenge.example.la ZPXvna6tBhq7XQMH7_t2WC2sg0F-BdmtmmpUJiK6Ho
|
||||
dns_laodc_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using LaoDC DNS API"
|
||||
|
||||
_laodc_validate_key || return 1
|
||||
|
||||
_debug "Checking root zone exists for [$fulldomain]"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain_hash "$domain_hash"
|
||||
|
||||
_info "Adding acme record"
|
||||
if _laodc_api "POST" "$domain_hash" "$_sub_domain" "$txtvalue"; then
|
||||
if [ "$_code" = "201" ]; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add TXT record error, invalid code. Code: $_code"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Add TXT record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
dns_laodc_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_laodc_validate_key || return 1
|
||||
|
||||
_debug "Checking root zone exists for [$fulldomain]"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
_debug _root_domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain_hash "$domain_hash"
|
||||
|
||||
_info "Deleting acme record"
|
||||
if _laodc_api "DELETE" "$domain_hash" "$_sub_domain" "$txtvalue"; then
|
||||
if [ "$_code" = "204" ]; then
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Delete TXT record error, invalid code. Code: $_code"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Delete TXT record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
# _acme-challenge.www.domain.com
|
||||
# returns
|
||||
# _domain=domain.com
|
||||
# _sub_domain=www
|
||||
_get_root() {
|
||||
fqdn=$1
|
||||
p=1
|
||||
i=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$fqdn" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
return 1 # not valid domain
|
||||
fi
|
||||
|
||||
# Check API if domain exists
|
||||
if _laodc_api "GET" "$h"; then
|
||||
if [ "$_code" = "200" ]; then
|
||||
_domain="$h"
|
||||
|
||||
# DNS alias mode - @ is alias for fqdn
|
||||
_sub_domain=$(printf "%s" "$fqdn" | cut -d . -f 1-"$p")
|
||||
if [ "$i" = "1" ]; then
|
||||
_sub_domain="@"
|
||||
fi
|
||||
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_laodc_validate_key() {
|
||||
LaoDC_Key="${LaoDC_Key:-$(_readaccountconf_mutable LaoDC_Key)}"
|
||||
|
||||
if [ -z "$LaoDC_Key" ]; then
|
||||
LaoDC_Key=""
|
||||
_err "You didn't specify a LaoDC API Key yet."
|
||||
_err "Please export LaoDC_Key and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save the api key to the account conf file.
|
||||
_saveaccountconf_mutable LaoDC_Key "$LaoDC_Key"
|
||||
}
|
||||
|
||||
_laodc_api() {
|
||||
method=$1
|
||||
domain=$2
|
||||
subdomain=$3
|
||||
value=$4
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="User-Agent: acme.sh/$VER laodc-dns-acme-sh/$LAODC_VER"
|
||||
export _H3="Authorization: Bearer $LaoDC_Key"
|
||||
|
||||
case $method in
|
||||
GET)
|
||||
if [ -n "$subdomain" ]; then
|
||||
response="$(_get "$LAODC_API_ENDPOINT/$domain/$subdomain?type=TXT")"
|
||||
else
|
||||
response="$(_get "$LAODC_API_ENDPOINT/$domain")"
|
||||
fi
|
||||
;;
|
||||
POST)
|
||||
# Sanitize value input
|
||||
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
data="{ \"type\": \"TXT\", \"value\": \"$value\", \"ttl\": \"60\" }"
|
||||
response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "POST" "application/json")"
|
||||
;;
|
||||
DELETE)
|
||||
# Sanitize value input
|
||||
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
data="{ \"type\": \"TXT\", \"value\": \"$value\" }"
|
||||
response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "DELETE" "application/json")"
|
||||
;;
|
||||
esac
|
||||
|
||||
_ret=$?
|
||||
|
||||
# Unset immediately after request to prevent leaks
|
||||
export _H1=
|
||||
export _H2=
|
||||
export _H3=
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
responseHeaders="$(cat "$HTTP_HEADER")"
|
||||
|
||||
if echo "$responseHeaders" | grep -i "Content-Type: *application/json" >/dev/null 2>&1; then
|
||||
response="$(echo "$response" | _json_decode | _normalizeJson)"
|
||||
fi
|
||||
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
|
||||
_debug "http response code $_code"
|
||||
_debug response "$response"
|
||||
return 0
|
||||
}
|
||||
197
dnsapi/dns_level27.sh
Normal file
197
dnsapi/dns_level27.sh
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_level27_info='Level27
|
||||
Site: Level27.be
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_level27
|
||||
Options:
|
||||
LEVEL27_API_KEY API key. Get one from the Level27 control panel (https://app.level27.eu/account/profile/security).
|
||||
OptionsAlt:
|
||||
LEVEL27_API API base URL. Optional. Default "https://api.level27.eu/v1".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues
|
||||
Author: Jeroen Moors <jeroen.moors@level27.be>
|
||||
'
|
||||
|
||||
LEVEL27_API_DEFAULT="https://api.level27.eu/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_level27_add _acme-challenge.www.example.com "TXT-value"
|
||||
dns_level27_add() {
|
||||
fulldomain="$(_idn "$1")"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using Level27 to add a TXT record for $fulldomain"
|
||||
|
||||
if ! _level27_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Could not determine the root zone for $fulldomain at Level27."
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_level27_data="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\"}"
|
||||
if ! _level27_rest POST "domains/$_domain_id/records" "$_level27_data"; then
|
||||
_err "Could not add the TXT record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"id\":"; then
|
||||
_info "TXT record added."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Unexpected response while adding the TXT record."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: dns_level27_rm _acme-challenge.www.example.com "TXT-value"
|
||||
dns_level27_rm() {
|
||||
fulldomain="$(_idn "$1")"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using Level27 to remove the TXT record for $fulldomain"
|
||||
|
||||
if ! _level27_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Could not determine the root zone for $fulldomain at Level27."
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _level27_rest GET "domains/$_domain_id/records?type=TXT"; then
|
||||
_err "Could not list the existing TXT records."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_record_id="$(_level27_find_record_id "$response" "$txtvalue")"
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "No matching TXT record found; nothing to remove."
|
||||
return 0
|
||||
fi
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if ! _level27_rest DELETE "domains/$_domain_id/records/$_record_id"; then
|
||||
_err "Could not remove the TXT record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record removed."
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# Reads and validates the API credentials and endpoint, and stores them for renewals.
|
||||
_level27_init() {
|
||||
LEVEL27_API_KEY="${LEVEL27_API_KEY:-$(_readaccountconf_mutable LEVEL27_API_KEY)}"
|
||||
if [ -z "$LEVEL27_API_KEY" ]; then
|
||||
LEVEL27_API_KEY=""
|
||||
_err "You must export the variable LEVEL27_API_KEY before using the Level27 DNS API."
|
||||
_err "Get an API key from the Level27 control panel (https://app.level27.eu/account/profile/security)."
|
||||
return 1
|
||||
fi
|
||||
LEVEL27_API_KEY="$(echo "$LEVEL27_API_KEY" | tr -d '"')"
|
||||
_saveaccountconf_mutable LEVEL27_API_KEY "$LEVEL27_API_KEY"
|
||||
|
||||
LEVEL27_API="${LEVEL27_API:-$(_readaccountconf_mutable LEVEL27_API)}"
|
||||
if [ -z "$LEVEL27_API" ]; then
|
||||
LEVEL27_API="$LEVEL27_API_DEFAULT"
|
||||
fi
|
||||
_saveaccountconf_mutable LEVEL27_API "$LEVEL27_API"
|
||||
|
||||
# Remove a trailing slash so endpoints can be appended consistently.
|
||||
LEVEL27_API="$(echo "$LEVEL27_API" | sed 's#/$##')"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _get_root _acme-challenge.www.example.com
|
||||
# Splits the full domain into the registered zone and the subdomain part.
|
||||
# Sets: _domain, _domain_id, _sub_domain
|
||||
_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 ! _level27_rest GET "domains?filter=$h"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_level27_zones="$(echo "$response" | _normalizeJson)"
|
||||
if _contains "$_level27_zones" "\"fullname\":\"$h\""; then
|
||||
_domain_line="$(echo "$_level27_zones" | sed 's/},{/}\n{/g' | grep "\"fullname\":\"$h\"" | _head_n 1)"
|
||||
_domain_id="$(echo "$_domain_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2)"
|
||||
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
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: _level27_find_record_id "<records-json>" "<txtvalue>"
|
||||
# Prints the id of the TXT record whose content matches the value, or nothing.
|
||||
_level27_find_record_id() {
|
||||
_records="$(echo "$1" | _normalizeJson | sed 's/},{/}\n{/g')"
|
||||
_wanted="$2"
|
||||
_record_line="$(echo "$_records" | grep "\"content\":\"$_wanted\"" | _head_n 1)"
|
||||
if [ -z "$_record_line" ]; then
|
||||
# Some APIs store TXT content wrapped in quotes.
|
||||
_record_line="$(echo "$_records" | grep "\"content\":\"\\\\\"$_wanted\\\\\"\"" | _head_n 1)"
|
||||
fi
|
||||
if [ -z "$_record_line" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "$_record_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2
|
||||
}
|
||||
|
||||
# Usage: _level27_rest <method> <endpoint> [data]
|
||||
# Performs an authenticated API call and stores the body in $response.
|
||||
_level27_rest() {
|
||||
m="$1"
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
export _H1="Authorization: $LEVEL27_API_KEY"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug2 data "$data"
|
||||
response="$(_post "$data" "$LEVEL27_API/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$LEVEL27_API/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Error querying the Level27 API endpoint: $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ _me_rest() {
|
|||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
cdate=$(LANG=C date -u +"%a, %d %b %Y %T %Z")
|
||||
cdate=$(LC_ALL=C date -u +"%a, %d %b %Y %T %Z")
|
||||
hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex)
|
||||
|
||||
export _H1="x-dnsme-apiKey: $ME_Key"
|
||||
|
|
|
|||
167
dnsapi/dns_muumuu.sh
Executable file
167
dnsapi/dns_muumuu.sh
Executable file
|
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_muumuu_info='muumuu-domain.com
|
||||
Site: muumuu-domain.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_muumuu
|
||||
Options:
|
||||
MUUMUU_PAT Personal Access Token (scopes: domains:read, dns:read, dns:write)
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7011
|
||||
'
|
||||
|
||||
MUUMUU_API="https://muumuu-domain.com/api/v2"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
dns_muumuu_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using muumuu-domain.com DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}"
|
||||
if [ -z "$MUUMUU_PAT" ]; then
|
||||
_err "MUUMUU_PAT is not set."
|
||||
_err "Please create a Personal Access Token at https://muumuu-domain.com"
|
||||
_err "with scopes: domains:read, dns:read, dns:write"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable MUUMUU_PAT "$MUUMUU_PAT"
|
||||
|
||||
if ! _muumuu_get_root "$fulldomain"; then
|
||||
_err "Unable to find the root domain for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Adding TXT record for ${fulldomain}"
|
||||
body="{\"fqdn\":\"${fulldomain}.\",\"type\":\"TXT\",\"value\":\"${txtvalue}\",\"ttl\":3600}"
|
||||
if _muumuu_rest POST "/me/domains/${_domain_id}/dns-records" "$body"; then
|
||||
if [ "$_muumuu_code" = "201" ]; then
|
||||
_info "TXT record added successfully"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Failed to add TXT record (HTTP ${_muumuu_code})"
|
||||
return 1
|
||||
}
|
||||
|
||||
dns_muumuu_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using muumuu-domain.com DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}"
|
||||
if [ -z "$MUUMUU_PAT" ]; then
|
||||
_err "MUUMUU_PAT is not set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _muumuu_get_root "$fulldomain"; then
|
||||
_err "Unable to find the root domain for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
|
||||
_info "Looking up TXT record for ${fulldomain}"
|
||||
if ! _muumuu_rest GET "/me/domains/${_domain_id}/dns-records?type=TXT&fqdn=${fulldomain}."; then
|
||||
_err "Failed to list TXT records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$response" | _egrep_o "\"id\":[0-9]+[^}]*\"value\":\"${txtvalue}\"" | _egrep_o "\"id\":[0-9]+" | _head_n 1 | cut -d: -f2)
|
||||
if [ -z "$record_id" ]; then
|
||||
_info "TXT record not found, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
_debug record_id "$record_id"
|
||||
|
||||
if _muumuu_rest DELETE "/me/domains/${_domain_id}/dns-records/${record_id}"; then
|
||||
if [ "$_muumuu_code" = "204" ]; then
|
||||
_info "TXT record deleted successfully"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Failed to delete TXT record (HTTP ${_muumuu_code})"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# _acme-challenge.www.example.com
|
||||
# sets:
|
||||
# _domain_id MU00000001
|
||||
# _sub_domain _acme-challenge.www
|
||||
# _domain example.com
|
||||
_muumuu_get_root() {
|
||||
domain="$1"
|
||||
i=1
|
||||
p=0
|
||||
h=""
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
if ! _muumuu_rest GET "/me/domains?fqdn=${h}&page-size=1"; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$_muumuu_code" = "401" ] || [ "$_muumuu_code" = "403" ]; then
|
||||
_err "Authentication failed (HTTP ${_muumuu_code}). Check MUUMUU_PAT."
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "\"fqdn\":\"${h}\""; then
|
||||
_domain_id=$(echo "$response" | _egrep_o "\"id\":\"MU[0-9]+\"" | _head_n 1 | cut -d: -f2 | tr -d '"')
|
||||
_domain="$h"
|
||||
if [ "$p" = "0" ]; then
|
||||
_sub_domain=""
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_muumuu_rest() {
|
||||
_muumuu_method="$1"
|
||||
_muumuu_path="$2"
|
||||
_muumuu_data="$3"
|
||||
_muumuu_url="${MUUMUU_API}${_muumuu_path}"
|
||||
|
||||
export _H1="Authorization: Bearer ${MUUMUU_PAT}"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
export _H4=""
|
||||
export _H5=""
|
||||
|
||||
_secure_debug2 data "$_muumuu_data"
|
||||
|
||||
if [ "$_muumuu_method" = "GET" ]; then
|
||||
response="$(_get "$_muumuu_url")"
|
||||
else
|
||||
response="$(_post "$_muumuu_data" "$_muumuu_url" "" "$_muumuu_method")"
|
||||
fi
|
||||
_muumuu_ret="$?"
|
||||
_muumuu_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
_debug "HTTP code: ${_muumuu_code}"
|
||||
_secure_debug2 response "$response"
|
||||
|
||||
if [ "$_muumuu_ret" != "0" ]; then
|
||||
_err "Error accessing ${_muumuu_url}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response="$(printf "%s" "$response" | _normalizeJson)"
|
||||
return 0
|
||||
}
|
||||
|
|
@ -186,7 +186,7 @@ _oauth2() {
|
|||
_oauth2_std() {
|
||||
# HTTP Basic Authentication
|
||||
_H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)"
|
||||
_H2="Accepts: application/json"
|
||||
_H2="Accept: application/json"
|
||||
export _H1 _H2
|
||||
body="grant_type=client_credentials"
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ _oauth2_std() {
|
|||
}
|
||||
|
||||
_oauth2_github() {
|
||||
_H1="Accepts: application/json"
|
||||
_H1="Accept: application/json"
|
||||
export _H1
|
||||
body="{\"login\":{\"handle\":\"$MB_AK\",\"pass\":\"$MB_AS\",\"floating\":1}}"
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ _mb_rest() {
|
|||
fi
|
||||
|
||||
_H1="Authorization: Bearer $MB_TK"
|
||||
_H2="Accepts: application/json"
|
||||
_H2="Accept: application/json"
|
||||
export _H1 _H2
|
||||
if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then
|
||||
# body url [needbase64] [POST|PUT|DELETE] [ContentType]
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue