diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml
index be9d3aae..ccce2ff6 100644
--- a/.github/workflows/DNS.yml
+++ b/.github/workflows/DNS.yml
@@ -441,7 +441,9 @@ jobs:
with:
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}}'
copyback: false
- prepare: pkgutil -y -i socat
+ prepare: |
+ pkgutil -U
+ pkgutil -y -i socat
run: |
pkg set-mediator -v -I default@1.1 openssl
export PATH=/usr/gnu/bin:$PATH
diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml
index b0326332..729874ce 100644
--- a/.github/workflows/PebbleStrict.yml
+++ b/.github/workflows/PebbleStrict.yml
@@ -65,7 +65,7 @@ jobs:
run: |
docker run --rm -itd --name=pebble \
-e PEBBLE_VA_ALWAYS_VALID=1 \
- -p 14000:14000 -p 15000:15000 letsencrypt/pebble:latest pebble -config /test/config/pebble-config.json -strict
+ -p 14000:14000 -p 15000:15000 ghcr.io/letsencrypt/pebble:latest -config /test/config/pebble-config.json -strict
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- name: Run acmetest
diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml
index 95bcd8d1..0ba3d2eb 100644
--- a/.github/workflows/Solaris.yml
+++ b/.github/workflows/Solaris.yml
@@ -66,7 +66,9 @@ jobs:
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
- prepare: pkgutil -y -i socat curl wget
+ prepare: |
+ pkgutil -U
+ pkgutil -y -i socat curl wget
copyback: false
run: |
cd ../acmetest \
diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml
index b0332775..a79d70a4 100644
--- a/.github/workflows/wiki-monitor.yml
+++ b/.github/workflows/wiki-monitor.yml
@@ -22,6 +22,7 @@ jobs:
page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH")
page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH")
page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH")
+ page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH")
now="$(date '+%Y-%m-%d %H:%M:%S')"
cd wiki
@@ -35,9 +36,11 @@ jobs:
{
echo "Wiki edited"
echo -n "User: "
- echo "[$actor]($sender_url)"
+ echo "@$actor [$actor]($sender_url)"
echo "Time: $now"
echo "Page: [$page_name]($page_url) (Action: $page_action)"
+ echo "Comment: $page_summary"
+ echo "[Click here to Revert](${page_url}/_history)"
echo ""
echo "----"
echo "### diff:"
diff --git a/Dockerfile b/Dockerfile
index 7523f0af..88edc4a2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.21
+FROM alpine:3.22
RUN apk --no-cache add -f \
openssl \
@@ -15,6 +15,8 @@ RUN apk --no-cache add -f \
jq \
cronie
+ENV LE_WORKING_DIR=/acmebin
+
ENV LE_CONFIG_HOME=/acme.sh
ARG AUTO_UPGRADE=1
@@ -30,7 +32,7 @@ COPY ./notify /install_acme.sh/notify
RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/
-RUN ln -s /root/.acme.sh/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab -
+RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab -
RUN for verb in help \
version \
@@ -64,7 +66,7 @@ RUN for verb in help \
set-default-ca \
set-default-chain \
; do \
- printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
+ printf -- "%b" "#!/usr/bin/env sh\n$LE_WORKING_DIR/acme.sh --${verb} --config-home $LE_CONFIG_HOME \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
; done
RUN printf "%b" '#!'"/usr/bin/env sh\n \
@@ -72,7 +74,7 @@ if [ \"\$1\" = \"daemon\" ]; then \n \
exec crond -n -s -m off \n \
else \n \
exec -- \"\$@\"\n \
-fi\n" >/entry.sh && chmod +x /entry.sh
+fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME
VOLUME /acme.sh
diff --git a/README.md b/README.md
index f7038f59..4afd90a8 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+[](https://zerossl.com/?fromacme.sh)
+
# An ACME Shell script: acme.sh
[](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)
@@ -84,7 +86,6 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|18|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|19|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|10|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
-|11|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|ClearLinux
|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111
|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)
@@ -207,6 +208,8 @@ The certs will be placed in `~/.acme.sh/example.com/`
The certs will be renewed automatically every **60** days.
+The certs will default to ECC certificates.
+
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
@@ -358,36 +361,33 @@ Ok, it's done.
**Please use dns api mode instead.**
-# 10. Issue ECC certificates
+# 10. Issue certificates of different key types and lengths (ECC or RSA)
+
+Just set the `keylength` to a valid, supported, value.
-Just set the `keylength` parameter with a prefix `ec-`.
+Valid values for the `keylength` parameter are:
+
+1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)**
+2. **ec-384 (secp384r1, "ECDSA P-384")**
+3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
+4. **2048 (RSA2048)**
+5. **3072 (RSA3072)**
+6. **4096 (RSA4096)**
For example:
-### Single domain ECC certificate
+### Single domain with ECDSA P-384 certificate
```bash
-acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256
+acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384
```
-### SAN multi domain ECC certificate
+### SAN multi domain with RSA4096 certificate
```bash
-acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength ec-256
+acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
```
-Please look at the `keylength` parameter above.
-
-Valid values are:
-
-1. **ec-256 (prime256v1, "ECDSA P-256", which is the default key type)**
-2. **ec-384 (secp384r1, "ECDSA P-384")**
-3. **ec-521 (secp521r1, "ECDSA P-521", which is not supported by Let's Encrypt yet.)**
-4. **2048 (RSA2048)**
-5. **3072 (RSA3072)**
-6. **4096 (RSA4096)**
-
-
# 11. Issue Wildcard certificates
It's simple, just give a wildcard domain as the `-d` parameter.
@@ -523,3 +523,20 @@ Your donation makes **acme.sh** better:
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
+
+# 21. About this repository
+
+> [!NOTE]
+> This repository is officially maintained by ZeroSSL as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community!
+> For more information about our services, including free and paid SSL/TLS certificates, visit https://zerossl.com.
+>
+> All donations made through this repository go directly to the original independent maintainer (Neil Pang), not to ZeroSSL.
+
+
+
+
+
+
+
+
+
diff --git a/acme.sh b/acme.sh
index 214d1fc7..22282fc1 100755
--- a/acme.sh
+++ b/acme.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env sh
-VER=3.1.2
+VER=3.1.3
PROJECT_NAME="acme.sh"
@@ -1031,7 +1031,7 @@ _digest() {
outputhex="$2"
- if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
+ if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
if [ "$outputhex" ]; then
${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
else
@@ -1250,7 +1250,7 @@ _idn() {
fi
}
-#_createcsr cn san_list keyfile csrfile conf acmeValidationv1
+#_createcsr cn san_list keyfile csrfile conf acmeValidationv1 extendedUsage
_createcsr() {
_debug _createcsr
domain="$1"
@@ -1259,6 +1259,7 @@ _createcsr() {
csr="$4"
csrconf="$5"
acmeValidationv1="$6"
+ extusage="$7"
_debug2 domain "$domain"
_debug2 domainlist "$domainlist"
_debug2 csrkey "$csrkey"
@@ -1267,9 +1268,8 @@ _createcsr() {
printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]" >"$csrconf"
- if [ "$Le_ExtKeyUse" ]; then
- _savedomainconf Le_ExtKeyUse "$Le_ExtKeyUse"
- printf "\nextendedKeyUsage=$Le_ExtKeyUse\n" >>"$csrconf"
+ if [ "$extusage" ]; then
+ printf "\nextendedKeyUsage=$extusage\n" >>"$csrconf"
else
printf "\nextendedKeyUsage=serverAuth,clientAuth\n" >>"$csrconf"
fi
@@ -1897,6 +1897,11 @@ _inithttp() {
if [ -z "$_ACME_CURL" ] && _exists "curl"; then
_ACME_CURL="curl --silent --dump-header $HTTP_HEADER "
+ if [ "$ACME_USE_IPV6_REQUESTS" ]; then
+ _ACME_CURL="$_ACME_CURL --ipv6 "
+ elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
+ _ACME_CURL="$_ACME_CURL --ipv4 "
+ fi
if [ -z "$ACME_HTTP_NO_REDIRECTS" ]; then
_ACME_CURL="$_ACME_CURL -L "
fi
@@ -1924,6 +1929,11 @@ _inithttp() {
if [ -z "$_ACME_WGET" ] && _exists "wget"; then
_ACME_WGET="wget -q"
+ if [ "$ACME_USE_IPV6_REQUESTS" ]; then
+ _ACME_WGET="$_ACME_WGET --inet6-only "
+ elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
+ _ACME_WGET="$_ACME_WGET --inet4-only "
+ fi
if [ "$ACME_HTTP_NO_REDIRECTS" ]; then
_ACME_WGET="$_ACME_WGET --max-redirect 0 "
fi
@@ -4435,6 +4445,7 @@ issue() {
_valid_from="${16}"
_valid_to="${17}"
_certificate_profile="${18}"
+ _extended_key_usage="${19}"
if [ -z "$_ACME_IS_RENEW" ]; then
_initpath "$_main_domain" "$_key_length"
@@ -4579,12 +4590,25 @@ issue() {
return 1
fi
fi
- if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then
+ _keyusage="$_extended_key_usage"
+ if [ "$Le_API" = "$CA_GOOGLE" ] || [ "$Le_API" = "$CA_GOOGLE_TEST" ]; then
+ if [ -z "$_keyusage" ]; then
+ #https://github.com/acmesh-official/acme.sh/issues/6610
+ #google accepts serverauth only
+ _keyusage="serverAuth"
+ fi
+ fi
+ if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF" "" "$_keyusage"; then
_err "Error creating CSR."
_clearup
_on_issue_err "$_post_hook"
return 1
fi
+ if [ "$_extended_key_usage" ]; then
+ _savedomainconf "Le_ExtKeyUse" "$_extended_key_usage"
+ else
+ _cleardomainconf "Le_ExtKeyUse"
+ fi
fi
_savedomainconf "Le_Keylength" "$_key_length"
@@ -5215,6 +5239,16 @@ $_authorizations_map"
return 1
fi
break
+ elif _contains "$response" "\"ready\""; then
+ _info "Order status is 'ready', let's sleep and retry."
+ _retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
+ _debug "_retryafter" "$_retryafter"
+ if [ "$_retryafter" ]; then
+ _info "Sleeping for $_retryafter seconds then retrying"
+ _sleep $_retryafter
+ else
+ _sleep 2
+ fi
elif _contains "$response" "\"processing\""; then
_info "Order status is 'processing', let's sleep and retry."
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *:" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
@@ -5540,7 +5574,7 @@ renew() {
_cleardomainconf Le_OCSP_Staple
fi
fi
- issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile"
+ issue "$Le_Webroot" "$Le_Domain" "$Le_Alt" "$Le_Keylength" "$Le_RealCertPath" "$Le_RealKeyPath" "$Le_RealCACertPath" "$Le_ReloadCmd" "$Le_RealFullChainPath" "$Le_PreHook" "$Le_PostHook" "$Le_RenewHook" "$Le_LocalAddress" "$Le_ChallengeAlias" "$Le_Preferred_Chain" "$Le_Valid_From" "$Le_Valid_To" "$Le_Certificate_Profile" "$Le_ExtKeyUse"
res="$?"
if [ "$res" != "0" ]; then
return "$res"
@@ -7134,6 +7168,8 @@ Parameters:
--auto-upgrade [0|1] Valid for '--upgrade' command, indicating whether to upgrade automatically in future. Defaults to 1 if argument is omitted.
--listen-v4 Force standalone/tls server to listen at ipv4.
--listen-v6 Force standalone/tls server to listen at ipv6.
+ --request-v4 Force client requests to use ipv4 to connect to the CA server.
+ --request-v6 Force client requests to use ipv6 to connect to the CA server.
--openssl-bin Specifies a custom openssl bin location.
--use-wget Force to use wget, if you have both curl and wget installed.
--yes-I-know-dns-manual-mode-enough-go-ahead-please Force use of dns manual mode.
@@ -7252,6 +7288,24 @@ _processAccountConf() {
_saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET"
fi
+ if [ "$_request_v6" ]; then
+ _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$_request_v6"
+ _clearaccountconf "ACME_USE_IPV4_REQUESTS"
+ ACME_USE_IPV4_REQUESTS=
+ elif [ "$_request_v4" ]; then
+ _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$_request_v4"
+ _clearaccountconf "ACME_USE_IPV6_REQUESTS"
+ ACME_USE_IPV6_REQUESTS=
+ elif [ "$ACME_USE_IPV6_REQUESTS" ]; then
+ _saveaccountconf "ACME_USE_IPV6_REQUESTS" "$ACME_USE_IPV6_REQUESTS"
+ _clearaccountconf "ACME_USE_IPV4_REQUESTS"
+ ACME_USE_IPV4_REQUESTS=
+ elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
+ _saveaccountconf "ACME_USE_IPV4_REQUESTS" "$ACME_USE_IPV4_REQUESTS"
+ _clearaccountconf "ACME_USE_IPV6_REQUESTS"
+ ACME_USE_IPV6_REQUESTS=
+ fi
+
}
_checkSudo() {
@@ -7417,6 +7471,8 @@ _process() {
_local_address=""
_log_level=""
_auto_upgrade=""
+ _request_v4=""
+ _request_v6=""
_listen_v4=""
_listen_v6=""
_openssl_bin=""
@@ -7434,6 +7490,7 @@ _process() {
_valid_from=""
_valid_to=""
_certificate_profile=""
+ _extended_key_usage=""
while [ ${#} -gt 0 ]; do
case "${1}" in
@@ -7829,7 +7886,7 @@ _process() {
shift
;;
--extended-key-usage)
- Le_ExtKeyUse="$2"
+ _extended_key_usage="$2"
shift
;;
--ocsp-must-staple | --ocsp)
@@ -7882,6 +7939,18 @@ _process() {
fi
AUTO_UPGRADE="$_auto_upgrade"
;;
+ --request-v4)
+ _request_v4="1"
+ ACME_USE_IPV4_REQUESTS="1"
+ _request_v6=""
+ ACME_USE_IPV6_REQUESTS=""
+ ;;
+ --request-v6)
+ _request_v6="1"
+ ACME_USE_IPV6_REQUESTS="1"
+ _request_v4=""
+ ACME_USE_IPV4_REQUESTS=""
+ ;;
--listen-v4)
_listen_v4="1"
Le_Listen_V4="$_listen_v4"
@@ -8034,7 +8103,7 @@ _process() {
uninstall) uninstall "$_nocron" ;;
upgrade) upgrade ;;
issue)
- issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile"
+ issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain" "$_valid_from" "$_valid_to" "$_certificate_profile" "$_extended_key_usage"
;;
deploy)
deploy "$_domain" "$_deploy_hook" "$_ecc"
diff --git a/deploy/ali_cdn.sh b/deploy/ali_cdn.sh
index 70a2e532..3c28674e 100644
--- a/deploy/ali_cdn.sh
+++ b/deploy/ali_cdn.sh
@@ -83,6 +83,6 @@ _set_cdn_domain_ssl_certificate_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2018-05-10'
}
diff --git a/deploy/ali_dcdn.sh b/deploy/ali_dcdn.sh
index 14ac500a..27d3a726 100644
--- a/deploy/ali_dcdn.sh
+++ b/deploy/ali_dcdn.sh
@@ -83,6 +83,6 @@ _set_dcdn_domain_ssl_certificate_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2018-01-15'
}
diff --git a/deploy/keyhelp_api.sh b/deploy/keyhelp_api.sh
new file mode 100644
index 00000000..75e9d951
--- /dev/null
+++ b/deploy/keyhelp_api.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env sh
+
+keyhelp_api_deploy() {
+ _cdomain="$1"
+ _ckey="$2"
+ _ccert="$3"
+ _cca="$4"
+
+ _debug _cdomain "$_cdomain"
+ _debug _ckey "$_ckey"
+ _debug _ccert "$_ccert"
+ _debug _cca "$_cca"
+
+ # Read config from saved values or env
+ _getdeployconf DEPLOY_KEYHELP_HOST
+ _getdeployconf DEPLOY_KEYHELP_API_KEY
+
+ _debug DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
+ _secure_debug DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
+
+ if [ -z "$DEPLOY_KEYHELP_HOST" ]; then
+ _err "KeyHelp host not found, please define DEPLOY_KEYHELP_HOST."
+ return 1
+ fi
+ if [ -z "$DEPLOY_KEYHELP_API_KEY" ]; then
+ _err "KeyHelp api key not found, please define DEPLOY_KEYHELP_API_KEY."
+ return 1
+ fi
+
+ # Save current values
+ _savedeployconf DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST"
+ _savedeployconf DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY"
+
+ _request_key="$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')"
+ _request_cert="$(tr '\n' ':' <"$_ccert" | sed 's/:/\\n/g')"
+ _request_ca="$(tr '\n' ':' <"$_cca" | sed 's/:/\\n/g')"
+
+ _request_body="{
+ \"name\": \"$_cdomain\",
+ \"components\": {
+ \"private_key\": \"$_request_key\",
+ \"certificate\": \"$_request_cert\",
+ \"ca_certificate\": \"$_request_ca\"
+ }
+ }"
+
+ _hosts="$(echo "$DEPLOY_KEYHELP_HOST" | tr "," " ")"
+ _keys="$(echo "$DEPLOY_KEYHELP_API_KEY" | tr "," " ")"
+ _i=1
+
+ for _host in $_hosts; do
+ _key="$(_getfield "$_keys" "$_i" " ")"
+ _i="$(_math "$_i" + 1)"
+
+ export _H1="X-API-Key: $_key"
+
+ _put_url="$_host/api/v2/certificates/name/$_cdomain"
+ if _post "$_request_body" "$_put_url" "" "PUT" "application/json" >/dev/null; then
+ _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ else
+ _err "Cannot make PUT request to $_put_url"
+ return 1
+ fi
+
+ if [ "$_code" = "404" ]; then
+ _info "$_cdomain not found, creating new entry at $_host"
+
+ _post_url="$_host/api/v2/certificates"
+ if _post "$_request_body" "$_post_url" "" "POST" "application/json" >/dev/null; then
+ _code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
+ else
+ _err "Cannot make POST request to $_post_url"
+ return 1
+ fi
+ fi
+
+ if _startswith "$_code" "2"; then
+ _info "$_cdomain set at $_host"
+ else
+ _err "HTTP status code is $_code"
+ return 1
+ fi
+ done
+
+ return 0
+}
diff --git a/deploy/panos.sh b/deploy/panos.sh
index a9232e79..c54d21fe 100644
--- a/deploy/panos.sh
+++ b/deploy/panos.sh
@@ -16,6 +16,7 @@
# export PANOS_TEMPLATE="" # Template Name of panorama managed devices
# export PANOS_TEMPLATE_STACK="" # set a Template Stack if certificate should also be pushed automatically
# export PANOS_VSYS="Shared" # name of the vsys to import the certificate
+# export PANOS_CERTNAME="" # use a custom certificate name to work around Panorama's 31-character limit
#
# The script will automatically generate a new API key if
# no key is found, or if a saved key has expired or is invalid.
@@ -89,7 +90,7 @@ deployer() {
if [ "$type" = 'cert' ]; then
panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\ncertificate"
- content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
+ content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cfullchain")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_cfullchain")"
@@ -103,11 +104,11 @@ deployer() {
if [ "$type" = 'key' ]; then
panos_url="${panos_url}?type=import"
content="--$delim${nl}Content-Disposition: form-data; name=\"category\"\r\n\r\nprivate-key"
- content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_cdomain"
+ content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"certificate-name\"\r\n\r\n$_panos_certname"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"key\"\r\n\r\n$_panos_key"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"format\"\r\n\r\npem"
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"passphrase\"\r\n\r\n123456"
- content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_cdomain.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
+ content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"file\"; filename=\"$(basename "$_panos_certname.key")\"${nl}Content-Type: application/octet-stream${nl}${nl}$(cat "$_ckey")"
if [ "$_panos_template" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
fi
@@ -242,6 +243,15 @@ panos_deploy() {
_getdeployconf PANOS_VSYS
fi
+ # PANOS_CERTNAME
+ if [ "$PANOS_CERTNAME" ]; then
+ _debug "Detected ENV variable PANOS_CERTNAME. Saving to file."
+ _savedeployconf PANOS_CERTNAME "$PANOS_CERTNAME" 1
+ else
+ _debug "Attempting to load variable PANOS_CERTNAME from file."
+ _getdeployconf PANOS_CERTNAME
+ fi
+
#Store variables
_panos_host=$PANOS_HOST
_panos_user=$PANOS_USER
@@ -249,6 +259,7 @@ panos_deploy() {
_panos_template=$PANOS_TEMPLATE
_panos_template_stack=$PANOS_TEMPLATE_STACK
_panos_vsys=$PANOS_VSYS
+ _panos_certname=$PANOS_CERTNAME
#Test API Key if found. If the key is invalid, the variable _panos_key will be unset.
if [ "$_panos_host" ] && [ "$_panos_key" ]; then
@@ -267,6 +278,12 @@ panos_deploy() {
_err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs."
return 1
else
+ # Use certificate name based on the first domain on the certificate if no custom certificate name is set
+ if [ -z "$_panos_certname" ]; then
+ _panos_certname="$_cdomain"
+ _savedeployconf PANOS_CERTNAME "$_panos_certname" 1
+ fi
+
# Generate a new API key if no valid API key is found
if [ -z "$_panos_key" ]; then
_debug "**** Generating new PANOS API KEY ****"
diff --git a/deploy/truenas_ws.sh b/deploy/truenas_ws.sh
index d334853e..df34f927 100644
--- a/deploy/truenas_ws.sh
+++ b/deploy/truenas_ws.sh
@@ -71,7 +71,7 @@ with Client(uri="$_ws_uri") as c:
fullchain = file.read()
with open('$2', 'r') as file:
privatekey = file.read()
- ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True)
+ ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey}, job=True)
print("R:" + str(ret["id"]))
sys.exit(0)
else:
diff --git a/deploy/unifi.sh b/deploy/unifi.sh
index 1f274236..1d13e04f 100644
--- a/deploy/unifi.sh
+++ b/deploy/unifi.sh
@@ -143,8 +143,10 @@ unifi_deploy() {
# correct file ownership according to the directory, the keystore is placed in
_unifi_keystore_dir=$(dirname "${_unifi_keystore}")
- _unifi_keystore_dir_owner=$(find "${_unifi_keystore_dir}" -maxdepth 0 -printf '%u\n')
- _unifi_keystore_owner=$(find "${_unifi_keystore}" -maxdepth 0 -printf '%u\n')
+ # shellcheck disable=SC2012
+ _unifi_keystore_dir_owner=$(ls -ld "${_unifi_keystore_dir}" | awk '{print $3}')
+ # shellcheck disable=SC2012
+ _unifi_keystore_owner=$(ls -l "${_unifi_keystore}" | awk '{print $3}')
if ! [ "${_unifi_keystore_owner}" = "${_unifi_keystore_dir_owner}" ]; then
_debug "Changing keystore owner to ${_unifi_keystore_dir_owner}"
chown "$_unifi_keystore_dir_owner" "${_unifi_keystore}" >/dev/null 2>&1 # fail quietly if we're not running as root
diff --git a/dnsapi/dns_ali.sh b/dnsapi/dns_ali.sh
index 53a82f91..90196c69 100755
--- a/dnsapi/dns_ali.sh
+++ b/dnsapi/dns_ali.sh
@@ -97,12 +97,13 @@ _ali_rest() {
}
_ali_nonce() {
- #_head_n 1 /dev/null && return 0
+ fi
+ printf "%s" "$(date +%s)$$$(date +%N)" | _digest sha256 hex | cut -c 1-32
}
-_timestamp() {
+_ali_timestamp() {
date -u +"%Y-%m-%dT%H%%3A%M%%3A%SZ"
}
@@ -150,7 +151,7 @@ _check_exist_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&TypeKeyWord=TXT'
query=$query'&Version=2015-01-09'
}
@@ -166,7 +167,7 @@ _add_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Type=TXT'
query=$query'&Value='$3
query=$query'&Version=2015-01-09'
@@ -182,7 +183,7 @@ _delete_record_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09'
}
@@ -196,7 +197,7 @@ _describe_records_query() {
query=$query'&SignatureMethod=HMAC-SHA1'
query=$query"&SignatureNonce=$(_ali_nonce)"
query=$query'&SignatureVersion=1.0'
- query=$query'&Timestamp='$(_timestamp)
+ query=$query'&Timestamp='$(_ali_timestamp)
query=$query'&Version=2015-01-09'
}
diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh
index c88c9d9c..b76d69c2 100755
--- a/dnsapi/dns_aws.sh
+++ b/dnsapi/dns_aws.sh
@@ -161,7 +161,7 @@ _get_root() {
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100 | sed 's/\./\\./g')
_debug "Checking domain: $h"
if [ -z "$h" ]; then
- _error "invalid domain"
+ _err "invalid domain"
return 1
fi
diff --git a/dnsapi/dns_cf.sh b/dnsapi/dns_cf.sh
index 736742f3..7b383c43 100755
--- a/dnsapi/dns_cf.sh
+++ b/dnsapi/dns_cf.sh
@@ -92,7 +92,9 @@ dns_cf_add() {
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
- elif _contains "$response" "The record already exists"; then
+ elif _contains "$response" "The record already exists" ||
+ _contains "$response" "An identical record already exists." ||
+ _contains "$response" '"code":81058'; then
_info "Already exists, OK"
return 0
else
diff --git a/dnsapi/dns_curanet.sh b/dnsapi/dns_curanet.sh
index f57afa1f..0ef03fea 100644
--- a/dnsapi/dns_curanet.sh
+++ b/dnsapi/dns_curanet.sh
@@ -15,7 +15,7 @@ CURANET_REST_URL="https://api.curanet.dk/dns/v1/Domains"
CURANET_AUTH_URL="https://apiauth.dk.team.blue/auth/realms/Curanet/protocol/openid-connect/token"
CURANET_ACCESS_TOKEN=""
-######## Public functions #####################
+######## Public functions ####################
#Usage: dns_curanet_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_curanet_add() {
@@ -154,7 +154,7 @@ _get_root() {
export _H3="Authorization: Bearer $CURANET_ACCESS_TOKEN"
response="$(_get "$CURANET_REST_URL/$h/Records" "" "")"
- if [ ! "$(echo "$response" | _egrep_o "Entity not found")" ]; then
+ if [ ! "$(echo "$response" | _egrep_o "Entity not found|Bad Request")" ]; then
_domain=$h
return 0
fi
diff --git a/dnsapi/dns_efficientip.sh b/dnsapi/dns_efficientip.sh
new file mode 100755
index 00000000..a485849a
--- /dev/null
+++ b/dnsapi/dns_efficientip.sh
@@ -0,0 +1,139 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_efficientip_info='efficientip.com
+Site: https://efficientip.com/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip
+Options:
+ EfficientIP_Creds HTTP Basic Authentication credentials. E.g. "username:password"
+ EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN.
+ EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional.
+ EfficientIP_View Name of the DNS view hosting the zone. Optional.
+OptionsAlt:
+ EfficientIP_Token_Key Alternative API token key, prefered over basic authentication.
+ EfficientIP_Token_Secret Alternative API token secret, required when using a token key.
+ EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN.
+ EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional.
+ EfficientIP_View Name of the DNS view hosting the zone. Optional.
+Issues: github.com/acmesh-official/acme.sh/issues/6325
+Author: EfficientIP-Labs
+'
+
+dns_efficientip_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _info "Using EfficientIP API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then
+ EfficientIP_Creds=""
+ EfficientIP_Token_Key=""
+ EfficientIP_Token_Secret=""
+ EfficientIP_Server=""
+ _err "You didn't specify any EfficientIP credentials or token or server (EfficientIP_Creds; EfficientIP_Token_Key; EfficientIP_Token_Secret; EfficientIP_Server)."
+ _err "Please set them via EXPORT EfficientIP_Creds=username:password or EXPORT EfficientIP_server=ip/hostname"
+ _err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey"
+ _err "and EXPORT EfficientIP_Token_Secret=yoursecret"
+ _err "then try again."
+ return 1
+ fi
+
+ if [ -z "${EfficientIP_DNS_Name}" ]; then
+ EfficientIP_DNS_Name=""
+ fi
+
+ EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode)
+
+ if [ -z "${EfficientIP_View}" ]; then
+ EfficientIP_View=""
+ fi
+
+ EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode)
+
+ _saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}"
+ _saveaccountconf EfficientIP_Token_Key "${EfficientIP_Token_Key}"
+ _saveaccountconf EfficientIP_Token_Secret "${EfficientIP_Token_Secret}"
+ _saveaccountconf EfficientIP_Server "${EfficientIP_Server}"
+ _saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}"
+ _saveaccountconf EfficientIP_View "${EfficientIP_View}"
+
+ export _H1="Accept-Language:en-US"
+ baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_ttl=300&rr_name=${fulldomain}&rr_value1=${txtvalue}"
+
+ if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}"
+ fi
+
+ if [ "${EfficientIP_ViewEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}"
+ fi
+
+ if [ -z "${EfficientIP_Token_Secret}" ] || [ -z "${EfficientIP_Token_Key}" ]; then
+ EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
+ export _H2="Authorization: Basic ${EfficientIP_CredsEncoded}"
+ else
+ TS=$(date +%s)
+ Sig=$(printf "%b\n$TS\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex)
+ EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig")
+ export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}"
+ export _H3="X-SDS-TS: ${TS}"
+ fi
+
+ result="$(_post "" "${baseurlnObject}" "" "POST")"
+
+ if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then
+ _info "DNS record successfully created"
+ return 0
+ else
+ _err "Error creating DNS record"
+ _err "${result}"
+ return 1
+ fi
+}
+
+dns_efficientip_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ _info "Using EfficientIP API"
+ _debug fulldomain "${fulldomain}"
+ _debug txtvalue "${txtvalue}"
+
+ EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode)
+ EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode)
+ EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
+
+ export _H1="Accept-Language:en-US"
+
+ baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_delete?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue"
+ if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}"
+ fi
+
+ if [ "${EfficientIP_ViewEncoded}" != "" ]; then
+ baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}"
+ fi
+
+ if [ -z "$EfficientIP_Token_Secret" ] || [ -z "$EfficientIP_Token_Key" ]; then
+ EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64)
+ export _H2="Authorization: Basic $EfficientIP_CredsEncoded"
+ else
+ TS=$(date +%s)
+ Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex)
+ EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig")
+ export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}"
+ export _H3="X-SDS-TS: $TS"
+ fi
+
+ result="$(_post "" "${baseurlnObject}" "" "DELETE")"
+
+ if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then
+ _info "DNS Record successfully deleted"
+ return 0
+ else
+ _err "Error deleting DNS record"
+ _err "${result}"
+ return 1
+ fi
+}
diff --git a/dnsapi/dns_exoscale.sh b/dnsapi/dns_exoscale.sh
old mode 100755
new mode 100644
index 6898ce38..ddd526a4
--- a/dnsapi/dns_exoscale.sh
+++ b/dnsapi/dns_exoscale.sh
@@ -8,9 +8,9 @@ Options:
EXOSCALE_SECRET_KEY API Secret key
'
-EXOSCALE_API=https://api.exoscale.com/dns/v1
+EXOSCALE_API="https://api-ch-gva-2.exoscale.com/v2"
-######## Public functions #####################
+######## Public functions ########
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
@@ -18,159 +18,197 @@ dns_exoscale_add() {
fulldomain=$1
txtvalue=$2
- if ! _checkAuth; then
+ _debug "Using Exoscale DNS v2 API"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _check_auth; then
return 1
fi
- _debug "First detect the root zone"
- if ! _get_root "$fulldomain"; then
- _err "invalid domain"
+ root_domain_id=$(_get_root_domain_id "$fulldomain")
+ if [ -z "$root_domain_id" ]; then
+ _err "Unable to determine root domain ID for $fulldomain"
return 1
fi
+ _debug root_domain_id "$root_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
+ # Always get the subdomain part first
+ sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id")
+ _debug sub_domain "$sub_domain"
- _info "Adding record"
- if _exoscale_rest POST "domains/$_domain_id/records" "{\"record\":{\"name\":\"$_sub_domain\",\"record_type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}}" "$_domain_token"; then
- if _contains "$response" "$txtvalue"; then
- _info "Added, OK"
- return 0
- fi
+ # Build the record name properly
+ if [ -z "$sub_domain" ]; then
+ record_name="_acme-challenge"
+ else
+ record_name="_acme-challenge.$sub_domain"
fi
- _err "Add txt record error."
- return 1
+ payload=$(printf '{"name":"%s","type":"TXT","content":"%s","ttl":120}' "$record_name" "$txtvalue")
+ _debug payload "$payload"
+
+ response=$(_exoscale_rest POST "/dns-domain/${root_domain_id}/record" "$payload")
+ if _contains "$response" "\"id\""; then
+ _info "TXT record added successfully."
+ return 0
+ else
+ _err "Error adding TXT record: $response"
+ return 1
+ fi
}
-# Usage: fulldomain txtvalue
-# Used to remove the txt record after validation
dns_exoscale_rm() {
fulldomain=$1
- txtvalue=$2
- if ! _checkAuth; then
+ _debug "Using Exoscale DNS v2 API for removal"
+ _debug fulldomain "$fulldomain"
+
+ if ! _check_auth; then
return 1
fi
- _debug "First detect the root zone"
- if ! _get_root "$fulldomain"; then
- _err "invalid domain"
+ root_domain_id=$(_get_root_domain_id "$fulldomain")
+ if [ -z "$root_domain_id" ]; then
+ _err "Unable to determine root domain ID for $fulldomain"
return 1
fi
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _debug "Getting txt records"
- _exoscale_rest GET "domains/${_domain_id}/records?type=TXT&name=$_sub_domain" "" "$_domain_token"
- if _contains "$response" "\"name\":\"$_sub_domain\"" >/dev/null; then
- _record_id=$(echo "$response" | tr '{' "\n" | grep "\"content\":\"$txtvalue\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
+ record_name="_acme-challenge"
+ sub_domain=$(_get_sub_domain "$fulldomain" "$root_domain_id")
+ if [ -n "$sub_domain" ]; then
+ record_name="_acme-challenge.$sub_domain"
fi
- if [ -z "$_record_id" ]; then
- _err "Can not get record id to remove."
+ record_id=$(_find_record_id "$root_domain_id" "$record_name")
+ if [ -z "$record_id" ]; then
+ _err "TXT record not found for deletion."
return 1
fi
- _debug "Deleting record $_record_id"
-
- if ! _exoscale_rest DELETE "domains/$_domain_id/records/$_record_id" "" "$_domain_token"; then
- _err "Delete record error."
+ response=$(_exoscale_rest DELETE "/dns-domain/$root_domain_id/record/$record_id")
+ if _contains "$response" "\"state\":\"success\""; then
+ _info "TXT record deleted successfully."
+ return 0
+ else
+ _err "Error deleting TXT record: $response"
return 1
fi
-
- return 0
}
-#################### Private functions below ##################################
+######## Private helpers ########
-_checkAuth() {
+_check_auth() {
EXOSCALE_API_KEY="${EXOSCALE_API_KEY:-$(_readaccountconf_mutable EXOSCALE_API_KEY)}"
EXOSCALE_SECRET_KEY="${EXOSCALE_SECRET_KEY:-$(_readaccountconf_mutable EXOSCALE_SECRET_KEY)}"
-
if [ -z "$EXOSCALE_API_KEY" ] || [ -z "$EXOSCALE_SECRET_KEY" ]; then
- EXOSCALE_API_KEY=""
- EXOSCALE_SECRET_KEY=""
- _err "You don't specify Exoscale application key and application secret yet."
- _err "Please create you key and try again."
+ _err "EXOSCALE_API_KEY and EXOSCALE_SECRET_KEY must be set."
return 1
fi
-
_saveaccountconf_mutable EXOSCALE_API_KEY "$EXOSCALE_API_KEY"
_saveaccountconf_mutable EXOSCALE_SECRET_KEY "$EXOSCALE_SECRET_KEY"
-
return 0
}
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
-# _domain_token=sdjkglgdfewsdfg
-_get_root() {
-
- if ! _exoscale_rest GET "domains"; then
- return 1
- fi
-
+_get_root_domain_id() {
domain=$1
- i=2
- p=1
+ i=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 _contains "$response" "\"name\":\"$h\"" >/dev/null; then
- _domain_id=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"id\":[^,]+" | _head_n 1 | cut -d : -f 2 | tr -d \")
- _domain_token=$(echo "$response" | tr '{' "\n" | grep "\"name\":\"$h\"" | _egrep_o "\"token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
- if [ "$_domain_token" ] && [ "$_domain_id" ]; then
- _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain=$h
- return 0
+ candidate=$(printf "%s" "$domain" | cut -d . -f "${i}-100")
+ [ -z "$candidate" ] && return 1
+ _debug "Trying root domain candidate: $candidate"
+ domains=$(_exoscale_rest GET "/dns-domain")
+ # Extract from dns-domains array
+ result=$(echo "$domains" | _egrep_o '"dns-domains":\[.*\]' | _egrep_o '\{"id":"[^"]*","created-at":"[^"]*","unicode-name":"[^"]*"\}' | while read -r item; do
+ name=$(echo "$item" | _egrep_o '"unicode-name":"[^"]*"' | cut -d'"' -f4)
+ id=$(echo "$item" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4)
+ if [ "$name" = "$candidate" ]; then
+ echo "$id"
+ break
fi
- return 1
+ done)
+ if [ -n "$result" ]; then
+ echo "$result"
+ return 0
fi
- p=$i
i=$(_math "$i" + 1)
done
- return 1
}
-# returns response
+_get_sub_domain() {
+ fulldomain=$1
+ root_id=$2
+ root_info=$(_exoscale_rest GET "/dns-domain/$root_id")
+ _debug root_info "$root_info"
+ root_name=$(echo "$root_info" | _egrep_o "\"unicode-name\":\"[^\"]*\"" | cut -d\" -f4)
+ sub=${fulldomain%%."$root_name"}
+
+ if [ "$sub" = "_acme-challenge" ]; then
+ echo ""
+ else
+ # Remove _acme-challenge. prefix to get the actual subdomain
+ echo "${sub#_acme-challenge.}"
+ fi
+}
+
+_find_record_id() {
+ root_id=$1
+ name=$2
+ records=$(_exoscale_rest GET "/dns-domain/$root_id/record")
+
+ # Convert search name to lowercase for case-insensitive matching
+ name_lower=$(echo "$name" | tr '[:upper:]' '[:lower:]')
+
+ echo "$records" | _egrep_o '\{[^}]*"name":"[^"]*"[^}]*\}' | while read -r record; do
+ record_name=$(echo "$record" | _egrep_o '"name":"[^"]*"' | cut -d'"' -f4)
+ record_name_lower=$(echo "$record_name" | tr '[:upper:]' '[:lower:]')
+ if [ "$record_name_lower" = "$name_lower" ]; then
+ echo "$record" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d'"' -f4
+ break
+ fi
+ done
+}
+
+_exoscale_sign() {
+ k=$1
+ shift
+ hex_key=$(printf %b "$k" | _hex_dump | tr -d ' ')
+ printf %s "$@" | _hmac sha256 "$hex_key"
+}
+
_exoscale_rest() {
method=$1
- path="$2"
- data="$3"
- token="$4"
- request_url="$EXOSCALE_API/$path"
- _debug "$path"
+ path=$2
+ data=$3
- export _H1="Accept: application/json"
+ url="${EXOSCALE_API}${path}"
+ expiration=$(_math "$(date +%s)" + 300) # 5m from now
- if [ "$token" ]; then
- export _H2="X-DNS-Domain-Token: $token"
- else
- export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY"
- fi
+ # Build the message with the actual body or empty line
+ message=$(printf "%s %s\n%s\n\n\n%s" "$method" "/v2$path" "$data" "$expiration")
+ signature=$(_exoscale_sign "$EXOSCALE_SECRET_KEY" "$message" | _base64)
+ auth="EXO2-HMAC-SHA256 credential=${EXOSCALE_API_KEY},expires=${expiration},signature=${signature}"
+
+ _debug "API request: $method $url"
+ _debug "Signed message: [$message]"
+ _debug "Authorization header: [$auth]"
+
+ export _H1="Accept: application/json"
+ export _H2="Authorization: ${auth}"
if [ "$data" ] || [ "$method" = "DELETE" ]; then
export _H3="Content-Type: application/json"
_debug data "$data"
- response="$(_post "$data" "$request_url" "" "$method")"
+ response="$(_post "$data" "$url" "" "$method")"
else
- response="$(_get "$request_url" "" "" "$method")"
+ response="$(_get "$url" "" "" "$method")"
fi
- if [ "$?" != "0" ]; then
- _err "error $request_url"
+ # shellcheck disable=SC2181
+ if [ "$?" -ne 0 ]; then
+ _err "error $url"
return 1
fi
_debug2 response "$response"
+ echo "$response"
return 0
}
diff --git a/dnsapi/dns_gandi_livedns.sh b/dnsapi/dns_gandi_livedns.sh
index 0516fee9..aaef07bf 100644
--- a/dnsapi/dns_gandi_livedns.sh
+++ b/dnsapi/dns_gandi_livedns.sh
@@ -23,6 +23,8 @@ dns_gandi_livedns_add() {
fulldomain=$1
txtvalue=$2
+ GANDI_LIVEDNS_KEY="${GANDI_LIVEDNS_KEY:-$(_readaccountconf_mutable GANDI_LIVEDNS_KEY)}"
+ GANDI_LIVEDNS_TOKEN="${GANDI_LIVEDNS_TOKEN:-$(_readaccountconf_mutable GANDI_LIVEDNS_TOKEN)}"
if [ -z "$GANDI_LIVEDNS_KEY" ] && [ -z "$GANDI_LIVEDNS_TOKEN" ]; then
_err "No Token or API key (deprecated) specified for Gandi LiveDNS."
_err "Create your token or key and export it as GANDI_LIVEDNS_KEY or GANDI_LIVEDNS_TOKEN respectively"
@@ -31,11 +33,11 @@ dns_gandi_livedns_add() {
# Keep only one secret in configuration
if [ -n "$GANDI_LIVEDNS_TOKEN" ]; then
- _saveaccountconf GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN"
- _clearaccountconf GANDI_LIVEDNS_KEY
+ _saveaccountconf_mutable GANDI_LIVEDNS_TOKEN "$GANDI_LIVEDNS_TOKEN"
+ _clearaccountconf_mutable GANDI_LIVEDNS_KEY
elif [ -n "$GANDI_LIVEDNS_KEY" ]; then
- _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
- _clearaccountconf GANDI_LIVEDNS_TOKEN
+ _saveaccountconf_mutable GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY"
+ _clearaccountconf_mutable GANDI_LIVEDNS_TOKEN
fi
_debug "First detect the root zone"
diff --git a/dnsapi/dns_hetznercloud.sh b/dnsapi/dns_hetznercloud.sh
new file mode 100644
index 00000000..4a7eea90
--- /dev/null
+++ b/dnsapi/dns_hetznercloud.sh
@@ -0,0 +1,593 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_hetznercloud_info='Hetzner Cloud DNS
+Site: Hetzner.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hetznercloud
+Options:
+ HETZNER_TOKEN API token for the Hetzner Cloud DNS API
+Optional:
+ HETZNER_TTL Custom TTL for new TXT rrsets (default 120)
+ HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1)
+ HETZNER_MAX_ATTEMPTS Number of 1s polls to wait for async actions (default 120)
+Issues: github.com/acmesh-official/acme.sh/issues
+'
+
+HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1"
+HETZNERCLOUD_TTL_DEFAULT=120
+HETZNER_MAX_ATTEMPTS_DEFAULT=120
+
+######## Public functions #####################
+
+dns_hetznercloud_add() {
+ fulldomain="$(_idn "${1}")"
+ txtvalue="${2}"
+
+ _info "Using Hetzner Cloud DNS API to add record"
+
+ if ! _hetznercloud_init; then
+ return 1
+ fi
+
+ if ! _hetznercloud_prepare_zone "${fulldomain}"; then
+ _err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
+ return 1
+ fi
+
+ if ! _hetznercloud_get_rrset; then
+ return 1
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" = "200" ]; then
+ if _hetznercloud_rrset_contains_value "${txtvalue}"; then
+ _info "TXT record already present; nothing to do."
+ return 0
+ fi
+ elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
+ _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ add_payload="$(_hetznercloud_build_add_payload "${txtvalue}")"
+ if [ -z "${add_payload}" ]; then
+ _err "Failed to build request payload."
+ return 1
+ fi
+
+ if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_add}" "${add_payload}"; then
+ return 1
+ fi
+
+ case "${_hetznercloud_last_http_code}" in
+ 200 | 201 | 202 | 204)
+ if ! _hetznercloud_handle_action_response "TXT record add"; then
+ return 1
+ fi
+ _info "Hetzner Cloud TXT record added."
+ return 0
+ ;;
+ 401 | 403)
+ _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
+ _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ 409 | 422)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the add_records request" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ *)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS add_records request failed" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ esac
+}
+
+dns_hetznercloud_rm() {
+ fulldomain="$(_idn "${1}")"
+ txtvalue="${2}"
+
+ _info "Using Hetzner Cloud DNS API to remove record"
+
+ if ! _hetznercloud_init; then
+ return 1
+ fi
+
+ if ! _hetznercloud_prepare_zone "${fulldomain}"; then
+ _err "Unable to determine Hetzner Cloud zone for ${fulldomain}"
+ return 1
+ fi
+
+ if ! _hetznercloud_get_rrset; then
+ return 1
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" = "404" ]; then
+ _info "TXT rrset does not exist; nothing to remove."
+ return 0
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" != "200" ]; then
+ _hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ if _hetznercloud_rrset_contains_value "${txtvalue}"; then
+ remove_payload="$(_hetznercloud_build_remove_payload "${txtvalue}")"
+ if [ -z "${remove_payload}" ]; then
+ _err "Failed to build remove_records payload."
+ return 1
+ fi
+ if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_remove}" "${remove_payload}"; then
+ return 1
+ fi
+ case "${_hetznercloud_last_http_code}" in
+ 200 | 201 | 202 | 204)
+ if ! _hetznercloud_handle_action_response "TXT record remove"; then
+ return 1
+ fi
+ _info "Hetzner Cloud TXT record removed."
+ return 0
+ ;;
+ 401 | 403)
+ _err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API."
+ _hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ 404)
+ _info "TXT rrset already absent after remove action."
+ return 0
+ ;;
+ 409 | 422)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS rejected the remove_records request" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ *)
+ _hetznercloud_log_http_error "Hetzner Cloud DNS remove_records request failed" "${_hetznercloud_last_http_code}"
+ return 1
+ ;;
+ esac
+ else
+ _info "TXT value not present; nothing to remove."
+ return 0
+ fi
+}
+
+#################### Private functions ##################################
+
+_hetznercloud_init() {
+ HETZNER_TOKEN="${HETZNER_TOKEN:-$(_readaccountconf_mutable HETZNER_TOKEN)}"
+ if [ -z "${HETZNER_TOKEN}" ]; then
+ _err "The environment variable HETZNER_TOKEN must be set for the Hetzner Cloud DNS API."
+ return 1
+ fi
+ HETZNER_TOKEN=$(echo "${HETZNER_TOKEN}" | tr -d '"')
+ _saveaccountconf_mutable HETZNER_TOKEN "${HETZNER_TOKEN}"
+
+ HETZNER_API="${HETZNER_API:-$(_readaccountconf_mutable HETZNER_API)}"
+ if [ -z "${HETZNER_API}" ]; then
+ HETZNER_API="${HETZNERCLOUD_API_DEFAULT}"
+ fi
+ _saveaccountconf_mutable HETZNER_API "${HETZNER_API}"
+
+ HETZNER_TTL="${HETZNER_TTL:-$(_readaccountconf_mutable HETZNER_TTL)}"
+ if [ -z "${HETZNER_TTL}" ]; then
+ HETZNER_TTL="${HETZNERCLOUD_TTL_DEFAULT}"
+ fi
+ ttl_check=$(printf "%s" "${HETZNER_TTL}" | tr -d '0-9')
+ if [ -n "${ttl_check}" ]; then
+ _err "HETZNER_TTL must be an integer value."
+ return 1
+ fi
+ _saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}"
+
+ HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS:-$(_readaccountconf_mutable HETZNER_MAX_ATTEMPTS)}"
+ if [ -z "${HETZNER_MAX_ATTEMPTS}" ]; then
+ HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS_DEFAULT}"
+ fi
+ attempts_check=$(printf "%s" "${HETZNER_MAX_ATTEMPTS}" | tr -d '0-9')
+ if [ -n "${attempts_check}" ]; then
+ _err "HETZNER_MAX_ATTEMPTS must be an integer value."
+ return 1
+ fi
+ _saveaccountconf_mutable HETZNER_MAX_ATTEMPTS "${HETZNER_MAX_ATTEMPTS}"
+
+ return 0
+}
+
+_hetznercloud_prepare_zone() {
+ _hetznercloud_zone_id=""
+ _hetznercloud_zone_name=""
+ _hetznercloud_zone_name_lc=""
+ _hetznercloud_rr_name=""
+ _hetznercloud_rrset_path=""
+ _hetznercloud_rrset_action_add=""
+ _hetznercloud_rrset_action_remove=""
+ fulldomain_lc=$(printf "%s" "${1}" | sed 's/\.$//' | _lower_case)
+
+ i=2
+ p=1
+ while true; do
+ candidate=$(printf "%s" "${fulldomain_lc}" | cut -d . -f "${i}"-100)
+ if [ -z "${candidate}" ]; then
+ return 1
+ fi
+
+ if _hetznercloud_get_zone_by_candidate "${candidate}"; then
+ zone_name_lc="${_hetznercloud_zone_name_lc}"
+ if [ "${fulldomain_lc}" = "${zone_name_lc}" ]; then
+ _hetznercloud_rr_name="@"
+ else
+ suffix=".${zone_name_lc}"
+ if _endswith "${fulldomain_lc}" "${suffix}"; then
+ _hetznercloud_rr_name="${fulldomain_lc%"${suffix}"}"
+ else
+ _hetznercloud_rr_name="${fulldomain_lc}"
+ fi
+ fi
+ _hetznercloud_rrset_path=$(printf "%s" "${_hetznercloud_rr_name}" | _url_encode)
+ _hetznercloud_rrset_action_add="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/add_records"
+ _hetznercloud_rrset_action_remove="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/remove_records"
+ return 0
+ fi
+ p=${i}
+ i=$(_math "${i}" + 1)
+ done
+}
+
+_hetznercloud_get_zone_by_candidate() {
+ candidate="${1}"
+ zone_key=$(printf "%s" "${candidate}" | sed 's/[^A-Za-z0-9]/_/g')
+ zone_conf_key="HETZNERCLOUD_ZONE_ID_for_${zone_key}"
+
+ cached_zone_id=$(_readdomainconf "${zone_conf_key}")
+ if [ -n "${cached_zone_id}" ]; then
+ if _hetznercloud_api GET "/zones/${cached_zone_id}"; then
+ if [ "${_hetznercloud_last_http_code}" = "200" ]; then
+ zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
+ if _hetznercloud_parse_zone_fields "${zone_data}"; then
+ zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
+ if [ "${zone_name_lc}" = "${candidate}" ]; then
+ return 0
+ fi
+ fi
+ elif [ "${_hetznercloud_last_http_code}" = "404" ]; then
+ _cleardomainconf "${zone_conf_key}"
+ fi
+ else
+ return 1
+ fi
+ fi
+
+ if _hetznercloud_api GET "/zones/${candidate}"; then
+ if [ "${_hetznercloud_last_http_code}" = "200" ]; then
+ zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//')
+ if _hetznercloud_parse_zone_fields "${zone_data}"; then
+ zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case)
+ if [ "${zone_name_lc}" = "${candidate}" ]; then
+ _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
+ return 0
+ fi
+ fi
+ elif [ "${_hetznercloud_last_http_code}" != "404" ]; then
+ _hetznercloud_log_http_error "Hetzner Cloud zone lookup failed" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+ else
+ return 1
+ fi
+
+ encoded_candidate=$(printf "%s" "${candidate}" | _url_encode)
+ if ! _hetznercloud_api GET "/zones?name=${encoded_candidate}"; then
+ return 1
+ fi
+ if [ "${_hetznercloud_last_http_code}" != "200" ]; then
+ if [ "${_hetznercloud_last_http_code}" = "404" ]; then
+ return 1
+ fi
+ _hetznercloud_log_http_error "Hetzner Cloud zone search failed" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ zone_data=$(_hetznercloud_extract_zone_from_list "${response}" "${candidate}")
+ if [ -z "${zone_data}" ]; then
+ return 1
+ fi
+ if ! _hetznercloud_parse_zone_fields "${zone_data}"; then
+ return 1
+ fi
+ _savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}"
+ return 0
+}
+
+_hetznercloud_parse_zone_fields() {
+ zone_json="${1}"
+ if [ -z "${zone_json}" ]; then
+ return 1
+ fi
+ normalized=$(printf "%s" "${zone_json}" | _normalizeJson)
+ zone_id=$(printf "%s" "${normalized}" | _egrep_o '"id":[^,}]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "')
+ zone_name=$(printf "%s" "${normalized}" | _egrep_o '"name":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then
+ return 1
+ fi
+ zone_name_trimmed=$(printf "%s" "${zone_name}" | sed 's/\.$//')
+ if zone_name_ascii=$(_idn "${zone_name_trimmed}"); then
+ zone_name="${zone_name_ascii}"
+ else
+ zone_name="${zone_name_trimmed}"
+ fi
+ _hetznercloud_zone_id="${zone_id}"
+ _hetznercloud_zone_name="${zone_name}"
+ _hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case)
+ return 0
+}
+
+_hetznercloud_extract_zone_from_list() {
+ list_response=$(printf "%s" "${1}" | _normalizeJson)
+ candidate="${2}"
+ escaped_candidate=$(_hetznercloud_escape_regex "${candidate}")
+ printf "%s" "${list_response}" | _egrep_o "{[^{}]*\"name\":\"${escaped_candidate}\"[^{}]*}" | _head_n 1
+}
+
+_hetznercloud_escape_regex() {
+ printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/\./\\./g' | sed 's/-/\\-/g'
+}
+
+_hetznercloud_get_rrset() {
+ if [ -z "${_hetznercloud_zone_id}" ] || [ -z "${_hetznercloud_rrset_path}" ]; then
+ return 1
+ fi
+ if ! _hetznercloud_api GET "/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT"; then
+ return 1
+ fi
+ return 0
+}
+
+_hetznercloud_rrset_contains_value() {
+ wanted_value="${1}"
+ normalized=$(printf "%s" "${response}" | _normalizeJson)
+ escaped_value=$(_hetznercloud_escape_value "${wanted_value}")
+ search_pattern="\"value\":\"\\\\\"${escaped_value}\\\\\"\""
+ if _contains "${normalized}" "${search_pattern}"; then
+ return 0
+ fi
+ return 1
+}
+
+_hetznercloud_build_add_payload() {
+ value="${1}"
+ escaped_value=$(_hetznercloud_escape_value "${value}")
+ printf '{"ttl":%s,"records":[{"value":"\\"%s\\""}]}' "${HETZNER_TTL}" "${escaped_value}"
+}
+
+_hetznercloud_build_remove_payload() {
+ value="${1}"
+ escaped_value=$(_hetznercloud_escape_value "${value}")
+ printf '{"records":[{"value":"\\"%s\\""}]}' "${escaped_value}"
+}
+
+_hetznercloud_escape_value() {
+ printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g'
+}
+
+_hetznercloud_error_message() {
+ if [ -z "${response}" ]; then
+ return 1
+ fi
+ message=$(printf "%s" "${response}" | _normalizeJson | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${message}" ]; then
+ printf "%s" "${message}"
+ return 0
+ fi
+ return 1
+}
+
+_hetznercloud_log_http_error() {
+ context="${1}"
+ code="${2}"
+ message="$(_hetznercloud_error_message)"
+ if [ -n "${context}" ]; then
+ if [ -n "${message}" ]; then
+ _err "${context} (HTTP ${code}): ${message}"
+ else
+ _err "${context} (HTTP ${code})"
+ fi
+ else
+ if [ -n "${message}" ]; then
+ _err "Hetzner Cloud DNS API error (HTTP ${code}): ${message}"
+ else
+ _err "Hetzner Cloud DNS API error (HTTP ${code})"
+ fi
+ fi
+}
+
+_hetznercloud_api() {
+ method="${1}"
+ ep="${2}"
+ data="${3}"
+ retried="${4}"
+
+ if [ -z "${method}" ]; then
+ method="GET"
+ fi
+
+ if ! _startswith "${ep}" "/"; then
+ ep="/${ep}"
+ fi
+ url="${HETZNER_API}${ep}"
+
+ export _H1="Authorization: Bearer ${HETZNER_TOKEN}"
+ export _H2="Accept: application/json"
+ export _H3=""
+ export _H4=""
+ export _H5=""
+
+ : >"${HTTP_HEADER}"
+
+ if [ "${method}" = "GET" ]; then
+ response="$(_get "${url}")"
+ else
+ if [ -z "${data}" ]; then
+ data="{}"
+ fi
+ response="$(_post "${data}" "${url}" "" "${method}" "application/json")"
+ fi
+ ret="${?}"
+
+ _hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
+
+ if [ "${ret}" != "0" ]; then
+ return 1
+ fi
+
+ if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then
+ retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r')
+ if [ -z "${retry_after}" ]; then
+ retry_after=1
+ fi
+ _info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds."
+ _sleep "${retry_after}"
+ if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then
+ return 1
+ fi
+ return 0
+ fi
+
+ return 0
+}
+
+_hetznercloud_handle_action_response() {
+ context="${1}"
+ if [ -z "${response}" ]; then
+ return 0
+ fi
+
+ normalized=$(printf "%s" "${response}" | _normalizeJson)
+
+ failed_message=""
+ if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then
+ if [ -n "${failed_message}" ]; then
+ _err "Hetzner Cloud DNS ${context} failed: ${failed_message}"
+ else
+ _err "Hetzner Cloud DNS ${context} failed."
+ fi
+ return 1
+ fi
+
+ action_ids=""
+ if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then
+ for action_id in ${action_ids}; do
+ if [ -z "${action_id}" ]; then
+ continue
+ fi
+ if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then
+ return 1
+ fi
+ done
+ fi
+
+ return 0
+}
+
+_hetznercloud_extract_failed_action_message() {
+ normalized="${1}"
+ failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]')
+ if [ -z "${failed_section}" ]; then
+ return 1
+ fi
+ if _contains "${failed_section}" '"failed_actions":[]'; then
+ return 1
+ fi
+ message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${message}" ]; then
+ printf "%s" "${message}"
+ else
+ printf "%s" "${failed_section}"
+ fi
+ return 0
+}
+
+_hetznercloud_extract_action_ids() {
+ normalized="${1}"
+ actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]')
+ if [ -z "${actions_section}" ]; then
+ return 1
+ fi
+ action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ')
+ action_ids=$(printf "%s" "${action_ids}" | tr -s ' ')
+ action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//')
+ if [ -z "${action_ids}" ]; then
+ return 1
+ fi
+ printf "%s" "${action_ids}"
+ return 0
+}
+
+_hetznercloud_wait_for_action() {
+ action_id="${1}"
+ context="${2}"
+ attempts="0"
+
+ while true; do
+ if ! _hetznercloud_api GET "/actions/${action_id}"; then
+ return 1
+ fi
+ if [ "${_hetznercloud_last_http_code}" != "200" ]; then
+ _hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}"
+ return 1
+ fi
+
+ normalized=$(printf "%s" "${response}" | _normalizeJson)
+ action_status=$(_hetznercloud_action_status_from_normalized "${normalized}")
+
+ if [ -z "${action_status}" ]; then
+ _err "Hetzner Cloud DNS ${context} action ${action_id} returned no status."
+ return 1
+ fi
+
+ if [ "${action_status}" = "success" ]; then
+ return 0
+ fi
+
+ if [ "${action_status}" = "error" ]; then
+ if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then
+ _err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}"
+ else
+ _err "Hetzner Cloud DNS ${context} action ${action_id} failed."
+ fi
+ return 1
+ fi
+
+ attempts=$(_math "${attempts}" + 1)
+ if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then
+ _err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts."
+ return 1
+ fi
+
+ _sleep 1
+ done
+}
+
+_hetznercloud_action_status_from_normalized() {
+ normalized="${1}"
+ status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ printf "%s" "${status}"
+}
+
+_hetznercloud_action_error_from_normalized() {
+ normalized="${1}"
+ error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}')
+ if [ -z "${error_section}" ]; then
+ return 1
+ fi
+ message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${message}" ]; then
+ printf "%s" "${message}"
+ return 0
+ fi
+ code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"')
+ if [ -n "${code}" ]; then
+ printf "%s" "${code}"
+ return 0
+ fi
+ return 1
+}
diff --git a/dnsapi/dns_hostup.sh b/dnsapi/dns_hostup.sh
new file mode 100644
index 00000000..b3211069
--- /dev/null
+++ b/dnsapi/dns_hostup.sh
@@ -0,0 +1,501 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034,SC2154
+
+dns_hostup_info='HostUp DNS
+Site: hostup.se
+Docs: https://developer.hostup.se/
+Options:
+ HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes.
+ HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api).
+ HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds).
+ HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection).
+Author: HostUp (https://cloud.hostup.se/contact/en)
+'
+
+HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api"
+HOSTUP_DEFAULT_TTL=60
+
+# Public: add TXT record
+# Usage: dns_hostup_add _acme-challenge.example.com "txt-value"
+dns_hostup_add() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using HostUp DNS API"
+
+ if ! _hostup_init; then
+ return 1
+ fi
+
+ if ! _hostup_detect_zone "$fulldomain"; then
+ _err "Unable to determine HostUp zone for $fulldomain"
+ return 1
+ fi
+
+ record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")"
+ record_name="$(_hostup_sanitize_name "$record_name")"
+ record_value="$(_hostup_json_escape "$txtvalue")"
+
+ ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}"
+
+ _debug "zone_id" "$HOSTUP_ZONE_ID"
+ _debug "zone_domain" "$HOSTUP_ZONE_DOMAIN"
+ _debug "record_name" "$record_name"
+ _debug "ttl" "$ttl"
+
+ request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}"
+
+ if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then
+ return 1
+ fi
+
+ if ! _contains "$_hostup_response" '"success":true'; then
+ _err "HostUp DNS API: failed to create TXT record for $fulldomain"
+ _debug2 "_hostup_response" "$_hostup_response"
+ return 1
+ fi
+
+ record_id="$(_hostup_extract_record_id "$_hostup_response")"
+ if [ -n "$record_id" ]; then
+ _hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id"
+ _debug "hostup_saved_record_id" "$record_id"
+ fi
+
+ _info "Added TXT record for $fulldomain"
+ return 0
+}
+
+# Public: remove TXT record
+# Usage: dns_hostup_rm _acme-challenge.example.com "txt-value"
+dns_hostup_rm() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using HostUp DNS API"
+
+ if ! _hostup_init; then
+ return 1
+ fi
+
+ if ! _hostup_detect_zone "$fulldomain"; then
+ _err "Unable to determine HostUp zone for $fulldomain"
+ return 1
+ fi
+
+ record_name_fqdn="$(_hostup_fqdn "$fulldomain")"
+ record_value="$txtvalue"
+
+ record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")"
+ if [ -n "$record_id_cached" ]; then
+ _debug "hostup_record_id_cached" "$record_id_cached"
+ if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then
+ _info "Deleted TXT record $record_id_cached"
+ _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
+ HOSTUP_ZONE_ID=""
+ return 0
+ fi
+ fi
+
+ if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then
+ _info "TXT record not found for $record_name_fqdn. Skipping removal."
+ _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
+ return 0
+ fi
+
+ _debug "Deleting record" "$HOSTUP_RECORD_ID"
+
+ if ! _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$HOSTUP_RECORD_ID"; then
+ return 1
+ fi
+
+ _info "Deleted TXT record $HOSTUP_RECORD_ID"
+ _hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain"
+ HOSTUP_ZONE_ID=""
+ return 0
+}
+
+##########################
+# Private helper methods #
+##########################
+
+_hostup_init() {
+ HOSTUP_API_KEY="${HOSTUP_API_KEY:-$(_readaccountconf_mutable HOSTUP_API_KEY)}"
+ HOSTUP_API_BASE="${HOSTUP_API_BASE:-$(_readaccountconf_mutable HOSTUP_API_BASE)}"
+ HOSTUP_TTL="${HOSTUP_TTL:-$(_readaccountconf_mutable HOSTUP_TTL)}"
+ HOSTUP_ZONE_ID="${HOSTUP_ZONE_ID:-$(_readaccountconf_mutable HOSTUP_ZONE_ID)}"
+
+ if [ -z "$HOSTUP_API_BASE" ]; then
+ HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT"
+ fi
+
+ if [ -z "$HOSTUP_API_KEY" ]; then
+ HOSTUP_API_KEY=""
+ _err "HOSTUP_API_KEY is not set."
+ _err "Please export your HostUp API key with read:dns and write:dns scopes."
+ return 1
+ fi
+
+ _saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY"
+ _saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE"
+
+ if [ -n "$HOSTUP_TTL" ]; then
+ _saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL"
+ fi
+
+ if [ -n "$HOSTUP_ZONE_ID" ]; then
+ _saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID"
+ fi
+
+ return 0
+}
+
+_hostup_detect_zone() {
+ fulldomain="$1"
+
+ if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then
+ return 0
+ fi
+
+ HOSTUP_ZONE_DOMAIN=""
+ _debug "hostup_full_domain" "$fulldomain"
+
+ if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then
+ # Attempt to fetch domain name for provided zone ID
+ if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then
+ return 0
+ fi
+ HOSTUP_ZONE_ID=""
+ fi
+
+ if ! _hostup_load_zones; then
+ return 1
+ fi
+
+ _domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)"
+ _debug "hostup_initial_candidate" "$_domain_candidate"
+
+ while [ -n "$_domain_candidate" ]; do
+ _debug "hostup_zone_candidate" "$_domain_candidate"
+ if _hostup_lookup_zone "$_domain_candidate"; then
+ HOSTUP_ZONE_DOMAIN="$_lookup_zone_domain"
+ HOSTUP_ZONE_ID="$_lookup_zone_id"
+ return 0
+ fi
+
+ case "$_domain_candidate" in
+ *.*) ;;
+ *) break ;;
+ esac
+
+ _domain_candidate="${_domain_candidate#*.}"
+ done
+
+ HOSTUP_ZONE_ID=""
+ return 1
+}
+
+_hostup_record_name() {
+ fulldomain="$1"
+ zonedomain="$2"
+
+ # Remove trailing dot, if any
+ fulldomain="${fulldomain%.}"
+ zonedomain="${zonedomain%.}"
+
+ if [ "$fulldomain" = "$zonedomain" ]; then
+ printf "%s" "@"
+ return 0
+ fi
+
+ suffix=".$zonedomain"
+ case "$fulldomain" in
+ *"$suffix")
+ printf "%s" "${fulldomain%"$suffix"}"
+ ;;
+ *)
+ # Domain not within zone, fall back to full host
+ printf "%s" "$fulldomain"
+ ;;
+ esac
+}
+
+_hostup_sanitize_name() {
+ name="$1"
+
+ if [ -z "$name" ] || [ "$name" = "." ]; then
+ printf "%s" "@"
+ return 0
+ fi
+
+ # Remove any trailing dot
+ name="${name%.}"
+ printf "%s" "$name"
+}
+
+_hostup_fqdn() {
+ domain="$1"
+ printf "%s" "${domain%.}"
+}
+
+_hostup_fetch_zone_details() {
+ zone_id="$1"
+
+ if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
+ return 1
+ fi
+
+ zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')"
+ if [ -n "$zonedomain" ]; then
+ HOSTUP_ZONE_DOMAIN="$zonedomain"
+ return 0
+ fi
+
+ return 1
+}
+
+_hostup_load_zones() {
+ if ! _hostup_rest "GET" "/dns/zones" ""; then
+ return 1
+ fi
+
+ HOSTUP_ZONES_CACHE=""
+ data="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
+
+ while IFS= read -r line; do
+ case "$line" in
+ *'"domain_id"'*'"domain"'*)
+ zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")"
+ zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")"
+ if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then
+ HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id}
+"
+ _debug "hostup_zone_loaded" "$zone_domain|$zone_id"
+ fi
+ ;;
+ esac
+ done <%s
+
+ content
+
+ %s
+
+
- ' "$_domain" "$_sub_domain")
+ ' "$_domain" "$_sub_domain" "$txtvalue")
response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
if ! _contains "$response" "Command completed successfully"; then
@@ -125,7 +132,7 @@ dns_inwx_rm() {
if ! printf "%s" "$response" | grep "count" >/dev/null; then
_info "Do not need to delete record"
else
- _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+')
+ _record_id=$(printf '%s' "$response" | _egrep_o '.*(record){1}(.*)([0-9]+){1}' | _egrep_o 'id<\/name>[0-9]+' | _egrep_o '[0-9]+')
_info "Deleting record"
_inwx_delete_record "$_record_id"
fi
@@ -324,7 +331,7 @@ _inwx_delete_record() {
id
- %s
+ %s
@@ -362,7 +369,7 @@ _inwx_update_record() {
id
- %s
+ %s
diff --git a/dnsapi/dns_mgwm.sh b/dnsapi/dns_mgwm.sh
new file mode 100644
index 00000000..57679127
--- /dev/null
+++ b/dnsapi/dns_mgwm.sh
@@ -0,0 +1,109 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_mgwm_info='mgw-media.de
+Site: mgw-media.de
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mgwm
+Options:
+ MGWM_CUSTOMER Your customer number
+ MGWM_API_HASH Your API Hash
+Issues: github.com/acmesh-official/acme.sh/issues/6669
+'
+# Base URL for the mgw-media.de API
+MGWM_API_BASE="https://api.mgw-media.de/record"
+
+######## Public functions #####################
+
+# This function is called by acme.sh to add a TXT record.
+dns_mgwm_add() {
+ fulldomain=$1
+ txtvalue=$2
+ _info "Using mgw-media.de DNS API for domain $fulldomain (add record)"
+ _debug "fulldomain: $fulldomain"
+ _debug "txtvalue: $txtvalue"
+
+ # Call the new private function to handle the API request.
+ # The 'add' action, fulldomain, type 'txt' and txtvalue are passed.
+ if _mgwm_request "add" "$fulldomain" "txt" "$txtvalue"; then
+ _info "TXT record for $fulldomain successfully added via mgw-media.de API."
+ _sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks.
+ return 0
+ else
+ # Error message already logged by _mgwm_request, but a specific one here helps.
+ _err "mgwm_add: Failed to add TXT record for $fulldomain."
+ return 1
+ fi
+}
+# This function is called by acme.sh to remove a TXT record after validation.
+dns_mgwm_rm() {
+ fulldomain=$1
+ txtvalue=$2 # This txtvalue is now used to identify the specific record to be removed.
+ _info "Removing TXT record for $fulldomain using mgw-media.de DNS API (remove record)"
+ _debug "fulldomain: $fulldomain"
+ _debug "txtvalue: $txtvalue"
+
+ # Call the new private function to handle the API request.
+ # The 'rm' action, fulldomain, type 'txt' and txtvalue are passed.
+ if _mgwm_request "rm" "$fulldomain" "txt" "$txtvalue"; then
+ _info "TXT record for $fulldomain successfully removed via mgw-media.de API."
+ return 0
+ else
+ # Error message already logged by _mgwm_request, but a specific one here helps.
+ _err "mgwm_rm: Failed to remove TXT record for $fulldomain."
+ return 1
+ fi
+}
+#################### Private functions below ##################################
+
+# _mgwm_request() encapsulates the API call logic, including
+# loading credentials, setting the Authorization header, and executing the request.
+# Arguments:
+# $1: action (e.g., "add", "rm")
+# $2: fulldomain
+# $3: type (e.g., "txt")
+# $4: content (the txtvalue)
+_mgwm_request() {
+ _action="$1"
+ _fulldomain="$2"
+ _type="$3"
+ _content="$4"
+
+ _debug "Calling _mgwm_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content"
+
+ # Load credentials from environment or acme.sh config
+ MGWM_CUSTOMER="${MGWM_CUSTOMER:-$(_readaccountconf_mutable MGWM_CUSTOMER)}"
+ MGWM_API_HASH="${MGWM_API_HASH:-$(_readaccountconf_mutable MGWM_API_HASH)}"
+
+ # Check if credentials are set
+ if [ -z "$MGWM_CUSTOMER" ] || [ -z "$MGWM_API_HASH" ]; then
+ _err "You didn't specify one or more of MGWM_CUSTOMER or MGWM_API_HASH."
+ _err "Please check these environment variables and try again."
+ return 1
+ fi
+
+ # Save credentials for automatic renewal and future calls
+ _saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER"
+ _saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH"
+
+ # Create the Basic Auth Header. acme.sh's _base64 function is used for encoding.
+ _credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)"
+ export _H1="Authorization: Basic $_credentials"
+ _debug "Set Authorization Header: Basic " # Log debug message without sensitive credentials
+
+ # Construct the API URL based on the action and provided parameters.
+ _request_url="${MGWM_API_BASE}/${_action}/${_fulldomain}/${_type}/${_content}"
+ _debug "Constructed mgw-media.de API URL for action '$_action': ${_request_url}"
+
+ # Execute the HTTP GET request with the Authorization Header.
+ # The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization.
+ response="$(_get "$_request_url")"
+ _debug "mgw-media.de API response for action '$_action': $response"
+
+ # Check the API response for success. The API returns "OK" on success.
+ if [ "$response" = "OK" ]; then
+ _info "mgw-media.de API action '$_action' for record '$_fulldomain' successful."
+ return 0
+ else
+ _err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'"
+ return 1
+ fi
+}
diff --git a/dnsapi/dns_nanelo.sh b/dnsapi/dns_nanelo.sh
index 1ab47a89..0c42989b 100644
--- a/dnsapi/dns_nanelo.sh
+++ b/dnsapi/dns_nanelo.sh
@@ -27,8 +27,16 @@ dns_nanelo_add() {
fi
_saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN"
+ _debug "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
_info "Adding TXT record to ${fulldomain}"
- response="$(_get "$NANELO_API$NANELO_TOKEN/dns/addrecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")"
+ response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/addrecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")"
if _contains "${response}" 'success'; then
return 0
fi
@@ -51,8 +59,16 @@ dns_nanelo_rm() {
fi
_saveaccountconf_mutable NANELO_TOKEN "$NANELO_TOKEN"
+ _debug "First, let's detect the root zone:"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
_info "Deleting resource record $fulldomain"
- response="$(_get "$NANELO_API$NANELO_TOKEN/dns/deleterecord?type=TXT&ttl=60&name=${fulldomain}&value=${txtvalue}")"
+ response="$(_post "" "$NANELO_API$NANELO_TOKEN/dns/deleterecord?domain=${_domain}&type=TXT&ttl=60&name=${_sub_domain}&value=${txtvalue}" "" "" "")"
if _contains "${response}" 'success'; then
return 0
fi
@@ -60,3 +76,45 @@ dns_nanelo_rm() {
_err "${response}"
return 1
}
+
+#################### Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+
+_get_root() {
+ fulldomain=$1
+
+ # Fetch all zones from Nanelo
+ response="$(_get "$NANELO_API$NANELO_TOKEN/dns/getzones")" || return 1
+
+ # Extract "zones" array into space-separated list
+ zones=$(echo "$response" |
+ tr -d ' \n' |
+ sed -n 's/.*"zones":\[\([^]]*\)\].*/\1/p' |
+ tr -d '"' |
+ tr , ' ')
+ _debug zones "$zones"
+
+ bestzone=""
+ for z in $zones; do
+ case "$fulldomain" in
+ *."$z" | "$z")
+ if [ ${#z} -gt ${#bestzone} ]; then
+ bestzone=$z
+ fi
+ ;;
+ esac
+ done
+
+ if [ -z "$bestzone" ]; then
+ _err "No matching zone found for $fulldomain"
+ return 1
+ fi
+
+ _domain="$bestzone"
+ _sub_domain=$(printf "%s" "$fulldomain" | sed "s/\\.$_domain\$//")
+
+ return 0
+}
diff --git a/dnsapi/dns_omglol.sh b/dnsapi/dns_omglol.sh
index df080bcf..fd38d046 100644
--- a/dnsapi/dns_omglol.sh
+++ b/dnsapi/dns_omglol.sh
@@ -4,8 +4,8 @@ dns_omglol_info='omg.lol
Site: omg.lol
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_omglol
Options:
- OMG_ApiKey API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
- OMG_Address Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
+ OMG_ApiKey - API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
+ OMG_Address - Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
Issues: github.com/acmesh-official/acme.sh/issues/5299
Author: @Kholin
'
@@ -35,7 +35,7 @@ dns_omglol_add() {
_debug "omg.lol Address" "$OMG_Address"
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
return 1
fi
@@ -67,7 +67,7 @@ dns_omglol_rm() {
_debug "omg.lol Address" "$OMG_Address"
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
return 1
fi
@@ -100,18 +100,49 @@ omg_validate() {
fi
_endswith "$fulldomain" "omg.lol"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
_err "Domain name requested is not under omg.lol"
return 1
fi
_endswith "$fulldomain" "$omg_address.omg.lol"
- if [ ! $? ]; then
+ if [ 1 = $? ]; then
_err "Domain name is not a subdomain of provided omg.lol address $omg_address"
return 1
fi
- _debug "Required environment parameters are all present"
+ omg_testconnect "$omg_apikey" "$omg_address"
+ if [ 1 = $? ]; then
+ _err "Authentication to omg.lol for address $omg_address using provided API key failed"
+ return 1
+ fi
+
+ _debug "Required environment parameters are all present and validated"
+}
+
+# Validate that the address and API key are both correct and associated to each other
+omg_testconnect() {
+ omg_apikey=$1
+ omg_address=$2
+
+ _debug2 "Function" "omg_testconnect"
+ _secure_debug2 "omg.lol API key" "$omg_apikey"
+ _debug2 "omg.lol Address" "$omg_address"
+
+ authheader="$(_createAuthHeader "$omg_apikey")"
+ export _H1="$authheader"
+ endpoint="https://api.omg.lol/address/$omg_address/info"
+ _debug2 "Endpoint for validation" "$endpoint"
+
+ response=$(_get "$endpoint" "" 30)
+
+ _jsonResponseCheck "$response" "status_code" 200
+ if [ 1 = $? ]; then
+ _debug2 "Failed to query omg.lol for $omg_address with provided API key"
+ _secure_debug2 "API Key" "omg_apikey"
+ _secure_debug3 "Raw response" "$response"
+ return 1
+ fi
}
# Add (or modify) an entry for a new ACME query
diff --git a/dnsapi/dns_sotoon.sh b/dnsapi/dns_sotoon.sh
new file mode 100644
index 00000000..4a0fc034
--- /dev/null
+++ b/dnsapi/dns_sotoon.sh
@@ -0,0 +1,319 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_sotoon_info='Sotoon.ir
+Site: Sotoon.ir
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon
+Options:
+ Sotoon_Token API Token
+ Sotoon_WorkspaceUUID Workspace UUID
+ Sotoon_WorkspaceName Workspace Name
+Issues: github.com/acmesh-official/acme.sh/issues/6656
+Author: Erfan Gholizade
+'
+
+SOTOON_API_URL="https://api.sotoon.ir/delivery/v2/global"
+
+######## Public functions #####################
+
+#Adding the txt record for validation.
+#Usage: dns_sotoon_add fulldomain TXT_record
+#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_sotoon_add() {
+ fulldomain=$1
+ txtvalue=$2
+ _info_sotoon "Using Sotoon"
+
+ Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
+ Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
+ Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}"
+
+ if [ -z "$Sotoon_Token" ]; then
+ _err_sotoon "You didn't specify \"Sotoon_Token\" token yet."
+ _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens"
+ return 1
+ fi
+ if [ -z "$Sotoon_WorkspaceUUID" ]; then
+ _err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet."
+ _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces"
+ return 1
+ fi
+ if [ -z "$Sotoon_WorkspaceName" ]; then
+ _err_sotoon "You didn't specify \"Sotoon_WorkspaceName\" Workspace Name yet."
+ _err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces"
+ return 1
+ fi
+
+ #save the info to the account conf file.
+ _saveaccountconf_mutable Sotoon_Token "$Sotoon_Token"
+ _saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID"
+ _saveaccountconf_mutable Sotoon_WorkspaceName "$Sotoon_WorkspaceName"
+
+ _debug_sotoon "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err_sotoon "invalid domain"
+ return 1
+ fi
+
+ _info_sotoon "Adding record"
+
+ _debug_sotoon _domain_id "$_domain_id"
+ _debug_sotoon _sub_domain "$_sub_domain"
+ _debug_sotoon _domain "$_domain"
+
+ # First, GET the current domain zone to check for existing TXT records
+ # This is needed for wildcard certs which require multiple TXT values
+ _info_sotoon "Checking for existing TXT records"
+ if ! _sotoon_rest GET "$_domain_id"; then
+ _err_sotoon "Failed to get domain zone"
+ return 1
+ fi
+
+ # Check if there are existing TXT records for this subdomain
+ _existing_txt=""
+ if _contains "$response" "\"$_sub_domain\""; then
+ _debug_sotoon "Found existing records for $_sub_domain"
+ # Extract existing TXT values from the response
+ # The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}]
+ _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
+ _debug_sotoon "Existing TXT records: $_existing_txt"
+ fi
+
+ # Build the new record entry
+ _new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}"
+
+ # If there are existing records, append to them; otherwise create new array
+ if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then
+ # Check if this exact TXT value already exists (avoid duplicates)
+ if _contains "$_existing_txt" "\"$txtvalue\""; then
+ _info_sotoon "TXT record already exists, skipping"
+ return 0
+ fi
+ # Remove the closing bracket and append new record
+ _combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]"
+ _debug_sotoon "Combined records: $_combined_records"
+ else
+ # No existing records, create new array
+ _combined_records="[$_new_record]"
+ fi
+
+ # Prepare the DNS record data in Kubernetes CRD format
+ _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}"
+
+ _debug_sotoon "DNS record payload: $_dns_record"
+
+ # Use PATCH to update/add the record to the domain zone
+ _info_sotoon "Updating domain zone $_domain_id with TXT record"
+ if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
+ if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then
+ _info_sotoon "Added, OK"
+ return 0
+ else
+ _debug_sotoon "Response: $response"
+ _err_sotoon "Add txt record error."
+ return 1
+ fi
+ fi
+
+ _err_sotoon "Add txt record error."
+ return 1
+}
+
+#Remove the txt record after validation.
+#Usage: dns_sotoon_rm fulldomain TXT_record
+#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_sotoon_rm() {
+ fulldomain=$1
+ txtvalue=$2
+ _info_sotoon "Using Sotoon"
+ _debug_sotoon fulldomain "$fulldomain"
+ _debug_sotoon txtvalue "$txtvalue"
+
+ Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}"
+ Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}"
+ Sotoon_WorkspaceName="${Sotoon_WorkspaceName:-$(_readaccountconf_mutable Sotoon_WorkspaceName)}"
+
+ _debug_sotoon "First detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err_sotoon "invalid domain"
+ return 1
+ fi
+ _debug_sotoon _domain_id "$_domain_id"
+ _debug_sotoon _sub_domain "$_sub_domain"
+ _debug_sotoon _domain "$_domain"
+
+ _info_sotoon "Removing TXT record"
+
+ # First, GET the current domain zone to check for existing TXT records
+ if ! _sotoon_rest GET "$_domain_id"; then
+ _err_sotoon "Failed to get domain zone"
+ return 1
+ fi
+
+ # Check if there are existing TXT records for this subdomain
+ _existing_txt=""
+ if _contains "$response" "\"$_sub_domain\""; then
+ _debug_sotoon "Found existing records for $_sub_domain"
+ _existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://")
+ _debug_sotoon "Existing TXT records: $_existing_txt"
+ fi
+
+ # If no existing records, nothing to remove
+ if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then
+ _info_sotoon "No TXT records found, nothing to remove"
+ return 0
+ fi
+
+ # Remove the specific TXT value from the array
+ # This handles the case where there are multiple TXT values (wildcard certs)
+ _remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g')
+ _debug_sotoon "Remaining records after removal: $_remaining_records"
+
+ # If no records remain, set to null to remove the subdomain entirely
+ if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then
+ _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}"
+ else
+ _dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}"
+ fi
+
+ _debug_sotoon "Remove record payload: $_dns_record"
+
+ # Use PATCH to remove the record from the domain zone
+ if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then
+ _info_sotoon "Record removed, OK"
+ return 0
+ else
+ _debug_sotoon "Response: $response"
+ _err_sotoon "Error removing record"
+ return 1
+ fi
+}
+
+#################### Private functions below ##################################
+
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ _debug_sotoon "Getting root domain for: $domain"
+ _debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID"
+ _debug_sotoon "Sotoon WorkspaceName: $Sotoon_WorkspaceName"
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug_sotoon "Checking domain part: $h"
+
+ if [ -z "$h" ]; then
+ #not valid
+ _err_sotoon "Could not find valid domain"
+ return 1
+ fi
+
+ _debug_sotoon "Fetching domain zones from Sotoon API"
+ if ! _sotoon_rest GET ""; then
+ _err_sotoon "Failed to get domain zones from Sotoon API"
+ _err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID, and Sotoon_WorkspaceName"
+ return 1
+ fi
+
+ _debug2_sotoon "API Response: $response"
+
+ # Check if the response contains our domain
+ # Sotoon API uses Kubernetes CRD format with spec.origin for domain matching
+ if _contains "$response" "\"origin\":\"$h\""; then
+ _debug_sotoon "Found domain by origin: $h"
+
+ # In Kubernetes CRD format, the metadata.name is the resource identifier
+ # The name can be either:
+ # 1. Same as origin
+ # 2. Origin with dots replaced by hyphens
+ # We check both patterns in the response to determine which one exists
+
+ # Convert origin to hyphenated version for checking
+ _h_hyphenated=$(echo "$h" | tr '.' '-')
+
+ # Check if the hyphenated name exists in the response
+ if _contains "$response" "\"name\":\"$_h_hyphenated\""; then
+ _domain_id="$_h_hyphenated"
+ _debug_sotoon "Found domain ID (hyphenated): $_domain_id"
+ # Check if the origin itself is used as name
+ elif _contains "$response" "\"name\":\"$h\""; then
+ _domain_id="$h"
+ _debug_sotoon "Found domain ID (same as origin): $_domain_id"
+ else
+ # Fallback: use the hyphenated version (more common)
+ _domain_id="$_h_hyphenated"
+ _debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id"
+ fi
+
+ if [ -n "$_domain_id" ]; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ _debug_sotoon "Domain ID (metadata.name): $_domain_id"
+ _debug_sotoon "Sub domain: $_sub_domain"
+ _debug_sotoon "Domain (origin): $_domain"
+ return 0
+ fi
+ _err_sotoon "Found domain $h but could not extract domain ID"
+ return 1
+ fi
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+ return 1
+}
+
+_sotoon_rest() {
+ mtd="$1"
+ resource_id="$2"
+ data="$3"
+
+ token_trimmed=$(echo "$Sotoon_Token" | tr -d '"')
+
+ # Construct the API endpoint
+ _api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/namespaces/$Sotoon_WorkspaceName/domainzones"
+
+ if [ -n "$resource_id" ]; then
+ _api_path="$_api_path/$resource_id"
+ fi
+
+ _debug_sotoon "API Path: $_api_path"
+ _debug_sotoon "Method: $mtd"
+
+ # Set authorization header - Sotoon API uses Bearer token
+ export _H1="Authorization: Bearer $token_trimmed"
+
+ if [ "$mtd" = "GET" ]; then
+ # GET request
+ _debug_sotoon "GET" "$_api_path"
+ response="$(_get "$_api_path")"
+ elif [ "$mtd" = "PATCH" ]; then
+ # PATCH Request
+ export _H2="Content-Type: application/merge-patch+json"
+ _debug_sotoon data "$data"
+ response="$(_post "$data" "$_api_path" "" "$mtd")"
+ else
+ _err_sotoon "Unknown method: $mtd"
+ return 1
+ fi
+
+ _debug2_sotoon response "$response"
+ return 0
+}
+
+#Wrappers for logging
+_info_sotoon() {
+ _info "[Sotoon]" "$@"
+}
+
+_err_sotoon() {
+ _err "[Sotoon]" "$@"
+}
+
+_debug_sotoon() {
+ _debug "[Sotoon]" "$@"
+}
+
+_debug2_sotoon() {
+ _debug2 "[Sotoon]" "$@"
+}
diff --git a/notify/ntfy.sh b/notify/ntfy.sh
index 21e39559..3a788a84 100644
--- a/notify/ntfy.sh
+++ b/notify/ntfy.sh
@@ -14,6 +14,13 @@ ntfy_send() {
_debug "_content" "$_content"
_debug "_statusCode" "$_statusCode"
+ _priority_default="default"
+ _priority_error="high"
+
+ _tag_success="white_check_mark"
+ _tag_error="warning"
+ _tag_info="information_source"
+
NTFY_URL="${NTFY_URL:-$(_readaccountconf_mutable NTFY_URL)}"
if [ "$NTFY_URL" ]; then
_saveaccountconf_mutable NTFY_URL "$NTFY_URL"
@@ -30,7 +37,26 @@ ntfy_send() {
export _H1="Authorization: Bearer $NTFY_TOKEN"
fi
- _data="${_subject}. $_content"
+ case "$_statusCode" in
+ 0)
+ _priority="$_priority_default"
+ _tag="$_tag_success"
+ ;;
+ 1)
+ _priority="$_priority_error"
+ _tag="$_tag_error"
+ ;;
+ 2)
+ _priority="$_priority_default"
+ _tag="$_tag_info"
+ ;;
+ esac
+
+ export _H2="Priority: $_priority"
+ export _H3="Tags: $_tag"
+ export _H4="Title: $PROJECT_NAME: $_subject"
+
+ _data="$_content"
response="$(_post "$_data" "$NTFY_URL/$NTFY_TOPIC" "" "POST" "")"
if [ "$?" = "0" ] && _contains "$response" "expires"; then
diff --git a/notify/opsgenie.sh b/notify/opsgenie.sh
new file mode 100644
index 00000000..d352a18c
--- /dev/null
+++ b/notify/opsgenie.sh
@@ -0,0 +1,130 @@
+#!/usr/bin/env sh
+
+#Support OpsGenie API integration
+
+#OPSGENIE_API_KEY="" Required, opsgenie api key
+#OPSGENIE_REGION="" Optional, opsgenie region, can be EU or US (default: US)
+#OPSGENIE_PRIORITY_SUCCESS="" Optional, opsgenie priority for success (default: P5)
+#OPSGENIE_PRIORITY_ERROR="" Optional, opsgenie priority for error (default: P2)
+#OPSGENIE_PRIORITY_SKIP="" Optional, opsgenie priority for renew skipped (default: P5)
+
+_OPSGENIE_AVAIL_REGION="US,EU"
+_OPSGENIE_AVAIL_PRIORITIES="P1,P2,P3,P4,P5"
+
+opsgenie_send() {
+ _subject="$1"
+ _content="$2"
+ _status_code="$3" #0: success, 1: error, 2($RENEW_SKIP): skipped
+
+ OPSGENIE_API_KEY="${OPSGENIE_API_KEY:-$(_readaccountconf_mutable OPSGENIE_API_KEY)}"
+ if [ -z "$OPSGENIE_API_KEY" ]; then
+ OPSGENIE_API_KEY=""
+ _err "You didn't specify an OpsGenie API key OPSGENIE_API_KEY yet."
+ return 1
+ fi
+ _saveaccountconf_mutable OPSGENIE_API_KEY "$OPSGENIE_API_KEY"
+ export _H1="Authorization: GenieKey $OPSGENIE_API_KEY"
+
+ OPSGENIE_REGION="${OPSGENIE_REGION:-$(_readaccountconf_mutable OPSGENIE_REGION)}"
+ if [ -z "$OPSGENIE_REGION" ]; then
+ OPSGENIE_REGION="US"
+ _info "The OPSGENIE_REGION is not set, so use the default US as regeion."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_REGION" "$OPSGENIE_REGION"; then
+ _err "The OPSGENIE_REGION \"$OPSGENIE_REGION\" is not available, should be one of $_OPSGENIE_AVAIL_REGION"
+ OPSGENIE_REGION=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_REGION "$OPSGENIE_REGION"
+ fi
+
+ OPSGENIE_PRIORITY_SUCCESS="${OPSGENIE_PRIORITY_SUCCESS:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS)}"
+ if [ -z "$OPSGENIE_PRIORITY_SUCCESS" ]; then
+ OPSGENIE_PRIORITY_SUCCESS="P5"
+ _info "The OPSGENIE_PRIORITY_SUCCESS is not set, so use the default P5 as priority."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SUCCESS"; then
+ _err "The OPSGENIE_PRIORITY_SUCCESS \"$OPSGENIE_PRIORITY_SUCCESS\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES"
+ OPSGENIE_PRIORITY_SUCCESS=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_PRIORITY_SUCCESS "$OPSGENIE_PRIORITY_SUCCESS"
+ fi
+
+ OPSGENIE_PRIORITY_ERROR="${OPSGENIE_PRIORITY_ERROR:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_ERROR)}"
+ if [ -z "$OPSGENIE_PRIORITY_ERROR" ]; then
+ OPSGENIE_PRIORITY_ERROR="P2"
+ _info "The OPSGENIE_PRIORITY_ERROR is not set, so use the default P2 as priority."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_ERROR"; then
+ _err "The OPSGENIE_PRIORITY_ERROR \"$OPSGENIE_PRIORITY_ERROR\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES"
+ OPSGENIE_PRIORITY_ERROR=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_PRIORITY_ERROR "$OPSGENIE_PRIORITY_ERROR"
+ fi
+
+ OPSGENIE_PRIORITY_SKIP="${OPSGENIE_PRIORITY_SKIP:-$(_readaccountconf_mutable OPSGENIE_PRIORITY_SKIP)}"
+ if [ -z "$OPSGENIE_PRIORITY_SKIP" ]; then
+ OPSGENIE_PRIORITY_SKIP="P5"
+ _info "The OPSGENIE_PRIORITY_SKIP is not set, so use the default P5 as priority."
+ elif ! _hasfield "$_OPSGENIE_AVAIL_PRIORITIES" "$OPSGENIE_PRIORITY_SKIP"; then
+ _err "The OPSGENIE_PRIORITY_SKIP \"$OPSGENIE_PRIORITY_SKIP\" is not available, should be one of $_OPSGENIE_AVAIL_PRIORITIES"
+ OPSGENIE_PRIORITY_SKIP=""
+ return 1
+ else
+ _saveaccountconf_mutable OPSGENIE_PRIORITY_SKIP "$OPSGENIE_PRIORITY_SKIP"
+ fi
+
+ case "$OPSGENIE_REGION" in
+ "US")
+ _opsgenie_url="https://api.opsgenie.com/v2/alerts"
+ ;;
+ "EU")
+ _opsgenie_url="https://api.eu.opsgenie.com/v2/alerts"
+ ;;
+ *)
+ _err "opsgenie region error."
+ return 1
+ ;;
+ esac
+
+ case $_status_code in
+ 0)
+ _priority=$OPSGENIE_PRIORITY_SUCCESS
+ ;;
+ 1)
+ _priority=$OPSGENIE_PRIORITY_ERROR
+ ;;
+ 2)
+ _priority=$OPSGENIE_PRIORITY_SKIP
+ ;;
+ *)
+ _priority=$OPSGENIE_PRIORITY_ERROR
+ ;;
+ esac
+
+ _subject_json=$(echo "$_subject" | _json_encode)
+ _content_json=$(echo "$_content" | _json_encode)
+ _subject_underscore=$(echo "$_subject" | sed 's/ /_/g')
+ _alias_json=$(echo "acme.sh-$(hostname)-$_subject_underscore-$(date +%Y%m%d)" | base64 --wrap=0 | _json_encode)
+
+ _data="{
+ \"message\": \"$_subject_json\",
+ \"alias\": \"$_alias_json\",
+ \"description\": \"$_content_json\",
+ \"tags\": [
+ \"acme.sh\",
+ \"host:$(hostname)\"
+ ],
+ \"entity\": \"$(hostname -f)\",
+ \"priority\": \"$_priority\"
+}"
+
+ if response=$(_post "$_data" "$_opsgenie_url" "" "" "application/json"); then
+ if ! _contains "$response" error; then
+ _info "opsgenie send success."
+ return 0
+ fi
+ fi
+ _err "opsgenie send error."
+ _err "$response"
+ return 1
+}