emme coverage


Directory: src/
File: src/router.c
Date: 2026-05-14 14:35:13
Exec Total Coverage
Lines: 190 467 40.7%
Functions: 14 22 63.6%
Branches: 118 344 34.3%

Line Branch Exec Source
1 /* router.c - Routing module for the web server (TLS version)
2 *
3 * This module examines the HTTP request (HttpRequest) and, based on the routes
4 * defined in the configuration (ServerConfig), decides whether to:
5 * - Serve a static file (using sendfile for zero-copy).
6 * - Forward the request to a backend (reverse proxy).
7 *
8 * In TLS mode, data sent to the client uses SSL_write() and data is read
9 * using SSL_read().
10 */
11
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <arpa/inet.h>
16 #include <openssl/ssl.h>
17 #include <openssl/err.h>
18 #include "router.h"
19
20 #define HEADER_BUFFER_SIZE 256
21 #define FILEPATH_BUFFER_SIZE 512
22 #define IP_BUFFER_SIZE 64
23 #define CIRCUIT_BREAKER_ERROR_BODY "{\"error\":\"Service temporarily unavailable\"}"
24 #define CIRCUIT_BREAKER_ERROR_LEN 38
25 #include "log.h"
26 #include "config.h"
27 #include "backend_pool.h"
28 #include "tls.h"
29 #include "http2_response.h"
30 #include "http2_client.h"
31 #include "metrics.h"
32 #include "http_status.h"
33
34 static int ssl_write_all(SSL *ssl, const char *buf, size_t len);
35
36 typedef enum {
37 STATIC_LOOKUP_ERROR = -1,
38 STATIC_LOOKUP_NO_ROUTE = 0,
39 STATIC_LOOKUP_READY = 1,
40 STATIC_LOOKUP_NOT_FOUND = 2,
41 STATIC_LOOKUP_FORBIDDEN = 3,
42 } StaticLookupResult;
43
44 2 static int send_health_response(SSL *ssl, Http2Response *h2resp, ServerConfig *config)
45 {
46 2 const char *body = "{\"status\":\"ok\"}";
47 2 size_t body_len = strlen(body);
48 2 shutdown_state_t state = atomic_load(&g_shutdown_ctx.state);
49
50
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (state == SHUTDOWN_STATE_DRAINING) {
51 const char *draining_body = "{\"status\":\"draining\",\"reason\":\"graceful_shutdown\"}";
52 size_t draining_len = strlen(draining_body);
53
54 if (h2resp) {
55 h2_response_init(h2resp);
56 h2_response_set_status(h2resp, HTTP_STATUS_SERVICE_UNAVAILABLE, "Service Unavailable");
57 h2_response_set_content_type(h2resp, "application/json");
58 h2_response_set_body(h2resp, draining_body, draining_len);
59 h2_response_add_security_headers(h2resp, &config->security_headers, NULL);
60 h2_response_finalize(h2resp);
61 } else {
62 const char *headers =
63 "HTTP/1.1 503 Service Unavailable\r\n"
64 "Content-Type: application/json\r\n"
65 "Retry-After: 5\r\n"
66 "Content-Length: 53\r\n"
67 "\r\n";
68 ssl_write_all(ssl, headers, strlen(headers));
69 ssl_write_all(ssl, draining_body, draining_len);
70 }
71
72 return 0;
73 }
74
75
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (h2resp) {
76 h2_response_init(h2resp);
77 h2_response_set_status(h2resp, HTTP_STATUS_OK, "OK");
78 h2_response_set_content_type(h2resp, "application/json");
79 h2_response_set_body(h2resp, body, body_len);
80 h2_response_add_security_headers(h2resp, &config->security_headers, NULL);
81 h2_response_finalize(h2resp);
82 } else {
83 2 const char *headers =
84 "HTTP/1.1 200 OK\r\n"
85 "Content-Type: application/json\r\n"
86 "Content-Length: 15\r\n"
87 "\r\n";
88 2 ssl_write_all(ssl, headers, strlen(headers));
89 2 ssl_write_all(ssl, body, body_len);
90 }
91
92 2 return 0;
93 }
94
95 9 static int is_health_check(const HttpRequest *req)
96 {
97 9 return (strcmp(req->path, "/health") == 0);
98 }
99
100 12 static SecurityHeadersConfig *get_security_headers_for_request(HttpRequest *req, ServerConfig *config)
101 {
102 static SecurityHeadersConfig default_config = {0};
103
104
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 12 times.
12 if (!config || !req || !req->path)
105 return &default_config;
106
107
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 for (int i = 0; i < config->route_count; i++) {
108 12 Route *route = &config->routes[i];
109 12 size_t path_len = strlen(route->path);
110
111
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (strncmp(req->path, route->path, path_len) == 0) {
112
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (route->security_headers.enabled) {
113 return &route->security_headers;
114 }
115
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 8 times.
12 if (route->inherit_global_headers) {
116 4 return &config->security_headers;
117 }
118 8 return &default_config;
119 }
120 }
121
122 return &config->security_headers;
123 }
124
125 12 static CORSConfig *get_cors_config_for_request(HttpRequest *req, ServerConfig *config)
126 {
127 static CORSConfig default_cors = {0};
128
129
3/6
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 12 times.
12 if (!config || !req || !req->path)
130 return &default_cors;
131
132
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 for (int i = 0; i < config->route_count; i++) {
133 12 Route *route = &config->routes[i];
134 12 size_t path_len = strlen(route->path);
135
136
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (strncmp(req->path, route->path, path_len) == 0) {
137
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 if (route->cors.enabled) {
138 return &route->cors;
139 }
140 12 return &default_cors;
141 }
142 }
143
144 return &default_cors;
145 }
146
147 23 static int ssl_write_all(SSL *ssl, const char *buf, size_t len)
148 {
149 23 size_t total = 0;
150
151
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 23 times.
46 while (total < len)
152 {
153 23 int written = SSL_write(ssl, buf + total, (int)(len - total));
154
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 if (written <= 0)
155 return -1;
156 23 total += (size_t)written;
157 }
158
159 23 return 0;
160 }
161
162 #define SECURITY_HEADERS_BUFFER_SIZE 1024
163
164 11 static int add_security_headers_to_buffer(char *buffer, size_t *len, SecurityHeadersConfig *config)
165 {
166
2/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
11 if (!config || !config->enabled || *len >= SECURITY_HEADERS_BUFFER_SIZE)
167 11 return 0;
168
169 size_t remaining = SECURITY_HEADERS_BUFFER_SIZE - *len;
170 int written = 0;
171
172 for (int i = 0; i < config->header_count && remaining > 20; i++) {
173 const SecurityHeader *header = &config->headers[i];
174 int header_len = snprintf(buffer + *len, remaining, "%s: %s\r\n",
175 header->name, header->value);
176 if (header_len > 0 && (size_t)header_len < remaining) {
177 *len += (size_t)header_len;
178 remaining -= (size_t)header_len;
179 written++;
180 } else {
181 break;
182 }
183 }
184
185 if (written > 0) {
186 metrics_increment_security_headers_sent();
187 }
188
189 return written;
190 }
191
192 11 static int add_cors_headers_to_buffer(char *buffer, size_t *len, CORSConfig *cors)
193 {
194
2/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 11 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
11 if (!cors || !cors->enabled || *len >= SECURITY_HEADERS_BUFFER_SIZE)
195 11 return 0;
196
197 size_t remaining = SECURITY_HEADERS_BUFFER_SIZE - *len;
198 int written = 0;
199 int header_len;
200
201 if (cors->allow_origin[0] != '\0') {
202 header_len = snprintf(buffer + *len, remaining, "Access-Control-Allow-Origin: %s\r\n",
203 cors->allow_origin);
204 if (header_len > 0 && (size_t)header_len < remaining) {
205 *len += (size_t)header_len;
206 remaining -= (size_t)header_len;
207 written++;
208 }
209 }
210
211 if (cors->allow_methods[0] != '\0' && remaining > 30) {
212 header_len = snprintf(buffer + *len, remaining, "Access-Control-Allow-Methods: %s\r\n",
213 cors->allow_methods);
214 if (header_len > 0 && (size_t)header_len < remaining) {
215 *len += (size_t)header_len;
216 remaining -= (size_t)header_len;
217 written++;
218 }
219 }
220
221 if (cors->allow_headers[0] != '\0' && remaining > 30) {
222 header_len = snprintf(buffer + *len, remaining, "Access-Control-Allow-Headers: %s\r\n",
223 cors->allow_headers);
224 if (header_len > 0 && (size_t)header_len < remaining) {
225 *len += (size_t)header_len;
226 remaining -= (size_t)header_len;
227 written++;
228 }
229 }
230
231 if (cors->allow_credentials && remaining > 40) {
232 header_len = snprintf(buffer + *len, remaining, "Access-Control-Allow-Credentials: true\r\n");
233 if (header_len > 0 && (size_t)header_len < remaining) {
234 *len += (size_t)header_len;
235 remaining -= (size_t)header_len;
236 written++;
237 }
238 }
239
240 if (cors->max_age_seconds > 0 && remaining > 30) {
241 header_len = snprintf(buffer + *len, remaining, "Access-Control-Max-Age: %d\r\n",
242 cors->max_age_seconds);
243 if (header_len > 0 && (size_t)header_len < remaining) {
244 *len += (size_t)header_len;
245 remaining -= (size_t)header_len;
246 written++;
247 }
248 }
249
250 if (written > 0) {
251 metrics_increment_cors_headers_sent();
252 }
253
254 return written;
255 }
256
257 3 static int send_simple_response_with_config(SSL *ssl, const char *status_line,
258 const char *content_type, const char *body,
259 HttpRequest *req, ServerConfig *config)
260 {
261
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 size_t body_len = body ? strlen(body) : 0;
262 3 SecurityHeadersConfig *sec_headers = get_security_headers_for_request(req, config);
263 3 CORSConfig *cors = get_cors_config_for_request(req, config);
264
265 char header[HEADER_BUFFER_SIZE];
266
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 int header_len = snprintf(header, sizeof(header),
267 "%s\r\n%sContent-Length: %zu\r\n",
268 status_line,
269 content_type ? content_type : "",
270 body_len);
271
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (header_len < 0 || (size_t)header_len >= sizeof(header))
272 return -1;
273
274 3 size_t current_len = (size_t)header_len;
275 3 add_security_headers_to_buffer(header, &current_len, sec_headers);
276 3 add_cors_headers_to_buffer(header, &current_len, cors);
277
278
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (current_len + 2 >= sizeof(header))
279 return -1;
280
281 3 strcpy(header + current_len, "\r\n");
282 3 current_len += 2;
283
284
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (ssl_write_all(ssl, header, current_len) != 0)
285 return -1;
286
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
3 if (body_len > 0 && ssl_write_all(ssl, body, body_len) != 0)
287 return -1;
288 3 return 0;
289 }
290
291
292
293 3 static int populate_http2_response(Http2Response *resp, const char *body,
294 int status_code, const char *status_text,
295 const char *content_type)
296 {
297 int body_len;
298
299
4/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
3 if (!resp || !body || !status_text || !content_type)
300 return -1;
301
302 3 body_len = snprintf(resp->body, sizeof(resp->body), "%s", body);
303
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (body_len < 0 || (size_t)body_len >= sizeof(resp->body))
304 return -1;
305
306 3 resp->body_len = (size_t)body_len;
307 3 resp->status_code = status_code;
308 3 snprintf(resp->status_text, sizeof(resp->status_text), "%s", status_text);
309 3 snprintf(resp->content_type, sizeof(resp->content_type), "%s", content_type);
310 3 resp->num_headers = 0;
311 3 return 0;
312 }
313
314 9 static const char *guess_content_type(const char *path)
315 {
316 9 const char *ext = strrchr(path, '.');
317
318
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (!ext)
319 return "application/octet-stream";
320
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 6 times.
9 if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0)
321 3 return "text/html";
322
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 if (strcmp(ext, ".css") == 0)
323 1 return "text/css";
324
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (strcmp(ext, ".js") == 0)
325 1 return "application/javascript";
326
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (strcmp(ext, ".json") == 0)
327 1 return "application/json";
328
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (strcmp(ext, ".txt") == 0)
329 1 return "text/plain";
330
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (strcmp(ext, ".png") == 0)
331 1 return "image/png";
332
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
333 1 return "image/jpeg";
334 return "application/octet-stream";
335 }
336
337 15 static StaticLookupResult lookup_static_file(const HttpRequest *req, ServerConfig *config,
338 int *fd_out, off_t *filesize_out,
339 const char **content_type_out)
340 {
341
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1 times.
16 for (int i = 0; i < config->route_count; i++)
342 {
343
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (strcmp(config->routes[i].technology, "static") == 0)
344 {
345 15 size_t prefix_len = strlen(config->routes[i].path);
346
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (strlen(req->path) < prefix_len)
347 continue;
348
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1 times.
15 if (strncmp(req->path, config->routes[i].path, prefix_len) == 0)
349 {
350 char filepath[FILEPATH_BUFFER_SIZE];
351 char root_real[PATH_MAX];
352 char file_real[PATH_MAX];
353 14 const char *root = config->routes[i].document_root;
354 14 size_t root_len = strlen(root);
355 bool has_slash;
356 int fd;
357 int written;
358
359
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 13 times.
14 if (root_len == 0)
360 1 return STATIC_LOOKUP_ERROR;
361
362 13 has_slash = (root[root_len - 1] == '/');
363 13 written = snprintf(filepath, sizeof(filepath),
364 has_slash ? "%s%s" : "%s/%s",
365 root,
366
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13 times.
13 req->path + prefix_len);
367
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 13 times.
13 if (written < 0 || (size_t)written >= sizeof(filepath))
368 return STATIC_LOOKUP_ERROR;
369
370
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 8 times.
13 if (config->routes[i].document_root_resolved) {
371 5 strncpy(root_real, config->routes[i].document_root_real, sizeof(root_real) - 1);
372 5 root_real[sizeof(root_real) - 1] = '\0';
373
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 } else if (!realpath(root, root_real)) {
374 return STATIC_LOOKUP_ERROR;
375 }
376
377 13 fd = open(filepath, O_RDONLY);
378
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 10 times.
13 if (fd < 0)
379 3 return STATIC_LOOKUP_NOT_FOUND;
380
381
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
10 if (!realpath(filepath, file_real))
382 {
383 close(fd);
384 return STATIC_LOOKUP_NOT_FOUND;
385 }
386
387 10 root_len = strlen(root_real);
388
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 1 times.
10 if (strncmp(file_real, root_real, root_len) != 0 ||
389
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
9 (file_real[root_len] != '\0' && file_real[root_len] != '/'))
390 {
391 1 close(fd);
392 1 return STATIC_LOOKUP_FORBIDDEN;
393 }
394
395 9 *filesize_out = lseek(fd, 0, SEEK_END);
396
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
9 if (*filesize_out < 0 || lseek(fd, 0, SEEK_SET) < 0)
397 {
398 close(fd);
399 return STATIC_LOOKUP_ERROR;
400 }
401
402 9 *fd_out = fd;
403 9 *content_type_out = guess_content_type(file_real);
404 9 return STATIC_LOOKUP_READY;
405 }
406 }
407 }
408
409 1 return STATIC_LOOKUP_NO_ROUTE;
410 }
411
412 2 static int serve_static_h2(HttpRequest *req, ServerConfig *config, Http2Response *h2resp)
413 {
414 2 int fd = -1;
415 2 off_t filesize = 0;
416 2 const char *content_type = "application/octet-stream";
417 StaticLookupResult lookup =
418 2 lookup_static_file(req, config, &fd, &filesize, &content_type);
419
420
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (lookup == STATIC_LOOKUP_NO_ROUTE)
421 return 1;
422
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (lookup == STATIC_LOOKUP_NOT_FOUND)
423 1 return populate_http2_response(h2resp, "", HTTP_STATUS_NOT_FOUND, "Not Found", "text/plain");
424
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (lookup == STATIC_LOOKUP_FORBIDDEN)
425 return populate_http2_response(h2resp, "", HTTP_STATUS_FORBIDDEN, "Forbidden", "text/plain");
426
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (lookup == STATIC_LOOKUP_ERROR)
427 return -1;
428
429
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if ((size_t)filesize > sizeof(h2resp->body))
430 {
431 close(fd);
432 return populate_http2_response(h2resp, "", HTTP_STATUS_PAYLOAD_TOO_LARGE, "Payload Too Large", "text/plain");
433 }
434
435 1 h2_response_init(h2resp);
436 1 h2_response_set_status(h2resp, HTTP_STATUS_OK, "OK");
437 1 h2_response_set_content_type(h2resp, content_type);
438
439 1 SecurityHeadersConfig *sec_headers = get_security_headers_for_request(req, config);
440 1 CORSConfig *cors = get_cors_config_for_request(req, config);
441
442 1 size_t total = 0;
443
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 while (total < (size_t)filesize)
444 {
445 1 ssize_t n = read(fd, h2resp->body + total, (size_t)filesize - total);
446
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (n < 0)
447 {
448 close(fd);
449 return -1;
450 }
451
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (n == 0)
452 break;
453 1 total += (size_t)n;
454 }
455 1 close(fd);
456
457 1 h2_response_set_body_len(h2resp, total);
458 1 h2_response_add_security_headers(h2resp, sec_headers, cors);
459 1 h2_response_finalize(h2resp);
460 1 return 0;
461 }
462
463 static int has_matching_proxy_route(HttpRequest *req, ServerConfig *config)
464 {
465 for (int i = 0; i < config->route_count; i++)
466 {
467 if (strcmp(config->routes[i].technology, "reverse_proxy") == 0)
468 {
469 size_t prefix_len = strlen(config->routes[i].path);
470 if (strlen(req->path) >= prefix_len &&
471 strncmp(req->path, config->routes[i].path, prefix_len) == 0)
472 return 1;
473 }
474 }
475 return 0;
476 }
477
478 /* serve_static_tls()
479 *
480 * If the HTTP request's path starts with a static route, constructs the full file path,
481 * opens the file, and sends it to the client using SSL_write() and sendfile() for zero-copy.
482 * If the file is not found, a 404 response is sent.
483 */
484 15 int serve_static_tls(HttpRequest *req, ServerConfig *config, SSL *ssl)
485 {
486
6/8
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 13 times.
15 if (!req || !req->path || !config || !ssl)
487 2 return -1;
488
489 13 int fd = -1;
490 13 off_t filesize = 0;
491 13 const char *content_type = "application/octet-stream";
492 StaticLookupResult lookup =
493 13 lookup_static_file(req, config, &fd, &filesize, &content_type);
494
495
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 12 times.
13 if (lookup == STATIC_LOOKUP_NO_ROUTE)
496 1 return -1;
497
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 10 times.
12 if (lookup == STATIC_LOOKUP_NOT_FOUND)
498 2 return send_simple_response_with_config(ssl, "HTTP/1.1 404 Not Found", NULL, NULL, req, config);
499
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 9 times.
10 if (lookup == STATIC_LOOKUP_FORBIDDEN)
500 1 return send_simple_response_with_config(ssl, "HTTP/1.1 403 Forbidden", NULL, NULL, req, config);
501
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
9 if (lookup == STATIC_LOOKUP_ERROR)
502 1 return -1;
503
504 8 SecurityHeadersConfig *sec_headers = get_security_headers_for_request(req, config);
505 8 CORSConfig *cors = get_cors_config_for_request(req, config);
506
507 char header[HEADER_BUFFER_SIZE];
508 8 int header_len = snprintf(header, sizeof(header),
509 "HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %ld\r\n",
510 content_type, (long)filesize);
511
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
8 if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
512 close(fd);
513 return -1;
514 }
515
516 8 size_t current_len = (size_t)header_len;
517 8 add_security_headers_to_buffer(header, &current_len, sec_headers);
518 8 add_cors_headers_to_buffer(header, &current_len, cors);
519
520
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (current_len + 2 >= sizeof(header)) {
521 close(fd);
522 return -1;
523 }
524
525 8 strcpy(header + current_len, "\r\n");
526 8 current_len += 2;
527
528
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (ssl_write_all(ssl, header, current_len) != 0)
529 {
530 close(fd);
531 return -1;
532 }
533
534 char filebuf[BUFFER_SIZE];
535 ssize_t bytes;
536
2/2
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 8 times.
16 while ((bytes = read(fd, filebuf, sizeof(filebuf))) > 0)
537 {
538
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (ssl_write_all(ssl, filebuf, (size_t)bytes) != 0)
539 {
540 close(fd);
541 return -1;
542 }
543 }
544
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (bytes < 0)
545 {
546 close(fd);
547 return -1;
548 }
549 8 close(fd);
550 8 return 0;
551 }
552
553 /* proxy_bidirectional_tls()
554 *
555 * Implements a bidirectional forwarding loop between a TLS client and a backend server.
556 * Data from the client is read with SSL_read() and forwarded to the backend via write(),
557 * while data from the backend is read with read() and forwarded to the client using SSL_write().
558 */
559 int proxy_bidirectional_tls(SSL *ssl, int backend_fd)
560 {
561 int done = 0;
562 char buf[BUFFER_SIZE];
563 while (!done)
564 {
565 /* Read from backend (plain) */
566 int n = read(backend_fd, buf, sizeof(buf));
567 if (n > 0)
568 {
569 if (ssl_write_all(ssl, buf, (size_t)n) != 0)
570 {
571 done = 1;
572 }
573 }
574 else if (n < 0)
575 {
576 done = 1;
577 }
578 /* Read from client (TLS) */
579 n = SSL_read(ssl, buf, sizeof(buf));
580 if (n > 0)
581 {
582 int sent = 0;
583 while (sent < n)
584 {
585 int s = send(backend_fd, buf + sent, n - sent, 0);
586 if (s <= 0)
587 {
588 done = 1;
589 break;
590 }
591 sent += s;
592 }
593 }
594 else if (n < 0)
595 {
596 done = 1;
597 }
598 }
599 return 0;
600 }
601
602 /* proxy_request_tls()
603 *
604 * Forwards the entire HTTP request to a backend for reverse proxy functionality.
605 * Then activates proxy_bidirectional_tls() to forward data bidirectionally.
606 */
607 int proxy_request_tls(HttpRequest *req, const char *raw_request, size_t req_len, ServerConfig *config, SSL *ssl)
608 {
609 if (!req || !req->path || !raw_request || !config || !ssl)
610 return -1;
611
612 for (int i = 0; i < config->route_count; i++)
613 {
614 if (strcmp(config->routes[i].technology, "reverse_proxy") == 0)
615 {
616 size_t prefix_len = strlen(config->routes[i].path);
617 if (strlen(req->path) < prefix_len)
618 continue;
619 if (strncmp(req->path, config->routes[i].path, prefix_len) == 0)
620 {
621 char ip[IP_BUFFER_SIZE];
622 int port;
623 if (sscanf(config->routes[i].backend, "%63[^:]:%d", ip, &port) != 2)
624 return -1;
625 int backend_fd = socket(AF_INET, SOCK_STREAM, 0);
626 if (backend_fd < 0)
627 return -1;
628 struct sockaddr_in backend_addr;
629 memset(&backend_addr, 0, sizeof(backend_addr));
630 backend_addr.sin_family = AF_INET;
631 backend_addr.sin_port = htons(port);
632 if (inet_pton(AF_INET, ip, &backend_addr.sin_addr) <= 0)
633 {
634 close(backend_fd);
635 return -1;
636 }
637 if (connect(backend_fd, (struct sockaddr *)&backend_addr, sizeof(backend_addr)) < 0)
638 {
639 close(backend_fd);
640 return -1;
641 }
642 size_t sent = 0;
643 while (sent < req_len)
644 {
645 ssize_t n = send(backend_fd, raw_request + sent, req_len - sent, 0);
646 if (n <= 0)
647 break;
648 sent += (size_t)n;
649 }
650 proxy_bidirectional_tls(ssl, backend_fd);
651 close(backend_fd);
652 return 0;
653 }
654 }
655 }
656 return -1;
657 }
658
659 static void set_h2_response(Http2Response *h2resp, int status, const char *body, size_t body_len,
660 SecurityHeadersConfig *sec_headers, CORSConfig *cors)
661 {
662 h2_response_init(h2resp);
663 h2_response_set_status(h2resp, status, "OK");
664 h2_response_set_content_type(h2resp, "application/json");
665 h2_response_set_body(h2resp, body, body_len);
666 h2_response_add_security_headers(h2resp, sec_headers, cors);
667 h2_response_finalize(h2resp);
668 }
669
670 static int proxy_to_backend_pooled(HttpRequest *req, Route *route,
671 Http2Response *h2resp, const char *body, size_t body_len)
672 {
673 backend_conn_t *conn;
674 int status;
675 const char *resp_body;
676 size_t resp_len;
677
678 if (!backend_pool_circuit_breaker_allow_request(route->pool)) {
679 log_message(LOG_LEVEL_WARN, "Circuit breaker OPEN, rejecting request to %s",
680 route->backend);
681 set_h2_response(h2resp, HTTP_STATUS_SERVICE_UNAVAILABLE,
682 CIRCUIT_BREAKER_ERROR_BODY, CIRCUIT_BREAKER_ERROR_LEN,
683 &route->security_headers, &route->cors);
684 return -1;
685 }
686
687 conn = backend_pool_acquire(route->pool);
688 if (!conn) {
689 log_message(LOG_LEVEL_ERROR, "Failed to acquire connection from pool");
690 backend_pool_circuit_breaker_record_failure(route->pool);
691 return -1;
692 }
693
694 log_message(LOG_LEVEL_INFO, "HTTP/2 proxy: forwarding %s %s via pooled connection",
695 req->method, req->path);
696
697 status = http2_client_send_request(&conn->client, req->method, req->path,
698 route->backend, body, body_len);
699 if (status < 0) {
700 log_message(LOG_LEVEL_ERROR, "Failed to send HTTP/2 request");
701 backend_pool_mark_failure(conn);
702 backend_pool_circuit_breaker_record_failure(route->pool);
703 backend_pool_release(conn);
704 return -1;
705 }
706
707 status = http2_client_recv_response(&conn->client);
708 if (status <= 0) {
709 log_message(LOG_LEVEL_ERROR, "Failed to receive HTTP/2 response");
710 backend_pool_mark_failure(conn);
711 backend_pool_circuit_breaker_record_failure(route->pool);
712 backend_pool_release(conn);
713 return -1;
714 }
715
716 resp_body = http2_client_get_response_body(&conn->client);
717 resp_len = http2_client_get_response_length(&conn->client);
718
719 set_h2_response(h2resp, status, resp_body, resp_len,
720 &route->security_headers, &route->cors);
721
722 log_message(LOG_LEVEL_INFO, "HTTP/2 proxy: received response status=%d, length=%zu",
723 status, resp_len);
724
725 backend_pool_mark_success(conn);
726 backend_pool_circuit_breaker_record_success(route->pool);
727 backend_pool_release(conn);
728 return 0;
729 }
730
731 static int proxy_to_backend_direct(HttpRequest *req, Route *route,
732 Http2Response *h2resp, const char *body, size_t body_len)
733 {
734 char ip[IP_BUFFER_SIZE];
735 int port;
736 if (sscanf(route->backend, "%63[^:]:%d", ip, &port) != 2) {
737 log_message(LOG_LEVEL_ERROR, "Invalid backend address: %s", route->backend);
738 return -1;
739 }
740
741 log_message(LOG_LEVEL_INFO, "HTTP/2 proxy: forwarding %s %s to %s:%d (no pool)",
742 req->method, req->path, ip, port);
743
744 backend_config_t backend_config = {
745 .host = {0},
746 .port = port,
747 .tls_enabled = route->tls_enabled,
748 .tls_verify = route->tls_verify
749 };
750 strncpy(backend_config.host, ip, sizeof(backend_config.host) - 1);
751
752 http2_client_t client;
753 int status;
754 const char *resp_body;
755 size_t resp_len;
756
757 if (http2_client_init(&client, &backend_config) != 0) {
758 log_message(LOG_LEVEL_ERROR, "Failed to initialize HTTP/2 client");
759 return -1;
760 }
761
762 if (http2_client_connect(&client, &backend_config) != 0) {
763 log_message(LOG_LEVEL_ERROR, "Failed to connect to HTTP/2 backend %s:%d", ip, port);
764 http2_client_cleanup(&client);
765 return -1;
766 }
767
768 if (http2_client_send_request(&client, req->method, req->path,
769 ip, body, body_len) < 0) {
770 log_message(LOG_LEVEL_ERROR, "Failed to send HTTP/2 request");
771 http2_client_cleanup(&client);
772 return -1;
773 }
774
775 status = http2_client_recv_response(&client);
776 if (status <= 0) {
777 log_message(LOG_LEVEL_ERROR, "Failed to receive HTTP/2 response");
778 http2_client_cleanup(&client);
779 return -1;
780 }
781
782 resp_body = http2_client_get_response_body(&client);
783 resp_len = http2_client_get_response_length(&client);
784
785 set_h2_response(h2resp, status, resp_body, resp_len,
786 &route->security_headers, &route->cors);
787
788 log_message(LOG_LEVEL_INFO, "HTTP/2 proxy: received response status=%d, length=%zu",
789 status, resp_len);
790
791 http2_client_cleanup(&client);
792 return 0;
793 }
794
795 static Route* find_reverse_proxy_route(HttpRequest *req, ServerConfig *config)
796 {
797 for (int i = 0; i < config->route_count; i++) {
798 if (strcmp(config->routes[i].technology, "reverse_proxy") == 0) {
799 size_t prefix_len = strlen(config->routes[i].path);
800 if (strlen(req->path) >= prefix_len &&
801 strncmp(req->path, config->routes[i].path, prefix_len) == 0) {
802 return &config->routes[i];
803 }
804 }
805 }
806 return NULL;
807 }
808
809 /* proxy_request_http2()
810 *
811 * HTTP/2 reverse proxy - forwards request to backend using HTTP/2 client.
812 * Uses connection pool for connection reuse when available.
813 */
814 static int proxy_request_http2(HttpRequest *req, ServerConfig *config,
815 Http2Response *h2resp, const char *body, size_t body_len)
816 {
817 if (!req || !config || !h2resp) {
818 return -1;
819 }
820
821 Route *matched_route = find_reverse_proxy_route(req, config);
822 if (!matched_route) {
823 return -1;
824 }
825
826 if (matched_route->pool) {
827 return proxy_to_backend_pooled(req, matched_route, h2resp, body, body_len);
828 } else {
829 return proxy_to_backend_direct(req, matched_route, h2resp, body, body_len);
830 }
831 }
832
833 /* route_request_tls()
834 *
835 * Decides how to handle the request:
836 * - If the request path is "/", serves a default HTML page.
837 * - Otherwise, it first tries to serve the request as a static file.
838 * - If that fails, it attempts to forward the request to the backend via reverse proxy.
839 * Note: All communication with the client is via the SSL pointer.
840 */
841 9 int route_request_tls(HttpRequest *req, const char *raw, size_t raw_len, ServerConfig *config, SSL *ssl, Http2Response *h2resp)
842 {
843 (void)raw;
844 (void)raw_len;
845
846 static const char *root_body =
847 "<html><head><title>High Performance Web Server</title></head>"
848 "<body><h1>Welcome to High Performance Web Server</h1>"
849 "<p>This server is designed to outperform Nginx and Apache by utilizing "
850 "advanced I/O techniques, a modular architecture, and an efficient reverse proxy mechanism.</p>"
851 "</body></html>";
852
853
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 9 times.
9 if (!req || !req->path)
854 return -1;
855
856 /* Health check endpoint - highest priority */
857
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 7 times.
9 if (is_health_check(req)) {
858 2 return send_health_response(ssl, h2resp, config);
859 }
860
861
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
7 if (h2resp)
862 {
863
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (strcmp(req->path, "/") == 0)
864 2 return populate_http2_response(h2resp, root_body, HTTP_STATUS_OK, "OK", "text/html");
865
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (config)
866 {
867 2 int static_result = serve_static_h2(req, config, h2resp);
868
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (static_result == 0)
869 2 return 0;
870 if (static_result < 0)
871 return -1;
872
873 if (has_matching_proxy_route(req, config)) {
874 if (proxy_request_http2(req, config, h2resp, NULL, 0) == 0) {
875 return 0;
876 }
877 return populate_http2_response(h2resp, "", HTTP_STATUS_NOT_IMPLEMENTED, "Not Implemented", "text/plain");
878 }
879 }
880 return populate_http2_response(h2resp, "", HTTP_STATUS_NOT_FOUND, "Not Found", "text/plain");
881 }
882
883
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (strcmp(req->path, "/") == 0)
884 return send_simple_response_with_config(ssl, "HTTP/1.1 200 OK", "Content-Type: text/html\r\n",
885 root_body, req, config);
886
887
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 if (serve_static_tls(req, config, ssl) == 0)
888 3 return 0;
889 if (proxy_request_tls(req, raw, raw_len, config, ssl) == 0)
890 return 0;
891
892 if (send_simple_response_with_config(ssl, "HTTP/1.1 404 Not Found", NULL, NULL, req, config) != 0)
893 return -1;
894 return -1;
895 }
896