diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index 543f2f717..972358733 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.2 +PLUGIN_VERSION= 1.3 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml index 51254439c..4dcfa42b4 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml @@ -24,19 +24,27 @@ Pre-defined commands for this restart action. - + header + action.configd dropdown Select a pre-defined system command which should be run for this action. + + + + + header + action.custom textbox Specify a custom commands which should be run for this action. + diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml index 4109e2c0d..30bceb613 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml @@ -35,9 +35,9 @@ - + header - + validation.http_opn_autodiscovery @@ -61,9 +61,9 @@ Enter IP addresses here. Finish each with TAB. - + header - + validation.http_haproxyInject @@ -79,20 +79,6 @@ true Choose the local HAProxy frontends. They will automatically be configured to redirect acme challenges to the internal acme client. The HAProxy service will automatically be restarted if a certificate was renewed. - header @@ -111,7 +97,7 @@ - + header @@ -122,7 +108,7 @@ - + header @@ -139,7 +125,7 @@ - + header @@ -156,7 +142,7 @@ - + header @@ -173,7 +159,7 @@ - + header @@ -190,7 +176,43 @@ - + + header + + + + + validation.dns_cyon_user + + text + + + + validation.dns_cyon_password + + text + + + + + header + + + + + validation.dns_do_pid + + text + + + + validation.dns_do_password + + text + + + + header @@ -207,7 +229,37 @@ - + + header + + + + + validation.dns_freedns_user + + text + + + + validation.dns_freedns_password + + text + + + + + header + + + + + validation.dns_gandi_livedns_key + + text + + + + header @@ -224,7 +276,7 @@ - + header @@ -253,7 +305,7 @@ - + header @@ -276,7 +328,19 @@ - + + header + + + + + validation.dns_linode_key + + text + + + + header @@ -293,7 +357,7 @@ - + header @@ -310,7 +374,7 @@ - + header @@ -327,7 +391,7 @@ - + header @@ -356,7 +420,7 @@ acme.sh documentation for further information.]]> - + header diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml index 65afd3a4f..8f5debb9d 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml @@ -30,4 +30,11 @@ true + + acmeclient.settings.restartTimeout + + text + + true + diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml index 50f6823de..65e463196 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml @@ -43,6 +43,12 @@ 65535 Y + + 600 + 10 + 86400 + Y + 0 N @@ -318,10 +324,15 @@ AWS Route 53 CloudFlare.com API CloudXNS.com API + cyon.ch API + Domain-Offensive API DNSPod.cn API + FreeDNS API + Gandi LiveDNS API GoDaddy.com API ISPConfig 3.1+ API lexicon DNS API + Linode API LuaDNS.com API DNSMadeEasy.com API nsupdate (RFC 2136) @@ -363,12 +374,33 @@ N + + N + + + N + + + N + + + N + N N + + N + + + N + + + N + N @@ -402,6 +434,9 @@ N + + N + N diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt index fbb8f54c5..2eef0c115 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt @@ -48,6 +48,19 @@ POSSIBILITY OF SUCH DAMAGE. } ); + // hook into on-show event for dialog to extend layout. + $('#DialogAction').on('shown.bs.modal', function (e) { + $("#action\\.type").change(function(){ + var service_id = 'table_optional_' + $(this).val(); + $(".table_optional").hide(); + $("."+service_id).show(); + }); + $("#action\\.type").change(function(){ + $(".method_table").hide(); + $(".method_table_"+$(this).val()).show(); + }); + $("#action\\.type").change(); + }) }); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt index b192c9477..d42a0f1ce 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -51,16 +51,25 @@ POSSIBILITY OF SUCH DAMAGE. // hook into on-show event for dialog to extend layout. $('#DialogValidation').on('shown.bs.modal', function (e) { $("#validation\\.dns_service").change(function(){ - var service_id = 'table_' + $(this).val() ; + var service_id = 'table_' + $(this).val(); $(".table_dns").hide(); if ($("#validation\\.method").val() == 'dns01') { $("."+service_id).show(); } }); + $("#validation\\.http_service").change(function(){ + var service_id = 'table_http_' + $(this).val(); + $(".table_http").hide(); + if ($("#validation\\.method").val() == 'http01') { + $("."+service_id).show(); + } else { + } + }); $("#validation\\.method").change(function(){ $(".method_table").hide(); $(".method_table_"+$(this).val()).show(); $("#validation\\.dns_service").change(); + $("#validation\\.http_service").change(); }); $("#validation\\.method").change(); }) diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh index 809b0e994..7906aa4c3 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/acme.sh @@ -1,6 +1,6 @@ #!/usr/bin/env sh -VER=2.6.6 +VER=2.6.7 PROJECT_NAME="acme.sh" @@ -41,8 +41,14 @@ NO_VALUE="no" W_TLS="tls" +MODE_STATELESS="stateless" + STATE_VERIFIED="verified_ok" +NGINX="nginx:" +NGINX_START="#ACME_NGINX_START" +NGINX_END="#ACME_NGINX_END" + BEGIN_CSR="-----BEGIN CERTIFICATE REQUEST-----" END_CSR="-----END CERTIFICATE REQUEST-----" @@ -59,8 +65,39 @@ LOG_LEVEL_2=2 LOG_LEVEL_3=3 DEFAULT_LOG_LEVEL="$LOG_LEVEL_1" +DEBUG_LEVEL_1=1 +DEBUG_LEVEL_2=2 +DEBUG_LEVEL_3=3 +DEBUG_LEVEL_DEFAULT=$DEBUG_LEVEL_1 +DEBUG_LEVEL_NONE=0 + +HIDDEN_VALUE="[hidden](please add '--output-insecure' to see this value)" + +SYSLOG_ERROR="user.error" +SYSLOG_INFO="user.info" +SYSLOG_DEBUG="user.debug" + +#error +SYSLOG_LEVEL_ERROR=3 +#info +SYSLOG_LEVEL_INFO=6 +#debug +SYSLOG_LEVEL_DEBUG=7 +#debug2 +SYSLOG_LEVEL_DEBUG_2=8 +#debug3 +SYSLOG_LEVEL_DEBUG_3=9 + +SYSLOG_LEVEL_DEFAULT=$SYSLOG_LEVEL_ERROR +#none +SYSLOG_LEVEL_NONE=0 + _DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh" +_PREPARE_LINK="https://github.com/Neilpang/acme.sh/wiki/Install-preparations" + +_STATELESS_WIKI="https://github.com/Neilpang/acme.sh/wiki/Stateless-Mode" + __INTERACTIVE="" if [ -t 1 ]; then __INTERACTIVE="1" @@ -100,16 +137,16 @@ _printargs() { _dlg_versions() { echo "Diagnosis versions: " - echo "openssl:$OPENSSL_BIN" - if _exists "$OPENSSL_BIN"; then - $OPENSSL_BIN version 2>&1 + echo "openssl:$ACME_OPENSSL_BIN" + if _exists "$ACME_OPENSSL_BIN"; then + $ACME_OPENSSL_BIN version 2>&1 else - echo "$OPENSSL_BIN doesn't exists." + echo "$ACME_OPENSSL_BIN doesn't exists." fi echo "apache:" if [ "$_APACHECTL" ] && _exists "$_APACHECTL"; then - _APACHECTL -V 2>&1 + $_APACHECTL -V 2>&1 else echo "apache doesn't exists." fi @@ -122,6 +159,16 @@ _dlg_versions() { fi } +#class +_syslog() { + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" = "$SYSLOG_LEVEL_NONE" ]; then + return + fi + _logclass="$1" + shift + logger -i -t "$PROJECT_NAME" -p "$_logclass" "$(_printargs "$@")" >/dev/null 2>&1 +} + _log() { [ -z "$LOG_FILE" ] && return _printargs "$@" >>"$LOG_FILE" @@ -129,10 +176,14 @@ _log() { _info() { _log "$@" + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_INFO" ]; then + _syslog "$SYSLOG_INFO" "$@" + fi _printargs "$@" } _err() { + _syslog "$SYSLOG_ERROR" "$@" _log "$@" if [ -z "$NO_TIMESTAMP" ] || [ "$NO_TIMESTAMP" = "0" ]; then printf -- "%s" "[$(date)] " >&2 @@ -152,31 +203,110 @@ _usage() { } _debug() { - if [ -z "$LOG_LEVEL" ] || [ "$LOG_LEVEL" -ge "$LOG_LEVEL_1" ]; then + if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_1" ]; then _log "$@" fi - if [ -z "$DEBUG" ]; then - return + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_DEBUG" ]; then + _syslog "$SYSLOG_DEBUG" "$@" + fi + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_1" ]; then + _printargs "$@" >&2 + fi +} + +#output the sensitive messages +_secure_debug() { + if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_1" ]; then + if [ "$OUTPUT_INSECURE" = "1" ]; then + _log "$@" + else + _log "$1" "$HIDDEN_VALUE" + fi + fi + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_DEBUG" ]; then + _syslog "$SYSLOG_DEBUG" "$1" "$HIDDEN_VALUE" + fi + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_1" ]; then + if [ "$OUTPUT_INSECURE" = "1" ]; then + _printargs "$@" >&2 + else + _printargs "$1" "$HIDDEN_VALUE" >&2 + fi fi - _printargs "$@" >&2 } _debug2() { - if [ "$LOG_LEVEL" ] && [ "$LOG_LEVEL" -ge "$LOG_LEVEL_2" ]; then + if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_2" ]; then _log "$@" fi - if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then - _debug "$@" + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_DEBUG_2" ]; then + _syslog "$SYSLOG_DEBUG" "$@" + fi + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then + _printargs "$@" >&2 + fi +} + +_secure_debug2() { + if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_2" ]; then + if [ "$OUTPUT_INSECURE" = "1" ]; then + _log "$@" + else + _log "$1" "$HIDDEN_VALUE" + fi + fi + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_DEBUG_2" ]; then + _syslog "$SYSLOG_DEBUG" "$1" "$HIDDEN_VALUE" + fi + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then + if [ "$OUTPUT_INSECURE" = "1" ]; then + _printargs "$@" >&2 + else + _printargs "$1" "$HIDDEN_VALUE" >&2 + fi fi } _debug3() { - if [ "$LOG_LEVEL" ] && [ "$LOG_LEVEL" -ge "$LOG_LEVEL_3" ]; then + if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_3" ]; then _log "$@" fi - if [ "$DEBUG" ] && [ "$DEBUG" -ge "3" ]; then - _debug "$@" + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_DEBUG_3" ]; then + _syslog "$SYSLOG_DEBUG" "$@" fi + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_3" ]; then + _printargs "$@" >&2 + fi +} + +_secure_debug3() { + if [ "${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}" -ge "$LOG_LEVEL_3" ]; then + if [ "$OUTPUT_INSECURE" = "1" ]; then + _log "$@" + else + _log "$1" "$HIDDEN_VALUE" + fi + fi + if [ "${SYS_LOG:-$SYSLOG_LEVEL_NONE}" -ge "$SYSLOG_LEVEL_DEBUG_3" ]; then + _syslog "$SYSLOG_DEBUG" "$1" "$HIDDEN_VALUE" + fi + if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_3" ]; then + if [ "$OUTPUT_INSECURE" = "1" ]; then + _printargs "$@" >&2 + else + _printargs "$1" "$HIDDEN_VALUE" >&2 + fi + fi +} + +_upper_case() { + # shellcheck disable=SC2018,SC2019 + tr 'a-z' 'A-Z' +} + +_lower_case() { + # shellcheck disable=SC2018,SC2019 + tr 'A-Z' 'a-z' } _startswith() { @@ -217,7 +347,7 @@ _hasfield() { fi done _debug2 "'$_str' does not contain '$_field'" - return 1 #not contains + return 1 #not contains } _getfield() { @@ -336,18 +466,270 @@ _h2b() { done } -#hex string -_hex() { +_is_solaris() { + _contains "${__OS__:=$(uname -a)}" "solaris" || _contains "${__OS__:=$(uname -a)}" "SunOS" +} + +#_ascii_hex str +#this can only process ascii chars, should only be used when od command is missing as a backup way. +_ascii_hex() { + _debug2 "Using _ascii_hex" _str="$1" _str_len=${#_str} _h_i=1 while [ "$_h_i" -le "$_str_len" ]; do _str_c="$(printf "%s" "$_str" | cut -c "$_h_i")" - printf "%02x" "'$_str_c" + printf " %02x" "'$_str_c" _h_i="$(_math "$_h_i" + 1)" done } +#stdin output hexstr splited by one space +#input:"abc" +#output: " 61 62 63" +_hex_dump() { + if _exists od; then + od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n" + elif _exists hexdump; then + _debug3 "using hexdump" + hexdump -v -e '/1 ""' -e '/1 " %02x" ""' + elif _exists xxd; then + _debug3 "using xxd" + xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " " + else + _debug3 "using _ascii_hex" + str=$(cat) + _ascii_hex "$str" + fi +} + +#url encode, no-preserved chars +#A B C D E F G H I J K L M N O P Q R S T U V W X Y Z +#41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a + +#a b c d e f g h i j k l m n o p q r s t u v w x y z +#61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a + +#0 1 2 3 4 5 6 7 8 9 - _ . ~ +#30 31 32 33 34 35 36 37 38 39 2d 5f 2e 7e + +#stdin stdout +_url_encode() { + _hex_str=$(_hex_dump) + _debug3 "_url_encode" + _debug3 "_hex_str" "$_hex_str" + 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 +} + #options file _sed_i() { options="$1" @@ -408,19 +790,19 @@ _base64() { [ "" ] #urgly if [ "$1" ]; then _debug3 "base64 multiline:'$1'" - $OPENSSL_BIN base64 -e + $ACME_OPENSSL_BIN base64 -e else _debug3 "base64 single line." - $OPENSSL_BIN base64 -e | tr -d '\r\n' + $ACME_OPENSSL_BIN base64 -e | tr -d '\r\n' fi } #Usage: multiline _dbase64() { if [ "$1" ]; then - $OPENSSL_BIN base64 -d -A + $ACME_OPENSSL_BIN base64 -d -A else - $OPENSSL_BIN base64 -d + $ACME_OPENSSL_BIN base64 -d fi } @@ -437,9 +819,9 @@ _digest() { if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ] || [ "$alg" = "md5" ]; then if [ "$outputhex" ]; then - $OPENSSL_BIN dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' + $ACME_OPENSSL_BIN dgst -"$alg" -hex | cut -d = -f 2 | tr -d ' ' else - $OPENSSL_BIN dgst -"$alg" -binary | _base64 + $ACME_OPENSSL_BIN dgst -"$alg" -binary | _base64 fi else _err "$alg is not supported yet" @@ -462,9 +844,9 @@ _hmac() { if [ "$alg" = "sha256" ] || [ "$alg" = "sha1" ]; then if [ "$outputhex" ]; then - $OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" | cut -d = -f 2 | tr -d ' ' + ($ACME_OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" 2>/dev/null || $ACME_OPENSSL_BIN dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)") | cut -d = -f 2 | tr -d ' ' else - $OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary + $ACME_OPENSSL_BIN dgst -"$alg" -mac HMAC -macopt "hexkey:$secret_hex" -binary 2>/dev/null || $ACME_OPENSSL_BIN dgst -"$alg" -hmac "$(printf "%s" "$secret_hex" | _h2b)" -binary fi else _err "$alg is not supported yet" @@ -483,7 +865,7 @@ _sign() { return 1 fi - _sign_openssl="$OPENSSL_BIN dgst -sign $keyfile " + _sign_openssl="$ACME_OPENSSL_BIN dgst -sign $keyfile " if [ "$alg" = "sha256" ]; then _sign_openssl="$_sign_openssl -$alg" else @@ -494,7 +876,7 @@ _sign() { if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then $_sign_openssl | _base64 elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then - if ! _signedECText="$($_sign_openssl | $OPENSSL_BIN asn1parse -inform DER)"; then + if ! _signedECText="$($_sign_openssl | $ACME_OPENSSL_BIN asn1parse -inform DER)"; then _err "Sign failed: $_sign_openssl" _err "Key file: $keyfile" _err "Key content:$(wc -l <"$keyfile") lises" @@ -555,12 +937,21 @@ _createkey() { _debug "Use length $length" + if ! touch "$f" >/dev/null 2>&1; then + _f_path="$(dirname "$f")" + _debug _f_path "$_f_path" + if ! mkdir -p "$_f_path"; then + _err "Can not create path: $_f_path" + return 1 + fi + fi + if _isEccKey "$length"; then _debug "Using ec name: $eccname" - $OPENSSL_BIN ecparam -name "$eccname" -genkey 2>/dev/null >"$f" + $ACME_OPENSSL_BIN ecparam -name "$eccname" -genkey 2>/dev/null >"$f" else _debug "Using RSA: $length" - $OPENSSL_BIN genrsa "$length" 2>/dev/null >"$f" + $ACME_OPENSSL_BIN genrsa "$length" 2>/dev/null >"$f" fi if [ "$?" != "0" ]; then @@ -634,7 +1025,7 @@ _createcsr() { else alt="DNS:$domainlist" fi - #multi + #multi _info "Multi domain" "$alt" printf -- "\nsubjectAltName=$alt" >>"$csrconf" fi @@ -646,7 +1037,11 @@ _createcsr() { _csr_cn="$(_idn "$domain")" _debug2 _csr_cn "$_csr_cn" - $OPENSSL_BIN req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" + if _contains "$(uname -a)" "MINGW"; then + $ACME_OPENSSL_BIN req -new -sha256 -key "$csrkey" -subj "//CN=$_csr_cn" -config "$csrconf" -out "$csr" + else + $ACME_OPENSSL_BIN req -new -sha256 -key "$csrkey" -subj "/CN=$_csr_cn" -config "$csrconf" -out "$csr" + fi } #_signcsr key csr conf cert @@ -657,7 +1052,7 @@ _signcsr() { cert="$4" _debug "_signcsr" - _msg="$($OPENSSL_BIN x509 -req -days 365 -in "$csr" -signkey "$key" -extensions v3_req -extfile "$conf" -out "$cert" 2>&1)" + _msg="$($ACME_OPENSSL_BIN x509 -req -days 365 -in "$csr" -signkey "$key" -extensions v3_req -extfile "$conf" -out "$cert" 2>&1)" _ret="$?" _debug "$_msg" return $_ret @@ -670,7 +1065,7 @@ _readSubjectFromCSR() { _usage "_readSubjectFromCSR mycsr.csr" return 1 fi - $OPENSSL_BIN req -noout -in "$_csrfile" -subject | _egrep_o "CN=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d '\n' + $ACME_OPENSSL_BIN req -noout -in "$_csrfile" -subject | _egrep_o "CN *=.*" | cut -d = -f 2 | cut -d / -f 1 | tr -d '\n' } #_csrfile @@ -685,7 +1080,7 @@ _readSubjectAltNamesFromCSR() { _csrsubj="$(_readSubjectFromCSR "$_csrfile")" _debug _csrsubj "$_csrsubj" - _dnsAltnames="$($OPENSSL_BIN req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" + _dnsAltnames="$($ACME_OPENSSL_BIN req -noout -text -in "$_csrfile" | grep "^ *DNS:.*" | tr -d ' \n')" _debug _dnsAltnames "$_dnsAltnames" if _contains "$_dnsAltnames," "DNS:$_csrsubj,"; then @@ -698,7 +1093,7 @@ _readSubjectAltNamesFromCSR() { printf "%s" "$_dnsAltnames" | sed "s/DNS://g" } -#_csrfile +#_csrfile _readKeyLengthFromCSR() { _csrfile="$1" if [ -z "$_csrfile" ]; then @@ -706,13 +1101,13 @@ _readKeyLengthFromCSR() { return 1 fi - _outcsr="$($OPENSSL_BIN req -noout -text -in "$_csrfile")" + _outcsr="$($ACME_OPENSSL_BIN req -noout -text -in "$_csrfile")" if _contains "$_outcsr" "Public Key Algorithm: id-ecPublicKey"; then _debug "ECC CSR" echo "$_outcsr" | _egrep_o "^ *ASN1 OID:.*" | cut -d ':' -f 2 | tr -d ' ' else _debug "RSA CSR" - echo "$_outcsr" | _egrep_o "^ *Public-Key:.*" | cut -d '(' -f 2 | cut -d ' ' -f 1 + echo "$_outcsr" | _egrep_o "(^ *|^RSA )Public.Key:.*" | cut -d '(' -f 2 | cut -d ' ' -f 1 fi } @@ -760,9 +1155,9 @@ toPkcs() { _initpath "$domain" "$_isEcc" if [ "$pfxPassword" ]; then - $OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" -password "pass:$pfxPassword" + $ACME_OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" -password "pass:$pfxPassword" else - $OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" + $ACME_OPENSSL_BIN pkcs12 -export -out "$CERT_PFX_PATH" -inkey "$CERT_KEY_PATH" -in "$CERT_PATH" -certfile "$CA_CERT_PATH" fi if [ "$?" = "0" ]; then @@ -771,7 +1166,28 @@ toPkcs() { } -#[2048] +#domain [isEcc] +toPkcs8() { + domain="$1" + + if [ -z "$domain" ]; then + _usage "Usage: $PROJECT_ENTRY --toPkcs8 -d domain [--ecc]" + return 1 + fi + + _isEcc="$2" + + _initpath "$domain" "$_isEcc" + + $ACME_OPENSSL_BIN pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in "$CERT_KEY_PATH" -out "$CERT_PKCS8_PATH" + + if [ "$?" = "0" ]; then + _info "Success, $CERT_PKCS8_PATH" + fi + +} + +#[2048] createAccountKey() { _info "Creating account key" if [ -z "$1" ]; then @@ -868,17 +1284,17 @@ createCSR() { } -_urlencode() { +_url_replace() { tr '/+' '_-' | tr -d '= ' } _time2str() { - #BSD + #Linux if date -u -d@"$1" 2>/dev/null; then return fi - #Linux + #BSD if date -u -r "$1" 2>/dev/null; then return fi @@ -924,7 +1340,7 @@ _calcjwk() { if grep "BEGIN RSA PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then _debug "RSA key" - pub_exp=$($OPENSSL_BIN rsa -in "$keyfile" -noout -text | grep "^publicExponent:" | cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1) + pub_exp=$($ACME_OPENSSL_BIN rsa -in "$keyfile" -noout -text | grep "^publicExponent:" | cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1) if [ "${#pub_exp}" = "5" ]; then pub_exp=0$pub_exp fi @@ -933,9 +1349,9 @@ _calcjwk() { e=$(echo "$pub_exp" | _h2b | _base64) _debug3 e "$e" - modulus=$($OPENSSL_BIN rsa -in "$keyfile" -modulus -noout | cut -d '=' -f 2) + modulus=$($ACME_OPENSSL_BIN rsa -in "$keyfile" -modulus -noout | cut -d '=' -f 2) _debug3 modulus "$modulus" - n="$(printf "%s" "$modulus" | _h2b | _base64 | _urlencode)" + n="$(printf "%s" "$modulus" | _h2b | _base64 | _url_replace)" _debug3 n "$n" jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}' @@ -946,12 +1362,12 @@ _calcjwk() { JWK_HEADERPLACE_PART2='", "alg": "RS256", "jwk": '$jwk'}' elif grep "BEGIN EC PRIVATE KEY" "$keyfile" >/dev/null 2>&1; then _debug "EC key" - crv="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" | cut -d ":" -f 2 | tr -d " \r\n")" + crv="$($ACME_OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^NIST CURVE:" | cut -d ":" -f 2 | tr -d " \r\n")" _debug3 crv "$crv" if [ -z "$crv" ]; then _debug "Let's try ASN1 OID" - crv_oid="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^ASN1 OID:" | cut -d ":" -f 2 | tr -d " \r\n")" + crv_oid="$($ACME_OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep "^ASN1 OID:" | cut -d ":" -f 2 | tr -d " \r\n")" _debug3 crv_oid "$crv_oid" case "${crv_oid}" in "prime256v1") @@ -971,15 +1387,15 @@ _calcjwk() { _debug3 crv "$crv" fi - pubi="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n pub: | cut -d : -f 1)" + pubi="$($ACME_OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n pub: | cut -d : -f 1)" pubi=$(_math "$pubi" + 1) _debug3 pubi "$pubi" - pubj="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n "ASN1 OID:" | cut -d : -f 1)" + pubj="$($ACME_OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | grep -n "ASN1 OID:" | cut -d : -f 1)" pubj=$(_math "$pubj" - 1) _debug3 pubj "$pubj" - pubtext="$($OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | sed -n "$pubi,${pubj}p" | tr -d " \n\r")" + pubtext="$($ACME_OPENSSL_BIN ec -in "$keyfile" -noout -text 2>/dev/null | sed -n "$pubi,${pubj}p" | tr -d " \n\r")" _debug3 pubtext "$pubtext" xlen="$(printf "%s" "$pubtext" | tr -d ':' | wc -c)" @@ -990,14 +1406,14 @@ _calcjwk() { x="$(printf "%s" "$pubtext" | cut -d : -f 2-"$xend")" _debug3 x "$x" - x64="$(printf "%s" "$x" | tr -d : | _h2b | _base64 | _urlencode)" + x64="$(printf "%s" "$x" | tr -d : | _h2b | _base64 | _url_replace)" _debug3 x64 "$x64" xend=$(_math "$xend" + 1) y="$(printf "%s" "$pubtext" | cut -d : -f "$xend"-10000)" _debug3 y "$y" - y64="$(printf "%s" "$y" | tr -d : | _h2b | _base64 | _urlencode)" + y64="$(printf "%s" "$y" | tr -d : | _h2b | _base64 | _url_replace)" _debug3 y64 "$y64" jwk='{"crv": "'$crv'", "kty": "EC", "x": "'$x64'", "y": "'$y64'"}' @@ -1019,6 +1435,10 @@ _time() { date -u "+%s" } +_utc_date() { + date -u "+%Y-%m-%d %H:%M:%S" +} + _mktemp() { if _exists mktemp; then if mktemp 2>/dev/null; then @@ -1075,6 +1495,11 @@ _inithttp() { fi fi + #from wget 1.14: do not skip body on 404 error + if [ "$_ACME_WGET" ] && _contains "$($_ACME_WGET --help 2>&1)" "--content-on-error"; then + _ACME_WGET="$_ACME_WGET --content-on-error " + fi + __HTTP_INITIALIZED=1 } @@ -1095,7 +1520,7 @@ _post() { _inithttp - if [ "$_ACME_CURL" ]; then + if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then _CURL="$_ACME_CURL" if [ "$HTTPS_INSECURE" ]; then _CURL="$_CURL --insecure " @@ -1136,7 +1561,7 @@ _post() { _ret="$?" if [ "$_ret" = "8" ]; then _ret=0 - _debug "wget returns 8, the server returns a 'Bad request' respons, lets process the response later." + _debug "wget returns 8, the server returns a 'Bad request' response, lets process the response later." fi if [ "$_ret" != "0" ]; then _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" @@ -1162,7 +1587,7 @@ _get() { _inithttp - if [ "$_ACME_CURL" ]; then + if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then _CURL="$_ACME_CURL" if [ "$HTTPS_INSECURE" ]; then _CURL="$_CURL --insecure " @@ -1199,9 +1624,9 @@ _get() { $_WGET --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" -O - "$url" fi ret=$? - if [ "$_ret" = "8" ]; then - _ret=0 - _debug "wget returns 8, the server returns a 'Bad request' respons, lets process the response later." + if [ "$ret" = "8" ]; then + ret=0 + _debug "wget returns 8, the server returns a 'Bad request' response, lets process the response later." fi if [ "$ret" != "0" ]; then _err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $ret" @@ -1241,65 +1666,78 @@ _send_signed_request() { return 1 fi - payload64=$(printf "%s" "$payload" | _base64 | _urlencode) + payload64=$(printf "%s" "$payload" | _base64 | _url_replace) _debug3 payload64 "$payload64" - if [ -z "$_CACHED_NONCE" ]; then - _debug2 "Get nonce." - nonceurl="$API/directory" - _headers="$(_get "$nonceurl" "onlyheader")" + MAX_REQUEST_RETRY_TIMES=5 + _request_retry_times=0 + while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do + _debug3 _request_retry_times "$_request_retry_times" + if [ -z "$_CACHED_NONCE" ]; then + _debug2 "Get nonce." + nonceurl="$API/directory" + _headers="$(_get "$nonceurl" "onlyheader")" - if [ "$?" != "0" ]; then - _err "Can not connect to $nonceurl to get nonce." + if [ "$?" != "0" ]; then + _err "Can not connect to $nonceurl to get nonce." + return 1 + fi + + _debug2 _headers "$_headers" + + _CACHED_NONCE="$(echo "$_headers" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + _debug2 _CACHED_NONCE "$_CACHED_NONCE" + else + _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" + fi + nonce="$_CACHED_NONCE" + _debug2 nonce "$nonce" + + protected="$JWK_HEADERPLACE_PART1$nonce$JWK_HEADERPLACE_PART2" + _debug3 protected "$protected" + + protected64="$(printf "%s" "$protected" | _base64 | _url_replace)" + _debug3 protected64 "$protected64" + + if ! _sig_t="$(printf "%s" "$protected64.$payload64" | _sign "$keyfile" "sha256")"; then + _err "Sign request failed." return 1 fi + _debug3 _sig_t "$_sig_t" - _debug2 _headers "$_headers" + sig="$(printf "%s" "$_sig_t" | _url_replace)" + _debug3 sig "$sig" - _CACHED_NONCE="$(echo "$_headers" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" - _debug2 _CACHED_NONCE "$_CACHED_NONCE" - else - _debug2 "Use _CACHED_NONCE" "$_CACHED_NONCE" - fi - nonce="$_CACHED_NONCE" - _debug2 nonce "$nonce" + body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + _debug3 body "$body" - protected="$JWK_HEADERPLACE_PART1$nonce$JWK_HEADERPLACE_PART2" - _debug3 protected "$protected" + response="$(_post "$body" "$url" "$needbase64")" + _CACHED_NONCE="" - protected64="$(printf "%s" "$protected" | _base64 | _urlencode)" - _debug3 protected64 "$protected64" + if [ "$?" != "0" ]; then + _err "Can not post to $url" + return 1 + fi + _debug2 original "$response" + response="$(echo "$response" | _normalizeJson)" - if ! _sig_t="$(printf "%s" "$protected64.$payload64" | _sign "$keyfile" "sha256")"; then - _err "Sign request failed." - return 1 - fi - _debug3 _sig_t "$_sig_t" + responseHeaders="$(cat "$HTTP_HEADER")" - sig="$(printf "%s" "$_sig_t" | _urlencode)" - _debug3 sig "$sig" + _debug2 responseHeaders "$responseHeaders" + _debug2 response "$response" + code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" + _debug code "$code" - body="{\"header\": $JWK_HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" - _debug3 body "$body" + _CACHED_NONCE="$(echo "$responseHeaders" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" - response="$(_post "$body" "$url" "$needbase64")" - _CACHED_NONCE="" - if [ "$?" != "0" ]; then - _err "Can not post to $url" - return 1 - fi - _debug2 original "$response" - - response="$(echo "$response" | _normalizeJson)" - - responseHeaders="$(cat "$HTTP_HEADER")" - - _debug2 responseHeaders "$responseHeaders" - _debug2 response "$response" - code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" - _debug code "$code" - - _CACHED_NONCE="$(echo "$responseHeaders" | grep "Replay-Nonce:" | _head_n 1 | tr -d "\r\n " | cut -d ':' -f 2)" + if _contains "$response" "JWS has invalid anti-replay nonce"; then + _info "It seems the CA server is busy now, let's wait and retry." + _request_retry_times=$(_math "$_request_retry_times" + 1) + _sleep 5 + continue + fi + break + done } @@ -1324,20 +1762,20 @@ _setopt() { __val="$(echo "$__val" | sed 's/&/\\&/g')" fi text="$(cat "$__conf")" - echo "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + printf -- "%s\n" "$text" | sed "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" elif grep -n "^#$__opt$__sep" "$__conf" >/dev/null; then if _contains "$__val" "&"; then __val="$(echo "$__val" | sed 's/&/\\&/g')" fi text="$(cat "$__conf")" - echo "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" + printf -- "%s\n" "$text" | sed "s|^#$__opt$__sep.*$|$__opt$__sep$__val$__end|" >"$__conf" else _debug3 APP echo "$__opt$__sep$__val$__end" >>"$__conf" fi - _debug2 "$(grep -n "^$__opt$__sep" "$__conf")" + _debug3 "$(grep -n "^$__opt$__sep" "$__conf")" } #_save_conf file key value @@ -1440,6 +1878,14 @@ _startserver() { _NC="$_NC -6" fi + if [ "$Le_Listen_V4$Le_Listen_V6$ncaddr" ]; then + if ! _contains "$nchelp" "-4"; then + _err "The nc doesn't support '-4', '-6' or local-address, please install 'netcat-openbsd' and try again." + _err "See $(__green $_PREPARE_LINK)" + return 1 + fi + fi + if echo "$nchelp" | grep "\-q[ ,]" >/dev/null; then _NC="$_NC -q 1 -l $ncaddr" else @@ -1563,7 +2009,7 @@ _starttlsserver() { return 1 fi - __S_OPENSSL="$OPENSSL_BIN s_server -cert $TLS_CERT -key $TLS_KEY " + __S_OPENSSL="$ACME_OPENSSL_BIN s_server -cert $TLS_CERT -key $TLS_KEY " if [ "$opaddr" ]; then __S_OPENSSL="$__S_OPENSSL -accept $opaddr:$port" else @@ -1594,14 +2040,18 @@ _starttlsserver() { _readlink() { _rf="$1" if ! readlink -f "$_rf" 2>/dev/null; then - if _startswith "$_rf" "\./$PROJECT_ENTRY"; then - printf -- "%s" "$(pwd)/$PROJECT_ENTRY" + if _startswith "$_rf" "/"; then + echo "$_rf" return 0 fi - readlink "$_rf" + echo "$(pwd)/$_rf" | _conapath fi } +_conapath() { + sed "s#/\./#/#g" +} + __initHome() { if [ -z "$_SCRIPT_HOME" ]; then if _exists readlink && _exists dirname; then @@ -1738,8 +2188,8 @@ _initpath() { CERT_HOME="$_DEFAULT_CERT_HOME" fi - if [ -z "$OPENSSL_BIN" ]; then - OPENSSL_BIN="$DEFAULT_OPENSSL_BIN" + if [ -z "$ACME_OPENSSL_BIN" ] || [ ! -f "$ACME_OPENSSL_BIN" ] || [ ! -x "$ACME_OPENSSL_BIN" ]; then + ACME_OPENSSL_BIN="$DEFAULT_OPENSSL_BIN" fi if [ -z "$1" ]; then @@ -1765,6 +2215,10 @@ _initpath() { _debug DOMAIN_PATH "$DOMAIN_PATH" fi + if [ -z "$DOMAIN_BACKUP_PATH" ]; then + DOMAIN_BACKUP_PATH="$DOMAIN_PATH/backup" + fi + if [ -z "$DOMAIN_CONF" ]; then DOMAIN_CONF="$DOMAIN_PATH/$domain.conf" fi @@ -1791,6 +2245,9 @@ _initpath() { if [ -z "$CERT_PFX_PATH" ]; then CERT_PFX_PATH="$DOMAIN_PATH/$domain.pfx" fi + if [ -z "$CERT_PKCS8_PATH" ]; then + CERT_PKCS8_PATH="$DOMAIN_PATH/$domain.pkcs8" + fi if [ -z "$TLS_CONF" ]; then TLS_CONF="$DOMAIN_PATH/tls.valdation.conf" @@ -1979,10 +2436,228 @@ Allow from all return 0 } +#find the real nginx conf file +#backup +#set the nginx conf +#returns the real nginx conf file +_setNginx() { + _d="$1" + _croot="$2" + _thumbpt="$3" + if ! _exists "nginx"; then + _err "nginx command is not found." + return 1 + fi + FOUND_REAL_NGINX_CONF="" + FOUND_REAL_NGINX_CONF_LN="" + BACKUP_NGINX_CONF="" + _debug _croot "$_croot" + _start_f="$(echo "$_croot" | cut -d : -f 2)" + _debug _start_f "$_start_f" + if [ -z "$_start_f" ]; then + _debug "find start conf from nginx command" + if [ -z "$NGINX_CONF" ]; then + NGINX_CONF="$(nginx -V 2>&1 | _egrep_o "--conf-path=[^ ]* " | tr -d " ")" + _debug NGINX_CONF "$NGINX_CONF" + NGINX_CONF="$(echo "$NGINX_CONF" | cut -d = -f 2)" + _debug NGINX_CONF "$NGINX_CONF" + if [ ! -f "$NGINX_CONF" ]; then + _err "'$NGINX_CONF' doesn't exist." + NGINX_CONF="" + return 1 + fi + _debug "Found nginx conf file:$NGINX_CONF" + fi + _start_f="$NGINX_CONF" + fi + _debug "Start detect nginx conf for $_d from:$_start_f" + if ! _checkConf "$_d" "$_start_f"; then + _err "Can not find conf file for domain $d" + return 1 + fi + _info "Found conf file: $FOUND_REAL_NGINX_CONF" + + _ln=$FOUND_REAL_NGINX_CONF_LN + _debug "_ln" "$_ln" + + _lnn=$(_math $_ln + 1) + _debug _lnn "$_lnn" + _start_tag="$(sed -n "$_lnn,${_lnn}p" "$FOUND_REAL_NGINX_CONF")" + _debug "_start_tag" "$_start_tag" + if [ "$_start_tag" = "$NGINX_START" ]; then + _info "The domain $_d is already configured, skip" + FOUND_REAL_NGINX_CONF="" + return 0 + fi + + mkdir -p "$DOMAIN_BACKUP_PATH" + _backup_conf="$DOMAIN_BACKUP_PATH/$_d.nginx.conf" + _debug _backup_conf "$_backup_conf" + BACKUP_NGINX_CONF="$_backup_conf" + _info "Backup $FOUND_REAL_NGINX_CONF to $_backup_conf" + if ! cp "$FOUND_REAL_NGINX_CONF" "$_backup_conf"; then + _err "backup error." + FOUND_REAL_NGINX_CONF="" + return 1 + fi + + _info "Check the nginx conf before setting up." + if ! _exec "nginx -t" >/dev/null; then + _exec_err + return 1 + fi + + _info "OK, Set up nginx config file" + + if ! sed -n "1,${_ln}p" "$_backup_conf" >"$FOUND_REAL_NGINX_CONF"; then + cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" + _err "write nginx conf error, but don't worry, the file is restored to the original version." + return 1 + fi + + echo "$NGINX_START +location ~ \"^/\.well-known/acme-challenge/([-_a-zA-Z0-9]+)\$\" { + default_type text/plain; + return 200 \"\$1.$_thumbpt\"; +} +#NGINX_START +" >>"$FOUND_REAL_NGINX_CONF" + + if ! sed -n "${_lnn},99999p" "$_backup_conf" >>"$FOUND_REAL_NGINX_CONF"; then + cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" + _err "write nginx conf error, but don't worry, the file is restored." + return 1 + fi + + _info "nginx conf is done, let's check it again." + if ! _exec "nginx -t" >/dev/null; then + _exec_err + _err "It seems that nginx conf was broken, let's restore." + cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" + return 1 + fi + + _info "Reload nginx" + if ! _exec "nginx -s reload" >/dev/null; then + _exec_err + _err "It seems that nginx reload error, let's restore." + cat "$_backup_conf" >"$FOUND_REAL_NGINX_CONF" + return 1 + fi + + return 0 +} + +#d , conf +_checkConf() { + _d="$1" + _c_file="$2" + _debug "Start _checkConf from:$_c_file" + if [ ! -f "$2" ] && ! echo "$2" | grep '*$' >/dev/null && echo "$2" | grep '*' >/dev/null; then + _debug "wildcard" + for _w_f in $2; do + if [ -f "$_w_f"] && _checkConf "$1" "$_w_f"; then + return 0 + fi + done + #not found + return 1 + elif [ -f "$2" ]; then + _debug "single" + if _isRealNginxConf "$1" "$2"; then + _debug "$2 is found." + FOUND_REAL_NGINX_CONF="$2" + return 0 + fi + if cat "$2" | tr "\t" " " | grep "^ *include *.*;" >/dev/null; then + _debug "Try include files" + for included in $(cat "$2" | tr "\t" " " | grep "^ *include *.*;" | sed "s/include //" | tr -d " ;"); do + _debug "check included $included" + if _checkConf "$1" "$included"; then + return 0 + fi + done + fi + return 1 + else + _debug "$2 not found." + return 1 + fi + return 1 +} + +#d , conf +_isRealNginxConf() { + _debug "_isRealNginxConf $1 $2" + if [ -f "$2" ]; then + for _fln in $(grep -n "^ *server_name.* $1" "$2" | cut -d : -f 1); do + _debug _fln "$_fln" + if [ "$_fln" ]; then + _start=$(cat "$2" | _head_n "$_fln" | grep -n "^ *server *{" | _tail_n 1) + _debug "_start" "$_start" + _start_n=$(echo "$_start" | cut -d : -f 1) + _start_nn=$(_math $_start_n + 1) + _debug "_start_n" "$_start_n" + _debug "_start_nn" "$_start_nn" + + _left="$(sed -n "${_start_nn},99999p" "$2")" + _debug2 _left "$_left" + if echo "$_left" | grep -n "^ *server *{" >/dev/null; then + _end=$(echo "$_left" | grep -n "^ *server *{" | _head_n 1) + _debug "_end" "$_end" + _end_n=$(echo "$_end" | cut -d : -f 1) + _debug "_end_n" "$_end_n" + _seg_n=$(echo "$_left" | sed -n "1,${_end_n}p") + else + _seg_n="$_left" + fi + + _debug "_seg_n" "$_seg_n" + + if [ "$(echo "$_seg_n" | _egrep_o "^ *ssl *on *;")" ]; then + _debug "ssl on, skip" + return 1 + fi + FOUND_REAL_NGINX_CONF_LN=$_fln + return 0 + fi + done + fi + return 1 +} + +#restore all the nginx conf +_restoreNginx() { + if [ -z "$NGINX_RESTORE_VLIST" ]; then + _debug "No need to restore nginx, skip." + return + fi + _debug "_restoreNginx" + _debug "NGINX_RESTORE_VLIST" "$NGINX_RESTORE_VLIST" + + for ng_entry in $(echo "$NGINX_RESTORE_VLIST" | tr "$dvsep" ' '); do + _debug "ng_entry" "$ng_entry" + _nd=$(echo "$ng_entry" | cut -d "$sep" -f 1) + _ngconf=$(echo "$ng_entry" | cut -d "$sep" -f 2) + _ngbackupconf=$(echo "$ng_entry" | cut -d "$sep" -f 3) + _info "Restoring from $_ngbackupconf to $_ngconf" + cat "$_ngbackupconf" >"$_ngconf" + done + + _info "Reload nginx" + if ! _exec "nginx -s reload" >/dev/null; then + _exec_err + _err "It seems that nginx reload error, please report bug." + return 1 + fi + return 0 +} + _clearup() { _stopserver "$serverproc" serverproc="" _restoreApache + _restoreNginx _clearupdns if [ -z "$DEBUG" ]; then rm -f "$TLS_CONF" @@ -2005,10 +2680,10 @@ _clearupdns() { keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _urlencode)" + txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" _debug txt "$txt" if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then - _info "$d is already verified, skip $vtype." + _debug "$d is already verified, skip $vtype." continue fi @@ -2080,34 +2755,39 @@ _clearupwebbroot() { } _on_before_issue() { + _chk_web_roots="$1" + _chk_main_domain="$2" + _chk_alt_domains="$3" + _chk_pre_hook="$4" + _chk_local_addr="$5" _debug _on_before_issue #run pre hook - if [ "$Le_PreHook" ]; then - _info "Run pre hook:'$Le_PreHook'" + if [ "$_chk_pre_hook" ]; then + _info "Run pre hook:'$_chk_pre_hook'" if ! ( - cd "$DOMAIN_PATH" && eval "$Le_PreHook" + cd "$DOMAIN_PATH" && eval "$_chk_pre_hook" ); then _err "Error when run pre hook." return 1 fi fi - if _hasfield "$Le_Webroot" "$NO_VALUE"; then + if _hasfield "$_chk_web_roots" "$NO_VALUE"; then if ! _exists "nc"; then _err "Please install netcat(nc) tools first." return 1 fi fi - _debug Le_LocalAddress "$Le_LocalAddress" + _debug Le_LocalAddress "$_chk_local_addr" - alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ') + alldomains=$(echo "$_chk_main_domain,$_chk_alt_domains" | tr ',' ' ') _index=1 _currentRoot="" _addrIndex=1 for d in $alldomains; do _debug "Check for domain" "$d" - _currentRoot="$(_getfield "$Le_Webroot" $_index)" + _currentRoot="$(_getfield "$_chk_web_roots" $_index)" _debug "_currentRoot" "$_currentRoot" _index=$(_math $_index + 1) _checkport="" @@ -2131,7 +2811,7 @@ _on_before_issue() { if [ "$_checkport" ]; then _debug _checkport "$_checkport" - _checkaddr="$(_getfield "$Le_LocalAddress" $_addrIndex)" + _checkaddr="$(_getfield "$_chk_local_addr" $_addrIndex)" _debug _checkaddr "$_checkaddr" _addrIndex="$(_math $_addrIndex + 1)" @@ -2150,7 +2830,7 @@ _on_before_issue() { fi done - if _hasfield "$Le_Webroot" "apache"; then + if _hasfield "$_chk_web_roots" "apache"; then if ! _setApache; then _err "set up apache error. Report error to me." return 1 @@ -2162,6 +2842,8 @@ _on_before_issue() { } _on_issue_err() { + _chk_post_hook="$1" + _chk_vlist="$2" _debug _on_issue_err if [ "$LOG_FILE" ]; then _err "Please check log file for more details: $LOG_FILE" @@ -2170,29 +2852,49 @@ _on_issue_err() { _err "See: $_DEBUG_WIKI" fi - if [ "$DEBUG" ] && [ "$DEBUG" -gt "0" ]; then - _debug "$(_dlg_versions)" - fi - #run the post hook - if [ "$Le_PostHook" ]; then - _info "Run post hook:'$Le_PostHook'" + if [ "$_chk_post_hook" ]; then + _info "Run post hook:'$_chk_post_hook'" if ! ( - cd "$DOMAIN_PATH" && eval "$Le_PostHook" + cd "$DOMAIN_PATH" && eval "$_chk_post_hook" ); then _err "Error when run post hook." return 1 fi fi + + #trigger the validation to flush the pending authz + if [ "$_chk_vlist" ]; then + ( + _debug2 "_chk_vlist" "$_chk_vlist" + _debug2 "start to deactivate authz" + ventries=$(echo "$_chk_vlist" | tr "$dvsep" ' ') + for ventry in $ventries; do + d=$(echo "$ventry" | cut -d "$sep" -f 1) + keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) + uri=$(echo "$ventry" | cut -d "$sep" -f 3) + vtype=$(echo "$ventry" | cut -d "$sep" -f 4) + _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) + __trigger_validaton "$uri" "$keyauthorization" + done + ) + fi + + if [ "$DEBUG" ] && [ "$DEBUG" -gt "0" ]; then + _debug "$(_dlg_versions)" + fi + } _on_issue_success() { + _chk_post_hook="$1" + _chk_renew_hook="$2" _debug _on_issue_success #run the post hook - if [ "$Le_PostHook" ]; then - _info "Run post hook:'$Le_PostHook'" + if [ "$_chk_post_hook" ]; then + _info "Run post hook:'$_chk_post_hook'" if ! ( - cd "$DOMAIN_PATH" && eval "$Le_PostHook" + cd "$DOMAIN_PATH" && eval "$_chk_post_hook" ); then _err "Error when run post hook." return 1 @@ -2200,10 +2902,10 @@ _on_issue_success() { fi #run renew hook - if [ "$IS_RENEW" ] && [ "$Le_RenewHook" ]; then - _info "Run renew hook:'$Le_RenewHook'" + if [ "$IS_RENEW" ] && [ "$_chk_renew_hook" ]; then + _info "Run renew hook:'$_chk_renew_hook'" if ! ( - cd "$DOMAIN_PATH" && eval "$Le_RenewHook" + cd "$DOMAIN_PATH" && eval "$_chk_renew_hook" ); then _err "Error when run renew hook." return 1 @@ -2227,6 +2929,10 @@ __calcAccountKeyHash() { [ -f "$ACCOUNT_KEY_PATH" ] && _digest sha256 <"$ACCOUNT_KEY_PATH" } +__calc_account_thumbprint() { + printf "%s" "$jwk" | tr -d ' ' | _digest "sha256" | _url_replace +} + #keylength _regAccount() { _initpath @@ -2317,6 +3023,8 @@ _regAccount() { return 1 fi fi + ACCOUNT_THUMBPRINT="$(__calc_account_thumbprint)" + _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT" return 0 done @@ -2388,44 +3096,54 @@ __get_domain_new_authz() { } -#webroot, domain domainlist keylength +#uri keyAuthorization +__trigger_validaton() { + _debug2 "tigger domain validation." + _t_url="$1" + _debug2 _t_url "$_t_url" + _t_key_authz="$2" + _debug2 _t_key_authz "$_t_key_authz" + _send_signed_request "$_t_url" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$_t_key_authz\"}" +} + +#webroot, domain domainlist keylength issue() { if [ -z "$2" ]; then _usage "Usage: $PROJECT_ENTRY --issue -d a.com -w /path/to/webroot/a.com/ " return 1 fi - Le_Webroot="$1" - Le_Domain="$2" - Le_Alt="$3" - if _contains "$Le_Domain" ","; then - Le_Domain=$(echo "$2,$3" | cut -d , -f 1) - Le_Alt=$(echo "$2,$3" | cut -d , -f 2- | sed "s/,${NO_VALUE}$//") + _web_roots="$1" + _main_domain="$2" + _alt_domains="$3" + if _contains "$_main_domain" ","; then + _main_domain=$(echo "$2,$3" | cut -d , -f 1) + _alt_domains=$(echo "$2,$3" | cut -d , -f 2- | sed "s/,${NO_VALUE}$//") fi - Le_Keylength="$4" - Le_RealCertPath="$5" - Le_RealKeyPath="$6" - Le_RealCACertPath="$7" - Le_ReloadCmd="$8" - Le_RealFullChainPath="$9" - Le_PreHook="${10}" - Le_PostHook="${11}" - Le_RenewHook="${12}" - Le_LocalAddress="${13}" + _key_length="$4" + _real_cert="$5" + _real_key="$6" + _real_ca="$7" + _reload_cmd="$8" + _real_fullchain="$9" + _pre_hook="${10}" + _post_hook="${11}" + _renew_hook="${12}" + _local_addr="${13}" #remove these later. - if [ "$Le_Webroot" = "dns-cf" ]; then - Le_Webroot="dns_cf" + if [ "$_web_roots" = "dns-cf" ]; then + _web_roots="dns_cf" fi - if [ "$Le_Webroot" = "dns-dp" ]; then - Le_Webroot="dns_dp" + if [ "$_web_roots" = "dns-dp" ]; then + _web_roots="dns_dp" fi - if [ "$Le_Webroot" = "dns-cx" ]; then - Le_Webroot="dns_cx" + if [ "$_web_roots" = "dns-cx" ]; then + _web_roots="dns_cx" fi _debug "Using api: $API" if [ ! "$IS_RENEW" ]; then - _initpath "$Le_Domain" "$Le_Keylength" + _initpath "$_main_domain" "$_key_length" mkdir -p "$DOMAIN_PATH" fi @@ -2437,7 +3155,7 @@ issue() { _debug _saved_domain "$_saved_domain" _saved_alt=$(_readdomainconf Le_Alt) _debug _saved_alt "$_saved_alt" - if [ "$_saved_domain,$_saved_alt" = "$Le_Domain,$Le_Alt" ]; then + if [ "$_saved_domain,$_saved_alt" = "$_main_domain,$_alt_domains" ]; then _info "Domains not changed." _info "Skip, Next renewal time is: $(__green "$(_readdomainconf Le_NextRenewTimeStr)")" _info "Add '$(__red '--force')' to force to renew." @@ -2448,16 +3166,16 @@ issue() { fi fi - _savedomainconf "Le_Domain" "$Le_Domain" - _savedomainconf "Le_Alt" "$Le_Alt" - _savedomainconf "Le_Webroot" "$Le_Webroot" + _savedomainconf "Le_Domain" "$_main_domain" + _savedomainconf "Le_Alt" "$_alt_domains" + _savedomainconf "Le_Webroot" "$_web_roots" - _savedomainconf "Le_PreHook" "$Le_PreHook" - _savedomainconf "Le_PostHook" "$Le_PostHook" - _savedomainconf "Le_RenewHook" "$Le_RenewHook" + _savedomainconf "Le_PreHook" "$_pre_hook" + _savedomainconf "Le_PostHook" "$_post_hook" + _savedomainconf "Le_RenewHook" "$_renew_hook" - if [ "$Le_LocalAddress" ]; then - _savedomainconf "Le_LocalAddress" "$Le_LocalAddress" + if [ "$_local_addr" ]; then + _savedomainconf "Le_LocalAddress" "$_local_addr" else _cleardomainconf "Le_LocalAddress" fi @@ -2465,15 +3183,15 @@ issue() { Le_API="$API" _savedomainconf "Le_API" "$Le_API" - if [ "$Le_Alt" = "$NO_VALUE" ]; then - Le_Alt="" + if [ "$_alt_domains" = "$NO_VALUE" ]; then + _alt_domains="" fi - if [ "$Le_Keylength" = "$NO_VALUE" ]; then - Le_Keylength="" + if [ "$_key_length" = "$NO_VALUE" ]; then + _key_length="" fi - if ! _on_before_issue; then + if ! _on_before_issue "$_web_roots" "$_main_domain" "$_alt_domains" "$_pre_hook" "$_local_addr"; then _err "_on_before_issue." return 1 fi @@ -2483,7 +3201,7 @@ issue() { if [ -z "$_saved_account_key_hash" ] || [ "$_saved_account_key_hash" != "$(__calcAccountKeyHash)" ]; then if ! _regAccount "$_accountkeylength"; then - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi else @@ -2495,37 +3213,38 @@ issue() { else _key=$(_readdomainconf Le_Keylength) _debug "Read key length:$_key" - if [ ! -f "$CERT_KEY_PATH" ] || [ "$Le_Keylength" != "$_key" ]; then - if ! createDomainKey "$Le_Domain" "$Le_Keylength"; then + if [ ! -f "$CERT_KEY_PATH" ] || [ "$_key_length" != "$_key" ]; then + if ! createDomainKey "$_main_domain" "$_key_length"; then _err "Create domain key error." _clearup - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi fi - if ! _createcsr "$Le_Domain" "$Le_Alt" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then + if ! _createcsr "$_main_domain" "$_alt_domains" "$CERT_KEY_PATH" "$CSR_PATH" "$DOMAIN_SSL_CONF"; then _err "Create CSR error." _clearup - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi fi - _savedomainconf "Le_Keylength" "$Le_Keylength" + _savedomainconf "Le_Keylength" "$_key_length" vlist="$Le_Vlist" _info "Getting domain auth token for each domain" sep='#' + dvsep=',' if [ -z "$vlist" ]; then - alldomains=$(echo "$Le_Domain,$Le_Alt" | tr ',' ' ') + alldomains=$(echo "$_main_domain,$_alt_domains" | tr ',' ' ') _index=1 _currentRoot="" for d in $alldomains; do _info "Getting webroot for domain" "$d" - _w="$(echo $Le_Webroot | cut -d , -f $_index)" - _info _w "$_w" + _w="$(echo $_web_roots | cut -d , -f $_index)" + _debug _w "$_w" if [ "$_w" ]; then _currentRoot="$_w" fi @@ -2543,13 +3262,12 @@ issue() { if ! __get_domain_new_authz "$d"; then _clearup - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi if [ -z "$thumbprint" ]; then - accountkey_json=$(printf "%s" "$jwk" | tr -d ' ') - thumbprint=$(printf "%s" "$accountkey_json" | _digest "sha256" | _urlencode) + thumbprint="$(__calc_account_thumbprint)" fi entry="$(printf "%s\n" "$response" | _egrep_o '[^\{]*"type":"'$vtype'"[^\}]*')" @@ -2557,7 +3275,7 @@ issue() { if [ -z "$entry" ]; then _err "Error, can not get domain token $d" _clearup - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi token="$(printf "%s\n" "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" @@ -2570,7 +3288,7 @@ issue() { _debug keyauthorization "$keyauthorization" if printf "%s" "$response" | grep '"status":"valid"' >/dev/null 2>&1; then - _info "$d is already verified, skip." + _debug "$d is already verified, skip." keyauthorization="$STATE_VERIFIED" _debug keyauthorization "$keyauthorization" fi @@ -2578,13 +3296,13 @@ issue() { dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot" _debug dvlist "$dvlist" - vlist="$vlist$dvlist," + vlist="$vlist$dvlist$dvsep" done - + _debug vlist "$vlist" #add entry dnsadded="" - ventries=$(echo "$vlist" | tr ',' ' ') + ventries=$(echo "$vlist" | tr "$dvsep" ' ') for ventry in $ventries; do d=$(echo "$ventry" | cut -d "$sep" -f 1) keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) @@ -2592,7 +3310,7 @@ issue() { _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) if [ "$keyauthorization" = "$STATE_VERIFIED" ]; then - _info "$d is already verified, skip $vtype." + _debug "$d is already verified, skip $vtype." continue fi @@ -2600,7 +3318,7 @@ issue() { dnsadded='0' txtdomain="_acme-challenge.$d" _debug txtdomain "$txtdomain" - txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _urlencode)" + txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)" _debug txt "$txt" d_api="$(_findHook "$d" dnsapi "$_currentRoot")" @@ -2638,7 +3356,7 @@ issue() { if [ "$?" != "0" ]; then _clearup - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi dnsadded='1' @@ -2650,7 +3368,7 @@ issue() { _debug "Dns record not added yet, so, save to $DOMAIN_CONF and exit." _err "Please add the TXT records to the domains, and retry again." _clearup - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi @@ -2667,10 +3385,11 @@ issue() { _sleep "$Le_DNSSleep" fi + NGINX_RESTORE_VLIST="" _debug "ok, let's start to verify" _ncIndex=1 - ventries=$(echo "$vlist" | tr ',' ' ') + ventries=$(echo "$vlist" | tr "$dvsep" ' ') for ventry in $ventries; do d=$(echo "$ventry" | cut -d "$sep" -f 1) keyauthorization=$(echo "$ventry" | cut -d "$sep" -f 2) @@ -2695,18 +3414,38 @@ issue() { if [ "$vtype" = "$VTYPE_HTTP" ]; then if [ "$_currentRoot" = "$NO_VALUE" ]; then _info "Standalone mode server" - _ncaddr="$(_getfield "$Le_LocalAddress" "$_ncIndex")" + _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")" _ncIndex="$(_math $_ncIndex + 1)" _startserver "$keyauthorization" "$_ncaddr" & if [ "$?" != "0" ]; then _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi serverproc="$!" sleep 1 _debug serverproc "$serverproc" + elif [ "$_currentRoot" = "$MODE_STATELESS" ]; then + _info "Stateless mode for domain:$d" + _sleep 1 + elif _startswith "$_currentRoot" "$NGINX"; then + _info "Nginx mode for domain:$d" + #set up nginx server + FOUND_REAL_NGINX_CONF="" + BACKUP_NGINX_CONF="" + if ! _setNginx "$d" "$_currentRoot" "$thumbprint"; then + _clearup + _on_issue_err "$_post_hook" "$vlist" + return 1 + fi + if [ "$FOUND_REAL_NGINX_CONF" ]; then + _realConf="$FOUND_REAL_NGINX_CONF" + _backup="$BACKUP_NGINX_CONF" + _debug _realConf "$_realConf" + NGINX_RESTORE_VLIST="$d$sep$_realConf$sep$_backup$dvsep$NGINX_RESTORE_VLIST" + fi + _sleep 1 else if [ "$_currentRoot" = "apache" ]; then wellknown_path="$ACME_DIR" @@ -2731,7 +3470,7 @@ issue() { _err "$d:Can not write token to file : $wellknown_path/$token" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi @@ -2770,22 +3509,22 @@ issue() { _SAN_B="$_x.$_y.acme.invalid" _debug2 _SAN_B "$_SAN_B" - _ncaddr="$(_getfield "$Le_LocalAddress" "$_ncIndex")" + _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")" _ncIndex="$(_math "$_ncIndex" + 1)" if ! _starttlsserver "$_SAN_B" "$_SAN_A" "$Le_TLSPort" "$keyauthorization" "$_ncaddr"; then _err "Start tls server error." _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi fi - if ! _send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"; then + if ! __trigger_validaton "$uri" "$keyauthorization"; then _err "$d:Can not get challenge: $response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi @@ -2793,7 +3532,7 @@ issue() { _err "$d:Challenge error: $response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi @@ -2808,7 +3547,7 @@ issue() { _err "$d:Timeout" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi @@ -2820,7 +3559,7 @@ issue() { _err "$d:Verify error:$response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi _debug2 original "$response" @@ -2855,7 +3594,7 @@ issue() { fi _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi @@ -2865,7 +3604,7 @@ issue() { _err "$d:Verify error:$response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup - _on_issue_err + _on_issue_err "$_post_hook" "$vlist" return 1 fi @@ -2875,11 +3614,11 @@ issue() { _clearup _info "Verify finished, start to sign." - der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _urlencode)" + der="$(_getfile "${CSR_PATH}" "${BEGIN_CSR}" "${END_CSR}" | tr -d "\r\n" | _url_replace)" if ! _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"; then _err "Sign failed." - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi @@ -2892,7 +3631,7 @@ issue() { #if ! _get "$Le_LinkCert" | _base64 "multiline" >> "$CERT_PATH" ; then # _debug "Get cert failed. Let's try last response." - # printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >> "$CERT_PATH" + # printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >> "$CERT_PATH" #fi if ! printf -- "%s" "$_rcert" | _dbase64 "multiline" | _base64 "multiline" >>"$CERT_PATH"; then @@ -2921,7 +3660,7 @@ issue() { if [ -z "$Le_LinkCert" ]; then response="$(echo "$response" | _dbase64 "multiline" | _normalizeJson)" _err "Sign failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')" - _on_issue_err + _on_issue_err "$_post_hook" return 1 fi @@ -2983,10 +3722,15 @@ issue() { Le_NextRenewTime=$(_math "$Le_NextRenewTime" - 86400) _savedomainconf "Le_NextRenewTime" "$Le_NextRenewTime" - _on_issue_success + _on_issue_success "$_post_hook" "$_renew_hook" - if [ "$Le_RealCertPath$Le_RealKeyPath$Le_RealCACertPath$Le_ReloadCmd$Le_RealFullChainPath" ]; then - _installcert + if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then + _savedomainconf "Le_RealCertPath" "$_real_cert" + _savedomainconf "Le_RealCACertPath" "$_real_ca" + _savedomainconf "Le_RealKeyPath" "$_real_key" + _savedomainconf "Le_ReloadCmd" "$_reload_cmd" + _savedomainconf "Le_RealFullChainPath" "$_real_fullchain" + _installcert "$_main_domain" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd" fi } @@ -3039,7 +3783,7 @@ renew() { fi if [ "$Le_DeployHook" ]; then - deploy "$Le_Domain" "$Le_DeployHook" "$Le_Keylength" + _deploy "$Le_Domain" "$Le_DeployHook" res="$?" fi @@ -3211,151 +3955,169 @@ list() { } +_deploy() { + _d="$1" + _hooks="$2" + + for _d_api in $(echo "$_hooks" | tr ',' " "); do + _deployApi="$(_findHook "$_d" deploy "$_d_api")" + if [ -z "$_deployApi" ]; then + _err "The deploy hook $_d_api is not found." + return 1 + fi + _debug _deployApi "$_deployApi" + + if ! ( + if ! . "$_deployApi"; then + _err "Load file $_deployApi error. Please check your api file and try again." + return 1 + fi + + d_command="${_d_api}_deploy" + if ! _exists "$d_command"; then + _err "It seems that your api file is not correct, it must have a function named: $d_command" + return 1 + fi + + if ! $d_command "$_d" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then + _err "Error deploy for domain:$_d" + return 1 + fi + ); then + _err "Deploy error." + return 1 + else + _info "$(__green Success)" + fi + done +} + +#domain hooks deploy() { - Le_Domain="$1" - Le_DeployHook="$2" + _d="$1" + _hooks="$2" _isEcc="$3" - if [ -z "$Le_DeployHook" ]; then + if [ -z "$_hooks" ]; then _usage "Usage: $PROJECT_ENTRY --deploy -d domain.com --deploy-hook cpanel [--ecc] " return 1 fi - _initpath "$Le_Domain" "$_isEcc" + _initpath "$_d" "$_isEcc" if [ ! -d "$DOMAIN_PATH" ]; then - _err "Domain is not valid:'$Le_Domain'" + _err "Domain is not valid:'$_d'" return 1 fi - _deployApi="$(_findHook "$Le_Domain" deploy "$Le_DeployHook")" - if [ -z "$_deployApi" ]; then - _err "The deploy hook $Le_DeployHook is not found." - return 1 - fi - _debug _deployApi "$_deployApi" + . "$DOMAIN_CONF" - _savedomainconf Le_DeployHook "$Le_DeployHook" - - if ! ( - if ! . "$_deployApi"; then - _err "Load file $_deployApi error. Please check your api file and try again." - return 1 - fi - - d_command="${Le_DeployHook}_deploy" - if ! _exists "$d_command"; then - _err "It seems that your api file is not correct, it must have a function named: $d_command" - return 1 - fi - - if ! $d_command "$Le_Domain" "$CERT_KEY_PATH" "$CERT_PATH" "$CA_CERT_PATH" "$CERT_FULLCHAIN_PATH"; then - _err "Error deploy for domain:$Le_Domain" - _on_issue_err - return 1 - fi - ); then - _err "Deploy error." - return 1 - else - _info "$(__green Success)" - fi + _savedomainconf Le_DeployHook "$_hooks" + _deploy "$_d" "$_hooks" } installcert() { - Le_Domain="$1" - if [ -z "$Le_Domain" ]; then + _main_domain="$1" + if [ -z "$_main_domain" ]; then _usage "Usage: $PROJECT_ENTRY --installcert -d domain.com [--ecc] [--certpath cert-file-path] [--keypath key-file-path] [--capath ca-cert-file-path] [ --reloadCmd reloadCmd] [--fullchainpath fullchain-path]" return 1 fi - Le_RealCertPath="$2" - Le_RealKeyPath="$3" - Le_RealCACertPath="$4" - Le_ReloadCmd="$5" - Le_RealFullChainPath="$6" + _real_cert="$2" + _real_key="$3" + _real_ca="$4" + _reload_cmd="$5" + _real_fullchain="$6" _isEcc="$7" - _initpath "$Le_Domain" "$_isEcc" + _initpath "$_main_domain" "$_isEcc" if [ ! -d "$DOMAIN_PATH" ]; then - _err "Domain is not valid:'$Le_Domain'" + _err "Domain is not valid:'$_main_domain'" return 1 fi - _installcert + _savedomainconf "Le_RealCertPath" "$_real_cert" + _savedomainconf "Le_RealCACertPath" "$_real_ca" + _savedomainconf "Le_RealKeyPath" "$_real_key" + _savedomainconf "Le_ReloadCmd" "$_reload_cmd" + _savedomainconf "Le_RealFullChainPath" "$_real_fullchain" + + _installcert "$_main_domain" "$_real_cert" "$_real_key" "$_real_ca" "$_real_fullchain" "$_reload_cmd" } +#domain cert key ca fullchain reloadcmd backup-prefix _installcert() { - _savedomainconf "Le_RealCertPath" "$Le_RealCertPath" - _savedomainconf "Le_RealCACertPath" "$Le_RealCACertPath" - _savedomainconf "Le_RealKeyPath" "$Le_RealKeyPath" - _savedomainconf "Le_ReloadCmd" "$Le_ReloadCmd" - _savedomainconf "Le_RealFullChainPath" "$Le_RealFullChainPath" + _main_domain="$1" + _real_cert="$2" + _real_key="$3" + _real_ca="$4" + _real_fullchain="$5" + _reload_cmd="$6" + _backup_prefix="$7" - if [ "$Le_RealCertPath" = "$NO_VALUE" ]; then - Le_RealCertPath="" + if [ "$_real_cert" = "$NO_VALUE" ]; then + _real_cert="" fi - if [ "$Le_RealKeyPath" = "$NO_VALUE" ]; then - Le_RealKeyPath="" + if [ "$_real_key" = "$NO_VALUE" ]; then + _real_key="" fi - if [ "$Le_RealCACertPath" = "$NO_VALUE" ]; then - Le_RealCACertPath="" + if [ "$_real_ca" = "$NO_VALUE" ]; then + _real_ca="" fi - if [ "$Le_ReloadCmd" = "$NO_VALUE" ]; then - Le_ReloadCmd="" + if [ "$_reload_cmd" = "$NO_VALUE" ]; then + _reload_cmd="" fi - if [ "$Le_RealFullChainPath" = "$NO_VALUE" ]; then - Le_RealFullChainPath="" + if [ "$_real_fullchain" = "$NO_VALUE" ]; then + _real_fullchain="" fi - if [ "$Le_RealCertPath" ]; then + _backup_path="$DOMAIN_BACKUP_PATH/$_backup_prefix" + mkdir -p "$_backup_path" - _info "Installing cert to:$Le_RealCertPath" - if [ -f "$Le_RealCertPath" ] && [ ! "$IS_RENEW" ]; then - cp "$Le_RealCertPath" "$Le_RealCertPath".bak + if [ "$_real_cert" ]; then + _info "Installing cert to:$_real_cert" + if [ -f "$_real_cert" ] && [ ! "$IS_RENEW" ]; then + cp "$_real_cert" "$_backup_path/cert.bak" fi - cat "$CERT_PATH" >"$Le_RealCertPath" + cat "$CERT_PATH" >"$_real_cert" fi - if [ "$Le_RealCACertPath" ]; then - - _info "Installing CA to:$Le_RealCACertPath" - if [ "$Le_RealCACertPath" = "$Le_RealCertPath" ]; then - echo "" >>"$Le_RealCACertPath" - cat "$CA_CERT_PATH" >>"$Le_RealCACertPath" + if [ "$_real_ca" ]; then + _info "Installing CA to:$_real_ca" + if [ "$_real_ca" = "$_real_cert" ]; then + echo "" >>"$_real_ca" + cat "$CA_CERT_PATH" >>"$_real_ca" else - if [ -f "$Le_RealCACertPath" ] && [ ! "$IS_RENEW" ]; then - cp "$Le_RealCACertPath" "$Le_RealCACertPath".bak + if [ -f "$_real_ca" ] && [ ! "$IS_RENEW" ]; then + cp "$_real_ca" "$_backup_path/ca.bak" fi - cat "$CA_CERT_PATH" >"$Le_RealCACertPath" + cat "$CA_CERT_PATH" >"$_real_ca" fi fi - if [ "$Le_RealKeyPath" ]; then - - _info "Installing key to:$Le_RealKeyPath" - if [ -f "$Le_RealKeyPath" ] && [ ! "$IS_RENEW" ]; then - cp "$Le_RealKeyPath" "$Le_RealKeyPath".bak + if [ "$_real_key" ]; then + _info "Installing key to:$_real_key" + if [ -f "$_real_key" ] && [ ! "$IS_RENEW" ]; then + cp "$_real_key" "$_backup_path/key.bak" fi - cat "$CERT_KEY_PATH" >"$Le_RealKeyPath" + cat "$CERT_KEY_PATH" >"$_real_key" fi - if [ "$Le_RealFullChainPath" ]; then - - _info "Installing full chain to:$Le_RealFullChainPath" - if [ -f "$Le_RealFullChainPath" ] && [ ! "$IS_RENEW" ]; then - cp "$Le_RealFullChainPath" "$Le_RealFullChainPath".bak + if [ "$_real_fullchain" ]; then + _info "Installing full chain to:$_real_fullchain" + if [ -f "$_real_fullchain" ] && [ ! "$IS_RENEW" ]; then + cp "$_real_fullchain" "$_backup_path/fullchain.bak" fi - cat "$CERT_FULLCHAIN_PATH" >"$Le_RealFullChainPath" + cat "$CERT_FULLCHAIN_PATH" >"$_real_fullchain" fi - if [ "$Le_ReloadCmd" ]; then - _info "Run Le_ReloadCmd: $Le_ReloadCmd" + if [ "$_reload_cmd" ]; then + _info "Run reload cmd: $_reload_cmd" if ( export CERT_PATH export CERT_KEY_PATH export CA_CERT_PATH export CERT_FULLCHAIN_PATH - cd "$DOMAIN_PATH" && eval "$Le_ReloadCmd" + cd "$DOMAIN_PATH" && eval "$_reload_cmd" ); then _info "$(__green "Reload success")" else @@ -3453,7 +4215,7 @@ revoke() { return 1 fi - cert="$(_getfile "${CERT_PATH}" "${BEGIN_CERT}" "${END_CERT}" | tr -d "\r\n" | _urlencode)" + cert="$(_getfile "${CERT_PATH}" "${BEGIN_CERT}" "${END_CERT}" | tr -d "\r\n" | _url_replace)" if [ -z "$cert" ]; then _err "Cert for $Le_Domain is empty found, skip." @@ -3648,12 +4410,7 @@ _detect_profile() { _initconf() { _initpath if [ ! -f "$ACCOUNT_CONF_PATH" ]; then - echo "#ACCOUNT_CONF_PATH=xxxx - -#ACCOUNT_EMAIL=aaa@example.com # the account email used to register account. -#ACCOUNT_KEY_PATH=\"/path/to/account.key\" -#CERT_HOME=\"/path/to/cert/home\" - + echo " #LOG_FILE=\"$DEFAULT_LOG_FILE\" #LOG_LEVEL=1 @@ -3661,12 +4418,6 @@ _initconf() { #AUTO_UPGRADE=\"1\" #NO_TIMESTAMP=1 -#OPENSSL_BIN=openssl - -#USER_AGENT=\"$USER_AGENT\" - -#USER_PATH= - " >"$ACCOUNT_CONF_PATH" fi @@ -3694,8 +4445,8 @@ _precheck() { fi fi - if ! _exists "$OPENSSL_BIN"; then - _err "Please install openssl first." + if ! _exists "$ACME_OPENSSL_BIN"; then + _err "Please install openssl first. ACME_OPENSSL_BIN=$ACME_OPENSSL_BIN" _err "We need openssl to generate keys." return 1 fi @@ -3877,7 +4628,7 @@ install() { #Modify shebang if _exists bash; then _info "Good, bash is found, so change the shebang to use bash as preferred." - _shebang='#!/usr/bin/env bash' + _shebang='#!'"$(env bash -c "command -v bash")" _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang" for subf in $_SUB_FOLDERS; do if [ -d "$LE_WORKING_DIR/$subf" ]; then @@ -3972,7 +4723,7 @@ Commands: --version, -v Show version info. --install Install $PROJECT_NAME to your system. --uninstall Uninstall $PROJECT_NAME, and uninstall the cron job. - --upgrade Upgrade $PROJECT_NAME to the latest code from $PROJECT . + --upgrade Upgrade $PROJECT_NAME to the latest code from $PROJECT. --issue Issue a cert. --signcsr Issue a cert from an existing csr. --deploy Deploy the cert to your server. @@ -3987,38 +4738,41 @@ Commands: --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. --cron Run cron job to renew all the certs. --toPkcs Export the certificate and key to a pfx file. + --toPkcs8 Convert to pkcs8 format. --update-account Update account info. --register-account Register account key. - --createAccountKey, -cak Create an account private key, professional use. - --createDomainKey, -cdk Create an domain private key, professional use. + --create-account-key Create an account private key, professional use. + --create-domain-key Create an domain private key, professional use. --createCSR, -ccsr Create CSR , professional use. --deactivate Deactivate the domain authz, professional use. - + Parameters: --domain, -d domain.tld Specifies a domain, used to issue, renew or revoke etc. --force, -f Used to force to install or force to renew a cert immediately. --staging, --test Use staging server, just for test. --debug Output debug info. - + --output-insecure Output all the sensitive messages. By default all the credentials/sensitive messages are hidden from the output/debug/log for secure. --webroot, -w /path/to/webroot Specifies the web root folder for web root mode. --standalone Use standalone mode. + --stateless Use stateless mode, see: $_STATELESS_WIKI --tls Use standalone tls mode. --apache Use apache mode. --dns [dns_cf|dns_dp|dns_cx|/path/to/api/file] Use dns mode or dns api. --dnssleep [$DEFAULT_DNS_SLEEP] The time in seconds to wait for all the txt records to take effect in dns api mode. Default $DEFAULT_DNS_SLEEP seconds. - + --keylength, -k [2048] Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384. --accountkeylength, -ak [2048] Specifies the account key length. --log [/path/to/logfile] Specifies the log file. The default is: \"$DEFAULT_LOG_FILE\" if you don't give a file path here. --log-level 1|2 Specifies the log level, default is 1. - + --syslog [0|3|6|7] Syslog level, 0: disable syslog, 3: error, 6: info, 7: debug. + These parameters are to install the cert to nginx/apache or anyother server after issue/renew a cert: - + --certpath /path/to/real/cert/file After issue/renew, the cert will be copied to this path. --keypath /path/to/real/key/file After issue/renew, the key will be copied to this path. --capath /path/to/real/ca/file After issue/renew, the intermediate cert will be copied to this path. --fullchainpath /path/to/fullchain/file After issue/renew, the fullchain cert will be copied to this path. - + --reloadcmd \"service nginx reload\" After issue/renew, it's used to reload the server. --accountconf Specifies a customized account config file. @@ -4048,6 +4802,7 @@ Parameters: --listen-v4 Force standalone/tls server to listen at ipv4. --listen-v6 Force standalone/tls server to listen at ipv6. --openssl-bin Specifies a custom openssl bin location. + --use-wget Force to use wget, if you have both curl and wget installed. " } @@ -4115,9 +4870,9 @@ _processAccountConf() { fi if [ "$_openssl_bin" ]; then - _saveaccountconf "OPENSSL_BIN" "$_openssl_bin" - elif [ "$OPENSSL_BIN" ] && [ "$OPENSSL_BIN" != "$DEFAULT_OPENSSL_BIN" ]; then - _saveaccountconf "OPENSSL_BIN" "$OPENSSL_BIN" + _saveaccountconf "ACME_OPENSSL_BIN" "$_openssl_bin" + elif [ "$ACME_OPENSSL_BIN" ] && [ "$ACME_OPENSSL_BIN" != "$DEFAULT_OPENSSL_BIN" ]; then + _saveaccountconf "ACME_OPENSSL_BIN" "$ACME_OPENSSL_BIN" fi if [ "$_auto_upgrade" ]; then @@ -4126,6 +4881,12 @@ _processAccountConf() { _saveaccountconf "AUTO_UPGRADE" "$AUTO_UPGRADE" fi + if [ "$_use_wget" ]; then + _saveaccountconf "ACME_USE_WGET" "$_use_wget" + elif [ "$ACME_USE_WGET" ]; then + _saveaccountconf "ACME_USE_WGET" "$ACME_USE_WGET" + fi + } _process() { @@ -4169,6 +4930,8 @@ _process() { _listen_v4="" _listen_v6="" _openssl_bin="" + _syslog="" + _use_wget="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -4231,10 +4994,13 @@ _process() { --toPkcs) _CMD="toPkcs" ;; - --createAccountKey | --createaccountkey | -cak) + --toPkcs8) + _CMD="toPkcs8" + ;; + --createAccountKey | --createaccountkey | -cak | --create-account-key) _CMD="createAccountKey" ;; - --createDomainKey | --createdomainkey | -cdk) + --createDomainKey | --createdomainkey | -cdk | --create-domain-key) _CMD="createDomainKey" ;; --createCSR | --createcsr | -ccr) @@ -4284,12 +5050,15 @@ _process() { ;; --debug) if [ -z "$2" ] || _startswith "$2" "-"; then - DEBUG="1" + DEBUG="$DEBUG_LEVEL_DEFAULT" else DEBUG="$2" shift fi ;; + --output-insecure) + export OUTPUT_INSECURE=1 + ;; --webroot | -w) wvalue="$2" if [ -z "$_webroot" ]; then @@ -4307,6 +5076,14 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; + --stateless) + wvalue="$MODE_STATELESS" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; --local-address) lvalue="$2" _local_address="$_local_address$lvalue," @@ -4320,6 +5097,14 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; + --nginx) + wvalue="$NGINX" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; --tls) wvalue="$W_TLS" if [ -z "$_webroot" ]; then @@ -4440,7 +5225,7 @@ _process() { HTTPS_INSECURE="1" ;; --ca-bundle) - _ca_bundle="$(readlink -f "$2")" + _ca_bundle="$(_readlink -f "$2")" CA_BUNDLE="$_ca_bundle" shift ;; @@ -4467,7 +5252,11 @@ _process() { shift ;; --deploy-hook) - _deploy_hook="$2" + if [ -z "$2" ] || _startswith "$2" "-"; then + _usage "Please specify a value for '--deploy-hook'" + return 1 + fi + _deploy_hook="$_deploy_hook$2," shift ;; --ocsp-must-staple | --ocsp) @@ -4491,6 +5280,15 @@ _process() { LOG_LEVEL="$_log_level" shift ;; + --syslog) + if ! _startswith "$2" '-'; then + _syslog="$2" + shift + fi + if [ -z "$_syslog" ]; then + _syslog="$SYSLOG_LEVEL_DEFAULT" + fi + ;; --auto-upgrade) _auto_upgrade="$2" if [ -z "$_auto_upgrade" ] || _startswith "$_auto_upgrade" '-'; then @@ -4510,7 +5308,12 @@ _process() { ;; --openssl-bin) _openssl_bin="$2" - OPENSSL_BIN="$_openssl_bin" + ACME_OPENSSL_BIN="$_openssl_bin" + shift + ;; + --use-wget) + _use_wget="1" + ACME_USE_WGET="1" ;; *) _err "Unknown parameter : $1" @@ -4538,6 +5341,21 @@ _process() { LOG_LEVEL="$_log_level" fi + if [ "$_syslog" ]; then + if _exists logger; then + if [ "$_syslog" = "0" ]; then + _clearaccountconf "SYS_LOG" + else + _saveaccountconf "SYS_LOG" "$_syslog" + fi + SYS_LOG="$_syslog" + else + _err "The 'logger' command is not found, can not enable syslog." + _clearaccountconf "SYS_LOG" + SYS_LOG="" + fi + fi + _processAccountConf fi @@ -4596,6 +5414,9 @@ _process() { toPkcs) toPkcs "$_domain" "$_password" "$_ecc" ;; + toPkcs8) + toPkcs8 "$_domain" "$_ecc" + ;; createAccountKey) createAccountKey "$_accountkeylength" ;; @@ -4630,6 +5451,21 @@ _process() { if [ "$_log_level" ]; then _saveaccountconf "LOG_LEVEL" "$_log_level" fi + + if [ "$_syslog" ]; then + if _exists logger; then + if [ "$_syslog" = "0" ]; then + _clearaccountconf "SYS_LOG" + else + _saveaccountconf "SYS_LOG" "$_syslog" + fi + else + _err "The 'logger' command is not found, can not enable syslog." + _clearaccountconf "SYS_LOG" + SYS_LOG="" + fi + fi + _processAccountConf fi diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php index 058004bd4..c1fbd1767 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php @@ -2,7 +2,7 @@ dns_cx_key; $proc_env['CX_Secret'] = (string)$valObj->dns_cx_secret; break; + case 'dns_cyon': + $proc_env['CY_Username'] = (string)$valObj->dns_cyon_user; + $proc_env['CY_Password'] = (string)$valObj->dns_cyon_user; + break; + case 'dns_do': + $proc_env['DO_PID'] = (string)$valObj->dns_do_pid; + $proc_env['DO_PW'] = (string)$valObj->dns_do_password; + break; case 'dns_dp': $proc_env['DP_Id'] = (string)$valObj->dns_dp_id; $proc_env['DP_Key'] = (string)$valObj->dns_dp_key; break; + case 'dns_freedns': + $proc_env['FREEDNS_User'] = (string)$valObj->dns_freedns_user; + $proc_env['FREEDNS_Password'] = (string)$valObj->dns_freedns_password; + break; + case 'dns_gandi_livedns': + $proc_env['GANDI_LIVEDNS_KEY'] = (string)$valObj->dns_gandi_livedns_key; + break; case 'dns_gd': $proc_env['GD_Key'] = (string)$valObj->dns_gd_key; $proc_env['GD_Secret'] = (string)$valObj->dns_gd_secret; @@ -624,6 +639,11 @@ function run_acme_validation($certObj, $valObj, $acctObj) $acme_hook_options[] = "--dnssleep 960"; } break; + case 'dns_linode': + $proc_env['LINODE_API_KEY'] = (string)$valObj->dns_linode_key; + // Linode can take up to 15 to update DNS records + $acme_hook_options[] = "--dnssleep 960"; + break; case 'dns_lua': $proc_env['LUA_Key'] = (string)$valObj->dns_lua_key; $proc_env['LUA_Email'] = (string)$valObj->dns_lua_email; @@ -757,12 +777,13 @@ function import_certificate($certObj, $modelObj) $cert_id = (string)$certObj->id; $cert_filename = "/var/etc/acme-client/certs/${cert_id}/cert.pem"; + $cert_chain_filename = "/var/etc/acme-client/certs/${cert_id}/chain.pem"; $cert_fullchain_filename = "/var/etc/acme-client/certs/${cert_id}/fullchain.pem"; $key_filename = "/var/etc/acme-client/keys/${cert_id}/private.key"; // Check if certificate files can be found clearstatcache(); // don't let the cache fool us - foreach (array($cert_filename, $key_filename, $cert_fullchain_filename) as $file) { + foreach (array($cert_filename, $key_filename, $cert_chain_filename, $cert_fullchain_filename) as $file) { if (is_file($file)) { // certificate file found } else { @@ -771,6 +792,63 @@ function import_certificate($certObj, $modelObj) } } + /* + * Step 1: import CA + */ + + // Read contents from CA file + $ca_content = @file_get_contents($cert_chain_filename); + if ($ca_content != false) { + $ca_subject = cert_get_subject($ca_content, false); + $ca_serial = cert_get_serial($ca_content, false); + $ca_cn = local_cert_get_cn($ca_content, false); + $ca_issuer = cert_get_issuer($ca_content, false); + $ca_purpose = cert_get_purpose($ca_content, false); + } else { + log_error("AcmeClient: unable to read CA certificate content from file"); + return(1); + } + + // Prepare CA for import in Cert Manager + $ca = array(); + $ca['crt'] = base64_encode($ca_content); + $ca['refid'] = uniqid(); + $ca_found = false; + + // Check if CA was previously imported + $cacnt = 0; + foreach ($config['ca'] as $cacrt) { + $cacrt_subject = cert_get_subject($cacrt['crt'], true); + $cacrt_issuer = cert_get_issuer($cacrt['crt'], true); + if (($ca_subject == $cacrt_subject) and ($ca_issuer == $cacrt_issuer)) { + // Use old refid instead of generating a new one + $ca['refid'] = (string)$cacrt['refid']; + $ca_found = true; + break; + } + $cacnt++; + } + + // Collect required CA information + $ca_cn = local_cert_get_cn($ca_content, false); + $ca['descr'] = (string)$ca_cn . ' (Let\'s Encrypt)'; + + // Prepare CA for import + local_ca_import($ca, $ca_content); + + // Update existing CA? + if ($ca_found == true) { + $config['ca'][$cacnt] = $ca; + } else { + // Create new CA item + $config['ca'][] = $ca; + log_error("AcmeClient: importing Let's Encrypt CA: ${ca_cn}"); + } + + /* + * Step 2: import certificate + */ + // Read contents from certificate file $cert_content = @file_get_contents($cert_filename); if ($cert_content != false) { @@ -789,6 +867,7 @@ function import_certificate($certObj, $modelObj) $cert = array(); $cert_refid = uniqid(); $cert['refid'] = $cert_refid; + $cert['caref'] = (string)$ca['refid']; $import_log_message = 'Imported'; $cert_found = false; @@ -822,20 +901,13 @@ function import_certificate($certObj, $modelObj) return(1); } - // Read cert fullchain - $cert_fullchain_content = @file_get_contents($cert_fullchain_filename); - if ($cert_fullchain_content == false) { - log_error("AcmeClient: unable to read full certificate chain from file: ${cert_fullchain_filename}"); - return(1); - } - // Collect required cert information $cert_cn = local_cert_get_cn($cert_content, false); $cert['descr'] = (string)$cert_cn . ' (Let\'s Encrypt)'; $cert['refid'] = $cert_refid; // Prepare certificate for import - cert_import($cert, $cert_fullchain_content, $key_content); + cert_import($cert, $cert_content, $key_content); // Update existing certificate? if ($cert_found == true) { @@ -854,6 +926,10 @@ function import_certificate($certObj, $modelObj) $config['cert'][] = $cert; } + /* + * Step 3: update configuration + */ + // Write changes to config // TODO: Legacy code, should be replaced with code from OPNsense framework write_config("${import_log_message} Let's Encrypt SSL certificate: ${cert_cn}"); @@ -881,6 +957,7 @@ function run_restart_actions($certlist, $modelObj) { global $config; $return = 0; + $configObj = Config::getInstance()->object(); // NOTE: Do NOT run any restart action twice, collect duplicates first. $restart_actions = array(); @@ -895,11 +972,11 @@ function run_restart_actions($certlist, $modelObj) continue; } // Extract restart actions - $_actions = explode(',', $certObj->restartActions); - if (empty($_actions)) { + if (empty((string)$certObj->restartActions)) { // No restart actions configured. continue; } + $_actions = explode(',', $certObj->restartActions); // Walk through all linked restart actions. foreach ($_actions as $_action) { // Extract restart action @@ -965,8 +1042,12 @@ function run_restart_actions($certlist, $modelObj) $proc_stderr = ''; $result = ''; // exit code (or '99' in case of timeout) - // TODO: Make the timeout configurable. - $timeout = '600'; + // Timeout for custom restart actions. + if (!empty((string)$configObj->OPNsense->AcmeClient->settings->restartTimeout)) { + $timeout = (string)$configObj->OPNsense->AcmeClient->settings->restartTimeout; + } else { + $timeout = '600'; + } $starttime = time(); $proc_cmd = (string)$action->custom; @@ -1081,6 +1162,48 @@ function local_cert_get_cn($crt, $decode = true) return ""; } +// taken from system_camanager.php +function local_ca_import(& $ca, $str, $key="", $serial=0) { + global $config; + + $ca['crt'] = base64_encode($str); + if (!empty($key)) { + $ca['prv'] = base64_encode($key); + } + if (!empty($serial)) { + $ca['serial'] = $serial; + } + $subject = cert_get_subject($str, false); + $issuer = cert_get_issuer($str, false); + + // Find my issuer unless self-signed + if($issuer <> $subject) { + $issuer_crt =& lookup_ca_by_subject($issuer); + if($issuer_crt) { + $ca['caref'] = $issuer_crt['refid']; + } + } + + /* Correct if child certificate was loaded first */ + if (is_array($config['ca'])) { + foreach ($config['ca'] as & $oca) { + $issuer = cert_get_issuer($oca['crt']); + if($ca['refid']<>$oca['refid'] && $issuer==$subject) { + $oca['caref'] = $ca['refid']; + } + } + } + if (is_array($config['cert'])) { + foreach ($config['cert'] as & $cert) { + $issuer = cert_get_issuer($cert['crt']); + if($issuer==$subject) { + $cert['caref'] = $ca['refid']; + } + } + } + return true; +} + function base64url_encode($str) { return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ad.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ad.sh old mode 100644 new mode 100755 diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh old mode 100644 new mode 100755 index 98c56f878..f796f076c --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ali.sh @@ -67,7 +67,7 @@ _get_root() { } _ali_rest() { - signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(_hex "$Ali_Secret&")" | _base64) + signature=$(printf "%s" "GET&%2F&$(_ali_urlencode "$query")" | _hmac "sha1" "$(printf "%s" "$Ali_Secret&" | _hex_dump | tr -d " ")" | _base64) signature=$(_ali_urlencode "$signature") url="$Ali_API?$query&Signature=$signature" diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_aws.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_aws.sh old mode 100644 new mode 100755 index 59ef6181d..84aa28d31 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_aws.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_aws.sh @@ -93,7 +93,7 @@ _get_root() { fi if _contains "$response" "$h."; then - hostedzone="$(echo "$response" | sed 's//\n&/g' | _egrep_o ".*?$h.<.Name>.*?<.HostedZone>")" + hostedzone="$(echo "$response" | sed 's//#&/g' | tr '#' '\n' | _egrep_o "[^<]*<.Id>$h.<.Name>.*<.HostedZone>")" _debug hostedzone "$hostedzone" if [ -z "$hostedzone" ]; then _err "Error, can not get hostedzone." @@ -181,10 +181,10 @@ aws_rest() { #kSecret="wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY" ############################ - _debug2 kSecret "$kSecret" + _secure_debug2 kSecret "$kSecret" - kSecretH="$(_hex "$kSecret")" - _debug2 kSecretH "$kSecretH" + kSecretH="$(printf "%s" "$kSecret" | _hex_dump | tr -d " ")" + _secure_debug2 kSecretH "$kSecretH" kDateH="$(printf "$RequestDateOnly%s" | _hmac "$Hash" "$kSecretH" hex)" _debug2 kDateH "$kDateH" diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh index 9c032fd7e..2b6d56915 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cx.sh @@ -82,7 +82,7 @@ existing_records() { return 1 fi - seg=$(printf "%s\n" "$response" | _egrep_o '[^{]*host":"'"$_sub_domain"'"[^}]*\}') + seg=$(printf "%s\n" "$response" | _egrep_o '"record_id":[^{]*host":"'"$_sub_domain"'"[^}]*\}') _debug seg "$seg" if [ -z "$seg" ]; then return 0 @@ -155,7 +155,7 @@ _get_root() { fi if _contains "$response" "$h."; then - seg=$(printf "%s\n" "$response" | _egrep_o '[^{]*"'"$h"'."[^}]*}') + seg=$(printf "%s\n" "$response" | _egrep_o '"id":[^{]*"'"$h"'."[^}]*}') _debug seg "$seg" _domain_id=$(printf "%s\n" "$seg" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \") _debug _domain_id "$_domain_id" diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cyon.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cyon.sh new file mode 100755 index 000000000..c096d8b07 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_cyon.sh @@ -0,0 +1,328 @@ +#!/usr/bin/env sh + +######## +# Custom cyon.ch DNS API for use with [acme.sh](https://github.com/Neilpang/acme.sh) +# +# Usage: acme.sh --issue --dns dns_cyon -d www.domain.com +# +# Dependencies: +# ------------- +# - oathtool (When using 2 Factor Authentication) +# +# Issues: +# ------- +# Any issues / questions / suggestions can be posted here: +# https://github.com/noplanman/cyon-api/issues +# +# Author: Armando Lüscher +######## + +dns_cyon_add() { + _cyon_load_credentials \ + && _cyon_load_parameters "$@" \ + && _cyon_print_header "add" \ + && _cyon_login \ + && _cyon_change_domain_env \ + && _cyon_add_txt \ + && _cyon_logout +} + +dns_cyon_rm() { + _cyon_load_credentials \ + && _cyon_load_parameters "$@" \ + && _cyon_print_header "delete" \ + && _cyon_login \ + && _cyon_change_domain_env \ + && _cyon_delete_txt \ + && _cyon_logout +} + +######################### +### PRIVATE FUNCTIONS ### +######################### + +_cyon_load_credentials() { + # Convert loaded password to/from base64 as needed. + if [ "${CY_Password_B64}" ]; then + CY_Password="$(printf "%s" "${CY_Password_B64}" | _dbase64 "multiline")" + elif [ "${CY_Password}" ]; then + CY_Password_B64="$(printf "%s" "${CY_Password}" | _base64)" + fi + + if [ -z "${CY_Username}" ] || [ -z "${CY_Password}" ]; then + # Dummy entries to satify script checker. + CY_Username="" + CY_Password="" + CY_OTP_Secret="" + + _err "" + _err "You haven't set your cyon.ch login credentials yet." + _err "Please set the required cyon environment variables." + _err "" + return 1 + fi + + # Save the login credentials to the account.conf file. + _debug "Save credentials to account.conf" + _saveaccountconf CY_Username "${CY_Username}" + _saveaccountconf CY_Password_B64 "$CY_Password_B64" + if [ ! -z "${CY_OTP_Secret}" ]; then + _saveaccountconf CY_OTP_Secret "$CY_OTP_Secret" + else + _clearaccountconf CY_OTP_Secret + fi +} + +_cyon_is_idn() { + _idn_temp="$(printf "%s" "${1}" | tr -d "0-9a-zA-Z.,-_")" + _idn_temp2="$(printf "%s" "${1}" | grep -o "xn--")" + [ "$_idn_temp" ] || [ "$_idn_temp2" ] +} + +_cyon_load_parameters() { + # Read the required parameters to add the TXT entry. + # shellcheck disable=SC2018,SC2019 + fulldomain="$(printf "%s" "${1}" | tr "A-Z" "a-z")" + fulldomain_idn="${fulldomain}" + + # Special case for IDNs, as cyon needs a domain environment change, + # which uses the "pretty" instead of the punycode version. + if _cyon_is_idn "${fulldomain}"; then + if ! _exists idn; then + _err "Please install idn to process IDN names." + _err "" + return 1 + fi + + fulldomain="$(idn -u "${fulldomain}")" + fulldomain_idn="$(idn -a "${fulldomain}")" + fi + + _debug fulldomain "${fulldomain}" + _debug fulldomain_idn "${fulldomain_idn}" + + txtvalue="${2}" + _debug txtvalue "${txtvalue}" + + # This header is required for curl calls. + _H1="X-Requested-With: XMLHttpRequest" + export _H1 +} + +_cyon_print_header() { + if [ "${1}" = "add" ]; then + _info "" + _info "+---------------------------------------------+" + _info "| Adding DNS TXT entry to your cyon.ch domain |" + _info "+---------------------------------------------+" + _info "" + _info " * Full Domain: ${fulldomain}" + _info " * TXT Value: ${txtvalue}" + _info "" + elif [ "${1}" = "delete" ]; then + _info "" + _info "+-------------------------------------------------+" + _info "| Deleting DNS TXT entry from your cyon.ch domain |" + _info "+-------------------------------------------------+" + _info "" + _info " * Full Domain: ${fulldomain}" + _info "" + fi +} + +_cyon_get_cookie_header() { + printf "Cookie: %s" "$(grep "cyon=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'cyon=[^;]*;' | tr -d ';')" +} + +_cyon_login() { + _info " - Logging in..." + + username_encoded="$(printf "%s" "${CY_Username}" | _url_encode)" + password_encoded="$(printf "%s" "${CY_Password}" | _url_encode)" + + login_url="https://my.cyon.ch/auth/index/dologin-async" + login_data="$(printf "%s" "username=${username_encoded}&password=${password_encoded}&pathname=%2F")" + + login_response="$(_post "$login_data" "$login_url")" + _debug login_response "${login_response}" + + # Bail if login fails. + if [ "$(printf "%s" "${login_response}" | _cyon_get_response_success)" != "success" ]; then + _err " $(printf "%s" "${login_response}" | _cyon_get_response_message)" + _err "" + return 1 + fi + + _info " success" + + # NECESSARY!! Load the main page after login, to get the new cookie. + _H2="$(_cyon_get_cookie_header)" + export _H2 + + _get "https://my.cyon.ch/" >/dev/null + + # todo: instead of just checking if the env variable is defined, check if we actually need to do a 2FA auth request. + + # 2FA authentication with OTP? + if [ ! -z "${CY_OTP_Secret}" ]; then + _info " - Authorising with OTP code..." + + if ! _exists oathtool; then + _err "Please install oathtool to use 2 Factor Authentication." + _err "" + return 1 + fi + + # Get OTP code with the defined secret. + otp_code="$(oathtool --base32 --totp "${CY_OTP_Secret}" 2>/dev/null)" + + login_otp_url="https://my.cyon.ch/auth/multi-factor/domultifactorauth-async" + login_otp_data="totpcode=${otp_code}&pathname=%2F&rememberme=0" + + login_otp_response="$(_post "$login_otp_data" "$login_otp_url")" + _debug login_otp_response "${login_otp_response}" + + # Bail if OTP authentication fails. + if [ "$(printf "%s" "${login_otp_response}" | _cyon_get_response_success)" != "success" ]; then + _err " $(printf "%s" "${login_otp_response}" | _cyon_get_response_message)" + _err "" + return 1 + fi + + _info " success" + fi + + _info "" +} + +_cyon_logout() { + _info " - Logging out..." + + _get "https://my.cyon.ch/auth/index/dologout" >/dev/null + + _info " success" + _info "" +} + +_cyon_change_domain_env() { + _info " - Changing domain environment..." + + # Get the "example.com" part of the full domain name. + domain_env="$(printf "%s" "${fulldomain}" | sed -E -e 's/.*\.(.*\..*)$/\1/')" + _debug "Changing domain environment to ${domain_env}" + + gloo_item_key="$(_get "https://my.cyon.ch/domain/" | tr '\n' ' ' | sed -E -e "s/.*data-domain=\"${domain_env}\"[^<]*data-itemkey=\"([^\"]*).*/\1/")" + _debug gloo_item_key "${gloo_item_key}" + + domain_env_url="https://my.cyon.ch/user/environment/setdomain/d/${domain_env}/gik/${gloo_item_key}" + + domain_env_response="$(_get "${domain_env_url}")" + _debug domain_env_response "${domain_env_response}" + + if ! _cyon_check_if_2fa_missed "${domain_env_response}"; then return 1; fi + + domain_env_success="$(printf "%s" "${domain_env_response}" | _egrep_o '"authenticated":\w*' | cut -d : -f 2)" + + # Bail if domain environment change fails. + if [ "${domain_env_success}" != "true" ]; then + _err " $(printf "%s" "${domain_env_response}" | _cyon_get_response_message)" + _err "" + return 1 + fi + + _info " success" + _info "" +} + +_cyon_add_txt() { + _info " - Adding DNS TXT entry..." + + add_txt_url="https://my.cyon.ch/domain/dnseditor/add-record-async" + add_txt_data="zone=${fulldomain_idn}.&ttl=900&type=TXT&value=${txtvalue}" + + add_txt_response="$(_post "$add_txt_data" "$add_txt_url")" + _debug add_txt_response "${add_txt_response}" + + if ! _cyon_check_if_2fa_missed "${add_txt_response}"; then return 1; fi + + add_txt_message="$(printf "%s" "${add_txt_response}" | _cyon_get_response_message)" + add_txt_status="$(printf "%s" "${add_txt_response}" | _cyon_get_response_status)" + + # Bail if adding TXT entry fails. + if [ "${add_txt_status}" != "true" ]; then + _err " ${add_txt_message}" + _err "" + return 1 + fi + + _info " success (TXT|${fulldomain_idn}.|${txtvalue})" + _info "" +} + +_cyon_delete_txt() { + _info " - Deleting DNS TXT entry..." + + list_txt_url="https://my.cyon.ch/domain/dnseditor/list-async" + + list_txt_response="$(_get "${list_txt_url}" | sed -e 's/data-hash/\\ndata-hash/g')" + _debug list_txt_response "${list_txt_response}" + + if ! _cyon_check_if_2fa_missed "${list_txt_response}"; then return 1; fi + + # Find and delete all acme challenge entries for the $fulldomain. + _dns_entries="$(printf "%b\n" "${list_txt_response}" | sed -n 's/data-hash=\\"\([^"]*\)\\" data-identifier=\\"\([^"]*\)\\".*/\1 \2/p')" + + printf "%s" "${_dns_entries}" | while read -r _hash _identifier; do + dns_type="$(printf "%s" "$_identifier" | cut -d'|' -f1)" + dns_domain="$(printf "%s" "$_identifier" | cut -d'|' -f2)" + + if [ "${dns_type}" != "TXT" ] || [ "${dns_domain}" != "${fulldomain_idn}." ]; then + continue + fi + + hash_encoded="$(printf "%s" "${_hash}" | _url_encode)" + identifier_encoded="$(printf "%s" "${_identifier}" | _url_encode)" + + delete_txt_url="https://my.cyon.ch/domain/dnseditor/delete-record-async" + delete_txt_data="$(printf "%s" "hash=${hash_encoded}&identifier=${identifier_encoded}")" + + delete_txt_response="$(_post "$delete_txt_data" "$delete_txt_url")" + _debug delete_txt_response "${delete_txt_response}" + + if ! _cyon_check_if_2fa_missed "${delete_txt_response}"; then return 1; fi + + delete_txt_message="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_message)" + delete_txt_status="$(printf "%s" "${delete_txt_response}" | _cyon_get_response_status)" + + # Skip if deleting TXT entry fails. + if [ "${delete_txt_status}" != "true" ]; then + _err " ${delete_txt_message} (${_identifier})" + else + _info " success (${_identifier})" + fi + done + + _info " done" + _info "" +} + +_cyon_get_response_message() { + _egrep_o '"message":"[^"]*"' | cut -d : -f 2 | tr -d '"' +} + +_cyon_get_response_status() { + _egrep_o '"status":\w*' | cut -d : -f 2 +} + +_cyon_get_response_success() { + _egrep_o '"onSuccess":"[^"]*"' | cut -d : -f 2 | tr -d '"' +} + +_cyon_check_if_2fa_missed() { + # Did we miss the 2FA? + if test "${1#*multi_factor_form}" != "${1}"; then + _err " Missed OTP authentication!" + _err "" + return 1 + fi +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_do.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_do.sh new file mode 100755 index 000000000..3a2f8f495 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_do.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env sh + +# DNS API for Domain-Offensive / Resellerinterface / Domainrobot + +# Report bugs at https://github.com/seidler2547/acme.sh/issues + +# set these environment variables to match your customer ID and password: +# DO_PID="KD-1234567" +# DO_PW="cdfkjl3n2" + +DO_URL="https://soap.resellerinterface.de/" + +######## Public functions ##################### + +#Usage: dns_myapi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_do_add() { + fulldomain=$1 + txtvalue=$2 + if _dns_do_authenticate; then + _info "Adding TXT record to ${_domain} as ${fulldomain}" + _dns_do_soap createRR origin "${_domain}" name "${fulldomain}" type TXT data "${txtvalue}" ttl 300 + if _contains "${response}" '>success<'; then + return 0 + fi + _err "Could not create resource record, check logs" + fi + return 1 +} + +#fulldomain +dns_do_rm() { + fulldomain=$1 + if _dns_do_authenticate; then + if _dns_do_list_rrs; then + _dns_do_had_error=0 + for _rrid in ${_rr_list}; do + _info "Deleting resource record $_rrid for $_domain" + _dns_do_soap deleteRR origin "${_domain}" rrid "${_rrid}" + if ! _contains "${response}" '>success<'; then + _dns_do_had_error=1 + _err "Could not delete resource record for ${_domain}, id ${_rrid}" + fi + done + return $_dns_do_had_error + fi + fi + return 1 +} + +#################### Private functions below ################################## +_dns_do_authenticate() { + _info "Authenticating as ${DO_PID}" + _dns_do_soap authPartner partner "${DO_PID}" password "${DO_PW}" + if _contains "${response}" '>success<'; then + _get_root "$fulldomain" + _debug "_domain $_domain" + return 0 + else + _err "Authentication failed, are DO_PID and DO_PW set correctly?" + fi + return 1 +} + +_dns_do_list_rrs() { + _dns_do_soap getRRList origin "${_domain}" + if ! _contains "${response}" 'SOAP-ENC:Array'; then + _err "getRRList origin ${_domain} failed" + return 1 + fi + _rr_list="$(echo "${response}" \ + | tr -d "\n\r\t" \ + | sed -e 's//\n/g' \ + | grep ">$(_regexcape "$fulldomain")" \ + | sed -e 's/<\/item>/\n/g' \ + | grep '>id[0-9]{1,16}<' \ + | tr -d '><')" + [ "${_rr_list}" ] +} + +_dns_do_soap() { + func="$1" + shift + # put the parameters to xml + body="" + while [ "$1" ]; do + _k="$1" + shift + _v="$1" + shift + body="$body<$_k>$_v" + done + body="$body" + _debug2 "SOAP request ${body}" + + # build SOAP XML + _xml=' + + '"$body"' +' + + # set SOAP headers + export _H1="SOAPAction: ${DO_URL}#${func}" + + if ! response="$(_post "${_xml}" "${DO_URL}")"; then + _err "Error <$1>" + return 1 + fi + _debug2 "SOAP response $response" + + # retrieve cookie header + _H2="$(_egrep_o 'Cookie: [^;]+' <"$HTTP_HEADER" | _head_n 1)" + export _H2 + + return 0 +} + +_get_root() { + domain=$1 + i=1 + + _dns_do_soap getDomainList + _all_domains="$(echo "${response}" \ + | tr -d "\n\r\t " \ + | _egrep_o 'domain]+>[^<]+' \ + | sed -e 's/^domain<\/key>]*>//g')" + + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + if [ -z "$h" ]; then + return 1 + fi + + if _contains "${_all_domains}" "^$(_regexcape "$h")\$"; then + _domain="$h" + return 0 + fi + + i=$(_math $i + 1) + done + _debug "$domain not found" + + return 1 +} + +_regexcape() { + echo "$1" | sed -e 's/\([]\.$*^[]\)/\\\1/g' +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_freedns.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_freedns.sh new file mode 100755 index 000000000..f30c89581 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_freedns.sh @@ -0,0 +1,375 @@ +#!/usr/bin/env sh + +#This file name is "dns_freedns.sh" +#So, here must be a method dns_freedns_add() +#Which will be called by acme.sh to add the txt record to your api system. +#returns 0 means success, otherwise error. +# +#Author: David Kerr +#Report Bugs here: https://github.com/dkerr64/acme.sh +# +######## Public functions ##################### + +# Export FreeDNS userid and password in folowing variables... +# FREEDNS_User=username +# FREEDNS_Password=password +# login cookie is saved in acme account config file so userid / pw +# need to be set only when changed. + +#Usage: dns_freedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_freedns_add() { + fulldomain="$1" + txtvalue="$2" + + _info "Add TXT record using FreeDNS" + _debug "fulldomain: $fulldomain" + _debug "txtvalue: $txtvalue" + + if [ -z "$FREEDNS_User" ] || [ -z "$FREEDNS_Password" ]; then + FREEDNS_User="" + FREEDNS_Password="" + if [ -z "$FREEDNS_COOKIE" ]; then + _err "You did not specify the FreeDNS username and password yet." + _err "Please export as FREEDNS_User / FREEDNS_Password and try again." + return 1 + fi + using_cached_cookies="true" + else + FREEDNS_COOKIE="$(_freedns_login "$FREEDNS_User" "$FREEDNS_Password")" + if [ -z "$FREEDNS_COOKIE" ]; then + return 1 + fi + using_cached_cookies="false" + fi + + _debug "FreeDNS login cookies: $FREEDNS_COOKIE (cached = $using_cached_cookies)" + + _saveaccountconf FREEDNS_COOKIE "$FREEDNS_COOKIE" + + # split our full domain name into two parts... + i="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" + i="$(_math "$i" - 1)" + top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)" + i="$(_math "$i" - 1)" + sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" + + # Sometimes FreeDNS does not reurn the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + _err "Has your FreeDNS username and password channged? If so..." + _err "Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + # Now convert the tables in the HTML to CSV. This litte gem from + # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv + subdomain_csv="$(echo "$htmlpage" \ + | grep -i -e ']*>/\n/Ig' \ + | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \ + | sed 's/^]*>\|<\/\?T[DH][^>]*>$//Ig' \ + | sed 's/<\/T[DH][^>]*>]*>/,/Ig' \ + | grep 'edit.php?' \ + | grep "$top_domain")" + # The above beauty ends with striping out rows that do not have an + # href to edit.php and do not have the top domain we are looking for. + # So all we should be left with is CSV of table of subdomains we are + # interested in. + + # Now we have to read through this table and extract the data we need + lines="$(echo "$subdomain_csv" | wc -l)" + nl=' +' + i=0 + found=0 + while [ "$i" -lt "$lines" ]; do + i="$(_math "$i" + 1)" + line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")" + tmp="$(echo "$line" | cut -d ',' -f 1)" + if [ $found = 0 ] && _startswith "$tmp" "$top_domain"; then + # this line will contain DNSdomainid for the top_domain + DNSdomainid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*domain_id=//;s/>.*//')" + found=1 + else + # lines contain DNS records for all subdomains + DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')" + DNStype="$(echo "$line" | cut -d ',' -f 3)" + if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then + DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')" + # Now get current value for the TXT record. This method may + # not produce accurate results as the value field is truncated + # on this webpage. To get full value we would need to load + # another page. However we don't really need this so long as + # there is only one TXT record for the acme chalenge subdomain. + DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^"]*"//;s/".*//;s/<\/td>.*//')" + if [ $found != 0 ]; then + break + # we are breaking out of the loop at the first match of DNS name + # and DNS type (if we are past finding the domainid). This assumes + # that there is only ever one TXT record for the LetsEncrypt/acme + # challenge subdomain. This seems to be a reasonable assumption + # as the acme client deletes the TXT record on successful validation. + fi + else + DNSname="" + DNStype="" + fi + fi + done + + _debug "DNSname: $DNSname DNStype: $DNStype DNSdomainid: $DNSdomainid DNSdataid: $DNSdataid" + _debug "DNSvalue: $DNSvalue" + + if [ -z "$DNSdomainid" ]; then + # If domain ID is empty then something went wrong (top level + # domain not found at FreeDNS). + if [ "$attempts" = "0" ]; then + # exhausted maximum retry attempts + _debug "$htmlpage" + _debug "$subdomain_csv" + _err "Domain $top_domain not found at FreeDNS" + return 1 + fi + else + # break out of the 'retry' loop... we have found our domain ID + break + fi + _info "Domain $top_domain not found at FreeDNS" + _info "Retry loading subdomain page ($attempts attempts remaining)" + done + + if [ -z "$DNSdataid" ]; then + # If data ID is empty then specific subdomain does not exist yet, need + # to create it this should always be the case as the acme client + # deletes the entry after domain is validated. + _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" + return $? + else + if [ "$txtvalue" = "$DNSvalue" ]; then + # if value in TXT record matches value requested then DNS record + # does not need to be updated. But... + # Testing value match fails. Website is truncating the value field. + # So for now we will always go down the else path. Though in theory + # should never come here anyway as the acme client deletes + # the TXT record on successful validation, so we should not even + # have found a TXT record !! + _info "No update necessary for $fulldomain at FreeDNS" + return 0 + else + # Delete the old TXT record (with the wrong value) + _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" + if [ "$?" = "0" ]; then + # And add in new TXT record with the value provided + _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" + fi + return $? + fi + fi + return 0 +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_freedns_rm() { + fulldomain="$1" + txtvalue="$2" + + _info "Delete TXT record using FreeDNS" + _debug "fulldomain: $fulldomain" + _debug "txtvalue: $txtvalue" + + # Need to read cookie from conf file again in case new value set + # during login to FreeDNS when TXT record was created. + # acme.sh does not have a _readaccountconf() fuction + FREEDNS_COOKIE="$(_read_conf "$ACCOUNT_CONF_PATH" "FREEDNS_COOKIE")" + _debug "FreeDNS login cookies: $FREEDNS_COOKIE" + + # Sometimes FreeDNS does not reurn the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our TXT record. + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + return 1 + fi + + # Now convert the tables in the HTML to CSV. This litte gem from + # http://stackoverflow.com/questions/1403087/how-can-i-convert-an-html-table-to-csv + subdomain_csv="$(echo "$htmlpage" \ + | grep -i -e ']*>/\n/Ig' \ + | sed 's/<\/\?\(TABLE\|TR\)[^>]*>//Ig' \ + | sed 's/^]*>\|<\/\?T[DH][^>]*>$//Ig' \ + | sed 's/<\/T[DH][^>]*>]*>/,/Ig' \ + | grep 'edit.php?' \ + | grep "$fulldomain")" + # The above beauty ends with striping out rows that do not have an + # href to edit.php and do not have the domain name we are looking for. + # So all we should be left with is CSV of table of subdomains we are + # interested in. + + # Now we have to read through this table and extract the data we need + lines="$(echo "$subdomain_csv" | wc -l)" + nl=' +' + i=0 + found=0 + while [ "$i" -lt "$lines" ]; do + i="$(_math "$i" + 1)" + line="$(echo "$subdomain_csv" | cut -d "$nl" -f "$i")" + DNSname="$(echo "$line" | cut -d ',' -f 2 | sed 's/^[^>]*>//;s/<\/a>.*//')" + DNStype="$(echo "$line" | cut -d ',' -f 3)" + if [ "$DNSname" = "$fulldomain" ] && [ "$DNStype" = "TXT" ]; then + DNSdataid="$(echo "$line" | cut -d ',' -f 2 | sed 's/^.*data_id=//;s/>.*//')" + DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^"]*"//;s/".*//;s/<\/td>.*//')" + _debug "DNSvalue: $DNSvalue" + # if [ "$DNSvalue" = "$txtvalue" ]; then + # Testing value match fails. Website is truncating the value + # field. So for now we will assume that there is only one TXT + # field for the sub domain and just delete it. Currently this + # is a safe assumption. + _freedns_delete_txt_record "$FREEDNS_COOKIE" "$DNSdataid" + return $? + # fi + fi + done + done + + # If we get this far we did not find a match (after two attempts) + # Not necessarily an error, but log anyway. + _debug2 "$subdomain_csv" + _info "Cannot delete TXT record for $fulldomain/$txtvalue. Does not exist at FreeDNS" + return 0 +} + +#################### Private functions below ################################## + +# usage: _freedns_login username password +# print string "cookie=value" etc. +# returns 0 success +_freedns_login() { + export _H1="Accept-Language:en-US" + username="$1" + password="$2" + url="https://freedns.afraid.org/zc.php?step=2" + + _debug "Login to FreeDNS as user $username" + + htmlpage="$(_post "username=$(printf '%s' "$username" | _url_encode)&password=$(printf '%s' "$password" | _url_encode)&submit=Login&action=auth" "$url")" + + if [ "$?" != "0" ]; then + _err "FreeDNS login failed for user $username bad RC from _post" + return 1 + fi + + cookies="$(grep -i '^Set-Cookie.*dns_cookie.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" + + # if cookies is not empty then logon successful + if [ -z "$cookies" ]; then + _debug "$htmlpage" + _err "FreeDNS login failed for user $username. Check $HTTP_HEADER file" + return 1 + fi + + printf "%s" "$cookies" + return 0 +} + +# usage _freedns_retrieve_subdomain_page login_cookies +# echo page retrieved (html) +# returns 0 success +_freedns_retrieve_subdomain_page() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + url="https://freedns.afraid.org/subdomain/" + + _debug "Retrieve subdmoain page from FreeDNS" + + htmlpage="$(_get "$url")" + + if [ "$?" != "0" ]; then + _err "FreeDNS retrieve subdomins failed bad RC from _get" + return 1 + elif [ -z "$htmlpage" ]; then + _err "FreeDNS returned empty subdomain page" + return 1 + fi + + _debug2 "$htmlpage" + + printf "%s" "$htmlpage" + return 0 +} + +# usage _freedns_add_txt_record login_cookies domain_id subdomain value +# returns 0 success +_freedns_add_txt_record() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + domain_id="$2" + subdomain="$3" + value="$(printf '%s' "$4" | _url_encode)" + url="http://freedns.afraid.org/subdomain/save.php?step=2" + + htmlpage="$(_post "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")" + + if [ "$?" != "0" ]; then + _err "FreeDNS failed to add TXT record for $subdomain bad RC from _post" + return 1 + elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then + _debug "$htmlpage" + _err "FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file" + return 1 + elif _contains "$htmlpage" "security code was incorrect"; then + _debug "$htmlpage" + _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested seurity code" + _err "Note that you cannot use automatic DNS validation for FreeDNS public domains" + return 1 + fi + + _debug2 "$htmlpage" + _info "Added acme challenge TXT record for $fulldomain at FreeDNS" + return 0 +} + +# usage _freedns_delete_txt_record login_cookies data_id +# returns 0 success +_freedns_delete_txt_record() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + data_id="$2" + url="https://freedns.afraid.org/subdomain/delete2.php" + + htmlheader="$(_get "$url?data_id%5B%5D=$data_id&submit=delete+selected" "onlyheader")" + + if [ "$?" != "0" ]; then + _err "FreeDNS failed to delete TXT record for $data_id bad RC from _get" + return 1 + elif ! _contains "$htmlheader" "200 OK"; then + _debug "$htmlheader" + _err "FreeDNS failed to delete TXT record $data_id" + return 1 + fi + + _info "Deleted acme challenge TXT record for $fulldomain at FreeDNS" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gandi_livedns.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gandi_livedns.sh new file mode 100755 index 000000000..41f42980b --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gandi_livedns.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env sh + +# Gandi LiveDNS v5 API +# http://doc.livedns.gandi.net/ +# currently under beta +# +# Requires GANDI API KEY set in GANDI_LIVEDNS_KEY set as environment variable +# +#Author: Frédéric Crozat +#Report Bugs here: https://github.com/fcrozat/acme.sh +# +######## Public functions ##################### + +GANDI_LIVEDNS_API="https://dns.beta.gandi.net/api/v5" + +#Usage: dns_gandi_livedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_gandi_livedns_add() { + fulldomain=$1 + txtvalue=$2 + + if [ -z "$GANDI_LIVEDNS_KEY" ]; then + _err "No API key specifed for Gandi LiveDNS." + _err "Create your key and export it as GANDI_LIVEDNS_KEY" + return 1 + fi + + _saveaccountconf GANDI_LIVEDNS_KEY "$GANDI_LIVEDNS_KEY" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + _debug domain "$_domain" + _debug sub_domain "$_sub_domain" + + _gandi_livedns_rest PUT "domains/$_domain/records/$_sub_domain/TXT" "{\"rrset_ttl\": 300, \"rrset_values\":[\"$txtvalue\"]}" \ + && _contains "$response" '{"message": "Zone Record Created"}' \ + && _info "Add $(__green "success")" +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_gandi_livedns_rm() { + fulldomain=$1 + txtvalue=$2 + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + + _debug fulldomain "$fulldomain" + _debug domain "$_domain" + _debug sub_domain "$_sub_domain" + + _gandi_livedns_rest DELETE "domains/$_domain/records/$_sub_domain/TXT" "" + +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +_get_root() { + domain=$1 + i=2 + 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 + + if ! _gandi_livedns_rest GET "domains/$h"; then + return 1 + fi + + if _contains "$response" '"code": 401'; then + _err "$response" + return 1 + elif _contains "$response" '"code": 404'; then + _debug "$h not found" + else + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain="$h" + return 0 + fi + p="$i" + i=$(_math "$i" + 1) + done + return 1 +} + +_gandi_livedns_rest() { + m=$1 + ep="$2" + data="$3" + _debug "$ep" + + export _H1="Content-Type: application/json" + export _H2="X-Api-Key: $GANDI_LIVEDNS_KEY" + + if [ "$m" = "GET" ]; then + response="$(_get "$GANDI_LIVEDNS_API/$ep")" + else + _debug data "$data" + response="$(_post "$data" "$GANDI_LIVEDNS_API/$ep" "" "$m")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh index 1abeeacf4..f2dd1fd5a 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_gd.sh @@ -40,7 +40,7 @@ dns_gd_add() { if _gd_rest PUT "domains/$_domain/records/TXT/$_sub_domain" "[{\"data\":\"$txtvalue\"}]"; then if [ "$response" = "{}" ]; then _info "Added, sleeping 10 seconds" - sleep 10 + _sleep 10 #todo: check if the record takes effect return 0 else diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh index c38ff3e38..c09f16fd6 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lexicon.sh @@ -34,7 +34,7 @@ dns_lexicon_add() { # shellcheck disable=SC2018,SC2019 Lx_name=$(echo LEXICON_"${PROVIDER}"_USERNAME | tr 'a-z' 'A-Z') Lx_name_v=$(eval echo \$"$Lx_name") - _debug "$Lx_name" "$Lx_name_v" + _secure_debug "$Lx_name" "$Lx_name_v" if [ "$Lx_name_v" ]; then _saveaccountconf "$Lx_name" "$Lx_name_v" eval export "$Lx_name" @@ -43,7 +43,7 @@ dns_lexicon_add() { # shellcheck disable=SC2018,SC2019 Lx_token=$(echo LEXICON_"${PROVIDER}"_TOKEN | tr 'a-z' 'A-Z') Lx_token_v=$(eval echo \$"$Lx_token") - _debug "$Lx_token" "$Lx_token_v" + _secure_debug "$Lx_token" "$Lx_token_v" if [ "$Lx_token_v" ]; then _saveaccountconf "$Lx_token" "$Lx_token_v" eval export "$Lx_token" @@ -52,7 +52,7 @@ dns_lexicon_add() { # shellcheck disable=SC2018,SC2019 Lx_password=$(echo LEXICON_"${PROVIDER}"_PASSWORD | tr 'a-z' 'A-Z') Lx_password_v=$(eval echo \$"$Lx_password") - _debug "$Lx_password" "$Lx_password_v" + _secure_debug "$Lx_password" "$Lx_password_v" if [ "$Lx_password_v" ]; then _saveaccountconf "$Lx_password" "$Lx_password_v" eval export "$Lx_password" @@ -61,7 +61,7 @@ dns_lexicon_add() { # shellcheck disable=SC2018,SC2019 Lx_domaintoken=$(echo LEXICON_"${PROVIDER}"_DOMAINTOKEN | tr 'a-z' 'A-Z') Lx_domaintoken_v=$(eval echo \$"$Lx_domaintoken") - _debug "$Lx_domaintoken" "$Lx_domaintoken_v" + _secure_debug "$Lx_domaintoken" "$Lx_domaintoken_v" if [ "$Lx_domaintoken_v" ]; then eval export "$Lx_domaintoken" _saveaccountconf "$Lx_domaintoken" "$Lx_domaintoken_v" diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_linode.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_linode.sh new file mode 100755 index 000000000..6d54e6c12 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_linode.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +#Author: Philipp Grosswiler + +LINODE_API_URL="https://api.linode.com/?api_key=$LINODE_API_KEY&api_action=" + +######## Public functions ##################### + +#Usage: dns_linode_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_linode_add() { + fulldomain="${1}" + txtvalue="${2}" + + if ! _Linode_API; then + return 1 + fi + + _info "Using Linode" + _debug "Calling: dns_linode_add() '${fulldomain}' '${txtvalue}'" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _parameters="&DomainID=$_domain_id&Type=TXT&Name=$_sub_domain&Target=$txtvalue" + + if _rest GET "domain.resource.create" "$_parameters" && [ -n "$response" ]; then + _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _resource_id "$_resource_id" + + if [ -z "$_resource_id" ]; then + _err "Error adding the domain resource." + return 1 + fi + + _info "Domain resource successfully added." + return 0 + fi + + return 1 +} + +#Usage: dns_linode_rm _acme-challenge.www.domain.com +dns_linode_rm() { + fulldomain="${1}" + + if ! _Linode_API; then + return 1 + fi + + _info "Using Linode" + _debug "Calling: dns_linode_rm() '${fulldomain}'" + + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + _parameters="&DomainID=$_domain_id" + + if _rest GET "domain.resource.list" "$_parameters" && [ -n "$response" ]; then + response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')" + + resource="$(echo "$response" | _egrep_o "{.*\"NAME\":\s*\"$_sub_domain\".*}")" + if [ "$resource" ]; then + _resource_id=$(printf "%s\n" "$resource" | _egrep_o "\"RESOURCEID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "$_resource_id" ]; then + _debug _resource_id "$_resource_id" + + _parameters="&DomainID=$_domain_id&ResourceID=$_resource_id" + + if _rest GET "domain.resource.delete" "$_parameters" && [ -n "$response" ]; then + _resource_id=$(printf "%s\n" "$response" | _egrep_o "\"ResourceID\":\s*[0-9]+" | cut -d : -f 2 | tr -d " " | _head_n 1) + _debug _resource_id "$_resource_id" + + if [ -z "$_resource_id" ]; then + _err "Error deleting the domain resource." + return 1 + fi + + _info "Domain resource successfully deleted." + return 0 + fi + fi + + return 1 + fi + + return 0 + fi + + return 1 +} + +#################### Private functions below ################################## + +_Linode_API() { + if [ -z "$LINODE_API_KEY" ]; then + LINODE_API_KEY="" + + _err "You didn't specify the Linode API key yet." + _err "Please create your key and try again." + + return 1 + fi + + _saveaccountconf LINODE_API_KEY "$LINODE_API_KEY" +} + +#################### Private functions below ################################## +#_acme-challenge.www.domain.com +#returns +# _sub_domain=_acme-challenge.www +# _domain=domain.com +# _domain_id=12345 +_get_root() { + domain=$1 + i=2 + p=1 + + if _rest GET "domain.list"; then + response="$(echo "$response" | tr -d "\n" | sed 's/{/\n&/g')" + while true; do + h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" + if [ -z "$h" ]; then + #not valid + return 1 + fi + + hostedzone="$(echo "$response" | _egrep_o "{.*\"DOMAIN\":\s*\"$h\".*}")" + if [ "$hostedzone" ]; then + _domain_id=$(printf "%s\n" "$hostedzone" | _egrep_o "\"DOMAINID\":\s*[0-9]+" | _head_n 1 | cut -d : -f 2 | tr -d \ ) + if [ "$_domain_id" ]; then + _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) + _domain=$h + return 0 + fi + return 1 + fi + p=$i + i=$(_math "$i" + 1) + done + fi + return 1 +} + +#method method action data +_rest() { + mtd="$1" + ep="$2" + data="$3" + + _debug mtd "$mtd" + _debug ep "$ep" + + export _H1="Accept: application/json" + export _H2="Content-Type: application/json" + + if [ "$mtd" != "GET" ]; then + # both POST and DELETE. + _debug data "$data" + response="$(_post "$data" "$LINODE_API_URL$ep" "" "$mtd")" + else + response="$(_get "$LINODE_API_URL$ep$data")" + fi + + if [ "$?" != "0" ]; then + _err "error $ep" + return 1 + fi + _debug2 response "$response" + return 0 +} diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh index 47f4497a6..00c544307 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_lua.sh @@ -46,12 +46,12 @@ dns_lua_add() { return 1 fi - count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain\"" | wc -l) + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | wc -l | tr -d " ") _debug count "$count" if [ "$count" = "0" ]; then _info "Adding record" if _LUA_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"ttl\":120}"; then - if printf -- "%s" "$response" | grep "$fulldomain" >/dev/null; then + if _contains "$response" "$fulldomain"; then _info "Added" #todo: check if the record takes effect return 0 @@ -63,11 +63,11 @@ dns_lua_add() { _err "Add txt record error." else _info "Updating record" - record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | cut -d: -f2 | cut -d, -f1) + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | _head_n 1 | cut -d: -f2 | cut -d, -f1) _debug "record_id" "$record_id" - _LUA_rest PUT "zones/$_domain_id/records/$record_id" "{\"id\":\"$record_id\",\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"zone_id\":\"$_domain_id\",\"ttl\":120}" - if [ "$?" = "0" ]; then + _LUA_rest PUT "zones/$_domain_id/records/$record_id" "{\"id\":$record_id,\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"content\":\"$txtvalue\",\"zone_id\":$_domain_id,\"ttl\":120}" + if [ "$?" = "0" ] && _contains "$response" "updated_at"; then _info "Updated!" #todo: check if the record takes effect return 0 @@ -81,7 +81,36 @@ dns_lua_add() { #fulldomain dns_lua_rm() { fulldomain=$1 + txtvalue=$2 + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug "Getting txt records" + _LUA_rest GET "zones/${_domain_id}/records" + + count=$(printf "%s\n" "$response" | _egrep_o "\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | wc -l | tr -d " ") + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$fulldomain.\",\"type\":\"TXT\"" | _head_n 1 | cut -d: -f2 | cut -d, -f1) + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _LUA_rest DELETE "/zones/$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" "$record_id" + fi } #################### Private functions below ################################## @@ -99,6 +128,7 @@ _get_root() { fi while true; do h=$(printf "%s" "$domain" | cut -d . -f $i-100) + _debug h "$h" if [ -z "$h" ]; then #not valid return 1 @@ -106,6 +136,7 @@ _get_root() { if _contains "$response" "\"name\":\"$h\""; then _domain_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*,\"name\":\"$h\"" | cut -d : -f 2 | cut -d , -f 1) + _debug _domain_id "$_domain_id" if [ "$_domain_id" ]; then _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p) _domain="$h" @@ -127,7 +158,7 @@ _LUA_rest() { export _H1="Accept: application/json" export _H2="Authorization: Basic $LUA_auth" - if [ "$data" ]; then + if [ "$m" != "GET" ]; then _debug data "$data" response="$(_post "$data" "$LUA_Api/$ep" "" "$m")" else diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh index a8405f8f3..3393fb757 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_me.sh @@ -2,7 +2,7 @@ # bug reports to dev@1e.ca -# ME_Key=qmlkdjflmkqdjf +# ME_Key=qmlkdjflmkqdjf # ME_Secret=qmsdlkqmlksdvnnpae ME_Api=https://api.dnsmadeeasy.com/V2.0/dns/managed @@ -78,7 +78,36 @@ dns_me_add() { #fulldomain dns_me_rm() { fulldomain=$1 + txtvalue=$2 + _debug "First detect the root zone" + if ! _get_root "$fulldomain"; then + _err "invalid domain" + return 1 + fi + _debug _domain_id "$_domain_id" + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + _debug "Getting txt records" + _me_rest GET "${_domain_id}/records?recordName=$_sub_domain&type=TXT" + + count=$(printf "%s\n" "$response" | _egrep_o "\"totalRecords\":[^,]*" | cut -d : -f 2) + _debug count "$count" + if [ "$count" = "0" ]; then + _info "Don't need to remove." + else + record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":[^,]*" | cut -d : -f 2 | head -n 1) + _debug "record_id" "$record_id" + if [ -z "$record_id" ]; then + _err "Can not get record id to remove." + return 1 + fi + if ! _me_rest DELETE "$_domain_id/records/$record_id"; then + _err "Delete record error." + return 1 + fi + _contains "$response" '' + fi } #################### Private functions below ################################## @@ -124,13 +153,13 @@ _me_rest() { _debug "$ep" cdate=$(date -u +"%a, %d %b %Y %T %Z") - hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(_hex "$ME_Secret")" hex) + hmac=$(printf "%s" "$cdate" | _hmac sha1 "$(printf "%s" "$ME_Secret" | _hex_dump | tr -d " ")" hex) export _H1="x-dnsme-apiKey: $ME_Key" export _H2="x-dnsme-requestDate: $cdate" export _H3="x-dnsme-hmac: $hmac" - if [ "$data" ]; then + if [ "$m" != "GET" ]; then _debug data "$data" response="$(_post "$data" "$ME_Api/$ep" "" "$m")" else diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh index 35bf126ec..faf5b42b3 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/dnsapi/dns_ovh.sh @@ -14,7 +14,7 @@ #'ovh-eu' OVH_EU='https://eu.api.ovh.com/1.0' -#'ovh-ca': +#'ovh-ca': OVH_CA='https://ca.api.ovh.com/1.0' #'kimsufi-eu' @@ -207,7 +207,7 @@ _ovh_authentication() { _err "Unable to get consumerKey" return 1 fi - _debug consumerKey "$consumerKey" + _secure_debug consumerKey "$consumerKey" OVH_CK="$consumerKey" _saveaccountconf OVH_CK "$OVH_CK" @@ -269,7 +269,7 @@ _ovh_rest() { _ovh_t="$(_ovh_timestamp)" _debug2 _ovh_t "$_ovh_t" _ovh_p="$OVH_AS+$OVH_CK+$m+$_ovh_url+$data+$_ovh_t" - _debug _ovh_p "$_ovh_p" + _secure_debug _ovh_p "$_ovh_p" _ovh_hex="$(printf "%s" "$_ovh_p" | _digest sha1 hex)" _debug2 _ovh_hex "$_ovh_hex" diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh index 8215a8262..583e3f9d6 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh @@ -5,7 +5,7 @@ ACME_DIRS="/var/etc/acme-client /var/etc/acme-client/certs /var/etc/acme-client/ for directory in ${ACME_DIRS}; do mkdir -p ${directory} chown -R root:wheel ${directory} - chmod -R 755 ${directory} + chmod -R 750 ${directory} done exit 0