diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_core.c | 30 | ||||
| -rw-r--r-- | tests/test_web.c | 175 |
2 files changed, 204 insertions, 1 deletions
diff --git a/tests/test_core.c b/tests/test_core.c index 6376517..a82c833 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -4904,7 +4904,15 @@ static void test_rate_limit(struct tctx *t) (void)t; yyjson_doc *d; - /* rate limiting must stay last: it blocks the login key */ + CHECK(login("admin", "secret123")); + d = call(reqf("{\"v\":1,\"id\":\"31\",\"cmd\":\"user.create\"," + "\"session\":\"%s\",\"args\":{\"username\":\"rluser\"," + "\"password\":\"rlpassword1\"}}", + g_session)); + CHECK_OK(d); + yyjson_doc_free(d); + + /* rate limiting must stay last: it blocks the admin login */ for (int i = 0; i < 5; i++) CHECK(!login("admin", "wrong")); d = call("{\"v\":1,\"id\":\"32\",\"cmd\":\"session.open\",\"args\":" @@ -4912,6 +4920,26 @@ static void test_rate_limit(struct tctx *t) "\"password\":\"secret123\"}}"); CHECK_STR(d, "error.code", "RATE_LIMITED"); yyjson_doc_free(d); + /* the limit is per user name: guessing one account never locks out + the others (a public login page must not be a lockout switch) */ + for (int i = 0; i < 5; i++) + CHECK(!login("nobody-here", "wrong")); + CHECK(login("rluser", "rlpassword1")); + /* flooding the table with names neither frees admin early nor turns + the limiter off */ + char name[32]; + for (int i = 0; i < 1100; i++) { + snprintf(name, sizeof name, "flood%d", i); + login(name, "x"); + } + d = call("{\"v\":1,\"id\":\"33\",\"cmd\":\"session.open\",\"args\":" + "{\"method\":\"password\",\"username\":\"admin\"," + "\"password\":\"secret123\"}}"); + CHECK_STR(d, "error.code", "RATE_LIMITED"); + yyjson_doc_free(d); + for (int i = 0; i < 5; i++) + CHECK(!login("rluser", "wrong")); + CHECK(!login("rluser", "rlpassword1")); } static void test_employees(struct tctx *t) diff --git a/tests/test_web.c b/tests/test_web.c new file mode 100644 index 0000000..c5a3bea --- /dev/null +++ b/tests/test_web.c @@ -0,0 +1,175 @@ +/* Unit tests for the pure parts of bokfweb (clients/web.c). */ +#include <stdio.h> +#include <string.h> + +#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, "<a href=\"x\">&'</a>"); + 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; +} |
