/*
* json-rpc-shell.c: trivial JSON-RPC 2.0 shell
*
* Copyright (c) 2014 - 2015, Přemysl Janouch
* All rights reserved.
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
/// Some arbitrary limit for the history file
#define HISTORY_LIMIT 10000
#include "config.h"
#include "utils.c"
#include
#include
#include
#include
#include
#include
#include
// --- Main program ------------------------------------------------------------
static struct app_context
{
CURL *curl; ///< cURL handle
char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
bool pretty_print; ///< Whether to pretty print
bool verbose; ///< Print requests
bool trust_all; ///< Don't verify peer certificates
bool auto_id; ///< Use automatically generated ID's
int64_t next_id; ///< Next autogenerated ID
iconv_t term_to_utf8; ///< Terminal encoding to UTF-8
iconv_t term_from_utf8; ///< UTF-8 to terminal encoding
}
g_ctx;
#define PARSE_FAIL(...) \
BLOCK_START \
print_error (__VA_ARGS__); \
goto fail; \
BLOCK_END
static bool
parse_response (struct app_context *ctx, struct str *buf)
{
json_error_t e;
json_t *response;
if (!(response = json_loadb (buf->str, buf->len, JSON_DECODE_ANY, &e)))
{
print_error ("failed to parse the response: %s", e.text);
return false;
}
bool success = false;
if (!json_is_object (response))
PARSE_FAIL ("the response is not a JSON object");
json_t *v;
if (!(v = json_object_get (response, "jsonrpc")))
print_warning ("`%s' field not present in response", "jsonrpc");
else if (!json_is_string (v) || strcmp (json_string_value (v), "2.0"))
print_warning ("invalid `%s' field in response", "jsonrpc");
json_t *result = json_object_get (response, "result");
json_t *error = json_object_get (response, "error");
json_t *data = NULL;
if (!result && !error)
PARSE_FAIL ("neither `result' nor `error' present in response");
if (result && error)
// Prohibited by the specification but happens in real life (null)
print_warning ("both `result' and `error' present in response");
if (error)
{
if (!json_is_object (error))
PARSE_FAIL ("invalid `%s' field in response", "error");
json_t *code = json_object_get (error, "code");
json_t *message = json_object_get (error, "message");
if (!code)
PARSE_FAIL ("missing `%s' field in error response", "code");
if (!message)
PARSE_FAIL ("missing `%s' field in error response", "message");
if (!json_is_integer (code))
PARSE_FAIL ("invalid `%s' field in error response", "code");
if (!json_is_string (message))
PARSE_FAIL ("invalid `%s' field in error response", "message");
json_int_t code_val = json_integer_value (code);
char *utf8 = xstrdup_printf ("error response: %" JSON_INTEGER_FORMAT
" (%s)", code_val, json_string_value (message));
char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
if (!s)
print_error ("character conversion failed for `%s'", "error");
else
printf ("%s\n", s);
free (s);
data = json_object_get (error, "data");
}
if (data)
{
char *utf8 = json_dumps (data, JSON_ENCODE_ANY);
char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
free (utf8);
if (!s)
print_error ("character conversion failed for `%s'", "error data");
else
printf ("error data: %s\n", s);
free (s);
}
if (result)
{
int flags = JSON_ENCODE_ANY;
if (ctx->pretty_print)
flags |= JSON_INDENT (2);
char *utf8 = json_dumps (result, flags);
char *s = iconv_xstrdup (ctx->term_from_utf8, utf8, -1, NULL);
free (utf8);
if (!s)
print_error ("character conversion failed for `%s'", "result");
else
printf ("%s\n", s);
free (s);
}
success = true;
fail:
json_decref (response);
return success;
}
static bool
is_valid_json_rpc_id (json_t *v)
{
return json_is_string (v) || json_is_integer (v)
|| json_is_real (v) || json_is_null (v); // These two shouldn't be used
}
static bool
is_valid_json_rpc_params (json_t *v)
{
return json_is_array (v) || json_is_object (v);
}
static bool
isspace_ascii (int c)
{
return strchr (" \f\n\r\t\v", c);
}
static size_t
write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
{
struct str *buf = user_data;
str_append_data (buf, ptr, size * nmemb);
return size * nmemb;
}
#define RPC_FAIL(...) \
BLOCK_START \
print_error (__VA_ARGS__); \
goto fail; \
BLOCK_END
static bool
try_advance (const char **p, const char *text)
{
size_t len = strlen (text);
if (strncmp (*p, text, len))
return false;
*p += len;
return true;
}
static bool
validate_content_type (const char *type)
{
const char *content_types[] =
{
"application/json-rpc", // obsolete
"application/json"
};
const char *tails[] =
{
"; charset=utf-8",
"; charset=UTF-8",
""
};
bool found = false;
for (size_t i = 0; i < N_ELEMENTS (content_types); i++)
if ((found = try_advance (&type, content_types[i])))
break;
if (!found)
return false;
for (size_t i = 0; i < N_ELEMENTS (tails); i++)
if ((found = try_advance (&type, tails[i])))
break;
if (!found)
return false;
return !*type;
}
static void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params)
{
json_t *request = json_object ();
json_object_set_new (request, "jsonrpc", json_string ("2.0"));
json_object_set_new (request, "method", json_string (method));
if (id) json_object_set (request, "id", id);
if (params) json_object_set (request, "params", params);
char *req_utf8 = json_dumps (request, 0);
if (ctx->verbose)
{
char *req_term = iconv_xstrdup
(ctx->term_from_utf8, req_utf8, -1, NULL);
if (!req_term)
print_error ("%s: %s", "verbose", "character conversion failed");
else
printf ("%s\n", req_term);
free (req_term);
}
struct str buf;
str_init (&buf);
if (curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDS, req_utf8)
|| curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDSIZE_LARGE,
(curl_off_t) -1)
|| curl_easy_setopt (ctx->curl, CURLOPT_WRITEDATA, &buf)
|| curl_easy_setopt (ctx->curl, CURLOPT_WRITEFUNCTION, write_callback))
RPC_FAIL ("cURL setup failed");
CURLcode ret;
if ((ret = curl_easy_perform (ctx->curl)))
RPC_FAIL ("HTTP request failed: %s", ctx->curl_error);
long code;
char *type;
if (curl_easy_getinfo (ctx->curl, CURLINFO_RESPONSE_CODE, &code)
|| curl_easy_getinfo (ctx->curl, CURLINFO_CONTENT_TYPE, &type))
RPC_FAIL ("cURL info retrieval failed");
if (code != 200)
RPC_FAIL ("unexpected HTTP response code: %ld", code);
bool success = false;
if (id)
{
if (!type)
print_warning ("missing `Content-Type' header");
else if (!validate_content_type (type))
print_warning ("unexpected `Content-Type' header: %s", type);
success = parse_response (ctx, &buf);
}
else
{
printf ("[Notification]\n");
if (buf.len)
print_warning ("we have been sent data back for a notification");
else
success = true;
}
if (!success)
{
char *s = iconv_xstrdup (ctx->term_from_utf8,
buf.str, buf.len + 1, NULL);
if (!s)
print_error ("character conversion failed for `%s'",
"raw response data");
else
printf ("%s: %s\n", "raw response data", s);
free (s);
}
fail:
str_free (&buf);
free (req_utf8);
json_decref (request);
}
static void
process_input (struct app_context *ctx, char *user_input)
{
char *input;
size_t len;
if (!(input = iconv_xstrdup (ctx->term_to_utf8, user_input, -1, &len)))
{
print_error ("character conversion failed for `%s'", "user input");
goto fail;
}
// Cut out the method name first
char *p = input;
while (*p && isspace_ascii (*p))
p++;
// No input
if (!*p)
goto fail;
char *method = p;
while (*p && !isspace_ascii (*p))
p++;
if (*p)
*p++ = '\0';
// Now we go through this madness, just so that the order can be arbitrary
json_error_t e;
size_t args_len = 0;
json_t *args[2] = { NULL, NULL }, *id = NULL, *params = NULL;
while (true)
{
// Jansson is too stupid to just tell us that there was nothing
while (*p && isspace_ascii (*p))
p++;
if (!*p)
break;
if (args_len == N_ELEMENTS (args))
{
print_error ("too many arguments");
goto fail_parse;
}
if (!(args[args_len] = json_loadb (p, len - (p - input),
JSON_DECODE_ANY | JSON_DISABLE_EOF_CHECK, &e)))
{
print_error ("failed to parse JSON value: %s", e.text);
goto fail_parse;
}
p += e.position;
args_len++;
}
for (size_t i = 0; i < args_len; i++)
{
json_t **target;
if (is_valid_json_rpc_id (args[i]))
target = &id;
else if (is_valid_json_rpc_params (args[i]))
target = ¶ms;
else
{
print_error ("unexpected value at index %zu", i);
goto fail_parse;
}
if (*target)
{
print_error ("cannot specify multiple `id' or `params'");
goto fail_parse;
}
*target = json_incref (args[i]);
}
if (!id && ctx->auto_id)
id = json_integer (ctx->next_id++);
make_json_rpc_call (ctx, method, id, params);
fail_parse:
if (id) json_decref (id);
if (params) json_decref (params);
for (size_t i = 0; i < args_len; i++)
json_decref (args[i]);
fail:
free (input);
}
static void
on_winch (EV_P_ ev_signal *handle, int revents)
{
(void) loop;
(void) handle;
(void) revents;
// This fucks up big time on terminals with automatic wrapping such as
// rxvt-unicode or newer VTE when the current line overflows, however we
// can't do much about that
rl_resize_terminal ();
}
static void
on_readline_input (char *line)
{
if (!line)
{
rl_callback_handler_remove ();
ev_break (EV_DEFAULT_ EVBREAK_ONE);
return;
}
if (*line)
add_history (line);
// Stupid readline forces us to use a global variable
process_input (&g_ctx, line);
free (line);
}
static void
on_tty_readable (EV_P_ ev_io *handle, int revents)
{
(void) loop;
(void) handle;
if (revents & EV_READ)
rl_callback_read_char ();
}
static void
parse_program_arguments (struct app_context *ctx, int argc, char **argv,
char **origin, char **endpoint)
{
static const struct opt opts[] =
{
{ 'd', "debug", NULL, 0, "run in debug mode" },
{ 'h', "help", NULL, 0, "display this help and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
{ 'a', "auto-id", NULL, 0, "automatic `id' fields" },
{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
{ 'p', "pretty", NULL, 0, "pretty-print the responses" },
{ 't', "trust-all", NULL, 0, "don't care about SSL/TLS certificates" },
{ 'v', "verbose", NULL, 0, "print the request before sending" },
{ 0, NULL, NULL, 0, NULL }
};
struct opt_handler oh;
opt_handler_init (&oh, argc, argv, opts,
"ENDPOINT", "Trivial JSON-RPC shell.");
int c;
while ((c = opt_handler_get (&oh)) != -1)
switch (c)
{
case 'd':
g_debug_mode = true;
break;
case 'h':
opt_handler_usage (&oh, stdout);
exit (EXIT_SUCCESS);
case 'V':
printf (PROGRAM_NAME " " PROGRAM_VERSION "\n");
exit (EXIT_SUCCESS);
case 'a':
ctx->auto_id = true;
break;
case 'o':
*origin = optarg;
break;
case 'p':
ctx->pretty_print = true;
break;
case 't':
ctx->trust_all = true;
break;
case 'v':
ctx->verbose = true;
break;
default:
print_error ("wrong options");
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
argc -= optind;
argv += optind;
if (argc != 1)
{
opt_handler_usage (&oh, stderr);
exit (EXIT_FAILURE);
}
*endpoint = argv[0];
opt_handler_free (&oh);
}
int
main (int argc, char *argv[])
{
char *origin = NULL;
char *endpoint = NULL;
parse_program_arguments (&g_ctx, argc, argv, &origin, &endpoint);
if (strncmp (endpoint, "http://", 7)
&& strncmp (endpoint, "https://", 8))
exit_fatal ("the endpoint address must begin with"
" either `http://' or `https://'");
CURL *curl;
if (!(g_ctx.curl = curl = curl_easy_init ()))
exit_fatal ("cURL initialization failed");
struct curl_slist *headers = NULL;
headers = curl_slist_append (headers, "Content-Type: application/json");
if (origin)
{
origin = xstrdup_printf ("Origin: %s", origin);
headers = curl_slist_append (headers, origin);
}
if (curl_easy_setopt (curl, CURLOPT_POST, 1L)
|| curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1L)
|| curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, g_ctx.curl_error)
|| curl_easy_setopt (curl, CURLOPT_HTTPHEADER, headers)
|| curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER,
g_ctx.trust_all ? 0L : 1L)
|| curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST,
g_ctx.trust_all ? 0L : 2L)
|| curl_easy_setopt (curl, CURLOPT_URL, endpoint))
exit_fatal ("cURL setup failed");
// We only need to convert to and from the terminal encoding
setlocale (LC_CTYPE, "");
char *encoding = nl_langinfo (CODESET);
#ifdef __linux__
// XXX: not quite sure if this is actually desirable
// TODO: instead retry with JSON_ENSURE_ASCII
encoding = xstrdup_printf ("%s//TRANSLIT", encoding);
#endif // __linux__
if ((g_ctx.term_from_utf8 = iconv_open (encoding, "UTF-8"))
== (iconv_t) -1
|| (g_ctx.term_to_utf8 = iconv_open ("UTF-8", nl_langinfo (CODESET)))
== (iconv_t) -1)
exit_fatal ("creating the UTF-8 conversion object failed: %s",
strerror (errno));
char *data_home = getenv ("XDG_DATA_HOME"), *home = getenv ("HOME");
if (!data_home || *data_home != '/')
{
if (!home)
exit_fatal ("where is your $HOME, kid?");
data_home = xstrdup_printf ("%s/.local/share", home);
}
using_history ();
stifle_history (HISTORY_LIMIT);
char *history_path =
xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home);
(void) read_history (history_path);
// XXX: we should use termcap/terminfo for the codes but who cares
char *prompt = xstrdup_printf ("%c\x1b[1m%cjson-rpc> %c\x1b[0m%c",
RL_PROMPT_START_IGNORE, RL_PROMPT_END_IGNORE,
RL_PROMPT_START_IGNORE, RL_PROMPT_END_IGNORE);
// readline 6.3 doesn't immediately redraw the terminal upon reception
// of SIGWINCH, so we must run it in an event loop to remediate that
struct ev_loop *loop = EV_DEFAULT;
if (!loop)
exit_fatal ("libev initialization failed");
ev_signal winch_watcher;
ev_io tty_watcher;
ev_signal_init (&winch_watcher, on_winch, SIGWINCH);
ev_signal_start (EV_DEFAULT_ &winch_watcher);
ev_io_init (&tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ);
ev_io_start (EV_DEFAULT_ &tty_watcher);
rl_catch_sigwinch = false;
rl_callback_handler_install (prompt, on_readline_input);
ev_run (loop, 0);
putchar ('\n');
ev_loop_destroy (loop);
// User has terminated the program, let's save the history and clean up
char *dir = xstrdup (history_path);
(void) mkdir_with_parents (dirname (dir), NULL);
free (dir);
if (write_history (history_path))
print_error ("writing the history file `%s' failed: %s",
history_path, strerror (errno));
free (history_path);
iconv_close (g_ctx.term_from_utf8);
iconv_close (g_ctx.term_to_utf8);
curl_slist_free_all (headers);
free (origin);
curl_easy_cleanup (curl);
return EXIT_SUCCESS;
}