Browse Source

Merge branch 'acmesh-official:master' into master

pull/6173/head
Gianluca Giacometti 5 days ago
committed by GitHub
parent
commit
f5de201069
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 165
      .github/workflows/DNS.yml
  2. 8
      .github/workflows/DragonFlyBSD.yml
  3. 8
      .github/workflows/FreeBSD.yml
  4. 83
      .github/workflows/Haiku.yml
  5. 2
      .github/workflows/Linux.yml
  6. 10
      .github/workflows/NetBSD.yml
  7. 8
      .github/workflows/Omnios.yml
  8. 8
      .github/workflows/OpenBSD.yml
  9. 79
      .github/workflows/OpenIndiana.yml
  10. 2
      .github/workflows/PebbleStrict.yml
  11. 12
      .github/workflows/Solaris.yml
  12. 2
      .github/workflows/dockerhub.yml
  13. 15
      .github/workflows/pr_dns.yml
  14. 1
      .github/workflows/pr_notify.yml
  15. 66
      .github/workflows/wiki-monitor.yml
  16. 21
      Dockerfile
  17. 452
      README.md
  18. 410
      acme.sh
  19. 2
      deploy/ali_cdn.sh
  20. 2
      deploy/ali_dcdn.sh
  21. 56
      deploy/cachefly.sh
  22. 86
      deploy/directadmin.sh
  23. 13
      deploy/docker.sh
  24. 86
      deploy/edgio.sh
  25. 2
      deploy/haproxy.sh
  26. 98
      deploy/kemplm.sh
  27. 131
      deploy/keyhelp.sh
  28. 86
      deploy/keyhelp_api.sh
  29. 276
      deploy/multideploy.sh
  30. 69
      deploy/netlify.sh
  31. 102
      deploy/panos.sh
  32. 130
      deploy/proxmoxbs.sh
  33. 12
      deploy/proxmoxve.sh
  34. 10
      deploy/qiniu.sh
  35. 4
      deploy/routeros.sh
  36. 28
      deploy/ruckus.sh
  37. 18
      deploy/strongswan.sh
  38. 8
      deploy/synology_dsm.sh
  39. 2
      deploy/truenas.sh
  40. 343
      deploy/truenas_ws.sh
  41. 93
      deploy/unifi.sh
  42. 98
      deploy/vault.sh
  43. 500
      deploy/zyxel_gs1900.sh
  44. 4
      dnsapi/dns_1984hosting.sh
  45. 160
      dnsapi/dns_active24.sh
  46. 17
      dnsapi/dns_ali.sh
  47. 2
      dnsapi/dns_aws.sh
  48. 15
      dnsapi/dns_azure.sh
  49. 281
      dnsapi/dns_beget.sh
  50. 2
      dnsapi/dns_bookmyname.sh
  51. 4
      dnsapi/dns_cf.sh
  52. 5
      dnsapi/dns_cloudns.sh
  53. 5
      dnsapi/dns_constellix.sh
  54. 4
      dnsapi/dns_curanet.sh
  55. 53
      dnsapi/dns_cyon.sh
  56. 2
      dnsapi/dns_ddnss.sh
  57. 2
      dnsapi/dns_dnshome.sh
  58. 2
      dnsapi/dns_duckdns.sh
  59. 2
      dnsapi/dns_dyn.sh
  60. 16
      dnsapi/dns_dynv6.sh
  61. 2
      dnsapi/dns_easydns.sh
  62. 163
      dnsapi/dns_edgecenter.sh
  63. 139
      dnsapi/dns_efficientip.sh
  64. 226
      dnsapi/dns_exoscale.sh
  65. 2
      dnsapi/dns_fornex.sh
  66. 2
      dnsapi/dns_freedns.sh
  67. 105
      dnsapi/dns_freemyip.sh
  68. 10
      dnsapi/dns_gandi_livedns.sh
  69. 45
      dnsapi/dns_he_ddns.sh
  70. 2
      dnsapi/dns_hetzner.sh
  71. 593
      dnsapi/dns_hetznercloud.sh
  72. 501
      dnsapi/dns_hostup.sh
  73. 244
      dnsapi/dns_infoblox_uddi.sh
  74. 15
      dnsapi/dns_inwx.sh
  75. 2
      dnsapi/dns_joker.sh
  76. 96
      dnsapi/dns_la.sh
  77. 18
      dnsapi/dns_limacity.sh
  78. 109
      dnsapi/dns_mgwm.sh
  79. 214
      dnsapi/dns_mijnhost.sh
  80. 10
      dnsapi/dns_myapi.sh
  81. 2
      dnsapi/dns_mydnsjp.sh
  82. 2
      dnsapi/dns_namecom.sh
  83. 2
      dnsapi/dns_namesilo.sh
  84. 62
      dnsapi/dns_nanelo.sh
  85. 6
      dnsapi/dns_netcup.sh
  86. 45
      dnsapi/dns_omglol.sh
  87. 1
      dnsapi/dns_openprovider.sh
  88. 186
      dnsapi/dns_openprovider_rest.sh
  89. 30
      dnsapi/dns_opnsense.sh
  90. 2
      dnsapi/dns_ovh.sh
  91. 2
      dnsapi/dns_pdns.sh
  92. 2
      dnsapi/dns_pleskxml.sh
  93. 216
      dnsapi/dns_qc.sh
  94. 18
      dnsapi/dns_rage4.sh
  95. 2
      dnsapi/dns_schlundtech.sh
  96. 447
      dnsapi/dns_selectel.sh
  97. 309
      dnsapi/dns_sotoon.sh
  98. 212
      dnsapi/dns_spaceship.sh
  99. 7
      dnsapi/dns_technitium.sh
  100. 2
      dnsapi/dns_tele3.sh

165
.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,70 @@ 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"
Haiku:
runs-on: ubuntu-latest
needs: OpenIndiana
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
CASE: le_test_dnsapi
TEST_LOCAL: 1
DEBUG: ${{ secrets.DEBUG }}
http_proxy: ${{ secrets.http_proxy }}
https_proxy: ${{ secrets.https_proxy }}
HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since 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/haiku-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: rsync
copyback: false
prepare: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
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"

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"

83
.github/workflows/Haiku.yml

@ -0,0 +1,83 @@
name: Haiku
on:
push:
branches:
- '*'
paths:
- '*.sh'
- '.github/workflows/Haiku.yml'
pull_request:
branches:
- dev
paths:
- '*.sh'
- '.github/workflows/Haiku.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
Haiku:
strategy:
fail-fast: false
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/haiku-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: |
mkdir -p /boot/home/.cache
pkgman install -y cronie
sync: rsync
copyback: false
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/Linux.yml

@ -26,7 +26,7 @@ jobs:
Linux:
strategy:
matrix:
os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "mageia", "gentoo/stage3"]
os: ["ubuntu:latest", "debian:latest", "almalinux:latest", "fedora:latest", "opensuse/leap:latest", "alpine:latest", "oraclelinux:8", "kalilinux/kali", "archlinux:latest", "gentoo/stage3"]
runs-on: ubuntu-latest
env:
TEST_LOCAL: 1

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"

2
.github/workflows/dockerhub.yml

@ -44,6 +44,8 @@ jobs:
steps:
- name: checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Extract Docker metadata

15
.github/workflows/pr_dns.yml

@ -11,6 +11,7 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v6
with:
@ -20,12 +21,14 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
body: `**Welcome**
First thing: don't send PR to the master branch, please send to the dev branch instead.
Please make sure you've read our [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide) and [DNS-API-Test](../wiki/DNS-API-Test).
Then reply on this message, otherwise, your code will not be reviewed or merged.
Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
We look forward to reviewing your Pull request shortly ✨
注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
READ ME !!!!!
Read me !!!!!!
First thing: don't send PR to the master branch, please send to the dev branch instead.
Please read the [DNS API Dev Guide](../wiki/DNS-API-Dev-Guide).
You MUST pass the [DNS-API-Test](../wiki/DNS-API-Test).
Then reply on this message, otherwise, your code will not be reviewed or merged.
Please also make sure to add/update the usage here: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2
注意: 必须通过了 [DNS-API-Test](../wiki/DNS-API-Test) 才会被 review. 无论是修改, 还是新加的 dns api, 都必须确保通过这个测试.
`
})

1
.github/workflows/pr_notify.yml

@ -13,6 +13,7 @@ on:
jobs:
welcome:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- uses: actions/github-script@v6
with:

66
.github/workflows/wiki-monitor.yml

@ -0,0 +1,66 @@
name: Notify via Issue on Wiki Edit
on:
gollum:
jobs:
notify:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- name: Checkout wiki repository
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}.wiki
path: wiki
fetch-depth: 0
- name: Generate wiki change message
run: |
actor="${{ github.actor }}"
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH")
page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH")
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
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1)
if [ -n "$prev_sha" ]; then
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff
else
echo "(no diff)" > ../wiki.diff
fi
cd ..
{
echo "Wiki edited"
echo -n "User: "
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:"
echo '```diff'
cat wiki.diff
echo '```'
} > wiki-change-msg.txt
- name: Create issue to notify Neilpang
uses: peter-evans/create-issue-from-file@v5
with:
title: "Wiki edited"
content-filepath: ./wiki-change-msg.txt
assignees: Neilpang
env:
TZ: Asia/Shanghai

21
Dockerfile

@ -1,4 +1,4 @@
FROM alpine:3.17
FROM alpine:3.22
RUN apk --no-cache add -f \
openssl \
@ -13,20 +13,27 @@ RUN apk --no-cache add -f \
tar \
libidn \
jq \
yq-go \
cronie
ENV LE_CONFIG_HOME /acme.sh
ENV LE_WORKING_DIR=/acmebin
ENV LE_CONFIG_HOME=/acme.sh
ARG AUTO_UPGRADE=1
ENV AUTO_UPGRADE $AUTO_UPGRADE
ENV AUTO_UPGRADE=$AUTO_UPGRADE
#Install
COPY ./ /install_acme.sh/
COPY ./acme.sh /install_acme.sh/acme.sh
COPY ./deploy /install_acme.sh/deploy
COPY ./dnsapi /install_acme.sh/dnsapi
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 \
@ -60,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 \
@ -68,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

452
README.md

@ -1,52 +1,72 @@
# 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>
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></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 +80,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 +96,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 +105,67 @@ 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)
|24|[![Haiku](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg)](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
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
- [BuyPass.com CA](https://github.com/acmesh-official/acme.sh/wiki/BuyPass.com-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)
- [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 +175,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 +192,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 +239,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 +262,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 +276,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 +388,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 +465,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 +551,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>

410
acme.sh

@ -1,6 +1,6 @@
#!/usr/bin/env sh
VER=3.1.0
VER=3.1.3
PROJECT_NAME="acme.sh"
@ -23,9 +23,6 @@ _SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY"
CA_LETSENCRYPT_V2="https://acme-v02.api.letsencrypt.org/directory"
CA_LETSENCRYPT_V2_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
CA_BUYPASS="https://api.buypass.com/acme/directory"
CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory"
CA_ZEROSSL="https://acme.zerossl.com/v2/DV90"
_ZERO_EAB_ENDPOINT="https://api.zerossl.com/acme/eab-credentials-email"
@ -35,6 +32,8 @@ CA_SSLCOM_ECC="https://acme.ssl.com/sslcom-dv-ecc"
CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory"
CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory"
CA_ACTALIS="https://acme-api.actalis.com/acme/directory"
DEFAULT_CA=$CA_ZEROSSL
DEFAULT_STAGING_CA=$CA_LETSENCRYPT_V2_TEST
@ -42,14 +41,13 @@ CA_NAMES="
ZeroSSL.com,zerossl
LetsEncrypt.org,letsencrypt
LetsEncrypt.org_test,letsencrypt_test,letsencrypttest
BuyPass.com,buypass
BuyPass.com_test,buypass_test,buypasstest
SSL.com,sslcom
Google.com,google
Google.com_test,googletest,google_test
Actalis.com,actalis.com,actalis
"
CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_BUYPASS,$CA_BUYPASS_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST"
CA_SERVERS="$CA_ZEROSSL,$CA_LETSENCRYPT_V2,$CA_LETSENCRYPT_V2_TEST,$CA_SSLCOM_RSA,$CA_GOOGLE,$CA_GOOGLE_TEST,$CA_ACTALIS"
DEFAULT_USER_AGENT="$PROJECT_NAME/$VER ($PROJECT)"
@ -67,7 +65,7 @@ ID_TYPE_IP="ip"
LOCAL_ANY_ADDRESS="0.0.0.0"
DEFAULT_RENEW=60
DEFAULT_RENEW=30
NO_VALUE="no"
@ -180,6 +178,8 @@ _VALIDITY_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Validity"
_DNSCHECK_WIKI="https://github.com/acmesh-official/acme.sh/wiki/dnscheck"
_PROFILESELECTION_WIKI="https://github.com/acmesh-official/acme.sh/wiki/Profile-selection"
_DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead."
_DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR"
@ -250,6 +250,13 @@ _dlg_versions() {
socat -V 2>&1
else
_debug "socat doesn't exist."
if _exists "python3"; then
python3 -V 2>&1
elif _exists "python2"; then
python2 -V 2>&1
elif _exists "python"; then
python -V 2>&1
fi
fi
}
@ -436,14 +443,28 @@ _secure_debug3() {
fi
}
__USE_TR_TAG=""
if [ "$(echo "abc" | LANG=C tr a-z A-Z 2>/dev/null)" != "ABC" ]; then
__USE_TR_TAG="1"
fi
export __USE_TR_TAG
_upper_case() {
# shellcheck disable=SC2018,SC2019
tr '[a-z]' '[A-Z]'
if [ "$__USE_TR_TAG" ]; then
LANG=C tr '[:lower:]' '[:upper:]'
else
# shellcheck disable=SC2018,SC2019
LANG=C tr '[a-z]' '[A-Z]'
fi
}
_lower_case() {
# shellcheck disable=SC2018,SC2019
tr '[A-Z]' '[a-z]'
if [ "$__USE_TR_TAG" ]; then
LANG=C tr '[:upper:]' '[:lower:]'
else
# shellcheck disable=SC2018,SC2019
LANG=C tr '[A-Z]' '[a-z]'
fi
}
_startswith() {
@ -921,6 +942,9 @@ _sed_i() {
if sed -h 2>&1 | grep "\-i\[SUFFIX]" >/dev/null 2>&1; then
_debug "Using sed -i"
sed -i "$options" "$filename"
elif sed -h 2>&1 | grep "\-i extension" >/dev/null 2>&1; then
_debug "Using FreeBSD sed -i"
sed -i "" "$options" "$filename"
else
_debug "No -i support in sed"
text="$(cat "$filename")"
@ -1014,7 +1038,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
@ -1233,7 +1257,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"
@ -1242,6 +1266,7 @@ _createcsr() {
csr="$4"
csrconf="$5"
acmeValidationv1="$6"
extusage="$7"
_debug2 domain "$domain"
_debug2 domainlist "$domainlist"
_debug2 csrkey "$csrkey"
@ -1250,9 +1275,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
@ -1398,6 +1422,12 @@ _ss() {
return 0
fi
if [ "$(uname)" = "AIX" ]; then
_debug "Using: AIX netstat"
netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port "
return 0
fi
if _exists "netstat"; then
_debug "Using: netstat"
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
@ -1443,7 +1473,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
}
@ -1802,6 +1832,10 @@ _time() {
# 2022-04-01 08:10:33 to 1648800633
#or 2022-04-01T08:10:33Z to 1648800633
_date2time() {
#Mac/BSD
if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
fi
#Linux
if date -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
@ -1811,10 +1845,6 @@ _date2time() {
if gdate -u -d "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
fi
#Mac/BSD
if date -u -j -f "%Y-%m-%d %H:%M:%S" "$(echo "$1" | tr -d "Z" | tr "T" ' ')" +"%s" 2>/dev/null; then
return
fi
#Omnios
if python3 -c "import datetime; print(int(datetime.datetime.strptime(\"$1\", \"%Y-%m-%d %H:%M:%S\").replace(tzinfo=datetime.timezone.utc).timestamp()))" 2>/dev/null; then
return
@ -1874,6 +1904,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
@ -1901,6 +1936,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
@ -2318,6 +2358,7 @@ _setopt() {
fi
if [ ! -f "$__conf" ]; then
touch "$__conf"
chmod 600 "$__conf"
fi
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
echo >>"$__conf"
@ -2526,37 +2567,76 @@ _startserver() {
_debug Le_Listen_V4 "$Le_Listen_V4"
_debug Le_Listen_V6 "$Le_Listen_V6"
_NC="socat"
if [ "$Le_Listen_V6" ]; then
_NC="$_NC -6"
else
_NC="$_NC -4"
fi
if _exists "socat"; then
_NC="socat"
if [ "$Le_Listen_V6" ]; then
_NC="$_NC -6"
SOCAT_OPTIONS=TCP6-LISTEN
elif [ "$Le_Listen_V4" ]; then
_NC="$_NC -4"
SOCAT_OPTIONS=TCP4-LISTEN
else
SOCAT_OPTIONS=TCP-LISTEN
fi
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
_NC="$_NC -d -d -v"
fi
if [ "$DEBUG" ] && [ "$DEBUG" -gt "1" ]; then
_NC="$_NC -d -d -v"
fi
SOCAT_OPTIONS=TCP-LISTEN:$Le_HTTPPort,crlf,reuseaddr,fork
SOCAT_OPTIONS=$SOCAT_OPTIONS:$Le_HTTPPort,crlf,reuseaddr,fork
#Adding bind to local-address
if [ "$ncaddr" ]; then
SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}"
fi
#Adding bind to local-address
if [ "$ncaddr" ]; then
SOCAT_OPTIONS="$SOCAT_OPTIONS,bind=${ncaddr}"
fi
_content_len="$(printf "%s" "$content" | wc -c)"
_debug _content_len "$_content_len"
_debug "_NC" "$_NC $SOCAT_OPTIONS"
export _SOCAT_ERR="$(_mktemp)"
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
_content_len="$(printf "%s" "$content" | wc -c)"
_debug _content_len "$_content_len"
_debug "_NC" "$_NC $SOCAT_OPTIONS"
export _SOCAT_ERR="$(_mktemp)"
$_NC $SOCAT_OPTIONS SYSTEM:"sleep 1; \
echo 'HTTP/1.0 200 OK'; \
echo 'Content-Length\: $_content_len'; \
echo ''; \
printf '%s' '$content';" 2>"$_SOCAT_ERR" &
serverproc="$!"
serverproc="$!"
else
_PYTHON=""
if _exists "python3"; then
_PYTHON="python3"
elif _exists "python2"; then
_PYTHON="python2"
elif _exists "python"; then
_PYTHON="python"
fi
if [ "$_PYTHON" ]; then
_debug "Using python: $_PYTHON"
_AF="socket.AF_INET"
_BIND_ADDR="0.0.0.0"
if [ "$Le_Listen_V6" ]; then
_AF="socket.AF_INET6"
_BIND_ADDR="::"
fi
if [ "$ncaddr" ]; then
_BIND_ADDR="$ncaddr"
fi
export _SOCAT_ERR="$(_mktemp)"
$_PYTHON -c "import socket,sys;s=socket.socket($_AF,socket.SOCK_STREAM);s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1);s.bind((sys.argv[2],int(sys.argv[1])));s.listen(5);res='HTTP/1.0 200 OK\r\nContent-Length: '+str(len(sys.argv[3]))+'\r\n\r\n'+sys.argv[3];
while True:
c,a=s.accept()
c.sendall(res.encode() if hasattr(res, 'encode') else res)
c.close()" "$Le_HTTPPort" "$_BIND_ADDR" "$content" 2>"$_SOCAT_ERR" &
serverproc="$!"
_NC="$_PYTHON"
else
_err "Please install socat or python first for standalone mode."
return 1
fi
fi
if [ -f "$_SOCAT_ERR" ]; then
if grep "Permission denied" "$_SOCAT_ERR" >/dev/null; then
_err "socat: $(cat $_SOCAT_ERR)"
_err "$_NC: $(cat $_SOCAT_ERR)"
_err "Can not listen for user: $(whoami)"
_err "Maybe try with root again?"
rm -f "$_SOCAT_ERR"
@ -2746,6 +2826,7 @@ _clearAPI() {
ACME_REVOKE_CERT=""
ACME_NEW_NONCE=""
ACME_AGREEMENT=""
ACME_RENEWAL_INFO=""
}
#server
@ -2758,7 +2839,7 @@ _initAPI() {
_request_retry_times=0
while [ -z "$ACME_NEW_ACCOUNT" ] && [ "${_request_retry_times}" -lt "$MAX_API_RETRY_TIMES" ]; do
_request_retry_times=$(_math "$_request_retry_times" + 1)
response=$(_get "$_api_server")
response=$(_get "$_api_server" "" 10)
if [ "$?" != "0" ]; then
_debug2 "response" "$response"
_info "Cannot init API for: $_api_server."
@ -2790,6 +2871,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"
@ -2797,6 +2881,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
@ -3504,7 +3589,7 @@ _on_before_issue() {
_debug _chk_alt_domains "$_chk_alt_domains"
#run pre hook
if [ "$_chk_pre_hook" ]; then
_info "Runing pre hook:'$_chk_pre_hook'"
_info "Running pre hook:'$_chk_pre_hook'"
if ! (
export Le_Domain="$_chk_main_domain"
export Le_Alt="$_chk_alt_domains"
@ -3515,9 +3600,9 @@ _on_before_issue() {
fi
fi
if _hasfield "$_chk_web_roots" "$NO_VALUE"; then
if ! _exists "socat"; then
_err "Please install socat tools first."
if _hasfield "$_chk_web_roots" "$NO_VALUE" && [ "$_chk_web_roots" = "$NO_VALUE" ]; then
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
_err "Please install socat or python tools first."
return 1
fi
fi
@ -4407,6 +4492,8 @@ issue() {
_preferred_chain="${15}"
_valid_from="${16}"
_valid_to="${17}"
_certificate_profile="${18}"
_extended_key_usage="${19}"
if [ -z "$_ACME_IS_RENEW" ]; then
_initpath "$_main_domain" "$_key_length"
@ -4426,7 +4513,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"
@ -4482,6 +4569,11 @@ issue() {
else
_cleardomainconf "Le_Preferred_Chain"
fi
if [ "$_certificate_profile" ]; then
_savedomainconf "Le_Certificate_Profile" "$_certificate_profile"
else
_cleardomainconf "Le_Certificate_Profile"
fi
Le_API="$ACME_DIRECTORY"
_savedomainconf "Le_API" "$Le_API"
@ -4493,6 +4585,7 @@ issue() {
if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then
_err "_on_before_issue."
_on_issue_err "$_post_hook"
return 1
fi
@ -4545,12 +4638,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"
@ -4613,6 +4719,9 @@ issue() {
if [ "$_notAfter" ]; then
_newOrderObj="$_newOrderObj,\"notAfter\": \"$_notAfter\""
fi
if [ "$_certificate_profile" ]; then
_newOrderObj="$_newOrderObj,\"profile\": \"$_certificate_profile\""
fi
_debug "STEP 1, Ordering a Certificate"
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order."
@ -4752,7 +4861,8 @@ $_authorizations_map"
_debug keyauthorization "$keyauthorization"
fi
entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
# Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018
entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
_debug entry "$entry"
if [ -z "$keyauthorization" -a -z "$entry" ]; then
@ -5002,9 +5112,11 @@ $_authorizations_map"
_debug "Writing token: $token to $wellknown_path/$token"
mkdir -p "$wellknown_path"
if ! printf "%s" "$keyauthorization" >"$wellknown_path/$token"; then
# Ensure .well-known is visible to web server user/group
# https://github.com/Neilpang/acme.sh/pull/32
if ! (umask ugo+rx &&
mkdir -p "$wellknown_path" &&
printf "%s" "$keyauthorization" >"$wellknown_path/$token"); then
_err "$d: Cannot write token to file: $wellknown_path/$token"
_clearupwebbroot "$_currentRoot" "$removelevel" "$token"
_clearup
@ -5178,6 +5290,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')
@ -5376,10 +5498,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"
@ -5447,10 +5569,6 @@ renew() {
_info "Switching back to $CA_LETSENCRYPT_V2"
Le_API="$CA_LETSENCRYPT_V2"
;;
"$CA_BUYPASS_TEST")
_info "Switching back to $CA_BUYPASS"
Le_API="$CA_BUYPASS"
;;
"$CA_GOOGLE_TEST")
_info "Switching back to $CA_GOOGLE"
Le_API="$CA_GOOGLE"
@ -5492,6 +5610,11 @@ renew() {
Le_PostHook="$(_readdomainconf Le_PostHook)"
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.
@ -5499,7 +5622,14 @@ renew() {
if [ -z "$Le_Keylength" ]; then
Le_Keylength=2048
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"
if [ "$CA_LETSENCRYPT_V2" = "$Le_API" ]; then
#letsencrypt doesn't support ocsp anymore
if [ "$Le_OCSP_Staple" ]; then
export Le_OCSP_Staple=""
_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" "$Le_ExtKeyUse"
res="$?"
if [ "$res" != "0" ]; then
return "$res"
@ -5666,6 +5796,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
@ -5709,7 +5843,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"
}
@ -5760,9 +5894,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"
(
@ -5775,7 +5910,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"
@ -5794,6 +5929,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"
@ -5818,7 +5995,7 @@ _deploy() {
return 1
fi
if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then
if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CERT_PFX_PATH"; then
_err "Error deploying for domain: $_d"
return 1
fi
@ -5981,7 +6158,7 @@ _installcert() {
); then
_info "$(__green "Reload successful")"
else
_err "Reload error for: $Le_Domain"
_err "Reload error for: $_main_domain"
fi
fi
@ -6061,7 +6238,7 @@ installcronjob() {
_script="$(_readlink "$_SCRIPT_")"
_debug _script "$_script"
if [ -f "$_script" ]; then
_info "Usinging the current script from: $_script"
_info "Using the current script from: $_script"
lesh="$_script"
else
_err "Cannot install cronjob, $PROJECT_ENTRY not found."
@ -6332,7 +6509,8 @@ _deactivate() {
fi
_debug "Trigger validation."
vtype="$(_getIdType "$_d_domain")"
entry="$(echo "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
# Fix for empty error objects in response which mess up the original code, adapted from fix suggested here: https://github.com/acmesh-official/acme.sh/issues/4933#issuecomment-1870499018
entry="$(echo "$response" | sed s/'"error":{}'/'"error":null'/ | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')"
_debug entry "$entry"
if [ -z "$entry" ]; then
_err "$d: Cannot get domain token"
@ -6416,6 +6594,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
@ -6464,6 +6672,7 @@ _initconf() {
#NO_TIMESTAMP=1
" >"$ACCOUNT_CONF_PATH"
chmod 600 "$ACCOUNT_CONF_PATH"
fi
}
@ -6499,9 +6708,9 @@ _precheck() {
return 1
fi
if ! _exists "socat"; then
_err "It is recommended to install socat first."
_err "We use socat for the standalone server, which is used for standalone mode."
if ! _exists "socat" && ! _exists "python" && ! _exists "python2" && ! _exists "python3"; then
_err "It is recommended to install socat or python first."
_err "We use socat or python for the standalone server, which is used for standalone mode."
_err "If you don't want to use standalone mode, you may ignore this warning."
fi
@ -6813,7 +7022,7 @@ _send_notify() {
_nsource="$NOTIFY_SOURCE"
if [ -z "$_nsource" ]; then
_nsource="$(hostname)"
_nsource="$(uname -n)"
fi
_nsubject="$_nsubject by $_nsource"
@ -6971,6 +7180,9 @@ Parameters:
If no match, the default offered chain will be used. (default: empty)
See: $_PREFERRED_CHAIN_WIKI
--cert-profile, --certificate-profile <profile> If the CA offers profiles, select the desired profile
See: $_PROFILESELECTION_WIKI
--valid-to <date-time> Request the NotAfter field of the cert.
See: $_VALIDITY_WIKI
--valid-from <date-time> Request the NotBefore field of the cert.
@ -7015,7 +7227,7 @@ Parameters:
--accountconf <file> Specifies a customized account config file.
--home <directory> Specifies the home dir for $PROJECT_NAME.
--cert-home <directory> Specifies the home dir to save all the certs, only valid for '--install' command.
--cert-home <directory> Specifies the home dir to save all the certs.
--config-home <directory> Specifies the home dir to save all the configurations.
--useragent <string> Specifies the user agent string. it will be saved for future use too.
-m, --email <email> Specifies the account email, only valid for the '--install' and '--update-account' command.
@ -7047,6 +7259,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.
@ -7165,6 +7379,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() {
@ -7330,6 +7562,8 @@ _process() {
_local_address=""
_log_level=""
_auto_upgrade=""
_request_v4=""
_request_v6=""
_listen_v4=""
_listen_v6=""
_openssl_bin=""
@ -7346,6 +7580,8 @@ _process() {
_preferred_chain=""
_valid_from=""
_valid_to=""
_certificate_profile=""
_extended_key_usage=""
while [ ${#} -gt 0 ]; do
case "${1}" in
@ -7449,6 +7685,9 @@ _process() {
--set-default-chain)
_CMD="setdefaultchain"
;;
--list-profiles)
_CMD="list_profiles"
;;
-d | --domain)
_dvalue="$2"
@ -7664,6 +7903,10 @@ _process() {
_valid_to="$2"
shift
;;
--certificate-profile | --cert-profile)
_certificate_profile="$2"
shift
;;
--httpport)
_httpport="$2"
Le_HTTPPort="$_httpport"
@ -7734,7 +7977,7 @@ _process() {
shift
;;
--extended-key-usage)
Le_ExtKeyUse="$2"
_extended_key_usage="$2"
shift
;;
--ocsp-must-staple | --ocsp)
@ -7787,6 +8030,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"
@ -7939,13 +8194,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"
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"
@ -8010,6 +8265,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
}

13
deploy/docker.sh

@ -18,6 +18,7 @@ docker_deploy() {
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_cpfx="$6"
_debug _cdomain "$_cdomain"
_getdeployconf DEPLOY_DOCKER_CONTAINER_LABEL
_debug2 DEPLOY_DOCKER_CONTAINER_LABEL "$DEPLOY_DOCKER_CONTAINER_LABEL"
@ -88,6 +89,12 @@ docker_deploy() {
_savedeployconf DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE "$DEPLOY_DOCKER_CONTAINER_FULLCHAIN_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE
_debug2 DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
_savedeployconf DEPLOY_DOCKER_CONTAINER_PFX_FILE "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"
fi
_getdeployconf DEPLOY_DOCKER_CONTAINER_RELOAD_CMD
_debug2 DEPLOY_DOCKER_CONTAINER_RELOAD_CMD "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
@ -125,6 +132,12 @@ docker_deploy() {
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_PFX_FILE" ]; then
if ! _docker_cp "$_cid" "$_cpfx" "$DEPLOY_DOCKER_CONTAINER_PFX_FILE"; then
return 1
fi
fi
if [ "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD" ]; then
_info "Reloading: $DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"
if ! _docker_exec "$_cid" "$DEPLOY_DOCKER_CONTAINER_RELOAD_CMD"; then

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
}

2
deploy/haproxy.sh

@ -357,7 +357,7 @@ haproxy_deploy() {
_info "Update existing certificate '${_pem}' over HAProxy ${_socketname}."
fi
_socat_cert_set_cmd="echo -e '${_cmdpfx}set ssl cert ${_pem} <<\n$(cat "${_pem}")\n' | socat '${_statssock}' - | grep -q 'Transaction created'"
_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
_secure_debug _socat_cert_set_cmd "${_socat_cert_set_cmd}"
eval "${_socat_cert_set_cmd}"
_ret=$?
if [ "${_ret}" != "0" ]; then

98
deploy/kemplm.sh

@ -0,0 +1,98 @@
#!/usr/bin/env sh
#Here is a script to deploy cert to a Kemp Loadmaster.
#returns 0 means success, otherwise error.
#DEPLOY_KEMP_TOKEN="token"
#DEPLOY_KEMP_URL="https://kemplm.example.com"
######## Public functions #####################
#domain keyfile certfile cafile fullchain
kemplm_deploy() {
_domain="$1"
_key_file="$2"
_cert_file="$3"
_ca_file="$4"
_fullchain_file="$5"
_debug _domain "$_domain"
_debug _key_file "$_key_file"
_debug _cert_file "$_cert_file"
_debug _ca_file "$_ca_file"
_debug _fullchain_file "$_fullchain_file"
if ! _exists jq; then
_err "jq not found"
return 1
fi
# Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename
_kemp_domain=$(echo "${_domain}" | sed 's/\*\.//')
_debug _kemp_domain "$_kemp_domain"
# Read config from saved values or env
_getdeployconf DEPLOY_KEMP_TOKEN
_getdeployconf DEPLOY_KEMP_URL
_debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
_secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
if [ -z "$DEPLOY_KEMP_TOKEN" ]; then
_err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN."
return 1
fi
if [ -z "$DEPLOY_KEMP_URL" ]; then
_err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL."
return 1
fi
# Save current values
_savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN"
_savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL"
# Check if certificate is already installed
_info "Check if certificate is already present"
_list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}"
_debug3 _list_request "${_list_request}"
_kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}")
_debug2 _kemp_cert_count "${_kemp_cert_count}"
_kemp_replace_cert=1
if [ "${_kemp_cert_count}" -eq 0 ]; then
_kemp_replace_cert=0
_info "Certificate does not exist on Kemp Loadmaster"
else
_info "Certificate already exists on Kemp Loadmaster"
fi
_debug _kemp_replace_cert "${_kemp_replace_cert}"
# Upload new certificate to Kemp Loadmaster
_kemp_upload_cert=$(_mktemp)
cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}"
_info "Uploading certificate to Kemp Loadmaster"
_add_data=$(cat "${_kemp_upload_cert}")
_add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}"
_debug3 _add_request "${_add_request}"
_kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2")
_retval=$?
_debug2 _kemp_post_result "${_kemp_post_result}"
if [ "${_retval}" -eq 0 ]; then
_kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status')
_kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message')
if [ "${_kemp_post_status}" = "ok" ]; then
_info "Upload successful"
else
_err "Upload failed: ${_kemp_post_message}"
fi
else
_err "Upload failed"
_retval=1
fi
rm "${_kemp_upload_cert}"
return $_retval
}

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
}

102
deploy/panos.sh

@ -7,20 +7,27 @@
#
# Firewall admin with superuser and IP address is required.
#
# REQURED:
# REQUIRED:
# export PANOS_HOST=""
# export PANOS_USER="" #User *MUST* have Commit and Import Permissions in XML API for Admin Role
# export PANOS_PASS=""
#
# OPTIONAL
# export PANOS_TEMPLATE="" #Template Name of panorama managed devices
# 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.
_COMMIT_WAIT_INTERVAL=30 # query commit status every 30 seconds
_COMMIT_WAIT_ITERATIONS=20 # query commit status 20 times (20*30 = 600 seconds = 10 minutes)
# This function is to parse the XML response from the firewall
parse_response() {
type=$2
_debug "API Response: $1"
if [ "$type" = 'keygen' ]; then
status=$(echo "$1" | sed 's/^.*\(['\'']\)\([a-z]*\)'\''.*/\2/g')
if [ "$status" = "success" ]; then
@ -30,6 +37,13 @@ parse_response() {
message="PAN-OS Key could not be set."
fi
else
if [ "$type" = 'commit' ]; then
job_id=$(echo "$1" | sed 's/^.*\(<job>\)\(.*\)<\/job>.*/\2/g')
_commit_job_id=$job_id
elif [ "$type" = 'job_status' ]; then
job_status=$(echo "$1" | tr -d '\n' | sed 's/^.*<result>\([^<]*\)<\/result>.*/\1/g')
_commit_job_status=$job_status
fi
status=$(echo "$1" | tr -d '\n' | sed 's/^.*"\([a-z]*\)".*/\1/g')
message=$(echo "$1" | tr -d '\n' | sed 's/.*\(<result>\|<msg>\|<line>\)\([^<]*\).*/\2/g')
_debug "Firewall message: $message"
@ -44,13 +58,13 @@ parse_response() {
#This function is used to deploy to the firewall
deployer() {
content=""
type=$1 # Types are keytest, keygen, cert, key, commit
type=$1 # Types are keytest, keygen, cert, key, commit, job_status, push
panos_url="https://$_panos_host/api/"
export _H1="Content-Type: application/x-www-form-urlencoded"
#Test API Key by performing a lookup
if [ "$type" = 'keytest' ]; then
_debug "**** Testing saved API Key ****"
_H1="Content-Type: application/x-www-form-urlencoded"
# Get Version Info to test key
content="type=version&key=$_panos_key"
## Exclude all scopes for the empty commit
@ -61,7 +75,6 @@ deployer() {
# Generate API Key
if [ "$type" = 'keygen' ]; then
_debug "**** Generating new API Key ****"
_H1="Content-Type: application/x-www-form-urlencoded"
content="type=keygen&user=$_panos_user&password=$_panos_pass"
# content="$content${nl}--$delim${nl}Content-Disposition: form-data; type=\"keygen\"; user=\"$_panos_user\"; password=\"$_panos_pass\"${nl}Content-Type: application/octet-stream${nl}${nl}"
fi
@ -77,25 +90,31 @@ 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")"
if [ "$_panos_template" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl\"\r\n\r\n$_panos_template"
fi
if [ "$_panos_vsys" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys"
fi
fi
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
if [ "$_panos_vsys" ]; then
content="$content${nl}--$delim${nl}Content-Disposition: form-data; name=\"target-tpl-vsys\"\r\n\r\n$_panos_vsys"
fi
fi
#Close multipart
content="$content${nl}--$delim--${nl}${nl}"
@ -106,7 +125,6 @@ deployer() {
# Commit changes
if [ "$type" = 'commit' ]; then
_debug "**** Committing changes ****"
export _H1="Content-Type: application/x-www-form-urlencoded"
#Check for force commit - will commit ALL uncommited changes to the firewall. Use with caution!
if [ "$FORCE" ]; then
_debug "Force switch detected. Committing ALL changes to the firewall."
@ -118,6 +136,20 @@ deployer() {
content="type=commit&action=partial&key=$_panos_key&cmd=$cmd"
fi
# Query job status
if [ "$type" = 'job_status' ]; then
echo "**** Querying job $_commit_job_id status ****"
cmd=$(printf "%s" "<show><jobs><id>$_commit_job_id</id></jobs></show>" | _url_encode)
content="type=op&key=$_panos_key&cmd=$cmd"
fi
# Push changes
if [ "$type" = 'push' ]; then
echo "**** Pushing changes ****"
cmd=$(printf "%s" "<commit-all><template-stack><name>$_panos_template_stack</name><admin><member>$_panos_user</member></admin></template-stack></commit-all>" | _url_encode)
content="type=commit&action=all&key=$_panos_key&cmd=$cmd"
fi
response=$(_post "$content" "$panos_url" "" "POST")
parse_response "$response" "$type"
# Saving response to variables
@ -126,6 +158,8 @@ deployer() {
if [ "$response_status" = "success" ]; then
_debug "Successfully deployed $type"
return 0
elif [ "$_commit_job_status" ]; then
_debug "Commit Job Status = $_commit_job_status"
else
_err "Deploy of type $type failed. Try deploying with --debug to troubleshoot."
_debug "$message"
@ -191,11 +225,41 @@ panos_deploy() {
_getdeployconf PANOS_TEMPLATE
fi
# PANOS_TEMPLATE_STACK
if [ "$PANOS_TEMPLATE_STACK" ]; then
_debug "Detected ENV variable PANOS_TEMPLATE_STACK. Saving to file."
_savedeployconf PANOS_TEMPLATE_STACK "$PANOS_TEMPLATE_STACK" 1
else
_debug "Attempting to load variable PANOS_TEMPLATE_STACK from file."
_getdeployconf PANOS_TEMPLATE_STACK
fi
# PANOS_TEMPLATE_STACK
if [ "$PANOS_VSYS" ]; then
_debug "Detected ENV variable PANOS_VSYS. Saving to file."
_savedeployconf PANOS_VSYS "$PANOS_VSYS" 1
else
_debug "Attempting to load variable PANOS_VSYS from file."
_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
_panos_pass=$PANOS_PASS
_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
@ -214,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 ****"
@ -229,6 +299,20 @@ panos_deploy() {
deployer cert
deployer key
deployer commit
if [ "$_panos_template_stack" ]; then
# try to get job status for 20 times in 30 sec interval
i=0
while [ "$i" -lt $_COMMIT_WAIT_ITERATIONS ]; do
deployer job_status
if [ "$_commit_job_status" = "OK" ]; then
echo "Commit finished!"
break
fi
sleep $_COMMIT_WAIT_INTERVAL
i=$((i + 1))
done
deployer push
fi
fi
fi
}

130
deploy/proxmoxbs.sh

@ -0,0 +1,130 @@
#!/usr/bin/env sh
# Deploy certificates to a proxmox backup server using the API.
#
# Environment variables that can be set are:
# `DEPLOY_PROXMOXBS_SERVER`: The hostname of the proxmox backup server. Defaults to
# _cdomain.
# `DEPLOY_PROXMOXBS_SERVER_PORT`: The port number the management interface is on.
# Defaults to 8007.
# `DEPLOY_PROXMOXBS_USER`: The user we'll connect as. Defaults to root.
# `DEPLOY_PROXMOXBS_USER_REALM`: The authentication realm the user authenticates
# with. Defaults to pam.
# `DEPLOY_PROXMOXBS_API_TOKEN_NAME`: The name of the API token created for the
# user account. Defaults to acme.
# `DEPLOY_PROXMOXBS_API_TOKEN_KEY`: The API token. Required.
proxmoxbs_deploy() {
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug2 _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
# "Sane" defaults.
_getdeployconf DEPLOY_PROXMOXBS_SERVER
if [ -z "$DEPLOY_PROXMOXBS_SERVER" ]; then
_target_hostname="$_cdomain"
else
_target_hostname="$DEPLOY_PROXMOXBS_SERVER"
_savedeployconf DEPLOY_PROXMOXBS_SERVER "$DEPLOY_PROXMOXBS_SERVER"
fi
_debug2 DEPLOY_PROXMOXBS_SERVER "$_target_hostname"
_getdeployconf DEPLOY_PROXMOXBS_SERVER_PORT
if [ -z "$DEPLOY_PROXMOXBS_SERVER_PORT" ]; then
_target_port="8007"
else
_target_port="$DEPLOY_PROXMOXBS_SERVER_PORT"
_savedeployconf DEPLOY_PROXMOXBS_SERVER_PORT "$DEPLOY_PROXMOXBS_SERVER_PORT"
fi
_debug2 DEPLOY_PROXMOXBS_SERVER_PORT "$_target_port"
# Complete URL.
_target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/localhost/certificates/custom"
_debug TARGET_URL "$_target_url"
# More "sane" defaults.
_getdeployconf DEPLOY_PROXMOXBS_USER
if [ -z "$DEPLOY_PROXMOXBS_USER" ]; then
_proxmoxbs_user="root"
else
_proxmoxbs_user="$DEPLOY_PROXMOXBS_USER"
_savedeployconf DEPLOY_PROXMOXBS_USER "$DEPLOY_PROXMOXBS_USER"
fi
_debug2 DEPLOY_PROXMOXBS_USER "$_proxmoxbs_user"
_getdeployconf DEPLOY_PROXMOXBS_USER_REALM
if [ -z "$DEPLOY_PROXMOXBS_USER_REALM" ]; then
_proxmoxbs_user_realm="pam"
else
_proxmoxbs_user_realm="$DEPLOY_PROXMOXBS_USER_REALM"
_savedeployconf DEPLOY_PROXMOXBS_USER_REALM "$DEPLOY_PROXMOXBS_USER_REALM"
fi
_debug2 DEPLOY_PROXMOXBS_USER_REALM "$_proxmoxbs_user_realm"
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" ]; then
_proxmoxbs_api_token_name="acme"
else
_proxmoxbs_api_token_name="$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME "$DEPLOY_PROXMOXBS_API_TOKEN_NAME"
fi
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_NAME "$_proxmoxbs_api_token_name"
# This is required.
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" ]; then
_err "API key not provided."
return 1
else
_proxmoxbs_api_token_key="$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY "$DEPLOY_PROXMOXBS_API_TOKEN_KEY"
fi
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_KEY "$_proxmoxbs_api_token_key"
# PBS API Token header value. Used in "Authorization: PBSAPIToken".
_proxmoxbs_header_api_token="${_proxmoxbs_user}@${_proxmoxbs_user_realm}!${_proxmoxbs_api_token_name}:${_proxmoxbs_api_token_key}"
_debug2 "Auth Header" "$_proxmoxbs_header_api_token"
# Ugly. I hate putting heredocs inside functions because heredocs don't
# account for whitespace correctly but it _does_ work and is several times
# cleaner than anything else I had here.
#
# This dumps the json payload to a variable that should be passable to the
# _psot function.
_json_payload=$(
cat <<HEREDOC
{
"certificates": "$(tr '\n' ':' <"$_cfullchain" | sed 's/:/\\n/g')",
"key": "$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')",
"node":"localhost",
"restart":true,
"force":true
}
HEREDOC
)
_debug2 Payload "$_json_payload"
_info "Push certificates to server"
export HTTPS_INSECURE=1
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
if [ "${_retval}" -eq 0 ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed"
_debug "Response" "$response"
return 1
fi
}

12
deploy/proxmoxve.sh

@ -127,6 +127,16 @@ HEREDOC
_info "Push certificates to server"
export HTTPS_INSECURE=1
export _H1="Authorization: PVEAPIToken=${_proxmoxve_header_api_token}"
_post "$_json_payload" "$_target_url" "" POST "application/json"
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json")
_retval=$?
if [ "${_retval}" -eq 0 ]; then
_debug3 response "$response"
_info "Certificate successfully deployed"
return 0
else
_err "Certificate deployment failed"
_debug "Response" "$response"
return 1
fi
}

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

4
deploy/routeros.sh

@ -144,8 +144,8 @@ source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
\n/certificate remove [ find name=$_cdomain.cer_1 ];\
\n/certificate remove [ find name=$_cdomain.cer_2 ];\
\ndelay 1;\
\n/certificate import file-name=$_cdomain.cer passphrase=\\\"\\\";\
\n/certificate import file-name=$_cdomain.key passphrase=\\\"\\\";\
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
\ndelay 1;\
\n:do {/file remove $_cdomain.cer; } on-error={ }\
\n:do {/file remove $_cdomain.key; } on-error={ }\

28
deploy/ruckus.sh

@ -116,6 +116,30 @@ ruckus_deploy() {
_H2="X-CSRF-Token: $(_response_header 'HTTP_X_CSRF_TOKEN')"
export _H2
if _isRSA "$_ckey" >/dev/null 2>&1; then
_debug "Using RSA certificate."
else
_info "Verifying ECC certificate support."
_ul_version="$(_get_unleashed_version)"
if [ -z "$_ul_version" ]; then
_err "Your controller doesn't support ECC certificates. Please deploy an RSA certificate."
return 1
fi
_ul_version_major="$(echo "$_ul_version" | cut -d . -f 1)"
_ul_version_minor="$(echo "$_ul_version" | cut -d . -f 2)"
if [ "$_ul_version_major" -lt "200" ]; then
_err "ZoneDirector doesn't support ECC certificates. Please deploy an RSA certificate."
return 1
elif [ "$_ul_version_minor" -lt "13" ]; then
_err "Unleashed $_ul_version_major.$_ul_version_minor doesn't support ECC certificates. Please deploy an RSA certificate or upgrade to Unleashed 200.13+."
return 1
fi
_debug "ECC certificates OK for Unleashed $_ul_version_major.$_ul_version_minor."
fi
_info "Uploading certificate"
_post_upload "uploadcert" "$_cfullchain"
@ -145,6 +169,10 @@ _response_cookie() {
_response_header 'Set-Cookie' | sed 's/;.*//'
}
_get_unleashed_version() {
_post '<ajax-request action="getstat" comp="system"><sysinfo/></ajax-request>' "$_base_url/_cmdstat.jsp" | _egrep_o "version-num=\"[^\"]*\"" | cut -d '"' -f 2
}
_post_upload() {
_post_action="$1"
_post_file="$2"

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

8
deploy/synology_dsm.sh

@ -186,8 +186,8 @@ synology_dsm_deploy() {
if [ -n "$SYNO_USE_TEMP_ADMIN" ]; then
_getdeployconf SYNO_LOCAL_HOSTNAME
_debug SYNO_LOCAL_HOSTNAME "${SYNO_LOCAL_HOSTNAME:-}"
if [ "$SYNO_LOCAL_HOSTNAME" != "1" ] && [ "$SYNO_LOCAL_HOSTNAME" == "$SYNO_HOSTNAME" ]; then
if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then
if [ "$SYNO_HOSTNAME" != "localhost" ] && [ "$SYNO_HOSTNAME" != "127.0.0.1" ]; then
if [ "$SYNO_LOCAL_HOSTNAME" != "1" ]; then
_err "SYNO_USE_TEMP_ADMIN=1 only support local deployment, though if you are sure that the hostname $SYNO_HOSTNAME is targeting to your **current local machine**, execute 'export SYNO_LOCAL_HOSTNAME=1' then rerun."
return 1
fi
@ -320,7 +320,7 @@ synology_dsm_deploy() {
_cleardeployconf SYNO_DEVICE_ID
_cleardeployconf SYNO_DEVICE_NAME
_savedeployconf SYNO_USE_TEMP_ADMIN "$SYNO_USE_TEMP_ADMIN"
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_HOSTNAME"
_savedeployconf SYNO_LOCAL_HOSTNAME "$SYNO_LOCAL_HOSTNAME"
else
_savedeployconf SYNO_USERNAME "$SYNO_USERNAME"
_savedeployconf SYNO_PASSWORD "$SYNO_PASSWORD"
@ -411,7 +411,7 @@ _temp_admin_create() {
_username="$1"
_password="$2"
synouser --del "$_username" >/dev/null 2>/dev/null
synouser --add "$_username" "$_password" "" 0 "scruelt@hotmail.com" 0 >/dev/null
synouser --add "$_username" "$_password" "" 0 "" 0 >/dev/null
}
_temp_admin_cleanup() {

2
deploy/truenas.sh

@ -217,7 +217,7 @@ truenas_deploy() {
_app_id=$(echo "$_app_id_list" | sed -n "${i}p")
_app_config="$(_post "\"$_app_id\"" "$_api_url/app/config" "" "POST" "application/json")"
# Check if the app use the same certificate TrueNAS web UI
_app_active_cert_config=$(echo "$_app_config" | _json_decode | jq -r ".ix_certificates[\"$_active_cert_id\"]")
_app_active_cert_config=$(echo "$_app_config" | tr -d '\000-\037' | _json_decode | jq -r ".ix_certificates[\"$_active_cert_id\"]")
if [ "$_app_active_cert_config" != "null" ]; then
_info "Updating certificate from $_active_cert_id to $_cert_id for app: $_app_id"
#Replace the old certificate id with the new one in path

343
deploy/truenas_ws.sh

@ -0,0 +1,343 @@
#!/usr/bin/env sh
# TrueNAS deploy script for SCALE/CORE using websocket
# It is recommend to use a wildcard certificate
#
# Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html
#
# Tested with TrueNAS Scale - Electric Eel 24.10
# Changes certificate in the following services:
# - Web UI
# - FTP
# - iX Apps
#
# The following environment variables must be set:
# ------------------------------------------------
#
# # API KEY
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
#
### Private functions
# Call websocket method
# Usage:
# _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'")
# _info "$_ws_response"
#
# Output:
# {"z": 9}
#
# Arguments:
# $@ - midclt arguments for call
#
# Returns:
# JSON/JOBID
_ws_call() {
_debug "_ws_call arg1" "$1"
_debug "_ws_call arg2" "$2"
_debug "_ws_call arg3" "$3"
if [ $# -eq 3 ]; then
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
fi
if [ $# -eq 2 ]; then
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
fi
if [ $# -eq 1 ]; then
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
fi
_debug "_ws_response" "$_ws_response"
printf "%s" "$_ws_response"
return 0
}
# Upload certificate with webclient api
_ws_upload_cert() {
/usr/bin/env python - <<EOF
import sys
from truenas_api_client import Client
with Client(uri="$_ws_uri") as c:
### Login with API key
print("I:Trying to upload new certificate...")
ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
if ret:
### upload certificate
with open('$1', 'r') as file:
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}, job=True)
print("R:" + str(ret["id"]))
sys.exit(0)
else:
print("R:0")
print("E:_ws_upload_cert error!")
sys.exit(7)
EOF
return $?
}
# Check argument is a number
# Usage:
#
# Output:
# n/a
#
# Arguments:
# $1 - Anything
#
# Returns:
# 0: true
# 1: false
_ws_check_jobid() {
case "$1" in
[0-9]*)
return 0
;;
esac
return 1
}
# Wait for job to finish and return result as JSON
# Usage:
# _ws_result=$(_ws_get_job_result "$_ws_jobid")
# _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
#
# Output:
# JSON result of the job
#
# Arguments:
# $1 - JobID
#
# Returns:
# n/a
_ws_get_job_result() {
while true; do
_sleep 2
_ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then
_ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
_debug "_ws_result" "$_ws_result"
printf "%s" "$_ws_result"
_ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
if [ "$_ws_error" != "null" ]; then
_err "Job $1 failed:"
_err "$_ws_error"
return 7
fi
break
fi
done
return 0
}
########################
### Public functions ###
########################
# truenas_ws_deploy
#
# Deploy new certificate to TrueNAS services
#
# Arguments
# 1: Domain
# 2: Key-File
# 3: Certificate-File
# 4: CA-File
# 5: FullChain-File
# Returns:
# 0: Success
# 1: Missing API Key
# 2: TrueNAS not ready
# 3: Not a JobID
# 4: FTP cert error
# 5: WebUI cert error
# 6: Job error
# 7: WS call error
#
truenas_ws_deploy() {
_domain="$1"
_file_key="$2"
_file_cert="$3"
_file_ca="$4"
_file_fullchain="$5"
_debug _domain "$_domain"
_debug _file_key "$_file_key"
_debug _file_cert "$_file_cert"
_debug _file_ca "$_file_ca"
_debug _file_fullchain "$_file_fullchain"
########## Environment check
_info "Checking environment variables..."
_getdeployconf DEPLOY_TRUENAS_APIKEY
_getdeployconf DEPLOY_TRUENAS_HOSTNAME
_getdeployconf DEPLOY_TRUENAS_PROTOCOL
# Check API Key
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
return 1
fi
# Check Hostname, default to localhost if not set
if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then
_info "TrueNAS hostname not set. Using 'localhost'."
DEPLOY_TRUENAS_HOSTNAME="localhost"
fi
# Check protocol, default to ws if not set
if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then
_info "TrueNAS protocol not set. Using 'ws'."
DEPLOY_TRUENAS_PROTOCOL="ws"
fi
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket"
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_debug _ws_uri "$_ws_uri"
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_info "Environment variables: OK"
########## Health check
_info "Checking TrueNAS health..."
_ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
_ws_ret=$?
if [ $_ws_ret -gt 0 ]; then
_err "Error calling system.ready:"
_err "$_ws_response"
return $_ws_ret
fi
if [ "$_ws_response" != "TRUE" ]; then
_err "TrueNAS is not ready."
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
_err "Verify API key."
return 2
fi
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME"
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL"
_info "TrueNAS health: OK"
########## System info
_info "Gather system info..."
_ws_response=$(_ws_call "system.info")
_truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
_info "TrueNAS version: $_truenas_version"
########## Gather current certificate
_info "Gather current WebUI certificate..."
_ws_response="$(_ws_call "system.general.config")"
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
_info "Current WebUI certificate ID: $_ui_certificate_id"
_info "Current WebUI certificate name: $_ui_certificate_name"
########## Upload new certificate
_info "Upload new certificate..."
_certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
_info "New WebUI certificate name: $_certname"
_debug _certname "$_certname"
_ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
echo "$_ws_out" | while IFS= read -r LINE; do
case "$LINE" in
I:*)
_info "${LINE#I:}"
;;
D:*)
_debug "${LINE#D:}"
;;
E*)
_err "${LINE#E:}"
;;
*) ;;
esac
done
_new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
_info "New certificate ID: $_new_certid"
########## FTP
_info "Replace FTP certificate..."
_ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
_ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
if [ "$_ftp_certid" != "$_new_certid" ]; then
_err "Cannot set FTP certificate."
_debug "_ws_response" "$_ws_response"
return 4
fi
########## ix Apps (SCALE only)
_info "Replace app certificates..."
_ws_response=$(_ws_call "app.query")
for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
_info "Checking app $_app_name..."
_ws_response=$(_ws_call "app.config" "$_app_name")
if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
_info "App has certificate option, setup new certificate..."
_info "App will be redeployed after updating the certificate."
_ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
_debug "_ws_jobid" "$_ws_jobid"
if ! _ws_check_jobid "$_ws_jobid"; then
_err "No JobID returned from websocket method."
return 3
fi
_ws_result=$(_ws_get_job_result "$_ws_jobid")
_ws_ret=$?
if [ $_ws_ret -gt 0 ]; then
return $_ws_ret
fi
_debug "_ws_result" "$_ws_result"
_info "App certificate replaced."
else
_info "App has no certificate option, skipping..."
fi
done
########## WebUI
_info "Replace WebUI certificate..."
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
if [ "$_changed_certid" != "$_new_certid" ]; then
_err "WebUI certificate change error.."
return 5
else
_info "WebUI certificate replaced."
fi
_info "Restarting WebUI..."
_ws_response=$(_ws_call "system.general.ui_restart")
_info "Waiting for UI restart..."
_sleep 15
########## Certificates
_info "Deleting old certificate..."
_ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
if ! _ws_check_jobid "$_ws_jobid"; then
_err "No JobID returned from websocket method."
return 3
fi
_ws_result=$(_ws_get_job_result "$_ws_jobid")
_ws_ret=$?
if [ $_ws_ret -gt 0 ]; then
return $_ws_ret
fi
_info "Have a nice day...bye!"
}

93
deploy/unifi.sh

@ -30,7 +30,9 @@
# Keystore password (built into Unifi Controller, not a user-set password):
#DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
# Command to restart Unifi Controller:
#DEPLOY_UNIFI_RELOAD="service unifi restart"
# DEPLOY_UNIFI_RELOAD="systemctl restart unifi"
# System Properties file location for controller
#DEPLOY_UNIFI_SYSTEM_PROPERTIES="/usr/lib/unifi/data/system.properties"
#
# Settings for Unifi Cloud Key Gen1 (nginx admin pages):
# Directory where cloudkey.crt and cloudkey.key live:
@ -43,7 +45,7 @@
# Directory where unifi-core.crt and unifi-core.key live:
#DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/"
# Command to restart unifi-core:
#DEPLOY_UNIFI_RELOAD="systemctl restart unifi-core"
# DEPLOY_UNIFI_OS_RELOAD="systemctl restart unifi-core"
#
# At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR,
# or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs.
@ -69,12 +71,16 @@ unifi_deploy() {
_getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR
_getdeployconf DEPLOY_UNIFI_CORE_CONFIG
_getdeployconf DEPLOY_UNIFI_RELOAD
_getdeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES
_getdeployconf DEPLOY_UNIFI_OS_RELOAD
_debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
_debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
_debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
_debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
_debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
_debug2 DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
_debug2 DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
# Space-separated list of environments detected and installed:
_services_updated=""
@ -135,33 +141,55 @@ unifi_deploy() {
cp -f "$_import_pkcs12" "$_unifi_keystore"
fi
# correct file ownership according to the directory, the keystore is placed in
_unifi_keystore_dir=$(dirname "${_unifi_keystore}")
# 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
fi
# Update unifi service for certificate cipher compatibility
_unifi_system_properties="${DEPLOY_UNIFI_SYSTEM_PROPERTIES:-/usr/lib/unifi/data/system.properties}"
if ${ACME_OPENSSL_BIN:-openssl} pkcs12 \
-in "$_import_pkcs12" \
-password pass:aircontrolenterprise \
-nokeys | ${ACME_OPENSSL_BIN:-openssl} x509 -text \
-noout | grep -i "signature" | grep -iq ecdsa >/dev/null 2>&1; then
cp -f /usr/lib/unifi/data/system.properties /usr/lib/unifi/data/system.properties_original
_info "Updating system configuration for cipher compatibility."
_info "Saved original system config to /usr/lib/unifi/data/system.properties_original"
sed -i '/unifi\.https\.ciphers/d' /usr/lib/unifi/data/system.properties
echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>/usr/lib/unifi/data/system.properties
sed -i '/unifi\.https\.sslEnabledProtocols/d' /usr/lib/unifi/data/system.properties
echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>/usr/lib/unifi/data/system.properties
_info "System configuration updated."
if [ -f "$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties" ]; then
_unifi_system_properties="$(dirname "${DEPLOY_UNIFI_KEYSTORE}")/system.properties"
else
_unifi_system_properties="/usr/lib/unifi/data/system.properties"
fi
if [ -f "${_unifi_system_properties}" ]; then
cp -f "${_unifi_system_properties}" "${_unifi_system_properties}"_original
_info "Updating system configuration for cipher compatibility."
_info "Saved original system config to ${_unifi_system_properties}_original"
sed -i '/unifi\.https\.ciphers/d' "${_unifi_system_properties}"
echo "unifi.https.ciphers=ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES128-GCM-SHA256" >>"${_unifi_system_properties}"
sed -i '/unifi\.https\.sslEnabledProtocols/d' "${_unifi_system_properties}"
echo "unifi.https.sslEnabledProtocols=TLSv1.3,TLSv1.2" >>"${_unifi_system_properties}"
_info "System configuration updated."
fi
fi
rm "$_import_pkcs12"
# Restarting unifi-core will bring up unifi, doing it out of order results in
# a certificate error, and breaks wifiman.
# Restart if we aren't doing unifi-core, otherwise stop for later restart.
if systemctl -q is-active unifi; then
if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then
_reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi"
else
_reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl stop unifi"
fi
# Restart if we aren't doing Unifi OS (e.g. unifi-core service), otherwise stop for later restart.
_unifi_reload="${DEPLOY_UNIFI_RELOAD:-systemctl restart unifi}"
if [ ! -f "${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}/unifi-core.key" ]; then
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
else
_info "Stopping Unifi Controller for later restart."
_unifi_stop=$(echo "${_unifi_reload}" | sed -e 's/restart/stop/')
$_unifi_stop
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_reload"
_info "Unifi Controller stopped."
fi
_services_updated="${_services_updated} unifi"
_info "Install Unifi Controller certificate success!"
@ -181,13 +209,24 @@ unifi_deploy() {
return 1
fi
# Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks.
# Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was
# updated above), but if not, we don't know how to handle this installation:
if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
_err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
return 1
# It appears that unifi won't start if this is a symlink, so we'll copy it instead.
# if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
# _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
# return 1
# fi
_info "Updating ${_cloudkey_certdir}/unifi.keystore.jks"
if [ -e "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
if [ -L "${_cloudkey_certdir}/unifi.keystore.jks" ]; then
rm -f "${_cloudkey_certdir}/unifi.keystore.jks"
else
mv "${_cloudkey_certdir}/unifi.keystore.jks" "${_cloudkey_certdir}/unifi.keystore.jks_original"
fi
fi
cp "${_unifi_keystore}" "${_cloudkey_certdir}/unifi.keystore.jks"
cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt"
cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key"
(cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks)
@ -215,14 +254,14 @@ unifi_deploy() {
# Save the existing certs in case something goes wrong.
cp -f "${_unifi_core_config}"/unifi-core.crt "${_unifi_core_config}"/unifi-core_original.crt
cp -f "${_unifi_core_config}"/unifi-core.key "${_unifi_core_config}"/unifi-core_original.key
_info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt/key."
_info "Previous certificate and key saved to ${_unifi_core_config}/unifi-core_original.crt.key."
cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt"
cat "$_ckey" >"${_unifi_core_config}/unifi-core.key"
if systemctl -q is-active unifi-core; then
_reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core"
fi
_unifi_os_reload="${DEPLOY_UNIFI_OS_RELOAD:-systemctl restart unifi-core}"
_reload_cmd="${_reload_cmd:+$_reload_cmd && }$_unifi_os_reload"
_info "Install UnifiOS certificate success!"
_services_updated="${_services_updated} unifi-core"
elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then
@ -261,6 +300,8 @@ unifi_deploy() {
_savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
_savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
_savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
_savedeployconf DEPLOY_UNIFI_OS_RELOAD "$DEPLOY_UNIFI_OS_RELOAD"
_savedeployconf DEPLOY_UNIFI_SYSTEM_PROPERTIES "$DEPLOY_UNIFI_SYSTEM_PROPERTIES"
return 0
}

98
deploy/vault.sh

@ -80,10 +80,15 @@ vault_deploy() {
if [ -n "$VAULT_RENEW_TOKEN" ]; then
URL="$VAULT_ADDR/v1/auth/token/renew-self"
_info "Renew the Vault token to default TTL"
if ! _post "" "$URL" >/dev/null; then
_response=$(_post "" "$URL")
if [ "$?" != "0" ]; then
_err "Failed to renew the Vault token"
return 1
fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Failed to renew the Vault token: $_response"
return 1
fi
fi
URL="$VAULT_ADDR/v1/$VAULT_PREFIX/$_cdomain"
@ -91,29 +96,85 @@ vault_deploy() {
if [ -n "$VAULT_FABIO_MODE" ]; then
_info "Writing certificate and key to $URL in Fabio mode"
if [ -n "$VAULT_KV_V2" ]; then
_post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL" >/dev/null || return 1
_response=$(_post "{ \"data\": {\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"} }" "$URL")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error: $_response"
return 1
fi
else
_post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL" >/dev/null || return 1
_response=$(_post "{\"cert\": \"$_cfullchain\", \"key\": \"$_ckey\"}" "$URL")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error: $_response"
return 1
fi
fi
else
if [ -n "$VAULT_KV_V2" ]; then
_info "Writing certificate to $URL/cert.pem"
_post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem" >/dev/null || return 1
_response=$(_post "{\"data\": {\"value\": \"$_ccert\"}}" "$URL/cert.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing cert.pem: $_response"
return 1
fi
_info "Writing key to $URL/cert.key"
_post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key" >/dev/null || return 1
_response=$(_post "{\"data\": {\"value\": \"$_ckey\"}}" "$URL/cert.key")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing cert.key: $_response"
return 1
fi
_info "Writing CA certificate to $URL/ca.pem"
_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem" >/dev/null || return 1
_response=$(_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/ca.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing ca.pem: $_response"
return 1
fi
_info "Writing full-chain certificate to $URL/fullchain.pem"
_post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem" >/dev/null || return 1
_response=$(_post "{\"data\": {\"value\": \"$_cfullchain\"}}" "$URL/fullchain.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing fullchain.pem: $_response"
return 1
fi
else
_info "Writing certificate to $URL/cert.pem"
_post "{\"value\": \"$_ccert\"}" "$URL/cert.pem" >/dev/null || return 1
_response=$(_post "{\"value\": \"$_ccert\"}" "$URL/cert.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing cert.pem: $_response"
return 1
fi
_info "Writing key to $URL/cert.key"
_post "{\"value\": \"$_ckey\"}" "$URL/cert.key" >/dev/null || return 1
_response=$(_post "{\"value\": \"$_ckey\"}" "$URL/cert.key")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing cert.key: $_response"
return 1
fi
_info "Writing CA certificate to $URL/ca.pem"
_post "{\"value\": \"$_cca\"}" "$URL/ca.pem" >/dev/null || return 1
_response=$(_post "{\"value\": \"$_cca\"}" "$URL/ca.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing ca.pem: $_response"
return 1
fi
_info "Writing full-chain certificate to $URL/fullchain.pem"
_post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem" >/dev/null || return 1
_response=$(_post "{\"value\": \"$_cfullchain\"}" "$URL/fullchain.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing fullchain.pem: $_response"
return 1
fi
fi
# To make it compatible with the wrong ca path `chain.pem` which was used in former versions
@ -121,11 +182,20 @@ vault_deploy() {
_err "The CA certificate has moved from chain.pem to ca.pem, if you don't depend on chain.pem anymore, you can delete it to avoid this warning"
_info "Updating CA certificate to $URL/chain.pem for backward compatibility"
if [ -n "$VAULT_KV_V2" ]; then
_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem" >/dev/null || return 1
_response=$(_post "{\"data\": {\"value\": \"$_cca\"}}" "$URL/chain.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing chain.pem: $_response"
return 1
fi
else
_post "{\"value\": \"$_cca\"}" "$URL/chain.pem" >/dev/null || return 1
_response=$(_post "{\"value\": \"$_cca\"}" "$URL/chain.pem")
if [ "$?" != "0" ]; then return 1; fi
if echo "$_response" | grep -q '"errors":\['; then
_err "Vault error writing chain.pem: $_response"
return 1
fi
fi
fi
fi
}

500
deploy/zyxel_gs1900.sh

@ -0,0 +1,500 @@
#!/usr/bin/env sh
# Deploy certificates to Zyxel GS1900 series switches
#
# This script uses the https web administration interface in order
# to upload updated certificates to Zyxel GS1900 series switches.
# Only a few models have been tested but untested switches from the
# same model line may work as well. If you test and confirm a switch
# as working please submit a pull request updating this compatibility
# list!
#
# Known Issues:
# 1. This is a consumer grade switch and is a bit underpowered
# the longer the RSA key size the slower your switch web UI
# will be. RSA 2048 will work, RSA 4096 will work but you may
# experience performance problems.
# 2. You must use RSA certificates. The switch will reject EC-256
# and EC-384 certificates in firmware 2.80
# See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/
#
# Current GS1900 Switch Compatibility:
# GS1900-8 - Working as of firmware V2.80
# GS1900-8HP - Untested
# GS1900-10HP - Untested
# GS1900-16 - Untested
# GS1900-24 - Untested
# GS1900-24E - Working as of firmware V2.80
# GS1900-24EP - Untested
# GS1900-24HP - Untested
# GS1900-48 - Untested
# GS1900-48HP - Untested
#
# Prerequisite Setup Steps:
# 1. Install at least firmware V2.80 on your switch
# 2. Enable HTTPS web management on your switch
#
# Usage:
# 1. Ensure the switch has firmware V2.80 or later.
# 2. Ensure the switch has HTTPS management enabled.
# 3. Set the appropriate environment variables for your environment.
#
# DEPLOY_ZYXEL_SWITCH - The switch hostname. (Default: _cdomain)
# DEPLOY_ZYXEL_SWITCH_USER - The webadmin user. (Default: admin)
# DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch.
# DEPLOY_ZYXEL_SWITCH_REBOOT - If "1" reboot after update. (Default: "0")
#
# 4. Run the deployment plugin:
# acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com
#
# returns 0 means success, otherwise error.
#domain keyfile certfile cafile fullchain
zyxel_gs1900_deploy() {
_zyxel_gs1900_minimum_firmware_version="v2.80"
_cdomain="$1"
_ckey="$2"
_ccert="$3"
_cca="$4"
_cfullchain="$5"
_debug _cdomain "$_cdomain"
_debug2 _ckey "$_ckey"
_debug _ccert "$_ccert"
_debug _cca "$_cca"
_debug _cfullchain "$_cfullchain"
_getdeployconf DEPLOY_ZYXEL_SWITCH
_getdeployconf DEPLOY_ZYXEL_SWITCH_USER
_getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD
_getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT
if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then
DEPLOY_ZYXEL_SWITCH="$_cdomain"
fi
if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then
DEPLOY_ZYXEL_SWITCH_USER="admin"
fi
if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then
DEPLOY_ZYXEL_SWITCH_PASSWORD="1234"
fi
if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then
DEPLOY_ZYXEL_SWITCH_REBOOT="0"
fi
_savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
_savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
_savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
_savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
_debug DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH"
_debug DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER"
_secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD"
_debug DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT"
_zyxel_switch_base_uri="https://${DEPLOY_ZYXEL_SWITCH}"
_info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}."
_zyxel_gs1900_deployment_precheck || return $?
_zyxel_gs1900_should_update
if [ "$?" != "0" ]; then
_info "The switch already has our certificate installed. No update required."
return 0
else
_info "The switch does not yet have our certificate installed."
fi
_info "Logging into the switch web interface."
_zyxel_gs1900_login || return $?
_info "Validating the switch is compatible with this deployment process."
_zyxel_gs1900_validate_device_compatibility || return $?
_info "Uploading the certificate."
_zyxel_gs1900_upload_certificate || return $?
if [ "$DEPLOY_ZYXEL_SWITCH_REBOOT" = "1" ]; then
_info "Rebooting the switch."
_zyxel_gs1900_trigger_reboot || return $?
fi
return 0
}
_zyxel_gs1900_deployment_precheck() {
# Initialize the keylength if it isn't already
if [ -z "$Le_Keylength" ]; then
Le_Keylength=""
fi
if _isEccKey "$Le_Keylength"; then
_info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!"
_info "You can continue, but your switch may reject your key."
elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then
_info "Warning: Your RSA key length is greater than 2048!"
_info "You can continue, but you may experience performance issues in the web administration interface."
fi
# Check the server for some common failure modes prior to authentication and certificate upload in order to avoid
# sending a certificate when we may not want to.
test_login_response=$(_post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" 2>&1)
test_login_page_exitcode="$?"
_debug3 "Test Login Response: ${test_login_response}"
if [ "$test_login_page_exitcode" -ne "0" ]; then
if { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then
_err "The SSL certificate at $_zyxel_switch_base_uri could not be validated."
_err "Please double check your hostname, port, and that you are actually connecting to your switch."
_err "If the problem persists then please ensure that the certificate is not self-signed, has not"
_err "expired, and matches the switch hostname. If you expect validation to fail then you can disable"
_err "certificate validation by running with --insecure."
return 1
elif [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then
_debug3 "Intentionally ignore curl exit code 56 in our precheck"
else
_err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri."
return 1
fi
fi
}
_zyxel_gs1900_login() {
# Login to the switch and set the appropriate auth cookie in _H1
username_encoded=$(printf "%s" "$DEPLOY_ZYXEL_SWITCH_USER" | _url_encode)
password_encoded=$(_zyxel_gs1900_password_obfuscate "$DEPLOY_ZYXEL_SWITCH_PASSWORD" | _url_encode)
login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n')
if [ "$auth_response" != "OK" ]; then
_err "Login failed due to invalid credentials."
_err "Please double check the configured username and password and try again."
return 1
fi
sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';')
_secure_debug2 "sessionid" "$sessionid"
export _H1="Cookie: $sessionid"
_secure_debug2 "_H1" "$_H1"
return 0
}
_zyxel_gs1900_validate_device_compatibility() {
# Check the switches model and firmware version and throw errors
# if this script isn't compatible.
device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n')
model_name=$(_zyxel_gs1900_get_model "$device_info_html")
_debug2 "model_name" "$model_name"
if [ -z "$model_name" ]; then
_err "Could not find the switch model name."
_err "Please re-run with --debug and report a bug."
return $?
fi
if ! expr "$model_name" : "GS1900-" >/dev/null; then
_err "Switch is an unsupported model: $model_name"
return 1
fi
firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html")
_debug2 "firmware_version" "$firmware_version"
if [ -z "$firmware_version" ]; then
_err "Could not find the switch firmware version."
_err "Please re-run with --debug and report a bug."
return $?
fi
_debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version"
minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version")
_debug2 "minimum_major_version" "$minimum_major_version"
minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version")
_debug2 "minimum_minor_version" "$minimum_minor_version"
_debug2 "firmware_version" "$firmware_version"
firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version")
_debug2 "firmware_major_version" "$firmware_major_version"
firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version")
_debug2 "firmware_minor_version" "$firmware_minor_version"
_ret=0
if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then
_ret=1
elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then
_ret=1
fi
if [ "$_ret" != "0" ]; then
_err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version."
fi
return $?
}
_zyxel_gs1900_should_update() {
# Get the remote certificate serial number
_remote_cert=$(${ACME_OPENSSL_BIN:-openssl} s_client -showcerts -connect "${DEPLOY_ZYXEL_SWITCH}:443" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p')
_debug3 "_remote_cert" "$_remote_cert"
_remote_cert_serial=$(printf "%s" "${_remote_cert}" | ${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial)
_debug2 "_remote_cert_serial" "$_remote_cert_serial"
# Get our certificate serial number
_our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial <"${_ccert}")
_debug2 "_our_cert_serial" "$_our_cert_serial"
[ "${_remote_cert_serial}" != "${_our_cert_serial}" ]
}
_zyxel_gs1900_upload_certificate() {
# Generate a PKCS12 certificate with a temporary password since the web interface
# requires a password be present. Then upload that certificate.
temp_cert_password=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 64)
_secure_debug2 "temp_cert_password" "$temp_cert_password"
temp_pkcs12="$(_mktemp)"
_debug2 "temp_pkcs12" "$temp_pkcs12"
_toPkcs "$temp_pkcs12" "$_ckey" "$_ccert" "$_cca" "$temp_cert_password"
if [ "$?" != "0" ]; then
_err "Failed to generate a pkcs12 certificate."
_err "Please re-run with --debug and report a bug."
# ensure the temporary certificate file is cleaned up
[ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
return $?
fi
# Load the upload page
upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n')
# Get the first instance of XSSID from the upload page
form_xss_value=$(printf "%s" "$upload_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' | head -n 1)
_secure_debug2 "form_xss_value" "$form_xss_value"
_info "Generating the certificate upload request"
upload_post_request="$(_mktemp)"
upload_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)"
{
printf -- "--%s\r\n" "${upload_post_boundary}"
printf "Content-Disposition: form-data; name=\"XSSID\"\r\n\r\n%s\r\n" "${form_xss_value}"
printf -- "--%s\r\n" "${upload_post_boundary}"
printf "Content-Disposition: form-data; name=\"http_file\"; filename=\"temp_pkcs12.pfx\"\r\n"
printf "Content-Type: application/pkcs12\r\n\r\n"
cat "${temp_pkcs12}"
printf "\r\n"
printf -- "--%s\r\n" "${upload_post_boundary}"
printf "Content-Disposition: form-data; name=\"pwd\"\r\n\r\n%s\r\n" "${temp_cert_password}"
printf -- "--%s\r\n" "${upload_post_boundary}"
printf "Content-Disposition: form-data; name=\"cmd\"\r\n\r\n%s\r\n" "31"
printf -- "--%s\r\n" "${upload_post_boundary}"
printf "Content-Disposition: form-data; name=\"sysSubmit\"\r\n\r\n%s\r\n" "Import"
printf -- "--%s--\r\n" "${upload_post_boundary}"
} >"${upload_post_request}"
_info "Upload certificate to the switch"
# Unfortunately we cannot rely upon the switch response across switch models
# to return a consistent body return - so we cannot inspect the result of this
# upload to determine success.
upload_response=$(_zyxel_upload_pkcs12 "${upload_post_request}" "${upload_post_boundary}" 2>&1)
_debug3 "Upload response: ${upload_response}"
rm "${upload_post_request}"
# Pause for a few seconds to give the switch a chance to process the certificate
# For some reason I've found this to be necessary on my GS1900-24E
_debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate."
sleep "4"
# Check to see whether or not our update was successful
_ret=0
_zyxel_gs1900_should_update
if [ "$?" != "0" ]; then
_info "The certificate was updated successfully"
else
_ret=1
_err "The certificate upload does not appear to have worked."
_err "The remote certificate does not match the certificate we tried to upload."
_err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug."
fi
# ensure the temporary files are cleaned up
[ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}"
return $_ret
}
# make the certificate upload request using either
# --data binary with @ for file access in CURL
# or using --post-file for wget to ensure we upload
# the pkcs12 without getting tripped up on null bytes
#
# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker]
_zyxel_upload_pkcs12() {
bodyfilename="$1"
multipartformmarker="$2"
_post_url="${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi"
httpmethod="POST"
_postContentType="multipart/form-data; boundary=${multipartformmarker}"
if [ -z "$httpmethod" ]; then
httpmethod="POST"
fi
_debug $httpmethod
_debug "_post_url" "$_post_url"
_debug2 "bodyfilename" "$bodyfilename"
_debug2 "_postContentType" "$_postContentType"
_inithttp
if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then
_CURL="$_ACME_CURL"
if [ "$HTTPS_INSECURE" ]; then
_CURL="$_CURL --insecure "
fi
if [ "$httpmethod" = "HEAD" ]; then
_CURL="$_CURL -I "
fi
_debug "_CURL" "$_CURL"
response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data-binary "@${bodyfilename}" "$_post_url")"
_ret="$?"
if [ "$_ret" != "0" ]; then
_err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret"
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
_err "Here is the curl dump log:"
_err "$(cat "$_CURL_DUMP")"
fi
fi
elif [ "$_ACME_WGET" ]; then
_WGET="$_ACME_WGET"
if [ "$HTTPS_INSECURE" ]; then
_WGET="$_WGET --no-check-certificate "
fi
_debug "_WGET" "$_WGET"
response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-file="${bodyfilename}" "$_post_url" 2>"$HTTP_HEADER")"
_ret="$?"
if [ "$_ret" = "8" ]; then
_ret=0
_debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later."
fi
if [ "$_ret" != "0" ]; then
_err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret"
fi
if _contains "$_WGET" " -d "; then
# Demultiplex wget debug output
cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi
# remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER"
else
_ret="$?"
_err "Neither curl nor wget have been found, cannot make $httpmethod request."
fi
_debug "_ret" "$_ret"
printf "%s" "$response"
return $_ret
}
_zyxel_gs1900_trigger_reboot() {
# Trigger a reboot via the management reboot page in the web ui
reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n')
reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g')
_secure_debug2 "reboot_xss_value" "$reboot_xss_value"
reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded")
reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...")
if [ -z "$reboot_message" ]; then
_err "Failed to trigger switch reboot!"
return 1
fi
return 0
}
# password
_zyxel_gs1900_password_obfuscate() {
# Return the password obfuscated via the same method used by the
# switch's web UI login process
echo "$1" | awk '{
encoded = "";
password = $1;
allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
len = length($1);
pwi = length($1);
for (i=1; i <= (321 - pwi); i++)
{
if (0 == i % 5 && pwi > 0)
{
encoded = (encoded)(substr(password, pwi--, 1));
}
else if (i == 123)
{
if (len < 10)
{
encoded = (encoded)(0);
}
else
{
encoded = (encoded)(int(len / 10));
}
}
else if (i == 289)
{
encoded = (encoded)(len % 10)
}
else
{
encoded = (encoded)(substr(allowed, int(rand() * length(allowed)), 1))
}
}
printf("%s", encoded);
}'
}
# html label
_zyxel_html_table_lookup() {
# Look up a value in the html representing the status page of the switch
# when provided with the html of the page and the label (i.e. "Model Name:")
html="$1"
label=$(printf "%s" "$2" | tr -d ' ')
lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's/<tr>/\n<tr>/g' | sed 's/<td[^>]*>/<td>/g' | tr -d ' ' | grep -i "$label" | sed "s/<tr><td>$label<\/td><td>\([^<]\{1,\}\)<\/td><\/tr>/\1/i")
printf "%s" "$lookup_result"
return 0
}
# html
_zyxel_gs1900_get_model() {
html="$1"
model_name=$(_zyxel_html_table_lookup "$html" "Model Name:")
printf "%s" "$model_name"
}
# html
_zyxel_gs1900_get_firmware_version() {
html="$1"
firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+")
printf "%s" "$firmware_version"
}
# version_number
_zyxel_gs1900_parse_major_version() {
printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi'
}
# version_number
_zyxel_gs1900_parse_minor_version() {
printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi'
}

4
dnsapi/dns_1984hosting.sh

@ -128,7 +128,7 @@ _1984hosting_login() {
_get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@ -145,7 +145,7 @@ _1984hosting_login() {
_debug2 response "$response"
if _contains "$response" '"loggedin": true'; then
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'sessionid=[^;]*;' | tr -d ';')"
One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
export One984HOSTING_SESSIONID_COOKIE
export One984HOSTING_CSRFTOKEN_COOKIE

160
dnsapi/dns_active24.sh

@ -1,17 +1,17 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_active24_info='Active24.com
Site: Active24.com
dns_active24_info='Active24.cz
Site: Active24.cz
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_active24
Options:
ACTIVE24_Token API Token
Active24_ApiKey API Key. Called "Identifier" in the Active24 Admin
Active24_ApiSecret API Secret. Called "Secret key" in the Active24 Admin
Issues: github.com/acmesh-official/acme.sh/issues/2059
Author: Milan Pála
'
ACTIVE24_Api="https://api.active24.com"
######## Public functions #####################
Active24_Api="https://rest.active24.cz"
# export Active24_ApiKey=ak48l3h7-ak5d-qn4t-p8gc-b6fs8c3l
# export Active24_ApiSecret=ajvkeo3y82ndsu2smvxy3o36496dcascksldncsq
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
@ -22,8 +22,8 @@ dns_active24_add() {
_active24_init
_info "Adding txt record"
if _active24_rest POST "dns/$_domain/txt/v1" "{\"name\":\"$_sub_domain\",\"text\":\"$txtvalue\",\"ttl\":0}"; then
if _contains "$response" "errors"; then
if _active24_rest POST "/v2/service/$_service_id/dns/record" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"content\":\"$txtvalue\",\"ttl\":300}"; then
if _contains "$response" "error"; then
_err "Add txt record error."
return 1
else
@ -31,6 +31,7 @@ dns_active24_add() {
return 0
fi
fi
_err "Add txt record error."
return 1
}
@ -44,19 +45,25 @@ dns_active24_rm() {
_active24_init
_debug "Getting txt records"
_active24_rest GET "dns/$_domain/records/v1"
# The API needs to send data in body in order the filter to work
# TODO: web can also add content $txtvalue to filter and then get the id from response
_active24_rest GET "/v2/service/$_service_id/dns/record" "{\"page\":1,\"descending\":true,\"sortBy\":\"name\",\"rowsPerPage\":100,\"totalRecords\":0,\"filters\":{\"type\":[\"TXT\"],\"name\":\"${_sub_domain}\"}}"
#_active24_rest GET "/v2/service/$_service_id/dns/record?rowsPerPage=100"
if _contains "$response" "errors"; then
if _contains "$response" "error"; then
_err "Error"
return 1
fi
hash_ids=$(echo "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o "hashId\":\"[^\"]+" | cut -c10-)
# Note: it might never be more than one record actually, NEEDS more INVESTIGATION
record_ids=$(printf "%s" "$response" | _egrep_o "[^{]+${txtvalue}[^}]+" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
_debug2 record_ids "$record_ids"
for hash_id in $hash_ids; do
_debug "Removing hash_id" "$hash_id"
if _active24_rest DELETE "dns/$_domain/$hash_id/v1" ""; then
if _contains "$response" "errors"; then
for redord_id in $record_ids; do
_debug "Removing record_id" "$redord_id"
_debug "txtvalue" "$txtvalue"
if _active24_rest DELETE "/v2/service/$_service_id/dns/record/$redord_id" ""; then
if _contains "$response" "error"; then
_err "Unable to remove txt record."
return 1
else
@ -70,21 +77,15 @@ dns_active24_rm() {
return 1
}
#################### 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
if ! _active24_rest GET "dns/domains/v1"; then
if ! _active24_rest GET "/v1/user/self/service"; then
return 1
fi
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug "h" "$h"
@ -104,45 +105,102 @@ _get_root() {
return 1
}
_active24_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
export _H1="Authorization: Bearer $ACTIVE24_Token"
if [ "$m" != "GET" ]; then
_debug "data" "$data"
response="$(_post "$data" "$ACTIVE24_Api/$ep" "" "$m" "application/json")"
else
response="$(_get "$ACTIVE24_Api/$ep")"
_active24_init() {
Active24_ApiKey="${Active24_ApiKey:-$(_readaccountconf_mutable Active24_ApiKey)}"
Active24_ApiSecret="${Active24_ApiSecret:-$(_readaccountconf_mutable Active24_ApiSecret)}"
#Active24_ServiceId="${Active24_ServiceId:-$(_readaccountconf_mutable Active24_ServiceId)}"
if [ -z "$Active24_ApiKey" ] || [ -z "$Active24_ApiSecret" ]; then
Active24_ApiKey=""
Active24_ApiSecret=""
_err "You don't specify Active24 api key and ApiSecret yet."
_err "Please create your key and try again."
return 1
fi
if [ "$?" != "0" ]; then
_err "error $ep"
#save the credentials to the account conf file.
_saveaccountconf_mutable Active24_ApiKey "$Active24_ApiKey"
_saveaccountconf_mutable Active24_ApiSecret "$Active24_ApiSecret"
_debug "A24 API CHECK"
if ! _active24_rest GET "/v2/check"; then
_err "A24 API check failed with: $response"
return 1
fi
_debug2 response "$response"
return 0
}
_active24_init() {
ACTIVE24_Token="${ACTIVE24_Token:-$(_readaccountconf_mutable ACTIVE24_Token)}"
if [ -z "$ACTIVE24_Token" ]; then
ACTIVE24_Token=""
_err "You didn't specify a Active24 api token yet."
_err "Please create the token and try again."
if ! echo "$response" | tr -d " " | grep \"verified\":true >/dev/null; then
_err "A24 API check failed with: $response"
return 1
fi
_saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_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"
_active24_get_service_id "$_domain"
_debug _service_id "$_service_id"
}
_active24_get_service_id() {
_d=$1
if ! _active24_rest GET "/v1/user/self/zone/${_d}"; then
return 1
else
response=$(echo "$response" | _json_decode)
_service_id=$(echo "$response" | _egrep_o '"id" *: *[^,]+' | cut -d ':' -f 2)
fi
}
_active24_rest() {
m=$1
ep_qs=$2 # with query string
# ep=$2
ep=$(printf "%s" "$ep_qs" | cut -d '?' -f1) # no query string
data="$3"
_debug "A24 $ep"
_debug "A24 $Active24_ApiKey"
_debug "A24 $Active24_ApiSecret"
timestamp=$(_time)
datez=$(date -u +"%Y%m%dT%H%M%SZ")
canonicalRequest="${m} ${ep} ${timestamp}"
signature=$(printf "%s" "$canonicalRequest" | _hmac sha1 "$(printf "%s" "$Active24_ApiSecret" | _hex_dump | tr -d " ")" hex)
authorization64="$(printf "%s:%s" "$Active24_ApiKey" "$signature" | _base64)"
export _H1="Date: ${datez}"
export _H2="Accept: application/json"
export _H3="Content-Type: application/json"
export _H4="Authorization: Basic ${authorization64}"
_debug2 H1 "$_H1"
_debug2 H2 "$_H2"
_debug2 H3 "$_H3"
_debug2 H4 "$_H4"
# _sleep 1
if [ "$m" != "GET" ]; then
_debug2 "${m} $Active24_Api${ep_qs}"
_debug "data" "$data"
response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
else
if [ -z "$data" ]; then
_debug2 "GET $Active24_Api${ep_qs}"
response="$(_get "$Active24_Api${ep_qs}")"
else
_debug2 "GET $Active24_Api${ep_qs} with data: ${data}"
response="$(_post "$data" "$Active24_Api${ep_qs}" "" "$m" "application/json")"
fi
fi
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
fi
_debug2 response "$response"
return 0
}

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

15
dnsapi/dns_azure.sh

@ -9,7 +9,7 @@ Options:
AZUREDNS_APPID App ID. App ID of the service principal
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
AZUREDNS_BEARERTOKEN Optional Bearer Token. Used instead of service principal credentials or managed identity
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
'
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
@ -340,8 +340,17 @@ _azure_getaccess_token() {
if [ "$managedIdentity" = true ]; then
# https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
export _H1="Metadata: true"
response="$(_get http://169.254.169.254/metadata/identity/oauth2/token\?api-version=2018-02-01\&resource=https://management.azure.com/)"
if [ -n "$IDENTITY_ENDPOINT" ]; then
# Some Azure environments may set IDENTITY_ENDPOINT (formerly MSI_ENDPOINT) to have an alternative metadata endpoint
url="$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=https://management.azure.com/"
headers="X-IDENTITY-HEADER: $IDENTITY_HEADER"
else
url="http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"
headers="Metadata: true"
fi
export _H1="$headers"
response="$(_get "$url")"
response="$(echo "$response" | _normalizeJson)"
accesstoken=$(echo "$response" | _egrep_o "\"access_token\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
expires_on=$(echo "$response" | _egrep_o "\"expires_on\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")

281
dnsapi/dns_beget.sh

@ -0,0 +1,281 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_beget_info='Beget.com
Site: Beget.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_beget
Options:
BEGET_User API user
BEGET_Password API password
Issues: github.com/acmesh-official/acme.sh/issues/6200
Author: ARNik <arnik@arnik.ru>
'
Beget_Api="https://api.beget.com/api"
#################### Public functions ####################
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
dns_beget_add() {
fulldomain=$1
txtvalue=$2
_debug "dns_beget_add() $fulldomain $txtvalue"
fulldomain=$(echo "$fulldomain" | _lower_case)
Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then
Beget_Username=""
Beget_Password=""
_err "You must export variables: Beget_Username, and Beget_Password"
return 1
fi
#save the credentials to the account conf file.
_saveaccountconf_mutable Beget_Username "$Beget_Username"
_saveaccountconf_mutable Beget_Password "$Beget_Password"
_info "Prepare subdomain."
if ! _prepare_subdomain "$fulldomain"; then
_err "Can't prepare subdomain."
return 1
fi
_info "Get domain records"
data="{\"fqdn\":\"$fulldomain\"}"
res=$(_api_call "$Beget_Api/dns/getData" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't get domain records."
return 1
fi
_info "Add new TXT record"
data="{\"fqdn\":\"$fulldomain\",\"records\":{"
data=${data}$(_parce_records "$res" "A")
data=${data}$(_parce_records "$res" "AAAA")
data=${data}$(_parce_records "$res" "CAA")
data=${data}$(_parce_records "$res" "MX")
data=${data}$(_parce_records "$res" "SRV")
data=${data}$(_parce_records "$res" "TXT")
data=$(echo "$data" | sed 's/,$//')
data=${data}'}}'
str=$(_txt_to_dns_json "$txtvalue")
data=$(_add_record "$data" "TXT" "$str")
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't change domain records."
return 1
fi
return 0
}
# Usage: fulldomain txtvalue
# Used to remove the txt record after validation
dns_beget_rm() {
fulldomain=$1
txtvalue=$2
_debug "dns_beget_rm() $fulldomain $txtvalue"
fulldomain=$(echo "$fulldomain" | _lower_case)
Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}"
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}"
_info "Get current domain records"
data="{\"fqdn\":\"$fulldomain\"}"
res=$(_api_call "$Beget_Api/dns/getData" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't get domain records."
return 1
fi
_info "Remove TXT record"
data="{\"fqdn\":\"$fulldomain\",\"records\":{"
data=${data}$(_parce_records "$res" "A")
data=${data}$(_parce_records "$res" "AAAA")
data=${data}$(_parce_records "$res" "CAA")
data=${data}$(_parce_records "$res" "MX")
data=${data}$(_parce_records "$res" "SRV")
data=${data}$(_parce_records "$res" "TXT")
data=$(echo "$data" | sed 's/,$//')
data=${data}'}}'
str=$(_txt_to_dns_json "$txtvalue")
data=$(_rm_record "$data" "$str")
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't change domain records."
return 1
fi
return 0
}
#################### Private functions below ####################
# Create subdomain if needed
# Usage: _prepare_subdomain [fulldomain]
_prepare_subdomain() {
fulldomain=$1
_info "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"
if [ -z "$_sub_domain" ]; then
_debug "$fulldomain is a root domain."
return 0
fi
_info "Get subdomain list"
res=$(_api_call "$Beget_Api/domain/getSubdomainList")
if ! _is_api_reply_ok "$res"; then
_err "Can't get subdomain list."
return 1
fi
if _contains "$res" "\"fqdn\":\"$fulldomain\""; then
_debug "Subdomain $fulldomain already exist."
return 0
fi
_info "Subdomain $fulldomain does not exist. Let's create one."
data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}"
res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data")
if ! _is_api_reply_ok "$res"; then
_err "Can't create subdomain."
return 1
fi
_debug "Cleanup subdomen records"
data="{\"fqdn\":\"$fulldomain\",\"records\":{}}"
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_debug "Can't cleanup $fulldomain records."
fi
data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}"
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data")
if ! _is_api_reply_ok "$res"; then
_debug "Can't cleanup www.$fulldomain records."
fi
return 0
}
# Usage: _get_root _acme-challenge.www.domain.com
#returns
# _sub_domain=_acme-challenge.www
# _domain=domain.com
# _domain_id=32436365
_get_root() {
fulldomain=$1
i=1
p=1
_debug "Get domain list"
res=$(_api_call "$Beget_Api/domain/getList")
if ! _is_api_reply_ok "$res"; then
_err "Can't get domain list."
return 1
fi
while true; do
h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
if _contains "$res" "$h"; then
_domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2)
if [ "$_domain_id" ]; then
if [ "$h" != "$fulldomain" ]; then
_sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p")
else
_sub_domain=""
fi
_domain=$h
return 0
fi
return 1
fi
p="$i"
i=$(_math "$i" + 1)
done
return 1
}
# Parce DNS records from json string
# Usage: _parce_records [j_str] [record_name]
_parce_records() {
j_str=$1
record_name=$2
res="\"$record_name\":["
res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1)
res=${res}"],"
echo "$res"
}
# Usage: _add_record [data] [record_name] [record_data]
_add_record() {
data=$1
record_name=$2
record_data=$3
echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/"
}
# Usage: _rm_record [data] [record_data]
_rm_record() {
data=$1
record_data=$2
echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" |
sed "s/{,/{/g" | sed "s/,}/}/g" |
sed "s/\[,/\[/g" | sed "s/,\]/\]/g"
}
_txt_to_dns_json() {
echo "{\"ttl\":600,\"txtdata\":\"$1\"}"
}
# Usage: _api_call [api_url] [input_data]
_api_call() {
api_url="$1"
input_data="$2"
_debug "_api_call $api_url"
_debug "Request: $input_data"
# res=$(curl -s -L -D ./http.header \
# "$api_url" \
# --data-urlencode login=$Beget_Username \
# --data-urlencode passwd=$Beget_Password \
# --data-urlencode input_format=json \
# --data-urlencode output_format=json \
# --data-urlencode "input_data=$input_data")
url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json"
if [ -n "$input_data" ]; then
url=${url}"&input_data="
url=${url}$(echo "$input_data" | _url_encode)
fi
res=$(_get "$url")
_debug "Reply: $res"
echo "$res"
}
# Usage: _is_api_reply_ok [api_reply]
_is_api_reply_ok() {
_contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$'
}

2
dnsapi/dns_bookmyname.sh

@ -7,7 +7,7 @@ Options:
BOOKMYNAME_USERNAME Username
BOOKMYNAME_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/3209
Author: Neilpang
Author: @Neilpang
'
######## Public functions #####################

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

5
dnsapi/dns_cloudns.sh

@ -197,10 +197,11 @@ _dns_cloudns_http_api_call() {
auth_user="auth-id=$CLOUDNS_AUTH_ID"
fi
encoded_password=$(echo "$CLOUDNS_AUTH_PASSWORD" | tr -d "\n\r" | _url_encode)
if [ -z "$2" ]; then
data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD"
data="$auth_user&auth-password=$encoded_password"
else
data="$auth_user&auth-password=$CLOUDNS_AUTH_PASSWORD&$2"
data="$auth_user&auth-password=$encoded_password&$2"
fi
response="$(_get "$CLOUDNS_API/$method?$data")"

5
dnsapi/dns_constellix.sh

@ -117,7 +117,7 @@ dns_constellix_rm() {
#################### Private functions below ##################################
_get_root() {
domain=$1
domain=$(echo "$1" | _lower_case)
i=2
p=1
_debug "Detecting root zone"
@ -156,6 +156,9 @@ _constellix_rest() {
data="$3"
_debug "$ep"
# Prevent rate limit
_sleep 2
rdate=$(date +"%s")"000"
hmac=$(printf "%s" "$rdate" | _hmac sha1 "$(printf "%s" "$CONSTELLIX_Secret" | _hex_dump | tr -d ' ')" | _base64)

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

53
dnsapi/dns_cyon.sh

@ -101,6 +101,8 @@ _cyon_load_parameters() {
# This header is required for curl calls.
_H1="X-Requested-With: XMLHttpRequest"
export _H1
_H3="User-Agent: cyon-dns-acmesh/1.0"
export _H3
}
_cyon_print_header() {
@ -125,7 +127,11 @@ _cyon_print_header() {
}
_cyon_get_cookie_header() {
printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')"
# Extract all cookies from the response headers (case-insensitive)
_cookies="$(grep -i "^set-cookie:" "$HTTP_HEADER" | sed 's/^[Ss]et-[Cc]ookie: //' | sed 's/;.*//' | tr '\n' '; ' | sed 's/; $//')"
if [ -n "$_cookies" ]; then
printf "Cookie: %s" "$_cookies"
fi
}
_cyon_login() {
@ -155,7 +161,12 @@ _cyon_login() {
_get "https://my.cyon.ch/" >/dev/null
# todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request.
# Update cookie after loading main page (only if new cookies are set)
_new_cookies="$(_cyon_get_cookie_header)"
if [ -n "$_new_cookies" ]; then
_H2="$_new_cookies"
export _H2
fi
# 2FA authentication with OTP?
if [ -n "${CY_OTP_Secret}" ]; then
@ -184,6 +195,13 @@ _cyon_login() {
fi
_info " success"
# Update cookie after 2FA (only if new cookies are set)
_new_cookies="$(_cyon_get_cookie_header)"
if [ -n "$_new_cookies" ]; then
_H2="$_new_cookies"
export _H2
fi
fi
_info ""
@ -205,7 +223,17 @@ _cyon_change_domain_env() {
domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')"
_debug "Changing domain environment to ${domain_env}"
gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
domain_page_response="$(_get "https://my.cyon.ch/domain/")"
_debug domain_page_response "${domain_page_response}"
# Check if we got an error response (JSON) instead of HTML
if printf "%s" "${domain_page_response}" | grep -q '"iserror":true'; then
_err " $(printf "%s" "${domain_page_response}" | _cyon_get_response_message)"
_err ""
return 1
fi
gloo_item_key="$(printf "%s" "${domain_page_response}" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")"
_debug gloo_item_key "${gloo_item_key}"
domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}"
@ -215,10 +243,8 @@ _cyon_change_domain_env() {
if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi
domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)"
# Bail if domain environment change fails.
if [ "${domain_env_success}" != "true" ]; then
if [ "$(printf "%s" "${domain_env_response}" | _cyon_get_environment_change_status)" != "true" ]; then
_err " $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)"
_err ""
return 1
@ -232,7 +258,7 @@ _cyon_add_txt() {
_info " - Adding DNS TXT entry..."
add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async"
add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}"
add_txt_data="name=${fulldomain_idn}.&ttl=900&type=TXT&dnscontent=${txtvalue}"
add_txt_response="$(_post "$add_txt_data" "$add_txt_url")"
_debug add_txt_response "${add_txt_response}"
@ -241,9 +267,10 @@ _cyon_add_txt() {
add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)"
add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)"
add_txt_validation="$(printf "%s" "${add_txt_response}" | _cyon_get_validation_status)"
# Bail if adding TXT entry fails.
if [ "${add_txt_status}" != "true" ]; then
if [ "${add_txt_status}" != "true" ] || [ "${add_txt_validation}" != "true" ]; then
_err " ${add_txt_message}"
_err ""
return 1
@ -305,13 +332,21 @@ _cyon_get_response_message() {
}
_cyon_get_response_status() {
_egrep_o '"status":\w*' | cut -d : -f 2
_egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2
}
_cyon_get_validation_status() {
_egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2
}
_cyon_get_response_success() {
_egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"'
}
_cyon_get_environment_change_status() {
_egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2
}
_cyon_check_if_2fa_missed() {
# Did we miss the 2FA?
if test "${1#*multi_factor_form}" != "${1}"; then

2
dnsapi/dns_ddnss.sh

@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_ddnss
Options:
DDNSS_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/2230
Author: RaidenII, helbgd, mod242
Author: @helbgd, @mod242
'
DDNSS_DNS_API="https://ddnss.de/upd.php"

2
dnsapi/dns_dnshome.sh

@ -7,7 +7,7 @@ Options:
DNSHOME_Subdomain Subdomain
DNSHOME_SubdomainPassword Subdomain Password
Issues: github.com/acmesh-official/acme.sh/issues/3819
Author: dnsHome.de https://github.com/dnsHome-de
Author: @dnsHome-de
'
# Usage: add subdomain.ddnsdomain.tld "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"

2
dnsapi/dns_duckdns.sh

@ -5,7 +5,7 @@ Site: www.DuckDNS.org
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns
Options:
DuckDNS_Token API Token
Author: RaidenII
Author: @RaidenII
'
DuckDNS_API="https://www.duckdns.org/update"

2
dnsapi/dns_dyn.sh

@ -8,7 +8,7 @@ Options:
DYN_Customer Customer
DYN_Username API Username
DYN_Password Secret
Author: Gerd Naschenweng <https://github.com/magicdude4eva>
Author: Gerd Naschenweng <@magicdude4eva>
'
# Dyn Managed DNS API

16
dnsapi/dns_dynv6.sh

@ -8,7 +8,7 @@ Options:
OptionsAlt:
KEY Path to SSH private key file. E.g. "/root/.ssh/dynv6"
Issues: github.com/acmesh-official/acme.sh/issues/2702
Author: StefanAbl
Author: @StefanAbl
'
dynv6_api="https://dynv6.com/api/v2"
@ -107,7 +107,7 @@ _get_domain() {
return 0
fi
done
_err "Either their is no such host on your dnyv6 account or it cannot be accessed with this key"
_err "Either there is no such host on your dynv6 account, or it cannot be accessed with this key"
return 1
}
@ -179,8 +179,8 @@ _dns_dynv6_rm_http() {
fi
}
#Usage: _get_zone_id $record
#get the zoneid for a specifc record or zone
#usage: _get_zone_id §record
#where $record is the record to get the id for
#returns _zone_id the id of the zone
_get_zone_id() {
@ -189,7 +189,6 @@ _get_zone_id() {
_dynv6_rest GET zones
zones="$(echo "$response" | tr '}' '\n' | tr ',' '\n' | grep name | sed 's/\[//g' | tr -d '{' | tr -d '"')"
#echo $zones
selected=""
for z in $zones; do
@ -217,9 +216,9 @@ _get_zone_name() {
_zone_name="${_zone_name#name:}"
}
#usaage _get_record_id $zone_id $record
# where zone_id is thevalue returned by _get_zone_id
# and record ist in the form _acme.www for an fqdn of _acme.www.example.com
#usage _get_record_id $zone_id $record
# where zone_id is the value returned by _get_zone_id
# and record is in the form _acme.www for an fqdn of _acme.www.example.com
# returns _record_id
_get_record_id() {
_zone_id="$1"
@ -234,8 +233,7 @@ _get_record_id() {
_get_record_id_from_response() {
response="$1"
_record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep id | tr -d '"' | tr -d 'id:')"
#_record_id="${_record_id#id:}"
_record_id="$(echo "$response" | tr '}' '\n' | grep "\"name\":\"$record\"" | grep "\"data\":\"$value\"" | tr ',' '\n' | grep '"id":' | tr -d '"' | tr -d 'id:' | tr -d '{')"
if [ -z "$_record_id" ]; then
_err "no such record: $record found in zone $_zone_id"
return 1

2
dnsapi/dns_easydns.sh

@ -7,7 +7,7 @@ Options:
EASYDNS_Token API Token
EASYDNS_Key API Key
Issues: github.com/acmesh-official/acme.sh/issues/2647
Author: Neilpang, wurzelpanzer <wurzelpanzer@maximolider.net>
Author: @Neilpang, wurzelpanzer <wurzelpanzer@maximolider.net>
'
# API Documentation: https://sandbox.rest.easydns.net:3001/

163
dnsapi/dns_edgecenter.sh

@ -0,0 +1,163 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_edgecenter_info='EdgeCenter.ru
Site: EdgeCenter.ru
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter
Options:
EDGECENTER_API_KEY API Key
Issues: github.com/acmesh-official/acme.sh/issues/6313
Author: Konstantin Ruchev <konstantin.ruchev@edgecenter.ru>
'
EDGECENTER_API="https://api.edgecenter.ru"
DOMAIN_TYPE=
DOMAIN_MASTER=
######## Public functions #####################
#Usage: dns_edgecenter_add _acme-challenge.www.domain.com "TXT_RECORD_VALUE"
dns_edgecenter_add() {
fulldomain="$1"
txtvalue="$2"
_info "Using EdgeCenter DNS API"
if ! _dns_edgecenter_init_check; then
return 1
fi
_debug "Detecting root zone for $fulldomain"
if ! _get_root "$fulldomain"; then
return 1
fi
subdomain="${fulldomain%."$_zone"}"
subdomain=${subdomain%.}
_debug "Zone: $_zone"
_debug "Subdomain: $subdomain"
_debug "TXT value: $txtvalue"
payload='{"resource_records": [ { "content": ["'"$txtvalue"'"] } ], "ttl": 60 }'
_dns_edgecenter_http_api_call "post" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$payload"
if _contains "$response" '"error":"rrset is already exists"'; then
_debug "RRSet exists, merging values"
_dns_edgecenter_http_api_call "get" "dns/v2/zones/$_zone/$subdomain.$_zone/txt"
current="$response"
newlist=""
for v in $(echo "$current" | sed -n 's/.*"content":\["\([^"]*\)"\].*/\1/p'); do
newlist="$newlist {\"content\":[\"$v\"]},"
done
newlist="$newlist{\"content\":[\"$txtvalue\"]}"
putdata="{\"resource_records\":[${newlist}]}
"
_dns_edgecenter_http_api_call "put" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$putdata"
_info "Updated existing RRSet with new TXT value."
return 0
fi
if _contains "$response" '"exception":'; then
_err "Record cannot be added."
return 1
fi
_info "TXT record added successfully."
return 0
}
#Usage: dns_edgecenter_rm _acme-challenge.www.domain.com "TXT_RECORD_VALUE"
dns_edgecenter_rm() {
fulldomain="$1"
txtvalue="$2"
_info "Removing TXT record for $fulldomain"
if ! _dns_edgecenter_init_check; then
return 1
fi
if ! _get_root "$fulldomain"; then
return 1
fi
subdomain="${fulldomain%."$_zone"}"
subdomain=${subdomain%.}
_dns_edgecenter_http_api_call "delete" "dns/v2/zones/$_zone/$subdomain.$_zone/txt"
if [ -z "$response" ]; then
_info "TXT record deleted successfully."
else
_info "TXT record may not have been deleted: $response"
fi
return 0
}
#################### Private functions below ##################################
_dns_edgecenter_init_check() {
EDGECENTER_API_KEY="${EDGECENTER_API_KEY:-$(_readaccountconf_mutable EDGECENTER_API_KEY)}"
if [ -z "$EDGECENTER_API_KEY" ]; then
_err "EDGECENTER_API_KEY was not exported."
return 1
fi
_saveaccountconf_mutable EDGECENTER_API_KEY "$EDGECENTER_API_KEY"
export _H1="Authorization: APIKey $EDGECENTER_API_KEY"
_dns_edgecenter_http_api_call "get" "dns/v2/clients/me/features"
if ! _contains "$response" '"id":'; then
_err "Invalid API key."
return 1
fi
return 0
}
_get_root() {
domain="$1"
i=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-)
if [ -z "$h" ]; then
return 1
fi
_dns_edgecenter_http_api_call "get" "dns/v2/zones/$h"
if ! _contains "$response" 'zone is not found'; then
_zone="$h"
return 0
fi
i=$((i + 1))
done
return 1
}
_dns_edgecenter_http_api_call() {
mtd="$1"
endpoint="$2"
data="$3"
export _H1="Authorization: APIKey $EDGECENTER_API_KEY"
case "$mtd" in
get)
response="$(_get "$EDGECENTER_API/$endpoint")"
;;
post)
response="$(_post "$data" "$EDGECENTER_API/$endpoint")"
;;
delete)
response="$(_post "" "$EDGECENTER_API/$endpoint" "" "DELETE")"
;;
put)
response="$(_post "$data" "$EDGECENTER_API/$endpoint" "" "PUT")"
;;
*)
_err "Unknown HTTP method $mtd"
return 1
;;
esac
_debug "HTTP $mtd response: $response"
return 0
}

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
}

2
dnsapi/dns_fornex.sh

@ -95,7 +95,7 @@ _get_root() {
return 1
fi
if ! _rest GET "dns/domain/"; then
if ! _rest GET "dns/domain/?q=$h"; then
return 1
fi

2
dnsapi/dns_freedns.sh

@ -7,7 +7,7 @@ Options:
FREEDNS_User Username
FREEDNS_Password Password
Issues: github.com/acmesh-official/acme.sh/issues/2305
Author: David Kerr <https://github.com/dkerr64>
Author: David Kerr <@dkerr64>
'
######## Public functions #####################

105
dnsapi/dns_freemyip.sh

@ -0,0 +1,105 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_freemyip_info='FreeMyIP.com
Site: FreeMyIP.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip
Options:
FREEMYIP_Token API Token
Issues: github.com/acmesh-official/acme.sh/issues/6247
Author: Recolic Keghart <root@recolic.net>, @Giova96
'
FREEMYIP_DNS_API="https://freemyip.com/update?"
################ Public functions ################
#Usage: dns_freemyip_add fulldomain txtvalue
dns_freemyip_add() {
fulldomain="$1"
txtvalue="$2"
_info "Add TXT record $txtvalue for $fulldomain using freemyip.com api"
FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}"
if [ -z "$FREEMYIP_Token" ]; then
FREEMYIP_Token=""
_err "You don't specify FREEMYIP_Token yet."
_err "Please specify your token and try again."
return 1
fi
#save the credentials to the account conf file.
_saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token"
if _is_root_domain_published "$fulldomain"; then
_err "freemyip API don't allow you to set multiple TXT record for the same subdomain!"
_err "You must apply certificate for only one domain at a time!"
_err "===="
_err "For example, aaa.yourdomain.freemyip.com and bbb.yourdomain.freemyip.com and yourdomain.freemyip.com ALWAYS share the same TXT record. They will overwrite each other if you apply multiple domain at the same time."
_debug "If you are testing this workflow in github pipeline or acmetest, please set TEST_DNS_NO_SUBDOMAIN=1 and TEST_DNS_NO_WILDCARD=1"
return 1
fi
# txtvalue must be url-encoded. But it's not necessary for acme txt value.
_freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=$txtvalue" 2>&1
return $?
}
#Usage: dns_freemyip_rm fulldomain txtvalue
dns_freemyip_rm() {
fulldomain="$1"
txtvalue="$2"
_info "Delete TXT record $txtvalue for $fulldomain using freemyip.com api"
FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}"
if [ -z "$FREEMYIP_Token" ]; then
FREEMYIP_Token=""
_err "You don't specify FREEMYIP_Token yet."
_err "Please specify your token and try again."
return 1
fi
#save the credentials to the account conf file.
_saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token"
# Leave the TXT record as empty or "null" to delete the record.
_freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=" 2>&1
return $?
}
################ Private functions below ################
_get_root() {
_fmi_d="$1"
echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev
}
# There is random failure while calling freemyip API too fast. This function automatically retry until success.
_freemyip_get_until_ok() {
_fmi_url="$1"
for i in $(seq 1 8); do
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..."
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0
_sleep 1 # DO NOT send the request too fast
done
_err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'"
return 1
}
# Verify in public dns if domain is already there.
_is_root_domain_published() {
_fmi_d="$1"
_webroot="$(_get_root "$_fmi_d")"
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet"
for i in $(seq 1 3); do
_debug "'$_webroot' ns lookup, retry $i/3..."
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then
_debug "'$_webroot' already has a TXT record published!"
return 0
fi
_sleep 10 # Give it some time to propagate the TXT record
done
return 1
}

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"

45
dnsapi/dns_he_ddns.sh

@ -0,0 +1,45 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_he_ddns_info='Hurricane Electric HE.net DDNS
Site: dns.he.net
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns
Options:
HE_DDNS_KEY The DDNS key
Issues: https://github.com/acmesh-official/acme.sh/issues/5238
Author: Markku Leiniö
'
HE_DDNS_URL="https://dyn.dns.he.net/nic/update"
######## Public functions #####################
#Usage: dns_he_ddns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
dns_he_ddns_add() {
fulldomain=$1
txtvalue=$2
HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}"
if [ -z "$HE_DDNS_KEY" ]; then
HE_DDNS_KEY=""
_err "You didn't specify a DDNS key for accessing the TXT record in HE API."
return 1
fi
#Save the DDNS key to the account conf file.
_saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY"
_info "Using Hurricane Electric DDNS API"
_debug fulldomain "$fulldomain"
_debug txtvalue "$txtvalue"
response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")"
_info "Response: $response"
_contains "$response" "good" && return 0 || return 1
}
# dns_he_ddns_rm() is not doing anything because the API call always updates the
# contents of the existing record (that the API key gives access to).
dns_he_ddns_rm() {
fulldomain=$1
_debug "Delete TXT record called for '${fulldomain}', not doing anything."
return 0
}

2
dnsapi/dns_hetzner.sh

@ -212,7 +212,7 @@ _get_root() {
_response_has_error() {
unset _response_error
err_part="$(echo "$response" | _egrep_o '"error":{[^}]*}')"
err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
if [ -n "$err_part" ]; then
err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)

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>

2
dnsapi/dns_joker.sh

@ -7,7 +7,7 @@ Options:
JOKER_USERNAME Username
JOKER_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/2840
Author: <https://github.com/aattww/>
Author: @aattww
'
JOKER_API="https://svc.joker.com/nic/replace"

96
dnsapi/dns_la.sh

@ -1,14 +1,17 @@
#!/usr/bin/env sh
# LA_Id="123"
# LA_Sk="456"
# shellcheck disable=SC2034
dns_la_info='dns.la
Site: dns.la
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_la
Options:
LA_Id API ID
LA_Key API key
LA_Id APIID
LA_Sk APISecret
LA_Token 用冒号连接 APIID APISecret 再base64生成
Issues: github.com/acmesh-official/acme.sh/issues/4257
'
LA_Api="https://api.dns.la/api"
######## Public functions #####################
@ -19,18 +22,23 @@ dns_la_add() {
txtvalue=$2
LA_Id="${LA_Id:-$(_readaccountconf_mutable LA_Id)}"
LA_Key="${LA_Key:-$(_readaccountconf_mutable LA_Key)}"
LA_Sk="${LA_Sk:-$(_readaccountconf_mutable LA_Sk)}"
_log "LA_Id=$LA_Id"
_log "LA_Sk=$LA_Sk"
if [ -z "$LA_Id" ] || [ -z "$LA_Key" ]; then
if [ -z "$LA_Id" ] || [ -z "$LA_Sk" ]; then
LA_Id=""
LA_Key=""
LA_Sk=""
_err "You didn't specify a dnsla api id and key yet."
return 1
fi
#save the api key and email to the account conf file.
_saveaccountconf_mutable LA_Id "$LA_Id"
_saveaccountconf_mutable LA_Key "$LA_Key"
_saveaccountconf_mutable LA_Sk "$LA_Sk"
# generate dnsla token
_la_token
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
@ -42,11 +50,13 @@ dns_la_add() {
_debug _domain "$_domain"
_info "Adding record"
if _la_rest "record.ashx?cmd=create&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&host=$_sub_domain&recordtype=TXT&recorddata=$txtvalue&recordline="; then
if _contains "$response" '"resultid":'; then
# record type is enum in new api, 16 for TXT
if _la_post "{\"domainId\":\"$_domain_id\",\"type\":16,\"host\":\"$_sub_domain\",\"data\":\"$txtvalue\",\"ttl\":600}" "record"; then
if _contains "$response" '"id":'; then
_info "Added, OK"
return 0
elif _contains "$response" '"code":532'; then
elif _contains "$response" '"msg":"与已有记录冲突"'; then
_info "Already exists, OK"
return 0
else
@ -54,7 +64,7 @@ dns_la_add() {
return 1
fi
fi
_err "Add txt record error."
_err "Add txt record failed."
return 1
}
@ -65,7 +75,9 @@ dns_la_rm() {
txtvalue=$2
LA_Id="${LA_Id:-$(_readaccountconf_mutable LA_Id)}"
LA_Key="${LA_Key:-$(_readaccountconf_mutable LA_Key)}"
LA_Sk="${LA_Sk:-$(_readaccountconf_mutable LA_Sk)}"
_la_token
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
@ -77,27 +89,29 @@ dns_la_rm() {
_debug _domain "$_domain"
_debug "Getting txt records"
if ! _la_rest "record.ashx?cmd=listn&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&domain=$_domain&host=$_sub_domain&recordtype=TXT&recorddata=$txtvalue"; then
# record type is enum in new api, 16 for TXT
if ! _la_get "recordList?pageIndex=1&pageSize=10&domainId=$_domain_id&host=$_sub_domain&type=16&data=$txtvalue"; then
_err "Error"
return 1
fi
if ! _contains "$response" '"recordid":'; then
if ! _contains "$response" '"id":'; then
_info "Don't need to remove."
return 0
fi
record_id=$(printf "%s" "$response" | grep '"recordid":' | cut -d : -f 2 | cut -d , -f 1 | tr -d '\r' | tr -d '\n')
record_id=$(printf "%s" "$response" | grep '"id":' | _head_n 1 | sed 's/.*"id": *"\([^"]*\)".*/\1/')
_debug "record_id" "$record_id"
if [ -z "$record_id" ]; then
_err "Can not get record id to remove."
return 1
fi
if ! _la_rest "record.ashx?cmd=remove&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domainid=$_domain_id&domain=$_domain&recordid=$record_id"; then
# remove record in new api is RESTful
if ! _la_post "" "record?id=$record_id" "DELETE"; then
_err "Delete record error."
return 1
fi
_contains "$response" '"code":300'
_contains "$response" '"code":200'
}
@ -119,12 +133,13 @@ _get_root() {
return 1
fi
if ! _la_rest "domain.ashx?cmd=get&apiid=$LA_Id&apipass=$LA_Key&rtype=json&domain=$h"; then
if ! _la_get "domain?domain=$h"; then
return 1
fi
if _contains "$response" '"domainid":'; then
_domain_id=$(printf "%s" "$response" | grep '"domainid":' | cut -d : -f 2 | cut -d , -f 1 | tr -d '\r' | tr -d '\n')
if _contains "$response" '"domain":'; then
_domain_id=$(echo "$response" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')
_log "_domain_id" "$_domain_id"
if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="$h"
@ -143,6 +158,21 @@ _la_rest() {
url="$LA_Api/$1"
_debug "$url"
if ! response="$(_get "$url" "Authorization: Basic $LA_Token" | tr -d ' ' | tr "}" ",")"; then
_err "Error: $url"
return 1
fi
_debug2 response "$response"
return 0
}
_la_get() {
url="$LA_Api/$1"
_debug "$url"
export _H1="Authorization: Basic $LA_Token"
if ! response="$(_get "$url" | tr -d ' ' | tr "}" ",")"; then
_err "Error: $url"
return 1
@ -151,3 +181,29 @@ _la_rest() {
_debug2 response "$response"
return 0
}
# Usage: _la_post body url [POST|PUT|DELETE]
_la_post() {
body=$1
url="$LA_Api/$2"
http_method=$3
_debug "$body"
_debug "$url"
export _H1="Authorization: Basic $LA_Token"
if ! response="$(_post "$body" "$url" "" "$http_method")"; then
_err "Error: $url"
return 1
fi
_debug2 response "$response"
return 0
}
_la_token() {
LA_Token=$(printf "%s:%s" "$LA_Id" "$LA_Sk" | _base64)
_debug "$LA_Token"
return 0
}

18
dnsapi/dns_limacity.sh

@ -1,13 +1,13 @@
#!/usr/bin/env sh
# Created by Laraveluser
#
# Pass credentials before "acme.sh --issue --dns dns_limacity ..."
# --
# export LIMACITY_APIKEY="<API-KEY>"
# --
#
# Pleas note: APIKEY must have following roles: dns.admin, domains.reader
# shellcheck disable=SC2034
dns_limacity_info='lima-city.de
Site: www.lima-city.de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_limacity
Options:
LIMACITY_APIKEY API Key. Note: The API Key must have following roles: dns.admin, domains.reader
Issues: github.com/acmesh-official/acme.sh/issues/4758
Author: @Laraveluser
'
######## Public functions #####################

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
}

214
dnsapi/dns_mijnhost.sh

@ -0,0 +1,214 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_mijnhost_info='mijn.host
Site: mijn.host
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mijnhost
Options:
MIJNHOST_API_KEY API Key
Issues: github.com/acmesh-official/acme.sh/issues/6177
Author: @peterv99
'
######## Public functions ######################
MIJNHOST_API="https://mijn.host/api/v2"
# Add TXT record for domain verification
dns_mijnhost_add() {
fulldomain=$1
txtvalue=$2
MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}"
if [ -z "$MIJNHOST_API_KEY" ]; then
MIJNHOST_API_KEY=""
_err "You haven't specified your mijn-host API key yet."
_err "Please add MIJNHOST_API_KEY to the env."
return 1
fi
# Save the API key for future use
_saveaccountconf_mutable MIJNHOST_API_KEY "$MIJNHOST_API_KEY"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
_debug2 _sub_domain "$_sub_domain"
_debug2 _domain "$_domain"
_debug "Adding DNS record" "${fulldomain}."
# Construct the API URL
api_url="$MIJNHOST_API/domains/$_domain/dns"
# Getting previous records
_mijnhost_rest GET "$api_url" ""
if [ "$_code" != "200" ]; then
_err "Error getting current DNS enties ($_code)"
return 1
fi
records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://')
_debug2 "Current records" "$records"
# Build the payload for the API
data="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"value\":\"$txtvalue\",\"ttl\":300}"
_debug2 "Record to add" "$data"
# Updating the records
updated_records=$(echo "$records" | sed -E "s/\]( *$)/,$data\]/")
_debug2 "Updated records" "$updated_records"
# data
data="{\"records\": $updated_records}"
_mijnhost_rest PUT "$api_url" "$data"
if [ "$_code" = "200" ]; then
_info "DNS record succesfully added."
return 0
else
_err "Error adding DNS record ($_code)."
return 1
fi
}
# Remove TXT record after verification
dns_mijnhost_rm() {
fulldomain=$1
txtvalue=$2
MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}"
if [ -z "$MIJNHOST_API_KEY" ]; then
MIJNHOST_API_KEY=""
_err "You haven't specified your mijn-host API key yet."
_err "Please add MIJNHOST_API_KEY to the env."
return 1
fi
_debug "Detecting root zone for" "${fulldomain}."
if ! _get_root "$fulldomain"; then
_err "Invalid domain"
return 1
fi
_debug "Removing DNS record for TXT value" "${txtvalue}."
# Construct the API URL
api_url="$MIJNHOST_API/domains/$_domain/dns"
# Get current records
_mijnhost_rest GET "$api_url" ""
if [ "$_code" != "200" ]; then
_err "Error getting current DNS enties ($_code)"
return 1
fi
_debug2 "Get current records response:" "$response"
records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://')
_debug2 "Current records:" "$records"
updated_records=$(echo "$records" | sed -E "s/\{[^}]*\"value\":\"$txtvalue\"[^}]*\},?//g" | sed 's/,]/]/g')
_debug2 "Updated records:" "$updated_records"
# Build the new payload
data="{\"records\": $updated_records}"
# Use the _put method to update the records
_mijnhost_rest PUT "$api_url" "$data"
if [ "$_code" = "200" ]; then
_info "DNS record removed successfully."
return 0
else
_err "Error removing DNS record ($_code)."
return 1
fi
}
# Helper function to detect the root zone
_get_root() {
domain=$1
# Get current records
_debug "Getting current domains"
_mijnhost_rest GET "$MIJNHOST_API/domains" ""
if [ "$_code" != "200" ]; then
_err "error getting current domains ($_code)"
return 1
fi
# Extract root domains from response
rootDomains=$(echo "$response" | _egrep_o '"domain":"[^"]*"' | sed -E 's/"domain":"([^"]*)"/\1/')
_debug "Root domains:" "$rootDomains"
for rootDomain in $rootDomains; do
if _contains "$domain" "$rootDomain"; then
_domain="$rootDomain"
_sub_domain=$(echo "$domain" | sed "s/.$rootDomain//g")
_debug "Found root domain" "$_domain" "and subdomain" "$_sub_domain" "for" "$domain"
return 0
fi
done
return 1
}
# Helper function for rest calls
_mijnhost_rest() {
m=$1
ep="$2"
data="$3"
MAX_REQUEST_RETRY_TIMES=15
_request_retry_times=0
_retry_sleep=5 #Initial sleep time in seconds.
while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do
_debug2 _request_retry_times "$_request_retry_times"
export _H1="API-Key: $MIJNHOST_API_KEY"
export _H2="Content-Type: application/json"
# clear headers from previous request to avoid getting wrong http code on timeouts
: >"$HTTP_HEADER"
_debug "$ep"
if [ "$m" != "GET" ]; then
_debug2 "data $data"
response="$(_post "$data" "$ep" "" "$m")"
else
response="$(_get "$ep")"
fi
_ret="$?"
_debug2 "response $response"
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
_debug "http response code $_code"
if [ "$_code" = "401" ]; then
# we have an invalid API token, maybe it is expired?
_err "Access denied. Invalid API token."
return 1
fi
if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "400" ] || _contains "$response" "DNS records not managed by mijn.host"; then #Sometimes API errors out
_request_retry_times="$(_math "$_request_retry_times" + 1)"
_info "REST call error $_code retrying $ep in ${_retry_sleep}s"
_sleep "$_retry_sleep"
_retry_sleep="$(_math "$_retry_sleep" \* 2)"
continue
fi
break
done
if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then
_err "Error mijn.host API call was retried $MAX_REQUEST_RETRY_TIMES times."
_err "Calling $ep failed."
return 1
fi
response="$(echo "$response" | _normalizeJson)"
return 0
}

10
dnsapi/dns_myapi.sh

@ -1,12 +1,14 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_myapi_info='Custom API Example
A sample custom DNS API script.
Domains: example.com
A sample custom DNS API script description.
Domains: example.com example.net
Site: github.com/acmesh-official/acme.sh/wiki/DNS-API-Dev-Guide
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_duckdns
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_myapi
Options:
MYAPI_Token API Token. Get API Token from https://example.com/api/. Optional.
MYAPI_Token API Token. Get API Token from https://example.com/api/
MYAPI_Variable2 Option 2. Default "default value".
MYAPI_Variable2 Option 3. Optional.
Issues: github.com/acmesh-official/acme.sh
Author: Neil Pang <neilgit@neilpang.com>
'

2
dnsapi/dns_mydnsjp.sh

@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_mydnsjp
Options:
MYDNSJP_MasterID Master ID
MYDNSJP_Password Password
Author: epgdatacapbon
Author: @tkmsst
'
######## Public functions #####################

2
dnsapi/dns_namecom.sh

@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_namecom
Options:
Namecom_Username Username
Namecom_Token API Token
Author: RaidenII
Author: @RaidenII
'
######## Public functions #####################

2
dnsapi/dns_namesilo.sh

@ -5,7 +5,7 @@ Site: NameSilo.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_namesilo
Options:
Namesilo_Key API Key
Author: meowthink
Author: @meowthink
'
#Utilize API to finish dns-01 verifications.

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
}

6
dnsapi/dns_netcup.sh

@ -19,7 +19,7 @@ client=""
dns_netcup_add() {
_debug NC_Apikey "$NC_Apikey"
login
_login
if [ "$NC_Apikey" = "" ] || [ "$NC_Apipw" = "" ] || [ "$NC_CID" = "" ]; then
_err "No Credentials given"
return 1
@ -61,7 +61,7 @@ dns_netcup_add() {
}
dns_netcup_rm() {
login
_login
fulldomain=$1
txtvalue=$2
@ -125,7 +125,7 @@ dns_netcup_rm() {
logout
}
login() {
_login() {
tmp=$(_post "{\"action\": \"login\", \"param\": {\"apikey\": \"$NC_Apikey\", \"apipassword\": \"$NC_Apipw\", \"customernumber\": \"$NC_CID\"}}" "$end" "" "POST")
sid=$(echo "$tmp" | tr '{}' '\n' | grep apisessionid | cut -d '"' -f 4)
_debug "$tmp"

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 from omg.lol. This is accessible from the bottom of the account page at https://home.omg.lol/account
OMG_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

1
dnsapi/dns_openprovider.sh

@ -2,6 +2,7 @@
# shellcheck disable=SC2034
dns_openprovider_info='OpenProvider.eu
Site: OpenProvider.eu
Domains: OpenProvider.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_openprovider
Options:
OPENPROVIDER_USER Username

186
dnsapi/dns_openprovider_rest.sh

@ -0,0 +1,186 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_openprovider_rest_info='OpenProvider (REST)
Domains: OpenProvider.com
Site: OpenProvider.eu
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_openprovider_rest
Options:
OPENPROVIDER_REST_USERNAME Openprovider Account Username
OPENPROVIDER_REST_PASSWORD Openprovider Account Password
Issues: github.com/acmesh-official/acme.sh/issues/6122
Author: Lambiek12
'
OPENPROVIDER_API_URL="https://api.openprovider.eu/v1beta"
######## Public functions #####################
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
dns_openprovider_rest_add() {
fulldomain=$1
txtvalue=$2
_openprovider_prepare_credentials || return 1
_debug "Try fetch OpenProvider DNS zone details"
if ! _get_dns_zone "$fulldomain"; then
_err "DNS zone not found within configured OpenProvider account."
return 1
fi
if [ -n "$_domain_id" ]; then
addzonerecordrequestparameters="dns/zones/$_domain_name"
addzonerecordrequestbody="{\"id\":$_domain_id,\"name\":\"$_domain_name\",\"records\":{\"add\":[{\"name\":\"$_sub_domain\",\"ttl\":900,\"type\":\"TXT\",\"value\":\"$txtvalue\"}]}}"
if _openprovider_rest PUT "$addzonerecordrequestparameters" "$addzonerecordrequestbody"; then
if _contains "$response" "\"success\":true"; then
return 0
elif _contains "$response" "\"Duplicate record\""; then
_debug "Record already existed"
return 0
else
_err "Adding TXT record failed due to errors."
return 1
fi
fi
fi
_err "Adding TXT record failed due to errors."
return 1
}
# Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to remove the txt record after validation
dns_openprovider_rest_rm() {
fulldomain=$1
txtvalue=$2
_openprovider_prepare_credentials || return 1
_debug "Try fetch OpenProvider DNS zone details"
if ! _get_dns_zone "$fulldomain"; then
_err "DNS zone not found within configured OpenProvider account."
return 1
fi
if [ -n "$_domain_id" ]; then
removezonerecordrequestparameters="dns/zones/$_domain_name"
removezonerecordrequestbody="{\"id\":$_domain_id,\"name\":\"$_domain_name\",\"records\":{\"remove\":[{\"name\":\"$_sub_domain\",\"ttl\":900,\"type\":\"TXT\",\"value\":\"\\\"$txtvalue\\\"\"}]}}"
if _openprovider_rest PUT "$removezonerecordrequestparameters" "$removezonerecordrequestbody"; then
if _contains "$response" "\"success\":true"; then
return 0
else
_err "Removing TXT record failed due to errors."
return 1
fi
fi
fi
_err "Removing TXT record failed due to errors."
return 1
}
#################### OpenProvider API common functions ####################
_openprovider_prepare_credentials() {
OPENPROVIDER_REST_USERNAME="${OPENPROVIDER_REST_USERNAME:-$(_readaccountconf_mutable OPENPROVIDER_REST_USERNAME)}"
OPENPROVIDER_REST_PASSWORD="${OPENPROVIDER_REST_PASSWORD:-$(_readaccountconf_mutable OPENPROVIDER_REST_PASSWORD)}"
if [ -z "$OPENPROVIDER_REST_USERNAME" ] || [ -z "$OPENPROVIDER_REST_PASSWORD" ]; then
OPENPROVIDER_REST_USERNAME=""
OPENPROVIDER_REST_PASSWORD=""
_err "You didn't specify the Openprovider username or password yet."
return 1
fi
#save the credentials to the account conf file.
_saveaccountconf_mutable OPENPROVIDER_REST_USERNAME "$OPENPROVIDER_REST_USERNAME"
_saveaccountconf_mutable OPENPROVIDER_REST_PASSWORD "$OPENPROVIDER_REST_PASSWORD"
}
_openprovider_rest() {
httpmethod=$1
queryparameters=$2
requestbody=$3
_openprovider_rest_login
if [ -z "$openproviderauthtoken" ]; then
_err "Unable to fetch authentication token from Openprovider API."
return 1
fi
export _H1="Content-Type: application/json"
export _H2="Accept: application/json"
export _H3="Authorization: Bearer $openproviderauthtoken"
if [ "$httpmethod" != "GET" ]; then
response="$(_post "$requestbody" "$OPENPROVIDER_API_URL/$queryparameters" "" "$httpmethod")"
else
response="$(_get "$OPENPROVIDER_API_URL/$queryparameters")"
fi
if [ "$?" != "0" ]; then
_err "No valid parameters supplied for Openprovider API: Error $queryparameters"
return 1
fi
_debug2 response "$response"
return 0
}
_openprovider_rest_login() {
export _H1="Content-Type: application/json"
export _H2="Accept: application/json"
loginrequesturl="$OPENPROVIDER_API_URL/auth/login"
loginrequestbody="{\"ip\":\"0.0.0.0\",\"password\":\"$OPENPROVIDER_REST_PASSWORD\",\"username\":\"$OPENPROVIDER_REST_USERNAME\"}"
loginresponse="$(_post "$loginrequestbody" "$loginrequesturl" "" "POST")"
openproviderauthtoken="$(printf "%s\n" "$loginresponse" | _egrep_o '"token" *: *"[^"]*' | _head_n 1 | sed 's#^"token" *: *"##')"
export openproviderauthtoken
}
#################### Private functions ##################################
# Usage: _get_dns_zone _acme-challenge.www.domain.com
# Returns:
# _domain_id=123456789
# _domain_name=domain.com
# _sub_domain=_acme-challenge.www
_get_dns_zone() {
domain=$1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
# Empty value not allowed
return 1
fi
if ! _openprovider_rest GET "dns/zones/$h" ""; then
return 1
fi
if _contains "$response" "\"name\":\"$h\""; then
_domain_id="$(printf "%s\n" "$response" | _egrep_o '"id" *: *[^,]*' | _head_n 1 | sed 's#^"id" *: *##')"
_debug _domain_id "$_domain_id"
_domain_name="$h"
_debug _domain_name "$_domain_name"
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_debug _sub_domain "$_sub_domain"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
}

30
dnsapi/dns_opnsense.sh

@ -110,15 +110,16 @@ rm_record() {
if _existingchallenge "$_domain" "$_host" "$new_challenge"; then
# Delete
if _opns_rest "POST" "/record/delRecord/${_uuid}" "\{\}"; then
if echo "$_return_str" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then
_opns_rest "POST" "/service/reconfigure" "{}"
if echo "$response" | _egrep_o "\"result\":\"deleted\"" >/dev/null; then
_debug "Record deleted"
_opns_rest "POST" "/service/reconfigure" "{}"
_debug "Service reconfigured"
else
_err "Error deleting record $_host from domain $fulldomain"
return 1
fi
else
_err "Error deleting record $_host from domain $fulldomain"
_err "Error requesting deletion of record $_host from domain $fulldomain"
return 1
fi
else
@ -150,14 +151,17 @@ _get_root() {
return 1
fi
_debug h "$h"
id=$(echo "$_domain_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
if [ -n "$id" ]; then
_debug id "$id"
_host=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="${h}"
_domainid="${id}"
return 0
fi
lines=$(echo "$_domain_response" | sed 's/{/\n/g')
for line in $lines; do
id=$(echo "$line" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"1\",\"type\":\"primary\",.*\"domainname\":\"${h}\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
if [ -n "$id" ]; then
_debug id "$id"
_host=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain="${h}"
_domainid="${id}"
return 0
fi
done
p=$i
i=$(_math "$i" + 1)
done
@ -206,13 +210,13 @@ _existingchallenge() {
return 1
fi
_uuid=""
_uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[^\"]*\",\"enabled\":\"[01]\",\"domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
_uuid=$(echo "$_record_response" | _egrep_o "\"uuid\":\"[a-z0-9\-]*\",\"enabled\":\"[01]\",\"domain\":\"[a-z0-9\-]*\",\"%domain\":\"$1\",\"name\":\"$2\",\"type\":\"TXT\",\"value\":\"$3\"" | cut -d ':' -f 2 | cut -d '"' -f 2)
if [ -n "$_uuid" ]; then
_debug uuid "$_uuid"
return 0
fi
_debug "${2}.$1{1} record not found"
_debug "${2}.${1} record not found"
return 1
}

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

2
dnsapi/dns_pdns.sh

@ -7,7 +7,7 @@ Options:
PDNS_Url API URL. E.g. "http://ns.example.com:8081"
PDNS_ServerId Server ID. E.g. "localhost"
PDNS_Token API Token
PDNS_Ttl=60 Domain TTL. Default: "60".
PDNS_Ttl Domain TTL. Default: "60".
'
DEFAULT_PDNS_TTL=60

2
dnsapi/dns_pleskxml.sh

@ -8,7 +8,7 @@ Options:
pleskxml_user Username
pleskxml_pass Password
Issues: github.com/acmesh-official/acme.sh/issues/2577
Author: Stilez, <https://github.com/romanlum>
Author: @Stilez, @romanlum
'
## Plesk XML API described at:

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
}

18
dnsapi/dns_rage4.sh

@ -42,6 +42,14 @@ dns_rage4_add() {
_debug _domain_id "$_domain_id"
_rage4_rest "createrecord/?id=$_domain_id&name=$fulldomain&content=$unquotedtxtvalue&type=TXT&active=true&ttl=1"
# Response after adding a TXT record should be something like this:
# {"status":true,"id":28160443,"error":null}
if ! _contains "$response" '"error":null' >/dev/null; then
_err "Error while adding TXT record: '$response'"
return 1
fi
return 0
}
@ -63,7 +71,12 @@ dns_rage4_rm() {
_debug "Getting txt records"
_rage4_rest "getrecords/?id=${_domain_id}"
_record_id=$(echo "$response" | sed -rn 's/.*"id":([[:digit:]]+)[^\}]*'"$txtvalue"'.*/\1/p')
_record_id=$(echo "$response" | tr '{' '\n' | grep '"TXT"' | grep "\"$txtvalue" | sed -rn 's/.*"id":([[:digit:]]+),.*/\1/p')
if [ -z "$_record_id" ]; then
_err "error retrieving the record_id of the new TXT record in order to delete it, got: '$_record_id'."
return 1
fi
_rage4_rest "deleterecord/?id=${_record_id}"
return 0
}
@ -105,8 +118,7 @@ _rage4_rest() {
token_trimmed=$(echo "$RAGE4_TOKEN" | tr -d '"')
auth=$(printf '%s:%s' "$username_trimmed" "$token_trimmed" | _base64)
export _H1="Content-Type: application/json"
export _H2="Authorization: Basic $auth"
export _H1="Authorization: Basic $auth"
response="$(_get "$RAGE4_Api$ep")"

2
dnsapi/dns_schlundtech.sh

@ -7,7 +7,7 @@ Options:
SCHLUNDTECH_USER Username
SCHLUNDTECH_PASSWORD Password
Issues: github.com/acmesh-official/acme.sh/issues/2246
Author: <https://github.com/mod242>
Author: @mod242
'
SCHLUNDTECH_API="https://gateway.schlundtech.de"

447
dnsapi/dns_selectel.sh

@ -4,11 +4,22 @@ dns_selectel_info='Selectel.com
Domains: Selectel.ru
Site: Selectel.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_selectel
Options:
SL_Key API Key
Options: For old API version v1 (deprecated)
SL_Ver API version. Use "v1".
SL_Key API Key
OptionsAlt: For the current API version v2
SL_Ver API version. Use "v2".
SL_Login_ID Account ID
SL_Project_Name Project name
SL_Login_Name Service user name
SL_Pswd Service user password
SL_Expire Token lifetime. In minutes (0-1440). Default "1400"
Issues: github.com/acmesh-official/acme.sh/issues/5126
'
SL_Api="https://api.selectel.ru/domains/v1"
SL_Api="https://api.selectel.ru/domains"
auth_uri="https://cloud.api.selcloud.ru/identity/v3/auth/tokens"
_sl_sep='#'
######## Public functions #####################
@ -17,17 +28,14 @@ dns_selectel_add() {
fulldomain=$1
txtvalue=$2
SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
if [ -z "$SL_Key" ]; then
SL_Key=""
_err "You don't specify selectel.ru api key yet."
_err "Please create you key and try again."
if ! _sl_init_vars; then
return 1
fi
#save the api key to the account conf file.
_saveaccountconf_mutable SL_Key "$SL_Key"
_debug2 SL_Ver "$SL_Ver"
_debug2 SL_Expire "$SL_Expire"
_debug2 SL_Login_Name "$SL_Login_Name"
_debug2 SL_Login_ID "$SL_Login_ID"
_debug2 SL_Project_Name "$SL_Project_Name"
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
@ -39,11 +47,63 @@ dns_selectel_add() {
_debug _domain "$_domain"
_info "Adding record"
if _sl_rest POST "/$_domain_id/records/" "{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"$fulldomain\", \"content\": \"$txtvalue\"}"; then
if _contains "$response" "$txtvalue" || _contains "$response" "record_already_exists"; then
if [ "$SL_Ver" = "v2" ]; then
_ext_srv1="/zones/"
_ext_srv2="/rrset/"
_text_tmp=$(echo "$txtvalue" | sed -En "s/[\"]*([^\"]*)/\1/p")
_text_tmp='\"'$_text_tmp'\"'
_data="{\"type\": \"TXT\", \"ttl\": 60, \"name\": \"${fulldomain}.\", \"records\": [{\"content\":\"$_text_tmp\"}]}"
elif [ "$SL_Ver" = "v1" ]; then
_ext_srv1="/"
_ext_srv2="/records/"
_data="{\"type\":\"TXT\",\"ttl\":60,\"name\":\"$fulldomain\",\"content\":\"$txtvalue\"}"
else
_err "Error. Unsupported version API $SL_Ver"
return 1
fi
_ext_uri="${_ext_srv1}$_domain_id${_ext_srv2}"
_debug _ext_uri "$_ext_uri"
_debug _data "$_data"
if _sl_rest POST "$_ext_uri" "$_data"; then
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
return 0
fi
if _contains "$response" "already_exists"; then
# record TXT with $fulldomain already exists
if [ "$SL_Ver" = "v2" ]; then
# It is necessary to add one more content to the comments
# read all records rrset
_debug "Getting txt records"
_sl_rest GET "${_ext_uri}"
# There is already a $txtvalue value, no need to add it
if _contains "$response" "$txtvalue"; then
_info "Added, OK"
_info "Txt record ${fulldomain} with value ${txtvalue} already exists"
return 0
fi
# group \1 - full record rrset; group \2 - records attribute value, exactly {"content":"\"value1\""},{"content":"\"value2\""}",...
_record_seg="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*${fulldomain}[^}]*records[^}]*\[(\{[^]]*\})\][^}]*}).*/\1/p")"
_record_array="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*${fulldomain}[^}]*records[^}]*\[(\{[^]]*\})\][^}]*}).*/\2/p")"
# record id
_record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"")"
# preparing _data
_tmp_str="${_record_array},{\"content\":\"${_text_tmp}\"}"
_data="{\"ttl\": 60, \"records\": [${_tmp_str}]}"
_debug2 _record_seg "$_record_seg"
_debug2 _record_array "$_record_array"
_debug2 _record_array "$_record_id"
_debug "New data for record" "$_data"
if _sl_rest PATCH "${_ext_uri}${_record_id}" "$_data"; then
_info "Added, OK"
return 0
fi
elif [ "$SL_Ver" = "v1" ]; then
_info "Added, OK"
return 0
fi
fi
fi
_err "Add txt record error."
return 1
@ -54,15 +114,15 @@ dns_selectel_rm() {
fulldomain=$1
txtvalue=$2
SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
if [ -z "$SL_Key" ]; then
SL_Key=""
_err "You don't specify slectel api key yet."
_err "Please create you key and try again."
if ! _sl_init_vars "nosave"; then
return 1
fi
_debug2 SL_Ver "$SL_Ver"
_debug2 SL_Expire "$SL_Expire"
_debug2 SL_Login_Name "$SL_Login_Name"
_debug2 SL_Login_ID "$SL_Login_ID"
_debug2 SL_Project_Name "$SL_Project_Name"
#
_debug "First detect the root zone"
if ! _get_root "$fulldomain"; then
_err "invalid domain"
@ -71,91 +131,195 @@ dns_selectel_rm() {
_debug _domain_id "$_domain_id"
_debug _sub_domain "$_sub_domain"
_debug _domain "$_domain"
#
if [ "$SL_Ver" = "v2" ]; then
_ext_srv1="/zones/"
_ext_srv2="/rrset/"
elif [ "$SL_Ver" = "v1" ]; then
_ext_srv1="/"
_ext_srv2="/records/"
else
_err "Error. Unsupported version API $SL_Ver"
return 1
fi
#
_debug "Getting txt records"
_sl_rest GET "/${_domain_id}/records/"
_ext_uri="${_ext_srv1}$_domain_id${_ext_srv2}"
_debug _ext_uri "$_ext_uri"
_sl_rest GET "${_ext_uri}"
#
if ! _contains "$response" "$txtvalue"; then
_err "Txt record not found"
return 1
fi
_record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")"
#
if [ "$SL_Ver" = "v2" ]; then
_record_seg="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\1/gp")"
_record_arr="$(echo "$response" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\3/p")"
elif [ "$SL_Ver" = "v1" ]; then
_record_seg="$(echo "$response" | _egrep_o "[^{]*\"content\" *: *\"$txtvalue\"[^}]*}")"
else
_err "Error. Unsupported version API $SL_Ver"
return 1
fi
_debug2 "_record_seg" "$_record_seg"
if [ -z "$_record_seg" ]; then
_err "can not find _record_seg"
return 1
fi
_record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2)"
_debug2 "_record_id" "$_record_id"
# record id
# the following lines change the algorithm for deleting records with the value $txtvalue
# if you use the 1st line, then all such records are deleted at once
# if you use the 2nd line, then only the first entry from them is deleted
#_record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"")"
_record_id="$(echo "$_record_seg" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\"" | cut -d : -f 2 | tr -d "\"" | sed '1!d')"
if [ -z "$_record_id" ]; then
_err "can not find _record_id"
return 1
fi
if ! _sl_rest DELETE "/$_domain_id/records/$_record_id"; then
_err "Delete record error."
return 1
_debug2 "_record_id" "$_record_id"
# delete all record type TXT with text $txtvalue
if [ "$SL_Ver" = "v2" ]; then
# actual
_new_arr="$(echo "$_record_seg" | sed -En "s/.*(\{\"id\"[^}]*records[^[]*(\[(\{[^]]*${txtvalue}[^]]*)\])[^}]*}).*/\3/gp" | sed -En "s/(\},\{)/}\n{/gp" | sed "/${txtvalue}/d" | sed ":a;N;s/\n/,/;ta")"
# uri record for DEL or PATCH
_del_uri="${_ext_uri}${_record_id}"
_debug _del_uri "$_del_uri"
if [ -z "$_new_arr" ]; then
# remove record
if ! _sl_rest DELETE "${_del_uri}"; then
_err "Delete record error: ${_del_uri}."
else
info "Delete record success: ${_del_uri}."
fi
else
# update a record by removing one element in content
_data="{\"ttl\": 60, \"records\": [${_new_arr}]}"
_debug2 _data "$_data"
# REST API PATCH call
if _sl_rest PATCH "${_del_uri}" "$_data"; then
_info "Patched, OK: ${_del_uri}"
else
_err "Patched record error: ${_del_uri}."
fi
fi
else
# legacy
for _one_id in $_record_id; do
_del_uri="${_ext_uri}${_one_id}"
_debug _del_uri "$_del_uri"
if ! _sl_rest DELETE "${_del_uri}"; then
_err "Delete record error: ${_del_uri}."
else
info "Delete record success: ${_del_uri}."
fi
done
fi
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
if ! _sl_rest GET "/"; then
return 1
fi
i=2
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
#not valid
if [ "$SL_Ver" = 'v1' ]; then
# version API 1
if ! _sl_rest GET "/"; then
return 1
fi
if _contains "$response" "\"name\" *: *\"$h\","; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
_debug "Getting domain id for $h"
if ! _sl_rest GET "/$h"; then
i=2
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
return 1
fi
_domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)"
return 0
if _contains "$response" "\"name\" *: *\"$h\","; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
_debug "Getting domain id for $h"
if ! _sl_rest GET "/$h"; then
_err "Error read records of all domains $SL_Ver"
return 1
fi
_domain_id="$(echo "$response" | tr "," "\n" | tr "}" "\n" | tr -d " " | grep "\"id\":" | cut -d : -f 2)"
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
_err "Error read records of all domains $SL_Ver"
return 1
elif [ "$SL_Ver" = "v2" ]; then
# version API 2
_ext_uri='/zones/'
domain="${domain}."
_debug "domain:: " "$domain"
# read records of all domains
if ! _sl_rest GET "$_ext_uri"; then
_err "Error read records of all domains $SL_Ver"
return 1
fi
p=$i
i=$(_math "$i" + 1)
done
return 1
i=1
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug h "$h"
if [ -z "$h" ]; then
_err "The domain was not found among the registered ones"
return 1
fi
_domain_record=$(echo "$response" | sed -En "s/.*(\{[^}]*id[^}]*\"name\" *: *\"$h\"[^}]*}).*/\1/p")
_debug "_domain_record:: " "$_domain_record"
if [ -n "$_domain_record" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
_domain=$h
_debug "Getting domain id for $h"
_domain_id=$(echo "$_domain_record" | sed -En "s/\{[^}]*\"id\" *: *\"([^\"]*)\"[^}]*\}/\1/p")
return 0
fi
p=$i
i=$(_math "$i" + 1)
done
_err "Error read records of all domains $SL_Ver"
return 1
else
_err "Error. Unsupported version API $SL_Ver"
return 1
fi
}
#################################################################
# use: method add_url body
_sl_rest() {
m=$1
ep="$2"
data="$3"
_debug "$ep"
export _H1="X-Token: $SL_Key"
_token=$(_get_auth_token)
if [ -z "$_token" ]; then
_err "BAD key or token $ep"
return 1
fi
if [ "$SL_Ver" = v2 ]; then
_h1_name="X-Auth-Token"
else
_h1_name='X-Token'
fi
export _H1="${_h1_name}: ${_token}"
export _H2="Content-Type: application/json"
_debug2 "Full URI: " "$SL_Api/${SL_Ver}${ep}"
_debug2 "_H1:" "$_H1"
_debug2 "_H2:" "$_H2"
if [ "$m" != "GET" ]; then
_debug data "$data"
response="$(_post "$data" "$SL_Api/$ep" "" "$m")"
response="$(_post "$data" "$SL_Api/${SL_Ver}${ep}" "" "$m")"
else
response="$(_get "$SL_Api/$ep")"
response="$(_get "$SL_Api/${SL_Ver}${ep}")"
fi
# shellcheck disable=SC2181
if [ "$?" != "0" ]; then
_err "error $ep"
return 1
@ -163,3 +327,152 @@ _sl_rest() {
_debug2 response "$response"
return 0
}
_get_auth_token() {
if [ "$SL_Ver" = 'v1' ]; then
# token for v1
_debug "Token v1"
_token_keystone=$SL_Key
elif [ "$SL_Ver" = 'v2' ]; then
# token for v2. Get a token for calling the API
_debug "Keystone Token v2"
token_v2=$(_readaccountconf_mutable SL_Token_V2)
if [ -n "$token_v2" ]; then
# The structure with the token was considered. Let's check its validity
# field 1 - SL_Login_Name
# field 2 - token keystone
# field 3 - SL_Login_ID
# field 4 - SL_Project_Name
# field 5 - Receipt time
# separator - '$_sl_sep'
_login_name=$(_getfield "$token_v2" 1 "$_sl_sep")
_token_keystone=$(_getfield "$token_v2" 2 "$_sl_sep")
_project_name=$(_getfield "$token_v2" 4 "$_sl_sep")
_receipt_time=$(_getfield "$token_v2" 5 "$_sl_sep")
_login_id=$(_getfield "$token_v2" 3 "$_sl_sep")
_debug2 _login_name "$_login_name"
_debug2 _login_id "$_login_id"
_debug2 _project_name "$_project_name"
# check the validity of the token for the user and the project and its lifetime
_dt_diff_minute=$((($(date +%s) - _receipt_time) / 60))
_debug2 _dt_diff_minute "$_dt_diff_minute"
[ "$_dt_diff_minute" -gt "$SL_Expire" ] && unset _token_keystone
if [ "$_project_name" != "$SL_Project_Name" ] || [ "$_login_name" != "$SL_Login_Name" ] || [ "$_login_id" != "$SL_Login_ID" ]; then
unset _token_keystone
fi
_debug "Get exists token"
fi
if [ -z "$_token_keystone" ]; then
# the previous token is incorrect or was not received, get a new one
_debug "Update (get new) token"
_data_auth="{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"name\":\"${SL_Login_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"},\"password\":\"${SL_Pswd}\"}}},\"scope\":{\"project\":{\"name\":\"${SL_Project_Name}\",\"domain\":{\"name\":\"${SL_Login_ID}\"}}}}}"
export _H1="Content-Type: application/json"
_result=$(_post "$_data_auth" "$auth_uri")
_token_keystone=$(grep 'x-subject-token' "$HTTP_HEADER" | sed -nE "s/[[:space:]]*x-subject-token:[[:space:]]*([[:print:]]*)(\r*)/\1/p")
_dt_curr=$(date +%s)
SL_Token_V2="${SL_Login_Name}${_sl_sep}${_token_keystone}${_sl_sep}${SL_Login_ID}${_sl_sep}${SL_Project_Name}${_sl_sep}${_dt_curr}"
_saveaccountconf_mutable SL_Token_V2 "$SL_Token_V2"
fi
else
# token set empty for unsupported version API
_token_keystone=""
fi
printf -- "%s" "$_token_keystone"
}
#################################################################
# use: [non_save]
_sl_init_vars() {
_non_save="${1}"
_debug2 _non_save "$_non_save"
_debug "First init variables"
# version API
SL_Ver="${SL_Ver:-$(_readaccountconf_mutable SL_Ver)}"
if [ -z "$SL_Ver" ]; then
SL_Ver="v1"
fi
if ! [ "$SL_Ver" = "v1" ] && ! [ "$SL_Ver" = "v2" ]; then
_err "You don't specify selectel.ru API version."
_err "Please define specify API version."
fi
_debug2 SL_Ver "$SL_Ver"
if [ "$SL_Ver" = "v1" ]; then
# token
SL_Key="${SL_Key:-$(_readaccountconf_mutable SL_Key)}"
if [ -z "$SL_Key" ]; then
SL_Key=""
_err "You don't specify selectel.ru api key yet."
_err "Please create you key and try again."
return 1
fi
#save the api key to the account conf file.
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Key "$SL_Key"
fi
elif [ "$SL_Ver" = "v2" ]; then
# time expire token
SL_Expire="${SL_Expire:-$(_readaccountconf_mutable SL_Expire)}"
if [ -z "$SL_Expire" ]; then
SL_Expire=1400 # 23h 20 min
fi
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Expire "$SL_Expire"
fi
# login service user
SL_Login_Name="${SL_Login_Name:-$(_readaccountconf_mutable SL_Login_Name)}"
if [ -z "$SL_Login_Name" ]; then
SL_Login_Name=''
_err "You did not specify the selectel.ru API service user name."
_err "Please provide a service user name and try again."
return 1
fi
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Login_Name "$SL_Login_Name"
fi
# user ID
SL_Login_ID="${SL_Login_ID:-$(_readaccountconf_mutable SL_Login_ID)}"
if [ -z "$SL_Login_ID" ]; then
SL_Login_ID=''
_err "You did not specify the selectel.ru API user ID."
_err "Please provide a user ID and try again."
return 1
fi
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Login_ID "$SL_Login_ID"
fi
# project name
SL_Project_Name="${SL_Project_Name:-$(_readaccountconf_mutable SL_Project_Name)}"
if [ -z "$SL_Project_Name" ]; then
SL_Project_Name=''
_err "You did not specify the project name."
_err "Please provide a project name and try again."
return 1
fi
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Project_Name "$SL_Project_Name"
fi
# service user password
SL_Pswd="${SL_Pswd:-$(_readaccountconf_mutable SL_Pswd)}"
if [ -z "$SL_Pswd" ]; then
SL_Pswd=''
_err "You did not specify the service user password."
_err "Please provide a service user password and try again."
return 1
fi
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Pswd "$SL_Pswd" "12345678"
fi
else
SL_Ver=""
_err "You also specified the wrong version of the selectel.ru API."
_err "Please provide the correct API version and try again."
return 1
fi
if [ -z "$_non_save" ]; then
_saveaccountconf_mutable SL_Ver "$SL_Ver"
fi
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]" "$@"
}

212
dnsapi/dns_spaceship.sh

@ -0,0 +1,212 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_spaceship_info='Spaceship.com
Site: Spaceship.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship
Options:
SPACESHIP_API_KEY API Key
SPACESHIP_API_SECRET API Secret
SPACESHIP_ROOT_DOMAIN Root domain. Manually specify the root domain if auto-detection fails. Optional.
Issues: github.com/acmesh-official/acme.sh/issues/6304
Author: Meow <@Meo597>
'
# Spaceship API
# https://docs.spaceship.dev/
######## Public functions #####################
SPACESHIP_API_BASE="https://spaceship.dev/api/v1"
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
# Used to add txt record
dns_spaceship_add() {
fulldomain="$1"
txtvalue="$2"
_info "Adding TXT record for $fulldomain with value $txtvalue"
# Initialize API credentials and headers
if ! _spaceship_init; then
return 1
fi
# Detect root zone
if ! _get_root "$fulldomain"; then
return 1
fi
# Extract subdomain part relative to root domain
subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//")
if [ "$subdomain" = "$fulldomain" ]; then
_err "Failed to extract subdomain from $fulldomain relative to root domain $_domain"
return 1
fi
_debug "Extracted subdomain: $subdomain for root domain: $_domain"
# Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue)
escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g')
# Prepare payload and URL for adding TXT record
# Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API
payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}"
url="$SPACESHIP_API_BASE/dns/records/$_domain"
# Send API request
if _spaceship_api_request "PUT" "$url" "$payload"; then
_info "Successfully added TXT record for $fulldomain"
return 0
else
_err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain."
return 1
fi
}
# Usage: fulldomain txtvalue
# Used to remove the txt record after validation
dns_spaceship_rm() {
fulldomain="$1"
txtvalue="$2"
_info "Removing TXT record for $fulldomain with value $txtvalue"
# Initialize API credentials and headers
if ! _spaceship_init; then
return 1
fi
# Detect root zone
if ! _get_root "$fulldomain"; then
return 1
fi
# Extract subdomain part relative to root domain
subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//")
if [ "$subdomain" = "$fulldomain" ]; then
_err "Failed to extract subdomain from $fulldomain relative to root domain $_domain"
return 1
fi
_debug "Extracted subdomain: $subdomain for root domain: $_domain"
# Escape txtvalue to prevent JSON injection
escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g')
# Prepare payload and URL for deleting TXT record
# Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API
payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]"
url="$SPACESHIP_API_BASE/dns/records/$_domain"
# Send API request
if _spaceship_api_request "DELETE" "$url" "$payload"; then
_info "Successfully deleted TXT record for $fulldomain"
return 0
else
_err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain."
return 1
fi
}
#################### Private functions below ##################################
_spaceship_init() {
SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}"
SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}"
if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then
_err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET."
_err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials."
return 1
fi
# Save credentials to account config for future renewals
_saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY"
_saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET"
# Set common headers for API requests
export _H1="X-API-Key: $SPACESHIP_API_KEY"
export _H2="X-API-Secret: $SPACESHIP_API_SECRET"
export _H3="Content-Type: application/json"
return 0
}
_get_root() {
domain="$1"
# Check manual override
SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}"
if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then
_domain="$SPACESHIP_ROOT_DOMAIN"
_debug "Using manually specified or saved root domain: $_domain"
_savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN"
return 0
fi
_debug "Detecting root zone for '$domain'"
i=1
p=1
while true; do
_cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
_debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)"
if [ -z "$_cutdomain" ]; then
_debug "Cut resulted in empty string, root zone not found."
break
fi
# Call the API to check if this _cutdomain is a manageable zone
if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then
# API call succeeded (HTTP 200 OK for GET /dns/records)
_domain="$_cutdomain"
_debug "Root zone found: '$_domain'"
# Save the detected root domain
_savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain"
_info "Root domain '$_domain' saved to configuration for future use."
return 0
fi
_debug "API check failed for '$_cutdomain'. Continuing search."
p=$i
i=$((i + 1))
done
_err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually."
return 1
}
_spaceship_api_request() {
method="$1"
url="$2"
payload="$3"
_debug2 "Sending $method request to $url with payload $payload"
if [ "$method" = "GET" ]; then
response="$(_get "$url")"
else
response="$(_post "$payload" "$url" "" "$method")"
fi
if [ "$?" != "0" ]; then
_err "API request failed. Response: $response"
return 1
fi
_debug2 "API response body: $response"
if [ "$method" = "GET" ]; then
if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then
return 0
fi
else
if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then
return 0
fi
fi
_debug2 "API response header: $HTTP_HEADER"
return 1
}

7
dnsapi/dns_technitium.sh

@ -1,13 +1,12 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
dns_Technitium_info='Technitium DNS Server
Site: https://technitium.com/dns/
dns_technitium_info='Technitium DNS Server
Site: Technitium.com/dns/
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_technitium
Options:
Technitium_Server Server Address
Technitium_Token API Token
Issues:https://github.com/acmesh-official/acme.sh/issues/6116
Issues: github.com/acmesh-official/acme.sh/issues/6116
Author: Henning Reich <acmesh@qupfer.de>
'

2
dnsapi/dns_tele3.sh

@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#tele3
Options:
TELE3_Key API Key
TELE3_Secret API Secret
Author: Roman Blizik <https://github.com/par-pa>
Author: Roman Blizik <@par-pa>
'
TELE3_API="https://www.tele3.cz/acme/"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save