mirror of
https://github.com/acmesh-official/acme.sh.git
synced 2026-10-21 13:31:18 +00:00
7 files changed
+787
-28
No files matched your search
@@ -2914,6 +2914,7 @@ _setopt() {
|
|||||||
_err "Cannot read $__conf."
|
_err "Cannot read $__conf."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
__old_text="$__text"
|
||||||
#build the new content first and write it once through _write_conf:
|
#build the new content first and write it once through _write_conf:
|
||||||
#redirecting straight into the conf truncates it before anything is
|
#redirecting straight into the conf truncates it before anything is
|
||||||
#written, so a failing sed (#2426) or a failing write (#7247) left a
|
#written, so a failing sed (#2426) or a failing write (#7247) left a
|
||||||
@@ -2930,6 +2931,13 @@ _setopt() {
|
|||||||
_debug3 APP
|
_debug3 APP
|
||||||
__text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
|
__text="$__text${__text:+$__nl}$__opt$__sep$__val$__end"
|
||||||
fi
|
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
|
if [ "$__sed_err" ] || ! _write_conf "$__conf" "$__text"; then
|
||||||
_err "Cannot save '$__opt' to $__conf."
|
_err "Cannot save '$__opt' to $__conf."
|
||||||
return 1
|
return 1
|
||||||
@@ -2959,11 +2967,19 @@ _clear_conf() {
|
|||||||
_c_c_f="$1"
|
_c_c_f="$1"
|
||||||
_sdkey="$2"
|
_sdkey="$2"
|
||||||
if [ "$_c_c_f" ]; then
|
if [ "$_c_c_f" ]; then
|
||||||
_conf_data="$(cat "$_c_c_f")"
|
_c_c_old="$(cat "$_c_c_f")"
|
||||||
#printf, not echo: dash's builtin echo interprets backslash escapes and
|
#printf, not echo: dash's builtin echo interprets backslash escapes and
|
||||||
#would corrupt saved values that contain them on every rewrite
|
#would corrupt saved values that contain them on every rewrite
|
||||||
if ! _conf_data="$(printf -- "%s\n" "$_conf_data" | sed "/^$_sdkey *=.*$/d")" ||
|
if ! _conf_data="$(printf -- "%s\n" "$_c_c_old" | sed "/^$_sdkey *=.*$/d")"; then
|
||||||
! _write_conf "$_c_c_f" "$_conf_data"; then
|
_err "Cannot clear '$_sdkey' in $_c_c_f."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
#the key is not there, the usual case for the legacy keys cleared on
|
||||||
|
#every _savedeployconf: nothing to write (#7335)
|
||||||
|
if [ "$_conf_data" = "$_c_c_old" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! _write_conf "$_c_c_f" "$_conf_data"; then
|
||||||
_err "Cannot clear '$_sdkey' in $_c_c_f."
|
_err "Cannot clear '$_sdkey' in $_c_c_f."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Script to deploy certificates to compatible BMCs (baseboard management
|
||||||
|
# controllers) using the [Redfish API](https://www.dmtf.org/standards/redfish).
|
||||||
|
#
|
||||||
|
# Environment Variables:
|
||||||
|
#
|
||||||
|
# ```sh
|
||||||
|
# DEPLOY_REDFISH_HOST="ipmi.example.com" # Required
|
||||||
|
# DEPLOY_REDFISH_USERNAME="Administrator" # Required
|
||||||
|
# DEPLOY_REDFISH_PASSWORD="superuser" # Required
|
||||||
|
# DEPLOY_REDFISH_USE_BASIC_AUTH=0 # Optional (set to "1" to enable)
|
||||||
|
# DEPLOY_REDFISH_RESTART=0 # Optional (set to "1" to enable)
|
||||||
|
# DEPLOY_REDFISH_MANAGER= # Optional (e.g. "/redfish/v1/Managers/Self")
|
||||||
|
# DEPLOY_REDFISH_CERTIFICATE= # Optional (e.g. "/redfish/v1/CertificateService/Certificates/1")
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Compatibility:
|
||||||
|
#
|
||||||
|
# Compatible with any IPMI/BMC solution supporting Redfish v1.6.1 or newer (or
|
||||||
|
# Redfish Schema Bundle 2018.3), as defined in the following documents:
|
||||||
|
#
|
||||||
|
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP0266_1.6.1.pdf>
|
||||||
|
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2018.3.pdf>
|
||||||
|
# <https://www.dmtf.org/sites/default/files/standards/documents/DSP2059_1.0.0.pdf>
|
||||||
|
#
|
||||||
|
# This script is confirmed to work on the following systems:
|
||||||
|
#
|
||||||
|
# | IPMI Solution | BMC Chipset | Firmware Family | Redfish Version |
|
||||||
|
# |------------------|----------------|------------------|-----------------|
|
||||||
|
# | ASUS ASMB12-iKVM | ASPEED AST2600 | AMI MegaRAC SP-X | 1.15.1 |
|
||||||
|
#
|
||||||
|
# However, this should also work with HPE iLO 5 v2.42 (or newer), Dell iDRAC9
|
||||||
|
# v5.0 (or newer), or any ASUS/Lenovo/Supermicro IPMI solution based on AMI
|
||||||
|
# MegaRAC SP-X v12.x (or newer).
|
||||||
|
|
||||||
|
#domain keyfile certfile cafile fullchain
|
||||||
|
redfish_deploy() {
|
||||||
|
_cdomain="$1"
|
||||||
|
_ckey="$2"
|
||||||
|
_ccert="$3"
|
||||||
|
_cca="$4"
|
||||||
|
_cfullchain="$5"
|
||||||
|
|
||||||
|
_debug _cdomain "$_cdomain"
|
||||||
|
_debug _ckey "$_ckey"
|
||||||
|
_debug _ccert "$_ccert"
|
||||||
|
_debug _cca "$_cca"
|
||||||
|
_debug _cfullchain "$_cfullchain"
|
||||||
|
|
||||||
|
_getdeployconf DEPLOY_REDFISH_HOST
|
||||||
|
_getdeployconf DEPLOY_REDFISH_USERNAME
|
||||||
|
_getdeployconf DEPLOY_REDFISH_PASSWORD
|
||||||
|
_getdeployconf DEPLOY_REDFISH_USE_BASIC_AUTH
|
||||||
|
_getdeployconf DEPLOY_REDFISH_RESTART
|
||||||
|
_getdeployconf DEPLOY_REDFISH_MANAGER
|
||||||
|
_getdeployconf DEPLOY_REDFISH_CERTIFICATE
|
||||||
|
|
||||||
|
if [ -z "${DEPLOY_REDFISH_HOST}" ]; then
|
||||||
|
_err 'DEPLOY_REDFISH_HOST must be set. Please specify the IP or domain name of the Redfish server.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${DEPLOY_REDFISH_USERNAME}" ] || [ -z "${DEPLOY_REDFISH_PASSWORD}" ]; then
|
||||||
|
_err 'DEPLOY_REDFISH_USERNAME and DEPLOY_REDFISH_PASSWORD must be set.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_REDFISH_USE_BASIC_AUTH="${DEPLOY_REDFISH_USE_BASIC_AUTH:-0}"
|
||||||
|
DEPLOY_REDFISH_RESTART="${DEPLOY_REDFISH_RESTART:-0}"
|
||||||
|
|
||||||
|
_debug DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}"
|
||||||
|
_debug DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}"
|
||||||
|
_secure_debug DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}"
|
||||||
|
_debug DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}"
|
||||||
|
|
||||||
|
_redfish_log_in "$DEPLOY_REDFISH_USERNAME" "$DEPLOY_REDFISH_PASSWORD" || return 1
|
||||||
|
|
||||||
|
# Credentials are proven correct now -- save them, rather than only at the
|
||||||
|
# very end, so a later step failing doesn't discard a working login.
|
||||||
|
_savedeployconf DEPLOY_REDFISH_HOST "${DEPLOY_REDFISH_HOST}"
|
||||||
|
_savedeployconf DEPLOY_REDFISH_USERNAME "${DEPLOY_REDFISH_USERNAME}"
|
||||||
|
_savedeployconf DEPLOY_REDFISH_PASSWORD "${DEPLOY_REDFISH_PASSWORD}" 'base64'
|
||||||
|
_savedeployconf DEPLOY_REDFISH_USE_BASIC_AUTH "${DEPLOY_REDFISH_USE_BASIC_AUTH}"
|
||||||
|
|
||||||
|
if ! _redfish_get_certificate_service_endpoint; then
|
||||||
|
_err "Redfish server ${DEPLOY_REDFISH_HOST} doesn't support certificate management API."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}"
|
||||||
|
_debug DEPLOY_REDFISH_MANAGER "${DEPLOY_REDFISH_MANAGER}"
|
||||||
|
_debug DEPLOY_REDFISH_CERTIFICATE "${DEPLOY_REDFISH_CERTIFICATE}"
|
||||||
|
|
||||||
|
if [ -n "${DEPLOY_REDFISH_MANAGER}" ]; then
|
||||||
|
_manager_endpoint="${DEPLOY_REDFISH_MANAGER}"
|
||||||
|
else
|
||||||
|
_info 'Identifying REST endpoint of primary Redfish manager.'
|
||||||
|
_redfish_get_primary_manager_endpoint || return 1
|
||||||
|
_savedeployconf DEPLOY_REDFISH_MANAGER "${_manager_endpoint}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${DEPLOY_REDFISH_CERTIFICATE}" ]; then
|
||||||
|
_certificate_endpoint="${DEPLOY_REDFISH_CERTIFICATE}"
|
||||||
|
else
|
||||||
|
_info "Identifying REST endpoint of primary TLS certificate on: ${_manager_endpoint}"
|
||||||
|
_redfish_get_manager_certificate_endpoint "${_manager_endpoint}" || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "${_ckey}" ] && [ -f "${_cfullchain}" ]; then
|
||||||
|
_info "Uploading private key and full certificate chain to: ${_certificate_endpoint}"
|
||||||
|
|
||||||
|
if ! _redfish_supports_full_certificate_chains; then
|
||||||
|
_err 'Redfish server does not support uploading full certificate chains!'
|
||||||
|
_err "Please call 'GenerateCSR' API on server first, pass the CSR to 'acme.sh --sign-csr --csr <key.pem>', and try again."
|
||||||
|
_err 'For more information: https://github.com/acmesh-official/acme.sh/wiki/deployhooks#generate-csr-on-server-optional'
|
||||||
|
return 1
|
||||||
|
elif ! _redfish_supports_private_key "${_ckey}" "${_certificate_service_endpoint}"; then
|
||||||
|
_err "Redfish server does not support this type of private key."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_ckey_pkcs8="$("${ACME_OPENSSL_BIN:-openssl}" pkcs8 -topk8 -nocrypt -in "${_ckey}")"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2181
|
||||||
|
if [ "$?" != '0' ] || [ -z "${_ckey_pkcs8}" ]; then
|
||||||
|
_err 'Failed to convert private key to PKCS#8 format!'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)"
|
||||||
|
_certificate_type='PEMchain'
|
||||||
|
else
|
||||||
|
_info "Private key and full certificate chain not available; we must be using a server-generated CSR."
|
||||||
|
_info "Uploading only leaf certificate to: ${_certificate_endpoint}"
|
||||||
|
|
||||||
|
_certificate_str="$(_json_encode <"${_ccert}")"
|
||||||
|
_certificate_type='PEM'
|
||||||
|
fi
|
||||||
|
|
||||||
|
_body="$(printf '{"CertificateString":"%s","CertificateType":"%s","CertificateUri":{"@odata.id":"%s"}}' "${_certificate_str}" "${_certificate_type}" "${_certificate_endpoint}")"
|
||||||
|
_redfish_rest POST "${_certificate_service_endpoint}/Actions/CertificateService.ReplaceCertificate" "${_body}" || return 1
|
||||||
|
_code="$(_redfish_response_code)"
|
||||||
|
|
||||||
|
if ! _startswith "${_code}" '2'; then
|
||||||
|
_err "Failed to update Redfish server TLS certificate! (HTTP ${_code})"
|
||||||
|
_err "Response: ${_response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info 'Successfully updated Redfish server TLS certificate!'
|
||||||
|
_savedeployconf DEPLOY_REDFISH_CERTIFICATE "${_certificate_endpoint}"
|
||||||
|
|
||||||
|
if [ "${DEPLOY_REDFISH_RESTART}" = '1' ]; then
|
||||||
|
_info 'Attempting to restart BMC gracefully.'
|
||||||
|
_redfish_attempt_graceful_restart "${_manager_endpoint}" || return 0
|
||||||
|
_info 'Successfully sent graceful restart command to BMC. After restarting, the new TLS certificate will be active.'
|
||||||
|
else
|
||||||
|
_info 'Please wait up to 20 seconds to take effect, or restart the BMC manually.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
_savedeployconf DEPLOY_REDFISH_RESTART "${DEPLOY_REDFISH_RESTART}"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_rest() {
|
||||||
|
_method="$1"
|
||||||
|
_endpoint="$2"
|
||||||
|
_body="${3:-}"
|
||||||
|
|
||||||
|
export _H1='OData-Version: 4.0'
|
||||||
|
|
||||||
|
if [ "${_method}" = 'GET' ]; then
|
||||||
|
_response="$(_get "https://${DEPLOY_REDFISH_HOST}${_endpoint}")"
|
||||||
|
else
|
||||||
|
_response="$(_post "${_body}" "https://${DEPLOY_REDFISH_HOST}${_endpoint}" '' "${_method}" "${_body:+application/json}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_ret="$?"
|
||||||
|
_response="$(echo "${_response}" | _normalizeJson)"
|
||||||
|
|
||||||
|
if [ "${_ret}" != '0' ]; then
|
||||||
|
_err "Error while calling ${_method} ${_endpoint}. Deploy with --insecure if current certificate is invalid. Try deploying with --debug to troubleshoot."
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "${_ret}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_response_code() {
|
||||||
|
_egrep_o <"${HTTP_HEADER}" '^HTTP[^ ]* [0-9]+' | _tail_n 1 | tr -d '\r\n' | cut -d ' ' -f 2
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_odata_id() {
|
||||||
|
_regex='s/{'
|
||||||
|
for key in $(echo "$1" | tr '.' '\n'); do
|
||||||
|
_regex="${_regex}.*\"${key}\":{"
|
||||||
|
done
|
||||||
|
sed -n "${_regex}[^}]*\"@odata\\.id\":[ ]*\"\\([^\"]*\\)\".*/\\1/p"
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_odata_members() {
|
||||||
|
sed -n 's/.*"Members":[ ]*\[\([^]]*\)\].*/\1/p' | sed 's/,/\n/g' | sed -n 's/.*{"@odata\.id":"\([^"]*\)"}.*/\1/p'
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_odata_count() {
|
||||||
|
sed -n 's/.*"Members@odata\.count":[ ]*\([0-9][0-9]*\).*/\1/p'
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_log_in() {
|
||||||
|
_username="$1"
|
||||||
|
_password="$2"
|
||||||
|
|
||||||
|
_redfish_rest GET '/redfish/v1/' || return 1
|
||||||
|
_info "Authenticating with Redfish API: ${DEPLOY_REDFISH_HOST}"
|
||||||
|
|
||||||
|
if [ "${DEPLOY_REDFISH_USE_BASIC_AUTH}" = '1' ]; then
|
||||||
|
_access_token="$(printf '%s:%s' "${_username}" "${_password}" | _base64)"
|
||||||
|
export _H2="Authorization: Basic ${_access_token}"
|
||||||
|
|
||||||
|
# Verify credentials are valid
|
||||||
|
_account_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'AccountService')"
|
||||||
|
_redfish_rest GET "${_account_service_endpoint}" || return 1
|
||||||
|
_code="$(_redfish_response_code)"
|
||||||
|
|
||||||
|
if [ "${_code}" != '200' ]; then
|
||||||
|
_err "Redfish authentication failed (HTTP ${_code})"
|
||||||
|
_err "Response: ${_response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new session
|
||||||
|
_sessions_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Links.Sessions')"
|
||||||
|
_username="$(printf '%s\n' "${_username}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
|
||||||
|
_password="$(printf '%s\n' "${_password}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')"
|
||||||
|
_body="$(printf '{"UserName":"%s","Password":"%s"}' "${_username}" "${_password}")"
|
||||||
|
_redfish_rest POST "${_sessions_endpoint}" "${_body}" || return 1
|
||||||
|
_code="$(_redfish_response_code)"
|
||||||
|
|
||||||
|
# Verify authentication succeeded
|
||||||
|
if [ "${_code}" != '201' ]; then
|
||||||
|
_err "Redfish authentication failed (HTTP ${_code})"
|
||||||
|
_err "Response: ${_response}"
|
||||||
|
if [ "${_code}" = '401' ] && grep -qi '^WWW-Authenticate: Basic.*$' "${HTTP_HEADER}"; then
|
||||||
|
_err 'NOTE: Server supports Basic auth scheme. Consider setting DEPLOY_REDFISH_USE_BASIC_AUTH=1 and try again.'
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_session="$(echo "${_response}" | _redfish_get_odata_id '')"
|
||||||
|
_auth_token="$(grep -i '^X-Auth-Token: .*$' "${HTTP_HEADER}" | _tail_n 1 | tr -d ' \r\n' | cut -d ':' -f 2)"
|
||||||
|
export _H2="X-Auth-Token: ${_auth_token}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap '_redfish_log_out "${_session:-}"' EXIT INT
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_log_out() {
|
||||||
|
_session_endpoint="${1:-}"
|
||||||
|
|
||||||
|
_info "Logging out of Redfish session on: ${DEPLOY_REDFISH_HOST}"
|
||||||
|
|
||||||
|
if [ -n "${_session_endpoint}" ]; then
|
||||||
|
_redfish_rest DELETE "${_session_endpoint}" || return 1
|
||||||
|
_code="$(_redfish_response_code)"
|
||||||
|
|
||||||
|
if ! _startswith "${_code}" '2'; then
|
||||||
|
_err "Failed to log out of Redfish server (HTTP ${_code})."
|
||||||
|
_err "Response: ${_response}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
_info 'Using basic HTTP auth, no need to log out of Redfish API.'
|
||||||
|
fi
|
||||||
|
|
||||||
|
export _H1=
|
||||||
|
export _H2=
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_certificate_service_endpoint() {
|
||||||
|
_redfish_rest GET '/redfish/v1/' || return 1
|
||||||
|
_certificate_service_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'CertificateService')"
|
||||||
|
[ -n "${_certificate_service_endpoint}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_supports_full_certificate_chains() {
|
||||||
|
_redfish_rest GET "/redfish/v1/\$metadata" || return 1
|
||||||
|
|
||||||
|
_newest_certificate_schema_version="$(echo "${_response}" |
|
||||||
|
_egrep_o 'Namespace="Certificate\.v[_0-9][_0-9]*"' |
|
||||||
|
sed -n 's/.*v\([_0-9][_0-9]*\)".*/\1/p' |
|
||||||
|
sort -n -t _ -k1,1 -k2,2 -k3,3 | _tail_n 1 | tr '_' '.')"
|
||||||
|
_debug _newest_certificate_schema_version "${_newest_certificate_schema_version}"
|
||||||
|
|
||||||
|
_schema_major=${_newest_certificate_schema_version%%.*}
|
||||||
|
_schema_patch_minor=${_newest_certificate_schema_version#*.}
|
||||||
|
_schema_minor=${_schema_patch_minor%%.*}
|
||||||
|
_schema_patch=${_schema_patch_minor#*.}
|
||||||
|
|
||||||
|
for var in _schema_major _schema_minor _schema_patch; do
|
||||||
|
_debug "$var" "$(eval echo "\$$var")"
|
||||||
|
case "$(eval echo "\$$var")" in
|
||||||
|
'' | *[!0-9]*) return 1 ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[ "${_schema_major}" -gt '1' ] || { [ "${_schema_major}" -ge '1' ] && [ "${_schema_minor}" -ge '4' ]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_action() {
|
||||||
|
unset _action _action_info
|
||||||
|
|
||||||
|
_redfish_rest GET "$1" || return 1
|
||||||
|
_action="$(echo "${_response}" | sed -n "s/{.*\"Actions\":{.*\"#$2\":\({[^}]*}\).*/\1/p")"
|
||||||
|
_action_info_endpoint="$(echo "${_action}" | sed -n 's/.*"@Redfish\.ActionInfo":"\([^"]*\)".*/\1/p')"
|
||||||
|
|
||||||
|
if [ -n "${_action_info_endpoint}" ]; then
|
||||||
|
_redfish_rest GET "${_action_info_endpoint}" || return 1
|
||||||
|
_code="$(_redfish_response_code)"
|
||||||
|
|
||||||
|
if [ "${_code}" = '200' ]; then
|
||||||
|
_action_info="${_response}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_action_allowable_values() {
|
||||||
|
_parameter="$1"
|
||||||
|
|
||||||
|
_allowable_values="$(echo "${_action}" |
|
||||||
|
sed -n "s/.*\"${_parameter}@Redfish\.AllowableValues\":[ ]*\[\([^]]*\)\].*/\1/p" |
|
||||||
|
tr -d '"')"
|
||||||
|
|
||||||
|
if [ -n "${_allowable_values}" ]; then
|
||||||
|
_debug2 "Found allowable values for '${_parameter}' in '@Redfish.Action': [${_allowable_values}]"
|
||||||
|
echo "${_allowable_values}"
|
||||||
|
elif [ -n "${_action_info}" ]; then
|
||||||
|
_parameter_info="$(echo "${_action_info}" |
|
||||||
|
sed -n 's/.*"Parameters":[ ]*\[\(\({[^}]*},\{0,1\}\)*\)\].*/\1/p' | _egrep_o '\{[^\}]{1,}\}' |
|
||||||
|
grep "\"Name\":\"${_parameter}\"")"
|
||||||
|
|
||||||
|
if [ -n "${_parameter_info}" ]; then
|
||||||
|
_debug2 "Found allowable values for '${_parameter}' in @Redfish.ActionInfo: ${_parameter_info}"
|
||||||
|
_data_type="$(echo "${_parameter_info}" | sed -n 's/.*"DataType":[ ]*"\([^"]*\)".*/\1/p')"
|
||||||
|
|
||||||
|
case "${_data_type}" in
|
||||||
|
String)
|
||||||
|
echo "${_parameter_info}" | sed -n 's/.*"AllowableValues":[ ]*\[\([^]]*\)\].*/\1/p' | tr -d '"'
|
||||||
|
;;
|
||||||
|
Number)
|
||||||
|
_min_value="$(echo "${_parameter_info}" | sed -n 's/.*"MinimumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')"
|
||||||
|
_max_value="$(echo "${_parameter_info}" | sed -n 's/.*"MaximumValue":[ ]*\([0-9]\{1,\}\).*/\1/p')"
|
||||||
|
echo "${_min_value},${_max_value}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_supports_private_key() {
|
||||||
|
_private_key="$1"
|
||||||
|
_certificate_service_endpoint="$2"
|
||||||
|
|
||||||
|
if _isRSA "${_private_key}"; then
|
||||||
|
_key_algo='RSA'
|
||||||
|
elif _isEcc "${_private_key}"; then
|
||||||
|
_key_algo='ECDSA'
|
||||||
|
else
|
||||||
|
_err 'Private key uses unknown cryptographic algorithm!'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_redfish_get_action "${_certificate_service_endpoint}" 'CertificateService.GenerateCSR' || return 1
|
||||||
|
_allowed_key_algos="$(_redfish_get_action_allowable_values 'KeyPairAlgorithm')"
|
||||||
|
_allowed_key_bit_lengths="$(_redfish_get_action_allowable_values 'KeyBitLength')"
|
||||||
|
_allowed_key_curve_ids="$(_redfish_get_action_allowable_values 'KeyCurveId')"
|
||||||
|
|
||||||
|
if [ -z "${_allowed_key_algos}" ]; then
|
||||||
|
_contains "${_response}" '"KeyBitLength"' && _allowed_key_algos='TPM_ALG_RSA'
|
||||||
|
_contains "${_response}" '"KeyCurveId"' && _allowed_key_algos="${_allowed_key_algos},TPM_ALG_ECDSA"
|
||||||
|
_allowed_key_algos="${_allowed_key_algos#,}"
|
||||||
|
|
||||||
|
if [ -z "${_allowed_key_algos}" ]; then
|
||||||
|
_debug 'Failed to determine server crypto requirements, proceeding blindly.'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug "GenerateCSR does not specify allowed KeyPairAlgorithm(s), so assuming ${_allowed_key_algos}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug _key_algo "${_key_algo}"
|
||||||
|
_debug _allowed_key_algos "${_allowed_key_algos}"
|
||||||
|
|
||||||
|
case "${_key_algo}:${_allowed_key_algos}" in
|
||||||
|
RSA:*RSA*)
|
||||||
|
_min_key_length="$(echo "${_allowed_key_bit_lengths}" | cut -d ',' -f 1)"
|
||||||
|
_max_key_length="${_allowed_key_bit_lengths##*,}"
|
||||||
|
_debug _min_key_length "${_min_key_length}"
|
||||||
|
_debug _max_key_length "${_max_key_length}"
|
||||||
|
|
||||||
|
if [ -z "${_min_key_length}" ] && [ -z "${_max_key_length}" ]; then
|
||||||
|
_debug "Server supports RSA private keys but doesn't specify minimum/maximum bit length, proceeding blindly."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||||
|
if [ "${Le_Keylength}" -lt "${_min_key_length:-${Le_Keylength}}" ] || [ "${Le_Keylength}" -gt "${_max_key_length:-${Le_Keylength}}" ]; then
|
||||||
|
_err "Unsupported RSA private key length ${Le_Keylength}!"
|
||||||
|
_err "Please re-run acme.sh with --keylength set to a value between ${_min_key_length} and ${_max_key_length}."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
ECDSA:*ECDSA*)
|
||||||
|
_debug _allowed_key_curve_ids "${_allowed_key_curve_ids}"
|
||||||
|
|
||||||
|
if [ -z "${_allowed_key_curve_ids}" ]; then
|
||||||
|
_debug "Server supports ECDSA private keys but doesn't specify which curves, proceeding blindly."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2154 # Le_Keylength is set by acme.sh core, not this hook
|
||||||
|
if ! _contains "${_allowed_key_curve_ids}" "TPM_ECC_NIST_P${Le_Keylength#ec-}"; then
|
||||||
|
_err "Unsupported ECDSA private key type! Server supports only: ${_allowed_key_curve_ids}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
_err "Unsupported key pair algorithm ${_key_algo}! Server supports only: ${_allowed_key_algos}"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_primary_manager_endpoint() {
|
||||||
|
_redfish_rest GET '/redfish/v1/' || return 1
|
||||||
|
_managers_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'Managers')"
|
||||||
|
_redfish_rest GET "${_managers_endpoint}" || return 1
|
||||||
|
_num_managers="$(echo "${_response}" | _redfish_get_odata_count)"
|
||||||
|
|
||||||
|
if [ "${_num_managers}" != '1' ]; then
|
||||||
|
_all_managers="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')"
|
||||||
|
_err "Multiple Redfish managers identified (${_all_managers}), but expected exactly one."
|
||||||
|
_err 'Please specify the correct manager in DEPLOY_REDFISH_MANAGER.'
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_manager_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)"
|
||||||
|
[ -n "${_manager_endpoint}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_get_manager_certificate_endpoint() {
|
||||||
|
_manager_endpoint="$1"
|
||||||
|
|
||||||
|
_redfish_rest GET "${_manager_endpoint}" || return 1
|
||||||
|
_protocol_endpoint="$(echo "${_response}" | _redfish_get_odata_id 'NetworkProtocol')"
|
||||||
|
_redfish_rest GET "${_protocol_endpoint}/HTTPS/Certificates" || return 1
|
||||||
|
_num_certificates="$(echo "${_response}" | _redfish_get_odata_count)"
|
||||||
|
|
||||||
|
if [ -z "${_num_certificates}" ]; then
|
||||||
|
_certificate_endpoint="${_protocol_endpoint}/HTTPS/Certificates/1"
|
||||||
|
return 0
|
||||||
|
elif [ "${_num_certificates}" != '1' ]; then
|
||||||
|
_all_certificates="$(echo "${_response}" | _redfish_get_odata_members | tr '\n' ',')"
|
||||||
|
_err "Multiple web service HTTPS certificates identified (${_all_certificates}), but expected exactly one."
|
||||||
|
_err "Please specify which certificate should be replaced in DEPLOY_REDFISH_CERTIFICATE."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_certificate_endpoint="$(echo "${_response}" | _redfish_get_odata_members | _head_n 1)"
|
||||||
|
[ -n "${_certificate_endpoint}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
_redfish_attempt_graceful_restart() {
|
||||||
|
_manager_endpoint="$1"
|
||||||
|
|
||||||
|
_redfish_get_action "${_manager_endpoint}" 'Manager.Reset' || return 1
|
||||||
|
_allowed_reset_types="$(_redfish_get_action_allowable_values 'ResetType')"
|
||||||
|
|
||||||
|
if ! _contains "${_allowed_reset_types}" 'GracefulRestart'; then
|
||||||
|
_err "BMC doesn't support graceful restarts. Please wait up to 20 seconds to take effect, or restart the BMC manually."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_redfish_rest POST "${_manager_endpoint}/Actions/Manager.Reset" '{"ResetType":"GracefulRestart"}' || return 1
|
||||||
|
_code="$(_redfish_response_code)"
|
||||||
|
|
||||||
|
if ! _startswith "${_code}" '2'; then
|
||||||
|
_err "Failed to gracefully restart BMC! (HTTP ${_code})"
|
||||||
|
_err "Response: ${_response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
+26
-4
@@ -4,11 +4,20 @@
|
|||||||
# Deploy the HTTPS server certificate to a Shelly device on the local network.
|
# Deploy the HTTPS server certificate to a Shelly device on the local network.
|
||||||
#
|
#
|
||||||
# ```sh
|
# ```sh
|
||||||
# export SHELLY_HOST=192.168.1.100
|
# export SHELLY_HOST=shelly.example.com
|
||||||
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
|
# export SHELLY_PASSWORD=mysecret # only if auth is enabled on the device
|
||||||
|
# export SHELLY_SCHEME=https # only if devices factory shipped with
|
||||||
|
# # firmware 2.0.0 or already has https
|
||||||
|
# # enabled
|
||||||
# acme.sh --deploy -d shelly.example.com --deploy-hook shelly
|
# 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:
|
# Environment variables:
|
||||||
# SHELLY_HOST (required) IP or hostname of the Shelly device
|
# SHELLY_HOST (required) IP or hostname of the Shelly device
|
||||||
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
|
# SHELLY_PASSWORD (optional) Admin password for digest authentication.
|
||||||
@@ -16,6 +25,8 @@
|
|||||||
# SHELLY_USER (optional) Username for auth. Default: admin
|
# 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)
|
# Default: 1 (reboot after upload)
|
||||||
|
# SHELLY_SCHEME (optional) Scheme used to call the API: http or https
|
||||||
|
# Default: http
|
||||||
#
|
#
|
||||||
# Requirements:
|
# Requirements:
|
||||||
# - Shelly Gen3+ device (Gen4 recommended)
|
# - Shelly Gen3+ device (Gen4 recommended)
|
||||||
@@ -23,7 +34,7 @@
|
|||||||
# - curl or wget
|
# - curl or wget
|
||||||
# - openssl (for SHA-256 digest and random cnonce)
|
# - openssl (for SHA-256 digest and random cnonce)
|
||||||
#
|
#
|
||||||
# The device must be reachable via HTTP on the local network.
|
# The device must be reachable on the local network.
|
||||||
# The hook uploads the fullchain.pem and private key,
|
# The hook uploads the fullchain.pem and private key,
|
||||||
# then reboots the device to apply the new certificate.
|
# then reboots the device to apply the new certificate.
|
||||||
#
|
#
|
||||||
@@ -52,11 +63,13 @@ shelly_deploy() {
|
|||||||
_getdeployconf SHELLY_PASSWORD
|
_getdeployconf SHELLY_PASSWORD
|
||||||
_getdeployconf SHELLY_USER
|
_getdeployconf SHELLY_USER
|
||||||
_getdeployconf SHELLY_REBOOT
|
_getdeployconf SHELLY_REBOOT
|
||||||
|
_getdeployconf SHELLY_SCHEME
|
||||||
|
|
||||||
_debug SHELLY_HOST "$SHELLY_HOST"
|
_debug SHELLY_HOST "$SHELLY_HOST"
|
||||||
_debug SHELLY_USER "$SHELLY_USER"
|
_debug SHELLY_USER "$SHELLY_USER"
|
||||||
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
_secure_debug SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
||||||
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
|
_debug SHELLY_REBOOT "$SHELLY_REBOOT"
|
||||||
|
_debug SHELLY_SCHEME "$SHELLY_SCHEME"
|
||||||
|
|
||||||
if [ -z "$SHELLY_HOST" ]; then
|
if [ -z "$SHELLY_HOST" ]; then
|
||||||
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
|
_err "SHELLY_HOST is required. Please set the IP or hostname of your Shelly device."
|
||||||
@@ -65,11 +78,20 @@ shelly_deploy() {
|
|||||||
|
|
||||||
SHELLY_USER="${SHELLY_USER:-admin}"
|
SHELLY_USER="${SHELLY_USER:-admin}"
|
||||||
SHELLY_REBOOT="${SHELLY_REBOOT:-1}"
|
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_HOST "$SHELLY_HOST"
|
||||||
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
_savedeployconf SHELLY_PASSWORD "$SHELLY_PASSWORD"
|
||||||
_savedeployconf SHELLY_USER "$SHELLY_USER"
|
_savedeployconf SHELLY_USER "$SHELLY_USER"
|
||||||
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
|
_savedeployconf SHELLY_REBOOT "$SHELLY_REBOOT"
|
||||||
|
_savedeployconf SHELLY_SCHEME "$SHELLY_SCHEME"
|
||||||
|
|
||||||
# --- Auth handshake (only if password is set) ---
|
# --- Auth handshake (only if password is set) ---
|
||||||
_shelly_auth_header=""
|
_shelly_auth_header=""
|
||||||
@@ -126,7 +148,7 @@ _shelly_handshake() {
|
|||||||
# Use a protected method (Shelly.GetStatus) to trigger 401.
|
# Use a protected method (Shelly.GetStatus) to trigger 401.
|
||||||
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
|
# Shelly.GetDeviceInfo is excluded from auth and would miss the challenge.
|
||||||
_post '{"id":1,"method":"Shelly.GetStatus"}' \
|
_post '{"id":1,"method":"Shelly.GetStatus"}' \
|
||||||
"http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
"${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||||
|
|
||||||
# Detect auth from HTTP status line rather than response body
|
# Detect auth from HTTP status line rather than response body
|
||||||
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
|
if ! _shelly_has_auth_challenge "$HTTP_HEADER"; then
|
||||||
@@ -210,7 +232,7 @@ _shelly_rpc() {
|
|||||||
export _H1=""
|
export _H1=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_post "$_shelly_body" "http://${SHELLY_HOST}/rpc" "" "" "application/json"
|
_post "$_shelly_body" "${SHELLY_SCHEME}://${SHELLY_HOST}/rpc" "" "" "application/json"
|
||||||
_shelly_ret=$?
|
_shelly_ret=$?
|
||||||
|
|
||||||
if [ "$_shelly_ret" != "0" ]; then
|
if [ "$_shelly_ret" != "0" ]; then
|
||||||
|
|||||||
+40
-14
@@ -189,6 +189,19 @@ ssh_deploy() {
|
|||||||
_info "Required commands batched and sent in single call to remote host"
|
_info "Required commands batched and sent in single call to remote host"
|
||||||
fi
|
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
|
_returnCode=0
|
||||||
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
|
_deploy_ssh_servers="$DEPLOY_SSH_SERVER"
|
||||||
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
|
for DEPLOY_SSH_SERVER in $_deploy_ssh_servers; do
|
||||||
@@ -228,9 +241,9 @@ _ssh_deploy() {
|
|||||||
_backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
|
_backupdir="$_backupprefix-$(_utc_date | tr ' ' '-')"
|
||||||
# run cleanup on the backup directory, erase all older
|
# run cleanup on the backup directory, erase all older
|
||||||
# than 180 days (15552000 seconds).
|
# than 180 days (15552000 seconds).
|
||||||
_cmdstr="{ now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
|
_ssh_add_step "now=\"\$(date -u +%s)\"; for fn in $_backupprefix*; \
|
||||||
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
|
do if [ -d \"\$fn\" ] && [ \"\$(expr \$now - \$(date -ur \$fn +%s) )\" -ge \"15552000\" ]; \
|
||||||
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done; }; $_cmdstr"
|
then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; done"
|
||||||
# Alternate version of above... _cmdstr="find $_backupprefix* -type d -mtime +180 2>/dev/null | xargs rm -rf; $_cmdstr"
|
# 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 of old certificate files will be placed in remote directory $_backupdir"
|
||||||
_info "Backup directories erased after 180 days."
|
_info "Backup directories erased after 180 days."
|
||||||
@@ -245,7 +258,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
|
if [ -n "$DEPLOY_SSH_KEYFILE" ]; then
|
||||||
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
if [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi;"
|
_ssh_add_step "if [ -f $DEPLOY_SSH_KEYFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_KEYFILE $_backupdir >/dev/null; fi"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -261,10 +274,9 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
return $_err_code
|
return $_err_code
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# If file doesn't exist, create it and change its permissions.
|
# If file doesn't exist, create it and change its permissions, then
|
||||||
_cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;"
|
# ssh echo to the file. One step: "test ! -f" fails if the file exists.
|
||||||
# ssh echo to the file
|
_ssh_add_step "test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE; echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE"
|
||||||
_cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;"
|
|
||||||
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
|
_info "will copy private key to remote file $DEPLOY_SSH_KEYFILE"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
@@ -282,7 +294,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
_pipe=">>"
|
_pipe=">>"
|
||||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi;"
|
_ssh_add_step "if [ -f $DEPLOY_SSH_CERTFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CERTFILE $_backupdir >/dev/null; fi"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -304,7 +316,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# ssh echo to the file
|
# ssh echo to the file
|
||||||
_cmdstr="$_cmdstr echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE;"
|
_ssh_add_step "echo \"$(cat "$_ccert")\" $_pipe $DEPLOY_SSH_CERTFILE"
|
||||||
_info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE"
|
_info "will copy certificate to remote file $DEPLOY_SSH_CERTFILE"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
@@ -323,7 +335,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
_pipe=">>"
|
_pipe=">>"
|
||||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi;"
|
_ssh_add_step "if [ -f $DEPLOY_SSH_CAFILE ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_CAFILE $_backupdir >/dev/null; fi"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -348,7 +360,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# ssh echo to the file
|
# ssh echo to the file
|
||||||
_cmdstr="$_cmdstr echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE;"
|
_ssh_add_step "echo \"$(cat "$_cca")\" $_pipe $DEPLOY_SSH_CAFILE"
|
||||||
_info "will copy CA file to remote file $DEPLOY_SSH_CAFILE"
|
_info "will copy CA file to remote file $DEPLOY_SSH_CAFILE"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
@@ -368,7 +380,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
_pipe=">>"
|
_pipe=">>"
|
||||||
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
elif [ "$DEPLOY_SSH_BACKUP" = "yes" ]; then
|
||||||
# backup file we are about to overwrite.
|
# backup file we are about to overwrite.
|
||||||
_cmdstr="$_cmdstr if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi;"
|
_ssh_add_step "if [ -f $DEPLOY_SSH_FULLCHAIN ]; then mkdir -p $_backupdir; cp $DEPLOY_SSH_FULLCHAIN $_backupdir >/dev/null; fi"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
return $_err_code
|
return $_err_code
|
||||||
@@ -396,7 +408,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# ssh echo to the file
|
# ssh echo to the file
|
||||||
_cmdstr="$_cmdstr echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN;"
|
_ssh_add_step "echo \"$(cat "$_cfullchain")\" $_pipe $DEPLOY_SSH_FULLCHAIN"
|
||||||
_info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN"
|
_info "will copy fullchain to remote file $DEPLOY_SSH_FULLCHAIN"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
@@ -419,7 +431,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
|
if [ -n "$DEPLOY_SSH_REMOTE_CMD" ]; then
|
||||||
_cmdstr="$_cmdstr $DEPLOY_SSH_REMOTE_CMD;"
|
_ssh_add_step "$DEPLOY_SSH_REMOTE_CMD"
|
||||||
_info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD"
|
_info "Will execute remote command $DEPLOY_SSH_REMOTE_CMD"
|
||||||
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then
|
||||||
if ! _ssh_remote_cmd "$_cmdstr"; then
|
if ! _ssh_remote_cmd "$_cmdstr"; then
|
||||||
@@ -439,6 +451,20 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d
|
|||||||
return 0
|
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
|
#cmd
|
||||||
_ssh_remote_cmd() {
|
_ssh_remote_cmd() {
|
||||||
_cmd="$1"
|
_cmd="$1"
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
dns_abion_info='Abion.com
|
||||||
|
Site: abion.com
|
||||||
|
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_abion
|
||||||
|
Options:
|
||||||
|
ABION_API_KEY API Key. Created in the Abion Core web interface.
|
||||||
|
Issues: github.com/acmesh-official/acme.sh/issues/7325
|
||||||
|
'
|
||||||
|
|
||||||
|
ABION_Api_Endpoint="https://api.abion.com/v1"
|
||||||
|
|
||||||
|
##################### Public functions #####################
|
||||||
|
|
||||||
|
# Usage: dns_abion_add <domain> <txt record>
|
||||||
|
# Example: dns_abion_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
|
dns_abion_add() {
|
||||||
|
fulldomain="$(echo "$1" | _lower_case)"
|
||||||
|
txtvalue=$2
|
||||||
|
|
||||||
|
ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}"
|
||||||
|
|
||||||
|
if [ -z "$ABION_API_KEY" ]; then
|
||||||
|
ABION_API_KEY=""
|
||||||
|
_err "You need to specify ABION_API_KEY."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_saveaccountconf_mutable ABION_API_KEY "$ABION_API_KEY"
|
||||||
|
|
||||||
|
_debug "First detect the root zone"
|
||||||
|
if ! _get_root "$fulldomain"; then
|
||||||
|
_err "invalid domain"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug _sub_domain "$_sub_domain"
|
||||||
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
|
if ! _abion_get_txt_records; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then
|
||||||
|
_info "TXT record already exists"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_records=$(echo "$_txt_records" | tr '\n' ',' | sed 's/,*$//')
|
||||||
|
if [ -n "$_records" ]; then
|
||||||
|
_records="$_records,"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info "Adding record"
|
||||||
|
if ! _abion_patch_txt "[$_records{\"rdata\":\"$txtvalue\",\"ttl\":60}]"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info "Added, OK"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage: dns_abion_rm <domain> <txt record>
|
||||||
|
dns_abion_rm() {
|
||||||
|
fulldomain="$(echo "$1" | _lower_case)"
|
||||||
|
txtvalue=$2
|
||||||
|
|
||||||
|
ABION_API_KEY="${ABION_API_KEY:-$(_readaccountconf_mutable ABION_API_KEY)}"
|
||||||
|
|
||||||
|
if [ -z "$ABION_API_KEY" ]; then
|
||||||
|
ABION_API_KEY=""
|
||||||
|
_err "You need to specify ABION_API_KEY."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug "First detect the root zone"
|
||||||
|
if ! _get_root "$fulldomain"; then
|
||||||
|
_err "invalid domain"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug _sub_domain "$_sub_domain"
|
||||||
|
_debug _domain "$_domain"
|
||||||
|
|
||||||
|
if ! _abion_get_txt_records; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _contains "$_txt_records" "\"rdata\":\"$txtvalue\""; then
|
||||||
|
_info "TXT record not found, nothing to remove"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_records=$(echo "$_txt_records" | grep -v "\"rdata\":\"$txtvalue\"" | tr '\n' ',' | sed 's/,*$//')
|
||||||
|
|
||||||
|
if [ -n "$_records" ]; then
|
||||||
|
_records="[$_records]"
|
||||||
|
else
|
||||||
|
_records="null"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info "Deleting record"
|
||||||
|
if ! _abion_patch_txt "$_records"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_info "Deleted, OK"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
##################### Private functions #####################
|
||||||
|
|
||||||
|
_get_root() {
|
||||||
|
domain=$1
|
||||||
|
i=1
|
||||||
|
p=0
|
||||||
|
while true; do
|
||||||
|
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
|
||||||
|
_debug h "$h"
|
||||||
|
if [ -z "$h" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _abion_rest GET "zones/$h"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _contains "$response" '"type" *: *"zone"'; then
|
||||||
|
if [ "$p" = "0" ]; then
|
||||||
|
_sub_domain="@"
|
||||||
|
else
|
||||||
|
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
|
||||||
|
fi
|
||||||
|
_domain="$h"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _contains "$response" "Zone not found"; then
|
||||||
|
_err "$response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
p="$i"
|
||||||
|
i=$(_math "$i" + 1)
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_abion_get_txt_records() {
|
||||||
|
if ! _abion_rest GET "zones/$_domain"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_sub_re=$(printf "%s" "$_sub_domain" | sed 's/[.]/\\./g')
|
||||||
|
_txt_records=$(echo "$response" | _normalizeJson |
|
||||||
|
sed -n "s/.*\"$_sub_re\":{//p" |
|
||||||
|
sed 's/\] *}.*/]/' |
|
||||||
|
_egrep_o '"TXT":\[[^]]*\]' |
|
||||||
|
sed 's/^"TXT":\[ *//;s/ *\]$//;s/} *, *{/}\
|
||||||
|
{/g')
|
||||||
|
_debug2 _txt_records "$_txt_records"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_abion_patch_txt() {
|
||||||
|
if ! _abion_rest PATCH "zones/$_domain" "{\"data\":{\"type\":\"zone\",\"id\":\"$_domain\",\"attributes\":{\"records\":{\"$_sub_domain\":{\"TXT\":$1}}}}}"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _contains "$response" '"type" *: *"zone"'; then
|
||||||
|
_err "Unexpected API response: $response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
_abion_rest() {
|
||||||
|
method=$1
|
||||||
|
endpoint=$2
|
||||||
|
data=$3
|
||||||
|
|
||||||
|
export _H1="X-API-KEY: $ABION_API_KEY"
|
||||||
|
export _H2="Content-Type: application/json"
|
||||||
|
|
||||||
|
if [ "$method" = "GET" ]; then
|
||||||
|
response="$(_get "$ABION_Api_Endpoint/$endpoint")"
|
||||||
|
else
|
||||||
|
response="$(_post "$data" "$ABION_Api_Endpoint/$endpoint" "" "$method")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$?" != "0" ]; then
|
||||||
|
_err "error $endpoint"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_debug2 response "$response"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
+2
-2
@@ -18,7 +18,7 @@ CF_Api="https://api.cloudflare.com/client/v4"
|
|||||||
|
|
||||||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
dns_cf_add() {
|
dns_cf_add() {
|
||||||
fulldomain=$1
|
fulldomain="$(_idn "$1")"
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
|
|
||||||
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
||||||
@@ -109,7 +109,7 @@ dns_cf_add() {
|
|||||||
|
|
||||||
#fulldomain txtvalue
|
#fulldomain txtvalue
|
||||||
dns_cf_rm() {
|
dns_cf_rm() {
|
||||||
fulldomain=$1
|
fulldomain="$(_idn "$1")"
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
|
|
||||||
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
CF_Token="${CF_Token:-$(_readaccountconf_mutable CF_Token)}"
|
||||||
|
|||||||
+10
-5
@@ -15,7 +15,7 @@ DNSimple_API="https://api.dnsimple.com/v2"
|
|||||||
|
|
||||||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
|
||||||
dns_dnsimple_add() {
|
dns_dnsimple_add() {
|
||||||
fulldomain=$1
|
fulldomain=$(echo "$1" | _lower_case)
|
||||||
txtvalue=$2
|
txtvalue=$2
|
||||||
|
|
||||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||||
@@ -56,7 +56,7 @@ dns_dnsimple_add() {
|
|||||||
|
|
||||||
# fulldomain
|
# fulldomain
|
||||||
dns_dnsimple_rm() {
|
dns_dnsimple_rm() {
|
||||||
fulldomain=$1
|
fulldomain=$(echo "$1" | _lower_case)
|
||||||
|
|
||||||
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
DNSimple_OAUTH_TOKEN="${DNSimple_OAUTH_TOKEN:-$(_readaccountconf_mutable DNSimple_OAUTH_TOKEN)}"
|
||||||
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
if [ -z "$DNSimple_OAUTH_TOKEN" ]; then
|
||||||
@@ -110,9 +110,13 @@ _get_root() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if _contains "$response" 'not found'; then
|
if ! _contains "$response" "\"name\":\"$h\""; then
|
||||||
_debug "$h not found"
|
if ! _dnsimple_rest GET "$_account_id/zones/$h/records?per_page=1"; then
|
||||||
else
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"pagination":'; then
|
||||||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
|
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$previous")
|
||||||
_domain="$h"
|
_domain="$h"
|
||||||
|
|
||||||
@@ -121,6 +125,7 @@ _get_root() {
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
_debug "$h not found"
|
||||||
|
|
||||||
previous="$i"
|
previous="$i"
|
||||||
i=$(_math "$i" + 1)
|
i=$(_math "$i" + 1)
|
||||||
|
|||||||