emme coverage


Directory: src/
File: src/server.c
Date: 2026-05-14 14:35:13
Exec Total Coverage
Lines: 411 664 61.9%
Functions: 22 25 88.0%
Branches: 166 358 46.4%

Line Branch Exec Source
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <poll.h>
5 #include <unistd.h>
6 #include <arpa/inet.h>
7 #include <sys/epoll.h>
8 #include <pthread.h>
9 #include <fcntl.h>
10 #include <liburing.h>
11 #include <sys/sendfile.h>
12 #include <sys/time.h>
13 #include <signal.h>
14 #include <errno.h>
15 #include <stdatomic.h>
16 #include "config.h"
17 #include "server.h"
18 #include "http_parser.h"
19 #include "router.h"
20 #include "tls.h"
21 #include "metrics.h"
22 #include <openssl/ssl.h>
23 #include <openssl/err.h>
24 #include "thread_pool.h"
25 #include "log.h"
26 #include <ctype.h>
27 #include "http2_response.h"
28 #include "uuid.h"
29 #include "ip_limiter.h"
30
31 #ifndef DEBUG_H2
32 #define DEBUG_H2 0
33 #endif
34
35 #define H2_LOG(...) \
36 do { \
37 if (DEBUG_H2) \
38 log_message(LOG_LEVEL_DEBUG, __VA_ARGS__); \
39 } while (0)
40
41 #define H2_POLL_TIMEOUT_MS 100
42 #define H2_POLL_ERROR_EVENTS (POLLERR | POLLHUP | POLLNVAL)
43
44 #define SERVER_BACKLOG 2048
45 #define THREAD_POOL_MIN_THREADS 32
46 #define THREAD_POOL_MAX_THREADS_RATIO 1
47 #define SESSION_STATS_INTERVAL_SEC 60
48
49 #define NS_PER_MS 1000000
50 #define US_PER_MS 1000
51
52 typedef struct {
53 SSL *ssl;
54 ServerConfig *config;
55 int want_read;
56 int want_write;
57 size_t total_read;
58 int request_count;
59 struct timeval request_start;
60 int request_timeout_ms;
61 } H2IO;
62
63 9 static int find_header_end(const char *buf, size_t len)
64 {
65
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (len < 4)
66 return -1;
67
2/2
✓ Branch 0 taken 53502 times.
✓ Branch 1 taken 2 times.
53504 for (size_t i = 0; i + 3 < len; i++)
68 {
69
3/4
✓ Branch 0 taken 312 times.
✓ Branch 1 taken 53190 times.
✓ Branch 2 taken 312 times.
✗ Branch 3 not taken.
53502 if (buf[i] == '\r' && buf[i + 1] == '\n' &&
70
3/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 305 times.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
312 buf[i + 2] == '\r' && buf[i + 3] == '\n')
71 7 return (int)(i + 4);
72 }
73 2 return -1;
74 }
75
76 /* Callback invoked for each header received in an HTTP/2 frame */
77 12 static int on_header_callback(nghttp2_session *session,
78 const nghttp2_frame *frame,
79 const uint8_t *name, size_t namelen,
80 const uint8_t *value, size_t valuelen,
81 uint8_t flags, void *user_data)
82 {
83 (void)flags;
84
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (frame->hd.type == NGHTTP2_HEADERS &&
85
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 frame->headers.cat == NGHTTP2_HCAT_REQUEST)
86 {
87 12 H2IO *io = (H2IO *)user_data;
88
4/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✓ Branch 5 taken 6 times.
12 if (io && frame->headers.cat == NGHTTP2_HCAT_REQUEST &&
89
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 namelen == 7 && strncmp((const char *)name, ":method", 7) == 0)
90 {
91 3 io->request_count++;
92 3 gettimeofday(&io->request_start, NULL);
93 }
94
95 12 StreamData *data = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
96
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 9 times.
12 if (!data)
97 {
98 3 data = calloc(1, sizeof(StreamData));
99
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!data)
100 {
101 log_message(LOG_LEVEL_ERROR, "Failed to allocate HTTP/2 stream state");
102 return NGHTTP2_ERR_CALLBACK_FAILURE;
103 }
104 3 data->req.method = NULL;
105 3 data->req.path = NULL;
106 3 data->req.version = strdup("HTTP/2");
107
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!data->req.version)
108 {
109 free(data);
110 log_message(LOG_LEVEL_ERROR, "Failed to allocate HTTP/2 request version");
111 return NGHTTP2_ERR_CALLBACK_FAILURE;
112 }
113 3 generate_uuid(data->req.request_id);
114 3 nghttp2_session_set_stream_user_data(session, frame->hd.stream_id, data);
115 }
116
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 if (namelen >= 1 && name[0] == ':')
117 {
118
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 9 times.
12 if (strncmp((const char *)name, ":method", namelen) == 0)
119 {
120 3 free((void *)data->req.method);
121 3 data->req.method = strndup((const char *)value, valuelen);
122
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!data->req.method)
123 {
124 log_message(LOG_LEVEL_ERROR, "Failed to allocate HTTP/2 method");
125 return NGHTTP2_ERR_CALLBACK_FAILURE;
126 }
127 }
128
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 6 times.
9 else if (strncmp((const char *)name, ":path", namelen) == 0)
129 {
130 3 free((void *)data->req.path);
131 3 data->req.path = strndup((const char *)value, valuelen);
132
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!data->req.path)
133 {
134 log_message(LOG_LEVEL_ERROR, "Failed to allocate HTTP/2 path");
135 return NGHTTP2_ERR_CALLBACK_FAILURE;
136 }
137 }
138
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 else if (strncmp((const char *)name, ":scheme", namelen) == 0)
139 ; /* ignore scheme */
140 3 else if (strncmp((const char *)name, ":authority", namelen) == 0)
141 {
142 /* authority not currently used by routing */
143 }
144 }
145 }
146 12 return 0;
147 }
148
149 3 static ssize_t http2_body_read_callback(
150 nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length,
151 uint32_t *data_flags, nghttp2_data_source *source, void *user_data)
152 {
153 (void)session;
154 (void)stream_id;
155 (void)user_data;
156
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (!source || !source->ptr)
157 {
158 *data_flags = NGHTTP2_DATA_FLAG_EOF;
159 return 0;
160 }
161 3 StreamData *data = (StreamData *)source->ptr;
162
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (!data || !data->resp)
163 {
164 *data_flags = NGHTTP2_DATA_FLAG_EOF;
165 return 0;
166 }
167 3 size_t remaining = data->resp->body_len - data->resp_sent;
168 3 size_t to_copy = remaining < length ? remaining : length;
169
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (to_copy > 0)
170 {
171 2 memcpy(buf, data->resp->body + data->resp_sent, to_copy);
172 2 data->resp_sent += to_copy;
173 }
174
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (data->resp_sent >= data->resp->body_len)
175 {
176 3 *data_flags = NGHTTP2_DATA_FLAG_EOF;
177 }
178 3 return to_copy;
179 }
180
181 /* Callback invoked when a complete frame is received */
182 6 static int on_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data)
183 {
184 6 H2IO *io = (H2IO *)user_data;
185
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 ServerConfig *config = io ? io->config : NULL;
186 H2_LOG("on_frame_recv_callback: start");
187
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (!frame)
188 return 0;
189
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 if (frame->hd.type == NGHTTP2_HEADERS &&
190
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 frame->headers.cat == NGHTTP2_HCAT_REQUEST &&
191
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 (frame->hd.flags & NGHTTP2_FLAG_END_HEADERS))
192 {
193 3 StreamData *data = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
194
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (data)
195 {
196 char raw_request[BUFFER_SIZE];
197 9 snprintf(raw_request, sizeof(raw_request), "%s %s %s\r\n",
198
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 data->req.method ? data->req.method : "GET",
199
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 data->req.path ? data->req.path : "/",
200
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 data->req.version ? data->req.version : "HTTP/2");
201 3 log_message(LOG_LEVEL_INFO, "Routing HTTP/2 request for path (synthesized): %s",
202
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 data->req.path ? data->req.path : "/");
203
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!data->resp)
204 3 data->resp = calloc(1, sizeof(Http2Response));
205
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!data->resp)
206 {
207 log_message(LOG_LEVEL_ERROR, "Failed to allocate Http2Response");
208 return 0;
209 }
210 3 data->resp_sent = 0;
211
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (route_request_tls(&data->req, raw_request, strlen(raw_request), config, NULL, data->resp) != 0 &&
212 data->resp->status_code == 0) {
213 data->resp->status_code = 500;
214 snprintf(data->resp->status_text, sizeof(data->resp->status_text), "Internal Server Error");
215 data->resp->content_type[0] = '\0';
216 data->resp->body[0] = '\0';
217 data->resp->body_len = 0;
218 }
219
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (data->resp->status_code == 0)
220 data->resp->status_code = 200;
221 3 snprintf(data->resp->status_code_str, sizeof(data->resp->status_code_str),
222 3 "%d", data->resp->status_code);
223 3 snprintf(data->resp->content_length_str, sizeof(data->resp->content_length_str),
224 3 "%zu", data->resp->body_len);
225 3 data->resp->headers[0] = MAKE_NV(":status", data->resp->status_code_str);
226
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 data->resp->headers[1] = MAKE_NV("content-type",
227 data->resp->content_type[0] ? data->resp->content_type : "text/plain");
228 3 data->resp->headers[2] = MAKE_NV("content-length", data->resp->content_length_str);
229 3 data->resp->num_headers = 3;
230 nghttp2_data_provider data_prd;
231 3 data_prd.source.ptr = data;
232 3 data_prd.read_callback = http2_body_read_callback;
233 3 int rv = nghttp2_submit_response(session, frame->hd.stream_id,
234 3 data->resp->headers, data->resp->num_headers,
235 &data_prd);
236
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (rv != 0)
237 log_message(LOG_LEVEL_ERROR, "nghttp2_submit_response failed: %s", nghttp2_strerror(rv));
238 else
239 3 log_message(LOG_LEVEL_INFO, "nghttp2_submit_response succeeded for stream %d", frame->hd.stream_id);
240
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (rv == 0)
241 {
242 3 int send_rv = nghttp2_session_send(session);
243
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 if (send_rv < 0 && send_rv != NGHTTP2_ERR_WOULDBLOCK)
244 log_message(LOG_LEVEL_ERROR, "nghttp2_session_send failed: %s", nghttp2_strerror(send_rv));
245 }
246 }
247 }
248 6 return 0;
249 }
250
251 /* Callback invoked when a stream is closed */
252 3 static int on_stream_close_callback(nghttp2_session *session, int32_t stream_id,
253 uint32_t error_code, void *user_data)
254 {
255 (void)error_code;
256 (void)user_data;
257 3 StreamData *data = nghttp2_session_get_stream_user_data(session, stream_id);
258
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (data)
259 {
260 3 free((void *)data->req.method);
261 3 free((void *)data->req.path);
262 3 free((void *)data->req.version);
263
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (data->resp)
264 {
265 3 free(data->resp);
266 }
267 3 free(data);
268 3 nghttp2_session_set_stream_user_data(session, stream_id, NULL);
269 }
270 3 return 0;
271 }
272
273 SSL_CTX *ssl_ctx = NULL;
274 static struct io_uring global_ring;
275 static int g_server_fd = -1;
276
277 shutdown_context_t g_shutdown_ctx = {0};
278 static ip_limiter_t g_ip_limiter;
279 static int g_ip_limiter_initialized = 0;
280
281 static void log_io_uring_error(const char *context, int ret)
282 {
283 log_message(LOG_LEVEL_ERROR, "%s: %s", context, strerror(-ret));
284 }
285
286 static void client_task(void *arg);
287 void handle_signal(int sig);
288
289 10 static void cleanup_server_resources(ThreadPool *pool, int server_fd)
290 {
291
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (pool) {
292 10 thread_pool_destroy(pool);
293 }
294
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (server_fd >= 0) {
295 10 close(server_fd);
296 10 g_server_fd = -1;
297 }
298 10 io_uring_queue_exit(&global_ring);
299
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (ssl_ctx) {
300 10 cleanup_ssl_context(ssl_ctx);
301 10 ssl_ctx = NULL;
302 }
303
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (g_ip_limiter_initialized) {
304 10 ip_limiter_destroy(&g_ip_limiter);
305 10 g_ip_limiter_initialized = 0;
306 }
307 10 }
308
309 10 static int initialize_server(ServerConfig *config, ThreadPool **out_pool)
310 {
311 int ring_ret;
312
313
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
10 if (!config || !out_pool) {
314 log_message(LOG_LEVEL_ERROR, "Invalid parameters to initialize_server");
315 return -1;
316 }
317
318 10 memset(&g_shutdown_ctx, 0, sizeof(g_shutdown_ctx));
319 10 g_shutdown_ctx.timeout_seconds = config->shutdown_timeout_seconds;
320 10 atomic_store(&g_shutdown_ctx.state, SHUTDOWN_STATE_RUNNING);
321
322 10 ring_ret = io_uring_queue_init(QUEUE_DEPTH * 2, &global_ring, 0);
323
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (ring_ret != 0) {
324 log_io_uring_error("io_uring_queue_init", ring_ret);
325 return -1;
326 }
327
328 10 g_server_fd = socket(AF_INET, SOCK_STREAM, 0);
329
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (g_server_fd == -1) {
330 log_message(LOG_LEVEL_ERROR, "Socket creation failed: %s", strerror(errno));
331 io_uring_queue_exit(&global_ring);
332 return -1;
333 }
334
335 10 int enable = 1;
336
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (setsockopt(g_server_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) == -1) {
337 log_message(LOG_LEVEL_ERROR, "setsockopt(SO_REUSEADDR) failed: %s", strerror(errno));
338 close(g_server_fd);
339 g_server_fd = -1;
340 io_uring_queue_exit(&global_ring);
341 return -1;
342 }
343
344 10 signal(SIGTERM, handle_signal);
345 10 signal(SIGINT, handle_signal);
346
347 10 struct sockaddr_in server_addr = {0};
348 10 server_addr.sin_family = AF_INET;
349 10 server_addr.sin_addr.s_addr = INADDR_ANY;
350 10 server_addr.sin_port = htons(config->port);
351
352
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (bind(g_server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
353 log_message(LOG_LEVEL_ERROR, "Bind error on port %d: %s", config->port, strerror(errno));
354 close(g_server_fd);
355 g_server_fd = -1;
356 io_uring_queue_exit(&global_ring);
357 return -1;
358 }
359
360
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (listen(g_server_fd, SERVER_BACKLOG) == -1) {
361 log_message(LOG_LEVEL_ERROR, "Listen error: %s", strerror(errno));
362 close(g_server_fd);
363 g_server_fd = -1;
364 io_uring_queue_exit(&global_ring);
365 return -1;
366 }
367
368
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (ip_limiter_init(&g_ip_limiter, config->per_ip_connection_limit) != 0) {
369 log_message(LOG_LEVEL_ERROR, "Failed to initialize IP limiter");
370 close(g_server_fd);
371 g_server_fd = -1;
372 io_uring_queue_exit(&global_ring);
373 return -1;
374 }
375 10 g_ip_limiter_initialized = 1;
376
377 10 ssl_ctx = create_ssl_context(config->ssl.certificate, config->ssl.private_key, config);
378
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!ssl_ctx) {
379 log_message(LOG_LEVEL_ERROR, "Failed to create SSL context");
380 close(g_server_fd);
381 g_server_fd = -1;
382 io_uring_queue_exit(&global_ring);
383 return -1;
384 }
385
386
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 size_t max_threads = config->max_connections > 0 ? (size_t)config->max_connections : 32;
387 10 size_t initial_threads = max_threads < THREAD_POOL_MIN_THREADS ? max_threads : THREAD_POOL_MIN_THREADS;
388
389 10 *out_pool = thread_pool_create(initial_threads, max_threads);
390
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!*out_pool) {
391 log_message(LOG_LEVEL_ERROR, "Failed to create thread pool");
392 close(g_server_fd);
393 g_server_fd = -1;
394 io_uring_queue_exit(&global_ring);
395 cleanup_ssl_context(ssl_ctx);
396 ssl_ctx = NULL;
397 return -1;
398 }
399
400 10 log_message(LOG_LEVEL_INFO, "Server initialized on port %d (max_connections=%zu)",
401 config->port, max_threads);
402 10 return 0;
403 }
404
405 static int send_429_response(int client_fd)
406 {
407 const char *response = "HTTP/1.1 429 Too Many Requests\r\n"
408 "Retry-After: 10\r\n"
409 "Content-Length: 0\r\n"
410 "Connection: close\r\n"
411 "\r\n";
412
413 ssize_t sent = send(client_fd, response, strlen(response), MSG_NOSIGNAL);
414 if (sent < 0) {
415 log_message(LOG_LEVEL_DEBUG, "Failed to send 429 response: %s", strerror(errno));
416 }
417 return 0;
418 }
419
420 static void handle_ip_limiter_rejection(int client_fd, uint32_t client_ip, uint32_t current_count, uint32_t limit)
421 {
422 char ip_str[INET_ADDRSTRLEN];
423 inet_ntop(AF_INET, &client_ip, ip_str, sizeof(ip_str));
424
425 log_message(LOG_LEVEL_WARN, "Per-IP connection limit exceeded: IP=%s, current=%u, limit=%u",
426 ip_str, current_count, limit);
427
428 metrics_increment_per_ip_limit_rejected();
429
430 send_429_response(client_fd);
431 close(client_fd);
432 }
433
434 20 static int accept_and_dispatch_client(ThreadPool *pool, ServerConfig *config)
435 {
436 struct io_uring_sqe *sqe;
437 struct sockaddr_in client_addr;
438 struct io_uring_cqe *cqe;
439 20 socklen_t client_len = sizeof(client_addr);
440 int client_fd;
441 int submit_ret, wait_ret;
442 shutdown_state_t state;
443 int flags;
444 ClientTaskData *task_data;
445 uint32_t client_ip;
446 uint32_t current_count;
447 ip_limiter_result_t limiter_result;
448
449 20 sqe = io_uring_get_sqe(&global_ring);
450
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (!sqe) {
451 log_message(LOG_LEVEL_ERROR, "Failed to get SQE for accept");
452 return -1;
453 }
454
455 20 io_uring_prep_accept(sqe, g_server_fd, (struct sockaddr *)&client_addr, &client_len, 0);
456
457 20 submit_ret = io_uring_submit(&global_ring);
458
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (submit_ret < 0) {
459 log_io_uring_error("io_uring_submit (accept)", submit_ret);
460 return -1;
461 }
462
463 20 wait_ret = io_uring_wait_cqe(&global_ring, &cqe);
464
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 10 times.
20 if (wait_ret < 0) {
465 10 state = atomic_load(&g_shutdown_ctx.state);
466
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
10 if (state != SHUTDOWN_STATE_RUNNING &&
467 (-wait_ret == EINTR || -wait_ret == EBADF || -wait_ret == ENXIO)) {
468 10 return 1;
469 }
470 log_io_uring_error("io_uring_wait_cqe (accept)", wait_ret);
471 return -1;
472 }
473
474 10 client_fd = cqe->res;
475 10 io_uring_cqe_seen(&global_ring, cqe);
476
477 10 state = atomic_load(&g_shutdown_ctx.state);
478
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (state != SHUTDOWN_STATE_RUNNING) {
479 if (client_fd >= 0) {
480 close(client_fd);
481 }
482 return 1;
483 }
484
485
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (client_fd < 0) {
486 return (state != SHUTDOWN_STATE_RUNNING) ? 1 : 0;
487 }
488
489 10 client_ip = client_addr.sin_addr.s_addr;
490
491 10 limiter_result = ip_limiter_check_and_increment(&g_ip_limiter, client_ip, &current_count);
492
493
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (limiter_result == IP_LIMITER_REJECTED) {
494 handle_ip_limiter_rejection(client_fd, client_ip, current_count, config->per_ip_connection_limit);
495 return 0;
496 }
497
498
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (limiter_result == IP_LIMITER_ERROR) {
499 log_message(LOG_LEVEL_ERROR, "IP limiter check failed, rejecting connection");
500 close(client_fd);
501 return 0;
502 }
503
504 10 flags = fcntl(client_fd, F_GETFL, 0);
505
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (flags == -1) {
506 log_message(LOG_LEVEL_ERROR, "fcntl(F_GETFL) failed: %s", strerror(errno));
507 ip_limiter_decrement(&g_ip_limiter, client_ip);
508 close(client_fd);
509 return 0;
510 }
511
512
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (fcntl(client_fd, F_SETFL, flags & ~O_NONBLOCK) == -1) {
513 log_message(LOG_LEVEL_ERROR, "fcntl(F_SETFL) failed: %s", strerror(errno));
514 ip_limiter_decrement(&g_ip_limiter, client_ip);
515 close(client_fd);
516 return 0;
517 }
518
519 10 task_data = malloc(sizeof(ClientTaskData));
520
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!task_data) {
521 log_message(LOG_LEVEL_ERROR, "Failed to allocate client task data: %s", strerror(errno));
522 ip_limiter_decrement(&g_ip_limiter, client_ip);
523 close(client_fd);
524 return 0;
525 }
526
527 10 task_data->client_fd = client_fd;
528 10 task_data->config = config;
529 10 task_data->client_ip = client_ip;
530
531 10 atomic_fetch_add(&g_shutdown_ctx.in_flight_requests, 1);
532 10 metrics_set_active_connections(atomic_load(&g_shutdown_ctx.in_flight_requests));
533
534
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (!thread_pool_add_task(pool, client_task, task_data)) {
535 log_message(LOG_LEVEL_ERROR, "Failed to add task to thread pool");
536 atomic_fetch_sub(&g_shutdown_ctx.in_flight_requests, 1);
537 metrics_set_active_connections(atomic_load(&g_shutdown_ctx.in_flight_requests));
538 ip_limiter_decrement(&g_ip_limiter, client_ip);
539 free(task_data);
540 close(client_fd);
541 }
542
543 10 return 0;
544 }
545
546 10 static void drain_in_flight_requests(void)
547 {
548 struct timespec now;
549 10 time_t last_log_time = 0;
550 size_t remaining, peak;
551 long drain_duration_ms;
552
553 10 clock_gettime(CLOCK_REALTIME, &now);
554
555
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 while (atomic_load(&g_shutdown_ctx.in_flight_requests) > 0) {
556 clock_gettime(CLOCK_REALTIME, &now);
557
558 if (now.tv_sec > g_shutdown_ctx.deadline.tv_sec ||
559 (now.tv_sec == g_shutdown_ctx.deadline.tv_sec &&
560 now.tv_nsec > g_shutdown_ctx.deadline.tv_nsec)) {
561
562 atomic_store(&g_shutdown_ctx.state, SHUTDOWN_STATE_FORCED);
563
564 remaining = atomic_load(&g_shutdown_ctx.in_flight_requests);
565 log_message(LOG_LEVEL_WARN,
566 "Graceful shutdown timeout (%ds) reached. Forcing shutdown with %zu in-flight requests",
567 g_shutdown_ctx.timeout_seconds, remaining);
568 break;
569 }
570
571 if (now.tv_sec != last_log_time) {
572 log_message(LOG_LEVEL_INFO,
573 "Draining: %zu requests still in-flight...",
574 atomic_load(&g_shutdown_ctx.in_flight_requests));
575 last_log_time = now.tv_sec;
576 }
577
578 struct timespec sleep_time = {.tv_sec = 0, .tv_nsec = 100 * NS_PER_MS};
579 nanosleep(&sleep_time, NULL);
580 }
581
582 10 clock_gettime(CLOCK_REALTIME, &g_shutdown_ctx.metrics.end_time);
583
584 10 peak = atomic_load(&g_shutdown_ctx.metrics.peak_in_flight);
585 10 remaining = atomic_load(&g_shutdown_ctx.in_flight_requests);
586 10 atomic_store(&g_shutdown_ctx.metrics.completed, peak - remaining);
587 10 atomic_store(&g_shutdown_ctx.metrics.forced, remaining);
588
589 10 drain_duration_ms =
590 10 (g_shutdown_ctx.metrics.end_time.tv_sec - g_shutdown_ctx.metrics.start_time.tv_sec) * 1000 +
591 10 (g_shutdown_ctx.metrics.end_time.tv_nsec - g_shutdown_ctx.metrics.start_time.tv_nsec) / NS_PER_MS;
592
593 10 log_message(LOG_LEVEL_INFO,
594 "Graceful shutdown complete. "
595 "Duration: %ldms | Completed: %zu | Forced: %zu | Peak: %zu",
596 drain_duration_ms,
597 10 atomic_load(&g_shutdown_ctx.metrics.completed),
598 10 atomic_load(&g_shutdown_ctx.metrics.forced),
599 10 atomic_load(&g_shutdown_ctx.metrics.peak_in_flight));
600 10 }
601
602 10 static void perform_shutdown(ThreadPool *pool, int server_fd)
603 {
604 10 shutdown_state_t state = atomic_load(&g_shutdown_ctx.state);
605
606
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (state == SHUTDOWN_STATE_DRAINING) {
607 10 drain_in_flight_requests();
608 } else if (state == SHUTDOWN_STATE_FORCED) {
609 log_message(LOG_LEVEL_INFO, "Immediate shutdown completed (SIGINT)");
610 }
611
612 10 cleanup_server_resources(pool, server_fd);
613
614 10 log_session_stats(ssl_ctx);
615 10 }
616
617 10 void handle_signal(int sig)
618 {
619 10 shutdown_state_t old_state = atomic_load(&g_shutdown_ctx.state);
620
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (old_state != SHUTDOWN_STATE_RUNNING) {
621 return;
622 }
623
624
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (sig == SIGINT) {
625 atomic_store(&g_shutdown_ctx.state, SHUTDOWN_STATE_FORCED);
626 log_message(LOG_LEVEL_WARN, "SIGINT received - immediate shutdown (development mode)");
627 } else {
628 10 atomic_store(&g_shutdown_ctx.state, SHUTDOWN_STATE_DRAINING);
629
630 10 clock_gettime(CLOCK_REALTIME, &g_shutdown_ctx.deadline);
631 10 g_shutdown_ctx.deadline.tv_sec += g_shutdown_ctx.timeout_seconds;
632
633 10 atomic_store(&g_shutdown_ctx.metrics.peak_in_flight,
634 atomic_load(&g_shutdown_ctx.in_flight_requests));
635 10 clock_gettime(CLOCK_REALTIME, &g_shutdown_ctx.metrics.start_time);
636
637 10 log_message(LOG_LEVEL_INFO,
638 "SIGTERM received - graceful shutdown initiated. "
639 "Draining %zu in-flight requests with %ds timeout",
640 10 atomic_load(&g_shutdown_ctx.in_flight_requests),
641 g_shutdown_ctx.timeout_seconds);
642 }
643
644
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (g_server_fd >= 0) {
645 10 shutdown(g_server_fd, SHUT_RD);
646 }
647 }
648
649 static void handle_http2_connection(SSL *ssl, int client_fd, ServerConfig *config, struct io_uring *ring);
650 static void handle_http1_connection(SSL *ssl, int client_fd, ServerConfig *config);
651
652 /* Callback for nghttp2 to send data via SSL */
653 12 static ssize_t send_callback(nghttp2_session *session, const uint8_t *data,
654 size_t length, int flags, void *user_data)
655 {
656 (void)session;
657 (void)flags;
658 12 H2IO *io = (H2IO *)user_data;
659 12 io->want_read = 0;
660 12 io->want_write = 0;
661
662 12 ssize_t ret = SSL_write(io->ssl, data, length);
663
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (ret > 0)
664 {
665 H2_LOG("h2 send_callback wrote=%zd", ret);
666 12 return ret;
667 }
668
669 int ssl_error = SSL_get_error(io->ssl, ret);
670 if (ssl_error == SSL_ERROR_WANT_READ) {
671 io->want_read = 1;
672 H2_LOG("h2 send_callback WANT_READ");
673 return NGHTTP2_ERR_WOULDBLOCK;
674 }
675 if (ssl_error == SSL_ERROR_WANT_WRITE) {
676 io->want_write = 1;
677 H2_LOG("h2 send_callback WANT_WRITE");
678 return NGHTTP2_ERR_WOULDBLOCK;
679 }
680 log_message(LOG_LEVEL_ERROR, "SSL_write failed in send_callback. Error: %d", ssl_error);
681 return NGHTTP2_ERR_CALLBACK_FAILURE;
682 }
683
684 /* Callback for nghttp2 to receive data via SSL */
685 15 static ssize_t recv_callback(nghttp2_session *session, uint8_t *buf, size_t length,
686 int flags, void *user_data)
687 {
688 H2_LOG("recv_callback: start");
689 (void)session;
690 (void)flags;
691 15 H2IO *io = (H2IO *)user_data;
692 15 io->want_read = 0;
693 15 io->want_write = 0;
694
695 15 ssize_t ret = SSL_read(io->ssl, buf, length);
696
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 9 times.
15 if (ret <= 0)
697 {
698 6 int ssl_error = SSL_get_error(io->ssl, ret);
699
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 if (ssl_error == SSL_ERROR_ZERO_RETURN)
700 {
701 3 log_message(LOG_LEVEL_INFO, "SSL connection closed by peer");
702 3 return NGHTTP2_ERR_EOF;
703 }
704
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (ssl_error == SSL_ERROR_WANT_READ) {
705 3 io->want_read = 1;
706 H2_LOG("h2 recv_callback WANT_READ");
707 3 return NGHTTP2_ERR_WOULDBLOCK;
708 }
709 if (ssl_error == SSL_ERROR_WANT_WRITE) {
710 io->want_write = 1;
711 H2_LOG("h2 recv_callback WANT_WRITE");
712 return NGHTTP2_ERR_WOULDBLOCK;
713 }
714 log_message(LOG_LEVEL_ERROR, "SSL_read failed in recv_callback. Error: %d", ssl_error);
715 return NGHTTP2_ERR_CALLBACK_FAILURE;
716 }
717 9 io->total_read += (size_t)ret;
718 H2_LOG("recv_callback: end, bytes read: %zd", ret);
719 9 return ret;
720 }
721
722 /* Nonblocking TLS handshake using thread-local io_uring with instrumentation */
723 10 static int perform_nonblocking_ssl_accept(SSL *ssl, int client_fd, struct io_uring *ring)
724 {
725 int ret;
726 struct timeval start, end;
727 10 gettimeofday(&start, NULL);
728
729 10 ServerConfig *config = SSL_get_app_data(ssl);
730
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 int timeout_ms = config ? config->tls_handshake_timeout_ms : 10000;
731
732
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 while ((ret = SSL_accept(ssl)) <= 0)
733 {
734 int err = SSL_get_error(ssl, ret);
735 if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE)
736 {
737 gettimeofday(&end, NULL);
738 long elapsed_ms = (end.tv_sec - start.tv_sec) * 1000 +
739 (end.tv_usec - start.tv_usec) / US_PER_MS;
740 if (elapsed_ms > timeout_ms) {
741 log_message(LOG_LEVEL_WARN, "TLS handshake timeout: %ldms exceeded (limit %dms)",
742 elapsed_ms, timeout_ms);
743 return -1;
744 }
745
746 int poll_flags = (err == SSL_ERROR_WANT_READ) ? POLLIN : POLLOUT;
747 struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
748 if (!sqe)
749 {
750 log_message(LOG_LEVEL_ERROR, "Failed to get SQE during handshake");
751 return -1;
752 }
753 io_uring_prep_poll_add(sqe, client_fd, poll_flags);
754 int submit_ret = io_uring_submit(ring);
755 if (submit_ret < 0)
756 {
757 log_io_uring_error("io_uring_submit failed during handshake", submit_ret);
758 return -1;
759 }
760 struct io_uring_cqe *cqe;
761 int wait_ret = io_uring_wait_cqe(ring, &cqe);
762 if (wait_ret < 0)
763 {
764 log_io_uring_error("io_uring_wait_cqe failed during handshake", wait_ret);
765 return -1;
766 }
767 io_uring_cqe_seen(ring, cqe);
768 }
769 else
770 {
771 log_message(LOG_LEVEL_ERROR, "SSL_accept failed nonblockingly, error: %d", err);
772 return -1;
773 }
774 }
775 10 gettimeofday(&end, NULL);
776 10 long handshake_ms = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / US_PER_MS;
777 10 log_message(LOG_LEVEL_INFO, "Nonblocking SSL handshake completed in %ld ms", handshake_ms);
778 10 metrics_increment_tls_handshake(1);
779 10 metrics_record_tls_handshake_duration(handshake_ms / 1000.0);
780 10 return ret;
781 }
782
783 3 static nghttp2_session *h2_session_init(nghttp2_session_callbacks **out_callbacks,
784 H2IO *io, ServerConfig *config)
785 {
786 3 nghttp2_session *session = NULL;
787 3 nghttp2_option *options = NULL;
788
789
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (nghttp2_session_callbacks_new(out_callbacks) != 0) {
790 log_message(LOG_LEVEL_ERROR, "Failed to initialize HTTP/2 callbacks");
791 return NULL;
792 }
793
794 3 nghttp2_session_callbacks_set_send_callback(*out_callbacks, send_callback);
795 3 nghttp2_session_callbacks_set_recv_callback(*out_callbacks, recv_callback);
796 3 nghttp2_session_callbacks_set_on_header_callback(*out_callbacks, on_header_callback);
797 3 nghttp2_session_callbacks_set_on_frame_recv_callback(*out_callbacks, on_frame_recv_callback);
798 3 nghttp2_session_callbacks_set_on_stream_close_callback(*out_callbacks, on_stream_close_callback);
799
800
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 if (nghttp2_option_new(&options) == 0) {
801 3 nghttp2_option_set_peer_max_concurrent_streams(options,
802 3 (uint32_t)config->http2.max_concurrent_streams);
803 }
804
805
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (nghttp2_session_server_new2(&session, *out_callbacks, io, options) != 0) {
806 log_message(LOG_LEVEL_ERROR, "Failed to create nghttp2 session");
807 nghttp2_session_callbacks_del(*out_callbacks);
808 *out_callbacks = NULL;
809 if (options) nghttp2_option_del(options);
810 return NULL;
811 }
812
813
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (options) nghttp2_option_del(options);
814
815 3 log_message(LOG_LEVEL_INFO, "HTTP/2 session started (max_streams=%d keepalive=%ds)",
816 config->http2.max_concurrent_streams, config->http2.keepalive_timeout);
817
818 3 return session;
819 }
820
821 3 static int h2_session_send_initial_settings(nghttp2_session *session)
822 {
823 3 int rv = nghttp2_submit_settings(session, NGHTTP2_FLAG_NONE, NULL, 0);
824
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (rv < 0) {
825 log_message(LOG_LEVEL_ERROR, "Failed to send initial SETTINGS frame: %s", nghttp2_strerror(rv));
826 return -1;
827 }
828
829 3 rv = nghttp2_session_send(session);
830
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 if (rv < 0 && rv != NGHTTP2_ERR_WOULDBLOCK) {
831 log_message(LOG_LEVEL_ERROR, "Failed to flush initial SETTINGS: %s", nghttp2_strerror(rv));
832 return -1;
833 }
834
835 3 return 0;
836 }
837
838 /* HTTP/2 connection handler using thread-local io_uring */
839 3 static void handle_http2_connection(SSL *ssl, int client_fd, ServerConfig *config, struct io_uring *ring)
840 {
841 (void)ring;
842 3 int flags = fcntl(client_fd, F_GETFL, 0);
843 3 fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
844
845 3 nghttp2_session_callbacks *callbacks = NULL;
846 3 H2IO io = {
847 .ssl = ssl,
848 .config = config,
849 .want_read = 0,
850 .want_write = 0,
851 .total_read = 0,
852 .request_count = 0,
853 3 .request_timeout_ms = config->request_timeout_ms,
854 };
855
856 3 nghttp2_session *session = h2_session_init(&callbacks, &io, config);
857
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!session) {
858 return;
859 }
860
861
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (h2_session_send_initial_settings(session) != 0) {
862 nghttp2_session_del(session);
863 nghttp2_session_callbacks_del(callbacks);
864 return;
865 }
866
867 3 time_t last_activity = time(NULL);
868 3 time_t connection_start = last_activity;
869 3 int rv = 0;
870
871
1/4
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
6 while (nghttp2_session_want_read(session) || nghttp2_session_want_write(session))
872 {
873 6 time_t now = time(NULL);
874
875
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (now - last_activity > config->http2.keepalive_timeout) {
876 log_message(LOG_LEVEL_INFO, "HTTP/2 connection timeout: idle %lds (max %ds)",
877 (long)(now - last_activity), config->http2.keepalive_timeout);
878 3 break;
879 }
880
881
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (io.request_count >= config->http2.max_requests_per_connection) {
882 log_message(LOG_LEVEL_INFO, "HTTP/2 connection closed: reached max requests (%d)",
883 config->http2.max_requests_per_connection);
884 break;
885 }
886
887 struct timeval tv_now;
888 6 gettimeofday(&tv_now, NULL);
889 6 int elapsed_ms = (int)((tv_now.tv_sec - io.request_start.tv_sec) * 1000 +
890 6 (tv_now.tv_usec - io.request_start.tv_usec) / 1000);
891
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
6 if (io.request_count > 0 && elapsed_ms > io.request_timeout_ms) {
892 log_message(LOG_LEVEL_WARN, "HTTP/2 request timeout: %dms exceeded (limit %dms)",
893 elapsed_ms, io.request_timeout_ms);
894 metrics_increment_request_timeouts();
895 break;
896 }
897
898 6 short events = 0;
899
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
6 if (io.want_read || io.want_write) {
900
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (io.want_read) events |= POLLIN;
901
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (io.want_write) events |= POLLOUT;
902 } else {
903
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 if (nghttp2_session_want_read(session)) events |= POLLIN;
904
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (nghttp2_session_want_write(session)) events |= POLLOUT;
905 }
906
907 H2_LOG("h2 loop: want_read=%d want_write=%d io.want_read=%d io.want_write=%d events=0x%x",
908 nghttp2_session_want_read(session), nghttp2_session_want_write(session),
909 io.want_read, io.want_write, events);
910
911 6 struct pollfd pfd = {.fd = client_fd, .events = events};
912 6 int pret = poll(&pfd, 1, H2_POLL_TIMEOUT_MS);
913
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (pret < 0) {
914 log_message(LOG_LEVEL_ERROR, "poll failed: %s", strerror(errno));
915 break;
916 }
917
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (pret == 0) continue;
918
919
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (pfd.revents & H2_POLL_ERROR_EVENTS) {
920 log_message(LOG_LEVEL_ERROR, "poll error/hangup: revents=%d", pfd.revents);
921 break;
922 }
923
924 H2_LOG("h2 loop: revents=0x%x", pfd.revents);
925
926
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (pfd.revents & POLLIN) {
927 6 rv = nghttp2_session_recv(session);
928
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 if (rv < 0) {
929
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (rv == NGHTTP2_ERR_EOF) {
930 3 log_message(LOG_LEVEL_INFO, "HTTP/2 session closed by client");
931 } else if (rv != NGHTTP2_ERR_WOULDBLOCK) {
932 log_message(LOG_LEVEL_ERROR, "nghttp2_session_recv error: %s", nghttp2_strerror(rv));
933 }
934
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (rv != NGHTTP2_ERR_WOULDBLOCK) break;
935 } else {
936 H2_LOG("h2 recv processed rv=%d total_read=%zu", rv, io.total_read);
937 3 last_activity = now;
938 }
939 }
940
941
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (pfd.revents & POLLOUT) {
942 rv = nghttp2_session_send(session);
943 if (rv < 0 && rv != NGHTTP2_ERR_WOULDBLOCK) {
944 log_message(LOG_LEVEL_ERROR, "nghttp2_session_send error: %s", nghttp2_strerror(rv));
945 break;
946 } else if (rv >= 0) {
947 H2_LOG("h2 send processed rv=%d", rv);
948 last_activity = now;
949 }
950 }
951 }
952
953 3 time_t conn_duration = time(NULL) - connection_start;
954 3 log_message(LOG_LEVEL_INFO, "HTTP/2 session ended: duration=%lds requests=%d",
955 (long)conn_duration, io.request_count);
956
957 3 nghttp2_session_del(session);
958 3 nghttp2_session_callbacks_del(callbacks);
959 }
960
961 #define SERVER_SECURITY_HEADERS_BUFFER_SIZE 512
962
963 3 static void add_global_security_headers(char *buffer, size_t *len, SecurityHeadersConfig *config)
964 {
965
2/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
3 if (!config || !config->enabled || *len >= SERVER_SECURITY_HEADERS_BUFFER_SIZE)
966 3 return;
967
968 size_t remaining = SERVER_SECURITY_HEADERS_BUFFER_SIZE - *len;
969
970 for (int i = 0; i < config->header_count && remaining > 20; i++) {
971 const SecurityHeader *header = &config->headers[i];
972 int header_len = snprintf(buffer + *len, remaining, "%s: %s\r\n",
973 header->name, header->value);
974 if (header_len > 0 && (size_t)header_len < remaining) {
975 *len += (size_t)header_len;
976 remaining -= (size_t)header_len;
977 } else {
978 break;
979 }
980 }
981 }
982
983 /* HTTP/1.1 connection handler - synchronous SSL I/O with keep-alive */
984 7 static void handle_http1_connection(SSL *ssl, int client_fd, ServerConfig *config)
985 {
986 (void)client_fd;
987 struct timeval request_start;
988 7 int timeout_ms = config->request_timeout_ms;
989
990 // Serve multiple HTTP/1.x requests on the same TLS socket
991 5 for (;;) {
992 char buffer[BUFFER_SIZE];
993 12 int total_read = 0;
994 12 gettimeofday(&request_start, NULL);
995
996 // 1) Read up to the end of headers (\r\n\r\n)
997 12 int header_end = -1;
998
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1 times.
14 while (total_read < BUFFER_SIZE - 1) {
999 13 int n = SSL_read(ssl, buffer + total_read,
1000 BUFFER_SIZE - 1 - total_read);
1001
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 9 times.
13 if (n <= 0) {
1002 // client closed connection or SSL error
1003 4 goto shutdown_and_close;
1004 }
1005 9 total_read += n;
1006
1007 struct timeval now;
1008 9 gettimeofday(&now, NULL);
1009 9 int elapsed_ms = (int)((now.tv_sec - request_start.tv_sec) * 1000 +
1010 9 (now.tv_usec - request_start.tv_usec) / 1000);
1011
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (elapsed_ms > timeout_ms) {
1012 log_message(LOG_LEVEL_WARN, "Request timeout: %dms exceeded (limit %dms)",
1013 elapsed_ms, timeout_ms);
1014 metrics_increment_request_timeouts();
1015
1016 char timeout_response[SERVER_SECURITY_HEADERS_BUFFER_SIZE];
1017 int len = snprintf(timeout_response, sizeof(timeout_response),
1018 "HTTP/1.1 408 Request Timeout\r\n"
1019 "Content-Length: 0\r\n"
1020 "Retry-After: 5\r\n");
1021 if (len > 0 && (size_t)len < sizeof(timeout_response)) {
1022 size_t current_len = (size_t)len;
1023 add_global_security_headers(timeout_response, &current_len, &config->security_headers);
1024 if (current_len + 2 < sizeof(timeout_response)) {
1025 strcpy(timeout_response + current_len, "\r\n");
1026 SSL_write(ssl, timeout_response, current_len + 2);
1027 }
1028 }
1029 goto shutdown_and_close;
1030 }
1031
1032 9 header_end = find_header_end(buffer, (size_t)total_read);
1033
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2 times.
9 if (header_end >= 0)
1034 7 break;
1035 }
1036 8 buffer[total_read] = '\0';
1037
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (header_end < 0) {
1038 char too_large[SERVER_SECURITY_HEADERS_BUFFER_SIZE];
1039 1 int len = snprintf(too_large, sizeof(too_large),
1040 "HTTP/1.1 431 Request Header Fields Too Large\r\n"
1041 "Content-Length: 0\r\n");
1042
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (len > 0 && (size_t)len < sizeof(too_large)) {
1043 1 size_t current_len = (size_t)len;
1044 1 add_global_security_headers(too_large, &current_len, &config->security_headers);
1045
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (current_len + 2 < sizeof(too_large)) {
1046 1 strcpy(too_large + current_len, "\r\n");
1047 1 SSL_write(ssl, too_large, current_len + 2);
1048 }
1049 }
1050 1 goto shutdown_and_close;
1051 }
1052
1053 // 2) Parse the request
1054 HttpRequest req;
1055
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 5 times.
7 if (parse_http_request(buffer, total_read, &req) != 0) {
1056 char bad_response[SERVER_SECURITY_HEADERS_BUFFER_SIZE];
1057 2 int len = snprintf(bad_response, sizeof(bad_response),
1058 "HTTP/1.1 400 Bad Request\r\n"
1059 "Content-Length: 0\r\n");
1060
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (len > 0 && (size_t)len < sizeof(bad_response)) {
1061 2 size_t current_len = (size_t)len;
1062 2 add_global_security_headers(bad_response, &current_len, &config->security_headers);
1063
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (current_len + 2 < sizeof(bad_response)) {
1064 2 strcpy(bad_response + current_len, "\r\n");
1065 2 SSL_write(ssl, bad_response, current_len + 2);
1066 }
1067 }
1068 // malformed request → close connection
1069 2 goto shutdown_and_close;
1070 }
1071
1072 5 log_message(LOG_LEVEL_INFO, "Valid HTTP request received [id=%s]. Routing...", req.request_id);
1073
1074 // 3) Route & send response (static, proxy, etc.)
1075 5 route_request_tls(&req, buffer, total_read, config, ssl, NULL);
1076
1077 struct timeval request_end;
1078 5 gettimeofday(&request_end, NULL);
1079 5 double request_duration = (request_end.tv_sec - request_start.tv_sec) +
1080 5 (request_end.tv_usec - request_start.tv_usec) / (double)US_PER_MS;
1081 5 metrics_increment_request(req.method, req.path, 200);
1082 5 metrics_record_request_duration(request_duration);
1083
1084 // 4) Loop back to read the next request
1085 // (do NOT shutdown/close here)
1086 }
1087
1088 7 shutdown_and_close:
1089 7 SSL_shutdown(ssl);
1090 7 close(client_fd);
1091 7 }
1092
1093 /* Worker task: uses thread-local io_uring for per-connection I/O */
1094 10 void client_task(void *arg)
1095 {
1096 10 ClientTaskData *data = (ClientTaskData *)arg;
1097 static __thread struct io_uring *local_ring = NULL;
1098
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (!local_ring)
1099 {
1100 10 local_ring = malloc(sizeof(struct io_uring));
1101
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
10 if (!local_ring || io_uring_queue_init(QUEUE_DEPTH, local_ring, 0) < 0)
1102 {
1103 log_message(LOG_LEVEL_ERROR, "Failed to initialize thread-local io_uring");
1104 if (local_ring)
1105 {
1106 free(local_ring);
1107 local_ring = NULL;
1108 }
1109 ip_limiter_decrement(&g_ip_limiter, data->client_ip);
1110 close(data->client_fd);
1111 free(data);
1112 atomic_fetch_sub(&g_shutdown_ctx.in_flight_requests, 1);
1113 metrics_set_active_connections(atomic_load(&g_shutdown_ctx.in_flight_requests));
1114 return;
1115 }
1116 }
1117 10 handle_client(data->client_fd, data->config, local_ring);
1118 10 io_uring_queue_exit(local_ring);
1119 10 free(local_ring);
1120 10 local_ring = NULL;
1121 10 ip_limiter_decrement(&g_ip_limiter, data->client_ip);
1122 10 free(data);
1123 10 atomic_fetch_sub(&g_shutdown_ctx.in_flight_requests, 1);
1124 10 metrics_set_active_connections(atomic_load(&g_shutdown_ctx.in_flight_requests));
1125 }
1126
1127 /* Main per-connection handler; performs nonblocking TLS handshake before dispatching via ALPN */
1128 10 void handle_client(int client_fd, ServerConfig *config, struct io_uring *ring)
1129 {
1130 struct timeval timeout;
1131 10 timeout.tv_sec = 5;
1132 10 timeout.tv_usec = 0;
1133 10 setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
1134 10 setsockopt(client_fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
1135 10 SSL *ssl = SSL_new(ssl_ctx);
1136
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!ssl)
1137 {
1138 close(client_fd);
1139 return;
1140 }
1141 10 SSL_set_fd(ssl, client_fd);
1142 10 SSL_set_app_data(ssl, config);
1143
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (perform_nonblocking_ssl_accept(ssl, client_fd, ring) <= 0)
1144 {
1145 log_message(LOG_LEVEL_ERROR, "Nonblocking SSL handshake failed");
1146 metrics_increment_tls_handshake(0);
1147 SSL_free(ssl);
1148 close(client_fd);
1149 return;
1150 }
1151 10 const unsigned char *alpn_proto = NULL;
1152 10 unsigned int alpn_len = 0;
1153 10 SSL_get0_alpn_selected(ssl, &alpn_proto, &alpn_len);
1154
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
10 if (alpn_len == 2 && memcmp(alpn_proto, "h2", 2) == 0)
1155 {
1156 3 log_message(LOG_LEVEL_INFO, "Negotiated HTTP/2");
1157 3 handle_http2_connection(ssl, client_fd, config, ring);
1158 }
1159 else
1160 {
1161 7 log_message(LOG_LEVEL_INFO, "Negotiated HTTP/1.1");
1162 7 handle_http1_connection(ssl, client_fd, config);
1163 }
1164 10 SSL_shutdown(ssl);
1165 10 SSL_free(ssl);
1166 10 close(client_fd);
1167 }
1168
1169 /* Main server accept loop using a global io_uring instance */
1170 10 int start_server(ServerConfig *config)
1171 {
1172 10 ThreadPool *pool = NULL;
1173 10 time_t last_stats_log = time(NULL);
1174 int accept_result;
1175
1176
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (!config) {
1177 log_message(LOG_LEVEL_ERROR, "start_server: config parameter is NULL");
1178 return -1;
1179 }
1180
1181
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (initialize_server(config, &pool) != 0) {
1182 log_message(LOG_LEVEL_ERROR, "Server initialization failed");
1183 return -1;
1184 }
1185
1186 10 log_message(LOG_LEVEL_INFO, "Emme listening on port %d...", config->port);
1187
1188
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 while (atomic_load(&g_shutdown_ctx.state) == SHUTDOWN_STATE_RUNNING) {
1189 20 time_t now = time(NULL);
1190
1191
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (now - last_stats_log >= SESSION_STATS_INTERVAL_SEC) {
1192 log_session_stats(ssl_ctx);
1193 ip_limiter_compact(&g_ip_limiter);
1194 metrics_set_ip_limiter_entries((long)ip_limiter_get_total_entries(&g_ip_limiter));
1195 last_stats_log = now;
1196 }
1197
1198 20 accept_result = accept_and_dispatch_client(pool, config);
1199
1200
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 10 times.
20 if (accept_result == 1) {
1201 10 break;
1202
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 } else if (accept_result < 0) {
1203 log_message(LOG_LEVEL_ERROR, "Accept loop error, shutting down");
1204 break;
1205 }
1206 }
1207
1208 10 perform_shutdown(pool, g_server_fd);
1209
1210 10 return 0;
1211 }
1212