aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Janouch <p.janouch@gmail.com>2015-03-29 00:37:12 +0100
committerPřemysl Janouch <p.janouch@gmail.com>2015-03-29 00:37:12 +0100
commitb0c712c146b208ac9dad5fd5829a2bc38773a4de (patch)
treef5446f2e46eac0a62d797c5113386998356a6ac8
parentb400ea8d09b516b40b767d9477634bab470d95ec (diff)
downloadjson-rpc-shell-b0c712c146b208ac9dad5fd5829a2bc38773a4de.tar.gz
json-rpc-shell-b0c712c146b208ac9dad5fd5829a2bc38773a4de.tar.xz
json-rpc-shell-b0c712c146b208ac9dad5fd5829a2bc38773a4de.zip
Stubplement WebSockets
-rw-r--r--.gitmodules3
-rw-r--r--CMakeLists.txt9
-rw-r--r--README20
m---------http-parser0
-rw-r--r--json-rpc-shell.c1902
5 files changed, 1811 insertions, 123 deletions
diff --git a/.gitmodules b/.gitmodules
index 5abb60c..c8d5acf 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "liberty"]
path = liberty
url = git://github.com/pjanouch/liberty.git
+[submodule "http-parser"]
+ path = http-parser
+ url = https://github.com/joyent/http-parser.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7898e3f..a706b0a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,11 +23,14 @@ set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
find_package (Curses)
find_package (PkgConfig REQUIRED)
pkg_check_modules (dependencies REQUIRED libcurl jansson)
+pkg_check_modules (libssl REQUIRED libssl libcrypto)
find_package (LibEV REQUIRED)
pkg_check_modules (ncursesw ncursesw)
-set (project_libraries ${dependencies_LIBRARIES} ${LIBEV_LIBRARIES} readline)
-include_directories (${dependencies_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS})
+set (project_libraries ${dependencies_LIBRARIES}
+ ${libssl_LIBRARIES} ${LIBEV_LIBRARIES} readline)
+include_directories (${dependencies_INCLUDE_DIRS}
+ ${libssl_INCLUDE_DIRS} ${LIBEV_INCLUDE_DIRS})
if (ncursesw_FOUND)
list (APPEND project_libraries ${ncursesw_LIBRARIES})
@@ -44,7 +47,7 @@ configure_file (${PROJECT_SOURCE_DIR}/config.h.in ${PROJECT_BINARY_DIR}/config.h
include_directories (${PROJECT_BINARY_DIR})
# Build the main executable and link it
-add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c)
+add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c http-parser/http_parser.c)
target_link_libraries (${PROJECT_NAME} ${project_libraries})
# The files to be installed
diff --git a/README b/README
index df9b968..96e02b7 100644
--- a/README
+++ b/README
@@ -6,17 +6,29 @@ json-rpc-shell
This software has been created as a replacement for the following shell, which
is written in Java: http://software.dzhuvinov.com/json-rpc-2.0-shell.html
-Fuck Java. With a sharp, pointy object. In the ass. Hard. json-c as well.
-
Supported transports
--------------------
- HTTP
- HTTPS
+ - WebSocket
+ - WebSocket over TLS
+
+WebSockets
+----------
+The WebSocket transport is rather experimental. As the JSON-RPC 2.0 spec
+doesn't say almost anything about the underlying transports, I'll shortly
+describe the way it's implemented: 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).
Building and Running
--------------------
-Build dependencies: CMake, pkg-config, help2man, liberty (included)
-Runtime dependencies: libev, Jansson, cURL, readline
+Build dependencies: CMake, pkg-config, help2man,
+ liberty (included), http-parser (included)
+Runtime dependencies: libev, Jansson, cURL, readline, openssl
$ git clone https://github.com/pjanouch/json-rpc-shell.git
$ git submodule init
diff --git a/http-parser b/http-parser
new file mode 160000
+Subproject 5d414fcb4b2ccc1ce9d6063292f9c63c9ec67b0
diff --git a/json-rpc-shell.c b/json-rpc-shell.c
index 1dbc286..35f41ca 100644
--- a/json-rpc-shell.c
+++ b/json-rpc-shell.c
@@ -34,23 +34,780 @@
#define print_error_data ATTR_ERROR
#define print_warning_data ATTR_WARNING
+#define LIBERTY_WANT_SSL
+
#include "config.h"
#include "liberty/liberty.c"
+#include "http-parser/http_parser.h"
#include <langinfo.h>
#include <locale.h>
#include <signal.h>
#include <strings.h>
+#include <arpa/inet.h>
+
#include <ev.h>
#include <readline/readline.h>
#include <readline/history.h>
#include <curl/curl.h>
#include <jansson.h>
+#include <openssl/rand.h>
#include <curses.h>
#include <term.h>
+// --- Extensions to liberty ---------------------------------------------------
+
+// COPIED OVER FROM ACID, DON'T CHANGE SEPARATELY
+
+#define UNPACKER_INT_BEGIN \
+ if (self->len - self->offset < sizeof *value) \
+ return false; \
+ uint8_t *x = (uint8_t *) self->data + self->offset; \
+ self->offset += sizeof *value;
+
+static bool
+msg_unpacker_u16 (struct msg_unpacker *self, uint16_t *value)
+{
+ UNPACKER_INT_BEGIN
+ *value
+ = (uint16_t) x[0] << 24 | (uint16_t) x[1] << 16;
+ return true;
+}
+
+static bool
+msg_unpacker_u32 (struct msg_unpacker *self, uint32_t *value)
+{
+ UNPACKER_INT_BEGIN
+ *value
+ = (uint32_t) x[0] << 24 | (uint32_t) x[1] << 16
+ | (uint32_t) x[2] << 8 | (uint32_t) x[3];
+ return true;
+}
+
+#undef UNPACKER_INT_BEGIN
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// "msg_writer" should be rewritten on top of this
+
+static void
+str_pack_u8 (struct str *self, uint8_t x)
+{
+ str_append_data (self, &x, 1);
+}
+
+static void
+str_pack_u16 (struct str *self, uint64_t x)
+{
+ uint8_t tmp[2] = { x >> 8, x };
+ str_append_data (self, tmp, sizeof tmp);
+}
+
+static void
+str_pack_u32 (struct str *self, uint32_t x)
+{
+ uint32_t u = x;
+ uint8_t tmp[4] = { u >> 24, u >> 16, u >> 8, u };
+ str_append_data (self, tmp, sizeof tmp);
+}
+
+static void
+str_pack_i32 (struct str *self, int32_t x)
+{
+ str_pack_u32 (self, (uint32_t) x);
+}
+
+static void
+str_pack_u64 (struct str *self, uint64_t x)
+{
+ uint8_t tmp[8] =
+ { x >> 56, x >> 48, x >> 40, x >> 32, x >> 24, x >> 16, x >> 8, x };
+ str_append_data (self, tmp, sizeof tmp);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+tolower_ascii (int c)
+{
+ return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c;
+}
+
+static size_t
+tolower_ascii_strxfrm (char *dest, const char *src, size_t n)
+{
+ size_t len = strlen (src);
+ while (n-- && (*dest++ = tolower_ascii (*src++)))
+ ;
+ return len;
+}
+
+static int
+strcasecmp_ascii (const char *a, const char *b)
+{
+ while (*a && tolower_ascii (*a) == tolower_ascii (*b))
+ {
+ a++;
+ b++;
+ }
+ return *(const unsigned char *) a - *(const unsigned char *) b;
+}
+
+static bool
+isspace_ascii (int c)
+{
+ return c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v';
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// Return a pointer to the next UTF-8 character, or NULL on error
+// TODO: decode the sequence while we're at it
+static const char *
+utf8_next (const char *s, size_t len)
+{
+ // End of string, we go no further
+ if (!len)
+ return NULL;
+
+ // In the middle of a character -> error
+ const uint8_t *p = (const unsigned char *) s;
+ if ((*p & 0xC0) == 0x80)
+ return NULL;
+
+ // Find out how long the sequence is
+ unsigned mask = 0xC0;
+ unsigned tail_len = 0;
+ while ((*p & mask) == mask)
+ {
+ // Invalid start of sequence
+ if (mask == 0xFE)
+ return NULL;
+
+ mask |= mask >> 1;
+ tail_len++;
+ }
+
+ p++;
+
+ // Check the rest of the sequence
+ if (tail_len > --len)
+ return NULL;
+
+ while (tail_len--)
+ if ((*p++ & 0xC0) != 0x80)
+ return NULL;
+
+ return (const char *) p;
+}
+
+/// Very rough UTF-8 validation, just makes sure codepoints can be iterated
+// TODO: also validate the codepoints
+static bool
+utf8_validate (const char *s, size_t len)
+{
+ const char *next;
+ while (len)
+ {
+ if (!(next = utf8_next (s, len)))
+ return false;
+
+ len -= next - s;
+ s = next;
+ }
+ return true;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static uint8_t g_base64_table[256] =
+{
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 0, 64, 64,
+ 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64,
+ 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64,
+
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64,
+};
+
+static inline bool
+base64_decode_group (const char **s, bool ignore_ws, struct str *output)
+{
+ uint8_t input[4];
+ size_t loaded = 0;
+ for (; loaded < 4; (*s)++)
+ {
+ if (!**s)
+ return loaded == 0;
+ if (!ignore_ws || !isspace_ascii (**s))
+ input[loaded++] = **s;
+ }
+
+ size_t len = 3;
+ if (input[0] == '=' || input[1] == '=')
+ return false;
+ if (input[2] == '=' && input[3] != '=')
+ return false;
+ if (input[2] == '=')
+ len--;
+ if (input[3] == '=')
+ len--;
+
+ uint8_t a = g_base64_table[input[0]];
+ uint8_t b = g_base64_table[input[1]];
+ uint8_t c = g_base64_table[input[2]];
+ uint8_t d = g_base64_table[input[3]];
+
+ if (((a | b) | (c | d)) & 0x40)
+ return false;
+
+ uint32_t block = a << 18 | b << 12 | c << 6 | d;
+ switch (len)
+ {
+ case 1:
+ str_append_c (output, block >> 16);
+ break;
+ case 2:
+ str_append_c (output, block >> 16);
+ str_append_c (output, block >> 8);
+ break;
+ case 3:
+ str_append_c (output, block >> 16);
+ str_append_c (output, block >> 8);
+ str_append_c (output, block);
+ }
+ return true;
+}
+
+static bool
+base64_decode (const char *s, bool ignore_ws, struct str *output)
+{
+ while (*s)
+ if (!base64_decode_group (&s, ignore_ws, output))
+ return false;
+ return true;
+}
+
+static void
+base64_encode (const void *data, size_t len, struct str *output)
+{
+ const char *alphabet =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ const uint8_t *p = data;
+ size_t n_groups = len / 3;
+ size_t tail = len - n_groups * 3;
+ uint32_t group;
+
+ for (; n_groups--; p += 3)
+ {
+ group = p[0] << 16 | p[1] << 8 | p[2];
+ str_append_c (output, alphabet[(group >> 18) & 63]);
+ str_append_c (output, alphabet[(group >> 12) & 63]);
+ str_append_c (output, alphabet[(group >> 6) & 63]);
+ str_append_c (output, alphabet[ group & 63]);
+ }
+
+ switch (tail)
+ {
+ case 2:
+ group = p[0] << 16 | p[1] << 8;
+ str_append_c (output, alphabet[(group >> 18) & 63]);
+ str_append_c (output, alphabet[(group >> 12) & 63]);
+ str_append_c (output, alphabet[(group >> 6) & 63]);
+ str_append_c (output, '=');
+ break;
+ case 1:
+ group = p[0] << 16;
+ str_append_c (output, alphabet[(group >> 18) & 63]);
+ str_append_c (output, alphabet[(group >> 12) & 63]);
+ str_append_c (output, '=');
+ str_append_c (output, '=');
+ default:
+ break;
+ }
+}
+
+// --- HTTP parsing ------------------------------------------------------------
+
+// COPIED OVER FROM ACID, DON'T CHANGE SEPARATELY
+
+// Basic tokenizer for HTTP header field values, to be used in various parsers.
+// The input should already be unwrapped.
+
+// Recommended literature:
+// http://tools.ietf.org/html/rfc7230#section-3.2.6
+// http://tools.ietf.org/html/rfc7230#appendix-B
+// http://tools.ietf.org/html/rfc5234#appendix-B.1
+
+#define HTTP_TOKENIZER_CLASS(name, definition) \
+ static inline bool \
+ http_tokenizer_is_ ## name (int c) \
+ { \
+ return (definition); \
+ }
+
+HTTP_TOKENIZER_CLASS (vchar, c >= 0x21 && c <= 0x7E)
+HTTP_TOKENIZER_CLASS (delimiter, !!strchr ("\"(),/:;<=>?@[\\]{}", c))
+HTTP_TOKENIZER_CLASS (whitespace, c == '\t' || c == ' ')
+HTTP_TOKENIZER_CLASS (obs_text, c >= 0x80 && c <= 0xFF)
+
+HTTP_TOKENIZER_CLASS (tchar,
+ http_tokenizer_is_vchar (c) && !http_tokenizer_is_delimiter (c))
+
+HTTP_TOKENIZER_CLASS (qdtext,
+ c == '\t' || c == ' ' || c == '!'
+ || (c >= 0x23 && c <= 0x5B)
+ || (c >= 0x5D && c <= 0x7E)
+ || http_tokenizer_is_obs_text (c))
+
+HTTP_TOKENIZER_CLASS (quoted_pair,
+ c == '\t' || c == ' '
+ || http_tokenizer_is_vchar (c)
+ || http_tokenizer_is_obs_text (c))
+
+#undef HTTP_TOKENIZER_CLASS
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum http_tokenizer_token
+{
+ HTTP_T_EOF, ///< Input error
+ HTTP_T_ERROR, ///< End of input
+
+ HTTP_T_TOKEN, ///< "token"
+ HTTP_T_QUOTED_STRING, ///< "quoted-string"
+ HTTP_T_DELIMITER, ///< "delimiters"
+ HTTP_T_WHITESPACE ///< RWS/OWS/BWS
+};
+
+struct http_tokenizer
+{
+ const unsigned char *input; ///< The input string
+ size_t input_len; ///< Length of the input
+ size_t offset; ///< Position in the input
+
+ char delimiter; ///< The delimiter character
+ struct str string; ///< "token" / "quoted-string" content
+};
+
+static void
+http_tokenizer_init (struct http_tokenizer *self, const char *input, size_t len)
+{
+ memset (self, 0, sizeof *self);
+ self->input = (const unsigned char *) input;
+ self->input_len = len;
+
+ str_init (&self->string);
+}
+
+static void
+http_tokenizer_free (struct http_tokenizer *self)
+{
+ str_free (&self->string);
+}
+
+static enum http_tokenizer_token
+http_tokenizer_quoted_string (struct http_tokenizer *self)
+{
+ bool quoted_pair = false;
+ while (self->offset < self->input_len)
+ {
+ int c = self->input[self->offset++];
+ if (quoted_pair)
+ {
+ if (!http_tokenizer_is_quoted_pair (c))
+ return HTTP_T_ERROR;
+
+ str_append_c (&self->string, c);
+ quoted_pair = false;
+ }
+ else if (c == '\\')
+ quoted_pair = true;
+ else if (c == '"')
+ return HTTP_T_QUOTED_STRING;
+ else if (http_tokenizer_is_qdtext (c))
+ str_append_c (&self->string, c);
+ else
+ return HTTP_T_ERROR;
+ }
+
+ // Premature end of input
+ return HTTP_T_ERROR;
+}
+
+static enum http_tokenizer_token
+http_tokenizer_next (struct http_tokenizer *self, bool skip_ows)
+{
+ str_reset (&self->string);
+ if (self->offset >= self->input_len)
+ return HTTP_T_EOF;
+
+ int c = self->input[self->offset++];
+
+ if (skip_ows)
+ while (http_tokenizer_is_whitespace (c))
+ {
+ if (self->offset >= self->input_len)
+ return HTTP_T_EOF;
+ c = self->input[self->offset++];
+ }
+
+ if (c == '"')
+ return http_tokenizer_quoted_string (self);
+
+ if (http_tokenizer_is_delimiter (c))
+ {
+ self->delimiter = c;
+ return HTTP_T_DELIMITER;
+ }
+
+ // Simple variable-length tokens
+ enum http_tokenizer_token result;
+ bool (*eater) (int c) = NULL;
+ if (http_tokenizer_is_whitespace (c))
+ {
+ eater = http_tokenizer_is_whitespace;
+ result = HTTP_T_WHITESPACE;
+ }
+ else if (http_tokenizer_is_tchar (c))
+ {
+ eater = http_tokenizer_is_tchar;
+ result = HTTP_T_TOKEN;
+ }
+ else
+ return HTTP_T_ERROR;
+
+ str_append_c (&self->string, c);
+ while (self->offset < self->input_len)
+ {
+ if (!eater (c = self->input[self->offset]))
+ break;
+
+ str_append_c (&self->string, c);
+ self->offset++;
+ }
+ return result;
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static bool
+http_parse_media_type_parameter
+ (struct http_tokenizer *t, struct str_map *parameters)
+{
+ bool result = false;
+ char *attribute = NULL;
+
+ if (http_tokenizer_next (t, true) != HTTP_T_TOKEN)
+ goto end;
+ attribute = xstrdup (t->string.str);
+
+ if (http_tokenizer_next (t, false) != HTTP_T_DELIMITER
+ || t->delimiter != '=')
+ goto end;
+
+ switch (http_tokenizer_next (t, false))
+ {
+ case HTTP_T_TOKEN:
+ case HTTP_T_QUOTED_STRING:
+ str_map_set (parameters, attribute, xstrdup (t->string.str));
+ result = true;
+ default:
+ break;
+ }
+
+end:
+ free (attribute);
+ return result;
+}
+
+/// Parser for "Content-Type". @a type and @a subtype may be non-NULL
+/// even if the function fails. @a parameters should be case-insensitive.
+static bool
+http_parse_media_type (const char *media_type,
+ char **type, char **subtype, struct str_map *parameters)
+{
+ bool result = false;
+ struct http_tokenizer t;
+ http_tokenizer_init (&t, media_type, strlen (media_type));
+
+ if (http_tokenizer_next (&t, true) != HTTP_T_TOKEN)
+ goto end;
+ *type = xstrdup (t.string.str);
+
+ if (http_tokenizer_next (&t, false) != HTTP_T_DELIMITER
+ || t.delimiter != '/')
+ goto end;
+
+ if (http_tokenizer_next (&t, false) != HTTP_T_TOKEN)
+ goto end;
+ *subtype = xstrdup (t.string.str);
+
+ while (true)
+ switch (http_tokenizer_next (&t, true))
+ {
+ case HTTP_T_DELIMITER:
+ if (t.delimiter != ';')
+ goto end;
+ if (!http_parse_media_type_parameter (&t, parameters))
+ goto end;
+ break;
+ case HTTP_T_EOF:
+ result = true;
+ default:
+ goto end;
+ }
+
+end:
+ http_tokenizer_free (&t);
+ return result;
+}
+
+// --- WebSockets --------------------------------------------------------------
+
+// COPIED OVER FROM ACID, DON'T CHANGE SEPARATELY
+
+#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+
+#define SEC_WS_KEY "Sec-WebSocket-Key"
+#define SEC_WS_ACCEPT "Sec-WebSocket-Accept"
+#define SEC_WS_PROTOCOL "Sec-WebSocket-Protocol"
+#define SEC_WS_EXTENSIONS "Sec-WebSocket-Extensions"
+#define SEC_WS_VERSION "Sec-WebSocket-Version"
+
+#define WS_MAX_CONTROL_PAYLOAD_LEN 125
+
+static char *
+ws_encode_response_key (const char *key)
+{
+ char *response_key = xstrdup_printf ("%s" WS_GUID, key);
+ unsigned char hash[SHA_DIGEST_LENGTH];
+ SHA1 ((unsigned char *) response_key, strlen (response_key), hash);
+ free (response_key);
+
+ struct str base64;
+ str_init (&base64);
+ base64_encode (hash, sizeof hash, &base64);
+ return str_steal (&base64);
+}
+
+enum ws_status
+{
+ // Named according to the meaning specified in RFC 6455, section 11.2
+
+ WS_STATUS_NORMAL_CLOSURE = 1000,
+ WS_STATUS_GOING_AWAY = 1001,
+ WS_STATUS_PROTOCOL_ERROR = 1002,
+ WS_STATUS_UNSUPPORTED_DATA = 1003,
+ WS_STATUS_INVALID_PAYLOAD_DATA = 1007,
+ WS_STATUS_POLICY_VIOLATION = 1008,
+ WS_STATUS_MESSAGE_TOO_BIG = 1009,
+ WS_STATUS_MANDATORY_EXTENSION = 1010,
+ WS_STATUS_INTERNAL_SERVER_ERROR = 1011,
+
+ // Reserved for internal usage
+ WS_STATUS_NO_STATUS_RECEIVED = 1005,
+ WS_STATUS_ABNORMAL_CLOSURE = 1006,
+ WS_STATUS_TLS_HANDSHAKE = 1015
+};
+
+// - - Frame parser - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+enum ws_parser_state
+{
+ WS_PARSER_FIXED, ///< Parsing fixed length part
+ WS_PARSER_PAYLOAD_LEN_16, ///< Parsing extended payload length
+ WS_PARSER_PAYLOAD_LEN_64, ///< Parsing extended payload length
+ WS_PARSER_MASK, ///< Parsing masking-key
+ WS_PARSER_PAYLOAD ///< Parsing payload
+};
+
+enum ws_opcode
+{
+ // Non-control
+ WS_OPCODE_CONT = 0,
+ WS_OPCODE_TEXT = 1,
+ WS_OPCODE_BINARY = 2,
+
+ // Control
+ WS_OPCODE_CLOSE = 8,
+ WS_OPCODE_PING = 9,
+ WS_OPCODE_PONG = 10
+};
+
+static bool
+ws_is_control_frame (int opcode)
+{
+ return opcode >= WS_OPCODE_CLOSE;
+}
+
+struct ws_parser
+{
+ struct str input; ///< External input buffer
+ enum ws_parser_state state; ///< Parsing state
+
+ unsigned is_fin : 1; ///< Final frame of a message?
+ unsigned is_masked : 1; ///< Is the frame masked?
+ unsigned reserved_1 : 1; ///< Reserved
+ unsigned reserved_2 : 1; ///< Reserved
+ unsigned reserved_3 : 1; ///< Reserved
+ enum ws_opcode opcode; ///< Opcode
+ uint32_t mask; ///< Frame mask
+ uint64_t payload_len; ///< Payload length
+
+ bool (*on_frame_header) (void *user_data, const struct ws_parser *self);
+
+ /// Callback for when a message is successfully parsed.
+ /// The actual payload is stored in "input", of length "payload_len".
+ bool (*on_frame) (void *user_data, const struct ws_parser *self);
+
+ void *user_data; ///< User data for callbacks
+};
+
+static void
+ws_parser_init (struct ws_parser *self)
+{
+ memset (self, 0, sizeof *self);
+ str_init (&self->input);
+}
+
+static void
+ws_parser_free (struct ws_parser *self)
+{
+ str_free (&self->input);
+}
+
+static void
+ws_parser_unmask (char *payload, size_t len, uint32_t mask)
+{
+ // This could be made faster. For example by reading the mask in
+ // native byte ordering and applying it directly here.
+
+ size_t end = len & ~(size_t) 3;
+ for (size_t i = 0; i < end; i += 4)
+ {
+ payload[i + 3] ^= mask & 0xFF;
+ payload[i + 2] ^= (mask >> 8) & 0xFF;
+ payload[i + 1] ^= (mask >> 16) & 0xFF;
+ payload[i ] ^= (mask >> 24) & 0xFF;
+ }
+
+ switch (len - end)
+ {
+ case 3:
+ payload[end + 2] ^= (mask >> 8) & 0xFF;
+ case 2:
+ payload[end + 1] ^= (mask >> 16) & 0xFF;
+ case 1:
+ payload[end ] ^= (mask >> 24) & 0xFF;
+ }
+}
+
+static bool
+ws_parser_push (struct ws_parser *self, const void *data, size_t len)
+{
+ bool success = false;
+ str_append_data (&self->input, data, len);
+
+ struct msg_unpacker unpacker;
+ msg_unpacker_init (&unpacker, self->input.str, self->input.len);
+
+ while (true)
+ switch (self->state)
+ {
+ uint8_t u8;
+ uint16_t u16;
+
+ case WS_PARSER_FIXED:
+ if (unpacker.len - unpacker.offset < 2)
+ goto need_data;
+
+ (void) msg_unpacker_u8 (&unpacker, &u8);
+ self->is_fin = (u8 >> 7) & 1;
+ self->reserved_1 = (u8 >> 6) & 1;
+ self->reserved_2 = (u8 >> 5) & 1;
+ self->reserved_3 = (u8 >> 4) & 1;
+ self->opcode = u8 & 15;
+
+ (void) msg_unpacker_u8 (&unpacker, &u8);
+ self->is_masked = (u8 >> 7) & 1;
+ self->payload_len = u8 & 127;
+
+ if (self->payload_len == 127)
+ self->state = WS_PARSER_PAYLOAD_LEN_64;
+ else if (self->payload_len == 126)
+ self->state = WS_PARSER_PAYLOAD_LEN_16;
+ else
+ self->state = WS_PARSER_MASK;
+ break;
+
+ case WS_PARSER_PAYLOAD_LEN_16:
+ if (!msg_unpacker_u16 (&unpacker, &u16))
+ goto need_data;
+ self->payload_len = u16;
+
+ self->state = WS_PARSER_MASK;
+ break;
+
+ case WS_PARSER_PAYLOAD_LEN_64:
+ if (!msg_unpacker_u64 (&unpacker, &self->payload_len))
+ goto need_data;
+
+ self->state = WS_PARSER_MASK;
+ break;
+
+ case WS_PARSER_MASK:
+ if (!self->is_masked)
+ goto end_of_header;
+ if (!msg_unpacker_u32 (&unpacker, &self->mask))
+ goto need_data;
+
+ end_of_header:
+ self->state = WS_PARSER_PAYLOAD;
+ if (!self->on_frame_header (self->user_data, self))
+ goto fail;
+ break;
+
+ case WS_PARSER_PAYLOAD:
+ // Move the buffer so that payload data is at the front
+ str_remove_slice (&self->input, 0, unpacker.offset);
+
+ // And continue unpacking frames past the payload
+ msg_unpacker_init (&unpacker, self->input.str, self->input.len);
+ unpacker.offset = self->payload_len;
+
+ if (self->input.len < self->payload_len)
+ goto need_data;
+ if (self->is_masked)
+ ws_parser_unmask (self->input.str, self->payload_len, self->mask);
+ if (!self->on_frame (self->user_data, self))
+ goto fail;
+
+ self->state = WS_PARSER_FIXED;
+ break;
+ }
+
+need_data:
+ success = true;
+fail:
+ str_remove_slice (&self->input, 0, unpacker.offset);
+ return success;
+}
+
// --- Configuration (application-specific) ------------------------------------
static struct config_item g_config_table[] =
@@ -66,6 +823,90 @@ static struct config_item g_config_table[] =
// --- Main program ------------------------------------------------------------
+struct app_context;
+
+struct backend_iface
+{
+ void (*init) (struct app_context *ctx,
+ const char *endpoint, struct http_parser_url *url);
+ void (*add_header) (struct app_context *ctx, const char *header);
+ bool (*make_call) (struct app_context *ctx,
+ const char *request, bool expect_content,
+ struct str *buf, struct error **e);
+ void (*destroy) (struct app_context *ctx);
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+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
+{
+ char *endpoint; ///< Endpoint URL
+ struct http_parser_url url; ///< Parsed URL
+
+ enum ws_handler_state state; ///< State
+ char *key; ///< Key for the current handshake
+ struct str *response_buffer; ///< Buffer for the response
+
+ 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
+
+ http_parser hp; ///< HTTP parser
+ bool parsing_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
+
+ struct str_vector extra_headers; ///< Extra headers for the handshake
+};
+
+static void
+ws_context_init (struct ws_context *self)
+{
+ memset (self, 0, sizeof *self);
+ http_parser_init (&self->hp, HTTP_RESPONSE);
+ str_init (&self->field);
+ str_init (&self->value);
+ str_map_init (&self->headers);
+ self->headers.key_xfrm = tolower_ascii_strxfrm;
+ self->headers.free = free;
+ ws_parser_init (&self->parser);
+ str_init (&self->message_data);
+ str_vector_init (&self->extra_headers);
+}
+
+struct curl_context
+{
+ CURL *curl; ///< cURL handle
+ char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
+ struct curl_slist *headers; ///< Headers
+};
+
+static void
+curl_context_init (struct curl_context *self)
+{
+ memset (self, 0, sizeof *self);
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
enum color_mode
{
COLOR_AUTO, ///< Autodetect if colours are available
@@ -73,10 +914,21 @@ enum color_mode
COLOR_NEVER ///< Never use coloured output
};
+enum backend
+{
+ BACKEND_CURL, ///< Communication is handled by cURL
+ BACKEND_WS ///< WebSockets
+};
+
static struct app_context
{
- CURL *curl; ///< cURL handle
- char curl_error[CURL_ERROR_SIZE]; ///< cURL error info buffer
+#if 0
+ enum backend backend; ///< Our current backend
+#endif
+ struct backend_iface *backend; ///< Our current backend
+
+ struct ws_context ws; ///< WebSockets backend data
+ struct curl_context curl; ///< cURL backend data
struct str_map config; ///< Program configuration
enum color_mode color_mode; ///< Colour output mode
@@ -408,6 +1260,902 @@ load_config (struct app_context *ctx)
str_map_free (&map);
}
+// --- WebSockets backend ------------------------------------------------------
+
+static void
+backend_ws_init (struct app_context *ctx,
+ const char *endpoint, struct http_parser_url *url)
+{
+ struct ws_context *self = &ctx->ws;
+ ws_context_init (self);
+ self->endpoint = xstrdup (endpoint);
+ self->url = *url;
+
+ SSL_library_init ();
+ atexit (EVP_cleanup);
+ SSL_load_error_strings ();
+ atexit (ERR_free_strings);
+}
+
+static void
+backend_ws_add_header (struct app_context *ctx, const char *header)
+{
+ str_vector_add (&ctx->ws.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 app_context *ctx, void *buf, size_t *len)
+{
+ int n_read;
+ struct ws_context *self = &ctx->ws;
+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 app_context *ctx, void *buf, size_t *len)
+{
+ ssize_t n_read;
+ struct ws_context *self = &ctx->ws;
+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->parsing_header_value)
+ {
+ backend_ws_on_header_read (self);
+ str_reset (&self->field);
+ str_reset (&self->value);
+ }
+ str_append_data (&self->field, at, len);
+ self->parsing_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->parsing_header_value = true;
+ return 0;
+}
+
+static int
+backend_ws_on_headers_complete (http_parser *parser)
+{
+ // We strictly require a protocol upgrade
+ if (!parser->upgrade)
+ return 2;
+
+ return 0;
+}
+
+static bool
+backend_ws_finish_handshake (struct app_context *ctx)
+{
+ // TODO: return the errors as a "struct error"
+ struct ws_context *self = &ctx->ws;
+ if (self->hp.http_major != 1 || self->hp.http_minor < 1)
+ return false;
+
+ if (self->hp.status_code != 101)
+ // TODO: handle other codes?
+ return false;
+
+ const char *upgrade = str_map_find (&self->headers, "Upgrade");
+ if (!upgrade || strcasecmp_ascii (upgrade, "websocket"))
+ return false;
+
+ 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
+ return false;
+
+ const char *accept = str_map_find (&self->headers, "Accept");
+ char *accept_expected = ws_encode_response_key (self->key);
+ bool accept_ok = accept && !strcmp (accept, accept_expected);
+ free (accept_expected);
+ if (!accept_ok)
+ return false;
+
+ 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
+ return false;
+
+ return true;
+}
+
+static bool
+backend_ws_on_data (struct app_context *ctx, const void *data, size_t len)
+{
+ struct ws_context *self = &ctx->ws;
+ 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)
+ {
+ if (!backend_ws_finish_handshake (ctx))
+ return false;
+ self->state = WS_HANDLER_OPEN;
+
+ // TODO: set the event loop to quit?
+
+ 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_debug ("WS handshake failed: %s", "missing `Upgrade' field");
+ else
+ print_debug ("WS handshake failed: %s",
+ http_errno_description (err));
+ return false;
+ }
+ return true;
+}
+
+static void
+backend_ws_on_fd_ready (EV_P_ ev_io *handle, int revents)
+{
+ (void) loop;
+ (void) revents;
+
+ struct app_context *ctx = handle->data;
+ struct ws_context *self = &ctx->ws;
+
+ (void) set_blocking (self->server_fd, false);
+
+ enum ws_read_result (*fill_buffer)(struct app_context *, void *, size_t *)
+ = self->ssl
+ ? backend_ws_fill_read_buffer_tls
+ : backend_ws_fill_read_buffer;
+ bool disconnected = false;
+
+ uint8_t buf[8192];
+ while (true)
+ {
+ size_t n_read = sizeof buf;
+ switch (fill_buffer (ctx, buf, &n_read))
+ {
+ case WS_READ_AGAIN:
+ goto end;
+ case WS_READ_ERROR:
+ print_error ("reading from the server failed");
+ disconnected = true;
+ goto end;
+ case WS_READ_EOF:
+ print_status ("the server closed the connection");
+ disconnected = true;
+ goto end;
+ case WS_READ_OK:
+ // XXX: this is a bit ugly
+ (void) set_blocking (self->server_fd, true);
+ // TODO: use the return value
+ backend_ws_on_data (ctx, buf, n_read);
+ (void) set_blocking (self->server_fd, false);
+ break;
+ }
+ }
+
+end:
+ (void) set_blocking (self->server_fd, true);
+ if (disconnected)
+ ; // TODO
+}
+
+static bool
+backend_ws_write (struct app_context *ctx, const void *data, size_t len)
+{
+ if (!soft_assert (ctx->ws.server_fd != -1))
+ return false;
+
+ bool result = true;
+ if (ctx->ws.ssl)
+ {
+ // TODO: call SSL_get_error() to detect if a clean shutdown has occured
+ if (SSL_write (ctx->ws.ssl, data, len) != (int) len)
+ {
+ print_debug ("%s: %s: %s", __func__, "SSL_write",
+ ERR_error_string (ERR_get_error (), NULL));
+ result = false;
+ }
+ }
+ else if (write (ctx->ws.server_fd, data, len) != (ssize_t) len)
+ {
+ print_debug ("%s: %s: %s", __func__, "write", strerror (errno));
+ result = false;
+ }
+
+ // TODO: destroy the connection on failure?
+ return result;
+}
+
+static bool
+backend_ws_establish_connection (struct app_context *ctx,
+ 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)
+ {
+ error_set (e, "%s: %s: %s",
+ "connection failed", "getaddrinfo", gai_strerror (err));
+ return false;
+ }
+
+ 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, 0);
+ if (err)
+ print_debug ("%s: %s", "getnameinfo", gai_strerror (err));
+ else
+ real_host = buf;
+
+ 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)
+ {
+ error_set (e, "connection failed");
+ return false;
+ }
+
+ ctx->ws.server_fd = sockfd;
+ return true;
+}
+
+static bool
+backend_ws_initialize_tls (struct app_context *ctx, struct error **e)
+{
+ struct ws_context *self = &ctx->ws;
+ const char *error_info = NULL;
+ if (!self->ssl_ctx)
+ {
+ if (!(self->ssl_ctx = SSL_CTX_new (SSLv23_client_method ())))
+ goto error_ssl_1;
+ if (ctx->trust_all)
+ SSL_CTX_set_verify (self->ssl_ctx, SSL_VERIFY_NONE, NULL);
+ // XXX: how do we check certificates?
+ }
+
+ 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);
+
+ 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);
+ error_set (e, "%s: %s", "could not initialize SSL", error_info);
+ return false;
+}
+
+static bool
+backend_ws_send_message (struct app_context *ctx,
+ enum ws_opcode opcode, const void *data, size_t len)
+{
+ struct str header;
+ str_init (&header);
+ 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);
+
+ // XXX: maybe we should do this in a loop, who knows how large this can be
+ char masked[len];
+ memcpy (masked, data, len);
+ ws_parser_unmask (masked, len, mask);
+
+ bool result = true;
+ if (!backend_ws_write (ctx, header.str, header.len)
+ || !backend_ws_write (ctx, masked, len))
+ result = false;
+ str_free (&header);
+ return result;
+}
+
+static void
+backend_ws_send_control (struct app_context *ctx,
+ 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;
+ }
+
+ backend_ws_send_control (ctx, opcode, data, len);
+}
+
+static void
+backend_ws_fail (struct app_context *ctx, enum ws_status reason)
+{
+ uint8_t payload[2] = { reason << 8, reason };
+ backend_ws_send_control (ctx, WS_OPCODE_CLOSE, payload, sizeof payload);
+
+ // TODO: set the close timer, ignore all further incoming input (either set
+ // some flag for the case that we're in the middle of backend_ws_push(),
+ // and/or add a mechanism to stop the caller from polling the socket for
+ // reads).
+ // TODO: set the state to FAILED (not CLOSED as that means the TCP
+ // connection is closed) and wait until all is sent?
+ // TODO: make sure we don't send pings after the close
+}
+
+static bool
+backend_ws_on_frame_header (void *user_data, const struct ws_parser *parser)
+{
+ struct app_context *ctx = user_data;
+ struct ws_context *self = &ctx->ws;
+
+ // 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 // client -> server payload must 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)
+ backend_ws_fail (ctx, WS_STATUS_PROTOCOL_ERROR);
+ else if (parser->payload_len > BACKEND_WS_MAX_PAYLOAD_LEN)
+ backend_ws_fail (ctx, WS_STATUS_MESSAGE_TOO_BIG);
+ else
+ return true;
+ return false;
+}
+
+static bool
+backend_ws_on_control_frame
+ (struct app_context *ctx, const struct ws_parser *parser)
+{
+ switch (parser->opcode)
+ {
+ case WS_OPCODE_CLOSE:
+ // TODO: confirm the close
+ // TODO: change the state to CLOSING
+ // TODO: call "on_close"
+ // NOTE: the reason is an empty string if omitted
+ break;
+ case WS_OPCODE_PING:
+ backend_ws_send_control (ctx, WS_OPCODE_PONG,
+ parser->input.str, parser->payload_len);
+ break;
+ case WS_OPCODE_PONG:
+ // Not sending any pings but w/e
+ break;
+ default:
+ // Unknown control frame
+ backend_ws_fail (ctx, WS_STATUS_PROTOCOL_ERROR);
+ return false;
+ }
+ return true;
+}
+
+static bool
+backend_ws_on_message (struct app_context *ctx,
+ enum ws_opcode type, const void *data, size_t len)
+{
+ struct ws_context *self = &ctx->ws;
+
+ if (type != WS_OPCODE_TEXT)
+ {
+ backend_ws_fail (ctx, WS_STATUS_UNSUPPORTED_DATA);
+ return false;
+ }
+
+ if (!self->response_buffer)
+ {
+ // TODO: warn about unexpected messages
+ return true;
+ }
+
+ str_append_data (self->response_buffer, data, len);
+ // TODO: exit the event loop
+ return true;
+}
+
+static bool
+backend_ws_on_frame (void *user_data, const struct ws_parser *parser)
+{
+ struct app_context *ctx = user_data;
+ struct ws_context *self = &ctx->ws;
+ if (ws_is_control_frame (parser->opcode))
+ return backend_ws_on_control_frame (ctx, parser);
+
+ // TODO: do this rather in "on_frame_header"
+ if (self->message_data.len + parser->payload_len
+ > BACKEND_WS_MAX_PAYLOAD_LEN)
+ {
+ backend_ws_fail (ctx, WS_STATUS_MESSAGE_TOO_BIG);
+ return false;
+ }
+
+ 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->parser.input.str, self->parser.input.len))
+ {
+ backend_ws_fail (ctx, WS_STATUS_INVALID_PAYLOAD_DATA);
+ return false;
+ }
+
+ bool result = backend_ws_on_message (ctx, self->message_opcode,
+ self->message_data.str, self->message_data.len);
+ str_reset (&self->message_data);
+ return result;
+}
+
+static bool
+backend_ws_connect (struct app_context *ctx, struct error **e)
+{
+ struct ws_context *self = &ctx->ws;
+ 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");
+
+ 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");
+
+ // FIXME: should include "?UF_QUERY" as well, if present
+ char *url_path = xstrndup (self->endpoint +
+ self->url.field_data[UF_PATH].off,
+ self->url.field_data[UF_PATH].len);
+
+ if (!backend_ws_establish_connection (ctx, url_host, url_port, e))
+ goto fail_1;
+
+ if (use_tls && !backend_ws_initialize_tls (ctx, 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_init (&key_b64);
+ base64_encode (key, sizeof key, &key_b64);
+
+ free (self->key);
+ char *key_b64_string = self->key = str_steal (&key_b64);
+
+ struct str request;
+ str_init (&request);
+
+ str_append_printf (&request, "GET %s HTTP/1,1\r\n", url_path);
+ // 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);
+ 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 (ctx, request.str, request.len);
+ str_free (&request);
+ if (!written)
+ {
+ error_set (e, "connection failed");
+ goto fail_2;
+ }
+
+ http_parser_init (&self->hp, HTTP_RESPONSE);
+ str_reset (&self->field);
+ str_reset (&self->value);
+ // TODO: write a function to free self->headers and call it here
+ ws_parser_free (&self->parser);
+ ws_parser_init (&self->parser);
+ self->parser.on_frame_header = backend_ws_on_frame_header;
+ self->parser.on_frame = backend_ws_on_frame;
+ self->parser.user_data = ctx;
+
+ ev_io_init (&self->read_watcher,
+ backend_ws_on_fd_ready, self->server_fd, EV_READ);
+ self->read_watcher.data = ctx;
+ ev_io_start (EV_DEFAULT_ &self->read_watcher);
+
+ // TODO: set a timeout timer
+ // TODO: run an event loop to process the handshake response
+
+ result = true;
+
+fail_2:
+ if (!result)
+ {
+ xclose (self->server_fd);
+ self->server_fd = -1;
+ }
+fail_1:
+ free (url_schema);
+ free (url_host);
+ free (url_port);
+ free (url_path);
+ return result;
+}
+
+static bool
+backend_ws_make_call (struct app_context *ctx,
+ const char *request, bool expect_content, struct str *buf, struct error **e)
+{
+ struct ws_context *self = &ctx->ws;
+
+ if (self->server_fd == -1)
+ if (!backend_ws_connect (ctx, e))
+ return false;
+
+ while (true)
+ {
+ if (backend_ws_send_message (ctx,
+ WS_OPCODE_TEXT, request, strlen (request)))
+ break;
+ print_status ("connection failed");
+ if (!backend_ws_connect (ctx, e))
+ return false;
+ }
+
+ if (expect_content)
+ {
+ self->response_buffer = buf;
+ // TODO: run an event loop to retrieve the answer into "buf"
+ self->response_buffer = NULL;
+ }
+ return true;
+}
+
+static void
+backend_ws_destroy (struct app_context *ctx)
+{
+ // TODO
+}
+
+static struct backend_iface g_backend_ws =
+{
+ .init = backend_ws_init,
+ .add_header = backend_ws_add_header,
+ .make_call = backend_ws_make_call,
+ .destroy = backend_ws_destroy,
+};
+
+// --- cURL backend ------------------------------------------------------------
+
+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_init (&parameters);
+ parameters.free = 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_init (struct app_context *ctx,
+ const char *endpoint, struct http_parser_url *url)
+{
+ (void) url;
+ curl_context_init (&ctx->curl);
+
+ CURL *curl;
+ if (!(ctx->curl.curl = curl = curl_easy_init ()))
+ exit_fatal ("cURL initialization failed");
+
+ ctx->curl.headers = NULL;
+ ctx->curl.headers = curl_slist_append
+ (ctx->curl.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, ctx->curl.curl_error)
+ || curl_easy_setopt (curl, CURLOPT_HTTPHEADER, ctx->curl.headers)
+ || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER,
+ ctx->trust_all ? 0L : 1L)
+ || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST,
+ ctx->trust_all ? 0L : 2L)
+ || curl_easy_setopt (curl, CURLOPT_URL, endpoint))
+ exit_fatal ("cURL setup failed");
+}
+
+static void
+backend_curl_add_header (struct app_context *ctx, const char *header)
+{
+ ctx->curl.headers = curl_slist_append (ctx->curl.headers, header);
+ if (curl_easy_setopt (ctx->curl.curl,
+ CURLOPT_HTTPHEADER, ctx->curl.headers))
+ exit_fatal ("cURL setup failed");
+}
+
+#define RPC_FAIL(...) \
+ BLOCK_START \
+ error_set (e, __VA_ARGS__); \
+ return false; \
+ BLOCK_END
+
+static bool
+backend_curl_make_call (struct app_context *ctx,
+ const char *request, bool expect_content, struct str *buf, struct error **e)
+{
+ CURL *curl = ctx->curl.curl;
+ if (curl_easy_setopt (curl, CURLOPT_POSTFIELDS, request)
+ || curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE_LARGE,
+ (curl_off_t) -1)
+ || curl_easy_setopt (curl, CURLOPT_WRITEDATA, buf)
+ || curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_callback))
+ RPC_FAIL ("cURL setup failed");
+
+ CURLcode ret;
+ if ((ret = curl_easy_perform (curl)))
+ RPC_FAIL ("HTTP request failed: %s", ctx->curl.curl_error);
+
+ long code;
+ char *type;
+ if (curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &code)
+ || curl_easy_getinfo (curl, CURLINFO_CONTENT_TYPE, &type))
+ RPC_FAIL ("cURL info retrieval failed");
+
+ if (code != 200)
+ RPC_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 app_context *ctx)
+{
+ curl_slist_free_all (ctx->curl.headers);
+ curl_easy_cleanup (ctx->curl.curl);
+}
+
+static struct backend_iface g_backend_curl =
+{
+ .init = backend_curl_init,
+ .add_header = backend_curl_add_header,
+ .make_call = backend_curl_make_call,
+ .destroy = backend_curl_destroy,
+};
+
// --- Main program ------------------------------------------------------------
#define PARSE_FAIL(...) \
@@ -529,68 +2277,6 @@ is_valid_json_rpc_params (json_t *v)
return json_is_array (v) || json_is_object (v);
}
-static bool
-isspace_ascii (int c)
-{
- return strchr (" \f\n\r\t\v", c);
-}
-
-static size_t
-write_callback (char *ptr, size_t size, size_t nmemb, void *user_data)
-{
- struct str *buf = user_data;
- str_append_data (buf, ptr, size * nmemb);
- return size * nmemb;
-}
-
-#define RPC_FAIL(...) \
- BLOCK_START \
- print_error (__VA_ARGS__); \
- goto fail; \
- BLOCK_END
-
-static bool
-try_advance (const char **p, const char *text)
-{
- size_t len = strlen (text);
- if (strncmp (*p, text, len))
- return false;
-
- *p += len;
- return true;
-}
-
-static bool
-validate_content_type (const char *type)
-{
- const char *content_types[] =
- {
- "application/json-rpc", // obsolete
- "application/json"
- };
- const char *tails[] =
- {
- "; charset=utf-8",
- "; charset=UTF-8",
- ""
- };
-
- bool found = false;
- for (size_t i = 0; i < N_ELEMENTS (content_types); i++)
- if ((found = try_advance (&type, content_types[i])))
- break;
- if (!found)
- return false;
-
- for (size_t i = 0; i < N_ELEMENTS (tails); i++)
- if ((found = try_advance (&type, tails[i])))
- break;
- if (!found)
- return false;
-
- return !*type;
-}
-
static void
make_json_rpc_call (struct app_context *ctx,
const char *method, json_t *id, json_t *params)
@@ -617,35 +2303,17 @@ make_json_rpc_call (struct app_context *ctx,
struct str buf;
str_init (&buf);
- if (curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDS, req_utf8)
- || curl_easy_setopt (ctx->curl, CURLOPT_POSTFIELDSIZE_LARGE,
- (curl_off_t) -1)
- || curl_easy_setopt (ctx->curl, CURLOPT_WRITEDATA, &buf)
- || curl_easy_setopt (ctx->curl, CURLOPT_WRITEFUNCTION, write_callback))
- RPC_FAIL ("cURL setup failed");
-
- CURLcode ret;
- if ((ret = curl_easy_perform (ctx->curl)))
- RPC_FAIL ("HTTP request failed: %s", ctx->curl_error);
-
- long code;
- char *type;
- if (curl_easy_getinfo (ctx->curl, CURLINFO_RESPONSE_CODE, &code)
- || curl_easy_getinfo (ctx->curl, CURLINFO_CONTENT_TYPE, &type))
- RPC_FAIL ("cURL info retrieval failed");
-
- if (code != 200)
- RPC_FAIL ("unexpected HTTP response code: %ld", code);
+ struct error *e = NULL;
+ if (!ctx->backend->make_call (ctx, req_utf8, id != NULL, &buf, &e))
+ {
+ print_error ("%s", e->message);
+ error_free (e);
+ goto fail;
+ }
bool success = false;
if (id)
- {
- if (!type)
- print_warning ("missing `Content-Type' header");
- else if (!validate_content_type (type))
- print_warning ("unexpected `Content-Type' header: %s", type);
success = parse_response (ctx, &buf);
- }
else
{
printf ("[Notification]\n");
@@ -706,7 +2374,8 @@ process_input (struct app_context *ctx, char *user_input)
while (true)
{
- // Jansson is too stupid to just tell us that there was nothing
+ // 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)
@@ -899,35 +2568,34 @@ main (int argc, char *argv[])
init_colors (&g_ctx);
load_config (&g_ctx);
- if (strncmp (endpoint, "http://", 7)
- && strncmp (endpoint, "https://", 8))
- exit_fatal ("the endpoint address must begin with"
- " either `http://' or `https://'");
-
- CURL *curl;
- if (!(g_ctx.curl = curl = curl_easy_init ()))
- exit_fatal ("cURL initialization failed");
+ struct 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);
+
+ if (!strcasecmp_ascii (url_schema, "http")
+ || !strcasecmp_ascii (url_schema, "https"))
+ g_ctx.backend = &g_backend_curl;
+ else if (!strcasecmp_ascii (url_schema, "ws")
+ || !strcasecmp_ascii (url_schema, "wss"))
+ g_ctx.backend = &g_backend_ws;
+ else
+ exit_fatal ("unsupported protocol");
- struct curl_slist *headers = NULL;
- headers = curl_slist_append (headers, "Content-Type: application/json");
+ free (url_schema);
+ g_ctx.backend->init (&g_ctx, endpoint, &url);
if (origin)
{
origin = xstrdup_printf ("Origin: %s", origin);
- headers = curl_slist_append (headers, origin);
+ g_ctx.backend->add_header (&g_ctx, origin);
}
- if (curl_easy_setopt (curl, CURLOPT_POST, 1L)
- || curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 1L)
- || curl_easy_setopt (curl, CURLOPT_ERRORBUFFER, g_ctx.curl_error)
- || curl_easy_setopt (curl, CURLOPT_HTTPHEADER, headers)
- || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER,
- g_ctx.trust_all ? 0L : 1L)
- || curl_easy_setopt (curl, CURLOPT_SSL_VERIFYHOST,
- g_ctx.trust_all ? 0L : 2L)
- || curl_easy_setopt (curl, CURLOPT_URL, endpoint))
- exit_fatal ("cURL setup failed");
-
// We only need to convert to and from the terminal encoding
setlocale (LC_CTYPE, "");
@@ -974,6 +2642,10 @@ main (int argc, char *argv[])
RL_PROMPT_START_IGNORE, reset_attrs, RL_PROMPT_END_IGNORE);
}
+ // 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);
+
// readline 6.3 doesn't immediately redraw the terminal upon reception
// of SIGWINCH, so we must run it in an event loop to remediate that
struct ev_loop *loop = EV_DEFAULT;
@@ -995,8 +2667,6 @@ main (int argc, char *argv[])
ev_run (loop, 0);
putchar ('\n');
- ev_loop_destroy (loop);
-
// User has terminated the program, let's save the history and clean up
char *dir = xstrdup (history_path);
(void) mkdir_with_parents (dirname (dir), NULL);
@@ -1009,10 +2679,10 @@ main (int argc, char *argv[])
free (history_path);
iconv_close (g_ctx.term_from_utf8);
iconv_close (g_ctx.term_to_utf8);
- curl_slist_free_all (headers);
+ g_ctx.backend->destroy (&g_ctx);
free (origin);
- curl_easy_cleanup (curl);
str_map_free (&g_ctx.config);
free_terminal ();
+ ev_loop_destroy (loop);
return EXIT_SUCCESS;
}