diff --git a/placeholder.sh b/placeholder.sh new file mode 100755 index 0000000..5d054cc --- /dev/null +++ b/placeholder.sh @@ -0,0 +1,175 @@ +#!/bin/bash +set -e +LENGTH=$(head -c 4 | perl -ne 'print unpack("L", $_)') +declare -gr REQUEST=$(head -c $LENGTH) +VERSION=3000000 + +# Write a number as little-endian binary +function writelen() +{ + printf +} + +# Require a command to be present, and quit if it's not +function require() +{ + if ! `command -v "$1" >/dev/null`; then + OUTPUT="{\n \"status\": \"error\"\n "version": $VERSION,\n \"message\": \"Required dependency '$1' is missing\"\n \"code\": 1\n}" + LANG=C LC_ALL=C LENGTH=${#OUTPUT} + echo -n + exit 1 + fi +} + +# Echo to stderr with failure status code +function fail() +{ + echo "$@" >&2 + return 1 +} + +# trim leading and trailing whitespace +function trim() +{ + echo "$1" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' +} + +# Build an error response message +function error() +{ + if [ -n "$1" ]; then + ERROR="$1" + else + ERROR=$(cat) + fi + [ -n "$2" ] && CODE="$2" || CODE=1 + OUTPUT="$(jq -n '.status = "error"' | jq --arg message "$ERROR" --arg code "$CODE" '.message = $message | .code = ($code|tonumber)')" + perl -e "print pack('L', ${#OUTPUT})" + echo -n "$OUTPUT" +} + +# Wrap a shell command and bail with a valid response message on error +function wrap() +{ + . <({ STDERR=$({ STDOUT=$("$@"); EXIT=$?; } 2>&1; declare -p EXIT >&2; declare -p STDOUT >&2); declare -p STDERR; } 2>&1; ) + if [ $EXIT -gt 0 ] || [ -n "$STDERR" ]; then + if [ -n "$STDERR" ]; then + echo -n "$STDERR" | error + elif [ -n "$STDOUT" ]; then + echo -n "$STDOUT" | error + else + error "$@" $EXIT + fi + else + OUTPUT="$(jq -n --arg version $VERSION --arg response "$STDOUT" '.status = "ok" | .version = $version | .response = ($response | if .[:1] == "{" then ( . | fromjson) else . end)')" + perl -e "print pack('L', ${#OUTPUT})" + echo -n "$OUTPUT" + fi + return $EXIT +} + +# jq wrapper around the request +function rq() +{ + jq -r "$@" <<< "$REQUEST" +} + +# Supply per-store configuration +function configure() +{ + set -e + + declare -A STORES + . <(rq '.settings.stores | to_entries | map("STORES[\(.key|@sh)]=\(.value.path|@sh)")[]') + + for STORE in "${!STORES[@]}"; do + STOREPATH=$(echo "${STORES["$STORE"]}" | sed 's/^~/$HOME/' | envsubst) + [ -d "$STOREPATH" ] || fail "Store directory for '$STORE' does not exist: $STOREPATH" + + if [ -f "$STOREPATH/.browserpass.json" ]; then + STORES["$STORE"]=$(cat "$STOREPATH/.browserpass.json") + else + STORES["$STORE"]= + fi + done + + OUTPUT='{}' + for STORE in "${!STORES[@]}"; do + OUTPUT=$(jq --arg store "$STORE" --arg settings "${STORES[$STORE]}" '.storeSettings[$store] = $settings' <<< "$OUTPUT") + done + + echo "$OUTPUT" +} + +# List all available logins by store +function list() +{ + set -e + + declare -A STORES + . <(rq '.settings.stores | to_entries | map("STORES[\(.key|@sh)]=\(.value.path|@sh)")[]') + for STORE in "${!STORES[@]}"; do + STOREPATH=$(echo "${STORES["$STORE"]}" | sed 's/^~/$HOME/' | envsubst) + [ -d "$STOREPATH" ] || fail "Store directory for '$STORE' does not exist: $STOREPATH" + + STORES[$STORE]="$(find -L "$STOREPATH" -type f -name '*.gpg' -printf '%P\n' | jq -R -s 'split("\n") | map(select(length > 0)) ')" + done + + OUTPUT='{}' + for STORE in "${!STORES[@]}"; do + OUTPUT=$(jq --arg store "$STORE" --arg files "${STORES[$STORE]}" '.files[$store] = ($files|fromjson)' <<< "$OUTPUT") + done + + echo "$OUTPUT" +} + +# Fetch the specified fields from a single login +function fetch() +{ + set -e + + STORE="$(rq .store)" + FILE="$(rq .file)" + + # sanity-check variables + [ -n "$STORE" ] || fail ".store is not set" + [ -n "$FILE" ] || fail ".file is not set" + + # get file path + STOREPATH="$(rq --arg store "$STORE" '.settings.stores[$store].path//empty' | sed 's/^~/$HOME/' | envsubst)" + [ -n "$STOREPATH" ] || fail "Store path is empty" + [ -d "$STOREPATH" ] || fail "Store directory for '$STORE' does not exist: $STOREPATH" + FILEPATH="$STOREPATH/$FILE" + + # get file contents + [ -f "$FILEPATH" ] || fail "Requested file does not exist: $STORE:$FILE" + DATA="$(gpg -q --decrypt "$FILEPATH")" + + # build output + echo "$(jq -n --arg data "$DATA" '.data = $data')" +} + +function run() +{ + case "$(rq .action)" in + configure) configure; return $?;; + list) list; return $?;; + fetch) fetch; return $?;; + *) echo "Unknown action: $(rq .action)" >&2; return 1;; + esac +} + +# Ensure dependencies are present +require cat +require envsubst +require find +require gpg +require grep +require head +require jq +require perl +require sed +require tac + +# Run the client +wrap run