diff options
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | README | 10 | ||||
-rw-r--r-- | liberty-proto.c | 1279 | ||||
-rw-r--r-- | liberty.c | 300 | ||||
-rw-r--r-- | tests/liberty.c | 47 | ||||
-rw-r--r-- | tests/proto.c | 167 |
6 files changed, 1764 insertions, 41 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 75ef930..63f5f6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,7 @@ set (common_sources ${PROJECT_BINARY_DIR}/liberty-config.h) # Build some unit tests enable_testing () -foreach (name liberty) +foreach (name liberty proto) add_executable (test-${name} tests/${name}.c ${common_sources}) target_link_libraries (test-${name} ${common_libraries}) add_test (test-${name} test-${name}) @@ -2,13 +2,21 @@ liberty ======= `liberty' is a pseudolibrary of all the common C code I have written for various -projects. It can be thought of as a successor to my other C library, libxtnd. +projects. I used to copy-paste large swaths of code with minimal changes to it +and it slowly became awfully painful to synchronize. The project can be thought +of as a successor to my other C library, libxtnd. You are supposed to import it as a git submodule and include the main source file directly everywhere you need it. Everything is declared "static". I have come to the conclusion that this style of C programming suits me the best, as it allows me to nearly forget about the mess that are header files. +The API is intentionally unstable, which allows for easy refactoring. + +All development is done on Linux, but other POSIX-compatible operating systems +should be supported as well. They have an extremely low priority, however, and +I'm not testing them at all. + License ------- `liberty' is written by Přemysl Janouch <p.janouch@gmail.com>. diff --git a/liberty-proto.c b/liberty-proto.c new file mode 100644 index 0000000..9207a4a --- /dev/null +++ b/liberty-proto.c @@ -0,0 +1,1279 @@ +/* + * liberty-proto.c: the ultimate C unlibrary: protocols + * + * Copyright (c) 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +// Mostly parsers and various utilities relating to various protocols + +#ifdef LIBERTY_WANT_PROTO_IRC + +// --- IRC utilities ----------------------------------------------------------- + +struct irc_message +{ + struct str_map tags; ///< IRC 3.2 message tags + char *prefix; ///< Message prefix + char *command; ///< IRC command + struct str_vector params; ///< Command parameters +}; + +static void +irc_parse_message_tags (const char *tags, struct str_map *out) +{ + struct str_vector v; + str_vector_init (&v); + split_str_ignore_empty (tags, ';', &v); + + for (size_t i = 0; i < v.len; i++) + { + char *key = v.vector[i], *equal_sign = strchr (key, '='); + if (equal_sign) + { + *equal_sign = '\0'; + str_map_set (out, key, xstrdup (equal_sign + 1)); + } + else + str_map_set (out, key, xstrdup ("")); + } + + str_vector_free (&v); +} + +static void +irc_parse_message (struct irc_message *msg, const char *line) +{ + str_map_init (&msg->tags); + msg->tags.free = free; + + msg->prefix = NULL; + msg->command = NULL; + str_vector_init (&msg->params); + + // IRC 3.2 message tags + if (*line == '@') + { + size_t tags_len = strcspn (++line, " "); + char *tags = xstrndup (line, tags_len); + irc_parse_message_tags (tags, &msg->tags); + free (tags); + + line += tags_len; + while (*line == ' ') + line++; + } + + // Prefix + if (*line == ':') + { + size_t prefix_len = strcspn (++line, " "); + msg->prefix = xstrndup (line, prefix_len); + line += prefix_len; + } + + // Command name + { + while (*line == ' ') + line++; + + size_t cmd_len = strcspn (line, " "); + msg->command = xstrndup (line, cmd_len); + line += cmd_len; + } + + // Arguments + while (true) + { + while (*line == ' ') + line++; + + if (*line == ':') + { + str_vector_add (&msg->params, ++line); + break; + } + + size_t param_len = strcspn (line, " "); + if (!param_len) + break; + + str_vector_add_owned (&msg->params, xstrndup (line, param_len)); + line += param_len; + } +} + +static void +irc_free_message (struct irc_message *msg) +{ + str_map_free (&msg->tags); + free (msg->prefix); + free (msg->command); + str_vector_free (&msg->params); +} + +static void +irc_process_buffer (struct str *buf, + void (*callback)(const struct irc_message *, const char *, void *), + void *user_data) +{ + char *start = buf->str, *end = start + buf->len; + for (char *p = start; p + 1 < end; p++) + { + // Split the input on newlines + if (p[0] != '\r' || p[1] != '\n') + continue; + + *p = 0; + + struct irc_message msg; + irc_parse_message (&msg, start); + callback (&msg, start, user_data); + irc_free_message (&msg); + + start = p + 2; + } + + // XXX: we might want to just advance some kind of an offset to avoid + // moving memory around unnecessarily. + str_remove_slice (buf, 0, start - buf->str); +} + +static int +irc_tolower (int c) +{ + if (c == '[') return '{'; + if (c == ']') return '}'; + if (c == '\\') return '|'; + if (c == '~') return '^'; + return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; +} + +static size_t +irc_strxfrm (char *dest, const char *src, size_t n) +{ + size_t len = strlen (src); + while (n-- && (*dest++ = irc_tolower (*src++))) + ; + return len; +} + +static int +irc_strcmp (const char *a, const char *b) +{ + int x; + while (*a || *b) + if ((x = irc_tolower (*a++) - irc_tolower (*b++))) + return x; + return 0; +} + +static int +irc_fnmatch (const char *pattern, const char *string) +{ + size_t pattern_size = strlen (pattern) + 1; + size_t string_size = strlen (string) + 1; + char x_pattern[pattern_size], x_string[string_size]; + irc_strxfrm (x_pattern, pattern, pattern_size); + irc_strxfrm (x_string, string, string_size); + return fnmatch (x_pattern, x_string, 0); +} + +#endif + +#ifdef LIBERTY_WANT_PROTO_HTTP + +// --- HTTP parsing ------------------------------------------------------------ + +// 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; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct http_protocol +{ + LIST_HEADER (struct http_protocol) + + char *name; ///< The protocol to upgrade to + char *version; ///< Version of the protocol, if any +}; + +static void +http_protocol_destroy (struct http_protocol *self) +{ + free (self->name); + free (self->version); + free (self); +} + +static bool +http_parse_upgrade (const char *upgrade, struct http_protocol **out) +{ + // HTTP grammar makes this more complicated than it should be + + bool result = false; + struct http_protocol *list = NULL; + struct http_protocol *tail = NULL; + + struct http_tokenizer t; + http_tokenizer_init (&t, upgrade, strlen (upgrade)); + + enum { + STATE_PROTOCOL_NAME, + STATE_SLASH, + STATE_PROTOCOL_VERSION, + STATE_EXPECT_COMMA + } state = STATE_PROTOCOL_NAME; + struct http_protocol *proto = NULL; + + while (true) + switch (state) + { + case STATE_PROTOCOL_NAME: + switch (http_tokenizer_next (&t, false)) + { + case HTTP_T_DELIMITER: + if (t.delimiter != ',') + goto end; + case HTTP_T_WHITESPACE: + break; + case HTTP_T_TOKEN: + proto = xcalloc (1, sizeof *proto); + proto->name = xstrdup (t.string.str); + LIST_APPEND_WITH_TAIL (list, tail, proto); + state = STATE_SLASH; + break; + case HTTP_T_EOF: + result = true; + default: + goto end; + } + break; + case STATE_SLASH: + switch (http_tokenizer_next (&t, false)) + { + case HTTP_T_DELIMITER: + if (t.delimiter == '/') + state = STATE_PROTOCOL_VERSION; + else if (t.delimiter == ',') + state = STATE_PROTOCOL_NAME; + else + goto end; + break; + case HTTP_T_WHITESPACE: + state = STATE_EXPECT_COMMA; + break; + case HTTP_T_EOF: + result = true; + default: + goto end; + } + break; + case STATE_PROTOCOL_VERSION: + switch (http_tokenizer_next (&t, false)) + { + case HTTP_T_TOKEN: + proto->version = xstrdup (t.string.str); + state = STATE_EXPECT_COMMA; + break; + default: + goto end; + } + break; + case STATE_EXPECT_COMMA: + switch (http_tokenizer_next (&t, false)) + { + case HTTP_T_DELIMITER: + if (t.delimiter != ',') + goto end; + state = STATE_PROTOCOL_NAME; + case HTTP_T_WHITESPACE: + break; + case HTTP_T_EOF: + result = true; + default: + goto end; + } + } + +end: + if (result) + *out = list; + else + LIST_FOR_EACH (struct http_protocol, iter, list) + http_protocol_destroy (iter); + + http_tokenizer_free (&t); + return result; +} + +#endif + +#ifdef LIBERTY_WANT_PROTO_SCGI + +// --- SCGI -------------------------------------------------------------------- + +enum scgi_parser_state +{ + SCGI_READING_NETSTRING_LENGTH, ///< The length of the header netstring + SCGI_READING_NAME, ///< Header name + SCGI_READING_VALUE, ///< Header value + SCGI_READING_CONTENT ///< Incoming data +}; + +struct scgi_parser +{ + enum scgi_parser_state state; ///< Parsing state + struct str input; ///< Input buffer + + struct str_map headers; ///< Headers parsed + + size_t headers_len; ///< Length of the netstring contents + struct str name; ///< Header name so far + struct str value; ///< Header value so far + + /// Finished parsing request headers. + /// Return false to abort further processing of input. + bool (*on_headers_read) (void *user_data); + + /// Content available; len == 0 means end of file. + /// Return false to abort further processing of input. + bool (*on_content) (void *user_data, const void *data, size_t len); + + void *user_data; ///< User data passed to callbacks +}; + +static void +scgi_parser_init (struct scgi_parser *self) +{ + memset (self, 0, sizeof *self); + + str_init (&self->input); + str_map_init (&self->headers); + self->headers.free = free; + str_init (&self->name); + str_init (&self->value); +} + +static void +scgi_parser_free (struct scgi_parser *self) +{ + str_free (&self->input); + str_map_free (&self->headers); + str_free (&self->name); + str_free (&self->value); +} + +static bool +scgi_parser_push (struct scgi_parser *self, + const void *data, size_t len, struct error **e) +{ + if (!len) + { + if (self->state != SCGI_READING_CONTENT) + { + error_set (e, "premature EOF"); + return false; + } + + // Indicate end of file + return self->on_content (self->user_data, NULL, 0); + } + + // Notice that this madness is significantly harder to parse than FastCGI; + // this procedure could also be optimized significantly + str_append_data (&self->input, data, len); + + bool keep_running = true; + while (keep_running) + switch (self->state) + { + case SCGI_READING_NETSTRING_LENGTH: + { + if (self->input.len < 1) + return true; + + char digit = *self->input.str; + // XXX: this allows for omitting the netstring length altogether + if (digit == ':') + { + self->state = SCGI_READING_NAME; + break; + } + + if (digit < '0' || digit >= '9') + { + error_set (e, "invalid header netstring"); + return false; + } + + size_t new_len = self->headers_len * 10 + (digit - '0'); + if (new_len < self->headers_len) + { + error_set (e, "header netstring is too long"); + return false; + } + self->headers_len = new_len; + str_remove_slice (&self->input, 0, 1); + break; + } + case SCGI_READING_NAME: + { + if (self->input.len < 1) + return true; + + char c = *self->input.str; + if (!self->headers_len) + { + // The netstring is ending but we haven't finished parsing it, + // or the netstring doesn't end with a comma + if (self->name.len || c != ',') + { + error_set (e, "invalid header netstring"); + return false; + } + self->state = SCGI_READING_CONTENT; + keep_running = self->on_headers_read (self->user_data); + } + else if (c != '\0') + str_append_c (&self->name, c); + else + self->state = SCGI_READING_VALUE; + + str_remove_slice (&self->input, 0, 1); + break; + } + case SCGI_READING_VALUE: + { + if (self->input.len < 1) + return true; + + char c = *self->input.str; + if (!self->headers_len) + { + // The netstring is ending but we haven't finished parsing it + error_set (e, "invalid header netstring"); + return false; + } + else if (c != '\0') + str_append_c (&self->value, c); + else + { + // We've got a name-value pair, let's put it in the map + str_map_set (&self->headers, + self->name.str, str_steal (&self->value)); + + str_reset (&self->name); + str_init (&self->value); + + self->state = SCGI_READING_NAME; + } + + str_remove_slice (&self->input, 0, 1); + break; + } + case SCGI_READING_CONTENT: + keep_running = self->on_content + (self->user_data, self->input.str, self->input.len); + str_remove_slice (&self->input, 0, self->input.len); + return keep_running; + } + return false; +} + +#endif + +#ifdef LIBERTY_WANT_PROTO_FASTCGI + +// --- FastCGI ----------------------------------------------------------------- + +// Constants from the FastCGI specification document + +#define FCGI_HEADER_LEN 8 + +#define FCGI_VERSION_1 1 +#define FCGI_NULL_REQUEST_ID 0 +#define FCGI_KEEP_CONN 1 + +enum fcgi_type +{ + FCGI_BEGIN_REQUEST = 1, + FCGI_ABORT_REQUEST = 2, + FCGI_END_REQUEST = 3, + FCGI_PARAMS = 4, + FCGI_STDIN = 5, + FCGI_STDOUT = 6, + FCGI_STDERR = 7, + FCGI_DATA = 8, + FCGI_GET_VALUES = 9, + FCGI_GET_VALUES_RESULT = 10, + FCGI_UNKNOWN_TYPE = 11, + FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE +}; + +enum fcgi_role +{ + FCGI_RESPONDER = 1, + FCGI_AUTHORIZER = 2, + FCGI_FILTER = 3 +}; + +enum fcgi_protocol_status +{ + FCGI_REQUEST_COMPLETE = 0, + FCGI_CANT_MPX_CONN = 1, + FCGI_OVERLOADED = 2, + FCGI_UNKNOWN_ROLE = 3 +}; + +#define FCGI_MAX_CONNS "FCGI_MAX_CONNS" +#define FCGI_MAX_REQS "FCGI_MAX_REQS" +#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS" + +// - - Message stream parser - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct fcgi_parser; + +typedef void (*fcgi_message_fn) + (const struct fcgi_parser *parser, void *user_data); + +enum fcgi_parser_state +{ + FCGI_READING_HEADER, ///< Reading the fixed header portion + FCGI_READING_CONTENT, ///< Reading the message content + FCGI_READING_PADDING ///< Reading the padding +}; + +struct fcgi_parser +{ + enum fcgi_parser_state state; ///< Parsing state + struct str input; ///< Input buffer + + // The next block of fields is considered public: + + uint8_t version; ///< FastCGI protocol version + uint8_t type; ///< FastCGI record type + uint16_t request_id; ///< FastCGI request ID + struct str content; ///< Message data + + uint16_t content_length; ///< Message content length + uint8_t padding_length; ///< Message padding length + + fcgi_message_fn on_message; ///< Callback on message + void *user_data; ///< User data +}; + +static void +fcgi_parser_init (struct fcgi_parser *self) +{ + memset (self, 0, sizeof *self); + str_init (&self->input); + str_init (&self->content); +} + +static void +fcgi_parser_free (struct fcgi_parser *self) +{ + str_free (&self->input); + str_free (&self->content); +} + +static void +fcgi_parser_unpack_header (struct fcgi_parser *self) +{ + struct msg_unpacker unpacker; + msg_unpacker_init (&unpacker, self->input.str, self->input.len); + + bool success = true; + uint8_t reserved; + success &= msg_unpacker_u8 (&unpacker, &self->version); + success &= msg_unpacker_u8 (&unpacker, &self->type); + success &= msg_unpacker_u16 (&unpacker, &self->request_id); + success &= msg_unpacker_u16 (&unpacker, &self->content_length); + success &= msg_unpacker_u8 (&unpacker, &self->padding_length); + success &= msg_unpacker_u8 (&unpacker, &reserved); + hard_assert (success); + + str_remove_slice (&self->input, 0, unpacker.offset); +} + +static void +fcgi_parser_push (struct fcgi_parser *self, const void *data, size_t len) +{ + // This could be made considerably faster for high-throughput applications + // if we use a circular buffer instead of constantly calling memmove() + str_append_data (&self->input, data, len); + + while (true) + switch (self->state) + { + case FCGI_READING_HEADER: + if (self->input.len < FCGI_HEADER_LEN) + return; + + fcgi_parser_unpack_header (self); + self->state = FCGI_READING_CONTENT; + break; + case FCGI_READING_CONTENT: + if (self->input.len < self->content_length) + return; + + // Move an appropriate part of the input buffer to the content buffer + str_reset (&self->content); + str_append_data (&self->content, self->input.str, self->content_length); + str_remove_slice (&self->input, 0, self->content_length); + self->state = FCGI_READING_PADDING; + break; + case FCGI_READING_PADDING: + if (self->input.len < self->padding_length) + return; + + // Call the callback to further process the message + self->on_message (self, self->user_data); + + // Remove the padding from the input buffer + str_remove_slice (&self->input, 0, self->padding_length); + self->state = FCGI_READING_HEADER; + break; + } +} + +// - - Name-value pair parser - - - - - - - - - - - - - - - - - - - - - - - - - + +enum fcgi_nv_parser_state +{ + FCGI_NV_PARSER_NAME_LEN, ///< The first name length octet + FCGI_NV_PARSER_NAME_LEN_FULL, ///< Remaining name length octets + FCGI_NV_PARSER_VALUE_LEN, ///< The first value length octet + FCGI_NV_PARSER_VALUE_LEN_FULL, ///< Remaining value length octets + FCGI_NV_PARSER_NAME, ///< Reading the name + FCGI_NV_PARSER_VALUE ///< Reading the value +}; + +struct fcgi_nv_parser +{ + struct str_map *output; ///< Where the pairs will be stored + + enum fcgi_nv_parser_state state; ///< Parsing state + struct str input; ///< Input buffer + + uint32_t name_len; ///< Length of the name + uint32_t value_len; ///< Length of the value + + char *name; ///< The current name, 0-terminated + char *value; ///< The current value, 0-terminated +}; + +static void +fcgi_nv_parser_init (struct fcgi_nv_parser *self) +{ + memset (self, 0, sizeof *self); + str_init (&self->input); +} + +static void +fcgi_nv_parser_free (struct fcgi_nv_parser *self) +{ + str_free (&self->input); + free (self->name); + free (self->value); +} + +static void +fcgi_nv_parser_push (struct fcgi_nv_parser *self, const void *data, size_t len) +{ + // This could be optimized significantly; I'm not even trying + str_append_data (&self->input, data, len); + + while (true) + { + struct msg_unpacker unpacker; + msg_unpacker_init (&unpacker, self->input.str, self->input.len); + + switch (self->state) + { + uint8_t len; + uint32_t len_full; + + case FCGI_NV_PARSER_NAME_LEN: + if (!msg_unpacker_u8 (&unpacker, &len)) + return; + + if (len >> 7) + self->state = FCGI_NV_PARSER_NAME_LEN_FULL; + else + { + self->name_len = len; + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_VALUE_LEN; + } + break; + case FCGI_NV_PARSER_NAME_LEN_FULL: + if (!msg_unpacker_u32 (&unpacker, &len_full)) + return; + + self->name_len = len_full & ~(1U << 31); + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_VALUE_LEN; + break; + case FCGI_NV_PARSER_VALUE_LEN: + if (!msg_unpacker_u8 (&unpacker, &len)) + return; + + if (len >> 7) + self->state = FCGI_NV_PARSER_VALUE_LEN_FULL; + else + { + self->value_len = len; + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_NAME; + } + break; + case FCGI_NV_PARSER_VALUE_LEN_FULL: + if (!msg_unpacker_u32 (&unpacker, &len_full)) + return; + + self->value_len = len_full & ~(1U << 31); + str_remove_slice (&self->input, 0, unpacker.offset); + self->state = FCGI_NV_PARSER_NAME; + break; + case FCGI_NV_PARSER_NAME: + if (self->input.len < self->name_len) + return; + + self->name = xmalloc (self->name_len + 1); + self->name[self->name_len] = '\0'; + memcpy (self->name, self->input.str, self->name_len); + str_remove_slice (&self->input, 0, self->name_len); + self->state = FCGI_NV_PARSER_VALUE; + break; + case FCGI_NV_PARSER_VALUE: + if (self->input.len < self->value_len) + return; + + self->value = xmalloc (self->value_len + 1); + self->value[self->value_len] = '\0'; + memcpy (self->value, self->input.str, self->value_len); + str_remove_slice (&self->input, 0, self->value_len); + self->state = FCGI_NV_PARSER_NAME_LEN; + + // The map takes ownership of the value + str_map_set (self->output, self->name, self->value); + free (self->name); + + self->name = NULL; + self->value = NULL; + break; + } + } +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +fcgi_nv_convert_len (size_t len, struct str *output) +{ + if (len < 0x80) + str_pack_u8 (output, len); + else + { + len |= (uint32_t) 1 << 31; + str_pack_u32 (output, len); + } +} + +static void +fcgi_nv_convert (struct str_map *map, struct str *output) +{ + struct str_map_iter iter; + str_map_iter_init (&iter, map); + while (str_map_iter_next (&iter)) + { + const char *name = iter.link->key; + const char *value = iter.link->data; + size_t name_len = iter.link->key_length; + size_t value_len = strlen (value); + + fcgi_nv_convert_len (name_len, output); + fcgi_nv_convert_len (value_len, output); + str_append_data (output, name, name_len); + str_append_data (output, value, value_len); + } +} + +#endif + +#ifdef LIBERTY_WANT_PROTO_WS + +// --- WebSockets -------------------------------------------------------------- + +#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, uint64_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. + + uint64_t end = len & ~(uint64_t) 3; + for (uint64_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; +} + +#endif @@ -61,6 +61,7 @@ #endif // ! NI_MAXSERV #ifdef LIBERTY_WANT_SSL +#include <openssl/sha.h> #include <openssl/ssl.h> #include <openssl/err.h> #endif // LIBERTY_WANT_SSL @@ -544,6 +545,42 @@ str_remove_slice (struct str *self, size_t start, size_t length) self->str = xrealloc (self->str, self->alloc >>= 2); } +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +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_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); +} + +#define str_pack_i8(self, x) str_pack_u8 ((self), (uint8_t) (x)) +#define str_pack_i16(self, x) str_pack_u16 ((self), (uint16_t) (x)) +#define str_pack_i32(self, x) str_pack_u32 ((self), (uint32_t) (x)) +#define str_pack_i64(self, x) str_pack_u64 ((self), (uint64_t) (x)) + // --- Errors ------------------------------------------------------------------ // Error reporting utilities. Inspired by GError, only much simpler. @@ -1660,7 +1697,16 @@ msg_unpacker_u8 (struct msg_unpacker *self, uint8_t *value) } static bool -msg_unpacker_i32 (struct msg_unpacker *self, int32_t *value) +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 @@ -1681,10 +1727,22 @@ msg_unpacker_u64 (struct msg_unpacker *self, uint64_t *value) return true; } +#define msg_unpacker_i8(self, value) \ + msg_unpacker_u8 ((self), (uint8_t *) (value)) +#define msg_unpacker_i16(self, value) \ + msg_unpacker_u16 ((self), (uint16_t *) (value)) +#define msg_unpacker_i32(self, value) \ + msg_unpacker_u32 ((self), (uint32_t *) (value)) +#define msg_unpacker_i64(self, value) \ + msg_unpacker_u64 ((self), (uint64_t *) (value)) + #undef UNPACKER_INT_BEGIN // --- Message packer and writer ----------------------------------------------- +// Use str_pack_*() or other methods to append to the internal buffer, then +// flush it to get a nice frame. Handy for iovec. + struct msg_writer { struct str buf; ///< Holds the message data @@ -1698,28 +1756,6 @@ msg_writer_init (struct msg_writer *self) str_append_data (&self->buf, "\x00\x00\x00\x00" "\x00\x00\x00\x00", 8); } -static void -msg_writer_u8 (struct msg_writer *self, uint8_t x) -{ - str_append_data (&self->buf, &x, 1); -} - -static void -msg_writer_i32 (struct msg_writer *self, int32_t x) -{ - uint32_t u = x; - uint8_t tmp[4] = { u >> 24, u >> 16, u >> 8, u }; - str_append_data (&self->buf, tmp, sizeof tmp); -} - -static void -msg_writer_u64 (struct msg_writer *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->buf, tmp, sizeof tmp); -} - static void * msg_writer_flush (struct msg_writer *self, size_t *len) { @@ -1733,6 +1769,220 @@ msg_writer_flush (struct msg_writer *self, size_t *len) return str_steal (&self->buf); } +// --- ASCII ------------------------------------------------------------------- + +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) +{ + int x; + while (*a || *b) + if ((x = tolower_ascii (*(const unsigned char *) a++) + - tolower_ascii (*(const unsigned char *) b++))) + return x; + return 0; +} + +static bool +isspace_ascii (int c) +{ + return c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v'; +} + +// --- UTF-8 ------------------------------------------------------------------- + +/// 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; +} + +// --- Base 64 ----------------------------------------------------------------- + +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; + } +} + // --- Utilities --------------------------------------------------------------- static void @@ -2575,3 +2825,7 @@ test_run (struct test *self) str_map_free (&self->blacklist); return 0; } + +// --- Protocol modules -------------------------------------------------------- + +#include "liberty-proto.c" diff --git a/tests/liberty.c b/tests/liberty.c index 8956ed0..349ee78 100644 --- a/tests/liberty.c +++ b/tests/liberty.c @@ -236,21 +236,6 @@ test_error (void) // --- Hash map ---------------------------------------------------------------- -static int -tolower_ascii (int c) -{ - return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; -} - -static size_t -tolower_strxfrm (char *dest, const char *src, size_t n) -{ - size_t len = strlen (src); - while (n-- && (*dest++ = tolower_ascii (*src++))) - ; - return len; -} - static void free_counter (void *data) { @@ -280,7 +265,7 @@ test_str_map (void) // Put two reference counted objects in the map under case-insensitive keys struct str_map m; str_map_init (&m); - m.key_xfrm = tolower_strxfrm; + m.key_xfrm = tolower_ascii_strxfrm; m.free = free_counter; int *a = make_counter (); @@ -323,6 +308,34 @@ test_str_map (void) free_counter (b); } +static void +test_utf8 (void) +{ + const char valid [] = "2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm"; + const char invalid[] = "\xf0\x90\x28\xbc"; + soft_assert ( utf8_validate (valid, sizeof valid)); + soft_assert (!utf8_validate (invalid, sizeof invalid)); +} + +static void +test_base64 (void) +{ + char data[65]; + for (size_t i = 0; i < N_ELEMENTS (data); i++) + data[i] = i; + + struct str encoded; str_init (&encoded); + struct str decoded; str_init (&decoded); + + base64_encode (data, sizeof data, &encoded); + soft_assert (base64_decode (encoded.str, false, &decoded)); + soft_assert (decoded.len == sizeof data); + soft_assert (!memcmp (decoded.str, data, sizeof data)); + + str_free (&encoded); + str_free (&decoded); +} + // --- Main -------------------------------------------------------------------- int @@ -338,6 +351,8 @@ main (int argc, char *argv[]) test_add_simple (&test, "/str", NULL, test_str); test_add_simple (&test, "/error", NULL, test_error); test_add_simple (&test, "/str-map", NULL, test_str_map); + test_add_simple (&test, "/utf-8", NULL, test_utf8); + test_add_simple (&test, "/base64", NULL, test_base64); // TODO: write tests for the rest of the library diff --git a/tests/proto.c b/tests/proto.c new file mode 100644 index 0000000..caa9cf2 --- /dev/null +++ b/tests/proto.c @@ -0,0 +1,167 @@ +/* + * tests/proto.c + * + * Copyright (c) 2015, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define PROGRAM_NAME "test" +#define PROGRAM_VERSION "0" + +#define LIBERTY_WANT_SSL + +#define LIBERTY_WANT_PROTO_IRC +#define LIBERTY_WANT_PROTO_HTTP +#define LIBERTY_WANT_PROTO_SCGI +#define LIBERTY_WANT_PROTO_FASTCGI +#define LIBERTY_WANT_PROTO_WS + +#include "../liberty.c" + +// --- Tests ------------------------------------------------------------------- + +static void +test_http_parser (void) +{ + struct str_map parameters; + str_map_init (¶meters); + parameters.key_xfrm = tolower_ascii_strxfrm; + + char *type = NULL; + char *subtype = NULL; + soft_assert (http_parse_media_type ("TEXT/html; CHARset=\"utf\\-8\"", + &type, &subtype, ¶meters)); + soft_assert (!strcasecmp_ascii (type, "text")); + soft_assert (!strcasecmp_ascii (subtype, "html")); + soft_assert (parameters.len == 1); + soft_assert (!strcmp (str_map_find (¶meters, "charset"), "utf-8")); + str_map_free (¶meters); + + struct http_protocol *protocols; + soft_assert (http_parse_upgrade ("websocket, HTTP/2.0, , ", &protocols)); + + soft_assert (!strcmp (protocols->name, "websocket")); + soft_assert (!protocols->version); + + soft_assert (!strcmp (protocols->next->name, "HTTP")); + soft_assert (!strcmp (protocols->next->version, "2.0")); + + soft_assert (!protocols->next->next); + + LIST_FOR_EACH (struct http_protocol, iter, protocols) + http_protocol_destroy (iter); +} + +static bool +test_scgi_parser_on_headers_read (void *user_data) +{ + struct scgi_parser *parser = user_data; + soft_assert (parser->headers.len == 4); + soft_assert (!strcmp (str_map_find (&parser->headers, + "CONTENT_LENGTH"), "27")); + soft_assert (!strcmp (str_map_find (&parser->headers, + "SCGI"), "1")); + soft_assert (!strcmp (str_map_find (&parser->headers, + "REQUEST_METHOD"), "POST")); + soft_assert (!strcmp (str_map_find (&parser->headers, + "REQUEST_URI"), "/deepthought")); + return true; +} + +static bool +test_scgi_parser_on_content (void *user_data, const void *data, size_t len) +{ + (void) user_data; + soft_assert (!strncmp (data, "What is the answer to life?", len)); + return true; +} + +static void +test_scgi_parser (void) +{ + struct scgi_parser parser; + scgi_parser_init (&parser); + parser.on_headers_read = test_scgi_parser_on_headers_read; + parser.on_content = test_scgi_parser_on_content; + parser.user_data = &parser; + + // This is an example straight from the specification + const char example[] = + "70:" + "CONTENT_LENGTH" "\0" "27" "\0" + "SCGI" "\0" "1" "\0" + "REQUEST_METHOD" "\0" "POST" "\0" + "REQUEST_URI" "\0" "/deepthought" "\0" + "," + "What is the answer to life?"; + + soft_assert (scgi_parser_push (&parser, example, sizeof example, NULL)); + scgi_parser_free (&parser); +} + +static bool +test_websockets_on_frame_header (void *user_data, const struct ws_parser *self) +{ + (void) user_data; + soft_assert (self->is_fin); + soft_assert (self->is_masked); + soft_assert (self->opcode == WS_OPCODE_TEXT); + return true; +} + +static bool +test_websockets_on_frame (void *user_data, const struct ws_parser *self) +{ + (void) user_data; + soft_assert (self->input.len == self->payload_len); + soft_assert (!strncmp (self->input.str, "Hello", self->input.len)); + return true; +} + +static void +test_websockets (void) +{ + char *accept = ws_encode_response_key ("dGhlIHNhbXBsZSBub25jZQ=="); + soft_assert (!strcmp (accept, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")); + free (accept); + + struct ws_parser parser; + ws_parser_init (&parser); + parser.on_frame_header = test_websockets_on_frame_header; + parser.on_frame = test_websockets_on_frame; + parser.user_data = &parser; + + const char frame[] = "\x81\x85\x37\xfa\x21\x3d\x7f\x9f\x4d\x51\x58"; + soft_assert (ws_parser_push (&parser, frame, sizeof frame - 1)); + ws_parser_free (&parser); +} + +// --- Main -------------------------------------------------------------------- + +int +main (int argc, char *argv[]) +{ + struct test test; + test_init (&test, argc, argv); + + test_add_simple (&test, "/http-parser", NULL, test_http_parser); + test_add_simple (&test, "/scgi-parser", NULL, test_scgi_parser); + test_add_simple (&test, "/websockets", NULL, test_websockets); + // TODO: test FastCGI + // TODO: test IRC + + return test_run (&test); +} |