cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
app.c
Go to the documentation of this file.
1#ifndef _GNU_SOURCE
2#define _GNU_SOURCE
3#endif
4#define _POSIX_C_SOURCE 200809L
5#ifndef _DEFAULT_SOURCE
6#define _DEFAULT_SOURCE
7#endif
8#include <sched.h>
9#if defined(__linux__) && defined(_GNU_SOURCE)
10#include "worker_affinity.h"
11#endif
12#include "assets_internal.h"
13#include "routes_internal.h"
14/* Application headers (external) */
15#include <cwist/sys/app/app.h>
20/* WASM/WASI header (external) */
21#include <cwist/sys/wasi.h>
22/* Healthz header (external) */
24/* Network headers (external) */
25#include <cwist/net/http/http.h>
29#include <cwist/net/grpc/grpc.h>
32
33/* Network headers (internal) */
34#include "../../net/http/async_internal.h"
35#include "../../net/http/http_live.h"
36#include "../../net/http/sockopts.h"
37#include "../../net/http/simd_parser.h"
38#include "../../net/websocket/ws_async_internal.h"
39/* I/O headers (internal) */
40#include "../io/async_debug.h"
41/* Core headers */
46/* Libttak headers */
47#include <ttak/mem/mem.h>
48#include <ttak/timing/timing.h>
49#include <ttak/net/lattice.h>
50#include <ttak/mols_control.h>
51#include <ttak/priority/scheduler.h>
52#include <ttak/async/sched.h>
53
54/* Standard C libraries & POSIX & etc */
55#include <stdlib.h>
56#include <stdio.h>
57#include <string.h>
58#include <ctype.h>
59#include <strings.h>
60#include <limits.h>
61#ifndef PATH_MAX
62#define PATH_MAX 4096
63#endif
64#include <unistd.h>
65#include <cwist/sys/platform.h>
66#if defined(CWIST_PLATFORM_HOSTED)
67#include <sys/wait.h>
68#include <signal.h>
69#endif
70#include <errno.h>
71#include <fcntl.h>
72#ifndef CWIST_BAREMETAL /* newlib ships no poll/inet/dirent headers */
73#include <poll.h>
74#endif
75#if defined(__GLIBC__)
76#include <malloc.h>
77#endif
78#ifndef CWIST_BAREMETAL
79#include <arpa/inet.h>
80#include <netinet/tcp.h>
81#include <dirent.h>
82#endif
83#include <sys/stat.h>
84#ifndef CWIST_PLATFORM_NO_PROC
85#include <sys/resource.h>
86#endif
87#include <time.h>
88#include <pthread.h>
89
90#define CWIST_ROUTE_BUCKETS 127
91#define CWIST_STATIC_RETIRE_NS TT_SECOND(5)
92
93#ifndef __EMSCRIPTEN__
94/* Default open-file soft-limit target: covers C1M's one-fd-per-connection
95 * budget with headroom. Overridable via CWIST_FD_LIMIT_TARGET for
96 * deployments that need a different budget (a smaller container quota, or a
97 * larger one for a workload with more fds-per-connection than plain HTTP -
98 * e.g. proxying, or per-connection log/temp files). */
99#define CWIST_DEFAULT_FD_LIMIT_TARGET ((rlim_t)1050000)
100
104static void cwist_app_tune_system(void) {
105#ifdef CWIST_PLATFORM_NO_PROC
106 /* WASI preview1 has no rlimit; fd budgeting is the host's concern. */
107#else
108 struct rlimit rl;
109 if (getrlimit(RLIMIT_NOFILE, &rl) != 0) {
110 fprintf(stderr, "[CWIST] Cannot read file limits: %s\n", strerror(errno));
111 return;
112 }
113 /* Keep the hard limit. Increase the soft limit only, up to a tunable
114 * target - CWIST_FD_LIMIT_TARGET overrides the default when set to a
115 * valid positive integer; any other value (unset, empty, non-numeric,
116 * trailing garbage, zero or negative) falls back to the default rather
117 * than silently using 0 or a partially-parsed number. */
119 const char *fd_limit_env = getenv("CWIST_FD_LIMIT_TARGET");
120 if (fd_limit_env && fd_limit_env[0]) {
121 char *end = NULL;
122 errno = 0;
123 long parsed = strtol(fd_limit_env, &end, 10);
124 if (errno == 0 && end && *end == '\0' && parsed > 0) {
126 } else {
128 "[CWIST] Ignoring invalid CWIST_FD_LIMIT_TARGET=\"%s\" (using default %llu)\n",
129 fd_limit_env, (unsigned long long)CWIST_DEFAULT_FD_LIMIT_TARGET);
130 }
131 }
132 if (rl.rlim_max != RLIM_INFINITY && target > rl.rlim_max) {
133 target = rl.rlim_max;
134 }
135 if (rl.rlim_cur < target) {
136 rl.rlim_cur = target;
137 if (setrlimit(RLIMIT_NOFILE, &rl) != 0) {
138 fprintf(stderr, "[CWIST] Cannot increase file limit: %s\n", strerror(errno));
139 return;
140 }
141 }
142 printf("[CWIST] Open file soft limit: %llu\n", (unsigned long long)rl.rlim_cur);
143#endif
144}
145#endif
146
147#if defined(CWIST_PLATFORM_HOSTED)
152static inline uint64_t cwist_app_mem_now(void) {
153 return ttak_get_tick_count();
154}
155#endif
156
157#if defined(CWIST_PLATFORM_HOSTED)
158/* The static-file memory cache rides on libttak's mem tree and a watcher
159 * thread; WASM hosts get neither (cwist_prepare_static() always declines). */
167static bool cwist_mem_has_capacity(cwist_fix_server_mem *mem, size_t incoming, size_t reclaimable) {
168 if (!mem || mem->total_capacity == 0) {
169 return true;
170 }
171 if (incoming > mem->total_capacity) {
172 return false;
173 }
174 size_t used = mem->current_used;
175 if (reclaimable > used) {
176 reclaimable = used;
177 }
178 size_t projected = used - reclaimable + incoming;
179 return projected <= mem->total_capacity;
180}
181
188 if (!mem) return NULL;
189 if (mem->file_count >= mem->files_capacity) {
190 size_t new_cap = mem->files_capacity == 0 ? 16 : mem->files_capacity * 2;
192 if (!new_files) {
193 return NULL;
194 }
195 mem->files = new_files;
196 mem->files_capacity = new_cap;
197 }
198 cwist_file_t *entry = &mem->files[mem->file_count];
199 memset(entry, 0, sizeof(*entry));
200 mem->file_count++;
201 return entry;
202}
203
213static bool cwist_mem_create_payload(cwist_fix_server_mem *mem, const char *fs_path, size_t size,
215 if (!mem || !fs_path || !data_out || !node_out) return false;
216
217 void *buffer = ttak_mem_alloc_safe(size ? size : 1, __TTAK_UNSAFE_MEM_FOREVER__,
219 if (!buffer) {
220 fprintf(stderr, "[StaticMem] Failed to allocate %zu bytes via libttak for %s\n", size,
221 fs_path);
222 return false;
223 }
224
225 FILE *f = fopen(fs_path, "rb");
226 if (!f) {
227 fprintf(stderr, "[StaticMem] Failed to open %s\n", fs_path);
228 ttak_mem_free(buffer);
229 return false;
230 }
231
232 size_t to_read = size;
233 if (to_read > 0) {
234 size_t read = fread(buffer, 1, to_read, f);
235 if (read != to_read) {
236 fprintf(stderr, "[StaticMem] Short read for %s (expected %zu, got %zu)\n", fs_path,
237 to_read, read);
238 fclose(f);
239 ttak_mem_free(buffer);
240 return false;
241 }
242 }
243 fclose(f);
244
245 ttak_mem_node_t *node = ttak_mem_tree_add(&mem->file_tree, buffer, size ? size : 1,
247 if (!node) {
248 ttak_mem_free(buffer);
249 return false;
250 }
251
252 *data_out = buffer;
253 *node_out = node;
254 return true;
255}
256
263 if (!mem || !node) return;
265 pthread_mutex_lock(&node->lock);
266 node->expires_tick = now + mem->retire_grace_ns;
267 pthread_mutex_unlock(&node->lock);
269}
270
282 const char *fs_path, const struct stat *st, void *data,
283 ttak_mem_node_t *node) {
284 if (!mem || !entry || !fs_path || !st) return false;
285 char *path_copy = cwist_strdup(fs_path);
286 if (!path_copy) {
287 ttak_mem_tree_remove(&mem->file_tree, node);
288 return false;
289 }
290
291 entry->path = NULL;
292 entry->fs_path = path_copy;
293 entry->data = data;
294 entry->size = st->st_size;
295 entry->last_mod = st->st_mtime;
296 snprintf(entry->etag, sizeof(entry->etag), "\"%lx-%lx\"", (unsigned long)st->st_mtime,
297 (unsigned long)st->st_size);
298 cwist_http_format_date(st->st_mtime, entry->last_mod_hdr, sizeof(entry->last_mod_hdr));
299 entry->node = node;
300
301 mem->current_used += st->st_size;
302 return true;
303}
304
312static bool cwist_mem_register_file(cwist_fix_server_mem *mem, const char *fs_path,
313 const struct stat *st) {
314 if (!mem || !fs_path || !st) return false;
315 if (!cwist_mem_has_capacity(mem, st->st_size, 0)) {
316 fprintf(stderr, "[StaticMem] Skipping %s (size %zu exceeds capacity)\n", fs_path,
317 st->st_size);
318 return false;
319 }
321 if (!entry) {
322 fprintf(stderr, "[StaticMem] Failed to allocate metadata entry for %s\n", fs_path);
323 return false;
324 }
325 void *data = NULL;
326 ttak_mem_node_t *node = NULL;
327 if (!cwist_mem_create_payload(mem, fs_path, st->st_size, &data, &node)) {
328 mem->file_count--;
329 return false;
330 }
331 if (!cwist_mem_attach_entry(mem, entry, fs_path, st, data, node)) {
332 mem->file_count--;
333 return false;
334 }
335 return true;
336}
337
346 const struct stat *st) {
347 if (!mem || !entry || !st) return false;
348 size_t reclaimable = entry->size;
349 if (!cwist_mem_has_capacity(mem, st->st_size, reclaimable)) {
350 fprintf(stderr, "[StaticMem] OOM reloading %s (%zu bytes)\n", entry->fs_path,
351 (size_t)st->st_size);
352 return false;
353 }
354
355 void *data = NULL;
356 ttak_mem_node_t *node = NULL;
357 if (!cwist_mem_create_payload(mem, entry->fs_path, st->st_size, &data, &node)) {
358 return false;
359 }
360
361 ttak_mem_node_t *old_node = entry->node;
362 size_t old_size = entry->size;
363
364 entry->data = data;
365 entry->node = node;
366 entry->size = st->st_size;
367 entry->last_mod = st->st_mtime;
368 snprintf(entry->etag, sizeof(entry->etag), "\"%lx-%lx\"", (unsigned long)st->st_mtime,
369 (unsigned long)st->st_size);
370 cwist_http_format_date(st->st_mtime, entry->last_mod_hdr, sizeof(entry->last_mod_hdr));
371
372 if (mem->current_used >= old_size) {
373 mem->current_used -= old_size;
374 } else {
375 mem->current_used = 0;
376 }
377 mem->current_used += st->st_size;
378
379 if (old_node) {
381 }
382 return true;
383}
384#endif /* __EMSCRIPTEN__ */
385
386typedef struct cwist_route_entry {
387 char *path;
388 size_t path_len;
389 char *name;
390 bool has_params;
391 cwist_http_method_t method;
392 cwist_handler_func handler;
393 cwist_handler_ex_func handler_ex;
394 void *user_ctx;
398 cwist_ws_handler_func ws_handler;
399 cwist_ws_on_message_t ws_async_on_message;
400 void *ws_async_user_data;
402 struct cwist_route_entry *next;
404
407typedef struct {
408 cwist_handler_func handler;
409 cwist_handler_ex_func handler_ex;
410 void *user_ctx;
412 cwist_ws_handler_func ws_handler;
413} cwist_route_target;
414
415struct cwist_route_table {
416 size_t bucket_count;
417 cwist_route_entry **buckets;
418 cwist_route_entry *param_routes;
419};
420
421struct cwist_static_dir {
422 char *url_prefix;
423 char *fs_root;
424 char *cache_control;
425 struct cwist_static_dir *next;
426};
427
428typedef struct {
429 cwist_middleware_node *current_mw_node;
430 cwist_handler_func final_handler;
431 void *handler_data;
432} mw_executor_ctx;
433
434typedef struct {
435 cwist_static_dir *mapping;
436 const char *relative_ptr;
437 bool use_index;
438} cwist_static_request_info;
439
440static cwist_route_table *cwist_route_table_create(void);
441static void cwist_route_table_destroy(cwist_route_table *table);
442static bool cwist_route_table_insert(cwist_route_table *table, const char *path, const char *name,
443 cwist_http_method_t method, const cwist_route_target *target,
445static cwist_route_entry *cwist_route_table_lookup(cwist_route_table *table,
446 cwist_http_method_t method, const char *path);
447static cwist_route_entry *cwist_route_table_match_params(cwist_route_table *table,
448 cwist_http_request *req);
449static bool match_path(const char *pattern, const char *actual, cwist_query_map *params);
450static void execute_chain(cwist_app *app, cwist_http_request *req, cwist_http_response *res,
451 cwist_handler_func final_handler, void *handler_data);
452static bool cwist_prepare_static(cwist_app *app, cwist_http_request *req,
453 cwist_static_request_info *info);
454static void cwist_static_handler(cwist_http_request *req, cwist_http_response *res);
455static void cwist_multiport_destroy_owned_subapps(cwist_app *root);
456static void cwist_multiport_unlink_app(cwist_app *app);
457static void cwist_multiport_h3_copy_tunables(cwist_http3_context *dst,
458 const cwist_http3_context *src);
459
465static bool route_has_params(const char *path) {
466 if (!path) return false;
467 return strchr(path, ':') != NULL;
468}
469
477static size_t cwist_route_hash(cwist_http_method_t method, const char *path, size_t path_len,
478 size_t bucket_count) {
479 const unsigned long long FNV_OFFSET = 1469598103934665603ULL;
480 const unsigned long long FNV_PRIME = 1099511628211ULL;
481 unsigned long long hash = FNV_OFFSET ^ (unsigned long long)method;
482 const unsigned char *ptr = (const unsigned char *)path;
483 for (size_t i = 0; i < path_len; i++) {
484 hash ^= (unsigned long long)ptr[i];
485 hash *= FNV_PRIME;
486 }
487 return (size_t)(hash % bucket_count);
488}
489
490/* Run and forget an entry's context destructor, if it owns one. */
491static void cwist_route_entry_release_ctx(cwist_route_entry *entry) {
492 if (entry->ctx_destroy) entry->ctx_destroy(entry->user_ctx);
493 entry->user_ctx = NULL;
494 entry->ctx_destroy = NULL;
495}
496
497static void cwist_route_entry_set_target(cwist_route_entry *entry,
498 const cwist_route_target *target) {
499 entry->handler = target->handler;
500 entry->handler_ex = target->handler_ex;
501 entry->user_ctx = target->user_ctx;
502 entry->ctx_destroy = target->ctx_destroy;
503 entry->ws_handler = target->ws_handler;
504 entry->ws_async_on_message = NULL;
505 entry->ws_async_user_data = NULL;
506}
507
508static cwist_route_entry *cwist_route_entry_create(const char *path, const char *name,
509 cwist_http_method_t method,
510 const cwist_route_target *target,
512 cwist_route_entry *entry = (cwist_route_entry *)cwist_alloc(sizeof(cwist_route_entry));
513 if (!entry) return NULL;
514 entry->path = cwist_strdup(path ? path : "/");
515 entry->name = name ? cwist_strdup(name) : NULL;
516 if (!entry->path || (name && !entry->name)) {
517 cwist_free(entry->path);
518 cwist_free(entry->name);
519 cwist_free(entry);
520 return NULL;
521 }
522 entry->path_len = strlen(entry->path);
523 entry->method = method;
524 cwist_route_entry_set_target(entry, target);
525 entry->opts = opts;
526 entry->has_params = route_has_params(entry->path);
527 entry->next = NULL;
528 return entry;
529}
530
535static void cwist_route_entry_free(cwist_route_entry *entry) {
536 if (!entry) return;
537 cwist_route_entry_release_ctx(entry);
538 cwist_free(entry->path);
539 cwist_free(entry->name);
540 cwist_free(entry);
541}
542
543static cwist_route_table *cwist_route_table_create(void) {
544 cwist_route_table *table = (cwist_route_table *)cwist_alloc(sizeof(cwist_route_table));
545 if (!table) return NULL;
546 table->bucket_count = CWIST_ROUTE_BUCKETS;
547 table->buckets =
548 (cwist_route_entry **)cwist_alloc_array(table->bucket_count, sizeof(cwist_route_entry *));
549 if (!table->buckets) {
551 return NULL;
552 }
553 table->param_routes = NULL;
554 return table;
555}
556
561static void cwist_route_table_destroy(cwist_route_table *table) {
562 if (!table) return;
563 for (size_t i = 0; i < table->bucket_count; i++) {
564 cwist_route_entry *curr = table->buckets[i];
565 while (curr) {
566 cwist_route_entry *next = curr->next;
567 cwist_route_entry_free(curr);
568 curr = next;
569 }
570 }
571 cwist_free(table->buckets);
572
573 cwist_route_entry *param = table->param_routes;
574 while (param) {
575 cwist_route_entry *next = param->next;
576 cwist_route_entry_free(param);
577 param = next;
578 }
580}
581
590static bool cwist_route_table_insert(cwist_route_table *table, const char *path, const char *name,
591 cwist_http_method_t method, const cwist_route_target *target,
593 cwist_route_entry *entry =
594 (table && path) ? cwist_route_entry_create(path, name, method, target, opts) : NULL;
595 if (!entry) {
596 if (target->ctx_destroy) target->ctx_destroy(target->user_ctx);
597 return false;
598 }
599
600 if (entry->has_params) {
601 entry->next = table->param_routes;
602 table->param_routes = entry;
603 return true;
604 }
605
606 size_t idx = cwist_route_hash(method, entry->path, entry->path_len, table->bucket_count);
607 cwist_route_entry **bucket = &table->buckets[idx];
608 cwist_route_entry *curr = *bucket;
609 while (curr) {
610 if (!curr->has_params && curr->method == method && strcmp(curr->path, entry->path) == 0) {
611 /* Re-registering the same non-NULL context keeps it alive; anything else
612 * releases the old one before the new target takes over. */
613 if (!target->user_ctx || curr->user_ctx != target->user_ctx) {
614 cwist_route_entry_release_ctx(curr);
615 }
616 cwist_route_entry_set_target(curr, target);
617 curr->opts = opts;
618 /* The replacement entry never owned the context now in curr. */
619 entry->ctx_destroy = NULL;
620 cwist_route_entry_free(entry);
621 return true;
622 }
623 curr = curr->next;
624 }
625
626 entry->next = *bucket;
627 *bucket = entry;
628 return true;
629}
630
631static cwist_route_entry *cwist_route_table_lookup(cwist_route_table *table,
632 cwist_http_method_t method, const char *path) {
633 if (!table || !path) return NULL;
634 /* One scan of the path: length feeds both the hash and the compare. */
635 size_t path_len = strlen(path);
636 size_t idx = cwist_route_hash(method, path, path_len, table->bucket_count);
637 cwist_route_entry *curr = table->buckets[idx];
638
639 uint64_t path_u64 = 0;
640 const bool use_fast_path = (path_len <= 8);
641 if (use_fast_path) {
642 memcpy(&path_u64, path, path_len); // Safe copy
643 }
644
645 while (curr) {
646 if (curr->method == method) {
647 if (use_fast_path) {
648 /* Length is cached on the entry; immutable after insert. */
649 if (curr->path_len == path_len) {
650 uint64_t curr_u64 = 0;
651 memcpy(&curr_u64, curr->path, curr->path_len);
652 if (path_u64 == curr_u64) return curr;
653 }
654 } else {
655 if (curr->path_len == path_len && memcmp(curr->path, path, path_len) == 0) {
656 return curr;
657 }
658 }
659 }
660 curr = curr->next;
661 }
662 return NULL;
663}
664
665static cwist_route_entry *cwist_route_table_match_params(cwist_route_table *table,
666 cwist_http_request *req) {
667 if (!table || !req || !req->path || !req->path->data) return NULL;
668 cwist_route_entry *curr = table->param_routes;
669 while (curr) {
670 if (curr->method == req->method) {
671 if (!req->path_params) {
672 /* Probe without capturing: no heap alloc, no map clearing per
673 * candidate. Only a confirmed match pays for the params map. */
674 if (match_path(curr->path, req->path->data, NULL)) {
676 if (!req->path_params) return NULL;
677 match_path(curr->path, req->path->data, req->path_params);
678 return curr;
679 }
680 } else if (match_path(curr->path, req->path->data, req->path_params)) {
681 return curr;
682 }
683 }
684 curr = curr->next;
685 }
686 return NULL;
687}
688
693static size_t cwist_url_decode(const char *src, char *dst, size_t dst_size) {
694 size_t i = 0, j = 0;
695 while (src[i] && j + 1 < dst_size) {
696 if (src[i] == '%' && isxdigit((unsigned char)src[i + 1]) &&
697 isxdigit((unsigned char)src[i + 2])) {
698 char hex[3] = {src[i + 1], src[i + 2], '\0'};
699 dst[j++] = (char)strtol(hex, NULL, 16);
700 i += 3;
701 } else if (src[i] == '+') {
702 dst[j++] = ' ';
703 i++;
704 } else {
705 dst[j++] = src[i++];
706 }
707 }
708 dst[j] = '\0';
709 return j;
710}
711
717static bool cwist_path_has_parent_ref(const char *path) {
718 if (!path) return false;
719 const char *cursor = path;
720 while (*cursor) {
721 if (*cursor == '.') {
722 char prev = (cursor == path) ? '/' : *(cursor - 1);
723 char next = *(cursor + 1);
724 char next_next = *(cursor + 2);
725 if (prev == '/' && next == '.' && (next_next == '/' || next_next == '\0')) {
726 return true;
727 }
728 }
729 cursor++;
730 }
731 return false;
732}
733
742static bool cwist_static_match_entry(const cwist_static_dir *entry, const char *req_path,
743 const char **relative_ptr, bool *use_index) {
744 if (!entry || !req_path || req_path[0] == '\0') return false;
745 size_t prefix_len = strlen(entry->url_prefix);
746 if (prefix_len == 0) return false;
747 if (prefix_len == 1 && entry->url_prefix[0] == '/') {
748 if (req_path[0] != '/') return false;
749 if (req_path[1] == '\0') {
750 if (use_index) *use_index = true;
751 if (relative_ptr) *relative_ptr = NULL;
752 } else {
753 if (use_index) *use_index = false;
754 if (relative_ptr) *relative_ptr = req_path + 1;
755 }
756 return true;
757 }
758
759 if (strncmp(req_path, entry->url_prefix, prefix_len) != 0) {
760 return false;
761 }
762
763 char separator = req_path[prefix_len];
764 if (separator == '\0') {
765 if (use_index) *use_index = true;
766 if (relative_ptr) *relative_ptr = NULL;
767 return true;
768 }
769 if (separator != '/') {
770 return false;
771 }
772 if (use_index) *use_index = false;
773 if (relative_ptr) *relative_ptr = req_path + prefix_len + 1;
774 return true;
775}
776
784static bool cwist_prepare_static(cwist_app *app, cwist_http_request *req,
785 cwist_static_request_info *info) {
786#if !defined(CWIST_PLATFORM_HOSTED)
787 /* No filesystem-backed static cache in WASM hosts. */
788 (void)app;
789 (void)req;
790 (void)info;
791 return false;
792#else
793 if (!app || !req || !req->path || !req->path->data) return false;
794 if (!app->static_dirs) return false;
795 if (req->method != CWIST_HTTP_GET && req->method != CWIST_HTTP_HEAD) return false;
796
797 cwist_static_dir *entry = app->static_dirs;
798 const char *path = req->path->data;
799 while (entry) {
800 bool use_index = false;
801 const char *relative = NULL;
802 if (cwist_static_match_entry(entry, path, &relative, &use_index)) {
803 if (info) {
804 info->mapping = entry;
805 info->relative_ptr = relative;
806 info->use_index = use_index;
807 }
808 return true;
809 }
810 entry = entry->next;
811 }
812 return false;
813#endif
814}
815
816#if defined(CWIST_PLATFORM_HOSTED)
824static void cwist_scan_recursive(const char *fs_root, size_t *total_size, cwist_fix_server_mem *mem,
825 bool dry_run) {
826 DIR *d = opendir(fs_root);
827 if (!d) return;
828
829 struct dirent *dir;
830 char full_path[PATH_MAX];
831 struct stat st;
832
833 while ((dir = readdir(d)) != NULL) {
834 if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
835
836 snprintf(full_path, sizeof(full_path), "%s/%s", fs_root, dir->d_name);
837 if (stat(full_path, &st) == -1) continue;
838
839 if (S_ISDIR(st.st_mode)) {
841 } else if (S_ISREG(st.st_mode)) {
842 if (dry_run) {
843 if (total_size) *total_size += st.st_size;
844 } else if (mem) {
845 if (!cwist_mem_register_file(mem, full_path, &st)) {
846 fprintf(stderr, "[StaticMem] Failed to load %s\n", full_path);
847 }
848 }
849 }
850 }
851 closedir(d);
852}
853
858static void cwist_mem_init(cwist_app *app) {
859 if (!app || !app->static_dirs) return;
860
862 if (!app->mem_manager) {
863 /* Serving still works: static files are then read from disk per
864 * request instead of from the in-memory cache (#341). */
865 fprintf(stderr, "[StaticMem] Out of memory; static cache disabled\n");
866 return;
867 }
868 app->mem_manager->check_interval_ms = 2000;
872
873 size_t total_size = 0;
875 while (curr) {
876 cwist_scan_recursive(curr->fs_root, &total_size, NULL, true);
877 curr = curr->next;
878 }
879
880 if (app->max_mem_space > 0) {
882 } else {
883 if (total_size == 0) total_size = CWIST_MIB(1);
885 }
886
887 app->mem_manager->current_used = 0;
888
889 // Load files
890 curr = app->static_dirs;
891 while (curr) {
892 cwist_scan_recursive(curr->fs_root, NULL, app->mem_manager, false);
893 curr = curr->next;
894 }
895
896 printf("Server Memory Initialized: %zu used / %zu total bytes (%zu files)\n",
898 app->mem_manager->file_count);
899}
900
901static void *cwist_mem_watcher(void *arg) {
902 cwist_app *app = (cwist_app *)arg;
904
905 /* watcher_running is a plain bool in the public layout; every access
906 * goes through __atomic builtins instead of changing its type (#351). */
908 usleep(mem->check_interval_ms * 1000);
909
911 for (size_t i = 0; i < mem->file_count; i++) {
912 cwist_file_t *f = &mem->files[i];
913 struct stat st;
914 if (stat(f->fs_path, &st) == 0) {
915 if (st.st_mtime > f->last_mod) {
916 if (cwist_mem_refresh_file(mem, f, &st)) {
917 printf("[Hot Reload] Updated: %s\n", f->fs_path);
918 }
919 }
920 }
921 }
923 }
924 return NULL;
925}
926
927static cwist_file_t *cwist_mem_get_file(cwist_fix_server_mem *mem, const char *fs_path) {
928 if (!mem || !fs_path) return NULL;
929 for (size_t i = 0; i < mem->file_count; i++) {
930 if (strcmp(mem->files[i].fs_path, fs_path) == 0) {
931 return &mem->files[i];
932 }
933 }
934 return NULL;
935}
936#endif /* __EMSCRIPTEN__ */
937
938static char *cwist_normalize_prefix(const char *prefix) {
939 if (!prefix || prefix[0] == '\0') {
940 return cwist_strdup("/");
941 }
942
943 size_t len = strlen(prefix);
944 bool needs_leading_slash = prefix[0] != '/';
945 char *buffer = (char *)cwist_alloc(len + needs_leading_slash + 1);
946 if (!buffer) {
947 return NULL;
948 }
949
951 buffer[0] = '/';
952 memcpy(buffer + 1, prefix, len + 1);
953 len += 1;
954 } else {
955 memcpy(buffer, prefix, len + 1);
956 }
957
958 while (len > 1 && buffer[len - 1] == '/') {
959 buffer[len - 1] = '\0';
960 len--;
961 }
962
963 return buffer;
964}
965
966static char *cwist_normalize_directory(const char *directory) {
967 if (!directory || directory[0] == '\0') {
968 directory = ".";
969 }
971 if (resolved) {
972 return resolved;
973 }
974 /* Fallback when the path does not yet exist: strip trailing slashes */
975 size_t len = strlen(directory);
976 while (len > 1 && directory[len - 1] == '/') {
977 len--;
978 }
979 char *copy = (char *)cwist_alloc(len + 1);
980 if (!copy) return NULL;
981 memcpy(copy, directory, len);
982 copy[len] = '\0';
983 return copy;
984}
985
986#if defined(CWIST_PLATFORM_HOSTED)
993static void cwist_static_release_body(const void *ptr, size_t len, void *ctx) {
994 (void)ptr;
995 (void)len;
996 ttak_mem_node_t *node = (ttak_mem_node_t *)ctx;
997 if (node) {
999 }
1000}
1001
1007static void cwist_static_handler(cwist_http_request *req, cwist_http_response *res) {
1008 mw_executor_ctx *ctx = (mw_executor_ctx *)req->private_data;
1009 cwist_static_request_info *info = ctx ? (cwist_static_request_info *)ctx->handler_data : NULL;
1010 if (!info || !info->mapping) {
1011 res->status_code = CWIST_HTTP_INTERNAL_ERROR;
1012 cwist_sstring_assign(res->body, "Static handler misconfigured");
1013 return;
1014 }
1015
1016 char relative_buf[PATH_MAX];
1017 if (info->use_index || !info->relative_ptr || info->relative_ptr[0] == '\0') {
1018 snprintf(relative_buf, sizeof(relative_buf), "index.html");
1019 } else {
1020 snprintf(relative_buf, sizeof(relative_buf), "%s", info->relative_ptr);
1021 }
1022 relative_buf[PATH_MAX - 1] = '\0';
1023
1025 cwist_url_decode(relative_buf, decoded_relative, sizeof(decoded_relative));
1026
1027 if (cwist_path_has_parent_ref(decoded_relative)) {
1028 res->status_code = CWIST_HTTP_FORBIDDEN;
1029 cwist_sstring_assign(res->body, "Directory traversal blocked");
1030 return;
1031 }
1032
1033 char fs_path[PATH_MAX];
1034 int written =
1035 snprintf(fs_path, sizeof(fs_path), "%s/%s", info->mapping->fs_root, decoded_relative);
1036 if (written < 0 || written >= (int)sizeof(fs_path)) {
1037 res->status_code = CWIST_HTTP_BAD_REQUEST;
1038 cwist_sstring_assign(res->body, "Static path too long");
1039 return;
1040 }
1041
1042 char canonical[PATH_MAX];
1043 if (!realpath(fs_path, canonical)) {
1044 res->status_code = CWIST_HTTP_NOT_FOUND;
1045 cwist_sstring_assign(res->body, "Not Found");
1046 return;
1047 }
1048
1049 /* Ensure the resolved path stays inside the configured static root */
1050 size_t root_len = strlen(info->mapping->fs_root);
1051 if (strncmp(canonical, info->mapping->fs_root, root_len) != 0 ||
1052 (canonical[root_len] != '/' && canonical[root_len] != '\0')) {
1053 res->status_code = CWIST_HTTP_FORBIDDEN;
1054 cwist_sstring_assign(res->body, "Directory traversal blocked");
1055 return;
1056 }
1057
1058 cwist_app *app = req->app;
1059 if (!app || !app->mem_manager) {
1060 res->status_code = CWIST_HTTP_INTERNAL_ERROR;
1061 cwist_sstring_assign(res->body, "Server memory not initialized");
1062 return;
1063 }
1064
1066 pthread_mutex_lock(&mem->lock);
1068
1069 if (file) {
1070 // Simple MIME guess
1071 const char *dot = strrchr(canonical, '.');
1072 const char *mime = "application/octet-stream";
1073 if (dot) {
1074 if (strcasecmp(dot, ".html") == 0)
1075 mime = "text/html; charset=utf-8";
1076 else if (strcasecmp(dot, ".css") == 0)
1077 mime = "text/css; charset=utf-8";
1078 else if (strcasecmp(dot, ".js") == 0)
1079 mime = "application/javascript";
1080 else if (strcasecmp(dot, ".json") == 0)
1081 mime = "application/json";
1082 else if (strcasecmp(dot, ".png") == 0)
1083 mime = "image/png";
1084 else if (strcasecmp(dot, ".jpg") == 0 || strcasecmp(dot, ".jpeg") == 0)
1085 mime = "image/jpeg";
1086 else if (strcasecmp(dot, ".gif") == 0)
1087 mime = "image/gif";
1088 else if (strcasecmp(dot, ".svg") == 0)
1089 mime = "image/svg+xml";
1090 else if (strcasecmp(dot, ".txt") == 0)
1091 mime = "text/plain; charset=utf-8";
1092 }
1093
1094 // Cache headers are formatted once at load/reload time.
1095 const char *etag = file->etag;
1096 const char *last_mod_buf = file->last_mod_hdr;
1097
1098 // Check conditional requests
1099 bool not_modified = false;
1100 const char *if_none_match = cwist_http_header_get(req->headers, "If-None-Match");
1101 if (if_none_match && strcmp(if_none_match, etag) == 0) {
1102 not_modified = true;
1103 } else {
1104 const char *if_modified_since =
1105 cwist_http_header_get(req->headers, "If-Modified-Since");
1106 if (if_modified_since) {
1108 if (ims != (time_t)-1 && file->last_mod <= ims) {
1109 not_modified = true;
1110 }
1111 }
1112 }
1113
1114 if (not_modified) {
1115 res->status_code = CWIST_HTTP_NOT_MODIFIED; // 304
1116 cwist_http_header_add(&res->headers, "ETag", etag);
1117 cwist_http_header_add(&res->headers, "Last-Modified", last_mod_buf);
1118 const char *cc = info->mapping->cache_control ? info->mapping->cache_control
1119 : "public, max-age=3600";
1120 cwist_http_header_add(&res->headers, "Cache-Control", cc);
1121 cwist_sstring_assign(res->body, "");
1122 } else {
1123 /* HEAD falls through here: app_serve_parsed_request suppresses the
1124 * body at send time for every route, so only the headers matter. */
1125 size_t send_offset = 0;
1126 size_t send_len = file->size;
1127
1128 /* --- Range Request Handling --- */
1129 const char *range_hdr = cwist_http_header_get(req->headers, "Range");
1130 if (range_hdr && strncmp(range_hdr, "bytes=", 6) == 0) {
1131 const char *p = range_hdr + 6;
1132 size_t range_start = 0, range_end = file->size - 1;
1133 bool range_valid = true;
1134
1135 if (p[0] == '-') {
1136 /* suffix-range: bytes=-N means the last N bytes */
1137 char *end = NULL;
1138 unsigned long long sv = strtoull(p + 1, &end, 10);
1139 if (end == p + 1 || *end != '\0') {
1140 range_valid = false;
1141 } else {
1142 range_start = (file->size > (size_t)sv) ? file->size - (size_t)sv : 0;
1143 range_end = file->size - 1;
1144 }
1145 } else {
1146 /* first-byte-pos [ "-" [ last-byte-pos ] ] */
1147 char *end = NULL;
1148 unsigned long long rs = strtoull(p, &end, 10);
1149 if (end == p) {
1150 range_valid = false;
1151 } else {
1153 char *dash = strchr(p, '-');
1154 if (dash && dash[1] != '\0') {
1155 unsigned long long re = strtoull(dash + 1, &end, 10);
1156 range_end = (end != dash + 1) ? (size_t)re : file->size - 1;
1157 } else {
1158 range_end = file->size - 1;
1159 }
1160 }
1161 }
1162
1163 if (!range_valid || range_start > range_end || range_start >= file->size) {
1164 res->status_code = CWIST_HTTP_RANGE_NOT_SATISFIABLE;
1165 char cr[128];
1166 snprintf(cr, sizeof(cr), "bytes */%zu", file->size);
1167 cwist_http_header_add(&res->headers, "Content-Range", cr);
1168 cwist_sstring_assign(res->body, "");
1169 } else {
1170 if (range_end >= file->size) range_end = file->size - 1;
1173 res->status_code = CWIST_HTTP_PARTIAL_CONTENT;
1174 char cr[128];
1175 snprintf(cr, sizeof(cr), "bytes %zu-%zu/%zu", range_start, range_end,
1176 file->size);
1177 cwist_http_header_add(&res->headers, "Content-Range", cr);
1178 }
1179 }
1180
1181 if (res->status_code != CWIST_HTTP_RANGE_NOT_SATISFIABLE) {
1182 if (file->data && file->node) {
1186 file->node);
1187 } else {
1188 res->status_code = CWIST_HTTP_INTERNAL_ERROR;
1189 cwist_sstring_assign(res->body, "Static buffer missing");
1190 }
1191 }
1192
1193 if (res->status_code != CWIST_HTTP_INTERNAL_ERROR &&
1194 res->status_code != CWIST_HTTP_RANGE_NOT_SATISFIABLE) {
1195 const char *cc = info->mapping->cache_control ? info->mapping->cache_control
1196 : "public, max-age=3600";
1197 char len_buf[32];
1198 snprintf(len_buf, sizeof(len_buf), "%zu", send_len);
1199 cwist_http_header_add(&res->headers, "Content-Length", len_buf);
1200 cwist_http_header_add(&res->headers, "Content-Type", mime);
1201 cwist_http_header_add(&res->headers, "ETag", etag);
1202 cwist_http_header_add(&res->headers, "Last-Modified", last_mod_buf);
1203 cwist_http_header_add(&res->headers, "Cache-Control", cc);
1204 cwist_http_header_add(&res->headers, "Accept-Ranges", "bytes");
1205 if (res->status_code != CWIST_HTTP_PARTIAL_CONTENT) {
1206 res->status_code = CWIST_HTTP_OK;
1207 }
1208 }
1209 }
1210 } else {
1211 res->status_code = CWIST_HTTP_NOT_FOUND;
1212 cwist_sstring_assign(res->body, "Not Found");
1213 }
1215}
1216#endif /* __EMSCRIPTEN__ */
1217#include <limits.h>
1218#include <errno.h>
1219
1220#if defined(CWIST_PLATFORM_HOSTED)
1227 if (!app || !app->cert_path || !app->key_path) {
1228 err.error.err_i16 = -1;
1229 return err;
1230 }
1231
1232 if (app->ssl_ctx) {
1234 app->ssl_ctx = NULL;
1235 }
1236
1237 cwist_https_options options = {.enable_http2 = app->use_https2,
1238 .enable_http3 = app->use_https3};
1240 &options, app);
1241}
1242
1243static void static_ssl_http1_handler(cwist_https_connection *conn, void *ctx);
1244static void static_ssl_http2_handler(cwist_https_connection *conn, void *ctx);
1245
1247 if (!app) return;
1249 app->use_https2 ? static_ssl_http2_handler : static_ssl_http1_handler;
1250}
1251
1257 if (!app) {
1258 err.error.err_i16 = -1;
1259 return err;
1260 }
1261
1262#ifdef CWIST_HTTP3
1263 if (app->h3_ctx) {
1265 app->h3_ctx = NULL;
1266 }
1267
1268 if (app->use_https3 || app->use_http3) {
1269 if (app->use_https3 && app->cert_path && app->key_path) {
1271 } else if (app->use_http3) {
1273 } else {
1274 err.error.err_i16 = -1; // Missing config for strict https3
1275 }
1276 if (app->h3_ctx && app->wt_handler) {
1278 }
1279 } else {
1280 err.error.err_i16 = 0;
1281 }
1282#else
1283 /* HTTP/3 compiled out (CWIST_HTTP3=0): the context stays NULL and an
1284 * explicit enable request reports unsupported, like the WASM arm. */
1285 app->h3_ctx = NULL;
1286 err.error.err_i16 = (app->use_https3 || app->use_http3) ? -1 : 0;
1287#endif
1288 return err;
1289}
1290#endif /* __EMSCRIPTEN__ */
1291
1293 cwist_app *app = (cwist_app *)cwist_alloc(sizeof(cwist_app));
1294 if (!app) return NULL;
1295
1296 app->port = 8080;
1297 app->use_ssl = false;
1298 app->use_http2 = false;
1299 app->use_http3 = false;
1300 app->use_https2 = false;
1301 app->use_https3 = false;
1302 app->cert_path = NULL;
1303 app->key_path = NULL;
1305 app->router = cwist_route_table_create();
1306 if (!app->router) {
1307 cwist_free(app);
1308 return NULL;
1309 }
1310 app->middlewares = NULL;
1311 app->ssl_ctx = NULL;
1312 app->h3_ctx = NULL;
1313 app->error_handler = NULL;
1314 app->error_handlers = NULL;
1315 app->static_dirs = NULL;
1316 app->config = cwist_config_create();
1317 app->logger = cwist_logger_create("cwist");
1318 app->db = NULL;
1319 app->db_path = NULL;
1320 app->nuke_enabled = false;
1321 app->max_mem_space = 0;
1322 app->mem_manager = NULL;
1323 app->bdr_ctx = cwist_bdr_create();
1324 app->pqc_layer_enabled = false;
1325 app->tls_groups = NULL;
1326 app->wt_handler = NULL;
1327
1328 app->session_secret = NULL;
1329 app->session_name = NULL;
1330 app->session_max_age = 0;
1331 app->db_pool = NULL;
1332 app->redis_pool = NULL;
1333 app->scheduler = NULL;
1334 app->grpc_routes = NULL;
1335
1336#if defined(CWIST_PLATFORM_HOSTED)
1338#endif
1339
1340 return app;
1341}
1342
1343/* Shared body of cwist_app_use() and cwist_app_use_ex(). The context is owned
1344 * by the chain from here on, including on every failure path, as with the
1345 * cwist_app_*_ex() routes. */
1346static bool add_middleware(cwist_app *app, cwist_middleware_func mw, cwist_middleware_func_ex mw_ex,
1347 void *user_ctx, cwist_middleware_ctx_destroy_func destroy) {
1348 bool valid = app && (mw != NULL) != (mw_ex != NULL);
1350 if (!node) {
1351 if (destroy) destroy(user_ctx);
1352 return false;
1353 }
1354 node->func = mw;
1355 node->func_ex = mw_ex;
1356 node->user_ctx = user_ctx;
1357 node->destroy = destroy;
1358 node->is_ex = mw_ex != NULL;
1359 node->next = NULL;
1360
1361 if (!app->middlewares) {
1362 app->middlewares = node;
1363 } else {
1365 while (curr->next) curr = curr->next;
1366 curr->next = node;
1367 }
1368 return true;
1369}
1370
1377 add_middleware(app, mw, NULL, NULL, NULL);
1378}
1379
1390 cwist_middleware_func_ex mw_ex, void *user_ctx,
1393 err.error.err_i16 = add_middleware(app, mw, mw_ex, user_ctx, destroy) ? 0 : -1;
1394 return err;
1395}
1396
1402void cwist_app_set_max_memspace(cwist_app *app, size_t size) {
1403 if (app) app->max_mem_space = size;
1404}
1405
1412 if (app) app->error_handler = handler;
1413}
1414
1416 cwist_error_handler_func handler) {
1417 if (!app || !handler) return;
1419 while (curr) {
1420 if (curr->status_code == status) {
1421 curr->handler = handler;
1422 return;
1423 }
1424 curr = curr->next;
1425 }
1428 entry->status_code = status;
1429 entry->handler = handler;
1430 entry->next = app->error_handlers;
1431 app->error_handlers = entry;
1432}
1433
1434static cwist_error_handler_func cwist_app_find_error_handler(cwist_app *app,
1435 cwist_http_status_t status) {
1436 if (!app) return NULL;
1438 while (curr) {
1439 if (curr->status_code == status) {
1440 return curr->handler;
1441 }
1442 curr = curr->next;
1443 }
1444 return app->error_handler;
1445}
1446
1454void cwist_app_configure_bdr(cwist_app *app, size_t max_bytes, time_t max_entry_age_sec,
1455 uint64_t revalidate_hits) {
1456 if (!app || !app->bdr_ctx) return;
1457 cwist_bdr_set_limits(app->bdr_ctx, max_bytes, max_entry_age_sec, revalidate_hits);
1458}
1459
1465 if (!app) return;
1466#if defined(CWIST_PLATFORM_HOSTED)
1467 cwist_multiport_destroy_owned_subapps(app);
1468 cwist_multiport_unlink_app(app);
1469#endif
1470 if (app->cert_path) cwist_free(app->cert_path);
1471 if (app->key_path) cwist_free(app->key_path);
1472 if (app->tls_groups) cwist_free(app->tls_groups);
1473#if defined(CWIST_PLATFORM_HOSTED)
1475#ifdef CWIST_HTTP3
1477#endif
1478#endif
1479
1480 cwist_route_table_destroy(app->router);
1481
1483 while (curr_m) {
1485 if (curr_m->destroy) curr_m->destroy(curr_m->user_ctx);
1487 curr_m = next;
1488 }
1489
1491 while (curr_e) {
1494 curr_e = next;
1495 }
1496
1497 if (app->config) cwist_config_destroy(app->config);
1498 if (app->logger) cwist_logger_destroy(app->logger);
1499 if (app->rdbms) {
1501 if (rt->host) cwist_free(rt->host);
1502 cwist_free(rt);
1503 }
1504
1506 while (curr_s) {
1507 cwist_static_dir *next = curr_s->next;
1508 cwist_free(curr_s->url_prefix);
1509 cwist_free(curr_s->fs_root);
1510 cwist_free(curr_s->cache_control);
1512 curr_s = next;
1513 }
1514
1515#if defined(CWIST_PLATFORM_HOSTED)
1516 if (app->mem_manager) {
1518 // If thread was started, join it.
1519 // Note: In simple destroy we might not have started it or might be crashing, but try join.
1520 if (app->mem_manager->watcher_thread) {
1522 }
1524
1525 for (size_t i = 0; i < app->mem_manager->file_count; i++) {
1528 }
1529 for (size_t i = 0; i < app->mem_manager->file_count; i++) {
1530 if (app->mem_manager->files[i].node) {
1532 app->mem_manager->files[i].node = NULL;
1533 }
1534 }
1537 cwist_free(app->mem_manager);
1538 }
1539#endif /* __EMSCRIPTEN__ */
1540
1541 if (app->bdr_ctx) {
1543 }
1545 app->assets = NULL;
1546
1547#if defined(CWIST_PLATFORM_HOSTED) && defined(CWIST_DB)
1548 if (app->nuke_enabled) {
1550 }
1551
1552 if (app->db) {
1553 // If nuke was used, app->db->conn was shared with nuke.
1554 // But nuke_close already closed its handles.
1555 // However, cwist_db_close will try to close it again if we are not careful.
1556 // Actually, NukeDB is a global singleton currently, so it's a bit messy.
1557 // If nuke_enabled is true, we should probably NOT call cwist_db_close(app->db)
1558 // OR we should make sure it's safe.
1559 // Based on cwist_db_close implementation, it calls sqlite3_close.
1560 if (app->nuke_enabled) {
1561 // Just free the wrapper, don't close the conn as Nuke already did it.
1562 ttak_mem_free(app->db);
1563 } else {
1564 cwist_db_close(app->db);
1565 }
1566 app->db = NULL;
1567 }
1568#endif /* __EMSCRIPTEN__ */
1569 if (app->db_path) {
1570 cwist_free(app->db_path);
1571 }
1572
1574 if (app->session_name) cwist_free(app->session_name);
1575
1576#if defined(CWIST_PLATFORM_HOSTED)
1577#ifdef CWIST_DB
1578 if (app->db_pool) {
1580 }
1581#endif
1582 if (app->redis_pool) {
1584 }
1585 if (app->scheduler) {
1587 }
1589#endif /* __EMSCRIPTEN__ */
1590
1591 cwist_free(app);
1592}
1593
1599static void mw_next_wrapper(cwist_http_request *req, cwist_http_response *res) {
1600 mw_executor_ctx *ctx = (mw_executor_ctx *)req->private_data;
1601 if (!ctx) return;
1602
1603 if (ctx->current_mw_node) {
1604 cwist_middleware_node *node = ctx->current_mw_node;
1605 // Advance the chain for the next "next" call
1606 ctx->current_mw_node = node->next;
1607 if (node->is_ex) {
1608 node->func_ex(req, res, mw_next_wrapper, node->user_ctx);
1609 } else {
1610 node->func(req, res, mw_next_wrapper);
1611 }
1612 } else if (ctx->final_handler) {
1613 ctx->final_handler(req, res);
1614 }
1615}
1616
1625static void execute_chain(cwist_app *app, cwist_http_request *req, cwist_http_response *res,
1626 cwist_handler_func final_handler, void *handler_data) {
1627 mw_executor_ctx ctx = {app->middlewares, final_handler, handler_data};
1628 req->private_data = &ctx;
1629 if (__builtin_expect(!app->middlewares, 1)) {
1630 /* No middleware: still expose the executor ctx so final handlers
1631 * (e.g. the static-file handler) can reach handler_data. */
1632 if (final_handler) final_handler(req, res);
1633 } else {
1634 mw_next_wrapper(req, res);
1635 }
1636 req->private_data = NULL;
1637}
1638
1646cwist_error_t cwist_app_use_https(cwist_app *app, const char *cert_path, const char *key_path) {
1647#if !defined(CWIST_PLATFORM_HOSTED)
1648 (void)cert_path;
1649 (void)key_path;
1651 err.error.err_i16 = -1; /* TLS needs BoringSSL sockets; native builds only */
1652 (void)app;
1653 return err;
1654#else
1656 if (!app || !cert_path || !key_path) {
1657 err.error.err_i16 = -1;
1658 return err;
1659 }
1660
1661 app->use_ssl = true;
1662 if (app->cert_path) cwist_free(app->cert_path);
1663 if (app->key_path) cwist_free(app->key_path);
1664 app->cert_path = cwist_strdup(cert_path);
1665 app->key_path = cwist_strdup(key_path);
1666
1667 if (!app->cert_path || !app->key_path) {
1668 if (app->cert_path) {
1669 cwist_free(app->cert_path);
1670 app->cert_path = NULL;
1671 }
1672 if (app->key_path) {
1673 cwist_free(app->key_path);
1674 app->key_path = NULL;
1675 }
1676 err.error.err_i16 = -1;
1677 return err;
1678 }
1679
1680 if (app->use_https3 || app->use_http3) {
1682 }
1684#endif
1685}
1686
1691cwist_error_t cwist_app_reload_tls(cwist_app *app, const char *cert_path, const char *key_path) {
1692#if !defined(CWIST_PLATFORM_HOSTED)
1693 (void)cert_path;
1694 (void)key_path;
1696 err.error.err_i16 = -1; /* TLS needs BoringSSL sockets; native builds only */
1697 (void)app;
1698 return err;
1699#else
1701 if (!app || !cert_path || !key_path || !app->use_ssl) {
1702 err.error.err_i16 = -1;
1703 return err;
1704 }
1705 if (!app->ssl_ctx) {
1706 err.error.err_i16 = -1;
1707 return err;
1708 }
1709
1710 err = cwist_https_reload_context(app->ssl_ctx, cert_path, key_path, app);
1711 if (!cwist_error_is_ok(&err)) {
1712 return err;
1713 }
1714
1715 /* HTTPS/3 keeps its own SSL_CTX for the QUIC engine; swap it too so both
1716 * transports present the new certificate together. */
1717#ifdef CWIST_HTTP3
1718 if (app->use_https3 && app->h3_ctx) {
1719 err = cwist_http3_reload_context(app->h3_ctx, cert_path, key_path);
1720 if (!cwist_error_is_ok(&err)) {
1721 return err;
1722 }
1723 }
1724#endif
1725
1726 /* Publish the new paths last so a later refresh (e.g. an HTTP/2 toggle)
1727 * rebuilds from them; they are config-time state, not read per accept. */
1728 char *new_cert = cwist_strdup(cert_path);
1729 char *new_key = cwist_strdup(key_path);
1730 if (!new_cert || !new_key) {
1733 err.error.err_i16 = -1;
1734 return err;
1735 }
1736 cwist_free(app->cert_path);
1737 cwist_free(app->key_path);
1738 app->cert_path = new_cert;
1739 app->key_path = new_key;
1740 return err;
1741#endif
1742}
1743
1744void cwist_app_use_pqc_layer(cwist_app *app, bool enabled) {
1745 if (!app) return;
1746 app->pqc_layer_enabled = enabled;
1747}
1748
1750 if (!app) return;
1751 if (app->tls_groups) {
1752 cwist_free(app->tls_groups);
1753 app->tls_groups = NULL;
1754 }
1755 if (groups) {
1757 }
1758}
1759
1762 if (!app) {
1763 err.error.err_i16 = -1;
1764 return err;
1765 }
1766
1767 app->use_https2 = enabled;
1768#if !defined(CWIST_PLATFORM_HOSTED)
1769 err.error.err_i16 = -1; /* HTTP/2 transport is native-only */
1770 return err;
1771#else
1773 err.error.err_i16 = 0;
1774
1775 if (!app->use_ssl || !app->cert_path || !app->key_path) {
1776 return err;
1777 }
1778
1780#endif
1781}
1782
1785 if (!app) {
1786 err.error.err_i16 = -1;
1787 return err;
1788 }
1789
1790 app->use_https3 = enabled;
1791#if !defined(CWIST_PLATFORM_HOSTED) || !defined(CWIST_HTTP3)
1792 err.error.err_i16 = -1; /* HTTP/3 transport is native-only, or compiled out (CWIST_HTTP3=0) */
1793 return err;
1794#else
1795 err.error.err_i16 = 0;
1796
1797 if (!app->use_ssl || !app->cert_path || !app->key_path) {
1798 return err;
1799 }
1800
1803#endif
1804}
1805
1808 if (!app) {
1809 err.error.err_i16 = -1;
1810 return err;
1811 }
1812
1813 app->use_http2 = enabled;
1814 err.error.err_i16 = 0;
1815 return err;
1816}
1817
1820 if (!app) {
1821 err.error.err_i16 = -1;
1822 return err;
1823 }
1824
1825 app->use_http3 = enabled;
1826#if !defined(CWIST_PLATFORM_HOSTED) || !defined(CWIST_HTTP3)
1827 err.error.err_i16 = -1; /* HTTP/3 transport is native-only, or compiled out (CWIST_HTTP3=0) */
1828 return err;
1829#else
1830 err.error.err_i16 = 0;
1832#endif
1833}
1834
1836 if (!app) return;
1837 app->wt_handler = handler;
1838#if defined(CWIST_PLATFORM_HOSTED) && defined(CWIST_HTTP3)
1839 if (app->h3_ctx) {
1841 }
1842#endif
1843}
1844
1851cwist_error_t cwist_app_use_db(cwist_app *app, const char *db_path) {
1853 if (!app || !db_path) {
1854 err.error.err_i16 = -1;
1855 return err;
1856 }
1857
1858#if !defined(CWIST_PLATFORM_HOSTED) || !defined(CWIST_DB)
1859 err.error.err_i16 =
1860 -1; /* SQLite lives outside the minimal WASM core, or is compiled out (CWIST_DB=0) */
1861 return err;
1862#else
1863 cwist_db *db = NULL;
1864 err = cwist_db_open(&db, db_path);
1865 if (err.error.err_i16 < 0) {
1866 return err;
1867 }
1868
1869 if (app->db) {
1870 cwist_db_close(app->db);
1871 }
1872 if (app->db_path) {
1873 cwist_free(app->db_path);
1874 }
1875
1876 app->db = db;
1877 app->db_path = cwist_strdup(db_path);
1878 app->nuke_enabled = false;
1879 return err;
1880#endif
1881}
1882
1890cwist_error_t cwist_app_use_nuke_db(cwist_app *app, const char *db_path, int sync_interval_ms) {
1892 if (!app || !db_path) {
1893 err.error.err_i16 = -1;
1894 return err;
1895 }
1896
1897#if !defined(CWIST_PLATFORM_HOSTED) || !defined(CWIST_DB)
1898 (void)sync_interval_ms;
1899 err.error.err_i16 =
1900 -1; /* NUKE DB needs native libttak memory, or is compiled out (CWIST_DB=0) */
1901 return err;
1902#else
1903 int nuke_rc = cwist_nuke_init(db_path, sync_interval_ms);
1905 fprintf(
1906 stderr,
1907 "[CWIST] Nuke DB disabled for '%s' (insufficient RAM). Falling back to standard SQLite.\n",
1908 db_path);
1909 return cwist_app_use_db(app, db_path);
1910 } else if (nuke_rc != CWIST_NUKE_OK) {
1911 err.error.err_i16 = -1;
1912 return err;
1913 }
1914
1915 if (app->db) {
1916 if (app->nuke_enabled)
1917 ttak_mem_free(app->db);
1918 else
1919 cwist_db_close(app->db);
1920 app->db = NULL;
1921 }
1922 if (app->db_path) {
1923 cwist_free(app->db_path);
1924 }
1925
1927 cwist_app_mem_now(), false, false, true, true,
1929 if (!app->db) {
1931 err.error.err_i16 = -1;
1932 return err;
1933 }
1934 app->db->conn = cwist_nuke_get_db();
1935 app->db_path = cwist_strdup(db_path);
1936 app->nuke_enabled = true;
1937
1938 err.error.err_i16 = 0;
1939 return err;
1940#endif
1941}
1942
1949 if (!app) return NULL;
1950#if defined(CWIST_PLATFORM_HOSTED) && defined(CWIST_DB)
1951 if (app->nuke_enabled) {
1952 app->db->conn = cwist_nuke_get_db();
1953 }
1954#endif
1955 return app->db;
1956}
1957
1958cwist_error_t cwist_app_use_db_pool(cwist_app *app, const char *db_path, size_t max_conns) {
1960 if (!app || !db_path || max_conns == 0) {
1961 err.error.err_i16 = -1;
1962 return err;
1963 }
1964#if !defined(CWIST_PLATFORM_HOSTED) || !defined(CWIST_DB)
1965 (void)db_path;
1966 (void)max_conns;
1967 err.error.err_i16 =
1968 -1; /* connection pools need native sockets/threads, or are compiled out (CWIST_DB=0) */
1969 return err;
1970#else
1971 if (app->db_pool) {
1973 }
1974 app->db_pool = cwist_db_pool_create(db_path, max_conns);
1975 if (!app->db_pool) {
1976 err.error.err_i16 = -1;
1977 return err;
1978 }
1979 err.error.err_i16 = 0;
1980 return err;
1981#endif
1982}
1983
1985 if (!app) return NULL;
1986 return (cwist_db_pool_t *)app->db_pool;
1987}
1988
1989cwist_error_t cwist_app_use_redis(cwist_app *app, const char *host, int port, size_t max_conns) {
1991 if (!app || !host || port <= 0 || max_conns == 0) {
1992 err.error.err_i16 = -1;
1993 return err;
1994 }
1995#if !defined(CWIST_PLATFORM_HOSTED)
1996 (void)host;
1997 (void)port;
1998 (void)max_conns;
1999 err.error.err_i16 = -1; /* Redis needs native sockets */
2000 return err;
2001#else
2002 if (app->redis_pool) {
2004 }
2005 app->redis_pool = cwist_redis_pool_create(host, port, max_conns);
2006 if (!app->redis_pool) {
2007 err.error.err_i16 = -1;
2008 return err;
2009 }
2010 err.error.err_i16 = 0;
2011 return err;
2012#endif
2013}
2014
2016 if (!app) return NULL;
2017 return (cwist_redis_pool_t *)app->redis_pool;
2018}
2019
2022 if (!app || worker_count == 0) {
2023 err.error.err_i16 = -1;
2024 return err;
2025 }
2026#if !defined(CWIST_PLATFORM_HOSTED)
2027 (void)worker_count;
2029 err.error.err_i16 = -1; /* worker pools need native threads */
2030 return err;
2031#else
2032 if (app->scheduler) {
2034 }
2035 app->scheduler = cwist_scheduler_create(worker_count, queue_capacity);
2036 if (!app->scheduler) {
2037 err.error.err_i16 = -1;
2038 return err;
2039 }
2040 err.error.err_i16 = 0;
2041 return err;
2042#endif
2043}
2044
2046 if (!app) return NULL;
2047 return (cwist_scheduler_t *)app->scheduler;
2048}
2049
2057cwist_error_t cwist_app_static(cwist_app *app, const char *url_prefix, const char *directory) {
2059 if (!app || !url_prefix || !directory) {
2060 err.error.err_i16 = -1;
2061 return err;
2062 }
2063
2064 char *normalized = cwist_normalize_prefix(url_prefix);
2065 if (!normalized) {
2066 err.error.err_i16 = -1;
2067 return err;
2068 }
2069
2070 char *resolved = cwist_normalize_directory(directory);
2071 if (!resolved) {
2073 err.error.err_i16 = -1;
2074 return err;
2075 }
2076
2078 if (!entry) {
2081 err.error.err_i16 = -1;
2082 return err;
2083 }
2084
2085 entry->url_prefix = normalized;
2086 entry->fs_root = resolved;
2087 entry->cache_control = NULL;
2088 entry->next = app->static_dirs;
2089 app->static_dirs = entry;
2090
2091 err.error.err_i16 = 0;
2092 return err;
2093}
2094
2104 const char *directory, const char *cache_control) {
2106 if (!app || !url_prefix || !directory || !cache_control) {
2107 err.error.err_i16 = -1;
2108 return err;
2109 }
2110
2111 char *normalized = cwist_normalize_prefix(url_prefix);
2112 if (!normalized) {
2113 err.error.err_i16 = -1;
2114 return err;
2115 }
2116
2117 char *resolved = cwist_normalize_directory(directory);
2118 if (!resolved) {
2120 err.error.err_i16 = -1;
2121 return err;
2122 }
2123
2125 if (!entry) {
2128 err.error.err_i16 = -1;
2129 return err;
2130 }
2131
2132 entry->url_prefix = normalized;
2133 entry->fs_root = resolved;
2134 entry->cache_control = cwist_strdup(cache_control);
2135 if (!entry->cache_control) {
2138 cwist_free(entry);
2139 err.error.err_i16 = -1;
2140 return err;
2141 }
2142 entry->next = app->static_dirs;
2143 app->static_dirs = entry;
2144
2145 err.error.err_i16 = 0;
2146 return err;
2147}
2148
2149static void add_route_named(cwist_app *app, const char *path, const char *name,
2151 cwist_endpoint_opt_t opts) {
2152 if (!app || !app->router || !path) return;
2153 if (opts == 0) {
2155 }
2156 const cwist_route_target target = {.handler = handler};
2157 cwist_route_table_insert(app->router, path, name, method, &target, opts);
2158}
2159
2160/* Shared body of the cwist_app_*_ex() registrations. The context is owned by
2161 * the route table from here on, including on every failure path. */
2162static cwist_error_t add_route_ex(cwist_app *app, const char *path, cwist_http_method_t method,
2163 cwist_handler_ex_func handler, void *user_ctx,
2166 if (!app || !app->router || !handler) {
2167 if (destroy) destroy(user_ctx);
2168 err.error.err_i16 = -1;
2169 return err;
2170 }
2171 const cwist_route_target target = {
2172 .handler_ex = handler, .user_ctx = user_ctx, .ctx_destroy = destroy};
2173 /* On failure the table has already released the context. */
2174 bool ok =
2175 cwist_route_table_insert(app->router, path, NULL, method, &target, CWIST_ENDPOINT_DEFAULT);
2176 err.error.err_i16 = ok ? 0 : -1;
2177 return err;
2178}
2179
2181 void *user_ctx, cwist_handler_ctx_destroy_func destroy) {
2182 return add_route_ex(app, path, CWIST_HTTP_GET, handler, user_ctx, destroy);
2183}
2184
2186 void *user_ctx, cwist_handler_ctx_destroy_func destroy) {
2187 return add_route_ex(app, path, CWIST_HTTP_POST, handler, user_ctx, destroy);
2188}
2189
2191 void *user_ctx, cwist_handler_ctx_destroy_func destroy) {
2192 return add_route_ex(app, path, CWIST_HTTP_PUT, handler, user_ctx, destroy);
2193}
2194
2196 void *user_ctx, cwist_handler_ctx_destroy_func destroy) {
2197 return add_route_ex(app, path, CWIST_HTTP_DELETE, handler, user_ctx, destroy);
2198}
2199
2201 void *user_ctx, cwist_handler_ctx_destroy_func destroy) {
2202 return add_route_ex(app, path, CWIST_HTTP_PATCH, handler, user_ctx, destroy);
2203}
2204
2205static void add_route(cwist_app *app, const char *path, cwist_http_method_t method,
2207 add_route_named(app, path, NULL, method, handler, opts);
2208}
2209
2216void cwist_app_get(cwist_app *app, const char *path, cwist_handler_func handler) {
2217 add_route(app, path, CWIST_HTTP_GET, handler, CWIST_ENDPOINT_DEFAULT);
2218}
2219
2221
2222static void cwist_metrics_route_handler(cwist_http_request *req, cwist_http_response *res) {
2223 (void)req;
2225}
2226
2228 if (!app) return;
2229 cwist_app_get(app, "/metrics", cwist_metrics_route_handler);
2230}
2231
2232static void cwist_healthz_route_handler(cwist_http_request *req, cwist_http_response *res) {
2233 (void)req;
2234 cwist_app_healthz(res);
2235}
2236
2237static void cwist_liveness_route_handler(cwist_http_request *req, cwist_http_response *res) {
2238 (void)req;
2240 cwist_sstring_assign(res->body, "{\"status\":\"alive\"}");
2241 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
2242}
2243
2244static void cwist_readiness_route_handler(cwist_http_request *req, cwist_http_response *res) {
2245 (void)req;
2246 cwist_app_healthz(res);
2247}
2248
2250 if (!app) return;
2251 cwist_app_get(app, "/healthz", cwist_healthz_route_handler);
2252 cwist_app_get(app, "/live", cwist_liveness_route_handler);
2253 cwist_app_get(app, "/ready", cwist_readiness_route_handler);
2254}
2255
2256void cwist_app_get_named(cwist_app *app, const char *path, const char *name,
2257 cwist_handler_func handler) {
2258 add_route_named(app, path, name, CWIST_HTTP_GET, handler, CWIST_ENDPOINT_DEFAULT);
2259}
2260
2267void cwist_app_post(cwist_app *app, const char *path, cwist_handler_func handler) {
2268 add_route(app, path, CWIST_HTTP_POST, handler, CWIST_ENDPOINT_DEFAULT);
2269}
2270
2271void cwist_app_put(cwist_app *app, const char *path, cwist_handler_func handler) {
2272 add_route(app, path, CWIST_HTTP_PUT, handler, CWIST_ENDPOINT_DEFAULT);
2273}
2274
2275void cwist_app_delete(cwist_app *app, const char *path, cwist_handler_func handler) {
2276 add_route(app, path, CWIST_HTTP_DELETE, handler, CWIST_ENDPOINT_DEFAULT);
2277}
2278
2279void cwist_app_patch(cwist_app *app, const char *path, cwist_handler_func handler) {
2280 add_route(app, path, CWIST_HTTP_PATCH, handler, CWIST_ENDPOINT_DEFAULT);
2281}
2282
2283void cwist_app_post_named(cwist_app *app, const char *path, const char *name,
2284 cwist_handler_func handler) {
2285 add_route_named(app, path, name, CWIST_HTTP_POST, handler, CWIST_ENDPOINT_DEFAULT);
2286}
2287
2288void cwist_app_put_named(cwist_app *app, const char *path, const char *name,
2289 cwist_handler_func handler) {
2290 add_route_named(app, path, name, CWIST_HTTP_PUT, handler, CWIST_ENDPOINT_DEFAULT);
2291}
2292
2293void cwist_app_delete_named(cwist_app *app, const char *path, const char *name,
2294 cwist_handler_func handler) {
2295 add_route_named(app, path, name, CWIST_HTTP_DELETE, handler, CWIST_ENDPOINT_DEFAULT);
2296}
2297
2298void cwist_app_patch_named(cwist_app *app, const char *path, const char *name,
2299 cwist_handler_func handler) {
2300 add_route_named(app, path, name, CWIST_HTTP_PATCH, handler, CWIST_ENDPOINT_DEFAULT);
2301}
2302
2309void cwist_app_ws(cwist_app *app, const char *path, cwist_ws_handler_func handler) {
2310#ifdef CWIST_WS
2311 if (!app || !app->router || !path) return;
2312 const cwist_route_target target = {.ws_handler = handler};
2313 cwist_route_table_insert(app->router, path, NULL, CWIST_HTTP_GET, &target,
2315#else
2316 /* WebSocket compiled out (CWIST_WS=0): registration is a no-op. */
2317 (void)app;
2318 (void)path;
2319 (void)handler;
2320#endif
2321}
2322
2330void cwist_app_ws_async(cwist_app *app, const char *path, cwist_ws_on_message_t on_message,
2331 void *user_data) {
2332#ifdef CWIST_WS
2333 if (!app || !app->router || !path || !on_message) return;
2334 const cwist_route_target target = {0};
2335 cwist_route_table_insert(app->router, path, NULL, CWIST_HTTP_GET, &target,
2337 cwist_route_entry *entry = cwist_route_table_lookup(app->router, CWIST_HTTP_GET, path);
2338 if (entry) {
2339 entry->ws_async_on_message = on_message;
2340 entry->ws_async_user_data = user_data;
2341 }
2342#else
2343 /* WebSocket compiled out (CWIST_WS=0): registration is a no-op. */
2344 (void)app;
2345 (void)path;
2346 (void)on_message;
2347 (void)user_data;
2348#endif
2349}
2350
2351/* Forwards to the inline helper so the two can never disagree. */
2353 return cwist_endpoint_has(opts, flag);
2354}
2355
2363void cwist_app_get_opt(cwist_app *app, const char *path, cwist_handler_func handler,
2364 cwist_endpoint_opt_t opts) {
2365 add_route(app, path, CWIST_HTTP_GET, handler, opts);
2366}
2367
2375void cwist_app_post_opt(cwist_app *app, const char *path, cwist_handler_func handler,
2376 cwist_endpoint_opt_t opts) {
2377 add_route(app, path, CWIST_HTTP_POST, handler, opts);
2378}
2379
2380void cwist_app_put_opt(cwist_app *app, const char *path, cwist_handler_func handler,
2381 cwist_endpoint_opt_t opts) {
2382 add_route(app, path, CWIST_HTTP_PUT, handler, opts);
2383}
2384
2385void cwist_app_delete_opt(cwist_app *app, const char *path, cwist_handler_func handler,
2386 cwist_endpoint_opt_t opts) {
2387 add_route(app, path, CWIST_HTTP_DELETE, handler, opts);
2388}
2389
2390void cwist_app_patch_opt(cwist_app *app, const char *path, cwist_handler_func handler,
2391 cwist_endpoint_opt_t opts) {
2392 add_route(app, path, CWIST_HTTP_PATCH, handler, opts);
2393}
2394
2402void cwist_app_ws_opt(cwist_app *app, const char *path, cwist_ws_handler_func handler,
2403 cwist_endpoint_opt_t opts) {
2404#ifdef CWIST_WS
2405 if (!app || !app->router || !path) return;
2406 if (opts == 0) {
2408 }
2409 const cwist_route_target target = {.ws_handler = handler};
2410 cwist_route_table_insert(app->router, path, NULL, CWIST_HTTP_GET, &target, opts);
2411#else
2412 /* WebSocket compiled out (CWIST_WS=0): registration is a no-op. */
2413 (void)app;
2414 (void)path;
2415 (void)handler;
2416 (void)opts;
2417#endif
2418}
2419
2420static bool match_path(const char *pattern, const char *actual, cwist_query_map *params) {
2421 if (params) {
2423 }
2424
2425 const char *p = pattern;
2426 const char *a = actual;
2427 for (;;) {
2428 while (*p == '/') p++;
2429 while (*a == '/') a++;
2430 if (*p == '\0' || *a == '\0') return *p == '\0' && *a == '\0';
2431
2432 const char *p_slash = strchr(p, '/');
2433 const size_t p_len = p_slash ? (size_t)(p_slash - p) : strlen(p);
2434 const char *a_slash = strchr(a, '/');
2435 const size_t a_len = a_slash ? (size_t)(a_slash - a) : strlen(a);
2436
2437 if (p[0] == ':') {
2438 if (params) {
2439 char key[256], val[256];
2440 const size_t klen = (p_len - 1 < 255) ? p_len - 1 : 255;
2441 const size_t vlen = (a_len < 255) ? a_len : 255;
2442 memcpy(key, p + 1, klen);
2443 key[klen] = '\0';
2444 memcpy(val, a, vlen);
2445 val[vlen] = '\0';
2447 }
2448 } else if (p_len != a_len || memcmp(p, a, p_len) != 0) {
2449 return false;
2450 }
2451
2452 p += p_len;
2453 a += a_len;
2454 }
2455}
2456
2457static cwist_route_entry *cwist_route_table_find_by_name(cwist_route_table *table,
2458 const char *name) {
2459 if (!table || !name) return NULL;
2460 for (size_t i = 0; i < table->bucket_count; i++) {
2461 cwist_route_entry *curr = table->buckets[i];
2462 while (curr) {
2463 if (curr->name && strcmp(curr->name, name) == 0) return curr;
2464 curr = curr->next;
2465 }
2466 }
2467 cwist_route_entry *curr = table->param_routes;
2468 while (curr) {
2469 if (curr->name && strcmp(curr->name, name) == 0) return curr;
2470 curr = curr->next;
2471 }
2472 return NULL;
2473}
2474
2475char *cwist_url_for(cwist_app *app, const char *name, cwist_query_map *params) {
2476 if (!app || !app->router || !name) return NULL;
2477 cwist_route_entry *entry = cwist_route_table_find_by_name(app->router, name);
2478 if (!entry) return NULL;
2479 const char *path = entry->path;
2480 if (!params || !entry->has_params) {
2481 return cwist_strdup(path);
2482 }
2483 // Build URL by replacing :param segments
2485 if (!result) return NULL;
2486 char path_copy[512];
2487 strncpy(path_copy, path, sizeof(path_copy) - 1);
2488 path_copy[sizeof(path_copy) - 1] = '\0';
2489 char *saveptr;
2490 char *tok = strtok_r(path_copy, "/", &saveptr);
2491 int first = 1;
2492 while (tok) {
2493 if (!first) cwist_sstring_append(result, "/");
2494 first = 0;
2495 if (tok[0] == ':') {
2496 const char *val = cwist_query_map_get(params, tok + 1);
2497 if (val) {
2498 cwist_sstring_append(result, val);
2499 } else {
2500 cwist_sstring_append(result, tok);
2501 }
2502 } else {
2503 cwist_sstring_append(result, tok);
2504 }
2505 tok = strtok_r(NULL, "/", &saveptr);
2506 }
2507 char *out = cwist_strdup(result->data);
2508 cwist_sstring_destroy(result);
2509 return out;
2510}
2511
2512// Forward declaration
2513static void internal_route_handler(cwist_app *app, cwist_http_request *req,
2514 cwist_http_response *res);
2515
2517 if (!app || !req || !res) return;
2518 req->app = app;
2519 req->db = app->db;
2520 internal_route_handler(app, req, res);
2521}
2522
2523int cwist_app_dispatch_memory(cwist_app *app, const char *req_buf, size_t req_len, char **res_buf,
2524 size_t *res_len) {
2525 if (!app || !req_buf || !res_buf || !res_len) return -1;
2526 *res_buf = NULL;
2527 *res_len = 0;
2528
2530 if (!req) return -1;
2531
2533 if (!res) {
2535 return -1;
2536 }
2537 cwist_app_dispatch(app, req, res);
2539
2540 /* Producer streams finalize at dispatch return when the handler did not
2541 * end them explicitly (terminator only; no sink is attached here). */
2543
2544 /* One-shot buffer exchange: Connection: close semantics. */
2545 res->keep_alive = false;
2548 return rc;
2549}
2550
2551/* --- WASM boundary streaming (issue #93 Phase 3) -------------------------- */
2552
2553/* Cap on the body bytes preallocated from the declared Content-Length.
2554 * Feeds beyond this grow the buffer on demand so a bogus huge declaration
2555 * cannot force a huge allocation at begin() time. */
2556#define CWIST_STREAM_REQ_EAGER_MAX ((size_t)1 << 20)
2557
2558struct cwist_stream_req {
2559 /* Head and body share one contiguous buffer: the head sits at
2560 * [0, head_len) and fed body bytes are appended after it, so each
2561 * chunk is copied exactly once into its final wire position. */
2562 char *wire;
2563 size_t head_len;
2564 size_t content_length; /* parsed from Content-Length; 0 when absent */
2565 size_t body_len;
2566 size_t wire_cap;
2567 bool complete; /* set by a successful cwist_stream_req_end() */
2568};
2569
2570/* Case-insensitive scan of the head block for a Content-Length header.
2571 * Returns the declared length, 0 when absent, -1 on malformed values. */
2572static long cwist_stream_parse_content_length(const char *head, size_t head_len) {
2573 static const char cl_name[] = "content-length:";
2574 const char *p = head;
2575 const char *end = head + head_len;
2576 while (p + sizeof(cl_name) - 1 <= end) {
2577 size_t i = 0;
2578 while (i < sizeof(cl_name) - 1) {
2579 unsigned char c = (unsigned char)p[i];
2580 if (c >= 'A' && c <= 'Z') c = (unsigned char)(c + ('a' - 'A'));
2581 if ((char)c != cl_name[i]) break;
2582 i++;
2583 }
2584 if (i == sizeof(cl_name) - 1) {
2585 const char *v = p + i;
2586 while (v < end && (*v == ' ' || *v == '\t')) v++;
2587 errno = 0;
2588 char *num_end = NULL;
2589 long n = strtol(v, &num_end, 10);
2590 if (errno != 0 || num_end == v || n < 0) return -1;
2591 return n;
2592 }
2593 /* advance to the next header line */
2594 while (p < end && *p != '\n') p++;
2595 if (p < end) p++;
2596 }
2597 return 0;
2598}
2599
2600cwist_stream_req_t *cwist_stream_req_begin(const char *head, size_t head_len) {
2601 if (!head || head_len == 0) return NULL;
2602 cwist_stream_req_t *r = cwist_alloc(sizeof(*r));
2603 if (!r) return NULL;
2604 long cl = cwist_stream_parse_content_length(head, head_len);
2605 if (cl < 0) {
2606 cwist_free(r);
2607 return NULL;
2608 }
2609 r->head_len = head_len;
2610 r->content_length = (size_t)cl;
2611 r->body_len = 0;
2612 /* Eagerly reserve the declared body when it is modest; larger bodies
2613 * grow on feed so a bogus Content-Length cannot force a big allocation. */
2614 size_t eager = (size_t)cl < CWIST_STREAM_REQ_EAGER_MAX ? (size_t)cl : 0;
2615 r->wire_cap = head_len + eager;
2616 r->wire = cwist_alloc(r->wire_cap);
2617 if (!r->wire) {
2618 cwist_free(r);
2619 return NULL;
2620 }
2621 memcpy(r->wire, head, head_len);
2622 r->complete = false;
2623 return r;
2624}
2625
2626int cwist_stream_req_feed(cwist_stream_req_t *r, const char *chunk, size_t len) {
2627 if (!r || (!chunk && len > 0)) return -1;
2628 if (len > r->content_length - r->body_len) return -1; /* overflow of declared length */
2629 if (len == 0) return 0;
2630 size_t used = r->head_len + r->body_len;
2631 if (len > r->wire_cap - used) {
2632 size_t ncap = r->wire_cap ? r->wire_cap : 4096;
2633 while (ncap - r->head_len < r->body_len + len) ncap *= 2;
2634 char *nb = cwist_alloc(ncap);
2635 if (!nb) return -1;
2636 memcpy(nb, r->wire, used);
2637 cwist_free(r->wire);
2638 r->wire = nb;
2639 r->wire_cap = ncap;
2640 }
2641 memcpy(r->wire + used, chunk, len);
2642 r->body_len += len;
2643 return 0;
2644}
2645
2647 if (!r) return -1;
2648 if (r->body_len != r->content_length) return -1;
2649 r->complete = true;
2650 return 0;
2651}
2652
2654 void *write_ctx) {
2655 if (!r) return -1;
2656 int rc;
2657 if (!app || !write_fn || !r->complete) {
2658 rc = -1;
2659 } else {
2660 rc =
2661 cwist_app_dispatch_stream(app, r->wire, r->head_len + r->body_len, write_fn, write_ctx);
2662 }
2663 cwist_free(r->wire);
2664 cwist_free(r);
2665 return rc;
2666}
2667
2670 if (!app || !req_buf || !write_fn) return -1;
2671
2673 if (!req) return -1;
2674
2676 if (!res) {
2678 return -1;
2679 }
2680 /* Attach the sink before dispatch: producer writes stream head-first
2681 * and chunk-by-chunk while the handler is still running. */
2682 res->stream_sink = write_fn;
2684 cwist_app_dispatch(app, req, res);
2686 res->keep_alive = false;
2687
2688 if (res->stream_mode) {
2689 /* Finalize an un-ended stream (flushes the terminator through the
2690 * sink), then detach: everything already travelled to write_fn. */
2692 res->stream_sink = NULL;
2694 return res->stream_failed ? -2 : 0;
2695 }
2696 res->stream_sink = NULL;
2697
2698 if (res->use_file_stream) {
2699 /* File-stream bodies are not resident memory; the streaming boundary
2700 * handles the same in-memory shapes as dispatch_memory. */
2702 return -1;
2703 }
2704
2706 size_t head_len = cwist_http_serialize_headers(res, head_buf, sizeof(head_buf));
2707 if (head_len == 0 || head_len >= sizeof(head_buf)) {
2709 return -1;
2710 }
2711 if (write_fn(write_ctx, head_buf, head_len) != 0) {
2713 return -2;
2714 }
2715
2716 const void *body_ptr = NULL;
2717 size_t body_len = 0;
2718 if (res->is_ptr_body) {
2719 body_ptr = res->ptr_body;
2720 body_len = res->ptr_body_len;
2721 } else if (res->body && res->body->data) {
2722 body_ptr = res->body->data;
2723 body_len = res->body->size;
2724 }
2725 int rc = 0;
2726 size_t off = 0;
2727 while (off < body_len) {
2728 size_t n = body_len - off;
2730 if (write_fn(write_ctx, (const char *)body_ptr + off, n) != 0) {
2731 rc = -2;
2732 break;
2733 }
2734 off += n;
2735 }
2737 return rc;
2738}
2739
2740// Internal Router Logic
2744static void cwist_asset_handler(cwist_http_request *req, cwist_http_response *res) {
2745 mw_executor_ctx *ctx = (mw_executor_ctx *)req->private_data;
2746 cwist_assets_respond(req, res, ctx ? (const cwist_asset_match *)ctx->handler_data : NULL);
2747}
2748
2749/* Final handler for cwist_app_*_ex() routes: execute_route() passes the route
2750 * entry as handler_data, which the middleware executor keeps reachable. */
2751static void cwist_route_ex_handler(cwist_http_request *req, cwist_http_response *res) {
2752 mw_executor_ctx *ctx = (mw_executor_ctx *)req->private_data;
2753 cwist_route_entry *route = ctx ? (cwist_route_entry *)ctx->handler_data : NULL;
2754 if (route && route->handler_ex) route->handler_ex(route->user_ctx, req, res);
2755}
2756
2757static void execute_route(cwist_app *app, cwist_http_request *req, cwist_http_response *res,
2758 cwist_route_entry *route) {
2759 if (route->handler_ex) {
2760 execute_chain(app, req, res, cwist_route_ex_handler, route);
2761 } else {
2762 execute_chain(app, req, res, route->handler, NULL);
2763 }
2764}
2765
2766static void internal_route_handler(cwist_app *app, cwist_http_request *req,
2767 cwist_http_response *res) {
2768 if (!req || !app || !app->router) return;
2769
2771 if (res) {
2772 res->endpoint_opts = req->endpoint_opts;
2773 }
2774
2775 const char *path = (req->path && req->path->data) ? req->path->data : "/";
2776 cwist_route_entry *found_route = cwist_route_table_lookup(app->router, req->method, path);
2777
2778 if (found_route) {
2780 if (res) res->endpoint_opts = req->endpoint_opts;
2781#ifdef CWIST_WS
2782 if (found_route->ws_async_on_message && req->async_conn) {
2783#if defined(CWIST_PLATFORM_HOSTED)
2784 /* C1M path (issue #181): the route handler runs on the reactor
2785 * thread, so invoking a blocking ws_handler here would park the
2786 * whole worker in recv(). Complete the upgrade, send the 101
2787 * through the coalesced writer, then hand the fd to the
2788 * reactor-driven callback-shaped WebSocket path. */
2789 if (cwist_websocket_upgrade_response(req, res)) {
2791 req->upgraded = true;
2792 res->keep_alive = true;
2793 cwist_http_send_response_coalesced(req->client_fd, res, aconn, false, false);
2795 if (cwist_websocket_async_attach(req->client_fd, aconn->reactor,
2796 found_route->ws_async_on_message,
2797 found_route->ws_async_user_data,
2798 (const uint8_t *)aconn->rbuf, aconn->len)) {
2799 /* Bytes already read past the upgrade request were
2800 * copied into the WS stash by attach. The HTTP layer
2801 * releases the connection shell (without closing the fd)
2802 * when the C1M loop reports CWIST_ASYNC_DETACH on
2803 * req->ws_async_handoff; the WS async state owns the fd
2804 * from here on. */
2805 req->async_conn = NULL;
2806 req->ws_async_handoff = true;
2807 } else {
2808 /* 101 already sent and attach closed the fd; detach so
2809 * the HTTP layer only releases the connection shell. */
2810 req->async_conn = NULL;
2811 req->ws_async_handoff = true;
2812 }
2813 } else {
2815 cwist_sstring_assign(res->body, "WebSocket Upgrade Failed");
2816 }
2817#endif
2818 } else if (found_route->ws_handler) {
2819#if !defined(CWIST_PLATFORM_HOSTED)
2820 /* WebSocket upgrades need a live socket; in-memory dispatch has none. */
2822 cwist_sstring_assign(res->body, "WebSocket Upgrade Failed");
2823#else
2824 if (req->client_fd >= 0) {
2826 if (ws) {
2827 found_route->ws_handler(ws);
2829 } else {
2831 cwist_sstring_assign(res->body, "WebSocket Upgrade Failed");
2832 }
2833 }
2834#endif
2835 } else if (found_route->ws_async_on_message) {
2836 /* Callback-shaped WS routes are C1M-only; classic mode keeps the
2837 * blocking cwist_websocket_receive() API via cwist_app_ws(). */
2839 cwist_sstring_assign(res->status_text, "Not Implemented");
2840 cwist_sstring_assign(res->body, "WebSocket async handler requires C1M mode");
2841 } else {
2842 execute_route(app, req, res, found_route);
2843 }
2844#else
2845 /* WebSocket compiled out (CWIST_WS=0): ws routes can never register,
2846 * so every matched route is a plain HTTP route. */
2847 execute_route(app, req, res, found_route);
2848#endif
2849 return;
2850 }
2851
2852 if (!found_route) {
2853 found_route = cwist_route_table_match_params(app->router, req);
2854 }
2855
2856 if (found_route) {
2858 if (res) res->endpoint_opts = req->endpoint_opts;
2859 execute_route(app, req, res, found_route);
2860 return;
2861 }
2862
2863 /* In-memory assets need no filesystem, so unlike static directories they
2864 * are served on WASM hosts too. */
2866 if (cwist_assets_match(app, req, &asset_match)) {
2868 if (res) res->endpoint_opts = req->endpoint_opts;
2869 execute_chain(app, req, res, cwist_asset_handler, &asset_match);
2870 return;
2871 }
2872
2873 cwist_static_request_info static_info = {0};
2874 if (cwist_prepare_static(app, req, &static_info)) {
2875#if defined(CWIST_PLATFORM_HOSTED)
2877 if (res) res->endpoint_opts = req->endpoint_opts;
2878 execute_chain(app, req, res, cwist_static_handler, &static_info);
2879 return;
2880#else
2881 /* Unreachable: cwist_prepare_static() always declines on WASM hosts,
2882 * and the filesystem-backed handler is not compiled in. Guarded so
2883 * the native-only handler reference stays out of the WASM link. */
2884 return;
2885#endif
2886 }
2887
2888 cwist_error_handler_func eh = cwist_app_find_error_handler(app, CWIST_HTTP_NOT_FOUND);
2889 if (eh) {
2890 eh(req, res, CWIST_HTTP_NOT_FOUND);
2891 } else {
2893 cwist_sstring_assign(res->body, "404 Not Found");
2894 }
2895}
2896
2897#if !defined(__EMSCRIPTEN__) && !defined(CWIST_PLATFORM_NO_SOCKETS)
2898/* Everything below up to the multiport section is socket serving machinery:
2899 * TLS/plain connection handlers, h2/h3 bridges, async flush paths. */
2900static void static_http2_route_bridge(void *user_ctx, cwist_http_request *req,
2901 cwist_http_response *res) {
2902 cwist_app *app = (cwist_app *)user_ctx;
2903 if (!app || !req || !res) return;
2904 req->app = app;
2905 req->db = app->db;
2906 internal_route_handler(app, req, res);
2907}
2908
2909#ifdef CWIST_HTTP3
2910static void static_http3_route_bridge(void *user_ctx, cwist_http_request *req,
2911 cwist_http_response *res) {
2912 cwist_app *app = (cwist_app *)user_ctx;
2913 if (!app || !req || !res) return;
2914 req->app = app;
2915 req->db = app->db;
2916 internal_route_handler(app, req, res);
2917 /* HTTP/3 has no deferred-response path yet; drop this thread's pending
2918 * handoff so a later dispatch cannot mistake it for its own. */
2920}
2921#endif
2922
2923#ifndef CWIST_PLATFORM_NO_PROC
2924/* TLS connection handlers: BoringSSL is not linked on WASI, and the only
2925 * call sites (cwist_app_listen's SSL branch, multiport) are compiled out
2926 * there. */
2927static void static_ssl_handler(cwist_https_connection *conn, void *ctx) {
2928 cwist_app *app = (cwist_app *)ctx;
2929 if (!app || !conn) return;
2930
2932 static_ssl_http2_handler(conn, ctx);
2933 return;
2934 }
2935
2936 static_ssl_http1_handler(conn, ctx);
2937}
2938
2939/* Optional hook for applications that need to take ownership of a
2940 * TLS-upgraded connection (e.g. reverse-session hijacking). The no-op
2941 * default lives in src/net/http/https_upgrade_hook.c and is overridden by
2942 * any strong definition from the embedding application at static link
2943 * time. Return true to detach the fd/ssl from cwist so they are not closed
2944 * after the response is sent. */
2946 cwist_http_response *res);
2947
2948static void static_ssl_http1_handler(cwist_https_connection *conn, void *ctx) {
2949 cwist_app *app = (cwist_app *)ctx;
2950
2951 /* HTTP/1.1 keep-alive loop: previously every request paid a full TCP
2952 * accept + TLS handshake + teardown because the connection closed after
2953 * a single response. cwist_https_receive_request bounds each header read
2954 * with CWIST_HTTP_HEADERS_TIMEOUT_MS, so idle keep-alive connections are
2955 * reaped automatically. */
2956 while (true) {
2957 /* Nothing to read: hand the connection to the park set instead of
2958 * holding this pool thread in the header read's poll(). */
2959 if (cwist_https_conn_idle(conn) &&
2961 return;
2963 if (!req) return;
2964 req->app = app;
2965 req->db = app->db;
2966 req->https_conn = conn;
2967
2969 if (!res) {
2971 return;
2972 }
2973 internal_route_handler(app, req, res);
2974
2975 /* Deferred-response handoff: ownership of req/res and conn moved to cwist_async */
2976 cwist_async *deferred = cwist_async_dispatch_take(res);
2977 if (deferred) {
2978 cwist_async_dispatch_ack(deferred);
2979 return;
2980 }
2981
2982 bool keep_alive = req->keep_alive && res->keep_alive;
2983 bool upgraded = req->upgraded;
2984
2985 /* During shutdown, answer in flight but mark the connection as
2986 * closing so the client does not race another request onto it. */
2988 res->keep_alive = false;
2989 keep_alive = false;
2990 }
2991
2992 if (req->method == CWIST_HTTP_HEAD) {
2994 } else {
2995 cwist_https_send_response(conn, res);
2996 }
2997
2998 bool detached = false;
2999 if (upgraded) {
3000 detached = cwist_https_upgrade_handler(conn, req, res);
3001 if (detached) {
3002 conn->ssl = NULL;
3003 conn->fd = -1;
3004 }
3005 }
3006
3009
3010 if (!keep_alive || upgraded) return;
3011 }
3012}
3013
3014static void static_ssl_http2_handler(cwist_https_connection *conn, void *ctx) {
3015 cwist_app *app = (cwist_app *)ctx;
3017 static_ssl_http1_handler(conn, ctx);
3018 return;
3019 }
3020
3021 cwist_error_t err = cwist_http2_serve_connection_ex(conn, app, static_http2_route_bridge,
3023 if (err.errtype == CWIST_ERR_JSON && err.error.err_json) {
3024 cJSON_Delete(err.error.err_json);
3025 }
3026}
3027#endif /* __wasi__ (TLS handlers need BoringSSL) */
3028
3033static int app_parse_error_status(cwist_http_parse_error_t perr) {
3034 switch (perr) {
3035 case CWIST_HTTP_PARSE_MALFORMED: return 400;
3036 case CWIST_HTTP_PARSE_BODY_TOO_LARGE: return 413;
3037 case CWIST_HTTP_PARSE_EXPECT_FAILED: return 417;
3038 case CWIST_HTTP_PARSE_HEADER_OVERFLOW: return 431;
3039 case CWIST_HTTP_PARSE_TE_UNSUPPORTED: return 501;
3040 default: return 0;
3041 }
3042}
3043
3044static void app_maybe_send_parse_error(int client_fd, cwist_http_parse_error_t perr) {
3045 int status = app_parse_error_status(perr);
3046 if (status > 0) {
3047 cwist_http_send_error_response(client_fd, status, NULL);
3048 }
3049}
3050
3051/* Async variant: the error goes through the coalescing stash so it cannot
3052 * overtake responses buffered earlier in the same batch turn. */
3053static void app_maybe_send_parse_error_async(cwist_http_async_conn_t *conn,
3055 int status = app_parse_error_status(perr);
3056 if (status > 0) {
3058 }
3059}
3060
3061/* Flush the coalescing stash at a batch-turn exit. A parked remainder owns
3062 * fd/conn from here (it re-arms or closes after draining), so the caller
3063 * must leave both untouched. */
3064static cwist_async_action_t app_async_flush_exit(int client_fd, cwist_http_async_conn_t *conn,
3065 cwist_async_action_t action, bool keep_alive) {
3066 static _Atomic long dbg_parked;
3067 int d = cwist_async_debug_enabled();
3068 cwist_coalesce_flush_status_t fs = cwist_http_coalesce_flush(client_fd, conn, keep_alive);
3070 if (d) {
3071 long n = atomic_fetch_add(&dbg_parked, 1) + 1;
3072 if (n <= 5 || n % 10000 == 0)
3073 fprintf(stderr, "[async] flush-parked fd=%d total=%ld\n", client_fd, n);
3074 }
3075 return CWIST_ASYNC_DEFER;
3076 }
3078 return action;
3079}
3080
3085typedef enum {
3086 APP_SERVE_CLOSE = 0, /* Close the connection. */
3087 APP_SERVE_KEEPALIVE, /* Connection stays open. */
3088 APP_SERVE_DEFERRED, /* Handler deferred via cwist_async_defer; skip everything. */
3089 APP_SERVE_DETACH /* Upgraded fd handed to another owner (WS async); do not close or re-arm. */
3091
3092#ifndef CWIST_PLATFORM_NO_SOCKETS
3093/* Socket-server request serving: preview1 hosts dispatch in memory instead. */
3094static app_serve_result_t app_serve_parsed_request(cwist_app *app, int client_fd,
3095 cwist_http_request *req,
3097 // --- Big Dumb Reply (Read) ---
3098 if (app->bdr_ctx && req->method == CWIST_HTTP_GET) {
3099 size_t cached_len = 0;
3101 const void *cached_blob;
3102 if (req->async_conn) {
3103 /* Keep-alive connections repeat the same route: the per-connection
3104 * cursor turns the lookup into a content-compare + epoch-validated
3105 * entry reuse instead of a SipHash + bucket walk per request. */
3108 req->path->size, &cached_len, &bdr_pin,
3109 &aconn->bdr_cursor);
3110 } else {
3111 cached_blob =
3112 cwist_bdr_get_pinned(app->bdr_ctx, "GET", req->path->data, &cached_len, &bdr_pin);
3113 }
3114 if (cached_blob && cached_len > 0) {
3115 bool keep_alive = req->keep_alive;
3116 if (req->async_conn) {
3120 if (cwist_http_coalesce_flush_blocking(client_fd, aconn) != 0) {
3123 return APP_SERVE_CLOSE;
3124 }
3125 }
3127 send(client_fd, cached_blob, cached_len, MSG_NOSIGNAL);
3131 return APP_SERVE_CLOSE;
3132 }
3133 } else {
3134 send(client_fd, cached_blob, cached_len, MSG_NOSIGNAL);
3135 }
3138 return keep_alive ? APP_SERVE_KEEPALIVE : APP_SERVE_CLOSE;
3139 }
3141 }
3142 // -----------------------------
3143
3144 /* Share the request arena: saves one arena create/destroy per request and
3145 * the response is always destroyed just before the request below. */
3147 if (!res) {
3149 return APP_SERVE_CLOSE;
3150 }
3151
3152 bool endpoint_fixed = cwist_endpoint_has(req->endpoint_opts, CWIST_ENDPOINT_FIXED);
3153 struct timespec start, end;
3155 if (app->bdr_ctx && !endpoint_fixed) {
3157 }
3158
3159 internal_route_handler(app, req, res);
3160
3161 /* Deferred-response handoff: ownership of req/res (and the connection)
3162 * moved to the cwist_async completion path. Skip the send, BDR learning,
3163 * and both destroys; ack before returning so the completion may free.
3164 * The completion writes out-of-band, so coalesced responses buffered
3165 * from earlier requests in this turn must drain first to preserve
3166 * response order -- do that before the ack, because once the completion
3167 * may run it owns the fd/conn. On flush failure the connection is dead,
3168 * but req/res ownership already moved to the cwist_async, so closing
3169 * here would leave the completion holding a dangling conn/fd (UAF, and
3170 * the response bytes could land on an unrelated fd reuse). Hand the
3171 * teardown to the completion instead: ack, then abort it; the abort
3172 * response send fails on the dead connection and the completion's
3173 * send-response path closes the conn, which owns close in all outcomes. */
3174 cwist_async *deferred = cwist_async_dispatch_take(res);
3175 if (deferred) {
3177 bool broken = aconn && aconn->olen > 0 &&
3179 cwist_async_dispatch_ack(deferred);
3181 return APP_SERVE_DEFERRED;
3182 }
3183
3184 /* WebSocket async handoff (issue #181): the 101 was sent and the fd was
3185 * handed to the reactor-driven WS path inside internal_route_handler.
3186 * The HTTP layer must neither close nor re-arm it. */
3187 if (req->ws_async_handoff) {
3190 return APP_SERVE_DETACH;
3191 }
3192
3193 if (app->bdr_ctx && !endpoint_fixed) {
3195 duration_ms = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_nsec - start.tv_nsec) / 1000000;
3196 }
3197
3198 bool keep_alive = req->keep_alive && res->keep_alive;
3199 bool upgraded = req->upgraded;
3200
3201 /* During shutdown, answer in flight but mark the connection as closing
3202 * so the client does not race another request onto it. */
3204 res->keep_alive = false;
3205 keep_alive = false;
3206 }
3207
3208 if (!upgraded) {
3209 if (req->async_conn) {
3211 client_fd, res, req->async_conn, keep_alive, req->method == CWIST_HTTP_HEAD);
3216 return APP_SERVE_CLOSE;
3217 }
3218
3219 /* RFC 9110 section 9.3.2: HEAD replies carry the GET headers (Content-Length
3220 * included) but no body bytes, for every route. */
3222 ? cwist_http_send_response_head(client_fd, res)
3223 : cwist_http_send_response(client_fd, res);
3224 if (send_err.error.err_i16 < 0) {
3227 return APP_SERVE_CLOSE;
3228 }
3229
3230 // --- Big Dumb Reply (Learn) ---
3231 if (app->bdr_ctx) {
3232 bool endpoint_file = cwist_endpoint_has(req->endpoint_opts, CWIST_ENDPOINT_FILE);
3233
3235 if (priority_weight > 50) {
3237 }
3238
3239 /* A response that declares Vary differs by request headers the
3240 * path-keyed cache cannot see (e.g. fragment vs full page), so
3241 * replaying it for another request would be wrong. */
3242 bool varies = cwist_http_header_get(res->headers, "Vary") != NULL;
3243 if (req->method == CWIST_HTTP_GET && !endpoint_file && !varies) {
3244 if (endpoint_fixed) {
3246 if (serialized) {
3247 cwist_bdr_put_fixed(app->bdr_ctx, "GET", req->path->data, serialized->data,
3248 serialized->size);
3250 }
3251 } else if (duration_ms > scaled_threshold) {
3253 if (serialized) {
3254 cwist_bdr_put(app->bdr_ctx, "GET", req->path->data, serialized->data,
3255 serialized->size);
3257 }
3258 }
3259 }
3260 }
3261 // ------------------------------
3262 }
3263
3266
3267 return (keep_alive && !upgraded) ? APP_SERVE_KEEPALIVE : APP_SERVE_CLOSE;
3268}
3269#endif /* __wasi__ */
3270
3276#define CWIST_HTTP_REQUESTS_PER_TURN_DEFAULT 16
3277#define CWIST_HTTP_REQUESTS_PER_TURN_MAX 1024
3278
3279/* Requests served per reactor callback before yielding to other connections.
3280 * CWIST_HTTP_BATCH overrides the default; values are clamped to [1, 1024]. */
3281static unsigned int app_http_requests_per_turn(void) {
3282 static _Atomic int cached = -1;
3284 if (v < 0) {
3285 const char *env = getenv("CWIST_HTTP_BATCH");
3286 long parsed = env ? strtol(env, NULL, 10) : 0;
3289 v = (int)parsed;
3291 }
3292 return (unsigned int)v;
3293}
3294
3295/* Yield granularity inside one batch: after this many served requests, if
3296 * pipelined bytes remain buffered the loop exits early and the continuation
3297 * path (cwist_http_async_rearm, http.c) reposts the connection so other
3298 * ready connections run first. Defaults to the full batch (legacy behavior);
3299 * CWIST_HTTP_YIELD_BATCH overrides, clamped to [1, 1024]. */
3300static unsigned int app_http_yield_batch(void) {
3301 static _Atomic int cached = -1;
3303 if (v < 0) {
3304 const char *env = getenv("CWIST_HTTP_YIELD_BATCH");
3305 long parsed = env ? strtol(env, NULL, 10) : 0;
3306 if (parsed < 1) parsed = app_http_requests_per_turn();
3308 v = (int)parsed;
3310 }
3311 return (unsigned int)v;
3312}
3313
3314#ifndef CWIST_PLATFORM_NO_SOCKETS
3315/* h2c connection handoff: the blocking HTTP/2 server runs on a detached
3316 * thread per connection so an abandoned connection's idle poll never pins
3317 * a reactor loop. Owns the fd, the sniffed-byte replay, and the app. */
3318#define CWIST_H2C_THREAD_STACK_SIZE (256 * 1024)
3319
3320typedef struct {
3321 int fd;
3322 char *replay;
3323 size_t replay_len;
3324 cwist_app *app;
3325} h2c_handoff_t;
3326
3327static void *h2c_handoff_main(void *arg) {
3328 h2c_handoff_t *h = (h2c_handoff_t *)arg;
3329 cwist_https_connection h2c = {.fd = h->fd,
3330 .ssl = NULL,
3331 .read_buf = h->replay,
3332 .buf_len = h->replay_len,
3333 .negotiated_http2 = true,
3334 .negotiated_protocol = CWIST_HTTPS_PROTOCOL_HTTP2};
3335 cwist_http2_serve_connection_ex(&h2c, h->app, static_http2_route_bridge,
3337 close(h->fd);
3338 cwist_free(h->replay);
3339 cwist_free(h);
3340 return NULL;
3341}
3342
3344 cwist_app *app = (cwist_app *)conn->user_ctx;
3345 static _Atomic long dbg_fill_fail, dbg_fatal, dbg_serve_close;
3346 /* getenv walks the whole environ vector; cache it instead of paying a
3347 * scan on every event. The racy recompute is benign (same result). */
3348 static _Atomic int dbg_cached = -1;
3350 if (d < 0) {
3351 d = getenv("CWIST_ASYNC_DEBUG") != NULL;
3353 }
3354 const bool dbg = (d != 0);
3355
3356 if (cwist_http_async_conn_fill(conn) != 0) {
3357 if (dbg) {
3358 long n = atomic_fetch_add(&dbg_fill_fail, 1) + 1;
3359 if (n <= 5 || n % 10000 == 0)
3361 "[async] fill-fail fd=%d total=%ld fatal=%ld serve=%ld errno=%d len=%zu\n",
3363 conn->len);
3364 }
3365 return CWIST_ASYNC_CLOSE;
3366 }
3367
3368 /* h2c preface: hand the whole connection to the blocking HTTP/2 server on
3369 * a dedicated thread. The h2 serve loop waits in poll() for up to the
3370 * idle budget (300 s default) on a connection the peer has abandoned --
3371 * running it inline here would pin this reactor for that long, and every
3372 * other connection on the loop would stall behind it (surfaced as h2spec
3373 * error-response timeouts cascading once a loop was pinned). The thread
3374 * owns fd and replay; the reactor never touches either again (DETACH). */
3375 if (app->use_http2 && conn->len >= 24 &&
3376 memcmp(conn->rbuf, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", 24) == 0) {
3377 char *replay = cwist_alloc(conn->len);
3378 h2c_handoff_t *handoff = replay ? cwist_alloc(sizeof(*handoff)) : NULL;
3379 if (handoff) {
3380 pthread_t tid;
3384 /* The h2 serve loop's stack need is shallow; cap the reservation
3385 * like the classic pool does so a burst of h2c connections cannot
3386 * reserve 8 MiB apiece. */
3389 int rc;
3390 memcpy(replay, conn->rbuf, conn->len);
3391 handoff->fd = client_fd;
3392 handoff->replay = replay;
3393 handoff->replay_len = conn->len;
3394 handoff->app = app;
3395 rc = pthread_create(&tid, &attr, h2c_handoff_main, handoff);
3397 if (rc == 0) return CWIST_ASYNC_DETACH;
3399 }
3400 /* Thread creation failed: preserve the old inline behavior rather
3401 * than dropping the connection. */
3402 if (replay) {
3403 memcpy(replay, conn->rbuf, conn->len);
3404 cwist_https_connection h2c = {.fd = client_fd,
3405 .ssl = NULL,
3406 .read_buf = replay,
3407 .buf_len = conn->len,
3408 .negotiated_http2 = true,
3409 .negotiated_protocol = CWIST_HTTPS_PROTOCOL_HTTP2};
3410 cwist_http2_serve_connection_ex(&h2c, app, static_http2_route_bridge,
3412 cwist_free(replay);
3413 }
3414 close(client_fd);
3415 return CWIST_ASYNC_DETACH;
3416 }
3417
3418 /* Apply Choi Seok-jeong's Lattice (Sanpan) for priority scaling. */
3422
3423 /* Jeungseung Gaebang Scaling for priority weighting. */
3424 uint32_t priority_weight = ((mixed_seed * 16777619U) >> 8) % 100;
3425
3426 /* One pipeline must not monopolize this reactor's other connections. */
3427 const unsigned int yield_batch = app_http_yield_batch();
3428 for (unsigned int handled = 0; handled < app_http_requests_per_turn(); ++handled) {
3429 cwist_http_request *req = NULL;
3432 if (st == CWIST_RECV_NEED_MORE) {
3433 /* EOF with no complete frame must close, not wait or repost. */
3434 return app_async_flush_exit(client_fd, conn,
3436 !conn->peer_eof);
3437 }
3438 if (st == CWIST_RECV_FATAL) {
3439 app_maybe_send_parse_error_async(conn, perr);
3440 if (dbg) {
3441 long n = atomic_fetch_add(&dbg_fatal, 1) + 1;
3442 if (n <= 5 || n % 10000 == 0)
3443 fprintf(stderr, "[async] recv-fatal fd=%d total=%ld len=%zu\n", client_fd, n,
3444 conn->len);
3445 }
3446 return app_async_flush_exit(client_fd, conn, CWIST_ASYNC_CLOSE, false);
3447 }
3448 req->client_fd = client_fd;
3449 req->app = app;
3450 req->db = app->db;
3451 req->async_conn = conn;
3452 app_serve_result_t sr = app_serve_parsed_request(app, client_fd, req, priority_weight);
3453 /* Deferred: buffered coalesced responses from this turn already
3454 * drained inside app_serve_parsed_request, before the defer was
3455 * acked; the completion path owns the fd/conn now (it re-arms or
3456 * closes, including the flush-failure abort). Nothing to do here. */
3457 if (sr == APP_SERVE_DEFERRED) {
3458 return CWIST_ASYNC_DEFER;
3459 }
3460 if (sr == APP_SERVE_DETACH) {
3461 /* fd ownership moved (WebSocket async upgrade, issue #181); the
3462 * HTTP layer must not close or re-arm it. */
3463 return CWIST_ASYNC_DETACH;
3464 }
3465 if (sr == APP_SERVE_CLOSE) {
3466 if (dbg) {
3467 long n = atomic_fetch_add(&dbg_serve_close, 1) + 1;
3468 if (n <= 5 || n % 10000 == 0)
3469 fprintf(stderr, "[async] serve-close fd=%d total=%ld\n", client_fd, n);
3470 }
3471 return app_async_flush_exit(client_fd, conn, CWIST_ASYNC_CLOSE, false);
3472 }
3473 /* Cooperative yield: buffered pipelined bytes cannot wait for a read
3474 * event, but serving the whole turn inline serializes every other
3475 * connection on this reactor. Exit to the continuation path below,
3476 * which reposts this connection so it resumes after the ready CQEs. */
3477 if ((handled + 1) % yield_batch == 0 && conn->len > 0) break;
3478 }
3479 if (conn->len > 0) {
3480 /* Buffered work cannot wait for another read event. The continuation
3481 * owns fd/conn on success; rearm closes both on failure. A parked
3482 * flush owns them instead and re-arms after draining. */
3486#ifdef CWIST_PFC_TESTING
3487 /* Same park counter as app_async_flush_exit: this continuation
3488 * flush can produce PARKED too (issue #172). The seam has no
3489 * in-tree caller; declare it so an external harness compiling
3490 * this file with -DCWIST_PFC_TESTING does not rely on an
3491 * implicit declaration. */
3492 extern void cwist_pfc_test_parked(void);
3494#endif
3495 return CWIST_ASYNC_DEFER;
3496 }
3497 cwist_http_async_rearm(client_fd, conn->reactor, conn);
3498 return CWIST_ASYNC_DEFER;
3499 }
3500 return app_async_flush_exit(
3501 client_fd, conn, conn->peer_eof ? CWIST_ASYNC_CLOSE : CWIST_ASYNC_REARM, !conn->peer_eof);
3502}
3503#endif /* CWIST_PLATFORM_NO_SOCKETS */
3504
3505#ifndef CWIST_PLATFORM_NO_SOCKETS
3506void cwist_app_http_handler(int client_fd, void *ctx) {
3507 cwist_app *app = (cwist_app *)ctx;
3508
3509 if (app->use_http2) {
3510 char peek_buf[24];
3511 ssize_t peeked = recv(client_fd, peek_buf, 24, MSG_PEEK);
3512 if (peeked >= 24 && memcmp(peek_buf, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", 24) == 0) {
3513 cwist_https_connection conn = {.fd = client_fd,
3514 .ssl = NULL,
3515 .negotiated_http2 = true,
3516 .negotiated_protocol = CWIST_HTTPS_PROTOCOL_HTTP2};
3517 cwist_http2_serve_connection_ex(&conn, app, static_http2_route_bridge,
3519 close(client_fd);
3520 return;
3521 }
3522 }
3523
3524 /* Apply Choi Seok-jeong's Lattice (Sanpan) for priority scaling. */
3528
3529 /* Jeungseung Gaebang Scaling for priority weighting. */
3530 uint32_t priority_weight = ((mixed_seed * 16777619U) >> 8) % 100;
3531
3532 /* Use stack-allocated buffer for zero-allocation ingress path.
3533 * Aligned to cache line to optimize lattice-friendly access and prevent buffer loss. */
3534 _Alignas(64) char read_buf[CWIST_HTTP_READ_BUFFER_SIZE];
3535 size_t buf_len = 0;
3536 read_buf[0] = '\0';
3537
3538 while (true) {
3539 // --- Zero-Alloc Ingress Fast-Path for Cached / Fixed BDR Endpoints ---
3540 if (app->bdr_ctx) {
3541 while (true) {
3542 if (buf_len == 0) {
3543 /* Keep-alive wait for the next request: a shutting-down
3544 * classic pool closes idle connections (http_live.h). */
3546 close(client_fd);
3547 return;
3548 }
3549 ssize_t bytes = recv(client_fd, read_buf, sizeof(read_buf) - 1, 0);
3551 if (bytes <= 0) {
3552 if (bytes < 0 && errno == EINTR) continue;
3553 close(client_fd);
3554 return;
3555 }
3556 buf_len = (size_t)bytes;
3557 read_buf[buf_len] = '\0';
3558 }
3559
3560 char *hdr_end = (char *)cwist_simd_find_crlfcrlf(read_buf, buf_len);
3561 if (hdr_end && (read_buf[0] == 'G' && read_buf[1] == 'E' && read_buf[2] == 'T' &&
3562 read_buf[3] == ' ')) {
3563 const char *path_start = read_buf + 4;
3564 const char *path_end =
3565 (const char *)memchr(path_start, ' ', (size_t)(hdr_end - path_start));
3566 if (path_end) {
3567 char path_tmp[256];
3568 size_t plen = (size_t)(path_end - path_start);
3569 if (plen < sizeof(path_tmp)) {
3571 path_tmp[plen] = '\0';
3572 size_t cached_len = 0;
3573 const void *cached_blob =
3574 cwist_bdr_get(app->bdr_ctx, "GET", path_tmp, &cached_len);
3575 if (cached_blob && cached_len > 0) {
3576 ssize_t sret =
3577 send(client_fd, cached_blob, cached_len, MSG_NOSIGNAL);
3578 if (sret <= 0) {
3579 close(client_fd);
3580 return;
3581 }
3582 size_t consumed = (size_t)(hdr_end + 4 - read_buf);
3583 if (buf_len > consumed) {
3584 memmove(read_buf, read_buf + consumed, buf_len - consumed);
3585 buf_len -= consumed;
3586 read_buf[buf_len] = '\0';
3587 } else {
3588 buf_len = 0;
3589 read_buf[0] = '\0';
3590 }
3591 continue;
3592 }
3593 }
3594 }
3595 }
3596 break;
3597 }
3598 }
3599
3601 cwist_http_request *req =
3602 cwist_http_receive_request(client_fd, read_buf, sizeof(read_buf), &buf_len, &perr);
3603 if (!req) {
3604 app_maybe_send_parse_error(client_fd, perr);
3605 break;
3606 }
3607 req->client_fd = client_fd;
3608 req->app = app;
3609 req->db = app->db;
3610
3611 app_serve_result_t sr = app_serve_parsed_request(app, client_fd, req, priority_weight);
3612 if (sr == APP_SERVE_DEFERRED) {
3613 /* The async completion owns the fd now; it re-arms on the pool
3614 * (keep-alive) or closes. Do not close here. */
3615 return;
3616 }
3617 if (sr == APP_SERVE_CLOSE) {
3618 break;
3619 }
3620 }
3621
3622 close(client_fd);
3623}
3624#endif /* CWIST_PLATFORM_NO_SOCKETS (socket serving machinery) */
3625#endif /* __EMSCRIPTEN__ (socket serving machinery) */
3626
3627#if defined(CWIST_PLATFORM_HOSTED)
3628/* Multiport listeners are socket/UDP machinery; WASM hosts never bind. */
3629#ifndef CWIST_MULTIPORT_MAX_PORTS
3630#define CWIST_MULTIPORT_MAX_PORTS 64
3631#endif
3632
3633typedef struct cwist_multiport_slot {
3634 unsigned short port;
3635 cwist_app *app;
3636 bool detached;
3637 struct cwist_multiport_slot *next;
3639
3640typedef struct cwist_multiport_h3_listener {
3641 unsigned short port;
3642 int udp_fd;
3643 pthread_t thread;
3644 bool thread_started;
3645 bool running;
3646 cwist_app *app;
3649
3650typedef struct cwist_multiport_group {
3651 cwist_app *root;
3652 unsigned short public_port;
3653 cwist_multiport_slot *slots;
3655 size_t h3_listener_count;
3656 struct cwist_multiport_group *next;
3658
3660
3667cwist_multiport_t cwist_create_multiport_from_array(const unsigned short *ports, size_t count) {
3668 cwist_multiport_t result;
3669 memset(&result, 0, sizeof(result));
3670 result.valid = false;
3671
3673 return result;
3674 }
3675 if (!ports && count > 0) {
3676 return result;
3677 }
3678
3679 for (size_t i = 0; i < count; i++) {
3680 if (ports[i] == 0) {
3681 break;
3682 }
3683 result.ports[result.count++] = ports[i];
3684 }
3685
3686 result.valid = true;
3687 return result;
3688}
3689
3697 while (group) {
3698 if (group->root == root) return group;
3699 group = group->next;
3700 }
3701 return NULL;
3702}
3703
3710 if (!root) return NULL;
3712 if (group) return group;
3713
3715 if (!group) return NULL;
3716 memset(group, 0, sizeof(*group));
3717 group->root = root;
3720 return group;
3721}
3722
3730 unsigned short port) {
3731 if (!group) return NULL;
3733 while (slot) {
3734 if (slot->port == port) return slot;
3735 slot = slot->next;
3736 }
3737 return NULL;
3738}
3739
3744static void cwist_multiport_unlink_app(cwist_app *app) {
3745 if (!app) return;
3747 while (group) {
3748 cwist_multiport_slot **link = &group->slots;
3749 while (*link) {
3751 if (slot->app == app) {
3752 *link = slot->next;
3754 continue;
3755 }
3756 link = &slot->next;
3757 }
3758 group = group->next;
3759 }
3760}
3761
3767 if (!group) return;
3768 for (size_t i = 0; i < group->h3_listener_count; i++) {
3769 cwist_multiport_h3_listener *listener = &group->h3_listeners[i];
3770 if (listener->ctx) {
3771 __atomic_store_n(&listener->ctx->running, 0, __ATOMIC_RELEASE);
3772 }
3773 /* Join before close: the listener thread may be inside recvmmsg() on
3774 * udp_fd, and closing under it races with the read. The loop notices
3775 * ctx->running within its poll timeout and exits on its own. */
3776 if (listener->thread_started) {
3777 pthread_join(listener->thread, NULL);
3778 listener->thread_started = false;
3779 listener->running = false;
3780 }
3781 if (listener->udp_fd >= 0) {
3782 close(listener->udp_fd);
3783 listener->udp_fd = -1;
3784 }
3785#ifdef CWIST_HTTP3
3786 if (listener->ctx) {
3788 listener->ctx = NULL;
3789 }
3790#endif
3791 }
3792 group->h3_listener_count = 0;
3793}
3794
3799static void cwist_multiport_destroy_owned_subapps(cwist_app *root) {
3800 if (!root) return;
3802 while (*link) {
3804 if (group->root != root) {
3805 link = &group->next;
3806 continue;
3807 }
3808
3810
3812 while (slot) {
3813 cwist_multiport_slot *next = slot->next;
3814 if (slot->detached && slot->app && slot->app != root) {
3815 cwist_app *sub_app = slot->app;
3816 slot->app = NULL;
3818 }
3820 slot = next;
3821 }
3822
3823 *link = group->next;
3825 return;
3826 }
3827}
3828
3834/* A cloned route shares the source's user context but never owns it: the
3835 * source app outlives its multiport sub-apps and releases the context once. */
3836static cwist_route_target cwist_route_clone_target(const cwist_route_entry *entry) {
3837 cwist_route_target target = {.handler = entry->handler,
3838 .handler_ex = entry->handler_ex,
3839 .user_ctx = entry->user_ctx,
3840 .ctx_destroy = NULL,
3841 .ws_handler = entry->ws_handler};
3842 return target;
3843}
3844
3845static bool cwist_route_is_http(const cwist_route_entry *entry) {
3846 return !entry->ws_handler && !entry->ws_async_on_message;
3847}
3848
3850 if (!app || !app->router || !visit) return;
3852 for (size_t i = 0; i < table->bucket_count; i++) {
3853 for (cwist_route_entry *entry = table->buckets[i]; entry; entry = entry->next) {
3854 if (cwist_route_is_http(entry)) visit(ctx, entry->method, entry->path);
3855 }
3856 }
3857 for (cwist_route_entry *entry = table->param_routes; entry; entry = entry->next) {
3858 if (cwist_route_is_http(entry)) visit(ctx, entry->method, entry->path);
3859 }
3860}
3861
3862static cwist_route_table *cwist_route_table_clone(cwist_route_table *src) {
3863 if (!src) return cwist_route_table_create();
3864 cwist_route_table *dst = cwist_route_table_create();
3865 if (!dst) return NULL;
3866
3867 for (size_t i = 0; i < src->bucket_count; i++) {
3868 for (cwist_route_entry *entry = src->buckets[i]; entry; entry = entry->next) {
3869 cwist_route_target target = cwist_route_clone_target(entry);
3870 cwist_route_table_insert(dst, entry->path, entry->name, entry->method, &target,
3871 entry->opts);
3872 }
3873 }
3874 for (cwist_route_entry *entry = src->param_routes; entry; entry = entry->next) {
3875 cwist_route_target target = cwist_route_clone_target(entry);
3876 cwist_route_table_insert(dst, entry->path, entry->name, entry->method, &target,
3877 entry->opts);
3878 }
3879 return dst;
3880}
3881
3889 cwist_middleware_node **tail = &head;
3890 while (src) {
3892 if (!node) break;
3893 node->func = src->func;
3894 /* Like a cloned route, a cloned extended middleware shares the source's
3895 * context without owning it: the source app releases it once. */
3896 node->func_ex = src->func_ex;
3897 node->user_ctx = src->user_ctx;
3898 node->destroy = NULL;
3899 node->is_ex = src->is_ex;
3900 node->next = NULL;
3901 *tail = node;
3902 tail = &node->next;
3903 src = src->next;
3904 }
3905 return head;
3906}
3907
3916 while (src) {
3918 if (!entry) break;
3919 entry->status_code = src->status_code;
3920 entry->handler = src->handler;
3921 entry->next = NULL;
3922 *tail = entry;
3923 tail = &entry->next;
3924 src = src->next;
3925 }
3926 return head;
3927}
3928
3934static cwist_static_dir *cwist_static_dirs_clone(cwist_static_dir *src) {
3936 cwist_static_dir **tail = &head;
3937 while (src) {
3938 cwist_static_dir *entry = (cwist_static_dir *)cwist_alloc(sizeof(*entry));
3939 if (!entry) break;
3940 entry->url_prefix = cwist_strdup(src->url_prefix);
3941 entry->fs_root = cwist_strdup(src->fs_root);
3942 entry->cache_control = src->cache_control ? cwist_strdup(src->cache_control) : NULL;
3943 entry->next = NULL;
3944 if (!entry->url_prefix || !entry->fs_root ||
3945 (src->cache_control && !entry->cache_control)) {
3946 cwist_free(entry->url_prefix);
3947 cwist_free(entry->fs_root);
3948 cwist_free(entry->cache_control);
3949 cwist_free(entry);
3950 break;
3951 }
3952 *tail = entry;
3953 tail = &entry->next;
3954 src = src->next;
3955 }
3956 return head;
3957}
3958
3965 if (!src) return NULL;
3967 if (!dst) return NULL;
3968
3970 if (!router) {
3972 return NULL;
3973 }
3974 cwist_route_table_destroy(dst->router);
3975 dst->router = router;
3976
3977 dst->port = src->port;
3978 dst->use_http2 = src->use_http2;
3979 dst->use_https2 = src->use_https2;
3980 dst->use_http3 = src->use_http3;
3981 dst->use_https3 = src->use_https3;
3982 dst->error_handler = src->error_handler;
3983 dst->max_mem_space = src->max_mem_space;
3984 dst->pqc_layer_enabled = src->pqc_layer_enabled;
3985 if (src->tls_groups) {
3986 dst->tls_groups = cwist_strdup(src->tls_groups);
3987 }
3988 dst->wt_handler = src->wt_handler;
3989
3990 dst->middlewares = cwist_middleware_clone(src->middlewares);
3991 dst->error_handlers = cwist_error_handlers_clone(src->error_handlers);
3992 dst->static_dirs = cwist_static_dirs_clone(src->static_dirs);
3993 if (cwist_assets_clone(&dst->assets, src->assets) != 0) {
3995 return NULL;
3996 }
3997 if (cwist_grpc_routes_clone(dst, src) != 0) {
3999 return NULL;
4000 }
4001
4002 if (src->use_ssl && src->cert_path && src->key_path) {
4003 dst->use_https2 = src->use_https2;
4004 dst->use_https3 = src->use_https3;
4006 }
4007 if (src->use_http3) {
4008 cwist_app_use_http3(dst, true);
4009 }
4010 if (src->h3_ctx && dst->h3_ctx) {
4011 cwist_multiport_h3_copy_tunables(dst->h3_ctx, src->h3_ctx);
4012 }
4013 if (src->db_path && !src->nuke_enabled) {
4015 }
4017 return dst;
4018}
4019
4026cwist_app *cwist_multiport_get_app(cwist_app **app_ref, unsigned short port) {
4028 if (!root || port == 0 || port == (unsigned short)root->port) return NULL;
4029
4031 if (!group) return NULL;
4032
4034 if (slot) {
4035 return slot->detached ? slot->app : NULL;
4036 }
4037
4039 if (!sub_app) return NULL;
4040 sub_app->port = port;
4041
4043 if (!slot) {
4045 return NULL;
4046 }
4047 slot->port = port;
4048 slot->app = sub_app;
4049 slot->detached = true;
4050 slot->next = group->slots;
4051 group->slots = slot;
4052 return sub_app;
4053}
4054
4055typedef struct cwist_multiport_http_client {
4056 int client_fd;
4057 cwist_app *app;
4059
4060static void *cwist_multiport_http_client_thread(void *arg) {
4062 if (!client) return NULL;
4063 cwist_app_http_handler(client->client_fd, client->app);
4064 free(client);
4065 return NULL;
4066}
4067
4068static int cwist_multiport_add_port(unsigned short *ports, size_t *count, unsigned short port) {
4069 if (!ports || !count || port == 0) return -1;
4070 for (size_t i = 0; i < *count; i++) {
4071 if (ports[i] == port) return 0;
4072 }
4073 if (*count >= CWIST_MULTIPORT_MAX_PORTS) return -1;
4074 ports[*count] = port;
4075 (*count)++;
4076 return 0;
4077}
4078
4079static int cwist_multiport_set_nonblocking(int fd) {
4080 int flags = fcntl(fd, F_GETFL, 0);
4081 if (flags < 0) return -1;
4082 return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
4083}
4084
4085static void cwist_multiport_close_all(struct pollfd *pfds, size_t count) {
4086 if (!pfds) return;
4087 for (size_t i = 0; i < count; i++) {
4088 if (pfds[i].fd >= 0) {
4089 close(pfds[i].fd);
4090 pfds[i].fd = -1;
4091 }
4092 }
4093}
4094
4095static int cwist_multiport_start_clear_client(int client_fd, cwist_app *app) {
4096 cwist_multiport_http_client *payload = malloc(sizeof(*payload));
4097 if (!payload) {
4098 close(client_fd);
4099 return -1;
4100 }
4101
4102 payload->client_fd = client_fd;
4103 payload->app = app;
4104
4105 pthread_t tid;
4107 close(client_fd);
4108 free(payload);
4109 return -1;
4110 }
4112 return 0;
4113}
4114
4121static bool cwist_multiport_contains(cwist_multiport_t ports, unsigned short port) {
4122 for (size_t i = 0; i < ports.count; i++) {
4123 if (ports.ports[i] == port) return true;
4124 }
4125 return false;
4126}
4127
4136 unsigned short port) {
4138 if (slot && slot->detached && slot->app) {
4139 return slot->app;
4140 }
4141 return root;
4142}
4143
4149static void cwist_multiport_h3_copy_tunables(cwist_http3_context *dst,
4150 const cwist_http3_context *src) {
4151 if (!dst || !src) return;
4152 dst->push_enabled = src->push_enabled;
4153 dst->early_data_enabled = src->early_data_enabled;
4154 dst->allow_migration = src->allow_migration;
4155 dst->datagram_enabled = src->datagram_enabled;
4156 dst->datagram_cb = src->datagram_cb;
4157 dst->datagram_user_ctx = src->datagram_user_ctx;
4158 dst->wt_handler = src->wt_handler;
4159 dst->idle_timeout_ms = src->idle_timeout_ms;
4160 dst->handshake_timeout_ms = src->handshake_timeout_ms;
4161 dst->ping_period_ms = src->ping_period_ms;
4162 dst->noprogress_timeout_ms = src->noprogress_timeout_ms;
4163}
4164
4165#ifdef CWIST_HTTP3
4174 if (!app || !out) {
4175 err.error.err_i16 = -1;
4176 return err;
4177 }
4178 *out = NULL;
4179
4180 if (app->use_https3) {
4181 if (!app->cert_path || !app->key_path) {
4182 err.error.err_i16 = -1;
4183 return err;
4184 }
4186 } else if (app->use_http3) {
4188 } else {
4189 err.error.err_i16 = 0;
4190 return err;
4191 }
4192
4193 if (err.error.err_i16 == 0 && app->h3_ctx && *out) {
4194 cwist_multiport_h3_copy_tunables(*out, app->h3_ctx);
4195 }
4196 return err;
4197}
4198
4204static int cwist_multiport_bind_udp(unsigned short port) {
4205 int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
4206 if (udp_fd < 0) return -1;
4207
4208 struct sockaddr_in udp_addr;
4209 memset(&udp_addr, 0, sizeof(udp_addr));
4210 udp_addr.sin_family = AF_INET;
4211 udp_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
4212 udp_addr.sin_port = htons(port);
4213
4214 int opt = 1;
4215 setsockopt(udp_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
4216#ifdef SO_REUSEPORT
4218 setsockopt(udp_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
4219 }
4220#endif
4221 int rcvbuf = 2 * 1024 * 1024;
4222 int sndbuf = 2 * 1024 * 1024;
4223 setsockopt(udp_fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
4224 setsockopt(udp_fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
4225
4226 if (bind(udp_fd, (struct sockaddr *)&udp_addr, sizeof(udp_addr)) != 0) {
4227 close(udp_fd);
4228 return -1;
4229 }
4230 return udp_fd;
4231}
4232
4238static void *cwist_multiport_h3_thread(void *arg) {
4240 if (!listener || !listener->ctx || !listener->app) return NULL;
4242 listener->app);
4243 listener->running = false;
4244 return NULL;
4245}
4246#endif /* CWIST_HTTP3 (multiport fan-out context, UDP bind, listener thread) */
4247
4256 unsigned short port) {
4257#ifdef CWIST_HTTP3
4258 if (!group || !app || (!app->use_http3 && !app->use_https3)) return 0;
4259 if (group->h3_listener_count >= CWIST_MULTIPORT_MAX_PORTS) return -1;
4260
4261 cwist_multiport_h3_listener *listener = &group->h3_listeners[group->h3_listener_count];
4262 memset(listener, 0, sizeof(*listener));
4263 listener->port = port;
4264 listener->udp_fd = -1;
4265 listener->app = app;
4266
4268 if (err.error.err_i16 < 0 || !listener->ctx) {
4269 return -1;
4270 }
4271
4272 listener->udp_fd = cwist_multiport_bind_udp(port);
4273 if (listener->udp_fd < 0) {
4275 listener->ctx = NULL;
4276 return -1;
4277 }
4278
4280 close(listener->udp_fd);
4281 listener->udp_fd = -1;
4283 listener->ctx = NULL;
4284 return -1;
4285 }
4286
4287 listener->thread_started = true;
4288 listener->running = true;
4289 group->h3_listener_count++;
4292 }
4293 printf("CWIST multiport HTTP/3 fan-out listening on UDP port %hu\n", port);
4294#else
4295 /* HTTP/3 compiled out (CWIST_HTTP3=0): no fan-out listeners to start. */
4296 (void)group;
4297 (void)app;
4298 (void)port;
4299#endif
4300 return 0;
4301}
4302
4312 const unsigned short *bind_ports, size_t port_count) {
4313 if (!group || !root || !bind_ports) return -1;
4315 for (size_t i = 0; i < port_count; i++) {
4319 return -1;
4320 }
4321 }
4322 return 0;
4323}
4324
4331static int cwist_multiport_validate_port_app(cwist_app *app, unsigned short port) {
4332 if (!app) return -1;
4333 if (app->use_ssl && app->use_http2) {
4334 fprintf(stderr, "Port %hu invalid: cleartext HTTP/2 cannot share a TLS listener.\n", port);
4335 return -1;
4336 }
4337 if (!app->use_ssl && app->use_https2) {
4338 fprintf(stderr, "Port %hu invalid: HTTPS/2 requires cwist_app_use_https.\n", port);
4339 return -1;
4340 }
4341 if (app->use_ssl && !app->ssl_ctx) {
4342 fprintf(stderr, "Port %hu invalid: SSL enabled but context not initialized.\n", port);
4343 return -1;
4344 }
4345 if (app->use_http3 && app->use_https3) {
4347 "Port %hu invalid: ephemeral HTTP/3 and TLS HTTP/3 cannot both be enabled.\n",
4348 port);
4349 return -1;
4350 }
4351 if (app->use_https3 && (!app->cert_path || !app->key_path)) {
4352 fprintf(stderr, "Port %hu invalid: HTTPS/3 requires certificate and key paths.\n", port);
4353 return -1;
4354 }
4355 return 0;
4356}
4357
4367 const unsigned short *bind_ports, size_t port_count) {
4368 for (size_t i = 0; i < port_count; i++) {
4370 if (port_app && port_app->use_ssl) return true;
4371 }
4372 return false;
4373}
4374
4384 cwist_app_tune_system();
4385
4386 cwist_app *app = app_ref ? *app_ref : NULL;
4387 if (!app || public_port == 0 || !ports.valid) return -1;
4388 app->port = public_port;
4389
4391 if (!group) return -1;
4392 group->public_port = public_port;
4393
4394 for (cwist_multiport_slot *slot = group->slots; slot; slot = slot->next) {
4395 if (slot->detached && slot->port == public_port) {
4396 fprintf(stderr, "cwist_multiport_get_app cannot detach the public/default port %hu.\n",
4397 public_port);
4398 return -1;
4399 }
4400 if (slot->detached && !cwist_multiport_contains(ports, slot->port)) {
4401 fprintf(
4402 stderr,
4403 "Detached multiport sub-app port %hu is not present in the multiport descriptor.\n",
4404 slot->port);
4405 return -1;
4406 }
4407 }
4408
4409 if (app->use_ssl && app->use_http2) {
4411 "Assertion failed: Cannot use cleartext HTTP/2 and HTTPS on the same port.\n");
4412 return -1;
4413 }
4414 if (!app->use_ssl && app->use_https2) {
4415 fprintf(
4416 stderr,
4417 "Assertion failed: Cannot use HTTPS/2 without configuring SSL via cwist_app_use_https.\n");
4418 return -1;
4419 }
4420 if (app->use_ssl && !app->ssl_ctx) {
4421 fprintf(stderr, "SSL enabled but context not initialized.\n");
4422 return -1;
4423 }
4424
4425 if (!app->mem_manager) {
4426 cwist_mem_init(app);
4427 }
4428 if (app->mem_manager &&
4432 }
4433
4434 unsigned short bind_ports[CWIST_MULTIPORT_MAX_PORTS];
4435 size_t port_count = 0;
4437 return -1;
4438 }
4439 for (size_t i = 0; i < ports.count; i++) {
4441 fprintf(stderr, "cwist_app_multiport supports up to %d total ports.\n",
4443 return -1;
4444 }
4445 }
4446
4447 for (size_t i = 0; i < port_count; i++) {
4450 return -1;
4451 }
4452 }
4454
4456 for (size_t i = 0; i < port_count; i++) {
4457 pfds[i].fd = -1;
4458 pfds[i].events = POLLIN;
4459 pfds[i].revents = 0;
4460 }
4461
4462 for (size_t i = 0; i < port_count; i++) {
4463 struct sockaddr_in addr;
4464 int fd = cwist_make_socket_ipv4(&addr, "0.0.0.0", bind_ports[i], 32768);
4465 if (fd < 0) {
4466 perror("Failed to bind multiport listener");
4468 return -1;
4469 }
4470 if (cwist_multiport_set_nonblocking(fd) != 0) {
4471 perror("Failed to set multiport listener non-blocking");
4472 close(fd);
4474 return -1;
4475 }
4476 pfds[i].fd = fd;
4477 printf("CWIST multiport facade listening on TCP port %hu\n", bind_ports[i]);
4478 }
4479
4481 fprintf(stderr, "Failed to initialize multiport HTTP/3 fan-out.\n");
4483 return -1;
4484 }
4485
4486 if (needs_https_pool && https_pool_init() != 0) {
4487 fprintf(stderr, "Failed to initialize HTTPS worker pool.\n");
4490 return -1;
4491 }
4492
4494 printf("CWIST App running on %zu TCP ports via multiport facade (SSL: %s)\n", port_count,
4495 app->use_ssl ? "On" : "Off");
4496
4497 while (atomic_load(&g_cwist_running)) {
4498 int ready = poll(pfds, (nfds_t)port_count, 1000);
4499 if (ready < 0) {
4500 if (errno == EINTR) continue;
4501 perror("cwist_app_multiport poll");
4502 break;
4503 }
4504 if (ready == 0) continue;
4505
4506 for (size_t i = 0; i < port_count; i++) {
4507 if (!(pfds[i].revents & POLLIN)) continue;
4508
4509 while (atomic_load(&g_cwist_running)) {
4510 struct sockaddr_in peer;
4511 socklen_t peer_len = sizeof(peer);
4512 int client_fd = accept(pfds[i].fd, (struct sockaddr *)&peer, &peer_len);
4513 if (client_fd < 0) {
4514 int accept_err = errno;
4516 break;
4517 if (accept_err == EBADF || accept_err == EINVAL || accept_err == ENOTSOCK) {
4519 break;
4520 }
4521 continue;
4522 }
4523
4524 int nodelay = 1;
4525 setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
4526
4528 if (port_app->use_ssl) {
4529 https_pool_submit(client_fd, port_app->ssl_ctx, static_ssl_handler, port_app);
4530 } else {
4532 }
4533 }
4534 }
4535 }
4536
4537 if (needs_https_pool) {
4539 }
4544
4545 if (app->mem_manager) {
4547 }
4548
4549 printf("[CWIST] Multiport facade shutdown complete.\n");
4550 return 0;
4551}
4552#endif /* __EMSCRIPTEN__ (multiport) */
4553
4554#if defined(CWIST_PLATFORM_HOSTED)
4555struct h3_thread_payload {
4556 int udp_fd;
4557 cwist_app *app;
4558};
4559
4560static void *h3_server_thread_func(void *arg) {
4561#ifdef CWIST_HTTP3
4562 struct h3_thread_payload *payload = arg;
4563 cwist_http3_server_loop(payload->udp_fd, payload->app->h3_ctx, static_http3_route_bridge,
4564 payload->app);
4565 free(payload);
4566#else
4567 /* HTTP/3 compiled out (CWIST_HTTP3=0). */
4568 (void)arg;
4569#endif
4570 return NULL;
4571}
4572#endif /* __EMSCRIPTEN__ */
4573
4594 const char *profile = getenv("CWIST_PROFILE");
4595 if (!profile || profile[0] == '\0' || strcmp(profile, "default") == 0) {
4596 setenv("CWIST_C1M_MODE", "1", 0);
4597 setenv("CWIST_REACTOR_DRAIN_CHUNK", "8", 0);
4598 return;
4599 }
4600
4601 if (strcmp(profile, "performance") == 0) {
4602 setenv("CWIST_C1M_MODE", "1", 0);
4603 setenv("CWIST_MALLOC_ARENA_MAX", "1", 0);
4604 setenv("CWIST_REACTOR_DRAIN_CHUNK", "8", 0);
4605 setenv("CWIST_HTTP_BATCH", "64", 0);
4606 printf("[CWIST] profile: performance\n");
4607 } else if (strcmp(profile, "lowmem") == 0) {
4608 setenv("CWIST_C1M_MODE", "1", 0);
4609 setenv("CWIST_MALLOC_ARENA_MAX", "1", 0);
4610 setenv("CWIST_WORKERS", "2", 0);
4611 printf("[CWIST] profile: lowmem\n");
4612 } else if (strcmp(profile, "lowlat") == 0) {
4613 setenv("CWIST_C1M_MODE", "0", 0);
4614 printf("[CWIST] profile: lowlat\n");
4615 } else {
4616 fprintf(stderr, "[CWIST] unknown CWIST_PROFILE value \"%s\"; using defaults\n", profile);
4617 setenv("CWIST_C1M_MODE", "1", 0);
4618 setenv("CWIST_REACTOR_DRAIN_CHUNK", "8", 0);
4619 }
4620}
4621
4628int cwist_app_listen(cwist_app *app, int port) {
4629 return cwist_app_listen_ex(app, port, 0, -1);
4630}
4631
4632#if !defined(__EMSCRIPTEN__) && !defined(CWIST_PLATFORM_NO_SOCKETS)
4633static int app_listen_serve(cwist_app *app, int port, int workers_override, int c1m_override);
4634#endif
4635
4637#if defined(__EMSCRIPTEN__) || defined(CWIST_PLATFORM_NO_SOCKETS)
4638 (void)port;
4641 if (app) app->port = port;
4642 return -1; /* WASM hosts drive requests through cwist_app_dispatch_memory() */
4643#else
4644#ifndef CWIST_PLATFORM_NO_PROC
4645 // Ignore SIGPIPE
4647#endif
4648 /* SIGTERM/SIGINT request a graceful stop only while this server runs:
4649 * the caller's own handlers are back in place when it returns, on every
4650 * path (including in forked workers, which also return here). */
4652 int rc = app_listen_serve(app, port, workers_override, c1m_override);
4654 return rc;
4655#endif
4656}
4657
4658#if !defined(__EMSCRIPTEN__) && !defined(CWIST_PLATFORM_NO_SOCKETS)
4659/* Body of cwist_app_listen_ex() between installing and restoring the
4660 * shutdown signal handlers. */
4661static int app_listen_serve(cwist_app *app, int port, int workers_override, int c1m_override) {
4662 cwist_app_tune_system();
4664 if (!app) return -1;
4665 app->port = port;
4666
4667 // Validate protocol combinations for the same port
4668 if (app->use_ssl) {
4669 if (app->use_http2) {
4671 "Assertion failed: Cannot use cleartext HTTP/2 and HTTPS on the same port.\n");
4672 abort();
4673 }
4674 } else {
4675 if (app->use_https2) {
4676 fprintf(
4677 stderr,
4678 "Assertion failed: Cannot use HTTPS/2 without configuring SSL via cwist_app_use_https.\n");
4679 abort();
4680 }
4681 }
4682
4683 if (app->use_http3 && app->use_https3) {
4684 fprintf(
4685 stderr,
4686 "Assertion failed: Cannot use both ephemeral HTTP/3 and TLS HTTP/3 simultaneously on the same port.\n");
4687 abort();
4688 }
4689
4690 /* Create the shared TCP listen socket before forking workers.
4691 * With SO_REUSEPORT each worker process gets its own accept queue and the
4692 * kernel load-balances incoming connections. Creating it here avoids the
4693 * previous anti-pattern where workers forked before binding and inherited
4694 * threads that do not exist in the child. */
4695 struct sockaddr_in addr;
4696 int server_fd = cwist_make_socket_ipv4(&addr, "0.0.0.0", port, 32768);
4697 if (server_fd < 0) {
4698 perror("Failed to bind port");
4699 return -1;
4700 }
4702
4703 /* Bind the HTTP/3 UDP socket before forking as well. The thread that
4704 * services it is started per-process after the fork. */
4705 int udp_fd = -1;
4706#ifndef CWIST_PLATFORM_NO_PROC
4707 if (app->h3_ctx && (app->use_http3 || app->use_https3)) {
4708 udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
4709 if (udp_fd >= 0) {
4710 struct sockaddr_in udp_addr;
4711 memset(&udp_addr, 0, sizeof(udp_addr));
4712 udp_addr.sin_family = AF_INET;
4713 udp_addr.sin_addr.s_addr = inet_addr("0.0.0.0");
4714 udp_addr.sin_port = htons(port);
4715
4716 int opt = 1;
4717 setsockopt(udp_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
4718#ifdef SO_REUSEPORT
4720 setsockopt(udp_fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
4721 }
4722#endif
4723 int rcvbuf = 2 * 1024 * 1024;
4724 int sndbuf = 2 * 1024 * 1024;
4725 setsockopt(udp_fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
4726 setsockopt(udp_fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
4727
4728 if (bind(udp_fd, (struct sockaddr *)&udp_addr, sizeof(udp_addr)) != 0) {
4729 perror("Failed to bind UDP port for HTTP/3");
4730 close(udp_fd);
4731 udp_fd = -1;
4732 }
4733 }
4734 }
4735#endif /* __wasi__ (no UDP/threads) */
4736
4737 // Fork worker processes before any threads are created.
4738 int workers = 1;
4739 const char *workers_env = getenv("CWIST_WORKERS");
4740 if (workers_override > 0) {
4741 workers = workers_override;
4742 } else if (workers_env) {
4743 if (strcmp(workers_env, "auto") == 0) {
4744 long cores = get_cpu_cores();
4745 workers = (cores > 0) ? (int)cores : 1;
4746 } else {
4747 char *end = NULL;
4748 long v = strtol(workers_env, &end, 10);
4749 workers = (end != workers_env && *end == '\0' && v >= 1 && v <= INT_MAX) ? (int)v : 1;
4750 }
4751 } else {
4752 // Default to auto (number of online CPU cores) to maximize performance on multi-core
4753 // systems out of the box.
4754 long cores = get_cpu_cores();
4755 workers = (cores > 0) ? (int)cores : 1;
4756 }
4757 if (workers < 1) workers = 1;
4758
4759#if defined(__GLIBC__)
4760 /* Experimental (issue #25, ROADMAP.md v3.5): cap glibc's per-process
4761 * arena count before forking, so the setting is inherited by every
4762 * worker. mallopt() state is plain process memory, not something a
4763 * live fork() can "share back" afterward - COW means a child's first
4764 * write to any inherited page (which malloc always does) forks its own
4765 * private copy immediately, so there's no way to keep multiple
4766 * processes pointed at one mutable arena. What *is* achievable is
4767 * capping how many arenas each process can independently accumulate:
4768 * by default glibc lets internal thread contention grow up to
4769 * ncpus*8 arenas *per process* (each worker here forks before
4770 * creating its own watcher/HTTP-3 threads, so every worker can hit
4771 * that ceiling independently) - with N worker processes that's a
4772 * multiplicative blow-up, and each extra arena is its own mmap'd
4773 * region that adds directly to RSS. CWIST_MALLOC_ARENA_MAX=1 forces
4774 * every worker down to its single main arena regardless of how many
4775 * threads it spins up afterward. Unset by default: preserves today's
4776 * behavior exactly. Measured against the mimalloc A/B in that issue
4777 * and adopted (PR #35, merged) after winning on every metric - the CI
4778 * benchmark job keeps confirming that decision on every run, this is
4779 * not an open question anymore, just still opt-in rather than a
4780 * default so existing deployments' behavior never changes silently. */
4781 const char *arena_max_env = getenv("CWIST_MALLOC_ARENA_MAX");
4782 if (arena_max_env && arena_max_env[0]) {
4783 char *end = NULL;
4784 long arena_max = strtol(arena_max_env, &end, 10);
4785 if (end && *end == '\0' && arena_max >= 0 && arena_max <= INT_MAX) {
4787 }
4788 }
4789#endif
4790
4791 /* Prime libttak's TSC calibration before forking workers. calibrate_tsc()
4792 * sleeps for 10 ms (timing.c) and is triggered lazily by the first
4793 * ttak_get_tick_count_ns() call - which used to arrive with the first
4794 * request's arena creation inside each worker process, adding ~10 ms to
4795 * that worker's first request. The calibrated g_tsc_freq_ghz/g_tsc_scale
4796 * globals are plain process memory, so fork children inherit the values
4797 * copy-on-write and never repeat the sleep. */
4798#ifndef CWIST_PLATFORM_NO_PROC
4800#endif
4801
4802 bool is_worker_child = false;
4803 pid_t worker_pids[workers > 1 ? workers - 1 : 1];
4804 size_t worker_count = 0;
4805#ifndef CWIST_PLATFORM_NO_PROC
4806 int child_idx = 0;
4807 for (int i = 1; i < workers; i++) {
4808 pid_t pid = fork();
4809 if (pid == 0) {
4810 is_worker_child = true;
4811 child_idx = i;
4812 break;
4813 } else if (pid < 0) {
4814 perror("fork worker failed");
4815 break;
4816 } else {
4817 worker_pids[worker_count++] = pid;
4818 }
4819 }
4820
4821#if defined(__linux__) && defined(_GNU_SOURCE)
4822 /* Keep each worker inside its inherited affinity mask (including sparse IDs). */
4823 if (workers > 1 && cwist_app_pin_worker((size_t)(is_worker_child ? child_idx : 0)) < 0) {
4824 /* On failure, retain the inherited mask rather than guessing a CPU ID. */
4825 perror("worker CPU affinity");
4826 }
4827#endif
4828
4829 if (is_worker_child) {
4831 server_fd = cwist_make_socket_ipv4(&addr, "0.0.0.0", port, 32768);
4832 if (server_fd >= 0) {
4834 }
4835 if (udp_fd >= 0) {
4836 close(udp_fd);
4837 udp_fd = -1;
4838 }
4839 }
4840#else
4841 (void)addr;
4842#endif /* __wasi__ (single process) */
4843
4844 /* The static cache owns a libttak cleanup thread as well as the watcher.
4845 * Initialize it only after all worker forks so no child inherits mutexes
4846 * or a pthread handle whose owning thread exists only in the parent. */
4847#ifndef CWIST_PLATFORM_NO_PROC
4848 cwist_mem_init(app);
4849
4850 // Per-process threads start here. Each worker gets its own watcher and
4851 // HTTP/3 thread, so fork-after-thread deadlock is avoided.
4852 if (app->mem_manager) {
4855 }
4856
4857 pthread_t h3_tid = 0;
4858 bool h3_thread_started = false;
4859 if (udp_fd >= 0) {
4860 struct h3_thread_payload *h3_p = malloc(sizeof(*h3_p));
4861 if (h3_p) {
4862 h3_p->udp_fd = udp_fd;
4863 h3_p->app = app;
4865 h3_thread_started = true;
4867 printf("HTTP/3 (QUIC) enabled on UDP port %d\n", port);
4868 } else {
4869 free(h3_p);
4870 close(udp_fd);
4871 udp_fd = -1;
4872 }
4873 } else {
4874 close(udp_fd);
4875 udp_fd = -1;
4876 }
4877 }
4878
4879#endif /* __wasi__ (no static-cache thread, watcher, or H3 thread) */
4880 printf("CWIST App running on port %d (SSL: %s) [Event-driven, workers=%d, pid=%d]\n", port,
4881 app->use_ssl ? "On" : "Off", workers, (int)getpid());
4882
4883 // Check config for non-blocking scale mode (default enabled)
4884 const char *c1m = getenv("CWIST_C1M_MODE");
4885#ifdef CWIST_PLATFORM_NO_PROC
4886 /* The C1M reactor is epoll/eventfd-based; WASI hosts run the blocking
4887 * accept loop instead. */
4888 bool use_c1m = false;
4889#else
4890 bool use_c1m = true;
4891#endif
4892 if (c1m_override < 0 && c1m) {
4893 if (c1m[0] == '0' || strcmp(c1m, "false") == 0) {
4894 use_c1m = false;
4895 }
4896 }
4897#ifndef CWIST_PLATFORM_NO_PROC
4898 if (c1m_override >= 0) use_c1m = c1m_override != 0;
4899#else
4901#endif
4902 if (use_c1m) {
4904 } else {
4905#ifdef CWIST_PLATFORM_NO_PROC
4906 if (app->use_ssl) {
4907 fprintf(stderr, "TLS is not available on WASI (no BoringSSL); serve cleartext.\n");
4909 return -1;
4910 }
4911 /* Single-threaded host: no pool, no epoll - the blocking accept
4912 * fallback in cwist_http_server_loop() handles one connection at a
4913 * time, which is what a WASM socket grant can drive anyway. */
4914 cwist_server_config config = {
4915 .use_forking = false, .use_threading = false, .use_epoll = false};
4917#else
4918 if (app->use_ssl) {
4919 if (!app->ssl_ctx) {
4920 fprintf(stderr, "SSL enabled but context not initialized.\n");
4922 return -1;
4923 }
4924 cwist_https_server_loop(server_fd, app->ssl_ctx, static_ssl_handler, app);
4925 } else {
4926 cwist_server_config config = {
4927 .use_forking = false, .use_threading = true, .use_epoll = false};
4929 }
4930#endif
4931 }
4932
4933 /* Graceful shutdown cleanup */
4936
4937 if (app->h3_ctx) {
4939 }
4940#ifndef CWIST_PLATFORM_NO_PROC
4941 if (h3_thread_started) {
4943 }
4944 if (udp_fd >= 0) {
4945 close(udp_fd);
4946 udp_fd = -1;
4947 }
4948#endif
4949
4950 if (app->mem_manager) {
4952 }
4953
4954 /* Post-stop drain: give in-flight C1M connections a bounded window to
4955 * finish before the process exits, but only for as long as something is
4956 * actually alive. The reactor and pool are already torn down at this
4957 * point, so an idle server used to sleep out the full timeout on every
4958 * shutdown; exit as soon as no connection remains. */
4959 printf("[CWIST] Draining connections for %d seconds...\n", g_cwist_drain_timeout_sec);
4960 if ((is_worker_child || workers == 1) && g_cwist_drain_timeout_sec > 0) {
4961 for (int i = 0; i < g_cwist_drain_timeout_sec * 10; i++) {
4962 if (cwist_http_inflight_count() <= 0) break;
4963 usleep(100 * 1000);
4964 }
4965 }
4966
4967 int worker_result = 0;
4968 /* Parent process reaps worker children so they do not become zombies. */
4969#ifndef CWIST_PLATFORM_NO_PROC
4970 if (!is_worker_child && workers > 1) {
4971 /* SIGTERM is delivered to the supervisor only. Ask every worker to
4972 * leave its inherited accept loop before waiting for it; otherwise a
4973 * supervisor shutdown can block indefinitely. */
4974 for (size_t i = 0; i < worker_count; i++) {
4976 }
4977 for (size_t i = 0; i < worker_count; i++) {
4978 int status;
4979 pid_t reaped;
4980 do {
4981 reaped = waitpid(worker_pids[i], &status, 0);
4982 } while (reaped < 0 && errno == EINTR);
4983 if (reaped < 0) {
4984 perror("waitpid worker");
4985 worker_result = -1;
4986 } else if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
4987 fprintf(stderr, "Worker %d exited abnormally (status=%d)\n", (int)worker_pids[i],
4988 status);
4989 worker_result = -1;
4990 }
4991 }
4992 }
4993#else
4995 (void)worker_count;
4996#endif /* __wasi__ (no child processes) */
4997
4998 printf("[CWIST] Shutdown complete.\n");
4999
5000 return worker_result;
5001}
5002#endif
5003
5004static char cwist_swagger_json_path[512] = "openapi.json";
5005
5006static void cwist_swagger_html_handler(cwist_http_request *req, cwist_http_response *res) {
5007 (void)req;
5008 static const char html[] =
5009 "<!DOCTYPE html>\n<html>\n<head>\n"
5010 "<title>CWIST Swagger UI</title>\n"
5011 "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\" />\n"
5012 "</head>\n<body>\n"
5013 "<div id=\"swagger-ui\"></div>\n"
5014 "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
5015 "<script>\n"
5016 "window.onload = () => {\n"
5017 " SwaggerUIBundle({\n"
5018 " url: '/openapi.json',\n"
5019 " dom_id: '#swagger-ui',\n"
5020 " });\n"
5021 "};\n"
5022 "</script>\n"
5023 "</body>\n</html>\n";
5024 cwist_sstring_assign(res->body, (char *)html);
5025 cwist_http_header_add(&res->headers, "Content-Type", "text/html; charset=utf-8");
5026}
5027
5028static void cwist_swagger_json_handler(cwist_http_request *req, cwist_http_response *res) {
5029 (void)req;
5030 FILE *f = fopen(cwist_swagger_json_path, "rb");
5031 if (!f) {
5032 res->status_code = 404;
5033 cwist_sstring_assign(res->body, (char *)"{\"error\":\"openapi.json not found\"}");
5034 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
5035 return;
5036 }
5037 fseek(f, 0, SEEK_END);
5038 long sz = ftell(f);
5039 fseek(f, 0, SEEK_SET);
5040 char *buf = (char *)malloc((size_t)sz + 1);
5041 if (buf) {
5042 size_t rd = fread(buf, 1, (size_t)sz, f);
5043 buf[rd] = '\0';
5044 cwist_sstring_assign(res->body, buf);
5045 free(buf);
5046 }
5047 fclose(f);
5048 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
5049}
5050
5052 const char *openapi_json_path) {
5053 if (!app) return;
5054 if (openapi_json_path) {
5055 snprintf(cwist_swagger_json_path, sizeof(cwist_swagger_json_path), "%s", openapi_json_path);
5056 }
5057 const char *mp = mount_path ? mount_path : "/docs";
5058 cwist_app_get(app, mp, cwist_swagger_html_handler);
5059 cwist_app_get(app, "/openapi.json", cwist_swagger_json_handler);
5060}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
Definition alloc.c:27
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc_array(size_t count, size_t elem_size)
Allocate zeroed memory for count elements.
Definition alloc.c:21
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
int cwist_app_listen(cwist_app *app, int port)
Initialize runtime services and enter the HTTP or HTTPS server loop.
Definition app.c:4628
void cwist_app_delete(cwist_app *app, const char *path, cwist_handler_func handler)
Definition app.c:2275
void cwist_app_enable_metrics(cwist_app *app)
Definition app.c:2227
app_serve_result_t
Route and respond to one fully parsed HTTP/1.1 request. Shared by the blocking keep-alive loop and th...
Definition app.c:3085
@ APP_SERVE_CLOSE
Definition app.c:3086
@ APP_SERVE_DETACH
Definition app.c:3089
@ APP_SERVE_DEFERRED
Definition app.c:3088
@ APP_SERVE_KEEPALIVE
Definition app.c:3087
void cwist_app_enable_swagger(cwist_app *app, const char *mount_path, const char *openapi_json_path)
Enables embedded Swagger UI interactive documentation page.
Definition app.c:5051
cwist_error_t cwist_app_static_with_cache(cwist_app *app, const char *url_prefix, const char *directory, const char *cache_control)
Register a filesystem directory with a custom Cache-Control header.
Definition app.c:2103
cwist_error_t cwist_app_use_ex(cwist_app *app, cwist_middleware_func mw, cwist_middleware_func_ex mw_ex, void *user_ctx, cwist_middleware_ctx_destroy_func destroy)
Append an extended middleware callback to the application's execution chain.
Definition app.c:1389
void cwist_app_put_named(cwist_app *app, const char *path, const char *name, cwist_handler_func handler)
Definition app.c:2288
void cwist_app_delete_opt(cwist_app *app, const char *path, cwist_handler_func handler, cwist_endpoint_opt_t opts)
Definition app.c:2385
cwist_stream_req_t * cwist_stream_req_begin(const char *head, size_t head_len)
Begin an incremental request: head is the serialized request line plus headers (everything up to and ...
Definition app.c:2600
void cwist_app_ws_opt(cwist_app *app, const char *path, cwist_ws_handler_func handler, cwist_endpoint_opt_t opts)
Register a WebSocket route with explicit endpoint options.
Definition app.c:2402
cwist_error_t cwist_app_use_nuke_db(cwist_app *app, const char *db_path, int sync_interval_ms)
Enable NUKE DB and fall back to standard SQLite when RAM bootstrap fails.
Definition app.c:1890
cwist_error_t cwist_app_static(cwist_app *app, const char *url_prefix, const char *directory)
Register a filesystem directory to be served beneath a URL prefix.
Definition app.c:2057
#define CWIST_HTTP_REQUESTS_PER_TURN_DEFAULT
Event-driven HTTP/1.1 handler for the C1M reactor path. Drains the socket without blocking and serves...
Definition app.c:3276
#define CWIST_STATIC_RETIRE_NS
Definition app.c:91
void cwist_app_post_named(cwist_app *app, const char *path, const char *name, cwist_handler_func handler)
Definition app.c:2283
int cwist_stream_req_end(cwist_stream_req_t *r)
Declare the body complete. The fed byte count must equal the declared Content-Length (or both be zero...
Definition app.c:2646
void cwist_app_get(cwist_app *app, const char *path, cwist_handler_func handler)
Register a GET handler with default endpoint options.
Definition app.c:2216
void cwist_apply_profile(void)
Apply a CWIST_PROFILE preset before the per-variable env reads in cwist_app_listen()....
Definition app.c:4593
#define CWIST_ROUTE_BUCKETS
Definition app.c:90
void cwist_app_http_handler(int client_fd, void *ctx)
Definition app.c:3506
char * cwist_url_for(cwist_app *app, const char *name, cwist_query_map *params)
Definition app.c:2475
void cwist_app_configure_bdr(cwist_app *app, size_t max_bytes, time_t max_entry_age_sec, uint64_t revalidate_hits)
Configure the Big Dumb Reply cache thresholds for the application.
Definition app.c:1454
#define CWIST_DEFAULT_FD_LIMIT_TARGET
Definition app.c:99
cwist_db_pool_t * cwist_app_get_db_pool(cwist_app *app)
Return the application's database connection pool.
Definition app.c:1984
cwist_error_t cwist_app_reload_tls(cwist_app *app, const char *cert_path, const char *key_path)
Hot-reload the TLS certificate pair on every live TLS context of the application. See the public cont...
Definition app.c:1691
void cwist_app_patch_named(cwist_app *app, const char *path, const char *name, cwist_handler_func handler)
Definition app.c:2298
cwist_error_t cwist_app_use_redis(cwist_app *app, const char *host, int port, size_t max_conns)
Configure a Redis connection pool for the application.
Definition app.c:1989
void cwist_app_post(cwist_app *app, const char *path, cwist_handler_func handler)
Register a POST handler with default endpoint options.
Definition app.c:2267
#define CWIST_HTTP_REQUESTS_PER_TURN_MAX
Definition app.c:3277
cwist_error_t cwist_app_use_db(cwist_app *app, const char *db_path)
Open a SQLite database and attach it as the shared application handle.
Definition app.c:1851
void cwist_app_put(cwist_app *app, const char *path, cwist_handler_func handler)
Definition app.c:2271
int cwist_app_dispatch_stream(cwist_app *app, const char *req_buf, size_t req_len, cwist_stream_write_fn write_fn, void *write_ctx)
Like cwist_app_dispatch_memory(), but streams the serialized response through write_fn instead of ret...
Definition app.c:2668
void cwist_app_get_opt(cwist_app *app, const char *path, cwist_handler_func handler, cwist_endpoint_opt_t opts)
Register a GET handler with explicit endpoint options.
Definition app.c:2363
cwist_error_t cwist_app_post_ex(cwist_app *app, const char *path, cwist_handler_ex_func handler, void *user_ctx, cwist_handler_ctx_destroy_func destroy)
POST counterpart of cwist_app_get_ex().
Definition app.c:2185
bool cwist_endpoint_has_extern(cwist_endpoint_opt_t opts, cwist_endpoint_opt_t flag)
Out-of-line cwist_endpoint_has(), for callers that cannot use a static inline function,...
Definition app.c:2352
cwist_error_t cwist_app_put_ex(cwist_app *app, const char *path, cwist_handler_ex_func handler, void *user_ctx, cwist_handler_ctx_destroy_func destroy)
PUT counterpart of cwist_app_get_ex().
Definition app.c:2190
void cwist_app_set_error_handler(cwist_app *app, cwist_error_handler_func handler)
Install a custom HTTP error callback.
Definition app.c:1411
cwist_error_t cwist_app_get_ex(cwist_app *app, const char *path, cwist_handler_ex_func handler, void *user_ctx, cwist_handler_ctx_destroy_func destroy)
Registers a GET route whose handler receives a user context.
Definition app.c:2180
void cwist_app_post_opt(cwist_app *app, const char *path, cwist_handler_func handler, cwist_endpoint_opt_t opts)
Register a POST handler with explicit endpoint options.
Definition app.c:2375
struct cwist_route_entry cwist_route_entry
void cwist_app_set_tls_groups(cwist_app *app, const char *groups)
Override TLS groups explicitly.
Definition app.c:1749
void cwist_app_patch_opt(cwist_app *app, const char *path, cwist_handler_func handler, cwist_endpoint_opt_t opts)
Definition app.c:2390
#define CWIST_STREAM_REQ_EAGER_MAX
Definition app.c:2556
void cwist_app_use(cwist_app *app, cwist_middleware_func mw)
Append a middleware callback to the application's execution chain.
Definition app.c:1376
cwist_error_t cwist_app_use_db_pool(cwist_app *app, const char *db_path, size_t max_conns)
Open a SQLite connection pool and attach it to the application.
Definition app.c:1958
void cwist_app_get_named(cwist_app *app, const char *path, const char *name, cwist_handler_func handler)
Definition app.c:2256
void cwist_app_destroy(cwist_app *app)
Release every resource owned by the application object.
Definition app.c:1464
int cwist_stream_req_feed(cwist_stream_req_t *r, const char *chunk, size_t len)
Feed one body chunk. Chunks may be any size and may split anywhere; reassembly is the implementation'...
Definition app.c:2626
void cwist_app_enable_healthz(cwist_app *app)
Register built-in health check routes on an application.
Definition app.c:2249
void cwist_app_put_opt(cwist_app *app, const char *path, cwist_handler_func handler, cwist_endpoint_opt_t opts)
Definition app.c:2380
cwist_error_t cwist_app_use_http2(cwist_app *app, bool enabled)
Enable HTTP/2 cleartext (h2c) over standard TCP.
Definition app.c:1806
cwist_scheduler_t * cwist_app_get_scheduler(cwist_app *app)
Return the application's background job scheduler.
Definition app.c:2045
cwist_app * cwist_app_create(void)
Creates a new CWIST application instance.
Definition app.c:1292
#define CWIST_H2C_THREAD_STACK_SIZE
Definition app.c:3318
cwist_error_t cwist_app_patch_ex(cwist_app *app, const char *path, cwist_handler_ex_func handler, void *user_ctx, cwist_handler_ctx_destroy_func destroy)
PATCH counterpart of cwist_app_get_ex().
Definition app.c:2200
cwist_error_t cwist_app_use_http3(cwist_app *app, bool enabled)
Enable HTTP/3 (QUIC) without manual SSL configuration. Auto-generates an ephemeral self-signed certif...
Definition app.c:1818
void cwist_app_use_webtransport(cwist_app *app, cwist_webtransport_handler_func handler)
Enable WebTransport over HTTP/3 on the application.
Definition app.c:1835
cwist_error_t cwist_app_use_https2(cwist_app *app, bool enabled)
Definition app.c:1760
void cwist_app_dispatch(cwist_app *app, cwist_http_request *req, cwist_http_response *res)
Dispatch a request internally through the app's router and middleware. Used by the test client and ad...
Definition app.c:2516
void cwist_app_ws_async(cwist_app *app, const char *path, cwist_ws_on_message_t on_message, void *user_data)
Register a callback-shaped non-blocking WebSocket endpoint (C1M mode).
Definition app.c:2330
cwist_async_action_t cwist_app_http_handler_async(int client_fd, cwist_http_async_conn_t *conn)
Definition app.c:3343
int cwist_app_listen_ex(cwist_app *app, int port, int workers_override, int c1m_override)
cwist_app_listen() with the worker count and server mode given explicitly.
Definition app.c:4636
void cwist_app_patch(cwist_app *app, const char *path, cwist_handler_func handler)
Definition app.c:2279
void cwist_app_register_error_handler(cwist_app *app, cwist_http_status_t status, cwist_error_handler_func handler)
Definition app.c:1415
int cwist_stream_req_dispatch(cwist_stream_req_t *r, cwist_app *app, cwist_stream_write_fn write_fn, void *write_ctx)
Dispatch a completed incremental request with a streaming response. Takes ownership of req regardless...
Definition app.c:2653
void cwist_app_set_max_memspace(cwist_app *app, size_t size)
Override the static file memory budget used by cwist_mem_init().
Definition app.c:1402
cwist_error_t cwist_app_delete_ex(cwist_app *app, const char *path, cwist_handler_ex_func handler, void *user_ctx, cwist_handler_ctx_destroy_func destroy)
DELETE counterpart of cwist_app_get_ex().
Definition app.c:2195
cwist_error_t cwist_app_use_scheduler(cwist_app *app, size_t worker_count, size_t queue_capacity)
Configure a background job scheduler for the application.
Definition app.c:2020
cwist_error_t cwist_app_use_https3(cwist_app *app, bool enabled)
Definition app.c:1783
void cwist_app_ws(cwist_app *app, const char *path, cwist_ws_handler_func handler)
Register a WebSocket upgrade endpoint with default options.
Definition app.c:2309
#define PATH_MAX
Definition app.c:62
int cwist_app_dispatch_memory(cwist_app *app, const char *req_buf, size_t req_len, char **res_buf, size_t *res_len)
Run the full router/middleware/handler pipeline on a raw HTTP/1.x request held in a memory buffer,...
Definition app.c:2523
bool cwist_https_upgrade_handler(cwist_https_connection *conn, cwist_http_request *req, cwist_http_response *res)
cwist_error_t cwist_app_use_https(cwist_app *app, const char *cert_path, const char *key_path)
Enable TLS for the application using the supplied certificate pair.
Definition app.c:1646
cwist_db * cwist_app_get_db(cwist_app *app)
Return the active shared database wrapper for the application.
Definition app.c:1948
void cwist_app_delete_named(cwist_app *app, const char *path, const char *name, cwist_handler_func handler)
Definition app.c:2293
void cwist_app_use_pqc_layer(cwist_app *app, bool enabled)
Enable Post-Quantum Cryptography (PQC) hybrid key exchange layer.
Definition app.c:1744
cwist_redis_pool_t * cwist_app_get_redis_pool(cwist_app *app)
Return the application's Redis connection pool.
Definition app.c:2015
void cwist_assets_destroy(void *registry)
Definition assets.c:570
void cwist_assets_respond(cwist_http_request *req, cwist_http_response *res, const cwist_asset_match *match)
Fill a response for a matched asset.
Definition assets.c:475
int cwist_assets_clone(void **dst, const void *src)
Deep-copy a registry (used when an app is cloned).
Definition assets.c:514
bool cwist_assets_match(cwist_app *app, const cwist_http_request *req, cwist_asset_match *out)
Match an HTTP request to a registered asset.
Definition assets.c:410
Private wiring between app.c and the asset registry (assets.c).
Deferred-response (async handler) API for CWIST.
void cwist_async_dispatch_ack(cwist_async *a)
Internal: on the dispatch thread, after all middleware has unwound. Call exactly once; do not touch t...
Definition async.c:167
bool cwist_async_abort(cwist_async *a, cwist_http_status_t status)
Complete the exchange with an error status and close the connection.
Definition async.c:370
struct cwist_async cwist_async
Definition async.h:54
struct cwist_async * cwist_async_dispatch_take(cwist_http_response *res)
Definition async_tls.c:57
cwist_error_t cwist_async_server_loop(int server_fd, cwist_app *app)
Run the async (reactor-based) HTTP/HTTPS server loop for C1M scale.
Auto-Caching Layer for repetitive requests.
void cwist_bdr_set_limits(cwist_bdr_t *bdr, size_t max_bytes, time_t max_entry_age_sec, uint64_t revalidate_hits)
Adjusts guard-rail policies for the in-memory cache.
void cwist_bdr_put_fixed(cwist_bdr_t *bdr, const char *method, const char *path, const void *data, size_t len)
Immediately cache a fixed static response on request 1.
void cwist_bdr_unpin(bdr_blob_t *pin)
Release a pin acquired by cwist_bdr_get_pinned().
void cwist_bdr_put(cwist_bdr_t *bdr, const char *method, const char *path, const void *data, size_t len)
Store a response in the cache (lock-free learn path).
void cwist_bdr_destroy(cwist_bdr_t *bdr)
Destroys a BDR context and frees all cached blobs.
const void * cwist_bdr_get_pinned(cwist_bdr_t *bdr, const char *method, const char *path, size_t *out_len, bdr_blob_t **out_pin)
Find a cached response and pin it for the duration of the serve.
const void * cwist_bdr_get(cwist_bdr_t *bdr, const char *method, const char *path, size_t *out_len)
Try to find a cached response.
const void * cwist_bdr_get_pinned_cursor(cwist_bdr_t *bdr, const char *method, const char *path, size_t path_len, size_t *out_len, bdr_blob_t **out_pin, cwist_bdr_cursor_t *cursor)
cwist_bdr_get_pinned() with a per-connection fast path.
cwist_bdr_t * cwist_bdr_create(void)
Creates a BDR context.
Application configuration management (.env, environment variables).
cwist_config * cwist_config_create(void)
Create a new configuration object.
Definition config.c:29
void cwist_config_destroy(cwist_config *cfg)
Destroy a configuration object.
Definition config.c:45
cwist_error_t cwist_db_open(cwist_db **db, const char *path)
Open a SQLite database and wrap the resulting connection in a CWIST handle.
Definition db.c:154
void cwist_db_close(cwist_db *db)
Close a CWIST database wrapper and its underlying SQLite connection.
Definition db.c:295
#define CWIST_MIB(x)
Convert binary mebibytes to bytes.
Definition macros.h:16
Safe string type with helper methods.
cwist_sstring * cwist_sstring_create(void)
Create a new sstring instance.
Definition sstring.c:802
cwist_error_t cwist_sstring_append(cwist_sstring *str, const char *data)
Append a C string.
Definition sstring.c:559
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
Simple JSON string builder utility.
struct cwist_redis_pool cwist_redis_pool_t
Redis connection pool handle.
Definition cwist_redis.h:24
void cwist_redis_pool_destroy(cwist_redis_pool_t *pool)
Destroy the pool and close all connections.
cwist_redis_pool_t * cwist_redis_pool_create(const char *host, int port, size_t max_conns)
Create a Redis connection pool.
uint32_t cwist_endpoint_opt_t
Bitmask type controlling endpoint behavior.
#define CWIST_ENDPOINT_FILE
#define CWIST_ENDPOINT_DEFAULT
Default option for new endpoints.
#define CWIST_ENDPOINT_FIXED
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
@ CWIST_ERR_JSON
Definition cwist_err.h:37
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
gRPC over HTTP/2 helpers for CWIST.
int cwist_grpc_routes_clone(struct cwist_app *dst, const struct cwist_app *src)
void cwist_grpc_routes_destroy(struct cwist_app *app)
Free all gRPC routes and per-route resources on an app.
Definition grpc.c:1382
const cwist_http2_stream_hooks * cwist_grpc_http2_hooks(void)
Access the gRPC HTTP/2 stream hooks table.
Definition grpc.c:2109
Standardised readiness and liveness probing for CWIST.
void cwist_app_healthz(cwist_http_response *res)
Populate an HTTP response with the healthz JSON payload.
Definition healthz.c:168
HTTP/2 Protocol Definitions for CWIST.
cwist_error_t cwist_http2_serve_connection_ex(cwist_https_connection *conn, void *user_ctx, cwist_http2_request_handler_func handler, const cwist_http2_stream_hooks *hooks)
Serve one HTTP/2 connection until it closes. Verifies the preface, sends our SETTINGS,...
Definition http2.c:3573
HTTP/3 (QUIC-based) Protocol Definitions for CWIST.
void(* cwist_webtransport_handler_func)(cwist_http_request *req, cwist_http_response *res, void *stream)
Callback function type for handling WebTransport sessions over HTTP/3.
Definition http3.h:27
void cwist_http3_set_webtransport_handler(cwist_http3_context *ctx, cwist_webtransport_handler_func handler)
Register a WebTransport session handler on the given HTTP/3 context.
Definition http3.c:3527
void cwist_http3_destroy_context(cwist_http3_context *ctx)
Destroy an HTTP/3 context.
Definition http3.c:2848
cwist_error_t cwist_http3_init_context(cwist_http3_context **ctx, const char *cert_path, const char *key_path)
Callback function type for handling WebTransport sessions over HTTP/3.
Definition http3.c:2673
cwist_error_t cwist_http3_reload_context(cwist_http3_context *ctx, const char *cert_path, const char *key_path)
Hot-reload the certificate chain and key of a live HTTP/3 context.
Definition http3.c:2879
cwist_error_t cwist_http3_server_loop(int udp_fd, cwist_http3_context *ctx, cwist_http3_request_handler_func handler, void *user_ctx)
Run the HTTP/3 server event loop.
Definition http3.c:2932
cwist_error_t cwist_http3_init_context_ephemeral(cwist_http3_context **ctx)
Initialize an HTTP/3 context with an ephemeral self-signed certificate.
Definition http3.c:2721
void cwist_http_live_idle_leave(void)
Definition http.c:434
bool cwist_reuseport_enabled(void)
Definition http.c:5347
bool cwist_http_live_idle_enter(void)
Definition http.c:423
cwist_http_request * cwist_http_receive_request(int client_fd, char *read_buf, size_t buf_size, size_t *buf_len, cwist_http_parse_error_t *err_out)
Receive and parse an HTTP request from a socket.
Definition http.c:4722
#define CWIST_HTTP_READ_BUFFER_SIZE
Definition http.h:42
void cwist_http_send_error_response(int fd, int status, const char *msg)
Send a minimal error response (Connection: close) for a failed request.
Definition http.c:4046
#define CWIST_HTTP_MAX_HEADER_SIZE
Definition http.h:40
size_t cwist_http_serialize_headers(cwist_http_response *res, char *buf, size_t buf_size)
Serialize only the status line and headers into a caller buffer.
Definition http.c:2609
cwist_error_t cwist_http_server_loop(int server_fd, cwist_server_config *config, void(*handler)(int, void *), void *ctx)
Run the main HTTP accept loop using the configured concurrency strategy.
Definition http.c:5590
cwist_error_t cwist_http_send_response(int client_fd, cwist_http_response *res)
Send a response over a socket.
Definition http.c:2878
cwist_http_parse_error_t
Failure reason reported by the request receive APIs.
Definition http.h:46
@ CWIST_HTTP_PARSE_OK
Definition http.h:47
@ CWIST_HTTP_PARSE_BODY_TOO_LARGE
Definition http.h:51
@ CWIST_HTTP_PARSE_HEADER_OVERFLOW
Definition http.h:50
@ CWIST_HTTP_PARSE_MALFORMED
Definition http.h:49
@ CWIST_HTTP_PARSE_TE_UNSUPPORTED
Definition http.h:52
@ CWIST_HTTP_PARSE_EXPECT_FAILED
Definition http.h:53
int cwist_make_socket_ipv4(struct sockaddr_in *sockv4, const char *address, uint16_t port, uint16_t backlog)
Create an IPv4 socket and bind/listen.
Definition http.c:5361
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
cwist_error_t cwist_http_send_response_head(int client_fd, cwist_http_response *res)
Send only the status line and headers (HEAD replies).
Definition http.c:2961
cwist_http_status_t
Definition http.h:27
@ CWIST_HTTP_FORBIDDEN
Definition http.h:33
@ CWIST_HTTP_INTERNAL_ERROR
Definition http.h:35
@ CWIST_HTTP_NOT_IMPLEMENTED
Definition http.h:36
@ CWIST_HTTP_BAD_REQUEST
Definition http.h:31
@ CWIST_HTTP_NOT_FOUND
Definition http.h:34
@ CWIST_HTTP_OK
Definition http.h:28
void cwist_http_request_destroy(cwist_http_request *req)
Destroy an HTTP request object.
Definition http.c:2180
cwist_http_method_t
Definition http.h:16
@ CWIST_HTTP_HEAD
Definition http.h:22
@ CWIST_HTTP_PUT
Definition http.h:19
@ CWIST_HTTP_PATCH
Definition http.h:21
@ CWIST_HTTP_POST
Definition http.h:18
@ CWIST_HTTP_GET
Definition http.h:17
@ CWIST_HTTP_DELETE
Definition http.h:20
cwist_sstring * cwist_http_stringify_response(cwist_http_response *res)
Serialize a response to a string.
Definition http.c:4099
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
void cwist_http_response_destroy(cwist_http_response *res)
Destroy an HTTP response object.
Definition http.c:2315
cwist_http_response * cwist_http_response_create(void)
Create a new HTTP response object.
Definition http.c:2291
cwist_http_request * cwist_https_receive_request(cwist_https_connection *conn)
Read from the TLS stream until a full HTTP request has been assembled.
Definition https.c:1887
cwist_error_t cwist_https_server_loop(int server_fd, cwist_https_context *ctx, void(*handler)(cwist_https_connection *conn, void *), void *user_ctx)
cwist_error_t cwist_https_send_response(cwist_https_connection *conn, cwist_http_response *res)
Serialize an HTTP response and send it over an active TLS connection.
Definition https.c:2102
void cwist_https_destroy_context(cwist_https_context *ctx)
Free an HTTPS context and release its OpenSSL resources.
Definition https.c:1558
Application-level structured logging.
cwist_logger * cwist_logger_create(const char *name)
Create a logger.
Definition logger.c:32
void cwist_logger_destroy(cwist_logger *log)
Destroy a logger.
Definition logger.c:48
Production-grade metrics engine for CWIST.
void cwist_metrics_serve_http(struct cwist_http_response *res)
Convenience helper: populate an HTTP response with /metrics body.
Definition metrics.c:366
HTTP Protocol Definitions and Helpers.
cwist_async_send_status_t cwist_http_send_response_coalesced(int client_fd, cwist_http_response *res, cwist_http_async_conn_t *conn, bool keep_alive, bool head_only)
Coalescing variant of the async send: serialize small responses directly into the connection's output...
Definition http.c:3779
int cwist_http_async_conn_fill(cwist_http_async_conn_t *conn)
Drain the socket into the connection stash. Stops at EAGAIN, and also after a short read: poll is lev...
Definition http.c:4956
bool cwist_http_async_rearm(int client_fd, cwist_reactor_t *reactor, cwist_http_async_conn_t *conn)
Re-arm an async connection for its next event.
Definition http.c:1357
long get_cpu_cores(void)
Return the number of CPUs available to this process.
Definition http.c:99
long cwist_http_inflight_count(void)
Definition http.c:301
cwist_async_send_status_t
Definition http.h:528
@ CWIST_ASYNC_SEND_DEFERRED
Definition http.h:531
@ CWIST_ASYNC_SEND_KEEPALIVE
Definition http.h:529
time_t cwist_http_parse_date(const char *str)
Parse an HTTP-date string into a time_t.
Definition http.c:2119
int cwist_http_response_stream_end(cwist_http_response *res)
Terminate a streaming response with the final zero-length chunk and flush.
Definition http.c:4549
void cwist_http_format_date(time_t t, char *buf, size_t len)
Format a time_t as an HTTP-date (RFC 7231).
Definition http.c:2108
#define CWIST_HTTP_COALESCE_MAX
Definition http.h:547
int cwist_http_coalesce_flush_blocking(int client_fd, cwist_http_async_conn_t *conn)
Flush the coalesced-output stash, blocking in poll() until drained.
Definition http.c:3737
cwist_recv_status_t cwist_http_receive_request_nb(cwist_http_async_conn_t *conn, cwist_http_request **out, cwist_http_parse_error_t *err_out)
Try to assemble one complete request from the stash without any blocking IO. On CWIST_RECV_OK the con...
Definition http.c:5035
@ CWIST_HTTP_NOT_MODIFIED
Definition http.h:71
@ CWIST_HTTP_PARTIAL_CONTENT
Definition http.h:62
@ CWIST_HTTP_RANGE_NOT_SATISFIABLE
Definition http.h:93
int cwist_http_coalesce_error_response(cwist_http_async_conn_t *conn, int status)
Build a minimal text error response into the coalesce stash.
Definition http.c:3756
void cwist_http_response_set_body_ptr_managed(cwist_http_response *res, const void *ptr, size_t len, cwist_http_body_cleanup_fn cleanup, void *ctx)
Attach a managed zero-copy body pointer and optional cleanup hook to a response.
Definition http.c:2358
cwist_coalesce_flush_status_t
Definition http.h:549
@ CWIST_COALESCE_FLUSH_ERROR
Definition http.h:552
@ CWIST_COALESCE_FLUSH_PARKED
Definition http.h:551
cwist_coalesce_flush_status_t cwist_http_coalesce_flush(int client_fd, cwist_http_async_conn_t *conn, bool keep_alive)
Flush the coalesced-output stash with one speculative write.
Definition http.c:3682
cwist_http_request * cwist_http_parse_request_len(const char *buf, size_t len)
Parse an HTTP/1.x request held in a memory buffer (length-based; the buffer may contain a binary body...
Definition http.c:4424
cwist_async_action_t
Definition http.h:447
@ CWIST_ASYNC_REARM
Definition http.h:449
@ CWIST_ASYNC_CLOSE
Definition http.h:448
@ CWIST_ASYNC_DEFER
Definition http.h:451
@ CWIST_ASYNC_DETACH
Definition http.h:450
cwist_http_response * cwist_http_response_create_in_arena(void *arena)
Create a response inside an existing (request) arena. The response is bump-allocated from arena and s...
Definition http.c:2306
cwist_recv_status_t
Definition http.h:565
@ CWIST_RECV_FATAL
Definition http.h:568
@ CWIST_RECV_NEED_MORE
Definition http.h:567
int cwist_http_response_serialize(cwist_http_response *res, char **out, size_t *out_len)
Serialize a response into a freshly allocated HTTP/1.x buffer (status line + headers + body)....
Definition http.c:4439
int cwist_http_coalesce_append(cwist_http_async_conn_t *conn, const void *data, size_t len)
Append bytes to a connection's coalesced-output stash.
Definition http.c:3653
HTTPS/TLS wrapper interface.
@ CWIST_HTTPS_PARK_HTTP1
Definition https.h:59
@ CWIST_HTTPS_PROTOCOL_HTTP2
Definition https.h:22
void https_pool_destroy(void)
Shut down the global HTTPS thread pool: signal workers to exit, join them, close any connections stil...
Definition https.c:370
cwist_error_t cwist_https_reload_context(cwist_https_context *ctx, const char *cert_path, const char *key_path, cwist_app *app)
Hot-reload the certificate chain and key of a live HTTPS context.
Definition https.c:1508
int https_pool_init(void)
Create the worker threads, mutex, and condition variables of the global HTTPS thread pool....
Definition https.c:268
uint64_t cwist_https_idle_timeout_ms(void)
Idle keep-alive budget for parked HTTP/1.1 TLS connections, in ms. CWIST_HTTPS_IDLE_TIMEOUT_MS overri...
Definition https.c:710
cwist_error_t cwist_https_send_response_head(cwist_https_connection *conn, cwist_http_response *res)
Definition https.c:2197
cwist_error_t cwist_https_init_context_with_options(cwist_https_context **ctx, const char *cert_path, const char *key_path, const cwist_https_options *options, cwist_app *app)
Definition https.c:1451
bool cwist_https_conn_idle(cwist_https_connection *conn)
True when conn has no buffered TLS bytes and nothing readable.
Definition https.c:683
bool cwist_https_connection_uses_http2(const cwist_https_connection *conn)
Gracefully close an HTTPS connection and free its buffers.
Definition https.c:1817
bool cwist_https_park(cwist_https_connection *conn, uint64_t idle_ms, cwist_https_park_class cls)
Hand an idle connection back to the HTTPS pool's park set.
Definition https.c:672
void https_pool_submit(int client_fd, cwist_https_context *ctx, void(*handler)(cwist_https_connection *, void *), void *user_ctx)
Queue a freshly accepted client socket whose TLS handshake still needs to run in a pool worker.
Definition https.c:292
const char * cwist_query_map_get(cwist_query_map *map, const char *key)
Get a value by key.
Definition query.c:196
void cwist_query_map_set(cwist_query_map *map, const char *key, const char *value)
Set a key-value pair.
Definition query.c:146
void cwist_query_map_clear(cwist_query_map *map)
Clear all entries.
Definition query.c:93
cwist_query_map * cwist_query_map_create(void)
Create a new query map.
Definition query.c:54
cwist_websocket * cwist_websocket_upgrade(cwist_http_request *req, int client_fd)
Upgrade a standard HTTP request to a WebSocket connection.
Definition websocket.c:119
bool cwist_websocket_upgrade_response(cwist_http_request *req, cwist_http_response *res)
Validate the upgrade request and fill a 101 Switching Protocols response.
Definition websocket.c:71
void cwist_websocket_destroy(cwist_websocket *ws)
Destroy the WebSocket context.
Definition websocket.c:523
High-Performance Read-Optimal Persistent Store using SQLite.
#define CWIST_NUKE_ERR_LOW_MEMORY
Definition nuke_db.h:23
sqlite3 * cwist_nuke_get_db(void)
Get the raw SQLite3 handle for the In-Memory DB.
Definition nuke_db.c:478
void cwist_nuke_close(void)
Close databases safely. Performs a final synchronization before closing handles.
Definition nuke_db.c:293
int cwist_nuke_init(const char *disk_path, int sync_interval_ms)
Initialize Nuke DB.
Definition nuke_db.c:377
#define CWIST_NUKE_OK
Return codes for cwist_nuke_init.
Definition nuke_db.h:21
size_t head
Definition otel.c:57
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
Target capability macros shared by all CWIST sources.
cwist_db_pool_t * cwist_db_pool_create(const char *path, size_t max_conns)
Create a pool of SQLite connections.
Definition pool.c:93
void cwist_db_pool_destroy(cwist_db_pool_t *pool)
Destroy the pool and close all connections.
Definition pool.c:256
struct cwist_db_pool cwist_db_pool_t
Opaque database connection pool handle.
Definition pool.h:21
Read-only walk of an app's route table for in-tree consumers (the runtime OpenAPI generator in openap...
void cwist_app_foreach_route(cwist_app *app, cwist_route_visit_fn visit, void *ctx)
void(* cwist_route_visit_fn)(void *ctx, cwist_http_method_t method, const char *path)
void cwist_scheduler_destroy(cwist_scheduler_t *s)
Destroy the scheduler and stop all worker threads.
Definition scheduler.c:327
struct cwist_scheduler cwist_scheduler_t
Opaque background job scheduler handle.
Definition scheduler.h:21
cwist_scheduler_t * cwist_scheduler_create(size_t worker_count, size_t queue_capacity)
Create a scheduler with a pool of background workers.
Definition scheduler.c:265
Unified Graceful Shutdown support for CWIST.
int g_cwist_drain_timeout_sec
Drain timeout in seconds before process exits.
Definition shutdown.c:24
int g_cwist_listen_fd
Registered TCP listening socket (for signal-handler closure).
Definition shutdown.c:18
atomic_int g_cwist_running
Global running flag. Checked by all server event loops.
Definition shutdown.c:17
void cwist_shutdown_install_handlers(void)
Install SIGTERM and SIGINT handlers for graceful shutdown.
Definition shutdown.c:111
void cwist_shutdown_restore_handlers(void)
Put back the SIGTERM and SIGINT handlers that were in place before cwist_shutdown_install_handlers()....
Definition shutdown.c:133
int g_cwist_udp_fd
Registered UDP socket for HTTP/3 (for signal-handler closure).
Definition shutdown.c:19
Refcount-free cached response blob, reclaimed via libttak EBR.
Main Application Context.
Definition app.h:155
bool nuke_enabled
True when NUKE DB integration is active.
Definition app.h:181
bool use_https2
TLS HTTP/2 (h2)
Definition app.h:160
void * redis_pool
Redis connection pool (when enabled).
Definition app.h:213
bool use_ssl
Definition app.h:157
size_t max_mem_space
Max memory space for static file pool (0 = auto-detected * 2)
Definition app.h:184
void * scheduler
Background job scheduler (when enabled).
Definition app.h:216
cwist_webtransport_handler_func wt_handler
WebTransport session handler.
Definition app.h:200
cwist_error_handler_func error_handler
Global fallback error handler.
Definition app.h:171
int session_max_age
Session cookie max-age in seconds.
Definition app.h:207
cwist_https_context * ssl_ctx
SSL context when TLS is enabled.
Definition app.h:177
struct cwist_fix_server_mem * mem_manager
Memory manager for static asset caching and hot-reloading.
Definition app.h:186
char * session_secret
Session signing secret (HMAC-SHA256).
Definition app.h:203
bool use_http2
Cleartext HTTP/2 (h2c)
Definition app.h:158
cwist_error_handler_entry * error_handlers
Per-status-code error handlers.
Definition app.h:172
bool use_http3
Ephemeral (Zero-config) HTTP/3.
Definition app.h:159
cwist_bdr_t * bdr_ctx
Legacy standalone BDR storage; never replayed by HTTP dispatch.
Definition app.h:189
cwist_route_table * router
Router definition.
Definition app.h:168
cwist_db * db
Shared database handle.
Definition app.h:179
char * session_name
Session cookie name.
Definition app.h:205
char * key_path
Definition app.h:163
cwist_https_request_handler_func https_request_handler
Definition app.h:164
char * tls_groups
Explicit TLS groups override (NULL = automatic)
Definition app.h:197
void * db_pool
Database connection pool (when enabled).
Definition app.h:210
char * db_path
Database path (if set).
Definition app.h:180
int port
Definition app.h:156
bool use_https3
TLS HTTP/3.
Definition app.h:161
struct cwist_rdbms_runtime * rdbms
Mounted RDBMS runtime (auto-detected PostgreSQL/MySQL/MariaDB)
Definition app.h:192
struct cwist_config * config
Application configuration.
Definition app.h:174
bool pqc_layer_enabled
PQC Layer enabled flag.
Definition app.h:195
struct cwist_http3_context * h3_ctx
HTTP/3 QUIC Context.
Definition app.h:178
char * cert_path
Definition app.h:162
cwist_middleware_node * middlewares
Head of the middleware chain.
Definition app.h:166
cwist_static_dir * static_dirs
Static directory mappings.
Definition app.h:169
void * grpc_routes
Unary gRPC route registry.
Definition app.h:219
struct cwist_logger * logger
Application logger.
Definition app.h:175
void * assets
Content-hashed in-memory assets (see <cwist/sys/app/assets.h>).
Definition app.h:222
int latency_threshold_ms
Latency threshold to trigger caching.
Wrapper for Database Operations. Currently uses SQLite3.
Definition sql.h:21
sqlite3 * conn
Definition sql.h:22
Definition app.h:74
cwist_error_handler_func handler
Definition app.h:76
struct cwist_error_handler_entry * next
Definition app.h:77
cwist_http_status_t status_code
Definition app.h:75
Integrated error structure used throughout the system.
Definition cwist_err.h:72
Represents a file loaded into the fixed memory pool.
Definition app.h:230
void * data
Pointer to memory-tracked file contents.
Definition app.h:233
size_t size
Size of the file in bytes.
Definition app.h:234
char etag[40]
Precomputed ETag for conditional requests.
Definition app.h:236
ttak_mem_node_t * node
Tracking node for libttak lifecycle.
Definition app.h:238
time_t last_mod
Last modification time.
Definition app.h:235
char * fs_path
Full filesystem path.
Definition app.h:232
char * path
Relative path (URL path)
Definition app.h:231
char last_mod_hdr[30]
Precomputed HTTP-date (29 chars + NUL)
Definition app.h:237
Fixed Server Memory Manager.
Definition app.h:247
pthread_mutex_t lock
Definition app.h:258
int check_interval_ms
Definition app.h:261
size_t files_capacity
Capacity of the files array.
Definition app.h:253
ttak_mem_tree_t file_tree
Lifetime tracking tree for file buffers.
Definition app.h:256
size_t current_used
Bytes accounted for by active files.
Definition app.h:249
bool watcher_running
Definition app.h:260
uint64_t retire_grace_ns
Delay before recycling replaced buffers.
Definition app.h:255
cwist_file_t * files
Array of tracked files.
Definition app.h:251
pthread_t watcher_thread
Definition app.h:259
size_t file_count
Definition app.h:252
size_t total_capacity
Total capacity (defaults to sum of files * 2)
Definition app.h:248
HTTP/3 server context encapsulating lsquic/BoringSSL settings.
Definition http3.h:35
cwist_webtransport_handler_func wt_handler
Definition http3.h:50
int noprogress_timeout_ms
Definition http3.h:57
int early_data_enabled
Definition http3.h:44
volatile int running
Definition http3.h:41
int handshake_timeout_ms
Definition http3.h:55
void * datagram_user_ctx
Definition http3.h:49
void(* datagram_cb)(const void *data, size_t len, void *user_ctx)
Definition http3.h:48
cwist_reactor_t * reactor
Definition http.h:469
cwist_sstring * path
e.g., "/users/1"
Definition http.h:67
void * async_conn
C1M connection shell when on the reactor path (internal).
Definition http.h:188
struct cwist_app * app
Owning app context (if any).
Definition http.h:176
bool ws_async_handoff
WebSocket async path took fd ownership; C1M layer must detach.
Definition http.h:189
void * arena
Per-request arena for bump-allocated structs (internal).
Definition http.h:187
void * private_data
Internal framework use (protocol-specific context).
Definition http.h:78
cwist_query_map * path_params
Parsed path parameters (e.g. :id)
Definition http.h:70
void * https_conn
HTTPS connection wrapper when on TLS path (internal).
Definition http.h:190
cwist_http_header_node * headers
Definition http.h:72
bool keep_alive
Definition http.h:74
cwist_endpoint_opt_t endpoint_opts
Behavior hints for the active endpoint.
Definition http.h:183
cwist_db * db
Shared database handle from cwist_app.
Definition http.h:177
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_endpoint_opt_t endpoint_opts
Mirrors req->endpoint_opts.
Definition http.h:206
cwist_sstring * status_text
e.g., "OK"
Definition http.h:85
bool use_file_stream
Fast File Streaming.
Definition http.h:216
cwist_sstring * body
Definition http.h:87
const void * ptr_body
Pointer to external data (e.g., mmap region)
Definition http.h:210
void * stream_sink_ctx
Definition http.h:231
cwist_http_status_t status_code
Definition http.h:84
cwist_http_header_node * headers
Definition http.h:86
bool is_ptr_body
Zero-Copy Pointer Body.
Definition http.h:209
int(* stream_sink)(void *ctx, const char *data, size_t len)
Live sink (streaming dispatch only).
Definition http.h:230
bool stream_mode
cwist_http_response_stream_begin() ran.
Definition http.h:225
size_t ptr_body_len
Length of external data.
Definition http.h:211
bool stream_failed
A live sink rejected a chunk/head.
Definition http.h:227
bool stream_ended
cwist_http_response_stream_end() ran.
Definition http.h:226
Linked list node for middleware chain.
Definition app.h:135
struct cwist_middleware_node * next
Definition app.h:141
cwist_middleware_ctx_destroy_func destroy
Definition app.h:139
cwist_middleware_func_ex func_ex
Definition app.h:137
cwist_middleware_func func
Definition app.h:136
Counted multiport descriptor created from a normal C array.
Definition app.h:593
bool valid
False when construction failed, e.g. too many ports.
Definition app.h:596
unsigned short ports[CWIST_MULTIPORT_MAX_PORTS]
Additional TCP ports.
Definition app.h:594
size_t count
Number of valid entries in ports.
Definition app.h:595
Mounted RDBMS runtime state.
Definition app.h:89
bool use_forking
Process per request.
Definition http.h:220
size_t size
Definition sstring.h:20
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
Core Application Structure and Lifecycle Management.
#define CWIST_MULTIPORT_MAX_PORTS
Definition app.h:587
cwist_app * cwist_multiport_get_app(cwist_app **app_ref, unsigned short port)
Detach one additional multiport port into its own tunable application.
void(* cwist_handler_func)(cwist_http_request *req, cwist_http_response *res)
Function pointer type for HTTP route handlers.
Definition app.h:32
void(* cwist_error_handler_func)(cwist_http_request *req, cwist_http_response *res, cwist_http_status_t status)
Function pointer type for error handlers.
Definition app.h:61
void(* cwist_ws_handler_func)(cwist_websocket *ws)
Function pointer type for WebSocket handlers.
Definition app.h:56
void(* cwist_middleware_func_ex)(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next, void *user_ctx)
Extended middleware type that also receives a user context.
Definition app.h:124
void(* cwist_handler_ctx_destroy_func)(void *user_ctx)
Releases a route's user context once the route no longer needs it.
Definition app.h:50
void(* cwist_handler_ex_func)(void *user_ctx, cwist_http_request *req, cwist_http_response *res)
Route handler that also receives the context it was registered with.
Definition app.h:43
int(* cwist_stream_write_fn)(void *ctx, const char *data, size_t len)
Definition app.h:708
void(* cwist_middleware_ctx_destroy_func)(void *user_ctx)
Destructor for a middleware user context.
Definition app.h:128
struct cwist_route_table cwist_route_table
Definition app.h:144
void(* cwist_middleware_func)(cwist_http_request *req, cwist_http_response *res, cwist_handler_func next)
Middleware type that receives req/res pair and the next stage in the chain.
Definition app.h:115
struct cwist_stream_req cwist_stream_req_t
Definition app.h:731
cwist_multiport_t cwist_create_multiport_from_array(const unsigned short *ports, size_t count)
Create a counted multiport descriptor from an explicit pointer and length.
#define CWIST_STREAM_CHUNK
Definition app.h:711
int cwist_app_multiport(cwist_app **app_ref, unsigned short public_port, cwist_multiport_t ports)
Start one app facade across a public port and counted backend port list.
struct cwist_static_dir cwist_static_dir
Definition app.h:145
WASI target feature detection shared by all CWIST sources.
bool cwist_websocket_async_attach(int fd, cwist_reactor_t *reactor, cwist_ws_on_message_t on_message, void *user_data, const uint8_t *initial, size_t initial_len)
Attach an upgraded fd to the reactor as an async WebSocket connection (no on_close callback variant).
void(* cwist_ws_on_message_t)(cwist_websocket_async *ws, cwist_ws_frame *frame, void *user_data)
Callback invoked for each complete WebSocket message on the C1M path.