]> git.ipfire.org Git - thirdparty/dehydrated.git/commitdiff
experimental json.sh support
authorLukas Schauer <lukas@schauer.so>
Sat, 4 Jul 2020 19:36:23 +0000 (21:36 +0200)
committerLukas Schauer <lukas@schauer.so>
Sat, 4 Jul 2020 19:36:23 +0000 (21:36 +0200)
dehydrated
json.sh [new file with mode: 0644]

index 351d35e95cbb6b334423a2dead86d3fd0274674c..efa6827b2021c92457253a2e7cffe24ebfda1860 100755 (executable)
@@ -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 (file)
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