@velox0/cerver 0.6.4 → 0.6.5
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/lib/codegen/emit.js +43 -8
- package/lib/validator/validate.js +3 -2
- package/package.json +1 -1
- package/runtime/cerver.h +0 -2
- package/runtime/fetch.c +9 -0
- package/runtime/http_parser.c +8 -3
- package/runtime/http_writer.c +26 -20
- package/runtime/server.c +74 -3
- package/runtime/static.c +70 -6
- package/runtime/str_ops.c +28 -25
- package/runtime/tests/runtime_tests.c +1 -1
- package/runtime/win_compat.h +15 -12
package/lib/codegen/emit.js
CHANGED
|
@@ -421,7 +421,10 @@ function emitStringOpBlock(expr, pad, varName) {
|
|
|
421
421
|
call = `""` ;
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
return [
|
|
424
|
+
return [
|
|
425
|
+
`${pad}char *${varName} = ${call};`,
|
|
426
|
+
`${pad}int ${varName}_owned = (${varName} != NULL);`,
|
|
427
|
+
];
|
|
425
428
|
}
|
|
426
429
|
|
|
427
430
|
/**
|
|
@@ -447,13 +450,13 @@ function emitConcatBlock(concatExpr, pad, varName) {
|
|
|
447
450
|
const format = cString(formatParts.join(""));
|
|
448
451
|
const argList = args.length > 0 ? `, ${args.join(", ")}` : "";
|
|
449
452
|
|
|
450
|
-
|
|
451
|
-
lines.push(`${pad}int ${
|
|
453
|
+
const lenVarName = `${varName}_len`;
|
|
454
|
+
lines.push(`${pad}int ${lenVarName} = snprintf(NULL, 0, ${format}${argList});`);
|
|
455
|
+
lines.push(`${pad}char *${varName} = malloc(${lenVarName} + 1);`);
|
|
452
456
|
lines.push(`${pad}if (${varName}) {`);
|
|
453
|
-
lines.push(`${pad} snprintf(${varName},
|
|
454
|
-
lines.push(`${pad}} else {`);
|
|
455
|
-
lines.push(`${pad} ${varName} = "";`);
|
|
457
|
+
lines.push(`${pad} snprintf(${varName}, ${lenVarName} + 1, ${format}${argList});`);
|
|
456
458
|
lines.push(`${pad}}`);
|
|
459
|
+
lines.push(`${pad}int ${varName}_owned = (${varName} != NULL);`);
|
|
457
460
|
|
|
458
461
|
return lines;
|
|
459
462
|
}
|
|
@@ -512,17 +515,41 @@ function emitStatement(stmt, level, ctx) {
|
|
|
512
515
|
const pad = indent(level);
|
|
513
516
|
const lines = [];
|
|
514
517
|
|
|
518
|
+
const genCleanup = (padStr, skipVar = null) => {
|
|
519
|
+
const cl = [];
|
|
520
|
+
for (const ownedVar of ctx.ownedStrings) {
|
|
521
|
+
if (ownedVar !== skipVar) {
|
|
522
|
+
cl.push(`${padStr}if (${ownedVar}_owned) free((void *)${ownedVar});`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return cl;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const genOomCheck = (varName) => {
|
|
529
|
+
const oomPad = pad + " ";
|
|
530
|
+
return [
|
|
531
|
+
`${pad}if (!${varName}) {`,
|
|
532
|
+
`${oomPad}cerver_res_text(res, 500, "Internal Server Error (OOM)");`,
|
|
533
|
+
...genCleanup(oomPad, varName),
|
|
534
|
+
`${oomPad}return;`,
|
|
535
|
+
`${pad}}`
|
|
536
|
+
];
|
|
537
|
+
};
|
|
538
|
+
|
|
515
539
|
switch (stmt.type) {
|
|
516
540
|
case "Return": {
|
|
517
541
|
const fnName = `cerver_res_${stmt.responseType}`;
|
|
542
|
+
const returnedVarName = (stmt.value && stmt.value.type === "Identifier") ? stmt.value.name : null;
|
|
518
543
|
|
|
519
544
|
/* Check if the return value involves a fetch() call */
|
|
520
545
|
if (stmt.value && stmt.value.type === "Fetch") {
|
|
521
546
|
const tempName = `_fetch_res_${ctx.fetchVarCounter++}`;
|
|
522
547
|
const fetchBlock = emitFetchBlock(stmt.value, pad, tempName);
|
|
523
548
|
lines.push(...fetchBlock.lines);
|
|
549
|
+
lines.push(...genOomCheck(tempName));
|
|
524
550
|
lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
|
|
525
551
|
lines.push(`${pad}res->_body_owned = 1;`);
|
|
552
|
+
lines.push(...genCleanup(pad, tempName));
|
|
526
553
|
lines.push(`${pad}return;`);
|
|
527
554
|
} else if (
|
|
528
555
|
stmt.value &&
|
|
@@ -531,8 +558,10 @@ function emitStatement(stmt, level, ctx) {
|
|
|
531
558
|
) {
|
|
532
559
|
const tempName = `_concat_res_${ctx.concatVarCounter++}`;
|
|
533
560
|
lines.push(...emitConcatBlock(stmt.value, pad, tempName));
|
|
561
|
+
lines.push(...genOomCheck(tempName));
|
|
534
562
|
lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
|
|
535
563
|
lines.push(`${pad}if (${tempName}_owned) res->_body_owned = 1;`);
|
|
564
|
+
lines.push(...genCleanup(pad, tempName));
|
|
536
565
|
lines.push(`${pad}return;`);
|
|
537
566
|
} else if (
|
|
538
567
|
stmt.value &&
|
|
@@ -542,8 +571,10 @@ function emitStatement(stmt, level, ctx) {
|
|
|
542
571
|
/* Heap-allocated string op (tolower, toupper, trim, slice, replace) */
|
|
543
572
|
const tempName = `_strop_res_${ctx.concatVarCounter++}`;
|
|
544
573
|
lines.push(...emitStringOpBlock(stmt.value, pad, tempName));
|
|
545
|
-
lines.push(
|
|
546
|
-
lines.push(`${pad}
|
|
574
|
+
lines.push(...genOomCheck(tempName));
|
|
575
|
+
lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
|
|
576
|
+
lines.push(`${pad}if (${tempName}_owned) res->_body_owned = 1;`);
|
|
577
|
+
lines.push(...genCleanup(pad, tempName));
|
|
547
578
|
lines.push(`${pad}return;`);
|
|
548
579
|
} else {
|
|
549
580
|
assertInlineExpression(stmt.value);
|
|
@@ -556,6 +587,7 @@ function emitStatement(stmt, level, ctx) {
|
|
|
556
587
|
) {
|
|
557
588
|
lines.push(`${pad}res->_body_owned = 1;`);
|
|
558
589
|
}
|
|
590
|
+
lines.push(...genCleanup(pad, returnedVarName));
|
|
559
591
|
lines.push(`${pad}return;`);
|
|
560
592
|
}
|
|
561
593
|
break;
|
|
@@ -632,6 +664,7 @@ function emitStatement(stmt, level, ctx) {
|
|
|
632
664
|
if (stmt.initExpr && stmt.initExpr.type === "Fetch") {
|
|
633
665
|
const fetchBlock = emitFetchBlock(stmt.initExpr, pad, stmt.name);
|
|
634
666
|
lines.push(...fetchBlock.lines);
|
|
667
|
+
lines.push(...genOomCheck(stmt.name));
|
|
635
668
|
ctx.ownedStrings.add(stmt.name);
|
|
636
669
|
} else if (
|
|
637
670
|
stmt.initExpr &&
|
|
@@ -639,6 +672,7 @@ function emitStatement(stmt, level, ctx) {
|
|
|
639
672
|
!allLiteralConcat(stmt.initExpr)
|
|
640
673
|
) {
|
|
641
674
|
lines.push(...emitConcatBlock(stmt.initExpr, pad, stmt.name));
|
|
675
|
+
lines.push(...genOomCheck(stmt.name));
|
|
642
676
|
ctx.ownedStrings.add(stmt.name);
|
|
643
677
|
} else if (
|
|
644
678
|
stmt.initExpr &&
|
|
@@ -647,6 +681,7 @@ function emitStatement(stmt, level, ctx) {
|
|
|
647
681
|
) {
|
|
648
682
|
/* Heap-allocated string op result stored in a mutable char * */
|
|
649
683
|
lines.push(...emitStringOpBlock(stmt.initExpr, pad, stmt.name));
|
|
684
|
+
lines.push(...genOomCheck(stmt.name));
|
|
650
685
|
ctx.ownedStrings.add(stmt.name);
|
|
651
686
|
} else {
|
|
652
687
|
assertInlineExpression(stmt.initExpr);
|
|
@@ -118,10 +118,11 @@ function validate(ast, filePath, source) {
|
|
|
118
118
|
const decl = node.declaration;
|
|
119
119
|
if (decl.type === "FunctionDeclaration") {
|
|
120
120
|
const name = decl.id.name;
|
|
121
|
-
|
|
121
|
+
const validMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"];
|
|
122
|
+
if (!validMethods.includes(name)) {
|
|
122
123
|
addError(
|
|
123
124
|
decl,
|
|
124
|
-
`exported function "${name}" is not a valid HTTP method (use
|
|
125
|
+
`exported function "${name}" is not a valid HTTP method (use ${validMethods.join(", ")})`,
|
|
125
126
|
);
|
|
126
127
|
}
|
|
127
128
|
|
package/package.json
CHANGED
package/runtime/cerver.h
CHANGED
|
@@ -303,8 +303,6 @@ char* cerver_str_replace(const char* s, const char* needle, const char* replacem
|
|
|
303
303
|
int cerver_str_endswith(const char* s, const char* suffix);
|
|
304
304
|
int cerver_str_indexof(const char* s, const char* needle);
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
308
306
|
/* ------------------------------------------------------------------ */
|
|
309
307
|
/* MIME (internal) */
|
|
310
308
|
/* ------------------------------------------------------------------ */
|
package/runtime/fetch.c
CHANGED
|
@@ -120,10 +120,19 @@ char* cerver_fetch(const char* url, const char* method, const char* body, const
|
|
|
120
120
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fetch_write_cb);
|
|
121
121
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&buf);
|
|
122
122
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
123
|
+
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); /* cap redirect chain */
|
|
123
124
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
|
124
125
|
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
|
125
126
|
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); /* thread-safe */
|
|
126
127
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "cerver/1.0");
|
|
128
|
+
/* Restrict to HTTP and HTTPS only — blocks file://, gopher://, dict://, etc. */
|
|
129
|
+
#if CURL_AT_LEAST_VERSION(7, 85, 0)
|
|
130
|
+
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https");
|
|
131
|
+
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
|
|
132
|
+
#else
|
|
133
|
+
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS));
|
|
134
|
+
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, (long)(CURLPROTO_HTTP | CURLPROTO_HTTPS));
|
|
135
|
+
#endif // CURL_AT_LEAST_VERSION(7, 85, 0)
|
|
127
136
|
|
|
128
137
|
/* Set HTTP method */
|
|
129
138
|
if (method) {
|
package/runtime/http_parser.c
CHANGED
|
@@ -180,9 +180,13 @@ int cerver_parse_request(const char* raw, size_t len, cerver_request_t* req) {
|
|
|
180
180
|
req->headers[req->header_count].value = val;
|
|
181
181
|
req->header_count++;
|
|
182
182
|
|
|
183
|
-
/* Track content-length */
|
|
183
|
+
/* Track content-length — use strtoul so negative values are rejected */
|
|
184
184
|
if (strcasecmp(hdr_start, "Content-Length") == 0) {
|
|
185
|
-
|
|
185
|
+
char* endp = NULL;
|
|
186
|
+
unsigned long cl = strtoul(val, &endp, 10);
|
|
187
|
+
if (endp != val && *endp == '\0' && cl > 0 && cl <= (unsigned long)CERVER_READ_BUF_MAX) {
|
|
188
|
+
content_length = (size_t)cl;
|
|
189
|
+
}
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
}
|
|
@@ -191,7 +195,8 @@ int cerver_parse_request(const char* raw, size_t len, cerver_request_t* req) {
|
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
/* ---- Body (for POST etc.) ---- */
|
|
194
|
-
if (content_length > 0 && hdr_start < buf + len)
|
|
198
|
+
if (content_length > 0 && hdr_start < buf + len && strcmp(req->method, "GET") != 0 &&
|
|
199
|
+
strcmp(req->method, "HEAD") != 0) {
|
|
195
200
|
req->body = hdr_start;
|
|
196
201
|
req->body_len = content_length;
|
|
197
202
|
/* Ensure we don't read past the buffer */
|
package/runtime/http_writer.c
CHANGED
|
@@ -142,28 +142,34 @@ static const char* status_text(int code) {
|
|
|
142
142
|
int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
|
|
143
143
|
cerver_sock_t sfd = (cerver_sock_t)fd;
|
|
144
144
|
|
|
145
|
-
char
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
char header[4096];
|
|
146
|
+
size_t hlen = 0;
|
|
147
|
+
|
|
148
|
+
#define HDR_APPEND(...) \
|
|
149
|
+
do { \
|
|
150
|
+
if (hlen < sizeof(header)) { \
|
|
151
|
+
int _n = snprintf(header + hlen, sizeof(header) - hlen, __VA_ARGS__); \
|
|
152
|
+
if (_n > 0) hlen += (size_t)_n; \
|
|
153
|
+
if (hlen > sizeof(header)) hlen = sizeof(header); \
|
|
154
|
+
} \
|
|
155
|
+
} while (0)
|
|
156
|
+
|
|
157
|
+
HDR_APPEND("HTTP/1.1 %d %s\r\n", res->status, status_text(res->status));
|
|
158
|
+
if (res->content_type) HDR_APPEND("Content-Type: %s\r\n", res->content_type);
|
|
159
|
+
HDR_APPEND("Content-Length: %zu\r\n", res->body_len);
|
|
155
160
|
for (int i = 0; i < res->header_count; i++)
|
|
156
|
-
|
|
157
|
-
res->headers[i].key, res->headers[i].value);
|
|
161
|
+
HDR_APPEND("%s: %s\r\n", res->headers[i].key, res->headers[i].value);
|
|
158
162
|
if (keepalive && !res->_force_close)
|
|
159
|
-
|
|
163
|
+
HDR_APPEND("Connection: keep-alive\r\n");
|
|
160
164
|
else
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
HDR_APPEND("Connection: close\r\n");
|
|
166
|
+
HDR_APPEND("Server: cerver\r\n\r\n");
|
|
167
|
+
|
|
168
|
+
#undef HDR_APPEND
|
|
163
169
|
|
|
164
170
|
if (res->_body_owned == 3) {
|
|
165
171
|
/* File-descriptor sendfile path */
|
|
166
|
-
if (send_all(sfd, header,
|
|
172
|
+
if (send_all(sfd, header, hlen) < 0) return -1;
|
|
167
173
|
size_t total = res->body_len, sent = 0;
|
|
168
174
|
while (sent < total) {
|
|
169
175
|
ssize_t n = do_sendfile(sfd, res->_file_fd, (off_t)sent, total - sent);
|
|
@@ -177,17 +183,17 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
|
|
|
177
183
|
sent += (size_t)n;
|
|
178
184
|
}
|
|
179
185
|
} else if (res->body && res->body_len > 0) {
|
|
180
|
-
if (
|
|
186
|
+
if (hlen + res->body_len <= sizeof(header)) {
|
|
181
187
|
/* Small response: one syscall */
|
|
182
188
|
memcpy(header + hlen, res->body, res->body_len);
|
|
183
|
-
if (send_all(sfd, header,
|
|
189
|
+
if (send_all(sfd, header, hlen + res->body_len) < 0) return -1;
|
|
184
190
|
} else {
|
|
185
191
|
/* Large response: header then body */
|
|
186
|
-
if (send_all(sfd, header,
|
|
192
|
+
if (send_all(sfd, header, hlen) < 0) return -1;
|
|
187
193
|
if (send_all(sfd, res->body, res->body_len) < 0) return -1;
|
|
188
194
|
}
|
|
189
195
|
} else {
|
|
190
|
-
if (send_all(sfd, header,
|
|
196
|
+
if (send_all(sfd, header, hlen) < 0) return -1;
|
|
191
197
|
}
|
|
192
198
|
|
|
193
199
|
return 0;
|
package/runtime/server.c
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
#include <netinet/in.h>
|
|
34
34
|
#include <netinet/tcp.h>
|
|
35
35
|
#include <arpa/inet.h>
|
|
36
|
+
#include <strings.h>
|
|
36
37
|
#endif // !CERVER_PLATFORM_WINDOWS
|
|
37
38
|
|
|
38
39
|
#if defined(__APPLE__) || defined(__FreeBSD__)
|
|
@@ -214,6 +215,7 @@ static char* read_full_request(cerver_sock_t fd, size_t* out_len) {
|
|
|
214
215
|
per-connection in handle_connection). Just read normally. */
|
|
215
216
|
#endif // CERVER_PLATFORM_WINDOWS
|
|
216
217
|
|
|
218
|
+
/* Phase 1: read until we have the full header section (\r\n\r\n). */
|
|
217
219
|
while (len < (size_t)CERVER_READ_BUF_MAX) {
|
|
218
220
|
ssize_t n = (ssize_t)cerver_sock_read(fd, buf + len, cap - len);
|
|
219
221
|
if (n <= 0) break;
|
|
@@ -236,6 +238,75 @@ static char* read_full_request(cerver_sock_t fd, size_t* out_len) {
|
|
|
236
238
|
free(buf);
|
|
237
239
|
return NULL;
|
|
238
240
|
}
|
|
241
|
+
|
|
242
|
+
/*
|
|
243
|
+
* Phase 2: if a Content-Length header is present, read the body.
|
|
244
|
+
*
|
|
245
|
+
* Scan the headers we just received for "Content-Length:" and compute
|
|
246
|
+
* how many body bytes we still need to pull from the socket. We do a
|
|
247
|
+
* minimal in-buffer scan here (before the real parser runs) so that the
|
|
248
|
+
* body always arrives in the same allocation that the parser will
|
|
249
|
+
* reference — preventing request smuggling over keep-alive connections.
|
|
250
|
+
*/
|
|
251
|
+
buf[len] = '\0';
|
|
252
|
+
const char* hdr_end_ptr = (const char*)memmem(buf, len, "\r\n\r\n", 4);
|
|
253
|
+
if (hdr_end_ptr) {
|
|
254
|
+
size_t hdr_block_len = (size_t)(hdr_end_ptr - buf) + 4;
|
|
255
|
+
size_t content_length = 0;
|
|
256
|
+
|
|
257
|
+
/* Case-insensitive scan for Content-Length within the header block. */
|
|
258
|
+
const char* p = buf;
|
|
259
|
+
while (p < buf + hdr_block_len) {
|
|
260
|
+
const char* eol = (const char*)memmem(p, (size_t)(buf + hdr_block_len - p), "\r\n", 2);
|
|
261
|
+
if (!eol) break;
|
|
262
|
+
size_t line_len = (size_t)(eol - p);
|
|
263
|
+
if (line_len > 15 && (p[0] == 'C' || p[0] == 'c') &&
|
|
264
|
+
strncasecmp(p, "Content-Length:", 15) == 0) {
|
|
265
|
+
const char* val = p + 15;
|
|
266
|
+
char* endp = NULL;
|
|
267
|
+
while (*val == ' ' || *val == '\t') val++;
|
|
268
|
+
unsigned long cl = strtoul(val, &endp, 10);
|
|
269
|
+
if (endp != val && cl > 0 && cl <= (unsigned long)CERVER_READ_BUF_MAX) {
|
|
270
|
+
content_length = (size_t)cl;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
p = eol + 2;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (content_length > 0) {
|
|
278
|
+
size_t body_received = (len > hdr_block_len) ? (len - hdr_block_len) : 0;
|
|
279
|
+
size_t body_needed = (content_length > body_received) ? (content_length - body_received) : 0;
|
|
280
|
+
size_t total_needed = len + body_needed;
|
|
281
|
+
|
|
282
|
+
if (total_needed > (size_t)CERVER_READ_BUF_MAX) {
|
|
283
|
+
/* Body would exceed hard limit — clamp to what we'll accept. */
|
|
284
|
+
total_needed = (size_t)CERVER_READ_BUF_MAX;
|
|
285
|
+
body_needed = (total_needed > len) ? (total_needed - len) : 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (body_needed > 0) {
|
|
289
|
+
if (total_needed > cap) {
|
|
290
|
+
char* tmp = realloc(buf, total_needed + 1);
|
|
291
|
+
if (!tmp) {
|
|
292
|
+
free(buf);
|
|
293
|
+
return NULL;
|
|
294
|
+
}
|
|
295
|
+
buf = tmp;
|
|
296
|
+
cap = total_needed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Read exactly the remaining body bytes. */
|
|
300
|
+
while (body_needed > 0) {
|
|
301
|
+
ssize_t n = (ssize_t)cerver_sock_read(fd, buf + len, body_needed);
|
|
302
|
+
if (n <= 0) break;
|
|
303
|
+
len += (size_t)n;
|
|
304
|
+
body_needed -= (size_t)n;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
239
310
|
buf[len] = '\0';
|
|
240
311
|
*out_len = len;
|
|
241
312
|
return buf;
|
|
@@ -261,10 +332,10 @@ static void handle_connection(cerver_server_t* srv, cerver_sock_t client_fd) {
|
|
|
261
332
|
sizeof(nodelay));
|
|
262
333
|
}
|
|
263
334
|
|
|
264
|
-
int request_count = 0;
|
|
265
|
-
int
|
|
335
|
+
unsigned int request_count = 0;
|
|
336
|
+
int keepalive = 1;
|
|
266
337
|
|
|
267
|
-
while (keepalive && srv->running && request_count < CERVER_KEEPALIVE_MAX) {
|
|
338
|
+
while (keepalive && srv->running && request_count < (unsigned int)CERVER_KEEPALIVE_MAX) {
|
|
268
339
|
int timeout_sec = (request_count == 0) ? 5 : CERVER_KEEPALIVE_TIMEOUT;
|
|
269
340
|
|
|
270
341
|
#if CERVER_PLATFORM_WINDOWS
|
package/runtime/static.c
CHANGED
|
@@ -43,7 +43,7 @@ static uint32_t fnv1a(const char* str) {
|
|
|
43
43
|
/* ------------------------------------------------------------------ */
|
|
44
44
|
|
|
45
45
|
static int path_is_safe(const char* path) {
|
|
46
|
-
/* Reject paths with ".." */
|
|
46
|
+
/* Reject paths with ".." (literal traversal) */
|
|
47
47
|
if (strstr(path, "..")) return 0;
|
|
48
48
|
|
|
49
49
|
/* Reject paths with null bytes */
|
|
@@ -52,6 +52,34 @@ static int path_is_safe(const char* path) {
|
|
|
52
52
|
/* Must start with "/" */
|
|
53
53
|
if (path[0] != '/') return 0;
|
|
54
54
|
|
|
55
|
+
/* Reject encoded traversal sequences that survive url_decode:
|
|
56
|
+
* url_decode runs before us, but double-encoding (%252e) and
|
|
57
|
+
* mixed-case variants may slip through on some paths. Reject any
|
|
58
|
+
* remaining %XX sequences that decode to '.' or '/'. */
|
|
59
|
+
const char* p = path;
|
|
60
|
+
while (*p) {
|
|
61
|
+
if (*p == '%' && p[1] && p[2]) {
|
|
62
|
+
int hi = (p[1] >= '0' && p[1] <= '9') ? p[1] - '0'
|
|
63
|
+
: (p[1] >= 'a' && p[1] <= 'f') ? p[1] - 'a' + 10
|
|
64
|
+
: (p[1] >= 'A' && p[1] <= 'F') ? p[1] - 'A' + 10
|
|
65
|
+
: -1;
|
|
66
|
+
int lo = (p[2] >= '0' && p[2] <= '9') ? p[2] - '0'
|
|
67
|
+
: (p[2] >= 'a' && p[2] <= 'f') ? p[2] - 'a' + 10
|
|
68
|
+
: (p[2] >= 'A' && p[2] <= 'F') ? p[2] - 'A' + 10
|
|
69
|
+
: -1;
|
|
70
|
+
if (hi >= 0 && lo >= 0) {
|
|
71
|
+
int decoded = (hi << 4) | lo;
|
|
72
|
+
/* Reject if it would decode to '.' (0x2e), '/' (0x2f), or '\' (0x5c) */
|
|
73
|
+
if (decoded == 0x2e || decoded == 0x2f || decoded == 0x5c) return 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
#if CERVER_PLATFORM_WINDOWS
|
|
77
|
+
/* Reject backslashes directly in the path */
|
|
78
|
+
if (*p == '\\') return 0;
|
|
79
|
+
#endif // CERVER_PLATFORM_WINDOWS
|
|
80
|
+
p++;
|
|
81
|
+
}
|
|
82
|
+
|
|
55
83
|
return 1;
|
|
56
84
|
}
|
|
57
85
|
|
|
@@ -64,13 +92,36 @@ typedef struct {
|
|
|
64
92
|
int accepts_br;
|
|
65
93
|
} encoding_prefs_t;
|
|
66
94
|
|
|
95
|
+
/*
|
|
96
|
+
* Check whether `token` appears as a standalone comma-separated token in
|
|
97
|
+
* the Accept-Encoding header value. Using strstr() would produce false
|
|
98
|
+
* positives (e.g. "br" matching inside "cobr" or "brotli").
|
|
99
|
+
*/
|
|
100
|
+
static int ae_has_token(const char* ae, const char* token) {
|
|
101
|
+
size_t tlen = strlen(token);
|
|
102
|
+
const char* p = ae;
|
|
103
|
+
while (*p) {
|
|
104
|
+
/* Skip whitespace and commas */
|
|
105
|
+
while (*p == ' ' || *p == '\t' || *p == ',') p++;
|
|
106
|
+
if (!*p) break;
|
|
107
|
+
/* Find end of this token (stop at comma, semicolon, space, or NUL) */
|
|
108
|
+
const char* start = p;
|
|
109
|
+
while (*p && *p != ',' && *p != ';' && *p != ' ' && *p != '\t') p++;
|
|
110
|
+
size_t len = (size_t)(p - start);
|
|
111
|
+
if (len == tlen && memcmp(start, token, tlen) == 0) return 1;
|
|
112
|
+
/* Skip quality value if present (;q=0.9) */
|
|
113
|
+
while (*p && *p != ',') p++;
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
|
|
68
119
|
encoding_prefs_t prefs = {0, 0};
|
|
69
120
|
const char* ae = cerver_req_header(req, "Accept-Encoding");
|
|
70
121
|
if (!ae) return prefs;
|
|
71
122
|
|
|
72
|
-
if (
|
|
73
|
-
if (
|
|
123
|
+
if (ae_has_token(ae, "br")) prefs.accepts_br = 1;
|
|
124
|
+
if (ae_has_token(ae, "gzip")) prefs.accepts_gzip = 1;
|
|
74
125
|
|
|
75
126
|
return prefs;
|
|
76
127
|
}
|
|
@@ -222,28 +273,41 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
|
|
|
222
273
|
const char* path = req->path;
|
|
223
274
|
if (!path_is_safe(path)) return -1;
|
|
224
275
|
|
|
225
|
-
/* Build the full filesystem path */
|
|
276
|
+
/* Build the full filesystem path — reject if it would overflow. */
|
|
277
|
+
size_t dir_len = strlen(srv->public_dir);
|
|
278
|
+
size_t path_len = strlen(path);
|
|
279
|
+
if (dir_len + path_len + 1 > sizeof(((struct { char b[CERVER_MAX_PATH * 2]; }){}).b)) return -1;
|
|
280
|
+
|
|
226
281
|
char full_path[CERVER_MAX_PATH * 2];
|
|
282
|
+
/* Bounds already verified above; silence -Wformat-truncation. */
|
|
283
|
+
#if defined(__GNUC__) && !defined(__clang__)
|
|
284
|
+
#pragma GCC diagnostic push
|
|
285
|
+
#pragma GCC diagnostic ignored "-Wformat-truncation"
|
|
286
|
+
#endif // __GNUC__ && !__clang__
|
|
227
287
|
snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
|
|
288
|
+
#if defined(__GNUC__) && !defined(__clang__)
|
|
289
|
+
#pragma GCC diagnostic pop
|
|
290
|
+
#endif // __GNUC__ && !__clang__
|
|
228
291
|
|
|
229
292
|
#if CERVER_PLATFORM_WINDOWS
|
|
230
293
|
/* Normalize forward slashes to backslashes for native Windows APIs */
|
|
231
294
|
for (char* p = full_path; *p; p++) {
|
|
232
295
|
if (*p == '/') *p = '\\';
|
|
233
296
|
}
|
|
234
|
-
#endif
|
|
297
|
+
#endif // CERVER_PLATFORM_WINDOWS
|
|
235
298
|
|
|
236
299
|
/* Check if it's a directory — try fallback path */
|
|
237
300
|
struct stat st;
|
|
238
301
|
if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
239
302
|
char fallback_path[CERVER_MAX_PATH];
|
|
240
303
|
get_fallback_path(path, fallback_path, sizeof(fallback_path));
|
|
304
|
+
if (dir_len + strlen(fallback_path) + 1 > sizeof(full_path)) return -1;
|
|
241
305
|
snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, fallback_path);
|
|
242
306
|
#if CERVER_PLATFORM_WINDOWS
|
|
243
307
|
for (char* p = full_path; *p; p++) {
|
|
244
308
|
if (*p == '/') *p = '\\';
|
|
245
309
|
}
|
|
246
|
-
#endif
|
|
310
|
+
#endif // CERVER_PLATFORM_WINDOWS
|
|
247
311
|
if (stat(full_path, &st) != 0) return -1;
|
|
248
312
|
}
|
|
249
313
|
|
package/runtime/str_ops.c
CHANGED
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
* Return a malloc'd copy of `s` with all ASCII characters lowercased.
|
|
24
24
|
* Returns NULL on allocation failure.
|
|
25
25
|
*/
|
|
26
|
-
char
|
|
26
|
+
char* cerver_str_tolower(const char* s) {
|
|
27
27
|
if (!s) return NULL;
|
|
28
28
|
size_t len = strlen(s);
|
|
29
|
-
char
|
|
29
|
+
char* out = (char*)malloc(len + 1);
|
|
30
30
|
if (!out) return NULL;
|
|
31
31
|
for (size_t i = 0; i <= len; i++) {
|
|
32
32
|
out[i] = (char)tolower((unsigned char)s[i]);
|
|
@@ -38,10 +38,10 @@ char *cerver_str_tolower(const char *s) {
|
|
|
38
38
|
* Return a malloc'd copy of `s` with all ASCII characters uppercased.
|
|
39
39
|
* Returns NULL on allocation failure.
|
|
40
40
|
*/
|
|
41
|
-
char
|
|
41
|
+
char* cerver_str_toupper(const char* s) {
|
|
42
42
|
if (!s) return NULL;
|
|
43
43
|
size_t len = strlen(s);
|
|
44
|
-
char
|
|
44
|
+
char* out = (char*)malloc(len + 1);
|
|
45
45
|
if (!out) return NULL;
|
|
46
46
|
for (size_t i = 0; i <= len; i++) {
|
|
47
47
|
out[i] = (char)toupper((unsigned char)s[i]);
|
|
@@ -57,18 +57,18 @@ char *cerver_str_toupper(const char *s) {
|
|
|
57
57
|
* Return a malloc'd copy of `s` with leading and trailing ASCII
|
|
58
58
|
* whitespace removed. Returns NULL on allocation failure.
|
|
59
59
|
*/
|
|
60
|
-
char
|
|
60
|
+
char* cerver_str_trim(const char* s) {
|
|
61
61
|
if (!s) return NULL;
|
|
62
62
|
|
|
63
63
|
/* Skip leading whitespace */
|
|
64
64
|
while (*s && isspace((unsigned char)*s)) s++;
|
|
65
65
|
|
|
66
|
-
const char
|
|
66
|
+
const char* end = s + strlen(s);
|
|
67
67
|
/* Skip trailing whitespace */
|
|
68
68
|
while (end > s && isspace((unsigned char)*(end - 1))) end--;
|
|
69
69
|
|
|
70
70
|
size_t len = (size_t)(end - s);
|
|
71
|
-
char
|
|
71
|
+
char* out = (char*)malloc(len + 1);
|
|
72
72
|
if (!out) return NULL;
|
|
73
73
|
memcpy(out, s, len);
|
|
74
74
|
out[len] = '\0';
|
|
@@ -88,23 +88,27 @@ char *cerver_str_trim(const char *s) {
|
|
|
88
88
|
*
|
|
89
89
|
* Returns NULL on allocation failure.
|
|
90
90
|
*/
|
|
91
|
-
char
|
|
91
|
+
char* cerver_str_slice(const char* s, int start, int end) {
|
|
92
92
|
if (!s) return NULL;
|
|
93
93
|
int len = (int)strlen(s);
|
|
94
94
|
|
|
95
95
|
/* Resolve negative indices */
|
|
96
96
|
if (start < 0) start = len + start;
|
|
97
|
-
if (end < 0)
|
|
97
|
+
if (end < 0) end = (end == -1) ? len : len + end;
|
|
98
98
|
|
|
99
99
|
/* Clamp */
|
|
100
|
-
if (start < 0)
|
|
100
|
+
if (start < 0) start = 0;
|
|
101
101
|
if (start > len) start = len;
|
|
102
|
-
if (end
|
|
103
|
-
if (end
|
|
104
|
-
if (start > end) {
|
|
102
|
+
if (end < 0) end = 0;
|
|
103
|
+
if (end > len) end = len;
|
|
104
|
+
if (start > end) {
|
|
105
|
+
int t = start;
|
|
106
|
+
start = end;
|
|
107
|
+
end = t;
|
|
108
|
+
}
|
|
105
109
|
|
|
106
|
-
int
|
|
107
|
-
char
|
|
110
|
+
int out_len = end - start;
|
|
111
|
+
char* out = (char*)malloc((size_t)out_len + 1);
|
|
108
112
|
if (!out) return NULL;
|
|
109
113
|
memcpy(out, s + start, (size_t)out_len);
|
|
110
114
|
out[out_len] = '\0';
|
|
@@ -120,23 +124,22 @@ char *cerver_str_slice(const char *s, int start, int end) {
|
|
|
120
124
|
* replaced by `replacement`. If `needle` is not found, returns a copy
|
|
121
125
|
* of `s`. Returns NULL on allocation failure.
|
|
122
126
|
*/
|
|
123
|
-
char
|
|
124
|
-
const char *replacement) {
|
|
127
|
+
char* cerver_str_replace(const char* s, const char* needle, const char* replacement) {
|
|
125
128
|
if (!s) return NULL;
|
|
126
129
|
if (!needle || *needle == '\0') {
|
|
127
130
|
/* Empty needle — return a copy */
|
|
128
131
|
size_t len = strlen(s);
|
|
129
|
-
char
|
|
132
|
+
char* out = (char*)malloc(len + 1);
|
|
130
133
|
if (!out) return NULL;
|
|
131
134
|
memcpy(out, s, len + 1);
|
|
132
135
|
return out;
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
const char
|
|
138
|
+
const char* pos = strstr(s, needle);
|
|
136
139
|
if (!pos) {
|
|
137
140
|
/* Needle not found — return a copy of s */
|
|
138
141
|
size_t len = strlen(s);
|
|
139
|
-
char
|
|
142
|
+
char* out = (char*)malloc(len + 1);
|
|
140
143
|
if (!out) return NULL;
|
|
141
144
|
memcpy(out, s, len + 1);
|
|
142
145
|
return out;
|
|
@@ -148,10 +151,10 @@ char *cerver_str_replace(const char *s, const char *needle,
|
|
|
148
151
|
size_t after_len = strlen(pos + needle_len);
|
|
149
152
|
size_t total = before_len + replacement_len + after_len + 1;
|
|
150
153
|
|
|
151
|
-
char
|
|
154
|
+
char* out = (char*)malloc(total);
|
|
152
155
|
if (!out) return NULL;
|
|
153
156
|
|
|
154
|
-
char
|
|
157
|
+
char* p = out;
|
|
155
158
|
memcpy(p, s, before_len);
|
|
156
159
|
p += before_len;
|
|
157
160
|
if (replacement_len) {
|
|
@@ -170,7 +173,7 @@ char *cerver_str_replace(const char *s, const char *needle,
|
|
|
170
173
|
* Return 1 if `s` ends with `suffix`, 0 otherwise.
|
|
171
174
|
* Mirrors JS String.prototype.endsWith().
|
|
172
175
|
*/
|
|
173
|
-
int cerver_str_endswith(const char
|
|
176
|
+
int cerver_str_endswith(const char* s, const char* suffix) {
|
|
174
177
|
if (!s || !suffix) return 0;
|
|
175
178
|
size_t slen = strlen(s);
|
|
176
179
|
size_t suffixlen = strlen(suffix);
|
|
@@ -182,9 +185,9 @@ int cerver_str_endswith(const char *s, const char *suffix) {
|
|
|
182
185
|
* Return the byte index of the first occurrence of `needle` in `s`,
|
|
183
186
|
* or -1 if not found. Mirrors JS String.prototype.indexOf().
|
|
184
187
|
*/
|
|
185
|
-
int cerver_str_indexof(const char
|
|
188
|
+
int cerver_str_indexof(const char* s, const char* needle) {
|
|
186
189
|
if (!s || !needle) return -1;
|
|
187
|
-
const char
|
|
190
|
+
const char* pos = strstr(s, needle);
|
|
188
191
|
if (!pos) return -1;
|
|
189
192
|
return (int)(pos - s);
|
|
190
193
|
}
|
|
@@ -60,7 +60,7 @@ static void cerver_test_tmpdir_template(char* buf, size_t len) {
|
|
|
60
60
|
snprintf(buf, len, "%s\\cerver-test-XXXXXX", tmp);
|
|
61
61
|
#else
|
|
62
62
|
snprintf(buf, len, "/tmp/cerver-test-XXXXXX");
|
|
63
|
-
#endif
|
|
63
|
+
#endif // CERVER_PLATFORM_WINDOWS
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
static const char* res_header(const cerver_response_t* res, const char* key) {
|
package/runtime/win_compat.h
CHANGED
|
@@ -97,6 +97,9 @@ static inline void* cerver_memmem_win(const void* hay, size_t hlen, const void*
|
|
|
97
97
|
#ifndef strcasecmp
|
|
98
98
|
#define strcasecmp _stricmp
|
|
99
99
|
#endif // strcasecmp
|
|
100
|
+
#ifndef strncasecmp
|
|
101
|
+
#define strncasecmp _strnicmp
|
|
102
|
+
#endif // strncasecmp
|
|
100
103
|
|
|
101
104
|
/* clock_gettime / CLOCK_REALTIME are missing from some Windows CRTs. */
|
|
102
105
|
#ifndef CLOCK_REALTIME
|
|
@@ -236,12 +239,12 @@ static inline int cerver_fetch_global_init_guard_run(cerver_fetch_global_init_gu
|
|
|
236
239
|
typedef CRITICAL_SECTION cerver_mutex_t;
|
|
237
240
|
|
|
238
241
|
typedef struct {
|
|
239
|
-
HANDLE event;
|
|
240
|
-
HANDLE bcast;
|
|
242
|
+
HANDLE event; /* auto-reset event for signal */
|
|
243
|
+
HANDLE bcast; /* manual-reset event for broadcast */
|
|
241
244
|
} cerver_cond_t;
|
|
242
245
|
|
|
243
246
|
typedef struct {
|
|
244
|
-
volatile LONG
|
|
247
|
+
volatile LONG done;
|
|
245
248
|
CRITICAL_SECTION lock;
|
|
246
249
|
} cerver_fetch_global_init_guard_t;
|
|
247
250
|
|
|
@@ -249,8 +252,8 @@ typedef struct {
|
|
|
249
252
|
we zero-init and rely on cerver_mutex_init() being called. For the
|
|
250
253
|
two static structs in server.c that use CERVER_MUTEX_INITIALIZER we
|
|
251
254
|
call cerver_mutex_init() at startup via cerver_init(). */
|
|
252
|
-
#define CERVER_MUTEX_INITIALIZER
|
|
253
|
-
#define CERVER_COND_INITIALIZER
|
|
255
|
+
#define CERVER_MUTEX_INITIALIZER {0}
|
|
256
|
+
#define CERVER_COND_INITIALIZER {0}
|
|
254
257
|
#define CERVER_FETCH_GLOBAL_INIT_GUARD_INITIALIZER {0, {0}}
|
|
255
258
|
|
|
256
259
|
static inline int cerver_mutex_init(cerver_mutex_t* m, void* attr) {
|
|
@@ -274,7 +277,7 @@ static inline int cerver_mutex_unlock(cerver_mutex_t* m) {
|
|
|
274
277
|
static inline int cerver_cond_init(cerver_cond_t* c, void* attr) {
|
|
275
278
|
(void)attr;
|
|
276
279
|
c->event = CreateEvent(NULL, FALSE, FALSE, NULL); /* auto-reset */
|
|
277
|
-
c->bcast = CreateEvent(NULL, TRUE,
|
|
280
|
+
c->bcast = CreateEvent(NULL, TRUE, FALSE, NULL); /* manual-reset */
|
|
278
281
|
return (c->event && c->bcast) ? 0 : -1;
|
|
279
282
|
}
|
|
280
283
|
static inline int cerver_cond_destroy(cerver_cond_t* c) {
|
|
@@ -302,7 +305,7 @@ static inline int cerver_cond_timedwait(cerver_cond_t* c, cerver_mutex_t* m,
|
|
|
302
305
|
if (ms < 0) ms = 0;
|
|
303
306
|
LeaveCriticalSection(m);
|
|
304
307
|
HANDLE h[2] = {c->event, c->bcast};
|
|
305
|
-
DWORD
|
|
308
|
+
DWORD r = WaitForMultipleObjects(2, h, FALSE, (DWORD)ms);
|
|
306
309
|
EnterCriticalSection(m);
|
|
307
310
|
return (r == WAIT_TIMEOUT) ? 110 : 0; /* 110 = ETIMEDOUT */
|
|
308
311
|
}
|
|
@@ -323,13 +326,13 @@ static inline int cerver_fetch_global_init_guard_run(cerver_fetch_global_init_gu
|
|
|
323
326
|
return 0;
|
|
324
327
|
}
|
|
325
328
|
|
|
326
|
-
#endif
|
|
329
|
+
#endif // __MINGW64_VERSION_MAJOR || _MSC_VER
|
|
327
330
|
|
|
328
331
|
/* ---- thread helpers (shared across all Windows toolchains) -------- */
|
|
329
|
-
typedef HANDLE
|
|
330
|
-
typedef HANDLE
|
|
331
|
-
typedef size_t
|
|
332
|
-
typedef size_t
|
|
332
|
+
typedef HANDLE cerver_connection_worker_thread_t;
|
|
333
|
+
typedef HANDLE cerver_acceptor_thread_t;
|
|
334
|
+
typedef size_t cerver_connection_worker_thread_attr_t;
|
|
335
|
+
typedef size_t cerver_acceptor_thread_attr_t;
|
|
333
336
|
|
|
334
337
|
static inline int cerver_connection_worker_thread_attr_init(
|
|
335
338
|
cerver_connection_worker_thread_attr_t* attr) {
|