diff --git a/.gitignore b/.gitignore index d38870f..d2391b1 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ grenache-get grenache-put grenache-keygen grenache-lookup +grenache-request grenache-announce # diff --git a/README.md b/README.md index d98b0d6..55f4887 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ * [Retreive items from the DHT](#retreive-items-from-the-dht) * [Lookup peers](#lookup-peers) * [Announce services](#announce-services) + * [Query services](#query-services) * [Maintainers](#maintainers) @@ -184,6 +185,55 @@ grenache-announce --help to retrieve the complete options list. +## Query services + +The `grenache-request` command sends a request to a service in the DHT. To query the [rest:ext:helpdesk:foo](https://github.com/bitfinexcom/bfx-ext-helpdesk-js) service, simply run something like this: + +```bash +grenache-request 'rest:ext:helpdesk:foo' 'getDepartments' +``` + +Action arguments are optional and can be omitted altogether. However, to pass an argument to an action, use the `-a` switch or its long form `--arg`; to query the [rest:net:util](https://github.com/bitfinexcom/bfx-util-net-js) service in order to get information about IP _208.67.222.222_, simply run something like this: + +```bash +grenache-request -a '"208.67.222.222"' 'rest:util:net' 'getIpInfo' +``` + +To send multiple arguments, simply repeat the `-a` switch or its long form `--arg` as many times as needed; arguments will be passed to the service action in the same order as they were supplied on the command line. To query the [rest:ext:gpg](https://github.com/bitfinexcom/bfx-ext-gpg-js) service in order to get a signature for the _hello_ message, simply run something like this: + +```bash +grenache-request -a '"68656c6c6f"' -a '{"userId":1}' 'rest:ext:gpg' 'getDigitalSignature' +``` + +Arguments are treated as _JSON_-encoded text; sometimes, when dealing with plain strings, this can be somewhat cumbersome. In such cases, the `-s` switch or its long form `--string` can be used to pass a string as-is or `-n` or its long form `--numeric` when accepting an option that is intended to be treated as a numeric value. + +Typically, the output is a _JSON_-encoded text. Sometimes, filtering this text can be useful; in such cases, output can be selected using the `-q` switch or its long form `--query`, as shown below: + +```bash +grenache-request -q 'map(select(.active_members > 3))' 'rest:ext:helpdesk:bar' 'getTeams' +``` + +The query is passed as-is to [jq](https://jqlang.github.io/jq/) and will be applied directly to the third position of the response array; refer to the official [filter documentation](https://jqlang.github.io/jq/manual/#basic-filters) for more details on the syntax to be used. However, there are cases where a plain string written directly to the standard output is preferable rather than formatted as a _JSON_ string with quotes, perhaps when pipe the output to another command; in such cases, the `-r` switch or its long form `--raw` can be used, as shown below: + +```bash +grenache-request -q '.[1].timezone' -r -s '208.67.222.222' 'rest:util:net' 'getIpGeo' +``` + +Under normal circumstances, workflow is to look up the service via [grenache-lookup](#lookup-peers) and then send the request to the worker. There might be cases, perhaps when dealing with a system without a running [grape](https://github.com/bitfinexcom/grenache-grape), where it would be useful to query a service without looking it up first; in such cases, the `-w` switch or its long form `--worker` can be used, as shown below: + +```bash +grenache-request -r -s '776f726c64' -a '{"userId":8}' -w '10.0.0.1:1337' 'rest:ext:gpg' 'getDigitalSignature' +``` + +Those are the main options; see + +```bash +grenache-request --help +``` + +to retrieve the complete options list. + + ## Maintainers Current maintainers: diff --git a/configure.ac b/configure.ac index 38cabda..afac23a 100644 --- a/configure.ac +++ b/configure.ac @@ -15,9 +15,9 @@ dnl implied. See the License for the specific language governing permissions dnl and limitations under the License. AC_PREREQ([2.69]) -AC_INIT([grenache-cli],[0.7.1],[davide@bitfinex.com]) +AC_INIT([grenache-cli],[0.8.0],[davide@bitfinex.com]) -AC_SUBST([VERSION], [0.7.1]) +AC_SUBST([VERSION], [0.8.0]) AC_SUBST([SB], [`$srcdir/shtool echo -n -e %B`]) AC_SUBST([EB], [`$srcdir/shtool echo -n -e %b`]) @@ -190,6 +190,7 @@ AC_CONFIG_FILES([ \ src/grenache-get \ src/grenache-keygen \ src/grenache-lookup \ + src/grenache-request \ src/grenache-announce \ tests/Makefile \ tests/sign-test-vector-1.sh \ diff --git a/doc/Makefile.am b/doc/Makefile.am index dfe1374..e9af795 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -21,6 +21,7 @@ grenache-get.1 \ grenache-put.1 \ grenache-keygen.1 \ grenache-lookup.1 \ +grenache-request.1 \ grenache-announce.1 MAINTAINERCLEANFILES = \ diff --git a/src/Makefile.am b/src/Makefile.am index 4b04cfe..36ab241 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -31,6 +31,7 @@ grenache-put \ grenache-get \ grenache-keygen \ grenache-lookup \ +grenache-request \ grenache-announce noinst_LTLIBRARIES = \ diff --git a/src/grenache-request.in b/src/grenache-request.in new file mode 100644 index 0000000..d71b2a1 --- /dev/null +++ b/src/grenache-request.in @@ -0,0 +1,225 @@ +#!/bin/bash +############################################################################ +# This file is part of Grenache Command Line Interface. # +# # +# Copyright (C) 2017-2025 Davide Scola # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # +# implied. See the License for the specific language governing permissions # +# and limitations under the License. # +############################################################################ + +readonly ME="${BASH_SOURCE[0]}" +readonly WHOAMI="$(@READLINK@ -snf "${ME}")" +readonly ARGV=$(@GETOPT@ -o 'a:cg:hn:p:q:rs:tVw:' --long 'arg:,color,grape:,help,numeric:,port:,query:,raw,string:,tls,version,worker:' -n "${ME##*/}" -- "$@") || exit 1 + +TLS='' +COLOR='' +EXIT_CODE=1 +REQUEST_ARGC=0 +RESPONSE_ARGV=() +REQUEST_WORKER='' +REQUEST_QUERY='.' +RESPONSE_QUERY='.' +REQUEST_ARGUMENTS='[]' +LOOKUP_ARGV=(--random) +REQUEST_ARGV=(--compact-output --monochrome-output) + + +# Show program's help. +function show_help { + @CAT@ <<_EOF +${ME##*/} sends a request to a service in the DHT + +Usage: ${ME##*/} [OPTION]... SERVICE ACTION + +Options: + -a, --arg Add an argument, JSON encoded + -c, --color Pretty print + -g, --grape Set the Grape hostname + -n, --numeric Add a numeric argument + -p, --port Set the Grape port number + -q, --query Filter response against query + -r, --raw Print string output as-is + -s, --string Add a string argument + -t, --tls Enable TLS + -w, --worker Don't lookup, use this worker + + -h, --help Show help message + -V, --version Show version information + +The \`--arg', \`--numeric' and \`--string' options can be repeated several times +to add multiple arguments, which will be passed in the same order as they were +supplied on the command line. + +Examples: + ${ME##*/} 'foo' 'bar' Call the bar action of the foo service + ${ME##*/} -n 1 'foo' 'bar' The same thing, but passing a numeric argument + +Report bugs to <@PACKAGE_BUGREPORT@>. +_EOF + + exit 1 +} + +# Show program's version. +function show_version { + @CAT@ <<_EOF +${WHOAMI##*/} @PACKAGE_VERSION@ + +Copyright (C) 2017-2025 Davide Scola <@PACKAGE_BUGREPORT@> +This is free software; see the source for copying conditions. There is +NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE. + +Written by Davide Scola. +_EOF + + exit 1 +} + + +while true; do + case "$1" in + -a | --arg ) + REQUEST_ARGV+=(--arg "a$((++REQUEST_ARGC))" "${2}") + REQUEST_QUERY+=' += [ $a'"${REQUEST_ARGC}"' | fromjson ] | .'; shift 2 + ;; + -c | --color ) + COLOR='yes'; shift 1 + ;; + -g | --grape ) + [[ -z "${2}" ]] && { + echo "${ME##*/}: error: empty Grape hostname." >&2 + exit 1 + } + + LOOKUP_ARGV+=(--grape "${2}"); shift 2 + ;; + -h | --help ) + show_help; shift 1 + ;; + -n | --numeric ) + REQUEST_ARGV+=(--arg "n$((++REQUEST_ARGC))" "${2}") + REQUEST_QUERY+=' += [ $n'"${REQUEST_ARGC}"' | tonumber ] | .'; shift 2 + ;; + -p | --port ) + [[ -z "${2}" ]] && { + echo "${ME##*/}: error: empty Grape port." >&2 + exit 1 + } + + LOOKUP_ARGV+=(--port "${2}"); shift 2 + ;; + -q | --query ) + [[ -z "${2}" ]] && { + echo "${ME##*/}: error: empty response query." >&2 + exit 1 + } + + RESPONSE_QUERY="${2}"; shift 2 + ;; + -r | --raw ) + RESPONSE_ARGV+=(--raw-output); shift 1 + ;; + -s | --string ) + REQUEST_ARGV+=(--arg "s$((++REQUEST_ARGC))" "${2}") + REQUEST_QUERY+=' += [ $s'"${REQUEST_ARGC}"' | tostring ] | .'; shift 2 + ;; + -t | --tls ) + TLS='yes'; LOOKUP_ARGV+=(--tls); shift 1 + ;; + -V | --version ) + show_version; shift 1 + ;; + -w | --worker ) + [[ -z "${2}" ]] && { + echo "${ME##*/}: error: empty worker address." >&2 + exit 1 + } + + [[ "${2##*:}" =~ ^[0-9]+$ ]] || { + echo "${ME##*/}: error: invalid worker port number." >&2 + exit 1 + } + + [[ "$((${2##*:} & 0xFFFF))" -eq 0 ]] && { + echo "${ME##*/}: error: worker port number must be greater than zero." >&2 + exit 1 + } + + [[ "${2##*:}" -ne "$((${2##*:} & 0xFFFF))" ]] && { + echo "${ME##*/}: error: worker port number too big." >&2 + exit 1 + } + + REQUEST_WORKER="${2%:*}:$((${2##*:} & 0xFFFF))"; shift 2 + ;; + -- ) shift; break ;; + * ) break ;; + esac +done + +[[ -z "${COLOR}" ]] && { + RESPONSE_ARGV+=(--compact-output --monochrome-output) +} + +[[ "${#}" -lt 2 ]] && { + show_help +} + +[[ -f "${HOME}/.grenache-cli/grenache-cli.conf" ]] || { + echo "${ME##*/}: error: you need to run \`grenache-keygen' first." >&2 + exit "${EXIT_CODE}" +} + +exec {stderr}>&2 + +[[ x"${GRENACHE_CLI_DEBUG:+set}" != xset ]] && { + exec 2>/dev/null +} + +[[ "${REQUEST_ARGC}" -gt 0 ]] && { + REQUEST_ARGUMENTS="$(@JQ@ "${REQUEST_ARGV[@]}" --from-file <(echo "${REQUEST_QUERY}") <<<"${REQUEST_ARGUMENTS}")" + + [[ "${?}" -ne 0 ]] && { + echo "${ME##*/}: error: provided arguments are invalid." >&"${stderr}" + + exec {stderr}>&- + exit "${EXIT_CODE}" + } +} + +JSON="$(@CURL@ -qK "${HOME}/.grenache-cli/grenache-cli.conf" -A '@PACKAGE@/@PACKAGE_VERSION@' "http${TLS:+s}://${REQUEST_WORKER:-$(grenache-lookup "${LOOKUP_ARGV[@]}" "${1}")}" < <( \ + @JQ@ --null-input --compact-output --monochrome-output \ + --arg 'action' "${2}" \ + --arg 'service' "${1}" \ + --arg 'rid' "$(@UUIDGEN@)" \ + --arg 'args' "${REQUEST_ARGUMENTS}" \ + '[ $rid, $service, { "action": $action, "args": $args | fromjson } ]' \ +))" + +[[ -z "${JSON}" ]] && { + echo "${ME##*/}: error: service ${1} cannot be queried." >&"${stderr}" + + exec {stderr}>&- + exit "${EXIT_CODE}" +} + +@JQ@ --exit-status '.[1]' >/dev/null <<<"${JSON}" && { + @JQ@ --raw-output '.[1]' <<<"${JSON}" >&"${stderr}" +} || { + EXIT_CODE=0 + @JQ@ "${RESPONSE_ARGV[@]}" --from-file <(printf '.[2] | %s' "${RESPONSE_QUERY}") <<<"${JSON}" +} + +exec {stderr}>&- +exit "${EXIT_CODE}"