@velox0/cerver 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +3 -0
- package/Makefile +26 -0
- package/README.md +22 -5
- package/bin/cerver.js +11 -0
- package/lib/codegen/dispatch_gen.js +3 -0
- package/lib/commands/dev.js +23 -9
- package/lib/commands/new.js +273 -79
- package/lib/ir/transform.js +14 -20
- package/package.json +3 -3
- package/runtime/cerver.h +55 -36
- package/runtime/http_parser.c +12 -11
- package/runtime/http_writer.c +152 -45
- package/runtime/mime.c +1 -0
- package/runtime/router.c +193 -10
- package/runtime/server.c +80 -61
- package/runtime/static.c +15 -55
- package/runtime/tests/minunit.c +76 -0
- package/runtime/tests/minunit.h +64 -0
- package/runtime/tests/runtime_tests.c +464 -0
- package/test/run.js +11 -0
package/runtime/static.c
CHANGED
|
@@ -62,7 +62,7 @@ typedef struct {
|
|
|
62
62
|
|
|
63
63
|
static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
|
|
64
64
|
encoding_prefs_t prefs = {0, 0};
|
|
65
|
-
const char*
|
|
65
|
+
const char* ae = cerver_req_header(req, "Accept-Encoding");
|
|
66
66
|
if (!ae) return prefs;
|
|
67
67
|
|
|
68
68
|
if (strstr(ae, "br")) prefs.accepts_br = 1;
|
|
@@ -92,7 +92,7 @@ static void add_cache_headers(cerver_response_t* res, const char* path) {
|
|
|
92
92
|
static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
93
93
|
if (!srv->assets || srv->asset_count == 0) return -1;
|
|
94
94
|
|
|
95
|
-
const char*
|
|
95
|
+
const char* path = req->path;
|
|
96
96
|
const cerver_asset_t* found = NULL;
|
|
97
97
|
|
|
98
98
|
/*
|
|
@@ -112,7 +112,7 @@ static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_re
|
|
|
112
112
|
|
|
113
113
|
/* Try with /index.html appended (for directory-like paths) */
|
|
114
114
|
if (!found) {
|
|
115
|
-
char
|
|
115
|
+
char index_path[CERVER_MAX_PATH];
|
|
116
116
|
size_t plen = strlen(path);
|
|
117
117
|
if (plen > 0 && path[plen - 1] == '/') {
|
|
118
118
|
snprintf(index_path, sizeof(index_path), "%sindex.html", path);
|
|
@@ -189,59 +189,19 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
|
|
|
189
189
|
const char* mime = cerver_mime_from_path(full_path);
|
|
190
190
|
|
|
191
191
|
/*
|
|
192
|
-
* Use
|
|
193
|
-
* The
|
|
194
|
-
*
|
|
195
|
-
* but for simplicity we'll use read() for small files and mmap for large.
|
|
192
|
+
* Use sendfile for zero-copy filesystem static serving.
|
|
193
|
+
* The file is opened and its descriptor is stored in the response structure
|
|
194
|
+
* for streaming directly to the client socket in the writer.
|
|
196
195
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
/* Advise the kernel we'll read sequentially */
|
|
208
|
-
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
|
209
|
-
|
|
210
|
-
res->status = 200;
|
|
211
|
-
res->content_type = mime;
|
|
212
|
-
res->body = (const char*)mapped;
|
|
213
|
-
res->body_len = file_size;
|
|
214
|
-
res->_body_owned = 2; /* Special flag: needs munmap, not free */
|
|
215
|
-
} else {
|
|
216
|
-
/* Small files: read into buffer (avoids mmap overhead) */
|
|
217
|
-
int fd = open(full_path, O_RDONLY);
|
|
218
|
-
if (fd < 0) return -1;
|
|
219
|
-
|
|
220
|
-
char* file_data = malloc(file_size);
|
|
221
|
-
if (!file_data) {
|
|
222
|
-
close(fd);
|
|
223
|
-
return -1;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
size_t total = 0;
|
|
227
|
-
while (total < file_size) {
|
|
228
|
-
ssize_t n = read(fd, file_data + total, file_size - total);
|
|
229
|
-
if (n <= 0) break;
|
|
230
|
-
total += (size_t)n;
|
|
231
|
-
}
|
|
232
|
-
close(fd);
|
|
233
|
-
|
|
234
|
-
if (total != file_size) {
|
|
235
|
-
free(file_data);
|
|
236
|
-
return -1;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
res->status = 200;
|
|
240
|
-
res->content_type = mime;
|
|
241
|
-
res->body = file_data;
|
|
242
|
-
res->body_len = file_size;
|
|
243
|
-
res->_body_owned = 1; /* malloc'd */
|
|
244
|
-
}
|
|
196
|
+
int fd = open(full_path, O_RDONLY);
|
|
197
|
+
if (fd < 0) return -1;
|
|
198
|
+
|
|
199
|
+
res->status = 200;
|
|
200
|
+
res->content_type = mime;
|
|
201
|
+
res->body = NULL;
|
|
202
|
+
res->body_len = file_size;
|
|
203
|
+
res->_body_owned = 3; /* Special flag: sendfile, close fd */
|
|
204
|
+
res->_file_fd = fd;
|
|
245
205
|
|
|
246
206
|
add_cache_headers(res, path);
|
|
247
207
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#include "minunit.h"
|
|
2
|
+
|
|
3
|
+
#include <stdio.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
#include <unistd.h>
|
|
6
|
+
|
|
7
|
+
int mu_tests_run = 0;
|
|
8
|
+
int mu_tests_failed = 0;
|
|
9
|
+
int mu_test_failed = 0;
|
|
10
|
+
|
|
11
|
+
static int mu_is_tty(FILE* f) { return isatty(fileno(f)); }
|
|
12
|
+
|
|
13
|
+
static const char* mu_color(FILE* f, const char* code) { return mu_is_tty(f) ? code : ""; }
|
|
14
|
+
|
|
15
|
+
const char* mu_snip(const char* s, char* buf, size_t cap) {
|
|
16
|
+
if (s == NULL) {
|
|
17
|
+
return "(null)";
|
|
18
|
+
}
|
|
19
|
+
if (cap == 0) {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
size_t len = strlen(s);
|
|
24
|
+
if (len < cap) {
|
|
25
|
+
snprintf(buf, cap, "%s", s);
|
|
26
|
+
return buf;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (cap <= 4) {
|
|
30
|
+
size_t keep = cap - 1;
|
|
31
|
+
memcpy(buf, s, keep);
|
|
32
|
+
buf[keep] = '\0';
|
|
33
|
+
return buf;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
size_t keep = cap - 4;
|
|
37
|
+
memcpy(buf, s, keep);
|
|
38
|
+
memcpy(buf + keep, "...", 3);
|
|
39
|
+
buf[keep + 3] = '\0';
|
|
40
|
+
return buf;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void mu_fail(const char* file, int line, const char* msg) {
|
|
44
|
+
const char* red = mu_color(stderr, "\x1b[31m");
|
|
45
|
+
const char* reset = mu_color(stderr, "\x1b[0m");
|
|
46
|
+
|
|
47
|
+
mu_test_failed = 1;
|
|
48
|
+
fprintf(stderr, "%sFAIL%s %s:%d: %s\n", red, reset, file, line, msg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void mu_run(const char* name, mu_test_fn fn) {
|
|
52
|
+
mu_tests_run++;
|
|
53
|
+
mu_test_failed = 0;
|
|
54
|
+
fn();
|
|
55
|
+
if (mu_test_failed) {
|
|
56
|
+
mu_tests_failed++;
|
|
57
|
+
fprintf(stderr, " in %s\n", name);
|
|
58
|
+
} else {
|
|
59
|
+
const char* green = mu_color(stdout, "\x1b[32m");
|
|
60
|
+
const char* reset = mu_color(stdout, "\x1b[0m");
|
|
61
|
+
printf("%sok%s %s\n", green, reset, name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
int mu_report(void) {
|
|
66
|
+
const char* reset = mu_color(stdout, "\x1b[0m");
|
|
67
|
+
if (mu_tests_failed) {
|
|
68
|
+
const char* red = mu_color(stdout, "\x1b[31m");
|
|
69
|
+
printf("%sSummary:%s %d tests, %s%d failures%s\n", red, reset, mu_tests_run, red,
|
|
70
|
+
mu_tests_failed, reset);
|
|
71
|
+
} else {
|
|
72
|
+
const char* green = mu_color(stdout, "\x1b[32m");
|
|
73
|
+
printf("%sSummary:%s %d tests, %s0 failures%s\n", green, reset, mu_tests_run, green, reset);
|
|
74
|
+
}
|
|
75
|
+
return mu_tests_failed ? 1 : 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#ifndef CERVER_MINUNIT_H
|
|
2
|
+
#define CERVER_MINUNIT_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
#include <stdio.h>
|
|
6
|
+
#include <string.h>
|
|
7
|
+
|
|
8
|
+
typedef void (*mu_test_fn)(void);
|
|
9
|
+
|
|
10
|
+
#define MU_SNIP_MAX 80
|
|
11
|
+
|
|
12
|
+
extern int mu_tests_run;
|
|
13
|
+
extern int mu_tests_failed;
|
|
14
|
+
extern int mu_test_failed;
|
|
15
|
+
|
|
16
|
+
void mu_fail(const char* file, int line, const char* msg);
|
|
17
|
+
void mu_run(const char* name, mu_test_fn fn);
|
|
18
|
+
int mu_report(void);
|
|
19
|
+
const char* mu_snip(const char* s, char* buf, size_t cap);
|
|
20
|
+
|
|
21
|
+
#define MU_ASSERT(cond) \
|
|
22
|
+
do { \
|
|
23
|
+
if (!(cond)) { \
|
|
24
|
+
mu_fail(__FILE__, __LINE__, #cond); \
|
|
25
|
+
return; \
|
|
26
|
+
} \
|
|
27
|
+
} while (0)
|
|
28
|
+
|
|
29
|
+
#define MU_ASSERT_EQ_INT(a, b) \
|
|
30
|
+
do { \
|
|
31
|
+
if ((a) != (b)) { \
|
|
32
|
+
char _mu_buf[128]; \
|
|
33
|
+
snprintf(_mu_buf, sizeof(_mu_buf), "got %d expected %d", (int)(a), (int)(b)); \
|
|
34
|
+
mu_fail(__FILE__, __LINE__, _mu_buf); \
|
|
35
|
+
return; \
|
|
36
|
+
} \
|
|
37
|
+
} while (0)
|
|
38
|
+
|
|
39
|
+
#define MU_ASSERT_EQ_SIZE(a, b) \
|
|
40
|
+
do { \
|
|
41
|
+
if ((a) != (b)) { \
|
|
42
|
+
char _mu_buf[128]; \
|
|
43
|
+
snprintf(_mu_buf, sizeof(_mu_buf), "got %zu expected %zu", (size_t)(a), (size_t)(b)); \
|
|
44
|
+
mu_fail(__FILE__, __LINE__, _mu_buf); \
|
|
45
|
+
return; \
|
|
46
|
+
} \
|
|
47
|
+
} while (0)
|
|
48
|
+
|
|
49
|
+
#define MU_ASSERT_STREQ(a, b) \
|
|
50
|
+
do { \
|
|
51
|
+
if (((a) == NULL && (b) != NULL) || ((a) != NULL && (b) == NULL) || \
|
|
52
|
+
((a) != NULL && (b) != NULL && strcmp((a), (b)) != 0)) { \
|
|
53
|
+
char _mu_buf[256]; \
|
|
54
|
+
char _mu_a[MU_SNIP_MAX]; \
|
|
55
|
+
char _mu_b[MU_SNIP_MAX]; \
|
|
56
|
+
const char* _mu_as = mu_snip((a), _mu_a, sizeof(_mu_a)); \
|
|
57
|
+
const char* _mu_bs = mu_snip((b), _mu_b, sizeof(_mu_b)); \
|
|
58
|
+
snprintf(_mu_buf, sizeof(_mu_buf), "got '%s' expected '%s'", _mu_as, _mu_bs); \
|
|
59
|
+
mu_fail(__FILE__, __LINE__, _mu_buf); \
|
|
60
|
+
return; \
|
|
61
|
+
} \
|
|
62
|
+
} while (0)
|
|
63
|
+
|
|
64
|
+
#endif
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#include "cerver.h"
|
|
2
|
+
#include "minunit.h"
|
|
3
|
+
|
|
4
|
+
#include <errno.h>
|
|
5
|
+
#include <fcntl.h>
|
|
6
|
+
#include <limits.h>
|
|
7
|
+
#include <stddef.h>
|
|
8
|
+
#include <stdio.h>
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
#include <string.h>
|
|
11
|
+
#include <sys/mman.h>
|
|
12
|
+
#include <sys/stat.h>
|
|
13
|
+
#include <time.h>
|
|
14
|
+
#include <unistd.h>
|
|
15
|
+
|
|
16
|
+
static const char* res_header(const cerver_response_t* res, const char* key) {
|
|
17
|
+
for (int i = 0; i < res->header_count; i++) {
|
|
18
|
+
if (strcmp(res->headers[i].key, key) == 0) {
|
|
19
|
+
return res->headers[i].value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return NULL;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static void req_add_header(cerver_request_t* req, const char* key, const char* value) {
|
|
26
|
+
req->headers[req->header_count].key = key;
|
|
27
|
+
req->headers[req->header_count].value = value;
|
|
28
|
+
req->header_count++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static int write_file(const char* path, const void* data, size_t len) {
|
|
32
|
+
int fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0600);
|
|
33
|
+
if (fd < 0) {
|
|
34
|
+
return -1;
|
|
35
|
+
}
|
|
36
|
+
const unsigned char* p = (const unsigned char*)data;
|
|
37
|
+
size_t off = 0;
|
|
38
|
+
while (off < len) {
|
|
39
|
+
ssize_t n = write(fd, p + off, len - off);
|
|
40
|
+
if (n <= 0) {
|
|
41
|
+
close(fd);
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
off += (size_t)n;
|
|
45
|
+
}
|
|
46
|
+
close(fd);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static ssize_t read_all(int fd, char* buf, size_t cap) {
|
|
51
|
+
size_t off = 0;
|
|
52
|
+
while (off + 1 < cap) {
|
|
53
|
+
ssize_t n = read(fd, buf + off, cap - off - 1);
|
|
54
|
+
if (n < 0) {
|
|
55
|
+
return -1;
|
|
56
|
+
}
|
|
57
|
+
if (n == 0) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
off += (size_t)n;
|
|
61
|
+
}
|
|
62
|
+
buf[off] = '\0';
|
|
63
|
+
return (ssize_t)off;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static void test_parse_request_basic(void) {
|
|
67
|
+
const char* raw =
|
|
68
|
+
"POST /files/hello%20world?foo=bar&name=Bob+Smith HTTP/1.1\r\n"
|
|
69
|
+
"Host: example.com\r\n"
|
|
70
|
+
"Content-Length: 5\r\n"
|
|
71
|
+
"Connection: keep-alive\r\n"
|
|
72
|
+
"\r\n"
|
|
73
|
+
"hello";
|
|
74
|
+
const size_t len = strlen(raw);
|
|
75
|
+
char* buf = (char*)malloc(len + 1);
|
|
76
|
+
MU_ASSERT(buf != NULL);
|
|
77
|
+
memcpy(buf, raw, len);
|
|
78
|
+
|
|
79
|
+
cerver_request_t req;
|
|
80
|
+
memset(&req, 0, sizeof(req));
|
|
81
|
+
|
|
82
|
+
MU_ASSERT_EQ_INT(0, cerver_parse_request(buf, len, &req));
|
|
83
|
+
MU_ASSERT_STREQ("POST", req.method);
|
|
84
|
+
MU_ASSERT_STREQ("/files/hello world", req.path);
|
|
85
|
+
MU_ASSERT_EQ_INT(2, req.query_count);
|
|
86
|
+
MU_ASSERT_STREQ("foo", req.query[0].key);
|
|
87
|
+
MU_ASSERT_STREQ("bar", req.query[0].value);
|
|
88
|
+
MU_ASSERT_STREQ("name", req.query[1].key);
|
|
89
|
+
MU_ASSERT_STREQ("Bob Smith", req.query[1].value);
|
|
90
|
+
MU_ASSERT_STREQ("example.com", cerver_req_header(&req, "host"));
|
|
91
|
+
MU_ASSERT_EQ_SIZE(5, req.body_len);
|
|
92
|
+
MU_ASSERT(memcmp(req.body, "hello", 5) == 0);
|
|
93
|
+
|
|
94
|
+
free(buf);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static void test_parse_request_trailing_slash(void) {
|
|
98
|
+
const char* raw = "GET /about/ HTTP/1.1\r\n\r\n";
|
|
99
|
+
const size_t len = strlen(raw);
|
|
100
|
+
char* buf = (char*)malloc(len + 1);
|
|
101
|
+
MU_ASSERT(buf != NULL);
|
|
102
|
+
memcpy(buf, raw, len);
|
|
103
|
+
|
|
104
|
+
cerver_request_t req;
|
|
105
|
+
memset(&req, 0, sizeof(req));
|
|
106
|
+
|
|
107
|
+
MU_ASSERT_EQ_INT(0, cerver_parse_request(buf, len, &req));
|
|
108
|
+
MU_ASSERT_STREQ("GET", req.method);
|
|
109
|
+
MU_ASSERT_STREQ("/about", req.path);
|
|
110
|
+
MU_ASSERT_EQ_INT(0, req.query_count);
|
|
111
|
+
|
|
112
|
+
free(buf);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static void test_write_response_keepalive(void) {
|
|
116
|
+
int fds[2];
|
|
117
|
+
MU_ASSERT(pipe(fds) == 0);
|
|
118
|
+
|
|
119
|
+
cerver_response_t res;
|
|
120
|
+
memset(&res, 0, sizeof(res));
|
|
121
|
+
cerver_res_text(&res, 200, "ok");
|
|
122
|
+
cerver_res_header(&res, "X-Test", "1");
|
|
123
|
+
|
|
124
|
+
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
125
|
+
close(fds[1]);
|
|
126
|
+
|
|
127
|
+
char out[1024];
|
|
128
|
+
MU_ASSERT(read_all(fds[0], out, sizeof(out)) > 0);
|
|
129
|
+
close(fds[0]);
|
|
130
|
+
|
|
131
|
+
MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
|
|
132
|
+
MU_ASSERT(strstr(out, "Content-Type: text/plain; charset=utf-8\r\n") != NULL);
|
|
133
|
+
MU_ASSERT(strstr(out, "Content-Length: 2\r\n") != NULL);
|
|
134
|
+
MU_ASSERT(strstr(out, "X-Test: 1\r\n") != NULL);
|
|
135
|
+
MU_ASSERT(strstr(out, "Connection: keep-alive\r\n") != NULL);
|
|
136
|
+
MU_ASSERT(strstr(out, "Server: cerver\r\n") != NULL);
|
|
137
|
+
MU_ASSERT(strstr(out, "\r\nok") != NULL);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static void test_write_response_force_close(void) {
|
|
141
|
+
int fds[2];
|
|
142
|
+
MU_ASSERT(pipe(fds) == 0);
|
|
143
|
+
|
|
144
|
+
cerver_response_t res;
|
|
145
|
+
memset(&res, 0, sizeof(res));
|
|
146
|
+
cerver_res_text(&res, 200, "ok");
|
|
147
|
+
res._force_close = 1;
|
|
148
|
+
|
|
149
|
+
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
150
|
+
close(fds[1]);
|
|
151
|
+
|
|
152
|
+
char out[512];
|
|
153
|
+
MU_ASSERT(read_all(fds[0], out, sizeof(out)) > 0);
|
|
154
|
+
close(fds[0]);
|
|
155
|
+
|
|
156
|
+
MU_ASSERT(strstr(out, "Connection: close\r\n") != NULL);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static void handler_a(cerver_request_t* req, cerver_response_t* res) {
|
|
160
|
+
(void)req;
|
|
161
|
+
(void)res;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static void handler_b(cerver_request_t* req, cerver_response_t* res) {
|
|
165
|
+
(void)req;
|
|
166
|
+
(void)res;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
static cerver_handler_fn dispatch_override(cerver_request_t* req) {
|
|
170
|
+
if (strcmp(req->path, "/override") == 0) {
|
|
171
|
+
return handler_a;
|
|
172
|
+
}
|
|
173
|
+
return NULL;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static void test_route_match_and_dispatch(void) {
|
|
177
|
+
cerver_server_t srv;
|
|
178
|
+
cerver_init(&srv, 8080, 1);
|
|
179
|
+
|
|
180
|
+
cerver_route_t routes[2];
|
|
181
|
+
routes[0].method = "GET";
|
|
182
|
+
routes[0].pattern = "/users/:id";
|
|
183
|
+
routes[0].handler = handler_b;
|
|
184
|
+
routes[1].method = "GET";
|
|
185
|
+
routes[1].pattern = "/about";
|
|
186
|
+
routes[1].handler = handler_a;
|
|
187
|
+
cerver_add_routes(&srv, routes, 2);
|
|
188
|
+
cerver_set_dispatch(&srv, dispatch_override);
|
|
189
|
+
|
|
190
|
+
cerver_request_t req;
|
|
191
|
+
memset(&req, 0, sizeof(req));
|
|
192
|
+
strcpy(req.method, "GET");
|
|
193
|
+
strcpy(req.path, "/users/123");
|
|
194
|
+
|
|
195
|
+
MU_ASSERT(cerver_route_match(&routes[0], &req) == 1);
|
|
196
|
+
MU_ASSERT_EQ_INT(1, req.params_count);
|
|
197
|
+
MU_ASSERT_STREQ("123", cerver_req_param(&req, "id"));
|
|
198
|
+
|
|
199
|
+
memset(&req, 0, sizeof(req));
|
|
200
|
+
strcpy(req.method, "GET");
|
|
201
|
+
strcpy(req.path, "/override");
|
|
202
|
+
|
|
203
|
+
MU_ASSERT(cerver_dispatch(&srv, &req) == handler_a);
|
|
204
|
+
|
|
205
|
+
memset(&req, 0, sizeof(req));
|
|
206
|
+
strcpy(req.method, "GET");
|
|
207
|
+
strcpy(req.path, "/about");
|
|
208
|
+
|
|
209
|
+
MU_ASSERT(cerver_dispatch(&srv, &req) == handler_a);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static void test_route_match_mismatch_resets_params(void) {
|
|
213
|
+
cerver_route_t route;
|
|
214
|
+
route.method = "GET";
|
|
215
|
+
route.pattern = "/users/:id/profile";
|
|
216
|
+
route.handler = handler_a;
|
|
217
|
+
|
|
218
|
+
cerver_request_t req;
|
|
219
|
+
memset(&req, 0, sizeof(req));
|
|
220
|
+
strcpy(req.method, "GET");
|
|
221
|
+
strcpy(req.path, "/users/123/settings");
|
|
222
|
+
|
|
223
|
+
MU_ASSERT(cerver_route_match(&route, &req) == 0);
|
|
224
|
+
MU_ASSERT_EQ_INT(0, req.params_count);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static void test_route_match_multi_segment(void) {
|
|
228
|
+
cerver_route_t route;
|
|
229
|
+
route.method = "GET";
|
|
230
|
+
route.pattern = "/users/:id/profile";
|
|
231
|
+
route.handler = handler_a;
|
|
232
|
+
|
|
233
|
+
cerver_request_t req;
|
|
234
|
+
memset(&req, 0, sizeof(req));
|
|
235
|
+
strcpy(req.method, "GET");
|
|
236
|
+
strcpy(req.path, "/users/123/profile");
|
|
237
|
+
|
|
238
|
+
MU_ASSERT(cerver_route_match(&route, &req) == 1);
|
|
239
|
+
MU_ASSERT_EQ_INT(1, req.params_count);
|
|
240
|
+
MU_ASSERT_STREQ("123", cerver_req_param(&req, "id"));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static void test_request_header_helpers(void) {
|
|
244
|
+
cerver_request_t req;
|
|
245
|
+
memset(&req, 0, sizeof(req));
|
|
246
|
+
req_add_header(&req, "Connection", "close");
|
|
247
|
+
req_add_header(&req, "X-Test", "value");
|
|
248
|
+
|
|
249
|
+
MU_ASSERT_STREQ("value", cerver_req_header(&req, "x-test"));
|
|
250
|
+
MU_ASSERT_EQ_INT(1, cerver_req_wants_close(&req));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
static void test_static_embedded_prefers_br(void) {
|
|
254
|
+
cerver_server_t srv;
|
|
255
|
+
cerver_init(&srv, 8080, 1);
|
|
256
|
+
|
|
257
|
+
static const unsigned char data[] = "hello";
|
|
258
|
+
static const unsigned char data_br[] = "brdata";
|
|
259
|
+
static const unsigned char data_gz[] = "gzdata";
|
|
260
|
+
|
|
261
|
+
cerver_asset_t assets[1];
|
|
262
|
+
assets[0].path = "/index.html";
|
|
263
|
+
assets[0].mime_type = "text/html";
|
|
264
|
+
assets[0].data = data;
|
|
265
|
+
assets[0].data_len = sizeof(data) - 1;
|
|
266
|
+
assets[0].data_br = data_br;
|
|
267
|
+
assets[0].data_br_len = sizeof(data_br) - 1;
|
|
268
|
+
assets[0].data_gz = data_gz;
|
|
269
|
+
assets[0].data_gz_len = sizeof(data_gz) - 1;
|
|
270
|
+
cerver_set_assets(&srv, assets, 1);
|
|
271
|
+
|
|
272
|
+
cerver_request_t req;
|
|
273
|
+
cerver_response_t res;
|
|
274
|
+
memset(&req, 0, sizeof(req));
|
|
275
|
+
memset(&res, 0, sizeof(res));
|
|
276
|
+
strcpy(req.method, "GET");
|
|
277
|
+
strcpy(req.path, "/index.html");
|
|
278
|
+
req_add_header(&req, "Accept-Encoding", "br, gzip");
|
|
279
|
+
|
|
280
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
281
|
+
MU_ASSERT(res.body == (const char*)data_br);
|
|
282
|
+
MU_ASSERT_STREQ("br", res_header(&res, "Content-Encoding"));
|
|
283
|
+
MU_ASSERT_STREQ("Accept-Encoding", res_header(&res, "Vary"));
|
|
284
|
+
MU_ASSERT(res_header(&res, "Cache-Control") != NULL);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
static void test_static_embedded_index_fallback(void) {
|
|
288
|
+
cerver_server_t srv;
|
|
289
|
+
cerver_init(&srv, 8080, 1);
|
|
290
|
+
|
|
291
|
+
static const unsigned char data[] = "docs";
|
|
292
|
+
cerver_asset_t assets[1];
|
|
293
|
+
assets[0].path = "/docs/index.html";
|
|
294
|
+
assets[0].mime_type = "text/html";
|
|
295
|
+
assets[0].data = data;
|
|
296
|
+
assets[0].data_len = sizeof(data) - 1;
|
|
297
|
+
assets[0].data_br = NULL;
|
|
298
|
+
assets[0].data_br_len = 0;
|
|
299
|
+
assets[0].data_gz = NULL;
|
|
300
|
+
assets[0].data_gz_len = 0;
|
|
301
|
+
cerver_set_assets(&srv, assets, 1);
|
|
302
|
+
|
|
303
|
+
cerver_request_t req;
|
|
304
|
+
cerver_response_t res;
|
|
305
|
+
memset(&req, 0, sizeof(req));
|
|
306
|
+
memset(&res, 0, sizeof(res));
|
|
307
|
+
strcpy(req.method, "GET");
|
|
308
|
+
strcpy(req.path, "/docs/");
|
|
309
|
+
|
|
310
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
311
|
+
MU_ASSERT(res.body == (const char*)data);
|
|
312
|
+
MU_ASSERT_STREQ("text/html", res.content_type);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
static void test_static_filesystem_small(void) {
|
|
316
|
+
char dir_template[] = "/tmp/cerver-test-XXXXXX";
|
|
317
|
+
char* dir = mkdtemp(dir_template);
|
|
318
|
+
MU_ASSERT(dir != NULL);
|
|
319
|
+
|
|
320
|
+
char file_path[PATH_MAX];
|
|
321
|
+
snprintf(file_path, sizeof(file_path), "%s/index.html", dir);
|
|
322
|
+
MU_ASSERT_EQ_INT(0, write_file(file_path, "small", 5));
|
|
323
|
+
|
|
324
|
+
cerver_server_t srv;
|
|
325
|
+
cerver_init(&srv, 8080, 1);
|
|
326
|
+
cerver_set_public_dir(&srv, dir);
|
|
327
|
+
|
|
328
|
+
cerver_request_t req;
|
|
329
|
+
cerver_response_t res;
|
|
330
|
+
memset(&req, 0, sizeof(req));
|
|
331
|
+
memset(&res, 0, sizeof(res));
|
|
332
|
+
strcpy(req.method, "GET");
|
|
333
|
+
strcpy(req.path, "/index.html");
|
|
334
|
+
|
|
335
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
336
|
+
MU_ASSERT_EQ_SIZE(5, res.body_len);
|
|
337
|
+
MU_ASSERT(res.body == NULL);
|
|
338
|
+
MU_ASSERT_EQ_INT(3, res._body_owned);
|
|
339
|
+
MU_ASSERT(res._file_fd >= 0);
|
|
340
|
+
|
|
341
|
+
int fds[2];
|
|
342
|
+
MU_ASSERT(pipe(fds) == 0);
|
|
343
|
+
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
344
|
+
close(fds[1]);
|
|
345
|
+
|
|
346
|
+
char out[1024];
|
|
347
|
+
ssize_t n = read_all(fds[0], out, sizeof(out));
|
|
348
|
+
MU_ASSERT(n > 0);
|
|
349
|
+
close(fds[0]);
|
|
350
|
+
|
|
351
|
+
MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
|
|
352
|
+
MU_ASSERT(strstr(out, "Content-Length: 5\r\n") != NULL);
|
|
353
|
+
MU_ASSERT(strstr(out, "\r\nsmall") != NULL);
|
|
354
|
+
|
|
355
|
+
if (res._body_owned == 3 && res._file_fd >= 0) {
|
|
356
|
+
close(res._file_fd);
|
|
357
|
+
}
|
|
358
|
+
unlink(file_path);
|
|
359
|
+
rmdir(dir);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
static void test_static_filesystem_large(void) {
|
|
363
|
+
char dir_template[] = "/tmp/cerver-test-XXXXXX";
|
|
364
|
+
char* dir = mkdtemp(dir_template);
|
|
365
|
+
MU_ASSERT(dir != NULL);
|
|
366
|
+
|
|
367
|
+
char file_path[PATH_MAX];
|
|
368
|
+
snprintf(file_path, sizeof(file_path), "%s/large.bin", dir);
|
|
369
|
+
|
|
370
|
+
char* payload = (char*)malloc(32000);
|
|
371
|
+
MU_ASSERT(payload != NULL);
|
|
372
|
+
memset(payload, 'a', 32000);
|
|
373
|
+
MU_ASSERT_EQ_INT(0, write_file(file_path, payload, 32000));
|
|
374
|
+
free(payload);
|
|
375
|
+
|
|
376
|
+
cerver_server_t srv;
|
|
377
|
+
cerver_init(&srv, 8080, 1);
|
|
378
|
+
cerver_set_public_dir(&srv, dir);
|
|
379
|
+
|
|
380
|
+
cerver_request_t req;
|
|
381
|
+
cerver_response_t res;
|
|
382
|
+
memset(&req, 0, sizeof(req));
|
|
383
|
+
memset(&res, 0, sizeof(res));
|
|
384
|
+
strcpy(req.method, "GET");
|
|
385
|
+
strcpy(req.path, "/large.bin");
|
|
386
|
+
|
|
387
|
+
MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
|
|
388
|
+
MU_ASSERT_EQ_SIZE(32000, res.body_len);
|
|
389
|
+
MU_ASSERT(res.body == NULL);
|
|
390
|
+
MU_ASSERT_EQ_INT(3, res._body_owned);
|
|
391
|
+
MU_ASSERT(res._file_fd >= 0);
|
|
392
|
+
|
|
393
|
+
int fds[2];
|
|
394
|
+
MU_ASSERT(pipe(fds) == 0);
|
|
395
|
+
MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
|
|
396
|
+
close(fds[1]);
|
|
397
|
+
|
|
398
|
+
char out[35000];
|
|
399
|
+
ssize_t n = read_all(fds[0], out, sizeof(out));
|
|
400
|
+
MU_ASSERT(n > 0);
|
|
401
|
+
close(fds[0]);
|
|
402
|
+
|
|
403
|
+
MU_ASSERT(strstr(out, "HTTP/1.1 200 OK\r\n") != NULL);
|
|
404
|
+
MU_ASSERT(strstr(out, "Content-Length: 32000\r\n") != NULL);
|
|
405
|
+
|
|
406
|
+
if (res._body_owned == 3 && res._file_fd >= 0) {
|
|
407
|
+
close(res._file_fd);
|
|
408
|
+
}
|
|
409
|
+
unlink(file_path);
|
|
410
|
+
rmdir(dir);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
static void test_static_rejects_unsafe_path(void) {
|
|
414
|
+
cerver_server_t srv;
|
|
415
|
+
cerver_init(&srv, 8080, 1);
|
|
416
|
+
|
|
417
|
+
cerver_request_t req;
|
|
418
|
+
cerver_response_t res;
|
|
419
|
+
memset(&req, 0, sizeof(req));
|
|
420
|
+
memset(&res, 0, sizeof(res));
|
|
421
|
+
strcpy(req.method, "GET");
|
|
422
|
+
strcpy(req.path, "/../secret");
|
|
423
|
+
|
|
424
|
+
MU_ASSERT_EQ_INT(-1, cerver_serve_static(&srv, &req, &res));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
static void test_stat_cache_store_lookup(void) {
|
|
428
|
+
cerver_stat_cache_t cache;
|
|
429
|
+
cerver_stat_cache_init(&cache);
|
|
430
|
+
|
|
431
|
+
size_t out_size = 0;
|
|
432
|
+
MU_ASSERT_EQ_INT(-1, cerver_stat_cache_lookup(&cache, "/tmp/none", &out_size));
|
|
433
|
+
cerver_stat_cache_store(&cache, "/tmp/file", 123, time(NULL));
|
|
434
|
+
MU_ASSERT_EQ_INT(0, cerver_stat_cache_lookup(&cache, "/tmp/file", &out_size));
|
|
435
|
+
MU_ASSERT_EQ_SIZE(123, out_size);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
static void test_cerver_init_fields(void) {
|
|
439
|
+
cerver_server_t srv;
|
|
440
|
+
cerver_init(&srv, 9090, 3);
|
|
441
|
+
MU_ASSERT_EQ_INT(9090, srv.port);
|
|
442
|
+
MU_ASSERT_EQ_INT(3, srv.worker_count);
|
|
443
|
+
MU_ASSERT_EQ_INT(-1, srv.sock_fd);
|
|
444
|
+
MU_ASSERT_EQ_INT(0, srv.running);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
int main(void) {
|
|
448
|
+
mu_run("parse_request_basic", test_parse_request_basic);
|
|
449
|
+
mu_run("parse_request_trailing_slash", test_parse_request_trailing_slash);
|
|
450
|
+
mu_run("write_response_keepalive", test_write_response_keepalive);
|
|
451
|
+
mu_run("write_response_force_close", test_write_response_force_close);
|
|
452
|
+
mu_run("route_match_and_dispatch", test_route_match_and_dispatch);
|
|
453
|
+
mu_run("route_match_mismatch_resets_params", test_route_match_mismatch_resets_params);
|
|
454
|
+
mu_run("route_match_multi_segment", test_route_match_multi_segment);
|
|
455
|
+
mu_run("request_header_helpers", test_request_header_helpers);
|
|
456
|
+
mu_run("static_embedded_prefers_br", test_static_embedded_prefers_br);
|
|
457
|
+
mu_run("static_embedded_index_fallback", test_static_embedded_index_fallback);
|
|
458
|
+
mu_run("static_filesystem_small", test_static_filesystem_small);
|
|
459
|
+
mu_run("static_filesystem_large", test_static_filesystem_large);
|
|
460
|
+
mu_run("static_rejects_unsafe_path", test_static_rejects_unsafe_path);
|
|
461
|
+
mu_run("stat_cache_store_lookup", test_stat_cache_store_lookup);
|
|
462
|
+
mu_run("cerver_init_fields", test_cerver_init_fields);
|
|
463
|
+
return mu_report();
|
|
464
|
+
}
|