diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml
index 232c9b0f..a972ae1a 100644
--- a/.github/workflows/DNS.yml
+++ b/.github/workflows/DNS.yml
@@ -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 }}
diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml
new file mode 100644
index 00000000..c77fdf2e
--- /dev/null
+++ b/.github/workflows/GhostBSD.yml
@@ -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"
diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml
index d10c17a8..7dc42290 100644
--- a/.github/workflows/dockerhub.yml
+++ b/.github/workflows/dockerhub.yml
@@ -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
diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml
index e92b0411..c659fce5 100644
--- a/.github/workflows/issue.yml
+++ b/.github/workflows/issue.yml
@@ -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({
diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml
index 558ebf48..19763a15 100644
--- a/.github/workflows/pr_dns.yml
+++ b/.github/workflows/pr_dns.yml
@@ -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({
diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml
index 416ed721..76ae76f6 100644
--- a/.github/workflows/pr_notify.yml
+++ b/.github/workflows/pr_notify.yml
@@ -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({
diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml
index a706529a..7e5d7ca3 100644
--- a/.github/workflows/wiki-monitor.yml
+++ b/.github/workflows/wiki-monitor.yml
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..33294ce7
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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.
diff --git a/README.md b/README.md
index 792ef979..b93a8a50 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@
+
@@ -128,6 +129,7 @@
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
+|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
@@ -627,6 +629,8 @@ This project exists thanks to all the people who contribute.
+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)]
diff --git a/acme.sh b/acme.sh
index 00192868..5529fe88 100755
--- a/acme.sh
+++ b/acme.sh
@@ -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 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 Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521.
- -ak, --accountkeylength Specifies the account key length: 2048, 3072, 4096
+ -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256 (default), ec-384, ec-521.
+ -ak, --accountkeylength 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 ` 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
diff --git a/deploy/routeros.sh b/deploy/routeros.sh
index ef9c6954..328fabbd 100644
--- a/deploy/routeros.sh
+++ b/deploy/routeros.sh
@@ -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=\\\"\\\";\
diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh
index df34f927..33e3dfa0 100644
--- a/deploy/truenas_ws.sh
+++ b/deploy/truenas_ws.sh
@@ -16,7 +16,12 @@
#
# # API KEY
# # Use the folowing URL to create a new API token: /ui/apikeys
-# export DEPLOY_TRUENAS_APIKEY="/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"
diff --git a/dnsapi/dns_arubabusiness.sh b/dnsapi/dns_arubabusiness.sh
new file mode 100644
index 00000000..90b3f18d
--- /dev/null
+++ b/dnsapi/dns_arubabusiness.sh
@@ -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
+}
diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh
index b76d69c2..1face1c8 100755
--- a/dnsapi/dns_aws.sh
+++ b/dnsapi/dns_aws.sh
@@ -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"
diff --git a/dnsapi/dns_baidu.sh b/dnsapi/dns_baidu.sh
index 8651deab..dfad8eeb 100644
--- a/dnsapi/dns_baidu.sh
+++ b/dnsapi/dns_baidu.sh
@@ -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 <[^<]*' |
+ 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
diff --git a/dnsapi/dns_ionos.sh b/dnsapi/dns_ionos.sh
index 9a464253..00662e82 100755
--- a/dnsapi/dns_ionos.sh
+++ b/dnsapi/dns_ionos.sh
@@ -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
diff --git a/dnsapi/dns_joker.sh b/dnsapi/dns_joker.sh
index 401471be..0ad80327 100644
--- a/dnsapi/dns_joker.sh
+++ b/dnsapi/dns_joker.sh
@@ -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
diff --git a/dnsapi/dns_level27.sh b/dnsapi/dns_level27.sh
new file mode 100644
index 00000000..3fbaf810
--- /dev/null
+++ b/dnsapi/dns_level27.sh
@@ -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
+'
+
+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 "" ""
+# 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 [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
+}
diff --git a/dnsapi/dns_namecheap.sh b/dnsapi/dns_namecheap.sh
index 5527b357..cca59735 100755
--- a/dnsapi/dns_namecheap.sh
+++ b/dnsapi/dns_namecheap.sh
@@ -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
diff --git a/dnsapi/dns_namesilo.sh b/dnsapi/dns_namesilo.sh
index 5d47a59a..df5871cf 100755
--- a/dnsapi/dns_namesilo.sh
+++ b/dnsapi/dns_namesilo.sh
@@ -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 "300")
if [ "$retcode" ]; then
- _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$fulldomain" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1)
+ _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$_sub_domain$txtvalue" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1)
_debug _record_id "$_record_id"
if [ "$_record_id" ]; then
_info "Successfully retrieved the record id for ACME challenge."
diff --git a/dnsapi/dns_selfhost.sh b/dnsapi/dns_selfhost.sh
index 4912dfdf..40cc0210 100644
--- a/dnsapi/dns_selfhost.sh
+++ b/dnsapi/dns_selfhost.sh
@@ -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)}"
diff --git a/dnsapi/dns_volcengine.sh b/dnsapi/dns_volcengine.sh
new file mode 100755
index 00000000..2cc805d5
--- /dev/null
+++ b/dnsapi/dns_volcengine.sh
@@ -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"
+}