#!/bin/bash VER=1.0 PROJECT_NAME="ProxmoxACME" USER_AGENT="$PROJECT_NAME/$VER" DNS_PLUGIN_PATH="/usr/share/proxmox-acme/dnsapi" HTTP_HEADER="$(mktemp)" DEBUG="0" _base64() { openssl base64 -e | tr -d '\r\n' } _dbase64() { openssl base64 -d } # Usage: hashalg [outputhex] # Output Base64-encoded digest _digest() { alg="$1" if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then if [ "$2" ]; then openssl dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' else openssl dgst -"$alg" -binary | _base64 fi fi } _usage() { __red "$@" >&2 printf "\n" >&2 } _upper_case() { # shellcheck disable=SC2018,SC2019 tr 'a-z' 'A-Z' } _lower_case() { # shellcheck disable=SC2018,SC2019 tr 'A-Z' 'a-z' } _startswith() { _str="$1" _sub="$2" echo "$_str" | grep "^$_sub" >/dev/null 2>&1 } _endswith() { _str="$1" _sub="$2" echo "$_str" | grep -- "$_sub\$" >/dev/null 2>&1 } _contains() { _str="$1" _sub="$2" echo "$_str" | grep -- "$_sub" >/dev/null 2>&1 } # str index [sep] _getfield() { _str="$1" _findex="$2" _sep="$3" if [ -z "$_sep" ]; then _sep="," fi _ffi="$_findex" while [ "$_ffi" -gt "0" ]; do _fv="$(echo "$_str" | cut -d "$_sep" -f "$_ffi")" if [ "$_fv" ]; then printf -- "%s" "$_fv" return 0 fi _ffi="$(_math "$_ffi" - 1)" done printf -- "%s" "$_str" } _exists() { cmd="$1" if eval type type >/dev/null 2>&1; then type "$cmd" >/dev/null 2>&1 else command command -v "$cmd" >/dev/null 2>&1 fi ret="$?" return $ret } # a + b _math() { _m_opts="$@" printf "%s" "$(($_m_opts))" } _egrep_o() { if ! egrep -o "$1" 2>/dev/null; then sed -n 's/.*\('"$1"'\).*/\1/p' fi } _h2b() { if _exists xxd; then if _contains "$(xxd --help 2>&1)" "assumes -c30"; then if xxd -r -p -c 9999 2>/dev/null; then return fi else if xxd -r -p 2>/dev/null; then return fi fi fi hex=$(cat) ic="" jc="" _debug2 _URGLY_PRINTF "$_URGLY_PRINTF" if [ -z "$_URGLY_PRINTF" ]; then if [ "$_ESCAPE_XARGS" ] && _exists xargs; then _debug2 "xargs" echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/g' | xargs printf else for h in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/ \1/g'); do if [ -z "$h" ]; then break fi printf "\x$h%s" done fi else for c in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\)/ \1/g'); do if [ -z "$ic" ]; then ic=$c continue fi jc=$c ic="$(_h_char_2_dec "$ic")" jc="$(_h_char_2_dec "$jc")" printf '\'"$(printf "%o" "$(_math "$ic" \* 16 + $jc)")""%s" ic="" jc="" done fi } #Usage: keyfile hashalg #Output: Base64-encoded signature value _sign() { keyfile="$1" alg="$2" if [ -z "$alg" ]; then _usage "Usage: _sign keyfile hashalg" return 1 fi _sign_openssl="${ACME_OPENSSL_BIN:-openssl} dgst -sign $keyfile " if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1 || grep "BEGIN PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then $_sign_openssl -$alg | _base64 elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then if ! _signedECText="$($_sign_openssl -sha$__ECC_KEY_LEN | ${ACME_OPENSSL_BIN:-openssl} asn1parse -inform DER)"; then _err "Sign failed: $_sign_openssl" _err "Key file: $keyfile" _err "Key content:$(wc -l <"$keyfile") lines" return 1 fi _debug3 "_signedECText" "$_signedECText" _ec_r="$(echo "$_signedECText" | _head_n 2 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" _ec_s="$(echo "$_signedECText" | _head_n 3 | _tail_n 1 | cut -d : -f 4 | tr -d "\r\n")" if [ "$__ECC_KEY_LEN" -eq "256" ]; then while [ "${#_ec_r}" -lt "64" ]; do _ec_r="0${_ec_r}" done while [ "${#_ec_s}" -lt "64" ]; do _ec_s="0${_ec_s}" done fi if [ "$__ECC_KEY_LEN" -eq "384" ]; then while [ "${#_ec_r}" -lt "96" ]; do _ec_r="0${_ec_r}" done while [ "${#_ec_s}" -lt "96" ]; do _ec_s="0${_ec_s}" done fi if [ "$__ECC_KEY_LEN" -eq "512" ]; then while [ "${#_ec_r}" -lt "132" ]; do _ec_r="0${_ec_r}" done while [ "${#_ec_s}" -lt "132" ]; do _ec_s="0${_ec_s}" done fi _debug3 "_ec_r" "$_ec_r" _debug3 "_ec_s" "$_ec_s" printf "%s" "$_ec_r$_ec_s" | _h2b | _base64 else _err "Unknown key file format." return 1 fi } #dummy function because proxmox-acme does not call inithttp _resethttp() { : } _HTTP_MAX_RETRY=8 # body url [needbase64] [POST|PUT|DELETE] [ContentType] _post() { body="$1" _post_url="$2" needbase64="$3" httpmethod="$4" _postContentType="$5" _sleep_retry_sec=1 _http_retry_times=0 _hcode=0 while [ "${_http_retry_times}" -le "$_HTTP_MAX_RETRY" ]; do [ "$_http_retry_times" = "$_HTTP_MAX_RETRY" ] _lastHCode="$?" _debug "Retrying post" _post_impl "$body" "$_post_url" "$needbase64" "$httpmethod" "$_postContentType" "$_lastHCode" _hcode="$?" _debug _hcode "$_hcode" if [ "$_hcode" = "0" ]; then break fi _http_retry_times=$(_math $_http_retry_times + 1) _sleep $_sleep_retry_sec done return $_hcode } # body url [needbase64] [POST|PUT|DELETE] [ContentType] [displayError] _post_impl() { body="$1" _post_url="$2" needbase64="$3" httpmethod="$4" _postContentType="$5" displayError="$6" if [ -z "$httpmethod" ]; then httpmethod="POST" fi _CURL="curl -L --silent --dump-header $HTTP_HEADER -g " if [ "$HTTPS_INSECURE" ]; then _CURL="$_CURL --insecure " fi if [ "$httpmethod" = "HEAD" ]; then _CURL="$_CURL -I " fi if [ "$needbase64" ]; then if [ "$body" ]; then if [ "$_postContentType" ]; then response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" else response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url" | _base64)" fi else if [ "$_postContentType" ]; then response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)" else response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url" | _base64)" fi fi else if [ "$body" ]; then if [ "$_postContentType" ]; then response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" else response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data "$body" "$_post_url")" fi else if [ "$_postContentType" ]; then response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "Content-Type: $_postContentType" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")" else response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$_post_url")" fi fi fi _ret="$?" if [ "$_ret" != "0" ]; then if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" fi fi printf "%s" "$response" return $_ret } # url getheader timeout _get() { url="$1" onlyheader="$2" t="$3" _sleep_retry_sec=1 _http_retry_times=0 _hcode=0 while [ "${_http_retry_times}" -le "$_HTTP_MAX_RETRY" ]; do [ "$_http_retry_times" = "$_HTTP_MAX_RETRY" ] _lastHCode="$?" _debug "Retrying GET" _get_impl "$url" "$onlyheader" "$t" "$_lastHCode" _hcode="$?" _debug _hcode "$_hcode" if [ "$_hcode" = "0" ]; then break fi _http_retry_times=$(_math $_http_retry_times + 1) _sleep $_sleep_retry_sec done return $_hcode } # url getheader timeout displayError _get_impl() { url="$1" onlyheader="$2" t="$3" displayError="$4" _CURL="curl -L --silent --dump-header $HTTP_HEADER -g " if [ "$HTTPS_INSECURE" ]; then _CURL="$_CURL --insecure " fi if [ "$t" ]; then _CURL="$_CURL --connect-timeout $t" fi if [ "$onlyheader" ]; then $_CURL -I --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url" else $_CURL --user-agent "USER_AGENT" -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" "$url" fi ret=$? if [ "$ret" != "0" ]; then if [ -z "$displayError" ] || [ "$displayError" = "0" ]; then _err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $ret" fi fi return $ret } _head_n() { head -n "$1" } _tail_n() { tail -n "$1" } # stdin output hexstr splited by one space # input:"abc" # output: " 61 62 63" _hex_dump() { od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n" } # stdin stdout _url_encode() { _hex_str=$(_hex_dump) for _hex_code in $_hex_str; do #upper case case "${_hex_code}" in "41") printf "%s" "A" ;; "42") printf "%s" "B" ;; "43") printf "%s" "C" ;; "44") printf "%s" "D" ;; "45") printf "%s" "E" ;; "46") printf "%s" "F" ;; "47") printf "%s" "G" ;; "48") printf "%s" "H" ;; "49") printf "%s" "I" ;; "4a") printf "%s" "J" ;; "4b") printf "%s" "K" ;; "4c") printf "%s" "L" ;; "4d") printf "%s" "M" ;; "4e") printf "%s" "N" ;; "4f") printf "%s" "O" ;; "50") printf "%s" "P" ;; "51") printf "%s" "Q" ;; "52") printf "%s" "R" ;; "53") printf "%s" "S" ;; "54") printf "%s" "T" ;; "55") printf "%s" "U" ;; "56") printf "%s" "V" ;; "57") printf "%s" "W" ;; "58") printf "%s" "X" ;; "59") printf "%s" "Y" ;; "5a") printf "%s" "Z" ;; #lower case "61") printf "%s" "a" ;; "62") printf "%s" "b" ;; "63") printf "%s" "c" ;; "64") printf "%s" "d" ;; "65") printf "%s" "e" ;; "66") printf "%s" "f" ;; "67") printf "%s" "g" ;; "68") printf "%s" "h" ;; "69") printf "%s" "i" ;; "6a") printf "%s" "j" ;; "6b") printf "%s" "k" ;; "6c") printf "%s" "l" ;; "6d") printf "%s" "m" ;; "6e") printf "%s" "n" ;; "6f") printf "%s" "o" ;; "70") printf "%s" "p" ;; "71") printf "%s" "q" ;; "72") printf "%s" "r" ;; "73") printf "%s" "s" ;; "74") printf "%s" "t" ;; "75") printf "%s" "u" ;; "76") printf "%s" "v" ;; "77") printf "%s" "w" ;; "78") printf "%s" "x" ;; "79") printf "%s" "y" ;; "7a") printf "%s" "z" ;; #numbers "30") printf "%s" "0" ;; "31") printf "%s" "1" ;; "32") printf "%s" "2" ;; "33") printf "%s" "3" ;; "34") printf "%s" "4" ;; "35") printf "%s" "5" ;; "36") printf "%s" "6" ;; "37") printf "%s" "7" ;; "38") printf "%s" "8" ;; "39") printf "%s" "9" ;; "2d") printf "%s" "-" ;; "5f") printf "%s" "_" ;; "2e") printf "%s" "." ;; "7e") printf "%s" "~" ;; #other hex *) printf '%%%s' "$_hex_code" ;; esac done } # Usage: hashalg secret_hex [outputhex] # Output binary hmac _hmac() { alg="$1" secret_hex="$2" outputhex="$3" if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then if [ "$outputhex" ]; then (openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)") | cut -d = -f 2 | tr -d ' ' else openssl dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary 2>/dev/null || openssl dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)" -binary fi fi } # domain _is_idn() { _is_idn_d="$1" _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-_') [ "$_idn_temp" ] } # aa.com _idn() { __idn_d="$1" if ! _is_idn "$__idn_d"; then printf "%s" "$__idn_d" return 0 fi if _exists idn; then idn "$__idn_d" | tr -d "\r\n" else _err "Please install idn to process IDN names." fi } _normalizeJson() { sed "s/\" *: *\([\"{\[]\)/\":\1/g" | sed "s/^ *\([^ ]\)/\1/" | tr -d "\r\n" } # options file _sed_i() { sed -i "$1" "$2" } # sleep sec _sleep() { sleep "$1" } _stat() { stat -c '%U:%G' "$1" 2>/dev/null } _time() { date -u "+%s" } _utc_date() { date -u "+%Y-%m-%d %H:%M:%S" } # stubbed/aliased: __green() { printf -- "%b" "$1" } __red() { printf -- "%b" "$1" } _log() { return 0 } _info() { printf -- "%s" "[$(date)] " >&1 echo "$1" } _err() { printf -- "%s" "[$(date)] " >&2 if [ -z "$2" ]; then __red "$1" >&2 else __red "$1='$2'" >&2 fi printf "\n" >&2 return 1 } # key _readaccountconf() { echo "${!1}" } # key _readaccountconf_mutable() { _readaccountconf "$1" } # no-ops: _clearaccountconf() { return 0 } _cleardomainconf() { return 0 } _debug() { if [[ $DEBUG -eq 0 ]]; then return fi printf -- "%s" "[$(date)] " >&1 echo "$1 $2" } _debug2() { _debug $1 $2 } _debug3() { _debug $1 $2 } _secure_debug() { _debug $1 $2 } _secure_debug2() { _debug $1 $2 } _secure_debug3() { _debug $1 $2 } _saveaccountconf() { return 0 } _saveaccountconf_mutable() { return 0 } _save_conf() { return 0 } _savedomainconf() { return 0 } _source_plugin_config() { return 0 } # Proxmox implementation to inject the DNSAPI variables _load_plugin_config() { while IFS= read -r line; do ADDR=(${line/=/ }) key="${ADDR[0]}" value="${ADDR[1]}" # acme.sh uses eval insted of export if [ -n "$key" ]; then export "$key"="$value" fi done } # call setup and teardown direct # the parameter must be set in the correct order # $1 DNS Plugin name # $2 Fully Qualified Domain Name # $3 value for TXT record # $4 DNS plugin auth and config parameter separated by "," # $5 0 is off, and the default all others are on. setup() { dns_plugin="dns_$1" dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh" fqdn="_acme-challenge.$2" DEBUG=$3 IFS= read -r txtvalue plugin_conf_string=$4 _load_plugin_config if ! . "$dns_plugin_path"; then _err "Load file $dns_plugin error." return 1 fi addcommand="${dns_plugin}_add" if ! _exists "$addcommand"; then _err "It seems that your api file is not correct, it must have a function named: $addcommand" return 1 fi if ! $addcommand "$fqdn" "$txtvalue"; then _err "Error add txt for domain:$fulldomain" return 1 fi } teardown() { dns_plugin="dns_$1" dns_plugin_path="${DNS_PLUGIN_PATH}/${dns_plugin}.sh" fqdn="_acme-challenge.$2" DEBUG=$3 IFS= read -r txtvalue _load_plugin_config if ! . "$dns_plugin_path"; then _err "Load file $dns_plugin error." return 1 fi rmcommand="${dns_plugin}_rm" if ! _exists "$rmcommand"; then _err "It seems that your api file is not correct, it must have a function named: $rmcommand" return 1 fi if ! $rmcommand "$fqdn" "$txtvalue"; then _err "Error add txt for domain:$fulldomain" return 1 fi } "$@"