From b0c712c146b208ac9dad5fd5829a2bc38773a4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 29 Mar 2015 00:37:12 +0100 Subject: [PATCH] Stubplement WebSockets --- .gitmodules | 3 + CMakeLists.txt | 9 +- README | 20 +- http-parser | 1 + json-rpc-shell.c | 1900 +++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 1811 insertions(+), 122 deletions(-) create mode 160000 http-parser 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 index 0000000..5d414fc --- /dev/null +++ b/http-parser @@ -0,0 +1 @@ +Subproject commit 5d414fcb4b2ccc1ce9d6063292f9c63c9ec67b04 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 #include #include #include +#include + #include #include #include #include #include +#include #include #include +// --- 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 (¶meters); + parameters.free = free; + parameters.key_xfrm = tolower_ascii_strxfrm; + + bool result = http_parse_media_type + (content_type, &type, &subtype, ¶meters); + if (!result) + goto end; + + if (strcasecmp_ascii (type, "application") + || (strcasecmp_ascii (subtype, "json") && + strcasecmp_ascii (subtype, "json-rpc" /* obsolete */))) + result = false; + + const char *charset = str_map_find (¶meters, "charset"); + if (charset && strcasecmp_ascii (charset, "UTF-8")) + result = false; + + // Currently ignoring all unknown parametrs + +end: + free (type); + free (subtype); + str_map_free (¶meters); + return result; +} + +static void +backend_curl_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://'"); + 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"); - CURL *curl; - if (!(g_ctx.curl = curl = curl_easy_init ())) - exit_fatal ("cURL initialization failed"); + char *url_schema = xstrndup (endpoint + + url.field_data[UF_SCHEMA].off, + url.field_data[UF_SCHEMA].len); - struct curl_slist *headers = NULL; - headers = curl_slist_append (headers, "Content-Type: application/json"); + 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"); + 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; }