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 @@ Solaris DragonFlyBSD MidnightBSD + GhostBSD Omnios OpenIndiana Tribblix @@ -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. +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" +}