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 | /** |
| 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 */ |
| 43 | static void* server_accept_thread(void *arg); |
| 44 | static ftp_session_t* allocate_session(ftp_server_context_t *ctx); |
| 45 | static 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 | */ |
| 54 | ftp_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 | */ |
| 151 | ftp_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 | */ |
| 220 | void 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 | */ |
| 276 | void 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 | */ |
| 302 | static 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 | */ |
| 408 | static 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 | */ |
| 457 | static 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 | */ |
| 487 | void 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 | */ |
| 514 | int 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 | */ |
| 526 | uint32_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 | */ |
| 538 | void 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 |