| 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, ¤t_len, sec_headers); | |
| 276 | 3 | add_cors_headers_to_buffer(header, ¤t_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, ¤t_len, sec_headers); | |
| 518 | 8 | add_cors_headers_to_buffer(header, ¤t_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 |