Add and fix some preliminary tests
This commit is contained in:
parent
6e9109df4c
commit
8aa232d32e
|
@ -130,10 +130,12 @@ tolower_ascii_strxfrm (char *dest, const char *src, size_t n)
|
||||||
static int
|
static int
|
||||||
strcasecmp_ascii (const char *a, const char *b)
|
strcasecmp_ascii (const char *a, const char *b)
|
||||||
{
|
{
|
||||||
while (*a && *b)
|
while (*a && tolower_ascii (*a) == tolower_ascii (*b))
|
||||||
if (tolower_ascii (*a) != tolower_ascii (*b))
|
{
|
||||||
break;
|
a++;
|
||||||
return *a - *b;
|
b++;
|
||||||
|
}
|
||||||
|
return *(const unsigned char *) a - *(const unsigned char *) b;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
@ -145,6 +147,7 @@ isspace_ascii (int c)
|
||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
/// Return a pointer to the next UTF-8 character, or NULL on error
|
/// 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 *
|
static const char *
|
||||||
utf8_next (const char *s, size_t len)
|
utf8_next (const char *s, size_t len)
|
||||||
{
|
{
|
||||||
|
@ -170,18 +173,21 @@ utf8_next (const char *s, size_t len)
|
||||||
tail_len++;
|
tail_len++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p++;
|
||||||
|
|
||||||
// Check the rest of the sequence
|
// Check the rest of the sequence
|
||||||
if (tail_len < --len)
|
if (tail_len > --len)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
while (tail_len--)
|
while (tail_len--)
|
||||||
if ((*++p & 0xC0) != 0x80)
|
if ((*p++ & 0xC0) != 0x80)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
return (const char *) p;
|
return (const char *) p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Very rough UTF-8 validation, just makes sure codepoints can be iterated
|
/// Very rough UTF-8 validation, just makes sure codepoints can be iterated
|
||||||
|
// TODO: also validate the codepoints
|
||||||
static bool
|
static bool
|
||||||
utf8_validate (const char *s, size_t len)
|
utf8_validate (const char *s, size_t len)
|
||||||
{
|
{
|
||||||
|
@ -1430,6 +1436,8 @@ struct scgi_parser
|
||||||
static void
|
static void
|
||||||
scgi_parser_init (struct scgi_parser *self)
|
scgi_parser_init (struct scgi_parser *self)
|
||||||
{
|
{
|
||||||
|
memset (self, 0, sizeof *self);
|
||||||
|
|
||||||
str_init (&self->input);
|
str_init (&self->input);
|
||||||
str_map_init (&self->headers);
|
str_map_init (&self->headers);
|
||||||
self->headers.free = free;
|
self->headers.free = free;
|
||||||
|
@ -1705,6 +1713,7 @@ ws_parser_unmask (struct ws_parser *self)
|
||||||
static bool
|
static bool
|
||||||
ws_parser_push (struct ws_parser *self, const void *data, size_t len)
|
ws_parser_push (struct ws_parser *self, const void *data, size_t len)
|
||||||
{
|
{
|
||||||
|
bool success = false;
|
||||||
str_append_data (&self->input, data, len);
|
str_append_data (&self->input, data, len);
|
||||||
|
|
||||||
struct msg_unpacker unpacker;
|
struct msg_unpacker unpacker;
|
||||||
|
@ -1717,8 +1726,8 @@ ws_parser_push (struct ws_parser *self, const void *data, size_t len)
|
||||||
uint16_t u16;
|
uint16_t u16;
|
||||||
|
|
||||||
case WS_PARSER_FIXED:
|
case WS_PARSER_FIXED:
|
||||||
if (self->input.len < 2)
|
if (unpacker.len - unpacker.offset < 2)
|
||||||
return true;
|
goto need_data;
|
||||||
|
|
||||||
(void) msg_unpacker_u8 (&unpacker, &u8);
|
(void) msg_unpacker_u8 (&unpacker, &u8);
|
||||||
self->is_fin = (u8 >> 7) & 1;
|
self->is_fin = (u8 >> 7) & 1;
|
||||||
|
@ -1737,59 +1746,59 @@ ws_parser_push (struct ws_parser *self, const void *data, size_t len)
|
||||||
self->state = WS_PARSER_PAYLOAD_LEN_16;
|
self->state = WS_PARSER_PAYLOAD_LEN_16;
|
||||||
else
|
else
|
||||||
self->state = WS_PARSER_MASK;
|
self->state = WS_PARSER_MASK;
|
||||||
|
|
||||||
str_remove_slice (&self->input, 0, 2);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_PARSER_PAYLOAD_LEN_16:
|
case WS_PARSER_PAYLOAD_LEN_16:
|
||||||
if (self->input.len < 2)
|
if (!msg_unpacker_u16 (&unpacker, &u16))
|
||||||
return true;
|
goto need_data;
|
||||||
|
|
||||||
(void) msg_unpacker_u16 (&unpacker, &u16);
|
|
||||||
self->payload_len = u16;
|
self->payload_len = u16;
|
||||||
|
|
||||||
self->state = WS_PARSER_MASK;
|
self->state = WS_PARSER_MASK;
|
||||||
str_remove_slice (&self->input, 0, 2);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_PARSER_PAYLOAD_LEN_64:
|
case WS_PARSER_PAYLOAD_LEN_64:
|
||||||
if (self->input.len < 8)
|
if (!msg_unpacker_u64 (&unpacker, &self->payload_len))
|
||||||
return true;
|
goto need_data;
|
||||||
|
|
||||||
(void) msg_unpacker_u64 (&unpacker, &self->payload_len);
|
|
||||||
|
|
||||||
self->state = WS_PARSER_MASK;
|
self->state = WS_PARSER_MASK;
|
||||||
str_remove_slice (&self->input, 0, 8);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_PARSER_MASK:
|
case WS_PARSER_MASK:
|
||||||
if (!self->is_masked)
|
if (!self->is_masked)
|
||||||
goto end_of_header;
|
goto end_of_header;
|
||||||
if (self->input.len < 4)
|
if (!msg_unpacker_u32 (&unpacker, &self->mask))
|
||||||
return true;
|
goto need_data;
|
||||||
|
|
||||||
(void) msg_unpacker_u32 (&unpacker, &self->mask);
|
|
||||||
str_remove_slice (&self->input, 0, 4);
|
|
||||||
|
|
||||||
end_of_header:
|
end_of_header:
|
||||||
self->state = WS_PARSER_PAYLOAD;
|
self->state = WS_PARSER_PAYLOAD;
|
||||||
if (!self->on_frame_header (self->user_data, self))
|
if (!self->on_frame_header (self->user_data, self))
|
||||||
return false;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_PARSER_PAYLOAD:
|
case WS_PARSER_PAYLOAD:
|
||||||
if (self->input.len < self->payload_len)
|
// Move the buffer so that payload data is at the front
|
||||||
return true;
|
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)
|
if (self->is_masked)
|
||||||
ws_parser_unmask (self);
|
ws_parser_unmask (self);
|
||||||
if (!self->on_frame (self->user_data, self))
|
if (!self->on_frame (self->user_data, self))
|
||||||
return false;
|
goto fail;
|
||||||
|
|
||||||
self->state = WS_PARSER_FIXED;
|
self->state = WS_PARSER_FIXED;
|
||||||
str_reset (&self->input);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
need_data:
|
||||||
|
success = true;
|
||||||
|
fail:
|
||||||
|
str_remove_slice (&self->input, 0, unpacker.offset);
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - Server handler - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
// - - Server handler - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
@ -2792,6 +2801,7 @@ struct request_handler g_request_handler_json_rpc =
|
||||||
static char *
|
static char *
|
||||||
canonicalize_url_path (const char *path)
|
canonicalize_url_path (const char *path)
|
||||||
{
|
{
|
||||||
|
// XXX: this strips any slashes at the end
|
||||||
struct str_vector v;
|
struct str_vector v;
|
||||||
str_vector_init (&v);
|
str_vector_init (&v);
|
||||||
split_str_ignore_empty (path, '/', &v);
|
split_str_ignore_empty (path, '/', &v);
|
||||||
|
@ -2810,7 +2820,7 @@ canonicalize_url_path (const char *path)
|
||||||
|
|
||||||
if (strcmp (dir, ".."))
|
if (strcmp (dir, ".."))
|
||||||
str_vector_add (&canonical, dir);
|
str_vector_add (&canonical, dir);
|
||||||
else if (canonical.len)
|
else if (canonical.len > 1)
|
||||||
// ".." never goes above the root
|
// ".." never goes above the root
|
||||||
str_vector_remove (&canonical, canonical.len - 1);
|
str_vector_remove (&canonical, canonical.len - 1);
|
||||||
}
|
}
|
||||||
|
@ -3631,6 +3641,186 @@ lock_pid_file (struct server_context *ctx, struct error **e)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Tests -------------------------------------------------------------------
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// TODO: test the server handler (happy path)
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_misc (void)
|
||||||
|
{
|
||||||
|
soft_assert ( validate_json_rpc_content_type
|
||||||
|
("application/JSON; charset=\"utf-8\""));
|
||||||
|
soft_assert (!validate_json_rpc_content_type
|
||||||
|
("text/html; charset=\"utf-8\""));
|
||||||
|
|
||||||
|
char *canon = canonicalize_url_path ("///../../../etc/./passwd");
|
||||||
|
soft_assert (!strcmp (canon, "/etc/passwd"));
|
||||||
|
free (canon);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
test_main (int argc, char *argv[])
|
||||||
|
{
|
||||||
|
struct test test;
|
||||||
|
test_init (&test, argc, argv);
|
||||||
|
|
||||||
|
test_add_simple (&test, "/utf-8", NULL, test_utf8);
|
||||||
|
test_add_simple (&test, "/base64", NULL, test_base64);
|
||||||
|
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);
|
||||||
|
|
||||||
|
test_add_simple (&test, "/misc", NULL, test_misc);
|
||||||
|
|
||||||
|
// TODO: write more tests
|
||||||
|
|
||||||
|
return test_run (&test);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Main program ------------------------------------------------------------
|
// --- Main program ------------------------------------------------------------
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
@ -3685,6 +3875,7 @@ parse_program_arguments (int argc, char **argv)
|
||||||
{
|
{
|
||||||
static const struct opt opts[] =
|
static const struct opt opts[] =
|
||||||
{
|
{
|
||||||
|
{ 't', "test", NULL, 0, "self-test" },
|
||||||
{ 'd', "debug", NULL, 0, "run in debug mode" },
|
{ 'd', "debug", NULL, 0, "run in debug mode" },
|
||||||
{ 'h', "help", NULL, 0, "display this help and exit" },
|
{ 'h', "help", NULL, 0, "display this help and exit" },
|
||||||
{ 'V', "version", NULL, 0, "output version information and exit" },
|
{ 'V', "version", NULL, 0, "output version information and exit" },
|
||||||
|
@ -3701,6 +3892,9 @@ parse_program_arguments (int argc, char **argv)
|
||||||
while ((c = opt_handler_get (&oh)) != -1)
|
while ((c = opt_handler_get (&oh)) != -1)
|
||||||
switch (c)
|
switch (c)
|
||||||
{
|
{
|
||||||
|
case 't':
|
||||||
|
test_main (argc, argv);
|
||||||
|
exit (EXIT_SUCCESS);
|
||||||
case 'd':
|
case 'd':
|
||||||
g_debug_mode = true;
|
g_debug_mode = true;
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in New Issue