From 8824903ae286dae28cf6f2ddc0f488e670bd5b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 7 Sep 2014 02:20:49 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + LICENSE | 14 + Makefile | 19 ++ README | 31 ++ json-rpc-shell.c | 724 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 793 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 json-rpc-shell.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7654c07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Build files +/json-rpc-shell + +# Qt Creator files +/json-rpc-shell.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..71e1e3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ + Copyright (c) 2014, 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9374f3b --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +SHELL = /bin/sh +CC = clang +# -Wunused-function is pretty annoying here, as everything is static +CFLAGS = -std=c99 -Wall -Wextra -Wno-unused-function -ggdb +# -lpthread is only there for debugging (gdb & errno) +LDFLAGS = `pkg-config --libs libcurl jansson` -lpthread -lreadline + +.PHONY: all clean +.SUFFIXES: + +targets = json-rpc-shell + +all: $(targets) + +clean: + rm -f $(targets) + +json-rpc-shell: json-rpc-shell.c + $(CC) $< -o $@ $(CFLAGS) $(LDFLAGS) diff --git a/README b/README new file mode 100644 index 0000000..2cd62f8 --- /dev/null +++ b/README @@ -0,0 +1,31 @@ +json-rpc-shell +============== + +`json-rpc-shell' is a trivial shell for running JSON-RPC 2.0 HTTP queries. + +This software has been created as a replacement for the following shell, which +is written in Java: http://software.dzhuvinov.com/json-rpc-2.0-shell.html + +Fuck Java. With a sharp, pointy object. In the ass. Hard. json-c as well. + +Building and Running +-------------------- +Build dependencies: clang, pkg-config, GNU make, Jansson, cURL, readline + +If you don't have Clang, you can edit the Makefile to use GCC or TCC, they work +just as good. But there's no CMake support yet, so I force it in the Makefile. + + $ git clone https://github.com/pjanouch/json-rpc-shell.git + $ make + +That is all, no installation is required, or supported for that matter. + +License +------- +`json-rpc-shell' is written by Přemysl Janouch . + +You may use the software under the terms of the ISC license, the text of which +is included within the package, or, at your option, you may relicense the work +under the MIT or the Modified BSD License, as listed at the following site: + +http://www.gnu.org/licenses/license-list.html diff --git a/json-rpc-shell.c b/json-rpc-shell.c new file mode 100644 index 0000000..554d91d --- /dev/null +++ b/json-rpc-shell.c @@ -0,0 +1,724 @@ +/* + * json-rpc-shell.c: trivial JSON-RPC 2.0 shell + * + * Copyright (c) 2014, 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. + * + */ + +#define PROGRAM_NAME "json-rpc-shell" +#define PROGRAM_VERSION "alpha" + +/// Some arbitrary limit for the history file +#define HISTORY_LIMIT 10000 + +#define _POSIX_C_SOURCE 199309L +#define _XOPEN_SOURCE 600 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#if defined __GNUC__ +#define ATTRIBUTE_PRINTF(x, y) __attribute__ ((format (printf, x, y))) +#else // ! __GNUC__ +#define ATTRIBUTE_PRINTF(x, y) +#endif // ! __GNUC__ + +#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0])) + +#define BLOCK_START do { +#define BLOCK_END } while (0) + +// --- Logging ----------------------------------------------------------------- + +static void +log_message_stdio (const char *quote, const char *fmt, va_list ap) +{ + FILE *stream = stderr; + + fputs (quote, stream); + vfprintf (stream, fmt, ap); + fputs ("\n", stream); +} + +static void +log_message (const char *quote, const char *fmt, ...) ATTRIBUTE_PRINTF (2, 3); + +static void +log_message (const char *quote, const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + log_message_stdio (quote, fmt, ap); + va_end (ap); +} + +// `fatal' is reserved for unexpected failures that would harm further operation + +#define print_fatal(...) log_message ("fatal: ", __VA_ARGS__) +#define print_error(...) log_message ("error: ", __VA_ARGS__) +#define print_warning(...) log_message ("warning: ", __VA_ARGS__) +#define print_status(...) log_message ("-- ", __VA_ARGS__) + +#define exit_fatal(...) \ + BLOCK_START \ + print_fatal (__VA_ARGS__); \ + exit (EXIT_FAILURE); \ + BLOCK_END + +// --- Dynamically allocated strings ------------------------------------------- + +// Basically a string builder to abstract away manual memory management. + +struct str +{ + char *str; ///< String data, null terminated + size_t alloc; ///< How many bytes are allocated + size_t len; ///< How long the string actually is +}; + +static void +str_init (struct str *self) +{ + self->alloc = 16; + self->len = 0; + self->str = strcpy (malloc (self->alloc), ""); +} + +static void +str_free (struct str *self) +{ + free (self->str); + self->str = NULL; + self->alloc = 0; + self->len = 0; +} + +static void +str_ensure_space (struct str *self, size_t n) +{ + // We allocate at least one more byte for the terminating null character + size_t new_alloc = self->alloc; + while (new_alloc <= self->len + n) + new_alloc <<= 1; + if (new_alloc != self->alloc) + self->str = realloc (self->str, (self->alloc = new_alloc)); +} + +static void +str_append_data (struct str *self, const char *data, size_t n) +{ + str_ensure_space (self, n); + memcpy (self->str + self->len, data, n); + self->len += n; + self->str[self->len] = '\0'; +} + +static void +str_append_c (struct str *self, char c) +{ + str_append_data (self, &c, 1); +} + +static void +str_append (struct str *self, const char *s) +{ + str_append_data (self, s, strlen (s)); +} + +// --- Utilities --------------------------------------------------------------- + +static char *strdup_printf (const char *format, ...) ATTRIBUTE_PRINTF (1, 2); + +static char * +strdup_printf (const char *format, ...) +{ + va_list ap; + va_start (ap, format); + int size = vsnprintf (NULL, 0, format, ap); + va_end (ap); + if (size < 0) + return NULL; + + char buf[size + 1]; + va_start (ap, format); + size = vsnprintf (buf, sizeof buf, format, ap); + va_end (ap); + if (size < 0) + return NULL; + + return strdup (buf); +} + +static char * +iconv_strdup (iconv_t conv, char *in, size_t in_len, size_t *out_len) +{ + char *buf, *buf_ptr; + size_t out_left, buf_alloc; + + buf = buf_ptr = malloc (out_left = buf_alloc = 64); + + char *in_ptr = in; + if (in_len == (size_t) -1) + in_len = strlen (in) + 1; + + while (iconv (conv, (char **) &in_ptr, &in_len, + (char **) &buf_ptr, &out_left) == (size_t) -1) + { + if (errno != E2BIG) + { + free (buf); + return NULL; + } + out_left += buf_alloc; + char *new_buf = realloc (buf, buf_alloc <<= 1); + buf_ptr += new_buf - buf; + buf = new_buf; + } + if (out_len) + *out_len = buf_alloc - out_left; + return buf; +} + +static bool +ensure_directory_existence (const char *path) +{ + struct stat st; + if (stat (path, &st)) + { + if (mkdir (path, S_IRWXU | S_IRWXG | S_IRWXO)) + return false; + } + else if (!S_ISDIR (st.st_mode)) + return false; + return true; +} + +static bool +mkdir_with_parents (char *path) +{ + char *p = path; + while ((p = strchr (p + 1, '/'))) + { + *p = '\0'; + bool success = ensure_directory_existence (path); + *p = '/'; + + if (!success) + return false; + } + return ensure_directory_existence (path); +} + +// --- Main program ------------------------------------------------------------ + +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 +}; + +#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 = strdup_printf ("error response: %" JSON_INTEGER_FORMAT + " (%s)", code_val, json_string_value (message)); + char *s = iconv_strdup (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_strdup (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_strdup (ctx->term_from_utf8, utf8, -1, NULL); + free (utf8); + + if (!s) + print_error ("character conversion failed for `%s'", "result"); + else + printf ("result: %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 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_strdup (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 (strcmp (type, "application/json")) + // FIXME: expect e.g. application/json; charset=UTF-8 + 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_strdup (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_strdup (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++; + 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], *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_tokener; + } + 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_tokener; + } + p += e.position; + args_len++; + } + + for (size_t i = 0; i < args_len; i++) + { + if (is_valid_json_rpc_id (args[i])) + id = json_incref (args[i]); + else if (is_valid_json_rpc_params (args[i])) + params = json_incref (args[i]); + else + { + print_error ("unexpected value at index %zu", i); + goto fail_tokener; + } + } + + if (!id && ctx->auto_id) + id = json_integer (ctx->next_id++); + + make_json_rpc_call (ctx, method, id, params); + +fail_tokener: + 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); + putchar ('\n'); +} + +static void +print_usage (const char *program_name) +{ + fprintf (stderr, + "Usage: %s [OPTION]... ENDPOINT\n" + "Trivial JSON-RPC shell.\n" + "\n" + " -h, --help display this help and exit\n" + " -V, --version output version information and exit\n" + " -a, --auto-id automatic `id' fields\n" + " -o, --origin O set the HTTP Origin header\n" + " -p, --pretty pretty-print the responses\n" + " -t, --trust-all don't care about SSL/TLS certificates\n" + " -v, --verbose print the request before sending\n", + program_name); +} + +int +main (int argc, char *argv[]) +{ + const char *invocation_name = argv[0]; + + struct app_context ctx; + memset (&ctx, 0, sizeof ctx); + + static struct option opts[] = + { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "auto-id", no_argument, NULL, 'a' }, + { "origin", required_argument, NULL, 'o' }, + { "pretty", no_argument, NULL, 'p' }, + { "trust-all", no_argument, NULL, 't' }, + { "verbose", no_argument, NULL, 'v' }, + { NULL, 0, NULL, 0 } + }; + + char *origin = NULL; + while (1) + { + int c, opt_index; + + c = getopt_long (argc, argv, "hVapvt", opts, &opt_index); + if (c == -1) + break; + + switch (c) + { + case 'h': + print_usage (invocation_name); + 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"); + exit (EXIT_FAILURE); + } + } + + argv += optind; + argc -= optind; + + if (argc != 1) + { + print_usage (invocation_name); + exit (EXIT_FAILURE); + } + + const char *endpoint = argv[0]; + if (strncmp (endpoint, "http://", 7) + && strncmp (endpoint, "https://", 8)) + exit_fatal ("the endpoint address must begin with" + " either `http://' or `https://'"); + + CURL *curl; + if (!(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 = strdup_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, ctx.curl_error) + || curl_easy_setopt (curl, CURLOPT_HTTPHEADER, headers) + || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, ctx.trust_all ? 0L : 1L) + || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST, 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 + encoding = strdup_printf ("%s//TRANSLIT", encoding); +#endif // __linux__ + + if ((ctx.term_from_utf8 = iconv_open (encoding, "utf-8")) + == (iconv_t) -1 + || (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 = strdup_printf ("%s/.local/share", home); + } + + using_history (); + stifle_history (HISTORY_LIMIT); + + char *history_path = + strdup_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 = strdup_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); + + char *line; + while ((line = readline (prompt))) + { + if (*line) + add_history (line); + + process_input (&ctx, line); + free (line); + } + + char *dir = strdup (history_path); + (void) mkdir_with_parents (dirname (dir)); + 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 (ctx.term_from_utf8); + iconv_close (ctx.term_to_utf8); + curl_slist_free_all (headers); + free (origin); + curl_easy_cleanup (curl); + return EXIT_SUCCESS; +}