/**
 * @file ftp_server.c
 * @brief FTP server (File Transfer Protocol)
 *
 * @section License
 *
 * Copyright (C) 2021-2026 Oryx Embedded SARL. All rights reserved.
 *
 * This file is part of CycloneTCP Open
 * 
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

 *
 * @author Oryx Embedded SARL (www.oryx-embedded.com)
 * @version 2.6.4
 **/

//Switch to the appropriate trace level
#define TRACE_LEVEL FTP_TRACE_LEVEL

//Dependencies
#include "core/net.h"
#include "ftp/ftp_server.h"
#include "ftp/ftp_server_control.h"
#include "ftp/ftp_server_data.h"
#include "ftp/ftp_server_misc.h"
#include "path.h"
#include "debug.h"

//Check TCP/IP stack configuration
#if (FTP_SERVER_SUPPORT == ENABLED)


/**
 * @brief Initialize settings with default values
 * @param[out] settings Structure that contains FTP server settings
 **/

void ftpServerGetDefaultSettings(FtpServerSettings *settings)
{
   //Default task parameters
   settings->task = OS_TASK_DEFAULT_PARAMS;
   settings->task.stackSize = FTP_SERVER_STACK_SIZE;
   settings->task.priority = FTP_SERVER_PRIORITY;

   //TCP/IP stack context
   settings->netContext = NULL;
   //The FTP server is not bound to any interface
   settings->interface = NULL;

   //FTP command port number
   settings->port = FTP_PORT;
   //FTP data port number
   settings->dataPort = FTP_DATA_PORT;

   //Passive port range
   settings->passivePortMin = FTP_SERVER_PASSIVE_PORT_MIN;
   settings->passivePortMax = FTP_SERVER_PASSIVE_PORT_MAX;

   //Public IPv4 address to be used in PASV replies
   settings->publicIpv4Addr = IPV4_UNSPECIFIED_ADDR;

   //Default security mode (no security)
   settings->mode = FTP_SERVER_MODE_PLAINTEXT;

   //Client connections
   settings->maxConnections = 0;
   settings->connections = NULL;

   //Set root directory
   settings->rootDir = NULL;

   //Connection callback function
   settings->connectCallback = NULL;
   //Disconnection callback function
   settings->disconnectCallback = NULL;

#if (FTP_SERVER_TLS_SUPPORT == ENABLED)
   //TLS initialization callback function
   settings->tlsInitCallback = NULL;
#endif

   //User verification callback function
   settings->checkUserCallback = NULL;
   //Password verification callback function
   settings->checkPasswordCallback = NULL;
   //Callback used to retrieve file permissions
   settings->getFilePermCallback = NULL;
   //Unknown command callback function
   settings->unknownCommandCallback = NULL;
}


/**
 * @brief FTP server initialization
 * @param[in] context Pointer to the FTP server context
 * @param[in] settings FTP server specific settings
 * @return Error code
 **/

error_t ftpServerInit(FtpServerContext *context,
   const FtpServerSettings *settings)
{
   error_t error;
   uint_t i;

   //Debug message
   TRACE_INFO("Initializing FTP server...\r\n");

   //Ensure the parameters are valid
   if(context == NULL || settings == NULL)
      return ERROR_INVALID_PARAMETER;

   //Sanity check
   if(settings->passivePortMax <= settings->passivePortMin)
   {
      return ERROR_INVALID_PARAMETER;
   }

   //Invalid number of client connections?
   if(settings->connections == NULL || settings->maxConnections < 1 ||
      settings->maxConnections > FTP_SERVER_MAX_CONNECTIONS)
   {
      return ERROR_INVALID_PARAMETER;
   }

   //Invalid root directory?
   if(settings->rootDir == NULL ||
      osStrlen(settings->rootDir) > FTP_SERVER_MAX_ROOT_DIR_LEN)
   {
      return ERROR_INVALID_PARAMETER;
   }

   //Initialize status code
   error = NO_ERROR;

   //Clear the FTP server context
   osMemset(context, 0, sizeof(FtpServerContext));

   //Initialize task parameters
   context->taskParams = settings->task;
   context->taskId = OS_INVALID_TASK_ID;

   //Attach TCP/IP stack context
   if(settings->netContext != NULL)
   {
      context->netContext = settings->netContext;
   }
   else if(settings->interface != NULL)
   {
      context->netContext = settings->interface->netContext;
   }
   else
   {
      context->netContext = netGetDefaultContext();
   }

   //Save user settings
   context->interface = settings->interface;
   context->port = settings->port;
   context->dataPort = settings->dataPort;
   context->passivePortMin = settings->passivePortMin;
   context->passivePortMax = settings->passivePortMax;
   context->publicIpv4Addr = settings->publicIpv4Addr;
   context->mode = settings->mode;
   context->maxConnections = settings->maxConnections;
   context->connections = settings->connections;
   context->connectCallback = settings->connectCallback;
   context->disconnectCallback = settings->disconnectCallback;
   context->checkUserCallback = settings->checkUserCallback;
   context->checkPasswordCallback = settings->checkPasswordCallback;
   context->getFilePermCallback = settings->getFilePermCallback;
   context->unknownCommandCallback = settings->unknownCommandCallback;

#if (FTP_SERVER_TLS_SUPPORT == ENABLED)
   //TLS initialization callback
   context->tlsInitCallback = settings->tlsInitCallback;
#endif

   //Set root directory
   osStrcpy(context->rootDir, settings->rootDir);

   //Clean the root directory path
   pathCanonicalize(context->rootDir);
   pathRemoveSlash(context->rootDir);

   //Loop through client connections
   for(i = 0; i < context->maxConnections; i++)
   {
      //Initialize the structure representing the client connection
      osMemset(&context->connections[i], 0, sizeof(FtpClientConnection));
   }

   //Create an event object to poll the state of sockets
   if(!osCreateEvent(&context->event))
   {
      //Failed to create event
      error = ERROR_OUT_OF_RESOURCES;
   }

#if (FTP_SERVER_TLS_SUPPORT == ENABLED && TLS_TICKET_SUPPORT == ENABLED)
   //Check status code
   if(!error)
   {
      //Initialize ticket encryption context
      error = tlsInitTicketContext(&context->tlsTicketContext);
   }
#endif

   //Any error to report?
   if(error)
   {
      //Clean up side effects
      ftpServerDeinit(context);
   }

   //Return status code
   return error;
}


/**
 * @brief Start FTP server
 * @param[in] context Pointer to the FTP server context
 * @return Error code
 **/

error_t ftpServerStart(FtpServerContext *context)
{
   error_t error;

   //Make sure the FTP server context is valid
   if(context == NULL)
      return ERROR_INVALID_PARAMETER;

   //Debug message
   TRACE_INFO("Starting FTP server...\r\n");

   //Make sure the FTP server is not already running
   if(context->running)
      return ERROR_ALREADY_RUNNING;

   //Start of exception handling block
   do
   {
      //Open a TCP socket
      context->socket = socketOpenEx(context->netContext, SOCKET_TYPE_STREAM,
         SOCKET_IP_PROTO_TCP);
      //Failed to open socket?
      if(context->socket == NULL)
      {
         //Report an error
         error = ERROR_OPEN_FAILED;
         break;
      }

      //Force the socket to operate in non-blocking mode
      error = socketSetTimeout(context->socket, 0);
      //Any error to report?
      if(error)
         break;

      //Adjust the size of the TX buffer
      error = socketSetTxBufferSize(context->socket,
         FTP_SERVER_MIN_TCP_BUFFER_SIZE);
      //Any error to report?
      if(error)
         break;

      //Adjust the size of the RX buffer
      error = socketSetRxBufferSize(context->socket,
         FTP_SERVER_MIN_TCP_BUFFER_SIZE);
      //Any error to report?
      if(error)
         break;

      //Associate the socket with the relevant interface
      error = socketBindToInterface(context->socket, context->interface);
      //Any error to report?
      if(error)
         break;

      //The FTP server listens for connection requests on port 21
      error = socketBind(context->socket, &IP_ADDR_ANY, context->port);
      //Any error to report?
      if(error)
         break;

      //Place socket in listening state
      error = socketListen(context->socket, FTP_SERVER_BACKLOG);
      //Any failure to report?
      if(error)
         break;

      //Start the FTP server
      context->stop = FALSE;
      context->running = TRUE;

      //Create a task
      context->taskId = osCreateTask("FTP Server", (OsTaskCode) ftpServerTask,
         context, &context->taskParams);

      //Failed to create task?
      if(context->taskId == OS_INVALID_TASK_ID)
      {
         //Report an error
         error = ERROR_OUT_OF_RESOURCES;
         break;
      }

      //End of exception handling block
   } while(0);

   //Any error to report?
   if(error)
   {
      //Clean up side effects
      context->running = FALSE;

      //Close listening socket
      socketClose(context->socket);
      context->socket = NULL;
   }

   //Return status code
   return error;
}


/**
 * @brief Stop FTP server
 * @param[in] context Pointer to the FTP server context
 * @return Error code
 **/

error_t ftpServerStop(FtpServerContext *context)
{
   uint_t i;

   //Make sure the FTP server context is valid
   if(context == NULL)
      return ERROR_INVALID_PARAMETER;

   //Debug message
   TRACE_INFO("Stopping FTP server...\r\n");

   //Check whether the FTP server is running
   if(context->running)
   {
#if (NET_RTOS_SUPPORT == ENABLED)
      //Stop the FTP server
      context->stop = TRUE;
      //Send a signal to the task to abort any blocking operation
      osSetEvent(&context->event);

      //Wait for the task to terminate
      while(context->running)
      {
         osDelayTask(1);
      }
#endif

      //Loop through the connection table
      for(i = 0; i < context->maxConnections; i++)
      {
         //Close client connection
         ftpServerCloseConnection(&context->connections[i]);
      }

      //Close listening socket
      socketClose(context->socket);
      context->socket = NULL;
   }

   //Successful processing
   return NO_ERROR;
}


/**
 * @brief Set user's root directory
 * @param[in] connection Pointer to the client connection
 * @param[in] rootDir NULL-terminated string specifying the root directory
 * @return Error code
 **/

error_t ftpServerSetRootDir(FtpClientConnection *connection,
   const char_t *rootDir)
{
   FtpServerContext *context;

   //Check parameters
   if(connection == NULL || rootDir == NULL)
      return ERROR_INVALID_PARAMETER;

   //Point to the FTP server context
   context = connection->context;

   //Set user's root directory
   pathCopy(connection->rootDir, context->rootDir, FTP_SERVER_MAX_ROOT_DIR_LEN);
   pathCombine(connection->rootDir, rootDir, FTP_SERVER_MAX_ROOT_DIR_LEN);

   //Clean the resulting path
   pathCanonicalize(connection->rootDir);
   pathRemoveSlash(connection->rootDir);

   //Set default user's home directory
   pathCopy(connection->currentDir, connection->rootDir,
      FTP_SERVER_MAX_PATH_LEN);

   //Successful processing
   return NO_ERROR;
}


/**
 * @brief Set user's home directory
 * @param[in] connection Pointer to the client connection
 * @param[in] homeDir NULL-terminated string specifying the home directory
 * @return Error code
 **/

error_t ftpServerSetHomeDir(FtpClientConnection *connection,
   const char_t *homeDir)
{
   FtpServerContext *context;

   //Check parameters
   if(connection == NULL || homeDir == NULL)
      return ERROR_INVALID_PARAMETER;

   //Point to the FTP server context
   context = connection->context;

   //Set user's home directory
   pathCopy(connection->currentDir, context->rootDir, FTP_SERVER_MAX_PATH_LEN);
   pathCombine(connection->currentDir, homeDir, FTP_SERVER_MAX_PATH_LEN);

   //Clean the resulting path
   pathCanonicalize(connection->currentDir);
   pathRemoveSlash(connection->currentDir);

   //Successful processing
   return NO_ERROR;
}


/**
 * @brief FTP server task
 * @param[in] context Pointer to the FTP server context
 **/

void ftpServerTask(FtpServerContext *context)
{
   error_t error;
   uint_t i;
   systime_t time;
   systime_t timeout;
   FtpClientConnection *connection;

#if (NET_RTOS_SUPPORT == ENABLED)
   //Task prologue
   osEnterTask();

   //Process events
   while(1)
   {
#endif
   //Set polling timeout
   timeout = FTP_SERVER_TICK_INTERVAL;

   //Clear event descriptor set
   osMemset(context->eventDesc, 0, sizeof(context->eventDesc));

   //Specify the events the application is interested in
   for(i = 0; i < context->maxConnections; i++)
   {
      //Point to the structure describing the current connection
      connection = &context->connections[i];

      //Check whether the control connection is active
      if(connection->controlChannel.socket != NULL)
      {
         //Register the events related to the control connection
         ftpServerRegisterControlChannelEvents(connection,
            &context->eventDesc[2 * i]);

         //Check whether the socket is ready for I/O operation
         if(context->eventDesc[2 * i].eventFlags != 0)
         {
            //No need to poll the underlying socket for incoming traffic
            timeout = 0;
         }
      }

      //Check whether the data connection is active
      if(connection->dataChannel.socket != NULL)
      {
         //Register the events related to the data connection
         ftpServerRegisterDataChannelEvents(connection,
            &context->eventDesc[2 * i + 1]);

         //Check whether the socket is ready for I/O operation
         if(context->eventDesc[2 * i + 1].eventFlags != 0)
         {
            //No need to poll the underlying socket for incoming traffic
            timeout = 0;
         }
      }
   }

   //Accept connection request events
   context->eventDesc[2 * i].socket = context->socket;
   context->eventDesc[2 * i].eventMask = SOCKET_EVENT_RX_READY;

   //Wait for one of the set of sockets to become ready to perform I/O
   error = socketPoll(context->eventDesc, 2 * context->maxConnections + 1,
      &context->event, timeout);

   //Get current time
   time = osGetSystemTime();

   //Check status code
   if(error == NO_ERROR || error == ERROR_TIMEOUT ||
      error == ERROR_WAIT_CANCELED)
   {
      //Stop request?
      if(context->stop)
      {
         //Stop FTP server operation
         context->running = FALSE;
         //Task epilogue
         osExitTask();
         //Kill ourselves
         osDeleteTask(OS_SELF_TASK_ID);
      }

      //Event-driven processing
      for(i = 0; i < context->maxConnections; i++)
      {
         //Point to the structure describing the current connection
         connection = &context->connections[i];

         //Check whether the control connection is active
         if(connection->controlChannel.socket != NULL)
         {
            //Check whether the control socket is to ready to perform I/O
            if(context->eventDesc[2 * i].eventFlags)
            {
               //Update time stamp
               connection->timestamp = time;

               //Control connection event handler
               ftpServerProcessControlChannelEvents(connection,
                  context->eventDesc[2 * i].eventFlags);
            }
         }

         //Check whether the data connection is active
         if(connection->dataChannel.socket != NULL)
         {
            //Check whether the data socket is ready to perform I/O
            if(context->eventDesc[2 * i + 1].eventFlags)
            {
               //Update time stamp
               connection->timestamp = time;

               //Data connection event handler
               ftpServerProcessDataChannelEvents(connection,
                  context->eventDesc[2 * i + 1].eventFlags);
            }
         }
      }

      //Check the state of the listening socket
      if(context->eventDesc[2 * i].eventFlags & SOCKET_EVENT_RX_READY)
      {
         //Accept connection request
         ftpServerAcceptControlChannel(context);
      }
   }

   //Handle periodic operations
   ftpServerTick(context);

#if (NET_RTOS_SUPPORT == ENABLED)
}
#endif
}


/**
 * @brief Release FTP server context
 * @param[in] context Pointer to the FTP server context
 **/

void ftpServerDeinit(FtpServerContext *context)
{
   //Make sure the FTP server context is valid
   if(context != NULL)
   {
      //Free previously allocated resources
      osDeleteEvent(&context->event);

#if (FTP_SERVER_TLS_SUPPORT == ENABLED && TLS_TICKET_SUPPORT == ENABLED)
      //Release ticket encryption context
      tlsFreeTicketContext(&context->tlsTicketContext);
#endif

      //Clear FTP server context
      osMemset(context, 0, sizeof(FtpServerContext));
   }
}

#endif
