emme coverage


Directory: src/
File: src/router.c
Date: 2026-03-27 20:24:50
Exec Total Coverage
Lines: 119 242 49.2%
Functions: 8 11 72.7%
Branches: 76 208 36.5%

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 <unistd.h>
16 #include <fcntl.h>
17 #include <sys/sendfile.h>
18 #include <arpa/inet.h>
19 #include <poll.h>
20 #include <errno.h>
21 #include <limits.h>
22 #include <stdbool.h>
23 #include "http2_response.h"
24 #include "router.h"
25 #include "server.h"
26
27 typedef enum {
28 STATIC_LOOKUP_ERROR = -1,
29 STATIC_LOOKUP_NO_ROUTE = 0,
30 STATIC_LOOKUP_READY = 1,
31 STATIC_LOOKUP_NOT_FOUND = 2,
32 STATIC_LOOKUP_FORBIDDEN = 3,
33 } StaticLookupResult;
34
35 5 static int ssl_write_all(SSL *ssl, const char *buf, size_t len)
36 {
37 5 size_t total = 0;
38
39
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
10 while (total < len)
40 {
41 5 int written = SSL_write(ssl, buf + total, (int)(len - total));
42
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (written <= 0)
43 return -1;
44 5 total += (size_t)written;
45 }
46
47 5 return 0;
48 }
49
50 3 static int send_simple_response(SSL *ssl, const char *status_line,
51 const char *content_type, const char *body)
52 {
53
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 size_t body_len = body ? strlen(body) : 0;
54 char header[256];
55
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 int header_len = snprintf(header, sizeof(header),
56 "%s\r\n%sContent-Length: %zu\r\n\r\n",
57 status_line,
58 content_type ? content_type : "",
59 body_len);
60
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))
61 return -1;
62
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
3 if (ssl_write_all(ssl, header, (size_t)header_len) != 0)
63 return -1;
64
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)
65 return -1;
66 3 return 0;
67 }
68
69 3 static int populate_http2_response(Http2Response *resp, const char *body,
70 int status_code, const char *status_text,
71 const char *content_type)
72 {
73 int body_len;
74
75
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)
76 return -1;
77
78 3 body_len = snprintf(resp->body, sizeof(resp->body), "%s", body);
79
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))
80 return -1;
81
82 3 resp->body_len = (size_t)body_len;
83 3 resp->status_code = status_code;
84 3 snprintf(resp->status_text, sizeof(resp->status_text), "%s", status_text);
85 3 snprintf(resp->content_type, sizeof(resp->content_type), "%s", content_type);
86 3 resp->num_headers = 0;
87 3 return 0;
88 }
89
90 3 static const char *guess_content_type(const char *path)
91 {
92 3 const char *ext = strrchr(path, '.');
93
94
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!ext)
95 return "application/octet-stream";
96
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
3 if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0)
97 3 return "text/html";
98 if (strcmp(ext, ".css") == 0)
99 return "text/css";
100 if (strcmp(ext, ".js") == 0)
101 return "application/javascript";
102 if (strcmp(ext, ".json") == 0)
103 return "application/json";
104 if (strcmp(ext, ".txt") == 0)
105 return "text/plain";
106 if (strcmp(ext, ".png") == 0)
107 return "image/png";
108 if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
109 return "image/jpeg";
110 return "application/octet-stream";
111 }
112
113 8 static StaticLookupResult lookup_static_file(const HttpRequest *req, ServerConfig *config,
114 int *fd_out, off_t *filesize_out,
115 const char **content_type_out)
116 {
117
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 for (int i = 0; i < config->route_count; i++)
118 {
119
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (strcmp(config->routes[i].technology, "static") == 0)
120 {
121 8 size_t prefix_len = strlen(config->routes[i].path);
122
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (strlen(req->path) < prefix_len)
123 continue;
124
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (strncmp(req->path, config->routes[i].path, prefix_len) == 0)
125 {
126 char filepath[512];
127 char root_real[PATH_MAX];
128 char file_real[PATH_MAX];
129 8 const char *root = config->routes[i].document_root;
130 8 size_t root_len = strlen(root);
131 bool has_slash;
132 int fd;
133 int written;
134
135
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
8 if (root_len == 0)
136 1 return STATIC_LOOKUP_ERROR;
137
138 7 has_slash = (root[root_len - 1] == '/');
139 7 written = snprintf(filepath, sizeof(filepath),
140 has_slash ? "%s%s" : "%s/%s",
141 root,
142
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 req->path + prefix_len);
143
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (written < 0 || (size_t)written >= sizeof(filepath))
144 return STATIC_LOOKUP_ERROR;
145
146
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
7 if (!realpath(root, root_real))
147 return STATIC_LOOKUP_ERROR;
148
149 7 fd = open(filepath, O_RDONLY);
150
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
7 if (fd < 0)
151 3 return STATIC_LOOKUP_NOT_FOUND;
152
153
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
4 if (!realpath(filepath, file_real))
154 {
155 close(fd);
156 return STATIC_LOOKUP_NOT_FOUND;
157 }
158
159 4 root_len = strlen(root_real);
160
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 if (strncmp(file_real, root_real, root_len) != 0 ||
161
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 (file_real[root_len] != '\0' && file_real[root_len] != '/'))
162 {
163 1 close(fd);
164 1 return STATIC_LOOKUP_FORBIDDEN;
165 }
166
167 3 *filesize_out = lseek(fd, 0, SEEK_END);
168
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
3 if (*filesize_out < 0 || lseek(fd, 0, SEEK_SET) < 0)
169 {
170 close(fd);
171 return STATIC_LOOKUP_ERROR;
172 }
173
174 3 *fd_out = fd;
175 3 *content_type_out = guess_content_type(file_real);
176 3 return STATIC_LOOKUP_READY;
177 }
178 }
179 }
180
181 return STATIC_LOOKUP_NO_ROUTE;
182 }
183
184 2 static int serve_static_h2(HttpRequest *req, ServerConfig *config, Http2Response *h2resp)
185 {
186 2 int fd = -1;
187 2 off_t filesize = 0;
188 2 const char *content_type = "application/octet-stream";
189 StaticLookupResult lookup =
190 2 lookup_static_file(req, config, &fd, &filesize, &content_type);
191
192
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (lookup == STATIC_LOOKUP_NO_ROUTE)
193 return 1;
194
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (lookup == STATIC_LOOKUP_NOT_FOUND)
195 1 return populate_http2_response(h2resp, "", 404, "Not Found", "text/plain");
196
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (lookup == STATIC_LOOKUP_FORBIDDEN)
197 return populate_http2_response(h2resp, "", 403, "Forbidden", "text/plain");
198
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (lookup == STATIC_LOOKUP_ERROR)
199 return -1;
200
201
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if ((size_t)filesize > sizeof(h2resp->body))
202 {
203 close(fd);
204 return populate_http2_response(h2resp, "", 413, "Payload Too Large", "text/plain");
205 }
206
207 1 size_t total = 0;
208
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 while (total < (size_t)filesize)
209 {
210 1 ssize_t n = read(fd, h2resp->body + total, (size_t)filesize - total);
211
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (n < 0)
212 {
213 close(fd);
214 return -1;
215 }
216
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (n == 0)
217 break;
218 1 total += (size_t)n;
219 }
220 1 close(fd);
221
222 1 h2resp->status_code = 200;
223 1 snprintf(h2resp->status_text, sizeof(h2resp->status_text), "OK");
224 1 snprintf(h2resp->content_type, sizeof(h2resp->content_type), "%s", content_type);
225 1 h2resp->body_len = total;
226 1 h2resp->num_headers = 0;
227 1 return 0;
228 }
229
230 static int has_matching_proxy_route(HttpRequest *req, ServerConfig *config)
231 {
232 for (int i = 0; i < config->route_count; i++)
233 {
234 if (strcmp(config->routes[i].technology, "reverse_proxy") == 0)
235 {
236 size_t prefix_len = strlen(config->routes[i].path);
237 if (strlen(req->path) >= prefix_len &&
238 strncmp(req->path, config->routes[i].path, prefix_len) == 0)
239 return 1;
240 }
241 }
242 return 0;
243 }
244
245 /* serve_static_tls()
246 *
247 * If the HTTP request's path starts with a static route, constructs the full file path,
248 * opens the file, and sends it to the client using SSL_write() and sendfile() for zero-copy.
249 * If the file is not found, a 404 response is sent.
250 */
251 6 int serve_static_tls(HttpRequest *req, ServerConfig *config, SSL *ssl)
252 {
253
4/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 6 times.
6 if (!req || !req->path || !config || !ssl)
254 return -1;
255
256 6 int fd = -1;
257 6 off_t filesize = 0;
258 6 const char *content_type = "application/octet-stream";
259 StaticLookupResult lookup =
260 6 lookup_static_file(req, config, &fd, &filesize, &content_type);
261
262
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (lookup == STATIC_LOOKUP_NO_ROUTE)
263 return -1;
264
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
6 if (lookup == STATIC_LOOKUP_NOT_FOUND)
265 2 return send_simple_response(ssl, "HTTP/1.1 404 Not Found", NULL, NULL);
266
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (lookup == STATIC_LOOKUP_FORBIDDEN)
267 1 return send_simple_response(ssl, "HTTP/1.1 403 Forbidden", NULL, NULL);
268
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 if (lookup == STATIC_LOOKUP_ERROR)
269 1 return -1;
270
271 char header[256];
272 2 int header_len = snprintf(header, sizeof(header),
273 "HTTP/1.1 200 OK\r\nContent-Type: %s\r\nContent-Length: %ld\r\n\r\n",
274 content_type, (long)filesize);
275
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
4 if (header_len < 0 || (size_t)header_len >= sizeof(header) ||
276 2 ssl_write_all(ssl, header, (size_t)header_len) != 0)
277 {
278 close(fd);
279 return -1;
280 }
281
282 char filebuf[BUFFER_SIZE];
283 ssize_t bytes;
284
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
4 while ((bytes = read(fd, filebuf, sizeof(filebuf))) > 0)
285 {
286 2 ssize_t sent = 0;
287
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 while (sent < bytes) {
288 2 int n = SSL_write(ssl, filebuf + sent, (int)(bytes - sent));
289
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (n <= 0) {
290 close(fd);
291 return -1;
292 }
293 2 sent += n;
294 }
295 }
296
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (bytes < 0)
297 {
298 close(fd);
299 return -1;
300 }
301 2 close(fd);
302 2 return 0;
303 }
304
305 /* proxy_bidirectional_tls()
306 *
307 * Implements a bidirectional forwarding loop between a TLS client and a backend server.
308 * Data from the client is read with SSL_read() and forwarded to the backend via write(),
309 * while data from the backend is read with read() and forwarded to the client using SSL_write().
310 */
311 int proxy_bidirectional_tls(SSL *ssl, int backend_fd)
312 {
313 int done = 0;
314 char buf[BUFFER_SIZE];
315 while (!done)
316 {
317 /* Read from backend (plain) */
318 int n = read(backend_fd, buf, sizeof(buf));
319 if (n > 0)
320 {
321 int sent = 0;
322 while (sent < n)
323 {
324 int s = SSL_write(ssl, buf + sent, n - sent);
325 if (s <= 0)
326 {
327 done = 1;
328 break;
329 }
330 sent += s;
331 }
332 }
333 else if (n < 0)
334 {
335 done = 1;
336 }
337 /* Read from client (TLS) */
338 n = SSL_read(ssl, buf, sizeof(buf));
339 if (n > 0)
340 {
341 int sent = 0;
342 while (sent < n)
343 {
344 int s = send(backend_fd, buf + sent, n - sent, 0);
345 if (s <= 0)
346 {
347 done = 1;
348 break;
349 }
350 sent += s;
351 }
352 }
353 else if (n < 0)
354 {
355 done = 1;
356 }
357 }
358 return 0;
359 }
360
361 /* proxy_request_tls()
362 *
363 * Forwards the entire HTTP request to a backend for reverse proxy functionality.
364 * Then activates proxy_bidirectional_tls() to forward data bidirectionally.
365 */
366 int proxy_request_tls(HttpRequest *req, const char *raw_request, size_t req_len, ServerConfig *config, SSL *ssl)
367 {
368 if (!req || !req->path || !raw_request || !config || !ssl)
369 return -1;
370
371 for (int i = 0; i < config->route_count; i++)
372 {
373 if (strcmp(config->routes[i].technology, "reverse_proxy") == 0)
374 {
375 size_t prefix_len = strlen(config->routes[i].path);
376 if (strlen(req->path) < prefix_len)
377 continue;
378 if (strncmp(req->path, config->routes[i].path, prefix_len) == 0)
379 {
380 char ip[64];
381 int port;
382 if (sscanf(config->routes[i].backend, "%63[^:]:%d", ip, &port) != 2)
383 return -1;
384 int backend_fd = socket(AF_INET, SOCK_STREAM, 0);
385 if (backend_fd < 0)
386 return -1;
387 struct sockaddr_in backend_addr;
388 memset(&backend_addr, 0, sizeof(backend_addr));
389 backend_addr.sin_family = AF_INET;
390 backend_addr.sin_port = htons(port);
391 if (inet_pton(AF_INET, ip, &backend_addr.sin_addr) <= 0)
392 {
393 close(backend_fd);
394 return -1;
395 }
396 if (connect(backend_fd, (struct sockaddr *)&backend_addr, sizeof(backend_addr)) < 0)
397 {
398 close(backend_fd);
399 return -1;
400 }
401 size_t sent = 0;
402 while (sent < req_len)
403 {
404 ssize_t n = send(backend_fd, raw_request + sent, req_len - sent, 0);
405 if (n <= 0)
406 break;
407 sent += (size_t)n;
408 }
409 proxy_bidirectional_tls(ssl, backend_fd);
410 close(backend_fd);
411 return 0;
412 }
413 }
414 }
415 return -1;
416 }
417
418 /* route_request_tls()
419 *
420 * Decides how to handle the request:
421 * - If the request path is "/", serves a default HTML page.
422 * - Otherwise, it first tries to serve the request as a static file.
423 * - If that fails, it attempts to forward the request to the backend via reverse proxy.
424 * Note: All communication with the client is via the SSL pointer.
425 */
426 7 int route_request_tls(HttpRequest *req, const char *raw, size_t raw_len, ServerConfig *config, SSL *ssl, Http2Response *h2resp)
427 {
428 static const char *root_body =
429 "<html><head><title>High Performance Web Server</title></head>"
430 "<body><h1>Welcome to High Performance Web Server</h1>"
431 "<p>This server is designed to outperform Nginx and Apache by utilizing "
432 "advanced I/O techniques, a modular architecture, and an efficient reverse proxy mechanism.</p>"
433 "</body></html>";
434
435
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
7 if (!req || !req->path)
436 return -1;
437
438
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
7 if (h2resp)
439 {
440
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (strcmp(req->path, "/") == 0)
441 2 return populate_http2_response(h2resp, root_body, 200, "OK", "text/html");
442
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (config)
443 {
444 2 int static_result = serve_static_h2(req, config, h2resp);
445
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (static_result == 0)
446 2 return 0;
447 if (static_result < 0)
448 return -1;
449
450 if (has_matching_proxy_route(req, config))
451 return populate_http2_response(h2resp, "", 501, "Not Implemented", "text/plain");
452 }
453 return populate_http2_response(h2resp, "", 404, "Not Found", "text/plain");
454 }
455
456
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (strcmp(req->path, "/") == 0)
457 return send_simple_response(ssl, "HTTP/1.1 200 OK", "Content-Type: text/html\r\n",
458 root_body);
459
460
1/2
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
3 if (serve_static_tls(req, config, ssl) == 0)
461 3 return 0;
462 if (proxy_request_tls(req, raw, raw_len, config, ssl) == 0)
463 return 0;
464
465 if (send_simple_response(ssl, "HTTP/1.1 404 Not Found", NULL, NULL) != 0)
466 return -1;
467 return -1;
468 }
469