cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
https.c
Go to the documentation of this file.
1#define _POSIX_C_SOURCE 200809L
2
8#include <cwist/core/mem/gc.h>
10#include "tls_chain.h"
11#include "async_internal.h"
12#include <openssl/ssl.h>
13#include <openssl/err.h>
14#include <openssl/evp.h>
15#include <openssl/hmac.h>
16#include <openssl/rand.h>
17#include <openssl/crypto.h>
18#include <stdio.h>
19#include <stdlib.h>
20#include <unistd.h>
21#include <string.h>
22#include <errno.h>
23#include <poll.h>
24#ifdef __linux__
25#include <sys/epoll.h>
26#endif
27#include <fcntl.h>
28#include <sys/socket.h>
29#include <arpa/inet.h>
30#include <netinet/tcp.h>
31#include <stdint.h>
32#include <pthread.h>
33#include <stdbool.h>
34#include <stdatomic.h>
35#include <time.h>
36
37/* Forward declaration: PQC layer applied inside TLS bootstrap */
39
40/* Bodies up to this size are written in the same TLS record as the
41 * response headers; larger bodies amortize the per-record overhead. */
42#define CWIST_TLS_COALESCE_MAX (16 * 1024)
43
48static uint64_t cwist_https_now_ms(void) {
49 struct timespec ts;
51 return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
52}
53
58static int cwist_ssl_wait(int fd, int ssl_error, int timeout_ms) {
59 struct pollfd pfd = {.fd = fd, .events = 0};
61 pfd.events = POLLIN;
62 } else if (ssl_error == SSL_ERROR_WANT_WRITE) {
63 pfd.events = POLLOUT;
64 } else {
65 return -1;
66 }
67
68 int ret = poll(&pfd, 1, timeout_ms);
69 if (ret <= 0) return -1;
70 if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) return -1;
71 return 0;
72}
73
74struct https_thread_payload {
75 int client_fd;
77 void (*handler)(cwist_https_connection *, void *);
78 void *user_ctx;
79 SSL *pres_ssl; /* non-NULL when the shepherd already finished the handshake */
80 cwist_https_connection *conn; /* non-NULL when re-arming an established connection */
81};
82
83/* --- Thread Pool for HTTPS --- */
84/* The task queue is a ring that starts small and doubles when a burst fills
85 * it, up to HTTPS_TASK_QUEUE_MAX tasks; past that, submitters block until a
86 * worker frees a slot (the old fixed-size behaviour). It used to be a static
87 * HTTPS_TASK_QUEUE_MAX-entry array (~96 MiB) that https_pool_init()'s memset
88 * made resident in every worker process before the first connection. The
89 * ring never shrinks: a process keeps the capacity of its largest burst. */
90#define HTTPS_TASK_QUEUE_INITIAL 1024
91#define HTTPS_TASK_QUEUE_MAX 2097152
92
93typedef struct {
94 int client_fd;
96 void (*handler)(cwist_https_connection *, void *);
97 void *user_ctx;
98 SSL *pres_ssl;
100} https_pool_task_t;
101
102typedef struct {
103 pthread_t threads[2048];
104 size_t threads_size;
105 https_pool_task_t *queue; /* ring of queue_len slots; cwist_malloc'd, never GC-tracked */
106 size_t queue_len;
107 size_t head;
108 size_t tail;
109 size_t count;
110 pthread_mutex_t mutex;
111 pthread_cond_t cond_not_empty;
112 pthread_cond_t cond_not_full;
113 int shutdown;
114 bool resizing; /* a submitter is allocating a bigger ring outside the mutex */
115} https_thread_pool_t;
116
117static https_thread_pool_t g_https_pool;
118static bool g_https_pool_initialized = false;
119
120// Forward declaration of existing https_thread_handler
121static void *https_thread_handler(void *arg);
122void https_pool_submit_ready(int client_fd, SSL *pres_ssl, cwist_https_context *ctx,
123 void (*handler)(cwist_https_connection *, void *), void *user_ctx);
124static cwist_error_t https_wrap_established(cwist_https_context *ctx, int client_fd, SSL *ssl,
127void https_hs_shepherd_stop(void);
128static void https_park_stop(void);
129static void https_park_reopen(void);
130static void https_connection_teardown(cwist_https_connection *conn);
131static void https_park_teardown(cwist_https_connection *conn);
132/* Set by cwist_https_park() on the pool thread that parked its connection:
133 * the connection may already be served by another thread, so the pool
134 * thread must not touch it (or close it) after the handler returns. */
135static __thread bool t_https_parked = false;
136static void https_close_conn_cb(void *handle);
137
144static inline https_pool_task_t *resize_queue(https_pool_task_t *queue, size_t new_len) {
145 https_pool_task_t *old = g_https_pool.queue;
146 size_t count = g_https_pool.count;
147 if (count > 0) {
148 /* Two contiguous copies instead of a per-slot modulo walk: this is
149 * the only part of a resize that still runs under the mutex. */
150 size_t first = g_https_pool.queue_len - g_https_pool.head;
151 if (first > count) first = count;
152 memcpy(queue, old + g_https_pool.head, first * sizeof(*queue));
153 memcpy(queue + first, old, (count - first) * sizeof(*queue));
154 }
155 g_https_pool.queue = queue;
156 g_https_pool.queue_len = new_len;
157 g_https_pool.head = 0;
158 g_https_pool.tail = count;
159 return old;
160}
161
174static bool https_pool_reserve_locked(void) {
175 while (g_https_pool.count >= g_https_pool.queue_len) {
176 if (g_https_pool.resizing || g_https_pool.shutdown) return false;
177 if (g_https_pool.queue_len >= HTTPS_TASK_QUEUE_MAX) return false;
178 size_t new_len = g_https_pool.queue_len ? g_https_pool.queue_len * 2 : HTTPS_TASK_QUEUE_INITIAL;
180
181 g_https_pool.resizing = true;
182 pthread_mutex_unlock(&g_https_pool.mutex);
183 /* cwist_malloc, not cwist_alloc: every pool thread touches the ring,
184 * so it must not sit on one thread's full-GC sweep list. */
185 https_pool_task_t *queue = cwist_malloc(new_len * sizeof(*queue));
186 pthread_mutex_lock(&g_https_pool.mutex);
187 if (!queue || g_https_pool.shutdown) {
188 /* On shutdown https_pool_destroy() owns the ring; drop ours. */
189 cwist_free(queue);
190 g_https_pool.resizing = false;
191 pthread_cond_broadcast(&g_https_pool.cond_not_full);
192 return false;
193 }
194
195 /* Only this submitter grows the ring and nothing shrinks it, so
196 * queue_len is unchanged and new_len still exceeds count. */
197 https_pool_task_t *old = resize_queue(queue, new_len);
198 pthread_cond_broadcast(&g_https_pool.cond_not_full); /* slots are free now */
199 pthread_mutex_unlock(&g_https_pool.mutex);
201 pthread_mutex_lock(&g_https_pool.mutex);
202 /* resizing stays set until here so https_pool_destroy() cannot tear
203 * down the mutex while this thread is still outside it. */
204 g_https_pool.resizing = false;
205 pthread_cond_broadcast(&g_https_pool.cond_not_full);
206 /* Others may have filled the new slots meanwhile: loop rechecks. */
207 }
208 return true;
209}
210
219static void *https_pool_worker(void *arg) {
220 (void)arg;
221 while (1) {
222 pthread_mutex_lock(&g_https_pool.mutex);
223 while (g_https_pool.count == 0 && !g_https_pool.shutdown) {
224 pthread_cond_wait(&g_https_pool.cond_not_empty, &g_https_pool.mutex);
225 }
226 if (g_https_pool.shutdown) {
227 pthread_mutex_unlock(&g_https_pool.mutex);
228 break;
229 }
230 https_pool_task_t task = g_https_pool.queue[g_https_pool.head];
231 g_https_pool.head = (g_https_pool.head + 1) % g_https_pool.queue_len;
232 g_https_pool.count--;
233 pthread_cond_signal(&g_https_pool.cond_not_full);
234 pthread_mutex_unlock(&g_https_pool.mutex);
235
236 /* A resubmitted or resumed connection was tracked by whichever
237 * thread wrapped it; this thread serves it now, so its exit sweep
238 * (not the tracker's) is the one that may close it (#345). */
240
241 struct https_thread_payload *payload = cwist_alloc(sizeof(*payload));
242 if (payload) {
243 payload->client_fd = task.client_fd;
244 payload->ctx = task.ctx;
245 payload->handler = task.handler;
246 payload->user_ctx = task.user_ctx;
247 payload->pres_ssl = task.pres_ssl;
248 payload->conn = task.conn;
249 https_thread_handler(payload);
250 } else {
251 if (task.conn) {
253 } else {
254 if (task.pres_ssl) SSL_free(task.pres_ssl);
255 close(task.client_fd);
256 }
257 }
258 }
259 return NULL;
260}
261
269 if (g_https_pool_initialized) return 0;
270 https_park_reopen();
271 memset(&g_https_pool, 0, sizeof(g_https_pool));
272 pthread_mutex_init(&g_https_pool.mutex, NULL);
273 pthread_cond_init(&g_https_pool.cond_not_empty, NULL);
274 pthread_cond_init(&g_https_pool.cond_not_full, NULL);
275 for (int i = 0; i < get_optimal_thread_count(); i++) {
276 if (pthread_create(&g_https_pool.threads[i], NULL, https_pool_worker, NULL) != 0) {
277 return -1;
278 }
279 }
280 g_https_pool_initialized = true;
281 return 0;
282}
283
292void https_pool_submit(int client_fd, cwist_https_context *ctx,
293 void (*handler)(cwist_https_connection *, void *), void *user_ctx) {
294 https_pool_submit_ready(client_fd, NULL, ctx, handler, user_ctx);
295}
296
308void https_pool_submit_ready(int client_fd, SSL *pres_ssl, cwist_https_context *ctx,
309 void (*handler)(cwist_https_connection *, void *), void *user_ctx) {
310 pthread_mutex_lock(&g_https_pool.mutex);
311 while (!g_https_pool.shutdown && !https_pool_reserve_locked()) {
312 pthread_cond_wait(&g_https_pool.cond_not_full, &g_https_pool.mutex);
313 }
314 if (g_https_pool.shutdown) {
315 pthread_mutex_unlock(&g_https_pool.mutex);
316 if (pres_ssl) SSL_free(pres_ssl);
317 close(client_fd);
318 return;
319 }
320 g_https_pool.queue[g_https_pool.tail].client_fd = client_fd;
321 g_https_pool.queue[g_https_pool.tail].ctx = ctx;
322 g_https_pool.queue[g_https_pool.tail].handler = handler;
323 g_https_pool.queue[g_https_pool.tail].user_ctx = user_ctx;
324 g_https_pool.queue[g_https_pool.tail].pres_ssl = pres_ssl;
325 g_https_pool.queue[g_https_pool.tail].conn = NULL;
326 g_https_pool.tail = (g_https_pool.tail + 1) % g_https_pool.queue_len;
327 g_https_pool.count++;
328 pthread_cond_signal(&g_https_pool.cond_not_empty);
329 pthread_mutex_unlock(&g_https_pool.mutex);
330}
331
342 void (*handler)(cwist_https_connection *, void *), void *user_ctx) {
343 if (!conn) return;
344 pthread_mutex_lock(&g_https_pool.mutex);
345 while (!g_https_pool.shutdown && !https_pool_reserve_locked()) {
346 pthread_cond_wait(&g_https_pool.cond_not_full, &g_https_pool.mutex);
347 }
348 if (g_https_pool.shutdown) {
349 pthread_mutex_unlock(&g_https_pool.mutex);
351 return;
352 }
353 g_https_pool.queue[g_https_pool.tail].client_fd = conn->fd;
354 g_https_pool.queue[g_https_pool.tail].ctx = ctx;
355 g_https_pool.queue[g_https_pool.tail].handler = handler;
356 g_https_pool.queue[g_https_pool.tail].user_ctx = user_ctx;
357 g_https_pool.queue[g_https_pool.tail].pres_ssl = NULL;
358 g_https_pool.queue[g_https_pool.tail].conn = conn;
359 g_https_pool.tail = (g_https_pool.tail + 1) % g_https_pool.queue_len;
360 g_https_pool.count++;
361 pthread_cond_signal(&g_https_pool.cond_not_empty);
362 pthread_mutex_unlock(&g_https_pool.mutex);
363}
364
371 if (!g_https_pool_initialized) return;
373 https_park_stop();
374 pthread_mutex_lock(&g_https_pool.mutex);
375 g_https_pool.shutdown = 1;
376 pthread_cond_broadcast(&g_https_pool.cond_not_empty);
377 /* Wake submitters blocked on a full ring so they see shutdown, and wait
378 * out a submitter that is growing the ring outside the mutex: the ring
379 * and the mutex are torn down below. */
380 pthread_cond_broadcast(&g_https_pool.cond_not_full);
381 while (g_https_pool.resizing) pthread_cond_wait(&g_https_pool.cond_not_full, &g_https_pool.mutex);
382 pthread_mutex_unlock(&g_https_pool.mutex);
383 for (int i = 0; i < get_optimal_thread_count(); i++) {
384 pthread_join(g_https_pool.threads[i], NULL);
385 }
386 while (g_https_pool.count > 0) {
387 https_pool_task_t task = g_https_pool.queue[g_https_pool.head];
388 g_https_pool.head = (g_https_pool.head + 1) % g_https_pool.queue_len;
389 g_https_pool.count--;
390 if (task.conn) {
392 } else {
393 if (task.pres_ssl) SSL_free(task.pres_ssl);
394 if (task.client_fd >= 0) close(task.client_fd);
395 }
396 }
397 pthread_mutex_destroy(&g_https_pool.mutex);
398 pthread_cond_destroy(&g_https_pool.cond_not_empty);
399 pthread_cond_destroy(&g_https_pool.cond_not_full);
400 cwist_free(g_https_pool.queue);
401 g_https_pool.queue = NULL;
402 g_https_pool.queue_len = 0;
403
404 g_https_pool_initialized = false;
405}
406/* --- End Thread Pool --- */
407
408/* --- Idle connection park set ---------------------------------------------
409 * Why this exists: a pool thread serving a TLS connection used to wait in
410 * poll() for that connection's next request (30 s for HTTP/1.1 keep-alive,
411 * up to the HTTP/2 idle timeout). An idle client therefore held a whole
412 * pool thread, and with one or two pool threads per worker only a few dozen
413 * TLS connections could be served at once; the rest queued behind idle
414 * ones. A pool thread with nothing to read now parks the connection here
415 * and returns. One epoll thread per process watches every parked fd and
416 * resubmits a connection to the pool when bytes arrive, so an idle
417 * connection costs memory, not a thread.
418 *
419 * Each class keeps a FIFO list; every entry of a class gets the same idle
420 * budget at park time, so the list is (nearly) ordered by deadline and
421 * expiry only looks at the head. HTTP/1.1 entries are closed on expiry,
422 * exactly as the blocking header read closed them. HTTP/2 entries are
423 * resubmitted with park_expired set so the session sends GOAWAY and keeps
424 * its grace window, as before.
425 *
426 * Parking is disabled under full GC: its allocations are owned per thread,
427 * and a parked HTTP/2 session would move between threads with them.
428 * ------------------------------------------------------------------------- */
429#ifdef __linux__
430typedef struct https_parked {
432 uint64_t deadline_ms;
433 int cls;
434 bool linked;
435 struct https_parked *prev, *next;
437
438static struct {
439 pthread_mutex_t lock;
440 pthread_t thread;
441 int epoll_fd;
442 bool running;
443 /* Set by https_park_stop() until the next https_pool_init(): pool
444 * workers still run while the pool shuts down, and one that parked a
445 * connection after the stop would start a second park thread, setting
446 * running again under the thread being joined. */
447 bool closed;
448 pid_t owner;
449 https_parked_t *head[2], *tail[2];
450} g_park = {.lock = PTHREAD_MUTEX_INITIALIZER, .epoll_fd = -1};
451
457static void park_unlink_locked(https_parked_t *p) {
458 if (p->prev) p->prev->next = p->next;
459 else g_park.head[p->cls] = p->next;
460 if (p->next) p->next->prev = p->prev;
461 else g_park.tail[p->cls] = p->prev;
462 p->prev = p->next = NULL;
463 p->linked = false;
464}
465
472static void park_resubmit(cwist_https_connection *conn, bool expired) {
473 conn->park_expired = expired;
475}
476
484static void *https_park_thread(void *arg) {
485 (void)arg;
486 struct epoll_event events[1024];
487 while (1) {
488 /* epoll_fd is (re)assigned under the lock by https_park_start_locked(),
489 * so read it there together with running. */
491 bool running = g_park.running;
492 int epfd = g_park.epoll_fd;
494 if (!running) break;
495
496 int n = epoll_wait(epfd, events, 1024, 200);
497 for (int i = 0; i < n; i++) {
498 https_parked_t *p = (https_parked_t *)events[i].data.ptr;
500 bool mine = p->linked;
503 if (!mine) continue;
504 epoll_ctl(epfd, EPOLL_CTL_DEL, p->conn->fd, NULL);
505 /* Readable or hung up: the pool thread's read reports either. */
506 park_resubmit(p->conn, false);
507 cwist_free(p);
508 }
509
510 uint64_t now = cwist_https_now_ms();
511 for (int cls = 0; cls < 2; cls++) {
514 while (g_park.head[cls] && g_park.head[cls]->deadline_ms <= now) {
515 https_parked_t *p = g_park.head[cls];
517 p->next = expired;
518 expired = p;
519 }
521 while (expired) {
523 expired = p->next;
524 epoll_ctl(epfd, EPOLL_CTL_DEL, p->conn->fd, NULL);
526 https_park_teardown(p->conn);
527 } else {
528 park_resubmit(p->conn, true);
529 }
530 cwist_free(p);
531 }
532 }
533 }
534 return NULL;
535}
536
545static bool https_park_start_locked(void) {
546 pid_t pid = getpid();
547 if (g_park.running && g_park.owner == pid) return true;
548 if (g_park.closed && g_park.owner == pid) return false; /* pool is shutting down */
549 /* A forked worker inherits the parent's lists and epoll fd but not its
550 * thread; start its own set (the parent's parked connections are the
551 * parent's to serve). */
552 g_park.head[0] = g_park.head[1] = g_park.tail[0] = g_park.tail[1] = NULL;
553 g_park.running = false;
555 if (g_park.epoll_fd < 0) return false;
556 g_park.running = true;
557 g_park.owner = pid;
558 if (pthread_create(&g_park.thread, NULL, https_park_thread, NULL) != 0) {
559 g_park.running = false;
560 close(g_park.epoll_fd);
561 g_park.epoll_fd = -1;
562 return false;
563 }
564 return true;
565}
566
578static bool https_park_enabled(void) {
579 static int enabled = -1;
580 if (enabled < 0) {
581 const char *env = getenv("CWIST_HTTPS_PARK");
582 enabled = !(env && (env[0] == '0' || strcmp(env, "false") == 0));
583 }
584 return enabled;
585}
586
592static void https_park_teardown(cwist_https_connection *conn) {
594 https_connection_teardown(conn);
595}
596
598 if (!conn || conn->fd < 0 || !conn->pool_handler || !https_park_enabled()) return false;
599 if (!atomic_load(&g_cwist_running)) return false;
600 /* An idle connection with nothing buffered does not need its 16 KiB
601 * read buffer while parked; cwist_https_receive_request() allocates a
602 * fresh one when the connection is served again. Measured on the C100k
603 * gate this was ~4 KB of resident memory per idle connection. Done
604 * before the fd is armed below, while no other thread can see conn. */
605 if (conn->read_buf && conn->buf_len == 0) {
606 cwist_free(conn->read_buf);
607 conn->read_buf = NULL;
608 }
610 if (!p) return false;
611 memset(p, 0, sizeof(*p));
613 p->conn = conn;
615 p->deadline_ms = cwist_https_now_ms() + idle_ms;
616
620 cwist_free(p);
621 return false;
622 }
623 p->prev = g_park.tail[p->cls];
624 if (p->prev) p->prev->next = p;
625 else g_park.head[p->cls] = p;
626 g_park.tail[p->cls] = p;
627 p->linked = true;
628 conn->park_expired = false;
629 /* Register while holding the lock: once the fd is armed the park thread
630 * may resubmit the connection, and it takes this lock first. */
631 struct epoll_event ev = {.events = EPOLLIN | EPOLLRDHUP | EPOLLONESHOT, .data.ptr = p};
632 if (epoll_ctl(g_park.epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev) != 0) {
635 cwist_free(p);
636 return false;
637 }
638 t_https_parked = true;
640 return true;
641}
642
644static void https_park_reopen(void) {
646 g_park.closed = false;
648}
649
650static void https_park_stop(void) {
652 bool running = g_park.running && g_park.owner == getpid();
653 g_park.running = false;
654 if (running) g_park.closed = true;
656 if (!running) return;
657 pthread_join(g_park.thread, NULL);
658 for (int cls = 0; cls < 2; cls++) {
659 https_parked_t *p = g_park.head[cls];
660 while (p) {
661 https_parked_t *next = p->next;
662 https_park_teardown(p->conn);
663 cwist_free(p);
664 p = next;
665 }
666 g_park.head[cls] = g_park.tail[cls] = NULL;
667 }
668 close(g_park.epoll_fd);
669 g_park.epoll_fd = -1;
670}
671#else
673 (void)conn;
674 (void)idle_ms;
675 (void)cls;
676 return false;
677}
678
679static void https_park_stop(void) {}
680static void https_park_reopen(void) {}
681#endif
682
684 if (!conn || !conn->ssl || conn->buf_len > 0) return false;
685 if (SSL_pending(conn->ssl) > 0) return false;
686 struct pollfd pfd = {.fd = conn->fd, .events = POLLIN};
687 return poll(&pfd, 1, 0) == 0;
688}
689
690/* Re-arm immediate ACKs on a connected socket.
691 *
692 * Why: TCP_QUICKACK is not persistent - the kernel re-enters delayed-ACK
693 * (pingpong) mode during the TLS handshake's request/response segment
694 * pattern. A client that sends its TLS Finished and then holds the first
695 * HTTP request behind Nagle (no TCP_NODELAY: stock ab, many OpenSSL-based
696 * clients) waits for the ACK of the Finished; the server's delayed ACK
697 * holds it for ~40 ms, adding a fixed ~40-50 ms to every connection and
698 * capping churn throughput at ~20 handshakes/s/thread. Re-arming around
699 * the handshake keeps the ACK of the client's final flight immediate, so
700 * the request arrives right behind the handshake instead of one delayed-ACK
701 * window later. Best-effort: non-Linux or failure is fine, this only
702 * tunes ACK timing. */
703static void cwist_tcp_quickack(int fd) {
704#if defined(__linux__) && defined(TCP_QUICKACK)
705 int one = 1;
706 (void)setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one));
707#endif
708}
709
711 static uint64_t cached = 0;
712 if (!cached) {
714 const char *env = getenv("CWIST_HTTPS_IDLE_TIMEOUT_MS");
715 if (env && *env) {
716 char *end = NULL;
717 unsigned long long parsed = strtoull(env, &end, 10);
718 if (end != env && *end == '\0' && parsed > 0) v = (uint64_t)parsed;
719 }
720 cached = v;
721 }
722 return cached;
723}
724
735static SSL *cwist_https_ssl_new(cwist_https_context *ctx) {
736 if (!ctx) return NULL;
738 SSL *ssl = ctx->ctx ? SSL_new(ctx->ctx) : NULL;
740 return ssl;
741}
742
743/* --- TLS handshake shepherd ------------------------------------------------
744 * Why this exists: the pool parks one worker per connection for the whole
745 * connection lifetime. A synchronous in-worker handshake made that fatal
746 * under churn — a client that delays its ClientHello by a few seconds
747 * (routine for h2load at thousands of connections per process) parked a
748 * worker for the full 30 s poll, the pool collapsed to ~tens of
749 * handshakes/s, the accept loop fell behind, the kernel accept queue
750 * overflowed, and overflowing handshakes were dropped silently, leaving
751 * clients ESTABLISHED with no server-side socket ("phantom" connections).
752 *
753 * The shepherd owns every in-progress handshake: cwist_https_dispatch()
754 * tries SSL_accept once on a non-blocking socket; incomplete handshakes are
755 * parked in a private epoll set and retried by a shepherd thread, which also
756 * reaps connections whose handshake stalls for CWIST_HTTPS_HANDSHAKE_TIMEOUT_MS
757 * (any completed event round refreshes the budget — the deadline bounds
758 * stalls, not queueing, so healthy clients under connect bursts are never
759 * swept for waiting). Only fully established sessions enter the worker pool.
760 *
761 * The shepherd is sharded (CWIST_HTTPS_HS_MAX_SHARDS threads, hashed by fd):
762 * a single thread caps per-process handshake crypto at one core, and under
763 * C100k-scale connect bursts the backlog outlived the handshake deadline,
764 * resetting clients mid-connect ("errored" in h2load). Shards carry
765 * independent epoll sets, locks and pending lists, so throughput scales with
766 * cores exactly like the pre-shepherd pool path did.
767 *
768 * The shepherd is epoll-based and therefore Linux-only. On other platforms
769 * cwist_https_dispatch() falls back to the legacy blocking pool path. */
770
771#ifdef __linux__
772
773typedef struct https_hs_pending {
774 int fd;
775 SSL *ssl;
777 void (*handler)(cwist_https_connection *, void *);
778 void *user_ctx;
779 uint64_t deadline_ms;
780 struct https_hs_pending *prev, *next;
781 bool linked;
783
784typedef struct https_hs_shard {
785 int epoll_fd;
786 int wakeup_rd;
787 int wakeup_wr;
788 pthread_t thread;
789 /* Read lock-free by the shepherd loop and written by start/stop on other
790 * threads, so it must stay atomic. */
791 _Atomic bool running;
792 pthread_mutex_t lock;
793 /* Pending handshakes in deadline order: every entry is (re)queued at the
794 * tail with now + CWIST_HTTPS_HANDSHAKE_TIMEOUT_MS, so the sweep only
795 * looks at the head. */
796 https_hs_pending_t *head, *tail;
797 _Atomic uint32_t pending;
799
800/* One shepherd thread per shard; the count is fixed at start (see below). */
801#define CWIST_HTTPS_HS_MAX_SHARDS 16
802
804static long g_hs_shard_count = 0;
806
807/* Both called with sh->lock held; O(1), so the shepherd's per-event and
808 * sweep costs no longer grow with the number of pending handshakes. */
810 p->next = NULL;
811 p->prev = sh->tail;
812 if (sh->tail)
813 sh->tail->next = p;
814 else
815 sh->head = p;
816 sh->tail = p;
817 p->linked = true;
819}
820
822 if (!p->linked) return;
823 if (p->prev)
824 p->prev->next = p->next;
825 else
826 sh->head = p->next;
827 if (p->next)
828 p->next->prev = p->prev;
829 else
830 sh->tail = p->prev;
831 p->prev = p->next = NULL;
832 p->linked = false;
834}
835
837 if (sh->epoll_fd >= 0) epoll_ctl(sh->epoll_fd, EPOLL_CTL_DEL, p->fd, NULL);
838 SSL_free(p->ssl);
839 close(p->fd);
840 cwist_free(p);
841}
842
843/* Move a completed handshake into the request worker pool. */
845 if (sh->epoll_fd >= 0) epoll_ctl(sh->epoll_fd, EPOLL_CTL_DEL, p->fd, NULL);
846 /* The request phase (cwist_https_receive_request & friends) predates
847 * non-blocking sockets; restore blocking mode before handing over. */
848 int fl = fcntl(p->fd, F_GETFL, 0);
849 if (fl >= 0) fcntl(p->fd, F_SETFL, fl & ~O_NONBLOCK);
850 https_pool_submit_ready(p->fd, p->ssl, p->ctx, p->handler, p->user_ctx);
851 cwist_free(p);
852}
853
854static void *https_hs_shepherd(void *arg) {
856 struct epoll_event events[512];
857 while (sh->running) {
858 int n = epoll_wait(sh->epoll_fd, events, 512, 500);
859 if (n < 0) {
860 if (errno == EINTR) continue;
861 break;
862 }
863 uint64_t now = cwist_https_now_ms();
864 for (int i = 0; i < n; i++) {
865 if (events[i].data.ptr == NULL) {
866 /* wakeup pipe: drain so writers never block */
867 char drain[256];
868 while (read(sh->wakeup_rd, drain, sizeof(drain)) > 0) { /* discard */
869 }
870 continue;
871 }
872 https_hs_pending_t *p = events[i].data.ptr;
873
874 /* Detach from the list first: every event path below either
875 * re-arms (re-add) or finishes with p freed. */
876 pthread_mutex_lock(&sh->lock);
877 bool found = p->linked;
879 pthread_mutex_unlock(&sh->lock);
880 if (!found) continue; /* already reaped by the sweeper */
881
882 int rc = SSL_accept(p->ssl);
883 cwist_tcp_quickack(p->fd);
884 if (rc > 0) {
886 continue;
887 }
888 int ssl_err = SSL_get_error(p->ssl, rc);
890 now < p->deadline_ms) {
891 /* The deadline bounds *stall*, not queueing: every completed
892 * event round means the client is alive and the handshake is
893 * progressing, so refresh the budget. Under C100k-scale
894 * bursts a fixed from-accept deadline was consumed by queue
895 * delay alone and healthy handshakes were swept en masse. */
896 p->deadline_ms = now + CWIST_HTTPS_HANDSHAKE_TIMEOUT_MS;
897 struct epoll_event ev = {
899 .data.ptr = p,
900 };
901 pthread_mutex_lock(&sh->lock);
903 pthread_mutex_unlock(&sh->lock);
904 if (epoll_ctl(sh->epoll_fd, EPOLL_CTL_MOD, p->fd, &ev) != 0 &&
905 epoll_ctl(sh->epoll_fd, EPOLL_CTL_ADD, p->fd, &ev) != 0) {
906 pthread_mutex_lock(&sh->lock);
908 pthread_mutex_unlock(&sh->lock);
910 }
911 continue;
912 }
913 https_hs_abort(sh, p); /* hard failure or deadline exceeded */
914 }
915
916 /* Sweep expired handshakes that never became readable; the list is
917 * in deadline order, so stop at the first live one. */
918 uint64_t sweep_now = cwist_https_now_ms();
919 pthread_mutex_lock(&sh->lock);
920 while (sh->head && sweep_now >= sh->head->deadline_ms) {
923 pthread_mutex_unlock(&sh->lock);
925 pthread_mutex_lock(&sh->lock);
926 }
927 pthread_mutex_unlock(&sh->lock);
928 }
929 return NULL;
930}
931
933 pthread_mutex_lock(&sh->lock);
934 if (!sh->running) {
935 pthread_mutex_unlock(&sh->lock);
936 return;
937 }
938 sh->running = false;
939 if (sh->wakeup_wr >= 0) {
940 char b = 1;
941 if (write(sh->wakeup_wr, &b, 1) < 0) { /* shutdown path */
942 }
943 }
944 pthread_mutex_unlock(&sh->lock);
945 pthread_join(sh->thread, NULL);
946
947 pthread_mutex_lock(&sh->lock);
948 https_hs_pending_t *cur = sh->head;
949 sh->head = sh->tail = NULL;
950 pthread_mutex_unlock(&sh->lock);
951 while (cur) {
952 https_hs_pending_t *next = cur->next;
953 SSL_free(cur->ssl);
954 close(cur->fd);
956 cur = next;
957 }
958 if (sh->epoll_fd >= 0) close(sh->epoll_fd);
959 if (sh->wakeup_rd >= 0) close(sh->wakeup_rd);
960 if (sh->wakeup_wr >= 0) close(sh->wakeup_wr);
961 sh->epoll_fd = -1;
962 sh->wakeup_rd = sh->wakeup_wr = -1;
963}
964
965int https_hs_shepherd_start(void) {
967 if (g_hs_shard_count > 0) {
969 return 0;
970 }
971 /* Spread handshakes across as many shepherd threads as this process has
972 * request workers (bounded), matching the parallelism of the legacy
973 * blocking pool path without parking request workers on handshakes. */
975 if (want < 4) want = 4;
977 /* CWIST_HTTPS_HS_SHARDS overrides the computed count for connect-burst
978 * tuning; values outside [1, CWIST_HTTPS_HS_MAX_SHARDS] are ignored. */
979 const char *env = getenv("CWIST_HTTPS_HS_SHARDS");
980 if (env && *env) {
981 char *end = NULL;
982 long parsed = strtol(env, &end, 10);
983 if (end != env && *end == '\0' && parsed >= 1 && parsed <= CWIST_HTTPS_HS_MAX_SHARDS)
984 want = parsed;
985 }
986
987 long started = 0;
988 for (long i = 0; i < want; i++) {
990 memset(sh, 0, sizeof(*sh));
991 sh->epoll_fd = -1;
992 sh->wakeup_rd = sh->wakeup_wr = -1;
993 pthread_mutex_init(&sh->lock, NULL);
994 atomic_init(&sh->pending, 0);
995
996 sh->epoll_fd = epoll_create1(0);
997 if (sh->epoll_fd < 0) break;
998 int pfd[2];
999 if (pipe(pfd) != 0) {
1000 close(sh->epoll_fd);
1001 sh->epoll_fd = -1;
1002 break;
1003 }
1004 sh->wakeup_rd = pfd[0];
1005 sh->wakeup_wr = pfd[1];
1006 fcntl(sh->wakeup_rd, F_SETFL, O_NONBLOCK);
1007 fcntl(sh->wakeup_wr, F_SETFL, O_NONBLOCK);
1008 struct epoll_event ev = {.events = EPOLLIN, .data.ptr = NULL};
1009 epoll_ctl(sh->epoll_fd, EPOLL_CTL_ADD, sh->wakeup_rd, &ev);
1010 sh->running = true;
1011 if (pthread_create(&sh->thread, NULL, https_hs_shepherd, sh) != 0) {
1012 sh->running = false;
1013 close(sh->wakeup_rd);
1014 close(sh->wakeup_wr);
1015 close(sh->epoll_fd);
1016 sh->epoll_fd = -1;
1017 sh->wakeup_rd = sh->wakeup_wr = -1;
1018 break;
1019 }
1020 started++;
1021 }
1022 if (started == 0) {
1024 return -1;
1025 }
1026 g_hs_shard_count = started;
1028 return 0;
1029}
1030
1031void https_hs_shepherd_stop(void) {
1033 long count = g_hs_shard_count;
1034 g_hs_shard_count = 0;
1036 for (long i = 0; i < count; i++) {
1038 }
1039}
1040
1042 long total = 0;
1043 for (long i = 0; i < g_hs_shard_count; i++) {
1045 }
1046 return total;
1047}
1048
1056void cwist_https_dispatch(int client_fd, cwist_https_context *ctx,
1057 void (*handler)(cwist_https_connection *, void *), void *user_ctx) {
1058 if (!ctx || client_fd < 0) {
1059 if (client_fd >= 0) close(client_fd);
1060 return;
1061 }
1062 if (https_hs_shepherd_start() != 0) {
1063 /* No shepherd, no dispatch: fall back to the blocking pool path. */
1064 https_pool_submit(client_fd, ctx, handler, user_ctx);
1065 return;
1066 }
1067
1068 int fl = fcntl(client_fd, F_GETFL, 0);
1069 if (fl >= 0) fcntl(client_fd, F_SETFL, fl | O_NONBLOCK);
1070
1071 SSL *ssl = cwist_https_ssl_new(ctx);
1072 if (!ssl) {
1073 close(client_fd);
1074 return;
1075 }
1076 SSL_set_fd(ssl, client_fd);
1077
1078 https_hs_pending_t *p = cwist_alloc(sizeof(*p));
1079 if (!p) {
1080 SSL_free(ssl);
1081 close(client_fd);
1082 return;
1083 }
1084 p->fd = client_fd;
1085 p->ssl = ssl;
1086 p->ctx = ctx;
1087 p->handler = handler;
1088 p->user_ctx = user_ctx;
1089 p->deadline_ms = cwist_https_now_ms() + CWIST_HTTPS_HANDSHAKE_TIMEOUT_MS;
1090
1091 /* P2C (Power of Two Choices) Shard Selection:
1092 * Samples two shepherd shards and assigns to the one with lower pending count. */
1093 unsigned shard_idx = 0;
1094 if (g_hs_shard_count > 1) {
1096 for (long i = 0; i < g_hs_shard_count; i++) {
1099 }
1100 shard_idx =
1102 }
1103
1105 struct epoll_event ev = {
1106 .events = EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET,
1107 .data.ptr = p,
1108 };
1109
1110 pthread_mutex_lock(&sh->lock);
1112 pthread_mutex_unlock(&sh->lock);
1113
1114 if (epoll_ctl(sh->epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) != 0) {
1115 pthread_mutex_lock(&sh->lock);
1117 pthread_mutex_unlock(&sh->lock);
1118 SSL_free(ssl);
1119 close(client_fd);
1120 cwist_free(p);
1121 return;
1122 }
1123 /* Wake the shepherd so pending handshakes are processed promptly. */
1124 char b = 1;
1125 if (write(sh->wakeup_wr, &b, 1) < 0) { /* non-fatal */
1126 }
1127}
1128
1129#else /* !__linux__ */
1130
1132 return -1;
1133}
1135
1137 return 0;
1138}
1139
1140/* Non-Linux fallback: no epoll shepherd, use the legacy blocking pool path. */
1142 void (*handler)(cwist_https_connection *, void *), void *user_ctx) {
1143 if (!ctx || client_fd < 0) {
1144 if (client_fd >= 0) close(client_fd);
1145 return;
1146 }
1147 https_pool_submit(client_fd, ctx, handler, user_ctx);
1148}
1149
1150#endif /* __linux__ */
1151
1152#define CWIST_ALPN_HTTP11 ((const unsigned char *)"\x08http/1.1")
1153#define CWIST_ALPN_H2_HTTP11 ((const unsigned char *)"\x02h2\x08http/1.1")
1154#define CWIST_ALPN_H3_H2_HTTP11 ((const unsigned char *)"\x02h3\x02h2\x08http/1.1")
1155#define CWIST_ALPN_HTTP11_LEN 9
1156#define CWIST_ALPN_H2_HTTP11_LEN 12
1157#define CWIST_ALPN_H3_H2_HTTP11_LEN 15
1158
1169static cwist_error_t make_ssl_error(const char *msg) {
1171 err.error.err_json = cJSON_CreateObject();
1172
1173 unsigned long ssl_err = ERR_get_error();
1174 char buf[256];
1175 ERR_error_string_n(ssl_err, buf, sizeof(buf));
1176
1177 cJSON_AddStringToObject(err.error.err_json, "module", "https");
1178 cJSON_AddStringToObject(err.error.err_json, "message", msg);
1179 cJSON_AddStringToObject(err.error.err_json, "openssl_error", buf);
1180
1181 return err;
1182}
1183
1191static void cwist_https_apply_base_tls_defaults(SSL_CTX *ssl_ctx) {
1193#ifdef SSL_OP_NO_RENEGOTIATION
1195#endif
1197
1198 /* Session resumption. TLS 1.3 tickets are stateless and already on by
1199 * default; this additionally enables the server-side session cache for
1200 * TLS 1.2 session IDs and sets the session-id context without which
1201 * OpenSSL refuses to resume cached sessions. Combined with HTTP/1.1
1202 * keep-alive this removes most full (asymmetric) handshakes. */
1205 SSL_CTX_set_session_id_context(ssl_ctx, (const unsigned char *)"cwist-v1", 8);
1206 SSL_CTX_set_timeout(ssl_ctx, 300);
1207}
1208
1209static cwist_error_t cwist_https_apply_http2_tls_profile(SSL_CTX *ssl_ctx) {
1211 if (SSL_CTX_set_cipher_list(ssl_ctx, "ECDHE-ECDSA-AES128-GCM-SHA256:"
1212 "ECDHE-RSA-AES128-GCM-SHA256:"
1213 "ECDHE-ECDSA-AES256-GCM-SHA384:"
1214 "ECDHE-RSA-AES256-GCM-SHA384:"
1215 "ECDHE-ECDSA-CHACHA20-POLY1305:"
1216 "ECDHE-RSA-CHACHA20-POLY1305") != 1) {
1217 return make_ssl_error("Unable to apply HTTP/2-compatible TLS 1.2 cipher profile");
1218 }
1219#ifdef SSL_CTX_set_ciphersuites
1220 if (SSL_CTX_set_ciphersuites(ssl_ctx, "TLS_AES_128_GCM_SHA256:"
1221 "TLS_AES_256_GCM_SHA384:"
1222 "TLS_CHACHA20_POLY1305_SHA256") != 1) {
1223 return make_ssl_error("Unable to apply HTTP/2-compatible TLS 1.3 cipher suites");
1224 }
1225#endif
1227 err.error.err_i16 = 0;
1228 return err;
1229}
1230
1231static int cwist_https_alpn_select_cb(SSL *ssl, const unsigned char **out, unsigned char *outlen,
1232 const unsigned char *in, unsigned int inlen, void *arg) {
1233 (void)ssl;
1234 const cwist_https_context *hctx = (const cwist_https_context *)arg;
1235 bool enable_http2 = hctx && hctx->http2_enabled;
1236
1237 const unsigned char *supported;
1238 unsigned int supported_len;
1239
1240 if (enable_http2) {
1243 } else {
1246 }
1247
1248 if (SSL_select_next_proto((unsigned char **)out, outlen, supported, supported_len, in, inlen) ==
1250 return SSL_TLSEXT_ERR_OK;
1251 }
1252
1253 return SSL_TLSEXT_ERR_NOACK;
1254}
1255
1256/* --- Context Management --- */
1257
1258/* --- Shared session ticket key -------------------------------------------
1259 * cwist_app_listen() forks one worker per CPU core. BoringSSL/OpenSSL
1260 * generate their internal session-ticket keys lazily on first use, i.e.
1261 * AFTER the fork, so every worker would mint its own key and tickets issued
1262 * by one worker could never be resumed on another (SO_REUSEPORT scatters
1263 * connections across workers). Instead we generate one random key at
1264 * context creation (in the parent, pre-fork) and install an explicit
1265 * ticket-key callback; every forked worker inherits identical key material
1266 * and tickets resume regardless of which worker accepts the connection.
1267 *
1268 * Security trade-off — process-lifetime STEK (tracked in issue #98):
1269 *
1270 * The key lives for the process lifetime (no rotation), the same design
1271 * nginx uses with a static `ssl_session_ticket_key` file. That is a
1272 * deliberate choice: rotation in a pre-fork model requires either a shared
1273 * mmap region with locking (cross-process visibility) or a control socket
1274 * from a parent/daemon process — significant complexity for a server that
1275 * is typically restarted nightly anyway.
1276 *
1277 * Actual exposure with the current codebase:
1278 *
1279 * 1. 0-RTT / early-data replay does NOT apply here. CWIST never calls
1280 * SSL_CTX_set_max_early_data() or equivalent, so early data is
1281 * disabled at the library level and TLS 1.3 0-RTT is never offered.
1282 *
1283 * 2. What remains is the blast-radius argument: if the STEK leaks (memory
1284 * disclosure, core dump, cold-boot) an attacker can decrypt any
1285 * session ticket issued by this process for the rest of its uptime.
1286 * Rotation would cap the valid window to the rotation interval.
1287 *
1288 * Mitigations short of full rotation:
1289 * - Enable perfect forward secrecy by preferring ECDHE cipher suites
1290 * (already the default here via BoringSSL's suite ordering) so that
1291 * bulk traffic keys are independent of the STEK even if the STEK leaks.
1292 * - Restart the server on a short cadence (e.g. daily) if the uptime
1293 * window is a concern for your threat model; a new process generates a
1294 * fresh STEK via RAND_bytes() in cwist_tls_setup_shared_ticket_key().
1295 * - Disable ticket resumption entirely (SSL_CTX_set_options with
1296 * SSL_OP_NO_TICKET) to opt into full handshakes on every connection.
1297 *
1298 * If in-process periodic rotation is ever added, the right approach is:
1299 * a) Generate a new key on a timer (e.g. every hour).
1300 * b) Keep the *previous* key for one rotation interval so in-flight
1301 * tickets from the overlap window can still decrypt.
1302 * c) Use an atomic pointer swap or reader-writer lock to avoid a
1303 * race between the rotate timer and cwist_tls_ticket_key_cb().
1304 *
1305 * Certificate hot reload (cwist_https_reload_context()) does NOT rotate this
1306 * key: the reloaded SSL_CTX carries the same STEK so session resumption
1307 * survives the swap. See docs/decisions/0002-tls-hot-reload-stek.md.
1308 * ------------------------------------------------------------------------- */
1309typedef struct cwist_tls_ticket_key {
1310 unsigned char name[16]; /* key_name sent in the ticket */
1311 unsigned char aes_key[32]; /* AES-256-CBC encryption key */
1312 unsigned char hmac_key[32]; /* HMAC-SHA-256 MAC key */
1314
1315static int g_ticket_key_ex_data_idx = -1;
1316static pthread_once_t g_ticket_key_ex_data_once = PTHREAD_ONCE_INIT;
1317
1318static void cwist_tls_ticket_key_ex_data_init(void) {
1319 g_ticket_key_ex_data_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
1320}
1321
1322static int cwist_tls_ticket_key_cb(SSL *ssl, uint8_t *key_name, uint8_t *iv, EVP_CIPHER_CTX *ectx,
1323 HMAC_CTX *hctx, int encrypt) {
1324 SSL_CTX *ssl_ctx = ssl ? SSL_get_SSL_CTX(ssl) : NULL;
1325 const cwist_tls_ticket_key *key = NULL;
1326 if (ssl_ctx && g_ticket_key_ex_data_idx >= 0) {
1327 key = (const cwist_tls_ticket_key *)SSL_CTX_get_ex_data(ssl_ctx, g_ticket_key_ex_data_idx);
1328 }
1329 if (!key) return 0;
1330
1331 if (encrypt) {
1332 memcpy(key_name, key->name, sizeof(key->name));
1333 if (RAND_bytes(iv, 16) != 1) return 0;
1334 if (EVP_EncryptInit_ex(ectx, EVP_aes_256_cbc(), NULL, key->aes_key, iv) != 1) return 0;
1335 if (HMAC_Init_ex(hctx, key->hmac_key, sizeof(key->hmac_key), EVP_sha256(), NULL) != 1)
1336 return 0;
1337 return 1;
1338 }
1339
1340 if (memcmp(key_name, key->name, sizeof(key->name)) != 0) {
1341 return 0; /* Unknown key: decline the ticket, do a full handshake. */
1342 }
1343 if (EVP_DecryptInit_ex(ectx, EVP_aes_256_cbc(), NULL, key->aes_key, iv) != 1) return 0;
1344 if (HMAC_Init_ex(hctx, key->hmac_key, sizeof(key->hmac_key), EVP_sha256(), NULL) != 1) return 0;
1345 return 1;
1346}
1347
1353static void cwist_tls_setup_shared_ticket_key(SSL_CTX *ssl_ctx, void **out_key) {
1354 *out_key = NULL;
1355 pthread_once(&g_ticket_key_ex_data_once, cwist_tls_ticket_key_ex_data_init);
1356 if (g_ticket_key_ex_data_idx < 0) return;
1357
1359 if (!key) return;
1360 if (RAND_bytes((uint8_t *)key, sizeof(*key)) != 1) {
1361 cwist_free(key);
1362 return; /* Fall back to the library-internal (per-worker) keys. */
1363 }
1364 SSL_CTX_set_ex_data(ssl_ctx, g_ticket_key_ex_data_idx, key);
1365 SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, cwist_tls_ticket_key_cb);
1366 *out_key = key;
1367}
1368
1370 const char *key_path) {
1371 return cwist_https_init_context_with_options(ctx, cert_path, key_path, NULL, NULL);
1372}
1373
1387static cwist_error_t cwist_https_build_ssl_ctx(SSL_CTX **out, const char *cert_path,
1388 const char *key_path,
1389 const cwist_https_options *options,
1390 cwist_app *app) {
1392 bool enable_http3 = options && options->enable_http3;
1393 bool enable_http2 = options && (options->enable_http2 || enable_http3);
1394
1395 // Initialize OpenSSL
1398
1399 const SSL_METHOD *method = TLS_server_method();
1400 SSL_CTX *ssl_ctx = SSL_CTX_new(method);
1401 if (!ssl_ctx) {
1402 return make_ssl_error("Unable to create SSL context");
1403 }
1404
1405 if (SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION) != 1) {
1406 SSL_CTX_free(ssl_ctx);
1407 return make_ssl_error("Unable to enforce TLS minimum version");
1408 }
1409 cwist_https_apply_base_tls_defaults(ssl_ctx);
1410
1411 if (!cwist_tls_apply_pqc_layer(app, ssl_ctx)) {
1412 SSL_CTX_free(ssl_ctx);
1413 return make_ssl_error("PQC layer configuration failed");
1414 }
1415
1416 if (enable_http2) {
1417 err = cwist_https_apply_http2_tls_profile(ssl_ctx);
1418 if (!cwist_error_is_ok(&err)) {
1419 SSL_CTX_free(ssl_ctx);
1420 return err;
1421 }
1422 }
1423
1424 // Load Cert and Key
1425 if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) <= 0) {
1426 SSL_CTX_free(ssl_ctx);
1427 return make_ssl_error("Unable to load certificate");
1428 }
1429
1430 if (cwist_tls_autoload_intermediates(ssl_ctx) < 0) {
1431 SSL_CTX_free(ssl_ctx);
1432 return make_ssl_error("Unable to complete certificate chain");
1433 }
1434
1435 if (SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
1436 SSL_CTX_free(ssl_ctx);
1437 return make_ssl_error("Unable to load private key");
1438 }
1439
1440 // Verify key matches cert
1441 if (!SSL_CTX_check_private_key(ssl_ctx)) {
1442 SSL_CTX_free(ssl_ctx);
1443 return make_ssl_error("Private key does not match certificate");
1444 }
1445
1446 *out = ssl_ctx;
1447 err.error.err_i16 = 0;
1448 return err;
1449}
1450
1452 const char *cert_path, const char *key_path,
1453 const cwist_https_options *options,
1454 cwist_app *app) {
1456 bool enable_http3 = options && options->enable_http3;
1457 bool enable_http2 = options && (options->enable_http2 || enable_http3);
1458
1459 if (!ctx || !cert_path || !key_path) {
1460 err.error.err_i16 = -1;
1461 return err;
1462 }
1463
1464 SSL_CTX *ssl_ctx = NULL;
1465 err = cwist_https_build_ssl_ctx(&ssl_ctx, cert_path, key_path, options, app);
1466 if (!cwist_error_is_ok(&err)) {
1467 return err;
1468 }
1469
1471 if (!*ctx) {
1472 SSL_CTX_free(ssl_ctx);
1473 err.error.err_i16 = -1;
1474 return err;
1475 }
1476 (*ctx)->ctx = ssl_ctx;
1477 (*ctx)->http2_enabled = enable_http2;
1478 (*ctx)->http3_enabled = enable_http3;
1479 (*ctx)->ticket_key = NULL;
1480 pthread_rwlock_init(&(*ctx)->ctx_lock, NULL);
1481
1482 /* Pre-fork shared ticket key so every forked worker can resume tickets
1483 * issued by any other worker. Must run before workers are forked. */
1484 cwist_tls_setup_shared_ticket_key(ssl_ctx, &(*ctx)->ticket_key);
1485
1486 SSL_CTX_set_alpn_select_cb(ssl_ctx, cwist_https_alpn_select_cb, (void *)*ctx);
1487
1488 err.error.err_i16 = 0; // Success
1489 return err;
1490}
1491
1509 const char *key_path, cwist_app *app) {
1511 if (!ctx || !cert_path || !key_path) {
1512 err.error.err_i16 = -1;
1513 return err;
1514 }
1515
1517 .enable_http3 = ctx->http3_enabled};
1518 SSL_CTX *new_ctx = NULL;
1519 err = cwist_https_build_ssl_ctx(&new_ctx, cert_path, key_path, &options, app);
1520 if (!cwist_error_is_ok(&err)) {
1521 return err;
1522 }
1523
1524 /* Carry the shared STEK over: the wrapper owns it and outlives every
1525 * SSL_CTX it is installed on (it is freed only by
1526 * cwist_https_destroy_context(), after the pool is drained). The once
1527 * gate runs here because the reloading thread may not be the one that
1528 * created the context. */
1529 pthread_once(&g_ticket_key_ex_data_once, cwist_tls_ticket_key_ex_data_init);
1530 if (ctx->ticket_key && g_ticket_key_ex_data_idx >= 0) {
1531 SSL_CTX_set_ex_data(new_ctx, g_ticket_key_ex_data_idx, ctx->ticket_key);
1532 SSL_CTX_set_tlsext_ticket_key_cb(new_ctx, cwist_tls_ticket_key_cb);
1533 } else if (!ctx->ticket_key) {
1534 /* No shared key was ever armed: do it now so the reloaded context
1535 * matches a fresh one. */
1536 cwist_tls_setup_shared_ticket_key(new_ctx, &ctx->ticket_key);
1537 }
1538
1539 SSL_CTX_set_alpn_select_cb(new_ctx, cwist_https_alpn_select_cb, (void *)ctx);
1540
1542 SSL_CTX *old_ctx = ctx->ctx;
1543 ctx->ctx = new_ctx;
1545
1546 /* Drops only the listener's reference; in-flight handshakes and
1547 * established connections hold their own. */
1549
1550 err.error.err_i16 = 0;
1551 return err;
1552}
1553
1559 if (ctx) {
1560 if (ctx->ctx) {
1561 SSL_CTX_free(ctx->ctx);
1562 }
1564 if (ctx->ticket_key) {
1565 /* Scrub key material before releasing it. */
1567 cwist_free(ctx->ticket_key);
1568 }
1569 cwist_free(ctx);
1570 /* EVP_cleanup() intentionally not called: it is deprecated since
1571 * OpenSSL 1.1.0 and tears down global state other code may use. */
1572 }
1573}
1574
1582/* --- TLS observability counters ------------------------------------------
1583 * Owned here (not in the metrics registry) so the hot path never depends
1584 * on the sys/metrics layer; metrics.c mirrors them into the Prometheus
1585 * exposition on load/render, same pattern as
1586 * cwist_http_continuation_shed_count() in http.c. */
1587static _Atomic long g_tls_handshakes_total;
1588static _Atomic long g_tls_handshakes_resumed;
1589static _Atomic long g_tls_handshakes_tls12;
1590static _Atomic long g_tls_handshakes_tls13;
1591static _Atomic long g_tls_ciphers_aes128_gcm;
1592static _Atomic long g_tls_ciphers_aes256_gcm;
1593static _Atomic long g_tls_ciphers_chacha20;
1594static _Atomic long g_tls_ciphers_other;
1595static _Atomic long g_tls_connections_active;
1596
1598 return atomic_load_explicit(&g_tls_handshakes_total, memory_order_relaxed);
1599}
1600
1602 return atomic_load_explicit(&g_tls_handshakes_resumed, memory_order_relaxed);
1603}
1604
1606 return atomic_load_explicit(&g_tls_handshakes_tls12, memory_order_relaxed);
1607}
1608
1610 return atomic_load_explicit(&g_tls_handshakes_tls13, memory_order_relaxed);
1611}
1612
1614 return atomic_load_explicit(&g_tls_ciphers_aes128_gcm, memory_order_relaxed);
1615}
1616
1618 return atomic_load_explicit(&g_tls_ciphers_aes256_gcm, memory_order_relaxed);
1619}
1620
1622 return atomic_load_explicit(&g_tls_ciphers_chacha20, memory_order_relaxed);
1623}
1624
1626 return atomic_load_explicit(&g_tls_ciphers_other, memory_order_relaxed);
1627}
1628
1630 return atomic_load_explicit(&g_tls_connections_active, memory_order_relaxed);
1631}
1632
1633/* Record a freshly established TLS session: version/cipher buckets and
1634 * resumption. Called from https_wrap_established only, which both the
1635 * in-worker handshake path (cwist_https_accept) and the shepherd's
1636 * pre-handshaked dispatch path funnel through. */
1637static void https_tls_record_handshake(SSL *ssl) {
1638 atomic_fetch_add_explicit(&g_tls_handshakes_total, 1, memory_order_relaxed);
1639 if (SSL_session_reused(ssl))
1640 atomic_fetch_add_explicit(&g_tls_handshakes_resumed, 1, memory_order_relaxed);
1641
1642 switch (SSL_version(ssl)) {
1643 case TLS1_2_VERSION:
1644 atomic_fetch_add_explicit(&g_tls_handshakes_tls12, 1, memory_order_relaxed);
1645 break;
1646 case TLS1_3_VERSION:
1647 atomic_fetch_add_explicit(&g_tls_handshakes_tls13, 1, memory_order_relaxed);
1648 break;
1649 default: break;
1650 }
1651
1652 const char *cipher = SSL_get_cipher_name(ssl);
1653 if (cipher) {
1654 if (strcmp(cipher, "TLS_AES_128_GCM_SHA256") == 0)
1655 atomic_fetch_add_explicit(&g_tls_ciphers_aes128_gcm, 1, memory_order_relaxed);
1656 else if (strcmp(cipher, "TLS_AES_256_GCM_SHA384") == 0)
1657 atomic_fetch_add_explicit(&g_tls_ciphers_aes256_gcm, 1, memory_order_relaxed);
1658 else if (strcmp(cipher, "TLS_CHACHA20_POLY1305_SHA256") == 0)
1659 atomic_fetch_add_explicit(&g_tls_ciphers_chacha20, 1, memory_order_relaxed);
1660 else
1661 atomic_fetch_add_explicit(&g_tls_ciphers_other, 1, memory_order_relaxed);
1662 }
1663}
1664
1665/* Build the connection wrapper around an already-established TLS session.
1666 * Shared by the blocking cwist_https_accept path and the shepherd's
1667 * non-blocking dispatch path. */
1668static cwist_error_t https_wrap_established(cwist_https_context *ctx, int client_fd, SSL *ssl,
1669 cwist_https_connection **conn) {
1671
1673 if (!*conn) {
1674 err.error.err_i16 = -1;
1675 return err;
1676 }
1677
1678 memset(*conn, 0, sizeof(**conn));
1679 (*conn)->fd = client_fd;
1680 (*conn)->ssl = ssl;
1681 (*conn)->read_buf = cwist_alloc(CWIST_HTTP_READ_BUFFER_SIZE);
1682 if (!(*conn)->read_buf) {
1683 cwist_free(*conn);
1684 *conn = NULL;
1685 err.error.err_i16 = -1;
1686 return err;
1687 }
1688 (*conn)->buf_len = 0;
1689 (*conn)->read_buf[0] = '\0';
1690 (*conn)->negotiated_http2 = false;
1691 (*conn)->negotiated_protocol = CWIST_HTTPS_PROTOCOL_HTTP11;
1692 (*conn)->http3_enabled = ctx->http3_enabled;
1693 (*conn)->deferred = false;
1694
1695 if (cwist_full_gc_enabled()) {
1696 /* *conn and its read_buf were just handed to the connection
1697 * registry's close_fn (below) as their sole eventual releaser.
1698 * They must NOT also sit on this thread's generic cwist_alloc
1699 * pending-sweep list: that list's own thread-exit destructor runs
1700 * independently of (and in unspecified order relative to) the
1701 * connection registry's destructor, so if it fired first it would
1702 * cwist_ebr_free() *conn out from under the registry's still-
1703 * pending entry -- the registry's sweep would then read a freed
1704 * conn->ssl. Disowning here hands sole ownership to the registry,
1705 * exactly the cross-list handoff cwist_gc_scope_disown() exists
1706 * for (see io_queue.c's job-handoff use of the same call). */
1707 cwist_gc_scope_disown(*conn);
1708 cwist_gc_scope_disown((*conn)->read_buf);
1709 /* Full-GC's safety net: if this connection is never explicitly
1710 * closed (worker crashes mid-request, an error path forgets), the
1711 * owning thread's exit -- or, failing that, process exit -- still
1712 * closes it instead of leaking the fd/TLS session. Normal explicit
1713 * close (cwist_https_close_connection) untracks first, so this is
1714 * a no-op on the common path. Registering here (rather than only
1715 * in cwist_https_accept) covers the shepherd's pre-handshaked
1716 * dispatch path too, which calls this function directly. */
1717 cwist_conn_registry_track(*conn, https_close_conn_cb);
1718 }
1719
1720 const unsigned char *alpn = NULL;
1721 unsigned int alpn_len = 0;
1723 if (alpn && alpn_len == 2 && memcmp(alpn, "h2", 2) == 0) {
1724 (*conn)->negotiated_http2 = true;
1725 (*conn)->negotiated_protocol = CWIST_HTTPS_PROTOCOL_HTTP2;
1726 }
1727 /* h3 is QUIC-only and never negotiated over TCP TLS */
1728
1729 /* The handshake's final client flight was just ACKed; stay in quickack
1730 * mode so the request that follows (held back by a Nagle-bound client)
1731 * is ACKed immediately and the connection does not stall one
1732 * delayed-ACK window before the request phase even starts. */
1733 cwist_tcp_quickack(client_fd);
1734
1735 /* Both handshake paths funnel through here, so this is the single
1736 * choke point for TLS observability (issue #306). */
1737 https_tls_record_handshake(ssl);
1738 atomic_fetch_add_explicit(&g_tls_connections_active, 1, memory_order_relaxed);
1739
1740 err.error.err_i16 = 0;
1741 return err;
1742}
1743
1752static void https_close_conn_cb(void *handle) {
1753 https_connection_teardown((cwist_https_connection *)handle);
1754}
1755
1757 cwist_https_connection **conn) {
1759
1760 if (!ctx || client_fd < 0) {
1761 err.error.err_i16 = -1;
1762 return err;
1763 }
1764
1765 SSL *ssl = cwist_https_ssl_new(ctx);
1766 if (!ssl) {
1767 return make_ssl_error("Failed to create SSL structure");
1768 }
1769
1770 SSL_set_fd(ssl, client_fd);
1771 cwist_tcp_quickack(client_fd);
1772
1773 /* Bound the whole handshake so a client dribbling bytes cannot pin a
1774 * pool worker forever. */
1776 int rc;
1777 while ((rc = SSL_accept(ssl)) <= 0) {
1778 /* Keep immediate ACKs armed: the kernel drops out of quickack mode
1779 * as the handshake segments flow, and the delayed ACK of the
1780 * client's Finished would hold a Nagle-bound request by ~40 ms. */
1781 cwist_tcp_quickack(client_fd);
1782 int ssl_err = SSL_get_error(ssl, rc);
1784 uint64_t now = cwist_https_now_ms();
1785 if (now >= handshake_deadline) {
1786 cwist_error_t err_obj = make_ssl_error("SSL handshake timed out");
1787 SSL_free(ssl);
1788 return err_obj;
1789 }
1793 if (cwist_ssl_wait(client_fd, ssl_err, wait_ms) != 0) {
1794 cwist_error_t err_obj = make_ssl_error("SSL handshake timed out or socket error");
1795 SSL_free(ssl);
1796 return err_obj;
1797 }
1798 continue;
1799 }
1800 cwist_error_t err_obj = make_ssl_error("SSL handshake failed");
1801 SSL_free(ssl);
1802 return err_obj;
1803 }
1804
1805 cwist_error_t wrap_err = https_wrap_established(ctx, client_fd, ssl, conn);
1806 if (!cwist_error_is_ok(&wrap_err)) {
1807 SSL_free(ssl);
1808 return wrap_err;
1809 }
1810 return wrap_err;
1811}
1812
1820
1825
1837static void https_connection_teardown(cwist_https_connection *conn) {
1838 if (conn) {
1839 /* Balances the increment at wrap time; teardown is the single exit
1840 * point for every established connection (public close and the
1841 * full-GC registry sweep alike). */
1842 atomic_fetch_sub_explicit(&g_tls_connections_active, 1, memory_order_relaxed);
1843 if (conn->proto_state && conn->proto_state_free) {
1844 conn->proto_state_free(conn->proto_state);
1845 conn->proto_state = NULL;
1846 }
1847 if (conn->ssl) {
1848 SSL_shutdown(conn->ssl);
1849 SSL_free(conn->ssl);
1850 }
1851 if (conn->fd >= 0) {
1852 /* close() on a socket with unread receive-queue data makes the
1853 * kernel answer with RST instead of a graceful FIN. TLS clients
1854 * routinely send their close_notify (or a pipelined next request)
1855 * that the request path never consumed, so under connection churn
1856 * nearly every teardown became an abortive close: the peer's
1857 * kernel flushes queued response bytes on the RST (the "read
1858 * error" burst load clients report) and the socket never settles
1859 * through TIME_WAIT normally. Drain whatever is pending before
1860 * closing; the socket is about to be destroyed anyway. */
1861 char drain[2048];
1862 ssize_t n;
1863 int guard = 16;
1864 while (guard-- > 0 && (n = recv(conn->fd, drain, sizeof(drain), MSG_DONTWAIT)) > 0) {
1865 /* discard */
1866 }
1867 (void)n;
1868 close(conn->fd);
1869 }
1870 cwist_free(conn->read_buf);
1871 cwist_free(conn);
1872 }
1873}
1874
1876 if (conn && cwist_full_gc_enabled()) {
1878 }
1879 https_connection_teardown(conn);
1880}
1881
1888 if (!conn || !conn->ssl) return NULL;
1889 if (!conn->read_buf) {
1890 /* Released when the connection was parked (cwist_https_park()). */
1892 if (!conn->read_buf) return NULL;
1893 /* Same ownership as at wrap time: the conn's teardown frees it, so
1894 * it must not sit on this thread's full-GC sweep list. */
1896 conn->buf_len = 0;
1897 conn->read_buf[0] = '\0';
1898 }
1899
1900 size_t total_received = conn->buf_len;
1901 char *header_end = NULL;
1902
1903 /* Total deadline for assembling the request headers. */
1905
1906 /* Rescanning from offset 0 on every read is O(n^2) over dribbled
1907 * headers; resume 3 bytes back so a terminator split across two reads
1908 * is still found. */
1909 size_t scan_from = 0;
1910 bool deadline_grace_used = false;
1911
1912 while (1) {
1913 header_end = strstr(conn->read_buf + scan_from, "\r\n\r\n");
1914 if (header_end) break;
1915 scan_from = total_received > 3 ? total_received - 3 : 0;
1916
1918 return NULL;
1919 }
1920
1921 if (SSL_pending(conn->ssl) == 0) {
1922 uint64_t now = cwist_https_now_ms();
1923 if (now >= headers_deadline) {
1924 /* A request may have landed just as the idle deadline
1925 * expired; dropping it here makes strict clients surface a
1926 * network protocol error where lenient ones silently retry.
1927 * Give the wire one non-blocking chance before closing.
1928 * Bounded to a single retry so a dribbling peer cannot pin
1929 * the worker past the deadline. */
1930 bool readable = false;
1931 if (!deadline_grace_used) {
1932 deadline_grace_used = true;
1933 struct pollfd gfd = {.fd = conn->fd, .events = POLLIN};
1934 readable = (poll(&gfd, 1, 0) > 0 && (gfd.revents & POLLIN));
1935 }
1936 if (!readable) {
1937 return NULL;
1938 }
1939 } else {
1943
1944 struct pollfd pfd = {.fd = conn->fd, .events = POLLIN};
1945 int pret = poll(&pfd, 1, wait_ms);
1946 if (pret <= 0) {
1947 return NULL;
1948 }
1949 }
1950 }
1951
1952 int bytes = SSL_read(conn->ssl, conn->read_buf + total_received,
1954 if (bytes <= 0) {
1955 int ssl_err = SSL_get_error(conn->ssl, bytes);
1957 if (cwist_ssl_wait(conn->fd, ssl_err, CWIST_HTTP_TIMEOUT_MS) != 0) return NULL;
1958 continue;
1959 }
1960 return NULL;
1961 }
1962
1963 total_received += (size_t)bytes;
1964 conn->read_buf[total_received] = '\0';
1965 }
1966
1968 if (!req) return NULL;
1969
1970 req->client_fd = conn->fd;
1971 req->https_conn = conn;
1972
1973 size_t header_len = (header_end + 4) - conn->read_buf;
1974 size_t body_received = total_received - header_len;
1975
1976 if (req->content_length > 0) {
1979 return NULL;
1980 }
1981
1982 char *body = cwist_alloc(req->content_length + 1);
1983 if (!body) {
1985 return NULL;
1986 }
1987
1989 memcpy(body, header_end + 4, to_copy);
1990 size_t current_body_len = to_copy;
1991
1992 /* No total cap (slow 1 GiB uploads must keep working); abort only
1993 * when no bytes arrive for a cumulative idle span. Any successful
1994 * read resets the idle clock. */
1995 uint64_t body_idle_start = cwist_https_now_ms();
1996
1997 while (current_body_len < req->content_length) {
1998 if (SSL_pending(conn->ssl) == 0) {
1999 uint64_t now = cwist_https_now_ms();
2001 cwist_free(body);
2003 return NULL;
2004 }
2008
2009 struct pollfd pfd = {.fd = conn->fd, .events = POLLIN};
2010 int pret = poll(&pfd, 1, wait_ms);
2011 if (pret <= 0) {
2012 cwist_free(body);
2014 return NULL;
2015 }
2016 }
2017
2018 int bytes = SSL_read(conn->ssl, body + current_body_len,
2019 (int)(req->content_length - current_body_len));
2020 if (bytes <= 0) {
2021 int ssl_err = SSL_get_error(conn->ssl, bytes);
2023 if (cwist_ssl_wait(conn->fd, ssl_err, CWIST_HTTP_TIMEOUT_MS) != 0) {
2024 cwist_free(body);
2026 return NULL;
2027 }
2028 continue;
2029 }
2030 cwist_free(body);
2032 return NULL;
2033 }
2034 current_body_len += (size_t)bytes;
2035 body_idle_start = cwist_https_now_ms();
2036 }
2037 body[req->content_length] = '\0';
2038 /* Adopt the filled buffer: one allocation, zero copies. */
2040
2041 if (body_received > req->content_length) {
2044 conn->buf_len = leftover_len;
2045 } else {
2046 conn->buf_len = 0;
2047 }
2048 } else {
2049 if (body_received > 0) {
2051 conn->buf_len = body_received;
2052 } else {
2053 conn->buf_len = 0;
2054 }
2055 }
2056 conn->read_buf[conn->buf_len] = '\0';
2057
2058 return req;
2059}
2060
2068static int cwist_ssl_write_all(cwist_https_connection *conn, const char *buf, size_t len) {
2069 size_t off = 0;
2070 while (off < len) {
2071 /* SSL_write takes an int; cap each call well below INT_MAX. */
2072 size_t left = len - off;
2073 int chunk = left > 0x3fffffff ? 0x3fffffff : (int)left;
2074 int sent = SSL_write(conn->ssl, buf + off, chunk);
2075 if (sent <= 0) {
2076 int ssl_err = SSL_get_error(conn->ssl, sent);
2078 if (cwist_ssl_wait(conn->fd, ssl_err, CWIST_HTTP_TIMEOUT_MS) != 0) {
2079 return -1;
2080 }
2081 continue;
2082 }
2083 return -1;
2084 }
2085 off += (size_t)sent;
2086 }
2087 return 0;
2088}
2089
2104
2105 if (!conn || !conn->ssl || !res) {
2106 err.error.err_i16 = -1;
2107 return err;
2108 }
2109
2110 // Inject Alt-Svc when HTTP/3 is enabled so clients discover the QUIC endpoint,
2111 // unless the application already set or cleared the Alt-Svc header.
2112 if (conn->http3_enabled && !cwist_http_header_get(res->headers, "Alt-Svc")) {
2113 struct sockaddr_storage ss;
2114 socklen_t ss_len = sizeof(ss);
2115 int port = 443;
2116 if (getsockname(conn->fd, (struct sockaddr *)&ss, &ss_len) == 0) {
2117 if (ss.ss_family == AF_INET) {
2118 port = ntohs(((struct sockaddr_in *)&ss)->sin_port);
2119 } else if (ss.ss_family == AF_INET6) {
2120 port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port);
2121 }
2122 }
2123 char alt_svc[64];
2124 snprintf(alt_svc, sizeof(alt_svc), "h3=\":%d\"; ma=86400", port);
2125 cwist_http_header_add(&res->headers, "Alt-Svc", alt_svc);
2126 }
2127
2128 // 1+2. Headers onto a stack buffer; a small body rides in the same TLS
2129 // record, saving one record's AEAD tag and one syscall on the common
2130 // short-response path. Larger bodies keep the split writes (BoringSSL
2131 // records a big SSL_write at 16 KiB internally).
2133 size_t header_len = cwist_http_serialize_headers(res, header_buf, sizeof(header_buf));
2134
2135 const char *body_ptr = NULL;
2136 size_t body_len = 0;
2137 if (res->is_ptr_body) {
2138 body_ptr = (const char *)res->ptr_body;
2139 body_len = res->ptr_body_len;
2140 } else if (res->body && res->body->data) {
2141 body_ptr = res->body->data;
2142 body_len = res->body->size;
2143 }
2144
2145 bool body_coalesced = false;
2146 if (body_ptr && body_len > 0 && body_len <= CWIST_TLS_COALESCE_MAX) {
2147 char *combined = (char *)cwist_alloc(header_len + body_len);
2148 if (combined) {
2149 memcpy(combined, header_buf, header_len);
2150 memcpy(combined + header_len, body_ptr, body_len);
2151 int rc = cwist_ssl_write_all(conn, combined, header_len + body_len);
2153 if (rc != 0) {
2154 return make_ssl_error("SSL coalesced write failed");
2155 }
2156 body_coalesced = true;
2157 }
2158 /* Allocation failure: fall through to the split writes. */
2159 }
2160 if (!body_coalesced) {
2161 if (cwist_ssl_write_all(conn, header_buf, header_len) != 0) {
2162 return make_ssl_error("SSL header write failed");
2163 }
2164 if (body_ptr && body_len > 0) {
2165 if (cwist_ssl_write_all(conn, body_ptr, body_len) != 0) {
2166 return make_ssl_error("SSL body write failed");
2167 }
2168 }
2169 }
2170
2171 // 3. File streams cannot use sendfile() through userland TLS; chunk them
2172 if (res->use_file_stream && res->file_stream_fd >= 0) {
2173 char fbuf[65536];
2174 size_t remaining = res->file_stream_len;
2175 off_t offset = res->file_stream_offset;
2176 while (remaining > 0) {
2177 size_t to_read = remaining < sizeof(fbuf) ? remaining : sizeof(fbuf);
2178 ssize_t n = pread(res->file_stream_fd, fbuf, to_read, offset);
2179 if (n < 0) {
2180 if (errno == EINTR) continue;
2181 return make_ssl_error("file stream read failed");
2182 }
2183 if (n == 0) break;
2184 if (cwist_ssl_write_all(conn, fbuf, (size_t)n) != 0) {
2185 return make_ssl_error("SSL file stream write failed");
2186 }
2187 offset += n;
2188 remaining -= (size_t)n;
2189 }
2190 res->file_stream_offset = offset;
2191 }
2192
2193 err.error.err_i16 = 0;
2194 return err;
2195}
2196
2198 cwist_http_response *res) {
2200
2201 if (!conn || !conn->ssl || !res) {
2202 err.error.err_i16 = -1;
2203 return err;
2204 }
2205
2206 if (conn->http3_enabled && !cwist_http_header_get(res->headers, "Alt-Svc")) {
2207 struct sockaddr_storage ss;
2208 socklen_t ss_len = sizeof(ss);
2209 int port = 443;
2210 if (getsockname(conn->fd, (struct sockaddr *)&ss, &ss_len) == 0) {
2211 if (ss.ss_family == AF_INET) {
2212 port = ntohs(((struct sockaddr_in *)&ss)->sin_port);
2213 } else if (ss.ss_family == AF_INET6) {
2214 port = ntohs(((struct sockaddr_in6 *)&ss)->sin6_port);
2215 }
2216 }
2217 char alt_svc[64];
2218 snprintf(alt_svc, sizeof(alt_svc), "h3=\":%d\"; ma=86400", port);
2219 cwist_http_header_add(&res->headers, "Alt-Svc", alt_svc);
2220 }
2221
2223 size_t header_len = cwist_http_serialize_headers(res, header_buf, sizeof(header_buf));
2224 if (cwist_ssl_write_all(conn, header_buf, header_len) != 0) {
2225 return make_ssl_error("SSL header write failed");
2226 }
2227
2228 err.error.err_i16 = 0;
2229 return err;
2230}
2231
2237static void *https_thread_handler(void *arg) {
2238 struct https_thread_payload *payload = (struct https_thread_payload *)arg;
2241
2242 if (payload->conn) {
2243 conn = payload->conn;
2245 hs_err.error.err_i16 = 0;
2246 } else if (payload->pres_ssl) {
2247 /* Handshake already completed by the shepherd thread. */
2248 hs_err = https_wrap_established(payload->ctx, payload->client_fd, payload->pres_ssl, &conn);
2249 if (!cwist_error_is_ok(&hs_err)) {
2250 SSL_free(payload->pres_ssl);
2251 }
2252 } else {
2253 hs_err = cwist_https_accept(payload->ctx, payload->client_fd, &conn);
2254 }
2255
2256 if (cwist_error_is_ok(&hs_err)) {
2257 conn->pool_ctx = payload->ctx;
2258 conn->pool_handler = payload->handler;
2259 conn->pool_user_ctx = payload->user_ctx;
2260 t_https_parked = false;
2262 payload->handler(conn, payload->user_ctx);
2263 /* A deferred conn belongs to its completion after the dispatch ack,
2264 * which may already have closed or resubmitted it: never read conn
2265 * here once it was deferred (#344). */
2266 bool deferred = cwist_async_https_take_deferred();
2267 if (t_https_parked) {
2268 t_https_parked = false;
2269 } else if (!deferred) {
2271 }
2272 } else {
2273 if (hs_err.errtype == CWIST_ERR_JSON) {
2274 cJSON_Delete(hs_err.error.err_json);
2275 }
2276 if (payload->conn) {
2277 cwist_https_close_connection(payload->conn);
2278 } else {
2279 close(payload->client_fd);
2280 }
2281 }
2282
2283 cwist_free(payload);
2284 return NULL;
2285}
2286
2296 void (*handler)(cwist_https_connection *, void *),
2297 void *user_ctx) {
2299 if (server_fd < 0 || !ctx || !handler) {
2300 err.error.err_i16 = -1;
2301 return err;
2302 }
2303
2304 if (https_pool_init() != 0) {
2305 err.error.err_i16 = -1;
2306 return err;
2307 }
2308
2309 while (atomic_load(&g_cwist_running)) {
2310 struct sockaddr_in addr;
2311 socklen_t len = sizeof(addr);
2312 int client_fd = accept(server_fd, (struct sockaddr *)&addr, &len);
2313
2314 if (client_fd < 0) {
2315 if (errno == EINTR) continue;
2316 if (errno == EBADF || errno == EINVAL) break;
2317 continue;
2318 }
2319
2320 /* Reap vanished peers within ~2 minutes instead of the ~2h kernel
2321 * default, so dead connections cannot park pool workers forever. */
2322 {
2323 int one = 1;
2324 setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one));
2325#ifdef TCP_KEEPIDLE
2326 int keepidle = 60;
2327 setsockopt(client_fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
2328#endif
2329#ifdef TCP_KEEPINTVL
2330 int keepintvl = 10;
2331 setsockopt(client_fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
2332#endif
2333#ifdef TCP_KEEPCNT
2334 int keepcnt = 6;
2335 setsockopt(client_fd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
2336#endif
2337 }
2338
2339 cwist_https_dispatch(client_fd, ctx, handler, user_ctx);
2340 }
2341
2343 return err;
2344}
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_malloc(size_t size)
Allocate zeroed memory via the CWIST owner context.
Definition alloc.c:13
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
bool cwist_async_https_take_deferred(void)
Definition async.c:97
Private wiring between the deferred-response path (async.c) and the C1M connection engine (http....
Safe string type with helper methods.
cwist_error_t cwist_sstring_adopt_len(cwist_sstring *str, char *buf, size_t len)
Adopt a heap buffer, taking ownership without copying.
Definition sstring.c:151
mbedTLS TLS backend: OPENSSL_cleanse / CRYPTO_memcmp.
#define OPENSSL_cleanse
Definition crypto.h:16
@ 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
mbedTLS TLS backend: error queue subset.
#define ERR_get_error
Definition err.h:18
#define ERR_error_string_n
Definition err.h:19
mbedTLS TLS backend: EVP subset (HMAC digest tags, AES-256-CBC).
#define EVP_DecryptInit_ex
Definition evp.h:37
struct cwist_mtls_cipher_ctx EVP_CIPHER_CTX
Definition evp.h:21
#define EVP_EncryptInit_ex
Definition evp.h:34
#define EVP_sha256
Definition evp.h:25
#define EVP_aes_256_cbc
Definition evp.h:26
Epoch-based garbage collection wrapper.
bool cwist_conn_registry_untrack(void *handle)
Remove a handle from the registry before closing it yourself through the normal explicit path.
Definition gc.c:1010
bool cwist_gc_scope_disown(void *ptr)
Remove ptr from the current thread's pending-sweep list without freeing it, because ownership is bein...
Definition gc.c:766
bool cwist_full_gc_enabled(void)
Check whether full-GC mode is currently enabled.
Definition gc.c:515
bool cwist_conn_registry_adopt(void *handle)
Make the calling thread the owner of a tracked handle.
Definition gc.c:1025
void cwist_conn_registry_track(void *handle, cwist_conn_close_fn close_fn)
Register a connection handle for the full-GC exit sweep, owned by the calling thread (full-GC mode on...
Definition gc.c:984
mbedTLS TLS backend: HMAC subset (HMAC-SHA256, one-shot + streaming).
#define HMAC_Init_ex
Definition hmac.h:24
struct cwist_mtls_hmac_ctx HMAC_CTX
Definition hmac.h:19
#define MSG_DONTWAIT
Definition http3.c:82
#define CWIST_HTTP_READ_BUFFER_SIZE
Definition http.h:42
#define CWIST_HTTP_MAX_BODY_SIZE
Definition http.h:41
#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
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value by key.
Definition http.c:1880
void cwist_http_request_destroy(cwist_http_request *req)
Destroy an HTTP request object.
Definition http.c:2180
cwist_http_request * cwist_http_parse_request(const char *raw_request)
Parse a raw HTTP request string.
Definition http.c:4412
#define CWIST_HTTP_TIMEOUT_MS
Definition http.h:43
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
#define CWIST_ALPN_HTTP11_LEN
Definition https.c:1155
#define CWIST_ALPN_H2_HTTP11
Definition https.c:1153
long cwist_https_tls_ciphers_aes128_gcm_total(void)
Handshakes negotiated with TLS_AES_128_GCM_SHA256.
Definition https.c:1613
struct cwist_tls_ticket_key cwist_tls_ticket_key
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
bool cwist_tls_apply_pqc_layer(cwist_app *app, SSL_CTX *ctx)
Apply PQC hybrid key exchange settings to an SSL context.
Definition pqc_layer.c:19
cwist_error_t cwist_https_accept(cwist_https_context *ctx, int client_fd, cwist_https_connection **conn)
Definition https.c:1756
#define CWIST_ALPN_H2_HTTP11_LEN
Definition https.c:1156
void https_pool_submit_conn(cwist_https_connection *conn, cwist_https_context *ctx, void(*handler)(cwist_https_connection *, void *), void *user_ctx)
Submit an established connection back to the worker pool (e.g. keep-alive re-arm after async defer)....
Definition https.c:341
#define HTTPS_TASK_QUEUE_MAX
Definition https.c:91
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
cwist_error_t cwist_https_init_context(cwist_https_context **ctx, const char *cert_path, const char *key_path)
Definition https.c:1369
void https_pool_submit_ready(int client_fd, SSL *pres_ssl, cwist_https_context *ctx, void(*handler)(cwist_https_connection *, void *), void *user_ctx)
Submit a connection whose TLS handshake already completed (pres_ssl). Blocks while the queue is full;...
Definition https.c:308
void cwist_https_close_connection(cwist_https_connection *conn)
Definition https.c:1875
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
#define CWIST_TLS_COALESCE_MAX
Definition https.c:42
cwist_https_protocol cwist_https_connection_protocol(const cwist_https_connection *conn)
Definition https.c:1821
cwist_error_t cwist_https_reload_context(cwist_https_context *ctx, const char *cert_path, const char *key_path, cwist_app *app)
Swap in a new certificate chain and key without dropping connections.
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
int https_hs_shepherd_start(void)
Definition https.c:1131
long cwist_https_tls_handshakes_total(void)
Total TLS handshakes that completed (full + resumed).
Definition https.c:1597
long cwist_https_tls_ciphers_other_total(void)
Handshakes negotiated with any other cipher.
Definition https.c:1625
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
long cwist_https_tls_handshakes_tls12_total(void)
Handshakes that negotiated TLS 1.2.
Definition https.c:1605
long cwist_https_tls_ciphers_chacha20_total(void)
Handshakes negotiated with TLS_CHACHA20_POLY1305_SHA256.
Definition https.c:1621
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
long cwist_https_pending_handshakes(void)
Number of TLS handshakes currently parked in the shepherd.
Definition https.c:1136
long cwist_https_tls_handshakes_resumed_total(void)
TLS handshakes completed via session resumption/ticket.
Definition https.c:1601
cwist_error_t cwist_https_server_loop(int server_fd, cwist_https_context *ctx, void(*handler)(cwist_https_connection *, void *), void *user_ctx)
Accept HTTPS clients in a loop and dispatch each one to the supplied handler.
Definition https.c:2295
bool cwist_https_connection_uses_http2(const cwist_https_connection *conn)
Gracefully close an HTTPS connection and free its buffers.
Definition https.c:1817
#define CWIST_ALPN_HTTP11
Definition https.c:1152
long cwist_https_tls_handshakes_tls13_total(void)
Handshakes that negotiated TLS 1.3.
Definition https.c:1609
void cwist_https_destroy_context(cwist_https_context *ctx)
Free an HTTPS context and release its OpenSSL resources.
Definition https.c:1558
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
long cwist_https_tls_connections_active(void)
Currently established TLS connections (wrap minus teardown).
Definition https.c:1629
void cwist_https_dispatch(int client_fd, cwist_https_context *ctx, void(*handler)(cwist_https_connection *, void *), void *user_ctx)
Accept a TLS connection without parking a worker on the handshake. Completes the handshake inline whe...
Definition https.c:1141
#define HTTPS_TASK_QUEUE_INITIAL
Definition https.c:90
void https_hs_shepherd_stop(void)
Definition https.c:1134
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
long cwist_https_tls_ciphers_aes256_gcm_total(void)
Handshakes negotiated with TLS_AES_256_GCM_SHA384.
Definition https.c:1617
#define CWIST_HTTPS_HANDSHAKE_TIMEOUT_MS
Definition http.h:146
#define CWIST_HTTP_HEADERS_TIMEOUT_MS
Definition http.h:147
#define CWIST_HTTP_BODY_IDLE_TIMEOUT_MS
Definition http.h:148
long get_optimal_thread_count(void)
Compute the HTTP worker/event-loop thread count.
Definition http.c:159
HTTPS/TLS wrapper interface.
cwist_https_park_class
Definition https.h:58
@ CWIST_HTTPS_PARK_HTTP2
Definition https.h:60
@ CWIST_HTTPS_PARK_HTTP1
Definition https.h:59
cwist_https_protocol
Definition https.h:19
@ CWIST_HTTPS_PROTOCOL_HTTP2
Definition https.h:22
@ CWIST_HTTPS_PROTOCOL_HTTP11
Definition https.h:21
@ CWIST_HTTPS_PROTOCOL_NONE
Definition https.h:20
size_t head
Definition otel.c:57
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
mbedTLS TLS backend: RAND_bytes over mbedTLS CTR-DRBG.
#define RAND_bytes
Definition rand.h:17
Unified Graceful Shutdown support for CWIST.
atomic_int g_cwist_running
Global running flag. Checked by all server event loops.
Definition shutdown.c:17
mbedTLS TLS backend: OpenSSL-shaped seam over the CWIST TLS subset.
#define SSL_new
Definition ssl.h:166
#define SSL_CTX_check_private_key
Definition ssl.h:113
#define SSL_OP_CIPHER_SERVER_PREFERENCE
Definition ssl.h:64
#define SSL_version
Definition ssl.h:178
#define SSL_shutdown
Definition ssl.h:174
#define SSL_CTX_get_ex_new_index
Definition ssl.h:121
#define TLS1_3_VERSION
Definition ssl.h:49
struct cwist_mtls_ssl SSL
Definition ssl.h:44
#define SSL_select_next_proto
Definition ssl.h:201
#define SSL_get0_alpn_selected
Definition ssl.h:180
#define SSL_MODE_AUTO_RETRY
Definition ssl.h:71
#define SSL_CTX_set_tlsext_ticket_key_cb
Definition ssl.h:120
#define OpenSSL_add_ssl_algorithms()
Definition ssl.h:92
#define SSL_TLSEXT_ERR_OK
Definition ssl.h:84
#define SSL_session_reused
Definition ssl.h:177
#define SSL_TLSEXT_ERR_NOACK
Definition ssl.h:85
#define SSL_CTX_set_options
Definition ssl.h:107
#define SSL_CTX_set_session_cache_mode
Definition ssl.h:114
#define TLS_server_method
Definition ssl.h:96
#define SSL_get_cipher_name
Definition ssl.h:179
#define OPENSSL_NPN_NEGOTIATED
Definition ssl.h:88
#define SSL_OP_NO_COMPRESSION
Definition ssl.h:62
#define SSL_ERROR_WANT_READ
Definition ssl.h:54
#define SSL_CTX_sess_set_cache_size
Definition ssl.h:115
#define SSL_CTX_new
Definition ssl.h:98
#define SSL_CTX_get_ex_data
Definition ssl.h:123
#define SSL_free
Definition ssl.h:167
#define SSL_CTX_use_PrivateKey_file
Definition ssl.h:112
#define SSL_FILETYPE_PEM
Definition ssl.h:77
#define SSL_load_error_strings()
Definition ssl.h:91
#define SSL_write
Definition ssl.h:172
#define SSL_CTX_set_timeout
Definition ssl.h:117
#define SSL_CTX_free
Definition ssl.h:99
#define SSL_set_fd
Definition ssl.h:168
#define SSL_CTX_set_ex_data
Definition ssl.h:122
#define TLS1_2_VERSION
Definition ssl.h:48
#define SSL_CTX_set_cipher_list
Definition ssl.h:109
#define SSL_OP_NO_RENEGOTIATION
Definition ssl.h:63
#define SSL_read
Definition ssl.h:171
#define SSL_CTX_use_certificate_chain_file
Definition ssl.h:111
struct cwist_mtls_method SSL_METHOD
Definition ssl.h:45
#define SSL_CTX_set_alpn_select_cb
Definition ssl.h:118
#define SSL_SESS_CACHE_SERVER
Definition ssl.h:74
#define SSL_accept
Definition ssl.h:169
#define SSL_CTX_set_session_id_context
Definition ssl.h:116
#define SSL_CTX_set_mode
Definition ssl.h:108
#define SSL_CTX_set_ciphersuites
Definition ssl.h:110
struct cwist_mtls_ctx SSL_CTX
Definition ssl.h:43
#define SSL_get_SSL_CTX
Definition ssl.h:176
#define SSL_get_error
Definition ssl.h:175
#define SSL_pending
Definition ssl.h:173
#define SSL_CTX_set_min_proto_version
Definition ssl.h:106
#define SSL_ERROR_WANT_WRITE
Definition ssl.h:55
Main Application Context.
Definition app.h:155
Integrated error structure used throughout the system.
Definition cwist_err.h:72
size_t content_length
Definition http.h:79
cwist_sstring * body
Definition http.h:73
void * https_conn
HTTPS connection wrapper when on TLS path (internal).
Definition http.h:190
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
int file_stream_fd
Open descriptor for sendfile.
Definition http.h:217
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
off_t file_stream_offset
Current offset for sendfile loop.
Definition http.h:219
cwist_http_header_node * headers
Definition http.h:86
bool is_ptr_body
Zero-Copy Pointer Body.
Definition http.h:209
size_t ptr_body_len
Length of external data.
Definition http.h:211
size_t file_stream_len
Total bytes to stream.
Definition http.h:218
void(* proto_state_free)(void *state)
Definition https.h:51
struct cwist_https_context * pool_ctx
Definition https.h:52
cwist_https_protocol negotiated_protocol
Definition https.h:44
void(* pool_handler)(struct cwist_https_connection *, void *)
Definition https.h:53
void * ticket_key
Shared session-ticket key material (forked workers inherit it)
Definition https.h:18
SSL_CTX * ctx
Definition https.h:17
pthread_rwlock_t ctx_lock
Definition https.h:35
size_t size
Definition sstring.h:20
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
Unified error code and type definitions for the CWIST system layer.
int cwist_tls_autoload_intermediates(SSL_CTX *ssl_ctx)
Autoload missing intermediate CA certificates into an SSL_CTX chain.
Definition tls_chain.c:568
unsigned long cwist_tls_sess_cache_size(void)
Definition tls_chain.c:537
Zero-latency speculative fast-path writer and P2C worker scheduler.
uint32_t cwist_sched_p2c_select_worker(const _Atomic uint32_t *worker_loads, uint32_t num_workers)
Select worker using Power of Two Random Choices (P2C).