summaryrefslogtreecommitdiff
path: root/tests/test_web.c
blob: c5a3beaa17f4db0ebc595f3eb31fb9f3fc1080cc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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,
                 "&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;") == 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;
}