From: Lukas Schauer Date: Sat, 4 Jul 2020 19:36:23 +0000 (+0200) Subject: experimental json.sh support X-Git-Tag: v0.7.0~40 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=7f970b527c6167ad5898b84889e88d646a70b709;p=thirdparty%2Fdehydrated.git experimental json.sh support --- diff --git a/dehydrated b/dehydrated index 351d35e..efa6827 100755 --- a/dehydrated +++ b/dehydrated @@ -17,7 +17,9 @@ umask 077 # paranoid umask, we're creating private keys exec 3>&- exec 4>&- -VERSION="0.6.5" +. ./json.sh + +VERSION="0.6.6" # Find directory in which this script is stored by traversing all symbolic links SOURCE="${0}" @@ -298,7 +300,8 @@ init_system() { fi # Get CA URLs - CA_DIRECTORY="$(http_request get "${CA}")" + CA_DIRECTORY_RAW="$(http_request get "${CA}")" + CA_DIRECTORY="$(printf "%s" "${CA_DIRECTORY_RAW}" | jsonsh)" # Automatic discovery of API version if [[ "${API}" = "auto" ]]; then @@ -407,11 +410,11 @@ init_system() { # Read account information or request from CA if missing if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then if [[ ${API} -eq 1 ]]; then - ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" + ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | jsonsh | get_json_int_value id)" ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}" else if [[ -e "${ACCOUNT_ID_JSON}" ]]; then - ACCOUNT_URL="$(cat "${ACCOUNT_ID_JSON}" | get_json_string_value url)" + ACCOUNT_URL="$(cat "${ACCOUNT_ID_JSON}" | jsonsh | get_json_string_value url)" fi # if account URL is not storred, fetch it from the CA if [[ -z "${ACCOUNT_URL:-}" ]]; then @@ -468,41 +471,6 @@ hex2bin() { printf -- "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" } -# Get string value from json dictionary -get_json_string_value() { - local filter - filter=$(printf 's/.*"%s": *"\([^"]*\)".*/\\1/p' "$1") - sed -n "${filter}" -} - -# Get array value from json dictionary -get_json_array_value() { - local filter - filter=$(printf 's/.*"%s": *\\[\([^]]*\)\\].*/\\1/p' "$1") - sed -n "${filter}" -} - -# Get sub-dictionary from json -get_json_dict_value() { - local filter - filter=$(printf 's/.*"%s": *{\([^}]*\)}.*/\\1/p' "$1") - sed -n "${filter}" -} - -# Get integer value from json -get_json_int_value() { - local filter - filter=$(printf 's/.*"%s": *\([0-9]*\).*/\\1/p' "$1") - sed -n "${filter}" -} - -rm_json_arrays() { - local filter - filter='s/\[[^][]*\]/null/g' - # remove three levels of nested arrays - sed -e "${filter}" -e "${filter}" -e "${filter}" -} - # OpenSSL writes to stderr/stdout even when there are no errors. So just # display the output if the exit code was != 0 to simplify debugging. _openssl() { @@ -702,14 +670,14 @@ sign_csr() { echo " + Requesting new certificate order from CA..." order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')" - result="$(signed_request "${order_location}" "" | clean_json)" + result="$(signed_request "${order_location}" "" | jsonsh)" - order_authorizations="$(echo ${result} | get_json_array_value authorizations)" + order_authorizations="$(echo "${result}" | get_json_array_values authorizations)" finalize="$(echo "${result}" | get_json_string_value finalize)" local idx=0 for uri in ${order_authorizations}; do - authorizations[${idx}]="$(echo "${uri}" | _sed -e 's/\"(.*)".*/\1/')" + authorizations[${idx}]="${uri}" idx=$((idx+1)) done echo " + Received ${idx} authorizations URLs from the CA" @@ -727,14 +695,14 @@ sign_csr() { for authorization in ${authorizations[*]}; do if [[ "${API}" -eq 2 ]]; then # Receive authorization ($authorization is authz uri) - response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | clean_json)" + response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)" identifier="$(echo "${response}" | get_json_dict_value identifier | get_json_string_value value)" echo " + Handling authorization for ${identifier}" else # Request new authorization ($authorization is altname) identifier="${authorization}" echo " + Requesting authorization for ${identifier}..." - response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${identifier}"'"}}' | clean_json)" + response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${identifier}"'"}}' | jsonsh)" fi # Check if authorization has already been validated @@ -744,20 +712,22 @@ sign_csr() { fi # Find challenge in authorization - challenges="$(echo "${response}" | _sed 's/.*"challenges": \[(\{.*\})\].*/\1/')" - challenge="$(<<<"${challenges}" _sed -e 's/^[^\[]+\[(.+)\]$/\1/' -e 's/\}(, (\{)|(\]))/}\'$'\n''\2/g' | grep \""${CHALLENGETYPE}"\" || true)" - if [ -z "${challenge}" ]; then - allowed_validations="$(grep -Eo '"type": "[^"]+"' <<< "${challenges}" | grep -Eo ' "[^"]+"' | _sed -e 's/"//g' -e 's/^ //g')" + challengeindex="$(echo "${response}" | grep -E '^\["challenges",[0-9]+,"type"\]\s+"'"${CHALLENGETYPE}"'"' | cut -d',' -f2 || true)" + + if [ -z "${challengeindex}" ]; then + allowed_validations="$(echo "${response}" | grep -E '^\["challenges",[0-9]+,"type"\]' | sed -e 's/\[[^\]*\]\s*//g' -e 's/^"//' -e 's/"$//' | tr '\n' ' ')" _exiterr "Validating this certificate is not possible using ${CHALLENGETYPE}. Possible validation methods are: ${allowed_validations}" fi + challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")" # Gather challenge information challenge_names[${idx}]="${identifier}" challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)" + if [[ ${API} -eq 2 ]]; then - challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value url)" + challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value url)" else - challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value uri)" + challenge_uris[${idx}]="$(echo "${challenge}" | get_json_dict_value validationRecord | get_json_string_value uri)" fi # Prepare challenge tokens and deployment parameters @@ -810,12 +780,12 @@ sign_csr() { # Ask the acme-server to verify our challenge and wait until it is no longer pending if [[ ${API} -eq 1 ]]; then - result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauths[${idx}]}"'"}' | clean_json)" + result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauths[${idx}]}"'"}' | jsonsh)" else - result="$(signed_request "${challenge_uris[${idx}]}" '{}' | clean_json)" + result="$(signed_request "${challenge_uris[${idx}]}" '{}' | jsonsh)" fi - reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)" + reqstatus="$(echo "${result}" | get_json_string_value status)" while [[ "${reqstatus}" = "pending" ]]; do sleep 1 @@ -824,7 +794,7 @@ sign_csr() { else result="$(http_request get "${challenge_uris[${idx}]}")" fi - reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)" + reqstatus="$(echo "${result}" | get_json_string_value status)" done [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" @@ -870,7 +840,7 @@ sign_csr() { crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)" crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )" else - result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | clean_json)" + result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | jsonsh)" while :; do status="$(echo "${result}" | get_json_string_value status)" case "${status}" diff --git a/json.sh b/json.sh new file mode 100644 index 0000000..84ce9d2 --- /dev/null +++ b/json.sh @@ -0,0 +1,190 @@ +#!/bin/sh + +# Generate json.sh path matching string +json_path() { + if [ ! "${1}" = "-p" ]; then + printf '"%s"' "${1}" + else + printf '%s' "${2}" + fi +} + +# Get string value from json dictionary +get_json_string_value() { + local filter + filter="$(printf 's/.*\[%s\]\s*"\([^"]*\)"/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" +} + +# Get array values from json dictionary +get_json_array_values() { + grep -E '^\["'"$1"'",[0-9]*\]' | sed -e 's/\[[^\]*\]\s*//g' -e 's/^"//' -e 's/"$//' +} + +# Get sub-dictionary from json +get_json_dict_value() { + local filter + echo "$(json_path "${1:-}" "${2:-}")" + filter="$(printf 's/.*\[%s\]\s*\(.*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" | jsonsh +} + +# Get integer value from json +get_json_int_value() { + local filter + filter="$(printf 's/.*\[%s\]\s*\([^"]*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" +} + +jsonsh() { + # Modified from https://github.com/dominictarr/JSON.sh + # Original Copyright (c) 2011 Dominic Tarr + # Licensed under The MIT License + + throw() { + echo "$*" >&2 + exit 1 + } + + awk_egrep () { + local pattern_string=$1 + + gawk '{ + while ($0) { + start=match($0, pattern); + token=substr($0, start, RLENGTH); + print token; + $0=substr($0, start+RLENGTH); + } + }' pattern="$pattern_string" + } + + tokenize () { + local GREP + local ESCAPE + local CHAR + + if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1 + then + GREP='egrep -ao --color=never' + else + GREP='egrep -ao' + fi + + if echo "test string" | egrep -o "test" >/dev/null 2>&1 + then + ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' + CHAR='[^[:cntrl:]"\\]' + else + GREP=awk_egrep + ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' + CHAR='[^[:cntrl:]"\\\\]' + fi + + local STRING="\"$CHAR*($ESCAPE$CHAR*)*\"" + local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?' + local KEYWORD='null|false|true' + local SPACE='[[:space:]]+' + + # Force zsh to expand $A into multiple words + local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$') + if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi + $GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$" + if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi + } + + parse_array () { + local index=0 + local ary='' + read -r token + case "$token" in + ']') ;; + *) + while : + do + parse_value "$1" "$index" + index=$((index+1)) + ary="$ary""$value" + read -r token + case "$token" in + ']') break ;; + ',') ary="$ary," ;; + *) throw "EXPECTED , or ] GOT ${token:-EOF}" ;; + esac + read -r token + done + ;; + esac + value=$(printf '[%s]' "$ary") || value= + : + } + + parse_object () { + local key + local obj='' + read -r token + case "$token" in + '}') ;; + *) + while : + do + case "$token" in + '"'*'"') key=$token ;; + *) throw "EXPECTED string GOT ${token:-EOF}" ;; + esac + read -r token + case "$token" in + ':') ;; + *) throw "EXPECTED : GOT ${token:-EOF}" ;; + esac + read -r token + parse_value "$1" "$key" + obj="$obj$key:$value" + read -r token + case "$token" in + '}') break ;; + ',') obj="$obj," ;; + *) throw "EXPECTED , or } GOT ${token:-EOF}" ;; + esac + read -r token + done + ;; + esac + value=$(printf '{%s}' "$obj") || value= + : + } + + parse_value () { + local jpath="${1:+$1,}${2:-}" isleaf=0 isempty=0 print=0 + case "$token" in + '{') parse_object "$jpath" ;; + '[') parse_array "$jpath" ;; + # At this point, the only valid single-character tokens are digits. + ''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;; + *) value=$token + # replace solidus ("\/") in json strings with normalized value: "/" + value=$(echo "$value" | sed 's#\\/#/#g') + isleaf=1 + [ "$value" = '""' ] && isempty=1 + ;; + esac + [ "$value" = '' ] && return + [ -z "$jpath" ] && return # do not print head + + printf "[%s]\t%s\n" "$jpath" "$value" + : + } + + parse () { + read -r token + parse_value + read -r token + case "$token" in + '') ;; + *) throw "EXPECTED EOF GOT $token" ;; + esac + } + + tokenize | parse +} +# vi: expandtab sw=2 ts=2