diff --git a/src/app/FTPClient.cpp b/src/app/FTPClient.cpp new file mode 100644 index 0000000..4ff90a0 --- /dev/null +++ b/src/app/FTPClient.cpp @@ -0,0 +1,88 @@ +#include "FTPClient.h" + +FTPClient::FTPClient(WiFiClient client, uint8_t id, uint16_t clientCommandDataBufferSize) : TCPClient(client, id, clientCommandDataBufferSize), +_ftpCommand({'\0'}), +_cmdParameters(NULL), +_loggedIn(false), +_username(NULL), +_currentDirectory(NULL), +_currentFile(NULL), +_fileSentBytes(0), +_waitingForDataConnection(false), +_fileIsBeeingReceived(false), +_ftpClientState(FTPServer::FTPClientState::INIT), +_binaryFlag(FTPServer::BinaryFlag::OFF), +_dataTransferPending(FTPServer::FTPClientDataTransfer::NONE) +{ + +} + +FTPClient::~FTPClient() +{ + delete _cmdParameters; + free(_username); + free(_currentDirectory); + free(_currentFile); + _dataClient.stop(); +} + +void FTPClient::setDataClient(WiFiClient dataClient) +{ + _dataClient = dataClient; +} + +boolean FTPClient::parseCommandAndParameters() +{ + //We remove the cr lf at the end + char *cr = strchr((char *)_data,'\r'); *cr = '\0'; + char *cmdDelimiter = strchr((char *)_data,' '); + + if(cmdDelimiter == NULL) //It means that we do not have any parameters + { + cmdDelimiter = (char *)_data + strlen((char *)_data) - 1; + strcpy(_ftpCommand, (char *)_data); + } + else //we do + { + strncpy(_ftpCommand, (char *)_data, cmdDelimiter - (char *)_data); + _ftpCommand[cmdDelimiter - (char *)_data] = '\0'; // /!\ strncpy does not append the terminating string character + } + + //We get the parameters : + DictionaryHelper::StringEntity params(cmdDelimiter+1); //+1 to skip the first space + delete _cmdParameters; + _cmdParameters = params.split(' '); + + //At the end, we flush the buffer: + freeDataBuffer(_dataSize); +} + +void FTPClient::setUsername(const char *username) +{ + free(_username);_username = NULL; + if(username != NULL) + { + _username = (char *) malloc((sizeof(char) * strlen(username)) + 1); + strcpy(_username, username); + } +} + +void FTPClient::setCurrentDirectory(const char *dir) +{ + free(_currentDirectory);_currentDirectory = NULL; + if(dir != NULL) + { + _currentDirectory = (char *) malloc((sizeof(char) * strlen(dir)) + 1); + strcpy(_currentDirectory, dir); + } +} + +void FTPClient::setCurrentFile(const char *file) +{ + free(_currentFile);_currentFile = NULL; + if(file != NULL) + { + _currentFile = (char *) malloc((sizeof(char) * strlen(file)) + 1); + strcpy(_currentFile, file); + } +} diff --git a/src/app/FTPClient.h b/src/app/FTPClient.h new file mode 100644 index 0000000..38aac89 --- /dev/null +++ b/src/app/FTPClient.h @@ -0,0 +1,40 @@ +#ifndef FTPCLIENT_H +#define FTPCLIENT_H + +#include "TCPClient.h" +#include "FTPServer.h" +#include "Dictionary.h" + +class FTPClient : public TCPClient +{ + template + friend class FTPServer; + public: + FTPClient(WiFiClient client, uint8_t id, uint16_t clientCommandDataBufferSize = 255); + virtual ~FTPClient(); + protected: + private: + FTPClient(const FTPClient &Object) : TCPClient(Object){} + void setDataClient(WiFiClient dataClient); //Also known as the data socket + boolean parseCommandAndParameters(void); + void setUsername(const char *username); + void setCurrentDirectory(const char *dir); + void setCurrentFile(const char *file); + + char _ftpCommand[5]; + Dictionary *_cmdParameters; + boolean _loggedIn; + char *_username; + char *_currentDirectory; + char *_currentFile; + uint64_t _fileSentBytes; + boolean _waitingForDataConnection; + boolean _fileIsBeeingReceived; + + FTPServer::FTPClientState _ftpClientState; + FTPServer::BinaryFlag _binaryFlag; + FTPServer::FTPClientDataTransfer _dataTransferPending; + WiFiClient _dataClient; //data socket +}; + +#endif //FTPCLIENT_H diff --git a/src/app/FTPServer.h b/src/app/FTPServer.h new file mode 100644 index 0000000..038bc8a --- /dev/null +++ b/src/app/FTPServer.h @@ -0,0 +1,578 @@ +#ifndef FTPSERVER_H +#define FTPSERVER_H + +#include "TCPServer.h" +#include "SDCardManager.h" +#include "definition.h" +#define DEBUG_FTPS +#define READ_WRITE_BUFFER_SIZE 2048 + +template +class FTPServer : public TCPServer +{ + public: + enum FTPClientState {INIT, WAITING_FOR_COMMANDS}; + enum FTPClientDataTransfer {NONE, LIST_DF, NLST_DF, RETR_DF, STOR_DF}; + enum FileTransferStatus {OK, NOT_FOUND}; + enum BinaryFlag {OFF = 0, ON}; + + FTPServer(unsigned int port = 21, SDCardManager *sdCardManager = NULL, const char *login = NULL, const char *password = NULL, uint8_t maxClient = MAX_CLIENT, uint16_t clientCommandDataBufferSize = 255) : TCPServer(port, maxClient, clientCommandDataBufferSize), + _login(NULL), + _password(NULL), + _dataPort(1024), + _dataServer(_dataPort), + _sdCardManager(sdCardManager) + { + if (login != NULL) + { + if (strlen(login) > 0) + { + _login = (char *)malloc((sizeof(char) * strlen(login)) + 1); + strcpy(_login, login); + } + } + + if (password != NULL) + { + if (strlen(password) > 0) + { + _password = (char *)malloc((sizeof(char) * strlen(password)) + 1); + strcpy(_password, password); + } + } + } + + void setCustomDataPort(unsigned int port) + { + _dataPort = port; + } + + virtual ~FTPServer() + { + free(_login); free(_password); + } + + protected: + virtual T* createNewClient(WiFiClient wc) + { + return new T(wc, TCPServer::freeClientId(), TCPServer::_clientDataBufferSize); + } + + virtual void greetClient(T *client) + { + //The first time the client connects, we send the server's information + client->_client.println("220 Welcome to the ESP8266SwissArmyBoard embedded FTP server."); + client->_clientState = TCPClient::HANDLED; + } + + virtual void processClientData(T *client) + { + if (client->_waitingForDataConnection) + { + /*#ifdef DEBUG_FTPS + Serial.println("Listening for new data client"); + #endif*/ + WiFiClient dataClient = _dataServer.available(); + + if (dataClient) + { + if (dataClient.connected()) + { + client->_waitingForDataConnection = false; + client->setDataClient(dataClient); + #ifdef DEBUG_FTPS + Serial.println("Data client accepted successfully"); + #endif + } + else + dataClient.stop(); + } + } + + switch(client->_dataTransferPending) + { + case LIST_DF: //We list the files of the current directory + //We check if the dataConnection is established: + if (client->_dataClient.connected()) + { + client->_client.println("150 File status okay;"); + sendFSTree(client); + + client->_client.println("226 Closing data connection."); + client->_dataClient.stop(); + client->_dataTransferPending = NONE; + } + //A timeout should be added + /*else + { + client->_client.println("425 Can't open data connection."); + }*/ + break; + case RETR_DF: + if (client->_dataClient.connected()) + { + if(client->_fileSentBytes == 0) + client->_client.println("150 File status okay;"); + + FileTransferStatus fts; + if(!sendFile(client,&fts))//File was sent or error occured + { + //we check the return code + switch(fts) + { + case OK: + client->_client.println("226 Closing data connection."); + client->_dataClient.stop(); + client->_dataTransferPending = NONE; + break; + case NOT_FOUND: + client->_client.println("451 File not found."); + client->_dataClient.stop(); + client->_dataTransferPending = NONE; + break; + } + } + } + //A timeout should be added + /*else + { + client->_client.println("425 Can't open data connection."); + }*/ + break; + case STOR_DF : + if (client->_dataClient.connected()) + { + char recvBuffer[READ_WRITE_BUFFER_SIZE]; + if(client->_dataClient.available()) + { + uint16_t size = client->_dataClient.read((uint8_t *)recvBuffer, READ_WRITE_BUFFER_SIZE-1); + recvBuffer[size] = '\0'; + Serial.printf("Data : %s\n", recvBuffer); + } + + /*client->_dataClient.stop(); + client->_dataTransferPending = NONE;*/ + client->_fileIsBeeingReceived = true; + } + else if(client->_fileIsBeeingReceived) + { + #ifdef DEBUG_FTPS + Serial.println("Whole file received"); + #endif + + client->_fileIsBeeingReceived = false; + client->_dataClient.stop(); + client->_dataTransferPending = NONE; + } + break; + } + + #ifdef DEBUG_FTPS + if (client->_newDataAvailable) + { + Serial.print("Client --> "); Serial.print(client->_id); Serial.print(" : "); Serial.print((char *)client->_data); Serial.println("#"); + } + #endif + + if (client->_dataSize > 0) + { + switch (client->_ftpClientState) + { + case INIT: + client->setCurrentDirectory(FTP_DIR); + client->_ftpClientState = WAITING_FOR_COMMANDS; + break; + case WAITING_FOR_COMMANDS: + processCommands(client); + break; + } + } + } + + private: + void processCommands(T *client) + { + if (!client->parseCommandAndParameters()) //Failed to retrieve command and parameters + { + //We can close the connection or do other things + return; + } + + #ifdef DEBUG_FTPS + Serial.printf("Issued command : '%s'\n", client->_ftpCommand); + Serial.println("Get params :"); + for (int i = 0; i < client->_cmdParameters->count(); i++) + { + Serial.print(client->_cmdParameters->getParameter(i)); Serial.print(" : "); Serial.printf("'%s'\n", client->_cmdParameters->getAt(i)->getString()); + } + #endif + + if (strcmp(client->_ftpCommand, "USER") == 0) + { + //We check if we set a login and a password : + if (_login != NULL && _password != NULL) + { + if (client->_cmdParameters->count() > 0) + { + client->_client.println("331 User name okay, need password."); + client->setUsername(client->_cmdParameters->getAt(0)->getString()); + } + else + client->_client.println("530 Username required."); + } + else //The ftp access is open + { + client->_client.println("230 User logged in, proceed."); + } + } + else if (strcmp(client->_ftpCommand, "PASS") == 0) + { + //We now check if the username and password correspond + if (client->_cmdParameters->count() > 0) + { + if (strcmp(_login, client->_username) == 0 && strcmp(_password, client->_cmdParameters->getAt(0)->getString()) == 0) + { + client->_loggedIn = true; + client->_client.println("230 User logged in, proceed."); + } + else + { + client->_client.println("530 Wrong username or password !."); + } + } + else + { + client->_client.println("530 Password required."); + } + } + else if (strcmp(client->_ftpCommand, "PWD") == 0) //We set the default directory + { + client->_client.printf("257 \"%s\"\r\n", client->_currentDirectory); + } + else if (strcmp(client->_ftpCommand, "TYPE") == 0) + { + if (client->_cmdParameters->count() > 0) + { + switch (client->_cmdParameters->getAt(0)->getString()[0]) + { + case 'I': + client->_binaryFlag = ON; + client->_client.println("200 Command okay."); + break; + case 'L': + client->_binaryFlag = ON; + client->_client.println("200 Command okay."); + break; + case 'A': + client->_binaryFlag = OFF; + client->_client.println("200 Command okay."); + break; + default: + client->_client.println("504 Command not implemented for TYPE."); + } + } + else + { + client->_client.println("504 Command not implemented for TYPE."); + } + } + else if (strcmp(client->_ftpCommand, "PASV") == 0) + { + _dataServer.begin(_dataPort); + client->_client.printf("227 Entering Passive Mode (%u,%u,%u,%u,%d,%d).\r\n", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3], _dataPort / 256, _dataPort % 256); + client->_waitingForDataConnection = true; + } + else if (strcmp(client->_ftpCommand, "LIST") == 0) + { + //We inform that a data transfer is pending + client->_dataTransferPending = LIST_DF; + } + else if (strcmp(client->_ftpCommand, "CWD") == 0) + { + if (client->_cmdParameters->count() > 0) + { + //Go back one level + if (strcmp(client->_cmdParameters->getAt(0)->getString(), "..") == 0) + { + char *dirCopy = (char *)malloc((sizeof(char) * strlen(client->_currentDirectory)) + 1); + strcpy(dirCopy, client->_currentDirectory); + + char *p = lastIndexOf(dirCopy, '/'); + if (dirCopy == p) + { + *(p + 1) = '\0'; + } + else + *(p) = '\0'; + + #ifdef DEBUG_FTPS + Serial.printf("Final dir : %s\n", dirCopy); + #endif + + client->setCurrentDirectory(dirCopy); + free(dirCopy); + } + else if (strcmp(client->_cmdParameters->getAt(0)->getString(), "/") == 0) + { + client->setCurrentDirectory("/"); + } + else + { + if(client->_cmdParameters->getAt(0)->getString()[0] == '/')//Then this is a name prefix + { + client->setCurrentDirectory(client->_cmdParameters->getAt(0)->getString()); + } + else + { + char *temp = (char *)malloc((sizeof(char) * strlen(client->_currentDirectory)) + (sizeof(char) * strlen(client->_cmdParameters->getAt(0)->getString())) + 2); + strcpy(temp,client->_currentDirectory); + if(strcmp(temp, "/") != 0)strcat(temp,"/"); + strcat(temp,client->_cmdParameters->getAt(0)->getString()); + #ifdef DEBUG_FTPS + Serial.printf("Final dir : %s\n",temp); + #endif + client->setCurrentDirectory(temp); + free(temp); + } + } + + client->_client.println("250 Requested file action okay, completed."); + #ifdef DEBUG_FTPS + Serial.printf("CWD new dir : %s\n", client->_currentDirectory); + #endif + } + } + else if(strcmp(client->_ftpCommand, "RETR") == 0) + { + if (client->_cmdParameters->count() > 0) + { + //We save the file path to be sent + char *temp = (char *)malloc((sizeof(char) * strlen(client->_currentDirectory)) + (sizeof(char) * strlen(client->_cmdParameters->getAt(0)->getString())) + (strcmp(client->_currentDirectory,"/") == 0 ? 0 : 1) + 1); + strcpy(temp, client->_currentDirectory); + if(strcmp(client->_currentDirectory,"/") != 0 )strcat(temp, "/"); + strcat(temp, client->_cmdParameters->getAt(0)->getString()); + + client->setCurrentFile(temp); + #ifdef DEBUG_FTPS + Serial.printf("File to donwload : %s\n", temp); + #endif + free(temp); + client->_dataTransferPending = RETR_DF; + } + } + else if(strcmp(client->_ftpCommand, "MKD") == 0) + { + if (client->_cmdParameters->count() > 0) + { + uint16_t dirNameSize(0), paramCount(client->_cmdParameters->count()); + char *dirName(NULL); + for(int i(0); i < paramCount; i++) + { + dirNameSize += strlen(client->_cmdParameters->getAt(i)->getString()); + + if(i != paramCount-1) + dirNameSize++; + } + + dirName = (char *)malloc((sizeof(char)*dirNameSize) + 1); + if(dirName != NULL) + { + dirName[0] = '\0'; + + for(int i(0); i < paramCount; i++) + { + strcat(dirName,client->_cmdParameters->getAt(i)->getString()); + + if(i != paramCount-1) + strcat(dirName," "); + } + + #ifdef DEBUG_FTPS + Serial.printf("dirName size : %d, dirName #%s#\n",dirNameSize,dirName); + #endif + + if(strlen(dirName) > 8) + dirName[8] = '\0'; + + //We have the dir name, we need to append the current directory... + uint16_t pathDirNameLength = strlen(dirName) + strlen(client->_currentDirectory) + /*If we need to add a /*/ (strcmp(client->_currentDirectory,"/") == 0 ? 0 : 1) + 1; //for \0 + char *pathWithDirName = (char *)malloc(sizeof(char) * pathDirNameLength); + + sprintf(pathWithDirName,"%s%s%s",client->_currentDirectory, strcmp(client->_currentDirectory,"/") == 0 ? "" : "/", dirName); + + #ifdef DEBUG_FTPS + Serial.printf("Final dirName and path #%s# #%s# size %d\n",dirName, pathWithDirName, pathDirNameLength); + #endif + + if(_sdCardManager->mkdir(strupr(pathWithDirName))) + { + client->_client.printf("257 \"%s\"\r\n", pathWithDirName); + } + else + client->_client.println("550 Failed to mkdir (no spaces allowed in dir name)."); + free(pathWithDirName); + free(dirName); + } + else + { + client->_client.println("550 Requested action not taken."); + } + } + else + { + client->_client.println("550 Requested action not taken."); + } + } + else if(strcmp(client->_ftpCommand, "RMD") == 0) + { + if (client->_cmdParameters->count() > 0) + { + //We have the dir name, we need to append the current directory... + uint16_t pathDirNameLength = strlen(client->_cmdParameters->getAt(0)->getString()) + strlen(client->_currentDirectory) + /*If we need to add a /*/ (strcmp(client->_currentDirectory,"/") == 0 ? 0 : 1) + 1; //for \0 + char *pathWithDirName = (char *)malloc(sizeof(char) * pathDirNameLength); + sprintf(pathWithDirName,"%s%s%s",client->_currentDirectory, strcmp(client->_currentDirectory,"/") == 0 ? "" : "/", client->_cmdParameters->getAt(0)->getString()); + + #ifdef DEBUG_FTPS + Serial.printf("pathDirName to delete : #%s#\n",pathWithDirName); + #endif + + if(_sdCardManager->rmdir(pathWithDirName)) + { + client->_client.println("250 Requested file action okay."); + } + else + { + client->_client.println("550 Requested action not taken."); + } + + free(pathWithDirName); + } + else + { + client->_client.println("550 Requested action not taken."); + } + } + else if(strcmp(client->_ftpCommand, "STOR") == 0) + { + if (client->_cmdParameters->count() > 0) + { + //We save the file path to be sent + char *temp = (char *)malloc((sizeof(char) * strlen(client->_currentDirectory)) + (sizeof(char) * strlen(client->_cmdParameters->getAt(0)->getString())) + (strcmp(client->_currentDirectory,"/") == 0 ? 0 : 1) + 1); + strcpy(temp, client->_currentDirectory); + if(strcmp(client->_currentDirectory,"/") != 0 )strcat(temp, "/"); + strcat(temp, client->_cmdParameters->getAt(0)->getString()); + + client->setCurrentFile(temp); + #ifdef DEBUG_FTPS + Serial.printf("File to store : %s\n", temp); + #endif + free(temp); + client->_dataTransferPending = STOR_DF; + } + } + else if(strcmp(client->_ftpCommand, "SYST") == 0) + { + client->_client.println("215 UNIX Type: L8"); + } + else + client->_client.println("502 Command not implemented."); + } + + //Here we send the fs tree to the ftp client + void sendFSTree(T *client) + { + if (client->_currentDirectory != NULL) + { + #ifdef DEBUG_FTPS + Serial.printf("Directory : %s\n",client->_currentDirectory); + #endif + File currentDirectory = _sdCardManager->open(client->_currentDirectory); + currentDirectory.rewindDirectory(); + if (currentDirectory) + { + while (true) //Maybe be remove in the future to improve responsiveness + { + File fileOrDir = currentDirectory.openNextFile(); + + if (!fileOrDir) //No more files in the directory + break; + + #ifdef DEBUG_FTPS + Serial.printf("Filename : %s\n", fileOrDir.name()); + #endif + + client->_dataClient.printf("%crwxrwxrwx 1 owner esp8266 %d Aug 26 16:31 %s\r\n", fileOrDir.isDirectory() ? 'd' : '-', fileOrDir.isDirectory() ? 0 : fileOrDir.size(), fileOrDir.name()); + + fileOrDir.close(); + } + + currentDirectory.close(); + } + else //Failed to open directory + { + #ifdef DEBUG_FTPS + Serial.println("Failed to open directory"); + #endif + } + } + } + + //The binary flag needs to be taken into consideration + boolean sendFile(T *client, FileTransferStatus *fts) + { + if (client->_currentFile != NULL) + { + char sendBuffer[READ_WRITE_BUFFER_SIZE]; + File fileToSend = _sdCardManager->open(client->_currentFile); + + if (fileToSend) + { + *fts = OK; + unsigned int readBytes(0); + fileToSend.seek(client->_fileSentBytes); + + if(fileToSend.available()) + { + readBytes = fileToSend.read(sendBuffer, READ_WRITE_BUFFER_SIZE); + client->_dataClient.write(sendBuffer, readBytes); + + client->_fileSentBytes += readBytes; + + #ifdef DEBUG_FTPS + Serial.printf("File : bytes sent : %u\n",readBytes); + #endif + } + else //The whole file has been sent + { + fileToSend.close(); + client->_fileSentBytes = 0; + return false; + } + + fileToSend.close(); + } + else //Failed to open file maybe not found + { + #ifdef DEBUG_FTPS + Serial.println("Failed to open file"); + #endif + *fts = NOT_FOUND; + return false; + } + + return true;//Still things to send + } + + *fts = NOT_FOUND; + return false; + } + + char *_login; + char *_password; + unsigned int _dataPort; + + WiFiServer _dataServer; //In passive mode, the FTP server opens two different ports (one for the commands and the other for the data stream) + SDCardManager *_sdCardManager; +}; + +#endif //FTPSERVER_H diff --git a/src/app/SAB.cpp b/src/app/SAB.cpp index 96e8ea0..50e0d99 100644 --- a/src/app/SAB.cpp +++ b/src/app/SAB.cpp @@ -9,6 +9,7 @@ _rtcManager(_rtc), _sdCardManager(), _connectivityManager(NULL), //_webServerManager(80, &_sdCardManager), _webServer(80, &_sdCardManager), +_ftpServer(21, &_sdCardManager, "ESP8266", "12345678"), _pcf(0x27, Wire), _ioManager(_pcf), _taskSchedulerManager(_rtcManager), @@ -40,6 +41,7 @@ _sdCardManager(), _connectivityManager(NULL), //_webServerManager(webServerPort, &_sdCardManager), _webServer(webServerPort, &_sdCardManager), +_ftpServer(21, &_sdCardManager, "ESP8266", "12345678"), _pcf(0x27, Wire), _ioManager(_pcf), _taskSchedulerManager(_rtcManager), @@ -94,6 +96,11 @@ WEBServer& SAB::getWebServer() return _webServer; } +FTPServer& SAB::getFtpServer() +{ + return _ftpServer; +} + IOManager& SAB::getIoManager() { return _ioManager; diff --git a/src/app/SAB.h b/src/app/SAB.h index e306f79..54ca819 100644 --- a/src/app/SAB.h +++ b/src/app/SAB.h @@ -9,6 +9,7 @@ #include "ConnectivityManager.h" //#include "WEBServerManager.h" #include "WEBClient.h" //includes WEBServer internally +#include "FTPClient.h" //includes FTPServer internally #include "IOManager.h" #include "TaskSchedulerManager.h" #include "PowerManager.h" @@ -35,6 +36,7 @@ class SAB ConnectivityManager& getConnectivityManager(); //WEBServerManager& getWebServerManager(); WEBServer& getWebServer(); + FTPServer& getFtpServer(); IOManager& getIoManager(); TaskSchedulerManager& getTaskSchedulerManager(); PowerManager& getPowerManager(); @@ -57,6 +59,7 @@ class SAB ConnectivityManager *_connectivityManager; //WEBServerManager _webServerManager; WEBServer _webServer; + FTPServer _ftpServer; PCF8574 _pcf; IOManager _ioManager; TaskSchedulerManager _taskSchedulerManager; diff --git a/src/app/app.ino b/src/app/app.ino index c39cbb6..b6db938 100644 --- a/src/app/app.ino +++ b/src/app/app.ino @@ -120,6 +120,7 @@ void loop() //Run the webServer sab.getWebServer().runServer(); + sab.getFtpServer().runServer(); sab.getTaskSchedulerManager().runTaskScheduler(); } diff --git a/src/app/versions.h b/src/app/versions.h index 0cfb552..f339d6c 100644 --- a/src/app/versions.h +++ b/src/app/versions.h @@ -18,5 +18,6 @@ #define SOFT_VERSION "1.3.0" //Implemented multi-client non blocking webserver #define SOFT_VERSION "1.3.1" //Fixed sdCardUnmount api call #define SOFT_VERSION "1.3.2" //Modified TCPServer and WEBServer core logic +#define SOFT_VERSION "1.4.0" //Added the new FTPServer #endif //VERSIONS_H