@velox0/cerver 0.4.0 → 0.4.1
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/publish.yml +3 -4
- package/package.json +1 -1
- package/runtime/cerver.h +129 -129
- package/runtime/http_parser.c +152 -152
- package/runtime/http_writer.c +136 -126
- package/runtime/mime.c +48 -49
- package/runtime/router.c +98 -98
- package/runtime/server.c +593 -436
- package/runtime/static.c +174 -183
package/runtime/static.c
CHANGED
|
@@ -25,30 +25,30 @@
|
|
|
25
25
|
/* FNV-1a hash for fast asset lookup */
|
|
26
26
|
/* ------------------------------------------------------------------ */
|
|
27
27
|
|
|
28
|
-
static uint32_t fnv1a(const char
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
static uint32_t fnv1a(const char* str) {
|
|
29
|
+
uint32_t hash = 2166136261u;
|
|
30
|
+
while (*str) {
|
|
31
|
+
hash ^= (uint8_t)*str++;
|
|
32
|
+
hash *= 16777619u;
|
|
33
|
+
}
|
|
34
|
+
return hash;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/* ------------------------------------------------------------------ */
|
|
38
38
|
/* Path safety: prevent directory traversal */
|
|
39
39
|
/* ------------------------------------------------------------------ */
|
|
40
40
|
|
|
41
|
-
static int path_is_safe(const char
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
static int path_is_safe(const char* path) {
|
|
42
|
+
/* Reject paths with ".." */
|
|
43
|
+
if (strstr(path, "..")) return 0;
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/* Reject paths with null bytes */
|
|
46
|
+
if (memchr(path, '\0', strlen(path))) return 0;
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/* Must start with "/" */
|
|
49
|
+
if (path[0] != '/') return 0;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
return 1;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/* ------------------------------------------------------------------ */
|
|
@@ -56,220 +56,211 @@ static int path_is_safe(const char *path) {
|
|
|
56
56
|
/* ------------------------------------------------------------------ */
|
|
57
57
|
|
|
58
58
|
typedef struct {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
int accepts_gzip;
|
|
60
|
+
int accepts_br;
|
|
61
61
|
} encoding_prefs_t;
|
|
62
62
|
|
|
63
|
-
static encoding_prefs_t parse_accept_encoding(const cerver_request_t
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
|
|
64
|
+
encoding_prefs_t prefs = {0, 0};
|
|
65
|
+
const char* ae = cerver_req_header(req, "Accept-Encoding");
|
|
66
|
+
if (!ae) return prefs;
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (strstr(ae, "br")) prefs.accepts_br = 1;
|
|
69
|
+
if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
return prefs;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/* ------------------------------------------------------------------ */
|
|
75
75
|
/* Cache header helper */
|
|
76
76
|
/* ------------------------------------------------------------------ */
|
|
77
77
|
|
|
78
|
-
static void add_cache_headers(cerver_response_t
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
static void add_cache_headers(cerver_response_t* res, const char* path) {
|
|
79
|
+
/* Hashed/versioned assets (in /static/) get long cache */
|
|
80
|
+
if (strstr(path, "/static/") || strstr(path, "/assets/")) {
|
|
81
|
+
cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
|
|
82
|
+
} else {
|
|
83
|
+
/* HTML and other top-level files get short cache with revalidation */
|
|
84
|
+
cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
|
|
85
|
+
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/* ------------------------------------------------------------------ */
|
|
89
89
|
/* Serve from embedded assets — hash-accelerated lookup */
|
|
90
90
|
/* ------------------------------------------------------------------ */
|
|
91
91
|
|
|
92
|
-
static int serve_embedded(cerver_server_t
|
|
93
|
-
|
|
94
|
-
if (!srv->assets || srv->asset_count == 0) return -1;
|
|
92
|
+
static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
93
|
+
if (!srv->assets || srv->asset_count == 0) return -1;
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
const char* path = req->path;
|
|
96
|
+
const cerver_asset_t* found = NULL;
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/* Try exact match first */
|
|
107
|
-
for (int i = 0; i < srv->asset_count; i++) {
|
|
108
|
-
if (fnv1a(srv->assets[i].path) == target_hash &&
|
|
109
|
-
strcmp(srv->assets[i].path, path) == 0) {
|
|
110
|
-
found = &srv->assets[i];
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
98
|
+
/*
|
|
99
|
+
* Use FNV-1a hash for O(1) average lookup instead of linear scan.
|
|
100
|
+
* For small asset counts (<64), linear scan is fine, but hash helps
|
|
101
|
+
* when there are hundreds of embedded assets.
|
|
102
|
+
*/
|
|
103
|
+
uint32_t target_hash = fnv1a(path);
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
snprintf(index_path, sizeof(index_path), "%sindex.html", path);
|
|
121
|
-
} else {
|
|
122
|
-
snprintf(index_path, sizeof(index_path), "%s/index.html", path);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
uint32_t idx_hash = fnv1a(index_path);
|
|
126
|
-
for (int i = 0; i < srv->asset_count; i++) {
|
|
127
|
-
if (fnv1a(srv->assets[i].path) == idx_hash &&
|
|
128
|
-
strcmp(srv->assets[i].path, index_path) == 0) {
|
|
129
|
-
found = &srv->assets[i];
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
105
|
+
/* Try exact match first */
|
|
106
|
+
for (int i = 0; i < srv->asset_count; i++) {
|
|
107
|
+
if (fnv1a(srv->assets[i].path) == target_hash && strcmp(srv->assets[i].path, path) == 0) {
|
|
108
|
+
found = &srv->assets[i];
|
|
109
|
+
break;
|
|
133
110
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
cerver_res_file(res, 200, found->mime_type,
|
|
143
|
-
found->data_br, found->data_br_len);
|
|
144
|
-
cerver_res_header(res, "Content-Encoding", "br");
|
|
145
|
-
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
146
|
-
} else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
|
|
147
|
-
/* Serve gzip */
|
|
148
|
-
cerver_res_file(res, 200, found->mime_type,
|
|
149
|
-
found->data_gz, found->data_gz_len);
|
|
150
|
-
cerver_res_header(res, "Content-Encoding", "gzip");
|
|
151
|
-
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Try with /index.html appended (for directory-like paths) */
|
|
114
|
+
if (!found) {
|
|
115
|
+
char index_path[CERVER_MAX_PATH];
|
|
116
|
+
size_t plen = strlen(path);
|
|
117
|
+
if (plen > 0 && path[plen - 1] == '/') {
|
|
118
|
+
snprintf(index_path, sizeof(index_path), "%sindex.html", path);
|
|
152
119
|
} else {
|
|
153
|
-
|
|
154
|
-
cerver_res_file(res, 200, found->mime_type,
|
|
155
|
-
found->data, found->data_len);
|
|
120
|
+
snprintf(index_path, sizeof(index_path), "%s/index.html", path);
|
|
156
121
|
}
|
|
157
122
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
123
|
+
uint32_t idx_hash = fnv1a(index_path);
|
|
124
|
+
for (int i = 0; i < srv->asset_count; i++) {
|
|
125
|
+
if (fnv1a(srv->assets[i].path) == idx_hash && strcmp(srv->assets[i].path, index_path) == 0) {
|
|
126
|
+
found = &srv->assets[i];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!found) return -1;
|
|
133
|
+
|
|
134
|
+
/* Check for pre-compressed variants */
|
|
135
|
+
encoding_prefs_t enc = parse_accept_encoding(req);
|
|
136
|
+
|
|
137
|
+
if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
|
|
138
|
+
/* Serve brotli */
|
|
139
|
+
cerver_res_file(res, 200, found->mime_type, found->data_br, found->data_br_len);
|
|
140
|
+
cerver_res_header(res, "Content-Encoding", "br");
|
|
141
|
+
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
142
|
+
} else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
|
|
143
|
+
/* Serve gzip */
|
|
144
|
+
cerver_res_file(res, 200, found->mime_type, found->data_gz, found->data_gz_len);
|
|
145
|
+
cerver_res_header(res, "Content-Encoding", "gzip");
|
|
146
|
+
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
147
|
+
} else {
|
|
148
|
+
/* Serve uncompressed */
|
|
149
|
+
cerver_res_file(res, 200, found->mime_type, found->data, found->data_len);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
add_cache_headers(res, found->path);
|
|
153
|
+
|
|
154
|
+
return 0;
|
|
161
155
|
}
|
|
162
156
|
|
|
163
157
|
/* ------------------------------------------------------------------ */
|
|
164
158
|
/* Serve from filesystem — sendfile/mmap + stat cache */
|
|
165
159
|
/* ------------------------------------------------------------------ */
|
|
166
160
|
|
|
167
|
-
static int serve_filesystem(cerver_server_t
|
|
168
|
-
|
|
169
|
-
if (!srv->public_dir) return -1;
|
|
161
|
+
static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
162
|
+
if (!srv->public_dir) return -1;
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
const char* path = req->path;
|
|
165
|
+
if (!path_is_safe(path)) return -1;
|
|
173
166
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
/* Build the full filesystem path */
|
|
168
|
+
char full_path[CERVER_MAX_PATH * 2];
|
|
169
|
+
snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
|
|
177
170
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
171
|
+
/* Check if it's a directory — try index.html */
|
|
172
|
+
struct stat st;
|
|
173
|
+
if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
174
|
+
snprintf(full_path, sizeof(full_path), "%s%s/index.html", srv->public_dir, path);
|
|
175
|
+
if (stat(full_path, &st) != 0) return -1;
|
|
176
|
+
}
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
/* Must be a regular file */
|
|
179
|
+
if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
180
|
+
return -1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
size_t file_size = (size_t)st.st_size;
|
|
184
|
+
|
|
185
|
+
/* Store in stat cache for future lookups */
|
|
186
|
+
cerver_stat_cache_store(&srv->stat_cache, full_path, file_size, st.st_mtime);
|
|
187
|
+
|
|
188
|
+
/* Determine MIME type */
|
|
189
|
+
const char* mime = cerver_mime_from_path(full_path);
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Use mmap for zero-copy serving instead of fopen+malloc+fread.
|
|
193
|
+
* The mmap'd region is used directly as the response body.
|
|
194
|
+
* We mark it as _body_owned=0 since munmap needs special handling,
|
|
195
|
+
* but for simplicity we'll use read() for small files and mmap for large.
|
|
196
|
+
*/
|
|
197
|
+
if (file_size > 65536) {
|
|
198
|
+
/* Large files: mmap for zero-copy */
|
|
199
|
+
int fd = open(full_path, O_RDONLY);
|
|
200
|
+
if (fd < 0) return -1;
|
|
201
|
+
|
|
202
|
+
void* mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
203
|
+
close(fd);
|
|
204
|
+
|
|
205
|
+
if (mapped == MAP_FAILED) return -1;
|
|
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;
|
|
189
224
|
}
|
|
190
225
|
|
|
191
|
-
size_t
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
/*
|
|
200
|
-
* Use mmap for zero-copy serving instead of fopen+malloc+fread.
|
|
201
|
-
* The mmap'd region is used directly as the response body.
|
|
202
|
-
* We mark it as _body_owned=0 since munmap needs special handling,
|
|
203
|
-
* but for simplicity we'll use read() for small files and mmap for large.
|
|
204
|
-
*/
|
|
205
|
-
if (file_size > 65536) {
|
|
206
|
-
/* Large files: mmap for zero-copy */
|
|
207
|
-
int fd = open(full_path, O_RDONLY);
|
|
208
|
-
if (fd < 0) return -1;
|
|
209
|
-
|
|
210
|
-
void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
211
|
-
close(fd);
|
|
212
|
-
|
|
213
|
-
if (mapped == MAP_FAILED) return -1;
|
|
214
|
-
|
|
215
|
-
/* Advise the kernel we'll read sequentially */
|
|
216
|
-
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
|
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);
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
res->body_len = file_size;
|
|
222
|
-
res->_body_owned = 2; /* Special flag: needs munmap, not free */
|
|
223
|
-
} else {
|
|
224
|
-
/* Small files: read into buffer (avoids mmap overhead) */
|
|
225
|
-
int fd = open(full_path, O_RDONLY);
|
|
226
|
-
if (fd < 0) return -1;
|
|
227
|
-
|
|
228
|
-
char *file_data = malloc(file_size);
|
|
229
|
-
if (!file_data) {
|
|
230
|
-
close(fd);
|
|
231
|
-
return -1;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
size_t total = 0;
|
|
235
|
-
while (total < file_size) {
|
|
236
|
-
ssize_t n = read(fd, file_data + total, file_size - total);
|
|
237
|
-
if (n <= 0) break;
|
|
238
|
-
total += (size_t)n;
|
|
239
|
-
}
|
|
240
|
-
close(fd);
|
|
241
|
-
|
|
242
|
-
if (total != file_size) {
|
|
243
|
-
free(file_data);
|
|
244
|
-
return -1;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
res->status = 200;
|
|
248
|
-
res->content_type = mime;
|
|
249
|
-
res->body = file_data;
|
|
250
|
-
res->body_len = file_size;
|
|
251
|
-
res->_body_owned = 1; /* malloc'd */
|
|
234
|
+
if (total != file_size) {
|
|
235
|
+
free(file_data);
|
|
236
|
+
return -1;
|
|
252
237
|
}
|
|
253
238
|
|
|
254
|
-
|
|
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
|
+
}
|
|
255
245
|
|
|
256
|
-
|
|
246
|
+
add_cache_headers(res, path);
|
|
247
|
+
|
|
248
|
+
return 0;
|
|
257
249
|
}
|
|
258
250
|
|
|
259
251
|
/* ------------------------------------------------------------------ */
|
|
260
252
|
/* Main static serving entry point */
|
|
261
253
|
/* ------------------------------------------------------------------ */
|
|
262
254
|
|
|
263
|
-
int cerver_serve_static(cerver_server_t
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (strcmp(req->method, "GET") != 0) return -1;
|
|
255
|
+
int cerver_serve_static(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
256
|
+
/* Only serve GET requests for static files */
|
|
257
|
+
if (strcmp(req->method, "GET") != 0) return -1;
|
|
267
258
|
|
|
268
|
-
|
|
269
|
-
|
|
259
|
+
/* Try embedded assets first */
|
|
260
|
+
if (serve_embedded(srv, req, res) == 0) return 0;
|
|
270
261
|
|
|
271
|
-
|
|
272
|
-
|
|
262
|
+
/* Fall back to filesystem */
|
|
263
|
+
if (serve_filesystem(srv, req, res) == 0) return 0;
|
|
273
264
|
|
|
274
|
-
|
|
265
|
+
return -1;
|
|
275
266
|
}
|