mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-10-21 13:31:18 +00:00
Compare commits
40
Commits
3.1.5
...
c9e9e24e24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e9e24e24 | ||
|
|
aef81df5eb | ||
|
|
e3004b84a1 | ||
|
|
3288c35c2a | ||
|
|
5d07634e0f | ||
|
|
a57c8a45fa | ||
|
|
0d1d0092b8 | ||
|
|
c472636bae | ||
|
|
579c994fad | ||
|
|
f659ba2ea2 | ||
|
|
f5f2242d60 | ||
|
|
d73d68ced4 | ||
|
|
228a140094 | ||
|
|
f5aeed1752 | ||
|
|
81fd2d2126 | ||
|
|
772ed2aa93 | ||
|
|
ee38f28e13 | ||
|
|
9d09edcd69 | ||
|
|
cf51ad22d1 | ||
|
|
3e6d4835e5 | ||
|
|
0e4d75325b | ||
|
|
04c3e64ea7 | ||
|
|
2f8a5535dc | ||
|
|
b2e6173b1a | ||
|
|
acbe85b7e9 | ||
|
|
6467ca9764 | ||
|
|
a1de626512 | ||
|
|
84a8d2a752 | ||
|
|
6ef16ffe63 | ||
|
|
427787cb8f | ||
|
|
31f06a4ecf | ||
|
|
6618d55bfc | ||
|
|
879c1519bb | ||
|
|
d51d853ad4 | ||
|
|
ceed0e0228 | ||
|
|
aafc4efe1f | ||
|
|
138e0dff84 | ||
|
|
84d6b5841e | ||
|
|
0ee2720d73 | ||
|
|
e4c0a4f5f5 |
No files matched your search
@@ -6,6 +6,9 @@ name: Mirror version tag
|
||||
# pointing to the same object, so both forms exist.
|
||||
# No retrigger loop: the tag filter never matches a "v"-prefixed tag, and
|
||||
# refs created with GITHUB_TOKEN do not fire workflows anyway.
|
||||
# The job mirrors first and then fails when the pushed tag is lightweight,
|
||||
# which is what the release form produces: that tag can never carry a
|
||||
# signature, so the failure has to be loud.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -26,19 +29,39 @@ jobs:
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag v$TAG already exists, nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
# Mirror the object the pushed tag actually points at: the commit
|
||||
# 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)"
|
||||
_ref="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq '.object.sha + " " + .object.type')"
|
||||
sha="${_ref%% *}"
|
||||
objtype="${_ref##* }"
|
||||
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"
|
||||
if gh api "repos/$REPO/git/ref/tags/v$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag v$TAG already exists, nothing to do."
|
||||
else
|
||||
gh api "repos/$REPO/git/refs" -f ref="refs/tags/v$TAG" -f sha="$sha"
|
||||
echo "Created tag v$TAG -> $sha"
|
||||
fi
|
||||
|
||||
- name: Check that the tag is signable
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
# A release published from the GitHub UI creates the tag server-side
|
||||
# as a lightweight ref, which points straight at a commit and can
|
||||
# never carry a signature (3.1.5 shipped that way, see issue 7273).
|
||||
# The tag has to be created locally with "git tag -s" and pushed
|
||||
# BEFORE the release is published, then selected on the release form.
|
||||
objtype="$(gh api "repos/$REPO/git/ref/tags/$TAG" --jq .object.type)"
|
||||
if [ "$objtype" != "tag" ]; then
|
||||
echo "::error::refs/tags/$TAG points at a $objtype, not a tag object. It is a lightweight tag and carries no signature. Recreate it locally with 'git tag -s $TAG' and push it before publishing the release."
|
||||
exit 1
|
||||
fi
|
||||
echo "refs/tags/$TAG is a tag object."
|
||||
@@ -233,7 +233,7 @@ acme.sh -h
|
||||
|
||||
#### 🔏 Verify a Release
|
||||
|
||||
Release tags from `3.1.5` on are signed with the maintainer's SSH key. The
|
||||
Release tags from `3.1.6` 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:
|
||||
@@ -243,7 +243,7 @@ git config gpg.ssh.allowedSignersFile allowed_signers
|
||||
```
|
||||
|
||||
```bash
|
||||
git verify-tag 3.1.5
|
||||
git verify-tag 3.1.6
|
||||
```
|
||||
|
||||
The signature covers the tag object, which pins the commit and therefore the
|
||||
@@ -251,10 +251,10 @@ 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
|
||||
git archive --format=tar.gz --prefix=acme.sh-3.1.6/ 3.1.6 > acme.sh-3.1.6.tar.gz
|
||||
```
|
||||
|
||||
> ⚠️ Tags up to `3.1.4` predate the signing key and are unsigned.
|
||||
> ⚠️ Tags up to `3.1.5` are unsigned.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
VER=3.1.5
|
||||
VER=3.1.7
|
||||
|
||||
PROJECT_NAME="acme.sh"
|
||||
|
||||
@@ -666,8 +666,15 @@ _hex_dump() {
|
||||
xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " "
|
||||
else
|
||||
_debug3 "using _ascii_hex"
|
||||
str=$(cat)
|
||||
_ascii_hex "$str"
|
||||
#line by line: the cut -c in _ascii_hex works per line, and a $(cat)
|
||||
#capture would drop the trailing newlines (illumos without od in PATH)
|
||||
while IFS= read -r _hd_line; do
|
||||
_ascii_hex "$_hd_line"
|
||||
printf " 0a"
|
||||
done
|
||||
if [ -n "$_hd_line" ]; then
|
||||
_ascii_hex "$_hd_line"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -907,7 +914,12 @@ _json_encode() {
|
||||
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
|
||||
_debug3 "_json_encode"
|
||||
_debug3 "_j_str" "$_j_str"
|
||||
echo "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
|
||||
#printf, not echo: dash's echo interprets backslash escapes and would
|
||||
#collapse the \\ sequences (e.g. Telegram MarkdownV2 escapes, issue 7212)
|
||||
_j_hex="$(printf "%s\n" "$_j_str" | _hex_dump | _lower_case)"
|
||||
#the hex line must end with a newline: Solaris sed drops an unterminated
|
||||
#last line, which emptied the whole output
|
||||
printf "%s\n" "$_j_hex" | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
|
||||
}
|
||||
|
||||
#from: http:\/\/ to http://
|
||||
@@ -915,7 +927,7 @@ _json_decode() {
|
||||
_j_str="$(sed 's#\\/#/#g')"
|
||||
_debug3 "_json_decode"
|
||||
_debug3 "_j_str" "$_j_str"
|
||||
echo "$_j_str"
|
||||
printf "%s\n" "$_j_str"
|
||||
}
|
||||
|
||||
#extract the authorization URLs from an order response on stdin, as a
|
||||
@@ -2140,6 +2152,7 @@ _resethttp() {
|
||||
__HTTP_INITIALIZED=""
|
||||
_ACME_CURL=""
|
||||
_ACME_WGET=""
|
||||
_ACME_WGET2=""
|
||||
ACME_HTTP_NO_REDIRECTS=""
|
||||
}
|
||||
|
||||
@@ -2190,7 +2203,15 @@ _inithttp() {
|
||||
fi
|
||||
|
||||
if [ -z "$_ACME_WGET" ] && _exists "wget"; then
|
||||
_ACME_WGET="wget -q"
|
||||
#wget2, the wget of Fedora 40 and later, prints nothing for -S under -q,
|
||||
#so it runs without -q and _wget2_fix_header cleans up after it
|
||||
_ACME_WGET2=""
|
||||
if _contains "$(wget --version 2>&1 | _head_n 1)" "Wget2"; then
|
||||
_ACME_WGET2=1
|
||||
_ACME_WGET="wget"
|
||||
else
|
||||
_ACME_WGET="wget -q"
|
||||
fi
|
||||
if [ "$ACME_USE_IPV6_REQUESTS" ]; then
|
||||
_ACME_WGET="$_ACME_WGET --inet6-only "
|
||||
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
|
||||
@@ -2200,7 +2221,8 @@ _inithttp() {
|
||||
_ACME_WGET="$_ACME_WGET --max-redirect 0 "
|
||||
fi
|
||||
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then
|
||||
if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then
|
||||
#the -d demultiplexing after each request expects wget 1.x output
|
||||
if [ -z "$_ACME_WGET2" ] && _contains "$($_ACME_WGET --help 2>&1)" "--debug"; then
|
||||
_ACME_WGET="$_ACME_WGET -d "
|
||||
fi
|
||||
fi
|
||||
@@ -2220,6 +2242,30 @@ _inithttp() {
|
||||
|
||||
}
|
||||
|
||||
#stdin: what wget2 -S wrote to stderr. Prints the response headers the way
|
||||
#curl --dump-header writes them. wget2 frames every header block with lines of
|
||||
#its own ("[0] Downloading ...", "# got header ...", and after the block
|
||||
#"HTTP response 200 OK [url]", which callers would take for the status line),
|
||||
#and prints no header block at all for a response without a body (a 204, the
|
||||
#empty 200 of a revocation): then only the status line can be rebuilt, from
|
||||
#the last "HTTP [ERROR ]response" line, with HTTP/1.1 assumed.
|
||||
_wget2_headers() {
|
||||
_w2h_in="$(cat)"
|
||||
_w2h_cr="$(printf '\r')"
|
||||
_w2h_blocks="$(printf "%s\n" "$_w2h_in" | sed -n "/^HTTP /d; /^HTTP\//,/^$_w2h_cr*\$/p")"
|
||||
if [ "$_w2h_blocks" ]; then
|
||||
printf "%s\n" "$_w2h_blocks"
|
||||
else
|
||||
printf "%s\n" "$_w2h_in" | sed -n 's/^HTTP ERROR response /HTTP response /; s/^HTTP response \([0-9][0-9]*\).*$/HTTP\/1.1 \1/p' | _tail_n 1
|
||||
fi
|
||||
}
|
||||
|
||||
#rewrite $HTTP_HEADER after a wget2 request, see _wget2_headers
|
||||
_wget2_fix_header() {
|
||||
_w2f_headers="$(_wget2_headers <"$HTTP_HEADER")"
|
||||
printf "%s\n" "$_w2f_headers" >"$HTTP_HEADER"
|
||||
}
|
||||
|
||||
# body url [needbase64] [POST|PUT|DELETE] [ContentType]
|
||||
_post() {
|
||||
body="$1"
|
||||
@@ -2315,7 +2361,15 @@ _post() {
|
||||
response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
|
||||
fi
|
||||
elif [ "$httpmethod" = "HEAD" ]; then
|
||||
if [ "$_postContentType" ]; then
|
||||
if [ "$_ACME_WGET2" ]; then
|
||||
#wget2 prints no headers for a HEAD response, not even with -S, but
|
||||
#--save-headers writes them into the -O file
|
||||
if [ "$_postContentType" ]; then
|
||||
response="$($_WGET --method HEAD --save-headers -O "$HTTP_HEADER" --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" "$_post_url" 2>/dev/null)"
|
||||
else
|
||||
response="$($_WGET --method HEAD --save-headers -O "$HTTP_HEADER" --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" "$_post_url" 2>/dev/null)"
|
||||
fi
|
||||
elif [ "$_postContentType" ]; then
|
||||
response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --header "Content-Type: $_postContentType" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
|
||||
else
|
||||
response="$($_WGET --spider -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-data="$body" "$_post_url" 2>"$HTTP_HEADER")"
|
||||
@@ -2341,6 +2395,9 @@ _post() {
|
||||
cat "$HTTP_HEADER" >&2
|
||||
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
|
||||
fi
|
||||
if [ "$_ACME_WGET2" ]; then
|
||||
_wget2_fix_header
|
||||
fi
|
||||
# remove leading whitespaces from header to match curl format
|
||||
_sed_i 's/^ //g' "$HTTP_HEADER"
|
||||
else
|
||||
@@ -2434,6 +2491,9 @@ _post_file() {
|
||||
cat "$HTTP_HEADER" >&2
|
||||
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
|
||||
fi
|
||||
if [ "$_ACME_WGET2" ]; then
|
||||
_wget2_fix_header
|
||||
fi
|
||||
# remove leading whitespaces from header to match curl format
|
||||
_sed_i 's/^ //g' "$HTTP_HEADER"
|
||||
else
|
||||
@@ -2501,6 +2561,9 @@ _get() {
|
||||
cat "$HTTP_HEADER" >&2
|
||||
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
|
||||
fi
|
||||
if [ "$_ACME_WGET2" ]; then
|
||||
_wget2_fix_header
|
||||
fi
|
||||
# remove leading whitespaces from header to match curl format
|
||||
_sed_i 's/^ //g' "$HTTP_HEADER"
|
||||
fi
|
||||
@@ -2687,10 +2750,6 @@ _send_signed_request() {
|
||||
_debug2 _headers "$_headers"
|
||||
_CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)"
|
||||
fi
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Cannot connect to $nonceurl to get nonce."
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE"
|
||||
fi
|
||||
@@ -2729,11 +2788,19 @@ _send_signed_request() {
|
||||
_debug3 body "$body"
|
||||
|
||||
response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")"
|
||||
_post_ret="$?"
|
||||
_CACHED_NONCE=""
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "Cannot make POST request to $url"
|
||||
return 1
|
||||
#A transport failure (curl 7, 28, 35, ...) brings no answer at all. Issue
|
||||
#7320: the status was tested only after the line above had reset it, so
|
||||
#such a failure went on as a success, the code read from whatever
|
||||
#$HTTP_HEADER still held, and the caller blamed the CA ("Finalize code was
|
||||
#not 200"). Retry it the way a gateway error is retried, with a new nonce.
|
||||
if [ "$_post_ret" != "0" ]; then
|
||||
_sleep_post_retry_sec="$(_retry_backoff_sec "$_request_retry_times")"
|
||||
_info "Cannot make POST request to $url, let's try again. Sleeping for $_sleep_post_retry_sec seconds. ($_request_retry_times/$MAX_REQUEST_RETRY_TIMES)"
|
||||
_sleep "$_sleep_post_retry_sec"
|
||||
continue
|
||||
fi
|
||||
|
||||
responseHeaders="$(cat "$HTTP_HEADER")"
|
||||
@@ -2800,6 +2867,35 @@ _sed_escape_rhs() {
|
||||
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g'
|
||||
}
|
||||
|
||||
#_write_conf file content
|
||||
#Replace the conf file with the content.
|
||||
#Redirecting straight into the conf truncates it before anything is written, so
|
||||
#a failed write (e.g. no space left on device) left an empty conf and the cert
|
||||
#could not be renewed any more (#7247). Write a temp file next to the conf and
|
||||
#rename it over the conf only after the content is verified.
|
||||
_write_conf() {
|
||||
__w_conf="$1"
|
||||
__w_text="$2"
|
||||
__w_tmp="$__w_conf.$$.tmp"
|
||||
#cp -p, so the temp file carries the mode and owner of the conf
|
||||
if ! cp -p "$__w_conf" "$__w_tmp" 2>/dev/null ||
|
||||
! printf -- "%s\n" "$__w_text" 2>/dev/null >"$__w_tmp" ||
|
||||
[ "$(cat "$__w_tmp")" != "$__w_text" ]; then
|
||||
rm -f "$__w_tmp"
|
||||
return 1
|
||||
fi
|
||||
if [ ! -L "$__w_conf" ] && mv -f "$__w_tmp" "$__w_conf" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
#a symlink or a bind mounted file cannot be renamed over, write in place
|
||||
if ! cat "$__w_tmp" 2>/dev/null >"$__w_conf"; then
|
||||
#the conf may be truncated now, the temp file is the only complete copy
|
||||
_err "Cannot write $__w_conf, the new content is kept in $__w_tmp"
|
||||
return 1
|
||||
fi
|
||||
rm -f "$__w_tmp"
|
||||
}
|
||||
|
||||
#setopt "file" "opt" "=" "value" [";"]
|
||||
_setopt() {
|
||||
__conf="$1"
|
||||
@@ -2826,39 +2922,37 @@ _setopt() {
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ -n "$(_tail_c 1 <"$__conf")" ]; then
|
||||
echo >>"$__conf"
|
||||
if ! __text="$(cat "$__conf")"; then
|
||||
_err "Cannot read $__conf."
|
||||
return 1
|
||||
fi
|
||||
|
||||
__old_text="$__text"
|
||||
#build the new content first and write it once through _write_conf:
|
||||
#redirecting straight into the conf truncates it before anything is
|
||||
#written, so a failing sed (#2426) or a failing write (#7247) left a
|
||||
#truncated conf, and a short append left a half written line
|
||||
__sed_err=""
|
||||
if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
|
||||
_debug3 OK
|
||||
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
|
||||
text="$(cat "$__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
|
||||
|
||||
__text="$(printf -- "%s\n" "$__text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
|
||||
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
|
||||
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
|
||||
text="$(cat "$__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
|
||||
|
||||
__text="$(printf -- "%s\n" "$__text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
|
||||
else
|
||||
_debug3 APP
|
||||
#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"
|
||||
__text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
|
||||
fi
|
||||
if [ -z "$__sed_err" ] && [ "$__text" = "$__old_text" ]; then
|
||||
#the value is already saved: callers re-save the same values on every
|
||||
#run, and each write is a temp file plus a rename, which is slow on
|
||||
#some storage (#7335)
|
||||
_debug3 "Unchanged, skip writing $__conf"
|
||||
return 0
|
||||
fi
|
||||
if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then
|
||||
_err "Cannot save '$__opt' to $__conf."
|
||||
return 1
|
||||
fi
|
||||
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
|
||||
}
|
||||
@@ -2885,10 +2979,22 @@ _clear_conf() {
|
||||
_c_c_f="$1"
|
||||
_sdkey="$2"
|
||||
if [ "$_c_c_f" ]; then
|
||||
_conf_data="$(cat "$_c_c_f")"
|
||||
_c_c_old="$(cat "$_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"
|
||||
if ! _conf_data="$(printf -- "%s\n" "$_c_c_old" | sed "/^$_sdkey *=.*$/d")"; then
|
||||
_err "Cannot clear '$_sdkey' in $_c_c_f."
|
||||
return 1
|
||||
fi
|
||||
#the key is not there, the usual case for the legacy keys cleared on
|
||||
#every _savedeployconf: nothing to write (#7335)
|
||||
if [ "$_conf_data" = "$_c_c_old" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! _write_conf "$_c_c_f" "$_conf_data"; then
|
||||
_err "Cannot clear '$_sdkey' in $_c_c_f."
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
_err "Config file is empty, cannot clear"
|
||||
fi
|
||||
@@ -3365,7 +3471,13 @@ __initHome() {
|
||||
|
||||
if [ -z "$ACCOUNT_CONF_PATH" ]; then
|
||||
if [ -f "$_DEFAULT_ACCOUNT_CONF_PATH" ]; then
|
||||
#Same as in _initpath: keep the live ACCOUNT_EMAIL across the sourcing,
|
||||
#so that the -m address _process() exported is not replaced by the
|
||||
#saved one. _process() calls __initHome directly, after the option
|
||||
#loop, so this is the sourcing that -m used to lose to.
|
||||
_ih_account_email="$ACCOUNT_EMAIL"
|
||||
. "$_DEFAULT_ACCOUNT_CONF_PATH"
|
||||
ACCOUNT_EMAIL="$_ih_account_email"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -3475,12 +3587,22 @@ _initpath() {
|
||||
domain="$1"
|
||||
_ilength="$2"
|
||||
|
||||
#Keep the live ACCOUNT_EMAIL, the one -m exported in _process() or the
|
||||
#caller put in the environment. account.conf is sourced twice below (here
|
||||
#and inside __initHome), and a sourced assignment would overwrite it with
|
||||
#the saved address, so -m silently lost to whatever account.conf held.
|
||||
#The saved address is not lost either way: _getAccountEmail() reads it
|
||||
#with _readaccountconf as its last resort, after the per-CA CA_EMAIL.
|
||||
_cli_account_email="$ACCOUNT_EMAIL"
|
||||
|
||||
__initHome
|
||||
|
||||
if [ -f "$ACCOUNT_CONF_PATH" ]; then
|
||||
. "$ACCOUNT_CONF_PATH"
|
||||
fi
|
||||
|
||||
ACCOUNT_EMAIL="$_cli_account_email"
|
||||
|
||||
if [ "$_ACME_IN_CRON" ]; then
|
||||
if [ ! "$_USER_PATH_EXPORTED" ]; then
|
||||
_USER_PATH_EXPORTED=1
|
||||
@@ -4420,9 +4542,11 @@ _regAccount() {
|
||||
_secure_debug3 _eab_kid "$_eab_kid"
|
||||
_secure_debug3 _eab_hmac_key "$_eab_hmac_key"
|
||||
_email="$(_getAccountEmail)"
|
||||
if [ "$_email" ]; then
|
||||
_savecaconf "CA_EMAIL" "$_email"
|
||||
fi
|
||||
#CA_EMAIL is saved only once the CA has actually taken the contact, which
|
||||
#is when it answers 201. For an account key it already knows it answers
|
||||
#200 and ignores the contact of the request, so saving here would record
|
||||
#an address the CA never stored.
|
||||
_saved_ca_email="$(_readcaconf CA_EMAIL)"
|
||||
|
||||
if [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then
|
||||
if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then
|
||||
@@ -4501,8 +4625,15 @@ _regAccount() {
|
||||
if [ "$code" = "" ] || [ "$code" = '201' ]; then
|
||||
echo "$response" >"$ACCOUNT_JSON_PATH"
|
||||
_info "Registered"
|
||||
if [ "$_email" ]; then
|
||||
_savecaconf "CA_EMAIL" "$_email"
|
||||
fi
|
||||
elif [ "$code" = '409' ] || [ "$code" = '200' ]; then
|
||||
_info "Already registered"
|
||||
if [ "$_email" ] && [ "$_email" != "$_saved_ca_email" ]; then
|
||||
_info "The account email was not changed, the CA ignores the contact of an account it already has."
|
||||
_info "Use '$PROJECT_ENTRY --update-account -m $_email' to change it."
|
||||
fi
|
||||
elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then
|
||||
_info "EAB already registered"
|
||||
_eabAlreadyBound=1
|
||||
@@ -5590,11 +5721,9 @@ issue() {
|
||||
_on_issue_err "$_post_hook"
|
||||
return 1
|
||||
fi
|
||||
# RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs
|
||||
# (Let's Encrypt) may also reject with a malformed error if the prior cert
|
||||
# was issued by a different issuer / different CA. Retry without "replaces"
|
||||
# whenever the failure mentions ARI or the replaces field.
|
||||
if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
|
||||
# Retry without "replaces" whenever the CA rejected that field, e.g. after
|
||||
# switching the ACME server: the prior cert belongs to the old CA.
|
||||
if [ "$_replaces_certID" ] && _isARIReplacesRejected "$code" "$response"; then
|
||||
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
|
||||
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
|
||||
_err "Error creating new order."
|
||||
@@ -7930,6 +8059,35 @@ _getARICertID() {
|
||||
printf "%s.%s" "$_akiurl" "$_serurl"
|
||||
}
|
||||
|
||||
#httpcode response
|
||||
#Returns 0 when a newOrder was rejected because of the ARI "replaces" field,
|
||||
#so that the order can be retried without it.
|
||||
#The status code decides first, and an empty code counts as "not rejected":
|
||||
#an ACCEPTED order echoes the field back, since RFC 9773 Section 5 says that
|
||||
#a server accepting a newOrder request with a "replaces" field "MUST reflect
|
||||
#that field in the response", and the certID it carries is base64url, so the
|
||||
#response of a SUCCESSFUL order can contain "replaces" and even "ARI".
|
||||
#Matching on the message alone would then re-order without "replaces" and
|
||||
#defeat ARI.
|
||||
#Only the 409 "alreadyReplaced" type is mandated by RFC 9773 Section 5; the
|
||||
#other checks it lists (same ACME account, shared identifier) are left to
|
||||
#server policy, so the wording differs per CA: Let's Encrypt answers
|
||||
#malformed when the prior cert was issued by a different issuer, ZeroSSL
|
||||
#answers 401 with 'The "replaces" field does not identify a certificate that
|
||||
#belongs to this ACME account'.
|
||||
#https://github.com/acmesh-official/acme.sh/issues/7280
|
||||
_isARIReplacesRejected() {
|
||||
_ari_rej_code="$1"
|
||||
_ari_rej_resp="$2"
|
||||
if [ -z "$_ari_rej_code" ] || _startswith "$_ari_rej_code" "2"; then
|
||||
return 1
|
||||
fi
|
||||
_contains "$_ari_rej_resp" "alreadyReplaced" ||
|
||||
_contains "$_ari_rej_resp" "replaces" ||
|
||||
_contains "$_ari_rej_resp" "ARI" ||
|
||||
_contains "$_ari_rej_resp" "urn:ietf:params:acme:error:malformed"
|
||||
}
|
||||
|
||||
#cert
|
||||
_get_ARI() {
|
||||
_cert="$1"
|
||||
|
||||
+4
-2
@@ -4,17 +4,19 @@
|
||||
# maintainer and is never available to CI, so a compromise of the build
|
||||
# pipeline cannot produce a tag that verifies against this file.
|
||||
#
|
||||
# Signing starts at 3.1.6; tags up to 3.1.5 are unsigned.
|
||||
#
|
||||
# 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
|
||||
# git verify-tag 3.1.6
|
||||
#
|
||||
# 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
|
||||
# git archive --format=tar.gz --prefix=acme.sh-3.1.6/ 3.1.6 > acme.sh-3.1.6.tar.gz
|
||||
#
|
||||
github@neilpang.com namespaces="git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBTjI0HBJn3uhfT2DsNcFybfAZi3ADbIacMpz1BItKdB
|
||||
+2
-2
@@ -74,9 +74,9 @@ fritzbox_deploy() {
|
||||
_info "Log in to the FRITZ!Box"
|
||||
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
|
||||
if _exists iconv; then
|
||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF16LE | _digest md5 hex)"
|
||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | iconv -f ASCII -t UTF-16LE | _digest md5 hex)"
|
||||
elif _exists uconv; then
|
||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF16LE | _digest md5 hex)"
|
||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | uconv -f ASCII -t UTF-16LE | _digest md5 hex)"
|
||||
else
|
||||
_fritzbox_hash="$(printf "%s-%s" "${_fritzbox_challenge}" "${DEPLOY_FRITZBOX_PASSWORD}" | perl -p -e 'use Encode qw/encode/; print encode("UTF-16LE","$_"); $_="";' | _digest md5 hex)"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Here is a script to deploy cert to an HP printer's Embedded Web Server (EWS).
|
||||
#
|
||||
# Docs: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#47-deploy-the-certificate-to-an-hp-printer-ews
|
||||
#
|
||||
# Tested on an HP Color LaserJet Pro MFP M479fdw, firmware
|
||||
# CLRWTRXXXN002.2539E.00. It should work on any modern HP printer whose EWS
|
||||
# exposes the "Install Certificate" flow at
|
||||
# /Security/DeviceCertificates/NewCertWithPassword/Upload -- that endpoint
|
||||
# takes a password-protected PKCS#12 bundle, which this hook builds from the
|
||||
# issued certificate, its key and the CA chain.
|
||||
#
|
||||
# ```sh
|
||||
# export HP_EWS_PASSWORD=mysecret
|
||||
# export HP_EWS_INSECURE=1 # needed until the printer serves a trusted cert
|
||||
# acme.sh --deploy -d printer.example.com --deploy-hook hp_ews
|
||||
# ```
|
||||
#
|
||||
# Environment variables:
|
||||
# HP_EWS_PASSWORD (required) EWS administrator password.
|
||||
# HP_EWS_HOST (optional) IP or hostname of the printer.
|
||||
# Default: the certificate's domain.
|
||||
# HP_EWS_USERNAME (optional) EWS administrator user. Default: admin
|
||||
# HP_EWS_PFX_PASSWORD (optional) Password for the generated PKCS#12
|
||||
# bundle. A random one is used per run if
|
||||
# unset; set it only if you want the
|
||||
# bundle to be reproducible.
|
||||
# HP_EWS_INSECURE (optional) Set to a non-empty value to skip TLS
|
||||
# verification when talking to the
|
||||
# printer. Needed on the first deploy,
|
||||
# because the printer still serves its
|
||||
# self-signed certificate at that point.
|
||||
# acme.sh's global HTTPS_INSECURE is
|
||||
# honoured as well.
|
||||
#
|
||||
# The hook authenticates with HTTP Basic auth. Printers whose EWS only offers
|
||||
# Digest auth are not supported.
|
||||
#
|
||||
# Requires openssl, to build the PKCS#12 bundle.
|
||||
#
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain pfx
|
||||
hp_ews_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"
|
||||
|
||||
_getdeployconf HP_EWS_HOST
|
||||
_getdeployconf HP_EWS_USERNAME
|
||||
_getdeployconf HP_EWS_PASSWORD
|
||||
_getdeployconf HP_EWS_PFX_PASSWORD
|
||||
_getdeployconf HP_EWS_INSECURE
|
||||
|
||||
HP_EWS_HOST="${HP_EWS_HOST:-$_cdomain}"
|
||||
HP_EWS_USERNAME="${HP_EWS_USERNAME:-admin}"
|
||||
|
||||
_debug HP_EWS_HOST "$HP_EWS_HOST"
|
||||
_debug HP_EWS_USERNAME "$HP_EWS_USERNAME"
|
||||
_secure_debug HP_EWS_PASSWORD "$HP_EWS_PASSWORD"
|
||||
_secure_debug HP_EWS_PFX_PASSWORD "$HP_EWS_PFX_PASSWORD"
|
||||
_debug HP_EWS_INSECURE "$HP_EWS_INSECURE"
|
||||
|
||||
if [ -z "$HP_EWS_PASSWORD" ]; then
|
||||
_err "HP_EWS_PASSWORD is required. Please set the password of the printer's EWS administrator account."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_savedeployconf HP_EWS_HOST "$HP_EWS_HOST"
|
||||
_savedeployconf HP_EWS_USERNAME "$HP_EWS_USERNAME"
|
||||
_savedeployconf HP_EWS_PASSWORD "$HP_EWS_PASSWORD" "base64"
|
||||
_savedeployconf HP_EWS_PFX_PASSWORD "$HP_EWS_PFX_PASSWORD" "base64"
|
||||
_savedeployconf HP_EWS_INSECURE "$HP_EWS_INSECURE"
|
||||
|
||||
if ! _exists "${ACME_OPENSSL_BIN:-openssl}"; then
|
||||
_err "openssl is required by this deploy hook, but was not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The bundle password only has to survive the single upload below, so a
|
||||
# fresh random one is used unless the operator asked for a fixed value.
|
||||
_HP_EWS_PFXPW="$HP_EWS_PFX_PASSWORD"
|
||||
if [ -z "$_HP_EWS_PFXPW" ]; then
|
||||
_HP_EWS_PFXPW="$(${ACME_OPENSSL_BIN:-openssl} rand -hex 24)"
|
||||
if [ -z "$_HP_EWS_PFXPW" ]; then
|
||||
_err "Could not generate a random password for the PKCS#12 bundle."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
export _HP_EWS_PFXPW
|
||||
|
||||
# $6 is only materialised when Le_PFXPassword is set for this domain, and
|
||||
# it would be built without the CA chain, so build our own bundle instead.
|
||||
_hp_ews_pfx="$(_mktemp)"
|
||||
_hp_ews_body="$(_mktemp)"
|
||||
if [ -z "$_hp_ews_pfx" ] || [ -z "$_hp_ews_body" ]; then
|
||||
_err "Could not create the temporary files needed for the upload."
|
||||
_hp_ews_cleanup
|
||||
return 1
|
||||
fi
|
||||
|
||||
_hp_ews_ret=0
|
||||
if ! _hp_ews_make_pfx; then
|
||||
_hp_ews_ret=1
|
||||
elif ! _hp_ews_upload; then
|
||||
_hp_ews_ret=1
|
||||
fi
|
||||
|
||||
_hp_ews_cleanup
|
||||
|
||||
return "$_hp_ews_ret"
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_hp_ews_cleanup() {
|
||||
[ -n "$_hp_ews_pfx" ] && rm -f "$_hp_ews_pfx"
|
||||
[ -n "$_hp_ews_body" ] && rm -f "$_hp_ews_body"
|
||||
unset _HP_EWS_PFXPW
|
||||
# Do not leak the request header into any hook that runs after this one.
|
||||
export _H1=""
|
||||
return 0
|
||||
}
|
||||
|
||||
# Bundle key, certificate and chain into $_hp_ews_pfx, protected by
|
||||
# $_HP_EWS_PFXPW. The password goes through the environment rather than the
|
||||
# command line so that it does not show up in ps output.
|
||||
_hp_ews_make_pfx() {
|
||||
_info "Building the PKCS#12 bundle"
|
||||
|
||||
if ! ${ACME_OPENSSL_BIN:-openssl} pkcs12 -export \
|
||||
-out "$_hp_ews_pfx" \
|
||||
-inkey "$_ckey" \
|
||||
-in "$_ccert" \
|
||||
-certfile "$_cca" \
|
||||
-passout env:_HP_EWS_PFXPW; then
|
||||
_err "Failed to build the PKCS#12 bundle."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$_hp_ews_pfx" ]; then
|
||||
_err "The PKCS#12 bundle is empty."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Build the multipart body in $_hp_ews_body and POST it to the printer.
|
||||
_hp_ews_upload() {
|
||||
_hp_ews_url="https://${HP_EWS_HOST}/Security/DeviceCertificates/NewCertWithPassword/Upload"
|
||||
_hp_ews_boundary="----------------------------acmesh$(${ACME_OPENSSL_BIN:-openssl} rand -hex 16)"
|
||||
|
||||
{
|
||||
printf -- "--%s\r\n" "$_hp_ews_boundary"
|
||||
printf "Content-Disposition: form-data; name=\"certificate\"; filename=\"%s.pfx\"\r\n" "$_cdomain"
|
||||
printf "Content-Type: application/x-pkcs12\r\n\r\n"
|
||||
cat "$_hp_ews_pfx"
|
||||
printf "\r\n"
|
||||
printf -- "--%s\r\n" "$_hp_ews_boundary"
|
||||
printf "Content-Disposition: form-data; name=\"password\"\r\n\r\n%s\r\n" "$_HP_EWS_PFXPW"
|
||||
printf -- "--%s--\r\n" "$_hp_ews_boundary"
|
||||
} >"$_hp_ews_body"
|
||||
|
||||
_H1="Authorization: Basic $(printf "%s:%s" "$HP_EWS_USERNAME" "$HP_EWS_PASSWORD" | _base64)"
|
||||
export _H1
|
||||
if [ "$HP_EWS_INSECURE" ]; then
|
||||
export HTTPS_INSECURE=1
|
||||
fi
|
||||
|
||||
_info "Uploading the certificate to $_hp_ews_url"
|
||||
if ! _post_file "$_hp_ews_body" "$_hp_ews_url" "" "POST" "multipart/form-data; boundary=$_hp_ews_boundary" >/dev/null; then
|
||||
if [ -z "$HTTPS_INSECURE" ]; then
|
||||
_err "If the printer still serves its self-signed certificate, set HP_EWS_INSECURE=1 and try again."
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The EWS answers 201 Created on a successful install.
|
||||
_hp_ews_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
_debug _hp_ews_code "$_hp_ews_code"
|
||||
|
||||
if [ "$_hp_ews_code" != "201" ] && [ "$_hp_ews_code" != "200" ]; then
|
||||
_err "The printer rejected the certificate (HTTP $_hp_ews_code)."
|
||||
if [ "$_hp_ews_code" = "401" ] || [ "$_hp_ews_code" = "403" ]; then
|
||||
_err "Check HP_EWS_USERNAME and HP_EWS_PASSWORD. Only HTTP Basic authentication is"
|
||||
_err "supported; printers that only offer Digest cannot be used with this hook."
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Certificate installed (HTTP $_hp_ews_code). The printer restarts its web server shortly."
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Deploy hook to deploy certificate and key to nginx proxy manager/NPMPlus using
|
||||
# REST API. Separately uploading the intermediate for nginx proxy manager is
|
||||
# not necessary as the "fullchain" is always used for the certificate.
|
||||
#
|
||||
# Environment variables:
|
||||
#
|
||||
# DEPLOY_NPM_HOST - example.com, 192.168.1.1, etc. (REQUIRED)
|
||||
# Host/IP address of NPM admin panel
|
||||
# DEPLOY_NPM_PORT - 80, 443, 8443, etc. (REQUIRED)
|
||||
# Port number of NPM admin panel
|
||||
# DEPLOY_NPM_PROTOCOL - http/https (REQUIRED)
|
||||
# Protocol to connect to NPM admin panel. https is
|
||||
# STRONGLY recommended
|
||||
# DEPLOY_NPM_CERTNUM - 1, 2, 3, etc. (OPTIONAL)
|
||||
# the ID number of the custom certificate to overwrite.
|
||||
# Find it in the NPM admin panel under certificates.
|
||||
# Must already exist.
|
||||
# DEPLOY_NPM_USER - admin, npmuser, etc. (REQUIRED)
|
||||
# Authorized user for NPM. Must have permission
|
||||
# to manage certificates.
|
||||
# DEPLOY_NPM_PASSWORD - mysecurepassword, mysecretpass, etc. (REQUIRED)
|
||||
# Authorized user's password for NPM 2FA is not supported
|
||||
# since neither nginx proxy manager nor NPMplus support
|
||||
# permanent API tokens. Either disable 2FA for the
|
||||
# administrator account, or (better option) create a new user
|
||||
# with 2FA disabled, and grant this user permissions for
|
||||
# managing certificates.
|
||||
# DEPLOY_NPM_CERTNAME My Custom Certificate, Cool Certificate Name, etc. (OPTIONAL)
|
||||
# Name of custom certificate to create. This is only used
|
||||
# to create a certificate the first time. It is IGNORED
|
||||
# once DEPLOY_NPM_CERTNUM is set. If neither certificate
|
||||
# ID number and name are provided, a default name of
|
||||
# "acme.sh custom certificate" with a timestamp suffix will be
|
||||
# used
|
||||
#
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
nginx_proxy_manager_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"
|
||||
|
||||
_getdeployconf DEPLOY_NPM_HOST
|
||||
_getdeployconf DEPLOY_NPM_PORT
|
||||
_getdeployconf DEPLOY_NPM_PROTOCOL
|
||||
_getdeployconf DEPLOY_NPM_CERTNUM
|
||||
_getdeployconf DEPLOY_NPM_USER
|
||||
_getdeployconf DEPLOY_NPM_PASSWORD
|
||||
_getdeployconf DEPLOY_NPM_CERTNAME
|
||||
|
||||
if [ -z "$DEPLOY_NPM_HOST" ]; then
|
||||
_err "DEPLOY_NPM_HOST must be defined"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$DEPLOY_NPM_PORT" ]; then
|
||||
_err "DEPLOY_NPM_PORT must be defined"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$DEPLOY_NPM_CERTNUM" ]; then
|
||||
_info "DEPLOY_NPM_CERTNUM not provided, a new certificate will be created."
|
||||
if [ -z "$DEPLOY_NPM_CERTNAME" ]; then
|
||||
DEPLOY_NPM_CERTNAME="acme.sh custom certificate - $(_utc_date | tr ' ' '_' | tr -d -- ':')"
|
||||
_info "DEPLOY_NPM_CERTNAME not set. Using default:"
|
||||
_info DEPLOY_NPM_CERTNAME "$DEPLOY_NPM_CERTNAME"
|
||||
fi
|
||||
fi
|
||||
if [ "$DEPLOY_NPM_PROTOCOL" != "https" ] && [ "$DEPLOY_NPM_PROTOCOL" != "http" ]; then
|
||||
_err "DEPLOY_NPM_PROTOCOL must be either http or https"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$DEPLOY_NPM_USER" ]; then
|
||||
_err "DEPLOY_NPM_USER must be defined"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$DEPLOY_NPM_PASSWORD" ]; then
|
||||
_err "DEPLOY_NPM_PASSWORD must be defined"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# extract and return response code
|
||||
_npm_response_code() {
|
||||
_npm_code="$(_egrep_o <"$HTTP_HEADER" "^HTTP[^ ]* .*$" | cut -d " " -f 2-100 | tr -d "\f\n")"
|
||||
printf '%s\n' "$_npm_code" | _egrep_o "^[0-9][0-9]*"
|
||||
}
|
||||
|
||||
# strip backslashes, encode for JSON, and remove newlines
|
||||
_npm_payload_format() {
|
||||
_npm_payload_out="$(printf "%s\n" "$1" | _json_encode)"
|
||||
printf '%s' "${_npm_payload_out%\\n}"
|
||||
}
|
||||
|
||||
_debug DEPLOY_NPM_USER "$DEPLOY_NPM_USER"
|
||||
_secure_debug DEPLOY_NPM_PASSWORD "$DEPLOY_NPM_PASSWORD"
|
||||
_npm_user_json="$(_npm_payload_format "$DEPLOY_NPM_USER")"
|
||||
_npm_password_json="$(_npm_payload_format "$DEPLOY_NPM_PASSWORD")"
|
||||
_debug _npm_user_json "$_npm_user_json"
|
||||
_secure_debug _npm_password_json "$_npm_password_json"
|
||||
|
||||
_info "Authenticating and fetching temporary API token"
|
||||
_npm_token_raw="$(_post '{"identity":"'"$_npm_user_json"'","secret":"'"$_npm_password_json"'"}' "${DEPLOY_NPM_PROTOCOL}://${DEPLOY_NPM_HOST}:${DEPLOY_NPM_PORT}/api/tokens" "" "POST" "application/json")"
|
||||
_secure_debug _npm_token_raw "$_npm_token_raw"
|
||||
_npm_requires2fa="$(printf '%s' "$_npm_token_raw" | _egrep_o '"requires_2fa" *: *[^,}]*' | cut -d : -f 2 | tr -d ' ')"
|
||||
_npm_requiresTotp="$(printf '%s' "$_npm_token_raw" | _egrep_o '"requiresTotp" *: *[^,}]*' | cut -d : -f 2 | tr -d ' ')"
|
||||
if [ "$_npm_requires2fa" = "true" ] || [ "$_npm_requiresTotp" = "true" ]; then
|
||||
_err "2FA/MFA is enabled, deployment is not supported. Disable it or create a user with 2FA/MFA disabled and update your configuration."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_npm_token_code="$(_npm_response_code)"
|
||||
_debug _npm_token_code "$_npm_token_code"
|
||||
if [ "$_npm_token_code" != "200" ]; then
|
||||
_err "Failed to retrieve a token, server response code: $_npm_token_code"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# first look for a token in JSON
|
||||
_npm_type="Nginx Proxy Manager API"
|
||||
_npm_token="$(printf '%s' "$_npm_token_raw" | _egrep_o '"token" *: *"[^"]*"' | cut -d '"' -f 4)"
|
||||
if [ "$_npm_token" = "" ]; then
|
||||
_debug "Token not found in JSON (nginx proxy manager API), checking HTTP header"
|
||||
# look for a token in the header
|
||||
_npm_type="NPMplus API"
|
||||
_npm_token="$(grep <"$HTTP_HEADER" -i "^Set-Cookie: *__Host-Http-token=" | _tail_n 1 | _egrep_o "__Host-Http-token=[^;]*" | _head_n 1 | cut -d'=' -f2-)"
|
||||
if [ "$_npm_token" = "" ]; then
|
||||
_err "Token not found in HTTP header (NPMplus API). Unable to obtain a token."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_info "Detected API type: $_npm_type"
|
||||
_secure_debug _npm_token "$_npm_token"
|
||||
_info "API token retrieved."
|
||||
|
||||
if [ "$_npm_type" = "Nginx Proxy Manager API" ]; then
|
||||
_H1="Authorization: Bearer $_npm_token"
|
||||
elif [ "$_npm_type" = "NPMplus API" ]; then
|
||||
_H1="Cookie: __Host-Http-token=$_npm_token"
|
||||
fi
|
||||
export _H1
|
||||
_secure_debug _H1 "$_H1"
|
||||
|
||||
_npm_nl="\0015\0012"
|
||||
|
||||
# Create new certificate if a certificate ID number was not provided
|
||||
if [ -z "$DEPLOY_NPM_CERTNUM" ]; then
|
||||
_npm_certname_json="$(_npm_payload_format "$DEPLOY_NPM_CERTNAME")"
|
||||
_npm_create_result="$(_post '{"provider":"other","nice_name":"'"$_npm_certname_json"'"}' "$DEPLOY_NPM_PROTOCOL://$DEPLOY_NPM_HOST:$DEPLOY_NPM_PORT/api/nginx/certificates" "" "POST" "application/json")"
|
||||
_debug _npm_create_result "$_npm_create_result"
|
||||
_npm_create_code="$(_npm_response_code)"
|
||||
if [ "$_npm_create_code" != "201" ]; then
|
||||
_err "Failed to create new certificate, server response code: $_npm_create_code"
|
||||
unset _H1
|
||||
return 1
|
||||
fi
|
||||
DEPLOY_NPM_CERTNUM="$(printf '%s' "$_npm_create_result" | _egrep_o '"id" *: *[^,}]*' | cut -d : -f 2 | tr -d ' "')"
|
||||
_info "Created certificate ID number $DEPLOY_NPM_CERTNUM"
|
||||
fi
|
||||
|
||||
_info "Uploading certificates into entry $DEPLOY_NPM_CERTNUM"
|
||||
_npm_form_boundary="--------------------------$(_utc_date | tr -d -- '-: ')"
|
||||
_npm_post_payload="--$_npm_form_boundary${_npm_nl}Content-Disposition: form-data; name=\"certificate_key\"; filename=\"$(basename "$_ckey")\"${_npm_nl}Content-Type: application/octet-stream${_npm_nl}${_npm_nl}$(cat "$_ckey")\0012"
|
||||
_npm_post_payload="$_npm_post_payload${_npm_nl}--$_npm_form_boundary${_npm_nl}Content-Disposition: form-data; name=\"certificate\"; filename=\"$(basename "$_cfullchain")\"${_npm_nl}Content-Type: application/octet-stream${_npm_nl}${_npm_nl}$(cat "$_cfullchain")\0012"
|
||||
_npm_post_payload="$_npm_post_payload${_npm_nl}--$_npm_form_boundary--${_npm_nl}"
|
||||
_npm_post_payload="$(printf "%b_" "$_npm_post_payload")"
|
||||
_npm_post_payload="${_npm_post_payload%_}"
|
||||
_secure_debug _npm_post_payload "$_npm_post_payload"
|
||||
_npm_upload_result="$(_post "$_npm_post_payload" "${DEPLOY_NPM_PROTOCOL}://${DEPLOY_NPM_HOST}:${DEPLOY_NPM_PORT}/api/nginx/certificates/${DEPLOY_NPM_CERTNUM}/upload" "" "POST" "multipart/form-data; boundary=${_npm_form_boundary}")"
|
||||
unset _H1
|
||||
_secure_debug _npm_upload_result "$_npm_upload_result"
|
||||
_npm_upload_code="$(_npm_response_code)"
|
||||
_debug _npm_upload_code "$_npm_upload_code"
|
||||
case "$_npm_upload_code" in
|
||||
"404")
|
||||
_err "Failed to upload certificates, server response code: $_npm_upload_code"
|
||||
_err "Check that certificate number $DEPLOY_NPM_CERTNUM exists."
|
||||
return 1
|
||||
;;
|
||||
"200")
|
||||
_info "Certificate uploaded successfully"
|
||||
;;
|
||||
*)
|
||||
_err "Failed to upload certificates, server response code: $_npm_upload_code"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_info "$(__green "Deployed successfully")"
|
||||
_savedeployconf DEPLOY_NPM_HOST "$DEPLOY_NPM_HOST"
|
||||
_savedeployconf DEPLOY_NPM_PORT "$DEPLOY_NPM_PORT"
|
||||
_savedeployconf DEPLOY_NPM_PROTOCOL "$DEPLOY_NPM_PROTOCOL"
|
||||
_savedeployconf DEPLOY_NPM_CERTNUM "$DEPLOY_NPM_CERTNUM"
|
||||
_savedeployconf DEPLOY_NPM_USER "$DEPLOY_NPM_USER"
|
||||
_savedeployconf DEPLOY_NPM_PASSWORD "$DEPLOY_NPM_PASSWORD" "base64"
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Script to deploy certificates to compatible BMCs (baseboard management
|
||||
# controllers) using the [Redfish API](https://www.dmtf.org/standards/redfish).
|
||||
#
|
||||
# Environment Variables:
|
||||
#
|
||||
# ```sh
|
||||
# DEPLOY_REDFISH_HOST="ipmi.example.com" # Required
|
||||
# DEPLOY_REDFISH_USERNAME="Administrator" # Required
|
||||
# DEPLOY_REDFISH_PASSWORD="superuser" # Required
|
||||
# DEPLOY_REDFISH_USE_BASIC_AUTH=0 # Optional (set to "1" to enable)
|
||||
# DEPLOY_REDFISH_RESTART=0 # Optional (set to "1" to enable)
|
||||
# DEPLOY_REDFISH_MANAGER= # Optional (e.g. "/redfish/v1/Managers/Self")
|
||||
# DEPLOY_REDFISH_CERTIFICATE= # Optional (e.g. "/redfish/v1/CertificateService/Certificates/1")
|
||||
# ```
|
||||
#
|
||||
# Compatibility:
|
||||
#
|
||||
# Compatible with any IPMI/BMC solution supporting Redfish v1.6.1 or newer (or
|
||||
# Redfish Schema Bundle 2018.3), as defined in the following documents:
|
||||
#
|
||||
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP0266_1.6.1.pdf>
|
||||
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2018.3.pdf>
|
||||
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP2059_1.0.0.pdf>
|
||||
#
|
||||
# This script is confirmed to work on the following systems:
|
||||
#
|
||||
# | IPMI Solution | BMC Chipset | Firmware Family | Redfish Version |
|
||||
# |------------------|----------------|------------------|-----------------|
|
||||
# | ASUS ASMB12-iKVM | ASPEED AST2600 | AMI MegaRAC SP-X | 1.15.1 |
|
||||
#
|
||||
# However, this should also work with HPE iLO 5 v2.42 (or newer), Dell iDRAC9
|
||||
# v5.0 (or newer), or any ASUS/Lenovo/Supermicro IPMI solution based on AMI
|
||||
# MegaRAC SP-X v12.x (or newer).
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
redfish_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
_getdeployconf DEPLOY_REDFISH_HOST
|
||||
_getdeployconf DEPLOY_REDFISH_USERNAME
|
||||
_getdeployconf DEPLOY_REDFISH_PASSWORD
|
||||
_getdeployconf DEPLOY_REDFISH_USE_BASIC_AUTH
|
||||
_getdeployconf DEPLOY_REDFISH_RESTART
|
||||
_getdeployconf DEPLOY_REDFISH_MANAGER
|
||||
_getdeployconf DEPLOY_REDFISH_CERTIFICATE
|
||||
|
||||
if [ -z "${DEPLOY_REDFISH_HOST}" ]; then
|
||||
_err 'DEPLOY_REDFISH_HOST must be set. Please specify the IP or domain name of the Redfish server.'
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "${DEPLOY_REDFISH_USERNAME}" ] || [ -z "${DEPLOY_REDFISH_PASSWORD}" ]; then
|
||||
_err 'DEPLOY_REDFISH_USERNAME and DEPLOY_REDFISH_PASSWORD must be set.'
|
||||
return 1
|
||||
fi
|
||||
|
||||
DEPLOY_REDFISH_USE_BASIC_AUTH="${DEPLOY_REDFISH_USE_BASIC_AUTH:-0}"
|
||||
DEPLOY_REDFISH_RESTART="${DEPLOY_REDFISH_RESTART:-0}"
|
||||
|
||||
_debug DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}"
|
||||
_debug DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}"
|
||||
_secure_debug DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}"
|
||||
_debug DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}"
|
||||
|
||||
_redfish_log_in "$DEPLOY_REDFISH_USERNAME" "$DEPLOY_REDFISH_PASSWORD" || return 1
|
||||
|
||||
# Credentials are proven correct now -- save them, rather than only at the
|
||||
# very end, so a later step failing doesn't discard a working login.
|
||||
_savedeployconf DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}"
|
||||
_savedeployconf DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}"
|
||||
_savedeployconf DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}" 'base64'
|
||||
_savedeployconf DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}"
|
||||
|
||||
if ! _redfish_get_certificate_service_endpoint; then
|
||||
_err "Redfish server ${DEPLOY_REDFISH_HOST} doesn't support certificate management API."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}"
|
||||
_debug DEPLOY_REDFISH_MANAGER "${DEPLOY_REDFISH_MANAGER}"
|
||||
_debug DEPLOY_REDFISH_CERTIFICATE "${DEPLOY_REDFISH_CERTIFICATE}"
|
||||
|
||||
if [ -n "${DEPLOY_REDFISH_MANAGER}" ]; then
|
||||
_manager_endpoint="${DEPLOY_REDFISH_MANAGER}"
|
||||
else
|
||||
_info 'Identifying REST endpoint of primary Redfish manager.'
|
||||
_redfish_get_primary_manager_endpoint || return 1
|
||||
_savedeployconf DEPLOY_REDFISH_MANAGER "${_manager_endpoint}"
|
||||
fi
|
||||
|
||||
if [ -n "${DEPLOY_REDFISH_CERTIFICATE}" ]; then
|
||||
_certificate_endpoint="${DEPLOY_REDFISH_CERTIFICATE}"
|
||||
else
|
||||
_info "Identifying REST endpoint of primary TLS certificate on: ${_manager_endpoint}"
|
||||
_redfish_get_manager_certificate_endpoint "${_manager_endpoint}" || return 1
|
||||
fi
|
||||
|
||||
if [ -f "${_ckey}" ] && [ -f "${_cfullchain}" ]; then
|
||||
_info "Uploading private key and full certificate chain to: ${_certificate_endpoint}"
|
||||
|
||||
if ! _redfish_supports_full_certificate_chains; then
|
||||
_err 'Redfish server does not support uploading full certificate chains!'
|
||||
_err "Please call 'GenerateCSR' API on server first, pass the CSR to 'acme.sh --sign-csr --csr <key.pem>', and try again."
|
||||
_err 'For more information: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#generate-csr-on-server-optional'
|
||||
return 1
|
||||
elif ! _redfish_supports_private_key "${_ckey}" "${_certificate_service_endpoint}"; then
|
||||
_err "Redfish server does not support this type of private key."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_ckey_pkcs8="$("${ACME_OPENSSL_BIN:-openssl}" pkcs8 -topk8 -nocrypt -in "${_ckey}")"
|
||||
|
||||
# shellcheck disable=SC2181
|
||||
if [ "$?" != '0' ] || [ -z "${_ckey_pkcs8}" ]; then
|
||||
_err 'Failed to convert private key to PKCS#8 format!'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)"
|
||||
_certificate_type='PEMchain'
|
||||
else
|
||||
_info "Private key and full certificate chain not available; we must be using a server-generated CSR."
|
||||
_info "Uploading only leaf certificate to: ${_certificate_endpoint}"
|
||||
|
||||
_certificate_str="$(_json_encode <"${_ccert}")"
|
||||
_certificate_type='PEM'
|
||||
fi
|
||||
|
||||
_body="$(printf '{"CertificateString":"%s","CertificateType":"%s","CertificateUri":{"@odata.id":"%s"}}' "${_certificate_str}" "${_certificate_type}" "${_certificate_endpoint}")"
|
||||
_redfish_rest POST "${_certificate_service_endpoint}/Actions/CertificateService.ReplaceCertificate" "${_body}" || return 1
|
||||
_code="$(_redfish_response_code)"
|
||||
|
||||
if ! _startswith "${_code}" '2'; then
|
||||
_err "Failed to update Redfish server TLS certificate! (HTTP ${_code})"
|
||||
_err "Response: ${_response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info 'Successfully updated Redfish server TLS certificate!'
|
||||
_savedeployconf DEPLOY_REDFISH_CERTIFICATE "${_certificate_endpoint}"
|
||||
|
||||
if [ "${DEPLOY_REDFISH_RESTART}" = '1' ]; then
|
||||
_info 'Attempting to restart BMC gracefully.'
|
||||
_redfish_attempt_graceful_restart "${_manager_endpoint}" || return 0
|
||||
_info 'Successfully sent graceful restart command to BMC. After restarting, the new TLS certificate will be active.'
|
||||
else
|
||||
_info 'Please wait up to 20 seconds to take effect, or restart the BMC manually.'
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_redfish_rest() {
|
||||
_method="$1"
|
||||
_endpoint="$2"
|
||||
_body="${3:-}"
|
||||
|
||||
export _H1='OData-Version: 4.0'
|
||||
|
||||
if [ "${_method}" = 'GET' ]; then
|
||||
_response="$(_get "https://${DEPLOY_REDFISH_HOST}${_endpoint}")"
|
||||
else
|
||||
_response="$(_post "${_body}" "https://${DEPLOY_REDFISH_HOST}${_endpoint}" '' "${_method}" "${_body:+application/json}")"
|
||||
fi
|
||||
|
||||
_ret="$?"
|
||||
_response="$(echo "${_response}" | _normalizeJson)"
|
||||
|
||||
if [ "${_ret}" != '0' ]; then
|
||||
_err "Error while calling ${_method} ${_endpoint}. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
|
||||
fi
|
||||
|
||||
return "${_ret}"
|
||||
}
|
||||
|
||||
_redfish_response_code() {
|
||||
_egrep_o <"${HTTP_HEADER}" '^HTTP[^ ]* [0-9]+' | _tail_n 1 | tr -d '\r\n' | cut -d ' ' -f 2
|
||||
}
|
||||
|
||||
_redfish_get_odata_id() {
|
||||
_regex='s/{'
|
||||
for key in $(echo "$1" | tr '.' '\n'); do
|
||||
_regex="${_regex}.*\"${key}\":{"
|
||||
done
|
||||
sed -n "${_regex}[^}]*\"@odata\\.id\":[ ]*\"\\([^\"]*\\)\".*/\\1/p"
|
||||
}
|
||||
|
||||
_redfish_get_odata_members() {
|
||||
sed -n 's/.*"Members":[ ]*\[\([^]]*\)\].*/\1/p' | sed 's/,/\n/g' | sed -n 's/.*{"@odata\.id":"\([^"]*\)"}.*/\1/p'
|
||||
}
|
||||
|
||||
_redfish_get_odata_count() {
|
||||
sed -n 's/.*"Members@odata\.count":[ ]*\([0-9][0-9]*\).*/\1/p'
|
||||
}
|
||||
|
||||
_redfish_log_in() {
|
||||
_username="$1"
|
||||
_password="$2"
|
||||
|
||||
_redfish_rest GET '/redfish/v1/' || return 1
|
||||
_info "Authenticating with Redfish API: ${DEPLOY_REDFISH_HOST}"
|
||||
|
||||
if [ "${DEPLOY_REDFISH_USE_BASIC_AUTH}" = '1' ]; then
|
||||
_access_token="$(printf '%s:%s' "${_username}" "${_password}" | _base64)"
|
||||
export _H2="Authorization: Basic ${_access_token}"
|
||||
|
||||
# Verify credentials are valid
|
||||
_account_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'AccountService')"
|
||||
_redfish_rest GET "${_account_service_endpoint}" || return 1
|
||||
_code="$(_redfish_response_code)"
|
||||
|
||||
if [ "${_code}" != '200' ]; then
|
||||
_err "Redfish authentication failed (HTTP ${_code})"
|
||||
_err "Response: ${_response}"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Create new session
|
||||
_sessions_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Links.Sessions')"
|
||||
_username="$(printf '%s\n' "${_username}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
|
||||
_password="$(printf '%s\n' "${_password}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
|
||||
_body="$(printf '{"UserName":"%s","Password":"%s"}' "${_username}" "${_password}")"
|
||||
_redfish_rest POST "${_sessions_endpoint}" "${_body}" || return 1
|
||||
_code="$(_redfish_response_code)"
|
||||
|
||||
# Verify authentication succeeded
|
||||
if [ "${_code}" != '201' ]; then
|
||||
_err "Redfish authentication failed (HTTP ${_code})"
|
||||
_err "Response: ${_response}"
|
||||
if [ "${_code}" = '401' ] && grep -qi '^WWW-Authenticate: Basic.*$' "${HTTP_HEADER}"; then
|
||||
_err 'NOTE: Server supports Basic auth scheme. Consider setting DEPLOY_REDFISH_USE_BASIC_AUTH=1 and try again.'
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
_session="$(echo "${_response}" | _redfish_get_odata_id '')"
|
||||
_auth_token="$(grep -i '^X-Auth-Token: .*$' "${HTTP_HEADER}" | _tail_n 1 | tr -d ' \r\n' | cut -d ':' -f 2)"
|
||||
export _H2="X-Auth-Token: ${_auth_token}"
|
||||
fi
|
||||
|
||||
trap '_redfish_log_out "${_session:-}"' EXIT INT
|
||||
}
|
||||
|
||||
_redfish_log_out() {
|
||||
_session_endpoint="${1:-}"
|
||||
|
||||
_info "Logging out of Redfish session on: ${DEPLOY_REDFISH_HOST}"
|
||||
|
||||
if [ -n "${_session_endpoint}" ]; then
|
||||
_redfish_rest DELETE "${_session_endpoint}" || return 1
|
||||
_code="$(_redfish_response_code)"
|
||||
|
||||
if ! _startswith "${_code}" '2'; then
|
||||
_err "Failed to log out of Redfish server (HTTP ${_code})."
|
||||
_err "Response: ${_response}"
|
||||
fi
|
||||
else
|
||||
_info 'Using basic HTTP auth, no need to log out of Redfish API.'
|
||||
fi
|
||||
|
||||
export _H1=
|
||||
export _H2=
|
||||
}
|
||||
|
||||
_redfish_get_certificate_service_endpoint() {
|
||||
_redfish_rest GET '/redfish/v1/' || return 1
|
||||
_certificate_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'CertificateService')"
|
||||
[ -n "${_certificate_service_endpoint}" ]
|
||||
}
|
||||
|
||||
_redfish_supports_full_certificate_chains() {
|
||||
_redfish_rest GET "/redfish/v1/\$metadata" || return 1
|
||||
|
||||
_newest_certificate_schema_version="$(echo "${_response}" |
|
||||
_egrep_o 'Namespace="Certificate\.v[_0-9][_0-9]*"' |
|
||||
sed -n 's/.*v\([_0-9][_0-9]*\)".*/\1/p' |
|
||||
sort -n -t _ -k1,1 -k2,2 -k3,3 | _tail_n 1 | tr '_' '.')"
|
||||
_debug _newest_certificate_schema_version "${_newest_certificate_schema_version}"
|
||||
|
||||
_schema_major=${_newest_certificate_schema_version%%.*}
|
||||
_schema_patch_minor=${_newest_certificate_schema_version#*.}
|
||||
_schema_minor=${_schema_patch_minor%%.*}
|
||||
_schema_patch=${_schema_patch_minor#*.}
|
||||
|
||||
for var in _schema_major _schema_minor _schema_patch; do
|
||||
_debug "$var" "$(eval echo "\$$var")"
|
||||
case "$(eval echo "\$$var")" in
|
||||
'' | *[!0-9]*) return 1 ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ "${_schema_major}" -gt '1' ] || { [ "${_schema_major}" -ge '1' ] && [ "${_schema_minor}" -ge '4' ]; }
|
||||
}
|
||||
|
||||
_redfish_get_action() {
|
||||
unset _action _action_info
|
||||
|
||||
_redfish_rest GET "$1" || return 1
|
||||
_action="$(echo "${_response}" | sed -n "s/{.*\"Actions\":{.*\"#$2\":\({[^}]*}\).*/\1/p")"
|
||||
_action_info_endpoint="$(echo "${_action}" | sed -n 's/.*"@Redfish\.ActionInfo":"\([^"]*\)".*/\1/p')"
|
||||
|
||||
if [ -n "${_action_info_endpoint}" ]; then
|
||||
_redfish_rest GET "${_action_info_endpoint}" || return 1
|
||||
_code="$(_redfish_response_code)"
|
||||
|
||||
if [ "${_code}" = '200' ]; then
|
||||
_action_info="${_response}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_redfish_get_action_allowable_values() {
|
||||
_parameter="$1"
|
||||
|
||||
_allowable_values="$(echo "${_action}" |
|
||||
sed -n "s/.*\"${_parameter}@Redfish\.AllowableValues\":[ ]*\[\([^]]*\)\].*/\1/p" |
|
||||
tr -d '"')"
|
||||
|
||||
if [ -n "${_allowable_values}" ]; then
|
||||
_debug2 "Found allowable values for '${_parameter}' in '@Redfish.Action': [${_allowable_values}]"
|
||||
echo "${_allowable_values}"
|
||||
elif [ -n "${_action_info}" ]; then
|
||||
_parameter_info="$(echo "${_action_info}" |
|
||||
sed -n 's/.*"Parameters":[ ]*\[\(\({[^}]*},\{0,1\}\)*\)\].*/\1/p' | _egrep_o '\{[^\}]{1,}\}' |
|
||||
grep "\"Name\":\"${_parameter}\"")"
|
||||
|
||||
if [ -n "${_parameter_info}" ]; then
|
||||
_debug2 "Found allowable values for '${_parameter}' in @Redfish.ActionInfo: ${_parameter_info}"
|
||||
_data_type="$(echo "${_parameter_info}" | sed -n 's/.*"DataType":[ ]*"\([^"]*\)".*/\1/p')"
|
||||
|
||||
case "${_data_type}" in
|
||||
String)
|
||||
echo "${_parameter_info}" | sed -n 's/.*"AllowableValues":[ ]*\[\([^]]*\)\].*/\1/p' | tr -d '"'
|
||||
;;
|
||||
Number)
|
||||
_min_value="$(echo "${_parameter_info}" | sed -n 's/.*"MinimumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')"
|
||||
_max_value="$(echo "${_parameter_info}" | sed -n 's/.*"MaximumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')"
|
||||
echo "${_min_value},${_max_value}"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
_redfish_supports_private_key() {
|
||||
_private_key="$1"
|
||||
_certificate_service_endpoint="$2"
|
||||
|
||||
if _isRSA "${_private_key}"; then
|
||||
_key_algo='RSA'
|
||||
elif _isEcc "${_private_key}"; then
|
||||
_key_algo='ECDSA'
|
||||
else
|
||||
_err 'Private key uses unknown cryptographic algorithm!'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_redfish_get_action "${_certificate_service_endpoint}" 'CertificateService.GenerateCSR' || return 1
|
||||
_allowed_key_algos="$(_redfish_get_action_allowable_values 'KeyPairAlgorithm')"
|
||||
_allowed_key_bit_lengths="$(_redfish_get_action_allowable_values 'KeyBitLength')"
|
||||
_allowed_key_curve_ids="$(_redfish_get_action_allowable_values 'KeyCurveId')"
|
||||
|
||||
if [ -z "${_allowed_key_algos}" ]; then
|
||||
_contains "${_response}" '"KeyBitLength"' && _allowed_key_algos='TPM_ALG_RSA'
|
||||
_contains "${_response}" '"KeyCurveId"' && _allowed_key_algos="${_allowed_key_algos},TPM_ALG_ECDSA"
|
||||
_allowed_key_algos="${_allowed_key_algos#,}"
|
||||
|
||||
if [ -z "${_allowed_key_algos}" ]; then
|
||||
_debug 'Failed to determine server crypto requirements, proceeding blindly.'
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug "GenerateCSR does not specify allowed KeyPairAlgorithm(s), so assuming ${_allowed_key_algos}."
|
||||
fi
|
||||
|
||||
_debug _key_algo "${_key_algo}"
|
||||
_debug _allowed_key_algos "${_allowed_key_algos}"
|
||||
|
||||
case "${_key_algo}:${_allowed_key_algos}" in
|
||||
RSA:*RSA*)
|
||||
_min_key_length="$(echo "${_allowed_key_bit_lengths}" | cut -d ',' -f 1)"
|
||||
_max_key_length="${_allowed_key_bit_lengths##*,}"
|
||||
_debug _min_key_length "${_min_key_length}"
|
||||
_debug _max_key_length "${_max_key_length}"
|
||||
|
||||
if [ -z "${_min_key_length}" ] && [ -z "${_max_key_length}" ]; then
|
||||
_debug "Server supports RSA private keys but doesn't specify minimum/maximum bit length, proceeding blindly."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||
if [ "${Le_Keylength}" -lt "${_min_key_length:-${Le_Keylength}}" ] || [ "${Le_Keylength}" -gt "${_max_key_length:-${Le_Keylength}}" ]; then
|
||||
_err "Unsupported RSA private key length ${Le_Keylength}!"
|
||||
_err "Please re-run acme.sh with --keylength set to a value between ${_min_key_length} and ${_max_key_length}."
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
ECDSA:*ECDSA*)
|
||||
_debug _allowed_key_curve_ids "${_allowed_key_curve_ids}"
|
||||
|
||||
if [ -z "${_allowed_key_curve_ids}" ]; then
|
||||
_debug "Server supports ECDSA private keys but doesn't specify which curves, proceeding blindly."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||
if ! _contains "${_allowed_key_curve_ids}" "TPM_ECC_NIST_P${Le_Keylength#ec-}"; then
|
||||
_err "Unsupported ECDSA private key type! Server supports only: ${_allowed_key_curve_ids}"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
_err "Unsupported key pair algorithm ${_key_algo}! Server supports only: ${_allowed_key_algos}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_redfish_get_primary_manager_endpoint() {
|
||||
_redfish_rest GET '/redfish/v1/' || return 1
|
||||
_managers_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Managers')"
|
||||
_redfish_rest GET "${_managers_endpoint}" || return 1
|
||||
_num_managers="$(echo "${_response}" | _redfish_get_odata_count)"
|
||||
|
||||
if [ "${_num_managers}" != '1' ]; then
|
||||
_all_managers="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')"
|
||||
_err "Multiple Redfish managers identified (${_all_managers}), but expected exactly one."
|
||||
_err 'Please specify the correct manager in DEPLOY_REDFISH_MANAGER.'
|
||||
return 1
|
||||
fi
|
||||
|
||||
_manager_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)"
|
||||
[ -n "${_manager_endpoint}" ]
|
||||
}
|
||||
|
||||
_redfish_get_manager_certificate_endpoint() {
|
||||
_manager_endpoint="$1"
|
||||
|
||||
_redfish_rest GET "${_manager_endpoint}" || return 1
|
||||
_protocol_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'NetworkProtocol')"
|
||||
_redfish_rest GET "${_protocol_endpoint}/HTTPS/Certificates" || return 1
|
||||
_num_certificates="$(echo "${_response}" | _redfish_get_odata_count)"
|
||||
|
||||
if [ -z "${_num_certificates}" ]; then
|
||||
_certificate_endpoint="${_protocol_endpoint}/HTTPS/Certificates/1"
|
||||
return 0
|
||||
elif [ "${_num_certificates}" != '1' ]; then
|
||||
_all_certificates="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')"
|
||||
_err "Multiple web service HTTPS certificates identified (${_all_certificates}), but expected exactly one."
|
||||
_err "Please specify which certificate should be replaced in DEPLOY_REDFISH_CERTIFICATE."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_certificate_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)"
|
||||
[ -n "${_certificate_endpoint}" ]
|
||||
}
|
||||
|
||||
_redfish_attempt_graceful_restart() {
|
||||
_manager_endpoint="$1"
|
||||
|
||||
_redfish_get_action "${_manager_endpoint}" 'Manager.Reset' || return 1
|
||||
_allowed_reset_types="$(_redfish_get_action_allowable_values 'ResetType')"
|
||||
|
||||
if ! _contains "${_allowed_reset_types}" 'GracefulRestart'; then
|
||||
_err "BMC doesn't support graceful restarts. Please wait up to 20 seconds to take effect, or restart the BMC manually."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_redfish_rest POST "${_manager_endpoint}/Actions/Manager.Reset" '{"ResetType":"GracefulRestart"}' || return 1
|
||||
_code="$(_redfish_response_code)"
|
||||
|
||||
if ! _startswith "${_code}" '2'; then
|
||||
_err "Failed to gracefully restart BMC! (HTTP ${_code})"
|
||||
_err "Response: ${_response}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
+28
-6
@@ -4,18 +4,29 @@
|
||||
# Deploy the HTTPS server certificate to a Shelly device on the local network.
|
||||
#
|
||||
# ```sh
|
||||
# export SHELLY_HOST=192.168.1.100
|
||||
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
|
||||
# export SHELLY_HOST=shelly.example.com
|
||||
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
|
||||
# export SHELLY_SCHEME=https # only if devices factory shipped with
|
||||
# # firmware 2.0.0 or already has https
|
||||
# # enabled
|
||||
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
|
||||
# ```
|
||||
#
|
||||
# If `SHELLY_SCHEME=https` is used, the first deploy is likely to require
|
||||
# the additional `--insecure` parameter due to factory signed certificates.
|
||||
# After the initial deployment make sure to retest deployment without
|
||||
# `--insecure` simply using `acme.sh --deploy -d shelly.example.com`
|
||||
# to ensure follow up deployment will work.
|
||||
#
|
||||
# Environment variables:
|
||||
# SHELLY_HOST (required) IP or hostname of the Shelly device
|
||||
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
|
||||
# Omit if auth is disabled on the device.
|
||||
# SHELLY_USER (optional) Username for auth. Default: admin
|
||||
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
|
||||
# SHELLY_REBOOT (optional) Set to "0" to skip auto-reboot.
|
||||
# Default: 1 (reboot after upload)
|
||||
# SHELLY_SCHEME (optional) Scheme used to call the API: http or https
|
||||
# Default: http
|
||||
#
|
||||
# Requirements:
|
||||
# - Shelly Gen3+ device (Gen4 recommended)
|
||||
@@ -23,7 +34,7 @@
|
||||
# - curl or wget
|
||||
# - openssl (for SHA-256 digest and random cnonce)
|
||||
#
|
||||
# The device must be reachable via HTTP on the local network.
|
||||
# The device must be reachable on the local network.
|
||||
# The hook uploads the fullchain.pem and private key,
|
||||
# then reboots the device to apply the new certificate.
|
||||
#
|
||||
@@ -52,11 +63,13 @@ shelly_deploy() {
|
||||
_getdeployconf SHELLY_PASSWORD
|
||||
_getdeployconf SHELLY_USER
|
||||
_getdeployconf SHELLY_REBOOT
|
||||
_getdeployconf SHELLY_SCHEME
|
||||
|
||||
_debug SHELLY_HOST "$SHELLY_HOST"
|
||||
_debug SHELLY_USER "$SHELLY_USER"
|
||||
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
||||
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
|
||||
_debug SHELLY_SCHEME "$SHELLY_SCHEME"
|
||||
|
||||
if [ -z "$SHELLY_HOST" ]; then
|
||||
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
|
||||
@@ -65,11 +78,20 @@ shelly_deploy() {
|
||||
|
||||
SHELLY_USER="${SHELLY_USER:-admin}"
|
||||
SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
|
||||
SHELLY_SCHEME="${SHELLY_SCHEME:-http}"
|
||||
case "$SHELLY_SCHEME" in
|
||||
http | https) ;;
|
||||
*)
|
||||
_err "SHELLY_SCHEME must be http or https"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_savedeployconf SHELLY_HOST "$SHELLY_HOST"
|
||||
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
||||
_savedeployconf SHELLY_USER "$SHELLY_USER"
|
||||
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
|
||||
_savedeployconf SHELLY_SCHEME "$SHELLY_SCHEME"
|
||||
|
||||
# --- Auth handshake (only if password is set) ---
|
||||
_shelly_auth_header=""
|
||||
@@ -126,7 +148,7 @@ _shelly_handshake() {
|
||||
# Use a protected method (Shelly.GetStatus) to trigger 401.
|
||||
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
|
||||
_post '{"id":1,"method":"Shelly.GetStatus"}' \
|
||||
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
"${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
|
||||
# Detect auth from HTTP status line rather than response body
|
||||
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
|
||||
@@ -210,7 +232,7 @@ _shelly_rpc() {
|
||||
export _H1=""
|
||||
fi
|
||||
|
||||
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
_post "$_shelly_body" "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
_shelly_ret=$?
|
||||
|
||||
if [ "$_shelly_ret" != "0" ]; then
|
||||
|
||||
+40
-14
@@ -189,6 +189,19 @@ ssh_deploy() {
|
||||
_info "Required commands batched and sent in single call to remote host"
|
||||
fi
|
||||
|
||||
# With a POSIX remote shell, steps are joined with && so that the first
|
||||
# failed step stops the rest (no REMOTE_CMD after a failed write). Other
|
||||
# shells ("powershell -c", "cmd /c") keep the plain ";" join.
|
||||
_ssh_shell_name="${DEPLOY_SSH_REMOTE_SHELL%% *}"
|
||||
case "${_ssh_shell_name##*/}" in
|
||||
sh | bash | dash | ash | ksh | mksh | zsh)
|
||||
_ssh_fail_fast="yes"
|
||||
;;
|
||||
*)
|
||||
_ssh_fail_fast="no"
|
||||
;;
|
||||
esac
|
||||
|
||||
_returnCode=0
|
||||
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
|
||||
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
|
||||
@@ -228,9 +241,9 @@ _ssh_deploy() {
|
||||
_backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
|
||||
# run cleanup on the backup directory, erase all older
|
||||
# than 180 days (15552000 seconds).
|
||||
_cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
|
||||
_ssh_add_step "now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
|
||||
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
|
||||
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
|
||||
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done"
|
||||
# Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
|
||||
_info "Backup of old certificate files will be placed in remote directory $_backupdir"
|
||||
_info "Backup directories erased after 180 days."
|
||||
@@ -245,7 +258,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
|
||||
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||
# backup file we are about to overwrite.
|
||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi;"
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
return $_err_code
|
||||
@@ -261,10 +274,9 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
return $_err_code
|
||||
fi
|
||||
else
|
||||
# If file doesn't exist, create it and change its permissions.
|
||||
_cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;"
|
||||
# ssh echo to the file
|
||||
_cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;"
|
||||
# If file doesn't exist, create it and change its permissions, then
|
||||
# ssh echo to the file. One step: "test ! -f" fails if the file exists.
|
||||
_ssh_add_step "test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE; echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE"
|
||||
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
@@ -282,7 +294,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
_pipe=">>"
|
||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||
# backup file we are about to overwrite.
|
||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi;"
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
return $_err_code
|
||||
@@ -304,7 +316,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
else
|
||||
# ssh echo to the file
|
||||
_cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;"
|
||||
_ssh_add_step "echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE"
|
||||
_info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
@@ -323,7 +335,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
_pipe=">>"
|
||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||
# backup file we are about to overwrite.
|
||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi;"
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
return $_err_code
|
||||
@@ -348,7 +360,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
else
|
||||
# ssh echo to the file
|
||||
_cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;"
|
||||
_ssh_add_step "echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE"
|
||||
_info "will copy CA file to remote file $DEPLOY_SSH_CAFILE"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
@@ -368,7 +380,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
_pipe=">>"
|
||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||
# backup file we are about to overwrite.
|
||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi;"
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
return $_err_code
|
||||
@@ -396,7 +408,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
else
|
||||
# ssh echo to the file
|
||||
_cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;"
|
||||
_ssh_add_step "echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN"
|
||||
_info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
@@ -419,7 +431,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
|
||||
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
|
||||
_cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;"
|
||||
_ssh_add_step "$DEPLOY_SSH_REMOTE_CMD"
|
||||
_info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
@@ -439,6 +451,20 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
return 0
|
||||
}
|
||||
|
||||
#step
|
||||
_ssh_add_step() {
|
||||
if [ "$_ssh_fail_fast" = "yes" ]; then
|
||||
if [ -n "$_cmdstr" ]; then
|
||||
_cmdstr="$_cmdstr && "
|
||||
fi
|
||||
# the newline ends the step even if it ends with a comment, ";" or "&"
|
||||
_cmdstr="$_cmdstr{ $1
|
||||
}"
|
||||
else
|
||||
_cmdstr="$_cmdstr $1;"
|
||||
fi
|
||||
}
|
||||
|
||||
#cmd
|
||||
_ssh_remote_cmd() {
|
||||
_cmd="$1"
|
||||
|
||||
+34
-7
@@ -34,6 +34,8 @@
|
||||
# interactive input the device name
|
||||
# - export SYNO_DEVICE_ID="" - (deprecated, auth with OTP code instead)
|
||||
# required for omitting 2FA-OTP
|
||||
# - export SYNO_USE_TOKEN=0 - disable SynoToken authentication
|
||||
# when deploying to Synology SRM
|
||||
# 3. Run command:
|
||||
# acme.sh --deploy --deploy-hook synology_dsm -d example.com
|
||||
################################################################################
|
||||
@@ -150,6 +152,20 @@ synology_dsm_deploy() {
|
||||
encoded_username="$(printf "%s" "$SYNO_USERNAME" | _url_encode)"
|
||||
encoded_password="$(printf "%s" "$SYNO_PASSWORD" | _url_encode)"
|
||||
|
||||
# Configure SynoToken authentication (DSM only)
|
||||
_getdeployconf SYNO_USE_TOKEN
|
||||
_check2cleardeployconfexp SYNO_USE_TOKEN
|
||||
_savedeployconf SYNO_USE_TOKEN "$SYNO_USE_TOKEN"
|
||||
_syno_use_token=1
|
||||
[ "$SYNO_USE_TOKEN" = "0" ] && _syno_use_token=""
|
||||
|
||||
if [ -n "$_syno_use_token" ]; then
|
||||
enable_token_param="&enable_syno_token=yes"
|
||||
else
|
||||
enable_token_param=""
|
||||
fi
|
||||
_debug enable_token_param "$enable_token_param"
|
||||
|
||||
# ## START ## - DEPRECATED, for backward compatibility
|
||||
_getdeployconf SYNO_TOTP_SECRET
|
||||
|
||||
@@ -173,7 +189,7 @@ synology_dsm_deploy() {
|
||||
_debug3 H1 "${_H1}"
|
||||
fi
|
||||
|
||||
response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version&enable_syno_token=yes&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DEVICE_ID" "$_base_url/webapi/$api_path?enable_syno_token=yes")
|
||||
response=$(_post "method=login&account=$encoded_username&passwd=$encoded_password&api=SYNO.API.Auth&version=$api_version$enable_token_param&otp_code=$DEPRECATED_otp_code&device_name=certrenewal&device_id=$SYNO_DEVICE_ID" "$_base_url/webapi/$api_path${enable_token_param:+?enable_syno_token=yes}")
|
||||
_debug3 response "$response"
|
||||
# ## END ## - DEPRECATED, for backward compatibility
|
||||
# If SYNO_DEVICE_ID or SYNO_OTP_CODE is set, we treat current account enabled 2FA-OTP.
|
||||
@@ -225,7 +241,7 @@ synology_dsm_deploy() {
|
||||
otp_enforce_option=""
|
||||
fi
|
||||
fi
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes")
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password$enable_token_param")
|
||||
if [ -n "$SYNO_USE_TEMP_ADMIN" ] && [ -n "$otp_enforce_option" ]; then
|
||||
synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option"
|
||||
_info "Restored previous enforce 2FA-OTP option."
|
||||
@@ -247,7 +263,7 @@ synology_dsm_deploy() {
|
||||
|
||||
if [ -n "$SYNO_DEVICE_ID" ]; then
|
||||
# Omit OTP code with SYNO_DEVICE_ID.
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&device_name=$SYNO_DEVICE_NAME&device_id=$SYNO_DEVICE_ID")
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password$enable_token_param&device_name=$SYNO_DEVICE_NAME&device_id=$SYNO_DEVICE_ID")
|
||||
_secure_debug3 response "$response"
|
||||
else
|
||||
# Require the OTP code if still unset.
|
||||
@@ -260,7 +276,7 @@ synology_dsm_deploy() {
|
||||
if [ -z "$SYNO_OTP_CODE" ]; then
|
||||
response='{"error":{"code":404}}'
|
||||
else
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password&enable_syno_token=yes&enable_device_token=yes&device_name=$SYNO_DEVICE_NAME&otp_code=$SYNO_OTP_CODE")
|
||||
response=$(_get "$_base_url/webapi/$api_path?api=SYNO.API.Auth&version=$api_version&method=login&format=sid&account=$encoded_username&passwd=$encoded_password$enable_token_param&enable_device_token=yes&device_name=$SYNO_DEVICE_NAME&otp_code=$SYNO_OTP_CODE")
|
||||
_secure_debug3 response "$response"
|
||||
|
||||
id_property='device_id'
|
||||
@@ -302,14 +318,18 @@ synology_dsm_deploy() {
|
||||
token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p')
|
||||
_debug "Session ID" "$sid"
|
||||
_debug SynoToken "$token"
|
||||
if [ -z "$sid" ] || [ -z "$token" ]; then
|
||||
if [ -z "$sid" ] || { [ -n "$_syno_use_token" ] && [ -z "$token" ]; }; then
|
||||
# Still can't get necessary info even got no errors, may Synology have API updated?
|
||||
_err "Unable to authenticate to $_base_url, you may report this by providing full log with '--debug 3'."
|
||||
_temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_H1="X-SYNO-TOKEN: $token"
|
||||
if [ -n "$_syno_use_token" ]; then
|
||||
_H1="X-SYNO-TOKEN: $token"
|
||||
else
|
||||
_H1=""
|
||||
fi
|
||||
export _H1
|
||||
_debug2 H1 "${_H1}"
|
||||
|
||||
@@ -381,8 +401,15 @@ synology_dsm_deploy() {
|
||||
content="$(printf "%b_" "$content")"
|
||||
content="${content%_}" # protect trailing \n
|
||||
|
||||
if [ -n "$_syno_use_token" ]; then
|
||||
token_param="&SynoToken=$token"
|
||||
else
|
||||
token_param=""
|
||||
fi
|
||||
_debug token_param "$token_param"
|
||||
|
||||
_info "Upload certificate to the Synology DSM."
|
||||
response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1&SynoToken=$token&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}")
|
||||
response=$(_post "$content" "$_base_url/webapi/entry.cgi?api=SYNO.Core.Certificate&method=import&version=1$token_param&_sid=$sid" "" "POST" "multipart/form-data; boundary=${delim}")
|
||||
_debug3 response "$response"
|
||||
|
||||
if ! echo "$response" | grep '"error":' >/dev/null; then
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_abion_info='Abion.com
|
||||
Site: abion.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_abion
|
||||
Options:
|
||||
ABION_API_KEY API Key. Created in the Abion Core web interface.
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7325
|
||||
'
|
||||
|
||||
ABION_Api_Endpoint="https://api.abion.com/v1"
|
||||
|
||||
##################### Public functions #####################
|
||||
|
||||
# Usage: dns_abion_add <domain> <txt record>
|
||||
# Example: dns_abion_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_abion_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}"
|
||||
|
||||
if [ -z "$ABION_API_KEY" ]; then
|
||||
ABION_API_KEY=""
|
||||
_err "You need to specify ABION_API_KEY."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable ABION_API_KEY "$ABION_API_KEY"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _abion_get_txt_records; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then
|
||||
_info "TXT record already exists"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_records=$(echo "$_txt_records" | tr '\n' ',' | sed 's/,*$//')
|
||||
if [ -n "$_records" ]; then
|
||||
_records="$_records,"
|
||||
fi
|
||||
|
||||
_info "Adding record"
|
||||
if ! _abion_patch_txt "[$_records{\"rdata\":\"$txtvalue\",\"ttl\":60}]"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: dns_abion_rm <domain> <txt record>
|
||||
dns_abion_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue=$2
|
||||
|
||||
ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}"
|
||||
|
||||
if [ -z "$ABION_API_KEY" ]; then
|
||||
ABION_API_KEY=""
|
||||
_err "You need to specify ABION_API_KEY."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
if ! _abion_get_txt_records; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then
|
||||
_info "TXT record not found, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_records=$(echo "$_txt_records" | grep -v "\"rdata\":\"$txtvalue\"" | tr '\n' ',' | sed 's/,*$//')
|
||||
|
||||
if [ -n "$_records" ]; then
|
||||
_records="[$_records]"
|
||||
else
|
||||
_records="null"
|
||||
fi
|
||||
|
||||
_info "Deleting record"
|
||||
if ! _abion_patch_txt "$_records"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Deleted, OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
##################### Private functions #####################
|
||||
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=0
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _abion_rest GET "zones/$h"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" '"type" *: *"zone"'; then
|
||||
if [ "$p" = "0" ]; then
|
||||
_sub_domain="@"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
fi
|
||||
_domain="$h"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! _contains "$response" "Zone not found"; then
|
||||
_err "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
p="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_abion_get_txt_records() {
|
||||
if ! _abion_rest GET "zones/$_domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_sub_re=$(printf "%s" "$_sub_domain" | sed 's/[.]/\\./g')
|
||||
_txt_records=$(echo "$response" | _normalizeJson |
|
||||
sed -n "s/.*\"$_sub_re\":{//p" |
|
||||
sed 's/\] *}.*/]/' |
|
||||
_egrep_o '"TXT":\[[^]]*\]' |
|
||||
sed 's/^"TXT":\[ *//;s/ *\]$//;s/} *, *{/}\
|
||||
{/g')
|
||||
_debug2 _txt_records "$_txt_records"
|
||||
return 0
|
||||
}
|
||||
|
||||
_abion_patch_txt() {
|
||||
if ! _abion_rest PATCH "zones/$_domain" "{\"data\":{\"type\":\"zone\",\"id\":\"$_domain\",\"attributes\":{\"records\":{\"$_sub_domain\":{\"TXT\":$1}}}}}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _contains "$response" '"type" *: *"zone"'; then
|
||||
_err "Unexpected API response: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_abion_rest() {
|
||||
method=$1
|
||||
endpoint=$2
|
||||
data=$3
|
||||
|
||||
export _H1="X-API-KEY: $ABION_API_KEY"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
if [ "$method" = "GET" ]; then
|
||||
response="$(_get "$ABION_Api_Endpoint/$endpoint")"
|
||||
else
|
||||
response="$(_post "$data" "$ABION_Api_Endpoint/$endpoint" "" "$method")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $endpoint"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug2 response "$response"
|
||||
return 0
|
||||
}
|
||||
+1
-1
@@ -120,7 +120,7 @@ _baidu_run_with_fallback() {
|
||||
}
|
||||
|
||||
_baidu_engine_order() {
|
||||
_pref="$(_lower_case "$(_baidu_trim_ws "${Baidu_API_Preference:-auto}")")"
|
||||
_pref="$(_baidu_trim_ws "${Baidu_API_Preference:-auto}" | _lower_case)"
|
||||
case "$_pref" in
|
||||
legacy)
|
||||
printf "%s" "legacy new"
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_bergdns_info='bergdns.at
|
||||
Site: bergdns.at
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_bergdns
|
||||
Options:
|
||||
BERGDNS_API_KEY API key, as issued in the account UI. Needs read (to find the zone and the record) and write over the challenge names.
|
||||
BERGDNS_API_URL API base URL. Optional. Default "https://bergdns.at/v1".
|
||||
BERGDNS_TTL TTL of the challenge record, in seconds. Optional. Default "60".
|
||||
BERGDNS_PROPAGATION_TIMEOUT Seconds to wait for the record to reach every secondary. Optional. Default "60". "0" does not wait.
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7261
|
||||
Author: Kenny Kropp <https://github.com/kekropp>
|
||||
'
|
||||
|
||||
_BERGDNS_DEFAULT_URL='https://bergdns.at/v1'
|
||||
_BERGDNS_DEFAULT_TTL='60'
|
||||
_BERGDNS_DEFAULT_WAIT='60'
|
||||
|
||||
######## Public functions ####################################################
|
||||
|
||||
# Usage: dns_bergdns_add _acme-challenge.www.example.com "token"
|
||||
# The value is added to the RRset, so a domain and its wildcard can be
|
||||
# validated at the same time.
|
||||
dns_bergdns_add() {
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
txtvalue=$2
|
||||
|
||||
_bergdns_init || return 1
|
||||
_bergdns_find_zone "$fulldomain" || return 1
|
||||
_bergdns_find_rrset "$fulldomain" || return 1
|
||||
|
||||
_info "Adding TXT $fulldomain in zone $_bergdns_zone_name"
|
||||
if [ -z "$_bergdns_rrset_id" ]; then
|
||||
if _bergdns_rest POST "zones/$_bergdns_zone_id/rrsets" \
|
||||
"{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":$BERGDNS_TTL,\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
|
||||
_bergdns_rrset_id=$(echo "$response" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
|
||||
_bergdns_wait "$fulldomain"
|
||||
return 0
|
||||
fi
|
||||
if [ "$_bergdns_code" != "rrset_exists" ]; then
|
||||
_err "bergdns: could not add the challenge record: $_bergdns_error"
|
||||
return 1
|
||||
fi
|
||||
# another run created it since the lookup above
|
||||
_bergdns_find_rrset "$fulldomain" || return 1
|
||||
if [ -z "$_bergdns_rrset_id" ]; then
|
||||
_err "bergdns: could not find the challenge record at $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! _bergdns_rest POST \
|
||||
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
|
||||
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
|
||||
_err "bergdns: could not add the challenge record: $_bergdns_error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_bergdns_wait "$fulldomain"
|
||||
}
|
||||
|
||||
# Usage: dns_bergdns_rm _acme-challenge.www.example.com "token"
|
||||
# Only this value is removed. Removing the last value deletes the RRset, and
|
||||
# removing a value that does not exist is not an error.
|
||||
dns_bergdns_rm() {
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
txtvalue=$2
|
||||
|
||||
_bergdns_init || return 1
|
||||
_bergdns_find_zone "$fulldomain" || return 1
|
||||
_bergdns_find_rrset "$fulldomain" || return 1
|
||||
|
||||
if [ -z "$_bergdns_rrset_id" ]; then
|
||||
_info "bergdns: no TXT records at $fulldomain, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Removing TXT $fulldomain from zone $_bergdns_zone_name"
|
||||
if ! _bergdns_rest DELETE \
|
||||
"zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/records" \
|
||||
"{\"records\":[\"\\\"$txtvalue\\\"\"]}"; then
|
||||
# Any flavour of not-found is a cleanup that has already happened: the
|
||||
# RRset was removed by a previous run, or by the other half of a
|
||||
# domain-and-wildcard pair taking the last value with it.
|
||||
if [ "$_bergdns_status" = "404" ]; then
|
||||
_info "bergdns: $fulldomain holds no such record any more, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
_err "bergdns: could not remove the challenge record: $_bergdns_error"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
######## Private functions ###################################################
|
||||
|
||||
_bergdns_init() {
|
||||
BERGDNS_API_KEY="${BERGDNS_API_KEY:-$(_readaccountconf_mutable BERGDNS_API_KEY)}"
|
||||
BERGDNS_API_URL="${BERGDNS_API_URL:-$(_readaccountconf_mutable BERGDNS_API_URL)}"
|
||||
BERGDNS_TTL="${BERGDNS_TTL:-$(_readaccountconf_mutable BERGDNS_TTL)}"
|
||||
BERGDNS_PROPAGATION_TIMEOUT="${BERGDNS_PROPAGATION_TIMEOUT:-$(_readaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT)}"
|
||||
|
||||
if [ -z "$BERGDNS_API_KEY" ]; then
|
||||
BERGDNS_API_KEY=""
|
||||
_clearaccountconf_mutable BERGDNS_API_KEY
|
||||
_err "You have not set BERGDNS_API_KEY. Create a key in the bergdns UI and export it:"
|
||||
_err " export BERGDNS_API_KEY=\"bgd_...\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
[ -n "$BERGDNS_API_URL" ] || BERGDNS_API_URL="$_BERGDNS_DEFAULT_URL"
|
||||
[ -n "$BERGDNS_TTL" ] || BERGDNS_TTL="$_BERGDNS_DEFAULT_TTL"
|
||||
[ -n "$BERGDNS_PROPAGATION_TIMEOUT" ] || BERGDNS_PROPAGATION_TIMEOUT="$_BERGDNS_DEFAULT_WAIT"
|
||||
|
||||
# strip trailing slashes
|
||||
BERGDNS_API_URL=$(echo "$BERGDNS_API_URL" | sed 's#/*$##')
|
||||
|
||||
# The TTL is interpolated into the request body and the timeout is counted
|
||||
# down in arithmetic, so a stray value from the environment or from an old
|
||||
# account.conf has to be caught here rather than become malformed JSON and
|
||||
# an opaque 400.
|
||||
case "$BERGDNS_TTL" in
|
||||
*[!0-9]* | '')
|
||||
_err "bergdns: BERGDNS_TTL must be a number of seconds, not \"$BERGDNS_TTL\"."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "$BERGDNS_PROPAGATION_TIMEOUT" in
|
||||
*[!0-9]* | '')
|
||||
_err "bergdns: BERGDNS_PROPAGATION_TIMEOUT must be a number of seconds, not \"$BERGDNS_PROPAGATION_TIMEOUT\"."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_saveaccountconf_mutable BERGDNS_API_KEY "$BERGDNS_API_KEY"
|
||||
# Only what the user actually chose is written back, and a value equal to
|
||||
# the default clears any older setting. Persisting a default would pin the
|
||||
# install to today's value, and a later change to the shipped one -- a move
|
||||
# of the API base above all -- would never reach it; leaving an old setting
|
||||
# in place would mean the environment could never put one back to default.
|
||||
if [ "$BERGDNS_API_URL" = "$_BERGDNS_DEFAULT_URL" ]; then
|
||||
_clearaccountconf_mutable BERGDNS_API_URL
|
||||
else
|
||||
_saveaccountconf_mutable BERGDNS_API_URL "$BERGDNS_API_URL"
|
||||
fi
|
||||
if [ "$BERGDNS_TTL" = "$_BERGDNS_DEFAULT_TTL" ]; then
|
||||
_clearaccountconf_mutable BERGDNS_TTL
|
||||
else
|
||||
_saveaccountconf_mutable BERGDNS_TTL "$BERGDNS_TTL"
|
||||
fi
|
||||
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "$_BERGDNS_DEFAULT_WAIT" ]; then
|
||||
_clearaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT
|
||||
else
|
||||
_saveaccountconf_mutable BERGDNS_PROPAGATION_TIMEOUT "$BERGDNS_PROPAGATION_TIMEOUT"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _bergdns_find_zone _acme-challenge.www.example.com
|
||||
# Sets _bergdns_zone_id and _bergdns_zone_name.
|
||||
# Zones are addressed by an id, not by name, so the zone list is fetched once
|
||||
# and the longest matching zone name wins.
|
||||
_bergdns_find_zone() {
|
||||
_bergdns_fqdn=$1
|
||||
_bergdns_zone_id=""
|
||||
_bergdns_zone_name=""
|
||||
|
||||
if ! _bergdns_rest GET "zones"; then
|
||||
_err "bergdns: could not list zones: $_bergdns_error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# one zone object per line, so id and name stay together
|
||||
_bergdns_zone_lines=$(echo "$response" | tr '{' '\n')
|
||||
|
||||
_bergdns_cand="$_bergdns_fqdn"
|
||||
while [ -n "$_bergdns_cand" ]; do
|
||||
_bergdns_line=$(echo "$_bergdns_zone_lines" | _bergdns_select "$_bergdns_cand" | _head_n 1)
|
||||
if [ -n "$_bergdns_line" ]; then
|
||||
_bergdns_zone_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
_bergdns_zone_name="$_bergdns_cand"
|
||||
_debug _bergdns_zone_id "$_bergdns_zone_id"
|
||||
_debug _bergdns_zone_name "$_bergdns_zone_name"
|
||||
[ -n "$_bergdns_zone_id" ] && return 0
|
||||
break
|
||||
fi
|
||||
case "$_bergdns_cand" in
|
||||
*.*) _bergdns_cand=${_bergdns_cand#*.} ;;
|
||||
*) _bergdns_cand="" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
_err "bergdns: no zone in this account holds $_bergdns_fqdn."
|
||||
_err "bergdns: the key must be able to read the zone as well as write the record."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Usage: _bergdns_find_rrset _acme-challenge.www.example.com
|
||||
# Sets _bergdns_rrset_id to the id of the TXT RRset at that name, or to an
|
||||
# empty string if there is none. Records are addressed by id, so the zone's
|
||||
# RRsets are listed to find it.
|
||||
_bergdns_find_rrset() {
|
||||
_bergdns_fqdn=$1
|
||||
_bergdns_rrset_id=""
|
||||
|
||||
if ! _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets"; then
|
||||
_err "bergdns: could not list the records of $_bergdns_zone_name: $_bergdns_error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_bergdns_line=$(echo "$response" | tr '{' '\n' | _bergdns_select "$_bergdns_fqdn" TXT | _head_n 1)
|
||||
if [ -n "$_bergdns_line" ]; then
|
||||
_bergdns_rrset_id=$(echo "$_bergdns_line" | _egrep_o '"id":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
fi
|
||||
_debug _bergdns_rrset_id "$_bergdns_rrset_id"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: ... | _bergdns_select name [type]
|
||||
# Reads one JSON object per line and prints those whose "name" is name and,
|
||||
# when a type is given, whose "type" is that type. The two fields are matched
|
||||
# one at a time, so neither the order the server writes its keys in nor
|
||||
# anything sitting between them changes the answer.
|
||||
#
|
||||
# The comparison is a shell case, which is literal by construction: grep -F
|
||||
# does not exist on Solaris, and _contains and _startswith would read the name
|
||||
# as a regular expression. Each pattern anchors on the start of the object or
|
||||
# on the comma before the key, so a key that merely ends in "name" cannot
|
||||
# match.
|
||||
_bergdns_select() {
|
||||
_bergdns_sel_name=$1
|
||||
_bergdns_sel_type=$2
|
||||
while IFS= read -r _bergdns_sel_line || [ -n "$_bergdns_sel_line" ]; do
|
||||
case "$_bergdns_sel_line" in
|
||||
'"name":"'"$_bergdns_sel_name"'"'* | *',"name":"'"$_bergdns_sel_name"'"'*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
if [ -n "$_bergdns_sel_type" ]; then
|
||||
case "$_bergdns_sel_line" in
|
||||
'"type":"'"$_bergdns_sel_type"'"'* | *',"type":"'"$_bergdns_sel_type"'"'*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s\n' "$_bergdns_sel_line"
|
||||
done
|
||||
}
|
||||
|
||||
# Usage: _bergdns_wait _acme-challenge.www.example.com
|
||||
# Polls the propagation endpoint until all bergdns nameservers serve the
|
||||
# record. A timeout is logged but does not fail the issuance.
|
||||
#
|
||||
# This covers the zone transfer from the primary to the secondaries, which
|
||||
# takes seconds; the resolver side is acme.sh's own _check_dns_entries, which
|
||||
# runs after every record has been added and has a timeout of its own.
|
||||
_bergdns_wait() {
|
||||
_bergdns_fqdn=$1
|
||||
if [ "$BERGDNS_PROPAGATION_TIMEOUT" = "0" ] || [ -z "$_bergdns_rrset_id" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
_bergdns_waited=0
|
||||
while [ "$_bergdns_waited" -lt "$BERGDNS_PROPAGATION_TIMEOUT" ]; do
|
||||
if _bergdns_rest GET "zones/$_bergdns_zone_id/rrsets/$_bergdns_rrset_id/propagation"; then
|
||||
case "$response" in
|
||||
*'"propagated":true'*)
|
||||
_info "bergdns: $_bergdns_fqdn is served by every secondary after ${_bergdns_waited}s"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
elif [ "$_bergdns_code" = "propagation_unavailable" ]; then
|
||||
# propagation checks are not configured on this server
|
||||
_info "bergdns: this deployment does not offer propagation checks; not waiting"
|
||||
return 0
|
||||
else
|
||||
case "$_bergdns_status" in
|
||||
429) ;; # rate limited, worth another go
|
||||
4*)
|
||||
# The check is refused rather than pending, and waiting will not
|
||||
# change that. _check_dns_entries still has to pass, so this is not
|
||||
# the place to fail the issuance.
|
||||
_info "bergdns: the propagation check is unavailable ($_bergdns_error); not waiting"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
_sleep 5
|
||||
_bergdns_waited=$((_bergdns_waited + 5))
|
||||
done
|
||||
|
||||
_info "bergdns: $_bergdns_fqdn was not on every secondary after ${BERGDNS_PROPAGATION_TIMEOUT}s; continuing anyway"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: _bergdns_rest method endpoint [body]
|
||||
# Sets response and _bergdns_status. On failure also sets _bergdns_error and,
|
||||
# where the API itself answered, _bergdns_code.
|
||||
_bergdns_rest() {
|
||||
_bergdns_method=$1
|
||||
_bergdns_endpoint=$2
|
||||
_bergdns_body=$3
|
||||
_bergdns_error=""
|
||||
_bergdns_code=""
|
||||
_bergdns_status=""
|
||||
|
||||
export _H1="Authorization: Bearer $BERGDNS_API_KEY"
|
||||
export _H2="Accept: application/json"
|
||||
|
||||
_bergdns_url="$BERGDNS_API_URL/$_bergdns_endpoint"
|
||||
_debug _bergdns_url "$_bergdns_url"
|
||||
|
||||
# drop the headers of the previous request, so that a request which never
|
||||
# reaches the server cannot be read as carrying its status
|
||||
if [ -f "$HTTP_HEADER" ]; then
|
||||
: >"$HTTP_HEADER"
|
||||
fi
|
||||
|
||||
if [ "$_bergdns_method" = "GET" ]; then
|
||||
response="$(_get "$_bergdns_url")"
|
||||
else
|
||||
_debug2 _bergdns_body "$_bergdns_body"
|
||||
response="$(_post "$_bergdns_body" "$_bergdns_url" "" "$_bergdns_method" "application/json")"
|
||||
fi
|
||||
_bergdns_ret="$?"
|
||||
_debug2 response "$response"
|
||||
|
||||
if [ "$_bergdns_ret" != "0" ]; then
|
||||
_bergdns_error="the request to $_bergdns_url could not be made"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_bergdns_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
_debug _bergdns_status "$_bergdns_status"
|
||||
|
||||
# The HTTP status decides. Errors from the API itself are RFC 9457
|
||||
# problem+json and carry a "detail" to show and a stable "code" to branch on,
|
||||
# but a request that never gets that far -- bergdns.at answers from behind a
|
||||
# reverse proxy, whose 502 and 504 are HTML -- has neither, and reading the
|
||||
# body alone would take those for success.
|
||||
case "$_bergdns_status" in
|
||||
2*) return 0 ;;
|
||||
esac
|
||||
_bergdns_error=$(echo "$response" | _egrep_o '"detail":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
_bergdns_code=$(echo "$response" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)
|
||||
[ -n "$_bergdns_error" ] || _bergdns_error="$_bergdns_url answered HTTP ${_bergdns_status:-(none)}"
|
||||
_debug _bergdns_code "$_bergdns_code"
|
||||
return 1
|
||||
}
|
||||
@@ -283,7 +283,7 @@ _bhosted_response_has_error() {
|
||||
fi
|
||||
|
||||
# Fallback for unexpected/non-XML responses
|
||||
_resp_lc="$(_lower_case "$_resp")"
|
||||
_resp_lc="$(printf "%s\n" "$_resp" | _lower_case)"
|
||||
|
||||
if _contains "$_resp_lc" "error"; then
|
||||
_debug "Detected 'error' in response"
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ CF_Api="https://api.cloudflare.com/client/v4"
|
||||
|
||||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_cf_add() {
|
||||
fulldomain=$1
|
||||
fulldomain="$(_idn "$1")"
|
||||
txtvalue=$2
|
||||
|
||||
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
||||
@@ -109,7 +109,7 @@ dns_cf_add() {
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_cf_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain="$(_idn "$1")"
|
||||
txtvalue=$2
|
||||
|
||||
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
||||
|
||||
+10
-5
@@ -15,7 +15,7 @@ DNSimple_API="https://api.dnsimple.com/v2"
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_dnsimple_add() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
txtvalue=$2
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
@@ -56,7 +56,7 @@ dns_dnsimple_add() {
|
||||
|
||||
# fulldomain
|
||||
dns_dnsimple_rm() {
|
||||
fulldomain=$1
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
@@ -110,9 +110,13 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" 'not found'; then
|
||||
_debug "$h not found"
|
||||
else
|
||||
if ! _contains "$response" "\"name\":\"$h\""; then
|
||||
if ! _dnsimple_rest GET "$_account_id/zones/$h/records?per_page=1"; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"pagination":'; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
|
||||
_domain="$h"
|
||||
|
||||
@@ -121,6 +125,7 @@ _get_root() {
|
||||
|
||||
return 0
|
||||
fi
|
||||
_debug "$h not found"
|
||||
|
||||
previous="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
|
||||
@@ -59,6 +59,13 @@ dns_gandi_livedns_rm() {
|
||||
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."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
@@ -80,6 +87,20 @@ dns_gandi_livedns_rm() {
|
||||
_new_rrset_values=$(echo "$_new_rrset_values" | sed "s/\[ *,/\[/g")
|
||||
_debug "New rrset_values" "$_new_rrset_values"
|
||||
|
||||
# Last value removed: delete the record instead of leaving an empty TXT rrset.
|
||||
if [ "$(echo "$_new_rrset_values" | tr -d ' ')" = "[]" ]; then
|
||||
if ! _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT"; then
|
||||
return 1
|
||||
fi
|
||||
_gandi_livedns_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
|
||||
if [ "$_gandi_livedns_code" != "204" ]; then
|
||||
_err "Error deleting record: $response"
|
||||
return 1
|
||||
fi
|
||||
_info "Removing record $(__green "success")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_gandi_livedns_rest PUT \
|
||||
"domains/$_domain/records/$_sub_domain/TXT" \
|
||||
"{\"rrset_ttl\": 300, \"rrset_values\": $_new_rrset_values}" &&
|
||||
|
||||
Executable
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_mvmnet_info='mvmnet.com
|
||||
Site: mvmnet.com
|
||||
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mvmnet
|
||||
Api Docs: https://api.mvmnet.com/api/1.0/documentation/
|
||||
Issues: https://github.com/acmesh-official/acme.sh/issues/7297
|
||||
Author: Matteo Gaggiano <github.com/marchrius>
|
||||
Options:
|
||||
MVMNET_ID application_id
|
||||
MVMNET_KEY application_key
|
||||
MVMNET_SEED application_seed
|
||||
MVMNET_API_URL API base URL (defaults to https://api.mvmnet.com/api/1.0, use https://api.mvmnet.com/ote/1.0 for test)
|
||||
'
|
||||
|
||||
MVMNET_API_URL="${MVMNET_API_URL:-https://api.mvmnet.com/api/1.0}"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
dns_mvmnet_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_mvmnet_retrieve_and_check_variables || return 1
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _mvmnet_get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _subdomain "$_subdomain"
|
||||
|
||||
_info "Adding TXT record to ${fulldomain}"
|
||||
postdata=$(printf '{"domain":"%s","rtype":"TXT","ldata":"%s","rdata":"%s"}' "${_domain}" "${_subdomain}" "${txtvalue}")
|
||||
_mvmnet_rest POST "dns/zone/record" "$postdata"
|
||||
|
||||
if ! _contains "${response}" 'error'; then
|
||||
return 0
|
||||
fi
|
||||
_err "Could not create resource record, check logs"
|
||||
_err "${response}"
|
||||
return 1
|
||||
}
|
||||
|
||||
dns_mvmnet_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
_mvmnet_check_variables || return 1
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _mvmnet_get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug _domain "$_domain"
|
||||
_debug _subdomain "$_subdomain"
|
||||
|
||||
if ! _mvmnet_get_record_id "$_subdomain" "$_domain" "$txtvalue"; then
|
||||
_warn "Record id for $_subdomain not found, please remove it manually"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_debug _subdomain_record_id "$_subdomain_record_id"
|
||||
|
||||
_info "Deleting resource record $fulldomain ($_subdomain_record_id)"
|
||||
_mvmnet_rest DELETE "dns/zone/record" "domain=${_domain}&id=${_subdomain_record_id}"
|
||||
|
||||
if ! _contains "${response}" 'error'; then
|
||||
return 0
|
||||
fi
|
||||
_err "Could not delete resource record, check logs"
|
||||
_err "${response}"
|
||||
return 1
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_mvmnet_check_variables() {
|
||||
MVMNET_ID="${MVMNET_ID:-$(_readaccountconf_mutable MVMNET_ID)}"
|
||||
MVMNET_KEY="${MVMNET_KEY:-$(_readaccountconf_mutable MVMNET_KEY)}"
|
||||
MVMNET_SEED="${MVMNET_SEED:-$(_readaccountconf_mutable MVMNET_SEED)}"
|
||||
if [ -z "$MVMNET_ID" ] || [ -z "$MVMNET_KEY" ] || [ -z "$MVMNET_SEED" ]; then
|
||||
MVMNET_ID=""
|
||||
MVMNET_KEY=""
|
||||
MVMNET_SEED=""
|
||||
_err "You don't specify application_id, application_key or application_seed."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
_mvmnet_retrieve_and_check_variables() {
|
||||
_mvmnet_check_variables || return 1
|
||||
|
||||
_saveaccountconf_mutable MVMNET_ID "$MVMNET_ID"
|
||||
_saveaccountconf_mutable MVMNET_KEY "$MVMNET_KEY"
|
||||
_saveaccountconf_mutable MVMNET_SEED "$MVMNET_SEED"
|
||||
}
|
||||
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _subdomain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
# _domain_id=123456789
|
||||
_mvmnet_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
p=1
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
_debug "not valid $h"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _mvmnet_rest GET "domain/info" "domain=$h"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _contains "$response" "\"domain_idn\":\"$h\""; then
|
||||
_domain_id=$(echo "$response" | _egrep_o ".\"id\": *\"[0-9]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d " ")
|
||||
if [ "$_domain_id" ]; then
|
||||
_subdomain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# _acme-challenge.www
|
||||
# domain.com
|
||||
# value (Optional)
|
||||
#returns
|
||||
# _subdomain_record_id=123456789
|
||||
_mvmnet_get_record_id() {
|
||||
subdomain=$1
|
||||
domain=$2
|
||||
txtvalue=$3
|
||||
|
||||
_debug3 subdomain "$subdomain"
|
||||
_debug3 domain "$domain"
|
||||
_debug3 txtvalue "$txtvalue"
|
||||
|
||||
hosttofind="${subdomain}.${domain}"
|
||||
|
||||
if ! _mvmnet_rest GET "dns/zone" "domain=$domain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
objectbody="$(echo "$response" | _egrep_o '\{[^{}]*\}' | grep -E "\"host\" *: *\"$hosttofind\.\".*\"type\" *: *\"TXT\"|\"type\" *: *\"TXT\".*\"host\" *: *\"$hosttofind\.\"")"
|
||||
|
||||
_debug3 objectbody "${objectbody}"
|
||||
|
||||
if [ "$txtvalue" != "" ]; then
|
||||
_info "A value will be searched: $txtvalue"
|
||||
objectbody="$(echo "$objectbody" | grep "\"value\" *: *\"\\\\\"$txtvalue\\\\\"\"")"
|
||||
_debug3 objectbody "${objectbody}"
|
||||
fi
|
||||
|
||||
_subdomain_record_id="$(echo "$objectbody" | _egrep_o ".\"id\": *\"[0-9]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \" | tr -d ' ')"
|
||||
|
||||
if [ "${_subdomain_record_id}x" = "x" ]; then
|
||||
_err "error retrieve sub domain record id"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
#returns
|
||||
# response
|
||||
_mvmnet_rest() {
|
||||
m=$1
|
||||
ep="$2"
|
||||
data="$3"
|
||||
_debug "$ep"
|
||||
|
||||
signature="$(printf "%s" "${MVMNET_ID}+${MVMNET_KEY}+$m+${MVMNET_SEED}" | _digest "sha1" "hex")"
|
||||
|
||||
export _H1="X-Mvm-Application: ${MVMNET_ID}"
|
||||
export _H2="X-Mvm-Signature: ${signature}"
|
||||
|
||||
case "$m" in
|
||||
POST | PUT)
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$MVMNET_API_URL/$ep" "" "$m" "application/json")"
|
||||
;;
|
||||
DELETE)
|
||||
response="$(_post "" "$MVMNET_API_URL/$ep?$data" "" "$m")"
|
||||
;;
|
||||
GET)
|
||||
response="$(_get "$MVMNET_API_URL/$ep?$data")"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug response "${response}"
|
||||
return 0
|
||||
}
|
||||
@@ -185,13 +185,14 @@ _get_root_by_getHosts() {
|
||||
}
|
||||
|
||||
_namecheap_set_publicip() {
|
||||
NAMECHEAP_SOURCEIP="${NAMECHEAP_SOURCEIP:-$(_readaccountconf_mutable NAMECHEAP_SOURCEIP)}"
|
||||
|
||||
if [ -z "$NAMECHEAP_SOURCEIP" ]; then
|
||||
_err "No Source IP specified for Namecheap API."
|
||||
_err "Use your public ip address or an url to retrieve it (e.g. https://ifconfig.co/ip) and export it as NAMECHEAP_SOURCEIP"
|
||||
return 1
|
||||
else
|
||||
_saveaccountconf NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
|
||||
_saveaccountconf_mutable NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
|
||||
_debug sourceip "$NAMECHEAP_SOURCEIP"
|
||||
|
||||
ip=$(echo "$NAMECHEAP_SOURCEIP" | _egrep_o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
|
||||
@@ -252,6 +253,8 @@ _namecheap_parse_host() {
|
||||
}
|
||||
|
||||
_namecheap_check_config() {
|
||||
NAMECHEAP_API_KEY="${NAMECHEAP_API_KEY:-$(_readaccountconf_mutable NAMECHEAP_API_KEY)}"
|
||||
NAMECHEAP_USERNAME="${NAMECHEAP_USERNAME:-$(_readaccountconf_mutable NAMECHEAP_USERNAME)}"
|
||||
|
||||
if [ -z "$NAMECHEAP_API_KEY" ]; then
|
||||
_err "No API key specified for Namecheap API."
|
||||
@@ -265,8 +268,8 @@ _namecheap_check_config() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
|
||||
_saveaccountconf NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
|
||||
_saveaccountconf_mutable NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
|
||||
_saveaccountconf_mutable NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_pblin_info='publicinternet.com
|
||||
Domains: public-internet.co.uk pblin.net
|
||||
Site: www.publicinternet.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_pblin
|
||||
Options:
|
||||
PBLIN_API_KEY API Key. Create an API key in the Public Internet customer portal at https://portal.publicinternet.com. A key is scoped to one domain, so each certificate needs the key for its own domain
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7314
|
||||
Author: Public Internet <https://github.com/publicinternet>
|
||||
'
|
||||
|
||||
PBLIN_Api="https://api.pblin.net/dns"
|
||||
|
||||
#Usage: dns_pblin_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_pblin_add() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _pblin_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
#an API key is scoped to one zone, so it is kept per certificate, and only once a zone lookup has accepted it
|
||||
_savedomainconf SAVED_PBLIN_API_KEY "$PBLIN_API_KEY"
|
||||
#the out-of-tree dns_pblin.sh saved the key unprefixed, which a renewal would load over a newly exported key
|
||||
_cleardomainconf PBLIN_API_KEY
|
||||
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
if ! _pblin_rest POST "domains/$_domain/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":600}"; then
|
||||
_err "Failed to add TXT record: $_message"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "TXT record added"
|
||||
return 0
|
||||
}
|
||||
|
||||
#Usage: dns_pblin_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_pblin_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
if ! _pblin_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _get_root "$fulldomain"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Removing TXT record for $fulldomain"
|
||||
if ! _pblin_rest GET "domains/$_domain/records?type=TXT&name=$fulldomain"; then
|
||||
_err "Failed to list TXT records: $_message"
|
||||
return 1
|
||||
fi
|
||||
|
||||
#the API has no content filter and accepts duplicates, so match the value here and remove every copy
|
||||
_record_ids="$(echo "$response" | tr '{' '\n' | grep "\"content\":\"$txtvalue\"" | _egrep_o '"id" *: *[0-9]*' | cut -d : -f 2 | tr -d ' ')"
|
||||
_debug _record_ids "$_record_ids"
|
||||
|
||||
if [ -z "$_record_ids" ]; then
|
||||
_info "TXT record already removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for _record_id in $_record_ids; do
|
||||
if ! _pblin_rest DELETE "domains/$_domain/records/$_record_id" && [ "$_code" != "404" ]; then
|
||||
_err "Failed to remove TXT record $_record_id: $_message"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
_info "TXT record removed"
|
||||
return 0
|
||||
}
|
||||
|
||||
_pblin_init() {
|
||||
#the SAVED_ prefix keeps a reloaded certificate conf from overriding a newly exported key
|
||||
PBLIN_API_KEY="${PBLIN_API_KEY:-$(_readdomainconf SAVED_PBLIN_API_KEY)}"
|
||||
#only renewals load the out-of-tree plugin's unprefixed key, so read it here for --issue too
|
||||
PBLIN_API_KEY="${PBLIN_API_KEY:-$(_readdomainconf PBLIN_API_KEY)}"
|
||||
|
||||
if [ -z "$PBLIN_API_KEY" ]; then
|
||||
_err "PBLIN_API_KEY is not set. Create an API key in the Public Internet customer portal at https://portal.publicinternet.com, then:"
|
||||
_err "export PBLIN_API_KEY=\"your-api-key\""
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_get_root() {
|
||||
domain=$1
|
||||
i=1
|
||||
|
||||
while true; do
|
||||
#-s: plain cut returns a dotless name (e.g. from --domain-alias) for every i, so the loop would keep querying it
|
||||
h=$(printf "%s" "$domain" | cut -s -d . -f "$i"-100)
|
||||
|
||||
if [ -z "$h" ]; then
|
||||
_err "No zone for $domain found in this Public Internet account"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if _pblin_rest GET "domains/$h"; then
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$_code" in
|
||||
404) ;;
|
||||
401 | 403)
|
||||
_err "The API key was rejected ($_message), check PBLIN_API_KEY"
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
_err "Failed to look up zone $h: $_message"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
_pblin_rest() {
|
||||
m=$1
|
||||
ep=$2
|
||||
data=$3
|
||||
|
||||
export _H1="Authorization: Bearer $PBLIN_API_KEY"
|
||||
export _H2="Content-Type: application/json"
|
||||
|
||||
if [ "$m" = "GET" ]; then
|
||||
response="$(_get "$PBLIN_Api/$ep")"
|
||||
else
|
||||
response="$(_post "$data" "$PBLIN_Api/$ep" "" "$m")"
|
||||
fi
|
||||
|
||||
if [ "$?" != "0" ]; then
|
||||
_code=""
|
||||
_message="connection failed"
|
||||
return 1
|
||||
fi
|
||||
|
||||
#printf, not echo: dash's builtin echo interprets backslash escapes in record content
|
||||
response="$(printf "%s\n" "$response" | _normalizeJson)"
|
||||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
_debug2 _code "$_code"
|
||||
_debug2 response "$response"
|
||||
|
||||
case "$_code" in
|
||||
2*) return 0 ;;
|
||||
esac
|
||||
|
||||
_message="$(echo "$response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
|
||||
_message="HTTP ${_code:-request failed}${_message:+: $_message}"
|
||||
return 1
|
||||
}
|
||||
+34
-18
@@ -114,21 +114,32 @@ _rm_record() {
|
||||
full=$1
|
||||
txtvalue=$2
|
||||
|
||||
if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then
|
||||
if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records?type=TXT"; then
|
||||
_err "Failed to retrieve records"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]}
|
||||
_txt_record_obj=$(
|
||||
printf '%s\n' "$response" |
|
||||
sed 's/^.*"data":\[//; s/\],"message":.*$//' |
|
||||
awk '{ gsub(/},{/, "}\n{"); print }' |
|
||||
grep -F "\"name\":\"$full\"" |
|
||||
grep -F "\"type\":\"TXT\"" |
|
||||
grep -F "\"content\":\"$txtvalue\"" |
|
||||
_head_n 1
|
||||
)
|
||||
# The API returns: {"success":true,"data":{"records":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]},"message":"..."}
|
||||
_records="$(printf '%s\n' "$response" | tr -d '\r\n' | _egrep_o '\{[^{}]*\}')"
|
||||
_txt_record_obj=""
|
||||
while IFS= read -r _rec || [ -n "$_rec" ]; do
|
||||
case "$_rec" in
|
||||
*"\"name\":\"$full\""*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case "$_rec" in
|
||||
*"\"type\":\"TXT\""*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
case "$_rec" in
|
||||
*"\"content\":\"$txtvalue\""*)
|
||||
_txt_record_obj="$_rec"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
$_records
|
||||
EOF
|
||||
|
||||
if [ -z "$_txt_record_obj" ]; then
|
||||
_info "TXT record not found for $full with content $txtvalue"
|
||||
@@ -173,7 +184,7 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_zones_response="$response"
|
||||
_zones="$(printf '%s\n' "$response" | tr -d '\r\n' | _egrep_o '\{[^{}]*\}')"
|
||||
|
||||
while true; do
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
@@ -183,12 +194,17 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_obj=$(
|
||||
printf '%s' "$_zones_response" |
|
||||
sed 's/},{/}\n{/g' |
|
||||
grep -F "\"name\":\"$h\"" |
|
||||
_head_n 1
|
||||
)
|
||||
zone_obj=""
|
||||
while IFS= read -r _zone || [ -n "$_zone" ]; do
|
||||
case "$_zone" in
|
||||
*"\"name\":\"$h\""*)
|
||||
zone_obj="$_zone"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
$_zones
|
||||
EOF
|
||||
|
||||
if [ -n "$zone_obj" ]; then
|
||||
_zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_primedns_info='PrimeDNS
|
||||
Site: primedns.net
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_primedns
|
||||
Options:
|
||||
PRIMEDNS_API_KEY API Key. Scope acme:name:_acme-challenge.<hostname> for each name you issue (or dns:write)
|
||||
OptionsAlt:
|
||||
PRIMEDNS_API_URL API base URL. Optional, default https://api.primedns.net/v1
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7330
|
||||
Author: PrimeDNS <acme@primedns.net>
|
||||
'
|
||||
|
||||
PRIMEDNS_Api_Default="https://api.primedns.net/v1"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#Usage: dns_primedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_primedns_add() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
PRIMEDNS_API_KEY="${PRIMEDNS_API_KEY:-$(_readaccountconf_mutable PRIMEDNS_API_KEY)}"
|
||||
if [ -z "$PRIMEDNS_API_KEY" ]; then
|
||||
PRIMEDNS_API_KEY=""
|
||||
_err "You didn't specify a PrimeDNS API key (PRIMEDNS_API_KEY) yet."
|
||||
_err "Create one in the PrimeDNS panel (Account -> API Keys) with the scope acme:name:_acme-challenge.<hostname> for each name you issue."
|
||||
return 1
|
||||
fi
|
||||
|
||||
PRIMEDNS_API_URL="${PRIMEDNS_API_URL:-$(_readaccountconf_mutable PRIMEDNS_API_URL)}"
|
||||
|
||||
#save the credentials to the account conf file.
|
||||
_saveaccountconf_mutable PRIMEDNS_API_KEY "$PRIMEDNS_API_KEY"
|
||||
if [ "$PRIMEDNS_API_URL" ]; then
|
||||
_saveaccountconf_mutable PRIMEDNS_API_URL "$PRIMEDNS_API_URL"
|
||||
fi
|
||||
|
||||
_info "Using PrimeDNS"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _primedns_rest POST "$fulldomain" "$txtvalue"; then
|
||||
return 1
|
||||
fi
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
#Usage: dns_primedns_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_primedns_rm() {
|
||||
fulldomain=$(_idn "$1")
|
||||
txtvalue=$2
|
||||
|
||||
PRIMEDNS_API_KEY="${PRIMEDNS_API_KEY:-$(_readaccountconf_mutable PRIMEDNS_API_KEY)}"
|
||||
if [ -z "$PRIMEDNS_API_KEY" ]; then
|
||||
PRIMEDNS_API_KEY=""
|
||||
_err "You didn't specify a PrimeDNS API key (PRIMEDNS_API_KEY) yet."
|
||||
return 1
|
||||
fi
|
||||
PRIMEDNS_API_URL="${PRIMEDNS_API_URL:-$(_readaccountconf_mutable PRIMEDNS_API_URL)}"
|
||||
|
||||
_info "Using PrimeDNS"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _primedns_rest DELETE "$fulldomain" "$txtvalue"; then
|
||||
return 1
|
||||
fi
|
||||
_info "Removed, OK"
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
#Usage: _primedns_rest POST|DELETE _acme-challenge.www.domain.com txtvalue
|
||||
#The API creates/removes the TXT record with exactly the name given, so the full
|
||||
#name acme.sh passes (_acme-challenge.<domain>, or the name of a DNS alias) is
|
||||
#sent as it is.
|
||||
_primedns_rest() {
|
||||
_method=$1
|
||||
_name=$2
|
||||
_value=$3
|
||||
_url="${PRIMEDNS_API_URL:-$PRIMEDNS_Api_Default}/dns/acme-challenge"
|
||||
|
||||
#The body is built by hand, so refuse anything that would need JSON escaping.
|
||||
case "$_name$_value" in
|
||||
*\"* | *\\*)
|
||||
_err "Refusing to send '$_name': it contains a quote or a backslash."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ -z "$_name" ] || _is_idn "$_name"; then
|
||||
_err "Cannot send '$_name': it is not a plain ASCII host name. For an IDN name install idn (acme.sh converts it to punycode) or pass the punycode name."
|
||||
return 1
|
||||
fi
|
||||
_body="{\"name\":\"$_name\",\"value\":\"$_value\"}"
|
||||
_debug _body "$_body"
|
||||
|
||||
_primedns_retried=""
|
||||
while true; do
|
||||
#acme.sh sends _H1.._H5 with every later request of this process, so the API key
|
||||
#header must not stay set once this call is done (it would reach other hosts).
|
||||
_primedns_old_h1="$_H1"
|
||||
export _H1="X-API-Key: $PRIMEDNS_API_KEY"
|
||||
: >"$HTTP_HEADER"
|
||||
response="$(_post "$_body" "$_url" "" "$_method" "application/json")"
|
||||
_ret=$?
|
||||
export _H1="$_primedns_old_h1"
|
||||
_status=$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
|
||||
if [ "$_ret" != "0" ]; then
|
||||
_err "Error calling the PrimeDNS API ($_method /dns/acme-challenge)."
|
||||
return 1
|
||||
fi
|
||||
response="$(echo "$response" | _normalizeJson)"
|
||||
_debug response "$response"
|
||||
_debug _status "$_status"
|
||||
|
||||
#Rate limited or temporary server error: wait and try once more.
|
||||
if [ -z "$_primedns_retried" ] && { [ "$_status" = "429" ] || [ "$_status" = "502" ] || [ "$_status" = "503" ] || [ "$_status" = "504" ]; }; then
|
||||
_wait="$(_retryafter_seconds <"$HTTP_HEADER")"
|
||||
case "$_wait" in
|
||||
'' | *[!0-9]*) _wait=5 ;;
|
||||
esac
|
||||
[ "$_wait" -gt 60 ] && _wait=60
|
||||
_info "PrimeDNS API answered HTTP $_status, retrying in $_wait seconds."
|
||||
_sleep "$_wait"
|
||||
_primedns_retried=1
|
||||
continue
|
||||
fi
|
||||
break
|
||||
done
|
||||
|
||||
if _contains "$response" '"success":true'; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$_status" = "401" ]; then
|
||||
_err "The PrimeDNS API refused the key (HTTP 401): it is wrong, revoked or expired. Check PRIMEDNS_API_KEY."
|
||||
elif _contains "$response" 'IP_NOT_ALLOWED'; then
|
||||
_err "This PrimeDNS API key may not be used from this address (allowed_ips). Add the address to the key or run acme.sh from an allowed host."
|
||||
elif _contains "$response" 'NAME_NOT_ALLOWED'; then
|
||||
_err "This PrimeDNS API key may not write $_name: add the scope acme:name:$_name to the key, or the zone to its allowed zones."
|
||||
elif _contains "$response" 'INSUFFICIENT_SCOPE'; then
|
||||
_err "This PrimeDNS API key needs the scope acme:name:$_name (or dns:write)."
|
||||
elif _contains "$response" 'DOMAIN_NOT_FOUND'; then
|
||||
_err "$_name is not in a zone of the PrimeDNS account that owns this API key."
|
||||
elif [ "$_status" = "429" ]; then
|
||||
_err "PrimeDNS API rate limit still hit after a retry. Try again in a minute."
|
||||
else
|
||||
_err "PrimeDNS API error (HTTP $_status): $response"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
+123
-41
@@ -4,14 +4,15 @@ dns_selfhost_info='SelfHost.de
|
||||
Site: SelfHost.de
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost
|
||||
Options:
|
||||
SELFHOSTDNS_USERNAME Username
|
||||
SELFHOSTDNS_PASSWORD Password
|
||||
SELFHOSTDNS_APIKEY API key
|
||||
SELFHOSTDNS_MAP Subdomain name
|
||||
SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://account.selfhost.de/cgi-bin/api.pl"
|
||||
SELFHOSTDNS_UPDATE_URL API url. Optional. Default "https://my.selfhost.de/cgi-bin/dns-api.pl"
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/4291
|
||||
Author: Marvin Edeler
|
||||
'
|
||||
|
||||
DEFAULT_SELFHOSTDNS_UPDATE_URL="https://my.selfhost.de/cgi-bin/dns-api.pl"
|
||||
|
||||
dns_selfhost_add() {
|
||||
fulldomain=$1
|
||||
txt=$2
|
||||
@@ -19,43 +20,26 @@ dns_selfhost_add() {
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txt"
|
||||
|
||||
DEFAULT_SELFHOSTDNS_UPDATE_URL="https://account.selfhost.de/cgi-bin/api.pl"
|
||||
case "$fulldomain" in
|
||||
acmetestXyzRandomName.*)
|
||||
# The synthetic record of the DNS-API-Test, which expects add and
|
||||
# rm to succeed. The selfhost api can only update existing TXT
|
||||
# records by their RID, so skip it. Real records are never treated
|
||||
# as a no-op.
|
||||
_info "Skipping the DNS-API-Test record $fulldomain, the selfhost api can only update existing TXT records."
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Get values, but don't save until we successfully validated
|
||||
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}"
|
||||
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}"
|
||||
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}"
|
||||
SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}"
|
||||
# These values are domain dependent, so read them from there
|
||||
SELFHOSTDNS_MAP="${SELFHOSTDNS_MAP:-$(_readdomainconf SELFHOSTDNS_MAP)}"
|
||||
if ! _selfhost_load_conf; then
|
||||
return 1
|
||||
fi
|
||||
# Selfhost api can't dynamically add TXT record,
|
||||
# so we have to store the last used RID of the domain to support a second RID for wildcard domains
|
||||
# (format: 'fulldomainA:lastRid fulldomainB:lastRid ...')
|
||||
SELFHOSTDNS_MAP_LAST_USED_INTERNAL=$(_readdomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL)
|
||||
|
||||
if [ -z "${SELFHOSTDNS_USERNAME:-}" ] || [ -z "${SELFHOSTDNS_PASSWORD:-}" ]; then
|
||||
_err "SELFHOSTDNS_USERNAME and SELFHOSTDNS_PASSWORD must be set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# get the domain entry from SELFHOSTDNS_MAP
|
||||
# only match full domains (at the beginning of the string or with a leading whitespace),
|
||||
# e.g. don't match mytest.example.com or sub.test.example.com for test.example.com
|
||||
# if the domain is defined multiple times only the last occurance will be matched
|
||||
# prepend a space to each line so "start of line" and "after whitespace"
|
||||
# can both be matched as "after a space/tab" (portable BRE, no ERE (^|..))
|
||||
_selfhost_tab="$(printf '\t')"
|
||||
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*:\{0,1\}[0-9]*\).*/\1/p")
|
||||
_debug2 mapEntry "$mapEntry"
|
||||
if test -z "$mapEntry"; then
|
||||
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# get the RIDs from the map entry
|
||||
rid1=$(echo "$mapEntry" | cut -d: -f2)
|
||||
rid2=$(echo "$mapEntry" | cut -d: -f3)
|
||||
|
||||
# read last used rid domain
|
||||
lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p")
|
||||
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
|
||||
@@ -68,11 +52,7 @@ dns_selfhost_add() {
|
||||
|
||||
_info "Trying to add $txt on selfhost for rid: $rid"
|
||||
|
||||
data="?username=$SELFHOSTDNS_USERNAME&password=$SELFHOSTDNS_PASSWORD&rid=$rid&content=$txt"
|
||||
response="$(_get "$SELFHOSTDNS_UPDATE_URL$data")"
|
||||
|
||||
if ! echo "$response" | grep "200 OK" >/dev/null; then
|
||||
_err "Invalid response of acme-dns for selfhost"
|
||||
if ! _selfhost_api "present" "$rid" "$txt"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -96,11 +76,22 @@ dns_selfhost_add() {
|
||||
fi
|
||||
|
||||
# Now that we know the values are good, save them
|
||||
_saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME"
|
||||
_saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD"
|
||||
_saveaccountconf_mutable SELFHOSTDNS_APIKEY "$SELFHOSTDNS_APIKEY"
|
||||
# The old api credentials are not used anymore
|
||||
_clearaccountconf_mutable SELFHOSTDNS_USERNAME
|
||||
_clearaccountconf_mutable SELFHOSTDNS_PASSWORD
|
||||
# These values are domain dependent, so store them there
|
||||
_savedomainconf SELFHOSTDNS_MAP "$SELFHOSTDNS_MAP"
|
||||
_savedomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL"
|
||||
# remember which RID holds this txt value, so it can be cleaned up later
|
||||
# (format: 'ridA:txtA ridB:txtB ...')
|
||||
SELFHOSTDNS_TXT_RID_INTERNAL=$(_readdomainconf SELFHOSTDNS_TXT_RID_INTERNAL)
|
||||
if test -z "$SELFHOSTDNS_TXT_RID_INTERNAL"; then
|
||||
SELFHOSTDNS_TXT_RID_INTERNAL="$rid:$txt"
|
||||
else
|
||||
SELFHOSTDNS_TXT_RID_INTERNAL="$SELFHOSTDNS_TXT_RID_INTERNAL $rid:$txt"
|
||||
fi
|
||||
_savedomainconf SELFHOSTDNS_TXT_RID_INTERNAL "$SELFHOSTDNS_TXT_RID_INTERNAL"
|
||||
}
|
||||
|
||||
dns_selfhost_rm() {
|
||||
@@ -108,5 +99,96 @@ dns_selfhost_rm() {
|
||||
txt=$2
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txt"
|
||||
_info "Creating and removing of records is not supported by selfhost API, will not delete anything."
|
||||
|
||||
case "$fulldomain" in
|
||||
acmetestXyzRandomName.*)
|
||||
# See dns_selfhost_add.
|
||||
_info "Skipping the DNS-API-Test record $fulldomain, the selfhost api can only update existing TXT records."
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! _selfhost_load_conf; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# find the RID which was used for this txt value
|
||||
SELFHOSTDNS_TXT_RID_INTERNAL=$(_readdomainconf SELFHOSTDNS_TXT_RID_INTERNAL)
|
||||
rids=""
|
||||
remainingTxtRidEntries=""
|
||||
for txtRidEntry in $SELFHOSTDNS_TXT_RID_INTERNAL; do
|
||||
if [ "${txtRidEntry#*:}" = "$txt" ]; then
|
||||
rids="${txtRidEntry%%:*}"
|
||||
elif test -z "$remainingTxtRidEntries"; then
|
||||
remainingTxtRidEntries="$txtRidEntry"
|
||||
else
|
||||
remainingTxtRidEntries="$remainingTxtRidEntries $txtRidEntry"
|
||||
fi
|
||||
done
|
||||
if test -z "$rids"; then
|
||||
# unknown txt value, so try all RIDs of the domain
|
||||
rids="$rid1 $rid2"
|
||||
fi
|
||||
|
||||
for rid in $rids; do
|
||||
_info "Trying to remove $txt on selfhost for rid: $rid"
|
||||
if ! _selfhost_api "cleanup" "$rid" "$txt"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
_savedomainconf SELFHOSTDNS_TXT_RID_INTERNAL "$remainingTxtRidEntries"
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# reads the config and sets rid1 and rid2 for $fulldomain
|
||||
_selfhost_load_conf() {
|
||||
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}"
|
||||
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}"
|
||||
SELFHOSTDNS_APIKEY="${SELFHOSTDNS_APIKEY:-$(_readaccountconf_mutable SELFHOSTDNS_APIKEY)}"
|
||||
# These values are domain dependent, so read them from there
|
||||
SELFHOSTDNS_MAP="${SELFHOSTDNS_MAP:-$(_readdomainconf SELFHOSTDNS_MAP)}"
|
||||
|
||||
if [ -z "${SELFHOSTDNS_APIKEY:-}" ]; then
|
||||
_err "SELFHOSTDNS_APIKEY must be set"
|
||||
# hint for users still configured for the old username/password api
|
||||
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}"
|
||||
SELFHOSTDNS_PASSWORD="${SELFHOSTDNS_PASSWORD:-$(_readaccountconf_mutable SELFHOSTDNS_PASSWORD)}"
|
||||
if [ -n "${SELFHOSTDNS_USERNAME:-}" ] || [ -n "${SELFHOSTDNS_PASSWORD:-}" ]; then
|
||||
_err "Selfhost has changed its API: SELFHOSTDNS_USERNAME and SELFHOSTDNS_PASSWORD are no longer supported, you have to migrate to SELFHOSTDNS_APIKEY."
|
||||
_err "See https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost for instructions."
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
# get the domain entry from SELFHOSTDNS_MAP
|
||||
# only match full domains (at the beginning of the string or with a leading whitespace),
|
||||
# e.g. don't match mytest.example.com or sub.test.example.com for test.example.com
|
||||
# if the domain is defined multiple times only the last occurance will be matched
|
||||
# prepend a space to each line so "start of line" and "after whitespace"
|
||||
# can both be matched as "after a space/tab" (portable BRE, no ERE (^|..))
|
||||
_selfhost_tab="$(printf '\t')"
|
||||
mapEntry=$(echo "$SELFHOSTDNS_MAP" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*:\{0,1\}[0-9]*\).*/\1/p")
|
||||
_debug2 mapEntry "$mapEntry"
|
||||
if test -z "$mapEntry"; then
|
||||
_err "SELFHOSTDNS_MAP must contain the fulldomain incl. prefix and at least one RID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# get the RIDs from the map entry
|
||||
rid1=$(echo "$mapEntry" | cut -d: -f2)
|
||||
rid2=$(echo "$mapEntry" | cut -d: -f3)
|
||||
}
|
||||
|
||||
# usage: _selfhost_api action rid txt
|
||||
_selfhost_api() {
|
||||
data="{\"api_key\":\"$SELFHOSTDNS_APIKEY\",\"action\":\"$1\",\"record_id\":$2,\"content\":\"$3\"}"
|
||||
response="$(_post "$data" "$SELFHOSTDNS_UPDATE_URL" "" "POST" "application/json")"
|
||||
_debug2 response "$response"
|
||||
|
||||
if ! echo "$response" | grep '"ok" *: *1' >/dev/null; then
|
||||
_err "Invalid response of acme-dns for selfhost: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
+13
-7
@@ -106,14 +106,17 @@ dns_yc_add() {
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting txt records"
|
||||
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
|
||||
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$fulldomain"; then
|
||||
_err "Error: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Adding record"
|
||||
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
||||
if _contains "$response" "\"done\": true"; then
|
||||
# The absolute name (trailing dot) also covers a record at the zone apex,
|
||||
# e.g. with --domain-alias, which has no relative form: an empty name is
|
||||
# rejected by the API.
|
||||
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
||||
if _contains "$response" "\"done\": *true"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
else
|
||||
@@ -167,8 +170,8 @@ dns_yc_rm() {
|
||||
# upsertRecordSets.deletions removes only the given value from the rrset,
|
||||
# leaving any other values at the same name (e.g. base + wildcard domain)
|
||||
# intact -- no need to read the current data set and recompute it.
|
||||
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
||||
if _contains "$response" "\"done\": true"; then
|
||||
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"deletions\": [ { \"name\":\"$fulldomain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
||||
if _contains "$response" "\"done\": *true"; then
|
||||
_info "Delete, OK"
|
||||
return 0
|
||||
else
|
||||
@@ -201,6 +204,8 @@ _get_root() {
|
||||
if [ "$_domain" ]; then
|
||||
_cutlength=$((${#domain} - ${#_domain}))
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength")
|
||||
# Strip the trailing dot: a relative name, as in the folder lookup below
|
||||
_sub_domain="${_sub_domain%.}"
|
||||
_domain_id=$YC_Zone_ID
|
||||
return 0
|
||||
else
|
||||
@@ -227,8 +232,9 @@ _get_root() {
|
||||
echo "You didn't specify a Yandex Cloud Folder ID."
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "\"zone\": \"$h\""; then
|
||||
_domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')
|
||||
if _contains "$response" "\"zone\": *\"$h\""; then
|
||||
# Drop labels objects first, their braces would break the [^{]* match
|
||||
_domain_id=$(echo "$response" | _normalizeJson | sed "s/\"labels\":{[^}]*}/\"labels\":0/g" | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"')
|
||||
_debug _domain_id "$_domain_id"
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#Support Healthchecks.io (hosted or self-hosted)
|
||||
#https://healthchecks.io/docs/http_api/
|
||||
|
||||
#Required:
|
||||
#HEALTHCHECKS_URL="https://hc-ping.com/your-uuid"
|
||||
|
||||
#Use with --notify-level 3, so a ping is sent on every cron run, even when
|
||||
#all certs are skipped. Otherwise Healthchecks reports the check as down.
|
||||
|
||||
healthchecks_send() {
|
||||
_subject="$1"
|
||||
_content="$2"
|
||||
_statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
|
||||
_debug "_subject" "$_subject"
|
||||
_debug "_content" "$_content"
|
||||
_debug "_statusCode" "$_statusCode"
|
||||
|
||||
HEALTHCHECKS_URL="${HEALTHCHECKS_URL:-$(_readaccountconf_mutable HEALTHCHECKS_URL)}"
|
||||
if [ -z "$HEALTHCHECKS_URL" ]; then
|
||||
HEALTHCHECKS_URL=""
|
||||
_err "You didn't specify the Healthchecks.io ping url HEALTHCHECKS_URL yet."
|
||||
_err "Example: export HEALTHCHECKS_URL=\"https://hc-ping.com/your-uuid\""
|
||||
return 1
|
||||
fi
|
||||
_saveaccountconf_mutable HEALTHCHECKS_URL "$HEALTHCHECKS_URL"
|
||||
|
||||
_hc_url="${HEALTHCHECKS_URL%/}"
|
||||
case "$_statusCode" in
|
||||
0 | 2) ;;
|
||||
1)
|
||||
_hc_url="$_hc_url/fail"
|
||||
;;
|
||||
*)
|
||||
_hc_url="$_hc_url/log"
|
||||
;;
|
||||
esac
|
||||
|
||||
_data="$_subject
|
||||
|
||||
$_content"
|
||||
|
||||
response="$(_post "$_data" "$_hc_url" "" "POST" "text/plain")"
|
||||
|
||||
if [ "$?" = "0" ] && [ "$response" = "OK" ]; then
|
||||
_info "healthchecks ping success."
|
||||
return 0
|
||||
fi
|
||||
_err "healthchecks ping error."
|
||||
_err "$response"
|
||||
return 1
|
||||
}
|
||||