@@ -973,3 +973,116 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
HardenedBSD:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: OpenEuler
|
||||||
|
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 }}
|
||||||
|
TokenName1: ${{ secrets.TokenName1}}
|
||||||
|
TokenName2: ${{ secrets.TokenName2}}
|
||||||
|
TokenName3: ${{ secrets.TokenName3}}
|
||||||
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Clone acmetest
|
||||||
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
|
- uses: vmactions/hardenedbsd-vm@v1
|
||||||
|
with:
|
||||||
|
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||||
|
cache-after-prepare: true
|
||||||
|
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
|
||||||
|
sync: nfs
|
||||||
|
run: |
|
||||||
|
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName2}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName3}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName4}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName5}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
|
||||||
|
fi
|
||||||
|
cd ../acmetest
|
||||||
|
./letest.sh
|
||||||
|
- name: DebugOnError
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: |
|
||||||
|
echo "See how to debug in VM:"
|
||||||
|
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
OPNsense:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: HardenedBSD
|
||||||
|
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 }}
|
||||||
|
TokenName1: ${{ secrets.TokenName1}}
|
||||||
|
TokenName2: ${{ secrets.TokenName2}}
|
||||||
|
TokenName3: ${{ secrets.TokenName3}}
|
||||||
|
TokenName4: ${{ secrets.TokenName4}}
|
||||||
|
TokenName5: ${{ secrets.TokenName5}}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- name: Clone acmetest
|
||||||
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
|
- uses: vmactions/opnsense-vm@v1
|
||||||
|
with:
|
||||||
|
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||||
|
cache-after-prepare: true
|
||||||
|
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}}'
|
||||||
|
#The dns-01 cases need no inbound port, so the appliance's web GUI can
|
||||||
|
#keep the 80 port here, unlike the standalone workflow.
|
||||||
|
prepare: pkg install -y socat curl
|
||||||
|
usesh: true
|
||||||
|
sync: nfs
|
||||||
|
run: |
|
||||||
|
if [ "${{ secrets.TokenName1}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName2}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName3}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName4}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
|
||||||
|
fi
|
||||||
|
if [ "${{ secrets.TokenName5}}" ] ; then
|
||||||
|
export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
|
||||||
|
fi
|
||||||
|
cd ../acmetest
|
||||||
|
./letest.sh
|
||||||
|
- name: DebugOnError
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: |
|
||||||
|
echo "See how to debug in VM:"
|
||||||
|
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: HardenedBSD
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/HardenedBSD.yml'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/HardenedBSD.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
HardenedBSD:
|
||||||
|
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
|
||||||
|
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@v7
|
||||||
|
- uses: anyvm-org/cf-tunnel@v0
|
||||||
|
id: tunnel
|
||||||
|
with:
|
||||||
|
protocol: http
|
||||||
|
port: 8080
|
||||||
|
- name: Set envs
|
||||||
|
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
|
||||||
|
- name: Clone acmetest
|
||||||
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
|
- uses: vmactions/hardenedbsd-vm@v1
|
||||||
|
with:
|
||||||
|
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||||
|
cache-after-prepare: true
|
||||||
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
|
nat: |
|
||||||
|
"8080": "80"
|
||||||
|
prepare: pkg install -y socat curl wget
|
||||||
|
usesh: true
|
||||||
|
sync: nfs
|
||||||
|
run: |
|
||||||
|
cd ../acmetest \
|
||||||
|
&& ./letest.sh
|
||||||
|
- name: DebugOnError
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: |
|
||||||
|
echo "See how to debug in VM:"
|
||||||
|
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
name: OPNsense
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '*'
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/OPNsense.yml'
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
paths:
|
||||||
|
- '*.sh'
|
||||||
|
- '.github/workflows/OPNsense.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
OPNsense:
|
||||||
|
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
|
||||||
|
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@v7
|
||||||
|
- uses: anyvm-org/cf-tunnel@v0
|
||||||
|
id: tunnel
|
||||||
|
with:
|
||||||
|
protocol: http
|
||||||
|
port: 8080
|
||||||
|
- name: Set envs
|
||||||
|
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
|
||||||
|
- name: Clone acmetest
|
||||||
|
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
|
||||||
|
- uses: vmactions/opnsense-vm@v1
|
||||||
|
with:
|
||||||
|
debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
|
||||||
|
cache-after-prepare: true
|
||||||
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
|
||||||
|
nat: |
|
||||||
|
"8080": "80"
|
||||||
|
prepare: pkg install -y socat curl wget
|
||||||
|
usesh: true
|
||||||
|
sync: nfs
|
||||||
|
run: |
|
||||||
|
#OPNsense is a firewall appliance whose web GUI holds the 80 port,
|
||||||
|
#where every --standalone case listens. configd has no "stop"
|
||||||
|
#action for it and the rc script cannot stop it either, so kill it.
|
||||||
|
#This belongs here and not in prepare: prepare runs before the
|
||||||
|
#cache-after-prepare reboot, which would bring the GUI back. And do
|
||||||
|
#NOT free the port by disabling the GUI's http redirect in
|
||||||
|
#config.xml: pf's automatic pass rule for the 80 port is generated
|
||||||
|
#from the web GUI settings, so dropping the redirect also drops the
|
||||||
|
#rule on the next boot, and the inbound challenge is filtered.
|
||||||
|
pkill lighttpd || true
|
||||||
|
cd ../acmetest \
|
||||||
|
&& ./letest.sh
|
||||||
|
- name: DebugOnError
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: |
|
||||||
|
echo "See how to debug in VM:"
|
||||||
|
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
|
||||||
@@ -56,7 +56,15 @@ jobs:
|
|||||||
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
|
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN'
|
||||||
nat: |
|
nat: |
|
||||||
"8080": "80"
|
"8080": "80"
|
||||||
prepare: dnf install -y curl socat cronie tar gzip
|
prepare: |
|
||||||
|
# openEuler ships every repo with both a baseurl and a metalink.
|
||||||
|
# The metalink mirror pool is split-brain (dl-cdn.openeuler.openatom.cn
|
||||||
|
# froze at the 2026-08-20 snapshot while repo.openeuler.org moved on),
|
||||||
|
# so dnf takes repomd.xml from the stale mirror and then 404s fetching
|
||||||
|
# the checksummed metadata it names from the fresh ones. Keep only the
|
||||||
|
# vendor baseurl, which is self-consistent.
|
||||||
|
sed -i '/^metalink=/d' /etc/yum.repos.d/*.repo
|
||||||
|
dnf install -y curl socat cronie tar gzip
|
||||||
usesh: true
|
usesh: true
|
||||||
sync: rsync
|
sync: rsync
|
||||||
copyback: false
|
copyback: false
|
||||||
|
|||||||
@@ -23,10 +23,22 @@ jobs:
|
|||||||
- name: Create the v-prefixed tag
|
- name: Create the v-prefixed tag
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
if gh api "repos/${{ github.repository }}/git/ref/tags/v${{ github.ref_name }}" >/dev/null 2>&1; then
|
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
|
||||||
echo "Tag v${{ github.ref_name }} already exists, nothing to do."
|
echo "Tag v$TAG already exists, nothing to do."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
gh api "repos/${{ github.repository }}/git/refs" -f ref="refs/tags/v${{ github.ref_name }}" -f sha="${{ github.sha }}"
|
# Mirror the object the pushed tag actually points at: the commit
|
||||||
echo "Created tag v${{ github.ref_name }} -> ${{ github.sha }}"
|
# for a lightweight tag, the tag object itself for an annotated or
|
||||||
|
# signed one. Pointing the mirror at the commit would strip the
|
||||||
|
# signature, so "git verify-tag v3.1.3" would fail while
|
||||||
|
# "git verify-tag 3.1.3" succeeds.
|
||||||
|
sha="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.sha)"
|
||||||
|
if [ -z "$sha" ] || [ "$sha" = "null" ]; then
|
||||||
|
echo "Could not resolve refs/tags/$TAG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
|
||||||
|
echo "Created tag v$TAG -> $sha"
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml/badge.svg" alt="Haiku"></a>
|
||||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg" alt="Hurd"></a>
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml/badge.svg" alt="Hurd"></a>
|
||||||
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg" alt="OpenEuler"></a>
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml/badge.svg" alt="OpenEuler"></a>
|
||||||
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml/badge.svg" alt="HardenedBSD"></a>
|
||||||
|
<a href="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml"><img src="https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml/badge.svg" alt="OPNsense"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -134,6 +136,8 @@
|
|||||||
|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|
|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
|
||||||
|28|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|
|28|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Hurd.yml)|GNU Hurd
|
||||||
|29|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
|
|29|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OpenEuler.yml)|openEuler
|
||||||
|
|30|[](https://github.com/acmesh-official/acme.sh/actions/workflows/HardenedBSD.yml)|HardenedBSD
|
||||||
|
|31|[](https://github.com/acmesh-official/acme.sh/actions/workflows/OPNsense.yml)|OPNsense
|
||||||
|
|
||||||
|
|
||||||
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
|
||||||
@@ -227,6 +231,31 @@ Cron entry example:
|
|||||||
acme.sh -h
|
acme.sh -h
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 🔏 Verify a Release
|
||||||
|
|
||||||
|
Release tags from `3.1.5` on are signed with the maintainer's SSH key. The
|
||||||
|
signing happens on the maintainer's machine, so the private key is never
|
||||||
|
available to CI. The public half is [`allowed_signers`](allowed_signers) in
|
||||||
|
this repository. From a clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config gpg.ssh.allowedSignersFile allowed_signers
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git verify-tag 3.1.5
|
||||||
|
```
|
||||||
|
|
||||||
|
The signature covers the tag object, which pins the commit and therefore the
|
||||||
|
whole tree, so a good signature verifies every file at that release and no
|
||||||
|
separate tarball checksum is needed. Build a tarball from the verified tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2️⃣ Issue a Certificate
|
### 2️⃣ Issue a Certificate
|
||||||
|
|||||||
@@ -1482,39 +1482,57 @@ _readKeyLengthFromCSR() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#port
|
||||||
|
#Reads a netstat or ss listing on stdin, prints the lines that show a socket
|
||||||
|
#listening on port.
|
||||||
|
#Linux and windows print the local address as "addr:port", aix, macos, the
|
||||||
|
#bsds and solaris print it as "addr.port", so both separators must match.
|
||||||
|
#The state is "LISTEN" nearly everywhere, "LISTENING" on windows and lower
|
||||||
|
#case "listen" on haiku, hence the substring match and the -i.
|
||||||
|
_filter_listen_port() {
|
||||||
|
_flp_port="$1"
|
||||||
|
if [ -z "$_flp_port" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
grep -i "LISTEN" | grep "[:.]$_flp_port "
|
||||||
|
}
|
||||||
|
|
||||||
|
#port
|
||||||
_ss() {
|
_ss() {
|
||||||
_port="$1"
|
_port="$1"
|
||||||
|
|
||||||
if _exists "ss"; then
|
if _exists "ss"; then
|
||||||
_debug "Using: ss"
|
_debug "Using: ss"
|
||||||
ss -ntpl 2>/dev/null | grep ":$_port "
|
ss -ntpl 2>/dev/null | _filter_listen_port "$_port"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$(uname)" = "AIX" ]; then
|
#aix, macos and the bsds have no "-p protocol" socket listing that works on
|
||||||
_debug "Using: AIX netstat"
|
#all of them: on netbsd "-p" is "Show statistics about protocol" instead
|
||||||
netstat -an | grep "^tcp" | grep "LISTEN" | grep "\.$_port "
|
#(netstat(1), NetBSD 10.1). Their default display does show "the state of
|
||||||
|
#all sockets" with -a, so use that and keep only the tcp lines.
|
||||||
|
case "$(uname)" in
|
||||||
|
AIX | Darwin | DragonFly | *BSD*)
|
||||||
|
_debug "Using: AIX/BSD netstat"
|
||||||
|
netstat -an | grep "^tcp" | _filter_listen_port "$_port"
|
||||||
return 0
|
return 0
|
||||||
fi
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if _exists "netstat"; then
|
if _exists "netstat"; then
|
||||||
_debug "Using: netstat"
|
_debug "Using: netstat"
|
||||||
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
|
if netstat -help 2>&1 | grep "\-p proto" >/dev/null; then
|
||||||
#for windows version netstat tool
|
#for windows version netstat tool
|
||||||
netstat -an -p tcp | grep "LISTENING" | grep ":$_port "
|
netstat -an -p tcp | _filter_listen_port "$_port"
|
||||||
|
elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then
|
||||||
|
#for solaris
|
||||||
|
netstat -an -P tcp | _filter_listen_port "$_port"
|
||||||
|
elif netstat -help 2>&1 | grep "\-p" >/dev/null; then
|
||||||
|
#for full linux
|
||||||
|
netstat -ntpl | _filter_listen_port "$_port"
|
||||||
else
|
else
|
||||||
if netstat -help 2>&1 | grep "\-p protocol" >/dev/null; then
|
#for busybox (embedded linux; no pid support)
|
||||||
netstat -an -p tcp | grep LISTEN | grep ":$_port "
|
netstat -ntl 2>/dev/null | _filter_listen_port "$_port"
|
||||||
elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then
|
|
||||||
#for solaris
|
|
||||||
netstat -an -P tcp | grep "\.$_port " | grep "LISTEN"
|
|
||||||
elif netstat -help 2>&1 | grep "\-p" >/dev/null; then
|
|
||||||
#for full linux
|
|
||||||
netstat -ntpl | grep ":$_port "
|
|
||||||
else
|
|
||||||
#for busybox (embedded linux; no pid support)
|
|
||||||
netstat -ntl 2>/dev/null | grep ":$_port "
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -2343,6 +2361,39 @@ _tail_c() {
|
|||||||
tail -c "$1" 2>/dev/null || tail -"$1"c
|
tail -c "$1" 2>/dev/null || tail -"$1"c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#code
|
||||||
|
#Is this status the CA's front end failing rather than its ACME
|
||||||
|
#implementation answering? 502 and 504 mean the proxy could not reach the
|
||||||
|
#backend or gave up waiting for it, 503 that it is overloaded. The body of
|
||||||
|
#those is the proxy's html, not problem+json, so no ACME status can be read
|
||||||
|
#out of it and a caller looking for one abandons an order that is fine.
|
||||||
|
#Anything else, a 500 from the ACME implementation included, is a real
|
||||||
|
#answer and must be passed through to the caller.
|
||||||
|
_is_gateway_error() {
|
||||||
|
case "$1" in
|
||||||
|
502 | 503 | 504) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#attempt
|
||||||
|
#Seconds to wait before retry number <attempt>, for the cases where the CA
|
||||||
|
#gave us no Retry-After to go by. A flat two seconds let the whole twenty
|
||||||
|
#attempt budget burn out in forty eight seconds, which is shorter than the
|
||||||
|
#gateway outages a CA really has: ZeroSSL answered 502 and 504 for over a
|
||||||
|
#minute at a time through August 2026, so every renewal that started during
|
||||||
|
#one of those died instead of waiting it out. Backing off spends the same
|
||||||
|
#twenty attempts over about six minutes, which is still far below the ten
|
||||||
|
#minutes at which a Retry-After is read as the CA refusing outright.
|
||||||
|
_retry_backoff_sec() {
|
||||||
|
case "$1" in
|
||||||
|
1) echo 2 ;;
|
||||||
|
2) echo 5 ;;
|
||||||
|
3) echo 10 ;;
|
||||||
|
*) echo 20 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# url payload needbase64 keyfile
|
# url payload needbase64 keyfile
|
||||||
_send_signed_request() {
|
_send_signed_request() {
|
||||||
url=$1
|
url=$1
|
||||||
@@ -2406,8 +2457,9 @@ _send_signed_request() {
|
|||||||
nonce="$_CACHED_NONCE"
|
nonce="$_CACHED_NONCE"
|
||||||
_debug2 nonce "$nonce"
|
_debug2 nonce "$nonce"
|
||||||
if [ -z "$nonce" ]; then
|
if [ -z "$nonce" ]; then
|
||||||
_info "Could not get nonce, let's try again."
|
_sleep_nonce_sec="$(_retry_backoff_sec "$_request_retry_times")"
|
||||||
_sleep 2
|
_info "Could not get nonce, let's try again. Sleeping for $_sleep_nonce_sec seconds."
|
||||||
|
_sleep "$_sleep_nonce_sec"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -2466,13 +2518,13 @@ _send_signed_request() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
_retryafter=$(echo "$responseHeaders" | grep -i "^Retry-After *: *[0-9]\+ *" | cut -d : -f 2 | tr -d ' ' | tr -d '\r')
|
||||||
if [ "$code" = '503' ]; then
|
if _is_gateway_error "$code"; then
|
||||||
_sleep_overload_retry_sec=$_retryafter
|
_sleep_overload_retry_sec=$_retryafter
|
||||||
if [ -z "$_sleep_overload_retry_sec" ]; then
|
if [ -z "$_sleep_overload_retry_sec" ]; then
|
||||||
_sleep_overload_retry_sec=5
|
_sleep_overload_retry_sec="$(_retry_backoff_sec "$_request_retry_times")"
|
||||||
fi
|
fi
|
||||||
if [ $_sleep_overload_retry_sec -le 600 ]; then
|
if [ $_sleep_overload_retry_sec -le 600 ]; then
|
||||||
_info "It seems the CA server is currently overloaded, let's wait and retry. Sleeping for $_sleep_overload_retry_sec seconds."
|
_info "The CA server answered $code, let's wait and retry. Sleeping for $_sleep_overload_retry_sec seconds."
|
||||||
_sleep $_sleep_overload_retry_sec
|
_sleep $_sleep_overload_retry_sec
|
||||||
continue
|
continue
|
||||||
else
|
else
|
||||||
@@ -2499,6 +2551,15 @@ _send_signed_request() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Reads a value from stdin, prints it escaped for use as the replacement text
|
||||||
|
#of a sed s command delimited by '|'. The backslash must go first: a bare one
|
||||||
|
#starts an escape sequence and backslash-digit is a backreference, both make
|
||||||
|
#sed error out. Then '&' (the whole-match reference) and the '|' delimiter.
|
||||||
|
#https://github.com/acmesh-official/acme.sh/issues/7213
|
||||||
|
_sed_escape_rhs() {
|
||||||
|
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g'
|
||||||
|
}
|
||||||
|
|
||||||
#setopt "file" "opt" "=" "value" [";"]
|
#setopt "file" "opt" "=" "value" [";"]
|
||||||
_setopt() {
|
_setopt() {
|
||||||
__conf="$1"
|
__conf="$1"
|
||||||
@@ -2514,34 +2575,50 @@ _setopt() {
|
|||||||
touch "$__conf"
|
touch "$__conf"
|
||||||
chmod 600 "$__conf"
|
chmod 600 "$__conf"
|
||||||
fi
|
fi
|
||||||
|
__nl="
|
||||||
|
"
|
||||||
|
case "$__val" in
|
||||||
|
*"$__nl"*)
|
||||||
|
#the conf format is line based and the file is sourced by the shell, so a
|
||||||
|
#value holding a line break cannot be represented in it (it would also
|
||||||
|
#make the replace sed below fail with an unterminated 's' command)
|
||||||
|
_err "The value of '$__opt' contains a line break, it cannot be saved to $__conf."
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
|
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
|
||||||
echo >>"$__conf"
|
echo >>"$__conf"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
|
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
|
||||||
_debug3 OK
|
_debug3 OK
|
||||||
if _contains "$__val" "&"; then
|
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
|
||||||
__val="$(echo "$__val" | sed 's/&/\\&/g')"
|
|
||||||
fi
|
|
||||||
if _contains "$__val" "|"; then
|
|
||||||
__val="$(echo "$__val" | sed 's/|/\\|/g')"
|
|
||||||
fi
|
|
||||||
text="$(cat "$__conf")"
|
text="$(cat "$__conf")"
|
||||||
printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf"
|
#capture first, write only on success: redirecting sed straight into the
|
||||||
|
#conf file truncates it before sed runs, so a failing sed (e.g. on an
|
||||||
|
#unescaped special character in the value) wiped the whole conf (#2426)
|
||||||
|
if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
|
||||||
|
printf -- "%s\n" "$__text" >"$__conf"
|
||||||
|
else
|
||||||
|
_err "Cannot save '$__opt' to $__conf."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
|
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
|
||||||
if _contains "$__val" "&"; then
|
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
|
||||||
__val="$(echo "$__val" | sed 's/&/\\&/g')"
|
|
||||||
fi
|
|
||||||
if _contains "$__val" "|"; then
|
|
||||||
__val="$(echo "$__val" | sed 's/|/\\|/g')"
|
|
||||||
fi
|
|
||||||
text="$(cat "$__conf")"
|
text="$(cat "$__conf")"
|
||||||
printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf"
|
if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
|
||||||
|
printf -- "%s\n" "$__text" >"$__conf"
|
||||||
|
else
|
||||||
|
_err "Cannot save '$__opt' to $__conf."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
_debug3 APP
|
_debug3 APP
|
||||||
echo "$__opt$__sep$__val$__end" >>"$__conf"
|
#printf, not echo: dash's builtin echo interprets backslash escapes in
|
||||||
|
#the value and would corrupt it
|
||||||
|
printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf"
|
||||||
fi
|
fi
|
||||||
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
|
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
|
||||||
}
|
}
|
||||||
@@ -2569,7 +2646,9 @@ _clear_conf() {
|
|||||||
_sdkey="$2"
|
_sdkey="$2"
|
||||||
if [ "$_c_c_f" ]; then
|
if [ "$_c_c_f" ]; then
|
||||||
_conf_data="$(cat "$_c_c_f")"
|
_conf_data="$(cat "$_c_c_f")"
|
||||||
echo "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
|
#printf, not echo: dash's builtin echo interprets backslash escapes and
|
||||||
|
#would corrupt saved values that contain them on every rewrite
|
||||||
|
printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d" >"$_c_c_f"
|
||||||
else
|
else
|
||||||
_err "Config file is empty, cannot clear"
|
_err "Config file is empty, cannot clear"
|
||||||
fi
|
fi
|
||||||
@@ -3911,6 +3990,11 @@ _on_before_issue() {
|
|||||||
if [ -z "$netprc" ]; then
|
if [ -z "$netprc" ]; then
|
||||||
netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS:$_checkport")"
|
netprc="$(echo "$_netprc" | grep "$LOCAL_ANY_ADDRESS:$_checkport")"
|
||||||
fi
|
fi
|
||||||
|
if [ -z "$netprc" ]; then
|
||||||
|
#aix, macos, the bsds and solaris print the wildcard local address as
|
||||||
|
#"*.port", not "0.0.0.0:port", and it blocks $_checkaddr just the same
|
||||||
|
netprc="$(echo "$_netprc" | grep " [*][:.]$_checkport ")"
|
||||||
|
fi
|
||||||
if [ "$netprc" ]; then
|
if [ "$netprc" ]; then
|
||||||
_err "$netprc"
|
_err "$netprc"
|
||||||
_err "tcp port $_checkport is already used by $(echo "$netprc" | cut -d : -f 4)"
|
_err "tcp port $_checkport is already used by $(echo "$netprc" | cut -d : -f 4)"
|
||||||
@@ -7072,6 +7156,30 @@ _uninstall_win_taskscheduler() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#binpath
|
||||||
|
#Reads a crontab listing from stdin, prints it without the acme.sh cron
|
||||||
|
#entries that call binpath.
|
||||||
|
_filter_cron_bin() {
|
||||||
|
_fcb_bin="$1"
|
||||||
|
if [ -z "$_fcb_bin" ]; then
|
||||||
|
cat
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
#a case pattern with a quoted variable matches binpath literally, which
|
||||||
|
#grep cannot do portably: Solaris /usr/bin/grep has no -F, and as a regex
|
||||||
|
#the dot of ~/.acme.sh would stand for any character
|
||||||
|
while IFS= read -r _fcb_line || [ -n "$_fcb_line" ]; do
|
||||||
|
case "$_fcb_line" in
|
||||||
|
*"$_fcb_bin --cron"*)
|
||||||
|
_debug3 "Dropping cron entry" "$_fcb_line"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$_fcb_line"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
#confighome
|
#confighome
|
||||||
installcronjob() {
|
installcronjob() {
|
||||||
_c_home="$1"
|
_c_home="$1"
|
||||||
@@ -7141,7 +7249,26 @@ installcronjob() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if ! echo "$_cron_entries" | grep "$PROJECT_ENTRY --cron"; then
|
#An entry that calls LE_WORKING_DIR/PROJECT_ENTRY is dead once that copy is
|
||||||
|
#gone: ACME_PACKAGED installs never write it, and the package manager
|
||||||
|
#removes it when it takes over. The entry below would then keep the install
|
||||||
|
#from adding a working one and cron would fail silently every day, so drop
|
||||||
|
#the stale entries first.
|
||||||
|
_cron_stale=""
|
||||||
|
if [ ! -f "$LE_WORKING_DIR/$PROJECT_ENTRY" ] && [ "$_cron_entries" ]; then
|
||||||
|
_cron_kept="$(echo "$_cron_entries" | _filter_cron_bin "\"$LE_WORKING_DIR\"/$PROJECT_ENTRY")"
|
||||||
|
if [ "$_cron_kept" != "$_cron_entries" ]; then
|
||||||
|
_info "Removing the cron job that calls the missing $LE_WORKING_DIR/$PROJECT_ENTRY"
|
||||||
|
_cron_entries="$_cron_kept"
|
||||||
|
_cron_stale=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
#>/dev/null: grep would print the matching crontab line to the console
|
||||||
|
_cron_add=""
|
||||||
|
if ! echo "$_cron_entries" | grep "$PROJECT_ENTRY --cron" >/dev/null; then
|
||||||
|
_cron_add=1
|
||||||
|
fi
|
||||||
|
if [ "$_cron_add" ] || [ "$_cron_stale" ]; then
|
||||||
if _exists uname && uname -a | grep SunOS >/dev/null; then
|
if _exists uname && uname -a | grep SunOS >/dev/null; then
|
||||||
_CRONTAB_STDIN="$_CRONTAB --"
|
_CRONTAB_STDIN="$_CRONTAB --"
|
||||||
else
|
else
|
||||||
@@ -7151,7 +7278,9 @@ installcronjob() {
|
|||||||
if [ "$_cron_entries" ]; then
|
if [ "$_cron_entries" ]; then
|
||||||
echo "$_cron_entries"
|
echo "$_cron_entries"
|
||||||
fi
|
fi
|
||||||
echo "$_cron_entry"
|
if [ "$_cron_add" ]; then
|
||||||
|
echo "$_cron_entry"
|
||||||
|
fi
|
||||||
} | $_CRONTAB_STDIN
|
} | $_CRONTAB_STDIN
|
||||||
fi
|
fi
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# acme.sh release signing key.
|
||||||
|
#
|
||||||
|
# Release tags are signed with this key. Its private half is held by the
|
||||||
|
# maintainer and is never available to CI, so a compromise of the build
|
||||||
|
# pipeline cannot produce a tag that verifies against this file.
|
||||||
|
#
|
||||||
|
# Fingerprint: SHA256:M60qVafm/NUywQHXAkoQcj2v6KgkfrdSXv6mPejUUeE
|
||||||
|
#
|
||||||
|
# To verify a release tag, from a clone of this repository:
|
||||||
|
#
|
||||||
|
# git config gpg.ssh.allowedSignersFile allowed_signers
|
||||||
|
# git verify-tag 3.1.5
|
||||||
|
#
|
||||||
|
# A good signature covers the tag object, which pins the commit, which pins
|
||||||
|
# the whole tree -- so verifying the tag verifies every file at that
|
||||||
|
# release. Build a tarball from the verified tag with:
|
||||||
|
#
|
||||||
|
# git archive --format=tar.gz --prefix=acme.sh-3.1.5/ 3.1.5 > acme.sh-3.1.5.tar.gz
|
||||||
|
#
|
||||||
|
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB
|
||||||
+29
-21
@@ -1,24 +1,29 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
# Deploy hook for UniFi OS Server (self-hosted).
|
# Deploy hook for UniFi OS, via the certificate REST API.
|
||||||
#
|
#
|
||||||
# Supports:
|
# Works against any UniFi OS whose management UI exposes
|
||||||
# - UniFi OS Server on macOS
|
# /api/userCertificates. Confirmed on:
|
||||||
# - UniFi OS Server on Linux
|
# - UniFi OS Server (the separately-installed, self-hosted application)
|
||||||
# - UniFi OS Server on Windows should also work (runs under WSL2), but
|
# on macOS and on Linux. Windows should also work (it runs under
|
||||||
# has not been tested.
|
# WSL2), but has not been tested.
|
||||||
|
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
|
||||||
|
# - UniFi OS hardware: UDM Pro on UniFi OS 5.1.26, UCG Fiber on
|
||||||
|
# UniFi OS 5.0.16 (user reports, see issues 7184 and 6916).
|
||||||
|
# No lower version bound is claimed -- if the UI has a certificate
|
||||||
|
# manager, this hook should work.
|
||||||
#
|
#
|
||||||
# Tested on: Ubuntu 26.04 (remote) and macOS 26.6 (local).
|
# `unifios` vs `unifi`: the split is the access method, not the product
|
||||||
|
# line. `unifi` writes files / a Java keystore and needs local or SSH
|
||||||
|
# access on the device; this hook drives the same REST API the web UI
|
||||||
|
# uses and works remotely. Use `unifi` where acme.sh runs on the device
|
||||||
|
# itself, this hook where it does not.
|
||||||
#
|
#
|
||||||
# This is a different product from the Cloud Key / UDM hardware and
|
# The API is served on the management port, which differs per install:
|
||||||
# self-hosted Unifi Controller covered by the `unifi` deploy hook above
|
# UniFi OS Server listens on 11443 (hence the default below), while
|
||||||
# (that hook already covers Cloud Key running UnifiOS v2.0.0+/Gen2/2+) --
|
# UniFi OS hardware serves it on 443 -- set DEPLOY_UNIFIOS_HOST to
|
||||||
# this hook targets the separately-installed, self-hosted "UniFi OS Server"
|
# "https://<host>" there.
|
||||||
# application instead, which stores certificates in its own Postgres
|
|
||||||
# database via a REST API rather than a Java keystore, so the `unifi`
|
|
||||||
# hook's approach does not apply here.
|
|
||||||
#
|
#
|
||||||
# UniFi OS Server exposes a REST API on its management port (default
|
# Endpoints used, all as the web UI itself calls them:
|
||||||
# 11443) that its own web UI uses for certificate management:
|
|
||||||
# POST /api/auth/login - session login (cookie + JWT)
|
# POST /api/auth/login - session login (cookie + JWT)
|
||||||
# GET /api/userCertificates - list uploaded certificates
|
# GET /api/userCertificates - list uploaded certificates
|
||||||
# POST /api/userCertificates - upload a new certificate
|
# POST /api/userCertificates - upload a new certificate
|
||||||
@@ -41,8 +46,9 @@
|
|||||||
# Uses core acme.sh helpers throughout (_post/_get, _json_encode,
|
# Uses core acme.sh helpers throughout (_post/_get, _json_encode,
|
||||||
# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or
|
# _durl_replace_base64, _dbase64, _egrep_o) rather than raw curl -k or
|
||||||
# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all
|
# python3, so the wget fallback, --debug tracing, and CA_BUNDLE are all
|
||||||
# honored the same as every other hook. The management API's cert is
|
# honored the same as every other hook. The management API's cert may be
|
||||||
# self-signed (it's a management-only port, not meant for public exposure),
|
# self-signed -- it always is on a fresh install, and there is no reliable
|
||||||
|
# way to tell in advance whether an earlier run has already replaced it --
|
||||||
# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see
|
# so this hook sets HTTPS_INSECURE=1 itself, scoped to its own subshell (see
|
||||||
# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS
|
# acme.sh's per-hook sourcing in _deploy) -- it does not weaken TLS
|
||||||
# verification for the rest of the acme.sh run, e.g. the connection to the
|
# verification for the rest of the acme.sh run, e.g. the connection to the
|
||||||
@@ -63,9 +69,11 @@
|
|||||||
#
|
#
|
||||||
# Settings:
|
# Settings:
|
||||||
# DEPLOY_UNIFIOS_HOST - base URL of the management API
|
# DEPLOY_UNIFIOS_HOST - base URL of the management API
|
||||||
# (default: "https://localhost:11443")
|
# (default: "https://localhost:11443", i.e. a UniFi OS Server on the
|
||||||
# DEPLOY_UNIFIOS_USERNAME - UniFi OS Server admin username (required)
|
# same machine as acme.sh; set it to "https://<host>" for UniFi OS
|
||||||
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS Server admin password (required)
|
# hardware or any remote target)
|
||||||
|
# DEPLOY_UNIFIOS_USERNAME - UniFi OS admin username (required)
|
||||||
|
# DEPLOY_UNIFIOS_PASSWORD - UniFi OS admin password (required)
|
||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
# export DEPLOY_UNIFIOS_USERNAME="acmeuser"
|
# export DEPLOY_UNIFIOS_USERNAME="acmeuser"
|
||||||
|
|||||||
+7
-5
@@ -9,7 +9,7 @@ Options:
|
|||||||
AZUREDNS_APPID App ID. App ID of the service principal
|
AZUREDNS_APPID App ID. App ID of the service principal
|
||||||
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
|
AZUREDNS_CLIENTSECRET Client Secret. Secret from creating the service principal
|
||||||
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
|
AZUREDNS_MANAGEDIDENTITY Use Managed Identity. Use Managed Identity assigned to a resource instead of a service principal. "true"/"false"
|
||||||
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Optional.
|
AZUREDNS_BEARERTOKEN Bearer Token. Used instead of service principal credentials or managed identity. Not saved, provide it on every run. Optional.
|
||||||
'
|
'
|
||||||
|
|
||||||
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
|
wiki=https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Azure-DNS
|
||||||
@@ -47,13 +47,15 @@ dns_azure_add() {
|
|||||||
_saveaccountconf_mutable AZUREDNS_TENANTID ""
|
_saveaccountconf_mutable AZUREDNS_TENANTID ""
|
||||||
_saveaccountconf_mutable AZUREDNS_APPID ""
|
_saveaccountconf_mutable AZUREDNS_APPID ""
|
||||||
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
|
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET ""
|
||||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN ""
|
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
|
||||||
else
|
else
|
||||||
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
|
_info "You didn't ask to use Azure managed identity, checking service principal credentials or provided bearer token"
|
||||||
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
||||||
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
||||||
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
||||||
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
#AZUREDNS_BEARERTOKEN is short-lived, so it is taken from the environment only and never
|
||||||
|
#read from or saved to the account conf. Versions up to 3.0.9 cached their internal access
|
||||||
|
#token under the same name, which must not be replayed as a user token (#7218).
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||||
AZUREDNS_SUBSCRIPTIONID=""
|
AZUREDNS_SUBSCRIPTIONID=""
|
||||||
@@ -93,7 +95,7 @@ dns_azure_add() {
|
|||||||
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
|
_saveaccountconf_mutable AZUREDNS_TENANTID "$AZUREDNS_TENANTID"
|
||||||
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
|
_saveaccountconf_mutable AZUREDNS_APPID "$AZUREDNS_APPID"
|
||||||
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
|
_saveaccountconf_mutable AZUREDNS_CLIENTSECRET "$AZUREDNS_CLIENTSECRET"
|
||||||
_saveaccountconf_mutable AZUREDNS_BEARERTOKEN "$AZUREDNS_BEARERTOKEN"
|
_clearaccountconf_mutable AZUREDNS_BEARERTOKEN
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
@@ -175,7 +177,7 @@ dns_azure_rm() {
|
|||||||
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
AZUREDNS_TENANTID="${AZUREDNS_TENANTID:-$(_readaccountconf_mutable AZUREDNS_TENANTID)}"
|
||||||
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
AZUREDNS_APPID="${AZUREDNS_APPID:-$(_readaccountconf_mutable AZUREDNS_APPID)}"
|
||||||
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
AZUREDNS_CLIENTSECRET="${AZUREDNS_CLIENTSECRET:-$(_readaccountconf_mutable AZUREDNS_CLIENTSECRET)}"
|
||||||
AZUREDNS_BEARERTOKEN="${AZUREDNS_BEARERTOKEN:-$(_readaccountconf_mutable AZUREDNS_BEARERTOKEN)}"
|
#AZUREDNS_BEARERTOKEN comes from the environment only, see the note in dns_azure_add
|
||||||
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
if [ -z "$AZUREDNS_BEARERTOKEN" ]; then
|
||||||
if [ -z "$AZUREDNS_TENANTID" ]; then
|
if [ -z "$AZUREDNS_TENANTID" ]; then
|
||||||
AZUREDNS_SUBSCRIPTIONID=""
|
AZUREDNS_SUBSCRIPTIONID=""
|
||||||
|
|||||||
+17
-15
@@ -75,6 +75,11 @@ dns_easydns_rm() {
|
|||||||
EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}"
|
EASYDNS_Token="${EASYDNS_Token:-$(_readaccountconf_mutable EASYDNS_Token)}"
|
||||||
EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}"
|
EASYDNS_Key="${EASYDNS_Key:-$(_readaccountconf_mutable EASYDNS_Key)}"
|
||||||
|
|
||||||
|
if [ -z "$EASYDNS_Token" ] || [ -z "$EASYDNS_Key" ]; then
|
||||||
|
_err "You didn't specify an easydns.net token or api key. Signup at https://cp.easydns.com/manage/security/api/signup.php"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
_debug "First detect the root zone"
|
||||||
if ! _get_root "$fulldomain"; then
|
if ! _get_root "$fulldomain"; then
|
||||||
_err "invalid domain"
|
_err "invalid domain"
|
||||||
@@ -91,24 +96,21 @@ dns_easydns_rm() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
|
record_id=$(printf "%s\n" "$response" | tr '{' '\n' | grep "\"rdata\":\"$txtvalue\"" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \")
|
||||||
_debug count "$count"
|
_debug "record_id" "$record_id"
|
||||||
if [ "$count" = "0" ]; then
|
|
||||||
|
if [ -z "$record_id" ]; then
|
||||||
_info "Don't need to remove."
|
_info "Don't need to remove."
|
||||||
else
|
return 0
|
||||||
record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
|
|
||||||
_debug "record_id" "$record_id"
|
|
||||||
if [ -z "$record_id" ]; then
|
|
||||||
_err "Can not get record id to remove."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
|
|
||||||
_err "Delete record error."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_contains "$response" "\"status\":200"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! _EASYDNS_rest DELETE "zones/records/$_domain/$record_id"; then
|
||||||
|
_err "Delete record error."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_contains "$response" "\"status\":200"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
#################### Private functions below ##################################
|
||||||
|
|||||||
+48
-40
@@ -7,22 +7,23 @@ Options:
|
|||||||
JD_ACCESS_KEY_ID Access key ID
|
JD_ACCESS_KEY_ID Access key ID
|
||||||
JD_ACCESS_KEY_SECRET Access key secret
|
JD_ACCESS_KEY_SECRET Access key secret
|
||||||
JD_REGION Region. E.g. "cn-north-1"
|
JD_REGION Region. E.g. "cn-north-1"
|
||||||
Issues: github.com/acmesh-official/acme.sh/issues/2388
|
Issues: github.com/acmesh-official/acme.sh/issues/7202
|
||||||
|
Author: @skysaint
|
||||||
'
|
'
|
||||||
|
|
||||||
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
|
_JD_ACCOUNT="https://uc.jdcloud.com/account/accesskey"
|
||||||
|
|
||||||
_JD_PROD="clouddnsservice"
|
_JD_PROD="domainservice"
|
||||||
_JD_API="jdcloud-api.com"
|
_JD_API="jdcloud-api.com"
|
||||||
|
|
||||||
_JD_API_VERSION="v1"
|
_JD_API_VERSION="v2"
|
||||||
_JD_DEFAULT_REGION="cn-north-1"
|
_JD_DEFAULT_REGION="cn-north-1"
|
||||||
|
|
||||||
_JD_HOST="$_JD_PROD.$_JD_API"
|
_JD_HOST="$_JD_PROD.$_JD_API"
|
||||||
|
|
||||||
######## Public functions #####################
|
######## Public functions #####################
|
||||||
|
|
||||||
#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
#Usage: dns_jd_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
dns_jd_add() {
|
dns_jd_add() {
|
||||||
fulldomain=$1
|
fulldomain=$1
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
@@ -58,24 +59,14 @@ dns_jd_add() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
#_debug "Getting getViewTree"
|
#_debug "Getting describeViewTree"
|
||||||
|
|
||||||
_debug "Adding records"
|
_debug "Adding records"
|
||||||
|
|
||||||
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1},\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
|
_addrr="{\"req\":{\"hostRecord\":\"$_sub_domain\",\"hostValue\":\"$txtvalue\",\"ttl\":300,\"type\":\"TXT\",\"viewValue\":-1}}"
|
||||||
#_addrr='{"req":{"hostRecord":"xx","hostValue":"\"value4\"","jcloudRes":false,"mxPriority":null,"port":null,"ttl":300,"type":"TXT","weight":null,"viewValue":-1},"regionId":"cn-north-1","domainId":"8824"}'
|
#_addrr='{"req":{"hostRecord":"_acme-challenge","hostValue":"XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs","ttl":300,"type":"TXT","viewValue":-1}}'
|
||||||
if jd_rest POST "domain/$_domain_id/RRAdd" "" "$_addrr"; then
|
if jd_rest POST "domain/$_domain_id/ResourceRecord" "" "$_addrr"; then
|
||||||
_rid="$(echo "$response" | tr '{},' '\n' | grep '"id":' | cut -d : -f 2)"
|
|
||||||
if [ -z "$_rid" ]; then
|
|
||||||
_err "Can not find record id from the result."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
_info "TXT record added successfully."
|
_info "TXT record added successfully."
|
||||||
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
|
|
||||||
if [ "$_srid" ]; then
|
|
||||||
_rid="$_srid,$_rid"
|
|
||||||
fi
|
|
||||||
_savedomainconf "JD_CLOUD_RIDS" "$_rid"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -97,14 +88,7 @@ dns_jd_rm() {
|
|||||||
|
|
||||||
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
|
_JD_BASE_URI="$_JD_API_VERSION/regions/$JD_REGION"
|
||||||
|
|
||||||
_info "Getting existing records for $fulldomain"
|
_info "Removing TXT record for $fulldomain"
|
||||||
_srid="$(_readdomainconf "JD_CLOUD_RIDS")"
|
|
||||||
_debug _srid "$_srid"
|
|
||||||
|
|
||||||
if [ -z "$_srid" ]; then
|
|
||||||
_err "Not rid skip"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
_debug "First detect the root zone"
|
_debug "First detect the root zone"
|
||||||
if ! _get_root "$fulldomain"; then
|
if ! _get_root "$fulldomain"; then
|
||||||
@@ -115,16 +99,37 @@ dns_jd_rm() {
|
|||||||
_debug _sub_domain "$_sub_domain"
|
_debug _sub_domain "$_sub_domain"
|
||||||
_debug _domain "$_domain"
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
_cleardomainconf JD_CLOUD_RIDS
|
# List records, filter by hostRecord and use a large pageSize so it isn't missed on record-heavy zones.
|
||||||
|
if ! jd_rest GET "domain/$_domain_id/ResourceRecord" "pageSize=50&search=$_sub_domain"; then
|
||||||
|
_err "Failed to list resource records"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
_aws_tmpl_xml="{\"ids\":[$_srid],\"action\":\"del\",\"regionId\":\"$JD_REGION\",\"domainId\":\"$_domain_id\"}"
|
# Match record by hostRecord + type TXT + hostValue
|
||||||
|
_record_id=""
|
||||||
|
_matched="$(echo "$response" | tr '{' '\n' | grep "\"hostRecord\":\"$_sub_domain\"" | grep "\"type\":\"TXT\"" | grep "\"hostValue\":\"$txtvalue\"")"
|
||||||
|
_debug2 _matched "$_matched"
|
||||||
|
|
||||||
if jd_rest POST "domain/$_domain_id/RROperate" "" "$_aws_tmpl_xml" && _contains "$response" "\"code\":\"OK\""; then
|
if [ -z "$_matched" ]; then
|
||||||
|
_info "TXT record not found, nothing to remove."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_record_id="$(echo "$_matched" | tr ',' '\n' | grep "\"id\":" | cut -d : -f 2 | tr -d '"' | _head_n 1)"
|
||||||
|
_debug _record_id "$_record_id"
|
||||||
|
|
||||||
|
if [ -z "$_record_id" ]; then
|
||||||
|
_info "Could not extract record id from response, nothing to remove."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if jd_rest DELETE "domain/$_domain_id/ResourceRecord/$_record_id"; then
|
||||||
_info "TXT record deleted successfully."
|
_info "TXT record deleted successfully."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
|
||||||
|
|
||||||
|
_err "Failed to delete TXT record."
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
#################### Private functions below ##################################
|
#################### Private functions below ##################################
|
||||||
@@ -134,13 +139,14 @@ _get_root() {
|
|||||||
i=1
|
i=1
|
||||||
p=1
|
p=1
|
||||||
|
|
||||||
|
if ! jd_rest GET "domain"; then
|
||||||
|
_err "error get domain list"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||||
_debug2 "Checking domain: $h"
|
_debug2 "Checking domain: $h"
|
||||||
if ! jd_rest GET "domain"; then
|
|
||||||
_err "error get domain list"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [ -z "$h" ]; then
|
if [ -z "$h" ]; then
|
||||||
#not valid
|
#not valid
|
||||||
_err "Invalid domain"
|
_err "Invalid domain"
|
||||||
@@ -168,6 +174,8 @@ _get_root() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Use '%b' with printf to expand \n escapes in CanonicalRequest and StringToSign.
|
||||||
|
# Use '%s' for plain values that contain no escapes to avoid unintended expansion.
|
||||||
#method uri qstr data
|
#method uri qstr data
|
||||||
jd_rest() {
|
jd_rest() {
|
||||||
mtd="$1"
|
mtd="$1"
|
||||||
@@ -220,7 +228,7 @@ jd_rest() {
|
|||||||
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash"
|
CanonicalRequest="$mtd\n$CanonicalURI\n$CanonicalQueryString\n$CanonicalHeaders\n$SignedHeaders\n$RequestPayloadHash"
|
||||||
_debug2 CanonicalRequest "$CanonicalRequest"
|
_debug2 CanonicalRequest "$CanonicalRequest"
|
||||||
|
|
||||||
HashedCanonicalRequest="$(printf "$CanonicalRequest%s" | _digest "$Hash" hex)"
|
HashedCanonicalRequest="$(printf '%b' "$CanonicalRequest" | _digest "$Hash" hex)"
|
||||||
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
_debug2 HashedCanonicalRequest "$HashedCanonicalRequest"
|
||||||
|
|
||||||
Algorithm="JDCLOUD2-HMAC-SHA256"
|
Algorithm="JDCLOUD2-HMAC-SHA256"
|
||||||
@@ -246,19 +254,19 @@ jd_rest() {
|
|||||||
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
|
kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")"
|
||||||
_secure_debug2 kSecretH "$kSecretH"
|
_secure_debug2 kSecretH "$kSecretH"
|
||||||
|
|
||||||
kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)"
|
kDateH="$(printf '%s' "$RequestDateOnly" | _hmac "$Hash" "$kSecretH" hex)"
|
||||||
_debug2 kDateH "$kDateH"
|
_debug2 kDateH "$kDateH"
|
||||||
|
|
||||||
kRegionH="$(printf "$Region%s" | _hmac "$Hash" "$kDateH" hex)"
|
kRegionH="$(printf '%s' "$Region" | _hmac "$Hash" "$kDateH" hex)"
|
||||||
_debug2 kRegionH "$kRegionH"
|
_debug2 kRegionH "$kRegionH"
|
||||||
|
|
||||||
kServiceH="$(printf "$Service%s" | _hmac "$Hash" "$kRegionH" hex)"
|
kServiceH="$(printf '%s' "$Service" | _hmac "$Hash" "$kRegionH" hex)"
|
||||||
_debug2 kServiceH "$kServiceH"
|
_debug2 kServiceH "$kServiceH"
|
||||||
|
|
||||||
kSigningH="$(printf "%s" "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
|
kSigningH="$(printf '%s' "jdcloud2_request" | _hmac "$Hash" "$kServiceH" hex)"
|
||||||
_debug2 kSigningH "$kSigningH"
|
_debug2 kSigningH "$kSigningH"
|
||||||
|
|
||||||
signature="$(printf "$StringToSign%s" | _hmac "$Hash" "$kSigningH" hex)"
|
signature="$(printf '%b' "$StringToSign" | _hmac "$Hash" "$kSigningH" hex)"
|
||||||
_debug2 signature "$signature"
|
_debug2 signature "$signature"
|
||||||
|
|
||||||
Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
|
Authorization="$Algorithm Credential=$JD_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature"
|
||||||
|
|||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
dns_myloc_info='myloc.de
|
||||||
|
Site: myloc.de
|
||||||
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_myloc
|
||||||
|
Issues: github.com/acmesh-official/acme.sh/issues/5193
|
||||||
|
Options:
|
||||||
|
MYLOC_token API token
|
||||||
|
'
|
||||||
|
|
||||||
|
# updater for the (experimental) API of myloc.de / webtropia.com
|
||||||
|
# usage: acme.sh --issue -d example.com --dns dns_myloc --dnssleep 60
|
||||||
|
# API documentation at https://apidoc.myloc.de/
|
||||||
|
# As the API does not support quering available zones yet, the zone for a given
|
||||||
|
# fulldomain is searched recursively by removing prefixes one-by-one.
|
||||||
|
|
||||||
|
_myloc_api="https://zkm.myloc.de/api"
|
||||||
|
|
||||||
|
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
|
dns_myloc_add() {
|
||||||
|
_myloc_fulldomain=$1
|
||||||
|
_myloc_txtvalue=$2
|
||||||
|
|
||||||
|
_myloc_token="${MYLOC_token:-$(_readaccountconf_mutable MYLOC_token)}"
|
||||||
|
if [ -z "$_myloc_token" ]; then
|
||||||
|
_err "You didn't specify MYLOC_token"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export _H1="Content-Type: application/json"
|
||||||
|
export _H2="Authorization: Bearer $_myloc_token"
|
||||||
|
|
||||||
|
_myloc_zone="$(_myloc_get_zone "$_myloc_fulldomain")"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# save token if the previous request was successful
|
||||||
|
_saveaccountconf_mutable MYLOC_token "$_myloc_token"
|
||||||
|
|
||||||
|
_info "Adding record"
|
||||||
|
_myloc_record="{\"type\":\"TXT\",\"name\":\"${_myloc_fulldomain}\",\"content\":\"\\\"${_myloc_txtvalue}\\\"\",\"ttl\":60}"
|
||||||
|
_debug "add record request $_myloc_record to ${_myloc_api}/dns/zone/${_myloc_zone}"
|
||||||
|
_myloc_response="$(_post "$_myloc_record" "${_myloc_api}/dns/zone/${_myloc_zone}" "" "PUT")"
|
||||||
|
_myloc_status=$?
|
||||||
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||||
|
_debug "add record response $_code $_myloc_response"
|
||||||
|
if [ $_myloc_status -ne 0 ]; then
|
||||||
|
_err "Add txt record curl error."
|
||||||
|
return 1
|
||||||
|
elif [ "$_code" = "204" ] && [ -z "$_myloc_response" ]; then
|
||||||
|
_info "Add txt record success"
|
||||||
|
return 0
|
||||||
|
elif _contains "$_myloc_response" "error" || _contains "$_myloc_response" "unexpected"; then
|
||||||
|
_err "Add txt record api error."
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
_err "Add txt record unknown response."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#_myloc_fulldomain _myloc_txtvalue
|
||||||
|
dns_myloc_rm() {
|
||||||
|
_myloc_fulldomain=$1
|
||||||
|
_myloc_txtvalue=$2
|
||||||
|
|
||||||
|
_myloc_token="${MYLOC_token:-$(_readaccountconf_mutable MYLOC_token)}"
|
||||||
|
if [ -z "$_myloc_token" ]; then
|
||||||
|
_err "You didn't specify MYLOC_token"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export _H1="Content-Type: application/json"
|
||||||
|
export _H2="Authorization: Bearer $_myloc_token"
|
||||||
|
|
||||||
|
_myloc_zone="$(_myloc_get_zone "$_myloc_fulldomain")"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# save token if the previous request was successful
|
||||||
|
_saveaccountconf_mutable MYLOC_token "$_myloc_token"
|
||||||
|
|
||||||
|
_info "Deleting record for $_myloc_fulldomain"
|
||||||
|
_myloc_record="{\"type\":\"TXT\",\"name\":\"${_myloc_fulldomain}\",\"content\":\"\\\"${_myloc_txtvalue}\\\"\"}"
|
||||||
|
_debug "delete record $_myloc_record"
|
||||||
|
_myloc_response="$(_post "$_myloc_record" "${_myloc_api}/dns/zone/${_myloc_zone}" "" "DELETE")"
|
||||||
|
_myloc_status=$?
|
||||||
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||||
|
_debug "delete response $_code $_myloc_response"
|
||||||
|
if [ $_myloc_status -ne 0 ] || [ "$_code" != "204" ]; then
|
||||||
|
_err "Failed to delete record"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage: _myloc_get_zone "_acme-challenge.sub1.mydomain.com"
|
||||||
|
# Subdomains are walked until a zone is found or TLD is reached
|
||||||
|
_myloc_get_zone() {
|
||||||
|
_myloc_zone=$1
|
||||||
|
|
||||||
|
while [ "${_myloc_zone#*.}" != "$_myloc_zone" ]; do
|
||||||
|
_debug "Get zone trying $_myloc_zone"
|
||||||
|
_myloc_response="$(_get "${_myloc_api}/dns/zone/${_myloc_zone}")"
|
||||||
|
_myloc_status=$?
|
||||||
|
_debug "Get zone response $_myloc_response"
|
||||||
|
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||||
|
if [ $_myloc_status -eq 0 ] && [ "$_code" = "200" ]; then
|
||||||
|
_debug "Get zone success for $_myloc_zone"
|
||||||
|
echo "${_myloc_zone}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
_myloc_zone="${_myloc_zone#*.}"
|
||||||
|
done
|
||||||
|
|
||||||
|
_err "Get zone failed for all candidates"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user