From e60aa0c5779bce199020236c0ac3a4beace571cb Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 20 Jun 2019 08:19:29 +0200 Subject: Implement additional framing functions (Fixes #3) --- README.md | 2 + builtins.c | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- config.c | 4 ++ config.h | 2 + plugin.c | 14 +++++++ websocket.c | 16 ++++++-- websocket.h | 1 + websocksy.c | 9 +++-- websocksy.h | 3 +- 9 files changed, 162 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index fda4e08..3c8df33 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,8 @@ framing functions (both built in and loaded from plugins). * `auto`: Send all data immediately, with the `text` type if the content was detected as valid UTF-8 string, otherwise use a `binary` frame * `binary`: Send all data immediately as a `binary` frame +* `separator`: Waits until a variable-length separator is found in the stream and sends data up to and including that position as binary frame. Takes as parameter the separator string. The escape sequences `\r`, `\t`, `\n`, `\0`, `\f` and `\\` are recognized, arbitrary bytes can be specified hexadecimally using `\x` +* `newline`: Waits until a newline sequence is found in the stream and sends data up to and including that position as text frame if all data is valid UTF-8 (data frame otherwise). The configuration string may be one of `crlf`, `lfcr`, `lf`, `cr` specifying the sequence to look for. # Configuration / Usage diff --git a/builtins.c b/builtins.c index 5c1dfb8..41d76d4 100644 --- a/builtins.c +++ b/builtins.c @@ -1,4 +1,6 @@ #include +#include +#include #include "websocksy.h" #include "builtins.h" @@ -151,7 +153,7 @@ int64_t framing_auto(uint8_t* data, size_t length, size_t last_read, ws_operatio } //if valid utf8, send as text frame - if(valid){ + if(valid && opcode){ *opcode = ws_frame_text; } @@ -165,12 +167,121 @@ int64_t framing_binary(uint8_t* data, size_t length, size_t last_read, ws_operat return length; } -int64_t framing_separator(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, const char* config){ - //TODO implement separator framer - return length; +/* + * The `separator` framing function waits until a variable-length separator is found in the stream and sends all + * data up to and including that separator as binary frame. The configuration string is used as the separator, with + * the escape sequences \r, \t, \n, \0, \f, \\ being recognized as their ASCII expressions. Arbitrary bytes can be + * specified hexadecimally using the syntax \x + */ +typedef struct /*_separator_framing_config*/ { + size_t length; + uint8_t* separator; +} framing_separator_config; + +int64_t framing_separator(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, const char* framing_config){ + size_t u, p = 0; + unsigned hex; + framing_separator_config* config = framing_data ? *framing_data : NULL; + + if(data && !config){ + //parse configuration + config = calloc(1, sizeof(framing_separator_config)); + config->separator = (uint8_t*) strdup(framing_config); + for(u = 0; config->separator[u]; u++){ + config->separator[p] = config->separator[u]; + if(config->separator[u] == '\\'){ + switch(config->separator[u + 1]){ + case 0: + u--; + //fall through + case '0': + config->separator[p] = 0; + break; + case 't': + config->separator[p] = '\t'; + break; + case 'n': + config->separator[p] = '\n'; + break; + case 'f': + config->separator[p] = '\f'; + break; + case 'r': + config->separator[p] = '\r'; + break; + case '\\': + config->separator[p] = '\\'; + break; + case 'x': + if(!isxdigit(config->separator[u + 2]) + || !isxdigit(config->separator[u + 3])){ + fprintf(stderr, "Prematurely terminated hex byte sequence in separator framing function\n"); + return -1; + } + sscanf((char*) (config->separator + u + 3), "%02x", &hex); + config->separator[p] = hex; + u += 2; + } + u++; + } + p++; + } + config->length = p; + *framing_data = config; + } + else if(!data && config){ + //free parsed configuration + free(config->separator); + config->separator = NULL; + config->length = 0; + free(config); + return 0; + } + + if(config->length){ + for(u = 0; u < last_read && (last_read - u) >= config->length; u++){ + if(!memcmp(data + (length - last_read) + u, config->separator, config->length)){ + return (length - last_read) + u + config->length; + } + } + } + else{ + return length; + } + return 0; } -int64_t framing_newline(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, const char* config){ - //TODO implement separator framer - return length; +/* + * The `newline` framing function waits until a newline sequence is found in the buffer and sends all data up to and + * including the newline as text frame, if the data is detected as valid UTF-8. Otherwise, a binary frame is used. + * The configuration string may be one of + * * crlf + * * lfcr + * * lf + * * cr + */ +int64_t framing_newline(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, const char* framing_config){ + int64_t bytes = 0; + char* expression = "\\r\\n"; + + if(data && (!framing_data || !(*framing_data))){ + if(!strcmp(framing_config, "crlf")){ + expression = "\\r\\n"; + } + if(!strcmp(framing_config, "lfcr")){ + expression = "\\n\\r"; + } + if(!strcmp(framing_config, "lf")){ + expression = "\\n"; + } + if(!strcmp(framing_config, "cr")){ + expression = "\\r"; + } + bytes = framing_separator(data, length, last_read, opcode, framing_data, expression); + } + else{ + bytes = framing_separator(data, length, last_read, opcode, framing_data, NULL); + } + + return framing_auto(data, bytes, 0, opcode, NULL, NULL); } diff --git a/config.c b/config.c index 0b57714..2d50f3e 100644 --- a/config.c +++ b/config.c @@ -6,11 +6,13 @@ #include "config.h" #include "plugin.h" +/* Configuration file parser state */ static enum /*_config_file_section*/ { cfg_main, cfg_backend } config_section = cfg_main; +/* Evaluate a single line within a configuration file */ static int config_file_line(ws_config* config, char* key, char* value, size_t line_no){ if(!strcmp(key, "port")){ free(config->port); @@ -37,6 +39,7 @@ static int config_file_line(ws_config* config, char* key, char* value, size_t li return 0; } +/* Read and parse a configuration file */ int config_parse_file(ws_config* config, char* filename){ ssize_t line_current; size_t line_alloc = 0, line_no = 1, key_len; @@ -105,6 +108,7 @@ int config_parse_file(ws_config* config, char* filename){ return 0; } +/* Parse an argument list */ int config_parse_arguments(ws_config* config, int argc, char** argv){ size_t u; char* option = NULL, *value = NULL; diff --git a/config.h b/config.h index 87502fb..e39f13e 100644 --- a/config.h +++ b/config.h @@ -1,8 +1,10 @@ +/* Core configuration storage */ typedef struct /*_websocksy_config*/ { char* host; char* port; ws_backend backend; } ws_config; +/* Configuration parsing functions */ int config_parse_file(ws_config* config, char* filename); int config_parse_arguments(ws_config* config, int argc, char** argv); diff --git a/plugin.c b/plugin.c index d8500fe..f48707f 100644 --- a/plugin.c +++ b/plugin.c @@ -18,6 +18,7 @@ static char** framing_function_name = NULL; static size_t attached_libraries = 0; static void** attached_library = NULL; +/* Attach a shared object and register it to be unloaded at shutdown */ static void* plugin_attach(char* path){ void* module = dlopen(path, RTLD_NOW); @@ -39,6 +40,12 @@ static void* plugin_attach(char* path){ return module; } +/* + * Try to load all framing plugins by attaching all shared objects in the + * plugin path that + * * are not backend plugins + * * have a filename ending in .so + */ int plugin_framing_load(char* path){ DIR* directory = opendir(path); struct dirent* file = NULL; @@ -74,6 +81,10 @@ int plugin_framing_load(char* path){ return 0; } +/* + * Load an external backend plugin by constructing it's module path and attaching it, + * then try to resolve the required backend symbols + */ int plugin_backend_load(char* path, char* backend_requested, ws_backend* backend){ char plugin_path[MAX_PLUGIN_PATH] = ""; void* handle = NULL; @@ -103,6 +114,7 @@ int plugin_backend_load(char* path, char* backend_requested, ws_backend* backend return 0; } +/* Register a new framing function to the library */ int plugin_register_framing(char* name, ws_framing func){ size_t u; @@ -129,6 +141,7 @@ int plugin_register_framing(char* name, ws_framing func){ return 0; } +/* Query the framing function library */ ws_framing plugin_framing(char* name){ size_t u; @@ -146,6 +159,7 @@ ws_framing plugin_framing(char* name){ return plugin_framing("auto"); } +/* Release all allocated memory, detach all loaded shared objects */ void plugin_cleanup(){ size_t u; diff --git a/websocket.c b/websocket.c index fe4a39a..710178f 100644 --- a/websocket.c +++ b/websocket.c @@ -19,6 +19,10 @@ #define WS_GET_MASK(a) ((a & 0x80) >> 7) #define WS_GET_LEN(a) ((a & 0x7F)) +/* + * Close and shut down a WebSocket connection, including a connected + * peer stream. Frees all resources associated with either connection. + */ int ws_close(websocket* ws, ws_close_reason code, char* reason){ size_t p; ws_peer_info empty_peer = { @@ -90,6 +94,7 @@ int ws_close(websocket* ws, ws_close_reason code, char* reason){ return 0; } +/* Accept a new WebSocket connection */ int ws_accept(int listen_fd){ websocket ws = { .ws_fd = accept(listen_fd, NULL, NULL), @@ -99,6 +104,7 @@ int ws_accept(int listen_fd){ return client_register(&ws); } +/* Handle data in the NEW state (expecting a HTTP negotiation) */ static int ws_handle_new(websocket* ws){ size_t u; char* path, *proto; @@ -126,6 +132,7 @@ static int ws_handle_new(websocket* ws){ return 0; } +/* Handle end of HTTP header data and upgrade the connection */ static int ws_upgrade_http(websocket* ws){ if(ws->websocket_version == 13 && ws->socket_key @@ -144,7 +151,7 @@ static int ws_upgrade_http(websocket* ws){ return 0; } - //calculate the websocket accept key, which for some reason is defined as + //calculate the websocket accept key, which for some reason is defined (RFC 4.2.2.5.4) as //base64(sha1(concat(trim(client-key), "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))) //requiring not one but 2 unnecessarily complex operations size_t encode_offset = 0; @@ -193,6 +200,7 @@ static int ws_upgrade_http(websocket* ws){ return 1; } +/* Handle incoming HTTP header lines */ static int ws_handle_http(websocket* ws){ char* header, *value; ssize_t p; @@ -296,8 +304,8 @@ static size_t ws_frame(websocket* ws){ //ignoring it for now } - //calculate the payload length (could've used a uint64 and be done with it...) - //TODO test this for the bigger frames + //calculate the payload length from one of 3 cases (RFC 5.2) + //could've used a uint64 and be done with it... payload_length = WS_GET_LEN(ws->read_buffer[1]); if(WS_GET_MASK(ws->read_buffer[1])){ if(ws->read_buffer_offset < 6){ @@ -401,6 +409,7 @@ static size_t ws_frame(websocket* ws){ return ((payload - ws->read_buffer) + payload_length); } +/* Construct and send a WebSocket frame */ int ws_send_frame(websocket* ws, ws_operation opcode, uint8_t* data, size_t len){ fprintf(stderr, "Peer -> WS %lu bytes (%02X)\n", len, opcode); uint8_t frame_header[WS_FRAME_HEADER_LEN]; @@ -433,6 +442,7 @@ int ws_send_frame(websocket* ws, ws_operation opcode, uint8_t* data, size_t len) return 0; } +/* Handle incoming data on a WebSocket client */ int ws_data(websocket* ws){ ssize_t bytes_read, n, bytes_left = sizeof(ws->read_buffer) - ws->read_buffer_offset; int rv = 0; diff --git a/websocket.h b/websocket.h index f2a4500..f57fccc 100644 --- a/websocket.h +++ b/websocket.h @@ -1,5 +1,6 @@ #include "websocksy.h" +/* WebSocket connection handling functions */ int ws_close(websocket* ws, ws_close_reason code, char* reason); int ws_accept(int listen_fd); int ws_send_frame(websocket* ws, ws_operation opcode, uint8_t* data, size_t len); diff --git a/websocksy.c b/websocksy.c index 33ee794..c2f0631 100644 --- a/websocksy.c +++ b/websocksy.c @@ -23,6 +23,7 @@ /* TODO * - TLS * - pings + * - continuation */ /* Main loop condition, to be set from signal handler */ @@ -240,9 +241,11 @@ static int ws_peer_data(websocket* ws){ fprintf(stderr, "Overrun by framing function, have %lu + %lu bytes, framed %lu\n", ws->peer_buffer_offset, bytes_read, bytes_framed); return 0; } - //send the indicated n bytes to the websocket peer - if(ws_send_frame(ws, opcode, ws->peer_buffer, bytes_framed)){ - return 1; + if(opcode != ws_frame_discard){ + //send the indicated n bytes to the websocket peer + if(ws_send_frame(ws, opcode, ws->peer_buffer, bytes_framed)){ + return 1; + } } //copy back memmove(ws->peer_buffer, ws->peer_buffer + bytes_framed, (ws->peer_buffer_offset + bytes_read) - bytes_framed); diff --git a/websocksy.h b/websocksy.h index b2b2285..9d9faf1 100644 --- a/websocksy.h +++ b/websocksy.h @@ -36,7 +36,8 @@ typedef enum { ws_frame_binary = 2, ws_frame_close = 8, ws_frame_ping = 9, - ws_frame_pong = 10 + ws_frame_pong = 10, + ws_frame_discard /* Special opcode to discard bytes from the peer buffer */ } ws_operation; /* -- cgit v1.2.3