Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* |
| 2 | MIT License |
| 3 | |
| 4 | Copyright (c) 2026 Seregon |
| 5 | |
| 6 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | of this software and associated documentation files (the "Software"), to deal |
| 8 | in the Software without restriction, including without limitation the rights |
| 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 | copies of the Software, and to permit persons to whom the Software is |
| 11 | furnished to do so, subject to the following conditions: |
| 12 | |
| 13 | The above copyright notice and this permission notice shall be included in all |
| 14 | copies or substantial portions of the Software. |
| 15 | |
| 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 22 | SOFTWARE. |
| 23 | */ |
| 24 | /** |
| 25 | * @file http_server.c |
| 26 | * @brief HTTP server — event-loop-driven, non-blocking connections |
| 27 | * |
| 28 | * ARCHITECTURE: |
| 29 | * |
| 30 | * ┌────────────────────────────┐ |
| 31 | * │ kqueue / epoll event loop │ |
| 32 | * │ │ |
| 33 | * │ listen_fd ──► accept() │ |
| 34 | * │ │ │ |
| 35 | * │ ┌───────────┘ │ |
| 36 | * │ ▼ │ |
| 37 | * │ client_fd ──► read() │ |
| 38 | * │ ▼ │ |
| 39 | * │ parse HTTP request │ |
| 40 | * │ ▼ │ |
| 41 | * │ route to API / static │ |
| 42 | * │ ▼ │ |
| 43 | * │ send response headers │ |
| 44 | * │ send file (if download) │ |
| 45 | * │ ▼ │ |
| 46 | * │ close or keep-alive │ |
| 47 | * └────────────────────────────┘ |
| 48 | */ |
| 49 | |
| 50 | #include "http_server.h" |
| 51 | #include "ftp_config.h" |
| 52 | #include "http_api.h" |
| 53 | #include "http_config.h" |
| 54 | #if ENABLE_WEB_UPLOAD |
| 55 | #include "http_csrf.h" |
| 56 | #endif |
| 57 | #include "http_parser.h" |
| 58 | #include "http_response.h" |
| 59 | #include "pal_fileio.h" |
| 60 | #include "pal_network.h" |
| 61 | #include <dirent.h> |
| 62 | #include <errno.h> |
| 63 | #include <fcntl.h> |
| 64 | #include <netinet/in.h> |
| 65 | #include <netinet/tcp.h> /* TCP_NODELAY */ |
| 66 | #include <stdatomic.h> |
| 67 | #include <stdio.h> |
| 68 | #include <stdlib.h> |
| 69 | #include <string.h> |
| 70 | #include <strings.h> |
| 71 | #include <sys/socket.h> |
| 72 | #include <sys/stat.h> |
| 73 | #include <unistd.h> |
| 74 | |
| 75 | |
| 76 | /*===========================================================================* |
| 77 | * INTERNAL TYPES |
| 78 | *===========================================================================*/ |
| 79 | |
| 80 | struct http_server { |
| 81 | event_loop_t *loop; |
| 82 | int listen_fd; |
| 83 | uint16_t port; |
| 84 | atomic_int connection_count; /* Phase 4: thread-safe counter */ |
| 85 | char root_path[FTP_PATH_MAX]; /* filesystem confinement root */ |
| 86 | }; |
| 87 | |
| 88 | typedef struct { |
| 89 | http_server_t *server; |
| 90 | int fd; |
| 91 | char buffer[HTTP_REQUEST_BUFFER_SIZE]; |
| 92 | size_t buffer_used; |
| 93 | #if ENABLE_WEB_UPLOAD |
| 94 | int upload_active; |
| 95 | int upload_fd; |
| 96 | size_t upload_remaining; |
| 97 | /* |
| 98 | * @field upload_chunk_buf |
| 99 | * Heap-allocated read buffer for streaming uploads. |
| 100 | * NULL when no upload is active. Allocated to HTTP_UPLOAD_CHUNK_SIZE |
| 101 | * at upload start; freed in http_connection_release(). |
| 102 | * |
| 103 | * WHY: the connection's header buffer (conn->buffer) is only |
| 104 | * HTTP_REQUEST_BUFFER_SIZE (8 KB). Reading 8 KB per event-loop |
| 105 | * iteration limits upload throughput to a few MB/s. A dedicated |
| 106 | * 256 KB buffer reduces the required syscall rate by 32× at the |
| 107 | * same throughput target. |
| 108 | * |
| 109 | * ISOLATION: this field is only accessed inside ENABLE_WEB_UPLOAD |
| 110 | * guards. FTP paths, download paths, and internal copy are unaffected. |
| 111 | * |
| 112 | * @note Thread-safety: NOT thread-safe (single event-loop thread) |
| 113 | * @note Must be freed (not closed) in http_connection_release() |
| 114 | */ |
| 115 | uint8_t *upload_chunk_buf; |
| 116 | #endif |
| 117 | } http_connection_t; |
| 118 | |
| 119 | static http_server_t g_http_server; |
| 120 | static atomic_int g_http_server_in_use = ATOMIC_VAR_INIT(0); |
| 121 | static http_connection_t g_http_connections[HTTP_MAX_CONNECTIONS]; |
| 122 | |
| 123 | static void http_connections_init(void) { |
| 124 | for (size_t i = 0; i < (size_t)HTTP_MAX_CONNECTIONS; i++) { |
| 125 | g_http_connections[i].fd = -1; |
| 126 | g_http_connections[i].server = NULL; |
| 127 | g_http_connections[i].buffer_used = 0; |
| 128 | #if ENABLE_WEB_UPLOAD |
| 129 | g_http_connections[i].upload_active = 0; |
| 130 | g_http_connections[i].upload_fd = -1; |
| 131 | g_http_connections[i].upload_remaining = 0; |
| 132 | g_http_connections[i].upload_chunk_buf = NULL; |
| 133 | #endif |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | static http_connection_t *http_connection_acquire(http_server_t *server, |
| 138 | int client_fd) { |
| 139 | if ((server == NULL) || (client_fd < 0)) { |
| 140 | return NULL; |
| 141 | } |
| 142 | for (size_t i = 0; i < (size_t)HTTP_MAX_CONNECTIONS; i++) { |
| 143 | if (g_http_connections[i].fd < 0) { |
| 144 | http_connection_t *conn = &g_http_connections[i]; |
| 145 | memset(conn, 0, sizeof(*conn)); |
| 146 | conn->server = server; |
| 147 | conn->fd = client_fd; |
| 148 | conn->buffer_used = 0; |
| 149 | #if ENABLE_WEB_UPLOAD |
| 150 | conn->upload_active = 0; |
| 151 | conn->upload_fd = -1; |
| 152 | conn->upload_remaining = 0; |
| 153 | #endif |
| 154 | return conn; |
| 155 | } |
| 156 | } |
| 157 | return NULL; |
| 158 | } |
| 159 | |
| 160 | static void http_connection_release(http_connection_t *conn) { |
| 161 | if (conn == NULL) { |
| 162 | return; |
| 163 | } |
| 164 | #if ENABLE_WEB_UPLOAD |
| 165 | if (conn->upload_fd >= 0) { |
| 166 | (void)pal_file_close(conn->upload_fd); |
| 167 | conn->upload_fd = -1; |
| 168 | } |
| 169 | conn->upload_active = 0; |
| 170 | conn->upload_remaining = 0; |
| 171 | /* Free the upload read buffer if it was allocated */ |
| 172 | if (conn->upload_chunk_buf != NULL) { |
| 173 | free(conn->upload_chunk_buf); |
| 174 | conn->upload_chunk_buf = NULL; |
| 175 | } |
| 176 | #endif |
| 177 | conn->fd = -1; |
| 178 | conn->server = NULL; |
| 179 | conn->buffer_used = 0; |
| 180 | } |
| 181 | |
| 182 | /*===========================================================================* |
| 183 | * FORWARD DECLARATIONS |
| 184 | *===========================================================================*/ |
| 185 | |
| 186 | static int http_accept_callback(int fd, uint32_t events, void *data); |
| 187 | static int http_client_callback(int fd, uint32_t events, void *data); |
| 188 | static int http_handle_request(http_connection_t *conn); |
| 189 | static void http_close_connection(http_connection_t *conn); |
| 190 | |
| 191 | /*===========================================================================* |
| 192 | * SET NON-BLOCKING |
| 193 | *===========================================================================*/ |
| 194 | |
| 195 | static int set_nonblocking(int fd) { |
| 196 | int flags = fcntl(fd, F_GETFL, 0); |
| 197 | if (flags < 0) { |
| 198 | return -1; |
| 199 | } |
| 200 | return fcntl(fd, F_SETFL, flags | O_NONBLOCK); |
| 201 | } |
| 202 | |
| 203 | static int http_parse_basic_request(const char *buf, char *method, |
| 204 | size_t method_cap, char *uri, |
| 205 | size_t uri_cap, size_t *header_len, |
| 206 | size_t *content_length) { |
| 207 | if ((buf == NULL) || (method == NULL) || (uri == NULL) || |
| 208 | (header_len == NULL) || (content_length == NULL)) { |
| 209 | return -1; |
| 210 | } |
| 211 | |
| 212 | const char *end = strstr(buf, "\r\n\r\n"); |
| 213 | if (end == NULL) { |
| 214 | return -1; |
| 215 | } |
| 216 | *header_len = (size_t)(end - buf) + 4U; |
| 217 | |
| 218 | const char *line_end = strstr(buf, "\r\n"); |
| 219 | if (line_end == NULL) { |
| 220 | return -1; |
| 221 | } |
| 222 | |
| 223 | const char *sp1 = strchr(buf, ' '); |
| 224 | if ((sp1 == NULL) || (sp1 >= line_end)) { |
| 225 | return -1; |
| 226 | } |
| 227 | const char *sp2 = strchr(sp1 + 1, ' '); |
| 228 | if ((sp2 == NULL) || (sp2 >= line_end)) { |
| 229 | return -1; |
| 230 | } |
| 231 | |
| 232 | size_t mlen = (size_t)(sp1 - buf); |
| 233 | if ((mlen == 0U) || (mlen >= method_cap)) { |
| 234 | return -1; |
| 235 | } |
| 236 | memcpy(method, buf, mlen); |
| 237 | method[mlen] = '\0'; |
| 238 | |
| 239 | size_t ulen = (size_t)(sp2 - (sp1 + 1)); |
| 240 | if ((ulen == 0U) || (ulen >= uri_cap)) { |
| 241 | return -1; |
| 242 | } |
| 243 | memcpy(uri, sp1 + 1, ulen); |
| 244 | uri[ulen] = '\0'; |
| 245 | |
| 246 | *content_length = 0U; |
| 247 | const char *p = line_end + 2; |
| 248 | while ((p < end) && (p[0] != '\0')) { |
| 249 | const char *eol = strstr(p, "\r\n"); |
| 250 | if ((eol == NULL) || (eol > end)) { |
| 251 | break; |
| 252 | } |
| 253 | if (eol == p) { |
| 254 | break; |
| 255 | } |
| 256 | |
| 257 | if (strncasecmp(p, "Content-Length:", 15) == 0) { |
| 258 | const char *v = p + 15; |
| 259 | while ((*v == ' ') || (*v == '\t')) { |
| 260 | v++; |
| 261 | } |
| 262 | unsigned long long cl = strtoull(v, NULL, 10); |
| 263 | *content_length = (size_t)cl; |
| 264 | } |
| 265 | |
| 266 | p = eol + 2; |
| 267 | } |
| 268 | |
| 269 | return 0; |
| 270 | } |
| 271 | |
| 272 | #if ENABLE_WEB_UPLOAD |
| 273 | |
| 274 | /** |
| 275 | * @brief Recursively create directories for a given path. |
| 276 | * |
| 277 | * Creates all intermediate directories in the path if they don't exist. |
| 278 | * Returns 0 on success, -1 on error. |
| 279 | */ |
| 280 | static int mkdir_recursive(const char *path) { |
| 281 | if (path == NULL || path[0] == '\0') { |
| 282 | return -1; |
| 283 | } |
| 284 | |
| 285 | char buf[1024]; |
| 286 | size_t len = strlen(path); |
| 287 | if (len >= sizeof(buf)) { |
| 288 | return -1; |
| 289 | } |
| 290 | strcpy(buf, path); |
| 291 | |
| 292 | for (size_t i = 1; i < len; i++) { |
| 293 | if (buf[i] == '/') { |
| 294 | buf[i] = '\0'; |
| 295 | struct stat st; |
| 296 | if (stat(buf, &st) != 0) { |
| 297 | if (mkdir(buf, 0777) != 0 && errno != EEXIST) { |
| 298 | return -1; |
| 299 | } |
| 300 | } |
| 301 | buf[i] = '/'; |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | struct stat st; |
| 306 | if (stat(buf, &st) != 0) { |
| 307 | if (mkdir(buf, 0777) != 0 && errno != EEXIST) { |
| 308 | return -1; |
| 309 | } |
| 310 | } |
| 311 | |
| 312 | return 0; |
| 313 | } |
| 314 | |
| 315 | static int url_decode_component(const char *in, char *out, size_t out_cap) { |
| 316 | if ((in == NULL) || (out == NULL) || (out_cap < 2U)) { |
| 317 | return -1; |
| 318 | } |
| 319 | |
| 320 | size_t in_pos = 0U; |
| 321 | size_t out_pos = 0U; |
| 322 | |
| 323 | while ((in[in_pos] != '\0') && (in[in_pos] != '&') && |
| 324 | (out_pos < (out_cap - 1U))) { |
| 325 | unsigned char ch = (unsigned char)in[in_pos]; |
| 326 | if ((ch == '%') && (in[in_pos + 1] != '\0') && (in[in_pos + 2] != '\0')) { |
| 327 | unsigned char hi = (unsigned char)in[in_pos + 1]; |
| 328 | unsigned char lo = (unsigned char)in[in_pos + 2]; |
| 329 | unsigned int v_hi; |
| 330 | unsigned int v_lo; |
| 331 | |
| 332 | if ((hi >= '0') && (hi <= '9')) { |
| 333 | v_hi = (unsigned int)(hi - '0'); |
| 334 | } else if ((hi >= 'A') && (hi <= 'F')) { |
| 335 | v_hi = 10U + (unsigned int)(hi - 'A'); |
| 336 | } else if ((hi >= 'a') && (hi <= 'f')) { |
| 337 | v_hi = 10U + (unsigned int)(hi - 'a'); |
| 338 | } else { |
| 339 | v_hi = 0xFFFFFFFFU; |
| 340 | } |
| 341 | |
| 342 | if ((lo >= '0') && (lo <= '9')) { |
| 343 | v_lo = (unsigned int)(lo - '0'); |
| 344 | } else if ((lo >= 'A') && (lo <= 'F')) { |
| 345 | v_lo = 10U + (unsigned int)(lo - 'A'); |
| 346 | } else if ((lo >= 'a') && (lo <= 'f')) { |
| 347 | v_lo = 10U + (unsigned int)(lo - 'a'); |
| 348 | } else { |
| 349 | v_lo = 0xFFFFFFFFU; |
| 350 | } |
| 351 | |
| 352 | if ((v_hi != 0xFFFFFFFFU) && (v_lo != 0xFFFFFFFFU)) { |
| 353 | unsigned char decoded = (unsigned char)((v_hi << 4U) | v_lo); |
| 354 | if (decoded == '\0') { |
| 355 | return -1; |
| 356 | } |
| 357 | out[out_pos++] = (char)decoded; |
| 358 | in_pos += 3U; |
| 359 | continue; |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | if (ch == '+') { |
| 364 | out[out_pos++] = ' '; |
| 365 | } else { |
| 366 | out[out_pos++] = (char)ch; |
| 367 | } |
| 368 | in_pos++; |
| 369 | } |
| 370 | |
| 371 | out[out_pos] = '\0'; |
| 372 | return 0; |
| 373 | } |
| 374 | |
| 375 | static int get_query_param(const char *uri, const char *key, char *out, |
| 376 | size_t out_cap) { |
| 377 | if ((uri == NULL) || (key == NULL) || (out == NULL) || (out_cap < 2U)) { |
| 378 | return -1; |
| 379 | } |
| 380 | |
| 381 | const char *q = strchr(uri, '?'); |
| 382 | if (q == NULL) { |
| 383 | return -1; |
| 384 | } |
| 385 | q++; |
| 386 | |
| 387 | char pattern[64]; |
| 388 | (void)snprintf(pattern, sizeof(pattern), "%s=", key); |
| 389 | const char *p = strstr(q, pattern); |
| 390 | if (p == NULL) { |
| 391 | return -1; |
| 392 | } |
| 393 | p += strlen(pattern); |
| 394 | |
| 395 | if (url_decode_component(p, out, out_cap) != 0) { |
| 396 | return -1; |
| 397 | } |
| 398 | if (out[0] == '\0') { |
| 399 | return -1; |
| 400 | } |
| 401 | return 0; |
| 402 | } |
| 403 | |
| 404 | static int is_safe_path_local(const char *path) { |
| 405 | if (path == NULL || path[0] != '/') { |
| 406 | return 0; |
| 407 | } |
| 408 | if (strstr(path, "//") != NULL) { |
| 409 | return 0; |
| 410 | } |
| 411 | const char *p = path; |
| 412 | while (*p != '\0') { |
| 413 | if ((p[0] == '.') && (p[1] == '.')) { |
| 414 | if ((p == path) || (p[-1] == '/')) { |
| 415 | return 0; |
| 416 | } |
| 417 | } |
| 418 | p++; |
| 419 | } |
| 420 | return 1; |
| 421 | } |
| 422 | |
| 423 | static int is_safe_filename_local(const char *name) { |
| 424 | if ((name == NULL) || (name[0] == '\0')) { |
| 425 | return 0; |
| 426 | } |
| 427 | if (strstr(name, "..") != NULL) { |
| 428 | return 0; |
| 429 | } |
| 430 | for (const char *p = name; *p != '\0'; p++) { |
| 431 | if ((*p == '/') || (*p == '\\')) { |
| 432 | return 0; |
| 433 | } |
| 434 | } |
| 435 | return 1; |
| 436 | } |
| 437 | #endif |
| 438 | |
| 439 | /*===========================================================================* |
| 440 | * CREATE / DESTROY |
| 441 | *===========================================================================*/ |
| 442 | |
| 443 | http_server_t *http_server_create(event_loop_t *loop, const char *bind_addr, |
| 444 | const char *root_path) { |
| 445 | if ((loop == NULL) || (bind_addr == NULL) || (root_path == NULL)) { |
| 446 | return NULL; |
| 447 | } |
| 448 | |
| 449 | if (atomic_load(&g_http_server_in_use) != 0) { |
| 450 | return NULL; |
| 451 | } |
| 452 | |
| 453 | memset(&g_http_server, 0, sizeof(g_http_server)); |
| 454 | g_http_server.listen_fd = -1; |
| 455 | g_http_server.loop = loop; |
| 456 | atomic_store(&g_http_server.connection_count, 0); |
| 457 | |
| 458 | /* Store root path for filesystem confinement */ |
| 459 | size_t rlen = strlen(root_path); |
| 460 | if (rlen >= sizeof(g_http_server.root_path)) { |
| 461 | return NULL; |
| 462 | } |
| 463 | memcpy(g_http_server.root_path, root_path, rlen + 1U); |
| 464 | |
| 465 | /* Propagate root to API layer */ |
| 466 | http_api_set_root(root_path); |
| 467 | |
| 468 | http_connections_init(); |
| 469 | |
| 470 | /* Parse bind address (supports "[::1]:8888" and "0.0.0.0:8888") */ |
| 471 | struct sockaddr_storage addr_storage; |
| 472 | socklen_t addr_len; |
| 473 | if (pal_make_sockaddr_ex(bind_addr, &addr_storage, &addr_len) != FTP_OK) { |
| 474 | return NULL; |
| 475 | } |
| 476 | |
| 477 | /* Determine address family and extract port */ |
| 478 | int af = addr_storage.ss_family; |
| 479 | uint16_t port = 0; |
| 480 | if (af == AF_INET6) { |
| 481 | struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)&addr_storage; |
| 482 | port = ntohs(addr6->sin6_port); |
| 483 | } else if (af == AF_INET) { |
| 484 | struct sockaddr_in *addr4 = (struct sockaddr_in *)&addr_storage; |
| 485 | port = ntohs(addr4->sin_port); |
| 486 | } else { |
| 487 | return NULL; |
| 488 | } |
| 489 | g_http_server.port = port; |
| 490 | |
| 491 | /* Create TCP listen socket with detected address family */ |
| 492 | g_http_server.listen_fd = socket(af, SOCK_STREAM, 0); |
| 493 | if (g_http_server.listen_fd < 0) { |
| 494 | return NULL; |
| 495 | } |
| 496 | |
| 497 | int reuse = 1; |
| 498 | setsockopt(g_http_server.listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, |
| 499 | sizeof(reuse)); |
| 500 | |
| 501 | /* Dual-stack: accept both IPv4 and IPv6 on a single [::] socket. |
| 502 | * FreeBSD/PS5 defaults IPV6_V6ONLY=1, so we must explicitly disable it. */ |
| 503 | if (af == AF_INET6) { |
| 504 | int v6only = 0; |
| 505 | setsockopt(g_http_server.listen_fd, IPPROTO_IPV6, IPV6_V6ONLY, |
| 506 | &v6only, sizeof(v6only)); |
| 507 | } |
| 508 | |
| 509 | if (bind(g_http_server.listen_fd, (struct sockaddr *)&addr_storage, |
| 510 | addr_len) < 0) { |
| 511 | close(g_http_server.listen_fd); |
| 512 | g_http_server.listen_fd = -1; |
| 513 | return NULL; |
| 514 | } |
| 515 | |
| 516 | if (listen(g_http_server.listen_fd, 128) < 0) { |
| 517 | close(g_http_server.listen_fd); |
| 518 | g_http_server.listen_fd = -1; |
| 519 | return NULL; |
| 520 | } |
| 521 | |
| 522 | /* Non-blocking accept */ |
| 523 | (void)set_nonblocking(g_http_server.listen_fd); |
| 524 | |
| 525 | /* Register with event loop */ |
| 526 | if (event_loop_add(loop, g_http_server.listen_fd, EVENT_READ, |
| 527 | http_accept_callback, &g_http_server) != 0) { |
| 528 | close(g_http_server.listen_fd); |
| 529 | g_http_server.listen_fd = -1; |
| 530 | return NULL; |
| 531 | } |
| 532 | |
| 533 | atomic_store(&g_http_server_in_use, 1); |
| 534 | return &g_http_server; |
| 535 | } |
| 536 | |
| 537 | void http_server_destroy(http_server_t *server) { |
| 538 | if (server != NULL) { |
| 539 | if (server == &g_http_server) { |
| 540 | for (size_t i = 0; i < (size_t)HTTP_MAX_CONNECTIONS; i++) { |
| 541 | if (g_http_connections[i].fd >= 0) { |
| 542 | http_close_connection(&g_http_connections[i]); |
| 543 | } |
| 544 | } |
| 545 | if (server->listen_fd >= 0) { |
| 546 | event_loop_remove(server->loop, server->listen_fd); |
| 547 | close(server->listen_fd); |
| 548 | server->listen_fd = -1; |
| 549 | } |
| 550 | atomic_store(&g_http_server_in_use, 0); |
| 551 | } |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | /*===========================================================================* |
| 556 | * ACCEPT CALLBACK — new client connecting |
| 557 | *===========================================================================*/ |
| 558 | |
| 559 | static int http_accept_callback(int fd, uint32_t events, void *data) { |
| 560 | http_server_t *server = (http_server_t *)data; |
| 561 | (void)events; |
| 562 | |
| 563 | struct sockaddr_storage client_addr; |
| 564 | socklen_t addr_len = sizeof(client_addr); |
| 565 | |
| 566 | int client_fd = accept(fd, (struct sockaddr *)&client_addr, &addr_len); |
| 567 | if (client_fd < 0) { |
| 568 | return 0; /* EAGAIN or error, keep listening */ |
| 569 | } |
| 570 | |
| 571 | /* |
| 572 | * SOCKET TUNING FOR UPLOAD THROUGHPUT |
| 573 | * |
| 574 | * TCP_NODELAY |
| 575 | * Disable Nagle's algorithm on the server's outgoing path. |
| 576 | * Nagle batches small writes, adding up to 200 ms of latency for |
| 577 | * ACKs and HTTP response headers. Disabling it ensures the 200-byte |
| 578 | * "HTTP/1.1 200 OK" response after upload completes is sent immediately |
| 579 | * rather than waiting for the kernel to accumulate more data. |
| 580 | * Has no effect on incoming data (upload body direction). |
| 581 | * |
| 582 | * SO_RCVBUF |
| 583 | * Hint the kernel to allocate a 2 MB receive buffer for this socket. |
| 584 | * A larger receive buffer allows the kernel to acknowledge incoming |
| 585 | * data in larger batches, keeping the sender's congestion window open |
| 586 | * between event-loop wakeups. Without this, the default receive buffer |
| 587 | * (~87 KB on Linux, ~256 KB on FreeBSD) can be drained faster than the |
| 588 | * event loop wakes up, forcing the remote sender to pause. |
| 589 | * |
| 590 | * IMPORTANT: setsockopt(SO_RCVBUF) is a hint. The kernel caps it at |
| 591 | * net.core.rmem_max (Linux) or kern.ipc.maxsockbuf (FreeBSD/PS5) and |
| 592 | * silently ignores requests above the system maximum. Setting it here |
| 593 | * is therefore always safe — worst case it has no effect. |
| 594 | * |
| 595 | * @note These options apply to ALL HTTP client connections, not just |
| 596 | * uploads. TCP_NODELAY is universally beneficial for request/ |
| 597 | * response HTTP. The SO_RCVBUF hint is harmless for short API |
| 598 | * requests (the kernel won't actually allocate the full buffer |
| 599 | * until data arrives). |
| 600 | * |
| 601 | * @note Thread-safety: called only in the accept callback (single |
| 602 | * event-loop thread). |
| 603 | */ |
| 604 | { |
| 605 | int nodelay = 1; |
| 606 | (void)setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, |
| 607 | &nodelay, sizeof(nodelay)); |
| 608 | |
| 609 | int rcvbuf = (int)HTTP_UPLOAD_RCVBUF_SIZE; |
| 610 | (void)setsockopt(client_fd, SOL_SOCKET, SO_RCVBUF, |
| 611 | &rcvbuf, sizeof(rcvbuf)); |
| 612 | |
| 613 | #if (HTTP_SNDBUF_SIZE) > 0U |
| 614 | /* |
| 615 | * SO_SNDBUF — bypass OrbisOS TCP send-buffer clamping. |
| 616 | * |
| 617 | * Without this, PS5/PS4 accepted sockets keep the kernel default |
| 618 | * (~256 KB), causing pal_send_all() to block as soon as the buffer |
| 619 | * fills and capping HTTP downloads to ~400 Mbps. 4 MB matches |
| 620 | * FTP_TCP_DATA_SNDBUF and keeps the TCP pipeline saturated at |
| 621 | * 1 Gbps LAN RTTs. On non-PS platforms HTTP_SNDBUF_SIZE == 0 so |
| 622 | * this block is compiled out and auto-tuning remains active. |
| 623 | */ |
| 624 | { |
| 625 | int sndbuf = (int)HTTP_SNDBUF_SIZE; |
| 626 | (void)setsockopt(client_fd, SOL_SOCKET, SO_SNDBUF, |
| 627 | &sndbuf, sizeof(sndbuf)); |
| 628 | } |
| 629 | #endif |
| 630 | } |
| 631 | |
| 632 | /* Connection limit */ |
| 633 | if (atomic_load(&server->connection_count) >= HTTP_MAX_CONNECTIONS) { |
| 634 | close(client_fd); |
| 635 | return 0; |
| 636 | } |
| 637 | |
| 638 | http_connection_t *conn = http_connection_acquire(server, client_fd); |
| 639 | if (conn == NULL) { |
| 640 | close(client_fd); |
| 641 | return 0; |
| 642 | } |
| 643 | |
| 644 | (void)atomic_fetch_add(&server->connection_count, 1); |
| 645 | |
| 646 | /* Register for read events */ |
| 647 | event_loop_add(server->loop, client_fd, EVENT_READ, http_client_callback, |
| 648 | conn); |
| 649 | |
| 650 | return 0; |
| 651 | } |
| 652 | |
| 653 | /*===========================================================================* |
| 654 | * CLIENT CALLBACK — data available on client socket |
| 655 | *===========================================================================*/ |
| 656 | |
| 657 | static int http_client_callback(int fd, uint32_t events, void *data) { |
| 658 | http_connection_t *conn = (http_connection_t *)data; |
| 659 | (void)fd; |
| 660 | |
| 661 | /* Connection closed or error */ |
| 662 | if (events & (EVENT_CLOSE | EVENT_ERROR)) { |
| 663 | http_close_connection(conn); |
| 664 | return -1; |
| 665 | } |
| 666 | |
| 667 | if (events & EVENT_READ) { |
| 668 | #if ENABLE_WEB_UPLOAD |
| 669 | if (conn->upload_active != 0) { |
| 670 | /* |
| 671 | * UPLOAD STREAMING READ |
| 672 | * |
| 673 | * Use the pre-allocated upload_chunk_buf (HTTP_UPLOAD_CHUNK_SIZE = |
| 674 | * 256 KB) instead of conn->buffer (HTTP_REQUEST_BUFFER_SIZE = 8 KB). |
| 675 | * |
| 676 | * WHY: reading 8 KB per event-loop iteration caps throughput because |
| 677 | * each iteration requires a kqueue/epoll round-trip (typically |
| 678 | * 5-20 µs). At 8 KB × 50 000 wakeups/s the ceiling is ~400 MB/s in |
| 679 | * theory, but in practice kernel scheduling overhead and interrupt |
| 680 | * coalescing bring the real ceiling much lower (measured ~5 MB/s). |
| 681 | * |
| 682 | * At 256 KB the required syscall rate for 113 MB/s drops to ~440/s, |
| 683 | * well within the event-loop budget. Each read() drains a much |
| 684 | * larger window of TCP receive-buffer data in one call, so the kernel |
| 685 | * spends far more time in DMA and far less in context switches. |
| 686 | * |
| 687 | * FALLBACK: if malloc failed during upload initialisation, |
| 688 | * upload_chunk_buf is NULL and we fall back to conn->buffer (8 KB). |
| 689 | * This preserves correctness at the cost of performance. |
| 690 | * |
| 691 | * ISOLATION: only active when upload_active != 0. All other HTTP |
| 692 | * paths (headers, download, API) continue to use conn->buffer. |
| 693 | * FTP and internal copy are completely unaffected. |
| 694 | * |
| 695 | * @pre conn->upload_chunk_buf allocated at upload start (or NULL) |
| 696 | * @pre conn->upload_remaining > 0 |
| 697 | * @post conn->upload_remaining decremented by bytes written to file |
| 698 | */ |
| 699 | uint8_t *rd_buf = (conn->upload_chunk_buf != NULL) |
| 700 | ? conn->upload_chunk_buf |
| 701 | : (uint8_t *)conn->buffer; |
| 702 | size_t rd_cap = (conn->upload_chunk_buf != NULL) |
| 703 | ? (size_t)HTTP_UPLOAD_CHUNK_SIZE |
| 704 | : sizeof(conn->buffer); |
| 705 | |
| 706 | ssize_t n = read(conn->fd, rd_buf, rd_cap); |
| 707 | if (n <= 0) { |
| 708 | http_close_connection(conn); |
| 709 | return -1; |
| 710 | } |
| 711 | |
| 712 | size_t got = (size_t)n; |
| 713 | if (got > conn->upload_remaining) { |
| 714 | got = conn->upload_remaining; |
| 715 | } |
| 716 | |
| 717 | if ((got > 0U) && (conn->upload_fd >= 0)) { |
| 718 | /* |
| 719 | * DIRECT WRITE — bypass pal_file_write_all() |
| 720 | * |
| 721 | * pal_file_write_all() subdivides writes into PAL_FILE_WRITE_CHUNK_MAX |
| 722 | * (128 KB on PS5, 64 KB on PS4). That limit exists for FTP STOR to |
| 723 | * prevent TCP recv-buffer stalls while the kernel is busy with a long |
| 724 | * write: the FTP STOR loop reads from the socket and writes to disk |
| 725 | * serially, so a slow 4 MB write would stall socket reads long enough |
| 726 | * to fill the TCP window and trigger a client inactivity timeout. |
| 727 | * |
| 728 | * HTTP upload is different: |
| 729 | * - We already have the full chunk (up to 256 KB) in rd_buf. |
| 730 | * - The socket read and the disk write are NOT interleaved inside a |
| 731 | * single loop iteration, so TCP flow control is not a concern. |
| 732 | * - Writing 256 KB in a single write() call instead of two 128 KB |
| 733 | * calls halves the number of PFS AES-XTS context setups per MB. |
| 734 | * This is the primary reason /data uploads were capped at ~25 MB/s |
| 735 | * while USB (no encryption) reached 80 MB/s. |
| 736 | * |
| 737 | * We reproduce the same direct-write loop used by pal_file_copy_atomic |
| 738 | * (which bypasses pal_file_write_all for identical reasons) and handle |
| 739 | * the PS4/PS5 PFS silent-ENOSPC (write() == 0) quirk. |
| 740 | * |
| 741 | * @pre rd_buf[0..got-1] contains valid data to write |
| 742 | * @pre conn->upload_fd is a valid open writable file descriptor |
| 743 | */ |
| 744 | const uint8_t *wr_p = rd_buf; |
| 745 | size_t wr_rem = got; |
| 746 | int wr_ok = 1; |
| 747 | |
| 748 | while (wr_rem > 0U) { |
| 749 | ssize_t w = write(conn->upload_fd, wr_p, wr_rem); |
| 750 | if (w > 0) { |
| 751 | wr_p += (size_t)w; |
| 752 | wr_rem -= (size_t)w; |
| 753 | continue; |
| 754 | } |
| 755 | if ((w < 0) && (errno == EINTR)) { |
| 756 | continue; |
| 757 | } |
| 758 | /* w == 0: PS4/PS5 PFS silent ENOSPC */ |
| 759 | wr_ok = 0; |
| 760 | break; |
| 761 | } |
| 762 | |
| 763 | if (wr_ok == 0) { |
| 764 | http_close_connection(conn); |
| 765 | return -1; |
| 766 | } |
| 767 | } |
| 768 | |
| 769 | conn->upload_remaining -= got; |
| 770 | if (conn->upload_remaining == 0U) { |
| 771 | if (conn->upload_fd >= 0) { |
| 772 | (void)pal_file_close(conn->upload_fd); |
| 773 | conn->upload_fd = -1; |
| 774 | } |
| 775 | |
| 776 | http_response_t *resp = http_response_create(HTTP_STATUS_200_OK); |
| 777 | if (resp != NULL) { |
| 778 | http_response_add_header(resp, "Content-Type", "application/json"); |
| 779 | http_response_add_header(resp, "Access-Control-Allow-Origin", "*"); |
| 780 | const char *body = "{\"ok\":true}"; |
| 781 | http_response_set_body(resp, body, strlen(body)); |
| 782 | if (resp->used > 0) { |
| 783 | size_t total = 0; |
| 784 | size_t remain = resp->used; |
| 785 | while (remain > 0) { |
| 786 | ssize_t sent = write(conn->fd, resp->data + total, remain); |
| 787 | if (sent <= 0) { |
| 788 | break; |
| 789 | } |
| 790 | total += (size_t)sent; |
| 791 | remain -= (size_t)sent; |
| 792 | } |
| 793 | } |
| 794 | http_response_destroy(resp); |
| 795 | } |
| 796 | |
| 797 | http_close_connection(conn); |
| 798 | return -1; |
| 799 | } |
| 800 | |
| 801 | return 0; |
| 802 | } |
| 803 | #endif |
| 804 | |
| 805 | size_t remaining = sizeof(conn->buffer) - conn->buffer_used - 1; |
| 806 | if (remaining == 0) { |
| 807 | /* Buffer full without complete request — drop */ |
| 808 | http_close_connection(conn); |
| 809 | return -1; |
| 810 | } |
| 811 | |
| 812 | ssize_t n = read(conn->fd, conn->buffer + conn->buffer_used, remaining); |
| 813 | |
| 814 | if (n <= 0) { |
| 815 | http_close_connection(conn); |
| 816 | return -1; |
| 817 | } |
| 818 | |
| 819 | conn->buffer_used += (size_t)n; |
| 820 | conn->buffer[conn->buffer_used] = '\0'; |
| 821 | |
| 822 | /* Check for complete HTTP request (headers end with \r\n\r\n) */ |
| 823 | const char *end = strstr(conn->buffer, "\r\n\r\n"); |
| 824 | if (end != NULL) { |
| 825 | char method[8]; |
| 826 | char uri[HTTP_URI_MAX_LENGTH]; |
| 827 | size_t header_len = 0U; |
| 828 | size_t content_length = 0U; |
| 829 | |
| 830 | if (http_parse_basic_request(conn->buffer, method, sizeof(method), uri, |
| 831 | sizeof(uri), &header_len, |
| 832 | &content_length) != 0) { |
| 833 | http_close_connection(conn); |
| 834 | return -1; |
| 835 | } |
| 836 | |
| 837 | #if ENABLE_WEB_UPLOAD |
| 838 | if ((strcmp(method, "POST") == 0) && |
| 839 | (strncmp(uri, "/api/upload", 11) == 0)) { |
| 840 | http_request_t up_req; |
| 841 | if ((http_parse_request(conn->buffer, conn->buffer_used, &up_req) < |
| 842 | 0) || |
| 843 | (http_csrf_validate(&up_req) != 0)) { |
| 844 | http_response_t *resp = |
| 845 | http_response_create(HTTP_STATUS_403_FORBIDDEN); |
| 846 | if (resp != NULL) { |
| 847 | http_response_add_header(resp, "Content-Type", "application/json"); |
| 848 | http_response_add_header(resp, "Access-Control-Allow-Origin", "*"); |
| 849 | const char *body = "{\"error\":\"Invalid or missing CSRF token\"}"; |
| 850 | http_response_set_body(resp, body, strlen(body)); |
| 851 | if (resp->used > 0) { |
| 852 | (void)write(conn->fd, resp->data, resp->used); |
| 853 | } |
| 854 | http_response_destroy(resp); |
| 855 | } |
| 856 | http_close_connection(conn); |
| 857 | return -1; |
| 858 | } |
| 859 | |
| 860 | if (content_length == 0U) { |
| 861 | http_response_t *resp = |
| 862 | http_response_create(HTTP_STATUS_400_BAD_REQUEST); |
| 863 | if (resp != NULL) { |
| 864 | const char *msg = "Missing Content-Length"; |
| 865 | http_response_set_body(resp, msg, strlen(msg)); |
| 866 | if (resp->used > 0) { |
| 867 | (void)write(conn->fd, resp->data, resp->used); |
| 868 | } |
| 869 | http_response_destroy(resp); |
| 870 | } |
| 871 | http_close_connection(conn); |
| 872 | return -1; |
| 873 | } |
| 874 | |
| 875 | char dir_path[1024]; |
| 876 | char file_name[256]; |
| 877 | if ((get_query_param(uri, "path", dir_path, sizeof(dir_path)) != 0) || |
| 878 | (get_query_param(uri, "name", file_name, sizeof(file_name)) != 0)) { |
| 879 | http_close_connection(conn); |
| 880 | return -1; |
| 881 | } |
| 882 | if (!is_safe_path_local(dir_path) || |
| 883 | !is_safe_filename_local(file_name)) { |
| 884 | http_close_connection(conn); |
| 885 | return -1; |
| 886 | } |
| 887 | |
| 888 | char full[1024]; |
| 889 | if (strcmp(dir_path, "/") == 0) { |
| 890 | (void)snprintf(full, sizeof(full), "/%s", file_name); |
| 891 | } else { |
| 892 | (void)snprintf(full, sizeof(full), "%s/%s", dir_path, file_name); |
| 893 | } |
| 894 | if (!is_safe_path_local(full)) { |
| 895 | http_close_connection(conn); |
| 896 | return -1; |
| 897 | } |
| 898 | |
| 899 | /* |
| 900 | * VULN-02 fix: confine upload path to the HTTP root |
| 901 | * |
| 902 | * is_safe_path_local() blocks ".." and "//" |
| 903 | * http_api_get_root() confines to server root directory |
| 904 | */ |
| 905 | { |
| 906 | const char *http_root = http_api_get_root(); |
| 907 | if (http_root[0] != '\0') { |
| 908 | size_t rlen = strlen(http_root); |
| 909 | /* root "/" allows everything */ |
| 910 | if (!(rlen == 1U && http_root[0] == '/')) { |
| 911 | if (strncmp(full, http_root, rlen) != 0 || |
| 912 | (full[rlen] != '/' && full[rlen] != '\0')) { |
| 913 | http_close_connection(conn); |
| 914 | return -1; |
| 915 | } |
| 916 | } |
| 917 | } |
| 918 | } |
| 919 | |
| 920 | /* Create intermediate directories if needed (for folder uploads) */ |
| 921 | char dir_buf[1024]; |
| 922 | const char *last_slash = strrchr(full, '/'); |
| 923 | if (last_slash != NULL && last_slash != full) { |
| 924 | size_t dir_len = (size_t)(last_slash - full); |
| 925 | if (dir_len < sizeof(dir_buf)) { |
| 926 | strncpy(dir_buf, full, dir_len); |
| 927 | dir_buf[dir_len] = '\0'; |
| 928 | if (mkdir_recursive(dir_buf) != 0) { |
| 929 | /* Directory creation failed — send HTTP 500 error */ |
| 930 | http_response_t *resp = http_response_create(HTTP_STATUS_500_INTERNAL_ERROR); |
| 931 | if (resp != NULL) { |
| 932 | http_response_add_header(resp, "Content-Type", "application/json"); |
| 933 | http_response_add_header(resp, "Access-Control-Allow-Origin", "*"); |
| 934 | const char *body = "{\"error\":\"Failed to create directory\"}"; |
| 935 | http_response_set_body(resp, body, strlen(body)); |
| 936 | if (resp->used > 0) { |
| 937 | (void)write(conn->fd, resp->data, resp->used); |
| 938 | } |
| 939 | http_response_destroy(resp); |
| 940 | } |
| 941 | http_close_connection(conn); |
| 942 | return -1; |
| 943 | } |
| 944 | } |
| 945 | } |
| 946 | |
| 947 | int out_fd = pal_file_open(full, O_WRONLY | O_CREAT | O_TRUNC, 0666); |
| 948 | if (out_fd < 0) { |
| 949 | http_response_t *resp = http_response_create(HTTP_STATUS_500_INTERNAL_ERROR); |
| 950 | if (resp != NULL) { |
| 951 | http_response_add_header(resp, "Content-Type", "application/json"); |
| 952 | http_response_add_header(resp, "Access-Control-Allow-Origin", "*"); |
| 953 | const char *body = "{\"error\":\"Failed to open file for writing\"}"; |
| 954 | http_response_set_body(resp, body, strlen(body)); |
| 955 | if (resp->used > 0) { |
| 956 | (void)write(conn->fd, resp->data, resp->used); |
| 957 | } |
| 958 | http_response_destroy(resp); |
| 959 | } |
| 960 | http_close_connection(conn); |
| 961 | return -1; |
| 962 | } |
| 963 | conn->upload_fd = out_fd; |
| 964 | conn->upload_active = 1; |
| 965 | |
| 966 | /* |
| 967 | * Allocate the large upload read buffer (HTTP_UPLOAD_CHUNK_SIZE = |
| 968 | * 256 KB). If malloc fails we fall back to conn->buffer (8 KB) — |
| 969 | * correctness is preserved, only throughput is affected. |
| 970 | * |
| 971 | * The buffer is freed in http_connection_release() regardless of |
| 972 | * how the connection terminates (success, error, or timeout). |
| 973 | */ |
| 974 | if (conn->upload_chunk_buf == NULL) { |
| 975 | conn->upload_chunk_buf = (uint8_t *)malloc(HTTP_UPLOAD_CHUNK_SIZE); |
| 976 | /* malloc failure is non-fatal: fallback path uses conn->buffer */ |
| 977 | } |
| 978 | |
| 979 | size_t in_buf = 0U; |
| 980 | if (conn->buffer_used > header_len) { |
| 981 | in_buf = conn->buffer_used - header_len; |
| 982 | if (in_buf > content_length) { |
| 983 | in_buf = content_length; |
| 984 | } |
| 985 | } |
| 986 | |
| 987 | if (in_buf > 0U) { |
| 988 | if (pal_file_write_all(out_fd, conn->buffer + header_len, in_buf) < |
| 989 | 0) { |
| 990 | http_close_connection(conn); |
| 991 | return -1; |
| 992 | } |
| 993 | } |
| 994 | |
| 995 | conn->upload_remaining = content_length - in_buf; |
| 996 | conn->buffer_used = 0; |
| 997 | conn->buffer[0] = '\0'; |
| 998 | |
| 999 | if (conn->upload_remaining == 0U) { |
| 1000 | (void)pal_file_close(conn->upload_fd); |
| 1001 | conn->upload_fd = -1; |
| 1002 | conn->upload_active = 0; |
| 1003 | http_response_t *resp = http_response_create(HTTP_STATUS_200_OK); |
| 1004 | if (resp != NULL) { |
| 1005 | http_response_add_header(resp, "Content-Type", "application/json"); |
| 1006 | http_response_add_header(resp, "Access-Control-Allow-Origin", "*"); |
| 1007 | const char *body = "{\"ok\":true}"; |
| 1008 | http_response_set_body(resp, body, strlen(body)); |
| 1009 | if (resp->used > 0) { |
| 1010 | (void)write(conn->fd, resp->data, resp->used); |
| 1011 | } |
| 1012 | http_response_destroy(resp); |
| 1013 | } |
| 1014 | http_close_connection(conn); |
| 1015 | return -1; |
| 1016 | } |
| 1017 | |
| 1018 | return 0; |
| 1019 | } |
| 1020 | #endif |
| 1021 | |
| 1022 | if (content_length > 0U) { |
| 1023 | /* |
| 1024 | * OVERFLOW-SAFE size check. |
| 1025 | * |
| 1026 | * WHY: a malicious client can send |
| 1027 | * Content-Length: 18446744073709551615 (SIZE_MAX on 64-bit) |
| 1028 | * Without the pre-addition guard, (header_len + SIZE_MAX) wraps to |
| 1029 | * (header_len - 1) via unsigned overflow, which is LESS than the |
| 1030 | * buffer limit. Both the "too large" rejection and the "wait for |
| 1031 | * more data" guard would then pass silently, dispatching a request |
| 1032 | * to the handler with a fabricated body pointer. |
| 1033 | * |
| 1034 | * Fix: reject if content_length alone already exceeds the available |
| 1035 | * space before performing the addition. This makes overflow |
| 1036 | * impossible because after the guard content_length <= |
| 1037 | * (sizeof(conn->buffer) - 1), and header_len is always < that same |
| 1038 | * limit (we have already confirmed \r\n\r\n fits inside the buffer). |
| 1039 | * |
| 1040 | * @pre header_len > 0 (guaranteed by the strstr("\r\n\r\n") check) |
| 1041 | * @post content_length + header_len <= SIZE_MAX (no wrap possible) |
| 1042 | */ |
| 1043 | const size_t buf_limit = sizeof(conn->buffer) - 1U; |
| 1044 | if (content_length > buf_limit || |
| 1045 | (header_len + content_length) > buf_limit) { |
| 1046 | http_response_t *resp = |
| 1047 | http_response_create(HTTP_STATUS_400_BAD_REQUEST); |
| 1048 | if (resp != NULL) { |
| 1049 | const char *msg = "Request too large"; |
| 1050 | http_response_set_body(resp, msg, strlen(msg)); |
| 1051 | if (resp->used > 0) { |
| 1052 | (void)write(conn->fd, resp->data, resp->used); |
| 1053 | } |
| 1054 | http_response_destroy(resp); |
| 1055 | } |
| 1056 | http_close_connection(conn); |
| 1057 | return -1; |
| 1058 | } |
| 1059 | |
| 1060 | if (conn->buffer_used < (header_len + content_length)) { |
| 1061 | return 0; |
| 1062 | } |
| 1063 | } |
| 1064 | |
| 1065 | (void)http_handle_request(conn); |
| 1066 | http_close_connection(conn); |
| 1067 | return -1; |
| 1068 | } |
| 1069 | } |
| 1070 | |
| 1071 | return 0; |
| 1072 | } |
| 1073 | |
| 1074 | /*===========================================================================* |
| 1075 | * HANDLE REQUEST — parse, route, respond |
| 1076 | *===========================================================================*/ |
| 1077 | |
| 1078 | static int http_handle_request(http_connection_t *conn) { |
| 1079 | http_request_t request; |
| 1080 | if (http_parse_request(conn->buffer, conn->buffer_used, &request) < 0) { |
| 1081 | return -1; |
| 1082 | } |
| 1083 | |
| 1084 | http_response_t *response = http_api_handle(&request); |
| 1085 | if (response == NULL) { |
| 1086 | response = http_response_create(HTTP_STATUS_500_INTERNAL_ERROR); |
| 1087 | if (response == NULL) { |
| 1088 | return -1; |
| 1089 | } |
| 1090 | const char *msg = "Internal Server Error"; |
| 1091 | http_response_set_body(response, msg, strlen(msg)); |
| 1092 | } |
| 1093 | |
| 1094 | /* Send response headers (+ body if no sendfile) */ |
| 1095 | if (response->used > 0) { |
| 1096 | if (pal_send_all(conn->fd, response->data, response->used, 0) < 0) { |
| 1097 | http_response_destroy(response); |
| 1098 | return -1; |
| 1099 | } |
| 1100 | } |
| 1101 | |
| 1102 | /* |
| 1103 | * ┌────────────────────────────────────────────────────┐ |
| 1104 | * │ MEMORY BODY PATH — stream embedded static assets │ |
| 1105 | * └────────────────────────────────────────────────────┘ |
| 1106 | */ |
| 1107 | if ((response->mem_seg_count > 0U) && |
| 1108 | (response->mem_seg_index < response->mem_seg_count)) { |
| 1109 | while (response->mem_seg_index < response->mem_seg_count) { |
| 1110 | const void *seg = response->mem_segs[response->mem_seg_index]; |
| 1111 | size_t seg_len = response->mem_lens[response->mem_seg_index]; |
| 1112 | if ((seg == NULL) || (seg_len == 0U)) { |
| 1113 | response->mem_seg_index++; |
| 1114 | response->mem_seg_sent = 0U; |
| 1115 | continue; |
| 1116 | } |
| 1117 | if (response->mem_seg_sent >= seg_len) { |
| 1118 | response->mem_seg_index++; |
| 1119 | response->mem_seg_sent = 0U; |
| 1120 | continue; |
| 1121 | } |
| 1122 | const unsigned char *p = (const unsigned char *)seg; |
| 1123 | const unsigned char *start = p + response->mem_seg_sent; |
| 1124 | size_t remaining = seg_len - response->mem_seg_sent; |
| 1125 | if (pal_send_all(conn->fd, start, remaining, 0) < 0) { |
| 1126 | http_response_destroy(response); |
| 1127 | return -1; |
| 1128 | } |
| 1129 | response->mem_seg_sent = seg_len; |
| 1130 | } |
| 1131 | } |
| 1132 | |
| 1133 | if ((response->mem_body != NULL) && |
| 1134 | (response->mem_sent < response->mem_length)) { |
| 1135 | const unsigned char *p = (const unsigned char *)response->mem_body; |
| 1136 | const unsigned char *start = p + response->mem_sent; |
| 1137 | size_t remaining = response->mem_length - response->mem_sent; |
| 1138 | if (pal_send_all(conn->fd, start, remaining, 0) < 0) { |
| 1139 | http_response_destroy(response); |
| 1140 | return -1; |
| 1141 | } |
| 1142 | response->mem_sent = response->mem_length; |
| 1143 | } |
| 1144 | |
| 1145 | /* |
| 1146 | * ┌────────────────────────────────────────┐ |
| 1147 | * │ SENDFILE PATH — stream file content │ |
| 1148 | * │ Used by /api/download │ |
| 1149 | * └────────────────────────────────────────┘ |
| 1150 | */ |
| 1151 | /* |
| 1152 | * ┌────────────────────────────────────────┐ |
| 1153 | * │ SENDFILE PATH — stream file content │ |
| 1154 | * │ Used by /api/download │ |
| 1155 | * └────────────────────────────────────────┘ |
| 1156 | */ |
| 1157 | if (response->sendfile_fd >= 0) { |
| 1158 | /* |
| 1159 | * FILE DOWNLOAD — two paths based on filesystem safety |
| 1160 | * |
| 1161 | * PATH A: sendfile_safe == 1 (Linux, macOS, FreeBSD ufs/zfs) |
| 1162 | * pal_sendfile() → zero-copy DMA from page-cache to NIC. |
| 1163 | * |
| 1164 | * PATH B: sendfile_safe == 0 (PS5/PS4 exFAT, PFS, nullfs, msdosfs) |
| 1165 | * pread() + pal_send_all() — explicit userspace copy. |
| 1166 | * |
| 1167 | * WHY: On PS5/PS4 (FreeBSD), calling sendfile(2) on exFAT, msdosfs, |
| 1168 | * nullfs, pfsmnt or pfs vnodes dereferences a null pager function |
| 1169 | * pointer inside the kernel and causes an IMMEDIATE KERNEL PANIC. |
| 1170 | * errno is never set — execution never returns to userspace. |
| 1171 | * The EINVAL fallback in pal_sendfile() therefore never executes. |
| 1172 | * The ONLY safe solution is to never call sendfile() on these FSes. |
| 1173 | * |
| 1174 | * api_download() (http_api.c) sets sendfile_safe via fstatfs() on |
| 1175 | * the open fd before returning the http_response_t to us. |
| 1176 | */ |
| 1177 | off_t sf_offset = (off_t)response->sendfile_offset; |
| 1178 | size_t sf_remaining = response->sendfile_count; |
| 1179 | |
| 1180 | if (response->sendfile_safe) { |
| 1181 | /* PATH A: zero-copy sendfile */ |
| 1182 | while (sf_remaining > 0U) { |
| 1183 | size_t chunk = sf_remaining; |
| 1184 | if (chunk > (size_t)HTTP_SENDFILE_CHUNK_SIZE) { |
| 1185 | chunk = (size_t)HTTP_SENDFILE_CHUNK_SIZE; |
| 1186 | } |
| 1187 | ssize_t sent = pal_sendfile(conn->fd, response->sendfile_fd, |
| 1188 | &sf_offset, chunk); |
| 1189 | if (sent < 0) { break; } |
| 1190 | if (sent == 0) { |
| 1191 | if (errno == EINTR) { continue; } |
| 1192 | break; |
| 1193 | } |
| 1194 | sf_remaining -= (size_t)sent; |
| 1195 | } |
| 1196 | |
| 1197 | } else { |
| 1198 | /* PATH B: pread + send_all (PS5/PS4 safe) */ |
| 1199 | /* |
| 1200 | * Heap-allocate the read buffer — HTTP_DOWNLOAD_PREAD_CHUNK (2 MB) |
| 1201 | * on the stack would overflow the event-loop thread's stack on PS5. |
| 1202 | * On malloc failure, fall back to a small on-stack buffer so the |
| 1203 | * transfer degrades in speed rather than aborting. |
| 1204 | */ |
| 1205 | uint8_t dl_stack[4096]; |
| 1206 | uint8_t *dl_buf = (uint8_t *)malloc(HTTP_DOWNLOAD_PREAD_CHUNK); |
| 1207 | size_t dl_cap = (dl_buf != NULL) |
| 1208 | ? (size_t)HTTP_DOWNLOAD_PREAD_CHUNK |
| 1209 | : sizeof(dl_stack); |
| 1210 | if (dl_buf == NULL) { |
| 1211 | dl_buf = dl_stack; |
| 1212 | } |
| 1213 | |
| 1214 | int dl_err = 0; |
| 1215 | while (sf_remaining > 0U) { |
| 1216 | size_t want = (sf_remaining < dl_cap) ? sf_remaining : dl_cap; |
| 1217 | ssize_t nr = pread(response->sendfile_fd, dl_buf, want, |
| 1218 | sf_offset); |
| 1219 | if (nr <= 0) { |
| 1220 | if ((nr < 0) && (errno == EINTR)) { continue; } |
| 1221 | dl_err = 1; |
| 1222 | break; |
| 1223 | } |
| 1224 | if (pal_send_all(conn->fd, dl_buf, (size_t)nr, 0) < 0) { |
| 1225 | dl_err = 1; |
| 1226 | break; |
| 1227 | } |
| 1228 | sf_offset += (off_t)nr; |
| 1229 | sf_remaining -= (size_t)nr; |
| 1230 | } |
| 1231 | (void)dl_err; |
| 1232 | |
| 1233 | if (dl_buf != dl_stack) { |
| 1234 | free(dl_buf); |
| 1235 | } |
| 1236 | } |
| 1237 | |
| 1238 | close(response->sendfile_fd); |
| 1239 | response->sendfile_fd = -1; |
| 1240 | } |
| 1241 | |
| 1242 | /* |
| 1243 | * ┌─────────────────────────────────────────────────────────┐ |
| 1244 | * │ CHUNKED STREAMING — for large directories (/api/list) │ |
| 1245 | * └─────────────────────────────────────────────────────────┘ |
| 1246 | */ |
| 1247 | if (response->stream_dir != NULL) { |
| 1248 | DIR *dir = (DIR *)response->stream_dir; |
| 1249 | struct dirent *entry; |
| 1250 | char buffer[4096]; |
| 1251 | int first = 1; |
| 1252 | |
| 1253 | /* |
| 1254 | * Iterate over directory entries and send them as chunks. |
| 1255 | * We don't need a huge buffer; we just send one entry at a time. |
| 1256 | */ |
| 1257 | while ((entry = readdir(dir)) != NULL) { |
| 1258 | if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { |
| 1259 | continue; |
| 1260 | } |
| 1261 | if ((strcmp(response->stream_path, "/") == 0) && |
| 1262 | ((strcmp(entry->d_name, "dev") == 0) || |
| 1263 | (strcmp(entry->d_name, "proc") == 0) || |
| 1264 | (strcmp(entry->d_name, "sys") == 0) || |
| 1265 | (strcmp(entry->d_name, "kern") == 0))) { |
| 1266 | continue; |
| 1267 | } |
| 1268 | |
| 1269 | char fullpath[2048]; |
| 1270 | snprintf(fullpath, sizeof(fullpath), "%s/%s", response->stream_path, |
| 1271 | entry->d_name); |
| 1272 | |
| 1273 | struct stat st; |
| 1274 | if (stat(fullpath, &st) < 0) { |
| 1275 | continue; |
| 1276 | } |
| 1277 | |
| 1278 | /* Format JSON entry: ,{"name":"...","type":"...","size":...} */ |
| 1279 | size_t pos = 0; |
| 1280 | if (!first) { |
| 1281 | buffer[pos++] = ','; |
| 1282 | } |
| 1283 | first = 0; |
| 1284 | |
| 1285 | pos += |
| 1286 | (size_t)snprintf(buffer + pos, sizeof(buffer) - pos, "{\"name\":\""); |
| 1287 | |
| 1288 | /* Simple escaping for now — relying on snprintf not to overflow */ |
| 1289 | for (const char *p = entry->d_name; *p && pos < sizeof(buffer) - 64; |
| 1290 | p++) { |
| 1291 | if (*p == '"' || *p == '\\') { |
| 1292 | buffer[pos++] = '\\'; |
| 1293 | } |
| 1294 | buffer[pos++] = *p; |
| 1295 | } |
| 1296 | |
| 1297 | long long entry_size = S_ISDIR(st.st_mode) |
| 1298 | ? (long long)st.st_blocks * 512LL |
| 1299 | : (long long)st.st_size; |
| 1300 | pos += (size_t)snprintf(buffer + pos, sizeof(buffer) - pos, |
| 1301 | "\",\"type\":\"%s\",\"size\":%lld}", |
| 1302 | S_ISDIR(st.st_mode) ? "directory" : "file", |
| 1303 | entry_size); |
| 1304 | |
| 1305 | /* Send chunk: <hex-len>\r\n<data>\r\n */ |
| 1306 | char chunk_header[32]; |
| 1307 | int header_len = |
| 1308 | snprintf(chunk_header, sizeof(chunk_header), "%zx\r\n", pos); |
| 1309 | |
| 1310 | if (pal_send_all(conn->fd, chunk_header, (size_t)header_len, 0) < 0) |
| 1311 | break; |
| 1312 | if (pal_send_all(conn->fd, buffer, pos, 0) < 0) |
| 1313 | break; |
| 1314 | if (pal_send_all(conn->fd, "\r\n", 2, 0) < 0) |
| 1315 | break; |
| 1316 | } |
| 1317 | |
| 1318 | closedir(dir); |
| 1319 | response->stream_dir = NULL; |
| 1320 | |
| 1321 | /* Send closing JSON: ]} */ |
| 1322 | const char *closer = "]}"; |
| 1323 | size_t closer_len = strlen(closer); |
| 1324 | char chunk_header[32]; |
| 1325 | int header_len = |
| 1326 | snprintf(chunk_header, sizeof(chunk_header), "%zx\r\n", closer_len); |
| 1327 | (void)pal_send_all(conn->fd, chunk_header, (size_t)header_len, 0); |
| 1328 | (void)pal_send_all(conn->fd, closer, closer_len, 0); |
| 1329 | (void)pal_send_all(conn->fd, "\r\n", 2, 0); |
| 1330 | |
| 1331 | /* End of stream: 0\r\n\r\n */ |
| 1332 | (void)pal_send_all(conn->fd, "0\r\n\r\n", 5, 0); |
| 1333 | } |
| 1334 | |
| 1335 | http_response_destroy(response); |
| 1336 | return 0; |
| 1337 | } |
| 1338 | |
| 1339 | /*===========================================================================* |
| 1340 | * CLOSE CONNECTION — cleanup resources |
| 1341 | *===========================================================================*/ |
| 1342 | |
| 1343 | static void http_close_connection(http_connection_t *conn) { |
| 1344 | if (conn == NULL) { |
| 1345 | return; |
| 1346 | } |
| 1347 | |
| 1348 | http_server_t *server = conn->server; |
| 1349 | int fd = conn->fd; |
| 1350 | |
| 1351 | if ((server != NULL) && (fd >= 0)) { |
| 1352 | event_loop_remove(server->loop, fd); |
| 1353 | if (atomic_load(&server->connection_count) > 0) { |
| 1354 | (void)atomic_fetch_sub(&server->connection_count, 1); |
| 1355 | } |
| 1356 | } |
| 1357 | |
| 1358 | if (fd >= 0) { |
| 1359 | close(fd); |
| 1360 | } |
| 1361 | http_connection_release(conn); |
| 1362 | } |
| 1363 |