mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2025-12-28 23:22:09 +08:00
Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bd2922bc3 | ||
|
|
47f24126f5 | ||
|
|
76fdac59bc | ||
|
|
49a3d586a3 | ||
|
|
361e7c5ad4 | ||
|
|
3ae9711892 | ||
|
|
fc7168e11d | ||
|
|
260df0048b | ||
|
|
11cae37405 | ||
|
|
61b59831c4 | ||
|
|
96f38655b4 | ||
|
|
ab7835ec58 | ||
|
|
b8b1f1e9b4 | ||
|
|
6b66e734a9 | ||
|
|
4f0a4850a6 | ||
|
|
8a78865174 | ||
|
|
69dd2cf78b | ||
|
|
a961e03a59 | ||
|
|
1eee4dee9c | ||
|
|
1d8788767f | ||
|
|
986a6138eb | ||
|
|
f850e8d0e4 | ||
|
|
d375012c5d | ||
|
|
37c25aa107 | ||
|
|
7b16526e7f | ||
|
|
093f36b4d6 | ||
|
|
e5b47f6402 | ||
|
|
a55d40be97 | ||
|
|
c1c49d5a01 | ||
|
|
88e4d64c1a | ||
|
|
17e0bbcbb6 | ||
|
|
95c7546051 | ||
|
|
c1e17c366f | ||
|
|
88cde7be6d | ||
|
|
c16e059535 | ||
|
|
88d4637ee3 | ||
|
|
74ed0354a3 | ||
|
|
2cc5e66517 | ||
|
|
ba7c368ee5 | ||
|
|
768de270bf | ||
|
|
db1dc4de0d | ||
|
|
fb0926dc81 | ||
|
|
34eb2a655a | ||
|
|
23e1a53ec8 | ||
|
|
67d58a12e7 | ||
|
|
0ed5e21232 | ||
|
|
b2eb1d2bbc | ||
|
|
3c184486c3 | ||
|
|
5a730bf00d | ||
|
|
cbd5dae3b4 | ||
|
|
b9c877adb9 | ||
|
|
d24c7e977e | ||
|
|
6a1ff1c0a6 | ||
|
|
383557df61 | ||
|
|
dba4be8065 | ||
|
|
a670c07caf | ||
|
|
09cc2bdfa5 | ||
|
|
cc897cab4c | ||
|
|
7ba9597928 | ||
|
|
d2d862420e | ||
|
|
fd6a14de8a | ||
|
|
0eb40c6ce6 | ||
|
|
e321b3c75c | ||
|
|
03d8d3bc1b | ||
|
|
f85de2b0d3 | ||
|
|
5fb42b7339 | ||
|
|
1b2630dc0d | ||
|
|
3fb4c313ec | ||
|
|
65892453be | ||
|
|
cba0ff8321 | ||
|
|
b6523c2301 | ||
|
|
e92d0a7492 | ||
|
|
0e5aab346f | ||
|
|
a5ad15be02 | ||
|
|
27ebf09c5c | ||
|
|
1c65c04b54 | ||
|
|
987882ea37 | ||
|
|
00aaed1b14 | ||
|
|
e3b1bccb6a | ||
|
|
85ff92170b | ||
|
|
7ac8c6c75b | ||
|
|
6004e7f5cd | ||
|
|
4a7e5d0720 | ||
|
|
b4042d5ccb | ||
|
|
1413aa332b | ||
|
|
37cf431e80 | ||
|
|
0c9d2dafe3 | ||
|
|
e8708a7489 | ||
|
|
79592c700f | ||
|
|
ad3783170e | ||
|
|
329dab9a67 | ||
|
|
f142f37064 | ||
|
|
0d2955b48d | ||
|
|
95da407de8 | ||
|
|
503ca1e9c2 | ||
|
|
d8722c46d9 | ||
|
|
546c2d47d5 | ||
|
|
2ba615555c | ||
|
|
e94c6be4a1 | ||
|
|
11eaad1fa7 | ||
|
|
daf7f7c268 | ||
|
|
4965c704d7 | ||
|
|
b7fe7a40ba | ||
|
|
3b2c2b16b2 | ||
|
|
45cb36f6d9 | ||
|
|
70e965fd55 | ||
|
|
bee01c938a | ||
|
|
5cbae50ec1 | ||
|
|
51b4fa0080 | ||
|
|
64a6ea68fa | ||
|
|
d97b4477b2 | ||
|
|
b8e394e76a | ||
|
|
671d542898 | ||
|
|
9980ad0fef | ||
|
|
004deaeea1 | ||
|
|
36b8ca2bc0 | ||
|
|
890ab4a7bb | ||
|
|
490b9e2d09 | ||
|
|
ca35e8c118 | ||
|
|
eeb91de6a3 | ||
|
|
657b7195d6 | ||
|
|
054a73f297 | ||
|
|
5c6d8aacbe | ||
|
|
ac0df6bc88 | ||
|
|
f007b46c1b | ||
|
|
c5566eafeb | ||
|
|
75ee17aeeb | ||
|
|
ad2cb507a4 | ||
|
|
dca23a98f1 | ||
|
|
8cb9713493 | ||
|
|
ef76831d37 | ||
|
|
c4671272c0 | ||
|
|
86f2584162 | ||
|
|
020c52e583 | ||
|
|
eda8614754 | ||
|
|
ca08ce4262 | ||
|
|
eb22a84db4 | ||
|
|
d66dd99621 | ||
|
|
5a085f2514 | ||
|
|
e79ee7fb74 | ||
|
|
718ff3a5f5 | ||
|
|
a2e52dadb9 | ||
|
|
ca4cb018d0 | ||
|
|
c2762d3b6f | ||
|
|
947e872850 | ||
|
|
3baa5e145f | ||
|
|
67fd35127c | ||
|
|
c421e2ddfc | ||
|
|
42febe97b5 | ||
|
|
74ca0fb763 | ||
|
|
13631ea2de | ||
|
|
a1eee5923a | ||
|
|
af92bbca2a | ||
|
|
c9287071e3 | ||
|
|
292026288a | ||
|
|
1f056998f3 | ||
|
|
4d7cb7de5f | ||
|
|
67855f21d4 | ||
|
|
8484565e95 | ||
|
|
f7d8abe8ea | ||
|
|
8ca90297e7 | ||
|
|
e08f9080c2 | ||
|
|
419738fbd5 | ||
|
|
7f1423dd6f | ||
|
|
9f09dcd18c | ||
|
|
91081ade3c | ||
|
|
4d933c23a8 | ||
|
|
f29bfd995d | ||
|
|
30d5d1aea9 | ||
|
|
7a0450a7f4 | ||
|
|
5bb09f469f | ||
|
|
90e9d8ff52 | ||
|
|
59a43ce5d1 | ||
|
|
5bc01aa251 | ||
|
|
9eeb979c7b | ||
|
|
eabd7592fe | ||
|
|
e089a3d8a1 | ||
|
|
7560375502 | ||
|
|
1f77b89266 | ||
|
|
7c610124d9 | ||
|
|
a0c5ef4e6f | ||
|
|
218934e767 | ||
|
|
c6a9825c0a |
65
.github/workflows/DNS.yml
vendored
65
.github/workflows/DNS.yml
vendored
@@ -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}}"
|
||||
@@ -283,7 +283,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}}"
|
||||
@@ -335,7 +335,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}}"
|
||||
@@ -387,7 +387,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}}"
|
||||
@@ -440,8 +440,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
|
||||
@@ -491,7 +493,56 @@ 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
|
||||
|
||||
|
||||
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@v0
|
||||
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
|
||||
|
||||
2
.github/workflows/DragonFlyBSD.yml
vendored
2
.github/workflows/DragonFlyBSD.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
prepare: |
|
||||
pkg install -y curl socat libnghttp2
|
||||
usesh: true
|
||||
copyback: false
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
|
||||
2
.github/workflows/FreeBSD.yml
vendored
2
.github/workflows/FreeBSD.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
"8080": "80"
|
||||
prepare: pkg install -y socat curl wget
|
||||
usesh: true
|
||||
copyback: false
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
|
||||
2
.github/workflows/NetBSD.yml
vendored
2
.github/workflows/NetBSD.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
prepare: |
|
||||
/usr/sbin/pkg_add curl socat
|
||||
usesh: true
|
||||
copyback: false
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
|
||||
2
.github/workflows/Omnios.yml
vendored
2
.github/workflows/Omnios.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
||||
nat: |
|
||||
"8080": "80"
|
||||
prepare: pkg install socat wget
|
||||
copyback: false
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
|
||||
2
.github/workflows/OpenBSD.yml
vendored
2
.github/workflows/OpenBSD.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
"8080": "80"
|
||||
prepare: pkg_add socat curl wget libnghttp2
|
||||
usesh: true
|
||||
copyback: false
|
||||
sync: nfs
|
||||
run: |
|
||||
cd ../acmetest \
|
||||
&& ./letest.sh
|
||||
|
||||
75
.github/workflows/OpenIndiana.yml
vendored
Normal file
75
.github/workflows/OpenIndiana.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
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@v0
|
||||
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
|
||||
|
||||
|
||||
6
.github/workflows/Solaris.yml
vendored
6
.github/workflows/Solaris.yml
vendored
@@ -66,8 +66,10 @@ 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
|
||||
|
||||
5
.github/workflows/wiki-monitor.yml
vendored
5
.github/workflows/wiki-monitor.yml
vendored
@@ -22,6 +22,7 @@ jobs:
|
||||
page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH")
|
||||
page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH")
|
||||
page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH")
|
||||
page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH")
|
||||
now="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
cd wiki
|
||||
@@ -35,9 +36,11 @@ jobs:
|
||||
{
|
||||
echo "Wiki edited"
|
||||
echo -n "User: "
|
||||
echo "[$actor]($sender_url)"
|
||||
echo "@$actor [$actor]($sender_url)"
|
||||
echo "Time: $now"
|
||||
echo "Page: [$page_name]($page_url) (Action: $page_action)"
|
||||
echo "Comment: $page_summary"
|
||||
echo "[Click here to Revert](${page_url}/_history)"
|
||||
echo ""
|
||||
echo "----"
|
||||
echo "### diff:"
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN apk --no-cache add -f \
|
||||
tar \
|
||||
libidn \
|
||||
jq \
|
||||
yq-go \
|
||||
cronie
|
||||
|
||||
ENV LE_WORKING_DIR=/acmebin
|
||||
|
||||
403
README.md
403
README.md
@@ -1,54 +1,71 @@
|
||||
[](https://zerossl.com/?fromacme.sh)
|
||||
<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>
|
||||
|
||||
# An ACME Shell script: acme.sh
|
||||
<h1 align="center">🔐 acme.sh</h1>
|
||||
<h3 align="center">An ACME Protocol Client Written Purely in Shell</h3>
|
||||
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)
|
||||
[](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)
|
||||
<p align="center">
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/FreeBSD.yml/badge.svg" alt="FreeBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenBSD.yml/badge.svg" alt="OpenBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml/badge.svg" alt="NetBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/MacOS.yml/badge.svg" alt="MacOS"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Ubuntu.yml/badge.svg" alt="Ubuntu"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Windows.yml/badge.svg" alt="Windows"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Solaris.yml/badge.svg" alt="Solaris"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml/badge.svg" alt="DragonFlyBSD"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml/badge.svg" alt="Omnios"></a>
|
||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml/badge.svg" alt="OpenIndiana"></a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||

|
||||
<p align="center">
|
||||
<img src="https://github.com/acmesh-official/acme.sh/workflows/Shellcheck/badge.svg" alt="Shellcheck">
|
||||
<img src="https://github.com/acmesh-official/acme.sh/workflows/PebbleStrict/badge.svg" alt="PebbleStrict">
|
||||
<img src="https://github.com/acmesh-official/acme.sh/workflows/Build%20DockerHub/badge.svg" alt="DockerHub">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://opencollective.com/acmesh"><img src="https://opencollective.com/acmesh/all/badge.svg?label=financial+contributors" alt="Financial Contributors on Open Collective"></a>
|
||||
<a href="https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/acme-sh/Lobby.svg" alt="Join the chat at Gitter"></a>
|
||||
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/stars/neilpang/acme.sh.svg" alt="Docker stars"></a>
|
||||
<a href="https://hub.docker.com/r/neilpang/acme.sh" title="Click to view the image on Docker Hub"><img src="https://img.shields.io/docker/pulls/neilpang/acme.sh.svg" alt="Docker pulls"></a>
|
||||
</p>
|
||||
|
||||
|
||||
<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>
|
||||
[](https://gitter.im/acme-sh/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
|
||||
[](https://hub.docker.com/r/neilpang/acme.sh "Click to view the image on Docker Hub")
|
||||
---
|
||||
|
||||
## ✨ 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
|
||||
|
||||
- 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 free certificates.
|
||||
|
||||
It's probably the `easiest & smartest` shell script to automatically issue & renew the 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>
|
||||
|
||||
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)
|
||||
## 🌏 [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
|
||||
|
||||
Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|
||||
---
|
||||
|
||||
|
||||
# [中文说明](https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E)
|
||||
|
||||
# Who:
|
||||
## 🏆 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)
|
||||
@@ -62,7 +79,9 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|
||||
- [lnmp.org](https://lnmp.org/)
|
||||
- [more...](https://github.com/acmesh-official/acme.sh/wiki/Blogs-and-tutorials)
|
||||
|
||||
# Tested OS
|
||||
---
|
||||
|
||||
## 🖥️ Tested OS
|
||||
|
||||
| NO | Status| Platform|
|
||||
|----|-------|---------|
|
||||
@@ -76,8 +95,8 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|
||||
|8|[](https://github.com/acmesh-official/acme.sh/actions/workflows/NetBSD.yml)|NetBSD
|
||||
|9|[](https://github.com/acmesh-official/acme.sh/actions/workflows/DragonFlyBSD.yml)|DragonFlyBSD
|
||||
|10|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Omnios.yml)|Omnios
|
||||
|11|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|
||||
|12|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|CentOS
|
||||
|11|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenIndiana.yml)|OpenIndiana
|
||||
|12|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)| Debian
|
||||
|13|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|openSUSE
|
||||
|14|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Alpine Linux (with curl)
|
||||
|15|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Archlinux
|
||||
@@ -85,56 +104,66 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
|
||||
|17|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Kali Linux
|
||||
|18|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Oracle Linux
|
||||
|19|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Mageia
|
||||
|10|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Linux.yml)|Gentoo Linux
|
||||
|22|-----| Cloud Linux https://github.com/acmesh-official/acme.sh/issues/111
|
||||
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|
||||
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|
||||
|20|[](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://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|
||||
|
||||
|
||||
Check our [testing project](https://github.com/acmesh-official/acmetest):
|
||||
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
||||
>
|
||||
> 🖥️ The testing VMs are supported by [vmactions.org](https://vmactions.org)
|
||||
|
||||
https://github.com/acmesh-official/acmetest
|
||||
---
|
||||
|
||||
# Supported CA
|
||||
## 🏛️ Supported CA
|
||||
|
||||
- [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA)(default)
|
||||
- Letsencrypt.org CA
|
||||
- [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA)
|
||||
- [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA)
|
||||
- [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA)
|
||||
- [Pebble strict Mode](https://github.com/letsencrypt/pebble)
|
||||
- Any other [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA
|
||||
| CA | Status |
|
||||
|---|---|
|
||||
| [ZeroSSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/ZeroSSL.com-CA) | ⭐ **Default** |
|
||||
| Letsencrypt.org CA | ✅ Supported |
|
||||
| [SSL.com CA](https://github.com/acmesh-official/acme.sh/wiki/SSL.com-CA) | ✅ Supported |
|
||||
| [Google.com Public CA](https://github.com/acmesh-official/acme.sh/wiki/Google-Public-CA) | ✅ Supported |
|
||||
| [Actalis.com CA](https://github.com/acmesh-official/acme.sh/wiki/Actalis.com-CA) | ✅ Supported |
|
||||
| [Pebble strict Mode](https://github.com/letsencrypt/pebble) | ✅ Supported |
|
||||
| Any [RFC8555](https://tools.ietf.org/html/rfc8555)-compliant CA | ✅ Supported |
|
||||
|
||||
# Supported modes
|
||||
---
|
||||
|
||||
- Webroot mode
|
||||
- Standalone mode
|
||||
- Standalone tls-alpn mode
|
||||
- Apache mode
|
||||
- Nginx mode
|
||||
- DNS mode
|
||||
- [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode)
|
||||
- [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode)
|
||||
## ⚙️ Supported Modes
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| 📁 Webroot mode | Use existing webroot directory |
|
||||
| 🖥️ Standalone mode | Built-in webserver on port 80 |
|
||||
| 🔐 Standalone tls-alpn mode | Built-in webserver on port 443 |
|
||||
| 🪶 Apache mode | Use Apache for verification |
|
||||
| ⚡ Nginx mode | Use Nginx for verification |
|
||||
| 🌐 DNS mode | Use DNS TXT records |
|
||||
| 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification |
|
||||
| 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification |
|
||||
|
||||
# 1. How to install
|
||||
---
|
||||
|
||||
### 1. Install online
|
||||
## 📖 Usage Guide
|
||||
|
||||
Check this project: https://github.com/acmesh-official/get.acme.sh
|
||||
### 1️⃣ How to Install
|
||||
|
||||
#### 📥 Install Online
|
||||
|
||||
> Check this project: https://github.com/acmesh-official/get.acme.sh
|
||||
|
||||
```bash
|
||||
curl https://get.acme.sh | sh -s email=my@example.com
|
||||
```
|
||||
|
||||
Or:
|
||||
**Or:**
|
||||
|
||||
```bash
|
||||
wget -O - https://get.acme.sh | sh -s email=my@example.com
|
||||
```
|
||||
|
||||
|
||||
### 2. Or, Install from git
|
||||
#### 📦 Install from Git
|
||||
|
||||
Clone this project and launch installation:
|
||||
|
||||
@@ -144,11 +173,11 @@ cd ./acme.sh
|
||||
./acme.sh --install -m my@example.com
|
||||
```
|
||||
|
||||
You `don't have to be root` then, although `it is recommended`.
|
||||
> 💡 You `don't have to be root` then, although `it is recommended`.
|
||||
|
||||
Advanced Installation: https://github.com/acmesh-official/acme.sh/wiki/How-to-install
|
||||
📚 **Advanced Installation:** https://github.com/acmesh-official/acme.sh/wiki/How-to-install
|
||||
|
||||
The installer will perform 3 actions:
|
||||
**The installer will perform 3 actions:**
|
||||
|
||||
1. Create and copy `acme.sh` to your home dir (`$HOME`): `~/.acme.sh/`.
|
||||
All certs will be placed in this folder too.
|
||||
@@ -161,17 +190,19 @@ Cron entry example:
|
||||
0 0 * * * "/home/user/.acme.sh"/acme.sh --cron --home "/home/user/.acme.sh" > /dev/null
|
||||
```
|
||||
|
||||
After the installation, you must close the current terminal and reopen it to make the alias take effect.
|
||||
> ⚠️ After the installation, you must close the current terminal and reopen it to make the alias take effect.
|
||||
|
||||
Ok, you are ready to issue certs now.
|
||||
✅ **You are ready to issue certs now!**
|
||||
|
||||
Show help message:
|
||||
**Show help message:**
|
||||
|
||||
```sh
|
||||
root@v1:~# acme.sh -h
|
||||
acme.sh -h
|
||||
```
|
||||
|
||||
# 2. Just issue a cert
|
||||
---
|
||||
|
||||
### 2️⃣ Issue a Certificate
|
||||
|
||||
**Example 1:** Single domain.
|
||||
|
||||
@@ -206,19 +237,21 @@ You must point and bind all the domains to the same webroot dir: `/home/wwwroot/
|
||||
|
||||
The certs will be placed in `~/.acme.sh/example.com/`
|
||||
|
||||
The certs will be renewed automatically every **60** days.
|
||||
> 🔄 The certs will be renewed automatically every **30** days.
|
||||
|
||||
The certs will default to ECC certificates.
|
||||
> 🔐 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 \
|
||||
@@ -227,7 +260,7 @@ acme.sh --install-cert -d example.com \
|
||||
--reloadcmd "service apache2 force-reload"
|
||||
```
|
||||
|
||||
**Nginx** example:
|
||||
#### ⚡ Nginx Example:
|
||||
```bash
|
||||
acme.sh --install-cert -d example.com \
|
||||
--key-file /path/to/keyfile/in/nginx/key.pem \
|
||||
@@ -241,91 +274,89 @@ The ownership and permission info of existing files are preserved. You can pre-c
|
||||
|
||||
Install/copy the cert/key to the production Apache or Nginx path.
|
||||
|
||||
The cert will be renewed every **60** days by default (which is configurable). Once the cert is renewed, the Apache/Nginx service will be reloaded automatically by the command: `service apache2 force-reload` or `service nginx force-reload`.
|
||||
> 🔄 The cert will be renewed every **30** days by default (configurable). Once renewed, the Apache/Nginx service will be reloaded automatically.
|
||||
|
||||
> ⚠️ **IMPORTANT:** The `reloadcmd` is very important. The cert can be automatically renewed, but without a correct `reloadcmd`, the cert may not be flushed to your server (like nginx or apache), then your website will not be able to show the renewed cert.
|
||||
|
||||
**Please take care: The reloadcmd is very important. The cert can be automatically renewed, but, without a correct 'reloadcmd' the cert may not be flushed to your server(like nginx or apache), then your website will not be able to show renewed cert in 60 days.**
|
||||
---
|
||||
|
||||
# 4. Use Standalone server to issue cert
|
||||
### 4️⃣ Use Standalone Server to Issue Certificate
|
||||
|
||||
**(requires you to be root/sudoer or have permission to listen on port 80 (TCP))**
|
||||
> 🔐 Requires root/sudoer or permission to listen on port **80** (TCP)
|
||||
|
||||
Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
|
||||
> ⚠️ Port `80` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
|
||||
|
||||
```bash
|
||||
acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com
|
||||
```
|
||||
|
||||
More examples: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
|
||||
📚 **More examples:** https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
|
||||
|
||||
# 5. Use Standalone ssl server to issue cert
|
||||
---
|
||||
|
||||
**(requires you to be root/sudoer or have permission to listen on port 443 (TCP))**
|
||||
### 5️⃣ Use Standalone TLS Server to Issue Certificate
|
||||
|
||||
Port `443` (TCP) **MUST** be free to listen on, otherwise you will be prompted to free it and try again.
|
||||
> 🔐 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.
|
||||
|
||||
```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)**
|
||||
### 7️⃣ Use Nginx Mode
|
||||
|
||||
> 🔐 Requires root/sudoer to interact with Nginx server
|
||||
|
||||
If you are running a web server, it is recommended to use the `Webroot 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.
|
||||
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.
|
||||
|
||||
Just set string "nginx" as the second argument.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -355,67 +386,74 @@ Then just rerun with `renew` argument:
|
||||
acme.sh --renew -d example.com
|
||||
```
|
||||
|
||||
Ok, it's done.
|
||||
✅ **Done!**
|
||||
|
||||
**Take care, this is dns manual mode, it can not be renewed automatically. you will have to add a new txt record to your domain by your hand when you renew your cert.**
|
||||
> ⚠️ **WARNING:** This is DNS manual mode — it **cannot** be renewed automatically. You will have to add a new TXT record to your domain manually when you renew your cert. **Please use DNS API mode instead.**
|
||||
|
||||
**Please use dns api mode instead.**
|
||||
---
|
||||
|
||||
# 10. Issue certificates of different key types and lengths (ECC or RSA)
|
||||
### 🔟 Issue Certificates of Different Key Types (ECC or RSA)
|
||||
|
||||
Just set the `keylength` to a valid, supported, value.
|
||||
Just set the `keylength` to a valid, supported value.
|
||||
|
||||
Valid values for the `keylength` parameter are:
|
||||
**Valid values for the `keylength` parameter:**
|
||||
|
||||
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)**
|
||||
| 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 |
|
||||
|
||||
For example:
|
||||
**Examples:**
|
||||
|
||||
### Single domain with ECDSA P-384 certificate
|
||||
#### Single domain with ECDSA P-384 certificate
|
||||
|
||||
```bash
|
||||
acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-384
|
||||
```
|
||||
|
||||
### SAN multi domain with RSA4096 certificate
|
||||
#### SAN multi domain with RSA4096 certificate
|
||||
|
||||
```bash
|
||||
acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com --keylength 4096
|
||||
```
|
||||
|
||||
# 11. Issue Wildcard certificates
|
||||
---
|
||||
|
||||
It's simple, just give a wildcard domain as the `-d` parameter.
|
||||
### 1️⃣1️⃣ Issue Wildcard Certificates
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
No, you don't need to renew the certs manually. All the certs will be renewed automatically every **60** days.
|
||||
### 1️⃣2️⃣ How to Renew Certificates
|
||||
|
||||
However, you can also force to renew a cert:
|
||||
> 🔄 No need to renew manually! All certs will be renewed automatically every **30** days.
|
||||
|
||||
However, you can force a renewal:
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --force
|
||||
```
|
||||
|
||||
or, for ECC cert:
|
||||
**For ECC cert:**
|
||||
|
||||
```sh
|
||||
acme.sh --renew -d example.com --force --ecc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 13. How to stop cert renewal
|
||||
### 1️⃣3️⃣ How to Stop Certificate Renewal
|
||||
|
||||
To stop renewal of a cert, you can execute the following to remove the cert from the renewal list:
|
||||
|
||||
@@ -425,73 +463,78 @@ acme.sh --remove -d example.com [--ecc]
|
||||
|
||||
The cert/key file is not removed from the disk.
|
||||
|
||||
You can remove the respective directory (e.g. `~/.acme.sh/example.com`) by yourself.
|
||||
> 💡 You can remove the respective directory (e.g. `~/.acme.sh/example.com`) manually.
|
||||
|
||||
---
|
||||
|
||||
# 14. How to upgrade `acme.sh`
|
||||
### 1️⃣4️⃣ How to Upgrade acme.sh
|
||||
|
||||
acme.sh is in constant development, so it's strongly recommended to use the latest code.
|
||||
> 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code.
|
||||
|
||||
You can update acme.sh to the latest code:
|
||||
**Update to latest:**
|
||||
|
||||
```sh
|
||||
acme.sh --upgrade
|
||||
```
|
||||
|
||||
You can also enable auto upgrade:
|
||||
**Enable auto upgrade:**
|
||||
|
||||
```sh
|
||||
acme.sh --upgrade --auto-upgrade
|
||||
```
|
||||
|
||||
Then **acme.sh** will be kept up to date automatically.
|
||||
|
||||
Disable auto upgrade:
|
||||
**Disable auto upgrade:**
|
||||
|
||||
```sh
|
||||
acme.sh --upgrade --auto-upgrade 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 15. Issue a cert from an existing CSR
|
||||
### 1️⃣5️⃣ Issue a Certificate from an Existing CSR
|
||||
|
||||
https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR
|
||||
|
||||
---
|
||||
|
||||
# 16. Send notifications in cronjob
|
||||
### 1️⃣6️⃣ Send Notifications in Cronjob
|
||||
|
||||
https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
📚 https://github.com/acmesh-official/acme.sh/wiki/notify
|
||||
|
||||
---
|
||||
|
||||
# 17. Under the Hood
|
||||
### 1️⃣7️⃣ Under the Hood
|
||||
|
||||
Speak ACME language using shell, directly to "Let's Encrypt".
|
||||
> 🔧 Speak ACME language using shell, directly to "Let's Encrypt".
|
||||
|
||||
TODO:
|
||||
---
|
||||
|
||||
### 1️⃣8️⃣ Acknowledgments
|
||||
|
||||
# 18. Acknowledgments
|
||||
| Project | Link |
|
||||
|---------|------|
|
||||
| 🙏 Acme-tiny | https://github.com/diafygi/acme-tiny |
|
||||
| 📜 ACME protocol | https://github.com/ietf-wg-acme/acme |
|
||||
|
||||
1. Acme-tiny: https://github.com/diafygi/acme-tiny
|
||||
2. 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,25 +549,31 @@ 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
|
||||
|
||||
# 19. License & Others
|
||||
📄 **License:** GPLv3
|
||||
|
||||
License is GPLv3
|
||||
⭐ Please **Star** and **Fork** this project!
|
||||
|
||||
Please Star and Fork me.
|
||||
🐛 [Issues](https://github.com/acmesh-official/acme.sh/issues) and 🔀 [Pull Requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
|
||||
|
||||
[Issues](https://github.com/acmesh-official/acme.sh/issues) and [pull requests](https://github.com/acmesh-official/acme.sh/pulls) are welcome.
|
||||
---
|
||||
|
||||
### 2️⃣0️⃣ Donate
|
||||
|
||||
# 20. Donate
|
||||
Your donation makes **acme.sh** better:
|
||||
> 💝 Your donation makes **acme.sh** better!
|
||||
|
||||
1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)
|
||||
| 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)
|
||||
📜 [Donate List](https://github.com/acmesh-official/acme.sh/wiki/Donate-list)
|
||||
|
||||
# 21. About this repository
|
||||
---
|
||||
|
||||
### 2️⃣1️⃣ About This Repository
|
||||
|
||||
> [!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!
|
||||
@@ -532,7 +581,7 @@ Your donation makes **acme.sh** better:
|
||||
>
|
||||
> 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.com">
|
||||
<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">
|
||||
|
||||
8
acme.sh
8
acme.sh
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
VER=3.1.2
|
||||
VER=3.1.3
|
||||
|
||||
PROJECT_NAME="acme.sh"
|
||||
|
||||
@@ -65,7 +65,7 @@ ID_TYPE_IP="ip"
|
||||
|
||||
LOCAL_ANY_ADDRESS="0.0.0.0"
|
||||
|
||||
DEFAULT_RENEW=60
|
||||
DEFAULT_RENEW=30
|
||||
|
||||
NO_VALUE="no"
|
||||
|
||||
@@ -1031,7 +1031,7 @@ _digest() {
|
||||
|
||||
outputhex="$2"
|
||||
|
||||
if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
|
||||
if [ "$alg" = "sha3-256" ] || [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then
|
||||
if [ "$outputhex" ]; then
|
||||
${ACME_OPENSSL_BIN:-openssl} dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' '
|
||||
else
|
||||
@@ -5840,7 +5840,7 @@ list() {
|
||||
if [ -z "$_domain" ]; then
|
||||
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}"/{*.*,*:*}/; do
|
||||
d=$(basename "$di")
|
||||
_debug d "$d"
|
||||
(
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
276
deploy/multideploy.sh
Normal file
276
deploy/multideploy.sh
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
139
dnsapi/dns_efficientip.sh
Executable file
139
dnsapi/dns_efficientip.sh
Executable file
@@ -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
Executable file → Normal file
226
dnsapi/dns_exoscale.sh
Executable file → Normal file
@@ -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
|
||||
|
||||
url="${EXOSCALE_API}${path}"
|
||||
expiration=$(_math "$(date +%s)" + 300) # 5m from now
|
||||
|
||||
# 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"
|
||||
|
||||
if [ "$token" ]; then
|
||||
export _H2="X-DNS-Domain-Token: $token"
|
||||
else
|
||||
export _H2="X-DNS-Token: $EXOSCALE_API_KEY:$EXOSCALE_SECRET_KEY"
|
||||
fi
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
501
dnsapi/dns_hostup.sh
Normal file
501
dnsapi/dns_hostup.sh
Normal file
@@ -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
Normal file
244
dnsapi/dns_infoblox_uddi.sh
Normal file
@@ -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
|
||||
}
|
||||
@@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_inwx
|
||||
Options:
|
||||
INWX_User Username
|
||||
INWX_Password Password
|
||||
INWX_Shared_Secret 2 Factor Authentication Shared Secret (optional requires oathtool)
|
||||
'
|
||||
|
||||
# Dependencies:
|
||||
@@ -110,11 +111,17 @@ dns_inwx_rm() {
|
||||
<string>%s</string>
|
||||
</value>
|
||||
</member>
|
||||
<member>
|
||||
<name>content</name>
|
||||
<value>
|
||||
<string>%s</string>
|
||||
</value>
|
||||
</member>
|
||||
</struct>
|
||||
</value>
|
||||
</param>
|
||||
</params>
|
||||
</methodCall>' "$_domain" "$_sub_domain")
|
||||
</methodCall>' "$_domain" "$_sub_domain" "$txtvalue")
|
||||
response="$(_post "$xml_content" "$INWX_Api" "" "POST")"
|
||||
|
||||
if ! _contains "$response" "Command completed successfully"; then
|
||||
@@ -125,7 +132,7 @@ dns_inwx_rm() {
|
||||
if ! printf "%s" "$response" | grep "count" >/dev/null; then
|
||||
_info "Do not need to delete record"
|
||||
else
|
||||
_record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><int>[0-9]+' | _egrep_o '[0-9]+')
|
||||
_record_id=$(printf '%s' "$response" | _egrep_o '.*(<member><name>record){1}(.*)([0-9]+){1}' | _egrep_o '<name>id<\/name><value><string>[0-9]+' | _egrep_o '[0-9]+')
|
||||
_info "Deleting record"
|
||||
_inwx_delete_record "$_record_id"
|
||||
fi
|
||||
@@ -324,7 +331,7 @@ _inwx_delete_record() {
|
||||
<member>
|
||||
<name>id</name>
|
||||
<value>
|
||||
<int>%s</int>
|
||||
<string>%s</string>
|
||||
</value>
|
||||
</member>
|
||||
</struct>
|
||||
@@ -362,7 +369,7 @@ _inwx_update_record() {
|
||||
<member>
|
||||
<name>id</name>
|
||||
<value>
|
||||
<int>%s</int>
|
||||
<string>%s</string>
|
||||
</value>
|
||||
</member>
|
||||
</struct>
|
||||
|
||||
109
dnsapi/dns_mgwm.sh
Normal file
109
dnsapi/dns_mgwm.sh
Normal file
@@ -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
|
||||
}
|
||||
@@ -4,8 +4,8 @@ dns_omglol_info='omg.lol
|
||||
Site: omg.lol
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_omglol
|
||||
Options:
|
||||
OMG_ApiKey API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
|
||||
OMG_Address Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
|
||||
OMG_ApiKey - API Key. This is accessible from the bottom of the account page at https://home.omg.lol/account
|
||||
OMG_Address - Address. This is your omg.lol address, without the preceding @ - you can see your list on your dashboard at https://home.omg.lol/dashboard
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/5299
|
||||
Author: @Kholin <kholin+acme.omglolapi@omg.lol>
|
||||
'
|
||||
@@ -35,7 +35,7 @@ dns_omglol_add() {
|
||||
_debug "omg.lol Address" "$OMG_Address"
|
||||
|
||||
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
|
||||
if [ ! $? ]; then
|
||||
if [ 1 = $? ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -67,7 +67,7 @@ dns_omglol_rm() {
|
||||
_debug "omg.lol Address" "$OMG_Address"
|
||||
|
||||
omg_validate "$OMG_ApiKey" "$OMG_Address" "$fulldomain"
|
||||
if [ ! $? ]; then
|
||||
if [ 1 = $? ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -100,18 +100,49 @@ omg_validate() {
|
||||
fi
|
||||
|
||||
_endswith "$fulldomain" "omg.lol"
|
||||
if [ ! $? ]; then
|
||||
if [ 1 = $? ]; then
|
||||
_err "Domain name requested is not under omg.lol"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_endswith "$fulldomain" "$omg_address.omg.lol"
|
||||
if [ ! $? ]; then
|
||||
if [ 1 = $? ]; then
|
||||
_err "Domain name is not a subdomain of provided omg.lol address $omg_address"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Required environment parameters are all present"
|
||||
omg_testconnect "$omg_apikey" "$omg_address"
|
||||
if [ 1 = $? ]; then
|
||||
_err "Authentication to omg.lol for address $omg_address using provided API key failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "Required environment parameters are all present and validated"
|
||||
}
|
||||
|
||||
# Validate that the address and API key are both correct and associated to each other
|
||||
omg_testconnect() {
|
||||
omg_apikey=$1
|
||||
omg_address=$2
|
||||
|
||||
_debug2 "Function" "omg_testconnect"
|
||||
_secure_debug2 "omg.lol API key" "$omg_apikey"
|
||||
_debug2 "omg.lol Address" "$omg_address"
|
||||
|
||||
authheader="$(_createAuthHeader "$omg_apikey")"
|
||||
export _H1="$authheader"
|
||||
endpoint="https://api.omg.lol/address/$omg_address/info"
|
||||
_debug2 "Endpoint for validation" "$endpoint"
|
||||
|
||||
response=$(_get "$endpoint" "" 30)
|
||||
|
||||
_jsonResponseCheck "$response" "status_code" 200
|
||||
if [ 1 = $? ]; then
|
||||
_debug2 "Failed to query omg.lol for $omg_address with provided API key"
|
||||
_secure_debug2 "API Key" "omg_apikey"
|
||||
_secure_debug3 "Raw response" "$response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Add (or modify) an entry for a new ACME query
|
||||
|
||||
309
dnsapi/dns_sotoon.sh
Normal file
309
dnsapi/dns_sotoon.sh
Normal file
@@ -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]" "$@"
|
||||
}
|
||||
Reference in New Issue
Block a user