diff options
-rw-r--r-- | README.md | 5 | ||||
-rw-r--r-- | plugins/framing_json.c | 279 | ||||
-rw-r--r-- | plugins/framing_json.md | 16 | ||||
-rw-r--r-- | plugins/makefile | 2 |
4 files changed, 299 insertions, 3 deletions
@@ -144,8 +144,9 @@ The integrated default backend takes the following configuration arguments: This repository comes with some plugins already included, in addition to the built-in backend and framing functions. Documentation for these resides in the `plugins/` directory. -* [backend\_file](plugins/backend_file.md) -* [framing\_fixedlength](plugins/framing_fixedlength.md) +* [backend\_file](plugins/backend_file.md) - Read connection peers from dynamic file paths +* [framing\_fixedlength](plugins/framing_fixedlength.md) - Frame the peer stream using fixed length segments +* [framing\_json](plugins/framing_json.md) - Segment the peer stream into JSON entities # Building diff --git a/plugins/framing_json.c b/plugins/framing_json.c new file mode 100644 index 0000000..677e4e7 --- /dev/null +++ b/plugins/framing_json.c @@ -0,0 +1,279 @@ +#include <stdio.h> +#include <ctype.h> +#include <string.h> + +#include "../websocksy.h" + +static ssize_t json_length(char* data, size_t length); +static ssize_t json_length_string(char* data, size_t length); + +static ssize_t json_length_array(char* data, size_t length){ + size_t current_offset = 1; + int64_t next_length = 0; + + if(!length){ + return 0; + } + + if(data[0] != '['){ + return -1; + } + + do{ + //skip whitespace + for(; current_offset < length && isspace(data[current_offset]); current_offset++){ + } + + //empty array + if(!next_length && data[current_offset] == ']'){ + break; + } + + //get length + next_length = json_length(data + current_offset, length - current_offset); + if(next_length <= 0){ + return next_length; + } + + current_offset += next_length; + //check for end or , + for(; current_offset < length && isspace(data[current_offset]); current_offset++){ + } + //the ordering matters, check for complete array before checking for eod + if(data[current_offset] == ']'){ + break; + } + if(current_offset >= length){ + return 0; + } + if(data[current_offset] != ','){ + break; + } + current_offset++; + } while(current_offset < length); + + if(data[current_offset] == ']'){ + return current_offset + 1; + } + + return -1; +} + +static ssize_t json_length_object(char* data, size_t length){ + size_t current_offset = 1; + int64_t next_length = 0; + + if(!length){ + return 0; + } + + if(data[0] != '{'){ + return -1; + } + + do{ + //skip whitespace + for(; current_offset < length && isspace(data[current_offset]); current_offset++){ + } + + //empty object + if(!next_length && data[current_offset] == '}'){ + break; + } + + //get key string length + next_length = json_length_string(data + current_offset, length - current_offset); + if(next_length <= 0){ + return next_length; + } + + current_offset += next_length; + //find colon + for(; current_offset < length && isspace(data[current_offset]); current_offset++){ + } + if(current_offset >= length - 1){ //need one more character + return 0; + } + else if(data[current_offset] != ':'){ + return -1; + } + current_offset++; + + //get value length + next_length = json_length(data + current_offset, length - current_offset); + if(next_length <= 0){ + return next_length; + } + + current_offset += next_length; + //check for end or , + for(; current_offset < length && isspace(data[current_offset]); current_offset++){ + } + //the ordering matters, check for complete object before checking for eod + if(data[current_offset] == '}'){ + break; + } + if(current_offset >= length){ + return 0; + } + if(data[current_offset] != ','){ + break; + } + current_offset++; + } while(current_offset < length); + + if(data[current_offset] == '}'){ + return current_offset + 1; + } + return -1; +} + +static ssize_t json_length_string(char* data, size_t length){ + size_t string_length = 0; + + if(!length){ + return 0; + } + + if(data[0] != '"'){ + return -1; + } + + //find terminating quotation mark not preceded by escape + for(string_length = 1; string_length < length + && isprint(data[string_length]) + && (data[string_length] != '"' || data[string_length - 1] == '\\'); string_length++){ + } + + //complete string found + if(data[string_length] == '"' && data[string_length - 1] != '\\'){ + return string_length + 1; + } + + //still missing data + if(string_length >= length){ + return 0; + } + + //anything else + return -1; +} + +static ssize_t json_length_value(char* data, size_t length){ + size_t value_length = 0; + + if(!length){ + return 0; + } + + if(data[0] == '-' || isdigit(data[0])){ + //a number consisting of [minus] int [.int] [eE[+-]] + //terminator (comma, space, eod, eoa, eoo) + //since this is not a full parser, just ensure all characters are from that general set... + for(value_length = 1; value_length < length && + (isdigit(data[value_length]) + || data[value_length] == '+' + || data[value_length] == '-' + || data[value_length] == '.' + || tolower(data[value_length]) == 'e'); value_length++){ + } + if(data[value_length] == ',' + || data[value_length] == ' ' + || data[value_length] == '}' + || data[value_length] == ']'){ + return value_length; + } + if(value_length >= length){ + if(data[value_length - 1] == '.' + || tolower(data[value_length - 1]) == 'e' + || data[value_length - 1] == '-' + || data[value_length - 1] == '+'){ + return 0; + } + //numbers can be a value on their own... + return value_length; + } + } + + //match complete values + if(length >= 4 && !strncmp(data, "null", 4)){ + return 4; + } + else if(length >= 4 && !strncmp(data, "true", 4)){ + return 4; + } + else if(length >= 5 && !strncmp(data, "false", 5)){ + return 5; + } + + //match prefix values + if(length < 4 && !strncmp(data, "null", length)){ + return 0; + } + else if(length < 4 && !strncmp(data, "true", length)){ + return 0; + } + else if(length < 5 && !strncmp(data, "false", length)){ + return 0; + } + + //invalid value + return -1; +} + +static ssize_t json_length(char* data, size_t length){ + size_t total_length = 0, current_offset = 0; + ssize_t rv = 0; + + if(!length){ + return 0; + } + + //advance to the first non-blank + for(current_offset = 0; current_offset < length && isspace(data[current_offset]); current_offset++){ + } + + switch(data[current_offset]){ + case '[': + rv = json_length_array(data + current_offset, length - current_offset); + break; + case '{': + rv = json_length_object(data + current_offset, length - current_offset); + break; + case '"': + rv = json_length_string(data + current_offset, length - current_offset); + break; + default: + //false null true + //0-9 | - -> number + rv = json_length_value(data + current_offset, length - current_offset); + break; + } + + if(rv <= 0){ + return rv; + } + + return current_offset + rv; +} + +static int64_t framing_json(uint8_t* data, size_t length, size_t last_read, ws_operation* opcode, void** framing_data, const char* config){ + ssize_t data_length = json_length(data, length); + + if(data_length > 0){ + *opcode = ws_frame_text; + return data_length; + } + //incomplete + else if(data_length == 0){ + return 0; + } + //failed to parse as json, just dump it + else{ + return length; + } +} + +static void __attribute__((constructor)) init(){ + core_register_framing("json", framing_json); +} diff --git a/plugins/framing_json.md b/plugins/framing_json.md new file mode 100644 index 0000000..c5a7e09 --- /dev/null +++ b/plugins/framing_json.md @@ -0,0 +1,16 @@ +# The `json` framing function + +The `json` framing function segments the peer stream at boundaries defined by complete JSON +entities (Objects, Arrays, Strings or Values) and forwards these as text frames. + +For example, on encountering the start of a JSON object at the beginning of the stream, it will read from the +peer until that objects closing brace and the send the entire object as a single text frame. + +The plugin does not implement a full parser and performs only rudimentary syntactic analysis to determine +the end of the current entity. When encountering an error, the entire buffer is forwarded as a binary frame. +It is possible to construct erroneous JSON objects that pass the parser without an error, however any +syntactically correct object should be forwarded correctly (please file a bug with an example otherwise). + +## Configuration + +The `json` framing function does not require any configuration. diff --git a/plugins/makefile b/plugins/makefile index feab4cd..b09466d 100644 --- a/plugins/makefile +++ b/plugins/makefile @@ -1,5 +1,5 @@ .PHONY: all clean -PLUGINS = backend_file.so framing_fixedlength.so +PLUGINS = backend_file.so framing_fixedlength.so framing_json.so CFLAGS += -fPIC -g -I../ LDFLAGS += -shared |