| 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, ¤t_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, ¤t_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, ¤t_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, ¤t_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 |