diff options
| -rw-r--r-- | .gitignore | 8 | ||||
| -rw-r--r-- | CMakeLists.txt | 73 | ||||
| -rw-r--r-- | LICENSE | 2 | ||||
| -rw-r--r-- | README.adoc | 82 | ||||
| -rw-r--r-- | config.h.in | 3 | ||||
| m--------- | http-parser | 0 | ||||
| -rwxr-xr-x | json-format.pl | 154 | ||||
| -rw-r--r-- | json-rpc-shell.c | 3493 | ||||
| -rw-r--r-- | json-rpc-test-server.c | 3 | 
9 files changed, 3767 insertions, 51 deletions
@@ -3,7 +3,7 @@  # Qt Creator files  /CMakeLists.txt.user* -/acid.config -/acid.files -/acid.creator* -/acid.includes +/json-rpc-shell.config +/json-rpc-shell.files +/json-rpc-shell.creator* +/json-rpc-shell.includes diff --git a/CMakeLists.txt b/CMakeLists.txt index f6d9b00..5b1bd3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,15 @@ -project (acid C) +project (json-rpc-shell C)  cmake_minimum_required (VERSION 2.8.5) +# Options +option (WANT_READLINE "Use GNU Readline for the UI (better)" ON) +option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF) +  # Moar warnings  if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)  	# -Wunused-function is pretty annoying here, as everything is static -	set (CMAKE_C_FLAGS "-std=c99 -Wall -Wextra -Wno-unused-function") +	set (CMAKE_C_FLAGS +		"${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-function")  endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)  # Version @@ -20,29 +25,65 @@ set (project_VERSION "${project_VERSION}.${project_VERSION_PATCH}")  set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)  # Dependencies +find_package (Curses)  find_package (PkgConfig REQUIRED) -pkg_check_modules (dependencies REQUIRED jansson) +pkg_check_modules (dependencies REQUIRED libcurl jansson) +# Note that cURL can link to a different version of libssl than we do, +# in which case the results are undefined  pkg_check_modules (libssl REQUIRED libssl libcrypto)  find_package (LibEV REQUIRED) -find_package (LibMagic REQUIRED) +pkg_check_modules (ncursesw ncursesw) + +set (project_libraries ${dependencies_LIBRARIES} +	${libssl_LIBRARIES} ${LIBEV_LIBRARIES}) +include_directories (${dependencies_INCLUDE_DIRS} +	${libssl_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS}) + +if (ncursesw_FOUND) +	list (APPEND project_libraries ${ncursesw_LIBRARIES}) +	include_directories (${ncursesw_INCLUDE_DIRS}) +elseif (CURSES_FOUND) +	list (APPEND project_libraries ${CURSES_LIBRARY}) +	include_directories (${CURSES_INCLUDE_DIR}) +else (CURSES_FOUND) +	message (SEND_ERROR "Curses not found") +endif (ncursesw_FOUND) -set (project_libraries ${dependencies_LIBRARIES} ${libssl_LIBRARIES} -	${LIBEV_LIBRARIES} ${LIBMAGIC_LIBRARIES}) -include_directories (${dependencies_INCLUDE_DIRS} ${libssl_INCLUDE_DIRS} -	${LIBEV_INCLUDE_DIRS} ${LIBMAGIC_INCLUDE_DIRS}) +if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT)) +	message (SEND_ERROR "You have to choose either GNU Readline or libedit") +elseif (WANT_READLINE) +	list (APPEND project_libraries readline) +elseif (WANT_LIBEDIT) +	pkg_check_modules (libedit REQUIRED libedit) +	list (APPEND project_libraries ${libedit_LIBRARIES}) +	include_directories (${libedit_INCLUDE_DIRS}) +endif ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))  # Generate a configuration file +set (HAVE_READLINE "${WANT_READLINE}") +set (HAVE_EDITLINE "${WANT_LIBEDIT}") +  configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h)  include_directories (${PROJECT_BINARY_DIR}) -# Build the executables -add_executable (json-rpc-test-server -	json-rpc-test-server.c http-parser/http_parser.c) -target_link_libraries (json-rpc-test-server ${project_libraries}) +# Build the main executable and link it +add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c http-parser/http_parser.c) +target_link_libraries (${PROJECT_NAME} ${project_libraries}) + +# Development tools +find_package (LibMagic) +if (LIBMAGIC_FOUND) +	include_directories (${LIBMAGIC_INCLUDE_DIRS}) +	add_executable (json-rpc-test-server +		json-rpc-test-server.c http-parser/http_parser.c) +	target_link_libraries (json-rpc-test-server +		${project_libraries} ${LIBMAGIC_LIBRARIES}) +endif ()  # The files to be installed  include (GNUInstallDirs) -install (TARGETS DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) +install (PROGRAMS json-format.pl DESTINATION ${CMAKE_INSTALL_BINDIR})  install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})  # Generate documentation from program help @@ -51,7 +92,7 @@ if (NOT HELP2MAN_EXECUTABLE)  	message (FATAL_ERROR "help2man not found")  endif (NOT HELP2MAN_EXECUTABLE) -foreach (page) +foreach (page ${PROJECT_NAME})  	set (page_output "${PROJECT_BINARY_DIR}/${page}.1")  	list (APPEND project_MAN_PAGES "${page_output}")  	add_custom_command (OUTPUT ${page_output} @@ -64,13 +105,13 @@ endforeach (page)  add_custom_target (docs ALL DEPENDS ${project_MAN_PAGES})  foreach (page ${project_MAN_PAGES}) -	string (REGEX MATCH "\\.([0-9])" manpage_suffix "${page}") +	string (REGEX MATCH "\\.([0-9])$" manpage_suffix "${page}")  	install (FILES "${page}"  		DESTINATION "${CMAKE_INSTALL_MANDIR}/man${CMAKE_MATCH_1}")  endforeach (page)  # CPack -set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "A Continuous Integration Daemon") +set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Shell for running JSON-RPC 2.0 queries")  set (CPACK_PACKAGE_VENDOR "Premysl Janouch")  set (CPACK_PACKAGE_CONTACT "Přemysl Janouch <p@janouch.name>")  set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") @@ -1,4 +1,4 @@ -Copyright (c) 2015 - 2018, Přemysl Janouch <p@janouch.name> +Copyright (c) 2014 - 2018, Přemysl Janouch <p@janouch.name>  Permission to use, copy, modify, and/or distribute this software for any  purpose with or without fee is hereby granted. diff --git a/README.adoc b/README.adoc index 80d708f..9c4fec4 100644 --- a/README.adoc +++ b/README.adoc @@ -1,37 +1,55 @@ -acid -==== +json-rpc-shell +============== +:compact-option: -'acid' is A Continuous Integration Daemon.  Currently under heavy development. -Right now I'm working on a demo JSON-RPC server that will serve as the basis for -the final daemon. +'json-rpc-shell' is a simple shell for running JSON-RPC 2.0 queries. -The aim of this project is to provide a dumbed-down alternative to Travis CI. -I find it way too complex to set up and run in a local setting, while the basic -gist of it is actually very simple -- run some stuff on new git commits. +This software has been created as a replacement for the following shell, which +is written in Java: http://software.dzhuvinov.com/json-rpc-2.0-shell.html -'acid' will provide a JSON-RPC 2.0 service for frontends over FastCGI, SCGI, or -WebSockets, as well as a webhook endpoint for notifications about new commits. -The daemon is supposed to be "firewalled" by a normal HTTP server and it will -not provide TLS support to secure the communications. +Features +-------- +In addition to most of the features provided by Vladimir Dzhuvinov's shell +you get the following niceties: -'acid' will be able to tell you about build results via e-mail and/or IRC. + - configurable JSON syntax highlight, which with prettyprinting turned on +   helps you make sense of the results significantly + - ability to pipe output through a shell command, so that you can view the +   results in your favourite editor or redirect them to a file + - ability to edit the input line in your favourite editor as well with Alt+E -Builds will only be supported on the same machine as the daemon.  Eventually I -might be able to add support for fully replicable builds using Docker. +Supported transports +-------------------- + - HTTP + - HTTPS + - WebSocket + - WebSocket over TLS -With this being my own project, of course it is written in event-looped C99 -where everything is stuffed into just a few files.  At least I hope it's written -in a somewhat clean manner.  Feel free to contribute. +WebSockets +~~~~~~~~~~ +The JSON-RPC 2.0 spec doesn't say almost anything about underlying transports. +The way it's implemented here is that every request is sent as a single text +message.  If it has an "id" field, i.e. it's not just a notification, the +client waits for a message from the server in response. -Building and Installing ------------------------ -Build dependencies: CMake, pkg-config, help2man, libmagic, +There's no support so far for any protocol extensions, nor for specifying +the higher-level protocol (the "Sec-Ws-Protocol" HTTP field). + +Packages +-------- +Regular releases are sporadic.  git master should be stable enough.  You can get +a package with the latest development version from Archlinux's AUR. + +Building and Usage +------------------ +Build dependencies: CMake, pkg-config, help2man,                      liberty (included), http-parser (included) + -Runtime dependencies: libev, Jansson +Runtime dependencies: libev, Jansson, cURL, openssl, +                      readline or libedit >= 2013-07-12, - $ git clone --recursive https://git.janouch.name/p/acid.git - $ mkdir acid/build - $ cd acid/build + $ git clone --recursive https://git.janouch.name/p/json-rpc-shell.git + $ mkdir json-rpc-shell/build + $ cd json-rpc-shell/build   $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug   $ make @@ -42,18 +60,22 @@ To install the application, you can do either the usual:  Or you can try telling CMake to make a package for you.  For Debian it is:   $ cpack -G DEB - # dpkg -i acid-*.deb + # dpkg -i json-rpc-shell-*.deb  Note that for versions of CMake before 2.8.9, you need to prefix `cpack` with  `fakeroot` or file ownership will end up wrong. -Usage ------ -TODO.  The main application hasn't been written yet. +Run the program with `--help` to obtain usage information. + +Test server +----------- +If you install development packages for libmagic, an included test server will +be built but not installed which provides a trivial JSON-RPC 2.0 service with +FastCGI, SCGI, and WebSocket interfaces.  It responds to the `ping` method.  Contributing and Support  ------------------------ -Use https://git.janouch.name/p/acid to report any bugs, request features, +Use https://git.janouch.name/p/json-rpc-shell to report bugs, request features,  or submit pull requests.  `git send-email` is tolerated.  If you want to discuss  the project, feel free to join me at ircs://irc.janouch.name, channel #dev. diff --git a/config.h.in b/config.h.in index 89cd306..b7b4101 100644 --- a/config.h.in +++ b/config.h.in @@ -4,5 +4,8 @@  #define PROGRAM_NAME "${PROJECT_NAME}"  #define PROGRAM_VERSION "${project_VERSION}" +#cmakedefine HAVE_READLINE +#cmakedefine HAVE_EDITLINE +  #endif  // ! CONFIG_H diff --git a/http-parser b/http-parser -Subproject 1b79abab34d4763c0467f1173a406ad2817c163 +Subproject 5d414fcb4b2ccc1ce9d6063292f9c63c9ec67b0 diff --git a/json-format.pl b/json-format.pl new file mode 100755 index 0000000..571e89e --- /dev/null +++ b/json-format.pl @@ -0,0 +1,154 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Term::ANSIColor; +use Getopt::Long; + +my $reset = color('reset'); +my %format = ( +  FIELD  => color('bold'), +  NULL   => color('cyan'), +  BOOL   => color('red'), +  NUMBER => color('magenta'), +  STRING => color('blue'), +  ERROR  => color('bold white on_red'), +); + +my ($color, $keep_ws, $help) = 'auto'; +if (!GetOptions('color=s' => \$color, 'keep-ws' => \$keep_ws, 'help' => \$help) +  || $help) { +  print STDERR +    "Usage: $0 [OPTION...] [FILE...]\n" . +    "Pretty-print and colorify JSON\n" . +    "\n" . +    "  --help         print this help\n" . +    "  --keep-ws      retain all original whitespace\n" . +    "  --color=COLOR  'always', 'never' or 'auto' (the default)\n"; +  exit 2; +} + +%format = () +  if $color eq 'never' || $color eq 'auto' && !-t STDOUT; + +# Hash lookup is the fastest way to qualify tokens, however it cannot be used +# for everything and we need to fall back to regular expressions +my %lookup = ( +  '[' => 'LBRACKET', '{' => 'LBRACE', +  ']' => 'RBRACKET', '}' => 'RBRACE', +  ':' => 'COLON', ',' => 'COMMA', +  'true' => 'BOOL', 'false' => 'BOOL', 'null' => 'NULL', +); +my @pats = ( +  ['"(?:[^\\\\"]*|\\\\(?:u[\da-f]{4}|["\\\\/bfnrt]))*"' => 'STRING'], +  ['-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?' => 'NUMBER'], +  ['[ \t\r\n]+' => 'WS'], +); +my @tokens = map {[qr/^$_->[0]$/s, $_->[1]]} @pats; + +# m//g is the fastest way to explode text into tokens in the first place +# and we need to construct an all-encompassing regular expression for it +my @all_pats = map {$_->[0]} @pats; +push @all_pats, quotemeta for keys %lookup; +my $any_token = qr/\G(${\join '|', @all_pats})/; + +# FIXME: this probably shouldn't be a global variable +my $indent = 0; + +sub nexttoken ($) { +  my $json = shift; +  if (!@$json) { +    return unless defined (my $line = <>); +    push @$json, $line =~ /$any_token/gsc; +    push @$json, substr $line, pos $line +      if pos $line != length $line; +  } + +  my $text = shift @$json; +  if (my $s = $lookup{$text}) { +    return $s, $text; +  } +  for my $s (@tokens) { +    return $s->[1], $text if $text =~ $s->[0]; +  } +  return 'ERROR', $text; +} + +sub gettoken ($) { +  my $json = shift; +  while (my ($token, $text) = nexttoken $json) { +    next if !$keep_ws && $token eq 'WS'; +    return $token, $text; +  } +  return; +} + +sub printindent () { +  print "\n", '  ' x $indent; +} + +sub do_value ($$$); +sub do_object ($) { +  my $json = shift; +  my $in_field_name = 1; +  my $first = 1; +  while (my ($token, $text) = gettoken $json) { +    if ($token eq 'COLON') { +      $in_field_name = 0; +    } elsif ($token eq 'COMMA') { +      $in_field_name = 1; +    } elsif ($token eq 'STRING') { +      $token = 'FIELD' if $in_field_name; +    } +    if ($token eq 'RBRACE') { +      $indent--; +      printindent unless $keep_ws; +    } elsif ($first) { +      printindent unless $keep_ws; +      $first = 0; +    } +    do_value $token, $text, $json; +    return if $token eq 'RBRACE'; +  } +} + +sub do_array ($) { +  my $json = shift; +  my $first = 1; +  while (my ($token, $text) = gettoken $json) { +    if ($token eq 'RBRACKET') { +      $indent--; +      printindent unless $keep_ws; +    } elsif ($first) { +      printindent unless $keep_ws; +      $first = 0; +    } +    do_value $token, $text, $json; +    return if $token eq 'RBRACKET'; +  } +} + +sub do_value ($$$) { +  my ($token, $text, $json) = @_; +  if (my $format = $format{$token}) { +    print $format, $text, $reset; +  } else { +    print $text; +  } +  if ($token eq 'LBRACE') { +    $indent++; +    do_object $json; +  } elsif ($token eq 'LBRACKET') { +    $indent++; +    do_array $json; +  } elsif ($token eq 'COMMA') { +    printindent unless $keep_ws; +  } elsif ($token eq 'COLON') { +    print ' ' unless $keep_ws; +  } +} + +my @buffer; +while (my ($token, $text) = gettoken \@buffer) { +  do_value $token, $text, \@buffer; +  print "\n" unless $keep_ws; +} diff --git a/json-rpc-shell.c b/json-rpc-shell.c new file mode 100644 index 0000000..47ef3b8 --- /dev/null +++ b/json-rpc-shell.c @@ -0,0 +1,3493 @@ +/* + * json-rpc-shell.c: simple JSON-RPC 2.0 shell + * + * Copyright (c) 2014 - 2016, Přemysl 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 +	(void) el_gets (self->editline, NULL); +} + +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) +{ +	// XXX: the ignore doesn't quite work, see https://gnats.netbsd.org/47539 +	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, true); +	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); + +	// 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 (self->ctx->verbose) +		{ +			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, ¶meters); +	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 (¶meters, "charset"); +	if (charset && strcasecmp_ascii (charset, "UTF-8")) +		result = false; + +	// Currently ignoring all unknown parametrs + +end: +	free (type); +	free (subtype); +	str_map_free (¶meters); +	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 +parse_response (struct app_context *ctx, struct str *buf, const char *pipeline) +{ +	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 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; +	} + +	bool success = false; +	if (id) +		success = parse_response (ctx, &buf, pipeline); +	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 byte */, 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 (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 = ¶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, 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 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)); + +	free (ctx->editor_filename); +	ctx->editor_filename = NULL; +	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 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); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +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); +		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 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_make (argc, argv, opts, +		"ENDPOINT", "Simple 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': +		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; +} diff --git a/json-rpc-test-server.c b/json-rpc-test-server.c index ac15f0a..ea38b7b 100644 --- a/json-rpc-test-server.c +++ b/json-rpc-test-server.c @@ -29,6 +29,9 @@  #define LIBERTY_WANT_PROTO_FASTCGI  #include "config.h" +#undef PROGRAM_NAME +#define PROGRAM_NAME "json-rpc-test-server" +  #include "liberty/liberty.c"  #include <langinfo.h>  | 
