Compare commits
@@ -0,0 +1,50 @@
|
||||
name: Apache
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Apache.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Apache.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Apache:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TestingDomain: example.com
|
||||
TEST_ACME_Server: https://localhost:14000/dir
|
||||
HTTPS_INSECURE: 1
|
||||
TEST_LOCAL: 1
|
||||
TEST_CA: "Pebble Intermediate CA"
|
||||
TEST_APACHE: 1
|
||||
CASE: le_test_apache
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat apache2
|
||||
- name: Run Pebble
|
||||
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||
- name: Set up Pebble
|
||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||
- name: Set up Apache
|
||||
# Apache serves on 5002, which is the HTTP-01 validation port in
|
||||
# Pebble's default config; acme.sh appends the challenge Alias to
|
||||
# the main config itself
|
||||
run: |
|
||||
echo "Listen 5002" | sudo tee /etc/apache2/ports.conf
|
||||
sudo sed -i "s/\*:80/*:5002/" /etc/apache2/sites-available/000-default.conf
|
||||
sudo apache2ctl configtest
|
||||
sudo systemctl restart apache2
|
||||
curl -s -o /dev/null -w "%{http_code}" -H "Host: example.com" http://127.0.0.1:5002/ | grep -E "200|403|404"
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
run: cd ../acmetest && sudo --preserve-env ./letest.sh
|
||||
+128
-7
@@ -26,9 +26,9 @@ jobs:
|
||||
id: step_one
|
||||
run: |
|
||||
if [ "${{secrets.TokenName1}}" ] ; then
|
||||
echo "::set-output name=hasToken::true"
|
||||
echo "hasToken=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::set-output name=hasToken::false"
|
||||
echo "hasToken=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Check the value
|
||||
run: echo ${{ steps.step_one.outputs.hasToken }}
|
||||
@@ -116,7 +116,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: brew install socat
|
||||
run: |
|
||||
brew untap aws/tap || true
|
||||
brew install socat
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
@@ -176,9 +178,14 @@ jobs:
|
||||
C:\tools\cygwin\cygwinsetup.exe -qgnNdO -R C:/tools/cygwin -s https://mirrors.kernel.org/sourceware/cygwin/ -P socat,curl,cron,unzip,git
|
||||
shell: cmd
|
||||
- name: Set ENV
|
||||
shell: cmd
|
||||
shell: bash
|
||||
run: |
|
||||
echo PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin >> %GITHUB_ENV%
|
||||
echo 'PATH=C:\tools\cygwin\bin;C:\tools\cygwin\usr\bin' >> "$GITHUB_ENV"
|
||||
# cygwin git sees the runner workspace as owned by another user and
|
||||
# fails with "dubious ownership" (exit 128) in the checkout post step
|
||||
echo 'GIT_CONFIG_COUNT=1' >> "$GITHUB_ENV"
|
||||
echo 'GIT_CONFIG_KEY_0=safe.directory' >> "$GITHUB_ENV"
|
||||
echo 'GIT_CONFIG_VALUE_0=*' >> "$GITHUB_ENV"
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
@@ -260,9 +267,67 @@ jobs:
|
||||
|
||||
|
||||
|
||||
OpenBSD:
|
||||
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@v6
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- uses: vmactions/ghostbsd-vm@v1
|
||||
with:
|
||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||
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"
|
||||
|
||||
|
||||
|
||||
OpenBSD:
|
||||
runs-on: ubuntu-latest
|
||||
needs: GhostBSD
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
@@ -661,9 +726,65 @@ jobs:
|
||||
|
||||
|
||||
|
||||
Haiku:
|
||||
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@v6
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- uses: vmactions/tribblix-vm@v1
|
||||
with:
|
||||
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||
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
|
||||
env:
|
||||
TEST_DNS : ${{ secrets.TEST_DNS }}
|
||||
TestingDomain: ${{ secrets.TestingDomain }}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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 }}
|
||||
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"
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Nginx
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Nginx.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '*.sh'
|
||||
- '.github/workflows/Nginx.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
Nginx:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TestingDomain: example.com
|
||||
TEST_ACME_Server: https://localhost:14000/dir
|
||||
HTTPS_INSECURE: 1
|
||||
TEST_LOCAL: 1
|
||||
TEST_CA: "Pebble Intermediate CA"
|
||||
TEST_NGINX: 1
|
||||
CASE: le_test_nginx
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y socat nginx
|
||||
- name: Run Pebble
|
||||
run: cd .. && curl https://raw.githubusercontent.com/letsencrypt/pebble/master/docker-compose.yml >docker-compose.yml && docker compose up -d
|
||||
- name: Set up Pebble
|
||||
run: curl --request POST --data '{"ip":"10.30.50.1"}' http://localhost:8055/set-default-ipv4
|
||||
- name: Set up nginx
|
||||
# a backend on 8081 plus a site with an aaPanel/BT style
|
||||
# "location ^~ /" proxy block that shadows plain regex locations
|
||||
# (regression for #6125); the site listens on 5002, which is the
|
||||
# HTTP-01 validation port in Pebble's default config
|
||||
run: |
|
||||
sudo tee /etc/nginx/sites-available/default >/dev/null <<'EOF'
|
||||
server {
|
||||
listen 127.0.0.1:8081;
|
||||
location / {
|
||||
default_type text/plain;
|
||||
return 200 "backend";
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 5002 default_server;
|
||||
server_name example.com;
|
||||
location ^~ / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $http_host;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
sudo nginx -t
|
||||
sudo systemctl restart nginx
|
||||
curl -s -H "Host: example.com" http://127.0.0.1:5002/ | grep backend
|
||||
- name: Clone acmetest
|
||||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||
- name: Run acmetest
|
||||
run: cd ../acmetest && sudo --preserve-env ./letest.sh
|
||||
@@ -0,0 +1,79 @@
|
||||
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 }}
|
||||
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"
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Blacklist Command
|
||||
|
||||
# An issue titled "blacklist: <login-or-email>" opened by the maintainer
|
||||
# or a write-access member adds that identity to the Blacklist wiki page
|
||||
# (see wiki-guard.yml) and closes the issue. The wiki-monitor notification
|
||||
# embeds a prefilled link that opens such an issue in one click.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
# Share the wiki-guard concurrency group so we never push to the wiki
|
||||
# at the same time as the guard.
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
blacklist:
|
||||
# Upstream only: forks have no <fork>.wiki repository to push to.
|
||||
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'blacklist:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check authorization
|
||||
id: auth
|
||||
run: |
|
||||
assoc="${{ github.event.issue.author_association }}"
|
||||
case "$assoc" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "issue author is not authorized ($assoc); ignoring"
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout wiki repository
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
|
||||
- name: Add the identity to the blacklist page
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
id: add
|
||||
env:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
target="$(printf '%s' "$TITLE" \
|
||||
| sed 's/^blacklist:[[:space:]]*//; s/^@//; s/[[:space:]].*$//' \
|
||||
| tr 'A-Z' 'a-z')"
|
||||
case "$target" in
|
||||
''|*[!a-z0-9._+@-]*)
|
||||
echo "invalid target: '$target'"
|
||||
echo "result=invalid" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
cd wiki
|
||||
if [ ! -e Blacklist.md ]; then
|
||||
echo "result=nopage" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if grep -Fxiq -- "- $target" Blacklist.md; then
|
||||
echo "result=already" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$(tail -c1 Blacklist.md)" ]; then
|
||||
echo >> Blacklist.md
|
||||
fi
|
||||
printf -- '- %s\n' "$target" >> Blacklist.md
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add Blacklist.md
|
||||
git commit -m "blacklist $target (requested in #${{ github.event.issue.number }})"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
echo "result=added" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Reply and close
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
RESULT: ${{ steps.add.outputs.result }}
|
||||
TARGET: ${{ steps.add.outputs.target }}
|
||||
with:
|
||||
script: |
|
||||
const result = process.env.RESULT;
|
||||
const target = process.env.TARGET;
|
||||
const messages = {
|
||||
added: `\`${target}\` has been added to the [Blacklist](https://github.com/${context.repo.owner}/${context.repo.repo}/wiki/Blacklist). The wiki guard will revert their recent wiki changes on its next run.`,
|
||||
already: `\`${target}\` is already on the blacklist.`,
|
||||
invalid: "Could not parse a valid login or email from the issue title.",
|
||||
nopage: "The Blacklist wiki page does not exist."
|
||||
};
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: messages[result] || "No action taken."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
state_reason: result === "added" ? "completed" : "not_planned"
|
||||
});
|
||||
@@ -41,23 +41,29 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: CheckToken
|
||||
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${DOCKER_IMAGE}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: login to docker hub
|
||||
run: |
|
||||
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
- name: login to ghcr
|
||||
run: |
|
||||
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
- name: build and push the image
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
@@ -73,6 +79,8 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
|
||||
|
||||
DOCKER_LABELS=()
|
||||
while read -r label; do
|
||||
DOCKER_LABELS+=(--label "${label}")
|
||||
@@ -84,3 +92,9 @@ jobs:
|
||||
--output "type=image,push=true" \
|
||||
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
|
||||
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
|
||||
- name: mirror the image to ghcr (best-effort)
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
|
||||
${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
|
||||
|| echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
|
||||
|
||||
+115
-5
@@ -2,18 +2,128 @@ name: "Update issues"
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
const item = context.payload.issue || context.payload.pull_request;
|
||||
|
||||
// Close on sight anything opened by a user on the wiki Blacklist
|
||||
// page (maintained by the Wiki Guard workflow).
|
||||
let blacklist = [];
|
||||
try {
|
||||
const res = await fetch(`https://raw.githubusercontent.com/wiki/${context.repo.owner}/${context.repo.repo}/Blacklist.md`);
|
||||
if (res.ok) {
|
||||
blacklist = (await res.text()).split("\n")
|
||||
.filter(l => l.startsWith("- "))
|
||||
.map(l => l.slice(2).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
core.warning(`Failed to fetch the blacklist: ${e}`);
|
||||
}
|
||||
// A comment on a closed tracking issue reopens it (the standard
|
||||
// closing note promises this). Bots, blacklisted users and the
|
||||
// maintainer's own comments don't reopen.
|
||||
if (context.eventName === "issue_comment") {
|
||||
const issue = context.payload.issue;
|
||||
const commenter = context.payload.comment.user;
|
||||
if (issue.pull_request || issue.state !== "closed") {
|
||||
return;
|
||||
}
|
||||
if (!/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
|
||||
return;
|
||||
}
|
||||
if (commenter.type === "Bot" ||
|
||||
commenter.login.toLowerCase() === "neilpang" ||
|
||||
blacklist.includes(commenter.login.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "open"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (blacklist.includes(item.user.login.toLowerCase())) {
|
||||
if (context.payload.pull_request) {
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: item.number,
|
||||
state: "closed"
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: item.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.payload.pull_request) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issue = context.payload.issue;
|
||||
if (issue.title.startsWith("blacklist:") || issue.title.startsWith("revert:")) {
|
||||
// Handled by the Blacklist / Revert Command workflows.
|
||||
return;
|
||||
}
|
||||
if (/^report\s+(bugs?|issues?)\b/i.test(issue.title)) {
|
||||
// Tracking issue for a third-party dns/deploy/notify api:
|
||||
// no upgrade boilerplate; assign it to the opener, label it,
|
||||
// then close it right away to keep the issue list clean. Any
|
||||
// later comment reopens it (see the issue_comment handler).
|
||||
await github.rest.issues.addAssignees({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
assignees: [issue.user.login]
|
||||
});
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["3rd party api"]
|
||||
});
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: "Closing this tracking issue for now to keep the issue list clean. It remains the place to report problems with this provider -- if you hit a bug, comment here and the issue will be reopened."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "completed"
|
||||
});
|
||||
return;
|
||||
}
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you."
|
||||
|
||||
body: "Please upgrade to the latest code and try again first. Maybe it's already fixed. ```acme.sh --upgrade``` If it's still not working, please provide the log with `--debug 2`, otherwise, nobody can help you. Before posting the log, review it and REDACT any secrets: private keys (`-----BEGIN ... PRIVATE KEY-----` blocks), API tokens and passwords."
|
||||
})
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- uses: actions/github-script@v6
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
name: Revert Command
|
||||
|
||||
# An issue titled "revert: <wiki-commit-sha>" opened by the maintainer or
|
||||
# a write-access member reverts that commit in the wiki repository and
|
||||
# closes the issue. The wiki-monitor notification embeds a prefilled link
|
||||
# that opens such an issue in one click.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
# Share the wiki-guard concurrency group so we never push to the wiki
|
||||
# at the same time as the guard.
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
# Upstream only: forks have no <fork>.wiki repository to push to.
|
||||
if: github.repository == 'acmesh-official/acme.sh' && startsWith(github.event.issue.title, 'revert:')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check authorization
|
||||
id: auth
|
||||
run: |
|
||||
assoc="${{ github.event.issue.author_association }}"
|
||||
case "$assoc" in
|
||||
OWNER|MEMBER|COLLABORATOR)
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
*)
|
||||
echo "issue author is not authorized ($assoc); ignoring"
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Checkout wiki repository
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Revert the wiki commit
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
id: revert
|
||||
env:
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
run: |
|
||||
target="$(printf '%s' "$TITLE" \
|
||||
| sed 's/^revert:[[:space:]]*//; s/[[:space:]].*$//' \
|
||||
| tr 'A-Z' 'a-z')"
|
||||
case "$target" in
|
||||
*[!0-9a-f]*|"")
|
||||
echo "invalid commit sha: '$target'"
|
||||
echo "result=invalid" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
echo "target=$target" >> "$GITHUB_OUTPUT"
|
||||
cd wiki
|
||||
if ! git cat-file -e "$target^{commit}" 2>/dev/null; then
|
||||
echo "result=notfound" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git revert --no-edit "$target"; then
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
echo "result=reverted" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git revert --abort || true
|
||||
echo "result=conflict" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Reply and close
|
||||
if: steps.auth.outputs.ok == 'true'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
RESULT: ${{ steps.revert.outputs.result }}
|
||||
TARGET: ${{ steps.revert.outputs.target }}
|
||||
with:
|
||||
script: |
|
||||
const result = process.env.RESULT;
|
||||
const target = process.env.TARGET;
|
||||
const messages = {
|
||||
reverted: `Wiki commit \`${target}\` has been reverted.`,
|
||||
conflict: `Reverting \`${target}\` conflicts with later edits; please revert manually from the page history.`,
|
||||
notfound: `Commit \`${target}\` was not found in the wiki repository.`,
|
||||
invalid: "Could not parse a commit sha from the issue title."
|
||||
};
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: messages[result] || "No action taken."
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
state_reason: result === "reverted" ? "completed" : "not_planned"
|
||||
});
|
||||
@@ -0,0 +1,325 @@
|
||||
name: Wiki Guard
|
||||
|
||||
# Rules enforced here:
|
||||
# - Only the maintainer and write-access members may delete or rename wiki
|
||||
# pages. Anyone else doing so gets blacklisted and the page restored to
|
||||
# its last good revision.
|
||||
# - Only the maintainer and write-access members may edit the Blacklist
|
||||
# wiki page. Anyone else touching it gets blacklisted and the page
|
||||
# reverted.
|
||||
# - Any wiki change made by a blacklisted identity is reverted.
|
||||
# A "good" revision is one authored by the maintainer, by this bot, or by
|
||||
# a non-blacklisted user -- restoring from the deleted commit's parent is
|
||||
# NOT safe, because vandals replace a page before destroying it and the
|
||||
# parent would launder their version into a bot commit.
|
||||
# The gollum event only fires on page create/update, never on deletion,
|
||||
# so violations are caught by polling the wiki git history.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/10 * * * *"
|
||||
gollum:
|
||||
# Piggyback on frequent repo activity, because the cron schedule is
|
||||
# best-effort and often delayed well beyond its interval.
|
||||
issues:
|
||||
types: [opened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: wiki-guard
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
guard:
|
||||
# Forks have no <fork>.wiki repository, so the checkout below would
|
||||
# fail there -- run only in the upstream repository.
|
||||
if: github.repository == 'acmesh-official/acme.sh'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Enforce wiki rules
|
||||
id: guard
|
||||
env:
|
||||
# WIKI_GUARD_TOKEN: a PAT with read:org, needed to enumerate
|
||||
# members whose write access comes via the organization -- the
|
||||
# repo-scoped GITHUB_TOKEN only sees direct collaborators.
|
||||
GH_TOKEN: ${{ secrets.WIKI_GUARD_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# Logins with write (push) access to the repository, including
|
||||
# organization members -- they may delete/rename pages and edit
|
||||
# the blacklist just like the maintainer. If the API call fails,
|
||||
# the list stays empty and enforcement falls back to
|
||||
# maintainer-only, which is the safe direction.
|
||||
gh api "repos/${GITHUB_REPOSITORY}/collaborators?per_page=100" --paginate \
|
||||
-q '.[] | select(.permissions.push) | .login' 2>/dev/null \
|
||||
| tr 'A-Z' 'a-z' | sort -u > writers.txt || true
|
||||
echo "write-access members loaded: $(wc -l < writers.txt)"
|
||||
cd wiki
|
||||
git config core.quotePath false
|
||||
|
||||
# Any author email under this domain is the maintainer.
|
||||
OWNER_DOMAIN="neilpang.com"
|
||||
# Our own enforcement commits.
|
||||
BOT_EMAIL="41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BL_PAGE="Blacklist.md"
|
||||
# Rolling window; the cron runs every 10 minutes, so 7 days gives
|
||||
# ample overlap without re-judging old changes the maintainer
|
||||
# already accepted.
|
||||
WINDOW="7 days ago"
|
||||
|
||||
: > ../actions.txt
|
||||
: > ../bl_new.txt
|
||||
|
||||
is_owner() {
|
||||
case "$1" in
|
||||
*@"$OWNER_DOMAIN") return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
is_bot() {
|
||||
[ "$1" = "$BOT_EMAIL" ]
|
||||
}
|
||||
|
||||
author_email() {
|
||||
git show -s --format=%ae "$1" | tr 'A-Z' 'a-z'
|
||||
}
|
||||
|
||||
# Identity of a commit author: the GitHub login when the email is a
|
||||
# users.noreply.github.com address, otherwise the email itself.
|
||||
identity_of() {
|
||||
case "$1" in
|
||||
*+*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/^[^+]*+//; s/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*@users.noreply.github.com)
|
||||
printf '%s\n' "$1" | sed 's/@users\.noreply\.github\.com$//'
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_blacklisted() {
|
||||
grep -Fxq "$1" ../bl_all.txt
|
||||
}
|
||||
|
||||
# Trusted committers: the maintainer (by email domain), this bot,
|
||||
# and anyone whose GitHub login has write access to the repo.
|
||||
is_trusted() {
|
||||
if is_owner "$1" || is_bot "$1"; then
|
||||
return 0
|
||||
fi
|
||||
grep -Fxq "$(identity_of "$1")" ../writers.txt
|
||||
}
|
||||
|
||||
# Newest commit on file $1 authored by a non-blacklisted user.
|
||||
last_good_for() {
|
||||
for g in $(git log --format=%H --no-renames -- "$1"); do
|
||||
gae="$(author_email "$g")"
|
||||
if is_trusted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
gid="$(identity_of "$gae")"
|
||||
if ! is_blacklisted "$gid" && ! is_blacklisted "$gae"; then
|
||||
printf '%s\n' "$g"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -e "$BL_PAGE" ]; then
|
||||
page_existed=1
|
||||
else
|
||||
page_existed=""
|
||||
fi
|
||||
|
||||
# ---- 1. Last good version of the blacklist page: the newest
|
||||
# revision authored by the maintainer or by this bot. Everything
|
||||
# else on that page is tampering and is discarded.
|
||||
bl_good_commit=""
|
||||
for c in $(git log --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
bl_good_commit="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$bl_good_commit" ] && git cat-file -e "$bl_good_commit:$BL_PAGE" 2>/dev/null; then
|
||||
git show "$bl_good_commit:$BL_PAGE" > ../bl_page.txt
|
||||
else
|
||||
{
|
||||
echo "# Blacklist"
|
||||
echo ""
|
||||
echo "Users listed below violated the wiki rules (deleted or renamed"
|
||||
echo "pages, or tampered with this page). Their new issues and pull"
|
||||
echo "requests are closed on sight and their wiki edits are reverted"
|
||||
echo "automatically. Only the maintainer and write-access members"
|
||||
echo "may edit this page."
|
||||
echo ""
|
||||
echo "To pardon a user while their violation is still inside the"
|
||||
echo "scan window, replace their entry with: pardon: username"
|
||||
echo ""
|
||||
} > ../bl_page.txt
|
||||
fi
|
||||
sed -n 's/^- *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_good.txt
|
||||
sed -n 's/^[Pp]ardon: *//p' ../bl_page.txt | tr -d '\r' | tr 'A-Z' 'a-z' | sort -u > ../bl_pardon.txt
|
||||
|
||||
bl_add() {
|
||||
if grep -Fxq "$1" ../bl_pardon.txt; then
|
||||
return 0
|
||||
fi
|
||||
if ! grep -Fxq "$1" ../bl_good.txt && ! grep -Fxq "$1" ../bl_new.txt; then
|
||||
printf '%s\n' "$1" >> ../bl_new.txt
|
||||
printf '%s\n' "- blacklisted \`$1\`: $2" >> ../actions.txt
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- 2. Blacklist everyone who deleted or renamed a page.
|
||||
# --no-renames makes a rename count as a deletion of the old path.
|
||||
for c in $(git log --since="$WINDOW" --diff-filter=D --no-renames --format=%H); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "deleted or renamed pages in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
# ---- 3. Blacklist everyone else who touched the blacklist page.
|
||||
# The revert of their tampering falls out of steps 5 and 6.
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames -- "$BL_PAGE"); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
an="$(git show -s --format=%an "$c")"
|
||||
bl_add "$(identity_of "$ae")" "tampered with \`$BL_PAGE\` in $c ($an <$ae>)"
|
||||
done
|
||||
|
||||
sort -u ../bl_new.txt > ../bl_new_u.txt
|
||||
cat ../bl_good.txt ../bl_new_u.txt | sort -u > ../bl_all.txt
|
||||
|
||||
# ---- 4. Restore pages that are currently missing because a
|
||||
# non-maintainer deleted them, using the last good revision.
|
||||
git log --since="$WINDOW" --diff-filter=D --no-renames --name-only --format= \
|
||||
| sort -u \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ] || [ -e "$f" ]; then
|
||||
continue
|
||||
fi
|
||||
del="$(git log -1 --diff-filter=D --no-renames --format=%H -- "$f")"
|
||||
if [ -z "$del" ]; then
|
||||
continue
|
||||
fi
|
||||
ae="$(author_email "$del")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- restored \`$f\` (deleted in $del) from its last good revision $good" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
|
||||
# ---- 5. Revert every recent change made by a blacklisted
|
||||
# identity: each touched file goes back to its newest revision
|
||||
# authored by a non-blacklisted user; a file that has no such
|
||||
# revision (they created it) is removed.
|
||||
if [ -s ../bl_all.txt ]; then
|
||||
for c in $(git log --since="$WINDOW" --format=%H --no-renames); do
|
||||
ae="$(author_email "$c")"
|
||||
if is_trusted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
id="$(identity_of "$ae")"
|
||||
if ! is_blacklisted "$id" && ! is_blacklisted "$ae"; then
|
||||
continue
|
||||
fi
|
||||
git show --name-only --no-renames --format= "$c" \
|
||||
| while IFS= read -r f; do
|
||||
if [ -z "$f" ] || [ "$f" = "$BL_PAGE" ]; then
|
||||
continue
|
||||
fi
|
||||
good="$(last_good_for "$f")"
|
||||
if [ -n "$good" ] && git cat-file -e "$good:$f" 2>/dev/null; then
|
||||
want="$(git rev-parse "$good:$f")"
|
||||
have="$(git hash-object -- "$f" 2>/dev/null || echo missing)"
|
||||
if [ "$want" != "$have" ]; then
|
||||
git checkout "$good" -- "$f"
|
||||
printf '%s\n' "- reverted \`$f\` to its last good revision $good (undoing change by \`$id\` in $c)" >> ../actions.txt
|
||||
fi
|
||||
elif [ -e "$f" ]; then
|
||||
git rm -q -- "$f"
|
||||
printf '%s\n' "- removed \`$f\` created by blacklisted \`$id\` in $c" >> ../actions.txt
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
# ---- 6. Regenerate the blacklist page: the last good text plus
|
||||
# any newly blacklisted identities. This both reverts tampering
|
||||
# and records new violators; manual edits by the maintainer are
|
||||
# preserved as the new good text.
|
||||
cp ../bl_page.txt ../bl_page_new.txt
|
||||
if [ -s ../bl_page_new.txt ] && [ -n "$(tail -c1 ../bl_page_new.txt)" ]; then
|
||||
echo >> ../bl_page_new.txt
|
||||
fi
|
||||
while IFS= read -r id; do
|
||||
if [ -n "$id" ] && ! grep -Fxiq -- "- $id" ../bl_page_new.txt; then
|
||||
printf -- '- %s\n' "$id" >> ../bl_page_new.txt
|
||||
fi
|
||||
done < ../bl_new_u.txt
|
||||
if ! cmp -s ../bl_page_new.txt "$BL_PAGE" 2>/dev/null; then
|
||||
cp ../bl_page_new.txt "$BL_PAGE"
|
||||
git add -- "$BL_PAGE"
|
||||
if [ -n "$page_existed" ] || [ -s ../bl_new_u.txt ]; then
|
||||
printf '%s\n' "- updated \`$BL_PAGE\`" >> ../actions.txt
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- 7. Commit, push, notify.
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "$BOT_EMAIL"
|
||||
git commit -m "wiki-guard: restore pages and enforce blacklist"
|
||||
git push origin HEAD || { git pull --rebase && git push origin HEAD; }
|
||||
fi
|
||||
if [ -s ../actions.txt ]; then
|
||||
{
|
||||
echo "The wiki guard handled the following rule violations:"
|
||||
echo ""
|
||||
cat ../actions.txt
|
||||
echo ""
|
||||
echo "Blacklist: https://github.com/${GITHUB_REPOSITORY}/wiki/Blacklist"
|
||||
echo "Wiki: https://github.com/${GITHUB_REPOSITORY}/wiki"
|
||||
} > ../guard-msg.txt
|
||||
echo "acted=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "No rule violations found."
|
||||
echo "acted=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
if: steps.guard.outputs.acted == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki guard: rule violations handled"
|
||||
content-filepath: ./guard-msg.txt
|
||||
assignees: Neilpang
|
||||
@@ -9,13 +9,14 @@ jobs:
|
||||
if: github.actor != 'neilpang'
|
||||
steps:
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
path: wiki
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate wiki change message
|
||||
id: msg
|
||||
run: |
|
||||
actor="${{ github.actor }}"
|
||||
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
|
||||
@@ -27,6 +28,17 @@ jobs:
|
||||
now="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
cd wiki
|
||||
# Skip notification when the change was authored by the
|
||||
# maintainer himself (any author email under neilpang.com),
|
||||
# e.g. a direct git push to the wiki repository.
|
||||
author_email=$(git show -s --format=%ae "$page_sha" 2>/dev/null | tr 'A-Z' 'a-z')
|
||||
case "$author_email" in
|
||||
*@neilpang.com)
|
||||
echo "Change authored by maintainer ($author_email); skipping notification."
|
||||
echo "notify=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
|
||||
if [ -n "$prev_sha" ]; then
|
||||
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
|
||||
@@ -41,17 +53,21 @@ jobs:
|
||||
echo "Time: $now"
|
||||
echo "Page: [$page_name]($page_url) (Action: $page_action)"
|
||||
echo "Comment: $page_summary"
|
||||
echo "[Click here to Revert](${page_url}/_history)"
|
||||
echo "[Click here to Revert](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=revert%3A+${page_sha}&body=Revert+wiki+commit+${page_sha}+by+@${actor}.)"
|
||||
echo ""
|
||||
echo "[Click here to Blacklist @$actor](https://github.com/${GITHUB_REPOSITORY}/issues/new?title=blacklist%3A+${actor}&body=Blacklist+@${actor},+requested+from+the+wiki+monitor.)"
|
||||
echo ""
|
||||
echo "----"
|
||||
echo "### diff:"
|
||||
echo "### diff:"
|
||||
echo '```diff'
|
||||
cat wiki.diff
|
||||
echo '```'
|
||||
} > wiki-change-msg.txt
|
||||
echo "notify=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create issue to notify Neilpang
|
||||
uses: peter-evans/create-issue-from-file@v5
|
||||
if: steps.msg.outputs.notify == 'true'
|
||||
uses: peter-evans/create-issue-from-file@v6
|
||||
with:
|
||||
title: "Wiki edited"
|
||||
content-filepath: ./wiki-change-msg.txt
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Contributing
|
||||
|
||||
1. Do NOT send pull request to `master` branch.
|
||||
Please send to `dev` branch instead.
|
||||
Any PR to `master` branch will NOT be merged.
|
||||
|
||||
2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
|
||||
You will NOT get any review without passing this guide. You also need to fix the CI errors.
|
||||
+2
-2
@@ -81,8 +81,8 @@ if [ \"\$1\" = \"daemon\" ]; then \n \
|
||||
echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \
|
||||
time=\$(date -u \"+%s\") \n \
|
||||
random_minute=\$((\$time % 60)) \n \
|
||||
random_hour=\$((\$time / 60 % 24)) \n \
|
||||
echo \"\$random_minute \$random_hour * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \
|
||||
random_hour=\$((\$time / 60 % 6)) \n \
|
||||
echo \"\$random_minute \$random_hour,\$((\$random_hour + 6)),\$((\$random_hour + 12)),\$((\$random_hour + 18)) * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \
|
||||
fi \n \
|
||||
echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \
|
||||
exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
<p align="center">
|
||||
<a href="https://zerossl.com/?fromacme.sh">
|
||||
<img src="https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05" alt="zerossl.com">
|
||||
</a>
|
||||
<a href="https://zerossl.com?utm_source=acme-sh">
|
||||
<picture>
|
||||
<!-- Dark mode -->
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://github.com/user-attachments/assets/1308516b-e0cc-496d-b5df-e3932423ead6" />
|
||||
<!-- Light mode -->
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043" />
|
||||
<!-- Fallback for environments without media queries -->
|
||||
<img
|
||||
alt="ZeroSSL"
|
||||
src="https://github.com/user-attachments/assets/4ba7a79e-8cc9-4d49-87fc-02d44fb7b043"
|
||||
height="auto" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">🔐 acme.sh</h1>
|
||||
@@ -17,8 +31,10 @@
|
||||
<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>
|
||||
</p>
|
||||
|
||||
@@ -112,6 +128,8 @@
|
||||
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|
||||
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|
||||
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|
||||
|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|
||||
|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|
||||
|
||||
|
||||
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
||||
@@ -146,6 +164,7 @@
|
||||
| 🌐 DNS mode | Use DNS TXT records |
|
||||
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
|
||||
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
|
||||
| 📌 [DNS persist mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode) | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) |
|
||||
|
||||
---
|
||||
|
||||
@@ -396,7 +415,53 @@ acme.sh --renew -d example.com
|
||||
|
||||
---
|
||||
|
||||
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
|
||||
### 🔟 Use DNS Persist Mode
|
||||
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/DNS-persist-mode
|
||||
|
||||
📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)
|
||||
|
||||
DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals.
|
||||
|
||||
#### 🪄 Step 1: Print the TXT record value
|
||||
|
||||
```bash
|
||||
acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] [--dns-persist-days 365]
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--server <ca>` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. |
|
||||
| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. |
|
||||
| `--dns-persist-ca-name <name>` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. |
|
||||
| `--dns-persist-days <N>` | Adds `persistUntil=<unix-timestamp>` to the record, set to N days from now. The CA will refuse new validations against the record after that time. Omit for a record with no expiry. |
|
||||
|
||||
You should get an output like:
|
||||
|
||||
```sh
|
||||
TXT persist domain:_validation-persist.example.com
|
||||
TXT persist value :"letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789"
|
||||
```
|
||||
|
||||
#### ✍️ Step 2: Add the TXT record to your DNS
|
||||
|
||||
Add the printed `TXT persist domain` / `TXT persist value` pair as a TXT record at your DNS provider, then wait for it to propagate.
|
||||
|
||||
#### 📜 Step 3: Issue the certificate
|
||||
|
||||
```bash
|
||||
acme.sh --issue -d example.com --dns-persist
|
||||
```
|
||||
|
||||
✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly.
|
||||
|
||||
> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA)
|
||||
|
||||
Just set the `keylength` to a valid, supported value.
|
||||
|
||||
@@ -427,7 +492,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com -
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣1️⃣ Issue Wildcard Certificates
|
||||
### 1️⃣2️⃣ Issue Wildcard Certificates
|
||||
|
||||
It's simple! Just give a wildcard domain as the `-d` parameter:
|
||||
|
||||
@@ -439,9 +504,9 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣2️⃣ How to Renew Certificates
|
||||
### 1️⃣3️⃣ How to Renew Certificates
|
||||
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days, **or earlier when the CA's ARI says so** (see below).
|
||||
|
||||
However, you can force a renewal:
|
||||
|
||||
@@ -455,9 +520,43 @@ acme.sh --renew -d example.com --force
|
||||
acme.sh --renew -d example.com --force --ecc
|
||||
```
|
||||
|
||||
#### 📡 ACME Renewal Information (ARI) — RFC 9773
|
||||
|
||||
📖 Wiki: https://github.com/acmesh-official/acme.sh/wiki/ARI
|
||||
|
||||
If the CA exposes a `renewalInfo` endpoint in its ACME directory (Let's Encrypt, ZeroSSL, etc.), `acme.sh` follows [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773.html) automatically — **no flag needed, no opt-in**:
|
||||
|
||||
| What | When | Why |
|
||||
|------|------|-----|
|
||||
| 🔍 **Polls `suggestedWindow`** | Every cron run, before deciding to skip | Lets the CA shift the renewal time forward in case of an incident (key compromise, mass revocation, etc.) |
|
||||
| 🎯 **Picks a random renewal time** inside the window | Right after a successful issuance/renewal | Disperses renewals across the network so all clients don't hit the CA at the same instant |
|
||||
| 🔗 **Sends `replaces=<certID>`** in `newOrder` | On renewal | Lets the CA correlate the new order with the certificate it supersedes (RFC 9773 §5) |
|
||||
| ↩️ **Retries without `replaces`** | If the CA rejects with `alreadyReplaced` or an ARI validation error | Robust against edge cases (e.g. switching CAs, retired issuers) |
|
||||
|
||||
**Renewal trigger logic:** the cert is renewed if **any one** of the following becomes true:
|
||||
|
||||
1. `--force` is given
|
||||
2. The CA's **ARI `suggestedWindow` has started**
|
||||
3. The cached `Le_NextRenewTime` has passed (default fallback for CAs without ARI)
|
||||
|
||||
You can see the resulting next renewal time (already ARI-picked when applicable) in:
|
||||
|
||||
```sh
|
||||
acme.sh --info -d example.com
|
||||
# Look for: Le_NextRenewTimeStr=...
|
||||
```
|
||||
|
||||
For the live ARI window the CA is currently advertising, run with `--debug 2`:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --debug 2 2>&1 | grep -i 'ARI suggestedWindow'
|
||||
```
|
||||
|
||||
> 💡 If your CA does not advertise `renewalInfo`, `acme.sh` falls back to the classic 30-day rule — no behavior change.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣3️⃣ How to Stop Certificate Renewal
|
||||
### 1️⃣4️⃣ How to Stop Certificate Renewal
|
||||
|
||||
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
|
||||
|
||||
@@ -471,7 +570,7 @@ The cert/key file is not removed from the disk.
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣4️⃣ How to Upgrade acme.sh
|
||||
### 1️⃣5️⃣ How to Upgrade acme.sh
|
||||
|
||||
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
|
||||
|
||||
@@ -495,25 +594,25 @@ acme.sh --upgrade --auto-upgrade 0
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣5️⃣ Issue a Certificate from an Existing CSR
|
||||
### 1️⃣6️⃣ Issue a Certificate from an Existing CSR
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣6️⃣ Send Notifications in Cronjob
|
||||
### 1️⃣7️⃣ Send Notifications in Cronjob
|
||||
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣7️⃣ Under the Hood
|
||||
### 1️⃣8️⃣ Under the Hood
|
||||
|
||||
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣8️⃣ Acknowledgments
|
||||
### 1️⃣9️⃣ Acknowledgments
|
||||
|
||||
| Project | Link |
|
||||
|---------|------|
|
||||
@@ -530,6 +629,8 @@ This project exists thanks to all the people who contribute.
|
||||
|
||||
<a href="https://github.com/acmesh-official/acme.sh/graphs/contributors"><img src="https://opencollective.com/acmesh/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
If you want to become a contributor make sure to read [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
### 💰 Financial Contributors
|
||||
|
||||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
|
||||
@@ -555,7 +656,7 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
---
|
||||
|
||||
### 1️⃣9️⃣ License & Others
|
||||
### 2️⃣0️⃣ License & Others
|
||||
|
||||
📄 **License:** GPLv3
|
||||
|
||||
@@ -565,7 +666,7 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣0️⃣ Donate
|
||||
### 2️⃣1️⃣ Donate
|
||||
|
||||
> 💝 Your donation makes **acme.sh** better!
|
||||
|
||||
@@ -577,7 +678,7 @@ Support this project with your organization. Your logo will show up here with a
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣1️⃣ About This Repository
|
||||
### 2️⃣2️⃣ About This Repository
|
||||
|
||||
> [!NOTE]
|
||||
> This repository is officially maintained by <strong>ZeroSSL</strong> as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
# Bash completion for acme.sh: https://github.com/acmesh-official/acme.sh
|
||||
#
|
||||
# "acme.sh --install" copies this file to the acme.sh home dir and wires
|
||||
# it into acme.sh.env, so the completion is loaded automatically in new
|
||||
# bash sessions after installation.
|
||||
#
|
||||
# To use it without installing acme.sh, source it from ~/.bashrc, or copy
|
||||
# it to /usr/share/bash-completion/completions/acme.sh
|
||||
#
|
||||
# Zsh users can load it with:
|
||||
# autoload -U +X bashcompinit && bashcompinit
|
||||
# . /path/to/acme.sh.completion
|
||||
|
||||
# This file may also be sourced by non-bash shells via acme.sh.env,
|
||||
# so silently do nothing if the "complete" builtin is not available.
|
||||
if ! command -v complete >/dev/null 2>&1; then
|
||||
return 0 2>/dev/null || exit 0
|
||||
fi
|
||||
|
||||
# Add each word of $1 that starts with $cur to COMPREPLY.
|
||||
# The words are read line by line, so that candidates like a wildcard
|
||||
# domain "*.example.com" are never glob-expanded against the cwd.
|
||||
_acme_sh_add_matches() {
|
||||
local _word
|
||||
while read -r _word; do
|
||||
[ -n "$_word" ] || continue
|
||||
case "$_word" in
|
||||
"$cur"*) COMPREPLY=("${COMPREPLY[@]}" "$_word") ;;
|
||||
esac
|
||||
done <<EOF
|
||||
$(printf '%s\n' "$1" | tr ' ' '\n')
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
_acme_sh_files() {
|
||||
local _file
|
||||
while IFS= read -r _file; do
|
||||
[ -n "$_file" ] || continue
|
||||
COMPREPLY=("${COMPREPLY[@]}" "$_file")
|
||||
done <<EOF
|
||||
$(compgen -f -- "$cur")
|
||||
EOF
|
||||
if command -v compopt >/dev/null 2>&1; then
|
||||
compopt -o filenames 2>/dev/null
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
_acme_sh_dirs() {
|
||||
local _dir
|
||||
while IFS= read -r _dir; do
|
||||
[ -n "$_dir" ] || continue
|
||||
COMPREPLY=("${COMPREPLY[@]}" "$_dir")
|
||||
done <<EOF
|
||||
$(compgen -d -- "$cur")
|
||||
EOF
|
||||
if command -v compopt >/dev/null 2>&1; then
|
||||
compopt -o filenames 2>/dev/null
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Complete the domains that already have a cert: every directory in the
|
||||
# config home that contains a "<domain>.conf" file ("_ecc" suffix stripped).
|
||||
_acme_sh_domains() {
|
||||
local _dir _name _domains=""
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
|
||||
for _dir in "$_acme_conf_home"/*/; do
|
||||
[ -d "$_dir" ] || continue
|
||||
_name="${_dir%/}"
|
||||
_name="${_name##*/}"
|
||||
_name="${_name%_ecc}"
|
||||
if [ -f "${_dir}${_name}.conf" ]; then
|
||||
case " $_domains " in
|
||||
*" $_name "*) ;;
|
||||
*) _domains="$_domains $_name" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
_acme_sh_add_matches "$_domains"
|
||||
}
|
||||
|
||||
# Complete hook names from a subfolder of the acme.sh home dir.
|
||||
# $1: subfolder (dnsapi/deploy/notify), $2: file name prefix or empty.
|
||||
_acme_sh_hooks() {
|
||||
local _file _hooks=""
|
||||
[ -n "${ZSH_VERSION:-}" ] && setopt localoptions nonomatch 2>/dev/null
|
||||
for _file in "$_acme_home/$1/$2"*.sh; do
|
||||
[ -f "$_file" ] || continue
|
||||
_file="${_file##*/}"
|
||||
_hooks="$_hooks ${_file%.sh}"
|
||||
done
|
||||
_acme_sh_add_matches "$_hooks"
|
||||
}
|
||||
|
||||
_acme_sh_completion() {
|
||||
local cur prev _acme_home _acme_conf_home
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev=""
|
||||
if [ "$COMP_CWORD" -gt 0 ]; then
|
||||
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||
fi
|
||||
_acme_home="${LE_WORKING_DIR:-$HOME/.acme.sh}"
|
||||
_acme_conf_home="${LE_CONFIG_HOME:-$_acme_home}"
|
||||
|
||||
# The first argument is the command.
|
||||
if [ "$COMP_CWORD" -eq 1 ]; then
|
||||
_acme_sh_add_matches "
|
||||
--help
|
||||
--version
|
||||
--install
|
||||
--install-online
|
||||
--uninstall
|
||||
--upgrade
|
||||
--issue
|
||||
--deploy
|
||||
--sign-csr
|
||||
--show-csr
|
||||
--install-cert
|
||||
--renew
|
||||
--renew-all
|
||||
--revoke
|
||||
--remove
|
||||
--list
|
||||
--list-profiles
|
||||
--info
|
||||
--to-pkcs12
|
||||
--to-pkcs8
|
||||
--create-account-key
|
||||
--create-domain-key
|
||||
--create-csr
|
||||
--deactivate
|
||||
--update-account
|
||||
--register-account
|
||||
--deactivate-account
|
||||
--make-dns-persist-value
|
||||
--install-cronjob
|
||||
--uninstall-cronjob
|
||||
--cron
|
||||
--set-notify
|
||||
--set-default-ca
|
||||
--set-default-chain
|
||||
"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Complete the value of the previous option.
|
||||
case "$prev" in
|
||||
-d | --domain | --challenge-alias | --domain-alias)
|
||||
_acme_sh_domains
|
||||
return 0
|
||||
;;
|
||||
--dns)
|
||||
# The dns hook argument is optional, keep completing options if the
|
||||
# current word already looks like one.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_hooks "dnsapi" "dns_"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--deploy-hook)
|
||||
_acme_sh_hooks "deploy" ""
|
||||
return 0
|
||||
;;
|
||||
--notify-hook)
|
||||
_acme_sh_hooks "notify" ""
|
||||
return 0
|
||||
;;
|
||||
--server)
|
||||
_acme_sh_add_matches "letsencrypt letsencrypt_test zerossl sslcom google google_test actalis"
|
||||
return 0
|
||||
;;
|
||||
-k | --keylength | -ak | --accountkeylength)
|
||||
_acme_sh_add_matches "2048 3072 4096 8192 ec-256 ec-384 ec-521"
|
||||
return 0
|
||||
;;
|
||||
--debug)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_add_matches "0 1 2 3"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--log)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--nginx)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--auto-upgrade | --always-force-new-domain-key)
|
||||
# Optional argument.
|
||||
case "$cur" in
|
||||
-*) ;;
|
||||
*)
|
||||
_acme_sh_add_matches "0 1"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
--log-level)
|
||||
_acme_sh_add_matches "1 2"
|
||||
return 0
|
||||
;;
|
||||
--syslog)
|
||||
_acme_sh_add_matches "0 3 6 7"
|
||||
return 0
|
||||
;;
|
||||
--notify-level)
|
||||
_acme_sh_add_matches "0 1 2 3"
|
||||
return 0
|
||||
;;
|
||||
--notify-mode)
|
||||
_acme_sh_add_matches "0 1"
|
||||
return 0
|
||||
;;
|
||||
--revoke-reason)
|
||||
_acme_sh_add_matches "0 1 2 3 4 5 6 7 8 9 10"
|
||||
return 0
|
||||
;;
|
||||
--cert-file | --key-file | --ca-file | --fullchain-file | --csr | --accountconf | --accountkey | --ca-bundle | --openssl-bin)
|
||||
_acme_sh_files
|
||||
return 0
|
||||
;;
|
||||
-w | --webroot | --home | --cert-home | --config-home | --ca-path)
|
||||
_acme_sh_dirs
|
||||
return 0
|
||||
;;
|
||||
-m | --email | --password | --useragent | --days | --valid-from | --valid-to | --httpport | --tlsport | --local-address | --dnssleep | --pre-hook | --post-hook | --renew-hook | --reloadcmd | --extended-key-usage | -b | --branch | --notify-source | --eab-kid | --eab-hmac-key | --preferred-chain | --cert-profile | --certificate-profile | --dns-persist-ca-name | --dns-persist-days)
|
||||
# These options take a free-form value, offer nothing.
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Complete the parameters.
|
||||
_acme_sh_add_matches "
|
||||
--accountconf
|
||||
--accountkey
|
||||
--accountkeylength
|
||||
--alpn
|
||||
--always-force-new-domain-key
|
||||
--apache
|
||||
--auto-upgrade
|
||||
--branch
|
||||
--ca-bundle
|
||||
--ca-file
|
||||
--ca-path
|
||||
--cert-file
|
||||
--cert-home
|
||||
--cert-profile
|
||||
--challenge-alias
|
||||
--config-home
|
||||
--csr
|
||||
--days
|
||||
--debug
|
||||
--deploy-hook
|
||||
--dns
|
||||
--dns-persist
|
||||
--dns-persist-ca-name
|
||||
--dns-persist-days
|
||||
--dns-persist-wildcard
|
||||
--dnssleep
|
||||
--domain
|
||||
--domain-alias
|
||||
--eab-hmac-key
|
||||
--eab-kid
|
||||
--ecc
|
||||
--email
|
||||
--extended-key-usage
|
||||
--force
|
||||
--force-color
|
||||
--fullchain-file
|
||||
--home
|
||||
--httpport
|
||||
--insecure
|
||||
--key-file
|
||||
--keylength
|
||||
--listen-v4
|
||||
--listen-v6
|
||||
--listraw
|
||||
--local-address
|
||||
--log
|
||||
--log-level
|
||||
--nginx
|
||||
--no-color
|
||||
--no-cron
|
||||
--no-profile
|
||||
--notify-hook
|
||||
--notify-level
|
||||
--notify-mode
|
||||
--notify-source
|
||||
--ocsp-must-staple
|
||||
--openssl-bin
|
||||
--output-insecure
|
||||
--password
|
||||
--post-hook
|
||||
--pre-hook
|
||||
--preferred-chain
|
||||
--reloadcmd
|
||||
--renew-hook
|
||||
--revoke-reason
|
||||
--server
|
||||
--staging
|
||||
--standalone
|
||||
--stateless
|
||||
--stop-renew-on-error
|
||||
--syslog
|
||||
--tlsport
|
||||
--treat-skip-as-success
|
||||
--use-wget
|
||||
--useragent
|
||||
--valid-from
|
||||
--valid-to
|
||||
--webroot
|
||||
--yes-I-know-dns-manual-mode-enough-go-ahead-please
|
||||
"
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _acme_sh_completion acme.sh
|
||||
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# Deploy hook: Baidu Cloud CDN
|
||||
#
|
||||
# Code generated by GitHub Copilot with Claude Sonnet 4.6 and OpenAI Codex with GPT-5.6 Sol
|
||||
#
|
||||
# API Doc: https://cloud.baidu.com/doc/CDN/s/Zkna2r57w
|
||||
#
|
||||
# Uses the same credential variables as dnsapi/dns_baidu.sh:
|
||||
# export Baidu_AK="your-access-key-id"
|
||||
# export Baidu_SK="your-secret-access-key"
|
||||
#
|
||||
# To deploy to a CDN domain different from the certificate CN
|
||||
# (e.g. wildcard or multi-domain certs):
|
||||
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn.example.com"
|
||||
#
|
||||
# Multiple CDN domains sharing the same certificate:
|
||||
# export DEPLOY_BAIDU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
|
||||
|
||||
BAIDU_CDN_HOST="cdn.baidubce.com"
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT=""
|
||||
|
||||
baidu_cdn_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
if ! _baidu_cdn_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_BAIDU_CDN_DOMAIN
|
||||
if [ "$DEPLOY_BAIDU_CDN_DOMAIN" ]; then
|
||||
_savedeployconf DEPLOY_BAIDU_CDN_DOMAIN "$DEPLOY_BAIDU_CDN_DOMAIN"
|
||||
else
|
||||
DEPLOY_BAIDU_CDN_DOMAIN="$_cdomain"
|
||||
fi
|
||||
|
||||
# Build JSON "domains" array from space-separated domain list
|
||||
_domains_json=""
|
||||
for _d in $DEPLOY_BAIDU_CDN_DOMAIN; do
|
||||
_d_e="$(_baidu_cdn_json_escape "$_d")"
|
||||
if [ -z "$_domains_json" ]; then
|
||||
_domains_json="\"${_d_e}\""
|
||||
else
|
||||
_domains_json="${_domains_json},\"${_d_e}\""
|
||||
fi
|
||||
done
|
||||
|
||||
# Build a valid cert name: must start with a letter, allow [A-Za-z0-9-/.], max 65 chars
|
||||
_cert_name="$(printf "%s" "$_cdomain" | sed 's/\*\./wildcard./g;s/[^A-Za-z0-9./]/-/g' | cut -c 1-65)"
|
||||
case "$_cert_name" in
|
||||
[A-Za-z]*) ;;
|
||||
*) _cert_name="c${_cert_name}" ;;
|
||||
esac
|
||||
|
||||
# PEM content is already Base64 inside the -----BEGIN/END----- wrappers.
|
||||
# The API expects the raw PEM as a JSON string, so newlines must be escaped as \n.
|
||||
_cert_pem="$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')"
|
||||
_key_pem="$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')"
|
||||
|
||||
_debug2 _cert_name "$_cert_name"
|
||||
_debug2 _domains_json "[$_domains_json]"
|
||||
|
||||
# Build JSON payload
|
||||
_payload="{\"domains\":[${_domains_json}],\"certificate\":{\"certName\":\"${_cert_name}\",\"certServerData\":\"${_cert_pem}\",\"certPrivateData\":\"${_key_pem}\"}}"
|
||||
|
||||
# Generate BCE v1 authorization header (query string included in canonical request)
|
||||
_cdn_path="/v2/domain/certificate"
|
||||
_cdn_query="action=put"
|
||||
_ts="$(_utc_date | sed 's/ /T/')Z"
|
||||
_content_type="application/json; charset=utf-8"
|
||||
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
|
||||
|
||||
if ! _baidu_cdn_bce_auth "POST" "$_cdn_path" "$_cdn_query" "$BAIDU_CDN_HOST" "$_ts" "3600" "$_content_type" "$_payload_hash"; then
|
||||
_err "Failed to sign request"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H1="Authorization: $_BAIDU_CDN_BCE_AUTH_RESULT"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_CDN_HOST"
|
||||
_H5=""
|
||||
|
||||
_url="https://${BAIDU_CDN_HOST}${_cdn_path}?${_cdn_query}"
|
||||
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Failed to call Baidu Cloud CDN API"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
|
||||
if _contains "$response" "\"certId\""; then
|
||||
_info "Certificate deployed to Baidu Cloud CDN for: $DEPLOY_BAIDU_CDN_DOMAIN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to deploy certificate to Baidu Cloud CDN: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# BCE v1 signing with canonical query string support.
|
||||
# The CDN endpoint uses ?action=put so it must be included in the canonical request.
|
||||
_baidu_cdn_bce_auth() {
|
||||
_method="$1"
|
||||
_uri="$2"
|
||||
_query="$3"
|
||||
_host="$4"
|
||||
_ts="$5"
|
||||
_expire="$6"
|
||||
_ct="$7"
|
||||
_payload_hash="$8"
|
||||
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT=""
|
||||
|
||||
_auth_prefix="bce-auth-v1/${Baidu_AK}/${_ts}/${_expire}"
|
||||
_signed_headers="content-type;host;x-bce-content-sha256;x-bce-date"
|
||||
_canonical_uri="$(_baidu_cdn_bce_encode_path "$_uri")"
|
||||
|
||||
_host_e="$(printf "%s" "$_host" | _url_encode upper-hex)"
|
||||
_date_e="$(printf "%s" "$_ts" | _url_encode upper-hex)"
|
||||
_ct_e="$(printf "%s" "$_ct" | _url_encode upper-hex)"
|
||||
_hash_e="$(printf "%s" "$_payload_hash" | _url_encode upper-hex)"
|
||||
|
||||
_canonical_headers="content-type:${_ct_e}
|
||||
host:${_host_e}
|
||||
x-bce-content-sha256:${_hash_e}
|
||||
x-bce-date:${_date_e}"
|
||||
|
||||
_canonical_request="${_method}
|
||||
${_canonical_uri}
|
||||
${_query}
|
||||
${_canonical_headers}"
|
||||
|
||||
_sk_hex="$(printf "%s" "$Baidu_SK" | _hex_dump | tr -d " ")"
|
||||
_signing_key="$(_baidu_cdn_hmac_sha256_hexkey "$_sk_hex" "$_auth_prefix")"
|
||||
_signing_key_hex="$(printf "%s" "$_signing_key" | _hex_dump | tr -d " ")"
|
||||
_signature="$(_baidu_cdn_hmac_sha256_hexkey "$_signing_key_hex" "$_canonical_request")"
|
||||
|
||||
_BAIDU_CDN_BCE_AUTH_RESULT="${_auth_prefix}/${_signed_headers}/${_signature}"
|
||||
}
|
||||
|
||||
_baidu_cdn_load_credentials() {
|
||||
Baidu_AK="${Baidu_AK:-$(_readaccountconf_mutable Baidu_AK)}"
|
||||
Baidu_SK="${Baidu_SK:-$(_readaccountconf_mutable Baidu_SK)}"
|
||||
|
||||
Baidu_AK="$(_baidu_cdn_trim_ws "$Baidu_AK")"
|
||||
Baidu_SK="$(_baidu_cdn_trim_ws "$Baidu_SK")"
|
||||
|
||||
if [ -z "$Baidu_AK" ] || [ -z "$Baidu_SK" ]; then
|
||||
_err "Baidu_AK and Baidu_SK are required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable Baidu_AK "$Baidu_AK"
|
||||
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_cdn_bce_encode_path() {
|
||||
_p="$1"
|
||||
_out=""
|
||||
if [ "${_p#"/"}" != "$_p" ]; then
|
||||
_out="/"
|
||||
fi
|
||||
|
||||
_rest="${_p#/}"
|
||||
while [ -n "$_rest" ]; do
|
||||
_seg="${_rest%%/*}"
|
||||
if [ "$_seg" ]; then
|
||||
if [ -z "$_out" ] || [ "$_out" = "/" ]; then
|
||||
_out="${_out}$(printf "%s" "$_seg" | _url_encode upper-hex)"
|
||||
else
|
||||
_out="${_out}/$(printf "%s" "$_seg" | _url_encode upper-hex)"
|
||||
fi
|
||||
fi
|
||||
if [ "${_rest#*/}" = "$_rest" ]; then
|
||||
break
|
||||
fi
|
||||
_rest="${_rest#*/}"
|
||||
done
|
||||
|
||||
if [ -z "$_out" ]; then
|
||||
_out="/"
|
||||
fi
|
||||
printf "%s" "$_out"
|
||||
}
|
||||
|
||||
_baidu_cdn_trim_ws() {
|
||||
printf "%s" "$1" | tr '\r\n\t' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//'
|
||||
}
|
||||
|
||||
_baidu_cdn_json_escape() {
|
||||
_s="$1"
|
||||
_s="$(printf "%s" "$_s" | tr -d '\r\n')"
|
||||
printf "%s" "$_s" |
|
||||
sed 's/\\/\\\\/g; s/ /\\t/g' |
|
||||
_baidu_cdn_json_encode
|
||||
}
|
||||
|
||||
_baidu_cdn_json_encode() {
|
||||
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
|
||||
printf "%s" "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
|
||||
}
|
||||
|
||||
_baidu_cdn_hmac_sha256_hexkey() {
|
||||
_key_hex="$1"
|
||||
_msg="$2"
|
||||
printf "%s" "$_msg" | _hmac sha256 "$_key_hex" hex
|
||||
}
|
||||
+11
-2
@@ -52,7 +52,15 @@ cpanel_uapi_deploy() {
|
||||
|
||||
# read cert and key files and urlencode both
|
||||
_cert=$(_url_encode <"$_ccert")
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
# with --signcsr the private key was never handed to acme.sh, so the key
|
||||
# file does not exist; skip it instead of spilling a shell redirection
|
||||
# error on every renewal (cPanel keeps using the already-installed key)
|
||||
if [ -f "$_ckey" ]; then
|
||||
_key=$(_url_encode <"$_ckey")
|
||||
else
|
||||
_debug "Key file $_ckey does not exist (csr mode), not sending a key."
|
||||
_key=""
|
||||
fi
|
||||
|
||||
_debug2 _cert "$_cert"
|
||||
_debug2 _key "$_key"
|
||||
@@ -194,7 +202,8 @@ __cpanel_parse_response() {
|
||||
printf("%s%s=%s\n", prefix, $2, $3);
|
||||
}
|
||||
}' |
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p'
|
||||
sed -En -e 's/^result\/data\/(main_domain|sub_domains\/-|addon_domains\/-|parked_domains\/-)=(.*)$/\2/p' |
|
||||
sed -e 's/^"//' -e 's/"$//' # YAML double-quotes values starting with '*' (wildcard subdomains)
|
||||
}
|
||||
|
||||
# Load parameter by prefix+name - fallback to default if not set, and save to config
|
||||
|
||||
+43
-3
@@ -3,6 +3,8 @@
|
||||
#DEPLOY_DOCKER_CONTAINER_LABEL="xxxxxxx"
|
||||
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_FILE="/path/to/key.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_MODE="0640"
|
||||
#DEPLOY_DOCKER_CONTAINER_KEY_OWNER="1000:1000"
|
||||
#DEPLOY_DOCKER_CONTAINER_CERT_FILE="/path/to/cert.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_CA_FILE="/path/to/ca.pem"
|
||||
#DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE="/path/to/fullchain.pem"
|
||||
@@ -71,6 +73,18 @@ docker_deploy() {
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_FILE "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_MODE "$DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
|
||||
_savedeployconf DEPLOY_DOCKER_CONTAINER_KEY_OWNER "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_DOCKER_CONTAINER_CERT_FILE
|
||||
_debug2 DEPLOY_DOCKER_CONTAINER_CERT_FILE "$DEPLOY_DOCKER_CONTAINER_CERT_FILE"
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
|
||||
@@ -112,6 +126,20 @@ docker_deploy() {
|
||||
if ! _docker_cp "$_cid" "$_ckey" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" ]; then
|
||||
_info "Setting key file owner to $DEPLOY_DOCKER_CONTAINER_KEY_OWNER"
|
||||
if ! _docker_exec "$_cid" chown "$DEPLOY_DOCKER_CONTAINER_KEY_OWNER" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
_err "Can not change owner of key file in container"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" ]; then
|
||||
_info "Setting key file mode to $DEPLOY_DOCKER_CONTAINER_KEY_MODE"
|
||||
if ! _docker_exec "$_cid" chmod "$DEPLOY_DOCKER_CONTAINER_KEY_MODE" "$DEPLOY_DOCKER_CONTAINER_KEY_FILE"; then
|
||||
_err "Can not change mode of key file in container"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DEPLOY_DOCKER_CONTAINER_CERT_FILE" ]; then
|
||||
@@ -189,10 +217,22 @@ _docker_exec() {
|
||||
_debug2 cjson "$cjson"
|
||||
execid="$(echo "$cjson" | cut -d '"' -f 4)"
|
||||
_debug execid "$execid"
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": false,\"Tty\": false}")"
|
||||
#Detach:true is required for podman's docker-compatible API: with
|
||||
#Detach:false it streams the command output on the connection, so the
|
||||
#non-empty response was misread as an error (issue #4977). The real
|
||||
#result is checked via the exec inspect ExitCode below instead.
|
||||
ejson="$(_curl_unix_sock "$_DOCKER_SOCK" POST "/exec/$execid/start" "{\"Detach\": true,\"Tty\": false}")"
|
||||
_debug2 ejson "$ejson"
|
||||
if [ "$ejson" ]; then
|
||||
_err "$ejson"
|
||||
_et=0
|
||||
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
|
||||
while _contains "$ijson" "\"Running\":true" && [ "$_et" -lt 10 ]; do
|
||||
sleep 1
|
||||
_et="$(_math "$_et" + 1)"
|
||||
ijson="$(_curl_unix_sock "$_DOCKER_SOCK" GET "/exec/$execid/json")"
|
||||
done
|
||||
_debug2 ijson "$ijson"
|
||||
if ! echo "$ijson" | _egrep_o "\"ExitCode\": *0[,}]" >/dev/null 2>&1; then
|
||||
_err "docker exec error: $ijson"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env sh
|
||||
# Script to deploy a certificate to FortiGate via API and set it as the current web GUI certificate.
|
||||
#
|
||||
# FortiGate's native ACME integration does not support wildcard certificates or domain validation,
|
||||
# and is not supported if you have a custom management web port (eg. DNAT web traffic).
|
||||
#
|
||||
# REQUIRED:
|
||||
# export FGT_HOST="fortigate_hostname-or-ip"
|
||||
# export FGT_TOKEN="fortigate_api_token"
|
||||
#
|
||||
# OPTIONAL:
|
||||
# export FGT_PORT="10443" # Custom HTTPS port (defaults to 443 if not set)
|
||||
#
|
||||
# Run `acme.sh --deploy -d example.com --deploy-hook fortigate --insecure` to use this script.
|
||||
# `--insecure` is required on first run if not already using a valid SSL certificate on firewall.
|
||||
|
||||
# Function to parse a FortiGate API response
|
||||
_fortigate_parse_response() {
|
||||
_fortigate_response="$1"
|
||||
_fortigate_func="$2"
|
||||
_fortigate_status=$(echo "$_fortigate_response" | _egrep_o '"status":[ ]*"[^"]*"' | cut -d '"' -f 4)
|
||||
|
||||
if [ "$_fortigate_status" != "success" ]; then
|
||||
_err "[$_fortigate_func] Operation failed. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "[$_fortigate_func] Operation successful."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to deploy a base64-encoded certificate to the firewall
|
||||
_fortigate_deployer() {
|
||||
_fortigate_cert_base64=$(_base64 <"$_fortigate_cfullchain" | tr -d '\n')
|
||||
_fortigate_key_base64=$(_base64 <"$_fortigate_ckey" | tr -d '\n')
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"type": "regular",
|
||||
"scope": "global",
|
||||
"certname": "$_fortigate_cert_name",
|
||||
"key_file_content": "$_fortigate_key_base64",
|
||||
"file_content": "$_fortigate_cert_base64"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/local/import"
|
||||
_debug "Uploading certificate via URL: $_fortigate_url"
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
|
||||
_debug "FortiGate API Response: $_fortigate_response"
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Deploying certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to upload a CA certificate to the firewall
|
||||
# FortiGate does not automatically extract the CA from the full chain.
|
||||
_fortigate_upload_ca_cert() {
|
||||
_fortigate_ca_base64=$(_base64 <"$_fortigate_cca" | tr -d '\n')
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"import_method": "file",
|
||||
"scope": "global",
|
||||
"file_content": "$_fortigate_ca_base64"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/monitor/vpn-certificate/ca/import"
|
||||
_debug "Uploading CA certificate via URL: $_fortigate_url"
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "POST" "application/json")
|
||||
_debug "FortiGate API CA Response: $_fortigate_response"
|
||||
|
||||
# FortiGate error -328 means that the CA certificate already exists.
|
||||
if echo "$_fortigate_response" | grep -q '"error":[ ]*-328'; then
|
||||
_debug "CA certificate already exists. Skipping CA upload."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Deploying CA certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to activate the new certificate
|
||||
_fortigate_set_active_web_cert() {
|
||||
_fortigate_payload=$(
|
||||
cat <<EOF
|
||||
{
|
||||
"admin-server-cert": "$_fortigate_cert_name"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/system/global"
|
||||
_debug "Setting GUI certificate..."
|
||||
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "$_fortigate_payload" "$_fortigate_url" "" "PUT" "application/json")
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Assigning active certificate" || return 1
|
||||
}
|
||||
|
||||
# Function to clean up the previously deployed certificate
|
||||
_fortigate_cleanup_previous_certificate() {
|
||||
_getdeployconf FGT_LAST_CERT
|
||||
|
||||
if [ -n "$FGT_LAST_CERT" ] && [ "$FGT_LAST_CERT" != "$_fortigate_cert_name" ]; then
|
||||
_debug "Found previously deployed certificate: $FGT_LAST_CERT. Deleting it."
|
||||
|
||||
_fortigate_url="https://${FGT_HOST}:${FGT_PORT}/api/v2/cmdb/vpn.certificate/local/${FGT_LAST_CERT}"
|
||||
_H1="Authorization: Bearer $FGT_TOKEN"
|
||||
_fortigate_response=$(_post "" "$_fortigate_url" "" "DELETE" "application/json")
|
||||
_debug "Delete certificate API response: $_fortigate_response"
|
||||
|
||||
_fortigate_parse_response "$_fortigate_response" "Delete previous certificate" || return 1
|
||||
else
|
||||
_debug "No previous certificate found."
|
||||
fi
|
||||
}
|
||||
|
||||
# Main deploy-hook function
|
||||
fortigate_deploy() {
|
||||
# Include date and time to ensure unique names.
|
||||
_fortigate_cert_name="$(echo "$1" | sed 's/*/WILDCARD_/g')_$(date -u +"%Y-%m-%d_%H-%M-%S")"
|
||||
_fortigate_ckey="$2"
|
||||
_fortigate_cca="$4"
|
||||
_fortigate_cfullchain="$5"
|
||||
|
||||
if [ ! -f "$_fortigate_ckey" ] || [ ! -f "$_fortigate_cfullchain" ]; then
|
||||
_err "Valid key and/or certificate not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save required environment variables if set; otherwise load saved values.
|
||||
for _fortigate_var in FGT_HOST FGT_TOKEN FGT_PORT; do
|
||||
if [ -n "$(eval echo "\$$_fortigate_var")" ]; then
|
||||
_debug "Detected ENV variable $_fortigate_var. Saving to file."
|
||||
_savedeployconf "$_fortigate_var" "$(eval echo "\$$_fortigate_var")" 1
|
||||
else
|
||||
_debug "Attempting to load variable $_fortigate_var from file."
|
||||
_getdeployconf "$_fortigate_var"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$FGT_HOST" ] || [ -z "$FGT_TOKEN" ]; then
|
||||
_err "FGT_HOST and FGT_TOKEN must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
FGT_PORT="${FGT_PORT:-443}"
|
||||
_debug "Using FortiGate port: $FGT_PORT"
|
||||
|
||||
# Upload the new certificate.
|
||||
_fortigate_deployer || return 1
|
||||
|
||||
# Upload the CA certificate.
|
||||
if [ -n "$_fortigate_cca" ] && [ -f "$_fortigate_cca" ]; then
|
||||
_fortigate_upload_ca_cert || return 1
|
||||
else
|
||||
_debug "No CA certificate provided."
|
||||
fi
|
||||
|
||||
# Activate the new certificate.
|
||||
_fortigate_set_active_web_cert || return 1
|
||||
|
||||
# Delete the previously deployed certificate only after successful activation.
|
||||
_fortigate_cleanup_previous_certificate || return 1
|
||||
|
||||
# Save the new certificate name for cleanup during the next deployment.
|
||||
_savedeployconf "FGT_LAST_CERT" "$_fortigate_cert_name" 1
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ gcore_cdn_deploy() {
|
||||
_request="{\"username\":\"$Le_Deploy_gcore_cdn_username\",\"password\":\"$Le_Deploy_gcore_cdn_password\"}"
|
||||
_debug _request "$_request"
|
||||
export _H1="Content-Type:application/json"
|
||||
_response=$(_post "$_request" "https://api.gcore.com/auth/jwt/login")
|
||||
_response=$(_post "$_request" "https://api.gcore.com/iam/auth/jwt/login")
|
||||
_debug _response "$_response"
|
||||
_regex=".*\"access\":\"\([-._0-9A-Za-z]*\)\".*$"
|
||||
_debug _regex "$_regex"
|
||||
|
||||
+63
-52
@@ -43,7 +43,8 @@
|
||||
# needing to reload HAProxy. Default is "no".
|
||||
#
|
||||
# Require the socat binary. DEPLOY_HAPROXY_STATS_SOCKET variable uses the socat
|
||||
# address format.
|
||||
# address format. The certificate can be deployed to a comma separated ',' list
|
||||
# of hosts ("TCP4:10.0.0.1:1999,TCP4:10.0.0.2:1999")
|
||||
#
|
||||
# export DEPLOY_HAPROXY_MASTER_CLI="UNIX:/run/haproxy-master.sock"
|
||||
#
|
||||
@@ -193,7 +194,6 @@ haproxy_deploy() {
|
||||
_issuer="${_pem}.issuer"
|
||||
_ocsp="${_pem}.ocsp"
|
||||
_reload="${Le_Deploy_haproxy_reload}"
|
||||
_statssock="${Le_Deploy_haproxy_stats_socket}"
|
||||
|
||||
_info "Deploying PEM file"
|
||||
# Create a temporary PEM file
|
||||
@@ -272,12 +272,18 @@ haproxy_deploy() {
|
||||
_cafile_argument=""
|
||||
fi
|
||||
_debug _cafile_argument "${_cafile_argument}"
|
||||
# if OpenSSL/LibreSSL is v1.1 or above, the format for the -header option has changed
|
||||
# OpenSSL 1.1+ expects -header Host=value (one argument), while
|
||||
# LibreSSL keeps the old two-argument form -header Host value at any
|
||||
# version (3.x/4.x), so it must be detected by name, not by number.
|
||||
_openssl_name=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f1)
|
||||
_openssl_version=$(${ACME_OPENSSL_BIN:-openssl} version | cut -d' ' -f2)
|
||||
_debug _openssl_name "${_openssl_name}"
|
||||
_debug _openssl_version "${_openssl_version}"
|
||||
_openssl_major=$(echo "${_openssl_version}" | cut -d '.' -f1)
|
||||
_openssl_minor=$(echo "${_openssl_version}" | cut -d '.' -f2)
|
||||
if [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
if [ "${_openssl_name}" = "LibreSSL" ]; then
|
||||
_header_sep=" "
|
||||
elif [ "${_openssl_major}" -eq "1" ] && [ "${_openssl_minor}" -ge "1" ] || [ "${_openssl_major}" -ge "2" ]; then
|
||||
_header_sep="="
|
||||
else
|
||||
_header_sep=" "
|
||||
@@ -327,62 +333,67 @@ haproxy_deploy() {
|
||||
|
||||
# Update certificate over HAProxy stats socket or master CLI.
|
||||
if _exists socat; then
|
||||
# look for the certificate on the stats socket, to chose between updating or creating one
|
||||
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
|
||||
_debug _socat_cert_cmd "${_socat_cert_cmd}"
|
||||
eval "${_socat_cert_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_newcert="1"
|
||||
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
|
||||
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
|
||||
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
|
||||
eval "${_socat_crtlist_show_cmd}"
|
||||
IFS=','
|
||||
for _statssock in ${Le_Deploy_haproxy_stats_socket}; do
|
||||
# look for the certificate on the stats socket, to choose between updating or creating one
|
||||
_socat_cert_cmd="echo '${_cmdpfx}show ssl cert' | socat '${_statssock}' - | grep -q '^${_pem}$'"
|
||||
_debug _socat_cert_cmd "${_socat_cert_cmd}"
|
||||
eval "${_socat_cert_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
_newcert="1"
|
||||
_info "Creating new certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
# certificate wasn't found, it's a new one. We should check if the crt-list exists and creates/inserts the certificate.
|
||||
_socat_crtlist_show_cmd="echo '${_cmdpfx}show ssl crt-list' | socat '${_statssock}' - | grep -q '^${Le_Deploy_haproxy_pem_path}$'"
|
||||
_debug _socat_crtlist_show_cmd "${_socat_crtlist_show_cmd}"
|
||||
eval "${_socat_crtlist_show_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't find '${Le_Deploy_haproxy_pem_path}' in haproxy 'show ssl crt-list'"
|
||||
return "${_ret}"
|
||||
fi
|
||||
# create a new certificate
|
||||
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
|
||||
_debug _socat_new_cmd "${_socat_new_cmd}"
|
||||
eval "${_socat_new_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't create '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
else
|
||||
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
fi
|
||||
# create a new certificate
|
||||
_socat_new_cmd="echo '${_cmdpfx}new ssl cert ${_pem}' | socat '${_statssock}' - | grep -q 'New empty'"
|
||||
_debug _socat_new_cmd "${_socat_new_cmd}"
|
||||
eval "${_socat_new_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Couldn't create '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
else
|
||||
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
|
||||
fi
|
||||
_socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
|
||||
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
|
||||
eval "${_socat_cert_set_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
|
||||
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
|
||||
eval "${_socat_cert_commit_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't commit '${_pem}' in haproxy"
|
||||
return ${_ret}
|
||||
fi
|
||||
if [ "${_newcert}" = "1" ]; then
|
||||
# if this is a new certificate, it needs to be inserted into the crt-list`
|
||||
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
|
||||
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
|
||||
eval "${_socat_cert_add_cmd}"
|
||||
# printf %b, not "echo -e": dash's echo has no -e and sends a literal "-e " to the socket.
|
||||
# "Transaction updated" is replied instead of "created" when an uncommitted transaction exists.
|
||||
_socat_cert_set_cmd="printf '%b\n' '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -qE 'Transaction (created|updated)'"
|
||||
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
|
||||
eval "${_socat_cert_set_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
_socat_cert_commit_cmd="echo '${_cmdpfx}commit ssl cert ${_pem}' | socat '${_statssock}' - | grep -q '^Success!$'"
|
||||
_debug _socat_cert_commit_cmd "${_socat_cert_commit_cmd}"
|
||||
eval "${_socat_cert_commit_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't commit '${_pem}' in haproxy"
|
||||
return ${_ret}
|
||||
fi
|
||||
if [ "${_newcert}" = "1" ]; then
|
||||
# if this is a new certificate, it needs to be inserted into the crt-list`
|
||||
_socat_cert_add_cmd="echo '${_cmdpfx}add ssl crt-list ${Le_Deploy_haproxy_pem_path} ${_pem}' | socat '${_statssock}' - | grep -q 'Success!'"
|
||||
_debug _socat_cert_add_cmd "${_socat_cert_add_cmd}"
|
||||
eval "${_socat_cert_add_cmd}"
|
||||
_ret=$?
|
||||
if [ "${_ret}" != "0" ]; then
|
||||
_err "Can't update '${_pem}' in haproxy"
|
||||
return "${_ret}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
_err "'socat' is not available, couldn't update over ${_socketname}"
|
||||
fi
|
||||
|
||||
@@ -210,7 +210,7 @@ _clear_envs() {
|
||||
|
||||
echo "$env_pairs" | while IFS='=' read -r _key _value; do
|
||||
_debug3 "Deleting key" "$_key"
|
||||
_cleardomainconf "SAVED_$_key"
|
||||
_cleardeployconf "$_key"
|
||||
unset -v "$_key"
|
||||
done
|
||||
}
|
||||
|
||||
+3
-1
@@ -54,6 +54,8 @@ mydevil_deploy() {
|
||||
# Usage: ip=$(mydevil_get_ip domain.com)
|
||||
# echo $ip
|
||||
mydevil_get_ip() {
|
||||
devil dns list "$1" | cut -w -s -f 3,7 | grep "^A$(printf '\t')" | cut -w -s -f 2 || return 1
|
||||
# tr squeezes runs of blanks into one tab so plain cut works everywhere;
|
||||
# cut -w is BSD-only and unknown to GNU coreutils
|
||||
devil dns list "$1" | tr -s ' \t' '\t' | cut -s -f 3,7 | grep "^A$(printf '\t')" | cut -s -f 2 || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
+14
-3
@@ -296,9 +296,20 @@ panos_deploy() {
|
||||
_err "Unable to generate an API key. The user and pass may be invalid or not authorized to generate a new key. Please check the PANOS_USER and PANOS_PASS credentials and try again"
|
||||
return 1
|
||||
else
|
||||
deployer cert
|
||||
deployer key
|
||||
deployer commit
|
||||
# A commit of a failed import would leave a mismatched cert/key pair
|
||||
# on the firewall and can lock the admin out of the management
|
||||
# interface, see https://github.com/acmesh-official/acme.sh/issues/4716
|
||||
if ! deployer cert; then
|
||||
_err "Cert import failed. Aborting without committing."
|
||||
return 1
|
||||
fi
|
||||
if ! deployer key; then
|
||||
_err "Key import failed. Aborting without committing. Warning: the firewall now has an uncommitted mismatched cert/key pair in its candidate config."
|
||||
return 1
|
||||
fi
|
||||
if ! deployer commit; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$_panos_template_stack" ]; then
|
||||
# try to get job status for 20 times in 30 sec interval
|
||||
i=0
|
||||
|
||||
+2
-1
@@ -125,7 +125,7 @@ routeros_deploy() {
|
||||
_savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
|
||||
_savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
|
||||
_savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
|
||||
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64"
|
||||
|
||||
# push key to routeros
|
||||
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
|
||||
@@ -143,6 +143,7 @@ comment=\"generated by routeros deploy script in acme.sh\" \
|
||||
source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_1 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_2 ];\
|
||||
\n/certificate remove [ find name=$_cdomain.cer_3 ];\
|
||||
\ndelay 1;\
|
||||
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
|
||||
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
|
||||
|
||||
+34
-5
@@ -25,7 +25,8 @@
|
||||
# export DEPLOY_SSH_MULTI_CALL="" # yes or no, default to no or previously saved value
|
||||
# export DEPLOY_SSH_USE_SCP="" yes or no, default to no
|
||||
# export DEPLOY_SSH_SCP_CMD="" defaults to "scp -q"
|
||||
#
|
||||
# export DEPLOY_SSH_REMOTE_SHELL="" # defaults to sh -c
|
||||
# export DEPLOY_SSH_REMOTE_CMD_QUOTE="" # yes or no, defaults to yes
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
@@ -71,6 +72,24 @@ ssh_deploy() {
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_CMD "$DEPLOY_SSH_CMD"
|
||||
|
||||
# REMOTE_SHELL is optional. If not provided then use sh
|
||||
_migratedeployconf Le_Deploy_ssh_remote_shell DEPLOY_SSH_REMOTE_SHELL
|
||||
_getdeployconf DEPLOY_SSH_REMOTE_SHELL
|
||||
_debug2 DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
|
||||
if [ -z "$DEPLOY_SSH_REMOTE_SHELL" ]; then
|
||||
DEPLOY_SSH_REMOTE_SHELL="sh -c"
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_REMOTE_SHELL "$DEPLOY_SSH_REMOTE_SHELL"
|
||||
|
||||
# REMOTE_CMD_QUOTE is optional. If not provided then yes
|
||||
_migratedeployconf Le_Deploy_ssh_remote_cmd_quote DEPLOY_SSH_REMOTE_CMD_QUOTE
|
||||
_getdeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE
|
||||
_debug2 DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
|
||||
if [ -z "$DEPLOY_SSH_REMOTE_CMD_QUOTE" ]; then
|
||||
DEPLOY_SSH_REMOTE_CMD_QUOTE="yes"
|
||||
fi
|
||||
_savedeployconf DEPLOY_SSH_REMOTE_CMD_QUOTE "$DEPLOY_SSH_REMOTE_CMD_QUOTE"
|
||||
|
||||
# BACKUP is optional. If not provided then default to previously saved value or yes.
|
||||
_migratedeployconf Le_Deploy_ssh_backup DEPLOY_SSH_BACKUP
|
||||
_getdeployconf DEPLOY_SSH_BACKUP
|
||||
@@ -170,10 +189,16 @@ ssh_deploy() {
|
||||
_info "Required commands batched and sent in single call to remote host"
|
||||
fi
|
||||
|
||||
_returnCode=0
|
||||
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
|
||||
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
|
||||
_ssh_deploy
|
||||
if ! _ssh_deploy; then
|
||||
# in case of an error, remember it, but keep going for the remaining servers
|
||||
_returnCode=1
|
||||
fi
|
||||
done
|
||||
|
||||
return $_returnCode
|
||||
}
|
||||
|
||||
_ssh_deploy() {
|
||||
@@ -428,9 +453,13 @@ _ssh_remote_cmd() {
|
||||
_secure_debug "Remote commands to execute: $_cmd"
|
||||
_info "Submitting sequence of commands to remote server by $_ssh_cmd"
|
||||
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" sh -c "'$_cmd'"
|
||||
if [ "$DEPLOY_SSH_REMOTE_CMD_QUOTE" = "yes" ]; then
|
||||
# quotations in bash cmd below intended. Squash travis spellcheck error
|
||||
# shellcheck disable=SC2029
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "'$_cmd'"
|
||||
else
|
||||
$_ssh_cmd "$DEPLOY_SSH_USER@$_host" "$DEPLOY_SSH_REMOTE_SHELL" "$_cmd"
|
||||
fi
|
||||
_err_code="$?"
|
||||
|
||||
if [ "$_err_code" != "0" ]; then
|
||||
|
||||
+15
-20
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
################################################################################
|
||||
# ACME.sh 3rd party deploy plugin for Synology DSM
|
||||
@@ -72,7 +72,7 @@ synology_dsm_deploy() {
|
||||
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
if ! _exists synouser || ! _exists synogroup || ! _exists synosetkeyvalue; then
|
||||
_err "Missing required tools to creat temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Missing required tools to create temp admin user, please set SYNO_USERNAME and SYNO_PASSWORD instead."
|
||||
_err "Notice: temp admin user authorization method only supports local deployment on DSM."
|
||||
return 1
|
||||
fi
|
||||
@@ -234,11 +234,11 @@ synology_dsm_deploy() {
|
||||
fi
|
||||
fi
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
# Account has 2FA-OTP enabled, since error 403 reported.
|
||||
# https://global.download.synology.com/download/Document/Software/DeveloperGuide/Os/DSM/All/enu/DSM_Login_Web_API_Guide_enu.pdf
|
||||
if [ "$error_code" == "403" ]; then
|
||||
if [ "$error_code" = "403" ]; then
|
||||
if [ -z "$SYNO_DEVICE_NAME" ]; then
|
||||
printf "Enter device name or leave empty for default (CertRenewal): "
|
||||
read -r SYNO_DEVICE_NAME
|
||||
@@ -269,27 +269,27 @@ synology_dsm_deploy() {
|
||||
_secure_debug2 SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
|
||||
fi
|
||||
fi
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
fi
|
||||
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" == "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
if [ "$error_code" = "403" ] && [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
_cleardeployconf SYNO_DEVICE_ID
|
||||
_err "Failed to authenticate with SYNO_DEVICE_ID (may expired or invalid), please try again in a new terminal window."
|
||||
elif [ "$error_code" == "404" ]; then
|
||||
_err "Failed to authenticate with SYNO_DEVICE_ID (may be expired or invalid), please try again in a new terminal window."
|
||||
elif [ "$error_code" = "404" ]; then
|
||||
_err "Failed to authenticate with provided 2FA-OTP code, please try again in a new terminal window."
|
||||
elif [ "$error_code" == "406" ]; then
|
||||
elif [ "$error_code" = "406" ]; then
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
|
||||
_err "Failed with unexcepted error, please report this by providing full log with '--debug 3'."
|
||||
else
|
||||
_err "Enforce auth with 2FA-OTP enabled, please configure the user to enable 2FA-OTP to continue."
|
||||
fi
|
||||
elif [ "$error_code" == "400" ]; then
|
||||
elif [ "$error_code" = "400" ]; then
|
||||
_err "Failed to authenticate, no such account or incorrect password."
|
||||
elif [ "$error_code" == "401" ]; then
|
||||
elif [ "$error_code" = "401" ]; then
|
||||
_err "Failed to authenticate with a non-existent account."
|
||||
elif [ "$error_code" == "408" ] || [ "$error_code" == "409" ] || [ "$error_code" == "410" ]; then
|
||||
elif [ "$error_code" = "408" ] || [ "$error_code" = "409" ] || [ "$error_code" = "410" ]; then
|
||||
_err "Failed to authenticate, the account password has expired or must be changed."
|
||||
else
|
||||
_err "Failed to authenticate with error: $error_code."
|
||||
@@ -322,8 +322,8 @@ synology_dsm_deploy() {
|
||||
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
|
||||
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
|
||||
else
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
|
||||
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME" "base64"
|
||||
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD" "base64"
|
||||
_savedeployconf SYNO_DEVICE_ID "$SYNO_DEVICE_ID"
|
||||
_savedeployconf SYNO_DEVICE_NAME "$SYNO_DEVICE_NAME"
|
||||
fi
|
||||
@@ -336,7 +336,7 @@ synology_dsm_deploy() {
|
||||
id=$(echo "$response" | sed -n "s/.*\"desc\":\"$escaped_certificate\",\"id\":\"\([^\"]*\).*/\1/p")
|
||||
_debug2 id "$id"
|
||||
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -o '[0-9]*')
|
||||
error_code=$(echo "$response" | grep '"error":' | grep -o '"code":[0-9]*' | grep -Eo '[0-9]+')
|
||||
_debug2 error_code "$error_code"
|
||||
if [ -n "$error_code" ]; then
|
||||
if [ "$error_code" -eq 105 ]; then
|
||||
@@ -424,11 +424,6 @@ _temp_admin_cleanup() {
|
||||
fi
|
||||
}
|
||||
|
||||
#_cleardeployconf key
|
||||
_cleardeployconf() {
|
||||
_cleardomainconf "SAVED_$1"
|
||||
}
|
||||
|
||||
# key
|
||||
_check2cleardeployconfexp() {
|
||||
_key="$1"
|
||||
|
||||
+25
-5
@@ -16,7 +16,12 @@
|
||||
#
|
||||
# # API KEY
|
||||
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
|
||||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI>"
|
||||
# Optional:
|
||||
# export DEPLOY_TRUENAS_HOSTNAME="<TRUENAS_HOSTNAME_OR_IP>"
|
||||
# export DEPLOY_TRUENAS_PROTOCOL="wss" # ws or wss
|
||||
# export DEPLOY_TRUENAS_PORT="443" # optional, e.g. 80, 443, 8443
|
||||
|
||||
#
|
||||
|
||||
### Private functions
|
||||
@@ -56,7 +61,6 @@ _ws_call() {
|
||||
_ws_upload_cert() {
|
||||
|
||||
/usr/bin/env python - <<EOF
|
||||
|
||||
import sys
|
||||
|
||||
from truenas_api_client import Client
|
||||
@@ -78,7 +82,6 @@ with Client(uri="$_ws_uri") as c:
|
||||
print("R:0")
|
||||
print("E:_ws_upload_cert error!")
|
||||
sys.exit(7)
|
||||
|
||||
EOF
|
||||
|
||||
return $?
|
||||
@@ -181,6 +184,8 @@ truenas_ws_deploy() {
|
||||
_getdeployconf DEPLOY_TRUENAS_APIKEY
|
||||
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
|
||||
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
|
||||
_getdeployconf DEPLOY_TRUENAS_PORT
|
||||
|
||||
# Check API Key
|
||||
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
|
||||
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
|
||||
@@ -196,7 +201,21 @@ truenas_ws_deploy() {
|
||||
_info "TrueNAS protocol not set. Using 'ws'."
|
||||
DEPLOY_TRUENAS_PROTOCOL="ws"
|
||||
fi
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
|
||||
|
||||
# Check port, optional
|
||||
if [ -n "$DEPLOY_TRUENAS_PORT" ]; then
|
||||
case "$DEPLOY_TRUENAS_PORT" in
|
||||
'' | *[!0-9]*)
|
||||
_err "Invalid TrueNAS port '$DEPLOY_TRUENAS_PORT'. DEPLOY_TRUENAS_PORT must be numeric."
|
||||
return 8
|
||||
;;
|
||||
esac
|
||||
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME:$DEPLOY_TRUENAS_PORT/websocket"
|
||||
else
|
||||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
|
||||
fi
|
||||
|
||||
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
||||
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
||||
_debug _ws_uri "$_ws_uri"
|
||||
@@ -216,13 +235,14 @@ truenas_ws_deploy() {
|
||||
|
||||
if [ "$_ws_response" != "TRUE" ]; then
|
||||
_err "TrueNAS is not ready."
|
||||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
|
||||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME, DEPLOY_TRUENAS_PROTOCOL and DEPLOY_TRUENAS_PORT."
|
||||
_err "Verify API key."
|
||||
return 2
|
||||
fi
|
||||
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
|
||||
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
|
||||
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
|
||||
_savedeployconf DEPLOY_TRUENAS_PORT "$DEPLOY_TRUENAS_PORT"
|
||||
_info "TrueNAS health: OK"
|
||||
|
||||
########## System info
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# install a certificate on a Windows host over OpenSSH and bind it to the Remote
|
||||
# Desktop listener (RDP-Tcp).
|
||||
#
|
||||
# One ssh invocation does the whole job:
|
||||
# * the PFX is built locally, base64'd, and embedded as a string literal
|
||||
# inside a generated PowerShell script;
|
||||
# * the script is piped to `powershell.exe -Command -` over ssh. No scp,
|
||||
# no temp files on the Windows host.
|
||||
#
|
||||
# First run:
|
||||
# export DEPLOY_WIN_RDP_HOST=winserver.example.com
|
||||
# acme.sh --deploy -d winserver.example.com --deploy-hook windows_rdp
|
||||
#
|
||||
# Available variables:
|
||||
# DEPLOY_WIN_RDP_HOST required SSH host
|
||||
# DEPLOY_WIN_RDP_USER optional SSH user, must be a local administrator (can also by set via ssh_config)
|
||||
# DEPLOY_WIN_RDP_PORT optional SSH port, default 22
|
||||
# DEPLOY_WIN_RDP_SSH_OPTS optional extra ssh options, e.g.
|
||||
# "-i /root/.ssh/win_id_ed25519 -o StrictHostKeyChecking=yes"
|
||||
# DEPLOY_WIN_RDP_LISTENER optional RDP listener name, default RDP-Tcp
|
||||
# DEPLOY_WIN_RDP_RESTART optional "1" to restart TermService after install.
|
||||
# Active RDP sessions will drop!
|
||||
|
||||
windows_rdp_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
if ! _exists "ssh"; then
|
||||
_err "ssh is required but was not found in PATH."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- configuration ------------------------------------------------------
|
||||
_getdeployconf DEPLOY_WIN_RDP_HOST
|
||||
_getdeployconf DEPLOY_WIN_RDP_USER
|
||||
_getdeployconf DEPLOY_WIN_RDP_PORT
|
||||
_getdeployconf DEPLOY_WIN_RDP_SSH_OPTS
|
||||
_getdeployconf DEPLOY_WIN_RDP_LISTENER
|
||||
_getdeployconf DEPLOY_WIN_RDP_RESTART
|
||||
|
||||
if [ -z "$DEPLOY_WIN_RDP_HOST" ]; then
|
||||
_err "DEPLOY_WIN_RDP_HOST must be set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_WIN_RDP_HOST "$DEPLOY_WIN_RDP_HOST"
|
||||
[ -n "$DEPLOY_WIN_RDP_USER" ] && _savedeployconf DEPLOY_WIN_RDP_USER "$DEPLOY_WIN_RDP_USER"
|
||||
[ -n "$DEPLOY_WIN_RDP_PORT" ] && _savedeployconf DEPLOY_WIN_RDP_PORT "$DEPLOY_WIN_RDP_PORT"
|
||||
[ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ] && _savedeployconf DEPLOY_WIN_RDP_SSH_OPTS "$DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
[ -n "$DEPLOY_WIN_RDP_LISTENER" ] && _savedeployconf DEPLOY_WIN_RDP_LISTENER "$DEPLOY_WIN_RDP_LISTENER"
|
||||
[ -n "$DEPLOY_WIN_RDP_RESTART" ] && _savedeployconf DEPLOY_WIN_RDP_RESTART "$DEPLOY_WIN_RDP_RESTART"
|
||||
|
||||
_port="${DEPLOY_WIN_RDP_PORT:-22}"
|
||||
_listener="${DEPLOY_WIN_RDP_LISTENER:-RDP-Tcp}"
|
||||
if [ -n "$DEPLOY_WIN_RDP_USER" ]; then
|
||||
_target="$DEPLOY_WIN_RDP_USER@$DEPLOY_WIN_RDP_HOST"
|
||||
else
|
||||
_target="$DEPLOY_WIN_RDP_HOST"
|
||||
fi
|
||||
_pfx_pass="acme"
|
||||
|
||||
# ---- build thumbprint + PFX locally ------------------------------------
|
||||
_thumb="$(_fingerprint "$_ccert" 'sha1')"
|
||||
if [ -z "$_thumb" ]; then
|
||||
_err "Failed to compute certificate thumbprint."
|
||||
return 1
|
||||
fi
|
||||
_debug "Thumbprint: $_thumb"
|
||||
|
||||
_debug "Building PFX at $_pfx_file"
|
||||
_pfx_file="$(_mktemp)"
|
||||
if ! _toPkcs "$_pfx_file" "$_ckey" "$_ccert" "$_cca" "$_pfx_pass"; then
|
||||
_err "Failed to build PFX archive."
|
||||
rm -f "$_pfx_file"
|
||||
return 1
|
||||
fi
|
||||
_pfx_b64=$(_base64 "multiline" <"$_pfx_file")
|
||||
rm -f "$_pfx_file"
|
||||
|
||||
# ---- build installer script --------------------------------------------
|
||||
if [ "$DEPLOY_WIN_RDP_RESTART" = "1" ]; then
|
||||
_restart_ps='Restart-Service -Name TermService -Force'
|
||||
else
|
||||
_restart_ps='# New RdP connections will pick up the new cert automatically.'
|
||||
fi
|
||||
|
||||
# Escape every literal `$` with `\$` so the shell does not expand it.
|
||||
# Values substituted from shell: $_pfx_b64, $_pfx_pass, $_thumb, $_listener.
|
||||
_ps1=$(
|
||||
cat <<PSEOF
|
||||
|
||||
\$ErrorActionPreference = 'Stop'
|
||||
|
||||
\$pfxBytes = [Convert]::FromBase64String('${_pfx_b64}')
|
||||
|
||||
# Note: It is quite important to use a X509Certificate2Collection here in any case, since we otherwise
|
||||
# could run into quite a lot of trouble when importing the certificate including its entire chain
|
||||
# and its private key. Windows might behave arbitrarily and not consistently import the certificate
|
||||
# at all - unless "Exportable" is included in the storage flags. However, then the certificate seems
|
||||
# unaccessible to TermService for some weird reasons despite all permissions being set (at least on my
|
||||
# Win 11 lab machine). This might be some security setting that prevents TermService from working with
|
||||
# exportable keys? I don't know - importing the entire collection including chain or not always fixes
|
||||
# the issues.
|
||||
#
|
||||
# Note2: If you should have kicked yourself out for some reason, then deleting the certificate will make
|
||||
# TermService restore the original, self-signed certificate after at least after the second login attempt.
|
||||
# Deleting the certificate can be easily accomplished via the Powershell, since SSH access will still be
|
||||
# present in any case - the following command should get you out of trouble:
|
||||
# \$cert = Get-ChildItem -Path 'Cert:\LocalMachine\My\\${_thumb}' | Select-Object -First 1 | Remove-Item
|
||||
|
||||
\$flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]'MachineKeySet,PersistKeySet'
|
||||
\$certs = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
\$certs.Import(\$pfxBytes, '${_pfx_pass}', \$flags)
|
||||
|
||||
\$store = [System.Security.Cryptography.X509Certificates.X509Store]::new('My', 'LocalMachine')
|
||||
\$store.Open('ReadWrite')
|
||||
\$store.AddRange(\$certs)
|
||||
\$store.Close()
|
||||
Write-Host "Installed certs into LocalMachine\\My"
|
||||
|
||||
\$ts = Get-CimInstance -Namespace root/cimv2/terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='${_listener}'"
|
||||
if (-not \$ts) { throw "Listener '${_listener}' not found." }
|
||||
Set-CimInstance -InputObject \$ts -Property @{SSLCertificateSHA1Hash="${_thumb}"}
|
||||
Write-Host "Listener ${_listener} now uses ${_thumb}"
|
||||
|
||||
${_restart_ps}
|
||||
PSEOF
|
||||
)
|
||||
_debug "Powershell script:${_ps1}"
|
||||
|
||||
# ---- run over a single ssh connection ----------------------------------
|
||||
_ssh_opts="-o BatchMode=yes -p $_port"
|
||||
if [ -n "$DEPLOY_WIN_RDP_SSH_OPTS" ]; then
|
||||
_ssh_opts="$_ssh_opts $DEPLOY_WIN_RDP_SSH_OPTS"
|
||||
fi
|
||||
|
||||
_info "Deploying to $DEPLOY_WIN_RDP_HOST ..."
|
||||
# shellcheck disable=SC2086
|
||||
if ! printf '%s\n' "$_ps1" | ssh $_ssh_opts "$_target" \
|
||||
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -'; then
|
||||
_err "Remote install failed. Re-run acme.sh with --debug to see the PowerShell output."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Certificate for $_cdomain deployed and bound to $_listener on $DEPLOY_WIN_RDP_HOST."
|
||||
return 0
|
||||
}
|
||||
+41
-10
@@ -7,6 +7,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting
|
||||
Options:
|
||||
One984HOSTING_Username Username
|
||||
One984HOSTING_Password Password
|
||||
One984HOSTING_TOTP_Secret Base32 TOTP shared secret. Required only if the account has 2FA enabled. Requires oathtool. Used to mint the OTP code automatically at login so cron renewals keep working.
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2851
|
||||
Author: Adrian Fedoreanu
|
||||
'
|
||||
@@ -124,11 +125,28 @@ _1984hosting_login() {
|
||||
_debug "Login to 1984Hosting as user $One984HOSTING_Username."
|
||||
username=$(printf '%s' "$One984HOSTING_Username" | _url_encode)
|
||||
password=$(printf '%s' "$One984HOSTING_Password" | _url_encode)
|
||||
url="https://1984.hosting/api/auth/"
|
||||
|
||||
_get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
|
||||
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
|
||||
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
|
||||
# When 2FA is enabled, mint a fresh TOTP code from the stored shared secret.
|
||||
# Empty otpkey is accepted by the server when 2FA is off.
|
||||
otpkey=""
|
||||
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
|
||||
if ! _exists oathtool; then
|
||||
_err "oathtool is required to use One984HOSTING_TOTP_Secret for 2FA. Please install it."
|
||||
return 1
|
||||
fi
|
||||
otpcode="$(oathtool --base32 --totp "$One984HOSTING_TOTP_Secret" 2>/dev/null)"
|
||||
if [ -z "$otpcode" ]; then
|
||||
_err "Failed to generate TOTP code from One984HOSTING_TOTP_Secret."
|
||||
return 1
|
||||
fi
|
||||
otpkey="$(printf '%s' "$otpcode" | _url_encode)"
|
||||
fi
|
||||
|
||||
# Fetch the login page to obtain CSRF and session cookies.
|
||||
# Note: _get sets the global 'url', so assign the auth URL afterwards.
|
||||
_get "https://1984.hosting/accounts/login/" >/dev/null
|
||||
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
|
||||
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
|
||||
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
|
||||
@@ -140,17 +158,23 @@ _1984hosting_login() {
|
||||
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
||||
response="$(_post "username=$username&password=$password&otpkey=" $url)"
|
||||
url="https://1984.hosting/api/auth/"
|
||||
response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
|
||||
response="$(echo "$response" | _normalizeJson)"
|
||||
_debug2 response "$response"
|
||||
|
||||
if _contains "$response" '"loggedin": true'; then
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
|
||||
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
|
||||
export One984HOSTING_SESSIONID_COOKIE
|
||||
export One984HOSTING_CSRFTOKEN_COOKIE
|
||||
_saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username"
|
||||
_saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password"
|
||||
if [ -n "$One984HOSTING_TOTP_Secret" ]; then
|
||||
_saveaccountconf_mutable One984HOSTING_TOTP_Secret "$One984HOSTING_TOTP_Secret"
|
||||
else
|
||||
_clearaccountconf_mutable One984HOSTING_TOTP_Secret
|
||||
fi
|
||||
_saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
|
||||
_saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
|
||||
return 0
|
||||
@@ -161,6 +185,7 @@ _1984hosting_login() {
|
||||
_check_credentials() {
|
||||
One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}"
|
||||
One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}"
|
||||
One984HOSTING_TOTP_Secret="${One984HOSTING_TOTP_Secret:-$(_readaccountconf_mutable One984HOSTING_TOTP_Secret)}"
|
||||
if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then
|
||||
One984HOSTING_Username=""
|
||||
One984HOSTING_Password=""
|
||||
@@ -225,9 +250,15 @@ _get_root() {
|
||||
|
||||
# Usage: _get_zone_id url domain.com
|
||||
# Returns zone id for domain.com
|
||||
# Memoized per-domain so add/rm don't re-fetch the same zone list within a run.
|
||||
# Keyed on domain (not url) since the url is always the domains listing.
|
||||
_get_zone_id() {
|
||||
url=$1
|
||||
domain=$2
|
||||
if [ "$_zone_id_for" = "$domain" ] && [ -n "$_zone_id" ]; then
|
||||
_debug2 _zone_id "$_zone_id (cached)"
|
||||
return 0
|
||||
fi
|
||||
_htmlget "$url" "$domain"
|
||||
_zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)"
|
||||
_debug2 _zone_id "$_zone_id"
|
||||
@@ -235,6 +266,7 @@ _get_zone_id() {
|
||||
_err "Error getting _zone_id for $2."
|
||||
return 1
|
||||
fi
|
||||
_zone_id_for="$domain"
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -257,9 +289,8 @@ _htmlget() {
|
||||
|
||||
# Add extra headers to request
|
||||
_authpost() {
|
||||
url="https://1984.hosting/domains"
|
||||
_get_zone_id "$url" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
|
||||
_get_zone_id "https://1984.hosting/domains" "$_domain"
|
||||
csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
|
||||
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
|
||||
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
|
||||
export _H3="X-CSRFToken: $csrf_header"
|
||||
|
||||
+11
-1
@@ -37,6 +37,16 @@ dns_acmedns_add() {
|
||||
ACMEDNS_PASSWORD="${ACMEDNS_PASSWORD:-$(_readdomainconf ACMEDNS_PASSWORD)}"
|
||||
ACMEDNS_SUBDOMAIN="${ACMEDNS_SUBDOMAIN:-$(_readdomainconf ACMEDNS_SUBDOMAIN)}"
|
||||
|
||||
#for compatibility: old versions stored ACMEDNS_UPDATE_URL in the account
|
||||
#conf (issue 3899). Do not clear it here: it must stay available for the
|
||||
#other domains that have not migrated to their domain conf yet.
|
||||
if [ -z "$ACMEDNS_BASE_URL" ]; then
|
||||
_acmedns_update_url="$(_readaccountconf_mutable ACMEDNS_UPDATE_URL)"
|
||||
if [ "$_acmedns_update_url" ]; then
|
||||
ACMEDNS_BASE_URL="$(echo "$_acmedns_update_url" | sed 's#/update$##')"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$ACMEDNS_BASE_URL" = "" ]; then
|
||||
ACMEDNS_BASE_URL="https://auth.acme-dns.io"
|
||||
fi
|
||||
@@ -71,7 +81,7 @@ dns_acmedns_add() {
|
||||
data="{\"subdomain\":\"$ACMEDNS_SUBDOMAIN\", \"txt\": \"$txtvalue\"}"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST")"
|
||||
response="$(_post "$data" "$ACMEDNS_UPDATE_URL" "" "POST" "application/json")"
|
||||
_debug response "$response"
|
||||
|
||||
if ! echo "$response" | grep "\"$txtvalue\"" >/dev/null; then
|
||||
|
||||
+20
-4
@@ -18,7 +18,9 @@ Ali_DNS_API="https://alidns.aliyuncs.com/"
|
||||
|
||||
#Usage: dns_ali_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_ali_add() {
|
||||
fulldomain=$1
|
||||
# the API only accepts punycode for IDN domains, and a raw UTF-8 domain
|
||||
# also breaks the request signature (issue 4733)
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
_prepare_ali_credentials || return 1
|
||||
@@ -33,7 +35,7 @@ dns_ali_add() {
|
||||
}
|
||||
|
||||
dns_ali_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
Ali_Key="${Ali_Key:-$(_readaccountconf_mutable Ali_Key)}"
|
||||
Ali_Secret="${Ali_Secret:-$(_readaccountconf_mutable Ali_Secret)}"
|
||||
@@ -69,8 +71,8 @@ _ali_rest() {
|
||||
ign="$2"
|
||||
mtd="${3:-GET}"
|
||||
|
||||
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _url_encode upper-hex)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(printf "%s" "$signature" | _url_encode upper-hex)
|
||||
signature=$(printf "%s" "$mtd&%2F&$(printf "%s" "$query" | _ali_urlencode_upper)" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64)
|
||||
signature=$(printf "%s" "$signature" | _ali_urlencode_upper)
|
||||
url="$endpoint?Signature=$signature"
|
||||
|
||||
if [ "$mtd" = "GET" ]; then
|
||||
@@ -96,6 +98,20 @@ _ali_rest() {
|
||||
fi
|
||||
}
|
||||
|
||||
# stdin stdout
|
||||
# The Aliyun signature requires percent-encoding with upper-case hex.
|
||||
# Do not use "_url_encode upper-hex" here: this file is also bundled by
|
||||
# third parties (e.g. Proxmox VE proxmox-acme) whose older copies of the
|
||||
# acme.sh function library ignore the upper-hex argument and output
|
||||
# lower-case hex, which invalidates the signature.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6272
|
||||
_ali_urlencode_upper() {
|
||||
{
|
||||
_url_encode
|
||||
echo
|
||||
} | sed 's/%a/%A/g;s/%b/%B/g;s/%c/%C/g;s/%d/%D/g;s/%e/%E/g;s/%f/%F/g;s/%\(.\)a/%\1A/g;s/%\(.\)b/%\1B/g;s/%\(.\)c/%\1C/g;s/%\(.\)d/%\1D/g;s/%\(.\)e/%\1E/g;s/%\(.\)f/%\1F/g'
|
||||
}
|
||||
|
||||
_ali_nonce() {
|
||||
if [ "$ACME_OPENSSL_BIN" ]; then
|
||||
"$ACME_OPENSSL_BIN" rand -hex 16 2>/dev/null && return 0
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
dns_arubabusiness_info='ArubaBusiness
|
||||
Site: business.aruba.it
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness
|
||||
Options:
|
||||
AB_Key Your ArubaBusiness API Key
|
||||
AB_User Your account user
|
||||
AB_Pass Your account password
|
||||
'
|
||||
|
||||
#
|
||||
# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type.
|
||||
# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges
|
||||
#
|
||||
|
||||
ARUBABUSINESS_API='https://api.arubabusiness.it'
|
||||
|
||||
######## Public functions ########
|
||||
|
||||
#
|
||||
# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333
|
||||
#
|
||||
# Add a new TXT record whose name and value match the given domain and value
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the name of the TXT record
|
||||
# _txt_value: $2 - the value of the TXT record
|
||||
# _body
|
||||
# dns_details
|
||||
# domain_id
|
||||
# dns_record_id
|
||||
# response
|
||||
#
|
||||
dns_arubabusiness_add() {
|
||||
_full_domain=$1
|
||||
_txt_value=$2
|
||||
|
||||
if ! _ab_authenticate; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_domain_id "$_full_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
|
||||
# This is very unlikely, but allow the process to use the existing record
|
||||
_info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }"
|
||||
|
||||
_debug "Adding TXT record with name: $_full_domain and value: $_txt_value"
|
||||
|
||||
if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then
|
||||
_err "Failed to add TXT record with name: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Sleeping 10 seconds to let ArubaBusiness do its magic"
|
||||
_sleep 10
|
||||
|
||||
# Refresh dns details and check that the record was really added
|
||||
if ! _ab_dns_details "$root_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
|
||||
# This should never happen
|
||||
_err "The TXT record with name: $_full_domain was not set"
|
||||
_err "Please check that the dns records are clean"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Added TXT record with id: $dns_record_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333
|
||||
#
|
||||
# Remove the TXT record whose name and value match the given domain and value
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the name of the TXT record
|
||||
# _txt_value: $2 - the value of the TXT record
|
||||
# dns_details
|
||||
# dns_record_id
|
||||
#
|
||||
dns_arubabusiness_rm() {
|
||||
_full_domain=$1
|
||||
_txt_value=$2
|
||||
|
||||
if ! _ab_authenticate; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_domain_id "$_full_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then
|
||||
_err "Could not retrieve the record id for: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record: $dns_record_id"
|
||||
if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then
|
||||
_err "Failed to delete TXT record: $dns_record_id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted TXT record: $dns_record_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ########
|
||||
|
||||
#
|
||||
# Usage: _ab_domain_id _acme-challenge.www.domain.com
|
||||
#
|
||||
# Split the input domain into subdomain + root domain and get the id of the root domain
|
||||
#
|
||||
# Variables
|
||||
# _full_domain: $1 - the domain whose root needs to be extracted
|
||||
# _domain_sections
|
||||
# _current_index
|
||||
# _candidate_subdomain
|
||||
# _candidate_domain
|
||||
# sub_domain
|
||||
# root_domain
|
||||
# domain_id
|
||||
# dns_details: a json containing all dns records registered on the root domain
|
||||
#
|
||||
# Example
|
||||
# _get_root _acme-challenge.www.domain.com
|
||||
#
|
||||
# Should return
|
||||
# sub_domain=_acme-challenge.www
|
||||
# root_domain=domain.com
|
||||
# domain_id=123123123123
|
||||
# dns_details="{JSON_CONTENT}"
|
||||
#
|
||||
_ab_domain_id() {
|
||||
_full_domain=$1
|
||||
|
||||
_info "Attempting to retrieve root domain details for: $_full_domain"
|
||||
|
||||
_domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1)
|
||||
|
||||
if [ "$_domain_sections" -lt 1 ]; then
|
||||
_err "Invalid input $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_current_index=1
|
||||
while true; do
|
||||
_candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi)
|
||||
_candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections")
|
||||
|
||||
if ! _ab_dns_details "$_candidate_domain"; then
|
||||
_debug2 "Could not fetch dns details for: $_candidate_domain"
|
||||
_current_index=$(_math "$_current_index" + 1)
|
||||
|
||||
# Fail if there are no candidates left
|
||||
if [ "$_current_index" -gt "$_domain_sections" ]; then
|
||||
_err "Could not determine the root domain for: $_full_domain"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
sub_domain="$_candidate_subdomain"
|
||||
root_domain="$_candidate_domain"
|
||||
# Extract the domain id, which is an integer and contains no commas
|
||||
domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')"
|
||||
|
||||
if [ -z "$domain_id" ]; then
|
||||
_err "Could not determine the domain id for: $root_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Retrieved root domain id: $domain_id"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}"
|
||||
#
|
||||
# Extract the record id of the first TXT record whose name and content match the input values
|
||||
#
|
||||
# Variables
|
||||
# _record_name: $1
|
||||
# _txt_value: $2
|
||||
# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain'
|
||||
# _record_ids
|
||||
# _record_names
|
||||
# _record_types
|
||||
# _record_contents
|
||||
# _record_ids_count
|
||||
# _record_names_count
|
||||
# _record_types_count
|
||||
# _record_contents_count
|
||||
# _i
|
||||
# dns_record_id
|
||||
#
|
||||
# Notes
|
||||
# TXT correspond to record type 5
|
||||
# ArubaBusiness appends a terminating dot (.) to the record name
|
||||
# The content field may contain the following character sequence: \"
|
||||
# All record names are always converted to lowercase
|
||||
#
|
||||
_ab_dns_record_id() {
|
||||
_record_name=$1
|
||||
_txt_value=$2
|
||||
_dns_details=$3
|
||||
|
||||
_record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case)
|
||||
|
||||
# Extract the record ids, which are integers and contain no commas, colons or spaces
|
||||
# The first id is skipped because it refers to the domain id
|
||||
_record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
|
||||
|
||||
# Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes
|
||||
# The first name is skipped because it refers to the domain name
|
||||
_record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ')
|
||||
|
||||
# Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces
|
||||
# The first type is skipped because it refers to the domain type
|
||||
_record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
|
||||
|
||||
# Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals
|
||||
# Note: There is no domain related entry here
|
||||
# Note: A " character is appended at the end of each content to make it easier to process the list later
|
||||
_record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#')
|
||||
|
||||
_info "IDS: $_record_ids"
|
||||
_info "NAMES: $_record_names"
|
||||
_info "TYPEs: $_record_types"
|
||||
_info "CONTENTS: $_record_contents"
|
||||
|
||||
_record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l)
|
||||
_record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l)
|
||||
_record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l)
|
||||
_record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l)
|
||||
|
||||
_info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
|
||||
|
||||
if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then
|
||||
_err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value"
|
||||
|
||||
_i=1
|
||||
while [ "$_i" -le "$_record_ids_count" ]; do
|
||||
_current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i")
|
||||
_current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i")
|
||||
_current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i")
|
||||
|
||||
if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then
|
||||
dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i")
|
||||
_info "Found matching record with id: $dns_record_id"
|
||||
return 0
|
||||
else
|
||||
_debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'"
|
||||
fi
|
||||
_i=$(_math "$_i" + 1)
|
||||
done
|
||||
|
||||
_debug2 "No matching record was found in $_dns_details"
|
||||
return 1
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_dns_details domain.com
|
||||
#
|
||||
# Retrieve dns info for the given input domain
|
||||
#
|
||||
# Variables
|
||||
# _domain: $1
|
||||
# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0)
|
||||
# response
|
||||
#
|
||||
_ab_dns_details() {
|
||||
_domain=$1
|
||||
|
||||
if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
dns_details="$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_authenticate
|
||||
#
|
||||
# Read account conf, update domain conf and perform user authentication to acquire an access token
|
||||
#
|
||||
# Variables
|
||||
# AB_Key
|
||||
# AB_User
|
||||
# AB_Pass
|
||||
# AB_Token
|
||||
#
|
||||
_ab_authenticate() {
|
||||
AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}"
|
||||
AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}"
|
||||
AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}"
|
||||
|
||||
if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then
|
||||
AB_Key=""
|
||||
AB_User=""
|
||||
AB_Pass=""
|
||||
_err "Either the ArubaBusiness API key, the user or the password has not been defined yet."
|
||||
_err "Please configure them and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable AB_Key "$AB_Key"
|
||||
_saveaccountconf_mutable AB_User "$AB_User"
|
||||
_saveaccountconf_mutable AB_Pass "$AB_Pass"
|
||||
|
||||
if ! _ab_get_token || [ -z "$AB_Token" ]; then
|
||||
_err "Failed to acquire an access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_get_token
|
||||
#
|
||||
# Try acquiring a temporary access token. The token should have a 24h lifespan
|
||||
#
|
||||
# Variables
|
||||
# _ab_user_enc
|
||||
# _ab_pass_enc
|
||||
# _ab_authdata
|
||||
# AB_User
|
||||
# AB_Pass
|
||||
# AB_Token
|
||||
# response
|
||||
# _H2
|
||||
#
|
||||
_ab_get_token() {
|
||||
_ab_user_enc=$(printf "%s" "$AB_User" | _url_encode)
|
||||
_ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode)
|
||||
_ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc"
|
||||
|
||||
_H2="Content-Type: application/x-www-form-urlencoded"
|
||||
|
||||
if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then
|
||||
_err "Authentication failure"
|
||||
return 1
|
||||
fi
|
||||
|
||||
AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')"
|
||||
|
||||
if [ -z "$AB_Token" ]; then
|
||||
_err "Could not extract access token"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Acquired access token"
|
||||
return 0
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_rest POST "example/endpoint" "password=123"
|
||||
#
|
||||
# Perform a REST request using the given method, endpoint and data
|
||||
#
|
||||
# Variables
|
||||
# _method: $1 - The http method
|
||||
# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API)
|
||||
# _data: $3 - The body of the request (optional)
|
||||
# _key_trimmed
|
||||
# _token_trimmed
|
||||
# _ret_code
|
||||
# AB_Key
|
||||
# AB_Token
|
||||
# ARUBABUSINESS_API
|
||||
# _H1
|
||||
# _H2
|
||||
# _H3
|
||||
# _H4
|
||||
#
|
||||
_ab_rest() {
|
||||
_method=$1
|
||||
_endpoint="$2"
|
||||
_data="$3"
|
||||
|
||||
_key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"')
|
||||
_token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"')
|
||||
|
||||
_H1="Accept: application/json"
|
||||
|
||||
if [ -z "$_H2" ]; then
|
||||
# Default to application/json
|
||||
_H2="Content-Type: application/json"
|
||||
fi
|
||||
|
||||
if [ "$_key_trimmed" ]; then
|
||||
_H3="Authorization-Key: $_key_trimmed"
|
||||
else
|
||||
_err "Missing Api Key"
|
||||
_ab_cleanup_headers
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_token_trimmed" ]; then
|
||||
_H4="Authorization: Bearer $_token_trimmed"
|
||||
else
|
||||
_debug "No access token set"
|
||||
fi
|
||||
|
||||
if [ "$_method" != "GET" ]; then
|
||||
response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")"
|
||||
else
|
||||
response="$(_get "$ARUBABUSINESS_API/$_endpoint")"
|
||||
fi
|
||||
|
||||
_ret_code=$?
|
||||
|
||||
if [ "$_ret_code" = "0" ] && _ab_call_is_success; then
|
||||
# Normalize the json response
|
||||
response="$(printf "%s" "$response" | _normalizeJson)"
|
||||
_ret_code=0
|
||||
else
|
||||
_err "Failed to call endpoint: $_endpoint"
|
||||
_ret_code=1
|
||||
fi
|
||||
|
||||
_ab_cleanup_headers
|
||||
|
||||
return $_ret_code
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_cleanup_headers
|
||||
#
|
||||
# Unset header variables to avoid interfering with other calls
|
||||
#
|
||||
# Variables
|
||||
# _H1
|
||||
# _H2
|
||||
# _H3
|
||||
# _H4
|
||||
#
|
||||
_ab_cleanup_headers() {
|
||||
# Cleanup request headers
|
||||
unset _H1 _H2 _H3 _H4 _H5
|
||||
|
||||
# Cleanup response headers
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
: >"$HTTP_HEADER"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Usage: _ab_call_is_success
|
||||
#
|
||||
# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled)
|
||||
#
|
||||
# Variables
|
||||
# _status
|
||||
# _http_status
|
||||
# _success_http_codes
|
||||
# HTTP_HEADER
|
||||
#
|
||||
_ab_call_is_success() {
|
||||
_success_http_codes="200 201 202 204"
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
_http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)
|
||||
for _status in $_success_http_codes; do
|
||||
if [ "$_status" = "$_http_status" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
+10
-1
@@ -139,12 +139,21 @@ _get_autodns_zone() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Escape the XML special characters (& < > ' ") so that credentials
|
||||
# containing them do not break the request document (issue 5317).
|
||||
_autodns_xml_encode() {
|
||||
sed "s/&/\&/g;s/</\</g;s/>/\>/g;s/'/\'/g;s/\"/\"/g"
|
||||
}
|
||||
|
||||
_build_request_auth_xml() {
|
||||
_autodns_user_xml="$(printf "%s" "$AUTODNS_USER" | _autodns_xml_encode)"
|
||||
_autodns_password_xml="$(printf "%s" "$AUTODNS_PASSWORD" | _autodns_xml_encode)"
|
||||
_autodns_context_xml="$(printf "%s" "$AUTODNS_CONTEXT" | _autodns_xml_encode)"
|
||||
printf "<auth>
|
||||
<user>%s</user>
|
||||
<password>%s</password>
|
||||
<context>%s</context>
|
||||
</auth>" "$AUTODNS_USER" "$AUTODNS_PASSWORD" "$AUTODNS_CONTEXT"
|
||||
</auth>" "$_autodns_user_xml" "$_autodns_password_xml" "$_autodns_context_xml"
|
||||
}
|
||||
|
||||
# Arguments:
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ Options:
|
||||
# All `_sleep` commands are included to avoid Route53 throttling, see
|
||||
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
|
||||
|
||||
AWS_HOST="route53.amazonaws.com"
|
||||
# Updated from "route53.amazonaws.com"
|
||||
AWS_HOST="route53.global.api.aws"
|
||||
AWS_URL="https://$AWS_HOST"
|
||||
|
||||
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
|
||||
|
||||
+278
-51
@@ -49,26 +49,95 @@ Options:
|
||||
Baidu_SK SecretAccessKey
|
||||
OptionsAlt:
|
||||
Baidu_BCD_Host API host, default: bcd.baidubce.com
|
||||
Baidu_DNS_Host New DNS API host, default: dns.baidubce.com
|
||||
Baidu_API_Preference Engine preference, default: auto
|
||||
Baidu_BCD_Version API version number, default: 1
|
||||
Baidu_BCD_Expire Signature expiration seconds, default: 3600
|
||||
Baidu_View Resolve view, default: DEFAULT
|
||||
Baidu_Line New DNS line, default: default
|
||||
Baidu_TTL Resolve ttl seconds, default: 300
|
||||
Baidu_RM_Max Max records to delete in one run, default: 20
|
||||
'
|
||||
|
||||
BAIDU_BCD_DEFAULT_HOST="bcd.baidubce.com"
|
||||
BAIDU_DNS_DEFAULT_HOST="dns.baidubce.com"
|
||||
|
||||
# --- Public API ---
|
||||
dns_baidu_add() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _baidu_prepare_record "$fulldomain"; then
|
||||
_baidu_err "baidu_prepare_record failed for add: $fulldomain"
|
||||
if ! _baidu_run_with_fallback "add" "$fulldomain" "$txtvalue"; then
|
||||
_baidu_err "all baidu api engines failed for add: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_baidu_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _baidu_run_with_fallback "rm" "$fulldomain" "$txtvalue"; then
|
||||
_baidu_err "all baidu api engines failed for delete: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_run_with_fallback() {
|
||||
_action="$1"
|
||||
_fulldomain="$2"
|
||||
_txtvalue="$3"
|
||||
|
||||
if ! _baidu_load_credentials; then
|
||||
_baidu_err "baidu_load_credentials failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for _baidu_api_engine in $(_baidu_engine_order); do
|
||||
if ! _baidu_prepare_record "$_fulldomain"; then
|
||||
_baidu_info "prepare failed for engine: $_baidu_api_engine"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ "$_action" = "add" ]; then
|
||||
if _baidu_add_record "$_txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
if _baidu_rm_record "$_txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_baidu_info "engine failed, try next if available: $_baidu_api_engine"
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_baidu_engine_order() {
|
||||
_pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")"
|
||||
case "$_pref" in
|
||||
legacy)
|
||||
printf "%s" "legacy new"
|
||||
;;
|
||||
new)
|
||||
printf "%s" "new legacy"
|
||||
;;
|
||||
*)
|
||||
printf "%s" "new legacy"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_baidu_add_record() {
|
||||
_txtvalue="$1"
|
||||
|
||||
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for add: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
@@ -85,16 +154,28 @@ dns_baidu_add() {
|
||||
_ttl="300"
|
||||
;;
|
||||
esac
|
||||
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
|
||||
txtvalue="$(_baidu_trim_ws "$txtvalue")"
|
||||
|
||||
txtvalue="$(_baidu_trim_ws "$_txtvalue")"
|
||||
_record_domain="$(_baidu_trim_ws "$_record_domain")"
|
||||
_zone_name="$(_baidu_trim_ws "$_zone_name")"
|
||||
|
||||
_body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")"
|
||||
|
||||
if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: add record"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
_line="$(_baidu_trim_ws "${Baidu_Line:-default}")"
|
||||
if [ -z "$_line" ]; then
|
||||
_line="default"
|
||||
fi
|
||||
_body="$(_baidu_payload_add_txt_dns "$_record_domain" "$txtvalue" "$_ttl" "$_line")"
|
||||
if ! _baidu_dns_call "POST" "/v1/dns/zone/${_zone_name}/record" "$_body"; then
|
||||
_baidu_err "baidu_dns_call failed: add record"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_view="$(_baidu_trim_ws "${Baidu_View:-DEFAULT}")"
|
||||
_body="$(_baidu_payload_add_txt "$_zone_name" "$_record_domain" "$txtvalue" "$_ttl" "$_view")"
|
||||
if ! _baidu_bcd_post "/domain/resolve/add" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: add record"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if _baidu_is_api_error "$response"; then
|
||||
@@ -105,16 +186,10 @@ dns_baidu_add() {
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_baidu_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
_baidu_rm_record() {
|
||||
_txtvalue="$1"
|
||||
|
||||
if ! _baidu_prepare_record "$fulldomain"; then
|
||||
_baidu_err "baidu_prepare_record failed for delete: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
|
||||
if ! _baidu_find_record_ids_current "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for delete: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
@@ -138,28 +213,37 @@ dns_baidu_rm() {
|
||||
fi
|
||||
|
||||
for _rid in $_ids; do
|
||||
_body="$(_baidu_payload_delete "$_zone_name" "$_rid")"
|
||||
if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "$response"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
if ! _baidu_dns_call "DELETE" "/v1/dns/zone/${_zone_name}/record/${_rid}" ""; then
|
||||
_baidu_err "baidu_dns_call failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_body="$(_baidu_payload_delete "$_zone_name" "$_rid")"
|
||||
if ! _baidu_bcd_post "/domain/resolve/delete" "$_body"; then
|
||||
_baidu_err "baidu_bcd_post failed: delete recordId=$_rid"
|
||||
return 1
|
||||
fi
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "$response"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
_left_ids="$_BAIDU_FIND_RESULT"
|
||||
if [ -z "$_left_ids" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$_left_ids" ]; then
|
||||
_baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "legacy" ]; then
|
||||
if ! _baidu_find_record_ids "$_zone_name" "$_record_domain" "TXT" "$_txtvalue"; then
|
||||
_baidu_err "baidu_find_record_ids failed for delete verify: $_record_domain.$_zone_name"
|
||||
return 1
|
||||
fi
|
||||
_left_ids="$_BAIDU_FIND_RESULT"
|
||||
if [ -z "$_left_ids" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$_left_ids" ]; then
|
||||
_baidu_err "delete verification failed: $_record_domain.$_zone_name still has TXT records"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
@@ -182,6 +266,7 @@ _baidu_load_credentials() {
|
||||
_saveaccountconf_mutable Baidu_SK "$Baidu_SK"
|
||||
|
||||
BAIDU_BCD_HOST="${Baidu_BCD_Host:-$BAIDU_BCD_DEFAULT_HOST}"
|
||||
BAIDU_DNS_HOST="${Baidu_DNS_Host:-$BAIDU_DNS_DEFAULT_HOST}"
|
||||
BAIDU_BCD_VERSION="${Baidu_BCD_Version:-1}"
|
||||
|
||||
return 0
|
||||
@@ -189,13 +274,16 @@ _baidu_load_credentials() {
|
||||
|
||||
_baidu_prepare_record() {
|
||||
_fulldomain="$1"
|
||||
if ! _baidu_load_credentials; then
|
||||
_baidu_err "baidu_load_credentials failed"
|
||||
return 1
|
||||
fi
|
||||
if ! _baidu_get_root "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone for $_fulldomain"
|
||||
return 1
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
if ! _baidu_get_root_dns "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone by new dns api for $_fulldomain"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if ! _baidu_get_root "$_fulldomain"; then
|
||||
_baidu_err "Could not find zone by legacy bcd api for $_fulldomain"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_record_domain="$_sub_domain"
|
||||
_zone_name="$_domain"
|
||||
@@ -234,6 +322,43 @@ _baidu_get_root() {
|
||||
done
|
||||
}
|
||||
|
||||
_baidu_get_root_dns() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
_baidu_err "invalid domain: $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _baidu_dns_call "GET" "/v1/dns/zone/${h}/record" ""; then
|
||||
_baidu_info "baidu_dns_call failed: list zones"
|
||||
elif ! _baidu_is_api_error "$response" && (_contains "$response" "\"records\"" || _contains "$response" "\"maxKeys\""); then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
if [ "$_sub_domain" = "$_domain" ]; then
|
||||
_sub_domain="@"
|
||||
fi
|
||||
_baidu_info "zone matched by dns api: $_domain (host: $_sub_domain)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_baidu_find_record_ids_current() {
|
||||
if [ "$_baidu_api_engine" = "new" ]; then
|
||||
_baidu_find_record_ids_dns "$@"
|
||||
else
|
||||
_baidu_find_record_ids "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
_baidu_find_record_ids() {
|
||||
_zone_name="$1"
|
||||
_record_domain="$2"
|
||||
@@ -293,6 +418,39 @@ EOF
|
||||
_BAIDU_FIND_RESULT="$_ids"
|
||||
}
|
||||
|
||||
_baidu_find_record_ids_dns() {
|
||||
_zone_name="$1"
|
||||
_record_domain="$2"
|
||||
_rdtype="$3"
|
||||
_rdata="$4"
|
||||
_BAIDU_FIND_RESULT=""
|
||||
|
||||
if ! _baidu_dns_call "GET" "/v1/dns/zone/${_zone_name}/record" ""; then
|
||||
_baidu_err "baidu_dns_call failed: list records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _baidu_is_api_error "$response"; then
|
||||
_baidu_err "baidu_dns error: $(_baidu_json_get_str "$response" "code") $(_baidu_json_get_str "$response" "message")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_normalized="$(printf "%s" "$response" | _normalizeJson)"
|
||||
_records=$(printf "%s" "$_normalized" | sed 's/},{/}\n{/g')
|
||||
_ids=""
|
||||
|
||||
while IFS= read -r _line; do
|
||||
_id="$(_baidu_match_record_id_dns "$_line" "$_record_domain" "$_rdtype" "$_rdata")"
|
||||
if [ "$_id" ]; then
|
||||
_ids="$_ids $_id"
|
||||
fi
|
||||
done <<EOF
|
||||
$_records
|
||||
EOF
|
||||
|
||||
_BAIDU_FIND_RESULT="$_ids"
|
||||
}
|
||||
|
||||
# --- HTTP ---
|
||||
_baidu_bcd_post() {
|
||||
_api_path="$1"
|
||||
@@ -317,18 +475,17 @@ _baidu_bcd_post() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_BCD_HOST"
|
||||
_H5=""
|
||||
|
||||
_url="https://${BAIDU_BCD_HOST}${_uri}"
|
||||
_signed_headers_dbg="$(printf "%s" "$_auth" | cut -d / -f 5)"
|
||||
_baidu_info "POST ${_uri}"
|
||||
_baidu_info "signedHeaders: $_signed_headers_dbg"
|
||||
_baidu_info "payload_sha256: $_payload_hash"
|
||||
_baidu_debug "baidu_bcd.http.payload" "$(_baidu_dbg_trim "$(_baidu_redact_txt "$_payload")")"
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_BCD_HOST"
|
||||
_H5=""
|
||||
response="$(_post "$_payload" "$_url" "" "POST" "$_content_type")"
|
||||
_ret="$?"
|
||||
_baidu_info "ret: $_ret"
|
||||
@@ -344,6 +501,56 @@ _baidu_bcd_post() {
|
||||
return 0
|
||||
}
|
||||
|
||||
_baidu_dns_call() {
|
||||
_method="$1"
|
||||
_uri="$2"
|
||||
_payload="$3"
|
||||
_content_type="application/json"
|
||||
_attempt=1
|
||||
_max_attempts=3
|
||||
|
||||
while [ "$_attempt" -le "$_max_attempts" ]; do
|
||||
_ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
_payload_hash="$(printf "%s" "$_payload" | _digest sha256 hex)"
|
||||
|
||||
if ! _baidu_bce_auth "$_method" "$_uri" "" "$BAIDU_DNS_HOST" "$_ts" "${Baidu_BCD_Expire:-3600}" "$_content_type" "$_payload_hash"; then
|
||||
_baidu_err "baidu_dns auth failed"
|
||||
return 1
|
||||
fi
|
||||
_auth="$_BAIDU_BCE_AUTH_RESULT"
|
||||
_url="https://${BAIDU_DNS_HOST}${_uri}"
|
||||
|
||||
# Route through acme.sh's _get/_post (they honor _H1.._H5); no raw curl.
|
||||
_H1="Authorization: $_auth"
|
||||
_H2="x-bce-date: $_ts"
|
||||
_H3="x-bce-content-sha256: $_payload_hash"
|
||||
_H4="Host: $BAIDU_DNS_HOST"
|
||||
_H5="Content-Type: $_content_type"
|
||||
|
||||
if [ "$_method" = "GET" ]; then
|
||||
response="$(_get "$_url")"
|
||||
elif [ "$_method" = "DELETE" ]; then
|
||||
response="$(_post "" "$_url" "" "DELETE")"
|
||||
else
|
||||
response="$(_post "$_payload" "$_url")"
|
||||
fi
|
||||
_ret="$?"
|
||||
_baidu_info "${_method} ${_uri} ret=${_ret}"
|
||||
|
||||
# Baidu may return a business error (Exception / 平台服务繁忙) inside HTTP 200.
|
||||
if [ "$_ret" = "0" ] && ! _contains "$response" "\"code\":\"Exception\"" && ! _contains "$response" "平台服务繁忙"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$_attempt" -lt "$_max_attempts" ]; then
|
||||
sleep 2
|
||||
fi
|
||||
_attempt=$(_math "$_attempt" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# --- Auth / Signing ---
|
||||
_baidu_bce_auth() {
|
||||
# Signing algorithm (bce-auth-v1):
|
||||
@@ -499,6 +706,14 @@ _baidu_payload_add_txt() {
|
||||
printf "%s" "{\"domain\":\"${_domain}\",\"view\":\"${_view}\",\"rdType\":\"TXT\",\"ttl\":${_ttl},\"rdata\":\"${_rdata}\",\"zoneName\":\"${_zoneName}\"}"
|
||||
}
|
||||
|
||||
_baidu_payload_add_txt_dns() {
|
||||
_rr="$(printf "%s" "$1" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_value="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_ttl="$3"
|
||||
_line="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
printf "%s" "{\"rr\":\"${_rr}\",\"type\":\"TXT\",\"value\":\"${_value}\",\"ttl\":${_ttl},\"line\":\"${_line}\",\"description\":\"acme.sh\"}"
|
||||
}
|
||||
|
||||
_baidu_payload_delete() {
|
||||
_zoneName="$(_baidu_json_escape "$1")"
|
||||
_recordId="$2"
|
||||
@@ -541,6 +756,18 @@ _baidu_match_record_id() {
|
||||
printf "%s" "$_line" | _egrep_o "\"recordId\": *[0-9]*" | _head_n 1 | cut -d : -f 2 | tr -d " "
|
||||
}
|
||||
|
||||
_baidu_match_record_id_dns() {
|
||||
_line="$1"
|
||||
_rr="$(printf "%s" "$2" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_type="$(printf "%s" "$3" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
_value="$(printf "%s" "$4" | tr -d '\r\n' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
case "$_line" in
|
||||
*"\"rr\":\"${_rr}\""*"\"type\":\"${_type}\""*"\"value\":\"${_value}\""*)
|
||||
printf "%s" "$_line" | sed -n 's/.*"id":"\{0,1\}\([^",}]*\)"\{0,1\}.*/\1/p' | _head_n 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_baidu_hmac_sha256_hexkey() {
|
||||
_key_hex="$1"
|
||||
_msg="$2"
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_calrissia_info='Calrissia.be DNS API
|
||||
Site: calrissia.be
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_calrissia
|
||||
Options:
|
||||
CALRISSIA_TOKEN Personal access token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6809
|
||||
Author: Ward Hus
|
||||
'
|
||||
|
||||
CALRISSIA_API="https://my.calrissia.com/api"
|
||||
|
||||
dns_calrissia_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_calrissia_load_token || return 1
|
||||
|
||||
if ! _calrissia_get_root "$fulldomain"; then
|
||||
_err "Unable to find domain in Calrissia account for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
|
||||
_body="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120,\"prio\":0}"
|
||||
_response="$(_calrissia_request POST "/domain/$_domain_id/record" "$_body")"
|
||||
|
||||
if ! _contains "$_response" '"id"'; then
|
||||
_err "Failed to create TXT record: $_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
dns_calrissia_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_calrissia_load_token || return 1
|
||||
|
||||
if ! _calrissia_get_root "$fulldomain"; then
|
||||
_err "Unable to find domain in Calrissia account for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "domain='$_domain' id='$_domain_id' sub='$_sub_domain'"
|
||||
|
||||
# Look the record up from the API instead of relying on local state.
|
||||
# The record list is embedded in the domain object.
|
||||
_response="$(_calrissia_request GET "/domain/$_domain_id")"
|
||||
_debug2 "Response: $_response"
|
||||
|
||||
# Split the record objects onto separate lines, then match on both the
|
||||
# subdomain name and the TXT value to find the record id to delete.
|
||||
_record_id="$(printf "%s" "$_response" |
|
||||
tr '{}' '\n' |
|
||||
grep "\"name\" *: *\"$_sub_domain\"" |
|
||||
grep "\"content\" *: *\"$txtvalue\"" |
|
||||
_egrep_o '"id" *: *[0-9]+' |
|
||||
_head_n 1 |
|
||||
_egrep_o '[0-9]+')"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "No matching TXT record found for $fulldomain; nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Removing TXT record id=$_record_id from domain id=$_domain_id"
|
||||
if ! _response="$(_calrissia_request DELETE "/domain/$_domain_id/record/$_record_id")" || _contains "$_response" '"error"'; then
|
||||
_err "Failed to remove TXT record: $_response"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
####################
|
||||
# Private helpers #
|
||||
####################
|
||||
|
||||
_calrissia_load_token() {
|
||||
CALRISSIA_TOKEN="${CALRISSIA_TOKEN:-$(_readaccountconf_mutable CALRISSIA_TOKEN)}"
|
||||
if [ -z "$CALRISSIA_TOKEN" ]; then
|
||||
_err "CALRISSIA_TOKEN is not set. Generate one at https://identity.calrissia.com under API Keys."
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable CALRISSIA_TOKEN "$CALRISSIA_TOKEN"
|
||||
}
|
||||
|
||||
# Sets _domain, _domain_id, _sub_domain for a given FQDN.
|
||||
_calrissia_get_root() {
|
||||
_fqdn="$1"
|
||||
|
||||
i=1
|
||||
while true; do
|
||||
_candidate="$(printf "%s" "$_fqdn" | cut -d . -f "$i"-)"
|
||||
[ -z "$_candidate" ] && return 1
|
||||
|
||||
_debug "Trying root domain: $_candidate"
|
||||
_response="$(_calrissia_request GET "/domain?full_domain_name=$_candidate")"
|
||||
_debug2 "Response: $_response"
|
||||
|
||||
_domain_id="$(printf "%s" "$_response" |
|
||||
_egrep_o '"id" *: *[0-9]+' |
|
||||
_head_n 1 |
|
||||
_egrep_o '[0-9]+')"
|
||||
|
||||
if [ -n "$_domain_id" ]; then
|
||||
if [ "$i" = "1" ]; then
|
||||
# The FQDN itself is the zone apex, e.g. a challenge-alias domain.
|
||||
_sub_domain=""
|
||||
else
|
||||
_sub_domain="$(printf "%s" "$_fqdn" | cut -d . -f "1-$((i - 1))")"
|
||||
fi
|
||||
_domain="$_candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
_calrissia_request() {
|
||||
_method="$1"
|
||||
_path="$2"
|
||||
_body="$3"
|
||||
export _H1="Authorization: Bearer $CALRISSIA_TOKEN"
|
||||
export _H2="Accept: application/json"
|
||||
if [ "$_method" = "GET" ]; then
|
||||
_get "$CALRISSIA_API$_path"
|
||||
else
|
||||
_post "$_body" "$CALRISSIA_API$_path" "" "$_method" "application/json"
|
||||
fi
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
dns_cdmon_info='cdmon
|
||||
Site: www.cdmon.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon
|
||||
Options:
|
||||
CDMON_Key API Key
|
||||
'
|
||||
|
||||
CDMON_Api="https://api-domains.cdmon.services/api-domains"
|
||||
|
||||
######## Public functions #####################
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
dns_cdmon_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
|
||||
|
||||
if [ -z "$CDMON_Key" ]; then
|
||||
CDMON_Key=""
|
||||
_err "You didn't specify your cdmon api key yet."
|
||||
_err "Please create your key and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable CDMON_Key "$CDMON_Key"
|
||||
|
||||
_debug "First, we detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
_info "Adding record"
|
||||
if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then
|
||||
if _contains "$response" "\"status\":\"ok\""; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: fulldomain txtvalue
|
||||
# Used to remove the txt record after validation
|
||||
dns_cdmon_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
|
||||
_debug "First, we detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Removing record"
|
||||
if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then
|
||||
if _contains "$response" "\"status\":\"ok\""; then
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Delete txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Delete txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
if ! _cdmon_rest "domains/list"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "\"domain\":\"$h\""; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_cdmon_rest() {
|
||||
ep="$1"
|
||||
data="$2"
|
||||
_debug "$ep"
|
||||
|
||||
key_trimmed=$(echo "$CDMON_Key" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="apikey: $key_trimmed"
|
||||
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$CDMON_Api/$ep")"
|
||||
_ret="$?"
|
||||
|
||||
unset _H1 _H2
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
@@ -135,7 +135,7 @@ _dns_cloudns_init_check() {
|
||||
_dns_cloudns_http_api_call "dns/login.json" ""
|
||||
|
||||
if ! _contains "$response" "\"status\":\"Success\""; then
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials."
|
||||
_err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Server response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
|
||||
+4
-2
@@ -15,7 +15,8 @@ CN_API="https://beta.api.core-networks.de"
|
||||
######## Public functions #####################
|
||||
|
||||
dns_cn_add() {
|
||||
fulldomain=$1
|
||||
# Core-Networks API requires punycode for IDN domains
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _cn_login; then
|
||||
@@ -58,7 +59,8 @@ dns_cn_add() {
|
||||
}
|
||||
|
||||
dns_cn_rm() {
|
||||
fulldomain=$1
|
||||
# Core-Networks API requires punycode for IDN domains
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _cn_login; then
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
dns_comlaude_info='comlaude.com
|
||||
Site: comlaude.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_comlaude
|
||||
Options:
|
||||
COMLAUDE_USERNAME User account
|
||||
COMLAUDE_PASSWORD User password
|
||||
COMLAUDE_API_KEY generated API key
|
||||
COMLAUDE_GROUP_ID Group ID in comlaude user profile
|
||||
Get it from the https://www.comlaude.com
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7112
|
||||
'
|
||||
# ===== CONFIG =====
|
||||
COMLAUDE_API="https://api.comlaude.com"
|
||||
|
||||
########## AUTH ##########
|
||||
|
||||
_comlaude_auth() {
|
||||
_debug "Checking cached ComLaude token"
|
||||
|
||||
# Try to get token from account.conf
|
||||
if [ -z "$COMLAUDE_ACCESS_TOKEN" ]; then
|
||||
COMLAUDE_ACCESS_TOKEN="$(_readaccountconf_mutable COMLAUDE_ACCESS_TOKEN)"
|
||||
COMLAUDE_TOKEN_EXPIRY="$(_readaccountconf_mutable COMLAUDE_TOKEN_EXPIRY)"
|
||||
fi
|
||||
|
||||
_now=$(_time)
|
||||
if [ -n "$COMLAUDE_ACCESS_TOKEN" ] && [ -n "$COMLAUDE_TOKEN_EXPIRY" ] && [ "$_now" -lt "$COMLAUDE_TOKEN_EXPIRY" ]; then
|
||||
_debug "Using cached ComLaude token (valid ${COMLAUDE_TOKEN_EXPIRY} > ${_now})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "ComLaude auth..."
|
||||
_comlaude_body="{\"username\":\"$COMLAUDE_USERNAME\",\"password\":\"$COMLAUDE_PASSWORD\",\"api_key\":\"$COMLAUDE_API_KEY\"}"
|
||||
_comlaude_response="$(_post "$_comlaude_body" "$COMLAUDE_API/api_login" "" "POST" "application/json")"
|
||||
|
||||
if ! _contains "$_comlaude_response" "access_token"; then
|
||||
_err "Auth failed: $_comlaude_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
COMLAUDE_ACCESS_TOKEN=$(echo "$_comlaude_response" | _egrep_o '"access_token":"[^"]*"' | cut -d'"' -f4)
|
||||
# store expiracy from api reply l'API ("expires_in" in seconds)
|
||||
_comlaude_expires_in=$(echo "$_comlaude_response" | _egrep_o '"expires_in":[0-9]*' | cut -d: -f2)
|
||||
[ -z "$_comlaude_expires_in" ] && _comlaude_expires_in=3000 # fallback if no info
|
||||
|
||||
COMLAUDE_TOKEN_EXPIRY=$(($(_time) + _comlaude_expires_in - 60)) # margin of 60s to secure renew
|
||||
|
||||
_saveaccountconf_mutable COMLAUDE_ACCESS_TOKEN "$COMLAUDE_ACCESS_TOKEN"
|
||||
_saveaccountconf_mutable COMLAUDE_TOKEN_EXPIRY "$COMLAUDE_TOKEN_EXPIRY"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########## DOMAIN RESOLUTION ##########
|
||||
|
||||
_comlaude_get_root() {
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
if [ -z "$COMLAUDE_GROUP_ID" ]; then
|
||||
_err "Missing COMLAUDE_GROUP_ID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_comlaude_input_domain="$1"
|
||||
_comlaude_input_domain="${_comlaude_input_domain#_acme-challenge.}"
|
||||
case "$_comlaude_input_domain" in
|
||||
\*.*) _comlaude_input_domain="${_comlaude_input_domain#*.}" ;;
|
||||
esac
|
||||
|
||||
_debug "Normalized domain: $_comlaude_input_domain"
|
||||
|
||||
_comlaude_i=1
|
||||
while true; do
|
||||
_comlaude_d=$(printf "%s" "$_comlaude_input_domain" | cut -d . -f "$_comlaude_i-")
|
||||
[ -z "$_comlaude_d" ] && {
|
||||
_debug "No matching domain found for $_comlaude_input_domain"
|
||||
return 1
|
||||
}
|
||||
|
||||
# don't test unnecessary levels
|
||||
# registered domain : TLD only (no dot after cut).
|
||||
case "$_comlaude_d" in
|
||||
*.*) : ;;
|
||||
*)
|
||||
_debug "Skipping bare TLD candidate: $_comlaude_d"
|
||||
_comlaude_i=$((_comlaude_i + 1))
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
_debug "Checking domain: $_comlaude_d"
|
||||
|
||||
_comlaude_retry=0
|
||||
_comlaude_max_retry=3 # to avoid network errors
|
||||
_comlaude_DOM_ID=""
|
||||
_comlaude_Z_ID=""
|
||||
|
||||
while [ "$_comlaude_retry" -lt "$_comlaude_max_retry" ]; do
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_debug "Full URL: $COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone"
|
||||
_comlaude_response="$(_get "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/domains?filter[name]=$_comlaude_d&fields=id,name,active_zone")"
|
||||
_H1=""
|
||||
|
||||
_debug "RAW response for $_comlaude_d (try $((_comlaude_retry + 1))): $_comlaude_response"
|
||||
|
||||
# If empty -> true network issue, we retry
|
||||
if [ -z "$_comlaude_response" ]; then
|
||||
_comlaude_retry=$((_comlaude_retry + 1))
|
||||
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
# 404 -> domain not found in that level. no retry : continue
|
||||
if echo "$_comlaude_response" | grep -q '"status_code":404'; then
|
||||
_debug "404 for $_comlaude_d, moving to next level (not retrying)"
|
||||
break
|
||||
fi
|
||||
|
||||
# Domain missing (200 reply, data empty) -> continue
|
||||
if echo "$_comlaude_response" | grep -q '"data":\[\]'; then
|
||||
_debug "Empty data for $_comlaude_d, moving to next level"
|
||||
break
|
||||
fi
|
||||
|
||||
# Extraction via _egrep_o
|
||||
_comlaude_DOM_ID="$(echo "$_comlaude_response" | _egrep_o '"id":"[^"]*"' | head -n1 | cut -d':' -f2 | tr -d '"')"
|
||||
_comlaude_Z_ID="$(echo "$_comlaude_response" | _egrep_o '"active_zone":\{"id":"[^"]*"' | _egrep_o '"id":"[^"]*"$' | cut -d':' -f2 | tr -d '"')"
|
||||
|
||||
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# 200 reply but malformed data / noid -> retry transport
|
||||
_comlaude_retry=$((_comlaude_retry + 1))
|
||||
[ "$_comlaude_retry" -lt "$_comlaude_max_retry" ] && sleep 2
|
||||
done
|
||||
|
||||
_debug "_comlaude_DOM_ID=$_comlaude_DOM_ID"
|
||||
_debug "_comlaude_Z_ID=$_comlaude_Z_ID"
|
||||
|
||||
if [ -n "$_comlaude_DOM_ID" ] && [ -n "$_comlaude_Z_ID" ]; then
|
||||
_comlaude_domain="$_comlaude_d"
|
||||
_comlaude_domain_id="$_comlaude_DOM_ID"
|
||||
_comlaude_zone_id="$_comlaude_Z_ID"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_comlaude_i=$((_comlaude_i + 1))
|
||||
done
|
||||
}
|
||||
########## ADD TXT ##########
|
||||
|
||||
dns_comlaude_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
|
||||
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
|
||||
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
|
||||
if [ -z "$COMLAUDE_USERNAME" ] || [ -z "$COMLAUDE_PASSWORD" ] || [ -z "$COMLAUDE_API_KEY" ]; then
|
||||
_err "You didn't specify ComLaude credentials (COMLAUDE_USERNAME, COMLAUDE_PASSWORD, COMLAUDE_API_KEY)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Backup variable after validation
|
||||
_saveaccountconf_mutable COMLAUDE_USERNAME "$COMLAUDE_USERNAME"
|
||||
_saveaccountconf_mutable COMLAUDE_PASSWORD "$COMLAUDE_PASSWORD"
|
||||
_saveaccountconf_mutable COMLAUDE_API_KEY "$COMLAUDE_API_KEY"
|
||||
_saveaccountconf_mutable COMLAUDE_GROUP_ID "$COMLAUDE_GROUP_ID"
|
||||
|
||||
_info "Adding TXT: $fulldomain"
|
||||
_comlaude_auth || return 1
|
||||
_comlaude_get_root "$fulldomain" || return 1
|
||||
|
||||
_debug "Root: $_comlaude_domain"
|
||||
|
||||
_comlaude_data="{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"value\":\"$txtvalue\",\"ttl\":60}"
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
_comlaude_response="$(_post "$_comlaude_data" "$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records")"
|
||||
|
||||
_H1=""
|
||||
_H2=""
|
||||
if ! echo "$_comlaude_response" | grep -q '"id"'; then
|
||||
_err "Failed to create TXT"
|
||||
_debug "$_comlaude_response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
########## REMOVE TXT ##########
|
||||
|
||||
dns_comlaude_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
COMLAUDE_USERNAME="${COMLAUDE_USERNAME:-$(_readaccountconf_mutable COMLAUDE_USERNAME)}"
|
||||
COMLAUDE_PASSWORD="${COMLAUDE_PASSWORD:-$(_readaccountconf_mutable COMLAUDE_PASSWORD)}"
|
||||
COMLAUDE_API_KEY="${COMLAUDE_API_KEY:-$(_readaccountconf_mutable COMLAUDE_API_KEY)}"
|
||||
COMLAUDE_GROUP_ID="${COMLAUDE_GROUP_ID:-$(_readaccountconf_mutable COMLAUDE_GROUP_ID)}"
|
||||
|
||||
_info "Removing TXT: $fulldomain"
|
||||
|
||||
_comlaude_auth || return 1
|
||||
_comlaude_get_root "$fulldomain" || return 1
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_comlaude_encoded_name="$(printf '%s' "$fulldomain" | _url_encode)"
|
||||
_comlaude_encoded_value="$(printf '%s' "$txtvalue" | _url_encode)"
|
||||
_comlaude_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records?filter[type]=TXT&filter[name]=$_comlaude_encoded_name&filter[value]=$_comlaude_encoded_value"
|
||||
_comlaude_response="$(_get "$_comlaude_url")"
|
||||
_H1=""
|
||||
|
||||
_debug "Filtered records response: $_comlaude_response"
|
||||
|
||||
# first "id" top-level of reply (record itself,
|
||||
# always on first position of each data[] object)
|
||||
_comlaude_record_id="$(echo "$_comlaude_response" | _egrep_o '"data":\[\{"id":"[^"]*"' | _egrep_o '"[^"]*"$' | tr -d '"')"
|
||||
|
||||
if [ -z "$_comlaude_record_id" ]; then
|
||||
_info "No matching TXT record found to delete for $fulldomain / $txtvalue"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting record $_comlaude_record_id"
|
||||
|
||||
export _H1="Authorization: Bearer $COMLAUDE_ACCESS_TOKEN"
|
||||
_comlaude_del_url="$COMLAUDE_API/groups/$COMLAUDE_GROUP_ID/zones/$_comlaude_zone_id/records/$_comlaude_record_id"
|
||||
_comlaude_del_resp="$(_post "" "$_comlaude_del_url" "" "DELETE")"
|
||||
_H1=""
|
||||
|
||||
if echo "$_comlaude_del_resp" | grep -q '"error"'; then
|
||||
_err "Delete failed for $_comlaude_record_id"
|
||||
_debug "$_comlaude_del_resp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted record $_comlaude_record_id"
|
||||
return 0
|
||||
}
|
||||
+21
-7
@@ -38,7 +38,7 @@ dns_cpanel_add() {
|
||||
fi
|
||||
# adding entry
|
||||
_info "Adding the entry"
|
||||
stripped_fulldomain=$(echo "$fulldomain" | sed "s/.$_domain//")
|
||||
stripped_fulldomain="${fulldomain%."$_domain"}"
|
||||
_debug "Adding $stripped_fulldomain to $_domain zone"
|
||||
_myget "json-api/cpanel?cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=ZoneEdit&cpanel_jsonapi_func=add_zone_record&domain=$_domain&name=$stripped_fulldomain&type=TXT&txtdata=$txtvalue&ttl=1"
|
||||
if _successful_update; then return 0; fi
|
||||
@@ -128,13 +128,27 @@ _get_root() {
|
||||
_err "Primary domain list not found!"
|
||||
return 1
|
||||
fi
|
||||
for _domain in $_domains; do
|
||||
_debug "Checking if $fulldomain ends with $_domain"
|
||||
if (_endswith "$fulldomain" "$_domain"); then
|
||||
_debug "Root domain: $_domain"
|
||||
return 0
|
||||
fi
|
||||
# Pick the LONGEST matching zone, dot-anchored: with both domain.tld and
|
||||
# sub.domain.tld zones on the account, cPanel stores the record in the
|
||||
# most specific zone, so add and rm must both resolve to that one.
|
||||
_domain=""
|
||||
for d in $_domains; do
|
||||
_debug "Checking if $fulldomain ends with $d"
|
||||
# case with quoted patterns gives an exact literal suffix match;
|
||||
# _endswith treats the needle as a regex, so its dots would let
|
||||
# xdomain.tld wrongly match zone domain.tld
|
||||
case "$fulldomain" in
|
||||
"$d" | *".$d")
|
||||
if [ "${#d}" -gt "${#_domain}" ]; then
|
||||
_domain="$d"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$_domain" ]; then
|
||||
_debug "Root domain: $_domain"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_creoline_info='creoline
|
||||
Site: https://www.creoline.com/de
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_creoline
|
||||
Help: https://help.creoline.com
|
||||
Options:
|
||||
creolineApiToken
|
||||
creolineApiSecret
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7103
|
||||
'
|
||||
|
||||
creolineApi="https://api.creoline.com/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPB8"
|
||||
dns_creoline_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
|
||||
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
|
||||
|
||||
if [ -z "$creolineApiToken" ] || [ -z "$creolineApiSecret" ]; then
|
||||
_err "Error required creoline API Token or creoline API Secret not specified."
|
||||
_err "Please set it with the Command 'export creolineApiToken=<YourToken>' and 'export creolineApiSecret=<YourSecret>'."
|
||||
return 1
|
||||
else
|
||||
_saveaccountconf_mutable creolineApiToken "$creolineApiToken"
|
||||
_saveaccountconf_mutable creolineApiSecret "$creolineApiSecret"
|
||||
fi
|
||||
|
||||
_debug "Detecting the root dns zone."
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Error on detecting the root dns zone."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Adding record"
|
||||
if _creoline_rest POST "dns/zone/$_domain/record" "{\"type\":\"TXT\",\"host\":\"$_sub_domain\",\"record\":\"$txtvalue\",\"ttl\":\"60\"}"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_creoline_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
creolineApiToken="${creolineApiToken:-$(_readaccountconf_mutable creolineApiToken)}"
|
||||
creolineApiSecret="${creolineApiSecret:-$(_readaccountconf_mutable creolineApiSecret)}"
|
||||
|
||||
_debug "Detecting the root dns zone."
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Error on detecting the root dns zone."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Getting earlier created txt record."
|
||||
if ! _creoline_rest GET "dns/zone/$_domain/record/type/TXT/record/$txtvalue"; then
|
||||
if _contains "$response" "errors" || _contains "$response" "message"; then
|
||||
_err "Error on getting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
_err "Error on getting earlier created txt record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$response" | _egrep_o "\"id\"[[:space:]]*:[[:space:]]*[0-9]+" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug "record_id" "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_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\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_domain=$(echo "$response" | _egrep_o "\"domain\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \" | _head_n 1 | tr -d " ")
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if [ -z "$_domain" ] || [ -z "$_sub_domain" ]; then
|
||||
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\"[[:space:]]*:[[:space:]]*\"[^\"]+\"" | cut -d : -f 2 | tr -d \")
|
||||
_err "Error: $message"
|
||||
_err "URI:$uri"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -76,7 +76,7 @@ dns_czechia_add() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
_nres="$(_normalizeJson "$_res")"
|
||||
_nres="$(printf '%s' "$_res" | _normalizeJson)"
|
||||
if [ "$?" -ne 0 ] || [ -z "$_nres" ]; then
|
||||
_nres="$_res"
|
||||
fi
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ dns_da_info='DirectAdmin Server API
|
||||
Site: DirectAdmin.com/api.php
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_da
|
||||
Options:
|
||||
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443"
|
||||
DA_Api API Server URL. E.g. "https://remoteUser:remotePassword@da.domain.tld:8443". Special characters in the user/password must be percent-encoded, e.g. "@" -> "%40".
|
||||
DA_Api_Insecure Insecure TLS. 0: check for cert validity, 1: always accept
|
||||
Issues: github.com/TigerP/acme.sh/issues
|
||||
'
|
||||
|
||||
+7
-4
@@ -4,7 +4,7 @@ dns_desec_info='deSEC.io
|
||||
Site: desec.readthedocs.io/en/latest/
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_desec
|
||||
Options:
|
||||
DDNSS_Token API Token
|
||||
DEDYN_TOKEN API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2180
|
||||
Author: Zheng Qian
|
||||
'
|
||||
@@ -39,6 +39,7 @@ dns_desec_add() {
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_sub_domain=$(echo "$_sub_domain" | _lower_case)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
@@ -48,7 +49,7 @@ dns_desec_add() {
|
||||
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
|
||||
|
||||
if [ "$_code" = "200" ]; then
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
_debug "existing TXT found"
|
||||
_debug oldtxtvalues "$oldtxtvalues"
|
||||
if [ -n "$oldtxtvalues" ]; then
|
||||
@@ -100,7 +101,7 @@ dns_desec_rm() {
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_sub_domain=$(echo "$_sub_domain" | _lower_case)
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
@@ -110,7 +111,7 @@ dns_desec_rm() {
|
||||
_desec_rest GET "$REST_API/$_domain/rrsets/$_sub_domain/TXT/"
|
||||
|
||||
if [ "$_code" = "200" ]; then
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"\\S*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
oldtxtvalues="$(echo "$response" | _egrep_o "\"records\":\\[\"[^ ]*\"\\]" | cut -d : -f 2 | tr -d "[]\\\\\"" | sed "s/,/ /g")"
|
||||
_debug "existing TXT found"
|
||||
_debug oldtxtvalues "$oldtxtvalues"
|
||||
if [ -n "$oldtxtvalues" ]; then
|
||||
@@ -150,6 +151,8 @@ _desec_rest() {
|
||||
if [ "$m" != "GET" ]; then
|
||||
_secure_debug2 data "$data"
|
||||
response="$(_post "$data" "$ep" "" "$m")"
|
||||
_info "Sleeping 1s to respect deSEC write rate limit"
|
||||
_sleep 1
|
||||
else
|
||||
response="$(_get "$ep")"
|
||||
fi
|
||||
|
||||
+25
-90
@@ -5,14 +5,11 @@ Site: DNSExit.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_dnsexit
|
||||
Options:
|
||||
DNSEXIT_API_KEY API Key
|
||||
DNSEXIT_AUTH_USER Username
|
||||
DNSEXIT_AUTH_PASS Password
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/4719
|
||||
Author: Samuel Jimenez
|
||||
'
|
||||
|
||||
DNSEXIT_API_URL="https://api.dnsexit.com/dns/"
|
||||
DNSEXIT_HOSTS_URL="https://update.dnsexit.com/ipupdate/hosts.jsp"
|
||||
|
||||
######## Public functions #####################
|
||||
#Usage: dns_dnsexit_add _acme-challenge.*.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
@@ -28,20 +25,7 @@ dns_dnsexit_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug 'First detect the root zone'
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"add\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":0,\"overwrite\":false}}"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
_dnsexit_zone_op add ',"ttl":1,"overwrite":false'
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
@@ -58,54 +42,43 @@ dns_dnsexit_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug 'First detect the root zone'
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _dnsexit_rest "{\"domain\":\"$_domain\",\"delete\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"}}"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
_dnsexit_zone_op delete ''
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
# The legacy zone-detection endpoint (update.dnsexit.com/ipupdate/hosts.jsp)
|
||||
# was shut down by DNSExit and now returns 503, and the JSON API offers no
|
||||
# zone-list call. So find the root zone by attempting the actual operation at
|
||||
# each domain level: the API answers "code":0 only when the domain matches a
|
||||
# zone of the account. https://github.com/acmesh-official/acme.sh/issues/6914
|
||||
#Usage: _dnsexit_zone_op <add|delete> <extra-json-fields>
|
||||
_dnsexit_zone_op() {
|
||||
_op="$1"
|
||||
_extra="$2"
|
||||
i=1
|
||||
while true; do
|
||||
_domain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$_domain"
|
||||
_domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
|
||||
_debug _domain "$_domain"
|
||||
if [ -z "$_domain" ]; then
|
||||
_err "Could not find the root zone of $fulldomain in your DNSExit account"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug login "$DNSEXIT_AUTH_USER"
|
||||
_debug password "$DNSEXIT_AUTH_PASS"
|
||||
_debug domain "$_domain"
|
||||
_sub_domain="$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")"
|
||||
if [ "$_sub_domain" = "$fulldomain" ]; then
|
||||
_sub_domain=""
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_dnsexit_http "login=$DNSEXIT_AUTH_USER&password=$DNSEXIT_AUTH_PASS&domain=$_domain"
|
||||
|
||||
if _contains "$response" "0=$_domain"; then
|
||||
_sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
|
||||
return 0
|
||||
else
|
||||
_debug "Go to next level of $_domain"
|
||||
if _dnsexit_rest "{\"domain\":\"$_domain\",\"$_op\":{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\"$_extra}}"; then
|
||||
if _contains "$response" "\"code\":0" || _contains "$response" "\"code\": 0"; then
|
||||
_debug2 _response "$response"
|
||||
return 0
|
||||
fi
|
||||
_debug "Zone $_domain was not accepted, trying the next level" "$response"
|
||||
fi
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_dnsexit_rest() {
|
||||
@@ -136,27 +109,7 @@ _dnsexit_rest() {
|
||||
return 0
|
||||
}
|
||||
|
||||
_dnsexit_http() {
|
||||
m=GET
|
||||
param="$1"
|
||||
_debug param "$param"
|
||||
_debug get "$DNSEXIT_HOSTS_URL?$param"
|
||||
|
||||
response="$(_get "$DNSEXIT_HOSTS_URL?$param")"
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Error $param"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_account_info() {
|
||||
|
||||
DNSEXIT_API_KEY="${DNSEXIT_API_KEY:-$(_readaccountconf_mutable DNSEXIT_API_KEY)}"
|
||||
if test -z "$DNSEXIT_API_KEY"; then
|
||||
DNSEXIT_API_KEY=''
|
||||
@@ -166,23 +119,5 @@ get_account_info() {
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_API_KEY "$DNSEXIT_API_KEY"
|
||||
|
||||
DNSEXIT_AUTH_USER="${DNSEXIT_AUTH_USER:-$(_readaccountconf_mutable DNSEXIT_AUTH_USER)}"
|
||||
if test -z "$DNSEXIT_AUTH_USER"; then
|
||||
DNSEXIT_AUTH_USER=""
|
||||
_err 'DNSEXIT_AUTH_USER was not exported'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_AUTH_USER "$DNSEXIT_AUTH_USER"
|
||||
|
||||
DNSEXIT_AUTH_PASS="${DNSEXIT_AUTH_PASS:-$(_readaccountconf_mutable DNSEXIT_AUTH_PASS)}"
|
||||
if test -z "$DNSEXIT_AUTH_PASS"; then
|
||||
DNSEXIT_AUTH_PASS=""
|
||||
_err 'DNSEXIT_AUTH_PASS was not exported'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable DNSEXIT_AUTH_PASS "$DNSEXIT_AUTH_PASS"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
+37
-8
@@ -5,6 +5,7 @@ Site: DNSimple.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsimple
|
||||
Options:
|
||||
DNSimple_OAUTH_TOKEN OAuth Token
|
||||
DNSimple_ACCOUNT_ID Account ID. Optional, only needed when the token can access multiple accounts.
|
||||
Issues: github.com/pho3nixf1re/acme.sh/issues
|
||||
'
|
||||
|
||||
@@ -17,6 +18,7 @@ dns_dnsimple_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
DNSimple_OAUTH_TOKEN=""
|
||||
_err "You have not set the dnsimple oauth token yet."
|
||||
@@ -25,10 +27,10 @@ dns_dnsimple_add() {
|
||||
fi
|
||||
|
||||
# save the oauth token for later
|
||||
_saveaccountconf DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
|
||||
_saveaccountconf_mutable DNSimple_OAUTH_TOKEN "$DNSimple_OAUTH_TOKEN"
|
||||
|
||||
if ! _get_account_id; then
|
||||
_err "failed to retrive account id"
|
||||
_err "failed to retrieve account id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -56,8 +58,14 @@ dns_dnsimple_add() {
|
||||
dns_dnsimple_rm() {
|
||||
fulldomain=$1
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
_err "You have not set the dnsimple oauth token yet."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _get_account_id; then
|
||||
_err "failed to retrive account id"
|
||||
_err "failed to retrieve account id"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -122,13 +130,16 @@ _get_root() {
|
||||
|
||||
# returns _account_id
|
||||
_get_account_id() {
|
||||
_debug "retrive account id"
|
||||
if ! _dnsimple_rest GET "whoami"; then
|
||||
return 1
|
||||
DNSimple_ACCOUNT_ID="${DNSimple_ACCOUNT_ID:-$(_readaccountconf_mutable DNSimple_ACCOUNT_ID)}"
|
||||
if [ "$DNSimple_ACCOUNT_ID" ]; then
|
||||
_saveaccountconf_mutable DNSimple_ACCOUNT_ID "$DNSimple_ACCOUNT_ID"
|
||||
_account_id="$DNSimple_ACCOUNT_ID"
|
||||
_debug _account_id "$_account_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"account\":null"; then
|
||||
_err "no account associated with this token"
|
||||
_debug "retrieve account id"
|
||||
if ! _dnsimple_rest GET "whoami"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -137,7 +148,25 @@ _get_account_id() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"account\":null"; then
|
||||
# the whoami of a user token (dnsimple_u_*) carries no account,
|
||||
# so list the accounts the token can access instead
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6491
|
||||
if ! _dnsimple_rest GET "accounts"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_account_id=$(printf "%s" "$response" | _egrep_o "\"id\":[^,]*,\"email\":" | cut -d: -f2 | cut -d, -f1)
|
||||
if [ -z "$_account_id" ]; then
|
||||
_err "no account associated with this token"
|
||||
return 1
|
||||
fi
|
||||
if [ "$(echo "$_account_id" | wc -l)" -gt 1 ]; then
|
||||
_err "The token has access to multiple accounts, please pick one and set it explicitly:"
|
||||
_err "export DNSimple_ACCOUNT_ID=<one of: $(echo "$_account_id" | tr '\n' ' ')>"
|
||||
return 1
|
||||
fi
|
||||
_debug _account_id "$_account_id"
|
||||
|
||||
return 0
|
||||
|
||||
+10
-6
@@ -23,6 +23,8 @@ dns_dynu_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
|
||||
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
|
||||
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
|
||||
Dynu_ClientId=""
|
||||
Dynu_Secret=""
|
||||
@@ -32,8 +34,8 @@ dns_dynu_add() {
|
||||
fi
|
||||
|
||||
#save the client id and secret to the account conf file.
|
||||
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf Dynu_Secret "$Dynu_Secret"
|
||||
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
|
||||
|
||||
if [ -z "$Dynu_Token" ]; then
|
||||
_info "Getting Dynu token."
|
||||
@@ -69,6 +71,8 @@ dns_dynu_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
Dynu_ClientId="${Dynu_ClientId:-$(_readaccountconf_mutable Dynu_ClientId)}"
|
||||
Dynu_Secret="${Dynu_Secret:-$(_readaccountconf_mutable Dynu_Secret)}"
|
||||
if [ -z "$Dynu_ClientId" ] || [ -z "$Dynu_Secret" ]; then
|
||||
Dynu_ClientId=""
|
||||
Dynu_Secret=""
|
||||
@@ -78,8 +82,8 @@ dns_dynu_rm() {
|
||||
fi
|
||||
|
||||
#save the client id and secret to the account conf file.
|
||||
_saveaccountconf Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf Dynu_Secret "$Dynu_Secret"
|
||||
_saveaccountconf_mutable Dynu_ClientId "$Dynu_ClientId"
|
||||
_saveaccountconf_mutable Dynu_Secret "$Dynu_Secret"
|
||||
|
||||
if [ -z "$Dynu_Token" ]; then
|
||||
_info "Getting Dynu token."
|
||||
@@ -214,11 +218,11 @@ _dynu_authentication() {
|
||||
|
||||
response="$(_get "$Dynu_EndPoint/oauth2/token")"
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Authentication failed."
|
||||
_err "Authentication failed: no response from $Dynu_EndPoint/oauth2/token"
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "Authentication Exception"; then
|
||||
_err "Authentication failed."
|
||||
_err "Authentication failed. Server response: $response"
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "access_token"; then
|
||||
|
||||
+6
-11
@@ -363,17 +363,12 @@ _edgedns_rest() {
|
||||
|
||||
_edgedns_eg_timestamp() {
|
||||
_debug "Generating signature Timestamp"
|
||||
_debug3 "Retriving ntp time"
|
||||
_timeheaders="$(_get "https://www.ntp.org" "onlyheader")"
|
||||
_debug3 "_timeheaders" "$_timeheaders"
|
||||
_ntpdate="$(echo "$_timeheaders" | grep -i "Date:" | _head_n 1 | cut -d ':' -f 2- | tr -d "\r\n")"
|
||||
_debug3 "_ntpdate" "$_ntpdate"
|
||||
_ntpdate="$(echo "${_ntpdate}" | sed -e 's/^[[:space:]]*//')"
|
||||
_debug3 "_NTPDATE" "$_ntpdate"
|
||||
_ntptime="$(echo "${_ntpdate}" | _head_n 1 | cut -d " " -f 5 | tr -d "\r\n")"
|
||||
_debug3 "_ntptime" "$_ntptime"
|
||||
_eg_timestamp=$(date -u "+%Y%m%dT")
|
||||
_eg_timestamp="$(printf "%s%s+0000" "$_eg_timestamp" "$_ntptime")"
|
||||
#Akamai accepts a clock skew of +/-30s, so use the system clock directly.
|
||||
#The previous code fetched the Date header from www.ntp.org, which is not
|
||||
#a reliable time source (it served a wrong time for hours, issue 3973),
|
||||
#cost an extra https round-trip for every API request, and combined the
|
||||
#remote time of day with the LOCAL date, breaking around UTC midnight.
|
||||
_eg_timestamp="$(date -u "+%Y%m%dT%H:%M:%S+0000")"
|
||||
_debug "_eg_timestamp" "$_eg_timestamp"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_eurodns_info='EuroDNS
|
||||
Site: eurodns.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_eurodns
|
||||
Options:
|
||||
EURODNS_APP_ID Application ID
|
||||
EURODNS_API_KEY API Key
|
||||
EURODNS_TTL TTL. Default: "600".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues
|
||||
Author: Nicolas Santorelli
|
||||
'
|
||||
|
||||
#
|
||||
# EuroDNS DNS API
|
||||
#
|
||||
# EuroDNS API documentation:
|
||||
# https://docapi.eurodns.com
|
||||
#
|
||||
# Usage:
|
||||
# export EURODNS_APP_ID="your-app-id"
|
||||
# export EURODNS_API_KEY="your-api-key"
|
||||
# acme.sh --issue --dns dns_eurodns -d example.com -d *.example.com
|
||||
#
|
||||
# The credentials will be saved in ~/.acme.sh/account.conf
|
||||
#
|
||||
# Optional:
|
||||
# export EURODNS_API_URL="https://rest-api.eurodns.com" # Default API URL
|
||||
# export EURODNS_TTL=600 # Default TTL (minimum 600 for EuroDNS)
|
||||
#
|
||||
|
||||
EURODNS_API_DEFAULT="https://rest-api.eurodns.com"
|
||||
EURODNS_TTL_DEFAULT=600
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: dns_eurodns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_eurodns_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$(_readaccountconf_mutable EURODNS_TTL)}"
|
||||
EURODNS_TTL="${EURODNS_TTL:-$EURODNS_TTL_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
_err "Please export EURODNS_APP_ID and EURODNS_API_KEY and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable EURODNS_APP_ID "$EURODNS_APP_ID"
|
||||
_saveaccountconf_mutable EURODNS_API_KEY "$EURODNS_API_KEY"
|
||||
if [ "$EURODNS_API_URL" != "$EURODNS_API_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_API_URL "$EURODNS_API_URL"
|
||||
fi
|
||||
if [ "$EURODNS_TTL" != "$EURODNS_TTL_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable EURODNS_TTL "$EURODNS_TTL"
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Adding TXT record"
|
||||
if _eurodns_add_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Added TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to add TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#Usage: fulldomain txtvalue
|
||||
dns_eurodns_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using EuroDNS DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
EURODNS_APP_ID="${EURODNS_APP_ID:-$(_readaccountconf_mutable EURODNS_APP_ID)}"
|
||||
EURODNS_API_KEY="${EURODNS_API_KEY:-$(_readaccountconf_mutable EURODNS_API_KEY)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$(_readaccountconf_mutable EURODNS_API_URL)}"
|
||||
EURODNS_API_URL="${EURODNS_API_URL:-$EURODNS_API_DEFAULT}"
|
||||
|
||||
if [ -z "$EURODNS_APP_ID" ] || [ -z "$EURODNS_API_KEY" ]; then
|
||||
EURODNS_APP_ID=""
|
||||
EURODNS_API_KEY=""
|
||||
_err "You didn't specify EuroDNS App ID and API Key."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Removing TXT record"
|
||||
if _eurodns_rm_txt_record "$_domain" "$_sub_domain" "$txtvalue"; then
|
||||
_info "Removed TXT record successfully."
|
||||
return 0
|
||||
else
|
||||
_err "Failed to remove TXT record."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_eurodns_rest GET "dns-zones/$h"
|
||||
if [ "$?" != "0" ]; then
|
||||
if [ "$_code" = "404" ]; then
|
||||
_debug "Zone $h not found, continuing..."
|
||||
else
|
||||
_err "API error looking up zone $h"
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
continue
|
||||
fi
|
||||
|
||||
if _contains "$response" '"name"'; then
|
||||
if [ "$i" = "1" ]; then
|
||||
_sub_domain="@"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_add_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
data='[{"type":"TXT","host":"'"$subdomain"'","rdata":"'"$txtvalue"'","ttl":'"$EURODNS_TTL"'}]'
|
||||
|
||||
_debug "Adding TXT record via API"
|
||||
if _eurodns_rest POST "dns-zones/$domain/dns-records" "$data"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Failed to add TXT record"
|
||||
return 1
|
||||
}
|
||||
|
||||
_eurodns_rm_txt_record() {
|
||||
domain=$1
|
||||
subdomain=$2
|
||||
txtvalue=$3
|
||||
|
||||
_debug "Getting current zone data for $domain"
|
||||
|
||||
if ! _eurodns_rest GET "dns-zones/$domain"; then
|
||||
_err "Failed to get zone data"
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_data=$(echo "$response" | _normalizeJson)
|
||||
_debug2 zone_data "$zone_data"
|
||||
|
||||
# Find the record ID matching our TXT record
|
||||
record_id=$(echo "$zone_data" | tr '{' '\n' | grep -F '"TXT"' | grep -F "\"$subdomain\"" | grep -F "\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]+' | cut -d : -f 2 | _head_n 1)
|
||||
_debug record_id "$record_id"
|
||||
|
||||
if [ -z "$record_id" ]; then
|
||||
_info "TXT record not found or already removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Deleting TXT record $record_id"
|
||||
if ! _eurodns_rest DELETE "dns-zones/$domain/dns-records/$record_id"; then
|
||||
_err "Failed to delete TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _eurodns_rest METHOD ENDPOINT [DATA]
|
||||
_eurodns_rest() {
|
||||
method=$1
|
||||
endpoint=$2
|
||||
data="$3"
|
||||
|
||||
export _H1="X-APP-ID: $EURODNS_APP_ID"
|
||||
export _H2="X-API-KEY: $EURODNS_API_KEY"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
url="$EURODNS_API_URL/$endpoint"
|
||||
|
||||
_debug2 url "$url"
|
||||
_debug2 method "$method"
|
||||
_debug2 data "$data"
|
||||
|
||||
: >"$HTTP_HEADER"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
unset _H1 _H2 _H3
|
||||
_debug2 response "$response"
|
||||
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
_debug2 _code "$_code"
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error calling API: $endpoint"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_code" != "200" ] && [ "$_code" != "201" ] && [ "$_code" != "204" ]; then
|
||||
if [ "$_code" != "404" ]; then
|
||||
_err "API error (HTTP $_code): $response"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_firestorm_info='Firestorm.ch
|
||||
Site: firestorm.ch
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_firestorm
|
||||
Options:
|
||||
FST_Key Customer ID
|
||||
FST_Secret API Secret
|
||||
FST_Url API URL. Optional. Default "https://api.firestorm.ch/acme-dns".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6839
|
||||
Author: FireStorm GmbH
|
||||
'
|
||||
|
||||
FST_Url_DEFAULT="https://api.firestorm.ch/acme-dns"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_firestorm_add _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
_err "Get your API credentials at https://admin.firestorm.ch"
|
||||
return 1
|
||||
fi
|
||||
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
_saveaccountconf_mutable FST_Key "$FST_Key"
|
||||
_saveaccountconf_mutable FST_Secret "$FST_Secret"
|
||||
if [ "$FST_Url" != "$FST_Url_DEFAULT" ]; then
|
||||
_saveaccountconf_mutable FST_Url "$FST_Url"
|
||||
else
|
||||
_clearaccountconf_mutable FST_Url
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
_debug "Subdomain" "$subdomain"
|
||||
_debug "TXT value" "$txtvalue"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "update" "$body")"
|
||||
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "TXT record added successfully"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to add TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: dns_firestorm_rm _acme-challenge.www.example.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_firestorm_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
FST_Key="${FST_Key:-$(_readaccountconf_mutable FST_Key)}"
|
||||
FST_Secret="${FST_Secret:-$(_readaccountconf_mutable FST_Secret)}"
|
||||
FST_Url="${FST_Url:-$(_readaccountconf_mutable FST_Url)}"
|
||||
FST_Url="${FST_Url:-$FST_Url_DEFAULT}"
|
||||
|
||||
if [ -z "$FST_Key" ] || [ -z "$FST_Secret" ]; then
|
||||
_err "FST_Key and FST_Secret must be set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
subdomain=$(printf "%s" "$fulldomain" | sed 's/^_acme-challenge\.//')
|
||||
|
||||
_info "Removing TXT record for $fulldomain"
|
||||
|
||||
body="{\"subdomain\":\"$(_json_safe "$subdomain")\",\"txt\":\"$(_json_safe "$txtvalue")\"}"
|
||||
|
||||
response="$(_firestorm_api "remove" "$body")"
|
||||
|
||||
if _contains "$response" "removed"; then
|
||||
_info "TXT record removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Failed to remove TXT record: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# Escape special characters for safe JSON string interpolation
|
||||
_json_safe() {
|
||||
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
|
||||
}
|
||||
|
||||
_firestorm_api() {
|
||||
action=$1
|
||||
data=$2
|
||||
|
||||
export _H1="X-Api-User: $FST_Key"
|
||||
export _H2="X-Api-Key: $FST_Secret"
|
||||
export _H3="Content-Type: application/json"
|
||||
|
||||
_post "$data" "$FST_Url/$action" "" "POST"
|
||||
}
|
||||
@@ -305,7 +305,7 @@ _freedns_domain_id() {
|
||||
fi
|
||||
|
||||
domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's/<tr>/@<tr>/g' | tr '@' '\n' |
|
||||
grep "<td>$search_domain</td>\|<td>$search_domain(.*)</td>" |
|
||||
grep -E "<td>$search_domain</td>|<td>$search_domain\(.*\)</td>" |
|
||||
sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' |
|
||||
cut -d = -f 2)"
|
||||
# The above beauty extracts domain ID from the html page...
|
||||
|
||||
+38
-9
@@ -69,7 +69,12 @@ dns_gd_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$response" "$txtvalue"; then
|
||||
if _contains "$response" "UNKNOWN_DOMAIN"; then
|
||||
# GoDaddy sometimes returns UNKNOWN_DOMAIN when reading a record back even
|
||||
# though the PUT above succeeded; skip the local readback check and let
|
||||
# acme.sh's own DNS propagation check verify the record was published.
|
||||
_info "GoDaddy API won't allow reading the record back; skipping local verification."
|
||||
elif ! _contains "$response" "$txtvalue"; then
|
||||
_err "TXT record '${txtvalue}' for '${fulldomain}', value wasn't set!"
|
||||
return 1
|
||||
fi
|
||||
@@ -145,8 +150,8 @@ dns_gd_rm() {
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=2
|
||||
p=1
|
||||
i=1
|
||||
p=0
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
@@ -154,17 +159,41 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _gd_rest GET "domains/$h"; then
|
||||
return 1
|
||||
# The record name is whatever precedes the candidate zone. Do not assume
|
||||
# _acme-challenge here: with DNS alias mode it can be any name, and the
|
||||
# record may even sit at the zone apex (name "@").
|
||||
if [ "$p" = "0" ]; then
|
||||
_probe_sub="@"
|
||||
else
|
||||
_probe_sub=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
|
||||
if _contains "$response" '"code":"NOT_FOUND"'; then
|
||||
_debug "$h not found"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
# Probe with the records endpoint instead of "GET domains/$h": since
|
||||
# 2024-05 GoDaddy rejects the domain details call for accounts with
|
||||
# fewer than 10 domains, while record-level calls keep working.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/4487
|
||||
if ! _gd_rest GET "domains/$h/records/TXT/$_probe_sub"; then
|
||||
return 1
|
||||
fi
|
||||
if _startswith "$response" '\['; then
|
||||
_sub_domain="$_probe_sub"
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Some accounts get UNKNOWN_DOMAIN when reading records of a valid zone
|
||||
# even though writes succeed (see issue #6517); fall back to the domain
|
||||
# details call for them.
|
||||
if ! _gd_rest GET "domains/$h"; then
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" '"domainId"'; then
|
||||
_sub_domain="$_probe_sub"
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "$h not found"
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_glesys_info='Glesys
|
||||
Site: Glesys.se
|
||||
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys
|
||||
Options:
|
||||
GLESYS_API_KEY Generated API key.
|
||||
GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345).
|
||||
GLESYS_API API endpoint. Default "https://api.glesys.com/domain".
|
||||
GLESYS_TTL TXT record TTL. Default 120.
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/7057
|
||||
Author: Toni Karppi
|
||||
'
|
||||
|
||||
GLESYS_API_DEFAULT="https://api.glesys.com/domain"
|
||||
GLESYS_TTL_DEFAULT="120"
|
||||
|
||||
######## Public functions #####################################################
|
||||
|
||||
# Usage:
|
||||
# dns_glesys_add _acme-challenge.www.example.com "txt-value"
|
||||
dns_glesys_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
_glesys_init || return 1
|
||||
|
||||
if ! _glesys_get_root "$fulldomain"; then
|
||||
_err "Could not find root zone for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
host_value="${_sub_domain:-@}"
|
||||
_debug _host_value "$host_value"
|
||||
|
||||
data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}"
|
||||
|
||||
_debug2 data "$data"
|
||||
|
||||
if ! _glesys_rest POST "/addrecord" "$data"; then
|
||||
_err "Failed to send HTTP request to add TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
_err "GleSYS API responded with an unexpected status when attempting to add TXT record"
|
||||
_debug2 "API response" "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record added"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage:
|
||||
# dns_glesys_rm _acme-challenge.www.example.com "txt-value"
|
||||
dns_glesys_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
_glesys_init || return 1
|
||||
|
||||
if ! _glesys_get_root "$fulldomain"; then
|
||||
_err "Could not find root zone for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _glesys_find_record_id "$txtvalue"; then
|
||||
_info "TXT record not present, skip removal"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then
|
||||
_err "Failed to send HTTP request to remove TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" != "200" ]; then
|
||||
_err "GleSYS API responded with unexpected status when attempting to remove TXT record"
|
||||
_debug2 "API response" "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record removed"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ####################################################
|
||||
|
||||
_glesys_find_record_id() {
|
||||
txtvalue="$1"
|
||||
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if [ -z "$txtvalue" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_record_id=""
|
||||
|
||||
_debug "Looking for TXT record with value" "$txtvalue"
|
||||
|
||||
if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then
|
||||
_err "Failed to list DNS records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
records="$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
sed 's/},{/}\
|
||||
{/g'
|
||||
)"
|
||||
|
||||
_debug2 records "$records"
|
||||
|
||||
expected_data="\"data\":\"$txtvalue\""
|
||||
|
||||
_record_id="$(
|
||||
printf "%s\n" "$records" |
|
||||
while IFS= read -r record; do
|
||||
printf "%s" "$record" | grep -q '"type":"TXT"' || continue
|
||||
printf "%s" "$record" | grep -Fq "$expected_data" || continue
|
||||
|
||||
printf "%s" "$record" |
|
||||
grep -E -o '"recordid":"?[0-9]+' |
|
||||
grep -E -o '[0-9]+$'
|
||||
|
||||
break
|
||||
done
|
||||
)"
|
||||
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Finds:
|
||||
# _domain example.com
|
||||
# _sub_domain _acme-challenge.www
|
||||
_glesys_get_root() {
|
||||
domain="$1"
|
||||
i=1
|
||||
|
||||
while true; do
|
||||
h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)"
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _glesys_rest GET "/listrecords?domainname=$h"; then
|
||||
response_code=$(
|
||||
printf "%s" "$response" |
|
||||
tr -d '\r\n\t ' |
|
||||
_egrep_o '"code":"?[0-9]+' |
|
||||
_egrep_o '[0-9]+$'
|
||||
)
|
||||
|
||||
_debug response_code "$response_code"
|
||||
|
||||
if [ "$response_code" = "200" ]; then
|
||||
cut_len="$((${#domain} - ${#h} - 1))"
|
||||
_domain="$h"
|
||||
_sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
i="$((i + 1))"
|
||||
done
|
||||
}
|
||||
|
||||
_glesys_init() {
|
||||
[ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT"
|
||||
[ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT"
|
||||
|
||||
_debug GLESYS_API "$GLESYS_API"
|
||||
_debug GLESYS_TTL "$GLESYS_TTL"
|
||||
|
||||
GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}"
|
||||
GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}"
|
||||
|
||||
if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then
|
||||
_err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_secure_debug GLESYS_API_KEY "$GLESYS_API_KEY"
|
||||
_secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
|
||||
|
||||
_glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)"
|
||||
_secure_debug2 _glesys_basic_auth "$_glesys_basic_auth"
|
||||
|
||||
_saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY"
|
||||
_saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_glesys_rest() {
|
||||
method="$1"
|
||||
path="$2"
|
||||
data="$3"
|
||||
|
||||
export _H1="Authorization: Basic $_glesys_basic_auth"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
|
||||
url="$GLESYS_API$path"
|
||||
_debug "$method $url"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "$method")"
|
||||
fi
|
||||
|
||||
ret="$?"
|
||||
_debug2 response "$response"
|
||||
_debug ret "$ret"
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_hetzner_info='Hetzner.com
|
||||
Site: Hetzner.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
|
||||
Options:
|
||||
HETZNER_Token API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2943
|
||||
'
|
||||
|
||||
HETZNER_Api="https://dns.hetzner.com/api/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
# Used to add txt record
|
||||
# Ref: https://dns.hetzner.com/api-docs/
|
||||
dns_hetzner_add() {
|
||||
full_domain=$1
|
||||
txt_value=$2
|
||||
|
||||
HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
|
||||
|
||||
if [ -z "$HETZNER_Token" ]; then
|
||||
HETZNER_Token=""
|
||||
_err "You didn't specify a Hetzner api token."
|
||||
_err "You can get yours from here https://dns.hetzner.com/settings/api-token."
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save the api key and email to the account conf file.
|
||||
_saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
|
||||
if ! _get_root "$full_domain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting TXT records"
|
||||
if ! _find_record "$_sub_domain" "$txt_value"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "Adding record"
|
||||
if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
|
||||
if _contains "$response" "$txt_value"; then
|
||||
_info "Record added, OK"
|
||||
_sleep 2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error${_response_error}"
|
||||
return 1
|
||||
else
|
||||
_info "Found record id: $_record_id."
|
||||
_info "Record found, do nothing."
|
||||
return 0
|
||||
# we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
|
||||
#if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
|
||||
# if _contains "$response" "$txt_value"; then
|
||||
# _info "Modified, OK"
|
||||
# return 0
|
||||
# fi
|
||||
#fi
|
||||
#_err "Add txt record error (modify)."
|
||||
#return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Usage: full_domain txt_value
|
||||
# Used to remove the txt record after validation
|
||||
dns_hetzner_rm() {
|
||||
full_domain=$1
|
||||
txt_value=$2
|
||||
|
||||
HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$full_domain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting TXT records"
|
||||
if ! _find_record "$_sub_domain" "$txt_value"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "Remove not needed. Record not found."
|
||||
else
|
||||
if ! _hetzner_rest DELETE "records/$_record_id"; then
|
||||
_err "Delete record error${_response_error}"
|
||||
return 1
|
||||
fi
|
||||
_sleep 2
|
||||
_info "Record deleted"
|
||||
fi
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#returns
|
||||
# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error
|
||||
_find_record() {
|
||||
unset _record_id
|
||||
_record_name=$1
|
||||
_record_value=$2
|
||||
|
||||
if [ -z "$_record_value" ]; then
|
||||
_record_value='[^"]*'
|
||||
fi
|
||||
|
||||
_debug "Getting all records"
|
||||
_hetzner_rest GET "records?zone_id=${_domain_id}"
|
||||
|
||||
if _response_has_error; then
|
||||
_err "Error${_response_error}"
|
||||
return 1
|
||||
else
|
||||
_record_id=$(
|
||||
echo "$response" |
|
||||
grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" |
|
||||
grep "\"value\":\"$_record_value\"" |
|
||||
while read -r record; do
|
||||
# test for type and
|
||||
if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
|
||||
echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
|
||||
break
|
||||
fi
|
||||
done
|
||||
)
|
||||
fi
|
||||
}
|
||||
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
# _domain_id=sdjkglgdfewsdfg
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
|
||||
domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
|
||||
|
||||
_debug "Reading zone_id for '$domain_without_acme' from config..."
|
||||
HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
|
||||
if [ "$HETZNER_Zone_ID" ]; then
|
||||
_debug "Found, using: $HETZNER_Zone_ID"
|
||||
if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
|
||||
_debug "Zone with id '$HETZNER_Zone_ID' does not exist."
|
||||
_cleardomainconf "$domain_param_name"
|
||||
unset HETZNER_Zone_ID
|
||||
else
|
||||
if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
|
||||
_domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
|
||||
if [ "$_domain" ]; then
|
||||
_cut_length=$((${#domain} - ${#_domain} - 1))
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
|
||||
_domain_id="$HETZNER_Zone_ID"
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
_debug "Trying to get zone id by domain name for '$domain_without_acme'."
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
_debug h "$h"
|
||||
|
||||
_hetzner_rest GET "zones?name=$h"
|
||||
|
||||
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
|
||||
_domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
HETZNER_Zone_ID=$_domain_id
|
||||
_savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
#returns
|
||||
# _response_error
|
||||
_response_has_error() {
|
||||
unset _response_error
|
||||
|
||||
err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
|
||||
|
||||
if [ -n "$err_part" ]; then
|
||||
err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
|
||||
err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
|
||||
|
||||
if [ -n "$err_code" ] && [ -n "$err_message" ]; then
|
||||
_response_error=" - message: ${err_message}, code: ${err_code}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
#returns
|
||||
# response
|
||||
_hetzner_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
|
||||
|
||||
export _H1="Content-TType: application/json"
|
||||
export _H2="Auth-API-Token: $key_trimmed"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$HETZNER_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ] || _response_has_error; then
|
||||
_debug "Error$_response_error"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
@@ -40,6 +40,11 @@ _hostingde_apiKey() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The endpoint is the base URL only; the api path is appended below.
|
||||
# hosting.de's own docs show the full api URL, so strip it if pasted in.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6896
|
||||
HOSTINGDE_ENDPOINT="$(echo "$HOSTINGDE_ENDPOINT" | sed 's|/api/dns/v1/json||; s|/*$||')"
|
||||
|
||||
_saveaccountconf_mutable HOSTINGDE_APIKEY "$HOSTINGDE_APIKEY"
|
||||
_saveaccountconf_mutable HOSTINGDE_ENDPOINT "$HOSTINGDE_ENDPOINT"
|
||||
}
|
||||
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_hostinger_info='Hostinger
|
||||
Site: Hostinger.com
|
||||
Domains: hostinger.nl
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hostinger
|
||||
Options:
|
||||
HOSTINGER_Token API Key
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/6831
|
||||
Author: Sasha Reid <github@sasha.hackl.es>
|
||||
'
|
||||
|
||||
HOSTINGER_Api="https://developers.hostinger.com/api/dns/v1/zones"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_hostinger_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}"
|
||||
|
||||
if [ -z "$HOSTINGER_Token" ]; then
|
||||
HOSTINGER_Token=""
|
||||
_err "You didn't specify a Hostinger API Key yet."
|
||||
_err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting existing records"
|
||||
_hostinger_rest GET "${_domain}"
|
||||
|
||||
if [ -z "$response" ]; then
|
||||
_err "Error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
|
||||
# we can not use updating anymore.
|
||||
# count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
|
||||
# _debug count "$count"
|
||||
# if [ "$count" = "0" ]; then
|
||||
_info "Adding record"
|
||||
if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [{\"content\":\"$txtvalue\"}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":false}"; then
|
||||
if _contains "$response" "Request accepted"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" ||
|
||||
_contains "$response" 'DNS:4008'; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_hostinger_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
HOSTINGER_Token="${HOSTINGER_Token:-$(_readaccountconf_mutable HOSTINGER_Token)}"
|
||||
|
||||
if [ -z "$HOSTINGER_Token" ]; then
|
||||
HOSTINGER_Token=""
|
||||
_err "You didn't specify a Hostinger API Key yet."
|
||||
_err "Please read the documentation for the Hostinger API authentication at https://developers.hostinger.com/#description/authentication"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HOSTINGER_Token "$HOSTINGER_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting existing records"
|
||||
_hostinger_rest GET "${_domain}"
|
||||
|
||||
if [ -z "$response" ]; then
|
||||
_err "Error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"name\":\"$_sub_domain\""; then
|
||||
# Match the record, and make certain it is a TXT record for the domain not another type. Then remove our target record from the list
|
||||
remaining_records=$(echo "$response" | _normalizeJson | _egrep_o '{"name":"'"$_sub_domain"'","records":\[[^]]+\],"ttl":[0-9]+,"type":"TXT"\}' | _egrep_o "\[.*\]" | sed -E 's#\{"content":"\\"'"$txtvalue"'\\"","is_disabled":false\},?##g')
|
||||
if [ "$remaining_records" != "[]" ]; then
|
||||
remaining_json=$(echo "$remaining_records" | _egrep_o '"content":"\\"[^}]+\\""' | sed -E 's/^(.*)$/{\1},/g' | tr -d '\n' | sed 's/,$//')
|
||||
# We need to set the remaining records back to Hostinger, as we can't partially delete
|
||||
_info "Removing $txtvalue from $_sub_domain by setting records to ${remaining_json}"
|
||||
if _hostinger_rest PUT "$_domain" "{\"zone\":[{\"name\": \"$_sub_domain\",\"records\": [${remaining_json}],\"type\":\"TXT\",\"ttl\":\"120\"}],\"overwrite\":true}"; then
|
||||
if _contains "$response" "Request accepted"; then
|
||||
_info "Updated remaining records, OK"
|
||||
return 0
|
||||
elif _contains "$response" "DNS resource record is not valid or conflicts with another resource record" ||
|
||||
_contains "$response" 'DNS:4008'; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
# Otherwise delete the TXT record that matches the subdomain
|
||||
else
|
||||
if ! _hostinger_rest DELETE "$_domain" "{\"filters\":[{\"name\":\"$_sub_domain\",\"type\":\"TXT\"}]}"; then
|
||||
_err "Delete record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
echo "$response" | grep "Request accepted" >/dev/null
|
||||
else
|
||||
_info "Don't need to remove."
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
|
||||
_hostinger_rest GET "$h"
|
||||
if _contains "$response" "records"; then
|
||||
if [ "$response" = "[]" ]; then
|
||||
_debug "Valid subdomains are not the root"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostinger_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
token_trimmed=$(echo "$HOSTINGER_Token" | tr -d '"')
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="Authorization: Bearer $token_trimmed"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$HOSTINGER_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$HOSTINGER_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
+201
-125
@@ -6,13 +6,13 @@ Site: hostup.se
|
||||
Docs: https://developer.hostup.se/
|
||||
Options:
|
||||
HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes.
|
||||
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
|
||||
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api/v2).
|
||||
HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
|
||||
HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection).
|
||||
HOSTUP_ZONE_ID Optional. Force a specific v2 zone ID (zone_...) and skip auto-detection.
|
||||
Author: HostUp (https://cloud.hostup.se/contact/en)
|
||||
'
|
||||
|
||||
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api"
|
||||
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api/v2"
|
||||
HOSTUP_DEFAULT_TTL=60
|
||||
|
||||
# Public: add TXT record
|
||||
@@ -20,6 +20,7 @@ HOSTUP_DEFAULT_TTL=60
|
||||
dns_hostup_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
hostup_add_txtvalue="$2"
|
||||
|
||||
_info "Using HostUp DNS API"
|
||||
|
||||
@@ -34,31 +35,34 @@ dns_hostup_add() {
|
||||
|
||||
record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")"
|
||||
record_name="$(_hostup_sanitize_name "$record_name")"
|
||||
record_value="$(_hostup_json_escape "$txtvalue")"
|
||||
hostup_add_record_value="$(_hostup_json_escape "$hostup_add_txtvalue")"
|
||||
|
||||
ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
|
||||
raw_ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
|
||||
ttl="$(_hostup_normalize_ttl "$raw_ttl")"
|
||||
if [ -z "$ttl" ]; then
|
||||
_err "HOSTUP_TTL must be a whole number between 60 and 86400 seconds."
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$HOSTUP_TTL" ]; then
|
||||
HOSTUP_TTL="$ttl"
|
||||
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
|
||||
fi
|
||||
|
||||
_debug "zone_id" "$HOSTUP_ZONE_ID"
|
||||
_debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
|
||||
_debug "record_name" "$record_name"
|
||||
_debug "ttl" "$ttl"
|
||||
|
||||
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}"
|
||||
|
||||
if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
|
||||
return 1
|
||||
record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
|
||||
if _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$hostup_add_txtvalue"; then
|
||||
_info "TXT record already exists for $fulldomain"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! _contains "$_hostup_response" '"success":true'; then
|
||||
_err "HostUp DNS API: failed to create TXT record for $fulldomain"
|
||||
_debug2 "_hostup_response" "$_hostup_response"
|
||||
return 1
|
||||
fi
|
||||
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$hostup_add_record_value\",\"ttl\":$ttl}"
|
||||
|
||||
record_id="$(_hostup_extract_record_id "$_hostup_response")"
|
||||
if [ -n "$record_id" ]; then
|
||||
_hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id"
|
||||
_debug "hostup_saved_record_id" "$record_id"
|
||||
if ! _hostup_rest "POST" "/dns-zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Added TXT record for $fulldomain"
|
||||
@@ -85,20 +89,9 @@ dns_hostup_rm() {
|
||||
record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
|
||||
record_value="$txtvalue"
|
||||
|
||||
record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")"
|
||||
if [ -n "$record_id_cached" ]; then
|
||||
_debug "hostup_record_id_cached" "$record_id_cached"
|
||||
if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then
|
||||
_info "Deleted TXT record $record_id_cached"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
|
||||
HOSTUP_ZONE_ID=""
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then
|
||||
_info "TXT record not found for $record_name_fqdn. Skipping removal."
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -109,7 +102,7 @@ dns_hostup_rm() {
|
||||
fi
|
||||
|
||||
_info "Deleted TXT record $HOSTUP_RECORD_ID"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
|
||||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_value"
|
||||
HOSTUP_ZONE_ID=""
|
||||
return 0
|
||||
}
|
||||
@@ -127,21 +120,18 @@ _hostup_init() {
|
||||
if [ -z "$HOSTUP_API_BASE" ]; then
|
||||
HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT"
|
||||
fi
|
||||
HOSTUP_API_BASE="$(_hostup_normalize_api_base "$HOSTUP_API_BASE")"
|
||||
|
||||
if [ -z "$HOSTUP_API_KEY" ]; then
|
||||
HOSTUP_API_KEY=""
|
||||
_err "HOSTUP_API_KEY is not set."
|
||||
_err "Please export your HostUp API key with read:dns and write:dns scopes."
|
||||
_err "Please export your HostUp API key with read:dns, write:dns, and read:domains scopes."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY"
|
||||
_saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE"
|
||||
|
||||
if [ -n "$HOSTUP_TTL" ]; then
|
||||
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
|
||||
fi
|
||||
|
||||
if [ -n "$HOSTUP_ZONE_ID" ]; then
|
||||
_saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID"
|
||||
fi
|
||||
@@ -149,11 +139,80 @@ _hostup_init() {
|
||||
return 0
|
||||
}
|
||||
|
||||
_hostup_normalize_api_base() {
|
||||
api_base="${1%/}"
|
||||
|
||||
case "$api_base" in
|
||||
*/api/v2)
|
||||
printf "%s" "$api_base"
|
||||
;;
|
||||
*/api)
|
||||
printf "%s/v2" "$api_base"
|
||||
;;
|
||||
*)
|
||||
printf "%s" "$api_base"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_hostup_normalize_ttl() {
|
||||
ttl_value="$1"
|
||||
|
||||
case "$ttl_value" in
|
||||
"" | *[!0-9]*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
while [ "${ttl_value#0}" != "$ttl_value" ]; do
|
||||
ttl_value="${ttl_value#0}"
|
||||
done
|
||||
[ -z "$ttl_value" ] && ttl_value=0
|
||||
|
||||
case "$ttl_value" in
|
||||
??????*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$ttl_value" -lt 60 ] || [ "$ttl_value" -gt 86400 ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "%s" "$ttl_value"
|
||||
}
|
||||
|
||||
_hostup_domain_in_zone() {
|
||||
host="$(printf "%s" "${1%.}" | _lower_case)"
|
||||
zone="$(printf "%s" "${2%.}" | _lower_case)"
|
||||
|
||||
if [ -z "$host" ] || [ -z "$zone" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$host" = "$zone" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$host" in
|
||||
*."$zone")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_detect_zone() {
|
||||
fulldomain="$1"
|
||||
|
||||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then
|
||||
return 0
|
||||
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_cached_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
|
||||
HOSTUP_ZONE_ID=""
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
fi
|
||||
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
@@ -162,16 +221,16 @@ _hostup_detect_zone() {
|
||||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then
|
||||
# Attempt to fetch domain name for provided zone ID
|
||||
if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then
|
||||
return 0
|
||||
if _hostup_domain_in_zone "$fulldomain" "$HOSTUP_ZONE_DOMAIN"; then
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_forced_zone_mismatch" "$HOSTUP_ZONE_DOMAIN"
|
||||
fi
|
||||
HOSTUP_ZONE_ID=""
|
||||
HOSTUP_ZONE_DOMAIN=""
|
||||
fi
|
||||
|
||||
if ! _hostup_load_zones; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)"
|
||||
_domain_candidate="$(printf "%s" "${fulldomain%.}" | _lower_case)"
|
||||
_debug "hostup_initial_candidate" "$_domain_candidate"
|
||||
|
||||
while [ -n "$_domain_candidate" ]; do
|
||||
@@ -240,11 +299,11 @@ _hostup_fqdn() {
|
||||
_hostup_fetch_zone_details() {
|
||||
zone_id="$1"
|
||||
|
||||
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
|
||||
if ! _hostup_rest "GET" "/dns-zones/$zone_id/records" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')"
|
||||
zonedomain="$(_hostup_json_extract "name" "$_hostup_response")"
|
||||
if [ -n "$zonedomain" ]; then
|
||||
HOSTUP_ZONE_DOMAIN="$zonedomain"
|
||||
return 0
|
||||
@@ -254,7 +313,7 @@ _hostup_fetch_zone_details() {
|
||||
}
|
||||
|
||||
_hostup_load_zones() {
|
||||
if ! _hostup_rest "GET" "/dns/zones" ""; then
|
||||
if ! _hostup_rest "GET" "/dns-zones?limit=1000" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -263,9 +322,9 @@ _hostup_load_zones() {
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*'"domain_id"'*'"domain"'*)
|
||||
zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")"
|
||||
zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")"
|
||||
*'"id"'*'"name"'*)
|
||||
zone_id="$(_hostup_json_extract "id" "$line")"
|
||||
zone_domain="$(_hostup_json_extract "name" "$line")"
|
||||
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
|
||||
HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id}
|
||||
"
|
||||
@@ -290,9 +349,30 @@ _hostup_lookup_zone() {
|
||||
_lookup_zone_id=""
|
||||
_lookup_zone_domain=""
|
||||
|
||||
encoded_domain="$(printf "%s" "$lookup_domain" | _url_encode)"
|
||||
if _hostup_rest "GET" "/dns-zones?name=$encoded_domain&limit=1" ""; then
|
||||
zone_id="$(_hostup_json_extract "id" "$_hostup_response")"
|
||||
zone_domain="$(_hostup_json_extract "name" "$_hostup_response")"
|
||||
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
|
||||
zone_domain_lower="$(printf "%s" "$zone_domain" | _lower_case)"
|
||||
if [ "$zone_domain_lower" = "$lookup_domain" ]; then
|
||||
_lookup_zone_domain="$zone_domain"
|
||||
_lookup_zone_id="$zone_id"
|
||||
HOSTUP_ZONE_DOMAIN="$zone_domain"
|
||||
HOSTUP_ZONE_ID="$zone_id"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$HOSTUP_ZONES_CACHE" ] && ! _hostup_load_zones; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS='|' read -r domain zone_id; do
|
||||
[ -z "$domain" ] && continue
|
||||
if [ "$domain" = "$lookup_domain" ]; then
|
||||
domain_lower="$(printf "%s" "$domain" | _lower_case)"
|
||||
if [ "$domain_lower" = "$lookup_domain" ]; then
|
||||
_lookup_zone_domain="$domain"
|
||||
_lookup_zone_id="$zone_id"
|
||||
HOSTUP_ZONE_DOMAIN="$domain"
|
||||
@@ -307,50 +387,50 @@ EOF
|
||||
}
|
||||
|
||||
_hostup_find_record() {
|
||||
zone_id="$1"
|
||||
fqdn="$2"
|
||||
txtvalue="$3"
|
||||
_hostup_find_zone_id="$1"
|
||||
_hostup_find_fqdn="$2"
|
||||
_hostup_find_txtvalue="$3"
|
||||
|
||||
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
|
||||
_hostup_find_encoded_name="$(printf "%s" "$_hostup_find_fqdn" | _url_encode)"
|
||||
if ! _hostup_rest "GET" "/dns-zones/$_hostup_find_zone_id/records?type=TXT&name=$_hostup_find_encoded_name" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
HOSTUP_RECORD_ID=""
|
||||
records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
|
||||
_hostup_find_records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
|
||||
|
||||
while IFS= read -r line; do
|
||||
while IFS= read -r _hostup_find_line; do
|
||||
# Normalize line to make TXT value matching reliable
|
||||
line_clean="$(printf "%s" "$line" | tr -d '\r\n')"
|
||||
line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')"
|
||||
_hostup_find_line_clean="$(printf "%s" "$_hostup_find_line" | tr -d '\r\n')"
|
||||
_hostup_find_line_value_clean="$(printf "%s" "$_hostup_find_line_clean" | sed 's/\\"//g')"
|
||||
|
||||
case "$line_clean" in
|
||||
*'"type":"TXT"'*'"name"'*'"value"'*)
|
||||
name_value="$(_hostup_json_extract "name" "$line_clean")"
|
||||
record_value="$(_hostup_json_extract "value" "$line_value_clean")"
|
||||
_hostup_find_record_type="$(_hostup_json_extract "type" "$_hostup_find_line_clean")"
|
||||
[ "$_hostup_find_record_type" != "TXT" ] && continue
|
||||
|
||||
_debug "hostup_record_raw" "$record_value"
|
||||
if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then
|
||||
record_value="${record_value#\"}"
|
||||
record_value="${record_value%\"}"
|
||||
fi
|
||||
if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then
|
||||
record_value="${record_value#\'}"
|
||||
record_value="${record_value%\'}"
|
||||
fi
|
||||
record_value="$(printf "%s" "$record_value" | tr -d '\r\n')"
|
||||
_debug "hostup_record_value" "$record_value"
|
||||
_hostup_find_name_value="$(_hostup_json_extract "name" "$_hostup_find_line_clean")"
|
||||
_hostup_find_record_value="$(_hostup_json_extract "value" "$_hostup_find_line_value_clean")"
|
||||
|
||||
if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then
|
||||
record_id="$(_hostup_json_extract "id" "$line_clean")"
|
||||
if [ -n "$record_id" ]; then
|
||||
HOSTUP_RECORD_ID="$record_id"
|
||||
return 0
|
||||
fi
|
||||
_debug "hostup_record_raw" "$_hostup_find_record_value"
|
||||
if [ "${_hostup_find_record_value#\"}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\"}" != "$_hostup_find_record_value" ]; then
|
||||
_hostup_find_record_value="${_hostup_find_record_value#\"}"
|
||||
_hostup_find_record_value="${_hostup_find_record_value%\"}"
|
||||
fi
|
||||
if [ "${_hostup_find_record_value#\'}" != "$_hostup_find_record_value" ] && [ "${_hostup_find_record_value%\'}" != "$_hostup_find_record_value" ]; then
|
||||
_hostup_find_record_value="${_hostup_find_record_value#\'}"
|
||||
_hostup_find_record_value="${_hostup_find_record_value%\'}"
|
||||
fi
|
||||
_hostup_find_record_value="$(printf "%s" "$_hostup_find_record_value" | tr -d '\r\n')"
|
||||
_debug "hostup_record_value" "$_hostup_find_record_value"
|
||||
|
||||
if [ "$_hostup_find_name_value" = "$_hostup_find_fqdn" ] && [ "$_hostup_find_record_value" = "$_hostup_find_txtvalue" ]; then
|
||||
_hostup_find_record_id="$(_hostup_json_extract "id" "$_hostup_find_line_clean")"
|
||||
if [ -n "$_hostup_find_record_id" ]; then
|
||||
HOSTUP_RECORD_ID="$_hostup_find_record_id"
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done <<EOF
|
||||
$records
|
||||
$_hostup_find_records
|
||||
EOF
|
||||
|
||||
return 1
|
||||
@@ -361,22 +441,22 @@ _hostup_json_extract() {
|
||||
input="${2:-$line}"
|
||||
|
||||
# First try to extract quoted values (strings)
|
||||
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)"
|
||||
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | _head_n 1)"
|
||||
if [ -n "$quoted_match" ]; then
|
||||
printf "%s" "$quoted_match" |
|
||||
cut -d : -f2- |
|
||||
sed 's/^"//' |
|
||||
sed 's/"$//' |
|
||||
sed 's/^[[:space:]]*"//' |
|
||||
sed 's/"[[:space:]]*$//' |
|
||||
sed 's/\\"/"/g'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fallback for unquoted values (e.g., numeric IDs)
|
||||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)"
|
||||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | _head_n 1)"
|
||||
if [ -n "$unquoted_match" ]; then
|
||||
printf "%s" "$unquoted_match" |
|
||||
cut -d : -f2- |
|
||||
tr -d '", ' |
|
||||
tr -d '", ' |
|
||||
tr -d '\r\n'
|
||||
return 0
|
||||
fi
|
||||
@@ -391,58 +471,56 @@ _hostup_json_escape() {
|
||||
_hostup_record_key() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
txtvalue="$3"
|
||||
safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')"
|
||||
safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')"
|
||||
if [ -n "$txtvalue" ]; then
|
||||
safe_value="$(printf "%s" "$txtvalue" | sed 's/[^A-Za-z0-9]/_/g')"
|
||||
printf "%s_%s_%s" "$safe_zone" "$safe_domain" "$safe_value"
|
||||
return 0
|
||||
fi
|
||||
printf "%s_%s" "$safe_zone" "$safe_domain"
|
||||
}
|
||||
|
||||
_hostup_save_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
record_id="$3"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
_saveaccountconf_mutable "HOSTUP_RECORD_$key" "$record_id"
|
||||
}
|
||||
|
||||
_hostup_get_saved_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
_readaccountconf_mutable "HOSTUP_RECORD_$key"
|
||||
}
|
||||
|
||||
_hostup_clear_record_id() {
|
||||
zone_id="$1"
|
||||
domain="$2"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
txtvalue="$3"
|
||||
key="$(_hostup_record_key "$zone_id" "$domain" "$txtvalue")"
|
||||
_clearaccountconf_mutable "HOSTUP_RECORD_$key"
|
||||
}
|
||||
|
||||
_hostup_extract_record_id() {
|
||||
record_id="$(_hostup_json_extract "id" "$1")"
|
||||
if [ -n "$record_id" ]; then
|
||||
printf "%s" "$record_id"
|
||||
return 0
|
||||
legacy_key="$(_hostup_record_key "$zone_id" "$domain")"
|
||||
if [ "$legacy_key" != "$key" ]; then
|
||||
_clearaccountconf_mutable "HOSTUP_RECORD_$legacy_key"
|
||||
fi
|
||||
|
||||
printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2
|
||||
}
|
||||
|
||||
_hostup_delete_record_by_id() {
|
||||
zone_id="$1"
|
||||
record_id="$2"
|
||||
|
||||
if ! _hostup_rest "DELETE" "/dns/zones/$zone_id/records/$record_id" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$_hostup_response" '"success":true'; then
|
||||
if ! _hostup_rest "DELETE" "/dns-zones/$zone_id/records/$record_id" ""; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_hostup_problem_error() {
|
||||
problem_code="$(_hostup_json_extract "code" "$_hostup_response")"
|
||||
problem_detail="$(_hostup_json_extract "detail" "$_hostup_response")"
|
||||
|
||||
if [ -n "$problem_detail" ]; then
|
||||
if [ -n "$problem_code" ]; then
|
||||
_err "HostUp API error ($problem_code): $problem_detail"
|
||||
else
|
||||
_err "HostUp API error: $problem_detail"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_hostup_rest() {
|
||||
method="$1"
|
||||
route="$2"
|
||||
@@ -451,8 +529,7 @@ _hostup_rest() {
|
||||
_hostup_response=""
|
||||
|
||||
export _H1="Authorization: Bearer $HOSTUP_API_KEY"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
export _H2="Accept: application/json"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
_hostup_response="$(_get "$HOSTUP_API_BASE$route")"
|
||||
@@ -464,7 +541,6 @@ _hostup_rest() {
|
||||
|
||||
unset _H1
|
||||
unset _H2
|
||||
unset _H3
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
_err "HTTP request failed for $route"
|
||||
@@ -478,23 +554,23 @@ _hostup_rest() {
|
||||
case "$http_status" in
|
||||
200 | 201 | 204) return 0 ;;
|
||||
401)
|
||||
_err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
|
||||
_hostup_problem_error || _err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
|
||||
return 1
|
||||
;;
|
||||
403)
|
||||
_err "HostUp API returned 403 Forbidden. The API key lacks required DNS scopes."
|
||||
_hostup_problem_error || _err "HostUp API returned 403 Forbidden. The API key lacks required DNS/domain scopes."
|
||||
return 1
|
||||
;;
|
||||
404)
|
||||
_err "HostUp API returned 404 Not Found for $route"
|
||||
_hostup_problem_error || _err "HostUp API returned 404 Not Found for $route"
|
||||
return 1
|
||||
;;
|
||||
429)
|
||||
_err "HostUp API rate limit exceeded. Please retry later."
|
||||
_hostup_problem_error || _err "HostUp API rate limit exceeded. Please retry later."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
_err "HostUp API request failed with status $http_status"
|
||||
_hostup_problem_error || _err "HostUp API request failed with status $http_status"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -7,11 +7,11 @@ Options:
|
||||
HUAWEICLOUD_Username Username
|
||||
HUAWEICLOUD_Password Password
|
||||
HUAWEICLOUD_DomainName DomainName
|
||||
HUAWEICLOUD_Region Region. E.g. "cn-north-4". Optional, defaults to "ap-southeast-1".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/3265
|
||||
'
|
||||
|
||||
iam_api="https://iam.myhuaweicloud.com"
|
||||
dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
@@ -30,6 +30,7 @@ dns_huaweicloud_add() {
|
||||
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
|
||||
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
|
||||
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
|
||||
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
|
||||
|
||||
# Check information
|
||||
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
|
||||
@@ -37,8 +38,11 @@ dns_huaweicloud_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
|
||||
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
|
||||
|
||||
unset token # Clear token
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
|
||||
if [ -z "${token}" ]; then # Check token
|
||||
_err "dns_api(dns_huaweicloud): Error getting token."
|
||||
return 1
|
||||
@@ -65,6 +69,9 @@ dns_huaweicloud_add() {
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Username "${HUAWEICLOUD_Username}"
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Password "${HUAWEICLOUD_Password}"
|
||||
_saveaccountconf_mutable HUAWEICLOUD_DomainName "${HUAWEICLOUD_DomainName}"
|
||||
if [ -n "${HUAWEICLOUD_Region}" ]; then
|
||||
_saveaccountconf_mutable HUAWEICLOUD_Region "${HUAWEICLOUD_Region}"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -81,6 +88,7 @@ dns_huaweicloud_rm() {
|
||||
HUAWEICLOUD_Username="${HUAWEICLOUD_Username:-$(_readaccountconf_mutable HUAWEICLOUD_Username)}"
|
||||
HUAWEICLOUD_Password="${HUAWEICLOUD_Password:-$(_readaccountconf_mutable HUAWEICLOUD_Password)}"
|
||||
HUAWEICLOUD_DomainName="${HUAWEICLOUD_DomainName:-$(_readaccountconf_mutable HUAWEICLOUD_DomainName)}"
|
||||
HUAWEICLOUD_Region="${HUAWEICLOUD_Region:-$(_readaccountconf_mutable HUAWEICLOUD_Region)}"
|
||||
|
||||
# Check information
|
||||
if [ -z "${HUAWEICLOUD_Username}" ] || [ -z "${HUAWEICLOUD_Password}" ] || [ -z "${HUAWEICLOUD_DomainName}" ]; then
|
||||
@@ -88,8 +96,11 @@ dns_huaweicloud_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_huaweicloud_region="${HUAWEICLOUD_Region:-ap-southeast-1}"
|
||||
dns_api="https://dns.${_huaweicloud_region}.myhuaweicloud.com"
|
||||
|
||||
unset token # Clear token
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}")"
|
||||
token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_DomainName}" "${_huaweicloud_region}")"
|
||||
if [ -z "${token}" ]; then # Check token
|
||||
_err "dns_api(dns_huaweicloud): Error getting token."
|
||||
return 1
|
||||
@@ -298,6 +309,7 @@ _get_token() {
|
||||
_username=$1
|
||||
_password=$2
|
||||
_domain_name=$3
|
||||
_region_name=$4
|
||||
|
||||
_debug "Getting Token"
|
||||
body="{
|
||||
@@ -318,7 +330,7 @@ _get_token() {
|
||||
},
|
||||
\"scope\": {
|
||||
\"project\": {
|
||||
\"name\": \"ap-southeast-1\"
|
||||
\"name\": \"${_region_name}\"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-15
@@ -85,12 +85,10 @@ dns_infomaniak_add() {
|
||||
|
||||
# API call
|
||||
response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records")
|
||||
if [ -n "$response" ]; then
|
||||
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
|
||||
_info "Record added"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
if _contains "$response" '"result":"success"'; then
|
||||
_info "Record added"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
_err "Could not create record."
|
||||
_debug "Response: $response"
|
||||
@@ -131,7 +129,7 @@ dns_infomaniak_rm() {
|
||||
fi
|
||||
|
||||
export _H1="Authorization: Bearer $INFOMANIAK_API_TOKEN"
|
||||
export _H2="ContentType: application/json"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
@@ -169,11 +167,10 @@ dns_infomaniak_rm() {
|
||||
|
||||
# API call
|
||||
response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE)
|
||||
if [ -n "$response" ]; then
|
||||
if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then
|
||||
_info "Record deleted"
|
||||
return 0
|
||||
fi
|
||||
if _contains "$response" '"result":"success"'; then
|
||||
_info "Record deleted"
|
||||
_debug "response: $response"
|
||||
return 0
|
||||
fi
|
||||
_err "Could not delete record."
|
||||
_debug "Response: $response"
|
||||
@@ -185,7 +182,11 @@ dns_infomaniak_rm() {
|
||||
_get_zone() {
|
||||
domain="$1"
|
||||
# Whatever the domain is, you can get the fqdn with the following.
|
||||
# shellcheck disable=SC1004
|
||||
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/')
|
||||
echo "${response%%\"*}"
|
||||
response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones")
|
||||
_debug2 "_get_zone response" "$response"
|
||||
if ! _contains "$response" '"result":"success"'; then
|
||||
_err "cannot get zones for ${domain}, response: ${response}"
|
||||
return 1
|
||||
fi
|
||||
echo "$response" | _egrep_o '"fqdn" *: *"[^"]*"' | _head_n 1 | cut -d '"' -f 4
|
||||
}
|
||||
|
||||
+22
-1
@@ -307,11 +307,32 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "$h"; then
|
||||
# Anchor the match to the XML tag and escape dots so $h is compared
|
||||
# literally: _contains uses grep, which treats "$h" as a regex, and a
|
||||
# bare "g.berlight.de" would match "<string>berlight.de" (the 'g' from
|
||||
# "<string>" plus '.' matching '>'). See issue #5129.
|
||||
_hregex=$(printf "%s" "$h" | sed 's/\./\\./g')
|
||||
if _contains "$response" "<string>$_hregex</string>"; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
# IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode,
|
||||
# encode each zone name via _idn() and compare -- no python dependency.
|
||||
if _contains "$h" "xn--"; then
|
||||
_zone_unicode=$(printf "%s" "$response" | _egrep_o '<string>[^<]*' |
|
||||
sed 's/<[^>]*>//g' | while IFS= read -r _z; do
|
||||
if [ "$(_idn "$_z")" = "$h" ]; then
|
||||
printf "%s" "$_z"
|
||||
break
|
||||
fi
|
||||
done)
|
||||
if [ -n "$_zone_unicode" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$_zone_unicode"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ IONOS_TXT_TTL=60 # minimum accepted by API
|
||||
IONOS_TXT_PRIO=10
|
||||
|
||||
dns_ionos_add() {
|
||||
fulldomain=$1
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
if ! _ionos_init; then
|
||||
@@ -34,7 +34,7 @@ dns_ionos_add() {
|
||||
}
|
||||
|
||||
dns_ionos_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
if ! _ionos_init; then
|
||||
@@ -146,7 +146,7 @@ _ionos_rest() {
|
||||
|
||||
if [ "$method" != "GET" ]; then
|
||||
export _H2="Accept: application/json"
|
||||
export _H3="Content-Type: application/json"
|
||||
export _H3=
|
||||
|
||||
_response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")"
|
||||
else
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_ipprojects_info='IP-Projects DNS
|
||||
Site: ip-projects.de/
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects
|
||||
Options:
|
||||
IPP_Apikey API Key
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6958
|
||||
Author: Markus Ebner
|
||||
'
|
||||
|
||||
IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
|
||||
IPP_API="https://api.ip-projects.de/v1/dns/acme"
|
||||
|
||||
######## Public functions ########
|
||||
|
||||
dns_ipprojects_add() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using IP-Projects DNS API to add record"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _IPP_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_IPP_api_request "add" "$fulldomain" "$txtvalue"
|
||||
}
|
||||
|
||||
dns_ipprojects_rm() {
|
||||
fulldomain="$1"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using IP-Projects DNS API to remove record"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _IPP_load_credentials; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_IPP_api_request "remove" "$fulldomain" "$txtvalue"
|
||||
}
|
||||
|
||||
######## Private helpers ########
|
||||
|
||||
_IPP_load_credentials() {
|
||||
IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
|
||||
|
||||
if [ -z "$IPP_Apikey" ]; then
|
||||
_err "You must export IPP_Apikey"
|
||||
_err "e.g.: export IPP_Apikey=\"your_api_key\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable IPP_Apikey "$IPP_Apikey"
|
||||
return 0
|
||||
}
|
||||
|
||||
_IPP_api_request() {
|
||||
action="$1"
|
||||
domain="$2"
|
||||
value="$3"
|
||||
|
||||
url="$IPP_API/$action"
|
||||
|
||||
data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}"
|
||||
_debug url "$url"
|
||||
_debug data "$data"
|
||||
export _H1="X-API-Key: $IPP_Apikey"
|
||||
|
||||
response="$(_post "$data" "$url" "" "POST" "application/json")"
|
||||
ret="$?"
|
||||
_ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
|
||||
|
||||
_debug response "$response"
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
_err "HTTP request failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$_ipprojects_last_http_code" != "200" ]; then
|
||||
_err "API returned an error [code: ${_ipprojects_last_http_code}]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -136,7 +136,7 @@ _ISPC_getZoneInfo() {
|
||||
curResult="$(_post "${curData}" "${ISPC_Api}?client_get_id")"
|
||||
_debug "Calling _ISPC_ClientGetID: '${curData}' '${ISPC_Api}?client_get_id'"
|
||||
_debug "Result of _ISPC_ClientGetID: '$curResult'"
|
||||
client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | tr -d '{}')
|
||||
client_id=$(echo "${curResult}" | _egrep_o "response.*" | cut -d ':' -f 2 | cut -d '"' -f 2 | cut -d '[' -f 1 | tr -d '{}')
|
||||
_debug "Client ID: '${client_id}'"
|
||||
case "${client_id}" in
|
||||
'' | *[!0-9]*)
|
||||
|
||||
+48
-3
@@ -35,9 +35,28 @@ dns_joker_add() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Joker's /nic/replace overwrites all TXT records at the label on every call,
|
||||
# and the API is not readable, so accumulate the values locally (keyed by the
|
||||
# full record name) and re-send the whole set each time. This is required so a
|
||||
# wildcard cert (base + *.domain both validating under the same
|
||||
# _acme-challenge label) does not overwrite its own first challenge value.
|
||||
_joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_')
|
||||
_joker_values=$(_readdomainconf "$_joker_conf_key")
|
||||
if [ -z "$_joker_values" ]; then
|
||||
_joker_values="$txtvalue"
|
||||
elif ! _contains " $_joker_values " " $txtvalue "; then
|
||||
_joker_values="$_joker_values $txtvalue"
|
||||
fi
|
||||
|
||||
_joker_value_params=""
|
||||
for _joker_v in $_joker_values; do
|
||||
_joker_value_params="$_joker_value_params&value=$_joker_v"
|
||||
done
|
||||
|
||||
_info "Adding TXT record"
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value=$txtvalue"; then
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then
|
||||
if _startswith "$response" "OK"; then
|
||||
_savedomainconf "$_joker_conf_key" "$_joker_values"
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
fi
|
||||
@@ -59,10 +78,36 @@ dns_joker_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remove only this value from the accumulated set and replace the label with
|
||||
# whatever remains (an empty value clears the label's TXT records entirely).
|
||||
_joker_conf_key=$(printf "%s" "JOKER_TXT_${fulldomain}" | tr '.-' '_')
|
||||
_joker_values=$(_readdomainconf "$_joker_conf_key")
|
||||
_joker_remaining=""
|
||||
for _joker_v in $_joker_values; do
|
||||
if [ "$_joker_v" != "$txtvalue" ]; then
|
||||
_joker_remaining="$_joker_remaining $_joker_v"
|
||||
fi
|
||||
done
|
||||
_joker_remaining=$(printf "%s" "$_joker_remaining" | sed 's/^ *//')
|
||||
|
||||
_joker_value_params=""
|
||||
for _joker_v in $_joker_remaining; do
|
||||
_joker_value_params="$_joker_value_params&value=$_joker_v"
|
||||
done
|
||||
if [ -z "$_joker_value_params" ]; then
|
||||
_joker_value_params="&value="
|
||||
fi
|
||||
|
||||
_info "Removing TXT record"
|
||||
# TXT record is removed by setting its value to empty.
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT&value="; then
|
||||
# TXT record is removed by replacing the label with the remaining values
|
||||
# (or an empty value, which clears all TXT records at the label).
|
||||
if _joker_rest "username=$JOKER_USERNAME&password=$JOKER_PASSWORD&zone=$_domain&label=$_sub_domain&type=TXT$_joker_value_params"; then
|
||||
if _startswith "$response" "OK"; then
|
||||
if [ -z "$_joker_remaining" ]; then
|
||||
_cleardomainconf "$_joker_conf_key"
|
||||
else
|
||||
_savedomainconf "$_joker_conf_key" "$_joker_remaining"
|
||||
fi
|
||||
_info "Removed, OK"
|
||||
return 0
|
||||
fi
|
||||
|
||||
+12
-1
@@ -5,7 +5,8 @@ Site: www.knot-dns.cz/docs/2.5/html/man_knsupdate.html
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_knot
|
||||
Options:
|
||||
KNOT_SERVER Server hostname. Default: "localhost".
|
||||
KNOT_KEY File path to TSIG key
|
||||
KNOT_KEY TSIG key data, not a file path. knsupdate "key" statement format: "[alg:]name secret". E.g. "hmac-sha256:acme_key BASE64SECRET="
|
||||
KNOT_ZONE Zone name. Optional, set it when the challenge record lives in a delegated subdomain zone. Default: the parent domain of the challenge record.
|
||||
'
|
||||
|
||||
# See also dns_nsupdate.sh
|
||||
@@ -21,6 +22,9 @@ dns_knot_add() {
|
||||
# save the dns server and key to the account.conf file.
|
||||
_saveaccountconf KNOT_SERVER "${KNOT_SERVER}"
|
||||
_saveaccountconf KNOT_KEY "${KNOT_KEY}"
|
||||
if [ -n "${KNOT_ZONE}" ]; then
|
||||
_saveaccountconf KNOT_ZONE "${KNOT_ZONE}"
|
||||
fi
|
||||
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Domain does not exist."
|
||||
@@ -84,6 +88,13 @@ EOF
|
||||
# _domain=domain.com
|
||||
_get_root() {
|
||||
domain=$1
|
||||
# a delegated subdomain zone cannot be derived from the record name;
|
||||
# let the user name the zone explicitly (issue 2881)
|
||||
if [ -n "${KNOT_ZONE}" ]; then
|
||||
_domain="${KNOT_ZONE%.}"
|
||||
_debug "Using KNOT_ZONE zone" "${_domain}"
|
||||
return 0
|
||||
fi
|
||||
i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)"
|
||||
i=$(_math "$i" - 1)
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_laodc_info='LaoDC DNS API Server
|
||||
Site: laodc.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_laodc
|
||||
Options:
|
||||
LaoDC_Key API Key
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/6973
|
||||
Author: @laodc
|
||||
'
|
||||
|
||||
# Usage:
|
||||
# export LaoDC_Key="your-api-key"
|
||||
# acme.sh --issue --dns dns_laodc -d example.la -d *.example.la --dnssleep 120
|
||||
#
|
||||
# The credentials will be saved in ~/.acme.sh/account.conf
|
||||
|
||||
LAODC_VER="0.1.2"
|
||||
LAODC_API_ENDPOINT="https://dns.laodc.com/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_laodc_add _acme-challenge.example.la ZPXvna6tBhq7XQMH7_t2WC2sg0F-BdmtmmpUJiK6Ho
|
||||
dns_laodc_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_info "Using LaoDC DNS API"
|
||||
|
||||
_laodc_validate_key || return 1
|
||||
|
||||
_debug "Checking root zone exists for [$fulldomain]"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain_hash "$domain_hash"
|
||||
|
||||
_info "Adding acme record"
|
||||
if _laodc_api "POST" "$domain_hash" "$_sub_domain" "$txtvalue"; then
|
||||
if [ "$_code" = "201" ]; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add TXT record error, invalid code. Code: $_code"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Add TXT record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
dns_laodc_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_laodc_validate_key || return 1
|
||||
|
||||
_debug "Checking root zone exists for [$fulldomain]"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
domain_hash=$(echo "$response" | _egrep_o "\"hash\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
|
||||
_debug _root_domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain_hash "$domain_hash"
|
||||
|
||||
_info "Deleting acme record"
|
||||
if _laodc_api "DELETE" "$domain_hash" "$_sub_domain" "$txtvalue"; then
|
||||
if [ "$_code" = "204" ]; then
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Delete TXT record error, invalid code. Code: $_code"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Delete TXT record error."
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
# _acme-challenge.www.domain.com
|
||||
# returns
|
||||
# _domain=domain.com
|
||||
# _sub_domain=www
|
||||
_get_root() {
|
||||
fqdn=$1
|
||||
p=1
|
||||
i=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$fqdn" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
return 1 # not valid domain
|
||||
fi
|
||||
|
||||
# Check API if domain exists
|
||||
if _laodc_api "GET" "$h"; then
|
||||
if [ "$_code" = "200" ]; then
|
||||
_domain="$h"
|
||||
|
||||
# DNS alias mode - @ is alias for fqdn
|
||||
_sub_domain=$(printf "%s" "$fqdn" | cut -d . -f 1-"$p")
|
||||
if [ "$i" = "1" ]; then
|
||||
_sub_domain="@"
|
||||
fi
|
||||
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_laodc_validate_key() {
|
||||
LaoDC_Key="${LaoDC_Key:-$(_readaccountconf_mutable LaoDC_Key)}"
|
||||
|
||||
if [ -z "$LaoDC_Key" ]; then
|
||||
LaoDC_Key=""
|
||||
_err "You didn't specify a LaoDC API Key yet."
|
||||
_err "Please export LaoDC_Key and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save the api key to the account conf file.
|
||||
_saveaccountconf_mutable LaoDC_Key "$LaoDC_Key"
|
||||
}
|
||||
|
||||
_laodc_api() {
|
||||
method=$1
|
||||
domain=$2
|
||||
subdomain=$3
|
||||
value=$4
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
export _H2="User-Agent: acme.sh/$VER laodc-dns-acme-sh/$LAODC_VER"
|
||||
export _H3="Authorization: Bearer $LaoDC_Key"
|
||||
|
||||
case $method in
|
||||
GET)
|
||||
if [ -n "$subdomain" ]; then
|
||||
response="$(_get "$LAODC_API_ENDPOINT/$domain/$subdomain?type=TXT")"
|
||||
else
|
||||
response="$(_get "$LAODC_API_ENDPOINT/$domain")"
|
||||
fi
|
||||
;;
|
||||
POST)
|
||||
# Sanitize value input
|
||||
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
data="{ \"type\": \"TXT\", \"value\": \"$value\", \"ttl\": \"60\" }"
|
||||
response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "POST" "application/json")"
|
||||
;;
|
||||
DELETE)
|
||||
# Sanitize value input
|
||||
value=$(printf '%s' "$value" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
data="{ \"type\": \"TXT\", \"value\": \"$value\" }"
|
||||
response="$(_post "$data" "$LAODC_API_ENDPOINT/$domain/$subdomain" "" "DELETE" "application/json")"
|
||||
;;
|
||||
esac
|
||||
|
||||
_ret=$?
|
||||
|
||||
# Unset immediately after request to prevent leaks
|
||||
export _H1=
|
||||
export _H2=
|
||||
export _H3=
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
responseHeaders="$(cat "$HTTP_HEADER")"
|
||||
|
||||
if echo "$responseHeaders" | grep -i "Content-Type: *application/json" >/dev/null 2>&1; then
|
||||
response="$(echo "$response" | _json_decode | _normalizeJson)"
|
||||
fi
|
||||
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
|
||||
_debug "http response code $_code"
|
||||
_debug response "$response"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_level27_info='Level27
|
||||
Site: Level27.be
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_level27
|
||||
Options:
|
||||
LEVEL27_API_KEY API key. Get one from the Level27 control panel (https://app.level27.eu/account/profile/security).
|
||||
OptionsAlt:
|
||||
LEVEL27_API API base URL. Optional. Default "https://api.level27.eu/v1".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues
|
||||
Author: Jeroen Moors <jeroen.moors@level27.be>
|
||||
'
|
||||
|
||||
LEVEL27_API_DEFAULT="https://api.level27.eu/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_level27_add _acme-challenge.www.example.com "TXT-value"
|
||||
dns_level27_add() {
|
||||
fulldomain="$(_idn "$1")"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using Level27 to add a TXT record for $fulldomain"
|
||||
|
||||
if ! _level27_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Could not determine the root zone for $fulldomain at Level27."
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_level27_data="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\"}"
|
||||
if ! _level27_rest POST "domains/$_domain_id/records" "$_level27_data"; then
|
||||
_err "Could not add the TXT record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"id\":"; then
|
||||
_info "TXT record added."
|
||||
return 0
|
||||
fi
|
||||
|
||||
_err "Unexpected response while adding the TXT record."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: dns_level27_rm _acme-challenge.www.example.com "TXT-value"
|
||||
dns_level27_rm() {
|
||||
fulldomain="$(_idn "$1")"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using Level27 to remove the TXT record for $fulldomain"
|
||||
|
||||
if ! _level27_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Could not determine the root zone for $fulldomain at Level27."
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _level27_rest GET "domains/$_domain_id/records?type=TXT"; then
|
||||
_err "Could not list the existing TXT records."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_record_id="$(_level27_find_record_id "$response" "$txtvalue")"
|
||||
if [ -z "$_record_id" ]; then
|
||||
_info "No matching TXT record found; nothing to remove."
|
||||
return 0
|
||||
fi
|
||||
_debug _record_id "$_record_id"
|
||||
|
||||
if ! _level27_rest DELETE "domains/$_domain_id/records/$_record_id"; then
|
||||
_err "Could not remove the TXT record."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record removed."
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# Reads and validates the API credentials and endpoint, and stores them for renewals.
|
||||
_level27_init() {
|
||||
LEVEL27_API_KEY="${LEVEL27_API_KEY:-$(_readaccountconf_mutable LEVEL27_API_KEY)}"
|
||||
if [ -z "$LEVEL27_API_KEY" ]; then
|
||||
LEVEL27_API_KEY=""
|
||||
_err "You must export the variable LEVEL27_API_KEY before using the Level27 DNS API."
|
||||
_err "Get an API key from the Level27 control panel (https://app.level27.eu/account/profile/security)."
|
||||
return 1
|
||||
fi
|
||||
LEVEL27_API_KEY="$(echo "$LEVEL27_API_KEY" | tr -d '"')"
|
||||
_saveaccountconf_mutable LEVEL27_API_KEY "$LEVEL27_API_KEY"
|
||||
|
||||
LEVEL27_API="${LEVEL27_API:-$(_readaccountconf_mutable LEVEL27_API)}"
|
||||
if [ -z "$LEVEL27_API" ]; then
|
||||
LEVEL27_API="$LEVEL27_API_DEFAULT"
|
||||
fi
|
||||
_saveaccountconf_mutable LEVEL27_API "$LEVEL27_API"
|
||||
|
||||
# Remove a trailing slash so endpoints can be appended consistently.
|
||||
LEVEL27_API="$(echo "$LEVEL27_API" | sed 's#/$##')"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _get_root _acme-challenge.www.example.com
|
||||
# Splits the full domain into the registered zone and the subdomain part.
|
||||
# Sets: _domain, _domain_id, _sub_domain
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
# not valid
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _level27_rest GET "domains?filter=$h"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_level27_zones="$(echo "$response" | _normalizeJson)"
|
||||
if _contains "$_level27_zones" "\"fullname\":\"$h\""; then
|
||||
_domain_line="$(echo "$_level27_zones" | sed 's/},{/}\n{/g' | grep "\"fullname\":\"$h\"" | _head_n 1)"
|
||||
_domain_id="$(echo "$_domain_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2)"
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: _level27_find_record_id "<records-json>" "<txtvalue>"
|
||||
# Prints the id of the TXT record whose content matches the value, or nothing.
|
||||
_level27_find_record_id() {
|
||||
_records="$(echo "$1" | _normalizeJson | sed 's/},{/}\n{/g')"
|
||||
_wanted="$2"
|
||||
_record_line="$(echo "$_records" | grep "\"content\":\"$_wanted\"" | _head_n 1)"
|
||||
if [ -z "$_record_line" ]; then
|
||||
# Some APIs store TXT content wrapped in quotes.
|
||||
_record_line="$(echo "$_records" | grep "\"content\":\"\\\\\"$_wanted\\\\\"\"" | _head_n 1)"
|
||||
fi
|
||||
if [ -z "$_record_line" ]; then
|
||||
return 0
|
||||
fi
|
||||
echo "$_record_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2
|
||||
}
|
||||
|
||||
# Usage: _level27_rest <method> <endpoint> [data]
|
||||
# Performs an authenticated API call and stores the body in $response.
|
||||
_level27_rest() {
|
||||
m="$1"
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
export _H1="Authorization: $LEVEL27_API_KEY"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
_debug2 data "$data"
|
||||
response="$(_post "$data" "$LEVEL27_API/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$LEVEL27_API/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Error querying the Level27 API endpoint: $ep"
|
||||
return 1
|
||||
fi
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
+1
-1
@@ -140,7 +140,7 @@ _me_rest() {
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
cdate=$(LANG=C date -u +"%a, %d %b %Y %T %Z")
|
||||
cdate=$(LC_ALL=C date -u +"%a, %d %b %Y %T %Z")
|
||||
hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex)
|
||||
|
||||
export _H1="x-dnsme-apiKey: $ME_Key"
|
||||
|
||||
Executable
+167
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_muumuu_info='muumuu-domain.com
|
||||
Site: muumuu-domain.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_muumuu
|
||||
Options:
|
||||
MUUMUU_PAT Personal Access Token (scopes: domains:read, dns:read, dns:write)
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7011
|
||||
'
|
||||
|
||||
MUUMUU_API="https://muumuu-domain.com/api/v2"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
dns_muumuu_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using muumuu-domain.com DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}"
|
||||
if [ -z "$MUUMUU_PAT" ]; then
|
||||
_err "MUUMUU_PAT is not set."
|
||||
_err "Please create a Personal Access Token at https://muumuu-domain.com"
|
||||
_err "with scopes: domains:read, dns:read, dns:write"
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable MUUMUU_PAT "$MUUMUU_PAT"
|
||||
|
||||
if ! _muumuu_get_root "$fulldomain"; then
|
||||
_err "Unable to find the root domain for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Adding TXT record for ${fulldomain}"
|
||||
body="{\"fqdn\":\"${fulldomain}.\",\"type\":\"TXT\",\"value\":\"${txtvalue}\",\"ttl\":3600}"
|
||||
if _muumuu_rest POST "/me/domains/${_domain_id}/dns-records" "$body"; then
|
||||
if [ "$_muumuu_code" = "201" ]; then
|
||||
_info "TXT record added successfully"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Failed to add TXT record (HTTP ${_muumuu_code})"
|
||||
return 1
|
||||
}
|
||||
|
||||
dns_muumuu_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue="$2"
|
||||
|
||||
_info "Using muumuu-domain.com DNS API"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
MUUMUU_PAT="${MUUMUU_PAT:-$(_readaccountconf_mutable MUUMUU_PAT)}"
|
||||
if [ -z "$MUUMUU_PAT" ]; then
|
||||
_err "MUUMUU_PAT is not set."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _muumuu_get_root "$fulldomain"; then
|
||||
_err "Unable to find the root domain for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
|
||||
_info "Looking up TXT record for ${fulldomain}"
|
||||
if ! _muumuu_rest GET "/me/domains/${_domain_id}/dns-records?type=TXT&fqdn=${fulldomain}."; then
|
||||
_err "Failed to list TXT records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
record_id=$(echo "$response" | _egrep_o "\"id\":[0-9]+[^}]*\"value\":\"${txtvalue}\"" | _egrep_o "\"id\":[0-9]+" | _head_n 1 | cut -d: -f2)
|
||||
if [ -z "$record_id" ]; then
|
||||
_info "TXT record not found, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
_debug record_id "$record_id"
|
||||
|
||||
if _muumuu_rest DELETE "/me/domains/${_domain_id}/dns-records/${record_id}"; then
|
||||
if [ "$_muumuu_code" = "204" ]; then
|
||||
_info "TXT record deleted successfully"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
_err "Failed to delete TXT record (HTTP ${_muumuu_code})"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# _acme-challenge.www.example.com
|
||||
# sets:
|
||||
# _domain_id MU00000001
|
||||
# _sub_domain _acme-challenge.www
|
||||
# _domain example.com
|
||||
_muumuu_get_root() {
|
||||
domain="$1"
|
||||
i=1
|
||||
p=0
|
||||
h=""
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
if ! _muumuu_rest GET "/me/domains?fqdn=${h}&page-size=1"; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$_muumuu_code" = "401" ] || [ "$_muumuu_code" = "403" ]; then
|
||||
_err "Authentication failed (HTTP ${_muumuu_code}). Check MUUMUU_PAT."
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "\"fqdn\":\"${h}\""; then
|
||||
_domain_id=$(echo "$response" | _egrep_o "\"id\":\"MU[0-9]+\"" | _head_n 1 | cut -d: -f2 | tr -d '"')
|
||||
_domain="$h"
|
||||
if [ "$p" = "0" ]; then
|
||||
_sub_domain=""
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_muumuu_rest() {
|
||||
_muumuu_method="$1"
|
||||
_muumuu_path="$2"
|
||||
_muumuu_data="$3"
|
||||
_muumuu_url="${MUUMUU_API}${_muumuu_path}"
|
||||
|
||||
export _H1="Authorization: Bearer ${MUUMUU_PAT}"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
export _H4=""
|
||||
export _H5=""
|
||||
|
||||
_secure_debug2 data "$_muumuu_data"
|
||||
|
||||
if [ "$_muumuu_method" = "GET" ]; then
|
||||
response="$(_get "$_muumuu_url")"
|
||||
else
|
||||
response="$(_post "$_muumuu_data" "$_muumuu_url" "" "$_muumuu_method")"
|
||||
fi
|
||||
_muumuu_ret="$?"
|
||||
_muumuu_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
_debug "HTTP code: ${_muumuu_code}"
|
||||
_secure_debug2 response "$response"
|
||||
|
||||
if [ "$_muumuu_ret" != "0" ]; then
|
||||
_err "Error accessing ${_muumuu_url}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
response="$(printf "%s" "$response" | _normalizeJson)"
|
||||
return 0
|
||||
}
|
||||
@@ -186,7 +186,7 @@ _oauth2() {
|
||||
_oauth2_std() {
|
||||
# HTTP Basic Authentication
|
||||
_H1="Authorization: Basic $(echo "$MB_AK:$MB_AS" | _base64)"
|
||||
_H2="Accepts: application/json"
|
||||
_H2="Accept: application/json"
|
||||
export _H1 _H2
|
||||
body="grant_type=client_credentials"
|
||||
|
||||
@@ -210,7 +210,7 @@ _oauth2_std() {
|
||||
}
|
||||
|
||||
_oauth2_github() {
|
||||
_H1="Accepts: application/json"
|
||||
_H1="Accept: application/json"
|
||||
export _H1
|
||||
body="{\"login\":{\"handle\":\"$MB_AK\",\"pass\":\"$MB_AS\",\"floating\":1}}"
|
||||
|
||||
@@ -241,7 +241,7 @@ _mb_rest() {
|
||||
fi
|
||||
|
||||
_H1="Authorization: Bearer $MB_TK"
|
||||
_H2="Accepts: application/json"
|
||||
_H2="Accept: application/json"
|
||||
export _H1 _H2
|
||||
if [ "$data" ] || [ "$m" = "POST" ] || [ "$m" = "PUT" ] || [ "$m" = "DELETE" ]; then
|
||||
# body url [needbase64] [POST|PUT|DELETE] [ContentType]
|
||||
|
||||
+10
-2
@@ -264,8 +264,16 @@ _set_namecheap_TXT() {
|
||||
_debug hosts "$hosts"
|
||||
|
||||
if [ -z "$hosts" ]; then
|
||||
_err "Hosts not found"
|
||||
return 1
|
||||
# An empty host list is only acceptable when the API positively confirms
|
||||
# a successful getHosts reply: setHosts below REPLACES all records, so
|
||||
# proceeding on a malformed/unparsed response would wipe the whole zone.
|
||||
# https://github.com/acmesh-official/acme.sh/issues/6963
|
||||
if _contains "$response" "Status=\"OK\"" && _contains "$response" "DomainDNSGetHostsResult"; then
|
||||
_debug "No existing host records, adding the TXT record as the first one"
|
||||
else
|
||||
_err "Hosts not found"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_namecheap_reset_hostList
|
||||
|
||||
@@ -15,7 +15,7 @@ Namecom_API="https://api.name.com/v4"
|
||||
|
||||
#Usage: dns_namecom_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_namecom_add() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
Namecom_Username="${Namecom_Username:-$(_readaccountconf_mutable Namecom_Username)}"
|
||||
@@ -68,7 +68,7 @@ dns_namecom_add() {
|
||||
#Usage: fulldomain txtvalue
|
||||
#Remove the txt record after validation.
|
||||
dns_namecom_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
Namecom_Username="${Namecom_Username:-$(_readaccountconf_mutable Namecom_Username)}"
|
||||
@@ -153,10 +153,9 @@ _namecom_get_root() {
|
||||
i=2
|
||||
p=1
|
||||
|
||||
if ! _namecom_rest GET "domains"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Probe each candidate with GetDomain (GET /v4/domains/{domainName}) instead
|
||||
# of listing all domains: the list is paginated at 1000 domains per page, so
|
||||
# larger accounts never found their domain on the first page.
|
||||
# Need to exclude the last field (tld)
|
||||
numfields=$(echo "$domain" | _egrep_o "\." | wc -l)
|
||||
while [ "$i" -le "$numfields" ]; do
|
||||
@@ -166,7 +165,7 @@ _namecom_get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "$host"; then
|
||||
if _namecom_rest GET "domains/$host" && _contains "$response" "\"domainName\":\"$host\""; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$host"
|
||||
return 0
|
||||
|
||||
@@ -65,7 +65,7 @@ dns_namesilo_rm() {
|
||||
if _namesilo_rest GET "dnsListRecords?version=1&type=xml&key=$Namesilo_Key&domain=$_domain"; then
|
||||
retcode=$(printf "%s\n" "$response" | _egrep_o "<code>300")
|
||||
if [ "$retcode" ]; then
|
||||
_record_id=$(echo "$response" | _egrep_o "<record_id>([^<]*)</record_id><type>TXT</type><host>$fulldomain</host>" | _egrep_o "<record_id>([^<]*)</record_id>" | sed -r "s/<record_id>([^<]*)<\/record_id>/\1/" | tail -n 1)
|
||||
_record_id=$(echo "$response" | _egrep_o "<record_id>([^<]*)</record_id><type>TXT</type><host>$_sub_domain</host><value>$txtvalue</value>" | _egrep_o "<record_id>([^<]*)</record_id>" | sed -r "s/<record_id>([^<]*)<\/record_id>/\1/" | tail -n 1)
|
||||
_debug _record_id "$_record_id"
|
||||
if [ "$_record_id" ]; then
|
||||
_info "Successfully retrieved the record id for ACME challenge."
|
||||
|
||||
@@ -98,7 +98,7 @@ dns_njalla_rm() {
|
||||
echo "$records" | while read -r record; do
|
||||
record_name=$(echo "$record" | _egrep_o "\"name\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \")
|
||||
record_content=$(echo "$record" | _egrep_o "\"content\":\s?\"[^\"]*\"" | cut -d : -f 2 | tr -d " " | tr -d \")
|
||||
record_id=$(echo "$record" | _egrep_o "\"id\":\s?[0-9]+" | cut -d : -f 2 | tr -d " " | tr -d \")
|
||||
record_id=$(echo "$record" | _egrep_o "\"id\":\s?\"?[^\",}]*" | cut -d : -f 2 | tr -d " " | tr -d \")
|
||||
if [ "$_sub_domain" = "$record_name" ]; then
|
||||
if [ "$txtvalue" = "$record_content" ]; then
|
||||
_debug "record_id" "$record_id"
|
||||
|
||||
+31
-22
@@ -115,12 +115,15 @@ _oci_config() {
|
||||
_clearaccountconf_mutable OCI_CLI_PROFILE
|
||||
fi
|
||||
|
||||
OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readaccountconf_mutable OCI_CLI_TENANCY)}"
|
||||
if [ -z "$OCI_CLI_TENANCY" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE"
|
||||
OCI_CLI_TENANCY=$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE")
|
||||
fi
|
||||
if [ -z "$OCI_CLI_TENANCY" ]; then
|
||||
OCI_CLI_TENANCY=$(_readaccountconf_mutable OCI_CLI_TENANCY)
|
||||
fi
|
||||
if [ "$OCI_CLI_TENANCY" ]; then
|
||||
_saveaccountconf_mutable OCI_CLI_TENANCY "$OCI_CLI_TENANCY"
|
||||
elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_TENANCY value from: $OCI_CLI_CONFIG_FILE"
|
||||
OCI_CLI_TENANCY="${OCI_CLI_TENANCY:-$(_readini "$OCI_CLI_CONFIG_FILE" tenancy "$OCI_CLI_PROFILE")}"
|
||||
fi
|
||||
|
||||
if [ -z "$OCI_CLI_TENANCY" ]; then
|
||||
@@ -128,41 +131,47 @@ _oci_config() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
OCI_CLI_USER="${OCI_CLI_USER:-$(_readaccountconf_mutable OCI_CLI_USER)}"
|
||||
if [ -z "$OCI_CLI_USER" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE"
|
||||
OCI_CLI_USER=$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE")
|
||||
fi
|
||||
if [ -z "$OCI_CLI_USER" ]; then
|
||||
OCI_CLI_USER=$(_readaccountconf_mutable OCI_CLI_USER)
|
||||
fi
|
||||
if [ "$OCI_CLI_USER" ]; then
|
||||
_saveaccountconf_mutable OCI_CLI_USER "$OCI_CLI_USER"
|
||||
elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_USER value from: $OCI_CLI_CONFIG_FILE"
|
||||
OCI_CLI_USER="${OCI_CLI_USER:-$(_readini "$OCI_CLI_CONFIG_FILE" user "$OCI_CLI_PROFILE")}"
|
||||
fi
|
||||
if [ -z "$OCI_CLI_USER" ]; then
|
||||
_err "Error: unable to read OCI_CLI_USER from config file or environment variable."
|
||||
return 1
|
||||
fi
|
||||
|
||||
OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readaccountconf_mutable OCI_CLI_REGION)}"
|
||||
if [ -z "$OCI_CLI_REGION" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE"
|
||||
OCI_CLI_REGION=$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE")
|
||||
fi
|
||||
if [ -z "$OCI_CLI_REGION" ]; then
|
||||
OCI_CLI_REGION=$(_readaccountconf_mutable OCI_CLI_REGION)
|
||||
fi
|
||||
if [ "$OCI_CLI_REGION" ]; then
|
||||
_saveaccountconf_mutable OCI_CLI_REGION "$OCI_CLI_REGION"
|
||||
elif [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_REGION value from: $OCI_CLI_CONFIG_FILE"
|
||||
OCI_CLI_REGION="${OCI_CLI_REGION:-$(_readini "$OCI_CLI_CONFIG_FILE" region "$OCI_CLI_PROFILE")}"
|
||||
fi
|
||||
if [ -z "$OCI_CLI_REGION" ]; then
|
||||
_err "Error: unable to read OCI_CLI_REGION from config file or environment variable."
|
||||
return 1
|
||||
fi
|
||||
|
||||
OCI_CLI_KEY="${OCI_CLI_KEY:-$(_readaccountconf_mutable OCI_CLI_KEY)}"
|
||||
if [ -z "$OCI_CLI_KEY" ]; then
|
||||
_clearaccountconf_mutable OCI_CLI_KEY
|
||||
OCI_CLI_KEY_FILE="${OCI_CLI_KEY_FILE:-$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE")}"
|
||||
if [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE"
|
||||
OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE")
|
||||
_saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY"
|
||||
fi
|
||||
else
|
||||
if [ -z "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_CONFIG_FILE" ]; then
|
||||
OCI_CLI_KEY_FILE=$(_readini "$OCI_CLI_CONFIG_FILE" key_file "$OCI_CLI_PROFILE")
|
||||
fi
|
||||
if [ "$OCI_CLI_KEY" ]; then
|
||||
_saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY"
|
||||
elif [ "$OCI_CLI_KEY_FILE" ] && [ -f "$OCI_CLI_KEY_FILE" ]; then
|
||||
_debug "Reading OCI_CLI_KEY value from: $OCI_CLI_KEY_FILE"
|
||||
OCI_CLI_KEY=$(_base64 <"$OCI_CLI_KEY_FILE")
|
||||
_saveaccountconf_mutable OCI_CLI_KEY "$OCI_CLI_KEY"
|
||||
else
|
||||
OCI_CLI_KEY=$(_readaccountconf_mutable OCI_CLI_KEY)
|
||||
fi
|
||||
|
||||
if [ -z "$OCI_CLI_KEY_FILE" ] && [ -z "$OCI_CLI_KEY" ]; then
|
||||
|
||||
+1
-1
@@ -224,7 +224,7 @@ _ovh_authentication() {
|
||||
_H3=""
|
||||
_H4=""
|
||||
|
||||
_ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}'
|
||||
_ovhdata='{"accessRules": [{"method": "GET","path": "/auth/time"},{"method": "GET","path": "/domain"},{"method": "GET","path": "/domain/zone/*"},{"method": "GET","path": "/domain/zone/*/record"},{"method": "GET","path": "/domain/zone/*/record/*"},{"method": "POST","path": "/domain/zone/*/record"},{"method": "POST","path": "/domain/zone/*/refresh"},{"method": "PUT","path": "/domain/zone/*/record/*"},{"method": "DELETE","path": "/domain/zone/*/record/*"}],"redirection":"'$ovh_success'"}'
|
||||
|
||||
response="$(_post "$_ovhdata" "$OVH_API/auth/credential")"
|
||||
_debug3 response "$response"
|
||||
|
||||
+16
-9
@@ -50,6 +50,9 @@ dns_pdns_add() {
|
||||
PDNS_Ttl="$DEFAULT_PDNS_TTL"
|
||||
fi
|
||||
|
||||
# Ensure PDNS_Url has no trailing slash ('/')
|
||||
PDNS_Url="${PDNS_Url%/}"
|
||||
|
||||
#save the api addr and key to the account conf file.
|
||||
_saveaccountconf_mutable PDNS_Url "$PDNS_Url"
|
||||
_saveaccountconf_mutable PDNS_ServerId "$PDNS_ServerId"
|
||||
@@ -186,19 +189,23 @@ _get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
|
||||
if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then
|
||||
_zones_response=$(echo "$response" | _normalizeJson)
|
||||
fi
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
|
||||
if _contains "$_zones_response" "\"name\":\"$h.\""; then
|
||||
_domain="$h."
|
||||
if [ -z "$h" ]; then
|
||||
_domain="=2E"
|
||||
# Probe each candidate zone with the server-side name filter instead of
|
||||
# listing every zone: with large installations (100k zones) the
|
||||
# unfiltered list takes minutes. Servers that ignore the parameter
|
||||
# return the full list, which the check below still handles.
|
||||
# https://doc.powerdns.com/authoritative/http-api/zone.html
|
||||
if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones?zone=$h."; then
|
||||
_zones_response=$(echo "$response" | _normalizeJson)
|
||||
if _contains "$_zones_response" "\"name\":\"$h.\""; then
|
||||
_domain="$h."
|
||||
if [ -z "$h" ]; then
|
||||
_domain="=2E"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
|
||||
@@ -151,8 +151,8 @@ dns_pleskxml_rm() {
|
||||
# Extracting the id of the TXT record for the full domain (NOT case-sensitive) and corresponding value
|
||||
recid="$(
|
||||
_value "$reclist" |
|
||||
grep -i "<host>${fulldomain}.</host>" |
|
||||
grep "<value>${txtvalue}</value>" |
|
||||
grep -Fi "<host>${fulldomain}.</host>" |
|
||||
grep -F "<value>${txtvalue}</value>" |
|
||||
sed 's/^.*<id>\([0-9]\{1,\}\)<\/id>.*$/\1/'
|
||||
)"
|
||||
|
||||
@@ -419,7 +419,7 @@ _pleskxml_get_root_domain() {
|
||||
|
||||
_debug "Checking if '$root_domain_name' is managed by the Plesk server..."
|
||||
|
||||
root_domain_id="$(_value "$output" | grep "<name>$root_domain_name</name>" | _head_n 1 | sed 's/^.*<id>\([0-9]\{1,\}\)<\/id>.*$/\1/')"
|
||||
root_domain_id="$(_value "$output" | grep -F "<name>$root_domain_name</name>" | _head_n 1 | sed 's/^.*<id>\([0-9]\{1,\}\)<\/id>.*$/\1/')"
|
||||
|
||||
if [ -n "$root_domain_id" ]; then
|
||||
# Found a match
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
# Credits to the authors of dnsapi/dns_pdns.sh as this reuses much of that code.
|
||||
|
||||
dns_poweradmin_info='Poweradmin API
|
||||
Site: https://www.poweradmin.org/
|
||||
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_poweradmin
|
||||
Options:
|
||||
POWERADMIN_URL API URL (with scheme). E.g. "https://poweradmin.example.com" or "http://192.168.0.10:8080"
|
||||
POWERADMIN_API_KEY API Token "pwa_xxxx"
|
||||
POWERADMIN_API_VERSION Optionally override Poweradmin API version.
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/6912
|
||||
Author: Jakob Næss <https://github.com/InvisibleDuck>
|
||||
'
|
||||
|
||||
######## Public functions ####################
|
||||
|
||||
# Usage: dns_poweradmin_add _acme-challenge.www.domain.com "123456789ABCDEF"
|
||||
# fulldomain
|
||||
# txtvalue
|
||||
dns_poweradmin_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}"
|
||||
POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}"
|
||||
POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}"
|
||||
POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}"
|
||||
|
||||
if [ -z "$POWERADMIN_URL" ]; then
|
||||
POWERADMIN_URL=""
|
||||
_err "You didn't specify Poweradmin URL."
|
||||
_err "Please set POWERADMIN_URL and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$POWERADMIN_API_KEY" ]; then
|
||||
POWERADMIN_API_KEY=""
|
||||
_err "You didn't specify Poweradmin token."
|
||||
_err "Please set POWERADMIN_API_KEY and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Save the api addr, key, and version to the account conf file.
|
||||
_saveaccountconf_mutable POWERADMIN_URL "$POWERADMIN_URL"
|
||||
_saveaccountconf_mutable POWERADMIN_API_KEY "$POWERADMIN_API_KEY"
|
||||
_saveaccountconf_mutable POWERADMIN_API_VERSION "$POWERADMIN_API_VERSION"
|
||||
|
||||
_debug "Detect root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _zone_id "$_zone_id"
|
||||
|
||||
if ! _set_record "$fulldomain" "$txtvalue"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: dns_poweradmin_rm _acme-challenge.www.domain.com "123456789ABCDEF"
|
||||
# fulldomain
|
||||
# txtvalue
|
||||
dns_poweradmin_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}"
|
||||
POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}"
|
||||
POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}"
|
||||
POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}"
|
||||
|
||||
_debug "Detect root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _zone_id "$_zone_id"
|
||||
|
||||
if ! _rm_record "$fulldomain" "$txtvalue"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions below #####################
|
||||
|
||||
_set_record() {
|
||||
_info "Adding TXT record"
|
||||
full=$1
|
||||
new_challenge=$2
|
||||
|
||||
data='{"name":"'$full'","type":"TXT","content":"'$new_challenge'","ttl":60}'
|
||||
|
||||
if ! _poweradmin_rest "POST" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records" "$data" "application/json"; then
|
||||
_err "Failed to add TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_rm_record() {
|
||||
_info "Remove TXT record"
|
||||
full=$1
|
||||
txtvalue=$2
|
||||
|
||||
if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then
|
||||
_err "Failed to retrieve records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]}
|
||||
_txt_record_obj=$(
|
||||
printf '%s\n' "$response" |
|
||||
sed 's/^.*"data":\[//; s/\],"message":.*$//' |
|
||||
awk '{ gsub(/},{/, "}\n{"); print }' |
|
||||
grep -F "\"name\":\"$full\"" |
|
||||
grep -F "\"type\":\"TXT\"" |
|
||||
grep -F "\"content\":\"$txtvalue\"" |
|
||||
_head_n 1
|
||||
)
|
||||
|
||||
if [ -z "$_txt_record_obj" ]; then
|
||||
_info "TXT record not found for $full with content $txtvalue"
|
||||
return 0
|
||||
fi
|
||||
|
||||
record_id=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' | _head_n 1)
|
||||
record_type=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p' | _head_n 1)
|
||||
record_name=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | _head_n 1)
|
||||
record_content=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | _head_n 1)
|
||||
|
||||
_debug2 "_txt_record_obj=$_txt_record_obj"
|
||||
_debug2 "record id: $record_id"
|
||||
_debug2 "record type: $record_type"
|
||||
_debug2 "record name: $record_name"
|
||||
_debug2 "record content: $record_content"
|
||||
|
||||
if [ "$record_type" != "TXT" ]; then
|
||||
_err "Refusing to delete non-TXT record id=$record_id type=$record_type name=$full"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _poweradmin_rest "DELETE" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records/$record_id"; then
|
||||
_err "Failed to delete TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Record deleted successfully"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _acme-challenge.www.domain.com
|
||||
# returns
|
||||
# _domain=domain.com
|
||||
# _zone_id=220
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
|
||||
if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones"; then
|
||||
_err "Failed to retrieve zones"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_zones_response="$response"
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
_debug "Root domain not found for $domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_obj=$(
|
||||
printf '%s' "$_zones_response" |
|
||||
sed 's/},{/}\n{/g' |
|
||||
grep -F "\"name\":\"$h\"" |
|
||||
_head_n 1
|
||||
)
|
||||
|
||||
if [ -n "$zone_obj" ]; then
|
||||
_zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2)
|
||||
_domain="$h"
|
||||
_debug "Found zone: $_domain with id: $_zone_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_poweradmin_rest() {
|
||||
method=$1
|
||||
ep=$2
|
||||
data=$3
|
||||
ct=$4
|
||||
|
||||
export _H1="X-API-Key: $POWERADMIN_API_KEY"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$POWERADMIN_URL$ep")"
|
||||
else
|
||||
_debug "API call: $method $ep"
|
||||
_debug "Content-Type: $ct"
|
||||
_debug "Payload: $data"
|
||||
response="$(_post "$data" "$POWERADMIN_URL$ep" "" "$method" "$ct")"
|
||||
fi
|
||||
|
||||
# Clear _H1 variable
|
||||
unset -v _H1
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "API error on $method $ep"
|
||||
_debug "Response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then
|
||||
_err "API reported failure on $method $ep"
|
||||
_debug "Response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 "API Response: $response"
|
||||
return 0
|
||||
}
|
||||
+2
-2
@@ -96,8 +96,8 @@ _get_root() {
|
||||
|
||||
for ITEM in ${domains_list}; do
|
||||
IDN_ITEM=${ITEM}
|
||||
case "${domain}" in
|
||||
*${IDN_ITEM}*)
|
||||
case ".${domain}" in
|
||||
*.${IDN_ITEM}*)
|
||||
_domain="$(_idn "${ITEM}")"
|
||||
_debug _domain "${_domain}"
|
||||
return 0
|
||||
|
||||
@@ -7,6 +7,7 @@ Options:
|
||||
SELFHOSTDNS_USERNAME Username
|
||||
SELFHOSTDNS_PASSWORD Password
|
||||
SELFHOSTDNS_MAP Subdomain name
|
||||
SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://account.selfhost.de/cgi-bin/api.pl"
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/4291
|
||||
Author: Marvin Edeler
|
||||
'
|
||||
@@ -18,9 +19,11 @@ dns_selfhost_add() {
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txt"
|
||||
|
||||
SELFHOSTDNS_UPDATE_URL="https://selfhost.de/cgi-bin/api.pl"
|
||||
DEFAULT_SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl"
|
||||
|
||||
# Get values, but don't save until we successfully validated
|
||||
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}"
|
||||
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}"
|
||||
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}"
|
||||
SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}"
|
||||
# These values are domain dependent, so read them from there
|
||||
@@ -84,6 +87,11 @@ dns_selfhost_add() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Save api url if different from default
|
||||
if [ "$DEFAULT_SELFHOSTDNS_UPDATE_URL" != "$SELFHOSTDNS_UPDATE_URL" ]; then
|
||||
_saveaccountconf_mutable SELFHOSTDNS_UPDATE_URL "$SELFHOSTDNS_UPDATE_URL"
|
||||
fi
|
||||
|
||||
# Now that we know the values are good, save them
|
||||
_saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME"
|
||||
_saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD"
|
||||
|
||||
+57
-35
@@ -8,11 +8,7 @@ Options:
|
||||
SIMPLY_ApiKey API Key
|
||||
'
|
||||
|
||||
#SIMPLY_Api="https://api.simply.com/2/"
|
||||
SIMPLY_Api_Default="https://api.simply.com/2"
|
||||
|
||||
#This is used for determining success of REST call
|
||||
SIMPLY_SUCCESS_CODE='"status":200'
|
||||
SIMPLY_Api="https://api.simply.com/2"
|
||||
|
||||
######## Public functions #####################
|
||||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
@@ -72,7 +68,16 @@ dns_simply_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ')
|
||||
case "$_simply_http_code" in
|
||||
2*) ;;
|
||||
*)
|
||||
_err "Failed to fetch DNS records (HTTP $_simply_http_code)"
|
||||
_err "$response"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
records=$(echo "$response" | tr '{' "\n" | grep -E 'record_id|type|data|name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ')
|
||||
|
||||
nr_of_deleted_records=0
|
||||
_info "Fetching txt record"
|
||||
@@ -95,7 +100,7 @@ dns_simply_rm() {
|
||||
|
||||
if [ "$record_id" -gt 0 ]; then
|
||||
|
||||
if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then
|
||||
if ! _simply_delete_record "$_domain" "$record_id"; then
|
||||
_err "Record with id $record_id could not be deleted"
|
||||
return 1
|
||||
fi
|
||||
@@ -122,14 +127,9 @@ dns_simply_rm() {
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_simply_load_config() {
|
||||
SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}"
|
||||
SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}"
|
||||
SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}"
|
||||
|
||||
if [ -z "$SIMPLY_Api" ]; then
|
||||
SIMPLY_Api="$SIMPLY_Api_Default"
|
||||
fi
|
||||
|
||||
if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then
|
||||
SIMPLY_AccountName=""
|
||||
SIMPLY_ApiKey=""
|
||||
@@ -144,9 +144,6 @@ _simply_load_config() {
|
||||
}
|
||||
|
||||
_simply_save_config() {
|
||||
if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then
|
||||
_saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api"
|
||||
fi
|
||||
_saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName"
|
||||
_saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey"
|
||||
}
|
||||
@@ -163,26 +160,39 @@ _simply_get_all_records() {
|
||||
|
||||
_get_root() {
|
||||
domain=$1
|
||||
|
||||
if ! _simply_rest GET "my/products/"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$_simply_http_code" in
|
||||
2*) ;;
|
||||
*)
|
||||
_err "Failed to fetch product list (HTTP $_simply_http_code)"
|
||||
_err "$response"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
i=2
|
||||
p=1
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _simply_rest GET "my/products/$h/dns/"; then
|
||||
return 1
|
||||
fi
|
||||
_domain=$(printf "%s" "$response" | tr '}' '\n' |
|
||||
grep -F -e "\"object\":\"$h\"" -e "\"name\":\"$h\"" -e "\"name_idn\":\"$h\"" |
|
||||
sed -n 's/.*"object":"\([^"]*\)".*/\1/p' |
|
||||
_head_n 1)
|
||||
|
||||
if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
|
||||
_debug "$h not found"
|
||||
else
|
||||
if [ -n "$_domain" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "No Simply.com product found for $h"
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
@@ -194,39 +204,44 @@ _simply_add_record() {
|
||||
sub_domain=$2
|
||||
txtval=$3
|
||||
|
||||
data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}"
|
||||
data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 120}"
|
||||
|
||||
if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then
|
||||
_err "Adding record not successfull!"
|
||||
_err "Adding record not successful!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
|
||||
_err "Call to API not sucessfull, see below message for more details"
|
||||
case "$_simply_http_code" in
|
||||
2*) ;;
|
||||
*)
|
||||
_err "Call to API not successful (HTTP $_simply_http_code), see below message for more details"
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_simply_delete_record() {
|
||||
domain=$1
|
||||
sub_domain=$2
|
||||
record_id=$3
|
||||
record_id=$2
|
||||
|
||||
_debug record_id "Delete record with id $record_id"
|
||||
|
||||
if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then
|
||||
_err "Deleting record not successfull!"
|
||||
_err "Deleting record not successful!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
|
||||
_err "Call to API not sucessfull, see below message for more details"
|
||||
case "$_simply_http_code" in
|
||||
2*) ;;
|
||||
*)
|
||||
_err "Call to API not successful (HTTP $_simply_http_code), see below message for more details"
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -248,17 +263,24 @@ _simply_rest() {
|
||||
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
: >"$HTTP_HEADER"
|
||||
|
||||
if [ "$m" != "GET" ]; then
|
||||
response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")"
|
||||
else
|
||||
response="$(_get "$SIMPLY_Api/$ep")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_ret="$?"
|
||||
unset _H1 _H2
|
||||
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_simply_http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d' ' -f2 | tr -d '\r\n')"
|
||||
|
||||
response="$(echo "$response" | _normalizeJson)"
|
||||
|
||||
_debug2 response "$response"
|
||||
|
||||
Executable
+297
@@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_volcengine_info='Volcano Engine DNS API
|
||||
Site: https://www.volcengine.com/docs/6758/155086
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_volcengine
|
||||
Options:
|
||||
Volcengine_ACCESS_KEY_ID API Key ID
|
||||
Volcengine_SECRET_ACCESS_KEY API Secret
|
||||
Volcengine_SESSION_TOKEN Session Token. Optional, only needed when using temporary STS credentials.
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7064
|
||||
'
|
||||
|
||||
Volcengine_HOST="dns.volcengineapi.com"
|
||||
Volcengine_URL="https://$Volcengine_HOST"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_volcengine_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
_record_id=""
|
||||
|
||||
Volcengine_ACCESS_KEY_ID="${Volcengine_ACCESS_KEY_ID:-$(_readaccountconf_mutable Volcengine_ACCESS_KEY_ID)}"
|
||||
Volcengine_SECRET_ACCESS_KEY="${Volcengine_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable Volcengine_SECRET_ACCESS_KEY)}"
|
||||
|
||||
if [ -z "$Volcengine_ACCESS_KEY_ID" ] || [ -z "$Volcengine_SECRET_ACCESS_KEY" ]; then
|
||||
Volcengine_ACCESS_KEY_ID=""
|
||||
Volcengine_SECRET_ACCESS_KEY=""
|
||||
_err "You haven't specified the volcengine dns api key id and api key secret yet."
|
||||
return 1
|
||||
fi
|
||||
|
||||
#save the api key and email to the account conf file.
|
||||
_saveaccountconf_mutable Volcengine_ACCESS_KEY_ID "$Volcengine_ACCESS_KEY_ID"
|
||||
_saveaccountconf_mutable Volcengine_SECRET_ACCESS_KEY "$Volcengine_SECRET_ACCESS_KEY"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
_sleep 1
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
# _info "Getting existing records for $fulldomain"
|
||||
if ! volcengine_rest POST "" "Action=ListRecords&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"SearchMode\":\"exact\"}"; then
|
||||
_sleep 1
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ListRecords already filtered by ZID + Host + Value + SearchMode:exact,
|
||||
# so any returned record is our target. Don't match on FQDN: Volcengine
|
||||
# lowercases the Host/FQDN in the response, which would break a
|
||||
# case-sensitive string compare against $fulldomain.
|
||||
_record_id="$(echo "$response" | _egrep_o "\"RecordID\":\"[0-9]+\"," | cut -d: -f2 | cut -d, -f1 | tr -d '"')"
|
||||
_debug "_record_id" "$_record_id"
|
||||
|
||||
if [ "$_record_id" ] && _contains "$response" "$txtvalue"; then
|
||||
_info "The TXT record already exists. Skipping."
|
||||
_sleep 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "Adding records"
|
||||
|
||||
if volcengine_rest POST "" "Action=CreateRecord&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"Remark\":\"acme.sh\"}"; then
|
||||
_info "TXT record updated successfully."
|
||||
_sleep 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
_sleep 1
|
||||
return 1
|
||||
}
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_volcengine_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
_record_id=""
|
||||
|
||||
Volcengine_ACCESS_KEY_ID="${Volcengine_ACCESS_KEY_ID:-$(_readaccountconf_mutable Volcengine_ACCESS_KEY_ID)}"
|
||||
Volcengine_SECRET_ACCESS_KEY="${Volcengine_SECRET_ACCESS_KEY:-$(_readaccountconf_mutable Volcengine_SECRET_ACCESS_KEY)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
_sleep 1
|
||||
return 1
|
||||
fi
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Getting existing records for $fulldomain"
|
||||
|
||||
if ! volcengine_rest POST "" "Action=ListRecords&Version=2018-08-01" "{\"ZID\":$_domain_id,\"Host\":\"$_sub_domain\",\"Type\":\"TXT\",\"Value\":\"$txtvalue\",\"SearchMode\":\"exact\"}"; then
|
||||
_sleep 1
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ListRecords already filtered by ZID + Host + Value + SearchMode:exact,
|
||||
# so any returned record is our target. Don't match on FQDN: Volcengine
|
||||
# lowercases the Host/FQDN in the response, which would break a
|
||||
# case-sensitive string compare against $fulldomain.
|
||||
_record_id="$(echo "$response" | _egrep_o "\"RecordID\":\"[0-9]+\"," | cut -d: -f2 | cut -d, -f1 | tr -d '"')"
|
||||
_debug "_record_id" "$_record_id"
|
||||
|
||||
if [ -z "$_record_id" ]; then
|
||||
_debug "no records exist, skip"
|
||||
_sleep 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
if volcengine_rest POST "" "Action=DeleteRecord&Version=2018-08-01" "{\"RecordID\":\"$_record_id\"}"; then
|
||||
_info "TXT record deleted successfully."
|
||||
_sleep 1
|
||||
return 0
|
||||
fi
|
||||
_sleep 1
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
# iterate over names (a.b.c.d -> b.c.d -> c.d -> d)
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug "Checking domain: $h"
|
||||
if [ -z "$h" ]; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# iterate over paginated result for list_hosted_zones
|
||||
if ! volcengine_rest POST "" "Action=ListZones&Version=2018-08-01" "{\"Key\":\"$h\",\"SearchMode\":\"exact\"}"; then
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "\"ZoneName\":\"$h\""; then
|
||||
_domain_id=$(printf "%s" "$response" | _egrep_o "\"ZID\":[0-9]+," | cut -d: -f2 | cut -d, -f1)
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
_err "Can't find domain with id: $h"
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
#method uri qstr data
|
||||
volcengine_rest() {
|
||||
mtd="$1"
|
||||
ep="$2"
|
||||
qsr="$3"
|
||||
data="$4"
|
||||
|
||||
_debug mtd "$mtd"
|
||||
_debug ep "$ep"
|
||||
_debug qsr "$qsr"
|
||||
_debug data "$data"
|
||||
|
||||
# clear any header state left over from a previous request so that
|
||||
# conditionally-set headers (e.g. x-content-sha256, x-security-token)
|
||||
# can't leak into the next request
|
||||
_H1=""
|
||||
_H2=""
|
||||
_H3=""
|
||||
_H4=""
|
||||
_H5=""
|
||||
|
||||
CanonicalURI="/$ep"
|
||||
_debug2 CanonicalURI "$CanonicalURI"
|
||||
|
||||
CanonicalQueryString="$qsr"
|
||||
_debug2 CanonicalQueryString "$CanonicalQueryString"
|
||||
|
||||
RequestDate="$(date -u +"%Y%m%dT%H%M%SZ")"
|
||||
_debug2 RequestDate "$RequestDate"
|
||||
|
||||
Hash="sha256"
|
||||
|
||||
_H1="X-Date: $RequestDate"
|
||||
_debug2 _H1 "$_H1"
|
||||
|
||||
volcengine_host="$Volcengine_HOST"
|
||||
CanonicalHeaders="host:$volcengine_host\n"
|
||||
SignedHeaders="host"
|
||||
|
||||
if [ -n "$data" ]; then
|
||||
XContentSha256="$(printf "%s" "$data" | _digest "$Hash" hex)"
|
||||
_H4="x-content-sha256: $XContentSha256"
|
||||
_debug2 _H4 "$_H4"
|
||||
|
||||
CanonicalHeaders="${CanonicalHeaders}x-content-sha256:$XContentSha256\n"
|
||||
SignedHeaders="${SignedHeaders};x-content-sha256"
|
||||
fi
|
||||
|
||||
CanonicalHeaders="${CanonicalHeaders}x-date:$RequestDate\n"
|
||||
SignedHeaders="${SignedHeaders};x-date"
|
||||
|
||||
if [ -n "$Volcengine_SESSION_TOKEN" ]; then
|
||||
_H3="x-security-token: $Volcengine_SESSION_TOKEN"
|
||||
CanonicalHeaders="${CanonicalHeaders}x-security-token:$Volcengine_SESSION_TOKEN\n"
|
||||
SignedHeaders="${SignedHeaders};x-security-token"
|
||||
fi
|
||||
|
||||
_debug2 CanonicalHeaders "$CanonicalHeaders"
|
||||
_debug2 SignedHeaders "$SignedHeaders"
|
||||
|
||||
RequestPayload="$data"
|
||||
_debug2 RequestPayload "$RequestPayload"
|
||||
|
||||
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$(printf "%s" "$RequestPayload" | _digest "$Hash" hex)"
|
||||
_debug2 CanonicalRequest "$CanonicalRequest"
|
||||
|
||||
HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)"
|
||||
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
||||
|
||||
Algorithm="HMAC-SHA256"
|
||||
_debug2 Algorithm "$Algorithm"
|
||||
|
||||
RequestDateOnly="$(echo "$RequestDate" | cut -c 1-8)"
|
||||
_debug2 RequestDateOnly "$RequestDateOnly"
|
||||
|
||||
Region="cn-beijing"
|
||||
Service="dns"
|
||||
|
||||
CredentialScope="$RequestDateOnly/$Region/$Service/request"
|
||||
_debug2 CredentialScope "$CredentialScope"
|
||||
|
||||
StringToSign="$Algorithm\n$RequestDate\n$CredentialScope\n$HashedCanonicalRequest"
|
||||
|
||||
_debug2 StringToSign "$StringToSign"
|
||||
|
||||
kSecret="$Volcengine_SECRET_ACCESS_KEY"
|
||||
|
||||
_secure_debug2 kSecret "$kSecret"
|
||||
|
||||
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
|
||||
_secure_debug2 kSecretH "$kSecretH"
|
||||
|
||||
kDateH="$(printf "%s" "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)"
|
||||
_debug2 kDateH "$kDateH"
|
||||
|
||||
kRegionH="$(printf "%s" "$Region" | _hmac "$Hash" "$kDateH" hex)"
|
||||
_debug2 kRegionH "$kRegionH"
|
||||
|
||||
kServiceH="$(printf "%s" "$Service" | _hmac "$Hash" "$kRegionH" hex)"
|
||||
_debug2 kServiceH "$kServiceH"
|
||||
|
||||
kSigningH="$(printf "%s" "request" | _hmac "$Hash" "$kServiceH" hex)"
|
||||
_debug2 kSigningH "$kSigningH"
|
||||
|
||||
signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)"
|
||||
_debug2 signature "$signature"
|
||||
|
||||
Authorization="$Algorithm Credential=$Volcengine_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
|
||||
_debug2 Authorization "$Authorization"
|
||||
|
||||
_H2="Authorization: $Authorization"
|
||||
_debug2 _H2 "$_H2"
|
||||
|
||||
url="$Volcengine_URL/$ep"
|
||||
if [ "$qsr" ]; then
|
||||
url="$Volcengine_URL/$ep?$qsr"
|
||||
fi
|
||||
|
||||
if [ "$mtd" = "GET" ]; then
|
||||
response="$(_get "$url")"
|
||||
else
|
||||
response="$(_post "$data" "$url" "" "POST" "application/json")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
_debug response "$response"
|
||||
if [ "$_ret" = "0" ]; then
|
||||
if _contains "$response" "\"Error\":{"; then
|
||||
_err "Response error:$response"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return "$_ret"
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_wedos_info='WEDOS.com
|
||||
Site: wedos.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_wedos
|
||||
Options:
|
||||
WEDOS_Username WAPI login (account email)
|
||||
WEDOS_Wapipass WAPI password
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7071
|
||||
Author: Jan Forman <jforman@jflab.cz>
|
||||
'
|
||||
|
||||
WEDOS_Api="https://api.wedos.com/wapi/json"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: dns_wedos_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_wedos_add() {
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
txtvalue=$2
|
||||
|
||||
if ! _wedos_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Detecting root zone for $fulldomain"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Cannot determine root zone for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
_info "Adding TXT record: $_sub_domain.$_domain"
|
||||
if ! _wedos_request "dns-row-add" "{\"domain\":\"$_domain\",\"name\":\"$_sub_domain\",\"ttl\":\"300\",\"type\":\"TXT\",\"rdata\":\"$txtvalue\"}"; then
|
||||
_err "Failed to add TXT record"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Committing DNS changes for $_domain"
|
||||
if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then
|
||||
_err "Failed to commit DNS changes"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#Usage: dns_wedos_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_wedos_rm() {
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
txtvalue=$2
|
||||
|
||||
if ! _wedos_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Detecting root zone for $fulldomain"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "Cannot determine root zone for: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
# _get_root leaves the dns-rows-list response for $_domain in $response
|
||||
_debug "Looking up row IDs for TXT value: $txtvalue"
|
||||
_row_ids=$(echo "$response" | tr '{' '\n' | grep -F -- "\"rdata\":\"$txtvalue\"" | grep -F -- "\"name\":\"$_sub_domain\"" | _egrep_o '"ID": *"[0-9]*"' | tr -dc '0-9\n')
|
||||
_debug _row_ids "$_row_ids"
|
||||
|
||||
if [ -z "$_row_ids" ]; then
|
||||
_info "TXT record not found, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for _row_id in $_row_ids; do
|
||||
_info "Removing TXT record ID $_row_id from $_domain"
|
||||
if ! _wedos_request "dns-row-delete" "{\"domain\":\"$_domain\",\"row_id\":\"$_row_id\"}"; then
|
||||
_err "Failed to delete TXT record"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
_info "Committing DNS changes for $_domain"
|
||||
if ! _wedos_request "dns-domain-commit" "{\"name\":\"$_domain\"}"; then
|
||||
_err "Failed to commit DNS changes"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_wedos_init() {
|
||||
WEDOS_Username="${WEDOS_Username:-$(_readaccountconf_mutable WEDOS_Username)}"
|
||||
WEDOS_Wapipass="${WEDOS_Wapipass:-$(_readaccountconf_mutable WEDOS_Wapipass)}"
|
||||
|
||||
if [ -z "$WEDOS_Username" ] || [ -z "$WEDOS_Wapipass" ]; then
|
||||
WEDOS_Username=""
|
||||
WEDOS_Wapipass=""
|
||||
_err "You didn't specify the WEDOS WAPI credentials yet."
|
||||
_err "Please export WEDOS_Username and WEDOS_Wapipass and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable WEDOS_Username "$WEDOS_Username"
|
||||
_saveaccountconf_mutable WEDOS_Wapipass "$WEDOS_Wapipass"
|
||||
return 0
|
||||
}
|
||||
|
||||
# WAPI auth token: sha1(login + sha1(password) + hour), where the hour is
|
||||
# the current hour on the WEDOS servers (Europe/Prague timezone).
|
||||
# The POSIX TZ string is used so no tzdata is required on the client.
|
||||
_wedos_auth() {
|
||||
if [ "$_wedos_utc" ]; then
|
||||
# fallback: WAPI accepts 1 hour of skew, UTC+1 fits both CET and CEST
|
||||
_wedos_hour=$(date -u +%H)
|
||||
_wedos_hour=$(printf '%02d' "$(((${_wedos_hour#0} + 1) % 24))")
|
||||
else
|
||||
_wedos_hour=$(TZ='CET-1CEST,M3.5.0,M10.5.0/3' date +%H)
|
||||
fi
|
||||
_wedos_phash=$(printf '%s' "$WEDOS_Wapipass" | _digest sha1 hex)
|
||||
printf '%s' "${WEDOS_Username}${_wedos_phash}${_wedos_hour}" | _digest sha1 hex
|
||||
}
|
||||
|
||||
#Usage: _wedos_request <command> <data-json>
|
||||
#Returns 0 and sets $response on WAPI code 1000, returns 1 otherwise.
|
||||
_wedos_request() {
|
||||
_wedos_cmd="$1"
|
||||
_wedos_data="$2"
|
||||
|
||||
_wedos_token=$(_wedos_auth)
|
||||
_secure_debug _wedos_token "$_wedos_token"
|
||||
|
||||
_wedos_json="{\"request\":{\"user\":\"$WEDOS_Username\",\"auth\":\"$_wedos_token\",\"command\":\"$_wedos_cmd\",\"data\":$_wedos_data}}"
|
||||
_debug2 "WAPI command: $_wedos_cmd"
|
||||
_debug2 "WAPI data: $_wedos_data"
|
||||
|
||||
# _post sends the global _H1.._H5 headers with every request; clear them so
|
||||
# headers from earlier API calls are not leaked to the WAPI endpoint.
|
||||
export _H1=""
|
||||
export _H2=""
|
||||
export _H3=""
|
||||
export _H4=""
|
||||
export _H5=""
|
||||
|
||||
_wedos_body="request=$(printf '%s' "$_wedos_json" | _url_encode)"
|
||||
response=$(_post "$_wedos_body" "$WEDOS_Api" "" "POST" "application/x-www-form-urlencoded")
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "WAPI request failed for command '$_wedos_cmd'"
|
||||
return 1
|
||||
fi
|
||||
_debug2 "WAPI response: $response"
|
||||
|
||||
_wedos_code=$(echo "$response" | _egrep_o '"code": *[0-9]*' | _head_n 1 | tr -dc '0-9')
|
||||
_debug2 "WAPI result code: $_wedos_code"
|
||||
if [ "$_wedos_code" = "1000" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# some systems ignore the TZ variable (Haiku), sending a wrong auth hour;
|
||||
# retry once with the UTC fallback in _wedos_auth
|
||||
if [ "$_wedos_code" = "2050" ] && [ -z "$_wedos_utc" ]; then
|
||||
_wedos_utc=1
|
||||
_wedos_request "$_wedos_cmd" "$_wedos_data"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# 2050 = bad credentials, 2051 = IP not whitelisted, 2052 = IP blocked
|
||||
if [ "$_wedos_code" = "2050" ] || [ "$_wedos_code" = "2051" ] || [ "$_wedos_code" = "2052" ]; then
|
||||
_wedos_result=$(echo "$response" | _egrep_o '"result": *"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
_err "WAPI authentication error $_wedos_code: $_wedos_result"
|
||||
_err "Check WEDOS_Username, WEDOS_Wapipass and the WAPI IP whitelist."
|
||||
_wedos_autherr=1
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "WAPI error for command '$_wedos_cmd': $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Determine the registered domain (_domain) and subdomain prefix (_sub_domain)
|
||||
# by walking up the labels and calling dns-rows-list until WAPI accepts one.
|
||||
# _acme-challenge.www.example.co.uk
|
||||
# -> _sub_domain=_acme-challenge.www _domain=example.co.uk
|
||||
# The full domain itself is tried first, so a zone apex (e.g. DNS alias mode
|
||||
# pointing at the registered domain) resolves to an empty _sub_domain.
|
||||
_get_root() {
|
||||
_gr_full="$1"
|
||||
_gr_i=1
|
||||
_wedos_autherr=""
|
||||
while true; do
|
||||
_gr_candidate=$(printf '%s' "$_gr_full" | cut -d . -f "${_gr_i}"-100)
|
||||
_debug2 "Checking zone candidate: $_gr_candidate"
|
||||
if [ -z "$_gr_candidate" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _wedos_request "dns-rows-list" "{\"domain\":\"$_gr_candidate\"}"; then
|
||||
_domain="$_gr_candidate"
|
||||
if [ "$_gr_i" = "1" ]; then
|
||||
_sub_domain=""
|
||||
else
|
||||
_sub_domain=$(printf '%s' "$_gr_full" | cut -d . -f 1-"$((_gr_i - 1))")
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# auth error hits every candidate, stop the walk
|
||||
if [ "$_wedos_autherr" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_gr_i=$((_gr_i + 1))
|
||||
done
|
||||
}
|
||||
+21
-1
@@ -5,9 +5,11 @@ Site: zonomi.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_zonomi
|
||||
Options:
|
||||
ZM_Key API Key
|
||||
OptionsAlt:
|
||||
ZM_Api API endpoint. Default: "https://zonomi.com/app/dns/dyndns.jsp". For RimuHosting use "https://rimuhosting.com/dns/dyndns.jsp".
|
||||
'
|
||||
|
||||
ZM_Api="https://zonomi.com/app/dns/dyndns.jsp"
|
||||
ZM_Api_Default="https://zonomi.com/app/dns/dyndns.jsp"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
@@ -28,6 +30,8 @@ dns_zonomi_add() {
|
||||
#save the api key to the account conf file.
|
||||
_saveaccountconf_mutable ZM_Key "$ZM_Key"
|
||||
|
||||
_zm_init_api
|
||||
|
||||
_info "Get existing txt records for $fulldomain"
|
||||
if ! _zm_request "action=QUERY&name=$fulldomain"; then
|
||||
_err "error"
|
||||
@@ -64,11 +68,27 @@ dns_zonomi_rm() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_zm_init_api
|
||||
|
||||
_zm_request "action=DELETE&type=TXT&name=$fulldomain"
|
||||
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# resolve the API endpoint: zonomi by default, overridable for providers
|
||||
# sharing the same API on another host (e.g. RimuHosting)
|
||||
_zm_init_api() {
|
||||
ZM_Api="${ZM_Api:-$(_readaccountconf_mutable ZM_Api)}"
|
||||
if [ -z "$ZM_Api" ]; then
|
||||
ZM_Api="$ZM_Api_Default"
|
||||
fi
|
||||
_debug2 ZM_Api "$ZM_Api"
|
||||
if [ "$ZM_Api" != "$ZM_Api_Default" ]; then
|
||||
_saveaccountconf_mutable ZM_Api "$ZM_Api"
|
||||
fi
|
||||
}
|
||||
|
||||
#qstr
|
||||
_zm_request() {
|
||||
qstr="$1"
|
||||
|
||||
@@ -83,7 +83,43 @@ aws_ses_send() {
|
||||
response="$(aws_rest POST "" "" "$_data")"
|
||||
}
|
||||
|
||||
_use_container_role() {
|
||||
# automatically set if running inside ECS
|
||||
if [ -z "$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ]; then
|
||||
_debug "No ECS environment variable detected"
|
||||
return 1
|
||||
fi
|
||||
_use_metadata "169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
|
||||
}
|
||||
|
||||
_use_instance_role() {
|
||||
_instance_role_name_url="http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
||||
|
||||
if _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 401; then
|
||||
_debug "Using IMDSv2"
|
||||
_token_url="http://169.254.169.254/latest/api/token"
|
||||
export _H1="X-aws-ec2-metadata-token-ttl-seconds: 21600"
|
||||
_token="$(_post "" "$_token_url" "" "PUT")"
|
||||
_secure_debug3 "_token" "$_token"
|
||||
if [ -z "$_token" ]; then
|
||||
_debug "Unable to fetch IMDSv2 token from instance metadata"
|
||||
return 1
|
||||
fi
|
||||
export _H1="X-aws-ec2-metadata-token: $_token"
|
||||
fi
|
||||
|
||||
if ! _get "$_instance_role_name_url" true 1 | _head_n 1 | grep -Fq 200; then
|
||||
_debug "Unable to fetch IAM role from instance metadata"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_instance_role_name=$(_get "$_instance_role_name_url" "" 1)
|
||||
_debug "_instance_role_name" "$_instance_role_name"
|
||||
_use_metadata "$_instance_role_name_url$_instance_role_name" "$_token"
|
||||
}
|
||||
|
||||
_use_metadata() {
|
||||
export _H1="X-aws-ec2-metadata-token: $2"
|
||||
_aws_creds="$(
|
||||
_get "$1" "" 1 |
|
||||
_normalizeJson |
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Support calling a custom script for notifications
|
||||
#
|
||||
# export CUSTOMSCRIPT_PATH="/usr/local/bin/acme-notification.sh"
|
||||
#
|
||||
# The script is called with three arguments:
|
||||
# $1 subject
|
||||
# $2 content
|
||||
# $3 status code (0: success, 1: error, 2: skipped)
|
||||
|
||||
customscript_send() {
|
||||
_subject="$1"
|
||||
_content="$2"
|
||||
_statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
|
||||
_debug "_subject" "$_subject"
|
||||
_debug "_content" "$_content"
|
||||
_debug "_statusCode" "$_statusCode"
|
||||
|
||||
CUSTOMSCRIPT_PATH="${CUSTOMSCRIPT_PATH:-$(_readaccountconf_mutable CUSTOMSCRIPT_PATH)}"
|
||||
if [ -z "$CUSTOMSCRIPT_PATH" ]; then
|
||||
_err "You didn't specify the custom script path CUSTOMSCRIPT_PATH yet."
|
||||
return 1
|
||||
fi
|
||||
if ! _exists "$CUSTOMSCRIPT_PATH"; then
|
||||
_err "The custom script $CUSTOMSCRIPT_PATH does not exist or is not executable."
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable CUSTOMSCRIPT_PATH "$CUSTOMSCRIPT_PATH"
|
||||
|
||||
# Invoke directly, never through eval: the subject and content contain
|
||||
# domain names and CA messages, eval would allow command injection.
|
||||
_customscript_result="$("$CUSTOMSCRIPT_PATH" "$_subject" "$_content" "$_statusCode" 2>&1)"
|
||||
_customscript_rc="$?"
|
||||
_debug2 "_customscript_result" "$_customscript_result"
|
||||
|
||||
if [ "$_customscript_rc" != "0" ]; then
|
||||
_err "custom script execution error ($_customscript_rc): $_customscript_result"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "custom script executed successfully."
|
||||
return 0
|
||||
}
|
||||
@@ -200,6 +200,7 @@ _smtp_send_curl() {
|
||||
|
||||
set -- "$@" \
|
||||
--upload-file - \
|
||||
--crlf \
|
||||
--mail-from "$SMTP_FROM" \
|
||||
--max-time "$SMTP_TIMEOUT"
|
||||
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#Support WAHA (WhatsApp HTTP API) - free, self-hosted WhatsApp API
|
||||
#https://waha.devlike.pro/
|
||||
|
||||
#Required:
|
||||
#WAHA_URL="http://localhost:3000"
|
||||
#WAHA_CHAT_ID="1234567890@c.us"
|
||||
|
||||
#Optional:
|
||||
#WAHA_API_KEY=""
|
||||
#WAHA_SESSION="default"
|
||||
|
||||
waha_send() {
|
||||
_subject="$1"
|
||||
_content="$2"
|
||||
_statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
|
||||
_debug "_subject" "$_subject"
|
||||
_debug "_content" "$_content"
|
||||
_debug "_statusCode" "$_statusCode"
|
||||
|
||||
WAHA_URL="${WAHA_URL:-$(_readaccountconf_mutable WAHA_URL)}"
|
||||
if [ -z "$WAHA_URL" ]; then
|
||||
WAHA_URL=""
|
||||
_err "You didn't specify the WAHA server url WAHA_URL yet."
|
||||
_err "Example: export WAHA_URL=\"http://localhost:3000\""
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable WAHA_URL "$WAHA_URL"
|
||||
|
||||
WAHA_CHAT_ID="${WAHA_CHAT_ID:-$(_readaccountconf_mutable WAHA_CHAT_ID)}"
|
||||
if [ -z "$WAHA_CHAT_ID" ]; then
|
||||
WAHA_CHAT_ID=""
|
||||
_err "You didn't specify the WhatsApp chat id WAHA_CHAT_ID yet."
|
||||
_err "Example: export WAHA_CHAT_ID=\"1234567890@c.us\""
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable WAHA_CHAT_ID "$WAHA_CHAT_ID"
|
||||
|
||||
WAHA_API_KEY="${WAHA_API_KEY:-$(_readaccountconf_mutable WAHA_API_KEY)}"
|
||||
if [ "$WAHA_API_KEY" ]; then
|
||||
_saveaccountconf_mutable WAHA_API_KEY "$WAHA_API_KEY"
|
||||
fi
|
||||
|
||||
WAHA_SESSION="${WAHA_SESSION:-$(_readaccountconf_mutable WAHA_SESSION)}"
|
||||
if [ -z "$WAHA_SESSION" ]; then
|
||||
WAHA_SESSION="default"
|
||||
else
|
||||
_saveaccountconf_mutable WAHA_SESSION "$WAHA_SESSION"
|
||||
fi
|
||||
|
||||
_content=$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)
|
||||
|
||||
_data="{\"chatId\": \"$WAHA_CHAT_ID\", "
|
||||
_data="$_data\"text\": \"$_content\", "
|
||||
_data="$_data\"session\": \"$WAHA_SESSION\"}"
|
||||
|
||||
_debug "_data" "$_data"
|
||||
|
||||
export _H1="Content-Type: application/json"
|
||||
if [ "$WAHA_API_KEY" ]; then
|
||||
export _H2="X-Api-Key: $WAHA_API_KEY"
|
||||
fi
|
||||
|
||||
_waha_url="${WAHA_URL}/api/sendText"
|
||||
response="$(_post "$_data" "$_waha_url" "" "POST" "application/json")"
|
||||
|
||||
if [ "$?" = "0" ] && _contains "$response" "\"id\""; then
|
||||
_info "waha send success."
|
||||
return 0
|
||||
fi
|
||||
_err "waha send error."
|
||||
_err "$response"
|
||||
return 1
|
||||
}
|
||||
Reference in New Issue
Block a user