Merge pull request #7078 from acmesh-official/dev
Some checks failed
DNS / CheckToken (push) Has been cancelled
DragonFlyBSD / DragonFlyBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
FreeBSD / FreeBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
FreeBSD / FreeBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
GhostBSD / GhostBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
GhostBSD / GhostBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Haiku / Haiku (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Haiku / Haiku (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Linux / Linux (almalinux:latest) (push) Has been cancelled
Linux / Linux (alpine:latest) (push) Has been cancelled
Linux / Linux (archlinux:latest) (push) Has been cancelled
Linux / Linux (debian:latest) (push) Has been cancelled
Linux / Linux (fedora:latest) (push) Has been cancelled
Linux / Linux (gentoo/stage3) (push) Has been cancelled
Linux / Linux (kalilinux/kali) (push) Has been cancelled
Linux / Linux (opensuse/leap:latest) (push) Has been cancelled
Linux / Linux (oraclelinux:8) (push) Has been cancelled
Linux / Linux (ubuntu:latest) (push) Has been cancelled
MacOS / MacOS (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
MidnightBSD / MidnightBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
NetBSD / NetBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Omnios / Omnios (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenBSD / OpenBSD (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
OpenIndiana / OpenIndiana (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
PebbleStrict / PebbleStrict (push) Has been cancelled
PebbleStrict / PebbleStrict_IPCert (push) Has been cancelled
Solaris / Solaris (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Solaris / Solaris (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Tribblix / Tribblix (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (1, , , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, ) (push) Has been cancelled
Ubuntu / Ubuntu (Smallstep Intermediate CA, Smallstep Intermediate CA, , 1, https://localhost:9000/acme/acme/directory, 1, , 172.17.0.1) (push) Has been cancelled
Ubuntu / Ubuntu (ZeroSSL RSA DV SSL CA 2, ZeroSSL ECC DV SSL CA 2, githubtest@acme.sh, ZeroSSL.com, ) (push) Has been cancelled
Windows / Windows (, , , LetsEncrypt.org_test, (STAGING)) (push) Has been cancelled
Build DockerHub / CheckToken (push) Has been cancelled
Shellcheck / ShellCheck (push) Has been cancelled
Shellcheck / shfmt (push) Has been cancelled
DNS / Fail (push) Has been cancelled
DNS / Docker (push) Has been cancelled
DNS / MacOS (push) Has been cancelled
DNS / Windows (push) Has been cancelled
DNS / FreeBSD (push) Has been cancelled
DNS / GhostBSD (push) Has been cancelled
DNS / OpenBSD (push) Has been cancelled
DNS / NetBSD (push) Has been cancelled
DNS / DragonFlyBSD (push) Has been cancelled
DNS / MidnightBSD (push) Has been cancelled
DNS / Solaris (push) Has been cancelled
DNS / Omnios (push) Has been cancelled
DNS / OpenIndiana (push) Has been cancelled
DNS / Tribblix (push) Has been cancelled
DNS / Haiku (push) Has been cancelled
Build DockerHub / build (push) Has been cancelled

sync
This commit is contained in:
neil 2026-07-03 19:56:09 +08:00 committed by GitHub
commit 992886c4eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 2238 additions and 234 deletions

View file

@ -260,9 +260,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 }}

82
.github/workflows/GhostBSD.yml vendored Normal file
View file

@ -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"

View file

@ -50,14 +50,14 @@ jobs:
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

View file

@ -7,7 +7,7 @@ jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
- uses: actions/github-script@v9
with:
script: |
github.rest.issues.createComment({

View file

@ -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({

View file

@ -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({

View file

@ -51,7 +51,7 @@ jobs:
} > wiki-change-msg.txt
- name: Create issue to notify Neilpang
uses: peter-evans/create-issue-from-file@v5
uses: peter-evans/create-issue-from-file@v6
with:
title: "Wiki edited"
content-filepath: ./wiki-change-msg.txt

8
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,8 @@
# Contributing
1. Do NOT send pull request to `master` branch.
Please send to `dev` branch instead.
Any PR to `master` branch will NOT be merged.
2. For dns api support, read this guide first: https://github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
You will NOT get any review without passing this guide. You also need to fix the CI errors.

View file

@ -31,6 +31,7 @@
<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>
@ -128,6 +129,7 @@
|24|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[![Tribblix](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
|27|[![GhostBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
@ -627,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)]

59
acme.sh
View file

@ -4707,6 +4707,13 @@ issue() {
else
_cleardomainconf "Le_ChallengeAlias"
fi
# Save Le_DNSSleep unconditionally here: the save inside the dns_entries
# branch is skipped when all authorizations are already valid (e.g. issuing
# the ECC twin of a just-issued RSA cert), which left the setting out of
# that cert's conf. https://github.com/acmesh-official/acme.sh/issues/6986
if [ "$Le_DNSSleep" ]; then
_savedomainconf "Le_DNSSleep" "$Le_DNSSleep"
fi
if [ "$_preferred_chain" ]; then
_savedomainconf "Le_Preferred_Chain" "$_preferred_chain" "base64"
else
@ -5312,6 +5319,8 @@ $_authorizations_map"
fi
fi
elif [ "$vtype" = "$VTYPE_ALPN" ]; then
_ncaddr="$(_getfield "$_local_addr" "$_ncIndex")"
_ncIndex="$(_math $_ncIndex + 1)"
acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")"
_debug acmevalidationv1 "$acmevalidationv1"
if ! _starttlsserver "$d" "" "$Le_TLSPort" "$keyauthorization" "$_ncaddr" "$acmevalidationv1"; then
@ -5542,6 +5551,13 @@ $_authorizations_map"
return 1
fi
if ! _contains "$response" "$BEGIN_CERT"; then
response="$(echo "$response" | _dbase64 "multiline" | tr -d '\0' | _normalizeJson)"
_err "Signing failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')"
_on_issue_err "$_post_hook"
return 1
fi
echo "$response" >"$CERT_PATH"
_split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH"
if [ -z "$_preferred_chain" ]; then
@ -5561,6 +5577,11 @@ $_authorizations_map"
_err "$response"
continue
fi
if ! _contains "$response" "$BEGIN_CERT"; then
_debug2 "Skipping alternate cert link due to unexpected response format."
continue
fi
_relcert="$CERT_PATH.alt"
_relfullchain="$CERT_FULLCHAIN_PATH.alt"
_relca="$CA_CERT_PATH.alt"
@ -5817,6 +5838,14 @@ renew() {
fi
_info "Renewing using Le_API=$Le_API"
# Honor --local-address given on the renew/renewAll command line: it overrides
# the value saved at issue time (and gets re-saved by issue() below), so certs
# issued before the machine gained multiple addresses can still be renewed.
# https://github.com/acmesh-official/acme.sh/issues/7009
if [ "$_local_address" ]; then
Le_LocalAddress="$_local_address"
fi
_clearAPI
_clearCA
export ACME_DIRECTORY="$Le_API"
@ -6449,6 +6478,7 @@ _install_win_taskscheduler() {
_lesh="$1"
_centry="$2"
_randomminute="$3"
_randomhour="$4"
if ! _exists cygpath; then
_err "cygpath not found"
return 1
@ -6518,7 +6548,7 @@ installcronjob() {
fi
_t=$(_time)
random_minute=$(_math $_t % 60)
random_hour=$(_math $_t / 60 % 24)
random_hour=$(_math $_t / 60 % 6)
if ! _exists "$_CRONTAB" && _exists "fcrontab"; then
_CRONTAB="fcrontab"
@ -6527,7 +6557,7 @@ installcronjob() {
if ! _exists "$_CRONTAB"; then
if _exists cygpath && _exists schtasks.exe; then
_info "It seems you are on Windows, let's install the Windows scheduler task."
if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute"; then
if _install_win_taskscheduler "$lesh" "$_c_entry" "$random_minute" "$random_hour"; then
_info "Successfully installed Windows scheduler task."
return 0
else
@ -6549,7 +6579,7 @@ installcronjob() {
fi
$_CRONTAB -l 2>/dev/null | {
cat
echo "$random_minute $random_hour * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null"
echo "$random_minute $random_hour,$(_math $random_hour + 6),$(_math $random_hour + 12),$(_math $random_hour + 18) * * * $lesh --cron --home \"$LE_WORKING_DIR\" $_c_entry> /dev/null"
} | $_CRONTAB_STDIN
fi
if [ "$?" != "0" ]; then
@ -7177,6 +7207,12 @@ install() {
if [ "$_DEFAULT_CERT_HOME" != "$CERT_HOME" ]; then
_saveaccountconf "CERT_HOME" "$CERT_HOME"
# Create the custom cert home now instead of on first issuance, so the
# user can see --install honored it.
# https://github.com/acmesh-official/acme.sh/issues/4756
if [ ! -d "$CERT_HOME" ]; then
mkdir -p "$CERT_HOME"
fi
fi
if [ "$_DEFAULT_ACCOUNT_KEY_PATH" != "$ACCOUNT_KEY_PATH" ]; then
@ -7498,8 +7534,8 @@ Parameters:
--dnssleep <seconds> The time in seconds to wait for all the txt records to propagate in dns api mode.
It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically.
-k, --keylength <bits> Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521.
-ak, --accountkeylength <bits> Specifies the account key length: 2048, 3072, 4096
-k, --keylength <bits> Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521.
-ak, --accountkeylength <bits> Specifies the account key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521.
--log [file] Specifies the log file. Defaults to \"$DEFAULT_LOG_FILE\" if argument is omitted.
--log-level <1|2> Specifies the log level, default is $DEFAULT_LOG_LEVEL.
--syslog <0|3|6|7> Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug.
@ -7717,9 +7753,16 @@ _checkSudo() {
return 0
fi
if [ -n "$SUDO_COMMAND" ]; then
#it's a normal user doing "sudo su", or `sudo -i` or `sudo -s`, or `sudo su acmeuser1`
_endswith "$SUDO_COMMAND" /bin/su || _contains "$SUDO_COMMAND" "/bin/su " || grep "^$SUDO_COMMAND\$" /etc/shells >/dev/null 2>&1
return $?
#The SUDO_* env vars are often inherited into shells that were not
#started as `sudo acme.sh` at all (e.g. `sudo su - user`, or
#`sudo pct enter <VID>` on Proxmox, which copies them into the
#container). Only warn when sudo was used to run acme.sh itself;
#anything else means the sudo happened further up and is fine.
#https://github.com/acmesh-official/acme.sh/issues/6400
if _contains "$SUDO_COMMAND" "$PROJECT_ENTRY"; then
return 1
fi
return 0
fi
#otherwise
return 1

View file

@ -125,7 +125,7 @@ routeros_deploy() {
_savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
_savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
_savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
_savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
_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=\\\"\\\";\

View file

@ -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

View file

@ -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"

490
dnsapi/dns_arubabusiness.sh Normal file
View file

@ -0,0 +1,490 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_arubabusiness_info='ArubaBusiness
Site: business.aruba.it
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_arubabusiness
Options:
AB_Key Your ArubaBusiness API Key
AB_User Your account user
AB_Pass Your account password
'
#
# A word of warning: as of this writing, api.arubabusiness.it only supports oauth authentication using the "password" grant type.
# If you are REALLY sure you want to use it, it would be wise set up a dedicated technical user without administrative privileges
#
ARUBABUSINESS_API='https://api.arubabusiness.it'
######## Public functions ########
#
# Usage: dns_arubabusiness_add _acme-challenge.www.domain.com aaaabbbbcccc111122223333
#
# Add a new TXT record whose name and value match the given domain and value
#
# Variables
# _full_domain: $1 - the name of the TXT record
# _txt_value: $2 - the value of the TXT record
# _body
# dns_details
# domain_id
# dns_record_id
# response
#
dns_arubabusiness_add() {
_full_domain=$1
_txt_value=$2
if ! _ab_authenticate; then
return 1
fi
if ! _ab_domain_id "$_full_domain"; then
return 1
fi
if _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
# This is very unlikely, but allow the process to use the existing record
_info "A TXT record with name: $_full_domain and value: $_txt_value already exists (id: $dns_record_id)"
return 0
fi
_body="{ \"IdDomain\": $domain_id, \"Type\": \"TXT\", \"Name\": \"$_full_domain\", \"Content\": \"\\\"$_txt_value\\\"\" }"
_debug "Adding TXT record with name: $_full_domain and value: $_txt_value"
if ! _ab_rest POST "api/domains/dns/record" "$_body" || ! _contains "$response" "DomainId"; then
_err "Failed to add TXT record with name: $_full_domain"
return 1
fi
_info "Sleeping 10 seconds to let ArubaBusiness do its magic"
_sleep 10
# Refresh dns details and check that the record was really added
if ! _ab_dns_details "$root_domain"; then
return 1
fi
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details"; then
# This should never happen
_err "The TXT record with name: $_full_domain was not set"
_err "Please check that the dns records are clean"
return 1
fi
_info "Added TXT record with id: $dns_record_id"
return 0
}
#
# Usage: dns_arubabusiness_rm _acme-challenge.www.domain.com aaaabbbbcccc111122223333
#
# Remove the TXT record whose name and value match the given domain and value
#
# Variables
# _full_domain: $1 - the name of the TXT record
# _txt_value: $2 - the value of the TXT record
# dns_details
# dns_record_id
#
dns_arubabusiness_rm() {
_full_domain=$1
_txt_value=$2
if ! _ab_authenticate; then
return 1
fi
if ! _ab_domain_id "$_full_domain"; then
return 1
fi
if ! _ab_dns_record_id "$_full_domain" "$_txt_value" "$dns_details" || [ -z "$dns_record_id" ]; then
_err "Could not retrieve the record id for: $_full_domain"
return 1
fi
_debug "Deleting TXT record: $dns_record_id"
if ! _ab_rest DELETE "api/domains/dns/record/$dns_record_id" || ! _contains "$response" "DomainId"; then
_err "Failed to delete TXT record: $dns_record_id"
return 1
fi
_info "Deleted TXT record: $dns_record_id"
return 0
}
######## Private functions ########
#
# Usage: _ab_domain_id _acme-challenge.www.domain.com
#
# Split the input domain into subdomain + root domain and get the id of the root domain
#
# Variables
# _full_domain: $1 - the domain whose root needs to be extracted
# _domain_sections
# _current_index
# _candidate_subdomain
# _candidate_domain
# sub_domain
# root_domain
# domain_id
# dns_details: a json containing all dns records registered on the root domain
#
# Example
# _get_root _acme-challenge.www.domain.com
#
# Should return
# sub_domain=_acme-challenge.www
# root_domain=domain.com
# domain_id=123123123123
# dns_details="{JSON_CONTENT}"
#
_ab_domain_id() {
_full_domain=$1
_info "Attempting to retrieve root domain details for: $_full_domain"
_domain_sections=$(_math "$(printf "%s" "$_full_domain" | tr '.' '\n' | wc -l)" + 1)
if [ "$_domain_sections" -lt 1 ]; then
_err "Invalid input $_full_domain"
return 1
fi
_current_index=1
while true; do
_candidate_subdomain=$(if [ "$_current_index" = "1" ]; then printf ""; else printf "%s" "$_full_domain" | cut -d . -f 1-"$(_math "$_current_index" - 1)"; fi)
_candidate_domain=$(printf "%s" "$_full_domain" | cut -d . -f "$_current_index"-"$_domain_sections")
if ! _ab_dns_details "$_candidate_domain"; then
_debug2 "Could not fetch dns details for: $_candidate_domain"
_current_index=$(_math "$_current_index" + 1)
# Fail if there are no candidates left
if [ "$_current_index" -gt "$_domain_sections" ]; then
_err "Could not determine the root domain for: $_full_domain"
return 1
fi
else
sub_domain="$_candidate_subdomain"
root_domain="$_candidate_domain"
# Extract the domain id, which is an integer and contains no commas
domain_id="$(printf "%s" "$dns_details" | _egrep_o '"Id":[^,]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')"
if [ -z "$domain_id" ]; then
_err "Could not determine the domain id for: $root_domain"
return 1
fi
_debug "Retrieved root domain id: $domain_id"
return 0
fi
done
}
#
# Usage: _ab_dns_record_id _acme-challenge.www.domain.com "aaaabbbbcccc111122223333" "{JSON_CONTENT}"
#
# Extract the record id of the first TXT record whose name and content match the input values
#
# Variables
# _record_name: $1
# _txt_value: $2
# _dns_details: $3 - the json returned by a previous call to '_ab_dns_details() $root_domain'
# _record_ids
# _record_names
# _record_types
# _record_contents
# _record_ids_count
# _record_names_count
# _record_types_count
# _record_contents_count
# _i
# dns_record_id
#
# Notes
# TXT correspond to record type 5
# ArubaBusiness appends a terminating dot (.) to the record name
# The content field may contain the following character sequence: \"
# All record names are always converted to lowercase
#
_ab_dns_record_id() {
_record_name=$1
_txt_value=$2
_dns_details=$3
_record_name_lowercase=$(printf "%s" "$_record_name" | _lower_case)
# Extract the record ids, which are integers and contain no commas, colons or spaces
# The first id is skipped because it refers to the domain id
_record_ids=$(printf "%s" "$_dns_details" | sed 's/"Id":/\n"Id":/g' | _egrep_o '"Id":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
# Extract the record names, which are strings but cannot contain commas, colons, spaces and quotes
# The first name is skipped because it refers to the domain name
_record_names=$(printf "%s" "$_dns_details" | sed 's/"Name":/\n"Name":/g' | _egrep_o '"Name":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' "' | tr '\n' ' ')
# Extract the record types, which are integers (except for the first one) and contain no commas, colons or spaces
# The first type is skipped because it refers to the domain type
_record_types=$(printf "%s" "$_dns_details" | sed 's/"Type":/\n"Type":/g' | _egrep_o '"Type":[^,]*' | _tail_n +2 | cut -d : -f 2 | tr -d ' ' | tr '\n' ' ')
# Extract the record contents, which are strings and may contain no quotes except for TXT records, which must be delimited by two \" literals
# Note: There is no domain related entry here
# Note: A " character is appended at the end of each content to make it easier to process the list later
_record_contents=$(printf "%s" "$_dns_details" | sed 's/"Content":/\n"Content":/g' | sed 's/\\"//g' | _egrep_o '"Content": *"[^"]*"' | cut -d : -f 2- | sed -n 's/"\(.*\)"/\1/p' | tr '\n' '#')
_info "IDS: $_record_ids"
_info "NAMES: $_record_names"
_info "TYPEs: $_record_types"
_info "CONTENTS: $_record_contents"
_record_ids_count=$(printf "%s" "$_record_ids" | tr ' ' '\n' | wc -l)
_record_names_count=$(printf "%s" "$_record_names" | tr ' ' '\n' | wc -l)
_record_types_count=$(printf "%s" "$_record_types" | tr ' ' '\n' | wc -l)
_record_contents_count=$(printf "%s" "$_record_contents" | tr '#' '\n' | wc -l)
_info "Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
if [ "$_record_ids_count" != "$_record_names_count" ] || [ "$_record_ids_count" != "$_record_types_count" ] || [ "$_record_ids_count" != "$_record_contents_count" ]; then
_err "Failed to parse record elements. Ids: $_record_ids_count, names: $_record_names_count, types: $_record_types_count, contents: $_record_contents_count"
return 1
fi
_info "Looking for a TXT record matching inputs - name: $_record_name_lowercase value: $_txt_value"
_i=1
while [ "$_i" -le "$_record_ids_count" ]; do
_current_name=$(printf "%s" "$_record_names" | cut -d " " -f "$_i")
_current_type=$(printf "%s" "$_record_types" | cut -d " " -f "$_i")
_current_content=$(printf "%s" "$_record_contents" | cut -d "#" -f "$_i")
if [ "$_record_name_lowercase." = "$_current_name" ] && [ "5" = "$_current_type" ] && [ "$_txt_value" = "$_current_content" ]; then
dns_record_id=$(printf "%s" "$_record_ids" | cut -d " " -f "$_i")
_info "Found matching record with id: $dns_record_id"
return 0
else
_debug2 "Record does not match - type: '$_current_type' name: '$_current_name' value: '$_current_content'; Expected '$_record_name_lowercase.' '5' '$_txt_value'"
fi
_i=$(_math "$_i" + 1)
done
_debug2 "No matching record was found in $_dns_details"
return 1
}
#
# Usage: _ab_dns_details domain.com
#
# Retrieve dns info for the given input domain
#
# Variables
# _domain: $1
# dns_details: the json returned by the call to $ARUBABUSINESS_API/api/domains/dns/$_domain/details (if return status is 0)
# response
#
_ab_dns_details() {
_domain=$1
if ! _ab_rest GET "api/domains/dns/$_domain/details" || ! _contains "$response" "DomainId"; then
return 1
fi
dns_details="$response"
return 0
}
#
# Usage: _ab_authenticate
#
# Read account conf, update domain conf and perform user authentication to acquire an access token
#
# Variables
# AB_Key
# AB_User
# AB_Pass
# AB_Token
#
_ab_authenticate() {
AB_Key="${AB_Key:-$(_readaccountconf_mutable AB_Key)}"
AB_User="${AB_User:-$(_readaccountconf_mutable AB_User)}"
AB_Pass="${AB_Pass:-$(_readaccountconf_mutable AB_Pass)}"
if [ -z "$AB_Key" ] || [ -z "$AB_User" ] || [ -z "$AB_Pass" ]; then
AB_Key=""
AB_User=""
AB_Pass=""
_err "Either the ArubaBusiness API key, the user or the password has not been defined yet."
_err "Please configure them and try again."
return 1
fi
_saveaccountconf_mutable AB_Key "$AB_Key"
_saveaccountconf_mutable AB_User "$AB_User"
_saveaccountconf_mutable AB_Pass "$AB_Pass"
if ! _ab_get_token || [ -z "$AB_Token" ]; then
_err "Failed to acquire an access token"
return 1
fi
return 0
}
#
# Usage: _ab_get_token
#
# Try acquiring a temporary access token. The token should have a 24h lifespan
#
# Variables
# _ab_user_enc
# _ab_pass_enc
# _ab_authdata
# AB_User
# AB_Pass
# AB_Token
# response
# _H2
#
_ab_get_token() {
_ab_user_enc=$(printf "%s" "$AB_User" | _url_encode)
_ab_pass_enc=$(printf "%s" "$AB_Pass" | _url_encode)
_ab_authdata="grant_type=password&username=$_ab_user_enc&password=$_ab_pass_enc"
_H2="Content-Type: application/x-www-form-urlencoded"
if ! _ab_rest POST "auth/token" "$_ab_authdata" || ! _contains "$response" "access_token"; then
_err "Authentication failure"
return 1
fi
AB_Token="$(printf "%s" "$response" | _egrep_o '"access_token":"[^\"]*"' | cut -d : -f 2 | tr -d '"')"
if [ -z "$AB_Token" ]; then
_err "Could not extract access token"
return 1
fi
_debug "Acquired access token"
return 0
}
#
# Usage: _ab_rest POST "example/endpoint" "password=123"
#
# Perform a REST request using the given method, endpoint and data
#
# Variables
# _method: $1 - The http method
# _endpoint: $2 - The api path (relative to $ARUBABUSINESS_API)
# _data: $3 - The body of the request (optional)
# _key_trimmed
# _token_trimmed
# _ret_code
# AB_Key
# AB_Token
# ARUBABUSINESS_API
# _H1
# _H2
# _H3
# _H4
#
_ab_rest() {
_method=$1
_endpoint="$2"
_data="$3"
_key_trimmed=$(printf "%s" "$AB_Key" | tr -d '"')
_token_trimmed=$(printf "%s" "$AB_Token" | tr -d '"')
_H1="Accept: application/json"
if [ -z "$_H2" ]; then
# Default to application/json
_H2="Content-Type: application/json"
fi
if [ "$_key_trimmed" ]; then
_H3="Authorization-Key: $_key_trimmed"
else
_err "Missing Api Key"
_ab_cleanup_headers
return 1
fi
if [ "$_token_trimmed" ]; then
_H4="Authorization: Bearer $_token_trimmed"
else
_debug "No access token set"
fi
if [ "$_method" != "GET" ]; then
response="$(_post "$_data" "$ARUBABUSINESS_API/$_endpoint" "" "$_method")"
else
response="$(_get "$ARUBABUSINESS_API/$_endpoint")"
fi
_ret_code=$?
if [ "$_ret_code" = "0" ] && _ab_call_is_success; then
# Normalize the json response
response="$(printf "%s" "$response" | _normalizeJson)"
_ret_code=0
else
_err "Failed to call endpoint: $_endpoint"
_ret_code=1
fi
_ab_cleanup_headers
return $_ret_code
}
#
# Usage: _ab_cleanup_headers
#
# Unset header variables to avoid interfering with other calls
#
# Variables
# _H1
# _H2
# _H3
# _H4
#
_ab_cleanup_headers() {
# Cleanup request headers
unset _H1 _H2 _H3 _H4 _H5
# Cleanup response headers
if [ -f "$HTTP_HEADER" ]; then
: >"$HTTP_HEADER"
fi
}
#
# Usage: _ab_call_is_success
#
# Check whether a call's response http status is one of 200, 201, 202 or 204 (other 2xx are not handled)
#
# Variables
# _status
# _http_status
# _success_http_codes
# HTTP_HEADER
#
_ab_call_is_success() {
_success_http_codes="200 201 202 204"
if [ -f "$HTTP_HEADER" ]; then
_http_status=$(_egrep_o "^HTTP[\/0-9. ]*" <"$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)
for _status in $_success_http_codes; do
if [ "$_status" = "$_http_status" ]; then
return 0
fi
done
fi
return 1
}

View file

@ -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"

View file

@ -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"

137
dnsapi/dns_cdmon.sh Normal file
View file

@ -0,0 +1,137 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_cdmon_info='cdmon
Site: www.cdmon.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon
Options:
CDMON_Key API Key
'
CDMON_Api="https://api-domains.cdmon.services/api-domains"
######## Public functions #####################
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
dns_cdmon_add() {
fulldomain=$1
txtvalue=$2
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
if [ -z "$CDMON_Key" ]; then
CDMON_Key=""
_err "You didn't specify your cdmon api key yet."
_err "Please create your key and try again."
return 1
fi
_saveaccountconf_mutable CDMON_Key "$CDMON_Key"
_debug "First, we detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Adding record"
if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then
if _contains "$response" "\"status\":\"ok\""; then
_info "Added, OK"
return 0
else
_err "Add txt record error."
return 1
fi
fi
_err "Add txt record error."
return 1
}
# Usage: fulldomain txtvalue
# Used to remove the txt record after validation
dns_cdmon_rm() {
fulldomain=$1
txtvalue=$2
CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
_debug "First, we detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_info "Removing record"
if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then
if _contains "$response" "\"status\":\"ok\""; then
_info "Deleted, OK"
return 0
else
_err "Delete txt record error."
return 1
fi
fi
_err "Delete txt record error."
return 1
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=1
if ! _cdmon_rest "domains/list"; then
return 1
fi
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
return 1
fi
if _contains "$response" "\"domain\":\"$h\""; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_cdmon_rest() {
ep="$1"
data="$2"
_debug "$ep"
key_trimmed=$(echo "$CDMON_Key" | tr -d '"')
export _H1="Content-Type: application/json"
export _H2="apikey: $key_trimmed"
_debug data "$data"
response="$(_post "$data" "$CDMON_Api/$ep")"
_ret="$?"
unset _H1 _H2
if [ "$_ret" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

View file

@ -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

View file

@ -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

263
dnsapi/dns_glesys.sh Normal file
View file

@ -0,0 +1,263 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_glesys_info='Glesys
Site: Glesys.se
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_glesys
Options:
GLESYS_API_KEY Generated API key.
GLESYS_PROJECT_ID Project ID for the API key (e.g. cl12345).
GLESYS_API API endpoint. Default "https://api.glesys.com/domain".
GLESYS_TTL TXT record TTL. Default 120.
Issues: https://github.com/acmesh-official/acme.sh/issues/7057
Author: Toni Karppi
'
GLESYS_API_DEFAULT="https://api.glesys.com/domain"
GLESYS_TTL_DEFAULT="120"
######## Public functions #####################################################
# Usage:
# dns_glesys_add _acme-challenge.www.example.com "txt-value"
dns_glesys_add() {
fulldomain="$1"
txtvalue="$2"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
_glesys_init || return 1
if ! _glesys_get_root "$fulldomain"; then
_err "Could not find root zone for $fulldomain"
return 1
fi
_debug _domain "$_domain"
_debug _sub_domain "$_sub_domain"
host_value="${_sub_domain:-@}"
_debug _host_value "$host_value"
data="{\"domainname\":\"$_domain\",\"host\":\"$host_value\",\"type\":\"TXT\",\"data\":\"$txtvalue\",\"ttl\":\"$GLESYS_TTL\"}"
_debug2 data "$data"
if ! _glesys_rest POST "/addrecord" "$data"; then
_err "Failed to send HTTP request to add TXT record"
return 1
fi
response_code=$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
_egrep_o '"code":"?[0-9]+' |
_egrep_o '[0-9]+$'
)
_debug response_code "$response_code"
if [ "$response_code" != "200" ]; then
_err "GleSYS API responded with an unexpected status when attempting to add TXT record"
_debug2 "API response" "$response"
return 1
fi
_info "TXT record added"
return 0
}
# Usage:
# dns_glesys_rm _acme-challenge.www.example.com "txt-value"
dns_glesys_rm() {
fulldomain="$1"
txtvalue="$2"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
_glesys_init || return 1
if ! _glesys_get_root "$fulldomain"; then
_err "Could not find root zone for $fulldomain"
return 1
fi
if ! _glesys_find_record_id "$txtvalue"; then
_info "TXT record not present, skip removal"
return 0
fi
_debug _record_id "$_record_id"
if ! _glesys_rest POST "/deleterecord" "{\"recordid\":$_record_id}"; then
_err "Failed to send HTTP request to remove TXT record"
return 1
fi
response_code=$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
_egrep_o '"code":"?[0-9]+' |
_egrep_o '[0-9]+$'
)
_debug response_code "$response_code"
if [ "$response_code" != "200" ]; then
_err "GleSYS API responded with unexpected status when attempting to remove TXT record"
_debug2 "API response" "$response"
return 1
fi
_info "TXT record removed"
return 0
}
######## Private functions ####################################################
_glesys_find_record_id() {
txtvalue="$1"
_debug txtvalue "$txtvalue"
if [ -z "$txtvalue" ]; then
return 1
fi
_record_id=""
_debug "Looking for TXT record with value" "$txtvalue"
if ! _glesys_rest GET "/listrecords?domainname=$_domain"; then
_err "Failed to list DNS records"
return 1
fi
records="$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
sed 's/},{/}\
{/g'
)"
_debug2 records "$records"
expected_data="\"data\":\"$txtvalue\""
_record_id="$(
printf "%s\n" "$records" |
while IFS= read -r record; do
printf "%s" "$record" | grep -q '"type":"TXT"' || continue
printf "%s" "$record" | grep -Fq "$expected_data" || continue
printf "%s" "$record" |
grep -E -o '"recordid":"?[0-9]+' |
grep -E -o '[0-9]+$'
break
done
)"
_debug _record_id "$_record_id"
if [ -z "$_record_id" ]; then
return 1
fi
return 0
}
# Finds:
# _domain example.com
# _sub_domain _acme-challenge.www
_glesys_get_root() {
domain="$1"
i=1
while true; do
h="$(printf "%s" "$domain" | cut -d . -f "$i"-100)"
if [ -z "$h" ]; then
return 1
fi
if _glesys_rest GET "/listrecords?domainname=$h"; then
response_code=$(
printf "%s" "$response" |
tr -d '\r\n\t ' |
_egrep_o '"code":"?[0-9]+' |
_egrep_o '[0-9]+$'
)
_debug response_code "$response_code"
if [ "$response_code" = "200" ]; then
cut_len="$((${#domain} - ${#h} - 1))"
_domain="$h"
_sub_domain="$(printf "%s" "$domain" | cut -c "1-$cut_len")"
return 0
fi
fi
i="$((i + 1))"
done
}
_glesys_init() {
[ -z "$GLESYS_API" ] && GLESYS_API="$GLESYS_API_DEFAULT"
[ -z "$GLESYS_TTL" ] && GLESYS_TTL="$GLESYS_TTL_DEFAULT"
_debug GLESYS_API "$GLESYS_API"
_debug GLESYS_TTL "$GLESYS_TTL"
GLESYS_API_KEY="${GLESYS_API_KEY:-$(_readaccountconf_mutable GLESYS_API_KEY)}"
GLESYS_PROJECT_ID="${GLESYS_PROJECT_ID:-$(_readaccountconf_mutable GLESYS_PROJECT_ID)}"
if [ -z "$GLESYS_API_KEY" ] || [ -z "$GLESYS_PROJECT_ID" ]; then
_err "GLESYS_API_KEY and GLESYS_PROJECT_ID must be set for this provider"
return 1
fi
_secure_debug GLESYS_API_KEY "$GLESYS_API_KEY"
_secure_debug GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
_glesys_basic_auth="$(printf "%s:%s" "$GLESYS_PROJECT_ID" "$GLESYS_API_KEY" | _base64)"
_secure_debug2 _glesys_basic_auth "$_glesys_basic_auth"
_saveaccountconf_mutable GLESYS_API_KEY "$GLESYS_API_KEY"
_saveaccountconf_mutable GLESYS_PROJECT_ID "$GLESYS_PROJECT_ID"
return 0
}
_glesys_rest() {
method="$1"
path="$2"
data="$3"
export _H1="Authorization: Basic $_glesys_basic_auth"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
url="$GLESYS_API$path"
_debug "$method $url"
if [ "$method" = "GET" ]; then
response="$(_get "$url")"
else
response="$(_post "$data" "$url" "" "$method")"
fi
ret="$?"
_debug2 response "$response"
_debug ret "$ret"
if [ "$ret" != "0" ]; then
return 1
fi
return 0
}

View file

@ -6,13 +6,13 @@ Site: hostup.se
Docs: https://developer.hostup.se/
Options:
HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes.
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
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

View file

@ -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"

View file

@ -312,6 +312,22 @@ _get_root() {
_domain="$h"
return 0
fi
# IDN fallback: INWX returns Unicode zone names; when $h is ACE/punycode,
# encode each zone name via _idn() and compare — no python dependency.
if _contains "$h" "xn--"; then
_zone_unicode=$(printf "%s" "$response" | _egrep_o '<string>[^<]*' |
sed 's/<[^>]*>//g' | while IFS= read -r _z; do
if [ "$(_idn "$_z")" = "$h" ]; then
printf "%s" "$_z"
break
fi
done)
if [ -n "$_zone_unicode" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$_zone_unicode"
return 0
fi
fi
p=$i
i=$(_math "$i" + 1)
done

View file

@ -16,7 +16,7 @@ IONOS_TXT_TTL=60 # minimum accepted by API
IONOS_TXT_PRIO=10
dns_ionos_add() {
fulldomain=$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

View file

@ -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

197
dnsapi/dns_level27.sh Normal file
View file

@ -0,0 +1,197 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_level27_info='Level27
Site: Level27.be
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_level27
Options:
LEVEL27_API_KEY API key. Get one from the Level27 control panel (https://app.level27.eu/account/profile/security).
OptionsAlt:
LEVEL27_API API base URL. Optional. Default "https://api.level27.eu/v1".
Issues: github.com/acmesh-official/acme.sh/issues
Author: Jeroen Moors <jeroen.moors@level27.be>
'
LEVEL27_API_DEFAULT="https://api.level27.eu/v1"
######## Public functions #####################
# Usage: dns_level27_add _acme-challenge.www.example.com "TXT-value"
dns_level27_add() {
fulldomain="$(_idn "$1")"
txtvalue="$2"
_info "Using Level27 to add a TXT record for $fulldomain"
if ! _level27_init; then
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Could not determine the root zone for $fulldomain at Level27."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_level27_data="{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\"}"
if ! _level27_rest POST "domains/$_domain_id/records" "$_level27_data"; then
_err "Could not add the TXT record."
return 1
fi
if _contains "$response" "\"id\":"; then
_info "TXT record added."
return 0
fi
_err "Unexpected response while adding the TXT record."
return 1
}
# Usage: dns_level27_rm _acme-challenge.www.example.com "TXT-value"
dns_level27_rm() {
fulldomain="$(_idn "$1")"
txtvalue="$2"
_info "Using Level27 to remove the TXT record for $fulldomain"
if ! _level27_init; then
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Could not determine the root zone for $fulldomain at Level27."
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
if ! _level27_rest GET "domains/$_domain_id/records?type=TXT"; then
_err "Could not list the existing TXT records."
return 1
fi
_record_id="$(_level27_find_record_id "$response" "$txtvalue")"
if [ -z "$_record_id" ]; then
_info "No matching TXT record found; nothing to remove."
return 0
fi
_debug _record_id "$_record_id"
if ! _level27_rest DELETE "domains/$_domain_id/records/$_record_id"; then
_err "Could not remove the TXT record."
return 1
fi
_info "TXT record removed."
return 0
}
#################### Private functions below ##################################
# Reads and validates the API credentials and endpoint, and stores them for renewals.
_level27_init() {
LEVEL27_API_KEY="${LEVEL27_API_KEY:-$(_readaccountconf_mutable LEVEL27_API_KEY)}"
if [ -z "$LEVEL27_API_KEY" ]; then
LEVEL27_API_KEY=""
_err "You must export the variable LEVEL27_API_KEY before using the Level27 DNS API."
_err "Get an API key from the Level27 control panel (https://app.level27.eu/account/profile/security)."
return 1
fi
LEVEL27_API_KEY="$(echo "$LEVEL27_API_KEY" | tr -d '"')"
_saveaccountconf_mutable LEVEL27_API_KEY "$LEVEL27_API_KEY"
LEVEL27_API="${LEVEL27_API:-$(_readaccountconf_mutable LEVEL27_API)}"
if [ -z "$LEVEL27_API" ]; then
LEVEL27_API="$LEVEL27_API_DEFAULT"
fi
_saveaccountconf_mutable LEVEL27_API "$LEVEL27_API"
# Remove a trailing slash so endpoints can be appended consistently.
LEVEL27_API="$(echo "$LEVEL27_API" | sed 's#/$##')"
return 0
}
# Usage: _get_root _acme-challenge.www.example.com
# Splits the full domain into the registered zone and the subdomain part.
# Sets: _domain, _domain_id, _sub_domain
_get_root() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
# not valid
return 1
fi
if ! _level27_rest GET "domains?filter=$h"; then
return 1
fi
_level27_zones="$(echo "$response" | _normalizeJson)"
if _contains "$_level27_zones" "\"fullname\":\"$h\""; then
_domain_line="$(echo "$_level27_zones" | sed 's/},{/}\n{/g' | grep "\"fullname\":\"$h\"" | _head_n 1)"
_domain_id="$(echo "$_domain_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2)"
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
return 1
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
# Usage: _level27_find_record_id "<records-json>" "<txtvalue>"
# Prints the id of the TXT record whose content matches the value, or nothing.
_level27_find_record_id() {
_records="$(echo "$1" | _normalizeJson | sed 's/},{/}\n{/g')"
_wanted="$2"
_record_line="$(echo "$_records" | grep "\"content\":\"$_wanted\"" | _head_n 1)"
if [ -z "$_record_line" ]; then
# Some APIs store TXT content wrapped in quotes.
_record_line="$(echo "$_records" | grep "\"content\":\"\\\\\"$_wanted\\\\\"\"" | _head_n 1)"
fi
if [ -z "$_record_line" ]; then
return 0
fi
echo "$_record_line" | _egrep_o '"id":[0-9]*' | _head_n 1 | cut -d : -f 2
}
# Usage: _level27_rest <method> <endpoint> [data]
# Performs an authenticated API call and stores the body in $response.
_level27_rest() {
m="$1"
ep="$2"
data="$3"
_debug "$ep"
export _H1="Authorization: $LEVEL27_API_KEY"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
if [ "$m" != "GET" ]; then
_debug2 data "$data"
response="$(_post "$data" "$LEVEL27_API/$ep" "" "$m")"
else
response="$(_get "$LEVEL27_API/$ep")"
fi
if [ "$?" != "0" ]; then
_err "Error querying the Level27 API endpoint: $ep"
return 1
fi
_debug2 response "$response"
return 0
}

View file

@ -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

View file

@ -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."

View file

@ -18,7 +18,7 @@ dns_selfhost_add() {
_debug fulldomain "$fulldomain"
_debug txtvalue "$txt"
SELFHOSTDNS_UPDATE_URL="https://selfhost.de/cgi-bin/api.pl"
SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl"
# Get values, but don't save until we successfully validated
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}"

297
dnsapi/dns_volcengine.sh Executable file
View file

@ -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"
}