Merge pull request #7321 from acmesh-official/dev

sync
This commit is contained in:
neil authored and GitHub committed 2026-10-12 18:00:45 +08:00
commit 3688b094ad
13 files changed
+1359 -116

No files matched your search

+173 -43
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env sh #!/usr/bin/env sh
VER=3.1.6 VER=3.1.7
PROJECT_NAME="acme.sh" PROJECT_NAME="acme.sh"
@@ -2140,6 +2140,7 @@ _resethttp() {
__HTTP_INITIALIZED="" __HTTP_INITIALIZED=""
_ACME_CURL="" _ACME_CURL=""
_ACME_WGET="" _ACME_WGET=""
_ACME_WGET2=""
ACME_HTTP_NO_REDIRECTS="" ACME_HTTP_NO_REDIRECTS=""
} }
@@ -2190,7 +2191,15 @@ _inithttp() {
fi fi
if [ -z "$_ACME_WGET" ] && _exists "wget"; then if [ -z "$_ACME_WGET" ] && _exists "wget"; then
#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" _ACME_WGET="wget -q"
fi
if [ "$ACME_USE_IPV6_REQUESTS" ]; then if [ "$ACME_USE_IPV6_REQUESTS" ]; then
_ACME_WGET="$_ACME_WGET --inet6-only " _ACME_WGET="$_ACME_WGET --inet6-only "
elif [ "$ACME_USE_IPV4_REQUESTS" ]; then elif [ "$ACME_USE_IPV4_REQUESTS" ]; then
@@ -2200,7 +2209,8 @@ _inithttp() {
_ACME_WGET="$_ACME_WGET --max-redirect 0 " _ACME_WGET="$_ACME_WGET --max-redirect 0 "
fi fi
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then 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 " _ACME_WGET="$_ACME_WGET -d "
fi fi
fi fi
@@ -2220,6 +2230,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] # body url [needbase64] [POST|PUT|DELETE] [ContentType]
_post() { _post() {
body="$1" body="$1"
@@ -2315,7 +2349,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")" 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 fi
elif [ "$httpmethod" = "HEAD" ]; then elif [ "$httpmethod" = "HEAD" ]; 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 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")" 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 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")" 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 +2383,9 @@ _post() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
else else
@@ -2434,6 +2479,9 @@ _post_file() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
else else
@@ -2501,6 +2549,9 @@ _get() {
cat "$HTTP_HEADER" >&2 cat "$HTTP_HEADER" >&2
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" _sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER"
fi fi
if [ "$_ACME_WGET2" ]; then
_wget2_fix_header
fi
# remove leading whitespaces from header to match curl format # remove leading whitespaces from header to match curl format
_sed_i 's/^ //g' "$HTTP_HEADER" _sed_i 's/^ //g' "$HTTP_HEADER"
fi fi
@@ -2687,10 +2738,6 @@ _send_signed_request() {
_debug2 _headers "$_headers" _debug2 _headers "$_headers"
_CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" _CACHED_NONCE="$(echo "$_headers" | grep -i "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)"
fi fi
if [ "$?" != "0" ]; then
_err "Cannot connect to $nonceurl to get nonce."
return 1
fi
else else
_debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE"
fi fi
@@ -2729,11 +2776,19 @@ _send_signed_request() {
_debug3 body "$body" _debug3 body "$body"
response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")" response="$(_post "$body" "$url" "$needbase64" "POST" "$__request_conent_type")"
_post_ret="$?"
_CACHED_NONCE="" _CACHED_NONCE=""
if [ "$?" != "0" ]; then #A transport failure (curl 7, 28, 35, ...) brings no answer at all. Issue
_err "Cannot make POST request to $url" #7320: the status was tested only after the line above had reset it, so
return 1 #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 fi
responseHeaders="$(cat "$HTTP_HEADER")" responseHeaders="$(cat "$HTTP_HEADER")"
@@ -2800,6 +2855,35 @@ _sed_escape_rhs() {
sed -e 's/\\/\\\\/g' -e 's/&/\\&/g' -e 's/|/\\|/g' 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 "file" "opt" "=" "value" [";"]
_setopt() { _setopt() {
__conf="$1" __conf="$1"
@@ -2826,39 +2910,29 @@ _setopt() {
return 1 return 1
;; ;;
esac esac
if [ -n "$(_tail_c 1 <"$__conf")" ]; then if ! __text="$(cat "$__conf")"; then
echo >>"$__conf" _err "Cannot read $__conf."
return 1
fi fi
#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 if grep -n "^$__opt$__sep" "$__conf" >/dev/null; then
_debug3 OK _debug3 OK
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
text="$(cat "$__conf")" __text="$(printf -- "%s\n" "$__text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
#capture first, write only on success: redirecting sed straight into the
#conf file truncates it before sed runs, so a failing sed (e.g. on an
#unescaped special character in the value) wiped the whole conf (#2426)
if __text="$(printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
printf -- "%s\n" "$__text" >"$__conf"
else
_err "Cannot save '$__opt' to $__conf."
return 1
fi
elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then
__val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)" __val="$(printf -- "%s\n" "$__val" | _sed_escape_rhs)"
text="$(cat "$__conf")" __text="$(printf -- "%s\n" "$__text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")" || __sed_err=1
if __text="$(printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|")"; then
printf -- "%s\n" "$__text" >"$__conf"
else
_err "Cannot save '$__opt' to $__conf."
return 1
fi
else else
_debug3 APP _debug3 APP
#printf, not echo: dash's builtin echo interprets backslash escapes in __text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
#the value and would corrupt it fi
printf -- "%s\n" "$__opt$__sep$__val$__end" >>"$__conf" if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then
_err "Cannot save '$__opt' to $__conf."
return 1
fi fi
_debug3 "$(grep -n "^$__opt$__sep" "$__conf")" _debug3 "$(grep -n "^$__opt$__sep" "$__conf")"
} }
@@ -2888,7 +2962,11 @@ _clear_conf() {
_conf_data="$(cat "$_c_c_f")" _conf_data="$(cat "$_c_c_f")"
#printf, not echo: dash's builtin echo interprets backslash escapes and #printf, not echo: dash's builtin echo interprets backslash escapes and
#would corrupt saved values that contain them on every rewrite #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" "$_conf_data" | sed "/^$_sdkey *=.*$/d")" ||
! _write_conf "$_c_c_f" "$_conf_data"; then
_err "Cannot clear '$_sdkey' in $_c_c_f."
return 1
fi
else else
_err "Config file is empty, cannot clear" _err "Config file is empty, cannot clear"
fi fi
@@ -3365,7 +3443,13 @@ __initHome() {
if [ -z "$ACCOUNT_CONF_PATH" ]; then if [ -z "$ACCOUNT_CONF_PATH" ]; then
if [ -f "$_DEFAULT_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" . "$_DEFAULT_ACCOUNT_CONF_PATH"
ACCOUNT_EMAIL="$_ih_account_email"
fi fi
fi fi
@@ -3475,12 +3559,22 @@ _initpath() {
domain="$1" domain="$1"
_ilength="$2" _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 __initHome
if [ -f "$ACCOUNT_CONF_PATH" ]; then if [ -f "$ACCOUNT_CONF_PATH" ]; then
. "$ACCOUNT_CONF_PATH" . "$ACCOUNT_CONF_PATH"
fi fi
ACCOUNT_EMAIL="$_cli_account_email"
if [ "$_ACME_IN_CRON" ]; then if [ "$_ACME_IN_CRON" ]; then
if [ ! "$_USER_PATH_EXPORTED" ]; then if [ ! "$_USER_PATH_EXPORTED" ]; then
_USER_PATH_EXPORTED=1 _USER_PATH_EXPORTED=1
@@ -4420,9 +4514,11 @@ _regAccount() {
_secure_debug3 _eab_kid "$_eab_kid" _secure_debug3 _eab_kid "$_eab_kid"
_secure_debug3 _eab_hmac_key "$_eab_hmac_key" _secure_debug3 _eab_hmac_key "$_eab_hmac_key"
_email="$(_getAccountEmail)" _email="$(_getAccountEmail)"
if [ "$_email" ]; then #CA_EMAIL is saved only once the CA has actually taken the contact, which
_savecaconf "CA_EMAIL" "$_email" #is when it answers 201. For an account key it already knows it answers
fi #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 [ "$ACME_DIRECTORY" = "$CA_ZEROSSL" ]; then
if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then if [ -z "$_eab_kid" ] || [ -z "$_eab_hmac_key" ]; then
@@ -4501,8 +4597,15 @@ _regAccount() {
if [ "$code" = "" ] || [ "$code" = '201' ]; then if [ "$code" = "" ] || [ "$code" = '201' ]; then
echo "$response" >"$ACCOUNT_JSON_PATH" echo "$response" >"$ACCOUNT_JSON_PATH"
_info "Registered" _info "Registered"
if [ "$_email" ]; then
_savecaconf "CA_EMAIL" "$_email"
fi
elif [ "$code" = '409' ] || [ "$code" = '200' ]; then elif [ "$code" = '409' ] || [ "$code" = '200' ]; then
_info "Already registered" _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 elif [ "$code" = '400' ] && _contains "$response" 'The account is not awaiting external account binding'; then
_info "EAB already registered" _info "EAB already registered"
_eabAlreadyBound=1 _eabAlreadyBound=1
@@ -5590,11 +5693,9 @@ issue() {
_on_issue_err "$_post_hook" _on_issue_err "$_post_hook"
return 1 return 1
fi fi
# RFC 9773 Section 5 only defines the "alreadyReplaced" error, but real CAs # Retry without "replaces" whenever the CA rejected that field, e.g. after
# (Let's Encrypt) may also reject with a malformed error if the prior cert # switching the ACME server: the prior cert belongs to the old CA.
# was issued by a different issuer / different CA. Retry without "replaces" if [ "$_replaces_certID" ] && _isARIReplacesRejected "$code" "$response"; then
# 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
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'." _info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order." _err "Error creating new order."
@@ -7930,6 +8031,35 @@ _getARICertID() {
printf "%s.%s" "$_akiurl" "$_serurl" 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 #cert
_get_ARI() { _get_ARI() {
_cert="$1" _cert="$1"
+2 -2
View File
@@ -74,9 +74,9 @@ fritzbox_deploy() {
_info "Log in to the FRITZ!Box" _info "Log in to the FRITZ!Box"
_fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')" _fritzbox_challenge="$(_get "${DEPLOY_FRITZBOX_URL}/login_sid.lua" | sed -e 's/^.*<Challenge>//' -e 's/<\/Challenge>.*$//')"
if _exists iconv; then 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 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 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)" _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 fi
+207
View File
@@ -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
}
+33 -6
View File
@@ -34,6 +34,8 @@
# interactive input the device name # interactive input the device name
# - export SYNO_DEVICE_ID="" - (deprecated, auth with OTP code instead) # - export SYNO_DEVICE_ID="" - (deprecated, auth with OTP code instead)
# required for omitting 2FA-OTP # required for omitting 2FA-OTP
# - export SYNO_USE_TOKEN=0 - disable SynoToken authentication
# when deploying to Synology SRM
# 3. Run command: # 3. Run command:
# acme.sh --deploy --deploy-hook synology_dsm -d example.com # 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_username="$(printf "%s" "$SYNO_USERNAME" | _url_encode)"
encoded_password="$(printf "%s" "$SYNO_PASSWORD" | _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 # ## START ## - DEPRECATED, for backward compatibility
_getdeployconf SYNO_TOTP_SECRET _getdeployconf SYNO_TOTP_SECRET
@@ -173,7 +189,7 @@ synology_dsm_deploy() {
_debug3 H1 "${_H1}" _debug3 H1 "${_H1}"
fi 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" _debug3 response "$response"
# ## END ## - DEPRECATED, for backward compatibility # ## END ## - DEPRECATED, for backward compatibility
# If SYNO_DEVICE_ID or SYNO_OTP_CODE is set, we treat current account enabled 2FA-OTP. # 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="" otp_enforce_option=""
fi fi
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 if [ -n "$SYNO_USE_TEMP_ADMIN" ] && [ -n "$otp_enforce_option" ]; then
synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option" synosetkeyvalue /etc/synoinfo.conf otp_enforce_option "$otp_enforce_option"
_info "Restored previous enforce 2FA-OTP option." _info "Restored previous enforce 2FA-OTP option."
@@ -247,7 +263,7 @@ synology_dsm_deploy() {
if [ -n "$SYNO_DEVICE_ID" ]; then if [ -n "$SYNO_DEVICE_ID" ]; then
# Omit OTP code with SYNO_DEVICE_ID. # 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" _secure_debug3 response "$response"
else else
# Require the OTP code if still unset. # Require the OTP code if still unset.
@@ -260,7 +276,7 @@ synology_dsm_deploy() {
if [ -z "$SYNO_OTP_CODE" ]; then if [ -z "$SYNO_OTP_CODE" ]; then
response='{"error":{"code":404}}' response='{"error":{"code":404}}'
else 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" _secure_debug3 response "$response"
id_property='device_id' id_property='device_id'
@@ -302,14 +318,18 @@ synology_dsm_deploy() {
token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p') token=$(echo "$response" | grep "synotoken" | sed -n 's/.*"synotoken" *: *"\([^"]*\).*/\1/p')
_debug "Session ID" "$sid" _debug "Session ID" "$sid"
_debug SynoToken "$token" _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? # 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'." _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" _temp_admin_cleanup "$SYNO_USE_TEMP_ADMIN" "$SYNO_USERNAME"
return 1 return 1
fi fi
if [ -n "$_syno_use_token" ]; then
_H1="X-SYNO-TOKEN: $token" _H1="X-SYNO-TOKEN: $token"
else
_H1=""
fi
export _H1 export _H1
_debug2 H1 "${_H1}" _debug2 H1 "${_H1}"
@@ -381,8 +401,15 @@ synology_dsm_deploy() {
content="$(printf "%b_" "$content")" content="$(printf "%b_" "$content")"
content="${content%_}" # protect trailing \n 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." _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" _debug3 response "$response"
if ! echo "$response" | grep '"error":' >/dev/null; then if ! echo "$response" | grep '"error":' >/dev/null; then
+1 -1
View File
@@ -120,7 +120,7 @@ _baidu_run_with_fallback() {
} }
_baidu_engine_order() { _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 case "$_pref" in
legacy) legacy)
printf "%s" "legacy new" printf "%s" "legacy new"
+348
View File
@@ -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
}
+1 -1
View File
@@ -283,7 +283,7 @@ _bhosted_response_has_error() {
fi fi
# Fallback for unexpected/non-XML responses # 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 if _contains "$_resp_lc" "error"; then
_debug "Detected 'error' in response" _debug "Detected 'error' in response"
+213
View File
@@ -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
}
+164
View File
@@ -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
View File
@@ -114,21 +114,32 @@ _rm_record() {
full=$1 full=$1
txtvalue=$2 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" _err "Failed to retrieve records"
return 1 return 1
fi fi
# The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]} # The API returns: {"success":true,"data":{"records":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]},"message":"..."}
_txt_record_obj=$( _records="$(printf '%s\n' "$response" | tr -d '\r\n' | _egrep_o '\{[^{}]*\}')"
printf '%s\n' "$response" | _txt_record_obj=""
sed 's/^.*"data":\[//; s/\],"message":.*$//' | while IFS= read -r _rec || [ -n "$_rec" ]; do
awk '{ gsub(/},{/, "}\n{"); print }' | case "$_rec" in
grep -F "\"name\":\"$full\"" | *"\"name\":\"$full\""*) ;;
grep -F "\"type\":\"TXT\"" | *) continue ;;
grep -F "\"content\":\"$txtvalue\"" | esac
_head_n 1 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 if [ -z "$_txt_record_obj" ]; then
_info "TXT record not found for $full with content $txtvalue" _info "TXT record not found for $full with content $txtvalue"
@@ -173,7 +184,7 @@ _get_root() {
return 1 return 1
fi fi
_zones_response="$response" _zones="$(printf '%s\n' "$response" | tr -d '\r\n' | _egrep_o '\{[^{}]*\}')"
while true; do while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
@@ -183,12 +194,17 @@ _get_root() {
return 1 return 1
fi fi
zone_obj=$( zone_obj=""
printf '%s' "$_zones_response" | while IFS= read -r _zone || [ -n "$_zone" ]; do
sed 's/},{/}\n{/g' | case "$_zone" in
grep -F "\"name\":\"$h\"" | *"\"name\":\"$h\""*)
_head_n 1 zone_obj="$_zone"
) break
;;
esac
done <<EOF
$_zones
EOF
if [ -n "$zone_obj" ]; then if [ -n "$zone_obj" ]; then
_zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2) _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2)
+123 -41
View File
@@ -4,14 +4,15 @@ dns_selfhost_info='SelfHost.de
Site: SelfHost.de Site: SelfHost.de
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_selfhost
Options: Options:
SELFHOSTDNS_USERNAME Username SELFHOSTDNS_APIKEY API key
SELFHOSTDNS_PASSWORD Password
SELFHOSTDNS_MAP Subdomain name 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 Issues: github.com/acmesh-official/acme.sh/issues/4291
Author: Marvin Edeler Author: Marvin Edeler
' '
DEFAULT_SELFHOSTDNS_UPDATE_URL="https://my.selfhost.de/cgi-bin/dns-api.pl"
dns_selfhost_add() { dns_selfhost_add() {
fulldomain=$1 fulldomain=$1
txt=$2 txt=$2
@@ -19,43 +20,26 @@ dns_selfhost_add() {
_debug fulldomain "$fulldomain" _debug fulldomain "$fulldomain"
_debug txtvalue "$txt" _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 # Get values, but don't save until we successfully validated
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$(_readaccountconf_mutable SELFHOSTDNS_UPDATE_URL)}" if ! _selfhost_load_conf; then
SELFHOSTDNS_UPDATE_URL="${SELFHOSTDNS_UPDATE_URL:-$DEFAULT_SELFHOSTDNS_UPDATE_URL}" return 1
SELFHOSTDNS_USERNAME="${SELFHOSTDNS_USERNAME:-$(_readaccountconf_mutable SELFHOSTDNS_USERNAME)}" fi
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)}"
# Selfhost api can't dynamically add TXT record, # 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 # 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 ...') # (format: 'fulldomainA:lastRid fulldomainB:lastRid ...')
SELFHOSTDNS_MAP_LAST_USED_INTERNAL=$(_readdomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL) 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 # 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") lastUsedRidForDomainEntry=$(echo "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" | sed 's/^/ /' | sed -n "s/.*[ $_selfhost_tab]\($fulldomain:[0-9][0-9]*\).*/\1/p")
_debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry" _debug2 lastUsedRidForDomainEntry "$lastUsedRidForDomainEntry"
@@ -68,11 +52,7 @@ dns_selfhost_add() {
_info "Trying to add $txt on selfhost for rid: $rid" _info "Trying to add $txt on selfhost for rid: $rid"
data="?username=$SELFHOSTDNS_USERNAME&password=$SELFHOSTDNS_PASSWORD&rid=$rid&content=$txt" if ! _selfhost_api "present" "$rid" "$txt"; then
response="$(_get "$SELFHOSTDNS_UPDATE_URL$data")"
if ! echo "$response" | grep "200 OK" >/dev/null; then
_err "Invalid response of acme-dns for selfhost"
return 1 return 1
fi fi
@@ -96,11 +76,22 @@ dns_selfhost_add() {
fi fi
# Now that we know the values are good, save them # Now that we know the values are good, save them
_saveaccountconf_mutable SELFHOSTDNS_USERNAME "$SELFHOSTDNS_USERNAME" _saveaccountconf_mutable SELFHOSTDNS_APIKEY "$SELFHOSTDNS_APIKEY"
_saveaccountconf_mutable SELFHOSTDNS_PASSWORD "$SELFHOSTDNS_PASSWORD" # 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 # These values are domain dependent, so store them there
_savedomainconf SELFHOSTDNS_MAP "$SELFHOSTDNS_MAP" _savedomainconf SELFHOSTDNS_MAP "$SELFHOSTDNS_MAP"
_savedomainconf SELFHOSTDNS_MAP_LAST_USED_INTERNAL "$SELFHOSTDNS_MAP_LAST_USED_INTERNAL" _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() { dns_selfhost_rm() {
@@ -108,5 +99,96 @@ dns_selfhost_rm() {
txt=$2 txt=$2
_debug fulldomain "$fulldomain" _debug fulldomain "$fulldomain"
_debug txtvalue "$txt" _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
} }
+7 -4
View File
@@ -113,7 +113,7 @@ dns_yc_add() {
_info "Adding record" _info "Adding record"
if _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then 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 if _contains "$response" "\"done\": *true"; then
_info "Added, OK" _info "Added, OK"
return 0 return 0
else else
@@ -168,7 +168,7 @@ dns_yc_rm() {
# leaving any other values at the same name (e.g. base + wildcard domain) # 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. # 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 _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 _contains "$response" "\"done\": *true"; then
_info "Delete, OK" _info "Delete, OK"
return 0 return 0
else else
@@ -201,6 +201,8 @@ _get_root() {
if [ "$_domain" ]; then if [ "$_domain" ]; then
_cutlength=$((${#domain} - ${#_domain})) _cutlength=$((${#domain} - ${#_domain}))
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") _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 _domain_id=$YC_Zone_ID
return 0 return 0
else else
@@ -227,8 +229,9 @@ _get_root() {
echo "You didn't specify a Yandex Cloud Folder ID." echo "You didn't specify a Yandex Cloud Folder ID."
return 1 return 1
fi fi
if _contains "$response" "\"zone\": \"$h\""; then if _contains "$response" "\"zone\": *\"$h\""; then
_domain_id=$(echo "$response" | _normalizeJson | _egrep_o "[^{]*\"zone\":\"$h\"[^}]*" | _egrep_o "\"id\"[^,]*" | _egrep_o "[^:][^:]*$" | tr -d '"') # 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" _debug _domain_id "$_domain_id"
if [ "$_domain_id" ]; then if [ "$_domain_id" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+53
View File
@@ -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
}
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.