#ifndef FTPSERVER_H #define FTPSERVER_H #include "TCPServer.h" #include "SDCardManager.h" #include "definition.h" #include "Dictionary.h" //#define DEBUG_FTPS #define READ_BUFFER_SIZE 2500 //2500 is max to read sd card, more will crash template class FTPServer : public TCPServer { public: enum FTPClientState {INIT, WAITING_FOR_COMMANDS}; enum FTPClientDataTransfer {NONE = 0, LIST_DF, NLST_DF, RETR_DF, STOR_DF, APPE_DF}; enum FileTransferStatus {OK, NOT_FOUND, NO_FILE_NAME}; enum BinaryFlag {OFF = 0, ON}; enum FtpMsgCode {_150, _200, _215, _220, _221, _230, _226, _227, _250, _257, _331, _350, _451, _5_502, _504, _530, _550 }; 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); } } _dataServer.begin(_dataPort); } 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; } ICACHE_RAM_ATTR 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) { #ifdef DEBUG_FTPS Serial.println("Data client returns true"); #endif 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: if (client->_dataClient.connected()) { #ifdef DEBUG_FTPS Serial.println("Listing started"); #endif client->_client.println("150 File status okay."); if(sendFSTree(client)) { client->_client.println("226 Closing data connection."); } else { client->_client.println("451 Requested action aborted."); } client->closeDataConnection(); client->_dataTransferPending = NONE; } 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 if(fts == OK) { client->_client.println("226 Closing data connection."); client->closeDataConnection(); client->_dataTransferPending = NONE; } else if(fts == NOT_FOUND) { client->_client.println("451 File not found."); client->closeDataConnection(); client->_dataTransferPending = NONE; } } } else if(client->_fileSentBytes != 0) { client->_client.println("426 Connection closed; transfer aborted."); client->closeDataConnection(); client->_dataTransferPending = NONE; } break; case STOR_DF: if (client->_dataClient.connected() || client->_dataClient.available())//Here we need to check if client has some data available for reading. IMPORTANT { if(client->_dataClient.available()) { client->_fileIsBeeingReceived = true; #ifdef DEBUG_FTPS Serial.printf("receiving file %s\n", client->_currentFile); #endif FileTransferStatus fts = OK; if(!writeToSdCard(client, &fts)) //An error occured { if(fts == NO_FILE_NAME) { client->_client.println("501 No file name given."); } else { client->_client.println("451 Requested action aborted: local error in processing."); } client->closeDataConnection(); client->_fileIsBeeingReceived = false; client->_dataTransferPending = NONE; } } } //If no data connection exists and no error was raised during writting, then it could be just a file creation with no data connection opened else if(client->_fileIsBeeingReceived || (client->_dataClientConnected || millis() - client->_actionTimeout > 5000)) { if(client->_fileRecvBytes == 0) //File was just created empty { if(_sdCardManager->exists(client->_currentFile)) _sdCardManager->remove(client->_currentFile); File file2create = _sdCardManager->open(client->_currentFile, FILE_WRITE); file2create.close(); #ifdef DEBUG_FTPS Serial.println("File just created"); #endif } else { #ifdef DEBUG_FTPS Serial.println("Whole file received"); #endif } client->_client.println("226 Closing data connection."); client->_fileIsBeeingReceived = false; client->closeDataConnection(); client->_dataTransferPending = NONE; client->_fileRecvBytes = 0; } break; case APPE_DF: if (client->_dataClient.connected() || client->_dataClient.available())//Here we need to check if client has some data for reading. IMPORTANT { if(client->_dataClient.available()) { client->_fileIsBeeingReceived = true; #ifdef DEBUG_FTPS Serial.printf("appending to file %s\n", client->_currentFile); #endif FileTransferStatus fts = OK; if(!writeToSdCard(client, &fts, true)) //An error occured { if(fts == NO_FILE_NAME) { client->_client.println("501 No file name given."); } else { client->_client.println("451 Requested action aborted: local error in processing."); } client->closeDataConnection(); client->_fileIsBeeingReceived = false; client->_dataTransferPending = NONE; } } } //If the connection is closed and data has been received, then we got the whole file else if(client->_fileIsBeeingReceived) { #ifdef DEBUG_FTPS Serial.println("Whole file received"); #endif client->_client.println("226 Closing data connection."); client->_fileIsBeeingReceived = false; client->closeDataConnection(); client->_dataTransferPending = NONE; client->_fileRecvBytes = 0; } 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 WAITING_FOR_COMMANDS: processCommands(client); break; case INIT: client->setCurrentDirectory(FTP_DIR); client->_ftpClientState = WAITING_FOR_COMMANDS; 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 sendInfoResponse(_530, client);//client->_client.println("530 Username required."); } else //The ftp access is open { client->_loggedIn = true; 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 { sendInfoResponse(_530, client, "Wrong username or password !");//client->_client.println("530 Wrong username or password !."); } } else { sendInfoResponse(_530, client,"");//client->_client.println("530 Password required."); } } else if (strcmp(client->_ftpCommand, "PWD") == 0) //We set the default directory { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } client->_client.printf("257 \"%s\"\r\n", client->_currentDirectory); } else if (strcmp(client->_ftpCommand, "TYPE") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { switch (client->_cmdParameters->getAt(0)->getString()[0]) { case 'I': client->_binaryFlag = ON; sendInfoResponse(_200, client,"");//client->_client.println("200 Command okay."); break; case 'L': client->_binaryFlag = ON; sendInfoResponse(_200, client,"");//client->_client.println("200 Command okay."); break; case 'A': client->_binaryFlag = OFF; sendInfoResponse(_200, client,"");//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) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } //We need to test if we are in softAP or STA to chose the right IP uint8_t addr[4] = {0}; if(WiFi.status() == WL_CONNECTED) { addr[0] = WiFi.localIP()[0]; addr[1] = WiFi.localIP()[1]; addr[2] = WiFi.localIP()[2]; addr[3] = WiFi.localIP()[3]; }else { addr[0] = WiFi.softAPIP()[0]; addr[1] = WiFi.softAPIP()[1]; addr[2] = WiFi.softAPIP()[2]; addr[3] = WiFi.softAPIP()[3]; } client->_client.printf("227 Entering Passive Mode (%u,%u,%u,%u,%d,%d).\r\n", addr[0], addr[1], addr[2], addr[3], _dataPort / 256, _dataPort % 256); #ifdef DEBUG_FTPS Serial.println("Opening data server for new data client"); #endif //We listen for 100 ms directly to accept the client uint64_t timeOut(millis()); while(true) { WiFiClient dataClient = _dataServer.available(); //Serial.printf("Client state : %d\n", dataClient.status()); if (dataClient) { //Serial.printf("Data client is true , available : %d\n", dataClient.available()); if (dataClient.connected())//Connected returns true if the status() is ESTABLISHED or available() returns true { client->setDataClient(dataClient); #ifdef DEBUG_FTPS Serial.println("Data client accepted from loop"); #endif break; } else dataClient.stop(); } if(millis() - timeOut > 100) { client->_waitingForDataConnection = true; break; } yield(); } } else if (strcmp(client->_ftpCommand, "LIST") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } //We inform that a data transfer is pending client->_dataTransferPending = LIST_DF; } else if (strcmp(client->_ftpCommand, "CWD") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } 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 the client already nows the path, he will send it with a / { if(client->_cmdParameters->getAt(0)->getString()[0] == '/')//Then this is a name prefix { char *fullDirPath = constructFileNameWithPath("",client->_cmdParameters); client->setCurrentDirectory(fullDirPath); free(fullDirPath); } else { //Directories with spaces are now working: char *directoryFullpath = constructFileNameWithPath(client->_currentDirectory,client->_cmdParameters); #ifdef DEBUG_FTPS Serial.printf("Final dir : %s\n", directoryFullpath); #endif client->setCurrentDirectory(directoryFullpath); free(directoryFullpath); } } 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->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { //We save the file path to be sent char *file2store(NULL); if(client->_cmdParameters->getAt(0)->getString()[0] == '/') file2store = constructFileNameWithPath("", client->_cmdParameters); else file2store = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); #ifdef DEBUG_FTPS Serial.printf("Final file path : %s\n",file2store); #endif client->setCurrentFile(file2store); free(file2store); client->_dataTransferPending = RETR_DF; } } else if(strcmp(client->_ftpCommand, "MKD") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { char *dirNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); if(dirNameWithPath != NULL) { #ifdef DEBUG_FTPS Serial.printf("Final dirName : #%s#\n",dirNameWithPath); #endif if(_sdCardManager->mkdir(dirNameWithPath)) { client->_client.printf("257 \"%s\"\r\n", dirNameWithPath); } else client->_client.println("550 Failed to mkdir (no spaces allowed in dir name)."); free(dirNameWithPath); } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } } else if(strcmp(client->_ftpCommand, "RMD") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { //We have the dir name, we need to append the current directory... char *dirNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); #ifdef DEBUG_FTPS Serial.printf("pathDirName to delete : #%s#\n",dirNameWithPath); #endif if(_sdCardManager->rmdir(dirNameWithPath)) { client->_client.println("250 Requested file action okay."); } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } free(dirNameWithPath); } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } } else if(strcmp(client->_ftpCommand, "STOR") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { client->_client.printf_P(PSTR("150 File status okay; about to open data connection.\r\n")); //We save the file path to be sent char *fileNameWithPath(NULL); if(client->_cmdParameters->getAt(0)->getString()[0] == '/') fileNameWithPath = constructFileNameWithPath("", client->_cmdParameters); else fileNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); client->setCurrentFile(fileNameWithPath); #ifdef DEBUG_FTPS Serial.printf("File to store : #%s#\n", client->_currentFile); #endif free(fileNameWithPath); client->_dataTransferPending = STOR_DF; client->startTimeout(); } } else if(strcmp(client->_ftpCommand, "APPE") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { client->_client.printf_P(PSTR("150 File status okay; about to open data connection.\r\n")); //We save the file path to be sent char *fileNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); client->setCurrentFile(fileNameWithPath); #ifdef DEBUG_FTPS Serial.printf("File to append : #%s#\n", client->_currentFile); #endif free(fileNameWithPath); client->_dataTransferPending = APPE_DF; } } else if(strcmp(client->_ftpCommand, "SYST") == 0) { client->_client.println("215 UNIX Type: L8"); } else if(strcmp(client->_ftpCommand, "DELE") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { //We have the file name, we need to append the current directory... char *file2deleteNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); #ifdef DEBUG_FTPS Serial.printf("file to delete : #%s#\n",file2deleteNameWithPath); #endif if(_sdCardManager->remove(file2deleteNameWithPath)) { client->_client.println("250 Requested file action okay."); } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } free(file2deleteNameWithPath); } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } } else if(strcmp(client->_ftpCommand, "RNFR") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) { //We have the file name, we need to append the current directory... char *fileNameWithPath(NULL); if(client->_cmdParameters->getAt(0)->getString()[0] == '/') fileNameWithPath = constructFileNameWithPath("", client->_cmdParameters); else fileNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); #ifdef DEBUG_FTPS Serial.printf("file to rename : #%s#\n",fileNameWithPath); #endif client->setCurrentFile(fileNameWithPath); free(fileNameWithPath); client->_client.println("350 Requested file action pending further information."); } else { sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); } } else if(strcmp(client->_ftpCommand, "RNTO") == 0) { if(!client->_loggedIn) { sendInfoResponse(_530, client); return; } if (client->_cmdParameters->count() > 0) //If the name starts with a /, we do not need to prepend the directory { //Here we rename the file char *file2RenameNameWithPath(NULL); if(client->_cmdParameters->getAt(0)->getString()[0] == '/') file2RenameNameWithPath = constructFileNameWithPath("", client->_cmdParameters); else file2RenameNameWithPath = constructFileNameWithPath(client->_currentDirectory, client->_cmdParameters); #ifdef DEBUG_FTPS Serial.printf("file to rename to : #%s#\n",file2RenameNameWithPath); Serial.printf("Old name : %s --> %s\n",client->_currentFile,file2RenameNameWithPath); #endif if(_sdCardManager->rename(client->_currentFile,file2RenameNameWithPath)) { client->_client.println("250 Requested file action okay."); }else sendInfoResponse(_550, client,"");//client->_client.println("550 Requested action not taken."); free(file2RenameNameWithPath); } } else if(strcmp(client->_ftpCommand, "QUIT") == 0) { if(client->_dataTransferPending == NONE)//If no transfers are in progress { sendInfoResponse(_221, client); client->_client.stop(); client->_clientState = TCPClient::ClientState::DISCARDED; } else sendInfoResponse(_221, client); } /*else if(strcmp(client->_ftpCommand, "SIZE") == 0) { if (client->_cmdParameters->count() > 0) { client->_client.println("213 15224"); } }*/ else { client->_client.println("502 Command not implemented."); #ifdef DEBUG_FTPS Serial.println("Command not implemented"); #endif } } //Here we write the received file to the sd card ICACHE_RAM_ATTR inline boolean writeToSdCard(T *client, FileTransferStatus *fts, boolean append = false) { if (client->_currentFile != NULL) { if(_sdCardManager->exists(client->_currentFile) && client->_fileRecvBytes == 0 && !append) _sdCardManager->remove(client->_currentFile); File fileBeeingReceived = _sdCardManager->open(client->_currentFile, FILE_WRITE); if(fileBeeingReceived) { uint8_t recvBuffer[2048]; /*fileBeeingReceived.seek(client->_fileRecvBytes);*/ uint16_t size = client->_dataClient.read(recvBuffer, 2048); fileBeeingReceived.write(recvBuffer, size); client->_fileRecvBytes += size; fileBeeingReceived.close(); #ifdef DEBUG_FTPS Serial.printf("Writting : %d bytes to file\n", size); #endif } else { return false; } } else { *fts = NO_FILE_NAME; return false; } return true; } //Here we send the fs tree to the ftp client ICACHE_RAM_ATTR inline boolean sendFSTree(T *client) { if (client->_currentDirectory != NULL) { #ifdef DEBUG_FTPS Serial.printf("Directory : %s\n",client->_currentDirectory); #endif File currentDirectory = _sdCardManager->open(client->_currentDirectory); if (currentDirectory) { currentDirectory.rewindDirectory(); 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 return false; } return true; } return false; } //The binary flag needs to be taken into consideration ICACHE_RAM_ATTR inline boolean sendFile(T *client, FileTransferStatus *fts) { if (client->_currentFile != NULL) { uint8_t sendBuffer[READ_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_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; } //This functions construct the full file path. //ie : if the current directory is : "/somedir/subdir" and the received file name is : "my file .txt" //then it will return : "/somedir/subdir/my file .txt" //Note that the file name is contained in a parameter list : "my" in the first element, then "file" in the second and finally ".txt" in the third. //Return NULL if malloc fails // DO NOT FORGET TO FREE THE ALLOCATED STRING AFTER USING IT.... static char *constructFileNameWithPath(const char *dir, Dictionary *parameters) { uint16_t fileWithPathSize(strlen(dir) + 1) /*dir path plus filename*/, paramCount(parameters->count()); char *fileNameWithPath(NULL); for(int i(0); i < paramCount; i++) { fileWithPathSize += strlen(parameters->getAt(i)->getString()) + 1; } fileNameWithPath = (char *)malloc( sizeof(char) * fileWithPathSize ); #ifdef DEBUG_FTPS Serial.printf("Allocated string size : %d\n",fileWithPathSize); #endif if(fileNameWithPath == NULL)//Malloc fails return NULL; strcpy(fileNameWithPath, dir); if(strcmp(fileNameWithPath, "/") != 0 && strlen(dir) != 0) strcat(fileNameWithPath,"/"); for(int i(0); i < paramCount; i++) { strcat(fileNameWithPath, parameters->getAt(i)->getString()); if(i != paramCount-1) strcat(fileNameWithPath," "); } return fileNameWithPath; } //Error code functions void sendInfoResponse(FtpMsgCode code, T *client, const char *msg = "") { switch(code) { case _200: client->_client.printf_P(PSTR("200 Command okay. %s\r\n"), msg); break; case _221: client->_client.printf_P(PSTR("221 Service closing control connection. %s\r\n")); break; case _530: client->_client.printf_P(PSTR("530 Password required. %s\r\n"), msg); break; case _550: client->_client.printf_P(PSTR("550 Requested action not taken. %s\r\n"), msg); break; } } 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