aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--CMakeLists.txt73
-rw-r--r--LICENSE2
-rw-r--r--README.adoc96
-rw-r--r--config.h.in3
m---------http-parser0
-rwxr-xr-xjson-format.pl154
-rw-r--r--json-rpc-shell.c3493
-rw-r--r--json-rpc-test-server.c3
9 files changed, 3774 insertions, 58 deletions
diff --git a/.gitignore b/.gitignore
index 465d9dd..5979c20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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")
diff --git a/LICENSE b/LICENSE
index 01dc408..a069461 100644
--- a/LICENSE
+++ b/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
-====
-
-'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.
-
-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.
-
-'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.
-
-'acid' will be able to tell you about build results via e-mail and/or IRC.
-
-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.
-
-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.
-
-Building and Installing
------------------------
-Build dependencies: CMake, pkg-config, help2man, libmagic,
+json-rpc-shell
+==============
+:compact-option:
+
+'json-rpc-shell' is a simple shell for running JSON-RPC 2.0 queries.
+
+This software has been created as a replacement for the following shell, which
+is written in Java: http://software.dzhuvinov.com/json-rpc-2.0-shell.html
+
+Features
+--------
+In addition to most of the features provided by Vladimir Dzhuvinov's shell
+you get the following niceties:
+
+ - 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
+
+Supported transports
+--------------------
+ - HTTP
+ - HTTPS
+ - WebSocket
+ - WebSocket over TLS
+
+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.
+
+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, &parameters);
+ if (!result)
+ goto end;
+
+ if (strcasecmp_ascii (type, "application")
+ || (strcasecmp_ascii (subtype, "json") &&
+ strcasecmp_ascii (subtype, "json-rpc" /* obsolete */)))
+ result = false;
+
+ const char *charset = str_map_find (&parameters, "charset");
+ if (charset && strcasecmp_ascii (charset, "UTF-8"))
+ result = false;
+
+ // Currently ignoring all unknown parametrs
+
+end:
+ free (type);
+ free (subtype);
+ str_map_free (&parameters);
+ return result;
+}
+
+static void
+backend_curl_add_header (struct backend *backend, const char *header)
+{
+ struct curl_context *self = (struct curl_context *) backend;
+ self->headers = curl_slist_append (self->headers, header);
+ if (curl_easy_setopt (self->curl, CURLOPT_HTTPHEADER, self->headers))
+ exit_fatal ("cURL setup failed");
+}
+
+static bool
+backend_curl_make_call (struct backend *backend,
+ const char *request, bool expect_content, struct str *buf, struct error **e)
+{
+ struct curl_context *self = (struct curl_context *) backend;
+ if (curl_easy_setopt (self->curl, CURLOPT_POSTFIELDS, request)
+ || curl_easy_setopt (self->curl, CURLOPT_POSTFIELDSIZE_LARGE,
+ (curl_off_t) -1)
+ || curl_easy_setopt (self->curl, CURLOPT_WRITEDATA, buf)
+ || curl_easy_setopt (self->curl, CURLOPT_WRITEFUNCTION, write_callback))
+ FAIL ("cURL setup failed");
+
+ CURLcode ret;
+ if ((ret = curl_easy_perform (self->curl)))
+ FAIL ("HTTP request failed: %s", self->curl_error);
+
+ long code;
+ char *type;
+ if (curl_easy_getinfo (self->curl, CURLINFO_RESPONSE_CODE, &code)
+ || curl_easy_getinfo (self->curl, CURLINFO_CONTENT_TYPE, &type))
+ FAIL ("cURL info retrieval failed");
+
+ if (code != 200)
+ FAIL ("unexpected HTTP response code: %ld", code);
+
+ if (!expect_content)
+ ; // Let there be anything
+ else if (!type)
+ print_warning ("missing `Content-Type' header");
+ else if (!validate_json_rpc_content_type (type))
+ print_warning ("unexpected `Content-Type' header: %s", type);
+ return true;
+}
+
+static void
+backend_curl_destroy (struct backend *backend)
+{
+ struct curl_context *self = (struct curl_context *) backend;
+ curl_slist_free_all (self->headers);
+ curl_easy_cleanup (self->curl);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static struct backend_vtable backend_curl_vtable =
+{
+ .add_header = backend_curl_add_header,
+ .make_call = backend_curl_make_call,
+ .destroy = backend_curl_destroy,
+};
+
+static struct backend *
+backend_curl_new (struct app_context *ctx, const char *endpoint)
+{
+ struct curl_context *self = xcalloc (1, sizeof *self);
+ self->super.vtable = &backend_curl_vtable;
+ self->ctx = ctx;
+
+ CURL *curl;
+ if (!(self->curl = curl = curl_easy_init ()))
+ exit_fatal ("cURL initialization failed");
+
+ self->headers = NULL;
+ self->headers = curl_slist_append
+ (self->headers, "Content-Type: application/json");
+
+ if (curl_easy_setopt (curl, CURLOPT_POST, 1L)
+ || curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1L)
+ || curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, self->curl_error)
+ || curl_easy_setopt (curl, CURLOPT_HTTPHEADER, self->headers)
+ || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER,
+ self->ctx->trust_all ? 0L : 1L)
+ || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST,
+ self->ctx->trust_all ? 0L : 2L)
+ || curl_easy_setopt (curl, CURLOPT_URL, endpoint))
+ exit_fatal ("cURL setup failed");
+
+ if (!self->ctx->trust_all)
+ {
+ // TODO: try to resolve filenames relative to configuration directories
+ const char *ca_file = get_config_string
+ (self->ctx->config.root, "connection.tls_ca_file");
+ const char *ca_path = get_config_string
+ (self->ctx->config.root, "connection.tls_ca_path");
+ if ((ca_file && curl_easy_setopt (curl, CURLOPT_CAINFO, ca_file))
+ || (ca_path && curl_easy_setopt (curl, CURLOPT_CAPATH, ca_path)))
+ exit_fatal ("cURL setup failed");
+ }
+ return &self->super;
+}
+
+// --- JSON tokenizer ----------------------------------------------------------
+
+// A dumb JSON tokenizer intended strictly just for syntax highlighting
+//
+// TODO: return also escape squences as a special token class (-> state)
+
+enum jtoken
+{
+ JTOKEN_EOF, ///< End of input
+ JTOKEN_ERROR, ///< EOF or error
+
+ JTOKEN_WHITESPACE, ///< Whitespace
+
+ JTOKEN_LBRACKET, ///< Left bracket
+ JTOKEN_RBRACKET, ///< Right bracket
+ JTOKEN_LBRACE, ///< Left curly bracket
+ JTOKEN_RBRACE, ///< Right curly bracket
+ JTOKEN_COLON, ///< Colon
+ JTOKEN_COMMA, ///< Comma
+
+ JTOKEN_NULL, ///< null
+ JTOKEN_BOOLEAN, ///< true, false
+ JTOKEN_NUMBER, ///< Number
+ JTOKEN_STRING ///< String
+};
+
+struct jtokenizer
+{
+ const char *p; ///< Current position in input
+ size_t len; ///< How many bytes of input are left
+ struct str chunk; ///< Parsed chunk
+};
+
+static void
+jtokenizer_init (struct jtokenizer *self, const char *p, size_t len)
+{
+ self->p = p;
+ self->len = len;
+ self->chunk = str_make ();
+}
+
+static void
+jtokenizer_free (struct jtokenizer *self)
+{
+ str_free (&self->chunk);
+}
+
+static void
+jtokenizer_advance (struct jtokenizer *self, size_t n)
+{
+ str_append_data (&self->chunk, self->p, n);
+ self->p += n;
+ self->len -= n;
+}
+
+static int
+jtokenizer_accept (struct jtokenizer *self, const char *chars)
+{
+ if (!self->len || !strchr (chars, *self->p))
+ return false;
+
+ jtokenizer_advance (self, 1);
+ return true;
+}
+
+static bool
+jtokenizer_ws (struct jtokenizer *self)
+{
+ size_t len = 0;
+ while (jtokenizer_accept (self, "\t\r\n "))
+ len++;
+ return len != 0;
+}
+
+static bool
+jtokenizer_word (struct jtokenizer *self, const char *word)
+{
+ size_t len = strlen (word);
+ if (self->len < len || memcmp (self->p, word, len))
+ return false;
+
+ jtokenizer_advance (self, len);
+ return true;
+}
+
+static bool
+jtokenizer_escape_sequence (struct jtokenizer *self)
+{
+ if (!self->len)
+ return false;
+
+ if (jtokenizer_accept (self, "u"))
+ {
+ for (int i = 0; i < 4; i++)
+ if (!jtokenizer_accept (self, "0123456789abcdefABCDEF"))
+ return false;
+ return true;
+ }
+ return jtokenizer_accept (self, "\"\\/bfnrt");
+}
+
+static bool
+jtokenizer_string (struct jtokenizer *self)
+{
+ while (self->len)
+ {
+ unsigned char c = *self->p;
+ jtokenizer_advance (self, 1);
+
+ if (c == '"')
+ return true;
+ if (c == '\\' && !jtokenizer_escape_sequence (self))
+ return false;
+ }
+ return false;
+}
+
+static bool
+jtokenizer_integer (struct jtokenizer *self)
+{
+ size_t len = 0;
+ while (jtokenizer_accept (self, "0123456789"))
+ len++;
+ return len != 0;
+}
+
+static bool
+jtokenizer_number (struct jtokenizer *self)
+{
+ (void) jtokenizer_accept (self, "-");
+
+ if (!self->len)
+ return false;
+ if (!jtokenizer_accept (self, "0")
+ && !jtokenizer_integer (self))
+ return false;
+
+ if (jtokenizer_accept (self, ".")
+ && !jtokenizer_integer (self))
+ return false;
+ if (jtokenizer_accept (self, "eE"))
+ {
+ (void) jtokenizer_accept (self, "+-");
+ if (!jtokenizer_integer (self))
+ return false;
+ }
+ return true;
+}
+
+static enum jtoken
+jtokenizer_next (struct jtokenizer *self)
+{
+ str_reset (&self->chunk);
+
+ if (!self->len) return JTOKEN_EOF;
+ if (jtokenizer_ws (self)) return JTOKEN_WHITESPACE;
+
+ if (jtokenizer_accept (self, "[")) return JTOKEN_LBRACKET;
+ if (jtokenizer_accept (self, "]")) return JTOKEN_RBRACKET;
+ if (jtokenizer_accept (self, "{")) return JTOKEN_LBRACE;
+ if (jtokenizer_accept (self, "}")) return JTOKEN_RBRACE;
+
+ if (jtokenizer_accept (self, ":")) return JTOKEN_COLON;
+ if (jtokenizer_accept (self, ",")) return JTOKEN_COMMA;
+
+ if (jtokenizer_word (self, "null")) return JTOKEN_NULL;
+ if (jtokenizer_word (self, "true")
+ || jtokenizer_word (self, "false")) return JTOKEN_BOOLEAN;
+
+ if (jtokenizer_accept (self, "\""))
+ {
+ if (jtokenizer_string (self)) return JTOKEN_STRING;
+ }
+ else if (jtokenizer_number (self)) return JTOKEN_NUMBER;
+
+ jtokenizer_advance (self, self->len);
+ return JTOKEN_ERROR;
+}
+
+// --- JSON highlighter --------------------------------------------------------
+
+// Currently errors in parsing only mean that the rest doesn't get highlighted
+
+struct json_highlight
+{
+ struct app_context *ctx; ///< Application context
+ struct jtokenizer tokenizer; ///< Tokenizer
+ FILE *output; ///< Output handle
+};
+
+static void
+json_highlight_print (struct json_highlight *self, int attr)
+{
+ print_attributed (self->ctx, self->output, attr,
+ "%s", self->tokenizer.chunk.str);
+}
+
+static void json_highlight_value
+ (struct json_highlight *self, enum jtoken token);
+
+static void
+json_highlight_object (struct json_highlight *self)
+{
+ // Distinguishing field names from regular string values in objects
+ bool in_field_name = true;
+
+ enum jtoken token;
+ while ((token = jtokenizer_next (&self->tokenizer)))
+ switch (token)
+ {
+ case JTOKEN_COLON:
+ in_field_name = false;
+ json_highlight_value (self, token);
+ break;
+ case JTOKEN_COMMA:
+ in_field_name = true;
+ json_highlight_value (self, token);
+ break;
+ case JTOKEN_STRING:
+ if (in_field_name)
+ json_highlight_print (self, ATTR_JSON_FIELD);
+ else
+ json_highlight_print (self, ATTR_JSON_STRING);
+ break;
+ case JTOKEN_RBRACE:
+ json_highlight_value (self, token);
+ return;
+ default:
+ json_highlight_value (self, token);
+ }
+}
+
+static void
+json_highlight_array (struct json_highlight *self)
+{
+ enum jtoken token;
+ while ((token = jtokenizer_next (&self->tokenizer)))
+ switch (token)
+ {
+ case JTOKEN_RBRACKET:
+ json_highlight_value (self, token);
+ return;
+ default:
+ json_highlight_value (self, token);
+ }
+}
+
+static void
+json_highlight_value (struct json_highlight *self, enum jtoken token)
+{
+ switch (token)
+ {
+ case JTOKEN_LBRACE:
+ json_highlight_print (self, ATTR_INCOMING);
+ json_highlight_object (self);
+ break;
+ case JTOKEN_LBRACKET:
+ json_highlight_print (self, ATTR_INCOMING);
+ json_highlight_array (self);
+ break;
+ case JTOKEN_NULL:
+ json_highlight_print (self, ATTR_JSON_NULL);
+ break;
+ case JTOKEN_BOOLEAN:
+ json_highlight_print (self, ATTR_JSON_BOOL);
+ break;
+ case JTOKEN_NUMBER:
+ json_highlight_print (self, ATTR_JSON_NUMBER);
+ break;
+ case JTOKEN_STRING:
+ json_highlight_print (self, ATTR_JSON_STRING);
+ break;
+ default:
+ json_highlight_print (self, ATTR_INCOMING);
+ }
+}
+
+static void
+json_highlight (struct app_context *ctx, const char *s, FILE *output)
+{
+ struct json_highlight self = { .ctx = ctx, .output = output };
+ jtokenizer_init (&self.tokenizer, s, strlen (s));
+
+ // There should be at maximum one value in the input however,
+ // but let's just keep on going and process it all
+ enum jtoken token;
+ while ((token = jtokenizer_next (&self.tokenizer)))
+ json_highlight_value (&self, token);
+ fflush (output);
+
+ jtokenizer_free (&self.tokenizer);
+}
+
+// --- Main program ------------------------------------------------------------
+
+static void
+quit (struct app_context *ctx)
+{
+ if (ctx->backend->vtable->on_quit)
+ ctx->backend->vtable->on_quit (ctx->backend);
+
+ ev_break (EV_DEFAULT_ EVBREAK_ALL);
+ ctx->input->vtable->hide (ctx->input);
+}
+
+static void
+suspend_terminal (struct app_context *ctx)
+{
+ ctx->input->vtable->hide (ctx->input);
+ ev_io_stop (EV_DEFAULT_ &ctx->tty_watcher);
+ ctx->input->vtable->prepare (ctx->input, false);
+}
+
+static void
+resume_terminal (struct app_context *ctx)
+{
+ ctx->input->vtable->prepare (ctx->input, true);
+ ev_io_start (EV_DEFAULT_ &ctx->tty_watcher);
+ ctx->input->vtable->show (ctx->input);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define PARSE_FAIL(...) \
+ BLOCK_START \
+ print_error (__VA_ARGS__); \
+ goto fail; \
+ BLOCK_END
+
+// XXX: should probably rather defer this action and use spawn_helper_child()
+static void
+display_via_pipeline (struct app_context *ctx,
+ const char *s, const char *pipeline)
+{
+ suspend_terminal (ctx);
+
+ errno = 0;
+ FILE *fp = popen (pipeline, "w");
+ if (fp)
+ {
+ fputs (s, fp);
+ pclose (fp);
+ }
+ if (errno)
+ print_error ("pipeline failed: %s", strerror (errno));
+
+ resume_terminal (ctx);
+}
+
+static bool
+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 = &params;
+ else
+ {
+ print_error ("unexpected value at index %zu", i);
+ goto fail_parse;
+ }
+
+ if (*target)
+ {
+ print_error ("cannot specify multiple `id' or `params'");
+ goto fail_parse;
+ }
+ *target = json_incref (args[i]);
+ }
+
+ if (!id && ctx->auto_id)
+ id = json_integer (ctx->next_id++);
+
+ make_json_rpc_call (ctx, method, id, params, pipeline);
+
+fail_parse:
+ free (pipeline);
+
+ if (id) json_decref (id);
+ if (params) json_decref (params);
+
+ for (size_t i = 0; i < args_len; i++)
+ json_decref (args[i]);
+fail:
+ free (input);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// The ability to use an external editor on the input line has been shamelessly
+// copypasted from degesch with minor changes only.
+
+/// This differs from the non-unique version in that we expect the filename
+/// to be something like a pattern for mkstemp(), so the resulting path can
+/// reside in a system-wide directory with no risk of a conflict.
+static char *
+resolve_relative_runtime_unique_filename (const char *filename)
+{
+ struct str path = str_make ();
+
+ const char *runtime_dir = getenv ("XDG_RUNTIME_DIR");
+ const char *tmpdir = getenv ("TMPDIR");
+ if (runtime_dir && *runtime_dir == '/')
+ str_append (&path, runtime_dir);
+ else if (tmpdir && *tmpdir == '/')
+ str_append (&path, tmpdir);
+ else
+ str_append (&path, "/tmp");
+ str_append_printf (&path, "/%s/%s", PROGRAM_NAME, filename);
+
+ // Try to create the file's ancestors;
+ // typically the user will want to immediately create a file in there
+ const char *last_slash = strrchr (path.str, '/');
+ if (last_slash && last_slash != path.str)
+ {
+ char *copy = xstrndup (path.str, last_slash - path.str);
+ (void) mkdir_with_parents (copy, NULL);
+ free (copy);
+ }
+ return str_steal (&path);
+}
+
+static bool
+xwrite (int fd, const char *data, size_t len, struct error **e)
+{
+ size_t written = 0;
+ while (written < len)
+ {
+ ssize_t res = write (fd, data + written, len - written);
+ if (res >= 0)
+ written += res;
+ else if (errno != EINTR)
+ FAIL ("%s", strerror (errno));
+ }
+ return true;
+}
+
+static bool
+dump_line_to_file (const char *line, char *template, struct error **e)
+{
+ int fd = mkstemp (template);
+ if (fd < 0)
+ FAIL ("%s", strerror (errno));
+
+ bool success = xwrite (fd, line, strlen (line), e);
+ if (!success)
+ (void) unlink (template);
+
+ xclose (fd);
+ return success;
+}
+
+static char *
+try_dump_line_to_file (const char *line)
+{
+ char *template = resolve_filename
+ ("input.XXXXXX", resolve_relative_runtime_unique_filename);
+
+ struct error *e = NULL;
+ if (dump_line_to_file (line, template, &e))
+ return template;
+
+ print_error ("%s: %s",
+ "failed to create a temporary file for editing", e->message);
+ error_free (e);
+ free (template);
+ return NULL;
+}
+
+static pid_t
+spawn_helper_child (struct app_context *ctx)
+{
+ suspend_terminal (ctx);
+ pid_t child = fork ();
+ switch (child)
+ {
+ case -1:
+ {
+ int saved_errno = errno;
+ resume_terminal (ctx);
+ errno = saved_errno;
+ break;
+ }
+ case 0:
+ // Put the child in a new foreground process group
+ hard_assert (setpgid (0, 0) != -1);
+ hard_assert (tcsetpgrp (STDOUT_FILENO, getpgid (0)) != -1);
+ break;
+ default:
+ // Make sure of it in the parent as well before continuing
+ (void) setpgid (child, child);
+ }
+ return child;
+}
+
+static void
+run_editor (const char *line, void *user_data)
+{
+ struct app_context *ctx = user_data;
+ hard_assert (!ctx->editor_filename);
+
+ char *filename;
+ if (!(filename = try_dump_line_to_file (line)))
+ return;
+
+ const char *command;
+ if (!(command = getenv ("VISUAL"))
+ && !(command = getenv ("EDITOR")))
+ command = "vi";
+
+ switch (spawn_helper_child (ctx))
+ {
+ case 0:
+ execlp (command, command, filename, NULL);
+ print_error ("%s: %s", "failed to launch editor", strerror (errno));
+ _exit (EXIT_FAILURE);
+ case -1:
+ print_error ("%s: %s", "failed to launch editor", strerror (errno));
+ free (filename);
+ break;
+ default:
+ ctx->editor_filename = filename;
+ }
+}
+
+static void
+process_edited_input (struct app_context *ctx)
+{
+ struct str input = str_make ();
+ struct error *e = NULL;
+ if (!read_file (ctx->editor_filename, &input, &e))
+ {
+ print_error ("%s: %s", "input editing failed", e->message);
+ error_free (e);
+ }
+ else 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>