diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml
index 00d180b9..232c9b0f 100644
--- a/.github/workflows/DNS.yml
+++ b/.github/workflows/DNS.yml
@@ -661,9 +661,65 @@ jobs:
- Haiku:
+ Tribblix:
runs-on: ubuntu-latest
needs: OpenIndiana
+ env:
+ TEST_DNS : ${{ secrets.TEST_DNS }}
+ TestingDomain: ${{ secrets.TestingDomain }}
+ TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
+ TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
+ TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
+ CASE: le_test_dnsapi
+ TEST_LOCAL: 1
+ DEBUG: ${{ secrets.DEBUG }}
+ http_proxy: ${{ secrets.http_proxy }}
+ https_proxy: ${{ secrets.https_proxy }}
+ HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Tribblix doesn't accept the expired ISRG X1 root
+ TokenName1: ${{ secrets.TokenName1}}
+ TokenName2: ${{ secrets.TokenName2}}
+ TokenName3: ${{ secrets.TokenName3}}
+ TokenName4: ${{ secrets.TokenName4}}
+ TokenName5: ${{ secrets.TokenName5}}
+ steps:
+ - uses: actions/checkout@v6
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/tribblix-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
+ sync: nfs
+ prepare: zap install socat
+ run: |
+ if [ "${{ secrets.TokenName1}}" ] ; then
+ export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
+ fi
+ if [ "${{ secrets.TokenName2}}" ] ; then
+ export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
+ fi
+ if [ "${{ secrets.TokenName3}}" ] ; then
+ export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
+ fi
+ if [ "${{ secrets.TokenName4}}" ] ; then
+ export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
+ fi
+ if [ "${{ secrets.TokenName5}}" ] ; then
+ export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
+ fi
+ cd ../acmetest
+ ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
+
+
+
+ Haiku:
+ runs-on: ubuntu-latest
+ needs: Tribblix
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml
new file mode 100644
index 00000000..cd43e0e3
--- /dev/null
+++ b/.github/workflows/Tribblix.yml
@@ -0,0 +1,79 @@
+name: Tribblix
+on:
+ push:
+ branches:
+ - '*'
+ paths:
+ - '*.sh'
+ - '.github/workflows/Tribblix.yml'
+
+ pull_request:
+ branches:
+ - dev
+ paths:
+ - '*.sh'
+ - '.github/workflows/Tribblix.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+
+
+jobs:
+ Tribblix:
+ strategy:
+ matrix:
+ include:
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ ACME_USE_WGET: 1
+ #- TEST_ACME_Server: "ZeroSSL.com"
+ # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
+ # CA: "ZeroSSL RSA DV SSL CA 2"
+ # CA_EMAIL: "githubtest@acme.sh"
+ # TEST_PREFERRED_CHAIN: ""
+ runs-on: ubuntu-latest
+ env:
+ TEST_LOCAL: 1
+ TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
+ CA_ECDSA: ${{ matrix.CA_ECDSA }}
+ CA: ${{ matrix.CA }}
+ CA_EMAIL: ${{ matrix.CA_EMAIL }}
+ TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
+ ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: anyvm-org/cf-tunnel@v0
+ id: tunnel
+ with:
+ protocol: http
+ port: 8080
+ - name: Set envs
+ run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/tribblix-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
+ nat: |
+ "8080": "80"
+ prepare: zap install socat curl wget
+ sync: nfs
+ run: |
+ cd ../acmetest \
+ && ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml
index 0e7ba748..d10c17a8 100644
--- a/.github/workflows/dockerhub.yml
+++ b/.github/workflows/dockerhub.yml
@@ -41,6 +41,9 @@ jobs:
runs-on: ubuntu-latest
needs: CheckToken
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
+ permissions:
+ contents: read
+ packages: write
steps:
- name: checkout code
uses: actions/checkout@v6
@@ -58,6 +61,9 @@ jobs:
- name: login to docker hub
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
+ - name: login to ghcr
+ run: |
+ echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: build and push the image
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
@@ -73,6 +79,8 @@ jobs:
fi
fi
+ echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
+
DOCKER_LABELS=()
while read -r label; do
DOCKER_LABELS+=(--label "${label}")
@@ -84,3 +92,9 @@ jobs:
--output "type=image,push=true" \
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
+ - name: mirror the image to ghcr (best-effort)
+ run: |
+ docker buildx imagetools create \
+ --tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
+ ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
+ || echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
diff --git a/README.md b/README.md
index ba5d3591..22700b4c 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@
+
@@ -112,6 +113,7 @@
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
+|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
diff --git a/acme.sh b/acme.sh
index aa9d34a0..00192868 100755
--- a/acme.sh
+++ b/acme.sh
@@ -4895,7 +4895,7 @@ issue() {
# (Let's Encrypt) may also reject with a malformed error if the prior cert
# was issued by a different issuer / different CA. Retry without "replaces"
# whenever the failure mentions ARI or the replaces field.
- if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
+ if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order."
@@ -5785,7 +5785,7 @@ renew() {
_debug "_renewServer" "$_renewServer"
_initpath "$Le_Domain" "$_isEcc"
-
+ _info "Renew: $Le_Domain"
_set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT}
_info "$(__green "Renewing: '$Le_Domain'")"
if [ ! -f "$DOMAIN_CONF" ]; then
@@ -6865,7 +6865,7 @@ deactivate() {
#cert
_getAKI() {
_cert="$1"
- ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
+ ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep -A 1 "X509v3 Authority Key Identifier" | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
}
#cert
diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh
deleted file mode 100755
index f1bddc61..00000000
--- a/dnsapi/dns_hetzner.sh
+++ /dev/null
@@ -1,256 +0,0 @@
-#!/usr/bin/env sh
-# shellcheck disable=SC2034
-dns_hetzner_info='Hetzner.com
-Site: Hetzner.com
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
-Options:
- HETZNER_Token API Token
-Issues: github.com/acmesh-official/acme.sh/issues/2943
-'
-
-HETZNER_Api="https://dns.hetzner.com/api/v1"
-
-######## Public functions #####################
-
-# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-# Used to add txt record
-# Ref: https://dns.hetzner.com/api-docs/
-dns_hetzner_add() {
- full_domain=$1
- txt_value=$2
-
- HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
-
- if [ -z "$HETZNER_Token" ]; then
- HETZNER_Token=""
- _err "You didn't specify a Hetzner api token."
- _err "You can get yours from here https://dns.hetzner.com/settings/api-token."
- return 1
- fi
-
- #save the api key and email to the account conf file.
- _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
-
- _debug "First detect the root zone"
-
- if ! _get_root "$full_domain"; then
- _err "Invalid domain"
- return 1
- fi
- _debug _domain_id "$_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _debug "Getting TXT records"
- if ! _find_record "$_sub_domain" "$txt_value"; then
- return 1
- fi
-
- if [ -z "$_record_id" ]; then
- _info "Adding record"
- if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
- if _contains "$response" "$txt_value"; then
- _info "Record added, OK"
- _sleep 2
- return 0
- fi
- fi
- _err "Add txt record error${_response_error}"
- return 1
- else
- _info "Found record id: $_record_id."
- _info "Record found, do nothing."
- return 0
- # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
- #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
- # if _contains "$response" "$txt_value"; then
- # _info "Modified, OK"
- # return 0
- # fi
- #fi
- #_err "Add txt record error (modify)."
- #return 1
- fi
-}
-
-# Usage: full_domain txt_value
-# Used to remove the txt record after validation
-dns_hetzner_rm() {
- full_domain=$1
- txt_value=$2
-
- HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
-
- _debug "First detect the root zone"
- if ! _get_root "$full_domain"; then
- _err "Invalid domain"
- return 1
- fi
- _debug _domain_id "$_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _debug "Getting TXT records"
- if ! _find_record "$_sub_domain" "$txt_value"; then
- return 1
- fi
-
- if [ -z "$_record_id" ]; then
- _info "Remove not needed. Record not found."
- else
- if ! _hetzner_rest DELETE "records/$_record_id"; then
- _err "Delete record error${_response_error}"
- return 1
- fi
- _sleep 2
- _info "Record deleted"
- fi
-}
-
-#################### Private functions below ##################################
-#returns
-# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error
-_find_record() {
- unset _record_id
- _record_name=$1
- _record_value=$2
-
- if [ -z "$_record_value" ]; then
- _record_value='[^"]*'
- fi
-
- _debug "Getting all records"
- _hetzner_rest GET "records?zone_id=${_domain_id}"
-
- if _response_has_error; then
- _err "Error${_response_error}"
- return 1
- else
- _record_id=$(
- echo "$response" |
- grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" |
- grep "\"value\":\"$_record_value\"" |
- while read -r record; do
- # test for type and
- if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
- echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
- break
- fi
- done
- )
- fi
-}
-
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
-_get_root() {
- domain=$1
- i=1
- p=1
-
- domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
- domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
-
- _debug "Reading zone_id for '$domain_without_acme' from config..."
- HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
- if [ "$HETZNER_Zone_ID" ]; then
- _debug "Found, using: $HETZNER_Zone_ID"
- if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
- _debug "Zone with id '$HETZNER_Zone_ID' does not exist."
- _cleardomainconf "$domain_param_name"
- unset HETZNER_Zone_ID
- else
- if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
- _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
- if [ "$_domain" ]; then
- _cut_length=$((${#domain} - ${#_domain} - 1))
- _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
- _domain_id="$HETZNER_Zone_ID"
- return 0
- else
- return 1
- fi
- else
- return 1
- fi
- fi
- fi
-
- _debug "Trying to get zone id by domain name for '$domain_without_acme'."
- while true; do
- h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
- if [ -z "$h" ]; then
- #not valid
- return 1
- fi
- _debug h "$h"
-
- _hetzner_rest GET "zones?name=$h"
-
- if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
- _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
- if [ "$_domain_id" ]; then
- _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain=$h
- HETZNER_Zone_ID=$_domain_id
- _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
- return 0
- fi
- return 1
- fi
- p=$i
- i=$(_math "$i" + 1)
- done
- return 1
-}
-
-#returns
-# _response_error
-_response_has_error() {
- unset _response_error
-
- err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
-
- if [ -n "$err_part" ]; then
- err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
- err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
-
- if [ -n "$err_code" ] && [ -n "$err_message" ]; then
- _response_error=" - message: ${err_message}, code: ${err_code}"
- return 0
- fi
- fi
-
- return 1
-}
-
-#returns
-# response
-_hetzner_rest() {
- m=$1
- ep="$2"
- data="$3"
- _debug "$ep"
-
- key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
-
- export _H1="Content-TType: application/json"
- export _H2="Auth-API-Token: $key_trimmed"
-
- if [ "$m" != "GET" ]; then
- _debug data "$data"
- response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
- else
- response="$(_get "$HETZNER_Api/$ep")"
- fi
-
- if [ "$?" != "0" ] || _response_has_error; then
- _debug "Error$_response_error"
- return 1
- fi
- _debug2 response "$response"
- return 0
-}
diff --git a/dnsapi/dns_ipprojects.sh b/dnsapi/dns_ipprojects.sh
new file mode 100644
index 00000000..dadd05f0
--- /dev/null
+++ b/dnsapi/dns_ipprojects.sh
@@ -0,0 +1,91 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_ipprojects_info='IP-Projects DNS
+Site: ip-projects.de/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects
+Options:
+ IPP_Apikey API Key
+Issues: github.com/acmesh-official/acme.sh/issues/6958
+Author: Markus Ebner
+'
+
+IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
+IPP_API="https://api.ip-projects.de/v1/dns/acme"
+
+######## Public functions ########
+
+dns_ipprojects_add() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using IP-Projects DNS API to add record"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _IPP_load_credentials; then
+ return 1
+ fi
+
+ _IPP_api_request "add" "$fulldomain" "$txtvalue"
+}
+
+dns_ipprojects_rm() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using IP-Projects DNS API to remove record"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _IPP_load_credentials; then
+ return 1
+ fi
+
+ _IPP_api_request "remove" "$fulldomain" "$txtvalue"
+}
+
+######## Private helpers ########
+
+_IPP_load_credentials() {
+ IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
+
+ if [ -z "$IPP_Apikey" ]; then
+ _err "You must export IPP_Apikey"
+ _err "e.g.: export IPP_Apikey=\"your_api_key\""
+ return 1
+ fi
+
+ _saveaccountconf_mutable IPP_Apikey "$IPP_Apikey"
+ return 0
+}
+
+_IPP_api_request() {
+ action="$1"
+ domain="$2"
+ value="$3"
+
+ url="$IPP_API/$action"
+
+ data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}"
+ _debug url "$url"
+ _debug data "$data"
+ export _H1="X-API-Key: $IPP_Apikey"
+
+ response="$(_post "$data" "$url" "" "POST" "application/json")"
+ ret="$?"
+ _ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
+
+ _debug response "$response"
+
+ if [ "$ret" != "0" ]; then
+ _err "HTTP request failed"
+ return 1
+ fi
+
+ if [ "$_ipprojects_last_http_code" != "200" ]; then
+ _err "API returned an error [code: ${_ipprojects_last_http_code}]"
+ return 1
+ fi
+
+ return 0
+}
diff --git a/dnsapi/dns_poweradmin.sh b/dnsapi/dns_poweradmin.sh
new file mode 100644
index 00000000..db31fa4f
--- /dev/null
+++ b/dnsapi/dns_poweradmin.sh
@@ -0,0 +1,238 @@
+#!/usr/bin/env sh
+
+# shellcheck disable=SC2034
+
+# Credits to the authors of dnsapi/dns_pdns.sh as this reuses much of that code.
+
+dns_poweradmin_info='Poweradmin API
+Site: https://www.poweradmin.org/
+Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_poweradmin
+Options:
+POWERADMIN_URL API URL (with scheme). E.g. "https://poweradmin.example.com" or "http://192.168.0.10:8080"
+POWERADMIN_API_KEY API Token "pwa_xxxx"
+POWERADMIN_API_VERSION Optionally override Poweradmin API version.
+Issues: https://github.com/acmesh-official/acme.sh/issues/6912
+Author: Jakob Næss
+'
+
+######## Public functions ####################
+
+# Usage: dns_poweradmin_add _acme-challenge.www.domain.com "123456789ABCDEF"
+# fulldomain
+# txtvalue
+dns_poweradmin_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}"
+ POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}"
+
+ if [ -z "$POWERADMIN_URL" ]; then
+ POWERADMIN_URL=""
+ _err "You didn't specify Poweradmin URL."
+ _err "Please set POWERADMIN_URL and try again."
+ return 1
+ fi
+
+ if [ -z "$POWERADMIN_API_KEY" ]; then
+ POWERADMIN_API_KEY=""
+ _err "You didn't specify Poweradmin token."
+ _err "Please set POWERADMIN_API_KEY and try again."
+ return 1
+ fi
+
+ # Save the api addr, key, and version to the account conf file.
+ _saveaccountconf_mutable POWERADMIN_URL "$POWERADMIN_URL"
+ _saveaccountconf_mutable POWERADMIN_API_KEY "$POWERADMIN_API_KEY"
+ _saveaccountconf_mutable POWERADMIN_API_VERSION "$POWERADMIN_API_VERSION"
+
+ _debug "Detect root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug _domain "$_domain"
+ _debug _zone_id "$_zone_id"
+
+ if ! _set_record "$fulldomain" "$txtvalue"; then
+ return 1
+ fi
+
+ return 0
+}
+
+# Usage: dns_poweradmin_rm _acme-challenge.www.domain.com "123456789ABCDEF"
+# fulldomain
+# txtvalue
+dns_poweradmin_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}"
+ POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}"
+
+ _debug "Detect root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug _domain "$_domain"
+ _debug _zone_id "$_zone_id"
+
+ if ! _rm_record "$fulldomain" "$txtvalue"; then
+ return 1
+ fi
+
+ return 0
+}
+
+######## Private functions below #####################
+
+_set_record() {
+ _info "Adding TXT record"
+ full=$1
+ new_challenge=$2
+
+ data='{"name":"'$full'","type":"TXT","content":"'$new_challenge'","ttl":60}'
+
+ if ! _poweradmin_rest "POST" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records" "$data" "application/json"; then
+ _err "Failed to add TXT record"
+ return 1
+ fi
+
+ return 0
+}
+
+_rm_record() {
+ _info "Remove TXT record"
+ full=$1
+ txtvalue=$2
+
+ if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then
+ _err "Failed to retrieve records"
+ return 1
+ fi
+
+ # The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]}
+ _txt_record_obj=$(
+ printf '%s\n' "$response" |
+ sed 's/^.*"data":\[//; s/\],"message":.*$//' |
+ awk '{ gsub(/},{/, "}\n{"); print }' |
+ grep -F "\"name\":\"$full\"" |
+ grep -F "\"type\":\"TXT\"" |
+ grep -F "\"content\":\"$txtvalue\"" |
+ _head_n 1
+ )
+
+ if [ -z "$_txt_record_obj" ]; then
+ _info "TXT record not found for $full with content $txtvalue"
+ return 0
+ fi
+
+ record_id=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' | _head_n 1)
+ record_type=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p' | _head_n 1)
+ record_name=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | _head_n 1)
+ record_content=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | _head_n 1)
+
+ _debug2 "_txt_record_obj=$_txt_record_obj"
+ _debug2 "record id: $record_id"
+ _debug2 "record type: $record_type"
+ _debug2 "record name: $record_name"
+ _debug2 "record content: $record_content"
+
+ if [ "$record_type" != "TXT" ]; then
+ _err "Refusing to delete non-TXT record id=$record_id type=$record_type name=$full"
+ return 1
+ fi
+
+ if ! _poweradmin_rest "DELETE" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records/$record_id"; then
+ _err "Failed to delete TXT record"
+ return 1
+ fi
+
+ _info "Record deleted successfully"
+ return 0
+}
+
+# _acme-challenge.www.domain.com
+# returns
+# _domain=domain.com
+# _zone_id=220
+_get_root() {
+ domain=$1
+ i=1
+
+ if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones"; then
+ _err "Failed to retrieve zones"
+ return 1
+ fi
+
+ _zones_response="$response"
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+
+ if [ -z "$h" ]; then
+ _debug "Root domain not found for $domain"
+ return 1
+ fi
+
+ zone_obj=$(
+ printf '%s' "$_zones_response" |
+ sed 's/},{/}\n{/g' |
+ grep -F "\"name\":\"$h\"" |
+ _head_n 1
+ )
+
+ if [ -n "$zone_obj" ]; then
+ _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2)
+ _domain="$h"
+ _debug "Found zone: $_domain with id: $_zone_id"
+ return 0
+ fi
+
+ i=$(_math "$i" + 1)
+ done
+}
+
+_poweradmin_rest() {
+ method=$1
+ ep=$2
+ data=$3
+ ct=$4
+
+ export _H1="X-API-Key: $POWERADMIN_API_KEY"
+
+ if [ "$method" = "GET" ]; then
+ response="$(_get "$POWERADMIN_URL$ep")"
+ else
+ _debug "API call: $method $ep"
+ _debug "Content-Type: $ct"
+ _debug "Payload: $data"
+ response="$(_post "$data" "$POWERADMIN_URL$ep" "" "$method" "$ct")"
+ fi
+
+ # Clear _H1 variable
+ unset -v _H1
+
+ if [ "$?" != "0" ]; then
+ _err "API error on $method $ep"
+ _debug "Response: $response"
+ return 1
+ fi
+
+ if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then
+ _err "API reported failure on $method $ep"
+ _debug "Response: $response"
+ return 1
+ fi
+
+ _debug2 "API Response: $response"
+ return 0
+}
diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh
index e0ad16e2..74e891ad 100644
--- a/dnsapi/dns_simply.sh
+++ b/dnsapi/dns_simply.sh
@@ -8,11 +8,7 @@ Options:
SIMPLY_ApiKey API Key
'
-#SIMPLY_Api="https://api.simply.com/2/"
-SIMPLY_Api_Default="https://api.simply.com/2"
-
-#This is used for determining success of REST call
-SIMPLY_SUCCESS_CODE='"status":200'
+SIMPLY_Api="https://api.simply.com/2"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -72,7 +68,16 @@ dns_simply_rm() {
return 1
fi
- records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ')
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Failed to fetch DNS records (HTTP $_simply_http_code)"
+ _err "$response"
+ return 1
+ ;;
+ esac
+
+ records=$(echo "$response" | tr '{' "\n" | grep -E 'record_id|type|data|name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ')
nr_of_deleted_records=0
_info "Fetching txt record"
@@ -95,7 +100,7 @@ dns_simply_rm() {
if [ "$record_id" -gt 0 ]; then
- if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then
+ if ! _simply_delete_record "$_domain" "$record_id"; then
_err "Record with id $record_id could not be deleted"
return 1
fi
@@ -122,14 +127,9 @@ dns_simply_rm() {
#################### Private functions below ##################################
_simply_load_config() {
- SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}"
SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}"
SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}"
- if [ -z "$SIMPLY_Api" ]; then
- SIMPLY_Api="$SIMPLY_Api_Default"
- fi
-
if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then
SIMPLY_AccountName=""
SIMPLY_ApiKey=""
@@ -144,9 +144,6 @@ _simply_load_config() {
}
_simply_save_config() {
- if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then
- _saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api"
- fi
_saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName"
_saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey"
}
@@ -163,26 +160,39 @@ _simply_get_all_records() {
_get_root() {
domain=$1
+
+ if ! _simply_rest GET "my/products/"; then
+ return 1
+ fi
+
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Failed to fetch product list (HTTP $_simply_http_code)"
+ _err "$response"
+ return 1
+ ;;
+ esac
+
i=2
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
- #not valid
return 1
fi
- if ! _simply_rest GET "my/products/$h/dns/"; then
- return 1
- fi
+ _domain=$(printf "%s" "$response" | tr '}' '\n' |
+ grep -F -e "\"object\":\"$h\"" -e "\"name\":\"$h\"" -e "\"name_idn\":\"$h\"" |
+ sed -n 's/.*"object":"\([^"]*\)".*/\1/p' |
+ _head_n 1)
- if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
- _debug "$h not found"
- else
+ if [ -n "$_domain" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain="$h"
return 0
fi
+
+ _debug "No Simply.com product found for $h"
p="$i"
i=$(_math "$i" + 1)
done
@@ -194,39 +204,44 @@ _simply_add_record() {
sub_domain=$2
txtval=$3
- data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}"
+ data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 120}"
if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then
- _err "Adding record not successfull!"
+ _err "Adding record not successful!"
return 1
fi
- if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
- _err "Call to API not sucessfull, see below message for more details"
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details"
_err "$response"
return 1
- fi
+ ;;
+ esac
return 0
}
_simply_delete_record() {
domain=$1
- sub_domain=$2
- record_id=$3
+ record_id=$2
_debug record_id "Delete record with id $record_id"
if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then
- _err "Deleting record not successfull!"
+ _err "Deleting record not successful!"
return 1
fi
- if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
- _err "Call to API not sucessfull, see below message for more details"
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details"
_err "$response"
return 1
- fi
+ ;;
+ esac
return 0
}
@@ -248,17 +263,24 @@ _simply_rest() {
export _H2="Content-Type: application/json"
+ : >"$HTTP_HEADER"
+
if [ "$m" != "GET" ]; then
response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")"
else
response="$(_get "$SIMPLY_Api/$ep")"
fi
- if [ "$?" != "0" ]; then
+ _ret="$?"
+ unset _H1 _H2
+
+ if [ "$_ret" != "0" ]; then
_err "error $ep"
return 1
fi
+ _simply_http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d' ' -f2 | tr -d '\r\n')"
+
response="$(echo "$response" | _normalizeJson)"
_debug2 response "$response"