Browse Source

Merge pull request #1 from acmesh-official/master

Merge acmesh-official/acme.sh/master into master
pull/6606/head
Matteo Gaggiano 3 weeks ago
committed by GitHub
parent
commit
9aeca8f130
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 107
      .github/workflows/DNS.yml
  2. 8
      .github/workflows/DragonFlyBSD.yml
  3. 8
      .github/workflows/FreeBSD.yml
  4. 10
      .github/workflows/NetBSD.yml
  5. 8
      .github/workflows/Omnios.yml
  6. 8
      .github/workflows/OpenBSD.yml
  7. 79
      .github/workflows/OpenIndiana.yml
  8. 2
      .github/workflows/PebbleStrict.yml
  9. 12
      .github/workflows/Solaris.yml
  10. 5
      .github/workflows/wiki-monitor.yml
  11. 11
      Dockerfile
  12. 450
      README.md
  13. 199
      acme.sh
  14. 2
      deploy/ali_cdn.sh
  15. 2
      deploy/ali_dcdn.sh
  16. 56
      deploy/cachefly.sh
  17. 86
      deploy/directadmin.sh
  18. 86
      deploy/edgio.sh
  19. 131
      deploy/keyhelp.sh
  20. 86
      deploy/keyhelp_api.sh
  21. 276
      deploy/multideploy.sh
  22. 69
      deploy/netlify.sh
  23. 23
      deploy/panos.sh
  24. 10
      deploy/qiniu.sh
  25. 18
      deploy/strongswan.sh
  26. 2
      deploy/truenas_ws.sh
  27. 6
      deploy/unifi.sh
  28. 17
      dnsapi/dns_ali.sh
  29. 2
      dnsapi/dns_aws.sh
  30. 4
      dnsapi/dns_cf.sh
  31. 4
      dnsapi/dns_curanet.sh
  32. 139
      dnsapi/dns_efficientip.sh
  33. 226
      dnsapi/dns_exoscale.sh
  34. 10
      dnsapi/dns_gandi_livedns.sh
  35. 593
      dnsapi/dns_hetznercloud.sh
  36. 501
      dnsapi/dns_hostup.sh
  37. 244
      dnsapi/dns_infoblox_uddi.sh
  38. 15
      dnsapi/dns_inwx.sh
  39. 109
      dnsapi/dns_mgwm.sh
  40. 62
      dnsapi/dns_nanelo.sh
  41. 45
      dnsapi/dns_omglol.sh
  42. 2
      dnsapi/dns_ovh.sh
  43. 216
      dnsapi/dns_qc.sh
  44. 309
      dnsapi/dns_sotoon.sh
  45. 229
      dnsapi/dns_virakcloud.sh
  46. 28
      notify/ntfy.sh
  47. 130
      notify/opsgenie.sh
  48. 4
      notify/pushover.sh
  49. 4
      notify/telegram.sh

107
.github/workflows/DNS.yml

@ -232,7 +232,7 @@ jobs:
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg install -y socat curl
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -251,7 +251,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@ -283,7 +287,7 @@ jobs:
envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
prepare: pkg_add socat curl libiconv
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -302,7 +306,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@ -335,7 +343,7 @@ jobs:
prepare: |
/usr/sbin/pkg_add curl socat
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -354,7 +362,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@ -387,7 +399,7 @@ jobs:
prepare: |
pkg install -y curl socat libnghttp2
usesh: true
copyback: false
sync: nfs
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
@ -406,7 +418,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
@ -440,8 +456,10 @@ jobs:
- uses: vmactions/solaris-vm@v1
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
sync: nfs
prepare: |
pkgutil -U
pkgutil -y -i socat
run: |
pkg set-mediator -v -I default@1.1 openssl
export PATH=/usr/gnu/bin:$PATH
@ -462,6 +480,11 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
Omnios:
@ -491,7 +514,62 @@ jobs:
- uses: vmactions/omnios-vm@v1
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
sync: nfs
prepare: pkg install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
fi
if [ "${{ secrets.TokenName2}}" ] ; then
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
fi
if [ "${{ secrets.TokenName3}}" ] ; then
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
fi
if [ "${{ secrets.TokenName4}}" ] ; then
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
fi
if [ "${{ secrets.TokenName5}}" ] ; then
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
OpenIndiana:
runs-on: ubuntu-latest
needs: Omnios
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since OpenIndiana doesn't accept the expired ISRG X1 root
TokenName1: ${{ secrets.TokenName1}}
TokenName2: ${{ secrets.TokenName2}}
TokenName3: ${{ secrets.TokenName3}}
TokenName4: ${{ secrets.TokenName4}}
TokenName5: ${{ secrets.TokenName5}}
steps:
- uses: actions/checkout@v4
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
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}}'
sync: nfs
prepare: pkg install socat
run: |
if [ "${{ secrets.TokenName1}}" ] ; then
@ -511,5 +589,12 @@ jobs:
fi
cd ../acmetest
./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

8
.github/workflows/DragonFlyBSD.yml

@ -63,9 +63,13 @@ jobs:
prepare: |
pkg install -y curl socat libnghttp2
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

8
.github/workflows/FreeBSD.yml

@ -68,9 +68,13 @@ jobs:
"8080": "80"
prepare: pkg install -y socat curl wget
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

10
.github/workflows/NetBSD.yml

@ -63,9 +63,13 @@ jobs:
prepare: |
/usr/sbin/pkg_add curl socat
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

8
.github/workflows/Omnios.yml

@ -67,9 +67,13 @@ jobs:
nat: |
"8080": "80"
prepare: pkg install socat wget
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

8
.github/workflows/OpenBSD.yml

@ -68,9 +68,13 @@ jobs:
"8080": "80"
prepare: pkg_add socat curl wget libnghttp2
usesh: true
copyback: false
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

79
.github/workflows/OpenIndiana.yml

@ -0,0 +1,79 @@
name: OpenIndiana
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/OpenIndiana.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/OpenIndiana.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
OpenIndiana:
strategy:
matrix:
include:
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
- TEST_ACME_Server: "LetsEncrypt.org_test"
CA_ECDSA: ""
CA: ""
CA_EMAIL: ""
TEST_PREFERRED_CHAIN: (STAGING)
ACME_USE_WGET: 1
#- TEST_ACME_Server: "ZeroSSL.com"
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA"
# CA: "ZeroSSL RSA Domain Secure Site CA"
# CA_EMAIL: "githubtest@acme.sh"
# TEST_PREFERRED_CHAIN: ""
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
CA_ECDSA: ${{ matrix.CA_ECDSA }}
CA: ${{ matrix.CA }}
CA_EMAIL: ${{ matrix.CA_EMAIL }}
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
steps:
- uses: actions/checkout@v4
- uses: vmactions/cf-tunnel@v0
id: tunnel
with:
protocol: http
port: 8080
- name: Set envs
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
- name: Clone acmetest
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
- uses: vmactions/openindiana-vm@v1
with:
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
nat: |
"8080": "80"
prepare: pkg install socat curl
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

2
.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

12
.github/workflows/Solaris.yml

@ -66,10 +66,16 @@ 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
copyback: false
prepare: |
pkgutil -U
pkgutil -y -i socat curl wget
sync: nfs
run: |
cd ../acmetest \
&& ./letest.sh
- name: onError
if: ${{ failure() }}
run: |
echo "See how to debug in VM:"
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"

5
.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:"

11
Dockerfile

@ -1,4 +1,4 @@
FROM alpine:3.21
FROM alpine:3.22
RUN apk --no-cache add -f \
openssl \
@ -13,8 +13,11 @@ RUN apk --no-cache add -f \
tar \
libidn \
jq \
yq-go \
cronie
ENV LE_WORKING_DIR=/acmebin
ENV LE_CONFIG_HOME=/acme.sh
ARG AUTO_UPGRADE=1
@ -30,7 +33,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 +67,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 +75,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

450
README.md

@ -1,52 +1,71 @@
# An ACME Shell script: acme.sh
[![FreeBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)
[![OpenBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)
[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)
[![MacOS](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)
[![Ubuntu](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)
[![Windows](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)
[![Solaris](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)
[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)
[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)
![Shellcheck](https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg)
![PebbleStrict](https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg)
![DockerHub](https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg)
<a href="https://opencollective.com/acmesh" alt="Financial Contributors on Open Collective"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" /></a>
[![Join the chat at https://gitter.im/acme-sh/Lobby](https://badges.gitter.im/acme-sh/Lobby.svg)](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Docker stars](https://img.shields.io/docker/stars/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
[![Docker pulls](https://img.shields.io/docker/pulls/neilpang/acme.sh.svg)](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
- An ACME protocol client written purely in Shell (Unix shell) language.
- Full ACME protocol implementation.
- Support ECDSA certs
- Support SAN and wildcard certs
- Simple, powerful and very easy to use. You only need 3 minutes to learn it.
- Bash, dash and sh compatible.
- Purely written in Shell with no dependencies on python.
- Just one script to issue, renew and install your certificates automatically.
- DOES NOT require `root/sudoer` access.
- Docker ready
- IPv6 ready
- Cron job notifications for renewal or error etc.
It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates.
Wiki: https://github.com/acmesh-official/acme.sh/wiki
For Docker Fans: [acme.sh :two_hearts: Docker ](https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker)
Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
# Who:
<p align="center">
<a href="https://zerossl.com/?fromacme.sh">
<img src="https://github.com/user-attachments/assets/7531085e-399b-4ac2-82a2-90d14a0b7f05" alt="zerossl.com">
</a>
</p>
<h1 align="center">🔐 acme.sh</h1>
<h3 align="center">An ACME Protocol Client Written Purely in Shell</h3>
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg" alt="FreeBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg" alt="OpenBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg" alt="NetBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg" alt="MacOS"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg" alt="Ubuntu"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg" alt="Windows"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg" alt="Solaris"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg" alt="DragonFlyBSD"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg" alt="Omnios"></a>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
</p>
<p align="center">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg" alt="Shellcheck">
<img src="https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg" alt="PebbleStrict">
<img src="https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg" alt="DockerHub">
</p>
<p align="center">
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" alt="Financial Contributors on Open Collective"></a>
<a href="https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/acme-sh/Lobby.svg" alt="Join the chat at Gitter"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/stars/neilpang/acme.sh.svg" alt="Docker stars"></a>
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/pulls/neilpang/acme.sh.svg" alt="Docker pulls"></a>
</p>
---
## ✨ Features
- 🐚 An ACME protocol client written **purely in Shell** (Unix shell) language
- 📜 Full ACME protocol implementation
- 🔑 Support **ECDSA** certificates
- 🌐 Support **SAN** and **wildcard** certificates
- ⚡ Simple, powerful and very easy to use — only **3 minutes** to learn!
- 🔧 Compatible with **Bash**, **dash** and **sh**
- 🚫 No dependencies on Python
- 🔄 One script to issue, renew and install your certificates automatically
- 👤 **DOES NOT** require `root/sudoer` access
- 🐳 Docker ready
- 🌍 IPv6 ready
- 📧 Cron job notifications for renewal or error
> 💡 It's probably the **easiest & smartest** shell script to automatically issue & renew free certificates.
<p align="center">
<a href="https://github.com/acmesh-official/acme.sh/wiki"><strong>📚 Wiki</strong></a>
<a href="https://github.com/acmesh-official/acme.sh/wiki/Run-acme.sh-in-docker"><strong>🐳 Docker Guide</strong></a>
<a href="https://twitter.com/neilpangxa"><strong>🐦 Twitter</strong></a>
</p>
---
## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
---
## 🏆 Who Uses acme.sh?
- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/)
- [ruby-china.org](https://ruby-china.org/topics/31983)
- [Proxmox](https://pve.proxmox.com/wiki/Certificate_Management)
@ -60,7 +79,9 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
- [lnmp.org](https://lnmp.org/)
- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
# Tested OS
---
## 🖥️ Tested OS
| NO | Status| Platform|
|----|-------|---------|
@ -74,8 +95,8 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|8|[![NetBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD
|9|[![DragonFlyBSD](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD
|10|[![Omnios](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS
|11|[![OpenIndiana](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana
|12|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|13|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|14|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|15|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
@ -83,57 +104,66 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|17|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|18|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|19|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|10|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|11|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](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://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|20|[![Linux](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|21|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111
|22|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|23|[![](https://acmesh-official.github.io/acmetest/status/proxmox.svg)](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
Check our [testing project](https://github.com/acmesh-official/acmetest):
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
>
> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org)
https://github.com/acmesh-official/acmetest
---
# Supported CA
## 🏛️ Supported CA
- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default)
- Letsencrypt.org CA
- [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA)
- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA)
- [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA)
- [Pebble strict Mode](https://github.com/letsencrypt/pebble)
- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
| CA | Status |
|---|---|
| [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) | ⭐ **Default** |
| Letsencrypt.org CA | ✅ Supported |
| [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) | ✅ Supported |
| [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) | ✅ Supported |
| [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) | ✅ Supported |
| [Pebble strict Mode](https://github.com/letsencrypt/pebble) | ✅ Supported |
| Any [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA | ✅ Supported |
# Supported modes
---
- Webroot mode
- Standalone mode
- Standalone tls-alpn mode
- Apache mode
- Nginx mode
- DNS mode
- [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode)
- [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode)
## ⚙️ Supported Modes
| Mode | Description |
|------|-------------|
| 📁 Webroot mode | Use existing webroot directory |
| 🖥️ Standalone mode | Built-in webserver on port 80 |
| 🔐 Standalone tls-alpn mode | Built-in webserver on port 443 |
| 🪶 Apache mode | Use Apache for verification |
| ⚡ Nginx mode | Use Nginx for verification |
| 🌐 DNS mode | Use DNS TXT records |
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
# 1. How to install
---
### 1. Install online
## 📖 Usage Guide
Check this project: https://github.com/acmesh-official/get.acme.sh
### 1️⃣ How to Install
#### 📥 Install Online
> Check this project: https://github.com/acmesh-official/get.acme.sh
```bash
curl https://get.acme.sh | sh -s email=my@example.com
```
Or:
**Or:**
```bash
wget -O - https://get.acme.sh | sh -s email=my@example.com
```
### 2. Or, Install from git
#### 📦 Install from Git
Clone this project and launch installation:
@ -143,11 +173,11 @@ cd ./acme.sh
./acme.sh --install -m my@example.com
```
You `don't have to be root` then, although `it is recommended`.
> 💡 You `don't have to be root` then, although `it is recommended`.
Advanced Installation: https://github.com/acmesh-official/acme.sh/wiki/How-to-install
📚 **Advanced Installation:** https://github.com/acmesh-official/acme.sh/wiki/How-to-install
The installer will perform 3 actions:
**The installer will perform 3 actions:**
1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`.
All certs will be placed in this folder too.
@ -160,17 +190,19 @@ Cron entry example:
0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null
```
After the installation, you must close the current terminal and reopen it to make the alias take effect.
> ⚠️ After the installation, you must close the current terminal and reopen it to make the alias take effect.
Ok, you are ready to issue certs now.
✅ **You are ready to issue certs now!**
Show help message:
**Show help message:**
```sh
root@v1:~# acme.sh -h
acme.sh -h
```
# 2. Just issue a cert
---
### 2️⃣ Issue a Certificate
**Example 1:** Single domain.
@ -205,17 +237,21 @@ You must point and bind all the domains to the same webroot dir: `/home/wwwroot/
The certs will be placed in `~/.acme.sh/example.com/`
The certs will be renewed automatically every **60** days.
> 🔄 The certs will be renewed automatically every **30** days.
> 🔐 The certs will default to **ECC** certificates.
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# 3. Install the cert to Apache/Nginx etc.
### 3️⃣ Install the Certificate to Apache/Nginx
After the cert is generated, you probably want to install/copy the cert to your Apache/Nginx or other servers.
You **MUST** use this command to copy the certs to the target files, **DO NOT** use the certs files in **~/.acme.sh/** folder, they are for internal use only, the folder structure may change in the future.
**Apache** example:
> ⚠️ **IMPORTANT:** You **MUST** use this command to copy the certs to the target files. **DO NOT** use the certs files in `~/.acme.sh/` folder — they are for internal use only, the folder structure may change in the future.
#### 🪶 Apache Example:
```bash
acme.sh --install-cert -d example.com \
--cert-file /path/to/certfile/in/apache/cert.pem \
@ -224,7 +260,7 @@ acme.sh --install-cert -d example.com \
--reloadcmd "service apache2 force-reload"
```
**Nginx** example:
#### ⚡ Nginx Example:
```bash
acme.sh --install-cert -d example.com \
--key-file /path/to/keyfile/in/nginx/key.pem \
@ -238,91 +274,89 @@ The ownership and permission info of existing files are preserved. You can pre-c
Install/copy the cert/key to the production Apache or Nginx path.
The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`.
> 🔄 The cert will be renewed every **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically.
> ⚠️ **IMPORTANT:** The `reloadcmd` is very important. The cert can be automatically renewed, but without a correct `reloadcmd`, the cert may not be flushed to your server (like nginx or apache), then your website will not be able to show the renewed cert.
**Please take care: The reloadcmd is very important. The cert can be automatically renewed, but, without a correct 'reloadcmd' the cert may not be flushed to your server(like nginx or apache), then your website will not be able to show renewed cert in 60 days.**
---
# 4. Use Standalone server to issue cert
### 4️⃣ Use Standalone Server to Issue Certificate
**(requires you to be root/sudoer or have permission to listen on port 80 (TCP))**
> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP)
Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
> ⚠️ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
```bash
acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
```
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# 5. Use Standalone ssl server to issue cert
### 5️⃣ Use Standalone TLS Server to Issue Certificate
**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
> 🔐 Requires root/sudoer or permission to listen on port **443** (TCP)
Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
> ⚠️ Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
```bash
acme.sh --issue --alpn -d example.com -d www.example.com -d cp.example.com
```
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# 6. Use Apache mode
### 6️⃣ Use Apache Mode
**(requires you to be root/sudoer, since it is required to interact with Apache server)**
> 🔐 Requires root/sudoer to interact with Apache server
If you are running a web server, it is recommended to use the `Webroot mode`.
Particularly, if you are running an Apache server, you can use Apache mode instead. This mode doesn't write any files to your web root folder.
Just set string "apache" as the second argument and it will force use of apache plugin automatically.
```sh
acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com
```
**This apache mode is only to issue the cert, it will not change your apache config files.
You will need to configure your website config files to use the cert by yourself.
We don't want to mess with your apache server, don't worry.**
> 💡 **Note:** This Apache mode is only to issue the cert, it will **not** change your Apache config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Apache server, don't worry!
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
# 7. Use Nginx mode
---
**(requires you to be root/sudoer, since it is required to interact with Nginx server)**
If you are running a web server, it is recommended to use the `Webroot mode`.
### 7️⃣ Use Nginx Mode
Particularly, if you are running an nginx server, you can use nginx mode instead. This mode doesn't write any files to your web root folder.
> 🔐 Requires root/sudoer to interact with Nginx server
Just set string "nginx" as the second argument.
If you are running a web server, it is recommended to use the `Webroot mode`.
It will configure nginx server automatically to verify the domain and then restore the nginx config to the original version.
Particularly, if you are running an Nginx server, you can use Nginx mode instead. This mode doesn't write any files to your web root folder.
So, the config is not changed.
It will configure Nginx server automatically to verify the domain and then restore the Nginx config to the original version. So, the config is not changed.
```sh
acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com
```
**This nginx mode is only to issue the cert, it will not change your nginx config files.
You will need to configure your website config files to use the cert by yourself.
We don't want to mess with your nginx server, don't worry.**
> 💡 **Note:** This Nginx mode is only to issue the cert, it will **not** change your Nginx config files. You will need to configure your website config files to use the cert by yourself. We don't want to mess with your Nginx server, don't worry!
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
---
# 8. Automatic DNS API integration
### 8️⃣ Automatic DNS API Integration
If your DNS provider supports API access, we can use that API to automatically issue the certs.
You don't have to do anything manually!
> ✨ **You don't have to do anything manually!**
### Currently acme.sh supports most of the dns providers:
📚 **Currently acme.sh supports most DNS providers:** https://github.com/acmesh-official/acme.sh/wiki/dnsapi
https://github.com/acmesh-official/acme.sh/wiki/dnsapi
---
# 9. Use DNS manual mode:
### 9️⃣ Use DNS Manual Mode
See: https://github.com/acmesh-official/acme.sh/wiki/dns-manual-mode first.
@ -352,70 +386,74 @@ Then just rerun with `renew` argument:
acme.sh --renew -d example.com
```
Ok, it's done.
✅ **Done!**
**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.**
> ⚠️ **WARNING:** This is DNS manual mode — it **cannot** be renewed automatically. You will have to add a new TXT record to your domain manually when you renew your cert. **Please use DNS API mode instead.**
**Please use dns api mode instead.**
---
# 10. Issue ECC certificates
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
Just set the `keylength` parameter with a prefix `ec-`.
Just set the `keylength` to a valid, supported value.
For example:
**Valid values for the `keylength` parameter:**
### Single domain ECC certificate
| Key Length | Description |
|------------|-------------|
| `ec-256` | prime256v1, "ECDSA P-256" ⭐ **Default** |
| `ec-384` | secp384r1, "ECDSA P-384" |
| `ec-521` | secp521r1, "ECDSA P-521" ⚠️ Not supported by Let's Encrypt yet |
| `2048` | RSA 2048-bit |
| `3072` | RSA 3072-bit |
| `4096` | RSA 4096-bit |
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256
```
**Examples:**
### SAN multi domain ECC certificate
#### Single domain with ECDSA P-384 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 --keylength ec-384
```
Please look at the `keylength` parameter above.
Valid values are:
#### SAN multi domain with RSA4096 certificate
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)**
```bash
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
```
---
# 11. Issue Wildcard certificates
### 1️⃣1️⃣ Issue Wildcard Certificates
It's simple, just give a wildcard domain as the `-d` parameter.
It's simple! Just give a wildcard domain as the `-d` parameter:
```sh
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf
```
# 12. How to renew the certs
---
### 1️⃣2️⃣ How to Renew Certificates
No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
However, you can also force to renew a cert:
However, you can force a renewal:
```sh
acme.sh --renew -d example.com --force
```
or, for ECC cert:
**For ECC cert:**
```sh
acme.sh --renew -d example.com --force --ecc
```
---
# 13. How to stop cert renewal
### 1️⃣3️⃣ How to Stop Certificate Renewal
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
@ -425,73 +463,78 @@ acme.sh --remove -d example.com [--ecc]
The cert/key file is not removed from the disk.
You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
> 💡 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) manually.
---
# 14. How to upgrade `acme.sh`
### 1️⃣4️⃣ How to Upgrade acme.sh
acme.sh is in constant development, so it's strongly recommended to use the latest code.
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
You can update acme.sh to the latest code:
**Update to latest:**
```sh
acme.sh --upgrade
```
You can also enable auto upgrade:
**Enable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade
```
Then **acme.sh** will be kept up to date automatically.
Disable auto upgrade:
**Disable auto upgrade:**
```sh
acme.sh --upgrade --auto-upgrade 0
```
---
# 15. Issue a cert from an existing CSR
https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
### 1️⃣5️⃣ Issue a Certificate from an Existing CSR
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
# 16. Send notifications in cronjob
---
https://github.com/acmesh-official/acme.sh/wiki/notify
### 1️⃣6️⃣ Send Notifications in Cronjob
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
# 17. Under the Hood
---
Speak ACME language using shell, directly to "Let's Encrypt".
### 1️⃣7️⃣ Under the Hood
TODO:
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
---
# 18. Acknowledgments
### 1️⃣8️⃣ Acknowledgments
1. Acme-tiny: https://github.com/diafygi/acme-tiny
2. ACME protocol: https://github.com/ietf-wg-acme/acme
| Project | Link |
|---------|------|
| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny |
| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme |
---
## Contributors
## 👥 Contributors
### Code Contributors
### 💻 Code Contributors
This project exists thanks to all the people who contribute.
<a href="https://github.com/acmesh-official/acme.sh/graphs/contributors"><img src="https://opencollective.com/acmesh/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
### 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/acmesh/contribute)]
#### Individuals
#### 👤 Individuals
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/individuals.svg?width=890"></a>
#### Organizations
#### 🏢 Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/acmesh/contribute)]
@ -506,20 +549,43 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/acmesh/organization/8/website"><img src="https://opencollective.com/acmesh/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/acmesh/organization/9/website"><img src="https://opencollective.com/acmesh/organization/9/avatar.svg"></a>
---
### 1️⃣9️⃣ License & Others
📄 **License:** GPLv3
⭐ Please **Star** and **Fork** this project!
🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
# 19. License & Others
---
License is GPLv3
### 2️⃣0️⃣ Donate
Please Star and Fork me.
> 💝 Your donation makes **acme.sh** better!
[Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
| Method | Link |
|--------|------|
| PayPal / Alipay(支付宝) / Wechat(微信) | [https://donate.acme.sh/](https://donate.acme.sh/) |
📜 [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
# 20. Donate
Your donation makes **acme.sh** better:
---
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
### 2️⃣1️⃣ About This Repository
[Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
> [!NOTE]
> This repository is officially maintained by <strong>ZeroSSL</strong> 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.
<p align="center">
<a href="https://zerossl.com">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://zerossl.com/assets/images/zerossl_logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://zerossl.com/assets/images/zerossl_logo.svg">
<img src="https://zerossl.com/assets/images/zerossl_logo.svg" alt="ZeroSSL" width="256">
</picture>
</a>
</p>

199
acme.sh

@ -1,6 +1,6 @@
#!/usr/bin/env sh
VER=3.1.2
VER=3.1.3
PROJECT_NAME="acme.sh"
@ -65,7 +65,7 @@ ID_TYPE_IP="ip"
LOCAL_ANY_ADDRESS="0.0.0.0"
DEFAULT_RENEW=60
DEFAULT_RENEW=30
NO_VALUE="no"
@ -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
@ -1466,7 +1466,7 @@ _toPkcs() {
${ACME_OPENSSL_BIN:-openssl} pkcs12 -export -out "$_cpfx" -inkey "$_ckey" -in "$_ccert" -certfile "$_cca"
fi
if [ "$?" = "0" ]; then
_savedomainconf "Le_PFXPassword" "$pfxPassword"
_savedomainconf "Le_PFXPassword" "$pfxPassword" "base64"
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
@ -2773,6 +2783,7 @@ _clearAPI() {
ACME_REVOKE_CERT=""
ACME_NEW_NONCE=""
ACME_AGREEMENT=""
ACME_RENEWAL_INFO=""
}
#server
@ -2817,6 +2828,9 @@ _initAPI() {
ACME_AGREEMENT=$(echo "$response" | _egrep_o 'termsOfService" *: *"[^"]*"' | cut -d '"' -f 3)
export ACME_AGREEMENT
ACME_RENEWAL_INFO=$(echo "$response" | _egrep_o 'renewalInfo" *: *"[^"]*"' | cut -d '"' -f 3)
export ACME_RENEWAL_INFO
_debug "ACME_KEY_CHANGE" "$ACME_KEY_CHANGE"
_debug "ACME_NEW_AUTHZ" "$ACME_NEW_AUTHZ"
_debug "ACME_NEW_ORDER" "$ACME_NEW_ORDER"
@ -2824,6 +2838,7 @@ _initAPI() {
_debug "ACME_REVOKE_CERT" "$ACME_REVOKE_CERT"
_debug "ACME_AGREEMENT" "$ACME_AGREEMENT"
_debug "ACME_NEW_NONCE" "$ACME_NEW_NONCE"
_debug "ACME_RENEWAL_INFO" "$ACME_RENEWAL_INFO"
if [ "$ACME_NEW_ACCOUNT" ] && [ "$ACME_NEW_ORDER" ]; then
return 0
fi
@ -4435,6 +4450,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"
@ -4454,7 +4470,7 @@ issue() {
Le_NextRenewTime=$(_readdomainconf Le_NextRenewTime)
_debug Le_NextRenewTime "$Le_NextRenewTime"
if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
_valid_to_saved=$(_readdomainconf Le_Valid_to)
_valid_to_saved=$(_readdomainconf Le_Valid_To)
if [ "$_valid_to_saved" ] && ! _startswith "$_valid_to_saved" "+"; then
_info "The domain is set to be valid to: $_valid_to_saved"
_info "It cannot be renewed automatically"
@ -4579,12 +4595,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"
@ -5218,6 +5247,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')
@ -5416,10 +5455,10 @@ $_authorizations_map"
_savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime"
#convert to pkcs12
Le_PFXPassword="$(_readdomainconf Le_PFXPassword)"
if [ "$Le_PFXPassword" ]; then
_toPkcs "$CERT_PFX_PATH" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$Le_PFXPassword"
fi
export CERT_PFX_PATH
if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
_savedomainconf "Le_RealCertPath" "$_real_cert"
@ -5529,6 +5568,10 @@ renew() {
Le_RenewHook="$(_readdomainconf Le_RenewHook)"
Le_Preferred_Chain="$(_readdomainconf Le_Preferred_Chain)"
Le_Certificate_Profile="$(_readdomainconf Le_Certificate_Profile)"
Le_Valid_From="$(_readdomainconf Le_Valid_From)"
Le_Valid_To="$(_readdomainconf Le_Valid_To)"
Le_ExtKeyUse="$(_readdomainconf Le_ExtKeyUse)"
# When renewing from an old version, the empty Le_Keylength means 2048.
# Note, do not use DEFAULT_DOMAIN_KEY_LENGTH as that value may change over
# time but an empty value implies 2048 specifically.
@ -5543,7 +5586,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"
@ -5710,6 +5753,10 @@ signcsr() {
_local_addr="${11}"
_challenge_alias="${12}"
_preferred_chain="${13}"
_valid_f="${14}"
_valid_t="${15}"
_cert_prof="${16}"
_en_key_usage="${17}"
_csrsubj=$(_readSubjectFromCSR "$_csrfile")
if [ "$?" != "0" ]; then
@ -5753,7 +5800,7 @@ signcsr() {
_info "Copying CSR to: $CSR_PATH"
cp "$_csrfile" "$CSR_PATH"
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain"
issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain" "$_valid_f" "$_valid_t" "$_cert_prof" "$_en_key_usage"
}
@ -5804,9 +5851,10 @@ list() {
_sep="|"
if [ "$_raw" ]; then
if [ -z "$_domain" ]; then
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}CA${_sep}Created${_sep}Renew"
printf "%s\n" "Main_Domain${_sep}KeyLength${_sep}SAN_Domains${_sep}Profile${_sep}CA${_sep}Created${_sep}Renew"
fi
for di in "${CERT_HOME}"/*.*/; do
for di in "${CERT_HOME}"/*.* "${CERT_HOME}"/*:*; do
[ -d "$di" ] || continue
d=$(basename "$di")
_debug d "$d"
(
@ -5819,7 +5867,7 @@ list() {
. "$DOMAIN_CONF"
_ca="$(_getCAShortName "$Le_API")"
if [ -z "$_domain" ]; then
printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr"
printf "%s\n" "$Le_Domain${_sep}\"$Le_Keylength\"${_sep}$Le_Alt${_sep}$Le_Certificate_Profile${_sep}$_ca${_sep}$Le_CertCreateTimeStr${_sep}$Le_NextRenewTimeStr"
else
if [ "$_domain" = "$d" ]; then
cat "$DOMAIN_CONF"
@ -5838,6 +5886,48 @@ list() {
}
list_profiles() {
_initpath
_initAPI
_l_server_url="$ACME_DIRECTORY"
_l_server_name="$(_getCAShortName "$_l_server_url")"
_info "Fetching profiles from $_l_server_name ($_l_server_url)..."
response=$(_get "$_l_server_url" "" 10)
if [ "$?" != "0" ]; then
_err "Failed to connect to CA directory: $_l_server_url"
return 1
fi
normalized_response=$(echo "$response" | _normalizeJson)
profiles_json=$(echo "$normalized_response" | _egrep_o '"profiles" *: *\{[^\}]*\}')
if [ -z "$profiles_json" ]; then
_info "The CA '$_l_server_name' does not publish certificate profiles via its directory endpoint."
return 0
fi
# Strip the outer layer to get the key-value pairs
profiles_kv=$(echo "$profiles_json" | sed 's/"profiles" *: *{//' | sed 's/}$//' | tr ',' '\n')
printf "\n%-15s %s\n" "name" "info"
printf -- "--------------------------------------------------------------------\n"
_old_IFS="$IFS"
IFS='
'
for pair in $profiles_kv; do
# Trim quotes and whitespace
_name=$(echo "$pair" | cut -d: -f1 | tr -d '" \t')
_info_url=$(echo "$pair" | cut -d: -f2- | sed 's/^ *//' | tr -d '"')
printf "%-15s %s\n" "$_name" "$_info_url"
done
IFS="$_old_IFS"
return 0
}
_deploy() {
_d="$1"
_hooks="$2"
@ -6461,6 +6551,36 @@ deactivate() {
done
}
#cert
_getAKI() {
_cert="$1"
openssl x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ' :'
}
#cert
_getSerial() {
_cert="$1"
openssl x509 -in "$_cert" -serial -noout | cut -d = -f 2
}
#cert
_get_ARI() {
_cert="$1"
_aki=$(_getAKI "$_cert")
_ser=$(_getSerial "$_cert")
_debug2 "_aki" "$_aki"
_debug2 "_ser" "$_ser"
_akiurl="$(echo "$_aki" | _h2b | _base64 | tr -d = | _url_encode)"
_debug2 "_akiurl" "$_akiurl"
_serurl="$(echo "$_ser" | _h2b | _base64 | tr -d = | _url_encode)"
_debug2 "_serurl" "$_serurl"
_ARI_URL="$ACME_RENEWAL_INFO/$_akiurl.$_serurl"
_get "$_ARI_URL"
}
# Detect profile file if not specified as environment variable
_detect_profile() {
if [ -n "$PROFILE" -a -f "$PROFILE" ]; then
@ -7095,6 +7215,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 <file> 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.
@ -7213,6 +7335,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() {
@ -7378,6 +7518,8 @@ _process() {
_local_address=""
_log_level=""
_auto_upgrade=""
_request_v4=""
_request_v6=""
_listen_v4=""
_listen_v6=""
_openssl_bin=""
@ -7395,6 +7537,7 @@ _process() {
_valid_from=""
_valid_to=""
_certificate_profile=""
_extended_key_usage=""
while [ ${#} -gt 0 ]; do
case "${1}" in
@ -7498,6 +7641,9 @@ _process() {
--set-default-chain)
_CMD="setdefaultchain"
;;
--list-profiles)
_CMD="list_profiles"
;;
-d | --domain)
_dvalue="$2"
@ -7787,7 +7933,7 @@ _process() {
shift
;;
--extended-key-usage)
Le_ExtKeyUse="$2"
_extended_key_usage="$2"
shift
;;
--ocsp-must-staple | --ocsp)
@ -7840,6 +7986,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"
@ -7992,13 +8150,13 @@ _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"
;;
signcsr)
signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain"
signcsr "$_csr" "$_webroot" "$_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"
;;
showcsr)
showcsr "$_csr" "$_domain"
@ -8063,6 +8221,9 @@ _process() {
setdefaultchain)
setdefaultchain "$_preferred_chain"
;;
list_profiles)
list_profiles
;;
*)
if [ "$_CMD" ]; then
_err "Invalid command: $_CMD"

2
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'
}

2
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'
}

56
deploy/cachefly.sh

@ -0,0 +1,56 @@
#!/usr/bin/env sh
# Script to deploy certificate to CacheFly
# https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post
# This deployment required following variables
# export CACHEFLY_TOKEN="Your CacheFly API Token"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
CACHEFLY_API_BASE="https://api.cachefly.com/api/2.5"
cachefly_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$CACHEFLY_TOKEN" ]; then
_err "CACHEFLY_TOKEN is not defined."
return 1
else
_savedomainconf CACHEFLY_TOKEN "$CACHEFLY_TOKEN"
fi
_info "Deploying certificate to CacheFly..."
## upload certificate
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
_request_body="{\"certificate\":\"$string_fullchain\",\"certificateKey\":\"$string_key\"}"
_debug _request_body "$_request_body"
_debug CACHEFLY_TOKEN "$CACHEFLY_TOKEN"
export _H1="Authorization: Bearer $CACHEFLY_TOKEN"
_response=$(_post "$_request_body" "$CACHEFLY_API_BASE/certificates" "" "POST" "application/json")
if _contains "$_response" "message"; then
_err "Error in deploying $_cdomain certificate to CacheFly."
_err "$_response"
return 1
fi
_debug response "$_response"
_info "Domain $_cdomain certificate successfully deployed to CacheFly."
return 0
}

86
deploy/directadmin.sh

@ -0,0 +1,86 @@
#!/usr/bin/env sh
# Script to deploy certificate to DirectAdmin
# https://docs.directadmin.com/directadmin/customizing-workflow/api-all-about.html#creating-a-login-key
# https://docs.directadmin.com/changelog/version-1.24.4.html#cmd-api-catch-all-pop-passwords-frontpage-protected-dirs-ssl-certs
# This deployment required following variables
# export DirectAdmin_SCHEME="https" # Optional, https or http, defaults to https
# export DirectAdmin_ENDPOINT="example.com:2222"
# export DirectAdmin_USERNAME="Your DirectAdmin Username"
# export DirectAdmin_KEY="Your DirectAdmin Login Key or Password"
# export DirectAdmin_MAIN_DOMAIN="Your DirectAdmin Main Domain, NOT Subdomain"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
directadmin_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$DirectAdmin_ENDPOINT" ]; then
_err "DirectAdmin_ENDPOINT is not defined."
return 1
else
_savedomainconf DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT"
fi
if [ -z "$DirectAdmin_USERNAME" ]; then
_err "DirectAdmin_USERNAME is not defined."
return 1
else
_savedomainconf DirectAdmin_USERNAME "$DirectAdmin_USERNAME"
fi
if [ -z "$DirectAdmin_KEY" ]; then
_err "DirectAdmin_KEY is not defined."
return 1
else
_savedomainconf DirectAdmin_KEY "$DirectAdmin_KEY"
fi
if [ -z "$DirectAdmin_MAIN_DOMAIN" ]; then
_err "DirectAdmin_MAIN_DOMAIN is not defined."
return 1
else
_savedomainconf DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN"
fi
# Optional SCHEME
_getdeployconf DirectAdmin_SCHEME
# set default values for DirectAdmin_SCHEME
[ -n "${DirectAdmin_SCHEME}" ] || DirectAdmin_SCHEME="https"
_info "Deploying certificate to DirectAdmin..."
# upload certificate
string_cfullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
_request_body="{\"domain\":\"$DirectAdmin_MAIN_DOMAIN\",\"action\":\"save\",\"type\":\"paste\",\"certificate\":\"$string_key\n$string_cfullchain\n\"}"
_debug _request_body "$_request_body"
_debug DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT"
_debug DirectAdmin_USERNAME "$DirectAdmin_USERNAME"
_debug DirectAdmin_KEY "$DirectAdmin_KEY"
_debug DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN"
_response=$(_post "$_request_body" "$DirectAdmin_SCHEME://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json")
if _contains "$_response" "error=1"; then
_err "Error in deploying $_cdomain certificate to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN."
_err "$_response"
return 1
fi
_info "$_response"
_info "Domain $_cdomain certificate successfully deployed to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN."
return 0
}

86
deploy/edgio.sh

@ -0,0 +1,86 @@
#!/usr/bin/env sh
# Here is a script to deploy cert to edgio using its API
# https://docs.edg.io/guides/v7/develop/rest_api/authentication
# https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts
# This deployment required following variables
# export EDGIO_CLIENT_ID="Your Edgio Client ID"
# export EDGIO_CLIENT_SECRET="Your Edgio Client Secret"
# export EDGIO_ENVIRONMENT_ID="Your Edgio Environment ID"
# If have more than one Environment ID
# export EDGIO_ENVIRONMENT_ID="ENVIRONMENT_ID_1 ENVIRONMENT_ID_2"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
edgio_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$EDGIO_CLIENT_ID" ]; then
_err "EDGIO_CLIENT_ID is not defined."
return 1
else
_savedomainconf EDGIO_CLIENT_ID "$EDGIO_CLIENT_ID"
fi
if [ -z "$EDGIO_CLIENT_SECRET" ]; then
_err "EDGIO_CLIENT_SECRET is not defined."
return 1
else
_savedomainconf EDGIO_CLIENT_SECRET "$EDGIO_CLIENT_SECRET"
fi
if [ -z "$EDGIO_ENVIRONMENT_ID" ]; then
_err "EDGIO_ENVIRONMENT_ID is not defined."
return 1
else
_savedomainconf EDGIO_ENVIRONMENT_ID "$EDGIO_ENVIRONMENT_ID"
fi
_info "Getting access token"
_data="client_id=$EDGIO_CLIENT_ID&client_secret=$EDGIO_CLIENT_SECRET&grant_type=client_credentials&scope=app.config"
_debug Get_access_token_data "$_data"
_response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded")
_debug Get_access_token_response "$_response"
_access_token=$(echo "$_response" | _json_decode | _egrep_o '"access_token":"[^"]*' | cut -d : -f 2 | tr -d '"')
_debug _access_token "$_access_token"
if [ -z "$_access_token" ]; then
_err "Error in getting access token"
return 1
fi
_info "Uploading certificate"
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
for ENVIRONMENT_ID in $EDGIO_ENVIRONMENT_ID; do
_data="{\"environment_id\":\"$ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}"
_debug Upload_certificate_data "$_data"
_H1="Authorization: Bearer $_access_token"
_response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json")
if _contains "$_response" "message"; then
_err "Error in deploying $_cdomain certificate to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID."
_err "$_response"
return 1
fi
_debug Upload_certificate_response "$_response"
_info "Domain $_cdomain certificate successfully deployed to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID."
done
return 0
}

131
deploy/keyhelp.sh

@ -0,0 +1,131 @@
#!/usr/bin/env sh
# Script to deploy certificate to KeyHelp
# This deployment required following variables
# export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com"
# export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username"
# export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password"
# export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID"
# Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID.
# https://DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=xxx
# If have more than one domain name
# export DEPLOY_KEYHELP_DOMAIN_ID="111 222 333"
keyhelp_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$DEPLOY_KEYHELP_BASEURL" ]; then
_err "DEPLOY_KEYHELP_BASEURL is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_BASEURL "$DEPLOY_KEYHELP_BASEURL"
fi
if [ -z "$DEPLOY_KEYHELP_USERNAME" ]; then
_err "DEPLOY_KEYHELP_USERNAME is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_USERNAME "$DEPLOY_KEYHELP_USERNAME"
fi
if [ -z "$DEPLOY_KEYHELP_PASSWORD" ]; then
_err "DEPLOY_KEYHELP_PASSWORD is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_PASSWORD "$DEPLOY_KEYHELP_PASSWORD"
fi
if [ -z "$DEPLOY_KEYHELP_DOMAIN_ID" ]; then
_err "DEPLOY_KEYHELP_DOMAIN_ID is not defined."
return 1
else
_savedomainconf DEPLOY_KEYHELP_DOMAIN_ID "$DEPLOY_KEYHELP_DOMAIN_ID"
fi
# Optional DEPLOY_KEYHELP_ENFORCE_HTTPS
_getdeployconf DEPLOY_KEYHELP_ENFORCE_HTTPS
# set default values for DEPLOY_KEYHELP_ENFORCE_HTTPS
[ -n "${DEPLOY_KEYHELP_ENFORCE_HTTPS}" ] || DEPLOY_KEYHELP_ENFORCE_HTTPS="1"
_info "Logging in to keyhelp panel"
username_encoded="$(printf "%s" "${DEPLOY_KEYHELP_USERNAME}" | _url_encode)"
password_encoded="$(printf "%s" "${DEPLOY_KEYHELP_PASSWORD}" | _url_encode)"
_H1="Content-Type: application/x-www-form-urlencoded"
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?submit=1&username=$username_encoded&password=$password_encoded" "TRUE")
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)"
# If cookies is not empty then logon successful
if [ -z "$_cookie" ]; then
_err "Fail to get cookie."
return 1
fi
_debug "cookie" "$_cookie"
_info "Uploading certificate"
_date=$(date +"%Y%m%d")
encoded_key="$(_url_encode <"$_ckey")"
encoded_ccert="$(_url_encode <"$_ccert")"
encoded_cca="$(_url_encode <"$_cca")"
certificate_name="$_cdomain-$_date"
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca"
_H1="Cookie: $_cookie"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST")
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to upload certificate."
return 1
fi
for DOMAIN_ID in $DEPLOY_KEYHELP_DOMAIN_ID; do
_info "Apply certificate to domain id $DOMAIN_ID"
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=$DOMAIN_ID")
cert_value=$(echo "$_response" | grep "$certificate_name" | sed -n 's/.*value="\([^"]*\).*/\1/p')
target_type=$(echo "$_response" | grep 'target_type' | grep 'checked' | sed -n 's/.*value="\([^"]*\).*/\1/p')
if [ "$target_type" = "directory" ]; then
path=$(echo "$_response" | awk '/name="path"/{getline; print}' | sed -n 's/.*value="\([^"]*\).*/\1/p')
fi
echo "$_response" | grep "is_prefer_https" | grep "checked" >/dev/null
if [ $? -eq 0 ]; then
is_prefer_https=1
else
is_prefer_https=0
fi
echo "$_response" | grep "hsts_enabled" | grep "checked" >/dev/null
if [ $? -eq 0 ]; then
hsts_enabled=1
else
hsts_enabled=0
fi
_debug "cert_value" "$cert_value"
if [ -z "$cert_value" ]; then
_err "Fail to get certificate id."
return 1
fi
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS"
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST")
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//')
_info "_message" "$_message"
if [ -z "$_message" ]; then
_err "Fail to apply certificate."
return 1
fi
done
_info "Domain $_cdomain certificate successfully deployed to KeyHelp Domain ID $DEPLOY_KEYHELP_DOMAIN_ID."
return 0
}

86
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
}

276
deploy/multideploy.sh

@ -0,0 +1,276 @@
#!/usr/bin/env sh
################################################################################
# ACME.sh 3rd party deploy plugin for multiple (same) services
################################################################################
# Authors: tomo2403 (creator), https://github.com/tomo2403
# Updated: 2025-03-01
# Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403
################################################################################
# Usage (shown values are the examples):
# 1. Set optional environment variables
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set"
#
# 2. Run command:
# acme.sh --deploy --deploy-hook multideploy -d example.com
################################################################################
# Dependencies:
# - yq
################################################################################
# Return value:
# 0 means success, otherwise error.
################################################################################
MULTIDEPLOY_VERSION="1.0"
# Description: This function handles the deployment of certificates to multiple services.
# It processes the provided certificate files and deploys them according to the
# configuration specified in the multideploy file.
#
# Parameters:
# _cdomain - The domain name for which the certificate is issued.
# _ckey - The private key file for the certificate.
# _ccert - The certificate file.
# _cca - The CA (Certificate Authority) file.
# _cfullchain - The full chain certificate file.
# _cpfx - The PFX (Personal Information Exchange) file.
multideploy_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_debug _cpfx "$_cpfx"
MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}"
if [ -z "$MULTIDEPLOY_FILENAME" ]; then
MULTIDEPLOY_FILENAME="multideploy.yml"
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'."
else
_savedeployconf "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME"
_debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME"
fi
if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then
_err "Failed to preprocess deploy file."
return 1
fi
_debug3 "File" "$file"
# Deploy to services
_deploy_services "$file"
_exitCode="$?"
return "$_exitCode"
}
# Description:
# This function preprocesses the deploy file by checking if 'yq' is installed,
# verifying the existence of the deploy file, and ensuring only one deploy file is present.
# Arguments:
# $@ - Posible deploy file names.
# Usage:
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?"
_preprocess_deployfile() {
# Check if yq is installed
if ! command -v yq >/dev/null 2>&1; then
_err "yq is not installed! Please install yq and try again."
return 1
fi
_debug3 "yq is installed."
# Check if deploy file exists
for file in "$@"; do
_debug3 "Checking file" "$DOMAIN_PATH/$file"
if [ -f "$DOMAIN_PATH/$file" ]; then
_debug3 "File found"
if [ -n "$found_file" ]; then
_err "Multiple deploy files found. Please keep only one deploy file."
return 1
fi
found_file="$file"
else
_debug3 "File not found"
fi
done
if [ -z "$found_file" ]; then
_err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one."
return 1
fi
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file"
return 1
fi
echo "$DOMAIN_PATH/$found_file"
}
# Description:
# This function checks the deploy file for version compatibility and the existence of the specified configuration and services.
# Arguments:
# $1 - The path to the deploy configuration file.
# $2 - The name of the deploy configuration to use.
# Usage:
# _check_deployfile "<deploy_file_path>"
_check_deployfile() {
_deploy_file="$1"
_debug2 "check: Deploy file" "$_deploy_file"
# Check version
_deploy_file_version=$(yq -r '.version' "$_deploy_file")
if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then
_err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version."
return 1
fi
_debug2 "check: Deploy file version is compatible: $_deploy_file_version"
# Extract all services from config
_services=$(yq -r '.services[].name' "$_deploy_file")
if [ -z "$_services" ]; then
_err "Config does not have any services to deploy to."
return 1
fi
_debug2 "check: Config has services."
echo "$_services" | while read -r _service; do
_debug3 " - $_service"
done
# Check if extracted services exist in services list
echo "$_services" | while read -r _service; do
_debug2 "check: Checking service: $_service"
# Check if service exists
_service_config=$(yq -r ".services[] | select(.name == \"$_service\")" "$_deploy_file")
if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then
_err "Service '$_service' not found."
return 1
fi
_service_hook=$(echo "$_service_config" | yq -r ".hook" -)
if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then
_err "Service '$_service' does not have a hook."
return 1
fi
_service_environment=$(echo "$_service_config" | yq -r ".environment" -)
if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then
_err "Service '$_service' does not have an environment."
return 1
fi
done
}
# Description: This function takes a list of environment variables in YAML format,
# parses them, and exports each key-value pair as environment variables.
# Arguments:
# $1 - A string containing the list of environment variables in YAML format.
# Usage:
# _export_envs "$env_list"
_export_envs() {
_env_list="$1"
_secure_debug3 "Exporting envs" "$_env_list"
echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do
# Using eval to expand nested variables in the configuration file
_value=$(eval 'echo "'"$_value"'"')
_savedeployconf "$_key" "$_value"
_secure_debug3 "Saved $_key" "$_value"
done
}
# Description:
# This function takes a YAML formatted string of environment variables, parses it,
# and clears each environment variable. It logs the process of clearing each variable.
#
# Note: Environment variables for a hook may be optional and differ between
# services using the same hook.
# If one service sets optional environment variables and another does not, the
# variables may persist and affect subsequent deployments.
# Clearing these variables after each service ensures that only the
# environment variables explicitly specified for each service in the deploy
# file are used.
# Arguments:
# $1 - A YAML formatted string containing environment variable key-value pairs.
# Usage:
# _clear_envs "<yaml_string>"
_clear_envs() {
_env_list="$1"
_secure_debug3 "Clearing envs" "$_env_list"
env_pairs=$(echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value')
echo "$env_pairs" | while IFS='=' read -r _key _value; do
_debug3 "Deleting key" "$_key"
_cleardomainconf "SAVED_$_key"
unset -v "$_key"
done
}
# Description:
# This function deploys services listed in the deploy configuration file.
# Arguments:
# $1 - The path to the deploy configuration file.
# $2 - The list of services to deploy.
# Usage:
# _deploy_services "<deploy_file_path>" "<services_list>"
_deploy_services() {
_deploy_file="$1"
_debug3 "Deploy file" "$_deploy_file"
_tempfile=$(mktemp)
trap 'rm -f $_tempfile' EXIT
yq -r '.services[].name' "$_deploy_file" >"$_tempfile"
_debug3 "Services" "$(cat "$_tempfile")"
_failedServices=""
_failedCount=0
while read -r _service <&3; do
_debug2 "Service" "$_service"
_hook=$(yq -r ".services[] | select(.name == \"$_service\").hook" "$_deploy_file")
_envs=$(yq -r ".services[] | select(.name == \"$_service\").environment" "$_deploy_file")
_export_envs "$_envs"
if ! _deploy_service "$_service" "$_hook"; then
_failedServices="$_service, $_failedServices"
_failedCount=$((_failedCount + 1))
fi
_clear_envs "$_envs"
done 3<"$_tempfile"
_debug3 "Failed services" "$_failedServices"
_debug2 "Failed count" "$_failedCount"
if [ -n "$_failedServices" ]; then
_info "$(__red "Deployment failed") for services: $_failedServices"
else
_debug "All services deployed successfully."
fi
return "$_failedCount"
}
# Description: Deploys a service using the specified hook.
# Arguments:
# $1 - The name of the service to deploy.
# $2 - The hook to use for deployment.
# Usage:
# _deploy_service <service_name> <hook>
_deploy_service() {
_name="$1"
_hook="$2"
_debug2 "SERVICE" "$_name"
_debug2 "HOOK" "$_hook"
_info "$(__green "Deploying") to '$_name' using '$_hook'"
_deploy "$_cdomain" "$_hook"
}

69
deploy/netlify.sh

@ -0,0 +1,69 @@
#!/usr/bin/env sh
# Script to deploy certificate to Netlify
# https://docs.netlify.com/api/get-started/#authentication
# https://open-api.netlify.com/#tag/sniCertificate
# This deployment required following variables
# export Netlify_ACCESS_TOKEN="Your Netlify Access Token"
# export Netlify_SITE_ID="Your Netlify Site ID"
# If have more than one SITE ID
# export Netlify_SITE_ID="SITE_ID_1 SITE_ID_2"
# returns 0 means success, otherwise error.
######## Public functions #####################
#domain keyfile certfile cafile fullchain
netlify_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
if [ -z "$Netlify_ACCESS_TOKEN" ]; then
_err "Netlify_ACCESS_TOKEN is not defined."
return 1
else
_savedomainconf Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
fi
if [ -z "$Netlify_SITE_ID" ]; then
_err "Netlify_SITE_ID is not defined."
return 1
else
_savedomainconf Netlify_SITE_ID "$Netlify_SITE_ID"
fi
_info "Deploying certificate to Netlify..."
## upload certificate
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n')
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
for SITE_ID in $Netlify_SITE_ID; do
_request_body="{\"certificate\":\"$string_ccert\",\"key\":\"$string_key\",\"ca_certificates\":\"$string_cca\"}"
_debug _request_body "$_request_body"
_debug Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN"
export _H1="Authorization: Bearer $Netlify_ACCESS_TOKEN"
_response=$(_post "$_request_body" "https://api.netlify.com/api/v1/sites/$SITE_ID/ssl" "" "POST" "application/json")
if _contains "$_response" "\"error\""; then
_err "Error in deploying $_cdomain certificate to Netlify SITE_ID $SITE_ID."
_err "$_response"
return 1
fi
_debug response "$_response"
_info "Domain $_cdomain certificate successfully deployed to Netlify SITE_ID $SITE_ID."
done
return 0
}

23
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 ****"

10
deploy/qiniu.sh

@ -8,6 +8,8 @@
# export QINIU_CDN_DOMAIN="cdn.example.com"
# If you have more than one domain, just
# export QINIU_CDN_DOMAIN="cdn1.example.com cdn2.example.com"
# Optional: force HTTPS redirect (default: false)
# export QINIU_FORCE_HTTPS="true"
QINIU_API_BASE="https://api.qiniu.com"
@ -44,6 +46,12 @@ qiniu_deploy() {
QINIU_CDN_DOMAIN="$_cdomain"
fi
if [ -z "$QINIU_FORCE_HTTPS" ]; then
QINIU_FORCE_HTTPS="false"
else
_savedomainconf QINIU_FORCE_HTTPS "$QINIU_FORCE_HTTPS"
fi
## upload certificate
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n')
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n')
@ -69,7 +77,7 @@ qiniu_deploy() {
_debug certId "$_certId"
## update domain ssl config
update_body="{\"certid\":$_certId,\"forceHttps\":false}"
update_body="{\"certid\":$_certId,\"forceHttps\":$QINIU_FORCE_HTTPS}"
for domain in $QINIU_CDN_DOMAIN; do
update_path="/domain/$domain/httpsconf"
update_access_token="$(_make_access_token "$update_path")"

18
deploy/strongswan.sh

@ -33,7 +33,7 @@ strongswan_deploy() {
return 1
fi
_info _confdir "${_confdir}"
__deploy_cert "$@" "stroke" "${_confdir}"
__deploy_cert "stroke" "${_confdir}" "$@"
${_ipsec} reload
fi
# For modern vici mode
@ -50,7 +50,7 @@ strongswan_deploy() {
_err "no swanctl config dir is found"
return 1
fi
__deploy_cert "$@" "vici" "${_confdir}"
__deploy_cert "vici" "${_confdir}" "$@"
${_swanctl} --load-creds
fi
if [ -z "${_swanctl}" ] && [ -z "${_ipsec}" ]; then
@ -63,13 +63,13 @@ strongswan_deploy() {
#################### Private functions below ##################################
__deploy_cert() {
_cdomain="${1}"
_ckey="${2}"
_ccert="${3}"
_cca="${4}"
_cfullchain="${5}"
_swan_mode="${6}"
_confdir="${7}"
_swan_mode="${1}"
_confdir="${2}"
_cdomain="${3}"
_ckey="${4}"
_ccert="${5}"
_cca="${6}"
_cfullchain="${7}"
_debug _cdomain "${_cdomain}"
_debug _ckey "${_ckey}"
_debug _ccert "${_ccert}"

2
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:

6
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

17
dnsapi/dns_ali.sh

@ -97,12 +97,13 @@ _ali_rest() {
}
_ali_nonce() {
#_head_n 1 </dev/urandom | _digest "sha256" hex | cut -c 1-31
#Not so good...
date +"%s%N" | sed 's/%N//g'
if [ "$ACME_OPENSSL_BIN" ]; then
"$ACME_OPENSSL_BIN" rand -hex 16 2>/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'
}

2
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

4
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

4
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

139
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 <contact@efficientip.com>
'
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
}

226
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
}

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

593
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
}

501
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 <<EOF
$data
EOF
if [ -z "$HOSTUP_ZONES_CACHE" ]; then
_err "HostUp DNS API: no zones returned for the current API key."
return 1
fi
return 0
}
_hostup_lookup_zone() {
lookup_domain="$1"
_lookup_zone_id=""
_lookup_zone_domain=""
while IFS='|' read -r domain zone_id; do
[ -z "$domain" ] && continue
if [ "$domain" = "$lookup_domain" ]; then
_lookup_zone_domain="$domain"
_lookup_zone_id="$zone_id"
HOSTUP_ZONE_DOMAIN="$domain"
HOSTUP_ZONE_ID="$zone_id"
return 0
fi
done <<EOF
$HOSTUP_ZONES_CACHE
EOF
return 1
}
_hostup_find_record() {
zone_id="$1"
fqdn="$2"
txtvalue="$3"
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then
return 1
fi
HOSTUP_RECORD_ID=""
records="$(printf "%s" "$_hostup_response" | tr '{' '\n')"
while IFS= read -r line; do
# Normalize line to make TXT value matching reliable
line_clean="$(printf "%s" "$line" | tr -d '\r\n')"
line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')"
case "$line_clean" in
*'"type":"TXT"'*'"name"'*'"value"'*)
name_value="$(_hostup_json_extract "name" "$line_clean")"
record_value="$(_hostup_json_extract "value" "$line_value_clean")"
_debug "hostup_record_raw" "$record_value"
if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then
record_value="${record_value#\"}"
record_value="${record_value%\"}"
fi
if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then
record_value="${record_value#\'}"
record_value="${record_value%\'}"
fi
record_value="$(printf "%s" "$record_value" | tr -d '\r\n')"
_debug "hostup_record_value" "$record_value"
if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then
record_id="$(_hostup_json_extract "id" "$line_clean")"
if [ -n "$record_id" ]; then
HOSTUP_RECORD_ID="$record_id"
return 0
fi
fi
;;
esac
done <<EOF
$records
EOF
return 1
}
_hostup_json_extract() {
key="$1"
input="${2:-$line}"
# First try to extract quoted values (strings)
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)"
if [ -n "$quoted_match" ]; then
printf "%s" "$quoted_match" |
cut -d : -f2- |
sed 's/^"//' |
sed 's/"$//' |
sed 's/\\"/"/g'
return 0
fi
# Fallback for unquoted values (e.g., numeric IDs)
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)"
if [ -n "$unquoted_match" ]; then
printf "%s" "$unquoted_match" |
cut -d : -f2- |
tr -d '", ' |
tr -d '\r\n'
return 0
fi
return 1
}
_hostup_json_escape() {
printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
_hostup_record_key() {
zone_id="$1"
domain="$2"
safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')"
safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')"
printf "%s_%s" "$safe_zone" "$safe_domain"
}
_hostup_save_record_id() {
zone_id="$1"
domain="$2"
record_id="$3"
key="$(_hostup_record_key "$zone_id" "$domain")"
_saveaccountconf_mutable "HOSTUP_RECORD_$key" "$record_id"
}
_hostup_get_saved_record_id() {
zone_id="$1"
domain="$2"
key="$(_hostup_record_key "$zone_id" "$domain")"
_readaccountconf_mutable "HOSTUP_RECORD_$key"
}
_hostup_clear_record_id() {
zone_id="$1"
domain="$2"
key="$(_hostup_record_key "$zone_id" "$domain")"
_clearaccountconf_mutable "HOSTUP_RECORD_$key"
}
_hostup_extract_record_id() {
record_id="$(_hostup_json_extract "id" "$1")"
if [ -n "$record_id" ]; then
printf "%s" "$record_id"
return 0
fi
printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2
}
_hostup_delete_record_by_id() {
zone_id="$1"
record_id="$2"
if ! _hostup_rest "DELETE" "/dns/zones/$zone_id/records/$record_id" ""; then
return 1
fi
if ! _contains "$_hostup_response" '"success":true'; then
return 1
fi
return 0
}
_hostup_rest() {
method="$1"
route="$2"
data="$3"
_hostup_response=""
export _H1="Authorization: Bearer $HOSTUP_API_KEY"
export _H2="Content-Type: application/json"
export _H3="Accept: application/json"
if [ "$method" = "GET" ]; then
_hostup_response="$(_get "$HOSTUP_API_BASE$route")"
else
_hostup_response="$(_post "$data" "$HOSTUP_API_BASE$route" "" "$method" "application/json")"
fi
ret="$?"
unset _H1
unset _H2
unset _H3
if [ "$ret" != "0" ]; then
_err "HTTP request failed for $route"
return 1
fi
http_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug2 "HTTP status" "$http_status"
_debug2 "_hostup_response" "$_hostup_response"
case "$http_status" in
200 | 201 | 204) return 0 ;;
401)
_err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions."
return 1
;;
403)
_err "HostUp API returned 403 Forbidden. The API key lacks required DNS scopes."
return 1
;;
404)
_err "HostUp API returned 404 Not Found for $route"
return 1
;;
429)
_err "HostUp API rate limit exceeded. Please retry later."
return 1
;;
*)
_err "HostUp API request failed with status $http_status"
return 1
;;
esac
}

244
dnsapi/dns_infoblox_uddi.sh

@ -0,0 +1,244 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_infoblox_uddi_info='Infoblox UDDI
Site: Infoblox.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infoblox_uddi
Options:
Infoblox_UDDI_Key API Key for Infoblox UDDI
Infoblox_Portal URL, e.g. "csp.infoblox.com" or "csp.eu.infoblox.com"
Issues: github.com/acmesh-official/acme.sh/issues
Author: Stefan Riegel
'
Infoblox_UDDI_Api="https://"
######## Public functions #####################
#Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_infoblox_uddi_add() {
fulldomain=$1
txtvalue=$2
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
_info "Using Infoblox UDDI API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then
Infoblox_UDDI_Key=""
Infoblox_Portal=""
_err "You didn't specify the Infoblox UDDI key or server (Infoblox_UDDI_Key; Infoblox_Portal)."
_err "Please set them via EXPORT Infoblox_UDDI_Key=your_key, EXPORT Infoblox_Portal=csp.infoblox.com and try again."
return 1
fi
_saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key"
_saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal"
export _H1="Authorization: Token $Infoblox_UDDI_Key"
export _H2="Content-Type: application/json"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting existing txt records"
_infoblox_rest GET "dns/record?_filter=type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'"
_info "Adding record"
body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}"
if _infoblox_rest POST "dns/record" "$body"; then
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
elif _contains "$response" '"error"'; then
# Check if record already exists
if _contains "$response" "already exists" || _contains "$response" "duplicate"; then
_info "Already exists, OK"
return 0
else
_err "Add txt record error."
_err "Response: $response"
return 1
fi
else
_info "Added, OK"
return 0
fi
fi
_err "Add txt record error."
return 1
}
#Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_infoblox_uddi_rm() {
fulldomain=$1
txtvalue=$2
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then
_err "Credentials not found"
return 1
fi
_info "Using Infoblox UDDI API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
export _H1="Authorization: Token $Infoblox_UDDI_Key"
export _H2="Content-Type: application/json"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records to delete"
# Filter by txtvalue to support wildcard certs (multiple TXT records)
filter="type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'%20and%20rdata.text%20eq%20'$txtvalue'"
_infoblox_rest GET "dns/record?_filter=$filter"
if ! _contains "$response" '"results"'; then
_info "Don't need to remove, record not found."
return 0
fi
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
_info "Don't need to remove, record not found."
return 0
fi
# Extract UUID from the full record ID (format: dns/record/uuid)
record_uuid=$(echo "$record_id" | sed 's|.*/||')
_debug "record_uuid" "$record_uuid"
if ! _infoblox_rest DELETE "dns/record/$record_uuid"; then
_err "Delete record error."
return 1
fi
_info "Removed record successfully"
return 0
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=dns/auth_zone/xxxx-xxxx
_get_root() {
domain=$1
i=1
p=1
# Remove _acme-challenge prefix if present
domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//')
while true; do
h=$(printf "%s" "$domain_no_acme" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
# not valid
return 1
fi
# Query for the zone with both trailing dot and without
filter="fqdn%20eq%20'$h.'%20or%20fqdn%20eq%20'$h'"
if ! _infoblox_rest GET "dns/auth_zone?_filter=$filter"; then
# API error - don't continue if we get auth errors
if _contains "$response" "401" || _contains "$response" "Authorization"; then
_err "Authentication failed. Please check your Infoblox_UDDI_Key."
return 1
fi
# For other errors, continue to parent domain
p=$i
i=$((i + 1))
continue
fi
# Check if response contains results (even if empty)
if _contains "$response" '"results"'; then
# Extract zone ID - must match the pattern dns/auth_zone/...
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4)
if [ -n "$zone_id" ]; then
# Found the zone
_domain="$h"
_domain_id="$zone_id"
# Calculate subdomain
if [ "$_domain" = "$domain" ]; then
_sub_domain=""
else
_cutlength=$((${#domain} - ${#_domain} - 1))
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
fi
return 0
fi
fi
p=$i
i=$((i + 1))
done
return 1
}
# _infoblox_rest GET "dns/record?_filter=..."
# _infoblox_rest POST "dns/record" "{json body}"
# _infoblox_rest DELETE "dns/record/uuid"
_infoblox_rest() {
method=$1
ep="$2"
data="$3"
_debug "$ep"
# Ensure credentials are available (when called from _get_root)
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}"
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}"
Infoblox_UDDI_Api="https://$Infoblox_Portal/api/ddi/v1"
export _H1="Authorization: Token $Infoblox_UDDI_Key"
export _H2="Content-Type: application/json"
# Debug (masked)
_tok_len=$(printf "%s" "$Infoblox_UDDI_Key" | wc -c | tr -d ' \n')
_debug2 "Auth header set" "Token len=${_tok_len} on $Infoblox_Portal"
if [ "$method" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$Infoblox_UDDI_Api/$ep" "" "$method")"
else
response="$(_get "$Infoblox_UDDI_Api/$ep")"
fi
_ret="$?"
_debug2 response "$response"
if [ "$_ret" != "0" ]; then
_err "Error: $ep"
return 1
fi
return 0
}

15
dnsapi/dns_inwx.sh

@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx
Options:
INWX_User Username
INWX_Password Password
INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional requires oathtool)
'
# Dependencies:
@ -110,11 +111,17 @@ dns_inwx_rm() {
<string>%s</string>
</value>
</member>
<member>
<name>content</name>
<value>
<string>%s</string>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>' "$_domain" "$_sub_domain")
</methodCall>' "$_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 '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><int>[0-9]+' | _egrep_o '[0-9]+')
_record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><string>[0-9]+' | _egrep_o '[0-9]+')
_info "Deleting record"
_inwx_delete_record "$_record_id"
fi
@ -324,7 +331,7 @@ _inwx_delete_record() {
<member>
<name>id</name>
<value>
<int>%s</int>
<string>%s</string>
</value>
</member>
</struct>
@ -362,7 +369,7 @@ _inwx_update_record() {
<member>
<name>id</name>
<value>
<int>%s</int>
<string>%s</string>
</value>
</member>
</struct>

109
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 <credentials_encoded>" # 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
}

62
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
}

45
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 <kholin+acme.omglolapi@omg.lol>
'
@ -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

2
dnsapi/dns_ovh.sh

@ -201,7 +201,7 @@ dns_ovh_rm() {
if ! _ovh_rest GET "domain/zone/$_domain/record/$rid"; then
return 1
fi
if _contains "$response" "\"target\":\"$txtvalue\""; then
if _contains "$response" "$txtvalue"; then
_debug "Found txt id:$rid"
if ! _ovh_rest DELETE "domain/zone/$_domain/record/$rid"; then
return 1

216
dnsapi/dns_qc.sh

@ -0,0 +1,216 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_qc_info='QUIC.cloud
Site: quic.cloud
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_qc
Options:
QC_API_KEY QC API Key
QC_API_EMAIL Your account email
'
QC_Api="https://api.quic.cloud/v2"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_qc_add() {
fulldomain=$1
txtvalue=$2
_debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue"
QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}"
QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}"
if [ "$QC_API_KEY" ]; then
_saveaccountconf_mutable QC_API_KEY "$QC_API_KEY"
else
_err "You didn't specify a QUIC.cloud api key as QC_API_KEY."
_err "You can get yours from here https://my.quic.cloud/up/api."
return 1
fi
if ! _contains "$QC_API_EMAIL" "@"; then
_err "It seems that the QC_API_EMAIL=$QC_API_EMAIL is not a valid email address."
_err "Please check and retry."
return 1
fi
#save the api key and email to the account conf file.
_saveaccountconf_mutable QC_API_EMAIL "$QC_API_EMAIL"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain during add"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records"
_qc_rest GET "zones/${_domain_id}/records"
if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
_err "Error failed response from QC GET: $response"
return 1
fi
# For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
# we can not use updating anymore.
# count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
# _debug count "$count"
# if [ "$count" = "0" ]; then
_info "Adding txt record"
if _qc_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":1800}"; then
if _contains "$response" "$txtvalue"; then
_info "Added txt record, OK"
return 0
elif _contains "$response" "Same record already exists"; then
_info "txt record already exists, OK"
return 0
else
_err "Add txt record error: $response"
return 1
fi
fi
_err "Add txt record error: POST failed: $response"
return 1
}
#fulldomain txtvalue
dns_qc_rm() {
fulldomain=$1
txtvalue=$2
_debug "Enter dns_qc_rm fulldomain: $fulldomain, txtvalue: $txtvalue"
QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}"
QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain during rm"
return 1
fi
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
_debug "Getting txt records"
_qc_rest GET "zones/${_domain_id}/records"
if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then
_err "Error rm GET response: $response"
return 1
fi
_debug "Pre-jq response:" "$response"
# Do not use jq or subsequent code
#response=$(echo "$response" | jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")")
#_debug "get txt response" "$response"
#if [ "${response}" = "" ]; then
# _info "Don't need to remove txt records."
# return 0
#fi
#record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//')
#_debug "txt record_id" "$record_id"
#Instead of jq
array=$(echo "$response" | grep -o '\[[^]]*\]' | sed 's/^\[\(.*\)\]$/\1/')
if [ -z "$array" ]; then
_err "Expected array in QC response: $response"
return 1
fi
# Temporary file to hold matched content (one per line)
tmpfile=$(_mktemp)
echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile"
record_id=""
while IFS= read -r obj || [ -n "$obj" ]; do
if echo "$obj" | grep -q '"TXT"' && echo "$obj" | grep -q '"id"' && echo "$obj" | grep -q "$txtvalue"; then
_debug "response includes" "$obj"
record_id=$(echo "$obj" | sed 's/^\"id\":\([0-9]\+\).*/\1/')
break
fi
done <"$tmpfile"
rm "$tmpfile"
if [ -z "$record_id" ]; then
_info "TXT record, or $txtvalue not found, nothing to remove"
return 0
fi
#End of jq replacement
if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then
_info "Delete txt record error."
return 1
fi
_info "TXT Record ID: $record_id successfully deleted"
return 0
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=sdjkglgdfewsdfg
_get_root() {
domain=$1
i=1
p=1
h=$(printf "%s" "$domain" | cut -d . -f2-)
_debug h "$h"
if [ -z "$h" ]; then
_err "$h ($domain) is an invalid domain"
return 1
fi
if ! _qc_rest GET "zones"; then
_err "qc_rest failed"
return 1
fi
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" "\"name\":\"$h.\""; then
_domain_id=$h
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
return 0
fi
_err "Empty domain_id $h"
return 1
fi
_err "Missing domain_id $h"
return 1
}
_qc_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
email_trimmed=$(echo "$QC_API_EMAIL" | tr -d '"')
token_trimmed=$(echo "$QC_API_KEY" | tr -d '"')
export _H1="Content-Type: application/json"
export _H2="X-Auth-Email: $email_trimmed"
export _H3="X-Auth-Key: $token_trimmed"
if [ "$m" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$QC_Api/$ep" "" "$m")"
else
response="$(_get "$QC_Api/$ep")"
fi
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

309
dnsapi/dns_sotoon.sh

@ -0,0 +1,309 @@
#!/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
Issues: github.com/acmesh-official/acme.sh/issues/6656
Author: Erfan Gholizade
'
SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/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)}"
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
#save the info to the account conf file.
_saveaccountconf_mutable Sotoon_Token "$Sotoon_Token"
_saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID"
_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)}"
_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"
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"
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/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]" "$@"
}

229
dnsapi/dns_virakcloud.sh

@ -0,0 +1,229 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_virakcloud_info='VirakCloud DNS API
Site: VirakCloud.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_virakcloud
Options:
VIRAKCLOUD_API_TOKEN VirakCloud API Bearer Token
'
VIRAKCLOUD_API_URL="https://public-api.virakcloud.com/dns"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
#Used to add txt record
dns_virakcloud_add() {
fulldomain=$1
txtvalue=$2
VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}"
if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then
_err "You haven't configured your VirakCloud API token yet."
_err "Please set VIRAKCLOUD_API_TOKEN environment variable or run:"
_err " export VIRAKCLOUD_API_TOKEN=\"your-api-token\""
return 1
fi
_saveaccountconf_mutable VIRAKCLOUD_API_TOKEN "$VIRAKCLOUD_API_TOKEN"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ "$http_code" = "401" ]; then
return 1
fi
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug fulldomain "$fulldomain"
_info "Adding TXT record"
if _virakcloud_rest POST "domains/${_domain}/records" "{\"record\":\"${fulldomain}\",\"type\":\"TXT\",\"ttl\":3600,\"content\":\"${txtvalue}\"}"; then
if echo "$response" | grep -q "success" || echo "$response" | grep -q "\"data\""; then
_info "Added, OK"
return 0
elif echo "$response" | grep -q "already exists" || echo "$response" | grep -q "duplicate"; then
_info "Record already exists, OK"
return 0
else
_err "Add TXT record error."
_err "Response: $response"
return 1
fi
fi
_err "Add TXT record error."
return 1
}
#Usage: fulldomain txtvalue
#Used to remove the txt record after validation
dns_virakcloud_rm() {
fulldomain=$1
txtvalue=$2
VIRAKCLOUD_API_TOKEN="${VIRAKCLOUD_API_TOKEN:-$(_readaccountconf_mutable VIRAKCLOUD_API_TOKEN)}"
if [ -z "$VIRAKCLOUD_API_TOKEN" ]; then
_err "You haven't configured your VirakCloud API token yet."
return 1
fi
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ "$http_code" = "401" ]; then
return 1
fi
_err "Invalid domain"
return 1
fi
_debug _domain "$_domain"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
_info "Removing TXT record"
_debug "Getting list of records to find content ID"
if ! _virakcloud_rest GET "domains/${_domain}/records" ""; then
return 1
fi
_debug2 "Records response" "$response"
contentid=""
# Extract innermost objects (content objects) which look like {"id":"...","content_raw":"..."}
# We filter for the one containing txtvalue
target_obj=$(echo "$response" | grep -o '{[^}]*}' | grep "$txtvalue" | _head_n 1)
if [ -n "$target_obj" ]; then
contentid=$(echo "$target_obj" | _egrep_o '"id":"[^"]*"' | cut -d '"' -f 4)
fi
if [ -z "$contentid" ]; then
_debug "Could not find matching record ID in response"
_info "Record not found, may have been already removed"
return 0
fi
_debug contentid "$contentid"
if _virakcloud_rest DELETE "domains/${_domain}/records/${fulldomain}/TXT/${contentid}" ""; then
if echo "$response" | grep -q "success" || [ -z "$response" ]; then
_info "Removed, OK"
return 0
elif echo "$response" | grep -q "not found" || echo "$response" | grep -q "404"; then
_info "Record not found, OK"
return 0
else
_err "Remove TXT record error."
_err "Response: $response"
return 1
fi
fi
_err "Remove TXT record error."
return 1
}
#################### Private functions below ##################################
#_acme-challenge.www.domain.com
#returns
# _domain=domain.com
_get_root() {
domain=$1
i=1
p=1
# Optimization: skip _acme-challenge subdomain to avoid 422 errors
if echo "$domain" | grep -q "^_acme-challenge."; then
i=2
fi
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
if ! _virakcloud_rest GET "domains/$h" ""; then
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
if [ "$http_code" = "401" ]; then
return 1
fi
p=$i
i=$(_math "$i" + 1)
continue
fi
if echo "$response" | grep -q "\"name\""; then
_domain="$h"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}
_virakcloud_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
export _H1="Content-Type: application/json"
export _H2="Authorization: Bearer $VIRAKCLOUD_API_TOKEN"
if [ "$m" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$VIRAKCLOUD_API_URL/$ep" "" "$m")"
else
response="$(_get "$VIRAKCLOUD_API_URL/$ep")"
fi
_ret="$?"
if [ "$_ret" != "0" ]; then
_err "error on $m $ep"
return 1
fi
http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
_debug "http response code" "$http_code"
if [ "$http_code" = "401" ]; then
_err "VirakCloud API returned 401 Unauthorized."
_err "Your VIRAKCLOUD_API_TOKEN is invalid or expired."
_err "Please check your API token and try again."
return 1
fi
if [ "$http_code" = "403" ]; then
_err "VirakCloud API returned 403 Forbidden."
_err "Your API token does not have permission to access this resource."
return 1
fi
if [ -n "$http_code" ] && [ "$http_code" -ge 400 ]; then
_err "VirakCloud API error. HTTP code: $http_code"
_err "Response: $response"
return 1
fi
_debug2 response "$response"
return 0
}

28
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

130
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
}

4
notify/pushover.sh

@ -46,8 +46,8 @@ pushover_send() {
fi
export _H1="Content-Type: application/json"
_content="$(printf "*%s*\n" "$_content" | _json_encode)"
_subject="$(printf "*%s*\n" "$_subject" | _json_encode)"
_content="$(printf "%s" "$_content" | _json_encode)"
_subject="$(printf "%s" "$_subject" | _json_encode)"
_data="{\"token\": \"$PUSHOVER_TOKEN\",\"user\": \"$PUSHOVER_USER\",\"title\": \"$_subject\",\"message\": \"$_content\",\"sound\": \"$PUSHOVER_SOUND\", \"device\": \"$PUSHOVER_DEVICE\", \"priority\": \"$PUSHOVER_PRIORITY\"}"
response="$(_post "$_data" "$PUSHOVER_URI")"

4
notify/telegram.sh

@ -34,8 +34,8 @@ telegram_send() {
fi
_saveaccountconf_mutable TELEGRAM_BOT_URLBASE "$TELEGRAM_BOT_URLBASE"
_subject="$(printf "%s" "$_subject" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([-_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')"
_content="$(printf "%s" "$_content" | sed 's/\\/\\\\\\\\/g' | sed 's/\]/\\\\\]/g' | sed 's/\([-_*[()~`>#+\-=|{}.!]\)/\\\\\1/g')"
_subject="$(printf "%s" "$_subject" | sed -E 's/([][()~`>#+=|{}.!*_\\-])/\\\\\1/g')"
_content="$(printf "%s" "$_content" | sed -E 's/([][()~`>#+=|{}.!*_\\-])/\\\\\1/g')"
_content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)"
_data="{\"text\": \"$_content\", "
_data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", "

Loading…
Cancel
Save