Seregon/zftpd

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

C/11.0 KB/No license
src/ftp_server.c
zftpd / src / ftp_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/**
26 * @file ftp_server.c
27 * @brief FTP server main loop and session management implementation
28 *
29 * @author SeregonWar
30 * @version 1.0.0
31 * @date 2026-02-13
32 *
33 */
34 
35#include "ftp_server.h"
36#include "ftp_session.h"
37#include "pal_network.h"
38#include <string.h>
39#include <unistd.h>
40#include <signal.h>
41 
42/* Forward declarations for internal functions */
43static void* server_accept_thread(void *arg);
44static ftp_session_t* allocate_session(ftp_server_context_t *ctx);
45static void free_session(ftp_server_context_t *ctx, ftp_session_t *session);
46 
47/*===========================================================================*
48 * SERVER LIFECYCLE
49 *===========================================================================*/
50 
51/**
52 * @brief Initialize FTP server
53 */
54ftp_error_t ftp_server_init(ftp_server_context_t *ctx,
55 const char *bind_ip,
56 uint16_t port,
57 const char *root_path)
58{
59 /* Validate parameters */
60 if ((ctx == NULL) || (bind_ip == NULL) || (root_path == NULL)) {
61 return FTP_ERR_INVALID_PARAM;
62 }
63
64 if (port == 0U) {
65 return FTP_ERR_INVALID_PARAM;
66 }
67
68 /* Zero-initialize context */
69 memset(ctx, 0, sizeof(*ctx));
70
71 /* Initialize network subsystem */
72 ftp_error_t err = pal_network_init();
73 if (err != FTP_OK) {
74 return err;
75 }
76
77 /* Create listen socket */
78 int fd = PAL_SOCKET(AF_INET, SOCK_STREAM, 0);
79 if (fd < 0) {
80 return FTP_ERR_SOCKET_CREATE;
81 }
82
83 /* Enable address reuse */
84 err = pal_socket_set_reuseaddr(fd);
85 if (err != FTP_OK) {
86 PAL_CLOSE(fd);
87 return err;
88 }
89
90 /* Build bind address */
91 err = pal_make_sockaddr(bind_ip, port, &ctx->listen_addr);
92 if (err != FTP_OK) {
93 PAL_CLOSE(fd);
94 return err;
95 }
96
97 /* Bind socket */
98 if (PAL_BIND(fd, (struct sockaddr*)&ctx->listen_addr,
99 sizeof(ctx->listen_addr)) < 0) {
100 PAL_CLOSE(fd);
101 return FTP_ERR_SOCKET_BIND;
102 }
103
104 /* Listen for connections */
105 if (PAL_LISTEN(fd, FTP_LISTEN_BACKLOG) < 0) {
106 PAL_CLOSE(fd);
107 return FTP_ERR_SOCKET_LISTEN;
108 }
109
110 ctx->listen_fd = fd;
111 ctx->port = port;
112
113 /* Store root path */
114 size_t root_len = strlen(root_path);
115 if (root_len >= sizeof(ctx->root_path)) {
116 PAL_CLOSE(fd);
117 return FTP_ERR_PATH_TOO_LONG;
118 }
119 memcpy(ctx->root_path, root_path, root_len + 1U);
120
121 /* Initialize server state */
122 atomic_store(&ctx->running, 0);
123 atomic_store(&ctx->active_sessions, 0U);
124
125 /* Initialize session pool */
126 for (size_t i = 0U; i < FTP_MAX_SESSIONS; i++) {
127 memset(&ctx->sessions[i], 0, sizeof(ctx->sessions[i]));
128 ctx->sessions[i].ctrl_fd = -1;
129 ctx->sessions[i].data_fd = -1;
130 ctx->sessions[i].pasv_fd = -1;
131 }
132
133 /* Initialize session lock */
134 if (pthread_mutex_init(&ctx->session_lock, NULL) != 0) {
135 PAL_CLOSE(fd);
136 return FTP_ERR_THREAD_CREATE;
137 }
138
139 /* Initialize statistics */
140 atomic_store(&ctx->stats.total_connections, 0U);
141 atomic_store(&ctx->stats.total_bytes_sent, 0U);
142 atomic_store(&ctx->stats.total_bytes_received, 0U);
143 atomic_store(&ctx->stats.total_errors, 0U);
144
145 return FTP_OK;
146}
147 
148/**
149 * @brief Start FTP server
150 */
151ftp_error_t ftp_server_start(ftp_server_context_t *ctx)
152{
153 if (ctx == NULL) {
154 return FTP_ERR_INVALID_PARAM;
155 }
156
157 if (ctx->listen_fd < 0) {
158 return FTP_ERR_INVALID_PARAM;
159 }
160
161 /* Set running flag */
162 atomic_store(&ctx->running, 1);
163
164 /* Create accept thread */
165 pthread_t accept_thread;
166 pthread_attr_t attr;
167 int attr_ok = (pthread_attr_init(&attr) == 0);
168 if (attr_ok != 0) {
169 (void)pthread_attr_setstacksize(&attr, (size_t)FTP_THREAD_STACK_SIZE);
170 }
171
172 if (pthread_create(&accept_thread, (attr_ok != 0) ? &attr : NULL, server_accept_thread, ctx) != 0) {
173 if (attr_ok != 0) {
174 (void)pthread_attr_destroy(&attr);
175 }
176 atomic_store(&ctx->running, 0);
177 return FTP_ERR_THREAD_CREATE;
178 }
179
180 if (attr_ok != 0) {
181 (void)pthread_attr_destroy(&attr);
182 }
183
184 /* Detach thread (we don't need to join it) */
185 pthread_detach(accept_thread);
186
187 return FTP_OK;
188}
189 
190/**
191 * @brief Stop FTP server (graceful shutdown with bounded wait)
192 *
193 * SHUTDOWN SEQUENCE (order matters):
194 *
195 * 1. Clear the running flag so the accept thread exits its loop check.
196 *
197 * 2. Close listen_fd immediately.
198 * WHY: PAL_ACCEPT() is a blocking syscall. The accept thread only checks
199 * ctx->running AFTER accept() returns. Without closing the socket, the
200 * thread blocks forever — even though running == 0. Closing the fd
201 * makes accept() return EBADF/EINVAL immediately, allowing the thread to
202 * observe running == 0 and exit.
203 *
204 * 3. Force shutdown(SHUT_RDWR) on every active session's ctrl_fd.
205 * WHY: Session threads block inside recv() waiting for the next FTP
206 * command. They only check for server shutdown when recv() returns.
207 * shutdown() injects an EOF into the socket without closing the fd,
208 * making recv() return 0 so the session loop exits cleanly.
209 * (close() would race with the session thread which also closes ctrl_fd
210 * in ftp_session_cleanup().)
211 *
212 * 4. Wait up to SERVER_STOP_TIMEOUT_MS for active_sessions to reach 0.
213 * WHY: Each session thread calls ftp_server_release_session() at exit,
214 * which decrements active_sessions. The timeout is a safety net: if a
215 * session thread is stuck (e.g. blocked in a long PFS write), we do not
216 * hang the entire PS5 shutdown sequence — the kernel will collect the
217 * remaining resources via SIGKILL/forced unmount anyway, but an orderly
218 * decrement is strongly preferred.
219 */
220void ftp_server_stop(ftp_server_context_t *ctx)
221{
222 if (ctx == NULL) {
223 return;
224 }
225 
226 /* Step 1 — signal stop */
227 atomic_store(&ctx->running, 0);
228 
229 /* Step 2 — unblock PAL_ACCEPT() in the accept thread */
230 if (ctx->listen_fd >= 0) {
231 PAL_CLOSE(ctx->listen_fd);
232 ctx->listen_fd = -1; /* ftp_server_cleanup() guards against double-close */
233 }
234 
235 /* Step 3 — interrupt blocking recv() in each session thread */
236 pthread_mutex_lock(&ctx->session_lock);
237 for (size_t i = 0U; i < FTP_MAX_SESSIONS; i++) {
238 int state = atomic_load(&ctx->sessions[i].state);
239 if ((state == FTP_STATE_CONNECTED) ||
240 (state == FTP_STATE_AUTHENTICATED)||
241 (state == FTP_STATE_TRANSFERRING)) {
242 int cfd = ctx->sessions[i].ctrl_fd;
243 if (cfd >= 0) {
244 /*
245 * shutdown() injects EOF without closing the fd.
246 * The session thread's recv() returns 0 → loop exits →
247 * ftp_session_cleanup() closes the fd normally.
248 * Using close() here would race with ftp_session_cleanup().
249 */
250 (void)shutdown(cfd, SHUT_RDWR);
251 }
252 }
253 }
254 pthread_mutex_unlock(&ctx->session_lock);
255 
256 /* Step 4 — wait for sessions to drain, with a hard timeout */
257#define SERVER_STOP_TIMEOUT_MS 5000U
258#define SERVER_STOP_POLL_MS 100U
259 uint32_t waited_ms = 0U;
260 while ((atomic_load(&ctx->active_sessions) > 0U) &&
261 (waited_ms < SERVER_STOP_TIMEOUT_MS)) {
262 usleep(SERVER_STOP_POLL_MS * 1000U);
263 waited_ms += SERVER_STOP_POLL_MS;
264 }
265 
266#undef SERVER_STOP_TIMEOUT_MS
267#undef SERVER_STOP_POLL_MS
268}
269 
270/**
271 * @brief Cleanup server resources
272 *
273 * @note listen_fd may already be closed by ftp_server_stop() — guard
274 * against double-close with the >= 0 check.
275 */
276void ftp_server_cleanup(ftp_server_context_t *ctx)
277{
278 if (ctx == NULL) {
279 return;
280 }
281
282 /* Close listen socket if not already closed by ftp_server_stop() */
283 if (ctx->listen_fd >= 0) {
284 PAL_CLOSE(ctx->listen_fd);
285 ctx->listen_fd = -1;
286 }
287
288 /* Destroy session lock */
289 pthread_mutex_destroy(&ctx->session_lock);
290
291 /* Cleanup network subsystem */
292 pal_network_fini();
293}
294 
295/*===========================================================================*
296 * ACCEPT THREAD
297 *===========================================================================*/
298 
299/**
300 * @brief Accept thread - handles incoming connections
301 */
302static void* server_accept_thread(void *arg)
303{
304 ftp_server_context_t *ctx = (ftp_server_context_t*)arg;
305
306 if (ctx == NULL) {
307 return NULL;
308 }
309
310 while (atomic_load(&ctx->running) != 0) {
311 /* Accept new connection */
312 struct sockaddr_in client_addr;
313 socklen_t addr_len = sizeof(client_addr);
314
315 int client_fd = PAL_ACCEPT(ctx->listen_fd,
316 (struct sockaddr*)&client_addr,
317 &addr_len);
318
319 if (client_fd < 0) {
320 /* Accept failed - check if server is stopping */
321 if (atomic_load(&ctx->running) == 0) {
322 break;
323 }
324
325 continue; /* Try again */
326 }
327
328 /* Configure client socket */
329 (void)pal_socket_configure(client_fd);
330
331 /* Allocate session */
332 ftp_session_t *session = allocate_session(ctx);
333
334 if (session == NULL) {
335 /* No available sessions - reject connection */
336 PAL_CLOSE(client_fd);
337 atomic_fetch_add(&ctx->stats.total_errors, 1U);
338 continue;
339 }
340
341 /* Initialize session */
342 static atomic_uint_fast32_t session_counter = ATOMIC_VAR_INIT(0);
343 uint32_t session_id = atomic_fetch_add(&session_counter, 1U);
344
345 ftp_error_t err = ftp_session_init(session, client_fd, &client_addr,
346 session_id, ctx->root_path);
347
348 if (err != FTP_OK) {
349 PAL_CLOSE(client_fd);
350 free_session(ctx, session);
351 atomic_fetch_add(&ctx->stats.total_errors, 1U);
352 continue;
353 }
354 
355 /*
356 * Store the back-pointer to the owning server context.
357 *
358 * WHY: The session thread calls ftp_server_release_session() on exit
359 * to decrement active_sessions and mark the slot as free. Without
360 * this pointer the thread cannot reach the server context.
361 *
362 * Written here — before pthread_create() — so it is visible to the
363 * new thread without any additional synchronisation (pthread_create
364 * acts as a memory barrier).
365 */
366 session->server_ctx = ctx;
367
368 /* Create session thread */
369 pthread_attr_t sess_attr;
370 int sess_attr_ok = (pthread_attr_init(&sess_attr) == 0);
371 if (sess_attr_ok != 0) {
372 (void)pthread_attr_setstacksize(&sess_attr, (size_t)FTP_THREAD_STACK_SIZE);
373 }
374
375 if (pthread_create(&session->thread, (sess_attr_ok != 0) ? &sess_attr : NULL,
376 ftp_session_thread, session) != 0) {
377 if (sess_attr_ok != 0) {
378 (void)pthread_attr_destroy(&sess_attr);
379 }
380 PAL_CLOSE(client_fd);
381 free_session(ctx, session);
382 atomic_fetch_add(&ctx->stats.total_errors, 1U);
383 continue;
384 }
385
386 if (sess_attr_ok != 0) {
387 (void)pthread_attr_destroy(&sess_attr);
388 }
389
390 /* Detach thread */
391 pthread_detach(session->thread);
392
393 /* Update statistics */
394 atomic_fetch_add(&ctx->stats.total_connections, 1U);
395 atomic_fetch_add(&ctx->active_sessions, 1U);
396 }
397
398 return NULL;
399}
400 
401/*===========================================================================*
402 * SESSION POOL MANAGEMENT
403 *===========================================================================*/
404 
405/**
406 * @brief Allocate session from pool
407 */
408static ftp_session_t* allocate_session(ftp_server_context_t *ctx)
409{
410 if (ctx == NULL) {
411 return NULL;
412 }
413
414 pthread_mutex_lock(&ctx->session_lock);
415
416 /* Find free session slot */
417 ftp_session_t *session = NULL;
418
419 for (size_t i = 0U; i < FTP_MAX_SESSIONS; i++) {
420 int state = atomic_load(&ctx->sessions[i].state);
421
422 if ((state == FTP_STATE_INIT) || (state == FTP_STATE_TERMINATING)) {
423 /* Available slot */
424 session = &ctx->sessions[i];
425 atomic_store(&session->state, FTP_STATE_CONNECTED);
426 break;
427 }
428 }
429
430 pthread_mutex_unlock(&ctx->session_lock);
431
432 return session;
433}
434 
435/**
436 * @brief Free session back to pool (internal error-path helper).
437 *
438 * Used ONLY in server_accept_thread for early error paths — BEFORE
439 * pthread_create() succeeds and BEFORE active_sessions is incremented.
440 *
441 * WHY NO DECREMENT:
442 * active_sessions is incremented at line:
443 * atomic_fetch_add(&ctx->active_sessions, 1U);
444 * which occurs AFTER pthread_create() returns 0. All calls to
445 * free_session() happen in error branches that execute BEFORE that
446 * line (ftp_session_init failure, pthread_create failure).
447 * Decrementing here would underflow the counter from 0 to UINT_FAST32_MAX,
448 * which would permanently prevent ftp_server_stop() from exiting even
449 * after all real sessions have finished.
450 *
451 * The only correct place to decrement active_sessions is
452 * ftp_server_release_session(), which is called from inside
453 * ftp_session_thread() — i.e. after the thread (and the increment) exist.
454 *
455 * @note Thread-safety: Protected by ctx->session_lock
456 */
457static void free_session(ftp_server_context_t *ctx, ftp_session_t *session)
458{
459 if ((ctx == NULL) || (session == NULL)) {
460 return;
461 }
462
463 pthread_mutex_lock(&ctx->session_lock);
464
465 /* Reset slot state so it can be reused — no counter decrement here */
466 atomic_store(&session->state, FTP_STATE_INIT);
467
468 pthread_mutex_unlock(&ctx->session_lock);
469}
470 
471/**
472 * @brief Release a session slot back to the server pool (public API).
473 *
474 * Called by ftp_session_thread() as its very last action, after
475 * ftp_session_cleanup() has closed all file descriptors.
476 *
477 * INVARIANT: active_sessions was already incremented (in server_accept_thread,
478 * after pthread_create() returned 0) before this thread started running.
479 * This function performs the matching decrement. free_session() (the internal
480 * error-path helper) deliberately does NOT decrement — it is only called from
481 * code paths that execute BEFORE the increment.
482 *
483 * @pre Called only from the session's own thread
484 * @pre ftp_session_cleanup() already called (all FDs closed)
485 * @pre ctx->active_sessions > 0
486 */
487void ftp_server_release_session(ftp_server_context_t *ctx,
488 ftp_session_t *session)
489{
490 if ((ctx == NULL) || (session == NULL)) {
491 return;
492 }
493 
494 /*
495 * NULL out the back-pointer before releasing the slot.
496 * Prevents stale pointer access if the slot is immediately reused.
497 */
498 session->server_ctx = NULL;
499 
500 /* Reset slot state and decrement the active-session counter */
501 pthread_mutex_lock(&ctx->session_lock);
502 atomic_store(&session->state, FTP_STATE_INIT);
503 atomic_fetch_sub(&ctx->active_sessions, 1U);
504 pthread_mutex_unlock(&ctx->session_lock);
505}
506 
507/*===========================================================================*
508 * SERVER CONTROL
509 *===========================================================================*/
510 
511/**
512 * @brief Check if server is running
513 */
514int ftp_server_is_running(const ftp_server_context_t *ctx)
515{
516 if (ctx == NULL) {
517 return 0;
518 }
519
520 return atomic_load(&ctx->running);
521}
522 
523/**
524 * @brief Get active session count
525 */
526uint32_t ftp_server_get_active_sessions(const ftp_server_context_t *ctx)
527{
528 if (ctx == NULL) {
529 return 0U;
530 }
531
532 return (uint32_t)atomic_load(&ctx->active_sessions);
533}
534 
535/**
536 * @brief Get server statistics
537 */
538void ftp_server_get_stats(const ftp_server_context_t *ctx,
539 uint64_t *total_conn,
540 uint64_t *bytes_sent,
541 uint64_t *bytes_received)
542{
543 if (ctx == NULL) {
544 return;
545 }
546
547 if (total_conn != NULL) {
548 *total_conn = atomic_load(&ctx->stats.total_connections);
549 }
550
551 if (bytes_sent != NULL) {
552 /* Sum all session statistics */
553 uint64_t total = 0U;
554 for (size_t i = 0U; i < FTP_MAX_SESSIONS; i++) {
555 total += atomic_load(&ctx->sessions[i].stats.bytes_sent);
556 }
557 *bytes_sent = total;
558 }
559
560 if (bytes_received != NULL) {
561 /* Sum all session statistics */
562 uint64_t total = 0U;
563 for (size_t i = 0U; i < FTP_MAX_SESSIONS; i++) {
564 total += atomic_load(&ctx->sessions[i].stats.bytes_received);
565 }
566 *bytes_received = total;
567 }
568}
569