Files
json-rpc-shell/json-rpc-shell.c
Přemysl Eric Janouch 2962a644da Write a nice new man page in AsciiDoc
Taking some preliminary steps for inclusion in Linux distributions.

The help message has been slightly improved and the README extended,
with part of it now residing in the man page.

One less GNU dependency, for what it's worth.
2020-09-05 03:51:36 +02:00

3541 lines
93 KiB
C

/*
* json-rpc-shell.c: simple JSON-RPC 2.0 shell
*
* Copyright (c) 2014 - 2020, Přemysl Eric Janouch <p@janouch.name>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted.
*
* 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
// A table of all attributes we use for output
#define ATTR_TABLE(XX) \
XX( PROMPT, "prompt", "Terminal attrs for the prompt" ) \
XX( RESET, "reset", "String to reset terminal attributes" ) \
XX( WARNING, "warning", "Terminal attrs for warnings" ) \
XX( ERROR, "error", "Terminal attrs for errors" ) \
XX( INCOMING, "incoming", "Terminal attrs for incoming traffic" ) \
XX( OUTGOING, "outgoing", "Terminal attrs for outgoing traffic" ) \
XX( JSON_FIELD, "json_field", "Terminal attrs for JSON field names" ) \
XX( JSON_NULL, "json_null", "Terminal attrs for JSON null values" ) \
XX( JSON_BOOL, "json_bool", "Terminal attrs for JSON booleans" ) \
XX( JSON_NUMBER, "json_number", "Terminal attrs for JSON numbers" ) \
XX( JSON_STRING, "json_string", "Terminal attrs for JSON strings" )
enum
{
#define XX(x, y, z) ATTR_ ## x,
ATTR_TABLE (XX)
#undef XX
ATTR_COUNT
};
// User data for logger functions to enable formatted logging
#define print_fatal_data ((void *) ATTR_ERROR)
#define print_error_data ((void *) ATTR_ERROR)
#define print_warning_data ((void *) ATTR_WARNING)
#define LIBERTY_WANT_SSL
#define LIBERTY_WANT_PROTO_HTTP
#define LIBERTY_WANT_PROTO_WS
#include "config.h"
#include "liberty/liberty.c"
#include "http-parser/http_parser.h"
#include <langinfo.h>
#include <locale.h>
#include <arpa/inet.h>
#include <ev.h>
#include <curl/curl.h>
#include <jansson.h>
#include <openssl/rand.h>
#include <curses.h>
#include <term.h>
/// Shorthand to set an error and return failure from the function
#define FAIL(...) \
BLOCK_START \
error_set (e, __VA_ARGS__); \
return false; \
BLOCK_END
// --- Terminal ----------------------------------------------------------------
static struct
{
bool initialized; ///< Terminal is available
bool stdout_is_tty; ///< `stdout' is a terminal
bool stderr_is_tty; ///< `stderr' is a terminal
struct termios termios; ///< Terminal attributes
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;
if (tcgetattr (tty_fd, &g_terminal.termios))
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);
}
// --- User interface ----------------------------------------------------------
// Not trying to do anything crazy here like switchable buffers.
// Not trying to be too universal here either, it's not going to be reusable.
struct input
{
struct input_vtable *vtable; ///< Virtual methods
void *user_data; ///< User data for callbacks
/// Process a single line input by the user
void (*on_input) (char *line, void *user_data);
/// User requested external line editing
void (*on_run_editor) (const char *line, void *user_data);
};
struct input_vtable
{
/// Start the interface under the given program name
void (*start) (struct input *input, const char *program_name);
/// Stop the interface
void (*stop) (struct input *input);
/// Prepare or unprepare terminal for our needs
void (*prepare) (struct input *input, bool enabled);
/// Destroy the object
void (*destroy) (struct input *input);
/// Hide the prompt if shown
void (*hide) (struct input *input);
/// Show the prompt if hidden
void (*show) (struct input *input);
/// Change the prompt string; takes ownership
void (*set_prompt) (struct input *input, char *prompt);
/// Change the current line input
bool (*replace_line) (struct input *input, const char *line);
/// Ring the terminal bell
void (*ding) (struct input *input);
/// Load history from file
bool (*load_history) (struct input *input, const char *filename,
struct error **e);
/// Save history to file
bool (*save_history) (struct input *input, const char *filename,
struct error **e);
/// Handle terminal resize
void (*on_terminal_resized) (struct input *input);
/// Handle terminal input
void (*on_tty_readable) (struct input *input);
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ifdef HAVE_READLINE
#include <readline/readline.h>
#include <readline/history.h>
#define INPUT_START_IGNORE RL_PROMPT_START_IGNORE
#define INPUT_END_IGNORE RL_PROMPT_END_IGNORE
struct input_rl
{
struct input super; ///< Parent class
bool active; ///< Interface has been started
char *prompt; ///< The prompt we use
int prompt_shown; ///< Whether the prompt is shown now
char *saved_line; ///< Saved line content
int saved_point; ///< Saved cursor position
int saved_mark; ///< Saved mark
};
/// Unfortunately Readline cannot pass us any pointer value in its callbacks
/// that would eliminate the need to use global variables ourselves
static struct input_rl *g_input_rl;
static void
input_rl_erase (void)
{
rl_set_prompt ("");
rl_replace_line ("", false);
rl_redisplay ();
}
static void
input_rl_on_input (char *line)
{
struct input_rl *self = g_input_rl;
// The prompt should always be visible at the moment we process input keys;
// confirming it de facto hides it because we move onto a new line
if (line)
self->prompt_shown = 0;
if (line && *line)
add_history (line);
self->super.on_input (line, self->super.user_data);
free (line);
// Readline automatically redisplays the prompt after we're done here;
// we could have actually hidden it by now in preparation of a quit though
if (line)
self->prompt_shown++;
}
static int
input_rl_on_run_editor (int count, int key)
{
(void) count;
(void) key;
struct input_rl *self = g_input_rl;
if (self->super.on_run_editor)
self->super.on_run_editor (rl_line_buffer, self->super.user_data);
return 0;
}
static int
input_rl_on_startup (void)
{
rl_add_defun ("run-editor", input_rl_on_run_editor, -1);
rl_bind_keyseq ("\\ee", rl_named_function ("run-editor"));
return 0;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_start (struct input *input, const char *program_name)
{
struct input_rl *self = (struct input_rl *) input;
using_history ();
// This can cause memory leaks, or maybe even a segfault. Funny, eh?
stifle_history (HISTORY_LIMIT);
const char *slash = strrchr (program_name, '/');
rl_readline_name = slash ? ++slash : program_name;
rl_startup_hook = input_rl_on_startup;
rl_catch_sigwinch = false;
hard_assert (self->prompt != NULL);
rl_callback_handler_install (self->prompt, input_rl_on_input);
self->active = true;
self->prompt_shown = 1;
g_input_rl = self;
}
static void
input_rl_stop (struct input *input)
{
struct input_rl *self = (struct input_rl *) input;
if (self->prompt_shown > 0)
input_rl_erase ();
// This is okay so long as we're not called from within readline
rl_callback_handler_remove ();
self->active = false;
self->prompt_shown = 0;
g_input_rl = NULL;
}
static void
input_rl_prepare (struct input *input, bool enabled)
{
(void) input;
if (enabled)
rl_prep_terminal (true);
else
rl_deprep_terminal ();
}
static void
input_rl_destroy (struct input *input)
{
struct input_rl *self = (struct input_rl *) input;
if (self->active)
input_rl_stop (input);
free (self->saved_line);
free (self->prompt);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_hide (struct input *input)
{
struct input_rl *self = (struct input_rl *) input;
if (!self->active || self->prompt_shown-- < 1)
return;
hard_assert (!self->saved_line);
self->saved_point = rl_point;
self->saved_mark = rl_mark;
self->saved_line = rl_copy_text (0, rl_end);
input_rl_erase ();
}
static void
input_rl_show (struct input *input)
{
struct input_rl *self = (struct input_rl *) input;
if (!self->active || ++self->prompt_shown < 1)
return;
hard_assert (self->saved_line);
rl_set_prompt (self->prompt);
rl_replace_line (self->saved_line, false);
rl_point = self->saved_point;
rl_mark = self->saved_mark;
free (self->saved_line);
self->saved_line = NULL;
rl_redisplay ();
}
static void
input_rl_set_prompt (struct input *input, char *prompt)
{
struct input_rl *self = (struct input_rl *) input;
free (self->prompt);
self->prompt = prompt;
if (!self->active)
return;
// First reset the prompt to work around a bug in readline
rl_set_prompt ("");
if (self->prompt_shown > 0)
rl_redisplay ();
rl_set_prompt (self->prompt);
if (self->prompt_shown > 0)
rl_redisplay ();
}
static bool
input_rl_replace_line (struct input *input, const char *line)
{
struct input_rl *self = (struct input_rl *) input;
if (!self->active || self->prompt_shown < 1)
return false;
rl_point = rl_mark = 0;
rl_replace_line (line, false);
rl_point = strlen (line);
rl_redisplay ();
return true;
}
static void
input_rl_ding (struct input *input)
{
(void) input;
rl_ding ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
input_rl_load_history (struct input *input, const char *filename,
struct error **e)
{
(void) input;
if (!(errno = read_history (filename)))
return true;
error_set (e, "%s", strerror (errno));
return false;
}
static bool
input_rl_save_history (struct input *input, const char *filename,
struct error **e)
{
(void) input;
if (!(errno = write_history (filename)))
return true;
error_set (e, "%s", strerror (errno));
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_rl_on_terminal_resized (struct input *input)
{
// 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
(void) input;
rl_resize_terminal ();
}
static void
input_rl_on_tty_readable (struct input *input)
{
(void) input;
rl_callback_read_char ();
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct input_vtable input_rl_vtable =
{
.start = input_rl_start,
.stop = input_rl_stop,
.prepare = input_rl_prepare,
.destroy = input_rl_destroy,
.hide = input_rl_hide,
.show = input_rl_show,
.set_prompt = input_rl_set_prompt,
.replace_line = input_rl_replace_line,
.ding = input_rl_ding,
.load_history = input_rl_load_history,
.save_history = input_rl_save_history,
.on_terminal_resized = input_rl_on_terminal_resized,
.on_tty_readable = input_rl_on_tty_readable,
};
static struct input *
input_rl_new (void)
{
struct input_rl *self = xcalloc (1, sizeof *self);
self->super.vtable = &input_rl_vtable;
return &self->super;
}
#define input_new input_rl_new
#endif // HAVE_READLINE
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ifdef HAVE_EDITLINE
#include <histedit.h>
#define INPUT_START_IGNORE '\x01'
#define INPUT_END_IGNORE '\x01'
struct input_el
{
struct input super; ///< Parent class
EditLine *editline; ///< The EditLine object
HistoryW *history; ///< The history object
char *entered_line; ///< Buffers the entered line
bool active; ///< Interface has been started
char *prompt; ///< The prompt we use
int prompt_shown; ///< Whether the prompt is shown now
wchar_t *saved_line; ///< Saved line content
int saved_point; ///< Saved cursor position
int saved_len; ///< Saved line length
};
static char *
input_el_wcstombs (const wchar_t *s)
{
size_t len = wcstombs (NULL, s, 0);
if (len++ == (size_t) -1)
return NULL;
char *mb = xmalloc (len);
mb[wcstombs (mb, s, len)] = 0;
return mb;
}
static int
input_el_get_termios (int character, int fallback)
{
if (!g_terminal.initialized)
return fallback;
cc_t value = g_terminal.termios.c_cc[character];
if (value == _POSIX_VDISABLE)
return fallback;
return value;
}
static void
input_el_redisplay (struct input_el *self)
{
char x[] = { input_el_get_termios (VREPRINT, 'R' - 0x40), 0 };
el_push (self->editline, x);
// We have to do this or it gets stuck and nothing is done
int count = 0;
(void) el_wgets (self->editline, &count);
}
static char *
input_el_make_prompt (EditLine *editline)
{
struct input_el *self;
el_get (editline, EL_CLIENTDATA, &self);
return self->prompt ? self->prompt : "";
}
static char *
input_el_make_empty_prompt (EditLine *editline)
{
(void) editline;
return "";
}
static void
input_el_erase (struct input_el *self)
{
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
el_cursor (self->editline, len - point);
el_wdeletestr (self->editline, len);
el_set (self->editline, EL_PROMPT, input_el_make_empty_prompt);
input_el_redisplay (self);
}
static unsigned char
input_el_on_return (EditLine *editline, int key)
{
(void) key;
struct input_el *self;
el_get (editline, EL_CLIENTDATA, &self);
const LineInfoW *info = el_wline (editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
wchar_t *line = calloc (sizeof *info->buffer, len + 1);
memcpy (line, info->buffer, sizeof *info->buffer * len);
if (*line)
{
HistEventW ev;
history_w (self->history, &ev, H_ENTER, line);
}
free (line);
// Convert to a multibyte string and store it for later
const LineInfo *info_mb = el_line (editline);
self->entered_line = xstrndup
(info_mb->buffer, info_mb->lastchar - info_mb->buffer);
// Now we need to force editline to actually print the newline
el_cursor (editline, len++ - point);
el_insertstr (editline, "\n");
input_el_redisplay (self);
// Finally we need to discard the old line's contents
el_wdeletestr (editline, len);
return CC_NEWLINE;
}
static unsigned char
input_el_on_run_editor (EditLine *editline, int key)
{
(void) key;
struct input_el *self;
el_get (editline, EL_CLIENTDATA, &self);
const LineInfo *info = el_line (editline);
char *line = xstrndup (info->buffer, info->lastchar - info->buffer);
if (self->super.on_run_editor)
self->super.on_run_editor (line, self->super.user_data);
free (line);
return CC_NORM;
}
static void
input_el_install_prompt (struct input_el *self)
{
el_set (self->editline, EL_PROMPT_ESC,
input_el_make_prompt, INPUT_START_IGNORE);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_start (struct input *input, const char *program_name)
{
struct input_el *self = (struct input_el *) input;
self->editline = el_init (program_name, stdin, stdout, stderr);
el_set (self->editline, EL_CLIENTDATA, self);
input_el_install_prompt (self);
el_set (self->editline, EL_SIGNAL, false);
el_set (self->editline, EL_UNBUFFERED, isatty (fileno (stdin)));
el_set (self->editline, EL_EDITOR, "emacs");
el_wset (self->editline, EL_HIST, history_w, self->history);
// No, editline, it's not supposed to kill the entire line
el_set (self->editline, EL_BIND, "^w", "ed-delete-prev-word", NULL);
// Just what are you doing?
el_set (self->editline, EL_BIND, "^u", "vi-kill-line-prev", NULL);
// It's probably better to handle this ourselves
el_set (self->editline, EL_ADDFN,
"send-line", "Send line", input_el_on_return);
el_set (self->editline, EL_BIND, "\n", "send-line", NULL);
// It's probably better to handle this ourselves
el_set (self->editline, EL_ADDFN,
"run-editor", "Run editor to edit line", input_el_on_run_editor);
el_set (self->editline, EL_BIND, "M-e", "run-editor", NULL);
// Source the user's defaults file
el_source (self->editline, NULL);
self->active = true;
self->prompt_shown = 1;
}
static void
input_el_stop (struct input *input)
{
struct input_el *self = (struct input_el *) input;
if (self->prompt_shown > 0)
input_el_erase (self);
el_end (self->editline);
self->editline = NULL;
self->active = false;
self->prompt_shown = 0;
}
static void
input_el_prepare (struct input *input, bool enabled)
{
struct input_el *self = (struct input_el *) input;
el_set (self->editline, EL_PREP_TERM, enabled);
}
static void
input_el_destroy (struct input *input)
{
struct input_el *self = (struct input_el *) input;
if (self->active)
input_el_stop (input);
history_wend (self->history);
free (self->saved_line);
free (self->prompt);
free (self);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_hide (struct input *input)
{
struct input_el *self = (struct input_el *) input;
if (!self->active || self->prompt_shown-- < 1)
return;
hard_assert (!self->saved_line);
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
wchar_t *line = calloc (sizeof *info->buffer, len + 1);
memcpy (line, info->buffer, sizeof *info->buffer * len);
el_cursor (self->editline, len - point);
el_wdeletestr (self->editline, len);
self->saved_line = line;
self->saved_point = point;
self->saved_len = len;
input_el_erase (self);
}
static void
input_el_show (struct input *input)
{
struct input_el *self = (struct input_el *) input;
if (!self->active || ++self->prompt_shown < 1)
return;
hard_assert (self->saved_line);
el_winsertstr (self->editline, self->saved_line);
el_cursor (self->editline,
-(self->saved_len - self->saved_point));
free (self->saved_line);
self->saved_line = NULL;
input_el_install_prompt (self);
input_el_redisplay (self);
}
static void
input_el_set_prompt (struct input *input, char *prompt)
{
struct input_el *self = (struct input_el *) input;
free (self->prompt);
self->prompt = prompt;
if (self->prompt_shown > 0)
input_el_redisplay (self);
}
static bool
input_el_replace_line (struct input *input, const char *line)
{
struct input_el *self = (struct input_el *) input;
if (!self->active || self->prompt_shown < 1)
return false;
const LineInfoW *info = el_wline (self->editline);
int len = info->lastchar - info->buffer;
int point = info->cursor - info->buffer;
el_cursor (self->editline, len - point);
el_wdeletestr (self->editline, len);
bool success = !*line || !el_insertstr (self->editline, line);
input_el_redisplay (self);
return success;
}
static void
input_el_ding (struct input *input)
{
(void) input;
const char *ding = bell ? bell : "\a";
write (STDOUT_FILENO, ding, strlen (ding));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
input_el_load_history (struct input *input, const char *filename,
struct error **e)
{
struct input_el *self = (struct input_el *) input;
HistEventW ev;
if (history_w (self->history, &ev, H_LOAD, filename) != -1)
return true;
char *error = input_el_wcstombs (ev.str);
error_set (e, "%s", error);
free (error);
return false;
}
static bool
input_el_save_history (struct input *input, const char *filename,
struct error **e)
{
struct input_el *self = (struct input_el *) input;
HistEventW ev;
if (history_w (self->history, &ev, H_SAVE, filename) != -1)
return true;
char *error = input_el_wcstombs (ev.str);
error_set (e, "%s", error);
free (error);
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
input_el_on_terminal_resized (struct input *input)
{
struct input_el *self = (struct input_el *) input;
el_resize (self->editline);
}
static void
input_el_on_tty_readable (struct input *input)
{
// We bind the return key to process it how we need to
struct input_el *self = (struct input_el *) input;
// el_gets() with EL_UNBUFFERED doesn't work with UTF-8,
// we must use the wide-character interface
int count = 0;
const wchar_t *buf = el_wgets (self->editline, &count);
// Editline works in a funny NO_TTY mode when the input is not a tty,
// we cannot use EL_UNBUFFERED and expect sane results then
int unbuffered = 0;
if (!el_get (self->editline, EL_UNBUFFERED, &unbuffered) && !unbuffered)
{
char *entered_line = buf ? input_el_wcstombs (buf) : NULL;
self->super.on_input (entered_line, self->super.user_data);
free (entered_line);
return;
}
// Process data from our newline handler (async-friendly handling)
if (self->entered_line)
{
// We can't have anything try to hide the old prompt with the appended
// newline, it needs to stay where it is and as it is
self->prompt_shown = 0;
self->super.on_input (self->entered_line, self->super.user_data);
free (self->entered_line);
self->entered_line = NULL;
// Forbid editline from trying to erase the old prompt (or worse)
// and let it redisplay the prompt in its clean state
el_set (self->editline, EL_REFRESH);
self->prompt_shown = 1;
}
if (count == 1 && buf[0] == ('D' - 0x40) /* hardcoded VEOF in editline */)
{
el_deletestr (self->editline, 1);
input_el_redisplay (self);
self->super.on_input (NULL, self->super.user_data);
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct input_vtable input_el_vtable =
{
.start = input_el_start,
.stop = input_el_stop,
.prepare = input_el_prepare,
.destroy = input_el_destroy,
.hide = input_el_hide,
.show = input_el_show,
.set_prompt = input_el_set_prompt,
.replace_line = input_el_replace_line,
.ding = input_el_ding,
.load_history = input_el_load_history,
.save_history = input_el_save_history,
.on_terminal_resized = input_el_on_terminal_resized,
.on_tty_readable = input_el_on_tty_readable,
};
static struct input *
input_el_new (void)
{
struct input_el *self = xcalloc (1, sizeof *self);
self->super.vtable = &input_el_vtable;
HistEventW ev;
self->history = history_winit ();
history_w (self->history, &ev, H_SETSIZE, HISTORY_LIMIT);
return &self->super;
}
#define input_new input_el_new
#endif // HAVE_EDITLINE
// --- 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
{
ev_child child_watcher; ///< SIGCHLD watcher
ev_signal winch_watcher; ///< SIGWINCH watcher
ev_signal term_watcher; ///< SIGTERM watcher
ev_signal int_watcher; ///< SIGINT watcher
ev_io tty_watcher; ///< Terminal watcher
struct input *input; ///< Input interface
char *attrs_defaults[ATTR_COUNT]; ///< Default terminal attributes
char *attrs[ATTR_COUNT]; ///< Terminal attributes
struct backend *backend; ///< Our current backend
char *editor_filename; ///< File for input line editor
struct config 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;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// HTTP/S and WS/S require significantly different handling. While for HTTP we
// can just use the cURL easy interface, with WebSockets it gets a bit more
// complicated and we implement it all by ourselves.
//
// Luckily on a higher level the application doesn't need to bother itself with
// the details and the backend API can be very simple.
struct backend
{
struct backend_vtable *vtable; ///< Virtual methods
};
struct backend_vtable
{
/// Add an HTTP header to send with requests
void (*add_header) (struct backend *backend, const char *header);
/// Make an RPC call
bool (*make_call) (struct backend *backend,
const char *request, bool expect_content,
struct str *buf, struct error **e);
/// Do everything necessary to deal with ev_break(EVBREAK_ALL)
void (*on_quit) (struct backend *backend);
/// Free any resources
void (*destroy) (struct backend *backend);
};
// --- Configuration -----------------------------------------------------------
static void on_config_attribute_change (struct config_item *item);
static struct config_schema g_config_connection[] =
{
{ .name = "tls_ca_file",
.comment = "OpenSSL CA bundle file",
.type = CONFIG_ITEM_STRING },
{ .name = "tls_ca_path",
.comment = "OpenSSL CA bundle path",
.type = CONFIG_ITEM_STRING },
{}
};
static struct config_schema g_config_attributes[] =
{
#define XX(x, y, z) { .name = y, .comment = z, .type = CONFIG_ITEM_STRING, \
.on_change = on_config_attribute_change },
ATTR_TABLE (XX)
#undef XX
{}
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_config_connection (struct config_item *subtree, void *user_data)
{
config_schema_apply_to_object (g_config_connection, subtree, user_data);
}
static void
load_config_attributes (struct config_item *subtree, void *user_data)
{
config_schema_apply_to_object (g_config_attributes, subtree, user_data);
}
static void
register_config_modules (struct app_context *ctx)
{
struct config *config = &ctx->config;
config_register_module (config, "connection", load_config_connection, ctx);
config_register_module (config, "attributes", load_config_attributes, ctx);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *
get_config_string (struct config_item *root, const char *key)
{
struct config_item *item = config_item_get (root, key, NULL);
hard_assert (item);
if (item->type == CONFIG_ITEM_NULL)
return NULL;
hard_assert (config_item_type_is_string (item->type));
return item->value.string.str;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
save_configuration (struct config_item *root, const char *path_hint)
{
struct str data = str_make ();
str_append (&data,
"# " PROGRAM_NAME " " PROGRAM_VERSION " configuration file\n"
"#\n"
"# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}\n"
"# /" PROGRAM_NAME " as well as in $XDG_CONFIG_DIRS/" PROGRAM_NAME "\n"
"#\n"
"# All text must be in UTF-8.\n"
"\n");
config_item_write (root, true, &data);
struct error *e = NULL;
char *filename = write_configuration_file (path_hint, &data, &e);
str_free (&data);
if (!filename)
{
print_error ("%s: %s", "saving configuration failed", e->message);
error_free (e);
}
else
print_status ("configuration written to `%s'", filename);
free (filename);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
load_configuration (struct app_context *ctx)
{
char *filename = resolve_filename
(PROGRAM_NAME ".conf", resolve_relative_config_filename);
if (!filename)
return;
struct error *e = NULL;
struct config_item *root = config_read_from_file (filename, &e);
free (filename);
if (e)
{
print_error ("error loading configuration: %s", e->message);
error_free (e);
exit (EXIT_FAILURE);
}
if (root)
{
config_load (&ctx->config, root);
config_schema_call_changed (ctx->config.root);
}
}
// --- Attributed output -------------------------------------------------------
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, intptr_t attribute, const char *fmt, va_list ap)
{
terminal_printer_fn printer = get_attribute_printer (stream);
if (!attribute)
printer = NULL;
if (printer)
tputs (ctx->attrs[attribute], 1, printer);
vfprintf (stream, fmt, ap);
if (printer)
tputs (ctx->attrs[ATTR_RESET], 1, printer);
}
static void
print_attributed (struct app_context *ctx,
FILE *stream, intptr_t 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;
g_ctx.input->vtable->hide (g_ctx.input);
print_attributed (&g_ctx, stream, (intptr_t) user_data, "%s", quote);
vprint_attributed (&g_ctx, stream, (intptr_t) user_data, fmt, ap);
fputs ("\n", stream);
g_ctx.input->vtable->show (g_ctx.input);
}
static void
init_colors (struct app_context *ctx)
{
char **defaults = ctx->attrs_defaults;
#define INIT_ATTR(id, ti) defaults[ATTR_ ## id] = xstrdup ((ti))
// Use escape sequences from terminfo if possible, and SGR as a fallback
if (init_terminal ())
{
INIT_ATTR (PROMPT, enter_bold_mode);
INIT_ATTR (RESET, exit_attribute_mode);
INIT_ATTR (WARNING, g_terminal.color_set[COLOR_YELLOW]);
INIT_ATTR (ERROR, g_terminal.color_set[COLOR_RED]);
INIT_ATTR (INCOMING, "");
INIT_ATTR (OUTGOING, "");
INIT_ATTR (JSON_FIELD, enter_bold_mode);
INIT_ATTR (JSON_NULL, g_terminal.color_set[COLOR_CYAN]);
INIT_ATTR (JSON_BOOL, g_terminal.color_set[COLOR_RED]);
INIT_ATTR (JSON_NUMBER, g_terminal.color_set[COLOR_MAGENTA]);
INIT_ATTR (JSON_STRING, g_terminal.color_set[COLOR_BLUE]);
}
else
{
INIT_ATTR (PROMPT, "\x1b[1m");
INIT_ATTR (RESET, "\x1b[0m");
INIT_ATTR (WARNING, "\x1b[33m");
INIT_ATTR (ERROR, "\x1b[31m");
INIT_ATTR (INCOMING, "");
INIT_ATTR (OUTGOING, "");
INIT_ATTR (JSON_FIELD, "\x1b[1m");
INIT_ATTR (JSON_NULL, "\x1b[36m");
INIT_ATTR (JSON_BOOL, "\x1b[31m");
INIT_ATTR (JSON_NUMBER, "\x1b[35m");
INIT_ATTR (JSON_STRING, "\x1b[32m");
}
#undef INIT_ATTR
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;
// Apply the default values so that we start with any formatting at all
config_schema_call_changed
(config_item_get (ctx->config.root, "attributes", NULL));
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static ssize_t
attr_by_name (const char *name)
{
static const char *table[ATTR_COUNT] =
{
#define XX(x, y, z) [ATTR_ ## x] = y,
ATTR_TABLE (XX)
#undef XX
};
for (size_t i = 0; i < N_ELEMENTS (table); i++)
if (!strcmp (name, table[i]))
return i;
return -1;
}
static void
on_config_attribute_change (struct config_item *item)
{
struct app_context *ctx = item->user_data;
ssize_t id = attr_by_name (item->schema->name);
if (id != -1)
{
free (ctx->attrs[id]);
ctx->attrs[id] = xstrdup (item->type == CONFIG_ITEM_NULL
? ctx->attrs_defaults[id]
: item->value.string.str);
}
}
// --- WebSockets backend ------------------------------------------------------
enum ws_handler_state
{
WS_HANDLER_CONNECTING, ///< Parsing HTTP
WS_HANDLER_OPEN, ///< Parsing WebSockets frames
WS_HANDLER_CLOSING, ///< Closing the connection
WS_HANDLER_CLOSED ///< Dead
};
#define BACKEND_WS_MAX_PAYLOAD_LEN UINT32_MAX
struct ws_context
{
struct backend super; ///< Parent class
struct app_context *ctx; ///< Application context
// Configuration:
char *endpoint; ///< Endpoint URL
struct http_parser_url url; ///< Parsed URL
struct strv extra_headers; ///< Extra headers for the handshake
// Events:
bool waiting_for_event; ///< Running a separate loop to wait?
struct error *e; ///< Error while waiting for event
ev_timer timeout_watcher; ///< Connection timeout watcher
struct str *response_buffer; ///< Buffer for the incoming messages
// The TCP transport:
int server_fd; ///< Socket FD of the server
ev_io read_watcher; ///< Server FD read watcher
SSL_CTX *ssl_ctx; ///< SSL context
SSL *ssl; ///< SSL connection
// WebSockets protocol handling:
enum ws_handler_state state; ///< State
char *key; ///< Key for the current handshake
http_parser hp; ///< HTTP parser
bool have_header_value; ///< Parsing header value or field?
struct str field; ///< Field part buffer
struct str value; ///< Value part buffer
struct str_map headers; ///< HTTP Headers
struct ws_parser parser; ///< Protocol frame parser
bool expecting_continuation; ///< For non-control traffic
enum ws_opcode message_opcode; ///< Opcode for the current message
struct str message_data; ///< Concatenated message data
};
static void
backend_ws_add_header (struct backend *backend, const char *header)
{
struct ws_context *self = (struct ws_context *) backend;
strv_append (&self->extra_headers, header);
}
enum ws_read_result
{
WS_READ_OK, ///< Some data were read successfully
WS_READ_EOF, ///< The server has closed connection
WS_READ_AGAIN, ///< No more data at the moment
WS_READ_ERROR ///< General connection failure
};
static enum ws_read_result
backend_ws_fill_read_buffer_tls
(struct ws_context *self, void *buf, size_t *len)
{
int n_read;
start:
n_read = SSL_read (self->ssl, buf, *len);
const char *error_info = NULL;
switch (xssl_get_error (self->ssl, n_read, &error_info))
{
case SSL_ERROR_NONE:
*len = n_read;
return WS_READ_OK;
case SSL_ERROR_ZERO_RETURN:
return WS_READ_EOF;
case SSL_ERROR_WANT_READ:
return WS_READ_AGAIN;
case SSL_ERROR_WANT_WRITE:
{
// Let it finish the handshake as we don't poll for writability;
// any errors are to be collected by SSL_read() in the next iteration
struct pollfd pfd = { .fd = self->server_fd, .events = POLLOUT };
soft_assert (poll (&pfd, 1, 0) > 0);
goto start;
}
case XSSL_ERROR_TRY_AGAIN:
goto start;
default:
print_debug ("%s: %s: %s", __func__, "SSL_read", error_info);
return WS_READ_ERROR;
}
}
static enum ws_read_result
backend_ws_fill_read_buffer
(struct ws_context *self, void *buf, size_t *len)
{
ssize_t n_read;
start:
n_read = recv (self->server_fd, buf, *len, 0);
if (n_read > 0)
{
*len = n_read;
return WS_READ_OK;
}
if (n_read == 0)
return WS_READ_EOF;
if (errno == EAGAIN)
return WS_READ_AGAIN;
if (errno == EINTR)
goto start;
print_debug ("%s: %s: %s", __func__, "recv", strerror (errno));
return WS_READ_ERROR;
}
static bool
backend_ws_header_field_is_a_list (const char *name)
{
// This must contain all header fields we use for anything
static const char *concatenable[] =
{ SEC_WS_PROTOCOL, SEC_WS_EXTENSIONS, "Connection", "Upgrade" };
for (size_t i = 0; i < N_ELEMENTS (concatenable); i++)
if (!strcasecmp_ascii (name, concatenable[i]))
return true;
return false;
}
static void
backend_ws_on_header_read (struct ws_context *self)
{
// The HTTP parser unfolds values and removes preceding whitespace, but
// otherwise doesn't touch the values or the following whitespace.
// RFC 7230 states that trailing whitespace is not part of a field value
char *value = self->field.str;
size_t len = self->field.len;
while (len--)
if (value[len] == '\t' || value[len] == ' ')
value[len] = '\0';
else
break;
self->field.len = len;
const char *field = self->field.str;
const char *current = str_map_find (&self->headers, field);
if (backend_ws_header_field_is_a_list (field) && current)
str_map_set (&self->headers, field,
xstrdup_printf ("%s, %s", current, self->value.str));
else
// If the field cannot be concatenated, just overwrite the last value.
// Maybe we should issue a warning or something.
str_map_set (&self->headers, field, xstrdup (self->value.str));
}
static int
backend_ws_on_header_field (http_parser *parser, const char *at, size_t len)
{
struct ws_context *self = parser->data;
if (self->have_header_value)
{
backend_ws_on_header_read (self);
str_reset (&self->field);
str_reset (&self->value);
}
str_append_data (&self->field, at, len);
self->have_header_value = false;
return 0;
}
static int
backend_ws_on_header_value (http_parser *parser, const char *at, size_t len)
{
struct ws_context *self = parser->data;
str_append_data (&self->value, at, len);
self->have_header_value = true;
return 0;
}
static int
backend_ws_on_headers_complete (http_parser *parser)
{
struct ws_context *self = parser->data;
if (self->have_header_value)
backend_ws_on_header_read (self);
// We strictly require a protocol upgrade
if (!parser->upgrade)
return 2;
return 0;
}
static bool
backend_ws_finish_handshake (struct ws_context *self, struct error **e)
{
if (self->hp.http_major != 1 || self->hp.http_minor < 1)
FAIL ("incompatible HTTP version: %d.%d",
self->hp.http_major, self->hp.http_minor);
if (self->hp.status_code != 101)
// TODO: handle other codes?
FAIL ("unexpected status code: %d", self->hp.status_code);
const char *upgrade = str_map_find (&self->headers, "Upgrade");
if (!upgrade || strcasecmp_ascii (upgrade, "websocket"))
FAIL ("cannot upgrade connection to WebSocket");
const char *connection = str_map_find (&self->headers, "Connection");
if (!connection || strcasecmp_ascii (connection, "Upgrade"))
// XXX: maybe we shouldn't be so strict and only check for presence
// of the "Upgrade" token in this list
FAIL ("cannot upgrade connection to WebSocket");
const char *accept = str_map_find (&self->headers, SEC_WS_ACCEPT);
char *accept_expected = ws_encode_response_key (self->key);
bool accept_ok = accept && !strcmp (accept, accept_expected);
free (accept_expected);
if (!accept_ok)
FAIL ("missing or invalid " SEC_WS_ACCEPT " header");
const char *extensions = str_map_find (&self->headers, SEC_WS_EXTENSIONS);
const char *protocol = str_map_find (&self->headers, SEC_WS_PROTOCOL);
if (extensions || protocol)
// TODO: actually parse these fields
FAIL ("unexpected WebSocket extension or protocol");
return true;
}
static bool
backend_ws_on_data (struct ws_context *self, const void *data, size_t len)
{
if (self->state != WS_HANDLER_CONNECTING)
return ws_parser_push (&self->parser, data, len);
// The handshake hasn't been done yet, process HTTP headers
static const http_parser_settings http_settings =
{
.on_header_field = backend_ws_on_header_field,
.on_header_value = backend_ws_on_header_value,
.on_headers_complete = backend_ws_on_headers_complete,
};
size_t n_parsed = http_parser_execute (&self->hp,
&http_settings, data, len);
if (self->hp.upgrade)
{
struct error *e = NULL;
if (!backend_ws_finish_handshake (self, &e))
{
print_error ("WS handshake failed: %s", e->message);
error_free (e);
return false;
}
// Finished the handshake, return to caller
// (we run a separate loop to wait for the handshake to finish)
self->state = WS_HANDLER_OPEN;
ev_break (EV_DEFAULT_ EVBREAK_ONE);
if ((len -= n_parsed))
return ws_parser_push (&self->parser,
(const uint8_t *) data + n_parsed, len);
return true;
}
enum http_errno err = HTTP_PARSER_ERRNO (&self->hp);
if (n_parsed != len || err != HPE_OK)
{
if (err == HPE_CB_headers_complete)
print_error ("WS handshake failed: %s", "missing `Upgrade' field");
else
print_error ("WS handshake failed: %s",
http_errno_description (err));
return false;
}
return true;
}
static void
backend_ws_close_connection (struct ws_context *self)
{
if (self->server_fd == -1)
return;
ev_io_stop (EV_DEFAULT_ &self->read_watcher);
if (self->ssl)
{
(void) SSL_shutdown (self->ssl);
SSL_free (self->ssl);
self->ssl = NULL;
}
xclose (self->server_fd);
self->server_fd = -1;
self->state = WS_HANDLER_CLOSED;
// That would have no way of succeeding
// XXX: what if we're waiting for the close?
if (self->waiting_for_event)
{
if (!self->e)
error_set (&self->e, "unexpected connection close");
ev_break (EV_DEFAULT_ EVBREAK_ONE);
}
}
static void
backend_ws_on_fd_ready (EV_P_ ev_io *handle, int revents)
{
(void) loop;
(void) revents;
struct ws_context *self = handle->data;
enum ws_read_result (*fill_buffer)(struct ws_context *, void *, size_t *)
= self->ssl
? backend_ws_fill_read_buffer_tls
: backend_ws_fill_read_buffer;
bool close_connection = false;
uint8_t buf[8192];
while (true)
{
// Try to read some data in a non-blocking manner
size_t n_read = sizeof buf;
(void) set_blocking (self->server_fd, false);
enum ws_read_result result = fill_buffer (self, buf, &n_read);
(void) set_blocking (self->server_fd, true);
switch (result)
{
case WS_READ_AGAIN:
goto end;
case WS_READ_ERROR:
print_error ("reading from the server failed");
close_connection = true;
goto end;
case WS_READ_EOF:
print_status ("the server closed the connection");
close_connection = true;
goto end;
case WS_READ_OK:
if (backend_ws_on_data (self, buf, n_read))
break;
// XXX: maybe we should wait until we receive an EOF
close_connection = true;
goto end;
}
}
end:
if (close_connection)
backend_ws_close_connection (self);
}
static bool
backend_ws_write (struct ws_context *self, const void *data, size_t len)
{
if (!soft_assert (self->server_fd != -1))
return false;
if (self->ssl)
{
// TODO: call SSL_get_error() to detect if a clean shutdown has occured
if (SSL_write (self->ssl, data, len) != (int) len)
{
print_debug ("%s: %s: %s", __func__, "SSL_write",
ERR_error_string (ERR_get_error (), NULL));
return false;
}
}
else if (write (self->server_fd, data, len) != (ssize_t) len)
{
print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
return false;
}
return true;
}
static bool
backend_ws_establish_connection (struct ws_context *self,
const char *host, const char *port, struct error **e)
{
struct addrinfo gai_hints, *gai_result, *gai_iter;
memset (&gai_hints, 0, sizeof gai_hints);
gai_hints.ai_socktype = SOCK_STREAM;
int err = getaddrinfo (host, port, &gai_hints, &gai_result);
if (err)
FAIL ("%s: %s: %s",
"connection failed", "getaddrinfo", gai_strerror (err));
int sockfd;
for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next)
{
sockfd = socket (gai_iter->ai_family,
gai_iter->ai_socktype, gai_iter->ai_protocol);
if (sockfd == -1)
continue;
set_cloexec (sockfd);
int yes = 1;
soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE,
&yes, sizeof yes) != -1);
const char *real_host = host;
// Let's try to resolve the address back into a real hostname;
// we don't really need this, so we can let it quietly fail
char buf[NI_MAXHOST];
err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen,
buf, sizeof buf, NULL, 0, NI_NUMERICHOST);
if (err)
print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
else
real_host = buf;
if (g_debug_mode)
{
char *address = format_host_port_pair (real_host, port);
print_status ("connecting to %s...", address);
free (address);
}
if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen))
break;
xclose (sockfd);
}
freeaddrinfo (gai_result);
if (!gai_iter)
FAIL ("connection failed");
self->server_fd = sockfd;
return true;
}
static bool
backend_ws_set_up_ssl_ctx (struct ws_context *self)
{
if (self->ctx->trust_all)
{
SSL_CTX_set_verify (self->ssl_ctx, SSL_VERIFY_NONE, NULL);
return true;
}
// TODO: try to resolve filenames relative to configuration directories
const char *ca_file = get_config_string
(self->ctx->config.root, "connection.tls_ca_file");
const char *ca_path = get_config_string
(self->ctx->config.root, "connection.tls_ca_path");
if (ca_file || ca_path)
{
if (SSL_CTX_load_verify_locations (self->ssl_ctx, ca_file, ca_path))
return true;
print_warning ("%s: %s",
"failed to set locations for trusted CA certificates",
ERR_reason_error_string (ERR_get_error ()));
}
return SSL_CTX_set_default_verify_paths (self->ssl_ctx);
}
static bool
backend_ws_initialize_tls (struct ws_context *self,
const char *server_name, struct error **e)
{
const char *error_info = NULL;
if (!self->ssl_ctx)
{
if (!(self->ssl_ctx = SSL_CTX_new (SSLv23_client_method ())))
goto error_ssl_1;
if (!backend_ws_set_up_ssl_ctx (self))
goto error_ssl_2;
}
self->ssl = SSL_new (self->ssl_ctx);
if (!self->ssl)
goto error_ssl_2;
SSL_set_connect_state (self->ssl);
if (!SSL_set_fd (self->ssl, self->server_fd))
goto error_ssl_3;
// Avoid SSL_write() returning SSL_ERROR_WANT_READ
SSL_set_mode (self->ssl, SSL_MODE_AUTO_RETRY);
// Literal IP addresses aren't allowed in the SNI
struct in6_addr dummy;
if (!inet_pton (AF_INET, server_name, &dummy)
&& !inet_pton (AF_INET6, server_name, &dummy))
SSL_set_tlsext_host_name (self->ssl, server_name);
switch (xssl_get_error (self->ssl, SSL_connect (self->ssl), &error_info))
{
case SSL_ERROR_NONE:
return true;
case SSL_ERROR_ZERO_RETURN:
error_info = "server closed the connection";
default:
break;
}
error_ssl_3:
SSL_free (self->ssl);
self->ssl = NULL;
error_ssl_2:
SSL_CTX_free (self->ssl_ctx);
self->ssl_ctx = NULL;
error_ssl_1:
// XXX: these error strings are really nasty; also there could be
// multiple errors on the OpenSSL stack.
if (!error_info)
error_info = ERR_error_string (ERR_get_error (), NULL);
FAIL ("%s: %s", "could not initialize SSL", error_info);
}
static bool
backend_ws_send_message (struct ws_context *self,
enum ws_opcode opcode, const void *data, size_t len)
{
struct str header = str_make ();
str_pack_u8 (&header, 0x80 | (opcode & 0x0F));
if (len > UINT16_MAX)
{
str_pack_u8 (&header, 0x80 | 127);
str_pack_u64 (&header, len);
}
else if (len > 125)
{
str_pack_u8 (&header, 0x80 | 126);
str_pack_u16 (&header, len);
}
else
str_pack_u8 (&header, 0x80 | len);
uint32_t mask;
if (!RAND_bytes ((unsigned char *) &mask, sizeof mask))
return false;
str_pack_u32 (&header, mask);
bool result = backend_ws_write (self, header.str, header.len);
str_free (&header);
while (result && len)
{
size_t block_size = MIN (len, 1 << 16);
char masked[block_size];
memcpy (masked, data, block_size);
ws_parser_unmask (masked, block_size, mask);
result = backend_ws_write (self, masked, block_size);
len -= block_size;
data = (const uint8_t *) data + block_size;
}
return result;
}
static bool
backend_ws_send_control (struct ws_context *self,
enum ws_opcode opcode, const void *data, size_t len)
{
if (len > WS_MAX_CONTROL_PAYLOAD_LEN)
{
print_debug ("truncating output control frame payload"
" from %zu to %zu bytes", len, (size_t) WS_MAX_CONTROL_PAYLOAD_LEN);
len = WS_MAX_CONTROL_PAYLOAD_LEN;
}
return backend_ws_send_message (self, opcode, data, len);
}
static bool
backend_ws_fail (struct ws_context *self, enum ws_status reason)
{
uint8_t payload[2] = { reason >> 8, reason };
(void) backend_ws_send_control (self, WS_OPCODE_CLOSE,
payload, sizeof payload);
// The caller should immediately proceed to close the TCP connection,
// e.g. by returning false from a handler
self->state = WS_HANDLER_CLOSING;
return false;
}
static bool
backend_ws_on_frame_header (void *user_data, const struct ws_parser *parser)
{
struct ws_context *self = user_data;
// Note that we aren't expected to send any close frame before closing the
// connection when the frame is unmasked
if (parser->reserved_1 || parser->reserved_2 || parser->reserved_3
|| parser->is_masked // server -> client payload must not be masked
|| (ws_is_control_frame (parser->opcode) &&
(!parser->is_fin || parser->payload_len > WS_MAX_CONTROL_PAYLOAD_LEN))
|| (!ws_is_control_frame (parser->opcode) &&
(self->expecting_continuation && parser->opcode != WS_OPCODE_CONT))
|| parser->payload_len >= 0x8000000000000000ULL)
return backend_ws_fail (self, WS_STATUS_PROTOCOL_ERROR);
else if (parser->payload_len > BACKEND_WS_MAX_PAYLOAD_LEN)
return backend_ws_fail (self, WS_STATUS_MESSAGE_TOO_BIG);
return true;
}
static bool
backend_ws_finish_closing_handshake
(struct ws_context *self, const struct ws_parser *parser)
{
struct str reason = str_make ();
if (parser->payload_len >= 2)
{
struct msg_unpacker unpacker =
msg_unpacker_make (parser->input.str, parser->payload_len);
uint16_t status_code;
msg_unpacker_u16 (&unpacker, &status_code);
print_debug ("close status code: %d", status_code);
str_append_data (&reason,
parser->input.str + 2, parser->payload_len - 2);
}
char *s = iconv_xstrdup (self->ctx->term_from_utf8,
reason.str, reason.len + 1 /* null byte */, NULL);
print_status ("server closed the connection (%s)", s);
str_free (&reason);
free (s);
return backend_ws_send_control (self, WS_OPCODE_CLOSE,
parser->input.str, parser->payload_len);
}
static bool
backend_ws_on_control_frame
(struct ws_context *self, const struct ws_parser *parser)
{
switch (parser->opcode)
{
case WS_OPCODE_CLOSE:
// We've received an unsolicited server close
if (self->state != WS_HANDLER_CLOSING)
(void) backend_ws_finish_closing_handshake (self, parser);
return false;
case WS_OPCODE_PING:
if (!backend_ws_send_control (self, WS_OPCODE_PONG,
parser->input.str, parser->payload_len))
return false;
break;
case WS_OPCODE_PONG:
// Not sending any pings but w/e
break;
default:
// Unknown control frame
return backend_ws_fail (self, WS_STATUS_PROTOCOL_ERROR);
}
return true;
}
static int normalize_whitespace (int c) { return isspace_ascii (c) ? ' ' : c; }
/// Caller guarantees that data[len] is a NUL byte (because of iconv_xstrdup())
static bool
backend_ws_on_message (struct ws_context *self,
enum ws_opcode type, const void *data, size_t len)
{
if (type != WS_OPCODE_TEXT)
return backend_ws_fail (self, WS_STATUS_UNSUPPORTED_DATA);
if (!self->waiting_for_event || !self->response_buffer)
{
char *s = iconv_xstrdup (self->ctx->term_from_utf8,
(char *) data, len + 1 /* null byte */, NULL);
// Does not affect JSON and ensures the message is printed out okay
cstr_transform (s, normalize_whitespace);
print_warning ("unexpected message received: %s", s);
free (s);
return true;
}
str_append_data (self->response_buffer, data, len);
ev_break (EV_DEFAULT_ EVBREAK_ONE);
return true;
}
static bool
backend_ws_on_frame (void *user_data, const struct ws_parser *parser)
{
struct ws_context *self = user_data;
if (ws_is_control_frame (parser->opcode))
return backend_ws_on_control_frame (self, parser);
// TODO: do this rather in "on_frame_header"
if (self->message_data.len + parser->payload_len
> BACKEND_WS_MAX_PAYLOAD_LEN)
return backend_ws_fail (self, WS_STATUS_MESSAGE_TOO_BIG);
if (!self->expecting_continuation)
self->message_opcode = parser->opcode;
str_append_data (&self->message_data,
parser->input.str, parser->payload_len);
self->expecting_continuation = !parser->is_fin;
if (!parser->is_fin)
return true;
if (self->message_opcode == WS_OPCODE_TEXT
&& !utf8_validate (self->message_data.str, self->message_data.len))
return backend_ws_fail (self, WS_STATUS_INVALID_PAYLOAD_DATA);
bool result = backend_ws_on_message (self, self->message_opcode,
self->message_data.str, self->message_data.len);
str_reset (&self->message_data);
return result;
}
static void
backend_ws_on_connection_timeout (EV_P_ ev_timer *handle, int revents)
{
(void) loop;
(void) revents;
struct ws_context *self = handle->data;
hard_assert (self->waiting_for_event);
error_set (&self->e, "connection timeout");
backend_ws_close_connection (self);
}
static bool
backend_ws_connect (struct ws_context *self, struct error **e)
{
bool result = false;
char *url_schema = xstrndup (self->endpoint +
self->url.field_data[UF_SCHEMA].off,
self->url.field_data[UF_SCHEMA].len);
bool use_tls = !strcasecmp_ascii (url_schema, "wss");
free (url_schema);
char *url_host = xstrndup (self->endpoint +
self->url.field_data[UF_HOST].off,
self->url.field_data[UF_HOST].len);
char *url_port = (self->url.field_set & (1 << UF_PORT))
? xstrndup (self->endpoint +
self->url.field_data[UF_PORT].off,
self->url.field_data[UF_PORT].len)
: xstrdup (use_tls ? "443" : "80");
struct str url_path = str_make ();
if (self->url.field_set & (1 << UF_PATH))
str_append_data (&url_path, self->endpoint +
self->url.field_data[UF_PATH].off,
self->url.field_data[UF_PATH].len);
else
str_append_c (&url_path, '/');
if (self->url.field_set & (1 << UF_QUERY))
{
str_append_c (&url_path, '?');
str_append_data (&url_path, self->endpoint +
self->url.field_data[UF_QUERY].off,
self->url.field_data[UF_QUERY].len);
}
// TODO: I guess we should also reset it on error
self->state = WS_HANDLER_CONNECTING;
if (!backend_ws_establish_connection (self, url_host, url_port, e))
goto fail_1;
if (use_tls && !backend_ws_initialize_tls (self, url_host, e))
goto fail_2;
unsigned char key[16];
if (!RAND_bytes (key, sizeof key))
{
error_set (e, "failed to get random bytes");
goto fail_2;
}
struct str key_b64 = str_make ();
base64_encode (key, sizeof key, &key_b64);
free (self->key);
char *key_b64_string = self->key = str_steal (&key_b64);
struct str request = str_make ();
str_append_printf (&request, "GET %s HTTP/1.1\r\n", url_path.str);
// TODO: omit the port if it's the default (check RFC for "SHOULD" or ...)
str_append_printf (&request, "Host: %s:%s\r\n", url_host, url_port);
str_append_printf (&request, "Upgrade: websocket\r\n");
str_append_printf (&request, "Connection: upgrade\r\n");
str_append_printf (&request, SEC_WS_KEY ": %s\r\n", key_b64_string);
str_append_printf (&request, SEC_WS_VERSION ": %s\r\n", "13");
for (size_t i = 0; i < self->extra_headers.len; i++)
str_append_printf (&request, "%s\r\n", self->extra_headers.vector[i]);
str_append_printf (&request, "\r\n");
bool written = backend_ws_write (self, request.str, request.len);
str_free (&request);
if (!written)
{
error_set (e, "connection failed");
goto fail_2;
}
http_parser_init (&self->hp, HTTP_RESPONSE);
self->hp.data = self;
str_reset (&self->field);
str_reset (&self->value);
str_map_clear (&self->headers);
ws_parser_free (&self->parser);
self->parser = ws_parser_make ();
self->parser.on_frame_header = backend_ws_on_frame_header;
self->parser.on_frame = backend_ws_on_frame;
self->parser.user_data = self;
ev_io_init (&self->read_watcher,
backend_ws_on_fd_ready, self->server_fd, EV_READ);
self->read_watcher.data = self;
ev_io_start (EV_DEFAULT_ &self->read_watcher);
// XXX: we should do everything non-blocking and include establishing
// the TCP connection in the timeout, but that requires a rewrite.
// As it is, this isn't really too useful.
ev_timer_init (&self->timeout_watcher,
backend_ws_on_connection_timeout, 30, 0);
self->timeout_watcher.data = self;
// Run an event loop to process the handshake
ev_timer_start (EV_DEFAULT_ &self->timeout_watcher);
self->waiting_for_event = true;
ev_run (EV_DEFAULT_ 0);
self->waiting_for_event = false;
ev_timer_stop (EV_DEFAULT_ &self->timeout_watcher);
if (self->e)
{
error_propagate (e, self->e);
self->e = NULL;
}
else
result = true;
fail_2:
if (!result && self->server_fd != -1)
{
xclose (self->server_fd);
self->server_fd = -1;
}
fail_1:
free (url_host);
free (url_port);
str_free (&url_path);
return result;
}
static bool
backend_ws_make_call (struct backend *backend,
const char *request, bool expect_content, struct str *buf, struct error **e)
{
struct ws_context *self = (struct ws_context *) backend;
if (self->server_fd == -1)
if (!backend_ws_connect (self, e))
return false;
while (true)
{
if (backend_ws_send_message (self,
WS_OPCODE_TEXT, request, strlen (request)))
break;
print_status ("connection failed, reconnecting");
if (!backend_ws_connect (self, e))
return false;
}
if (expect_content)
{
// Run an event loop to retrieve the response
self->response_buffer = buf;
self->waiting_for_event = true;
ev_run (EV_DEFAULT_ 0);
self->waiting_for_event = false;
self->response_buffer = NULL;
if (self->e)
{
error_propagate (e, self->e);
self->e = NULL;
return false;
}
}
return true;
}
static void
backend_ws_on_quit (struct backend *backend)
{
struct ws_context *self = (struct ws_context *) backend;
if (self->waiting_for_event && !self->e)
error_set (&self->e, "aborted by user");
// We also have to be careful not to change the ev_break status
}
static void
backend_ws_destroy (struct backend *backend)
{
struct ws_context *self = (struct ws_context *) backend;
// TODO: maybe attempt a graceful shutdown, but for that there should
// probably be another backend method that runs an event loop
if (self->server_fd != -1)
backend_ws_close_connection (self);
free (self->endpoint);
strv_free (&self->extra_headers);
if (self->e)
error_free (self->e);
ev_timer_stop (EV_DEFAULT_ &self->timeout_watcher);
if (self->ssl_ctx)
SSL_CTX_free (self->ssl_ctx);
free (self->key);
str_free (&self->field);
str_free (&self->value);
str_map_free (&self->headers);
ws_parser_free (&self->parser);
str_free (&self->message_data);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct backend_vtable backend_ws_vtable =
{
.add_header = backend_ws_add_header,
.make_call = backend_ws_make_call,
.on_quit = backend_ws_on_quit,
.destroy = backend_ws_destroy,
};
static struct backend *
backend_ws_new (struct app_context *ctx,
const char *endpoint, struct http_parser_url *url)
{
struct ws_context *self = xcalloc (1, sizeof *self);
self->super.vtable = &backend_ws_vtable;
self->ctx = ctx;
ev_timer_init (&self->timeout_watcher, NULL, 0, 0);
self->server_fd = -1;
ev_io_init (&self->read_watcher, NULL, 0, 0);
http_parser_init (&self->hp, HTTP_RESPONSE);
self->field = str_make ();
self->value = str_make ();
self->headers = str_map_make (free);
self->headers.key_xfrm = tolower_ascii_strxfrm;
self->parser = ws_parser_make ();
self->message_data = str_make ();
self->extra_headers = strv_make ();
self->endpoint = xstrdup (endpoint);
self->url = *url;
#if OPENSSL_VERSION_NUMBER < 0x10100000L || LIBRESSL_VERSION_NUMBER
SSL_library_init ();
atexit (EVP_cleanup);
SSL_load_error_strings ();
atexit (ERR_free_strings);
#else
// Cleanup is done automatically via atexit()
OPENSSL_init_ssl (0, NULL);
#endif
return &self->super;
}
// --- cURL backend ------------------------------------------------------------
struct curl_context
{
struct backend super; ///< Parent class
struct app_context *ctx; ///< Application context
CURL *curl; ///< cURL handle
char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
struct curl_slist *headers; ///< Headers
};
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;
}
static bool
validate_json_rpc_content_type (const char *content_type)
{
char *type = NULL;
char *subtype = NULL;
struct str_map parameters = str_map_make (free);
parameters.key_xfrm = tolower_ascii_strxfrm;
bool result = http_parse_media_type
(content_type, &type, &subtype, &parameters);
if (!result)
goto end;
if (strcasecmp_ascii (type, "application")
|| (strcasecmp_ascii (subtype, "json") &&
strcasecmp_ascii (subtype, "json-rpc" /* obsolete */)))
result = false;
const char *charset = str_map_find (&parameters, "charset");
if (charset && strcasecmp_ascii (charset, "UTF-8"))
result = false;
// Currently ignoring all unknown parametrs
end:
free (type);
free (subtype);
str_map_free (&parameters);
return result;
}
static void
backend_curl_add_header (struct backend *backend, const char *header)
{
struct curl_context *self = (struct curl_context *) backend;
self->headers = curl_slist_append (self->headers, header);
if (curl_easy_setopt (self->curl, CURLOPT_HTTPHEADER, self->headers))
exit_fatal ("cURL setup failed");
}
static bool
backend_curl_make_call (struct backend *backend,
const char *request, bool expect_content, struct str *buf, struct error **e)
{
struct curl_context *self = (struct curl_context *) backend;
if (curl_easy_setopt (self->curl, CURLOPT_POSTFIELDS, request)
|| curl_easy_setopt (self->curl, CURLOPT_POSTFIELDSIZE_LARGE,
(curl_off_t) -1)
|| curl_easy_setopt (self->curl, CURLOPT_WRITEDATA, buf)
|| curl_easy_setopt (self->curl, CURLOPT_WRITEFUNCTION, write_callback))
FAIL ("cURL setup failed");
CURLcode ret;
if ((ret = curl_easy_perform (self->curl)))
FAIL ("HTTP request failed: %s", self->curl_error);
long code;
char *type;
if (curl_easy_getinfo (self->curl, CURLINFO_RESPONSE_CODE, &code)
|| curl_easy_getinfo (self->curl, CURLINFO_CONTENT_TYPE, &type))
FAIL ("cURL info retrieval failed");
if (code != 200)
FAIL ("unexpected HTTP response code: %ld", code);
if (!expect_content)
; // Let there be anything
else if (!type)
print_warning ("missing `Content-Type' header");
else if (!validate_json_rpc_content_type (type))
print_warning ("unexpected `Content-Type' header: %s", type);
return true;
}
static void
backend_curl_destroy (struct backend *backend)
{
struct curl_context *self = (struct curl_context *) backend;
curl_slist_free_all (self->headers);
curl_easy_cleanup (self->curl);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static struct backend_vtable backend_curl_vtable =
{
.add_header = backend_curl_add_header,
.make_call = backend_curl_make_call,
.destroy = backend_curl_destroy,
};
static struct backend *
backend_curl_new (struct app_context *ctx, const char *endpoint)
{
struct curl_context *self = xcalloc (1, sizeof *self);
self->super.vtable = &backend_curl_vtable;
self->ctx = ctx;
CURL *curl;
if (!(self->curl = curl = curl_easy_init ()))
exit_fatal ("cURL initialization failed");
self->headers = NULL;
self->headers = curl_slist_append
(self->headers, "Content-Type: application/json");
if (curl_easy_setopt (curl, CURLOPT_POST, 1L)
|| curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1L)
|| curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, self->curl_error)
|| curl_easy_setopt (curl, CURLOPT_HTTPHEADER, self->headers)
|| curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER,
self->ctx->trust_all ? 0L : 1L)
|| curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST,
self->ctx->trust_all ? 0L : 2L)
|| curl_easy_setopt (curl, CURLOPT_URL, endpoint))
exit_fatal ("cURL setup failed");
if (!self->ctx->trust_all)
{
// TODO: try to resolve filenames relative to configuration directories
const char *ca_file = get_config_string
(self->ctx->config.root, "connection.tls_ca_file");
const char *ca_path = get_config_string
(self->ctx->config.root, "connection.tls_ca_path");
if ((ca_file && curl_easy_setopt (curl, CURLOPT_CAINFO, ca_file))
|| (ca_path && curl_easy_setopt (curl, CURLOPT_CAPATH, ca_path)))
exit_fatal ("cURL setup failed");
}
return &self->super;
}
// --- JSON tokenizer ----------------------------------------------------------
// A dumb JSON tokenizer intended strictly just for syntax highlighting
//
// TODO: return also escape squences as a special token class (-> state)
enum jtoken
{
JTOKEN_EOF, ///< End of input
JTOKEN_ERROR, ///< EOF or error
JTOKEN_WHITESPACE, ///< Whitespace
JTOKEN_LBRACKET, ///< Left bracket
JTOKEN_RBRACKET, ///< Right bracket
JTOKEN_LBRACE, ///< Left curly bracket
JTOKEN_RBRACE, ///< Right curly bracket
JTOKEN_COLON, ///< Colon
JTOKEN_COMMA, ///< Comma
JTOKEN_NULL, ///< null
JTOKEN_BOOLEAN, ///< true, false
JTOKEN_NUMBER, ///< Number
JTOKEN_STRING ///< String
};
struct jtokenizer
{
const char *p; ///< Current position in input
size_t len; ///< How many bytes of input are left
struct str chunk; ///< Parsed chunk
};
static void
jtokenizer_init (struct jtokenizer *self, const char *p, size_t len)
{
self->p = p;
self->len = len;
self->chunk = str_make ();
}
static void
jtokenizer_free (struct jtokenizer *self)
{
str_free (&self->chunk);
}
static void
jtokenizer_advance (struct jtokenizer *self, size_t n)
{
str_append_data (&self->chunk, self->p, n);
self->p += n;
self->len -= n;
}
static int
jtokenizer_accept (struct jtokenizer *self, const char *chars)
{
if (!self->len || !strchr (chars, *self->p))
return false;
jtokenizer_advance (self, 1);
return true;
}
static bool
jtokenizer_ws (struct jtokenizer *self)
{
size_t len = 0;
while (jtokenizer_accept (self, "\t\r\n "))
len++;
return len != 0;
}
static bool
jtokenizer_word (struct jtokenizer *self, const char *word)
{
size_t len = strlen (word);
if (self->len < len || memcmp (self->p, word, len))
return false;
jtokenizer_advance (self, len);
return true;
}
static bool
jtokenizer_escape_sequence (struct jtokenizer *self)
{
if (!self->len)
return false;
if (jtokenizer_accept (self, "u"))
{
for (int i = 0; i < 4; i++)
if (!jtokenizer_accept (self, "0123456789abcdefABCDEF"))
return false;
return true;
}
return jtokenizer_accept (self, "\"\\/bfnrt");
}
static bool
jtokenizer_string (struct jtokenizer *self)
{
while (self->len)
{
unsigned char c = *self->p;
jtokenizer_advance (self, 1);
if (c == '"')
return true;
if (c == '\\' && !jtokenizer_escape_sequence (self))
return false;
}
return false;
}
static bool
jtokenizer_integer (struct jtokenizer *self)
{
size_t len = 0;
while (jtokenizer_accept (self, "0123456789"))
len++;
return len != 0;
}
static bool
jtokenizer_number (struct jtokenizer *self)
{
(void) jtokenizer_accept (self, "-");
if (!self->len)
return false;
if (!jtokenizer_accept (self, "0")
&& !jtokenizer_integer (self))
return false;
if (jtokenizer_accept (self, ".")
&& !jtokenizer_integer (self))
return false;
if (jtokenizer_accept (self, "eE"))
{
(void) jtokenizer_accept (self, "+-");
if (!jtokenizer_integer (self))
return false;
}
return true;
}
static enum jtoken
jtokenizer_next (struct jtokenizer *self)
{
str_reset (&self->chunk);
if (!self->len) return JTOKEN_EOF;
if (jtokenizer_ws (self)) return JTOKEN_WHITESPACE;
if (jtokenizer_accept (self, "[")) return JTOKEN_LBRACKET;
if (jtokenizer_accept (self, "]")) return JTOKEN_RBRACKET;
if (jtokenizer_accept (self, "{")) return JTOKEN_LBRACE;
if (jtokenizer_accept (self, "}")) return JTOKEN_RBRACE;
if (jtokenizer_accept (self, ":")) return JTOKEN_COLON;
if (jtokenizer_accept (self, ",")) return JTOKEN_COMMA;
if (jtokenizer_word (self, "null")) return JTOKEN_NULL;
if (jtokenizer_word (self, "true")
|| jtokenizer_word (self, "false")) return JTOKEN_BOOLEAN;
if (jtokenizer_accept (self, "\""))
{
if (jtokenizer_string (self)) return JTOKEN_STRING;
}
else if (jtokenizer_number (self)) return JTOKEN_NUMBER;
jtokenizer_advance (self, self->len);
return JTOKEN_ERROR;
}
// --- JSON highlighter --------------------------------------------------------
// Currently errors in parsing only mean that the rest doesn't get highlighted
struct json_highlight
{
struct app_context *ctx; ///< Application context
struct jtokenizer tokenizer; ///< Tokenizer
FILE *output; ///< Output handle
};
static void
json_highlight_print (struct json_highlight *self, int attr)
{
print_attributed (self->ctx, self->output, attr,
"%s", self->tokenizer.chunk.str);
}
static void json_highlight_value
(struct json_highlight *self, enum jtoken token);
static void
json_highlight_object (struct json_highlight *self)
{
// Distinguishing field names from regular string values in objects
bool in_field_name = true;
enum jtoken token;
while ((token = jtokenizer_next (&self->tokenizer)))
switch (token)
{
case JTOKEN_COLON:
in_field_name = false;
json_highlight_value (self, token);
break;
case JTOKEN_COMMA:
in_field_name = true;
json_highlight_value (self, token);
break;
case JTOKEN_STRING:
if (in_field_name)
json_highlight_print (self, ATTR_JSON_FIELD);
else
json_highlight_print (self, ATTR_JSON_STRING);
break;
case JTOKEN_RBRACE:
json_highlight_value (self, token);
return;
default:
json_highlight_value (self, token);
}
}
static void
json_highlight_array (struct json_highlight *self)
{
enum jtoken token;
while ((token = jtokenizer_next (&self->tokenizer)))
switch (token)
{
case JTOKEN_RBRACKET:
json_highlight_value (self, token);
return;
default:
json_highlight_value (self, token);
}
}
static void
json_highlight_value (struct json_highlight *self, enum jtoken token)
{
switch (token)
{
case JTOKEN_LBRACE:
json_highlight_print (self, ATTR_INCOMING);
json_highlight_object (self);
break;
case JTOKEN_LBRACKET:
json_highlight_print (self, ATTR_INCOMING);
json_highlight_array (self);
break;
case JTOKEN_NULL:
json_highlight_print (self, ATTR_JSON_NULL);
break;
case JTOKEN_BOOLEAN:
json_highlight_print (self, ATTR_JSON_BOOL);
break;
case JTOKEN_NUMBER:
json_highlight_print (self, ATTR_JSON_NUMBER);
break;
case JTOKEN_STRING:
json_highlight_print (self, ATTR_JSON_STRING);
break;
default:
json_highlight_print (self, ATTR_INCOMING);
}
}
static void
json_highlight (struct app_context *ctx, const char *s, FILE *output)
{
struct json_highlight self = { .ctx = ctx, .output = output };
jtokenizer_init (&self.tokenizer, s, strlen (s));
// There should be at maximum one value in the input however,
// but let's just keep on going and process it all
enum jtoken token;
while ((token = jtokenizer_next (&self.tokenizer)))
json_highlight_value (&self, token);
fflush (output);
jtokenizer_free (&self.tokenizer);
}
// --- Main program ------------------------------------------------------------
static void
quit (struct app_context *ctx)
{
if (ctx->backend->vtable->on_quit)
ctx->backend->vtable->on_quit (ctx->backend);
ev_break (EV_DEFAULT_ EVBREAK_ALL);
ctx->input->vtable->hide (ctx->input);
}
static void
suspend_terminal (struct app_context *ctx)
{
ctx->input->vtable->hide (ctx->input);
ev_io_stop (EV_DEFAULT_ &ctx->tty_watcher);
ctx->input->vtable->prepare (ctx->input, false);
}
static void
resume_terminal (struct app_context *ctx)
{
ctx->input->vtable->prepare (ctx->input, true);
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
ctx->input->vtable->show (ctx->input);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#define PARSE_FAIL(...) \
BLOCK_START \
print_error (__VA_ARGS__); \
goto fail; \
BLOCK_END
// XXX: should probably rather defer this action and use spawn_helper_child()
static void
display_via_pipeline (struct app_context *ctx,
const char *s, const char *pipeline)
{
suspend_terminal (ctx);
errno = 0;
FILE *fp = popen (pipeline, "w");
if (fp)
{
fputs (s, fp);
pclose (fp);
}
if (errno)
print_error ("pipeline failed: %s", strerror (errno));
resume_terminal (ctx);
}
static bool
process_response (struct app_context *ctx, const json_t *id, struct str *buf,
const char *pipeline)
{
if (!id)
{
printf ("[Notification]\n");
if (!buf->len)
return true;
print_warning ("we have been sent data back for a notification");
return false;
}
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 *returned_id = json_object_get (response, "id");
json_t *result = json_object_get (response, "result");
json_t *error = json_object_get (response, "error");
json_t *data = NULL;
if (!returned_id)
print_warning ("`%s' field not present in response", "id");
if (!json_equal (id, returned_id))
print_warning ("mismatching `%s' field in response", "id");
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 if (pipeline)
display_via_pipeline (ctx, s, pipeline);
else
{
json_highlight (ctx, s, stdout);
fputc ('\n', stdout);
}
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 void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params, const char *pipeline)
{
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", req_term);
fputs ("\n", stdout);
}
free (req_term);
}
struct str buf = str_make ();
struct error *e = NULL;
if (!ctx->backend->vtable->make_call
(ctx->backend, req_utf8, id != NULL, &buf, &e))
{
print_error ("%s", e->message);
error_free (e);
goto fail;
}
if (ctx->verbose)
{
char *buf_term =
iconv_xstrdup (ctx->term_from_utf8, buf.str, buf.len, NULL);
if (!buf_term)
print_error ("%s: %s", "verbose", "character conversion failed");
else
{
print_attributed (ctx, stdout, ATTR_INCOMING, "%s", buf_term);
fputs ("\n", stdout);
}
free (buf_term);
}
if (!process_response (ctx, id, &buf, pipeline))
{
char *s = iconv_xstrdup (ctx->term_from_utf8,
buf.str, buf.len + 1 /* null byte */, NULL);
if (!s)
print_error ("character conversion failed for `%s'",
"raw response data");
else if (!ctx->verbose /* already printed */)
printf ("%s: %s\n", "raw response data", s);
free (s);
}
fail:
str_free (&buf);
free (req_utf8);
json_decref (request);
}
static void
process_input (char *user_input, void *user_data)
{
struct app_context *ctx = user_data;
if (!user_input)
{
quit (ctx);
return;
}
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;
char *pipeline = NULL;
while (true)
{
// Jansson is too stupid to just tell us that there was nothing;
// still genius compared to the clusterfuck of json-c
while (*p && isspace_ascii (*p))
p++;
if (!*p)
break;
if (*p == '|')
{
pipeline = xstrdup (++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 = &params;
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, pipeline);
fail_parse:
free (pipeline);
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);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// The ability to use an external editor on the input line has been shamelessly
// copypasted from degesch with minor changes only.
/// This differs from the non-unique version in that we expect the filename
/// to be something like a pattern for mkstemp(), so the resulting path can
/// reside in a system-wide directory with no risk of a conflict.
static char *
resolve_relative_runtime_unique_filename (const char *filename)
{
struct str path = str_make ();
const char *runtime_dir = getenv ("XDG_RUNTIME_DIR");
const char *tmpdir = getenv ("TMPDIR");
if (runtime_dir && *runtime_dir == '/')
str_append (&path, runtime_dir);
else if (tmpdir && *tmpdir == '/')
str_append (&path, tmpdir);
else
str_append (&path, "/tmp");
str_append_printf (&path, "/%s/%s", PROGRAM_NAME, filename);
// Try to create the file's ancestors;
// typically the user will want to immediately create a file in there
const char *last_slash = strrchr (path.str, '/');
if (last_slash && last_slash != path.str)
{
char *copy = xstrndup (path.str, last_slash - path.str);
(void) mkdir_with_parents (copy, NULL);
free (copy);
}
return str_steal (&path);
}
static bool
xwrite (int fd, const char *data, size_t len, struct error **e)
{
size_t written = 0;
while (written < len)
{
ssize_t res = write (fd, data + written, len - written);
if (res >= 0)
written += res;
else if (errno != EINTR)
FAIL ("%s", strerror (errno));
}
return true;
}
static bool
dump_line_to_file (const char *line, char *template, struct error **e)
{
int fd = mkstemp (template);
if (fd < 0)
FAIL ("%s", strerror (errno));
bool success = xwrite (fd, line, strlen (line), e);
if (!success)
(void) unlink (template);
xclose (fd);
return success;
}
static char *
try_dump_line_to_file (const char *line)
{
char *template = resolve_filename
("input.XXXXXX", resolve_relative_runtime_unique_filename);
struct error *e = NULL;
if (dump_line_to_file (line, template, &e))
return template;
print_error ("%s: %s",
"failed to create a temporary file for editing", e->message);
error_free (e);
free (template);
return NULL;
}
static pid_t
spawn_helper_child (struct app_context *ctx)
{
suspend_terminal (ctx);
pid_t child = fork ();
switch (child)
{
case -1:
{
int saved_errno = errno;
resume_terminal (ctx);
errno = saved_errno;
break;
}
case 0:
// Put the child in a new foreground process group
hard_assert (setpgid (0, 0) != -1);
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
break;
default:
// Make sure of it in the parent as well before continuing
(void) setpgid (child, child);
}
return child;
}
static void
run_editor (const char *line, void *user_data)
{
struct app_context *ctx = user_data;
hard_assert (!ctx->editor_filename);
char *filename;
if (!(filename = try_dump_line_to_file (line)))
return;
const char *command;
if (!(command = getenv ("VISUAL"))
&& !(command = getenv ("EDITOR")))
command = "vi";
switch (spawn_helper_child (ctx))
{
case 0:
execlp (command, command, filename, NULL);
print_error ("%s: %s", "failed to launch editor", strerror (errno));
_exit (EXIT_FAILURE);
case -1:
print_error ("%s: %s", "failed to launch editor", strerror (errno));
free (filename);
break;
default:
ctx->editor_filename = filename;
}
}
static void
process_edited_input (struct app_context *ctx)
{
struct str input = str_make ();
struct error *e = NULL;
if (!read_file (ctx->editor_filename, &input, &e))
{
print_error ("%s: %s", "input editing failed", e->message);
error_free (e);
}
else
{
// Strip trailing newlines, added automatically by editors
while (input.len && strchr ("\r\n", input.str[input.len - 1]))
input.str[--input.len] = 0;
if (!ctx->input->vtable->replace_line (ctx->input, input.str))
print_error ("%s: %s", "input editing failed",
"could not re-insert modified text");
}
if (unlink (ctx->editor_filename))
print_error ("could not unlink `%s': %s",
ctx->editor_filename, strerror (errno));
str_free (&input);
}
static void
on_child (EV_P_ ev_child *handle, int revents)
{
(void) revents;
struct app_context *ctx = ev_userdata (loop);
// I am not a shell, stopping not allowed
int status = handle->rstatus;
if (WIFSTOPPED (status)
|| WIFCONTINUED (status))
{
kill (-handle->rpid, SIGKILL);
return;
}
// I don't recognize this child (we should also check its PID)
if (!ctx->editor_filename)
return;
hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
resume_terminal (ctx);
if (WIFSIGNALED (status))
print_error ("editor died from signal %d", WTERMSIG (status));
else if (WIFEXITED (status) && WEXITSTATUS (status) != 0)
print_error ("editor returned status %d", WEXITSTATUS (status));
else
process_edited_input (ctx);
free (ctx->editor_filename);
ctx->editor_filename = NULL;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
on_winch (EV_P_ ev_signal *handle, int revents)
{
(void) handle;
(void) revents;
struct app_context *ctx = ev_userdata (loop);
ctx->input->vtable->on_terminal_resized (ctx->input);
}
static void
on_terminated (EV_P_ ev_signal *handle, int revents)
{
(void) handle;
(void) revents;
struct app_context *ctx = ev_userdata (loop);
quit (ctx);
}
static void
on_tty_readable (EV_P_ ev_io *handle, int revents)
{
(void) handle;
struct app_context *ctx = ev_userdata (loop);
if (revents & EV_READ)
{
// rl_callback_read_char() is not reentrant, may happen on EOF
ev_io_stop (EV_DEFAULT_ &ctx->tty_watcher);
ctx->input->vtable->on_tty_readable (ctx->input);
// Don't make ourselves receive a SIGTTIN. Ideally we'd prevent
// reentrancy without inciting conflicts with
// {suspend,resume}_terminal() but I can't figure anything out.
if (!ctx->editor_filename)
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
}
}
static void
init_watchers (struct app_context *ctx)
{
if (!EV_DEFAULT)
exit_fatal ("libev initialization failed");
// So that if the remote end closes the connection, attempts to write to
// the socket don't terminate the program
(void) signal (SIGPIPE, SIG_IGN);
// So that we can write to the terminal while we're running a backlog
// helper. This is also inherited by the child so that it doesn't stop
// when it calls tcsetpgrp().
(void) signal (SIGTTOU, SIG_IGN);
ev_child_init (&ctx->child_watcher, on_child, 0, true);
ev_child_start (EV_DEFAULT_ &ctx->child_watcher);
ev_signal_init (&ctx->winch_watcher, on_winch, SIGWINCH);
ev_signal_start (EV_DEFAULT_ &ctx->winch_watcher);
ev_signal_init (&ctx->term_watcher, on_terminated, SIGTERM);
ev_signal_start (EV_DEFAULT_ &ctx->term_watcher);
ev_signal_init (&ctx->int_watcher, on_terminated, SIGINT);
ev_signal_start (EV_DEFAULT_ &ctx->int_watcher);
ev_io_init (&ctx->tty_watcher, on_tty_readable, STDIN_FILENO, EV_READ);
ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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 message and exit" },
{ 'V', "version", NULL, 0, "output version information and exit" },
// TODO: consider making this the default and instead adding
// an option to accept JSON null as an id.
{ 'a', "auto-id", NULL, 0, "automatic `id' fields" },
{ 'o', "origin", "O", 0, "set the HTTP Origin header" },
// TODO: consider inverting this to -c/--compact-output
{ '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 raw requests and responses" },
{ '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_make (argc, argv, opts,
"ENDPOINT", "A simple JSON-RPC 2.0 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':
save_configuration (ctx->config.root, optarg);
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[])
{
g_ctx.config = config_make ();
register_config_modules (&g_ctx);
config_load (&g_ctx.config, config_item_object ());
char *origin = NULL;
char *endpoint = NULL;
parse_program_arguments (&g_ctx, argc, argv, &origin, &endpoint);
g_ctx.input = input_new ();
g_ctx.input->user_data = &g_ctx;
g_ctx.input->on_input = process_input;
g_ctx.input->on_run_editor = run_editor;
init_colors (&g_ctx);
load_configuration (&g_ctx);
struct http_parser_url url;
if (http_parser_parse_url (endpoint, strlen (endpoint), false, &url))
exit_fatal ("invalid endpoint address");
if (!(url.field_set & (1 << UF_SCHEMA)))
exit_fatal ("invalid endpoint address, must contain the schema");
char *url_schema = xstrndup (endpoint +
url.field_data[UF_SCHEMA].off,
url.field_data[UF_SCHEMA].len);
// TODO: try to avoid the need to pass application context to backends
if (!strcasecmp_ascii (url_schema, "http")
|| !strcasecmp_ascii (url_schema, "https"))
g_ctx.backend = backend_curl_new (&g_ctx, endpoint);
else if (!strcasecmp_ascii (url_schema, "ws")
|| !strcasecmp_ascii (url_schema, "wss"))
g_ctx.backend = backend_ws_new (&g_ctx, endpoint, &url);
else
exit_fatal ("unsupported protocol");
free (url_schema);
if (origin)
{
origin = xstrdup_printf ("Origin: %s", origin);
g_ctx.backend->vtable->add_header (g_ctx.backend, origin);
}
free (origin);
// 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);
}
char *history_path =
xstrdup_printf ("%s/" PROGRAM_NAME "/history", data_home);
(void) g_ctx.input->vtable->load_history (g_ctx.input, history_path, NULL);
if (!get_attribute_printer (stdout))
g_ctx.input->vtable->set_prompt (g_ctx.input,
xstrdup_printf ("json-rpc> "));
else
{
// XXX: to be completely correct, we should use tputs, but we cannot
g_ctx.input->vtable->set_prompt (g_ctx.input,
xstrdup_printf ("%c%s%cjson-rpc>%c%s%c ",
INPUT_START_IGNORE, g_ctx.attrs[ATTR_PROMPT],
INPUT_END_IGNORE,
INPUT_START_IGNORE, g_ctx.attrs[ATTR_RESET],
INPUT_END_IGNORE));
}
init_watchers (&g_ctx);
g_ctx.input->vtable->start (g_ctx.input, PROGRAM_NAME);
ev_set_userdata (EV_DEFAULT_ &g_ctx);
ev_run (EV_DEFAULT_ 0);
// User has terminated the program, let's save the history and clean up
struct error *e = NULL;
char *dir = xstrdup (history_path);
if (!mkdir_with_parents (dirname (dir), &e)
|| !g_ctx.input->vtable->save_history (g_ctx.input, history_path, &e))
{
print_error ("writing the history file `%s' failed: %s",
history_path, e->message);
error_free (e);
}
free (dir);
free (history_path);
g_ctx.backend->vtable->destroy (g_ctx.backend);
g_ctx.input->vtable->destroy (g_ctx.input);
iconv_close (g_ctx.term_from_utf8);
iconv_close (g_ctx.term_to_utf8);
config_free (&g_ctx.config);
free_terminal ();
ev_loop_destroy (EV_DEFAULT);
return EXIT_SUCCESS;
}