/* Unit tests for the pure parts of bokfweb (clients/web.c). */ #include #include #include "util.h" #include "web.h" static int failures = 0; static int checks = 0; #define CHECK(cond) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ #cond); \ } \ } while (0) static int parse(const char *s, struct web_req *r) { return web_parse_request(s, strlen(s), r); } static void test_parse(void) { struct web_req r; CHECK(parse("GET /web/ HTTP/1.1\r\nHost: x\r\n\r\n", &r) == 0); CHECK(strcmp(r.method, "GET") == 0 && strcmp(r.path, "/web/") == 0); CHECK(r.query[0] == '\0' && r.body_len == 0); const char *post = "POST /web/login?next=1 HTTP/1.1\r\n" "content-length: 27\r\n" "Cookie: a=1; bokf_web=tok_EN-9\r\n" "X-Forwarded-For: 203.0.113.7 \r\n\r\n" "username=anna&password=x%21"; CHECK(parse(post, &r) == 0); CHECK(strcmp(r.path, "/web/login") == 0); CHECK(strcmp(r.query, "next=1") == 0); CHECK(strcmp(r.forwarded_for, "203.0.113.7") == 0); CHECK(r.body_len == 27 && strncmp(r.body, "username=", 9) == 0); /* incomplete: headers or body still coming */ CHECK(parse("GET / HTTP/1.1\r\nHost: x\r\n", &r) == 1); CHECK(parse("POST /l HTTP/1.1\r\nContent-Length: 10\r\n\r\nabc", &r) == 1); /* malformed */ CHECK(parse("GET\r\n\r\n", &r) == -1); CHECK(parse("GET nopath HTTP/1.1\r\n\r\n", &r) == -1); CHECK(parse("GET / FTP/1.0\r\n\r\n", &r) == -1); CHECK(parse("POST / HTTP/1.1\r\nContent-Length: -1\r\n\r\n", &r) == -1); CHECK(parse("POST / HTTP/1.1\r\nContent-Length: 99999\r\n\r\n", &r) == -1); CHECK(parse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n", &r) == -1); char big[WEB_MAX_REQUEST + 16]; memset(big, 'a', sizeof big); memcpy(big, "GET /", 5); CHECK(web_parse_request(big, sizeof big, &r) == -1); /* a path longer than the field is refused, not truncated */ char lp[400] = "GET /"; memset(lp + 5, 'p', 300); strcpy(lp + 305, " HTTP/1.1\r\n\r\n"); CHECK(parse(lp, &r) == -1); } static void test_form_cookie(void) { char v[64]; const char *f = "username=anna+b&password=p%C3%A5ss%26x&empty="; CHECK(web_form_get(f, strlen(f), "username", v, sizeof v) == 0 && strcmp(v, "anna b") == 0); CHECK(web_form_get(f, strlen(f), "password", v, sizeof v) == 0 && strcmp(v, "p\xc3\xa5ss&x") == 0); CHECK(web_form_get(f, strlen(f), "empty", v, sizeof v) == 0 && v[0] == '\0'); CHECK(web_form_get(f, strlen(f), "user", v, sizeof v) == -1); CHECK(web_form_get("a=%2", 4, "a", v, sizeof v) == -1); CHECK(web_form_get("a=%zz", 5, "a", v, sizeof v) == -1); CHECK(web_form_get("a=%00", 5, "a", v, sizeof v) == -1); CHECK(web_form_get("a=12345", 7, "a", v, 4) == -1); /* does not fit */ /* the body is not NUL-terminated: len bounds it */ CHECK(web_form_get("a=1&b=2XXXX", 7, "b", v, sizeof v) == 0 && strcmp(v, "2") == 0); CHECK(web_cookie_get("a=1; bokf_web=tok; c=3", "bokf_web", v, sizeof v) == 0 && strcmp(v, "tok") == 0); CHECK(web_cookie_get("xbokf_web=tok", "bokf_web", v, sizeof v) == -1); CHECK(web_cookie_get("", "bokf_web", v, sizeof v) == -1); CHECK(web_token_ok("aZ09_-")); CHECK(!web_token_ok("")); CHECK(!web_token_ok("a b")); CHECK(!web_token_ok("a;b")); struct buf b; buf_init(&b); web_html_escape(&b, "&'"); buf_append(&b, "", 1); CHECK(strcmp((char *)b.p, "<a href="x">&'</a>") == 0); buf_free(&b); } static void test_store(void) { static struct web_store st; memset(&st, 0, sizeof st); struct web_session *s = web_store_add(&st, "s_bokfd1", "anna", 1000); CHECK(s && web_token_ok(s->token) && web_token_ok(s->handle)); CHECK(strcmp(s->token, s->handle) != 0); char tok[64], hdl[64]; snprintf(tok, sizeof tok, "%s", s->token); snprintf(hdl, sizeof hdl, "%s", s->handle); CHECK(web_store_by_token(&st, tok, 1001) == s); CHECK(web_store_by_handle(&st, hdl, 1001) == s); /* token and handle are not interchangeable */ CHECK(web_store_by_token(&st, hdl, 1001) == NULL); CHECK(web_store_by_handle(&st, tok, 1001) == NULL); CHECK(web_store_by_token(&st, "nope", 1001) == NULL); CHECK(web_store_by_token(&st, "bad;value", 1001) == NULL); /* absolute expiry */ CHECK(web_store_by_token(&st, tok, 1000 + WEB_SESSION_TTL) == NULL); web_store_del(s); CHECK(web_store_by_token(&st, tok, 1001) == NULL); /* a full store replaces the oldest session */ for (int i = 0; i < WEB_MAX_SESSIONS; i++) web_store_add(&st, "x", "u", 2000 + i); struct web_session *n = web_store_add(&st, "new", "u", 5000); CHECK(n && strcmp(n->bokf, "new") == 0); int oldest_gone = 1; for (int i = 0; i < WEB_MAX_SESSIONS; i++) if (st.s[i].created == 2000) oldest_gone = 0; CHECK(oldest_gone); } static void test_rl(void) { static struct web_rl rl; memset(&rl, 0, sizeof rl); for (int i = 0; i < WEB_RL_MAX_FAILS - 1; i++) web_rl_fail(&rl, "203.0.113.7", 100); CHECK(web_rl_blocked(&rl, "203.0.113.7", 100) == 0); web_rl_fail(&rl, "203.0.113.7", 100); CHECK(web_rl_blocked(&rl, "203.0.113.7", 100) == WEB_RL_WINDOW); /* other addresses are not affected */ CHECK(web_rl_blocked(&rl, "198.51.100.1", 100) == 0); /* the window ends */ CHECK(web_rl_blocked(&rl, "203.0.113.7", 100 + WEB_RL_WINDOW) == 0); web_rl_fail(&rl, "203.0.113.7", 100 + WEB_RL_WINDOW); CHECK(web_rl_blocked(&rl, "203.0.113.7", 100 + WEB_RL_WINDOW) == 0); /* success clears */ web_rl_ok(&rl, "203.0.113.7"); CHECK(web_rl_blocked(&rl, "203.0.113.7", 101) == 0); /* many addresses: the table never overflows */ char a[32]; for (int i = 0; i < WEB_RL_SLOTS * 2; i++) { snprintf(a, sizeof a, "10.0.%d.%d", i / 256, i % 256); web_rl_fail(&rl, a, 200); } CHECK(web_rl_blocked(&rl, a, 200) == 0); } int main(void) { test_parse(); test_form_cookie(); test_store(); test_rl(); printf("test_web: %d checks, %d failures\n", checks, failures); return failures ? 1 : 0; }