Compare commits

..

No commits in common. "master" and "3.1.3" have entirely different histories.

128 changed files with 1318 additions and 10033 deletions

View file

@ -1,50 +0,0 @@
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

View file

@ -26,9 +26,9 @@ jobs:
id: step_one
run: |
if [ "${{secrets.TokenName1}}" ] ; then
echo "hasToken=true" >> "$GITHUB_OUTPUT"
echo "::set-output name=hasToken::true"
else
echo "hasToken=false" >> "$GITHUB_OUTPUT"
echo "::set-output name=hasToken::false"
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@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Set env file
@ -114,11 +114,9 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install tools
run: |
brew untap aws/tap || true
brew install socat
run: 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
@ -167,7 +165,7 @@ jobs:
- name: Set git to use LF
run: |
git config --global core.autocrlf false
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install cygwin base packages with chocolatey
run: |
choco config get cacheLocation
@ -178,14 +176,9 @@ 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: bash
shell: cmd
run: |
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"
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %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
@ -231,72 +224,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg install -y socat curl
usesh: true
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
@ -329,7 +262,7 @@ jobs:
OpenBSD:
runs-on: ubuntu-latest
needs: GhostBSD
needs: FreeBSD
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
@ -347,13 +280,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg_add socat curl libiconv
usesh: true
@ -404,13 +336,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
/usr/sbin/pkg_add curl socat
@ -462,13 +393,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: |
pkg install -y libnghttp2
@ -524,13 +454,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: mport install socat curl || true
usesh: true
@ -582,13 +511,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: |
@ -642,13 +570,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
@ -699,13 +626,12 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: pkg install socat
@ -735,66 +661,9 @@ jobs:
Tribblix:
runs-on: ubuntu-latest
needs: OpenIndiana
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 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@v7
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/tribblix-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: nfs
prepare: zap install socat
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
needs: OpenIndiana
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
@ -813,140 +682,19 @@ jobs:
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
sync: rsync
copyback: false
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}}"

View file

@ -58,7 +58,6 @@ jobs:
- uses: vmactions/dragonflybsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -64,7 +64,6 @@ jobs:
- uses: vmactions/freebsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -1,83 +0,0 @@
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"

View file

@ -65,7 +65,6 @@ jobs:
- uses: vmactions/haiku-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -1,76 +0,0 @@
name: Hurd
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Hurd.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Hurd.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Hurd:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/hurd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
nat: |
"8080": "80"
# Do NOT install socat: socat's SYSTEM: address is broken on GNU Hurd
# (the child shell output goes to socat's stdout instead of the socket,
# so clients get an empty reply). Without socat, acme.sh standalone
# mode falls back to its python3 server, which works on Hurd.
prepare: |
apt-get update -y
apt-get install -y curl cron
usesh: true
sync: rsync
copyback: false
run: |
cd ../acmetest \
&& ./letest.sh
- name: DebugOnError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

View file

@ -58,7 +58,6 @@ jobs:
- uses: vmactions/midnightbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -58,7 +58,6 @@ jobs:
- uses: vmactions/netbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -1,66 +0,0 @@
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

View file

@ -64,7 +64,6 @@ jobs:
- uses: vmactions/omnios-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -64,7 +64,6 @@ jobs:
- uses: vmactions/openbsd-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -1,70 +0,0 @@
name: OpenEuler
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenEuler.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenEuler.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenEuler:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
steps:
- uses: actions/checkout@v7
- uses: anyvm-org/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openeuler-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
nat: |
"8080": "80"
prepare: 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"

View file

@ -64,7 +64,6 @@ jobs:
- uses: vmactions/openindiana-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -64,7 +64,6 @@ jobs:
- uses: vmactions/solaris-vm@v1
with:
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
cache-after-prepare: true
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"

View file

@ -1,80 +0,0 @@
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"

View file

@ -1,114 +0,0 @@
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"
});

View file

@ -41,29 +41,23 @@ 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@v4
uses: docker/setup-qemu-action@v2
- 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@v4
uses: docker/setup-buildx-action@v2
- 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
@ -79,8 +73,6 @@ jobs:
fi
fi
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
DOCKER_LABELS=()
while read -r label; do
DOCKER_LABELS+=(--label "${label}")
@ -92,9 +84,3 @@ 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"

View file

@ -2,128 +2,18 @@ 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@v9
- uses: actions/github-script@v6
with:
script: |
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,
github.rest.issues.createComment({
issue_number: context.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. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
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."
})

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v9
- uses: actions/github-script@v6
with:
script: |
await github.rest.issues.createComment({

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v9
- uses: actions/github-script@v6
with:
script: |
await github.rest.issues.createComment({

View file

@ -1,110 +0,0 @@
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"
});

View file

@ -1,32 +0,0 @@
name: Mirror version tag
# Historical release tags are plain version numbers ("3.1.3") and cannot be
# renamed. When a plain version tag is pushed (including the tag created by
# publishing a GitHub release), mirror it as a "v"-prefixed tag ("v3.1.3")
# pointing to the same object, so both forms exist.
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
# refs created with GITHUB_TOKEN do not fire workflows anyway.
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 }}"

View file

@ -1,325 +0,0 @@
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

View file

@ -9,14 +9,13 @@ jobs:
if: github.actor != 'neilpang'
steps:
- name: Checkout wiki repository
uses: actions/checkout@v7
uses: actions/checkout@v6
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")
@ -28,17 +27,6 @@ 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
@ -53,21 +41,17 @@ jobs:
echo "Time: $now"
echo "Page: [$page_name]($page_url) (Action: $page_action)"
echo "Comment: $page_summary"
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 "[Click here to Revert](${page_url}/_history)"
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
if: steps.msg.outputs.notify == 'true'
uses: peter-evans/create-issue-from-file@v6
uses: peter-evans/create-issue-from-file@v5
with:
title: "Wiki edited"
content-filepath: ./wiki-change-msg.txt

View file

@ -1,8 +0,0 @@
# 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.

View file

@ -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 % 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 \
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 \
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
View file

@ -1,21 +1,7 @@
<p align="center">
<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>
<a href="https://zerossl.com/?fromacme.sh">
<img src="https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05" alt="zerossl.com">
</a>
</p>
<h1 align="center">🔐 acme.sh</h1>
@ -31,13 +17,9 @@
<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">
@ -130,10 +112,6 @@
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](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|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|28|[![Hurd](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|29|[![OpenEuler](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
@ -168,7 +146,6 @@
| 🌐 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/)) |
---
@ -419,53 +396,7 @@ acme.sh --renew -d example.com
---
### 🔟 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, longlived `_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)
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
Just set the `keylength` to a valid, supported value.
@ -496,7 +427,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com -
---
### 12️⃣ Issue Wildcard Certificates
### 11️⃣ Issue Wildcard Certificates
It's simple! Just give a wildcard domain as the `-d` parameter:
@ -508,9 +439,9 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
---
### 13️⃣ How to Renew Certificates
### 12️⃣ How to Renew Certificates
> 🔄 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).
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
However, you can force a renewal:
@ -524,43 +455,9 @@ 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.
---
### 14️⃣ How to Stop Certificate Renewal
### 1⃣3⃣ How to Stop Certificate Renewal
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
@ -574,7 +471,7 @@ The cert/key file is not removed from the disk.
---
### 15️⃣ How to Upgrade acme.sh
### 14️⃣ How to Upgrade acme.sh
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
@ -598,25 +495,25 @@ acme.sh --upgrade --auto-upgrade 0
---
### 16️⃣ Issue a Certificate from an Existing CSR
### 15️⃣ Issue a Certificate from an Existing CSR
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
---
### 17️⃣ Send Notifications in Cronjob
### 16️⃣ Send Notifications in Cronjob
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
---
### 18️⃣ Under the Hood
### 17️⃣ Under the Hood
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
---
### 19️⃣ Acknowledgments
### 18️⃣ Acknowledgments
| Project | Link |
|---------|------|
@ -633,8 +530,6 @@ 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)]
@ -660,7 +555,7 @@ Support this project with your organization. Your logo will show up here with a
---
### 2⃣0️⃣ License & Others
### 1⃣9️⃣ License & Others
📄 **License:** GPLv3
@ -670,7 +565,7 @@ Support this project with your organization. Your logo will show up here with a
---
### 21️⃣ Donate
### 20️⃣ Donate
> 💝 Your donation makes **acme.sh** better!
@ -682,7 +577,7 @@ Support this project with your organization. Your logo will show up here with a
---
### 22️⃣ About This Repository
### 21️⃣ 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!

1421
acme.sh

File diff suppressed because it is too large Load diff

View file

@ -1,341 +0,0 @@
# 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

View file

@ -1,222 +0,0 @@
#!/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
}

View file

@ -163,8 +163,8 @@ byteplus_alb_deploy() {
# ── 3. Read cert and key ─────────────────────────────────────────────────────
# BytePlus requires NO blank lines between PEM blocks in the certificate chain
_public_key=$(_strip_blank_lines <"$_cfullchain" | tr -d '\r')
_private_key=$(_strip_blank_lines <"$_ckey" | tr -d '\r')
_public_key=$(sed '/^[[:space:]]*$/d' "$_cfullchain" | tr -d '\r')
_private_key=$(sed '/^[[:space:]]*$/d' "$_ckey" | tr -d '\r')
if [ -z "$_public_key" ] || [ -z "$_private_key" ]; then
_err "Failed to read certificate or key file."

View file

@ -52,15 +52,7 @@ cpanel_uapi_deploy() {
# read cert and key files and urlencode both
_cert=$(_url_encode <"$_ccert")
# 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
_key=$(_url_encode <"$_ckey")
_debug2 _cert "$_cert"
_debug2 _key "$_key"
@ -87,11 +79,7 @@ cpanel_uapi_deploy() {
# Auto mode
if [ "$DEPLOY_CPANEL_AUTO_ENABLED" = "true" ]; then
# call API for site config
if [ -n "$_uapi_user" ]; then
_response=$(uapi --user="$_uapi_user" DomainInfo list_domains)
else
_response=$(uapi DomainInfo list_domains)
fi
_response=$(uapi DomainInfo list_domains)
# exit if error in response
if [ -z "$_response" ] || [ "${_response#*"$uapi_error_response"}" != "$_response" ]; then
_err "Error in deploying certificate - cannot retrieve sitelist:"
@ -206,8 +194,7 @@ __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 -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
}
# Load parameter by prefix+name - fallback to default if not set, and save to config

View file

@ -3,8 +3,6 @@
#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"
@ -73,18 +71,6 @@ 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
@ -126,20 +112,6 @@ 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
@ -217,22 +189,10 @@ _docker_exec() {
_debug2 cjson "$cjson"
execid="$(echo "$cjson" | cut -d '"' -f 4)"
_debug execid "$execid"
#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}")"
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
_debug2 ejson "$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"
if [ "$ejson" ]; then
_err "$ejson"
return 1
fi
else

View file

@ -1,175 +0,0 @@
#!/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
}

View file

@ -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/iam/auth/jwt/login")
_response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login")
_debug _response "$_response"
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
_debug _regex "$_regex"

View file

@ -43,8 +43,7 @@
# needing to reload HAProxy. Default is "no".
#
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
# 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")
# address format.
#
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
#
@ -194,6 +193,7 @@ 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,18 +272,12 @@ haproxy_deploy() {
_cafile_argument=""
fi
_debug _cafile_argument "${_cafile_argument}"
# 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)
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
_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_name}" = "LibreSSL" ]; then
_header_sep=" "
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
_header_sep="="
else
_header_sep=" "
@ -333,67 +327,62 @@ haproxy_deploy() {
# Update certificate over HAProxy stats socket or master CLI.
if _exists socat; then
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}"
# 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}"
_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}"
_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}."
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
return "${_ret}"
fi
# 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}"
# 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}"
_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}"
_ret=$?
if [ "${_ret}" != "0" ]; then
_err "Can't update '${_pem}' in haproxy"
return "${_ret}"
fi
fi
done
fi
else
_err "'socat' is not available, couldn't update over ${_socketname}"
fi

View file

@ -1,114 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to ikuai using curl
#
# it requires following environment variables:
#
# IKUAI_SCHEME="http" - http or https , defaults to "http"
# IKUAI_HOSTNAME="localhost" - host , defaults to "192.168.9.1"
# IKUAI_PORT="80" - port , defaults to "80"
# IKUAI_USERNAME="admin" - username , defaults to "admin"
# IKUAI_PASSWORD="yourPassword" - password
# IKUAI_CERT_ID=1 - ikuai cert id , defaults to 1, and only 1 is supported for now !!!
#
#returns 0 means success, otherwise error.
#
######## Public functions #####################
#
#domain keyfile certfile cafile fullchain
ikuai_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# Get deploy conf
_getdeployconf IKUAI_SCHEME
_getdeployconf IKUAI_HOSTNAME
_getdeployconf IKUAI_PORT
_getdeployconf IKUAI_USERNAME
_getdeployconf IKUAI_PASSWORD
_getdeployconf IKUAI_CERT_ID
# Use default if not provided
[ -n "$IKUAI_SCHEME" ] || IKUAI_SCHEME="http"
[ -n "$IKUAI_HOSTNAME" ] || IKUAI_HOSTNAME="192.168.9.1"
[ -n "$IKUAI_PORT" ] || IKUAI_PORT=80
[ -n "$IKUAI_USERNAME" ] || IKUAI_USERNAME="admin"
[ -n "$IKUAI_CERT_ID" ] || IKUAI_CERT_ID=1
if [ -z "$IKUAI_PASSWORD" ]; then
_err "please define IKUAI_PASSWORD."
return 1
fi
_debug2 IKUAI_SCHEME "$IKUAI_SCHEME"
_debug2 IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
_debug2 IKUAI_PORT "$IKUAI_PORT"
_debug2 IKUAI_USERNAME "$IKUAI_USERNAME"
_secure_debug2 IKUAI_PASSWORD "$IKUAI_PASSWORD"
_info "Login to ikuai ..."
_ikuai_url="$IKUAI_SCHEME://$IKUAI_HOSTNAME:$IKUAI_PORT"
_pass_md5="$(printf "%s" "$IKUAI_PASSWORD" | _digest md5 hex | _lower_case)"
_pass_salt="$(printf "salt_11%s" "$IKUAI_PASSWORD" | _base64)"
_debug2 _ikuai_url "$_ikuai_url"
_login_req="{\"username\":\"$IKUAI_USERNAME\",\"passwd\":\"$_pass_md5\",\"pass\":\"$_pass_salt\",\"remember_password\":\"\"}"
_response=$(_post "$_login_req" "$_ikuai_url/Action/login" "" "POST" "application/json")
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
# check ErrMsg
if [ "$_err_msg" != "Success" ]; then
_err "Failed to login to ikuai: $_err_msg"
return 1
fi
# check cookie
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2 | sed 's/;.*//')"
if [ -z "$_cookie" ]; then
_err "Fail to get the cookie."
return 1
fi
# Set cookie header
_H1="Cookie: $_cookie; username=$IKUAI_USERNAME; login=1"
_info "Deploy the cert to ikuai ... "
# Should replace \n to @ ," " to #
_cert_content_single_line="$(tr <"$_cfullchain" '\n' '@' | tr ' ' '#')"
_key_content_single_line="$(tr <"$_ckey" '\n' '@' | tr ' ' '#')"
_debug2 _cert_content_single_line "$_cert_content_single_line"
_secure_debug2 _key_content_single_line "$_key_content_single_line"
_key_manager_req="{\"func_name\":\"key_manager\",\"action\":\"save\",\"param\":{\"ca\":\"$_cert_content_single_line\",\"key\":\"$_key_content_single_line\",\"id\":$IKUAI_CERT_ID,\"enabled\":\"yes\",\"comment\":\"\"}}"
_response=$(_post "$_key_manager_req" "$_ikuai_url/Action/call" "" "POST" "application/json")
_err_msg="$(printf "%s" "$_response" | _normalizeJson | _egrep_o '"ErrMsg":"[^"]*"' | cut -d'"' -f 4)"
# check ErrMsg
if [ "$_err_msg" != "Success" ]; then
_err "Failed to deploy the cert to ikuai: $_err_msg"
return 1
fi
_info "Save the deploy config ... "
# Save the config
_savedeployconf IKUAI_SCHEME "$IKUAI_SCHEME"
_savedeployconf IKUAI_HOSTNAME "$IKUAI_HOSTNAME"
_savedeployconf IKUAI_PORT "$IKUAI_PORT"
_savedeployconf IKUAI_USERNAME "$IKUAI_USERNAME"
_savedeployconf IKUAI_PASSWORD "$IKUAI_PASSWORD"
_savedeployconf IKUAI_CERT_ID "$IKUAI_CERT_ID"
_info "Successfully deployed certificate to ikuai. Enjoy! :>"
return 0
}

View file

@ -83,7 +83,7 @@ keyhelp_deploy() {
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
_H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to upload certificate."
@ -118,7 +118,7 @@ keyhelp_deploy() {
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
_message=$(echo "$_response" | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to apply certificate."

View file

@ -10,10 +10,6 @@
# Usage (shown values are the examples):
# 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
# A name without a leading '/' is looked up in the certificate directory
# of the domain. An absolute path is used as is, so a single deploy file
# can be shared by all domains, e.g.
# - export MULTIDEPLOY_FILENAME="/etc/acme/multideploy.yml"
#
# 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com
@ -53,7 +49,7 @@ multideploy_deploy() {
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
_getdeployconf MULTIDEPLOY_FILENAME
MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}"
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
@ -79,8 +75,7 @@ multideploy_deploy() {
# This function preprocesses the deploy file by checking if 'yq' is installed,
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments:
# $@ - Posible deploy file names. A name starting with '/' is treated as an
# absolute path, any other name is relative to the domain directory.
# $@ - Posible deploy file names.
# Usage:
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
_preprocess_deployfile() {
@ -92,21 +87,15 @@ _preprocess_deployfile() {
_debug3 "yq is installed."
# Check if deploy file exists
found_file=""
for file in "$@"; do
if _startswith "$file" "/"; then
_multideploy_path="$file"
else
_multideploy_path="$DOMAIN_PATH/$file"
fi
_debug3 "Checking file" "$_multideploy_path"
if [ -f "$_multideploy_path" ]; then
_debug3 "Checking file" "$DOMAIN_PATH/$file"
if [ -f "$DOMAIN_PATH/$file" ]; then
_debug3 "File found"
if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file."
return 1
fi
found_file="$_multideploy_path"
found_file="$file"
else
_debug3 "File not found"
fi
@ -116,12 +105,12 @@ _preprocess_deployfile() {
_err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one."
return 1
fi
if ! _check_deployfile "$found_file"; then
_err "Deploy file is not valid: $found_file"
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file"
return 1
fi
echo "$found_file"
echo "$DOMAIN_PATH/$found_file"
}
# Description:
@ -221,7 +210,7 @@ _clear_envs() {
echo "$env_pairs" | while IFS='=' read -r _key _value; do
_debug3 "Deleting key" "$_key"
_cleardeployconf "$_key"
_cleardomainconf "SAVED_$_key"
unset -v "$_key"
done
}

View file

@ -54,8 +54,6 @@ mydevil_deploy() {
# Usage: ip=$(mydevil_get_ip domain.com)
# echo $ip
mydevil_get_ip() {
# 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
devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
return 0
}

View file

@ -296,20 +296,9 @@ 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
# 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
deployer cert
deployer key
deployer commit
if [ "$_panos_template_stack" ]; then
# try to get job status for 20 times in 30 sec interval
i=0

View file

@ -116,24 +116,17 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
_retval=$?
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed: $message"
_debug "Response" "$response"
return 1
fi
}

View file

@ -128,24 +128,17 @@ HEREDOC
export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
# The API errors out with a non-2xx HTTP status and an empty body,
# so the status line is checked too, not only the response body.
_status_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$_status_code"
response="$(echo "$response" | _json_decode | _normalizeJson)"
message=$(echo "$response" | _egrep_o '"message":"[^"]*' | cut -d : -f 2 | tr -d '"')
case "$_status_code" in
2[0-9][0-9])
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
fi
;;
esac
_err "Certificate deployment failed (HTTP status $_status_code). $message"
_debug "Response" "$response"
return 1
_retval=$?
if [ "${_retval}" -eq 0 ] && [ -z "$message" ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed: $message"
_debug "Response" "$response"
return 1
fi
}

View file

@ -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" "base64"
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
# push key to routeros
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
@ -143,7 +143,6 @@ 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=\\\"\\\";\

View file

@ -1,280 +0,0 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to a Shelly Gen3+ device.
# Deploy the HTTPS server certificate to a Shelly device on the local network.
#
# ```sh
# export SHELLY_HOST=192.168.1.100
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
# ```
#
# Environment variables:
# SHELLY_HOST (required) IP or hostname of the Shelly device
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
# Omit if auth is disabled on the device.
# SHELLY_USER (optional) Username for auth. Default: admin
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
# Default: 1 (reboot after upload)
#
# Requirements:
# - Shelly Gen3+ device (Gen4 recommended)
# - Firmware 2.0.0+ for HTTPS server certificate support
# - curl or wget
# - openssl (for SHA-256 digest and random cnonce)
#
# The device must be reachable via HTTP on the local network.
# The hook uploads the fullchain.pem and private key,
# then reboots the device to apply the new certificate.
#
# Authentication uses standard RFC 7616 HTTP Digest (SHA-256) since
# firmware 2.0.0. The JSON-RPC auth object is not used for HTTP transport.
#
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
shelly_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf SHELLY_HOST
_getdeployconf SHELLY_PASSWORD
_getdeployconf SHELLY_USER
_getdeployconf SHELLY_REBOOT
_debug SHELLY_HOST "$SHELLY_HOST"
_debug SHELLY_USER "$SHELLY_USER"
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
if [ -z "$SHELLY_HOST" ]; then
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
return 1
fi
SHELLY_USER="${SHELLY_USER:-admin}"
SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
_savedeployconf SHELLY_HOST "$SHELLY_HOST"
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
_savedeployconf SHELLY_USER "$SHELLY_USER"
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
# --- Auth handshake (only if password is set) ---
_shelly_auth_header=""
if [ -n "$SHELLY_PASSWORD" ]; then
_info "Authenticating to Shelly device at $SHELLY_HOST"
if ! _shelly_handshake; then
_err "Authentication handshake failed. Check SHELLY_PASSWORD and device accessibility."
return 1
fi
_info "Authentication successful"
fi
# --- Upload certificate ---
_info "Uploading certificate to Shelly device at $SHELLY_HOST"
if ! _shelly_upload_cert; then
_err "Certificate upload failed"
return 1
fi
# --- Upload key ---
_info "Uploading private key to Shelly device"
if ! _shelly_upload_key; then
_err "Private key upload failed"
return 1
fi
_info "Certificate and key uploaded successfully"
# --- Reboot ---
if [ "$SHELLY_REBOOT" != "0" ]; then
_info "Rebooting Shelly device to apply certificate"
# Reboot may close the connection before sending a response
_shelly_rpc "Shelly.Reboot" '{}' || _debug "Reboot may have closed connection (expected)"
_info "Reboot command sent. Device will restart shortly."
else
_info "Skipping reboot (SHELLY_REBOOT=0). Certificate will apply on next restart."
fi
# Clear auth header so it does not leak to other hooks
export _H1=""
return 0
}
# --- Helper functions ---
# Perform RFC 7616 HTTP Digest auth handshake.
# Sets _shelly_auth_header on success (the Authorization header value).
_shelly_handshake() {
_inithttp
_debug "Probing device for auth challenge"
# Use a protected method (Shelly.GetStatus) to trigger 401.
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
_post '{"id":1,"method":"Shelly.GetStatus"}' \
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
# Detect auth from HTTP status line rather than response body
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
# No auth challenge — device accepted the request without credentials
_debug "Device responded without auth challenge. Proceeding without auth."
return 0
fi
_shelly_realm="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*realm="//;s/".*//')"
_shelly_nonce="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*nonce="//;s/".*//')"
_shelly_qop="$(grep -i '^WWW-Authenticate:' "$HTTP_HEADER" | sed 's/.*qop="//;s/".*//')"
if [ -z "$_shelly_nonce" ]; then
_err "Failed to extract nonce from WWW-Authenticate header. Is SHELLY_PASSWORD correct?"
return 1
fi
_shelly_qop="${_shelly_qop:-auth}"
_debug "Shelly realm: $_shelly_realm"
_debug "Shelly qop: $_shelly_qop"
_secure_debug "Shelly nonce" "$_shelly_nonce"
# ha1 = SHA256(username:realm:password)
_shelly_ha1="$(printf '%s' "${SHELLY_USER}:${_shelly_realm}:${SHELLY_PASSWORD}" | _digest sha256 hex)"
_secure_debug "Shelly ha1" "$_shelly_ha1"
# Generate client nonce (openssl is required for _digest, so always available)
_shelly_cnonce="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 8 2>/dev/null)"
_debug "Shelly cnonce: $_shelly_cnonce"
# Build the digest Authorization header value (stored for reuse)
_shelly_nc=1
_shelly_build_auth_header
return 0
}
# Check whether the HTTP response headers contain a digest auth challenge.
# Returns 0 (true) if a 401 with WWW-Authenticate is present.
_shelly_has_auth_challenge() {
_shelly_headers_file="$1"
_shelly_status="$(grep -i '^HTTP/' "$_shelly_headers_file" | _tail_n 1 | awk '{print $2}')"
[ "$_shelly_status" = "401" ] && grep -qi '^WWW-Authenticate:' "$_shelly_headers_file"
}
# Build or rebuild the RFC 7616 Authorization header.
# Uses: _shelly_ha1, _shelly_nonce, _shelly_cnonce, _shelly_qop, _shelly_realm, _shelly_nc
# Sets: _shelly_auth_header
_shelly_build_auth_header() {
_shelly_nc_hex="$(printf '%08x' "$_shelly_nc")"
# ha2 = SHA256(POST:/rpc)
_shelly_ha2="$(printf '%s' "POST:/rpc" | _digest sha256 hex)"
# response = SHA256(ha1:nonce:nc:cnonce:qop:ha2)
_shelly_digest_response="$(printf '%s' "${_shelly_ha1}:${_shelly_nonce}:${_shelly_nc_hex}:${_shelly_cnonce}:${_shelly_qop}:${_shelly_ha2}" | _digest sha256 hex)"
# Build the Authorization header value (without the "Authorization: " prefix)
_shelly_auth_header="Digest username=\"${SHELLY_USER}\", realm=\"${_shelly_realm}\", nonce=\"${_shelly_nonce}\", uri=\"/rpc\", qop=${_shelly_qop}, nc=${_shelly_nc_hex}, cnonce=\"${_shelly_cnonce}\", response=\"${_shelly_digest_response}\", algorithm=SHA-256"
_secure_debug "Authorization header" "$_shelly_auth_header"
}
# Make a Shelly JSON-RPC call.
# Usage: _shelly_rpc <method> <params_json>
# Returns 0 on success, 1 on error.
_shelly_rpc() {
_shelly_method="$1"
_shelly_params="$2"
_shelly_body='{"id":1,"method":"'"$_shelly_method"'","params":'"$_shelly_params"'}'
_debug "RPC method: $_shelly_method"
_debug2 "RPC body: $_shelly_body"
# shellcheck disable=SC2090
if [ -n "$_shelly_auth_header" ]; then
export _H1="Authorization: $_shelly_auth_header"
else
export _H1=""
fi
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
_shelly_ret=$?
if [ "$_shelly_ret" != "0" ]; then
_err "HTTP request failed for $_shelly_method (curl/wget error $_shelly_ret)"
return 1
fi
# Empty response means something went wrong (auth required but not provided, etc.)
if [ -z "$response" ]; then
_err "Empty response from Shelly device. If authentication is enabled on the device, set SHELLY_PASSWORD."
return 1
fi
# Validate response looks like a Shelly JSON-RPC response.
# Catches non-JSON responses such as HTTP 429 "Too Many Requests" which
# would otherwise pass the empty and "error" checks below.
if ! _startswith "$response" '{' || ! _contains "$response" '"id"'; then
_err "Invalid response from Shelly device: $response"
return 1
fi
# Check for JSON-RPC error in response
if _contains "$response" '"error"'; then
_err "RPC error from Shelly: $response"
return 1
fi
_debug "RPC response: $response"
# Increment nonce counter and rebuild auth header for next request
if [ -n "$_shelly_auth_header" ]; then
_shelly_nc=$((_shelly_nc + 1))
_shelly_build_auth_header
fi
return 0
}
# Upload the certificate to the device.
# Note: We do NOT clear the existing certificate first, because the Shelly
# auto-removes all three files (cert, key, CA) when any one is cleared.
# Uploading overwrites in place — no clearing needed.
_shelly_upload_cert() {
_shelly_cert_data="$(_json_encode <"$_cfullchain")"
_debug "Uploading certificate"
if ! _shelly_rpc "Shelly.PutHTTPServerCert" '{"data":"'"$_shelly_cert_data"'"}'; then
_err "Failed to upload certificate to device"
return 1
fi
return 0
}
# Upload the private key to the device.
# Note: Do not clear first — see _shelly_upload_cert for rationale.
_shelly_upload_key() {
_shelly_key_data="$(_json_encode <"$_ckey")"
_debug "Uploading key"
if ! _shelly_rpc "Shelly.PutHTTPServerKey" '{"data":"'"$_shelly_key_data"'"}'; then
_err "Failed to upload key to device"
return 1
fi
return 0
}

View file

@ -25,8 +25,7 @@
# 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
@ -72,24 +71,6 @@ 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
@ -189,16 +170,10 @@ 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
if ! _ssh_deploy; then
# in case of an error, remember it, but keep going for the remaining servers
_returnCode=1
fi
_ssh_deploy
done
return $_returnCode
}
_ssh_deploy() {
@ -453,13 +428,9 @@ _ssh_remote_cmd() {
_secure_debug "Remote commands to execute: $_cmd"
_info "Submitting sequence of commands to remote server by $_ssh_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
# quotations in bash cmd below intended. Squash travis spellcheck error
# shellcheck disable=SC2029
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'"
_err_code="$?"
if [ "$_err_code" != "0" ]; then

View file

@ -1,4 +1,4 @@
#!/usr/bin/env sh
#!/bin/bash
################################################################################
# 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 create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
_err "Missing required tools to creat 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 -Eo '[0-9]+')
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[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 -Eo '[0-9]+')
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[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 be 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 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" "base64"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
_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 -Eo '[0-9]+')
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
_debug2 error_code "$error_code"
if [ -n "$error_code" ]; then
if [ "$error_code" -eq 105 ]; then
@ -344,7 +344,6 @@ synology_dsm_deploy() {
else
_err "Failed to fetch certificate info: $error_code, please try again or contact Synology to learn more."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@ -355,7 +354,6 @@ synology_dsm_deploy() {
if [ -z "$id" ] && [ -z "$SYNO_CREATE" ]; then
_err "Unable to find certificate: $SYNO_CERTIFICATE and \$SYNO_CREATE is not set."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
@ -391,13 +389,13 @@ synology_dsm_deploy() {
else
_info "Restart HTTP services not necessary."
fi
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_logout
return 0
else
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
_err "Unable to update certificate, got error response: $response."
_logout
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1
fi
}
@ -405,8 +403,6 @@ synology_dsm_deploy() {
#################### Private functions below ##################################
_logout() {
# Logout CERT user only to not occupy a permanent session, e.g. in DSM's "Connected Users" widget (based on previous variables)
# Must be called before _temp_admin_cleanup: once the temp admin is deleted, its session can no longer be logged out.
# Note: this overwrites $response, so print any error message that needs it before calling.
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=logout&_sid=$sid")
_debug3 response "$response"
}
@ -428,6 +424,11 @@ _temp_admin_cleanup() {
fi
}
#_cleardeployconf key
_cleardeployconf() {
_cleardomainconf "SAVED_$1"
}
# key
_check2cleardeployconfexp() {
_key="$1"

View file

@ -16,12 +16,7 @@
#
# # 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>"
# 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
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
#
### Private functions
@ -61,6 +56,7 @@ _ws_call() {
_ws_upload_cert() {
/usr/bin/env python - <<EOF
import sys
from truenas_api_client import Client
@ -82,6 +78,7 @@ with Client(uri="$_ws_uri") as c:
print("R:0")
print("E:_ws_upload_cert error!")
sys.exit(7)
EOF
return $?
@ -184,8 +181,6 @@ 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."
@ -201,21 +196,7 @@ truenas_ws_deploy() {
_info "TrueNAS protocol not set. Using 'ws'."
DEPLOY_TRUENAS_PROTOCOL="ws"
fi
# 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
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_debug _ws_uri "$_ws_uri"
@ -235,14 +216,13 @@ truenas_ws_deploy() {
if [ "$_ws_response" != "TRUE" ]; then
_err "TrueNAS is not ready."
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME, DEPLOY_TRUENAS_PROTOCOL and DEPLOY_TRUENAS_PORT."
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
_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

View file

@ -1,307 +0,0 @@
#!/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
}

View file

@ -1,158 +0,0 @@
#!/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
}

View file

@ -7,7 +7,6 @@ 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
'
@ -125,28 +124,11 @@ _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/"
# 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 ';')"
_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 ';')"
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@ -158,23 +140,17 @@ _1984hosting_login() {
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
export _H3="X-CSRFToken: $csrf_header"
url="https://1984.hosting/api/auth/"
response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
response="$(_post "username=$username&password=$password&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=[^;]*;' | _head_n 1 | tr -d ';')"
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
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 ';')"
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
@ -185,7 +161,6 @@ _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=""
@ -250,15 +225,9 @@ _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"
@ -266,7 +235,6 @@ _get_zone_id() {
_err "Error getting _zone_id for $2."
return 1
fi
_zone_id_for="$domain"
return 0
}
@ -289,8 +257,9 @@ _htmlget() {
# Add extra headers to request
_authpost() {
_get_zone_id "https://1984.hosting/domains" "$_domain"
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
url="https://1984.hosting/domains"
_get_zone_id "$url" "$_domain"
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
export _H3="X-CSRFToken: $csrf_header"

View file

@ -37,16 +37,6 @@ 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
@ -81,7 +71,7 @@ dns_acmedns_add() {
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
_debug data "$data"
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
_debug response "$response"
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then

View file

@ -18,9 +18,7 @@ Ali_DNS_API="https://alidns.aliyuncs.com/"
#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_ali_add() {
# the API only accepts punycode for IDN domains, and a raw UTF-8 domain
# also breaks the request signature (issue 4733)
fulldomain=$(_idn "$1")
fulldomain=$1
txtvalue=$2
_prepare_ali_credentials || return 1
@ -35,7 +33,7 @@ dns_ali_add() {
}
dns_ali_rm() {
fulldomain=$(_idn "$1")
fulldomain=$1
txtvalue=$2
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
@ -71,8 +69,8 @@ _ali_rest() {
ign="$2"
mtd="${3:-GET}"
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)
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)
url="$endpoint?Signature=$signature"
if [ "$mtd" = "GET" ]; then
@ -98,20 +96,6 @@ _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

View file

@ -1,490 +0,0 @@
#!/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
}

View file

@ -139,21 +139,12 @@ _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/&/\&amp;/g;s/</\&lt;/g;s/>/\&gt;/g;s/'/\&apos;/g;s/\"/\&quot;/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_xml" "$_autodns_password_xml" "$_autodns_context_xml"
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
}
# Arguments:

View file

@ -11,8 +11,7 @@ Options:
# All `_sleep` commands are included to avoid Route53 throttling, see
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
# Updated from "route53.amazonaws.com"
AWS_HOST="route53.global.api.aws"
AWS_HOST="route53.amazonaws.com"
AWS_URL="https://$AWS_HOST"
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"

View file

@ -49,95 +49,26 @@ 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_run_with_fallback "add" "$fulldomain" "$txtvalue"; then
_baidu_err "all baidu api engines failed for add: $fulldomain"
if ! _baidu_prepare_record "$fulldomain"; then
_baidu_err "baidu_prepare_record failed for add: $fulldomain"
return 1
fi
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
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
_baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name"
return 1
fi
@ -154,28 +85,16 @@ _baidu_add_record() {
_ttl="300"
;;
esac
txtvalue="$(_baidu_trim_ws "$_txtvalue")"
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
txtvalue="$(_baidu_trim_ws "$txtvalue")"
_record_domain="$(_baidu_trim_ws "$_record_domain")"
_zone_name="$(_baidu_trim_ws "$_zone_name")"
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
_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
if _baidu_is_api_error "$response"; then
@ -186,10 +105,16 @@ _baidu_add_record() {
return 0
}
_baidu_rm_record() {
_txtvalue="$1"
dns_baidu_rm() {
fulldomain=$(_idn "$1")
txtvalue=$2
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
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
_baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name"
return 1
fi
@ -213,37 +138,28 @@ _baidu_rm_record() {
fi
for _rid in $_ids; do
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
_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
done
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
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
return 0
@ -266,7 +182,6 @@ _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
@ -274,16 +189,13 @@ _baidu_load_credentials() {
_baidu_prepare_record() {
_fulldomain="$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
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
fi
_record_domain="$_sub_domain"
_zone_name="$_domain"
@ -322,43 +234,6 @@ _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"
@ -418,39 +293,6 @@ 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"
@ -475,17 +317,18 @@ _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"
@ -501,56 +344,6 @@ _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):
@ -706,14 +499,6 @@ _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"
@ -756,18 +541,6 @@ _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"

View file

@ -323,21 +323,21 @@ _bhosted_extract_id() {
fi
# JSON: "id":12345
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[ ]*:[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
_id="$(printf "%s" "$_resp" | _egrep_o '"id"[[:space:]]*:[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0
fi
# key=value: id=12345
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[^0-9a-zA-Z])id[ ]*=[ ]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
_id="$(printf "%s" "$_resp" | _egrep_o '(^|[[:space:][:punct:]])id[[:space:]]*=[[:space:]]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0
fi
# "record id 12345" / "recordid 12345"
_id="$(printf "%s" "$_resp" | _egrep_o '(record[ ]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
_id="$(printf "%s" "$_resp" | _egrep_o '(record[[:space:]]*id|recordid)[^0-9]*[0-9]+' | _head_n 1 | tr -cd '0-9')"
if [ -n "$_id" ]; then
printf "%s" "$_id"
return 0

View file

@ -1,137 +0,0 @@
#!/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
}

View file

@ -1,137 +0,0 @@
#!/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
}

View file

@ -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. Server response: $response"
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
return 1
fi

View file

@ -15,8 +15,7 @@ CN_API="https://beta.api.core-networks.de"
######## Public functions #####################
dns_cn_add() {
# Core-Networks API requires punycode for IDN domains
fulldomain=$(_idn "$1")
fulldomain=$1
txtvalue=$2
if ! _cn_login; then
@ -59,8 +58,7 @@ dns_cn_add() {
}
dns_cn_rm() {
# Core-Networks API requires punycode for IDN domains
fulldomain=$(_idn "$1")
fulldomain=$1
txtvalue=$2
if ! _cn_login; then

View file

@ -1,248 +0,0 @@
#!/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
}

View file

@ -38,7 +38,7 @@ dns_cpanel_add() {
fi
# adding entry
_info "Adding the entry"
stripped_fulldomain="${fulldomain%."$_domain"}"
stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_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,27 +128,13 @@ _get_root() {
_err "Primary domain list not found!"
return 1
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
for _domain in $_domains; do
_debug "Checking if $fulldomain ends with $_domain"
if (_endswith "$fulldomain" "$_domain"); then
_debug "Root domain: $_domain"
return 0
fi
done
if [ -n "$_domain" ]; then
_debug "Root domain: $_domain"
return 0
fi
return 1
}

View file

@ -1,181 +0,0 @@
#!/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
}

View file

@ -285,15 +285,15 @@ _cyon_delete_txt() {
list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async"
list_txt_response="$(_get "${list_txt_url}")"
list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')"
_debug list_txt_response "${list_txt_response}"
if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi
# Find and delete all acme challenge entries for the $fulldomain.
_dns_entries="$(printf "%s\n" "${list_txt_response}" | _egrep_o 'data-hash=\\"[^"]*\\" data-identifier=\\"[^"]*\\"' | sed 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\"/\1 \2/')"
_dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')"
printf "%s\n" "${_dns_entries}" | while read -r _hash _identifier; do
printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do
dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)"
dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)"

View file

@ -30,9 +30,8 @@ dns_czechia_add() {
return 1
fi
_czechia_tab="$(printf '\t')"
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken."
@ -77,7 +76,7 @@ dns_czechia_add() {
return 0
fi
_nres="$(printf '%s' "$_res" | _normalizeJson)"
_nres="$(_normalizeJson "$_res")"
if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then
_nres="$_res"
fi
@ -109,9 +108,8 @@ dns_czechia_rm() {
return 1
fi
_czechia_tab="$(printf '\t')"
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed "s/[ $_czechia_tab]//g; s/\.\$//")
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed "s/^[ $_czechia_tab]*//; s/[ $_czechia_tab]*\$//")
_cz=$(printf "%s" "$_current_zone" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
_tk=$(printf "%s" "$CZ_AuthorizationToken" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ -z "$_cz" ] || [ -z "$_tk" ]; then
_err "Missing zone or CZ_AuthorizationToken."
@ -182,13 +180,12 @@ _czechia_load_conf() {
}
_czechia_pick_zone() {
_czechia_pz_tab="$(printf '\t')"
_fd=$(printf "%s" "$1" | _lower_case | sed 's/\.$//')
_best_zone=""
_zones_space=$(printf "%s" "$CZ_Zones" | sed 's/,/ /g')
for _z in $_zones_space; do
_clean_z=$(printf "%s" "$_z" | _lower_case | sed "s/[ $_czechia_pz_tab]//g; s/\.\$//")
_clean_z=$(printf "%s" "$_z" | _lower_case | sed 's/[[:space:]]//g; s/\.$//')
[ -z "$_clean_z" ] && continue
case "$_fd" in

View file

@ -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". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40".
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443"
DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
Issues: github.com/TigerP/acme.sh/issues
'

View file

@ -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:
DEDYN_TOKEN API Token
DDNSS_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/2180
Author: Zheng Qian
'
@ -39,7 +39,6 @@ dns_desec_add() {
_err "invalid domain"
return 1
fi
_sub_domain=$(echo "$_sub_domain" | _lower_case)
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
@ -49,7 +48,7 @@ dns_desec_add() {
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
if [ "$_code" = "200" ]; then
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
_debug "existing TXT found"
_debug oldtxtvalues "$oldtxtvalues"
if [ -n "$oldtxtvalues" ]; then
@ -101,7 +100,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"
@ -111,7 +110,7 @@ dns_desec_rm() {
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
if [ "$_code" = "200" ]; then
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
_debug "existing TXT found"
_debug oldtxtvalues "$oldtxtvalues"
if [ -n "$oldtxtvalues" ]; then
@ -151,8 +150,6 @@ _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

View file

@ -5,11 +5,14 @@ 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"
@ -25,7 +28,20 @@ dns_dnsexit_add() {
return 1
fi
_dnsexit_zone_op add ',"ttl":1,"overwrite":false'
_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
}
#Usage: fulldomain txtvalue
@ -42,43 +58,54 @@ dns_dnsexit_rm() {
return 1
fi
_dnsexit_zone_op delete ''
_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
}
#################### Private functions below ##################################
# 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"
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
i=1
while true; do
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
_debug _domain "$_domain"
_domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$_domain"
if [ -z "$_domain" ]; then
_err "Could not find the root zone of $fulldomain in your DNSExit account"
return 1
fi
_sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")"
if [ "$_sub_domain" = "$fulldomain" ]; then
_sub_domain=""
fi
_debug _sub_domain "$_sub_domain"
_debug login "$DNSEXIT_AUTH_USER"
_debug password "$DNSEXIT_AUTH_PASS"
_debug domain "$_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"
_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"
fi
i=$(_math "$i" + 1)
done
return 1
}
_dnsexit_rest() {
@ -109,7 +136,27 @@ _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=''
@ -119,5 +166,23 @@ 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
}

View file

@ -5,7 +5,6 @@ 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
'
@ -18,7 +17,6 @@ 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."
@ -27,10 +25,10 @@ dns_dnsimple_add() {
fi
# save the oauth token for later
_saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
_saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
if ! _get_account_id; then
_err "failed to retrieve account id"
_err "failed to retrive account id"
return 1
fi
@ -58,14 +56,8 @@ 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 retrieve account id"
_err "failed to retrive account id"
return 1
fi
@ -130,16 +122,13 @@ _get_root() {
# returns _account_id
_get_account_id() {
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
_debug "retrive account id"
if ! _dnsimple_rest GET "whoami"; then
return 1
fi
_debug "retrieve account id"
if ! _dnsimple_rest GET "whoami"; then
if _contains "$response" "\"account\":null"; then
_err "no account associated with this token"
return 1
fi
@ -148,25 +137,7 @@ _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

View file

@ -23,8 +23,6 @@ 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=""
@ -34,8 +32,8 @@ dns_dynu_add() {
fi
#save the client id and secret to the account conf file.
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf Dynu_Secret "$Dynu_Secret"
if [ -z "$Dynu_Token" ]; then
_info "Getting Dynu token."
@ -71,8 +69,6 @@ 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=""
@ -82,8 +78,8 @@ dns_dynu_rm() {
fi
#save the client id and secret to the account conf file.
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
_saveaccountconf Dynu_Secret "$Dynu_Secret"
if [ -z "$Dynu_Token" ]; then
_info "Getting Dynu token."
@ -218,11 +214,11 @@ _dynu_authentication() {
response="$(_get "$Dynu_EndPoint/oauth2/token")"
if [ "$?" != "0" ]; then
_err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token"
_err "Authentication failed."
return 1
fi
if _contains "$response" "Authentication Exception"; then
_err "Authentication failed. Server response: $response"
_err "Authentication failed."
return 1
fi
if _contains "$response" "access_token"; then

View file

@ -363,12 +363,17 @@ _edgedns_rest() {
_edgedns_eg_timestamp() {
_debug "Generating signature Timestamp"
#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")"
_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")"
_debug "_eg_timestamp" "$_eg_timestamp"
}

View file

@ -1,267 +0,0 @@
#!/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
}

View file

@ -1,110 +0,0 @@
#!/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"
}

View file

@ -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 -E "<td>$search_domain</td>|<td>$search_domain\(.*\)</td>" |
grep "<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...

View file

@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
Options:
FREEMYIP_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/6247
Author: Recolic Keghart <root@recolic.net>, @Giova96, ExtremeFiretop
Author: Recolic Keghart <root@recolic.net>, @Giova96
'
FREEMYIP_DNS_API="https://freemyip.com/update?"
@ -68,30 +68,22 @@ dns_freemyip_rm() {
return $?
}
################ Private functions below ################
################ Private functions below ################
_get_root() {
_fmi_d="$1"
echo "$_fmi_d" | sed 's/.*\.\([^.]*\.[^.]*\.[^.]*\)$/\1/'
echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
}
# There is random failure while calling freemyip API too fast. This function automatically retry until success.
_freemyip_get_until_ok() {
_fmi_url="$1"
_fmi_i=1
while [ "$_fmi_i" -le 8 ]; do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $_fmi_i/8..."
_fmi_response="$(_get "$_fmi_url")"
printf '%s\n' "$_fmi_response" >&2
if _contains "$_fmi_response" "OK"; then
return 0
fi
for i in $(seq 1 8); do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
_sleep 1 # DO NOT send the request too fast
_fmi_i=$((_fmi_i + 1))
done
_err "Failed to request freemyip API. Server does not say 'OK'"
_err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
return 1
}
@ -101,16 +93,13 @@ _is_root_domain_published() {
_webroot="$(_get_root "$_fmi_d")"
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
_fmi_i=1
while [ "$_fmi_i" -le 3 ]; do
_debug "'$_webroot' ns lookup, retry $_fmi_i/3..."
for i in $(seq 1 3); do
_debug "'$_webroot' ns lookup, retry $i/3..."
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
_debug "'$_webroot' already has a TXT record published!"
return 0
fi
_sleep 10 # Give it some time to propagate the TXT record
_fmi_i=$((_fmi_i + 1))
done
return 1
}

View file

@ -69,12 +69,7 @@ dns_gd_add() {
return 1
fi
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
if ! _contains "$response" "$txtvalue"; then
_err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
return 1
fi
@ -150,8 +145,8 @@ dns_gd_rm() {
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=0
i=2
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
@ -159,41 +154,17 @@ _get_root() {
return 1
fi
# 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
# 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"
if _contains "$response" '"code":"NOT_FOUND"'; then
_debug "$h not found"
else
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$h"
return 0
fi
_debug "$h not found"
p="$i"
i=$(_math "$i" + 1)
done

View file

@ -1,263 +0,0 @@
#!/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
}

View file

@ -1,198 +0,0 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_hestiacp_info='HestiaCP Server API
Site: hestiacp.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hestiacp
Options:
HESTIA_HOST Panel URL. E.g. "https://panel.example.com:8083"
HESTIA_ACCESS API access key
HESTIA_SECRET API secret key
HESTIA_USER Username owning the DNS zones. Default "admin". Optional.
Issues: github.com/acmesh-official/acme.sh/issues/6251
Author: Radu Malica <radu.malica@gmail.com>
'
######## Public functions #####################
# Usage: dns_hestiacp_add fulldomain txtvalue
dns_hestiacp_add() {
fulldomain=$1
txtvalue=$2
if ! _hestia_init; then
return 1
fi
_debug "Detecting the root zone for $fulldomain"
if ! _hestia_get_root "$fulldomain"; then
_err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER"
return 1
fi
_debug _hestia_domain "$_hestia_domain"
_debug _hestia_sub "$_hestia_sub"
# _hestia_get_root left the zone record listing in _hestia_response
if _hestia_find_records "$_hestia_sub" "TXT" | grep -F -- "$txtvalue" >/dev/null; then
_info "The TXT record already exists, skipping"
return 0
fi
_info "Adding TXT record for $fulldomain"
if ! _hestia_rest "v-add-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_sub" "TXT" "$txtvalue" "" "" "yes" "600"; then
_err "Error adding TXT record: $_hestia_response"
return 1
fi
_info "TXT record added successfully"
return 0
}
# Usage: dns_hestiacp_rm fulldomain txtvalue
dns_hestiacp_rm() {
fulldomain=$1
txtvalue=$2
if ! _hestia_init; then
return 1
fi
_debug "Detecting the root zone for $fulldomain"
if ! _hestia_get_root "$fulldomain"; then
_err "Cannot find a DNS zone for $fulldomain under user $HESTIA_USER"
return 1
fi
_debug _hestia_domain "$_hestia_domain"
_debug _hestia_sub "$_hestia_sub"
_hestia_removed=0
_hestia_failed=0
while IFS='|' read -r _hestia_id _hestia_value || [ -n "$_hestia_id" ]; do
if [ -z "$_hestia_id" ]; then
continue
fi
if ! _contains "$_hestia_value" "$txtvalue"; then
continue
fi
_info "Deleting TXT record $_hestia_id"
if ! _hestia_rest "v-delete-dns-record" "$HESTIA_USER" "$_hestia_domain" "$_hestia_id" "yes"; then
_err "Error deleting TXT record $_hestia_id: $_hestia_response"
_hestia_failed=$(_math "$_hestia_failed" + 1)
continue
fi
_hestia_removed=$(_math "$_hestia_removed" + 1)
done <<EOF
$(_hestia_find_records "$_hestia_sub" "TXT")
EOF
if [ "$_hestia_removed" = "0" ] && [ "$_hestia_failed" = "0" ]; then
_info "No matching TXT record found to remove"
else
_info "Removed $_hestia_removed TXT record(s)"
fi
if [ "$_hestia_failed" != "0" ]; then
return 1
fi
return 0
}
#################### Private functions below ##################################
_hestia_init() {
HESTIA_HOST="${HESTIA_HOST:-$(_readaccountconf_mutable HESTIA_HOST)}"
HESTIA_ACCESS="${HESTIA_ACCESS:-$(_readaccountconf_mutable HESTIA_ACCESS)}"
HESTIA_SECRET="${HESTIA_SECRET:-$(_readaccountconf_mutable HESTIA_SECRET)}"
HESTIA_USER="${HESTIA_USER:-$(_readaccountconf_mutable HESTIA_USER)}"
if [ -z "$HESTIA_HOST" ] || [ -z "$HESTIA_ACCESS" ] || [ -z "$HESTIA_SECRET" ]; then
HESTIA_HOST=""
HESTIA_ACCESS=""
HESTIA_SECRET=""
_err "You must export HESTIA_HOST, HESTIA_ACCESS and HESTIA_SECRET first"
return 1
fi
HESTIA_HOST="${HESTIA_HOST%/}"
if ! echo "$HESTIA_HOST" | grep -qE '^https?://[^/]+$'; then
_err "HESTIA_HOST must be a valid URL (e.g. https://panel.example.com:8083)"
return 1
fi
if [ -z "$HESTIA_USER" ]; then
HESTIA_USER="admin"
fi
_saveaccountconf_mutable HESTIA_HOST "$HESTIA_HOST"
_saveaccountconf_mutable HESTIA_ACCESS "$HESTIA_ACCESS"
_saveaccountconf_mutable HESTIA_SECRET "$HESTIA_SECRET"
_saveaccountconf_mutable HESTIA_USER "$HESTIA_USER"
return 0
}
# Walk up the domain labels until the API returns a DNS zone.
# Sets _hestia_domain to the zone and _hestia_sub to the record name
# relative to the zone. The zone record listing stays in _hestia_response.
_hestia_get_root() {
_hestia_fqdn="${1%.}"
_hestia_i=1
while true; do
_hestia_h=$(printf "%s" "$_hestia_fqdn" | cut -d . -f "$_hestia_i"-100)
_debug2 _hestia_h "$_hestia_h"
if [ -z "$_hestia_h" ]; then
return 1
fi
if _hestia_rest "v-list-dns-records" "$HESTIA_USER" "$_hestia_h" "json"; then
_hestia_domain="$_hestia_h"
if [ "$_hestia_h" = "$_hestia_fqdn" ]; then
_hestia_sub="@"
else
_hestia_sub=$(printf "%s" "$_hestia_fqdn" | cut -d . -f 1-"$(_math "$_hestia_i" - 1)")
fi
return 0
fi
_hestia_i=$(_math "$_hestia_i" + 1)
done
}
# Call the HestiaCP API. Args: cmd [arg1 arg2 ...]
# The response body is stored in _hestia_response.
_hestia_rest() {
_hestia_cmd=$1
shift
_hestia_data="{\"access_key\":\"$HESTIA_ACCESS\",\"secret_key\":\"$HESTIA_SECRET\",\"cmd\":\"$_hestia_cmd\""
_hestia_argn=1
for _hestia_arg in "$@"; do
_hestia_data="$_hestia_data,\"arg$_hestia_argn\":\"$_hestia_arg\""
_hestia_argn=$(_math "$_hestia_argn" + 1)
done
_hestia_data="$_hestia_data}"
_debug2 "Calling $_hestia_cmd"
_hestia_response=$(_post "$_hestia_data" "$HESTIA_HOST/api/" "" "POST" "application/json")
_hestia_ret=$?
_debug2 _hestia_response "$_hestia_response"
if [ "$_hestia_ret" != "0" ]; then
_err "Error connecting to the HestiaCP API"
return 1
fi
if _contains "$_hestia_response" "Error:"; then
return 1
fi
return 0
}
# Extract records matching name and type from the v-list-dns-records
# response in _hestia_response. Prints one "id|value" line per match.
_hestia_find_records() {
_hestia_fname=$1
_hestia_ftype=$2
echo "$_hestia_response" | tr -d '\n' | sed 's/},/}\
/g' | grep -F -- "\"RECORD\": \"$_hestia_fname\"" | grep -F -- "\"TYPE\": \"$_hestia_ftype\"" | while read -r _hestia_line; do
_hestia_id=$(echo "$_hestia_line" | _egrep_o '"ID": "[^"]*' | cut -d '"' -f 4)
_hestia_value=$(echo "$_hestia_line" | _egrep_o '"VALUE": "[^"]*' | cut -d '"' -f 4)
if [ -n "$_hestia_id" ]; then
echo "$_hestia_id|$_hestia_value"
fi
done
}

256
dnsapi/dns_hetzner.sh Executable file
View file

@ -0,0 +1,256 @@
#!/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
}

View file

@ -40,11 +40,6 @@ _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"
}

View file

@ -1,196 +0,0 @@
#!/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
}

View file

@ -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/v2).
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection.
HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection).
Author: HostUp (https://cloud.hostup.se/contact/en)
'
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2"
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api"
HOSTUP_DEFAULT_TTL=60
# Public: add TXT record
@ -20,7 +20,6 @@ HOSTUP_DEFAULT_TTL=60
dns_hostup_add() {
fulldomain="$1"
txtvalue="$2"
hostup_add_txtvalue="$2"
_info "Using HostUp DNS API"
@ -35,36 +34,33 @@ dns_hostup_add() {
record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")"
record_name="$(_hostup_sanitize_name "$record_name")"
hostup_add_record_value="$(_hostup_json_escape "$hostup_add_txtvalue")"
record_value="$(_hostup_json_escape "$txtvalue")"
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
ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
_debug "zone_id" "$HOSTUP_ZONE_ID"
_debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
_debug "record_name" "$record_name"
_debug "ttl" "$ttl"
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
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
fi
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$hostup_add_record_value\",\"ttl\":$ttl}"
if ! _hostup_rest "POST" "/dns-zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
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
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"
fi
_info "Added TXT record for $fulldomain"
return 0
}
@ -89,9 +85,20 @@ 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" "$record_value"
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
return 0
fi
@ -102,7 +109,7 @@ dns_hostup_rm() {
fi
_info "Deleted TXT record $HOSTUP_RECORD_ID"
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value"
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
HOSTUP_ZONE_ID=""
return 0
}
@ -120,18 +127,21 @@ _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, write:dns, and read:domains scopes."
_err "Please export your HostUp API key with read:dns and write:dns 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
@ -139,80 +149,11 @@ _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
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=""
return 0
fi
HOSTUP_ZONE_DOMAIN=""
@ -221,16 +162,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
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
return 0
fi
_debug "hostup_forced_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
return 0
fi
HOSTUP_ZONE_ID=""
HOSTUP_ZONE_DOMAIN=""
fi
_domain_candidate="$(printf "%s" "${fulldomain%.}" | _lower_case)"
if ! _hostup_load_zones; then
return 1
fi
_domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)"
_debug "hostup_initial_candidate" "$_domain_candidate"
while [ -n "$_domain_candidate" ]; do
@ -299,11 +240,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="$(_hostup_json_extract "name" "$_hostup_response")"
zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')"
if [ -n "$zonedomain" ]; then
HOSTUP_ZONE_DOMAIN="$zonedomain"
return 0
@ -313,7 +254,7 @@ _hostup_fetch_zone_details() {
}
_hostup_load_zones() {
if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then
if ! _hostup_rest "GET" "/dns/zones" ""; then
return 1
fi
@ -322,9 +263,9 @@ _hostup_load_zones() {
while IFS= read -r line; do
case "$line" in
*'"id"'*'"name"'*)
zone_id="$(_hostup_json_extract "id" "$line")"
zone_domain="$(_hostup_json_extract "name" "$line")"
*'"domain_id"'*'"domain"'*)
zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")"
zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")"
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id}
"
@ -349,30 +290,9 @@ _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
domain_lower="$(printf "%s" "$domain" | _lower_case)"
if [ "$domain_lower" = "$lookup_domain" ]; then
if [ "$domain" = "$lookup_domain" ]; then
_lookup_zone_domain="$domain"
_lookup_zone_id="$zone_id"
HOSTUP_ZONE_DOMAIN="$domain"
@ -387,50 +307,50 @@ EOF
}
_hostup_find_record() {
_hostup_find_zone_id="$1"
_hostup_find_fqdn="$2"
_hostup_find_txtvalue="$3"
zone_id="$1"
fqdn="$2"
txtvalue="$3"
_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
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
return 1
fi
HOSTUP_RECORD_ID=""
_hostup_find_records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
while IFS= read -r _hostup_find_line; do
while IFS= read -r line; do
# Normalize line to make TXT value matching reliable
_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')"
line_clean="$(printf "%s" "$line" | tr -d '\r\n')"
line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')"
_hostup_find_record_type="$(_hostup_json_extract "type" "$_hostup_find_line_clean")"
[ "$_hostup_find_record_type" != "TXT" ] && continue
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_name_value="$(_hostup_json_extract "name" "$_hostup_find_line_clean")"
_hostup_find_record_value="$(_hostup_json_extract "value" "$_hostup_find_line_value_clean")"
_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
_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
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"
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
fi
;;
esac
done <<EOF
$_hostup_find_records
$records
EOF
return 1
@ -441,22 +361,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
@ -471,56 +391,58 @@ _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"
txtvalue="$3"
key="$(_hostup_record_key "$zone_id" "$domain" "$txtvalue")"
key="$(_hostup_record_key "$zone_id" "$domain")"
_clearaccountconf_mutable "HOSTUP_RECORD_$key"
legacy_key="$(_hostup_record_key "$zone_id" "$domain")"
if [ "$legacy_key" != "$key" ]; then
_clearaccountconf_mutable "HOSTUP_RECORD_$legacy_key"
}
_hostup_extract_record_id() {
record_id="$(_hostup_json_extract "id" "$1")"
if [ -n "$record_id" ]; then
printf "%s" "$record_id"
return 0
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
if ! _hostup_rest "DELETE" "/dns/zones/$zone_id/records/$record_id" ""; then
return 1
fi
if ! _contains "$_hostup_response" '"success":true'; 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"
@ -529,7 +451,8 @@ _hostup_rest() {
_hostup_response=""
export _H1="Authorization: Bearer $HOSTUP_API_KEY"
export _H2="Accept: application/json"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
if [ "$method" = "GET" ]; then
_hostup_response="$(_get "$HOSTUP_API_BASE$route")"
@ -541,6 +464,7 @@ _hostup_rest() {
unset _H1
unset _H2
unset _H3
if [ "$ret" != "0" ]; then
_err "HTTP request failed for $route"
@ -554,23 +478,23 @@ _hostup_rest() {
case "$http_status" in
200 | 201 | 204) return 0 ;;
401)
_hostup_problem_error || _err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
_err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
return 1
;;
403)
_hostup_problem_error || _err "HostUp API returned 403 Forbidden. The API key lacks required DNS/domain scopes."
_err "HostUp API returned 403 Forbidden. The API key lacks required DNS scopes."
return 1
;;
404)
_hostup_problem_error || _err "HostUp API returned 404 Not Found for $route"
_err "HostUp API returned 404 Not Found for $route"
return 1
;;
429)
_hostup_problem_error || _err "HostUp API rate limit exceeded. Please retry later."
_err "HostUp API rate limit exceeded. Please retry later."
return 1
;;
*)
_hostup_problem_error || _err "HostUp API request failed with status $http_status"
_err "HostUp API request failed with status $http_status"
return 1
;;
esac

View file

@ -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,7 +30,6 @@ 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
@ -38,11 +37,8 @@ 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}" "${_huaweicloud_region}")"
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
if [ -z "${token}" ]; then # Check token
_err "dns_api(dns_huaweicloud): Error getting token."
return 1
@ -69,9 +65,6 @@ 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
}
@ -88,7 +81,6 @@ 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
@ -96,11 +88,8 @@ 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}" "${_huaweicloud_region}")"
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
if [ -z "${token}" ]; then # Check token
_err "dns_api(dns_huaweicloud): Error getting token."
return 1
@ -309,7 +298,6 @@ _get_token() {
_username=$1
_password=$2
_domain_name=$3
_region_name=$4
_debug "Getting Token"
body="{
@ -330,7 +318,7 @@ _get_token() {
},
\"scope\": {
\"project\": {
\"name\": \"${_region_name}\"
\"name\": \"ap-southeast-1\"
}
}
}

View file

@ -117,7 +117,7 @@ dns_infoblox_uddi_rm() {
return 0
fi
record_id=$(echo "$response" | _egrep_o '"id":[ ]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
@ -178,7 +178,7 @@ _get_root() {
# Check if response contains results (even if empty)
if _contains "$response" '"results"'; then
# Extract zone ID - must match the pattern dns/auth_zone/...
zone_id=$(echo "$response" | _egrep_o '"id":[ ]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
if [ -n "$zone_id" ]; then
# Found the zone
_domain="$h"

View file

@ -85,10 +85,12 @@ dns_infomaniak_add() {
# API call
response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records")
if _contains "$response" '"result":"success"'; then
_info "Record added"
_debug "response: $response"
return 0
if [ -n "$response" ]; then
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
_info "Record added"
_debug "response: $response"
return 0
fi
fi
_err "Could not create record."
_debug "Response: $response"
@ -129,7 +131,7 @@ dns_infomaniak_rm() {
fi
export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN"
export _H2="Content-Type: application/json"
export _H2="ContentType: application/json"
fulldomain=$1
txtvalue=$2
@ -167,10 +169,11 @@ dns_infomaniak_rm() {
# API call
response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE)
if _contains "$response" '"result":"success"'; then
_info "Record deleted"
_debug "response: $response"
return 0
if [ -n "$response" ]; then
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
_info "Record deleted"
return 0
fi
fi
_err "Could not delete record."
_debug "Response: $response"
@ -182,11 +185,7 @@ dns_infomaniak_rm() {
_get_zone() {
domain="$1"
# Whatever the domain is, you can get the fqdn with the following.
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
# shellcheck disable=SC1004
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/')
echo "${response%%\"*}"
}

View file

@ -307,32 +307,11 @@ _get_root() {
return 1
fi
# 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
if _contains "$response" "$h"; 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

View file

@ -16,7 +16,7 @@ IONOS_TXT_TTL=60 # minimum accepted by API
IONOS_TXT_PRIO=10
dns_ionos_add() {
fulldomain="$(echo "$1" | _lower_case)"
fulldomain=$1
txtvalue=$2
if ! _ionos_init; then
@ -34,7 +34,7 @@ dns_ionos_add() {
}
dns_ionos_rm() {
fulldomain="$(echo "$1" | _lower_case)"
fulldomain=$1
txtvalue=$2
if ! _ionos_init; then
@ -146,7 +146,7 @@ _ionos_rest() {
if [ "$method" != "GET" ]; then
export _H2="Accept: application/json"
export _H3=
export _H3="Content-Type: application/json"
_response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")"
else

View file

@ -1,91 +0,0 @@
#!/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
}

View file

@ -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 | cut -d '[' -f 1 | tr -d '{}')
client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}')
_debug "Client ID: '${client_id}'"
case "${client_id}" in
'' | *[!0-9]*)

View file

@ -35,28 +35,9 @@ 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$_joker_value_params"; then
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value=$txtvalue"; then
if _startswith "$response" "OK"; then
_savedomainconf "$_joker_conf_key" "$_joker_values"
_info "Added, OK"
return 0
fi
@ -78,36 +59,10 @@ 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 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
# 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
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

View file

@ -5,8 +5,7 @@ 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 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.
KNOT_KEY File path to TSIG key
'
# See also dns_nsupdate.sh
@ -22,9 +21,6 @@ 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."
@ -88,13 +84,6 @@ 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)

View file

@ -1,197 +0,0 @@
#!/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
}

View file

@ -1,197 +0,0 @@
#!/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
}

189
dnsapi/dns_linode.sh Executable file
View file

@ -0,0 +1,189 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_linode_info='Linode.com (Old)
Deprecated. Use dns_linode_v4
Site: Linode.com
Options:
LINODE_API_KEY API Key
Author: Philipp Grosswiler <philipp.grosswiler@swiss-design.net>
'
LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action="
######## Public functions #####################
#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_linode_add() {
fulldomain="${1}"
txtvalue="${2}"
if ! _Linode_API; then
return 1
fi
_info "Using Linode"
_debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Domain does not exist."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue"
if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then
_resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
_debug _resource_id "$_resource_id"
if [ -z "$_resource_id" ]; then
_err "Error adding the domain resource."
return 1
fi
_info "Domain resource successfully added."
return 0
fi
return 1
}
#Usage: dns_linode_rm _acme-challenge.www.domain.com
dns_linode_rm() {
fulldomain="${1}"
if ! _Linode_API; then
return 1
fi
_info "Using Linode"
_debug "Calling: dns_linode_rm() '${fulldomain}'"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Domain does not exist."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_parameters="&DomainID=$_domain_id"
if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then
response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")"
if [ "$resource" ]; then
_resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
if [ "$_resource_id" ]; then
_debug _resource_id "$_resource_id"
_parameters="&DomainID=$_domain_id&ResourceID=$_resource_id"
if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then
_resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1)
_debug _resource_id "$_resource_id"
if [ -z "$_resource_id" ]; then
_err "Error deleting the domain resource."
return 1
fi
_info "Domain resource successfully deleted."
return 0
fi
fi
return 1
fi
return 0
fi
return 1
}
#################### Private functions below ##################################
_Linode_API() {
if [ -z "$LINODE_API_KEY" ]; then
LINODE_API_KEY=""
_err "You didn't specify the Linode API key yet."
_err "Please create your key and try again."
return 1
fi
_saveaccountconf LINODE_API_KEY "$LINODE_API_KEY"
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=12345
_get_root() {
domain=$1
i=2
p=1
if _rest GET "domain.list"; then
response="$(echo "$response" | tr -d "\n" | tr '{' "|" | sed 's/|/&{/g' | tr "|" "\n")"
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
return 1
fi
hostedzone="$(echo "$response" | _egrep_o "{.*\"DOMAIN\":\s*\"$h\".*}")"
if [ "$hostedzone" ]; then
_domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"DOMAINID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ )
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
return 1
fi
p=$i
i=$(_math "$i" + 1)
done
fi
return 1
}
#method method action data
_rest() {
mtd="$1"
ep="$2"
data="$3"
_debug mtd "$mtd"
_debug ep "$ep"
export _H1="Accept: application/json"
export _H2="Content-Type: application/json"
if [ "$mtd" != "GET" ]; then
# both POST and DELETE.
_debug data "$data"
response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")"
else
response="$(_get "$LINODE_API_URL$ep$data")"
fi
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

View file

@ -140,7 +140,7 @@ _me_rest() {
data="$3"
_debug "$ep"
cdate=$(LC_ALL=C date -u +"%a, %d %b %Y %T %Z")
cdate=$(LANG=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"

View file

@ -1,167 +0,0 @@
#!/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
}

View file

@ -186,7 +186,7 @@ _oauth2() {
_oauth2_std() {
# HTTP Basic Authentication
_H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)"
_H2="Accept: application/json"
_H2="Accepts: application/json"
export _H1 _H2
body="grant_type=client_credentials"
@ -210,7 +210,7 @@ _oauth2_std() {
}
_oauth2_github() {
_H1="Accept: application/json"
_H1="Accepts: 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="Accept: application/json"
_H2="Accepts: 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