diff options
author | sternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org> | 2020-09-21 13:54:21 +0200 |
---|---|---|
committer | sternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org> | 2020-09-21 13:54:21 +0200 |
commit | 58628d7279d427ac96e7f3263bc97376cf74efd4 (patch) | |
tree | f3639600465a31b568b0bfbb525f3e9f62bb57dd | |
parent | 5fd1ff55f47d857ce3a45f1cb92fe56ef7aa0953 (diff) |
feat(form): let form_parse parse a form instead of a sequence of tokens
test(test_form): test form parsing functionality in a number of cases
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | README.adoc | 15 | ||||
-rw-r--r-- | default.nix | 3 | ||||
-rw-r--r-- | warteraum/all.do | 2 | ||||
-rw-r--r-- | warteraum/default.o.do | 4 | ||||
-rw-r--r-- | warteraum/form.c | 119 | ||||
-rw-r--r-- | warteraum/form.h | 26 | ||||
-rw-r--r-- | warteraum/http_string.h | 15 | ||||
-rw-r--r-- | warteraum/main.c | 34 | ||||
-rw-r--r-- | warteraum/test/all.do | 1 | ||||
-rw-r--r-- | warteraum/test/default.exe.do | 18 | ||||
-rwxr-xr-x | warteraum/test/run | 9 | ||||
-rw-r--r-- | warteraum/test/test.h | 48 | ||||
-rw-r--r-- | warteraum/test/test_form.c | 78 | ||||
-rw-r--r-- | warteraum/test/test_queue.c | 36 | ||||
-rw-r--r-- | warteraum/test/test_queue.do | 7 |
16 files changed, 318 insertions, 100 deletions
diff --git a/.gitignore b/.gitignore index 67e1eb7..b63ba59 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,11 @@ vgcore.* /warteraum/all /warteraum/warteraum /warteraum/hashtoken -/warteraum/test/test_queue +/warteraum/test/*.exe # redo-sh .redo +*.tmp2 # redo-c .dep.* .depend.* diff --git a/README.adoc b/README.adoc index cdd445b..8e48093 100644 --- a/README.adoc +++ b/README.adoc @@ -29,16 +29,11 @@ API Documentation Caveats ~~~~~~~ -Current form parsing is somewhat limited. Make sure you don't -send any extra unnecessary form fields in your request body or -`warteraum` may refuse to cooperate with you. - -The `v2` API may support `application/json` request bodies in -the future. This is not yet the case. - -Any endpoint may also return different status codes than the -ones listed here in unusual cases. These would typically -be codes like 500 or 502. +* The `v2` API may support `application/json` request bodies in + the future. This is not yet the case. +* Any endpoint may also return different status codes than the + ones listed here in unusual cases. These would typically + be codes like 500 or 502. API `v2` ~~~~~~~~ diff --git a/default.nix b/default.nix index 2e959bb..fa2ae65 100644 --- a/default.nix +++ b/default.nix @@ -68,6 +68,7 @@ let patchPhase = '' runHook prePatch + patchShebangs test/run ${saltReplace} ${tokensReplace} @@ -77,7 +78,7 @@ let buildPhase = "redo"; doCheck = true; - checkPhase = "./test/test_queue"; + checkPhase = "./test/run"; installPhase = '' install -Dm755 warteraum -t $out/bin diff --git a/warteraum/all.do b/warteraum/all.do index 83a3c23..709e512 100644 --- a/warteraum/all.do +++ b/warteraum/all.do @@ -1 +1 @@ -redo-ifchange warteraum hashtoken test/test_queue +redo-ifchange warteraum hashtoken test/all diff --git a/warteraum/default.o.do b/warteraum/default.o.do index 30281dd..9a75d37 100644 --- a/warteraum/default.o.do +++ b/warteraum/default.o.do @@ -10,11 +10,13 @@ fi case "$2" in main) - redo-ifchange queue.h routing.h form.h v1_static.h scrypt.h tokens.h + redo-ifchange queue.h routing.h form.h v1_static.h + redo-ifchange scrypt.h tokens.h http_string.h redo-ifchange ../third_party/json_output/json_output.h redo-ifchange ../third_party/httpserver.h/httpserver.h ;; form) + redo-ifchange http_string.h redo-ifchange ../third_party/httpserver.h/httpserver.h ;; routing) diff --git a/warteraum/form.c b/warteraum/form.c index b29c076..ad2487d 100644 --- a/warteraum/form.c +++ b/warteraum/form.c @@ -1,6 +1,18 @@ #include <stddef.h> #include <stdbool.h> #include "form.h" +#include "http_string.h" + +enum form_token_type { + FORM_TOKEN_EQUAL_SIGN, + FORM_TOKEN_AND_SIGN, + FORM_TOKEN_STRING +}; + +struct form_token { + enum form_token_type type; + struct http_string_s token; +}; struct form_token *form_next_token(struct http_string_s s, int *pos) { static struct form_token t; @@ -38,23 +50,114 @@ struct form_token *form_next_token(struct http_string_s s, int *pos) { return &t; } -bool form_parse(struct http_string_s s, const struct form_token_spec specs[], size_t len) { +enum parser_state { + FORM_PARSER_IN_KEY, + FORM_PARSER_IN_CON, + FORM_PARSER_IN_VAL +}; + +enum parser_need { + FORM_PARSER_NEED_KEY = 0x1, + FORM_PARSER_NEED_VAL = 0x2, + FORM_PARSER_SKIP = 0x4 +}; + +bool form_parse(struct http_string_s s, const struct form_field_spec specs[], size_t len) { + const struct http_string_s empty = STATIC_HTTP_STRING(""); struct form_token *t; int pos = 0; + size_t remaining = len; + size_t optionals = 0; - for(size_t i = 0; i < len; i++) { - t = form_next_token(s, &pos); - - if(t == NULL || t->type != specs[i].expected) { - return 0; + for(size_t i = 0; i < len && remaining > 0; i++) { + if(specs[i].type == FIELD_TYPE_OPTIONAL_STRING) { + remaining -= 1; + optionals += 1; } if(specs[i].target != NULL) { - *(specs[i].target) = t->token; + *(specs[i].target) = empty; + specs[i].target->len = -1; } } - return 1; + enum parser_state state = FORM_PARSER_IN_KEY; + enum parser_need need = FORM_PARSER_NEED_KEY; + size_t current_key_index; + bool parse_error = false; + + while((t = form_next_token(s, &pos)) != NULL && !parse_error + && (remaining > 0 || optionals > 0)) { + struct http_string_s key, val; + + switch(state) { + case FORM_PARSER_IN_KEY: + + if(t->type != FORM_TOKEN_STRING) { + key = empty; + } else { + key = t->token; + } + + need = FORM_PARSER_SKIP; + + // TODO binary search over alphabetically ordered? + for(size_t i = 0; i < len; i++) { + if(HTTP_STRING_EQ(key, specs[i].field)) { + current_key_index = i; + + if(specs[i].type == FIELD_TYPE_STRING) { + need = FORM_PARSER_NEED_VAL; + } else if(specs[i].type == FIELD_TYPE_OPTIONAL_STRING) { + need = FORM_PARSER_NEED_VAL | FORM_PARSER_NEED_KEY; + + // set to empty string in case no value is coming + *(specs[i].target) = empty; + } else { + parse_error = true; + } + break; + } + } + + state = FORM_PARSER_IN_CON; + break; + case FORM_PARSER_IN_CON: + if(t->type == FORM_TOKEN_EQUAL_SIGN) { + parse_error = !(need & (FORM_PARSER_NEED_VAL | FORM_PARSER_SKIP)); + state = FORM_PARSER_IN_VAL; + } else if(t->type == FORM_TOKEN_AND_SIGN) { + parse_error = !(need & (FORM_PARSER_NEED_KEY | FORM_PARSER_SKIP)); + state = FORM_PARSER_IN_KEY; + } else { + parse_error = true; + } + break; + case FORM_PARSER_IN_VAL: + if(t->type != FORM_TOKEN_STRING) { + val = empty; + } else { + val = t->token; + } + + if(need & FORM_PARSER_NEED_VAL) { + if(specs[current_key_index].target != NULL) { + *(specs[current_key_index].target) = val; + remaining -= remaining > 0 ? 1 : 0; + } + } + + need = FORM_PARSER_NEED_KEY; + state = FORM_PARSER_IN_CON; + } + } + + if(state == FORM_PARSER_IN_VAL && (need & FORM_PARSER_NEED_VAL)) { + *(specs[current_key_index].target) = empty; + remaining -= remaining > 0 ? 1 : 0; + } + + return (!parse_error && remaining == 0); } char hex_to_int(char c) { diff --git a/warteraum/form.h b/warteraum/form.h index 2a2728b..5fbb56d 100644 --- a/warteraum/form.h +++ b/warteraum/form.h @@ -2,24 +2,26 @@ #include <stdbool.h> -enum form_token_type { - FORM_TOKEN_EQUAL_SIGN, - FORM_TOKEN_AND_SIGN, - FORM_TOKEN_STRING -}; +/* Simple parser for application/x-www-form-urlencoded + * See: https://url.spec.whatwg.org/#urlencoded-parsing + * May not conform 100%: “The application/x-www-form-urlencoded + * format is in many ways an aberrant monstrosity” + */ -struct form_token { - enum form_token_type type; - struct http_string_s token; +enum field_type { + FIELD_TYPE_STRING, + FIELD_TYPE_OPTIONAL_STRING }; -struct form_token_spec { - enum form_token_type expected; +struct form_field_spec { + struct http_string_s field; + enum field_type type; struct http_string_s *target; }; -struct form_token *form_next_token(struct http_string_s, int *); +bool form_parse(struct http_string_s, const struct form_field_spec[], size_t); -bool form_parse(struct http_string_s, const struct form_token_spec[], size_t); +#define STATIC_FORM_PARSE(s, sp) \ + form_parse(s, sp, sizeof(sp) / sizeof(struct form_field_spec)) int urldecode(struct http_string_s, char *, size_t); diff --git a/warteraum/http_string.h b/warteraum/http_string.h new file mode 100644 index 0000000..ee47aa6 --- /dev/null +++ b/warteraum/http_string.h @@ -0,0 +1,15 @@ +#ifndef WARTERAUM_HTTP_STRING_H +#define WARTERAUM_HTTP_STRING_H + +#include <string.h> + +#define STATIC_HTTP_STRING(s) \ + { s, sizeof(s) - 1 } + +#define HTTP_STRING_EQ(a, b) \ + (a.len == b.len && strncmp(a.buf, b.buf, a.len) == 0) + +#define HTTP_STRING_IS(a, s) \ + (a.len == sizeof(s) - 1 && strncmp(a.buf, s, a.len) == 0) + +#endif diff --git a/warteraum/main.c b/warteraum/main.c index e7aa22c..3d64b5a 100644 --- a/warteraum/main.c +++ b/warteraum/main.c @@ -8,6 +8,8 @@ #include "../third_party/json_output/json_output.h" +#include "http_string.h" + #include "queue.h" #include "routing.h" #include "form.h" @@ -18,18 +20,9 @@ #define LISTEN_PORT 9000 -#define STATIC_HTTP_STRING(s) \ - { s, sizeof(s) - 1 } - #define JSO_STATIC_PROP(s, str) \ jso_prop_len(s, str, sizeof(str) - 1) -#define HTTP_STRING_EQ(a, b) \ - (a.len == b.len && strncmp(a.buf, b.buf, a.len) == 0) - -#define HTTP_STRING_IS(a, s) \ - (a.len == sizeof(s) - 1 && strncmp(a.buf, s, a.len) == 0) - // compare http_string against a static string, // but optionally allow an ;… after it. // i.e. application/json;charset=utf8 matches with @@ -174,13 +167,9 @@ enum warteraum_result response_queue(enum warteraum_version v, http_request_t *r // POST /api/{v1,v2}/queue/add enum warteraum_result response_queue_add(enum warteraum_version version, http_request_t *request, http_response_t *response) { - http_string_t field_name; http_string_t text; - - const struct form_token_spec request_spec[] = { - { FORM_TOKEN_STRING, &field_name }, - { FORM_TOKEN_EQUAL_SIGN, NULL }, - { FORM_TOKEN_STRING, &text } + const struct form_field_spec request_spec[] = { + { STATIC_HTTP_STRING("text"), FIELD_TYPE_STRING, &text } }; if(flip_queue.last != NULL && flip_queue.last->id == QUEUE_MAX_ID) { @@ -197,9 +186,9 @@ enum warteraum_result response_queue_add(enum warteraum_version version, http_re http_string_t body = http_request_body(request); - bool parse_res = form_parse(body, request_spec, sizeof(request_spec) / sizeof(struct form_token_spec)); + bool parse_res = STATIC_FORM_PARSE(body, request_spec); - if(!parse_res || !HTTP_STRING_IS(field_name, "text")) { + if(!parse_res) { return WARTERAUM_BAD_REQUEST; } @@ -266,17 +255,14 @@ enum warteraum_result response_queue_del(http_string_t id_str, enum warteraum_ve http_string_t body = http_request_body(request); http_string_t token; - http_string_t field_name; - const struct form_token_spec request_spec[] = { - { FORM_TOKEN_STRING, &field_name }, - { FORM_TOKEN_EQUAL_SIGN, NULL }, - { FORM_TOKEN_STRING, &token } + const struct form_field_spec request_spec[] = { + { STATIC_HTTP_STRING("token"), FIELD_TYPE_STRING, &token } }; - bool parse_res = form_parse(body, request_spec, sizeof(request_spec) / sizeof(struct form_token_spec)); + bool parse_res = STATIC_FORM_PARSE(body, request_spec); - if(!parse_res || !HTTP_STRING_IS(field_name, "token")) { + if(!parse_res) { return WARTERAUM_BAD_REQUEST; } diff --git a/warteraum/test/all.do b/warteraum/test/all.do new file mode 100644 index 0000000..b1a5a94 --- /dev/null +++ b/warteraum/test/all.do @@ -0,0 +1 @@ +redo-ifchange test_queue.exe test_form.exe diff --git a/warteraum/test/default.exe.do b/warteraum/test/default.exe.do new file mode 100644 index 0000000..c01fe3e --- /dev/null +++ b/warteraum/test/default.exe.do @@ -0,0 +1,18 @@ +source ../build_config +redo-ifchange ../build_config +redo-ifchange test.h +redo-ifchange "$2.c" + +case "$2" in + test_queue) + OBJS="../queue.o" + ;; + test_form) + OBJS="../form.o" + redo-ifchange ../http_string.h + ;; +esac + +redo-ifchange $OBJS + +$CC $CFLAGS -o "$3" $OBJS "$2.c" diff --git a/warteraum/test/run b/warteraum/test/run new file mode 100755 index 0000000..a337b99 --- /dev/null +++ b/warteraum/test/run @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e +cd "$(dirname "$0")" + +echo -e "\n# queue tests\n" +./test_queue.exe +echo -e "\n# form parsing tests\n" +./test_form.exe diff --git a/warteraum/test/test.h b/warteraum/test/test.h new file mode 100644 index 0000000..5e76d3f --- /dev/null +++ b/warteraum/test/test.h @@ -0,0 +1,48 @@ +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> + +#ifndef TEST_EXIT_ON_FAIL +#define TEST_EXIT_ON_FAIL false +#endif + +#ifndef TEST_INFO_WIDTH +#define TEST_INFO_WIDTH 50 +#endif + +static bool test_result; + +void test_case(char *info, bool result) { + char *result_str = "okay"; + FILE *output = stdout; + + if(!result) { + result_str = "FAIL"; + output = stderr; + } + + int w = TEST_INFO_WIDTH; + + while(*info != '\0') { + fputc(*info, output); + w--; info++; + } + + fputs(":", output); + w--; + + while(w > 0) { + fputc(' ', output); + w--; + } + + fputs(info, output); + fputs(result_str, output); + fputc('\n', output); + + test_result = test_result && result; + + if(!test_result && TEST_EXIT_ON_FAIL) { + exit(EXIT_FAILURE); + } +} diff --git a/warteraum/test/test_form.c b/warteraum/test/test_form.c new file mode 100644 index 0000000..71835b7 --- /dev/null +++ b/warteraum/test/test_form.c @@ -0,0 +1,78 @@ +#define TEST_EXIT_ON_FAIL true +#include "test.h" +#include "../http_string.h" +#include "../form.h" + +int main(void) { + test_result = true; + + struct http_string_s normal; + const struct form_field_spec single[] = { + { STATIC_HTTP_STRING("test"), FIELD_TYPE_STRING, &normal } + }; + + struct http_string_s form_single = STATIC_HTTP_STRING("test=hello"); + test_case("single field parse", STATIC_FORM_PARSE(form_single, single)); + test_case("result matches", HTTP_STRING_IS(normal, "hello")); + + struct http_string_s form_single_garbage = + STATIC_HTTP_STRING("foo&test=hello+world&one=nope&two=no&bar"); + test_case("parse single field with garbage", + STATIC_FORM_PARSE(form_single_garbage, single)); + test_case("result matches", HTTP_STRING_IS(normal, "hello+world")); + + struct http_string_s empty_string_end = STATIC_HTTP_STRING("foo=bar&test="); + struct http_string_s empty_string_mid = STATIC_HTTP_STRING("test=&foo=bar"); + test_case("empty string at end of input parses", + STATIC_FORM_PARSE(empty_string_end, single) && normal.len == 0); + test_case("empty string in mid of input parses", + STATIC_FORM_PARSE(empty_string_mid, single) && normal.len == 0); + + struct http_string_s normal2, normal3; + const struct form_field_spec multiple[] = { + { STATIC_HTTP_STRING("one"), FIELD_TYPE_STRING, &normal }, + { STATIC_HTTP_STRING("two"), FIELD_TYPE_STRING, &normal2 }, + { STATIC_HTTP_STRING("three"), FIELD_TYPE_STRING, &normal3 } + }; + + struct http_string_s multiple_clean = + STATIC_HTTP_STRING("two=2&one=1&three=3"); + struct http_string_s multiple_garbage = + STATIC_HTTP_STRING("bar=foo&one=1&yak&xyz&two=2&three=3&hello=world"); + + test_case("parse fails on missing field", + !STATIC_FORM_PARSE(form_single_garbage, multiple)); + test_case("multiple field parse", + STATIC_FORM_PARSE(multiple_clean, multiple)); + test_case("results match", HTTP_STRING_IS(normal, "1") + && HTTP_STRING_IS(normal2, "2") && HTTP_STRING_IS(normal3, "3")); + test_case("multiple field parse with garbage", + STATIC_FORM_PARSE(multiple_garbage, multiple)); + test_case("results match", HTTP_STRING_IS(normal, "1") + && HTTP_STRING_IS(normal2, "2") && HTTP_STRING_IS(normal3, "3")); + + struct http_string_s required, optional; + const struct form_field_spec optionals[] = { + { STATIC_HTTP_STRING("required"), FIELD_TYPE_STRING, &required }, + { STATIC_HTTP_STRING("optional"), FIELD_TYPE_OPTIONAL_STRING, &optional } + }; + + struct http_string_s both = + STATIC_HTTP_STRING("required=lol&optional=lel"); + struct http_string_s one_missing = + STATIC_HTTP_STRING("required=lol"); + struct http_string_s both_flag = + STATIC_HTTP_STRING("optional&required=lol"); + + test_case("parse of all values", STATIC_FORM_PARSE(both, optionals)); + test_case("results match", HTTP_STRING_IS(required, "lol") + && HTTP_STRING_IS(optional, "lel")); + test_case("parse without one optional value", + STATIC_FORM_PARSE(one_missing, optionals)); + test_case("missing value is marked correctly", optional.len == -1); + test_case("parse with one flag-ish value", + STATIC_FORM_PARSE(both_flag, optionals)); + test_case("present flag is empty string", optional.len == 0); + + return !test_result; +} diff --git a/warteraum/test/test_queue.c b/warteraum/test/test_queue.c index cdfac68..1245074 100644 --- a/warteraum/test/test_queue.c +++ b/warteraum/test/test_queue.c @@ -1,43 +1,9 @@ #include <stdbool.h> #include <stdio.h> #include <string.h> +#include "test.h" #include "../queue.h" -#define INFO_WIDTH 50 - -static bool test_result; - -void test_case(char *info, bool result) { - char *result_str = "okay"; - FILE *output = stdout; - - if(!result) { - result_str = "FAIL"; - output = stderr; - } - - int w = INFO_WIDTH; - - while(*info != '\0') { - fputc(*info, output); - w--; info++; - } - - fputs(":", output); - w--; - - while(w > 0) { - fputc(' ', output); - w--; - } - - fputs(info, output); - fputs(result_str, output); - fputc('\n', output); - - test_result = test_result && result; -} - int main(void) { struct queue q; queue_new(&q); diff --git a/warteraum/test/test_queue.do b/warteraum/test/test_queue.do deleted file mode 100644 index 5091c55..0000000 --- a/warteraum/test/test_queue.do +++ /dev/null @@ -1,7 +0,0 @@ -source ../build_config -redo-ifchange ../build_config -DEPS="../queue.o test_queue.o" -redo-ifchange test_queue.c -redo-ifchange $DEPS - -$CC $CFLAGS -o "$3" $DEPS |