summary refs log tree commit diff
diff options
context:
space:
mode:
authorsternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org>2020-09-21 13:54:21 +0200
committersternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org>2020-09-21 13:54:21 +0200
commit58628d7279d427ac96e7f3263bc97376cf74efd4 (patch)
treef3639600465a31b568b0bfbb525f3e9f62bb57dd
parent5fd1ff55f47d857ce3a45f1cb92fe56ef7aa0953 (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--.gitignore3
-rw-r--r--README.adoc15
-rw-r--r--default.nix3
-rw-r--r--warteraum/all.do2
-rw-r--r--warteraum/default.o.do4
-rw-r--r--warteraum/form.c119
-rw-r--r--warteraum/form.h26
-rw-r--r--warteraum/http_string.h15
-rw-r--r--warteraum/main.c34
-rw-r--r--warteraum/test/all.do1
-rw-r--r--warteraum/test/default.exe.do18
-rwxr-xr-xwarteraum/test/run9
-rw-r--r--warteraum/test/test.h48
-rw-r--r--warteraum/test/test_form.c78
-rw-r--r--warteraum/test/test_queue.c36
-rw-r--r--warteraum/test/test_queue.do7
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