1019 lines
26 KiB
C
1019 lines
26 KiB
C
/*
|
|
* json-rpc-shell.c: trivial JSON-RPC 2.0 shell
|
|
*
|
|
* Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
|
|
* 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
|
|
|
|
// String constants for all attributes we use for output
|
|
#define ATTR_PROMPT "attr_prompt"
|
|
#define ATTR_RESET "attr_reset"
|
|
#define ATTR_WARNING "attr_warning"
|
|
#define ATTR_ERROR "attr_error"
|
|
#define ATTR_INCOMING "attr_incoming"
|
|
#define ATTR_OUTGOING "attr_outgoing"
|
|
|
|
// User data for logger functions to enable formatted logging
|
|
#define print_fatal_data ATTR_ERROR
|
|
#define print_error_data ATTR_ERROR
|
|
#define print_warning_data ATTR_WARNING
|
|
|
|
#include "config.h"
|
|
#include "liberty/liberty.c"
|
|
|
|
#include <langinfo.h>
|
|
#include <locale.h>
|
|
#include <signal.h>
|
|
#include <strings.h>
|
|
|
|
#include <ev.h>
|
|
#include <readline/readline.h>
|
|
#include <readline/history.h>
|
|
#include <curl/curl.h>
|
|
#include <jansson.h>
|
|
|
|
#include <curses.h>
|
|
#include <term.h>
|
|
|
|
// --- Configuration (application-specific) ------------------------------------
|
|
|
|
static struct config_item g_config_table[] =
|
|
{
|
|
{ ATTR_PROMPT, NULL, "Terminal attributes for the prompt" },
|
|
{ ATTR_RESET, NULL, "String to reset terminal attributes" },
|
|
{ ATTR_WARNING, NULL, "Terminal attributes for warnings" },
|
|
{ ATTR_ERROR, NULL, "Terminal attributes for errors" },
|
|
{ ATTR_INCOMING, NULL, "Terminal attributes for incoming traffic" },
|
|
{ ATTR_OUTGOING, NULL, "Terminal attributes for outgoing traffic" },
|
|
{ NULL, NULL, NULL }
|
|
};
|
|
|
|
// --- Main program ------------------------------------------------------------
|
|
|
|
enum color_mode
|
|
{
|
|
COLOR_AUTO, ///< Autodetect if colours are available
|
|
COLOR_ALWAYS, ///< Always use coloured output
|
|
COLOR_NEVER ///< Never use coloured output
|
|
};
|
|
|
|
static struct app_context
|
|
{
|
|
CURL *curl; ///< cURL handle
|
|
char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
|
|
|
|
struct str_map config; ///< Program configuration
|
|
enum color_mode color_mode; ///< Colour output mode
|
|
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;
|
|
|
|
// --- Attributed output -------------------------------------------------------
|
|
|
|
static struct
|
|
{
|
|
bool initialized; ///< Terminal is available
|
|
bool stdout_is_tty; ///< `stdout' is a terminal
|
|
bool stderr_is_tty; ///< `stderr' is a terminal
|
|
|
|
char *color_set[8]; ///< Codes to set the foreground colour
|
|
}
|
|
g_terminal;
|
|
|
|
static bool
|
|
init_terminal (void)
|
|
{
|
|
int tty_fd = -1;
|
|
if ((g_terminal.stderr_is_tty = isatty (STDERR_FILENO)))
|
|
tty_fd = STDERR_FILENO;
|
|
if ((g_terminal.stdout_is_tty = isatty (STDOUT_FILENO)))
|
|
tty_fd = STDOUT_FILENO;
|
|
|
|
int err;
|
|
if (tty_fd == -1 || setupterm (NULL, tty_fd, &err) == ERR)
|
|
return false;
|
|
|
|
// Make sure all terminal features used by us are supported
|
|
if (!set_a_foreground || !enter_bold_mode || !exit_attribute_mode)
|
|
{
|
|
del_curterm (cur_term);
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++)
|
|
g_terminal.color_set[i] = xstrdup (tparm (set_a_foreground,
|
|
i, 0, 0, 0, 0, 0, 0, 0, 0));
|
|
|
|
return g_terminal.initialized = true;
|
|
}
|
|
|
|
static void
|
|
free_terminal (void)
|
|
{
|
|
if (!g_terminal.initialized)
|
|
return;
|
|
|
|
for (size_t i = 0; i < N_ELEMENTS (g_terminal.color_set); i++)
|
|
free (g_terminal.color_set[i]);
|
|
del_curterm (cur_term);
|
|
}
|
|
|
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
|
|
typedef int (*terminal_printer_fn) (int);
|
|
|
|
static int
|
|
putchar_stderr (int c)
|
|
{
|
|
return fputc (c, stderr);
|
|
}
|
|
|
|
static terminal_printer_fn
|
|
get_attribute_printer (FILE *stream)
|
|
{
|
|
if (stream == stdout && g_terminal.stdout_is_tty)
|
|
return putchar;
|
|
if (stream == stderr && g_terminal.stderr_is_tty)
|
|
return putchar_stderr;
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
vprint_attributed (struct app_context *ctx,
|
|
FILE *stream, const char *attribute, const char *fmt, va_list ap)
|
|
{
|
|
terminal_printer_fn printer = get_attribute_printer (stream);
|
|
if (!attribute)
|
|
printer = NULL;
|
|
|
|
if (printer)
|
|
{
|
|
const char *value = str_map_find (&ctx->config, attribute);
|
|
tputs (value, 1, printer);
|
|
}
|
|
|
|
vfprintf (stream, fmt, ap);
|
|
|
|
if (printer)
|
|
{
|
|
const char *value = str_map_find (&ctx->config, ATTR_RESET);
|
|
tputs (value, 1, printer);
|
|
}
|
|
}
|
|
|
|
static void
|
|
print_attributed (struct app_context *ctx,
|
|
FILE *stream, const char *attribute, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
va_start (ap, fmt);
|
|
vprint_attributed (ctx, stream, attribute, fmt, ap);
|
|
va_end (ap);
|
|
}
|
|
|
|
static void
|
|
log_message_attributed (void *user_data, const char *quote, const char *fmt,
|
|
va_list ap)
|
|
{
|
|
FILE *stream = stderr;
|
|
|
|
print_attributed (&g_ctx, stream, user_data, "%s", quote);
|
|
vprint_attributed (&g_ctx, stream, user_data, fmt, ap);
|
|
fputs ("\n", stream);
|
|
}
|
|
|
|
static void
|
|
init_colors (struct app_context *ctx)
|
|
{
|
|
// Use escape sequences from terminfo if possible, and SGR as a fallback
|
|
if (init_terminal ())
|
|
{
|
|
const char *attrs[][2] =
|
|
{
|
|
{ ATTR_PROMPT, enter_bold_mode },
|
|
{ ATTR_RESET, exit_attribute_mode },
|
|
{ ATTR_WARNING, g_terminal.color_set[3] },
|
|
{ ATTR_ERROR, g_terminal.color_set[1] },
|
|
{ ATTR_INCOMING, "" },
|
|
{ ATTR_OUTGOING, "" },
|
|
};
|
|
for (size_t i = 0; i < N_ELEMENTS (attrs); i++)
|
|
str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1]));
|
|
}
|
|
else
|
|
{
|
|
const char *attrs[][2] =
|
|
{
|
|
{ ATTR_PROMPT, "\x1b[1m" },
|
|
{ ATTR_RESET, "\x1b[0m" },
|
|
{ ATTR_WARNING, "\x1b[33m" },
|
|
{ ATTR_ERROR, "\x1b[31m" },
|
|
{ ATTR_INCOMING, "" },
|
|
{ ATTR_OUTGOING, "" },
|
|
};
|
|
for (size_t i = 0; i < N_ELEMENTS (attrs); i++)
|
|
str_map_set (&ctx->config, attrs[i][0], xstrdup (attrs[i][1]));
|
|
}
|
|
|
|
switch (ctx->color_mode)
|
|
{
|
|
case COLOR_ALWAYS:
|
|
g_terminal.stdout_is_tty = true;
|
|
g_terminal.stderr_is_tty = true;
|
|
break;
|
|
case COLOR_AUTO:
|
|
if (!g_terminal.initialized)
|
|
{
|
|
case COLOR_NEVER:
|
|
g_terminal.stdout_is_tty = false;
|
|
g_terminal.stderr_is_tty = false;
|
|
}
|
|
}
|
|
|
|
g_log_message_real = log_message_attributed;
|
|
}
|
|
|
|
// --- Configuration loading ---------------------------------------------------
|
|
|
|
static bool
|
|
read_hexa_escape (const char **cursor, struct str *output)
|
|
{
|
|
int i;
|
|
char c, code = 0;
|
|
|
|
for (i = 0; i < 2; i++)
|
|
{
|
|
c = tolower (*(*cursor));
|
|
if (c >= '0' && c <= '9')
|
|
code = (code << 4) | (c - '0');
|
|
else if (c >= 'a' && c <= 'f')
|
|
code = (code << 4) | (c - 'a' + 10);
|
|
else
|
|
break;
|
|
|
|
(*cursor)++;
|
|
}
|
|
|
|
if (!i)
|
|
return false;
|
|
|
|
str_append_c (output, code);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
read_octal_escape (const char **cursor, struct str *output)
|
|
{
|
|
int i;
|
|
char c, code = 0;
|
|
|
|
for (i = 0; i < 3; i++)
|
|
{
|
|
c = *(*cursor);
|
|
if (c < '0' || c > '7')
|
|
break;
|
|
|
|
code = (code << 3) | (c - '0');
|
|
(*cursor)++;
|
|
}
|
|
|
|
if (!i)
|
|
return false;
|
|
|
|
str_append_c (output, code);
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
read_string_escape_sequence (const char **cursor,
|
|
struct str *output, struct error **e)
|
|
{
|
|
int c;
|
|
switch ((c = *(*cursor)++))
|
|
{
|
|
case '?': str_append_c (output, '?'); break;
|
|
case '"': str_append_c (output, '"'); break;
|
|
case '\\': str_append_c (output, '\\'); break;
|
|
case 'a': str_append_c (output, '\a'); break;
|
|
case 'b': str_append_c (output, '\b'); break;
|
|
case 'f': str_append_c (output, '\f'); break;
|
|
case 'n': str_append_c (output, '\n'); break;
|
|
case 'r': str_append_c (output, '\r'); break;
|
|
case 't': str_append_c (output, '\t'); break;
|
|
case 'v': str_append_c (output, '\v'); break;
|
|
|
|
case 'e':
|
|
case 'E':
|
|
str_append_c (output, '\x1b');
|
|
break;
|
|
|
|
case 'x':
|
|
case 'X':
|
|
if (!read_hexa_escape (cursor, output))
|
|
{
|
|
error_set (e, "invalid hexadecimal escape");
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case '\0':
|
|
error_set (e, "premature end of escape sequence");
|
|
return false;
|
|
|
|
default:
|
|
(*cursor)--;
|
|
if (!read_octal_escape (cursor, output))
|
|
{
|
|
error_set (e, "unknown escape sequence");
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
unescape_string (const char *s, struct str *output, struct error **e)
|
|
{
|
|
int c;
|
|
while ((c = *s++))
|
|
{
|
|
if (c != '\\')
|
|
str_append_c (output, c);
|
|
else if (!read_string_escape_sequence (&s, output, e))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
load_config (struct app_context *ctx)
|
|
{
|
|
// TODO: employ a better configuration file format, so that we don't have
|
|
// to do this convoluted post-processing anymore.
|
|
|
|
struct str_map map;
|
|
str_map_init (&map);
|
|
map.free = free;
|
|
|
|
struct error *e = NULL;
|
|
if (!read_config_file (&map, &e))
|
|
{
|
|
print_error ("error loading configuration: %s", e->message);
|
|
error_free (e);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
struct str_map_iter iter;
|
|
str_map_iter_init (&iter, &map);
|
|
while (str_map_iter_next (&iter))
|
|
{
|
|
struct error *e = NULL;
|
|
struct str value;
|
|
str_init (&value);
|
|
if (!unescape_string (iter.link->data, &value, &e))
|
|
{
|
|
print_error ("error reading configuration: %s: %s",
|
|
iter.link->key, e->message);
|
|
error_free (e);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
str_map_set (&ctx->config, iter.link->key, str_steal (&value));
|
|
}
|
|
|
|
str_map_free (&map);
|
|
}
|
|
|
|
// --- Main program ------------------------------------------------------------
|
|
|
|
#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);
|
|
free (utf8);
|
|
|
|
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
|
|
print_attributed (ctx, stdout, ATTR_INCOMING, "%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
|
|
print_attributed (ctx, stdout, ATTR_OUTGOING, "%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" },
|
|
{ 'c', "color", "WHEN", OPT_LONG_ONLY,
|
|
"colorize output: never, always, or auto" },
|
|
{ 'w', "write-default-cfg", "FILENAME",
|
|
OPT_OPTIONAL_ARG | OPT_LONG_ONLY,
|
|
"write a default configuration file and exit" },
|
|
{ 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 'o': *origin = optarg; break;
|
|
case 'a': ctx->auto_id = true; break;
|
|
case 'p': ctx->pretty_print = true; break;
|
|
case 't': ctx->trust_all = true; break;
|
|
case 'v': ctx->verbose = true; break;
|
|
|
|
case 'c':
|
|
if (!strcasecmp (optarg, "never"))
|
|
ctx->color_mode = COLOR_NEVER;
|
|
else if (!strcasecmp (optarg, "always"))
|
|
ctx->color_mode = COLOR_ALWAYS;
|
|
else if (!strcasecmp (optarg, "auto"))
|
|
ctx->color_mode = COLOR_AUTO;
|
|
else
|
|
{
|
|
print_error ("`%s' is not a valid value for `%s'", optarg, "color");
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
break;
|
|
case 'w':
|
|
call_write_default_config (optarg, g_config_table);
|
|
exit (EXIT_SUCCESS);
|
|
|
|
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[])
|
|
{
|
|
str_map_init (&g_ctx.config);
|
|
g_ctx.config.free = free;
|
|
|
|
char *origin = NULL;
|
|
char *endpoint = NULL;
|
|
parse_program_arguments (&g_ctx, argc, argv, &origin, &endpoint);
|
|
|
|
init_colors (&g_ctx);
|
|
load_config (&g_ctx);
|
|
|
|
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);
|
|
|
|
char *prompt;
|
|
if (!get_attribute_printer (stdout))
|
|
prompt = xstrdup_printf ("json-rpc> ");
|
|
else
|
|
{
|
|
// XXX: to be completely correct, we should use tputs, but we cannot
|
|
const char *prompt_attrs = str_map_find (&g_ctx.config, ATTR_PROMPT);
|
|
const char *reset_attrs = str_map_find (&g_ctx.config, ATTR_RESET);
|
|
prompt = xstrdup_printf ("%c%s%cjson-rpc> %c%s%c",
|
|
RL_PROMPT_START_IGNORE, prompt_attrs, RL_PROMPT_END_IGNORE,
|
|
RL_PROMPT_START_IGNORE, reset_attrs, 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);
|
|
str_map_free (&g_ctx.config);
|
|
free_terminal ();
|
|
return EXIT_SUCCESS;
|
|
}
|