mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-10-22 13:42:05 +00:00
Compare commits
12
Commits
dev
..
3688b094ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3688b094ad | ||
|
|
807da64983 | ||
|
|
c37315c194 | ||
|
|
d5fc938d80 | ||
|
|
2f8e8ed085 | ||
|
|
181425b3c8 | ||
|
|
19980c86e8 | ||
|
|
9656842a30 | ||
|
|
a7e6ceaab0 | ||
|
|
261653ebf4 | ||
|
|
2feb392bd0 | ||
|
|
3661fd86b6 |
No files matched your search
@@ -666,15 +666,8 @@ _hex_dump() {
|
||||
xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " "
|
||||
else
|
||||
_debug3 "using _ascii_hex"
|
||||
#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
|
||||
str=$(cat)
|
||||
_ascii_hex "$str"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -914,12 +907,7 @@ _json_encode() {
|
||||
_j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
|
||||
_debug3 "_json_encode"
|
||||
_debug3 "_j_str" "$_j_str"
|
||||
#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"
|
||||
echo "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
|
||||
}
|
||||
|
||||
#from: http:\/\/ to http://
|
||||
@@ -927,7 +915,7 @@ _json_decode() {
|
||||
_j_str="$(sed 's#\\/#/#g')"
|
||||
_debug3 "_json_decode"
|
||||
_debug3 "_j_str" "$_j_str"
|
||||
printf "%s\n" "$_j_str"
|
||||
echo "$_j_str"
|
||||
}
|
||||
|
||||
#extract the authorization URLs from an order response on stdin, as a
|
||||
@@ -2926,7 +2914,6 @@ _setopt() {
|
||||
_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
|
||||
@@ -2943,13 +2930,6 @@ _setopt() {
|
||||
_debug3 APP
|
||||
__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
|
||||
@@ -2979,19 +2959,11 @@ _clear_conf() {
|
||||
_c_c_f="$1"
|
||||
_sdkey="$2"
|
||||
if [ "$_c_c_f" ]; then
|
||||
_c_c_old="$(cat "$_c_c_f")"
|
||||
_conf_data="$(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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Deploy hook to deploy a certificate to pfSense using the pfREST API
|
||||
# (https://pfrest.org). Tested with pfSense 2.9.0 and pfREST 2.10.2.
|
||||
#
|
||||
# The certificate is created under DEPLOY_PFSENSE_CERT_NAME, or key and
|
||||
# certificate are replaced in place if it already exists. Then it is
|
||||
# optionally assigned to the Unbound DNS-over-TLS listener and finally to the
|
||||
# web UI, which gets restarted (the API is briefly unreachable afterwards).
|
||||
#
|
||||
# Preparation on the pfSense:
|
||||
#
|
||||
# - Install pfREST and enable key authentication:
|
||||
# https://pfrest.org/INSTALL_AND_CONFIG/
|
||||
# https://pfrest.org/AUTHENTICATION_AND_AUTHORIZATION/
|
||||
# - Create the API key (System > REST API > Keys) with a user that has
|
||||
# "WebCfg - All pages". Afterwards use a dedicated user with only these
|
||||
# privileges, the key is bound to the user that creates it:
|
||||
# REST API - /api/v2/system/certificate PATCH
|
||||
# REST API - /api/v2/system/certificate POST
|
||||
# REST API - /api/v2/system/certificates GET
|
||||
# REST API - /api/v2/system/webgui/settings PATCH
|
||||
# REST API - /api/v2/services/dns_resolver/apply POST
|
||||
# REST API - /api/v2/services/dns_resolver/settings GET
|
||||
# REST API - /api/v2/services/dns_resolver/settings PATCH
|
||||
# - DNS-over-TLS only: "Enable SSL/TLS Service" must already be enabled in
|
||||
# Services > DNS Resolver, otherwise Unbound is skipped.
|
||||
#
|
||||
# Environment variables:
|
||||
#
|
||||
# DEPLOY_PFSENSE_APIKEY - (REQUIRED)
|
||||
# pfREST API key.
|
||||
# DEPLOY_PFSENSE_HOST - pfsense.example.com, 192.168.1.1, etc. (OPTIONAL)
|
||||
# Host/IP of the pfSense. Defaults to the
|
||||
# certificate domain, required for wildcard
|
||||
# certificates.
|
||||
# DEPLOY_PFSENSE_PORT - 443, 8443, etc. (OPTIONAL)
|
||||
# Web UI/API port. Defaults to 443.
|
||||
# DEPLOY_PFSENSE_CERT_NAME - My Certificate, etc. (OPTIONAL)
|
||||
# Certificate name shown in pfSense (printable
|
||||
# ASCII only). Defaults to the certificate
|
||||
# domain.
|
||||
# DEPLOY_PFSENSE_WEBUI - yes/no (OPTIONAL)
|
||||
# Assign to the web UI and restart it. Defaults
|
||||
# to "yes".
|
||||
# DEPLOY_PFSENSE_UNBOUND_DOT - yes/no (OPTIONAL)
|
||||
# Also assign to Unbound DNS-over-TLS. Defaults
|
||||
# to "no".
|
||||
# DEPLOY_PFSENSE_INSECURE - yes/no (OPTIONAL)
|
||||
# Do not verify the pfSense TLS certificate,
|
||||
# needed on the first run with the self-signed
|
||||
# default certificate. Defaults to "no".
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# export DEPLOY_PFSENSE_APIKEY="<api key>"
|
||||
# acme.sh --deploy -d pfsense.example.com --deploy-hook pfsense
|
||||
#
|
||||
# returns 0 means success, otherwise error.
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
#domain keyfile certfile cafile fullchain
|
||||
pfsense_deploy() {
|
||||
_cdomain="$1"
|
||||
_ckey="$2"
|
||||
_ccert="$3"
|
||||
_cca="$4"
|
||||
_cfullchain="$5"
|
||||
|
||||
_debug _cdomain "$_cdomain"
|
||||
_debug _ckey "$_ckey"
|
||||
_debug _ccert "$_ccert"
|
||||
_debug _cca "$_cca"
|
||||
_debug _cfullchain "$_cfullchain"
|
||||
|
||||
if [ ! -f "$_ckey" ] || [ ! -f "$_cfullchain" ]; then
|
||||
_err "Key or fullchain file not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_getdeployconf DEPLOY_PFSENSE_APIKEY
|
||||
if [ -z "$DEPLOY_PFSENSE_APIKEY" ]; then
|
||||
_err "DEPLOY_PFSENSE_APIKEY is not set."
|
||||
return 1
|
||||
fi
|
||||
_secure_debug2 DEPLOY_PFSENSE_APIKEY "$DEPLOY_PFSENSE_APIKEY"
|
||||
|
||||
_getdeployconf DEPLOY_PFSENSE_HOST
|
||||
_getdeployconf DEPLOY_PFSENSE_PORT
|
||||
_getdeployconf DEPLOY_PFSENSE_CERT_NAME
|
||||
_getdeployconf DEPLOY_PFSENSE_WEBUI
|
||||
_getdeployconf DEPLOY_PFSENSE_UNBOUND_DOT
|
||||
_getdeployconf DEPLOY_PFSENSE_INSECURE
|
||||
|
||||
if [ -z "$DEPLOY_PFSENSE_HOST" ]; then
|
||||
if _startswith "$_cdomain" "*"; then
|
||||
_err "DEPLOY_PFSENSE_HOST must be set for wildcard certificates."
|
||||
return 1
|
||||
fi
|
||||
DEPLOY_PFSENSE_HOST="$_cdomain"
|
||||
fi
|
||||
[ -n "$DEPLOY_PFSENSE_PORT" ] || DEPLOY_PFSENSE_PORT="443"
|
||||
[ -n "$DEPLOY_PFSENSE_CERT_NAME" ] || DEPLOY_PFSENSE_CERT_NAME="$_cdomain"
|
||||
[ -n "$DEPLOY_PFSENSE_WEBUI" ] || DEPLOY_PFSENSE_WEBUI="yes"
|
||||
[ -n "$DEPLOY_PFSENSE_UNBOUND_DOT" ] || DEPLOY_PFSENSE_UNBOUND_DOT="no"
|
||||
[ -n "$DEPLOY_PFSENSE_INSECURE" ] || DEPLOY_PFSENSE_INSECURE="no"
|
||||
|
||||
# pfREST returns non-ASCII as \uXXXX, which cannot be matched reliably
|
||||
if printf '%s' "$DEPLOY_PFSENSE_CERT_NAME" | LC_ALL=C grep -q '[^ -~]'; then
|
||||
_err "DEPLOY_PFSENSE_CERT_NAME must only contain printable ASCII characters."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# pfREST rejects these characters in a certificate description
|
||||
case "$DEPLOY_PFSENSE_CERT_NAME" in
|
||||
*[\?\>\<\&/\\\"\']*)
|
||||
_err "DEPLOY_PFSENSE_CERT_NAME must not contain any of: ? > < & / \\ \" '"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_debug2 DEPLOY_PFSENSE_HOST "$DEPLOY_PFSENSE_HOST"
|
||||
_debug2 DEPLOY_PFSENSE_PORT "$DEPLOY_PFSENSE_PORT"
|
||||
_debug2 DEPLOY_PFSENSE_CERT_NAME "$DEPLOY_PFSENSE_CERT_NAME"
|
||||
_debug2 DEPLOY_PFSENSE_WEBUI "$DEPLOY_PFSENSE_WEBUI"
|
||||
_debug2 DEPLOY_PFSENSE_UNBOUND_DOT "$DEPLOY_PFSENSE_UNBOUND_DOT"
|
||||
_debug2 DEPLOY_PFSENSE_INSECURE "$DEPLOY_PFSENSE_INSECURE"
|
||||
|
||||
_pfsense_api_url="https://${DEPLOY_PFSENSE_HOST}:${DEPLOY_PFSENSE_PORT}/api/v2"
|
||||
if [ "$DEPLOY_PFSENSE_INSECURE" = "yes" ]; then
|
||||
export HTTPS_INSECURE=1
|
||||
fi
|
||||
|
||||
_info "Looking up certificate '$DEPLOY_PFSENSE_CERT_NAME' on pfSense"
|
||||
_pfsense_call GET "/system/certificates?descr=$(printf '%s' "$DEPLOY_PFSENSE_CERT_NAME" | _url_encode)" || return 1
|
||||
# do not rely on the server-side filter, only accept an exact descr match
|
||||
_pf_match=""
|
||||
_pf_objs="$(printf '%s\n' "$_pfsense_response" | sed 's/} *, *{/}\
|
||||
{/g')"
|
||||
while IFS= read -r _pf_line; do
|
||||
case "$_pf_line" in
|
||||
*"\"descr\":\"$DEPLOY_PFSENSE_CERT_NAME\""*)
|
||||
_pf_match="$_pf_line"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <<EOF
|
||||
$_pf_objs
|
||||
EOF
|
||||
_pf_id="$(printf '%s' "$_pf_match" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d : -f 2)"
|
||||
_pf_refid="$(printf '%s' "$_pf_match" | _egrep_o '"refid":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
|
||||
_debug2 "existing id/refid" "$_pf_id/$_pf_refid"
|
||||
|
||||
_pf_crt="$(_json_encode <"$_cfullchain")"
|
||||
_pf_prv="$(_json_encode <"$_ckey")"
|
||||
|
||||
if [ -n "$_pf_id" ] && [ -n "$_pf_refid" ]; then
|
||||
_info "Updating existing certificate (refid $_pf_refid)"
|
||||
_pfsense_call PATCH "/system/certificate" \
|
||||
"{\"id\": $_pf_id, \"crt\": \"$_pf_crt\", \"prv\": \"$_pf_prv\"}" || return 1
|
||||
else
|
||||
_info "Creating new certificate"
|
||||
_pfsense_call POST "/system/certificate" \
|
||||
"{\"descr\": \"$DEPLOY_PFSENSE_CERT_NAME\", \"type\": \"server\", \"crt\": \"$_pf_crt\", \"prv\": \"$_pf_prv\"}" || return 1
|
||||
_pf_refid="$(printf '%s' "$_pfsense_response" | _egrep_o '"refid":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
|
||||
if [ -z "$_pf_refid" ]; then
|
||||
_err "Certificate was created but no refid was returned."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
_savedeployconf DEPLOY_PFSENSE_APIKEY "$DEPLOY_PFSENSE_APIKEY"
|
||||
_savedeployconf DEPLOY_PFSENSE_HOST "$DEPLOY_PFSENSE_HOST"
|
||||
_savedeployconf DEPLOY_PFSENSE_PORT "$DEPLOY_PFSENSE_PORT"
|
||||
_savedeployconf DEPLOY_PFSENSE_CERT_NAME "$DEPLOY_PFSENSE_CERT_NAME"
|
||||
_savedeployconf DEPLOY_PFSENSE_WEBUI "$DEPLOY_PFSENSE_WEBUI"
|
||||
_savedeployconf DEPLOY_PFSENSE_UNBOUND_DOT "$DEPLOY_PFSENSE_UNBOUND_DOT"
|
||||
_savedeployconf DEPLOY_PFSENSE_INSECURE "$DEPLOY_PFSENSE_INSECURE"
|
||||
|
||||
if [ "$DEPLOY_PFSENSE_UNBOUND_DOT" = "yes" ]; then
|
||||
_pfsense_unbound_dot "$_pf_refid" || return 1
|
||||
fi
|
||||
|
||||
if [ "$DEPLOY_PFSENSE_WEBUI" != "no" ]; then
|
||||
_info "Assigning certificate to the web UI and restarting it"
|
||||
_pfsense_call PATCH "/system/webgui/settings" "{\"sslcertref\": \"$_pf_refid\"}" || return 1
|
||||
fi
|
||||
|
||||
_info "Certificate successfully deployed to pfSense"
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
# method path [body] -> sets _pfsense_response, returns 1 on non-2xx
|
||||
_pfsense_call() {
|
||||
_pf_method="$1"
|
||||
_pf_path="$2"
|
||||
_pf_body="$3"
|
||||
|
||||
_H1="x-api-key: $DEPLOY_PFSENSE_APIKEY"
|
||||
export _H1
|
||||
_debug "$_pf_method" "$_pfsense_api_url$_pf_path"
|
||||
|
||||
if [ "$_pf_method" = "GET" ]; then
|
||||
_pfsense_response="$(_get "$_pfsense_api_url$_pf_path")"
|
||||
else
|
||||
_pfsense_response="$(_post "$_pf_body" "$_pfsense_api_url$_pf_path" "" "$_pf_method" "application/json")"
|
||||
fi
|
||||
_pf_ret=$?
|
||||
|
||||
_pf_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")"
|
||||
_pfsense_response="$(printf '%s' "$_pfsense_response" | tr -d '\r\n' | sed 's/" *: */":/g')"
|
||||
_debug2 "HTTP status" "$_pf_status"
|
||||
_secure_debug3 response "$_pfsense_response"
|
||||
|
||||
case "$_pf_status" in
|
||||
2[0-9][0-9])
|
||||
[ "$_pf_ret" = "0" ] && return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
_pf_msg="$(printf '%s' "$_pfsense_response" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d '"' -f 4)"
|
||||
_err "pfSense API call failed: $_pf_method $_pf_path (HTTP ${_pf_status:-none}). $_pf_msg"
|
||||
if [ "$_pf_status" = "401" ] || [ "$_pf_status" = "403" ] || [ -z "$_pf_status" ]; then
|
||||
_err "Check DEPLOY_PFSENSE_APIKEY, the user privileges and DEPLOY_PFSENSE_HOST/PORT. Use DEPLOY_PFSENSE_INSECURE=yes for an untrusted web UI certificate."
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# refid
|
||||
_pfsense_unbound_dot() {
|
||||
_pf_dot_refid="$1"
|
||||
|
||||
_info "Checking Unbound DNS-over-TLS"
|
||||
_pfsense_call GET "/services/dns_resolver/settings" || return 1
|
||||
if ! _contains "$_pfsense_response" '"enablessl":true'; then
|
||||
_info "SSL/TLS is not enabled in the DNS Resolver, skipping Unbound"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_info "Assigning certificate to Unbound and applying"
|
||||
_pfsense_call PATCH "/services/dns_resolver/settings" "{\"sslcertref\": \"$_pf_dot_refid\"}" || return 1
|
||||
_pfsense_call POST "/services/dns_resolver/apply" "{}" || return 1
|
||||
return 0
|
||||
}
|
||||
@@ -1,492 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
+6
-28
@@ -4,29 +4,18 @@
|
||||
# Deploy the HTTPS server certificate to a Shelly device on the local network.
|
||||
#
|
||||
# ```sh
|
||||
# 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
|
||||
# export SHELLY_HOST=192.168.1.100
|
||||
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
|
||||
# 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)
|
||||
@@ -34,7 +23,7 @@
|
||||
# - curl or wget
|
||||
# - openssl (for SHA-256 digest and random cnonce)
|
||||
#
|
||||
# The device must be reachable on the local network.
|
||||
# The device must be reachable via HTTP on the local network.
|
||||
# The hook uploads the fullchain.pem and private key,
|
||||
# then reboots the device to apply the new certificate.
|
||||
#
|
||||
@@ -63,13 +52,11 @@ 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."
|
||||
@@ -78,20 +65,11 @@ 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=""
|
||||
@@ -148,7 +126,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"}' \
|
||||
"${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
|
||||
# Detect auth from HTTP status line rather than response body
|
||||
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
|
||||
@@ -232,7 +210,7 @@ _shelly_rpc() {
|
||||
export _H1=""
|
||||
fi
|
||||
|
||||
_post "$_shelly_body" "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||
_shelly_ret=$?
|
||||
|
||||
if [ "$_shelly_ret" != "0" ]; then
|
||||
|
||||
+14
-40
@@ -189,19 +189,6 @@ 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
|
||||
@@ -241,9 +228,9 @@ _ssh_deploy() {
|
||||
_backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
|
||||
# run cleanup on the backup directory, erase all older
|
||||
# than 180 days (15552000 seconds).
|
||||
_ssh_add_step "now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
|
||||
_cmdstr="{ 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"
|
||||
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
|
||||
# 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."
|
||||
@@ -258,7 +245,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.
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -274,9 +261,10 @@ 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, 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"
|
||||
# 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;"
|
||||
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
|
||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||
@@ -294,7 +282,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.
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -316,7 +304,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
else
|
||||
# ssh echo to the file
|
||||
_ssh_add_step "echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -335,7 +323,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.
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -360,7 +348,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
else
|
||||
# ssh echo to the file
|
||||
_ssh_add_step "echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -380,7 +368,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.
|
||||
_ssh_add_step "if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -408,7 +396,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
else
|
||||
# ssh echo to the file
|
||||
_ssh_add_step "echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN"
|
||||
_cmdstr="$_cmdstr 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
|
||||
@@ -431,7 +419,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
||||
fi
|
||||
|
||||
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
|
||||
_ssh_add_step "$DEPLOY_SSH_REMOTE_CMD"
|
||||
_cmdstr="$_cmdstr $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
|
||||
@@ -451,20 +439,6 @@ 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"
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
+106
-269
@@ -4,316 +4,153 @@ dns_arvan_info='ArvanCloud.ir
|
||||
Site: ArvanCloud.ir
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_arvan
|
||||
Options:
|
||||
Arvan_Token API key, with or without the Apikey prefix
|
||||
Arvan_Token API Token
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/2796
|
||||
Author: Vahid Fardi, Abolfazl Rajabpour (abooraja)
|
||||
Author: Vahid Fardi
|
||||
'
|
||||
|
||||
ARVAN_API_URL="https://napi.arvancloud.ir/cdn/4.0/domains"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_arvan_add _acme-challenge.www.example.com "TXT value"
|
||||
#Usage: dns_arvan_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_arvan_add() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
_info "Using Arvan"
|
||||
|
||||
if ! _arvan_init; then
|
||||
Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}"
|
||||
|
||||
if [ -z "$Arvan_Token" ]; then
|
||||
_err "You didn't specify \"Arvan_Token\" token yet."
|
||||
_err "You can get yours from here https://npanel.arvancloud.ir/profile/api-keys"
|
||||
return 1
|
||||
fi
|
||||
if ! _saveaccountconf_mutable Arvan_Token "$Arvan_Token"; then
|
||||
_err "Unable to save the Arvan API credentials."
|
||||
#save the api token to the account conf file.
|
||||
_saveaccountconf_mutable Arvan_Token "$Arvan_Token"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
if ! _get_root "$fulldomain" || ! _arvan_find_record; then
|
||||
return 1
|
||||
fi
|
||||
if [ "$_arvan_record_ids" ]; then
|
||||
_info "TXT record already exists."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Append a separate record: apex and wildcard challenges can share a name.
|
||||
if ! _arvan_rest POST "$_domain/dns-records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":{\"text\":\"$txtvalue\"},\"ttl\":120}"; then
|
||||
_arvan_add_code=$_arvan_code
|
||||
# Another client may have added the same value since the lookup.
|
||||
case "$_arvan_add_code" in
|
||||
409 | 422)
|
||||
if _arvan_find_record && [ "$_arvan_record_ids" ]; then
|
||||
_info "TXT record already exists."
|
||||
return 0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
_err "Unable to add the Arvan TXT record (HTTP $_arvan_add_code)."
|
||||
return 1
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Adding record"
|
||||
if _arvan_rest POST "$_domain/dns-records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":{\"text\":\"$txtvalue\"},\"ttl\":120}"; then
|
||||
if _contains "$response" "$txtvalue"; then
|
||||
_info "response id is $response"
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
elif _contains "$response" "Record Data is duplicate"; then
|
||||
_info "Already exists, OK"
|
||||
return 0
|
||||
else
|
||||
_err "Add txt record error."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_info "TXT record added."
|
||||
_err "Add txt record error."
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: dns_arvan_rm _acme-challenge.www.example.com "TXT value"
|
||||
#Usage: fulldomain txtvalue
|
||||
#Remove the txt record after validation.
|
||||
dns_arvan_rm() {
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
_info "Using Arvan"
|
||||
_debug fulldomain "$fulldomain"
|
||||
_debug txtvalue "$txtvalue"
|
||||
|
||||
if ! _arvan_init || ! _get_root "$fulldomain" || ! _arvan_find_record; then
|
||||
Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}"
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$_arvan_record_ids" ]; then
|
||||
_info "TXT record is already absent."
|
||||
return 0
|
||||
_debug _domain_id "$_domain_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting txt records"
|
||||
_arvan_rest GET "${_domain}/dns-records"
|
||||
if ! printf "%s" "$response" | grep \"current_page\":1 >/dev/null; then
|
||||
_err "Error on Arvan Api"
|
||||
_err "Please create a github issue with debbug log"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Record IDs are validated before they are used as path components.
|
||||
for _arvan_record_id in $_arvan_record_ids; do
|
||||
if ! _arvan_rest DELETE "$_domain/dns-records/$_arvan_record_id"; then
|
||||
if [ "$_arvan_code" != "404" ]; then
|
||||
_err "Unable to remove the Arvan TXT record (HTTP $_arvan_code)."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
_info "TXT record removed."
|
||||
_record_id=$(echo "$response" | _egrep_o ".\"id\":\"[^\"]*\",\"type\":\"txt\",\"name\":\"_acme-challenge\",\"value\":{\"text\":\"$txtvalue\"}" | cut -d : -f 2 | cut -d , -f 1 | tr -d \")
|
||||
if ! _arvan_rest "DELETE" "${_domain}/dns-records/${_record_id}"; then
|
||||
_err "Error on Arvan Api"
|
||||
return 1
|
||||
fi
|
||||
_debug "$response"
|
||||
_contains "$response" 'dns record deleted'
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_arvan_init() {
|
||||
Arvan_Token="${Arvan_Token:-$(_readaccountconf_mutable Arvan_Token)}"
|
||||
Arvan_Token=${Arvan_Token#\"}
|
||||
Arvan_Token=${Arvan_Token%\"}
|
||||
if [ -z "$Arvan_Token" ]; then
|
||||
_err "You didn't specify Arvan_Token."
|
||||
_err "Get an API key from https://npanel.arvancloud.ir/profile/api-keys"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Find the longest managed suffix, including the full name for DNS alias mode.
|
||||
#_acme-challenge.www.domain.com
|
||||
#returns
|
||||
# _sub_domain=_acme-challenge.www
|
||||
# _domain=domain.com
|
||||
# _domain_id=sdjkglgdfewsdfg
|
||||
_get_root() {
|
||||
domain=$(printf "%s" "$1" | _lower_case)
|
||||
domain=${domain%.}
|
||||
_domain=""
|
||||
_sub_domain=""
|
||||
case "$domain" in
|
||||
"" | *[!a-z0-9_.-]* | .* | *..*)
|
||||
_err "Invalid DNS name."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! _arvan_list ""; then
|
||||
return 1
|
||||
fi
|
||||
if ! _arvan_zones=$(_arvan_get_data domains); then
|
||||
_err "Invalid Arvan domains response."
|
||||
return 1
|
||||
fi
|
||||
h=$domain
|
||||
domain=$1
|
||||
i=2
|
||||
p=1
|
||||
while true; do
|
||||
case "
|
||||
$_arvan_zones
|
||||
" in
|
||||
*"
|
||||
$h
|
||||
"*)
|
||||
_domain=$h
|
||||
_sub_domain=${domain%."$h"}
|
||||
if [ "$domain" = "$h" ]; then
|
||||
_sub_domain="@"
|
||||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||
_debug h "$h"
|
||||
if [ -z "$h" ]; then
|
||||
#not valid
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _arvan_rest GET "$h"; then
|
||||
return 1
|
||||
fi
|
||||
if _contains "$response" "\"domain\":\"$h\""; then
|
||||
_domain_id=$(echo "$response" | cut -d : -f 3 | cut -d , -f 1 | tr -d \")
|
||||
if [ "$_domain_id" ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
_domain=$h
|
||||
return 0
|
||||
fi
|
||||
_debug _domain "$_domain"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
case "$h" in
|
||||
*.*) h=${h#*.} ;;
|
||||
*) break ;;
|
||||
esac
|
||||
return 1
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
_err "No matching zone found in the Arvan account."
|
||||
return 1
|
||||
}
|
||||
|
||||
_arvan_find_record() {
|
||||
_arvan_record_ids=""
|
||||
if ! _arvan_list "$_domain/dns-records?type=txt"; then
|
||||
return 1
|
||||
fi
|
||||
if ! _arvan_record_ids=$(_arvan_get_data records); then
|
||||
_err "Invalid Arvan DNS records response."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Collect all pages before matching, so a delegated zone on a later page wins.
|
||||
_arvan_list() {
|
||||
_arvan_path=$1
|
||||
_arvan_page=1
|
||||
_arvan_result=""
|
||||
while true; do
|
||||
case "$_arvan_path" in
|
||||
*\?*) _arvan_page_endpoint="$_arvan_path&page=$_arvan_page" ;;
|
||||
*) _arvan_page_endpoint="$_arvan_path?page=$_arvan_page" ;;
|
||||
esac
|
||||
if ! _arvan_rest GET "$_arvan_page_endpoint"; then
|
||||
_err "Unable to list Arvan resources (HTTP $_arvan_code)."
|
||||
return 1
|
||||
fi
|
||||
_arvan_current_page=$(printf "%s" "$response" | _egrep_o '"current_page"[ ]*:[ ]*[0-9][0-9]*' | cut -d : -f 2 | tr -d ' \t')
|
||||
_arvan_last_page=$(printf "%s" "$response" | _egrep_o '"last_page"[ ]*:[ ]*[0-9][0-9]*' | cut -d : -f 2 | tr -d ' \t')
|
||||
case "$_arvan_last_page" in
|
||||
"" | *[!0-9]*)
|
||||
_err "Invalid Arvan pagination."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [ "$_arvan_current_page" != "$_arvan_page" ] || [ "$_arvan_last_page" -lt "$_arvan_page" ]; then
|
||||
_err "Unexpected Arvan page number."
|
||||
return 1
|
||||
fi
|
||||
_arvan_result="$_arvan_result
|
||||
$response"
|
||||
if [ "$_arvan_page" -eq "$_arvan_last_page" ]; then
|
||||
response=$_arvan_result
|
||||
return 0
|
||||
fi
|
||||
_arvan_page=$(_math "$_arvan_page" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
# Read only fields in data[] objects, independently of JSON field order.
|
||||
# Tokenize quoted strings first so braces in unrelated TXT values stay intact.
|
||||
_arvan_get_data() {
|
||||
_arvan_mode=$1
|
||||
printf "%s\n" "$response" | sed 's/\("[^"\\]*\(\\.[^"\\]*\)*"\)/\
|
||||
\1\
|
||||
/g' | sed '/^"/!{
|
||||
s/[ ]//g
|
||||
s/\([][{}:,]\)/\
|
||||
\1\
|
||||
/g
|
||||
}
|
||||
/^$/d' | (
|
||||
_arvan_depth=0
|
||||
_arvan_key=""
|
||||
_arvan_in_data=""
|
||||
_arvan_in_value=""
|
||||
_arvan_seen_data=""
|
||||
while IFS= read -r _arvan_token; do
|
||||
case "$_arvan_token" in
|
||||
"{" | "[")
|
||||
if [ "$_arvan_depth" = "1" ] && [ "$_arvan_key" = '"data"' ] && [ "$_arvan_token" = "[" ]; then
|
||||
_arvan_in_data=1
|
||||
_arvan_seen_data=1
|
||||
elif [ "$_arvan_in_data" ] && [ "$_arvan_depth" = "2" ]; then
|
||||
_arvan_id=""
|
||||
_arvan_type=""
|
||||
_arvan_name=""
|
||||
_arvan_text=""
|
||||
_arvan_zone=""
|
||||
elif [ "$_arvan_in_data" ] && [ "$_arvan_depth" = "3" ] && [ "$_arvan_key" = '"value"' ] && [ "$_arvan_token" = "{" ]; then
|
||||
_arvan_in_value=1
|
||||
fi
|
||||
_arvan_depth=$(_math "$_arvan_depth" + 1)
|
||||
_arvan_key=""
|
||||
;;
|
||||
"}" | "]")
|
||||
if [ "$_arvan_in_data" ] && [ "$_arvan_depth" = "3" ] && [ "$_arvan_token" = "}" ]; then
|
||||
if [ "$_arvan_mode" = "domains" ]; then
|
||||
if [ "$_arvan_zone" ]; then
|
||||
printf "%s\n" "$_arvan_zone"
|
||||
fi
|
||||
elif [ "$_arvan_name" = "$_sub_domain" ] && [ "$_arvan_text" = "$txtvalue" ]; then
|
||||
case "$_arvan_type" in
|
||||
[Tt][Xx][Tt])
|
||||
case "$_arvan_id" in
|
||||
"" | *[!a-zA-Z0-9-]*) return 1 ;;
|
||||
esac
|
||||
printf "%s\n" "$_arvan_id"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
elif [ "$_arvan_depth" = "2" ]; then
|
||||
_arvan_in_data=""
|
||||
fi
|
||||
if [ "$_arvan_depth" = "4" ]; then
|
||||
_arvan_in_value=""
|
||||
fi
|
||||
_arvan_depth=$(_math "$_arvan_depth" - 1)
|
||||
_arvan_key=""
|
||||
;;
|
||||
"" | ":") ;;
|
||||
",") _arvan_key="" ;;
|
||||
*)
|
||||
if [ -z "$_arvan_key" ]; then
|
||||
_arvan_key=$_arvan_token
|
||||
else
|
||||
_arvan_value=${_arvan_token#\"}
|
||||
_arvan_value=${_arvan_value%\"}
|
||||
if [ "$_arvan_in_data" ]; then
|
||||
case "$_arvan_depth:$_arvan_key" in
|
||||
'3:"domain"') _arvan_zone=$_arvan_value ;;
|
||||
'3:"id"') _arvan_id=$_arvan_value ;;
|
||||
'3:"type"') _arvan_type=$_arvan_value ;;
|
||||
'3:"name"') _arvan_name=$_arvan_value ;;
|
||||
'4:"text"')
|
||||
if [ "$_arvan_in_value" ]; then
|
||||
_arvan_text=$_arvan_value
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
_arvan_key=""
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ "$_arvan_depth" = "0" ] && [ "$_arvan_seen_data" = "1" ]
|
||||
)
|
||||
}
|
||||
|
||||
_arvan_rest() {
|
||||
_arvan_method=$1
|
||||
_arvan_endpoint=$2
|
||||
_arvan_data=$3
|
||||
_arvan_code=""
|
||||
_arvan_auth=$Arvan_Token
|
||||
# Preserve existing authorization values; accept a bare UUID API key too.
|
||||
case "$_arvan_auth" in
|
||||
????????-????-????-????-????????????) _arvan_auth="Apikey $_arvan_auth" ;;
|
||||
esac
|
||||
export _H1="Authorization: $_arvan_auth"
|
||||
export _H2="Accept: application/json"
|
||||
export _H3=""
|
||||
export _H4=""
|
||||
export _H5=""
|
||||
if [ "$_arvan_method" = "POST" ]; then
|
||||
export _H3="Content-Type: application/json"
|
||||
fi
|
||||
case "$_arvan_endpoint" in
|
||||
"" | \?*) _arvan_url="$ARVAN_API_URL$_arvan_endpoint" ;;
|
||||
*) _arvan_url="$ARVAN_API_URL/$_arvan_endpoint" ;;
|
||||
esac
|
||||
mtd="$1"
|
||||
ep="$2"
|
||||
data="$3"
|
||||
|
||||
if [ "$_arvan_method" = "GET" ]; then
|
||||
if ! response=$(_get "$_arvan_url"); then
|
||||
_err "Arvan API request failed."
|
||||
return 1
|
||||
fi
|
||||
elif ! response=$(_post "$_arvan_data" "$_arvan_url" "" "$_arvan_method"); then
|
||||
_err "Arvan API request failed."
|
||||
return 1
|
||||
token_trimmed=$(echo "$Arvan_Token" | tr -d '"')
|
||||
export _H1="Authorization: $token_trimmed"
|
||||
|
||||
if [ "$mtd" = "DELETE" ]; then
|
||||
#DELETE Request shouldn't have Content-Type
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")"
|
||||
elif [ "$mtd" = "POST" ]; then
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Accept: application/json"
|
||||
_debug data "$data"
|
||||
response="$(_post "$data" "$ARVAN_API_URL/$ep" "" "$mtd")"
|
||||
else
|
||||
response="$(_get "$ARVAN_API_URL/$ep$data")"
|
||||
fi
|
||||
_arvan_code=$(grep '^HTTP/' "$HTTP_HEADER" | _tail_n 1 | cut -d ' ' -f 2 | tr -d '\r\n')
|
||||
_debug "Arvan HTTP status" "$_arvan_code"
|
||||
_secure_debug2 response "$response"
|
||||
response=$(printf "%s" "$response" | _normalizeJson)
|
||||
case "$_arvan_code" in
|
||||
2[0-9][0-9]) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
return 0
|
||||
}
|
||||
+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="$(_idn "$1")"
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
||||
@@ -109,7 +109,7 @@ dns_cf_add() {
|
||||
|
||||
#fulldomain txtvalue
|
||||
dns_cf_rm() {
|
||||
fulldomain="$(_idn "$1")"
|
||||
fulldomain=$1
|
||||
txtvalue=$2
|
||||
|
||||
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# shellcheck disable=SC2034
|
||||
dns_connbyte_info='ConnByte
|
||||
Site: connbyte.com
|
||||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_connbyte
|
||||
Options:
|
||||
CONNBYTE_Token API Token. Create it in the ConnByte customer panel, see https://connbyte.com/
|
||||
CONNBYTE_TTL TTL of the TXT record in seconds. Optional. Default "120".
|
||||
Issues: github.com/acmesh-official/acme.sh/issues/7342
|
||||
Author: ConnByte <team@connbyte.com>
|
||||
'
|
||||
|
||||
CONNBYTE_Api="https://api.connbyte.com"
|
||||
|
||||
######## Public functions #####################
|
||||
|
||||
# Usage: dns_connbyte_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_connbyte_add() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue="$2"
|
||||
|
||||
if ! _connbyte_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_debug "First detect the root zone"
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _zone_id "$_zone_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_info "Adding TXT record for $fulldomain"
|
||||
data="{\"zoneId\":\"$_zone_id\",\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":$CONNBYTE_TTL,\"priority\":0,\"disabled\":false}"
|
||||
if ! _connbyte_rest "ns/record/create" "$data"; then
|
||||
_err "Could not add TXT record for $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_info "TXT record added"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Usage: dns_connbyte_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_connbyte_rm() {
|
||||
fulldomain="$(echo "$1" | _lower_case)"
|
||||
txtvalue="$2"
|
||||
|
||||
if ! _connbyte_init; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! _get_root "$fulldomain"; then
|
||||
_err "invalid domain: $fulldomain"
|
||||
return 1
|
||||
fi
|
||||
_debug _zone_id "$_zone_id"
|
||||
_debug _sub_domain "$_sub_domain"
|
||||
|
||||
if ! _connbyte_rest "ns/record/list" "{\"zoneId\":\"$_zone_id\"}"; then
|
||||
_err "Could not list records of zone $_domain"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Each record is an innermost JSON object, e.g. (TXT content may be stored quoted)
|
||||
# {"id":435,"name":"_acme-challenge.example.com","type":"TXT","content":"...","ttl":120,"priority":0,"disabled":0}
|
||||
# Split on braces with tr: the egrep on illumos (OmniOS) does not handle the "\{[^{}]*\}" pattern.
|
||||
record_ids="$(echo "$response" | tr -d '\n\r' | tr '{}' '\n' |
|
||||
grep "\"type\" *: *\"TXT\"" |
|
||||
grep "\"name\" *: *\"$fulldomain\"" |
|
||||
grep "\"$txtvalue" |
|
||||
_egrep_o "\"id\" *: *[0-9]+" | _egrep_o "[0-9]+")"
|
||||
_debug record_ids "$record_ids"
|
||||
|
||||
if [ -z "$record_ids" ]; then
|
||||
_info "TXT record not found, nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for record_id in $record_ids; do
|
||||
_info "Removing TXT record $record_id"
|
||||
if ! _connbyte_rest "ns/record/delete" "{\"zoneId\":\"$_zone_id\",\"recordId\":$record_id}"; then
|
||||
_err "Could not remove TXT record $record_id"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
#################### Private functions below ##################################
|
||||
|
||||
_connbyte_init() {
|
||||
CONNBYTE_Token="${CONNBYTE_Token:-$(_readaccountconf_mutable CONNBYTE_Token)}"
|
||||
CONNBYTE_TTL="${CONNBYTE_TTL:-$(_readaccountconf_mutable CONNBYTE_TTL)}"
|
||||
|
||||
if [ -z "$CONNBYTE_Token" ]; then
|
||||
CONNBYTE_Token=""
|
||||
_err "You did not specify the ConnByte API token."
|
||||
_err "Please export CONNBYTE_Token and try again."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -z "$CONNBYTE_TTL" ]; then
|
||||
CONNBYTE_TTL=120
|
||||
fi
|
||||
case "$CONNBYTE_TTL" in
|
||||
'' | *[!0-9]*)
|
||||
_err "CONNBYTE_TTL must be a number"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_saveaccountconf_mutable CONNBYTE_Token "$CONNBYTE_Token"
|
||||
_saveaccountconf_mutable CONNBYTE_TTL "$CONNBYTE_TTL"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _acme-challenge.www.domain.com
|
||||
# returns
|
||||
# _sub_domain=_acme-challenge.www ("@" for the zone apex)
|
||||
# _domain=domain.com
|
||||
# _zone_id=01DXF6DT00ZZZHQPP3JFJS8VEE
|
||||
_get_root() {
|
||||
domain=$1
|
||||
|
||||
if ! _connbyte_rest "ns/zone/list" "{}"; then
|
||||
return 1
|
||||
fi
|
||||
zones="$response"
|
||||
|
||||
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
|
||||
return 1
|
||||
fi
|
||||
|
||||
zone_id="$(echo "$zones" | tr -d '\n\r' | tr '{}' '\n' |
|
||||
grep "\"zone\" *: *\"$h\"" | _head_n 1 |
|
||||
_egrep_o "\"id\" *: *\"[0-9A-Za-z]+\"" | cut -d '"' -f 4)"
|
||||
if [ -n "$zone_id" ]; then
|
||||
_zone_id="$zone_id"
|
||||
_domain="$h"
|
||||
if [ "$i" -gt 1 ]; then
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||
else
|
||||
# The API rejects an empty name and uses "@" for the zone apex (DNS alias mode).
|
||||
_sub_domain="@"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
p=$i
|
||||
i=$(_math "$i" + 1)
|
||||
done
|
||||
}
|
||||
|
||||
# Usage: _connbyte_rest path json_body
|
||||
# Sets $response. Returns 1 on transport error or when the API status code is not 2xx.
|
||||
_connbyte_rest() {
|
||||
ep="$1"
|
||||
data="$2"
|
||||
|
||||
export _H1="Accept: application/json"
|
||||
export _H2="Content-Type: application/json"
|
||||
export _H3="Authorization: Bearer $CONNBYTE_Token"
|
||||
|
||||
_debug ep "$ep"
|
||||
_debug2 data "$data"
|
||||
response="$(_post "$data" "$CONNBYTE_Api/$ep" "" "POST")"
|
||||
ret="$?"
|
||||
_debug2 response "$response"
|
||||
|
||||
if [ "$ret" != "0" ]; then
|
||||
_err "error $ep"
|
||||
return 1
|
||||
fi
|
||||
|
||||
status_code="$(echo "$response" | tr -d '\n\r' | _egrep_o "\"status\" *: *\{ *\"code\" *: *[0-9]+" | _egrep_o "[0-9]+$")"
|
||||
_debug status_code "$status_code"
|
||||
case "$status_code" in
|
||||
2*) return 0 ;;
|
||||
esac
|
||||
|
||||
_err "ConnByte API error: $response"
|
||||
return 1
|
||||
}
|
||||
+5
-10
@@ -15,7 +15,7 @@ DNSimple_API="https://api.dnsimple.com/v2"
|
||||
|
||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||
dns_dnsimple_add() {
|
||||
fulldomain=$(echo "$1" | _lower_case)
|
||||
fulldomain=$1
|
||||
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=$(echo "$1" | _lower_case)
|
||||
fulldomain=$1
|
||||
|
||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||
@@ -110,13 +110,9 @@ _get_root() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
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
|
||||
if _contains "$response" 'not found'; then
|
||||
_debug "$h not found"
|
||||
else
|
||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
|
||||
_domain="$h"
|
||||
|
||||
@@ -125,7 +121,6 @@ _get_root() {
|
||||
|
||||
return 0
|
||||
fi
|
||||
_debug "$h not found"
|
||||
|
||||
previous="$i"
|
||||
i=$(_math "$i" + 1)
|
||||
|
||||
@@ -59,13 +59,6 @@ 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"
|
||||
@@ -87,20 +80,6 @@ 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}" &&
|
||||
|
||||
@@ -185,14 +185,13 @@ _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_mutable NAMECHEAP_SOURCEIP "$NAMECHEAP_SOURCEIP"
|
||||
_saveaccountconf 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}')
|
||||
@@ -253,8 +252,6 @@ _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."
|
||||
@@ -268,8 +265,8 @@ _namecheap_check_config() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
_saveaccountconf_mutable NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
|
||||
_saveaccountconf_mutable NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
|
||||
_saveaccountconf NAMECHEAP_API_KEY "$NAMECHEAP_API_KEY"
|
||||
_saveaccountconf NAMECHEAP_USERNAME "$NAMECHEAP_USERNAME"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
+3
-6
@@ -106,16 +106,13 @@ dns_yc_add() {
|
||||
_debug _domain "$_domain"
|
||||
|
||||
_debug "Getting txt records"
|
||||
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$fulldomain"; then
|
||||
if ! _yc_rest GET "zones/${_domain_id}:getRecordSet?type=TXT&name=$_sub_domain"; then
|
||||
_err "Error: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
_info "Adding record"
|
||||
# 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 _yc_rest POST "zones/$_domain_id:upsertRecordSets" "{\"merges\": [ { \"name\":\"$_sub_domain\",\"type\":\"TXT\",\"ttl\":\"120\",\"data\":[\"$txtvalue\"]}]}"; then
|
||||
if _contains "$response" "\"done\": *true"; then
|
||||
_info "Added, OK"
|
||||
return 0
|
||||
@@ -170,7 +167,7 @@ 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\":\"$fulldomain\",\"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
|
||||
_info "Delete, OK"
|
||||
return 0
|
||||
|
||||