Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
src/http_server.c
zftpd / src / http_server.c
1/*
2MIT License
3 
4Copyright (c) 2026 Seregon
5 
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12 
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15 
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
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 
80struct 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 
88typedef 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 
119static http_server_t g_http_server;
120static atomic_int g_http_server_in_use = ATOMIC_VAR_INIT(0);
121static http_connection_t g_http_connections[HTTP_MAX_CONNECTIONS];
122 
123static 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 
137static 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 
160static 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 
186static int http_accept_callback(int fd, uint32_t events, void *data);
187static int http_client_callback(int fd, uint32_t events, void *data);
188static int http_handle_request(http_connection_t *conn);
189static void http_close_connection(http_connection_t *conn);
190 
191/*===========================================================================*
192 * SET NON-BLOCKING
193 *===========================================================================*/
194 
195static 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 
203static 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 */
280static 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 
315static 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 
375static 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 
404static 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 
423static 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 
443http_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 
537void 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 
559static 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 
657static 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 
1078static 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 
1343static 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