diff options
author | Daniel Plasa <dplasa@gmail.com> | 2020-05-28 22:12:35 +0200 |
---|---|---|
committer | Daniel Plasa <dplasa@gmail.com> | 2020-05-28 22:12:35 +0200 |
commit | 808b4da8512f9289e664f841ea91d314b7de2e94 (patch) | |
tree | 21b0a333fa16d92c243e7f0753b49e648d1b214d | |
parent | Add FTP Client (diff) | |
download | FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.tar FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.tar.gz FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.tar.bz2 FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.tar.lz FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.tar.xz FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.tar.zst FTPCLientServer-808b4da8512f9289e664f841ea91d314b7de2e94.zip |
-rw-r--r-- | FTPServer.cpp | 1205 | ||||
-rw-r--r-- | FTPServer.h | 103 |
2 files changed, 1308 insertions, 0 deletions
diff --git a/FTPServer.cpp b/FTPServer.cpp new file mode 100644 index 0000000..4a03712 --- /dev/null +++ b/FTPServer.cpp @@ -0,0 +1,1205 @@ +/* + * FTP Server for ESP8266/ESP32 + * based on FTP Serveur for Arduino Due and Ethernet shield (W5100) or WIZ820io (W5200) + * based on Jean-Michel Gallego's work + * modified to work with esp8266 SPIFFS by David Paiva david@nailbuster.com + * modified to work with esp8266 LitteFS by Daniel Plasa dplasa@gmail.com + * Also done some code reworks and all string contants are now in flash memory + * by using F(), PSTR() ... on the string literals. + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#include "FTPServer.h" + +#ifdef ESP8266 +#include <ESP8266WiFi.h> +#elif defined ESP32 +#include <WiFi.h> +#endif + +#include "FTPCommon.h" + +WiFiServer controlServer(FTP_CTRL_PORT); +WiFiServer dataServer(FTP_DATA_PORT_PASV); + +// helper macros +#define FTP_STR(s) FTP_STR2(s) +#define FTP_STR2(s) #s +#define FTP_SEND_MSG(code, fmt, ...) \ + do \ + { \ + FTP_DEBUG_MSG(">>> " FTP_STR(code) " " fmt, ##__VA_ARGS__); \ + control.printf_P(PSTR(FTP_STR(code) " " fmt "\r\n"), ##__VA_ARGS__); \ + } while (0) + +#define FTP_SEND_DASHMSG(code, fmt, ...) \ + do \ + { \ + FTP_DEBUG_MSG(">>> " FTP_STR(code) "-" fmt, ##__VA_ARGS__); \ + control.printf_P(PSTR(FTP_STR(code) "-" fmt "\r\n"), ##__VA_ARGS__); \ + } while (0) + +// some constants +static const char aSpace[] PROGMEM = " "; +static const char aSlash[] PROGMEM = "/"; + +// constructor +FTPServer::FTPServer(FS &_FSImplementation) : FTPCommon(_FSImplementation) {} + +void FTPServer::begin(const String &uname, const String &pword) +{ + _FTP_USER = uname; + _FTP_PASS = pword; + + iniVariables(); + + // Tells the ftp server to begin listening for incoming connections + controlServer.begin(); + dataServer.begin(); +} + +void FTPServer::stop() +{ + abortTransfer(); + disconnectClient(false); + controlServer.stop(); + dataServer.stop(); + + FTPCommon::stop(); +} + +void FTPServer::iniVariables() +{ + // Default Data connection is Active + dataPassiveConn = true; + + // Set the root directory + cwd = FPSTR(aSlash); + + // init internal status vars + cmdState = cInit; + transferState = tIdle; + rnFrom.clear(); + + // reset control connection input buffer, clear previous command + cmdLine.clear(); + cmdString.clear(); + parameters.clear(); + command = 0; + + // free any used fileBuffer + freeBuffer(); +} + +void FTPServer::handleFTP() +{ + // + // control connection state sequence is + // cInit + // | + // V + // cWait + // | + // V + // cCheck -----------+ + // | | (no username but password set) + // V | + // cUserId ----------+---+ + // | | | + // +<--------------+ | + // V | (no password set) + // cPassword | + // | | + // +<------------------+ + // V + // cLoginOk + // | + // V + // cProcess + // + + // if ((int32_t)(millisDelay - millis()) > 0) + // return; + + if (cmdState == cInit) + { + if (control.connected()) + { + abortTransfer(); + disconnectClient(false); + } + iniVariables(); + cmdState = cWait; + } + + else if (cmdState == cWait) // FTP control server waiting for connection + { + if (controlServer.hasClient()) + { + control = controlServer.available(); + + // wait 10s for login command + updateTimeout(10); + cmdState = cCheck; + } + } + + else if (cmdState == cCheck) // FTP control server check/setup control connection + { + if (control.connected()) // A client connected, say "220 Hello client!" + { + FTP_DEBUG_MSG("control server got connection from %s:%d", + control.remoteIP().toString().c_str(), control.remotePort()); + + FTP_SEND_MSG(220, "(espFTP " FTP_SERVER_VERSION ")"); + + if (_FTP_USER.length()) + { + // FTP_SEND_MSG(332, "Need account for login."); + cmdState = cUserId; + } + else if (_FTP_PASS.length()) + { + // FTP_SEND_MSG(331, "Please specify the password."); + cmdState = cPassword; + } + else + { + cmdState = cLoginOk; + } + } + } + + else if (cmdState == cLoginOk) // tell client "Login ok!" + { + FTP_SEND_MSG(230, "Login successful."); + updateTimeout(sTimeOut); + cmdState = cProcess; + } + + // + // all other command states need to process commands froms control connection + // + else if (readChar() > 0) + { + // enforce USER than PASS commands before anything else + if (((cmdState == cUserId) && (FTP_CMD(USER) != command)) || + ((cmdState == cPassword) && (FTP_CMD(PASS) != command))) + { + FTP_SEND_MSG(530, "Please login with USER and PASS."); + FTP_DEBUG_MSG("ignoring before login: cwd=%s cmd[%x]=%s, params='%s'", cwd.c_str(), command, cmdString.c_str(), parameters.c_str()); + command = 0; + return; + } + + // process the command + int8_t rc = processCommand(); + // returns + // -1 : command processing indicates, we have to close control (e.g. QUIT) + // 0 : not yet finished, just call processCommend() again + // 1 : finished + if (rc < 0) + { + cmdState = cInit; + } + if (rc > 0) + { + // clear current command, so readChar() can fetch the next command + command = 0; + + // command was successful, update command state + if (cmdState == cUserId) + { + if (_FTP_PASS.length()) + { + // wait 10s for PASS command + updateTimeout(10); + FTP_SEND_MSG(331, "Please specify the password."); + cmdState = cPassword; + } + else + { + cmdState = cLoginOk; + } + } + else if (cmdState == cPassword) + { + cmdState = cLoginOk; + } + else + { + updateTimeout(sTimeOut); + } + } + } + + // + // general connection handling + // (if we have an established control connection) + // + if (cmdState >= cCheck) + { + // detect lost/closed by remote connections + if (!control.connected() || !control) + { + cmdState = cInit; + FTP_DEBUG_MSG("client lost or disconnected"); + } + + // check for timeout + if (!((int32_t)(millisEndConnection - millis()) > 0)) + { + FTP_SEND_MSG(530, "Timeout."); + FTP_DEBUG_MSG("client connection timed out"); + cmdState = cInit; + } + + // handle data file transfer + if (transferState == tRetrieve) // Retrieve data + { + if (!doFiletoNetwork()) + { + closeTransfer(); + transferState = tIdle; + } + } + else if (transferState == tStore) // Store data + { + if (!doNetworkToFile()) + { + closeTransfer(); + transferState = tIdle; + } + } + } +} + +void FTPServer::disconnectClient(bool gracious) +{ + FTP_DEBUG_MSG("Disconnecting client"); + abortTransfer(); + if (gracious) + { + FTP_SEND_MSG(221, "Goodbye."); + } + else + { + FTP_SEND_MSG(231, "Service terminated."); + } + control.stop(); +} + +int8_t FTPServer::processCommand() +{ + // assume successful operation by default + int8_t rc = 1; + + // make the full path of parameters (even if this makes no sense for all commands) + String path = getFileName(parameters, true); + FTP_DEBUG_MSG("processing: cmd=%s[%x], params='%s' (cwd='%s')", cmdString.c_str(), command, parameters.c_str()); + + /////////////////////////////////////// + // // + // ACCESS CONTROL COMMANDS // + // // + /////////////////////////////////////// + + // + // USER - Provide username + // + if (FTP_CMD(USER) == command) + { + if (_FTP_USER.length() && (_FTP_USER != parameters)) + { + FTP_SEND_MSG(430, "User not found."); + command = 0; + rc = 0; + } + else + { + FTP_DEBUG_MSG("USER ok"); + } + } + + // + // PASS - Provide password + // + else if (FTP_CMD(PASS) == command) + { + if (_FTP_PASS.length() && (_FTP_PASS != parameters)) + { + FTP_SEND_MSG(430, "Password invalid."); + command = 0; + rc = 0; + } + else + { + FTP_DEBUG_MSG("PASS ok"); + } + } + + // + // QUIT + // + else if (FTP_CMD(QUIT) == command) + { + disconnectClient(); + rc = -1; + } + + // + // NOOP + // + else if (FTP_CMD(NOOP) == command) + { + FTP_SEND_MSG(200, "Zzz..."); + } + + // + // CDUP - Change to Parent Directory + // + else if (FTP_CMD(CDUP) == command) + { + // up one level + cwd = getPathName("", false); + FTP_SEND_MSG(250, "Directory successfully changed."); + } + + // + // CWD - Change Working Directory + // + else if (FTP_CMD(CWD) == command) + { + if (parameters == F(".")) // 'CWD .' is the same as PWD command + { + command = FTP_CMD(PWD); // make CWD a PWD command ;-) + rc = 0; // indicate we need another processCommand() call + } + else if (parameters == F("..")) // 'CWD ..' is the same as CDUP command + { + command = FTP_CMD(CDUP); // make CWD a CDUP command ;-) + rc = 0; // indicate we need another processCommand() call + } + else + { +#if (defined esp8266FTPServer_SPIFFS) + // SPIFFS has no directories, it's always ok + cwd = path; + FTP_SEND_MSG(250, "Directory successfully changed."); +#else + // check if directory exists + file = THEFS.open(path, "r"); + if (file.isDirectory()) + { + cwd = path; + FTP_SEND_MSG(250, "Directory successfully changed."); + } + else + { + FTP_SEND_MSG(550, "Failed to change directory."); + } + file.close(); +#endif + } + } + + // + // PWD - Print Directory + // + else if (FTP_CMD(PWD) == command) + { + FTP_SEND_MSG(257, "\"%s\" is the current directory.", cwd.c_str()); + } + + /////////////////////////////////////// + // // + // TRANSFER PARAMETER COMMANDS // + // // + /////////////////////////////////////// + + // + // MODE - Transfer Mode + // + else if (FTP_CMD(MODE) == command) + { + if (parameters == F("S")) + FTP_SEND_MSG(504, "Only S(tream) mode is suported"); + else + FTP_SEND_MSG(200, "Mode set to S."); + } + + // + // PASV - Passive data connection management + // + else if (FTP_CMD(PASV) == command) + { + // stop a possible previous data connection + data.stop(); + // tell client to open data connection to our ip:dataPort + dataPort = FTP_DATA_PORT_PASV; + dataPassiveConn = true; + String ip = control.localIP().toString(); + ip.replace(".", ","); + FTP_SEND_MSG(227, "Entering Passive Mode (%s,%d,%d).", ip.c_str(), dataPort >> 8, dataPort & 255); + } + + // + // PORT - Data Port, Active data connection management + // + else if (FTP_CMD(PORT) == command) + { + if (data) + data.stop(); + + if (parseDataIpPort(parameters.c_str())) + { + dataPassiveConn = false; + FTP_SEND_MSG(200, "PORT command successful"); + FTP_DEBUG_MSG("Data connection management Active, using %s:%u", dataIP.toString().c_str(), dataPort); + } + else + { + FTP_SEND_MSG(501, "Can't interpret parameters"); + } + } + + // + // STRU - File Structure + // + else if (FTP_CMD(STRU) == command) + { + if (parameters == F("F")) + FTP_SEND_MSG(504, "Only F(ile) is suported"); + else + FTP_SEND_MSG(200, "Structure set to F."); + } + + // + // TYPE - Data Type + // + else if (FTP_CMD(TYPE) == command) + { + if (parameters == F("A")) + FTP_SEND_MSG(200, "TYPE is now ASII."); + else if (parameters == F("I")) + FTP_SEND_MSG(200, "TYPE is now 8-bit Binary."); + else + FTP_SEND_MSG(504, "Unrecognised TYPE."); + } + + /////////////////////////////////////// + // // + // FTP SERVICE COMMANDS // + // // + /////////////////////////////////////// + + // + // ABOR - Abort + // + else if (FTP_CMD(ABOR) == command) + { + abortTransfer(); + FTP_SEND_MSG(226, "Data connection closed"); + } + + // + // DELE - Delete a File + // + else if (FTP_CMD(DELE) == command) + { + if (parameters.length() == 0) + FTP_SEND_MSG(501, "No file name"); + else + { + if (!THEFS.exists(path)) + { + FTP_SEND_MSG(550, "Delete operation failed, file '%s' not found.", path.c_str()); + } + else if (THEFS.remove(path)) + { + FTP_SEND_MSG(250, "Delete operation successful."); + } + else + { + FTP_SEND_MSG(450, "Delete operation failed."); + } + } + } + + // + // LIST - List directory contents + // MLSD - Listing for Machine Processing (see RFC 3659) + // NLST - Name List + // + else if ((FTP_CMD(LIST) == command) || (FTP_CMD(MLSD) == command) || (FTP_CMD(NLST) == command)) + { + rc = dataConnect(); // returns -1: no data connection, 0: need more time, 1: data ok + if (rc < 0) + { + FTP_SEND_MSG(425, "No data connection"); + rc = 1; // mark command as processed + } + else if (rc > 0) + { + FTP_SEND_MSG(150, "Accepted data connection"); + uint16_t dirCount = 0; + Dir dir = THEFS.openDir(path); + while (dir.next()) + { + ++dirCount; + bool isDir = false; + String fn = dir.fileName(); + if (cwd == FPSTR(aSlash) && fn[0] == '/') + fn.remove(0, 1); + isDir = dir.isDirectory(); + if (FTP_CMD(LIST) == command) + { + if (isDir) + { + data.printf_P(PSTR("+d\r\n,\t%s\r\n"), fn.c_str()); + } + else + { + data.printf_P(PSTR("+r,s%lu\r\n,\t%s\r\n"), (uint32_t)dir.fileSize(), fn.c_str()); + } + } + else if (FTP_CMD(MLSD) == command) + { + // "modify=20170122163911;type=dir;UNIX.group=0;UNIX.mode=0775;UNIX.owner=0; dirname" + // "modify=20170121000817;size=12;type=file;UNIX.group=0;UNIX.mode=0644;UNIX.owner=0; filename" + file = dir.openFile("r"); + data.printf_P(PSTR("modify=%s;UNIX.group=0;UNIX.owner=0;UNIX.mode="), makeDateTimeStr(file.getLastWrite()).c_str()); + file.close(); + if (isDir) + { + data.printf_P(PSTR("0755;type=dir; ")); + } + else + { + data.printf_P(PSTR("0644;size=%lu;type=file; "), (uint32_t)dir.fileSize()); + } + data.printf_P(PSTR("%s\r\n"), fn.c_str()); + } + else if (FTP_CMD(NLST) == command) + { + data.println(fn); + } + else + { + FTP_DEBUG_MSG("Implemetation of %s [%x] command - internal BUG", cmdString.c_str(), command); + } + } + + if (FTP_CMD(MLSD) == command) + { + control.println(F("226-options: -a -l\r\n")); + } + FTP_SEND_MSG(226, "%d matches total", dirCount); + } +#if defined ESP32 + File root = THEFS.open(cwd); + if (!root) + { + FTP_SEND_MSG(550, "Can't open directory " + cwd); + // return; + } + else + { + // if(!root.isDirectory()){ + // FTP_DEBUG_MSG("Not a directory: '%s'", cwd.c_str()); + // return; + // } + + File file = root.openNextFile(); + while (file) + { + if (file.isDirectory()) + { + data.println("+r,s <DIR> " + String(file.name())); + // Serial.print(" DIR : "); + // Serial.println(file.name()); + // if(levels){ + // listDir(fs, file.name(), levels -1); + // } + } + else + { + String fn, fs; + fn = file.name(); + // fn.remove(0, 1); + fs = String(file.size()); + data.println("+r,s" + fs); + data.println(",\t" + fn); + nm++; + } + file = root.openNextFile(); + } + FTP_SEND_MSG(226, "%s matches total", nm); + } +#endif + data.stop(); + } + +#if defined ESP32 + // + // FIXME MLSD ESP32 + // + else if (!strcmp(command, "MLSD")) + { + File root = THEFS.open(cwd); + // if(!root){ + // control.println( "550, "Can't open directory " + cwd ); + // // return; + // } else { + // if(!root.isDirectory()){ + // Serial.println("Not a directory"); + // return; + // } + + File file = root.openNextFile(); + while (file) + { + // if(file.isDirectory()){ + // data.println( "+r,s <DIR> " + String(file.name())); + // // Serial.print(" DIR : "); + // // Serial.println(file.name()); + // // if(levels){ + // // listDir(fs, file.name(), levels -1); + // // } + // } else { + String fn, fs; + fn = file.name(); + fn.remove(0, 1); + fs = String(file.size()); + data.println("Type=file;Size=" + fs + ";" + "modify=20000101160656;" + " " + fn); + nm++; + // } + file = root.openNextFile(); + } + FTP_SEND_MSG(226, "-options: -a -l"); + FTP_SEND_MSG(226, "%d matches total", nm); + // } + data.stop(); + } + // + // NLST + // + else if (!strcmp(command, "NLST")) + { + File root = THEFS.open(cwd); + if (!root) + { + FTP_SEND_MSG(550, "Can't open directory %s\n"), cwd.c_str()); + } + else + { + File file = root.openNextFile(); + while (file) + { + data.println(file.name()); + nm++; + file = root.openNextFile(); + } + FTP_SEND_MSG(226, "%d matches total", nm); + } + data.stop(); + } +#endif + + // + // RETR - Retrieve + // + else if (FTP_CMD(RETR) == command) + { + if (parameters.length() == 0) + { + FTP_SEND_MSG(501, "No file name"); + } + else + { + // open the file if not opened before (when re-running processCommand() since data connetion needs time) + if (!file) + file = THEFS.open(path, "r"); + if (!file) + { + FTP_SEND_MSG(550, "File '%s' not found.", parameters.c_str()); + } + else if (!file.isFile()) + { + FTP_SEND_MSG(450, "Cannot open file \"%s\".", parameters.c_str()); + } + else + { + rc = dataConnect(); // returns -1: no data connection, 0: need more time, 1: data ok + if (rc < 0) + { + FTP_SEND_MSG(425, "No data connection"); + rc = 1; // mark command as processed + } + else if (rc > 0) + { + transferState = tRetrieve; + millisBeginTrans = millis(); + bytesTransfered = 0; + uint32_t fs = file.size(); + if (allocateBuffer(fs > 32768 ? 32768 : fs)) + { + FTP_DEBUG_MSG("Sending file '%s'", path.c_str()); + FTP_SEND_MSG(150, "%lu bytes to download", fs); + } + else + { + closeTransfer(); + FTP_SEND_MSG(451, "Internal error. Not enough memory."); + } + } + } + } + } + + // + // STOR - Store + // + else if (FTP_CMD(STOR) == command) + { + if (parameters.length() == 0) + { + FTP_SEND_MSG(501, "No file name."); + } + else + { + FTP_DEBUG_MSG("STOR '%s'", path.c_str()); + if (!file) + file = THEFS.open(path, "w"); + if (!file) + { + FTP_SEND_MSG(451, "Cannot open/create \"%s\"", path.c_str()); + } + else + { + rc = dataConnect(); // returns -1: no data connection, 0: need more time, 1: data ok + if (rc < 0) + { + FTP_SEND_MSG(425, "No data connection"); + file.close(); + rc = 1; // mark command as processed + } + else if (rc > 0) + { + transferState = tStore; + millisBeginTrans = millis(); + bytesTransfered = 0; + if (allocateBuffer(2048)) + { + FTP_DEBUG_MSG("Receiving file '%s' => %s", parameters.c_str(), path.c_str()); + FTP_SEND_MSG(150, "Connected to port %d", dataPort); + } + else + { + closeTransfer(); + FTP_SEND_MSG(451, "Internal error. Not enough memory."); + } + } + } + } + } + + // + // MKD - Make Directory + // + else if (FTP_CMD(MKD) == command) + { +#if (defined esp8266FTPServer_SPIFFS) + FTP_SEND_MSG(550, "Create directory operation failed."); //not support on SPIFFS +#else + FTP_DEBUG_MSG("mkdir(%s)", path.c_str()); + if (THEFS.mkdir(path)) + { + FTP_SEND_MSG(257, "\"%s\" created.", path.c_str()); + } + else + { + FTP_SEND_MSG(550, "Create directory operation failed."); + } +#endif + } + + // + // RMD - Remove a Directory + // + else if (FTP_CMD(RMD) == command) + { +#if (defined esp8266FTPServer_SPIFFS) + FTP_SEND_MSG(550, "Remove directory operation failed."); //not support on SPIFFS +#else + // check directory for files + Dir dir = THEFS.openDir(path); + if (dir.next()) + { + //only delete if dir is empty! + FTP_SEND_MSG(550, "Remove directory operation failed, directory is not empty."); + } + else + { + THEFS.rmdir(path); + FTP_SEND_MSG(250, "Remove directory operation successful."); + } +#endif + } + // + // RNFR - Rename From + // + else if (FTP_CMD(RNFR) == command) + { + if (parameters.length() == 0) + FTP_SEND_MSG(501, "No file name"); + else + { + if (!THEFS.exists(path)) + FTP_SEND_MSG(550, "File \"%s\" not found.", path.c_str()); + else + { + FTP_SEND_MSG(350, "RNFR accepted - file \"%s\" exists, ready for destination", path.c_str()); + rnFrom = path; + } + } + } + // + // RNTO - Rename To + // + else if (FTP_CMD(RNTO) == command) + { + if (rnFrom.length() == 0) + FTP_SEND_MSG(503, "Need RNFR before RNTO"); + else if (parameters.length() == 0) + FTP_SEND_MSG(501, "No file name"); + else if (THEFS.exists(path)) + FTP_SEND_MSG(553, "\"%s\" already exists.", parameters.c_str()); + else + { + FTP_DEBUG_MSG("Renaming '%s' to '%s'", rnFrom.c_str(), path.c_str()); + if (THEFS.rename(rnFrom, path)) + FTP_SEND_MSG(250, "File successfully renamed or moved"); + else + FTP_SEND_MSG(451, "Rename/move failure."); + } + rnFrom.clear(); + } + + /////////////////////////////////////// + // // + // EXTENSIONS COMMANDS (RFC 3659) // + // // + /////////////////////////////////////// + + // + // FEAT - New Features + // + else if (FTP_CMD(FEAT) == command) + { + FTP_SEND_DASHMSG(211, "Features:\r\n MLSD\r\n MDTM\r\n SIZE\r\n211 End."); + } + + // + // MDTM - File Modification Time (see RFC 3659) + // + else if (FTP_CMD(MDTM) == command) + { + file = THEFS.open(path, "r"); + if ((!file) || (0 == parameters.length())) + { + FTP_SEND_MSG(550, "Unable to retrieve time"); + } + else + { + FTP_SEND_MSG(213, "%s", makeDateTimeStr(file.getLastWrite()).c_str()); + } + file.close(); + } + + // + // SIZE - Size of the file + // + else if (FTP_CMD(SIZE) == command) + { + file = THEFS.open(path, "r"); + if ((!file) || (0 == parameters.length())) + { + FTP_SEND_MSG(450, "Cannot open file."); + } + else + { + FTP_SEND_MSG(213, "%lu", (uint32_t)file.size()); + } + file.close(); + } + + // + // SITE - System command + // + else if (FTP_CMD(SITE) == command) + { + FTP_SEND_MSG(502, "SITE command not implemented"); + } + + // + // SYST - System information + // + else if (FTP_CMD(SYST) == command) + { + FTP_SEND_MSG(215, "UNIX Type: L8"); + } + + // + // Unrecognized commands ... + // + else + { + FTP_DEBUG_MSG("Unknown command: %s [%#x], param: '%s')", cmdString.c_str(), command, parameters.c_str()); + FTP_SEND_MSG(500, "unknown command \"%s\"", cmdString.c_str()); + } + + return rc; +} + +int8_t FTPServer::dataConnect() +{ + int8_t rc = 1; // assume success + + if (!dataPassiveConn) + { + // active mode + // open our own data connection + return FTPCommon::dataConnect(); + } + else + { + // passive mode + // wait for data connection from the client + if (!data.connected()) + { + if (dataServer.hasClient()) + { + data.stop(); + data = dataServer.available(); + FTP_DEBUG_MSG("Got incoming (passive) data connection from %s:%u", data.remoteIP().toString().c_str(), data.remotePort()); + } + else + { + // give me more time waiting for a data connection + rc = 0; + } + } + } + return rc; +} + + +void FTPServer::closeTransfer() +{ + uint32_t deltaT = (int32_t)(millis() - millisBeginTrans); + if (deltaT > 0 && bytesTransfered > 0) + { + FTP_SEND_MSG(226, "File successfully transferred, %lu ms, %f kB/s.", deltaT, float(bytesTransfered) / deltaT); + } + else + FTP_SEND_MSG(226, "File successfully transferred"); + + FTPCommon::closeTransfer(); +} + +void FTPServer::abortTransfer() +{ + if (transferState > tIdle) + { + file.close(); + data.stop(); + FTP_SEND_MSG(426, "Transfer aborted"); + } + freeBuffer(); + transferState = tIdle; +} + +// Read a char from client connected to ftp server +// +// returns: +// -1 if cmdLine too long +// 0 cmdLine still incomplete (no \r or \n received yet) +// 1 cmdLine processed, command and parameters available + +int8_t FTPServer::readChar() +{ + // only read/parse, if the previous command has been fully processed! + if (command) + return 1; + + while (control.available()) + { + char c = control.read(); + // FTP_DEBUG_MSG("readChar() cmdLine='%s' <= %c", cmdLine.c_str(), c); + + // substitute '\' with '/' + if (c == '\\') + c = '/'; + + // nl detected? then process line + if (c == '\n' || c == '\r') + { + cmdLine.trim(); + + // but only if we got at least chars in the line! + if (0 == cmdLine.length()) + break; + + // search for space between command and parameters + int pos = cmdLine.indexOf(FPSTR(aSpace)); + if (pos > 0) + { + parameters = cmdLine.substring(pos + 1); + parameters.trim(); + cmdLine.remove(pos); + } + else + { + parameters.remove(0); + } + cmdString = cmdLine; + + // convert command to upper case + cmdString.toUpperCase(); + + // convert the (up to 4 command chars to numerical value) + command = *(const uint32_t *)cmdString.c_str(); + + // clear cmdline + cmdLine.clear(); + FTP_DEBUG_MSG("readChar() success, command=%x, cmdString='%s', params='%s'", command, cmdString.c_str(), parameters.c_str()); + return 1; + } + else + { + // just add char + cmdLine += c; + if (cmdLine.length() > FTP_CMD_SIZE) + { + cmdLine.clear(); + FTP_SEND_MSG(500, "Line too long"); + } + } + } + return 0; +} + +// Get the complete path from cwd + parameters or complete filename from cwd + parameters +// +// 3 possible cases: parameters can be absolute path, relative path or only the name +// +// returns: +// path WITHOUT file-/dirname (fullname=false) +// full path WITH file-/dirname (fullname=true) +String FTPServer::getPathName(const String ¶m, bool fullname) +{ + String tmp; + + // is param an absoulte path? + if (param[0] == '/') + { + tmp = param; + } + else + { + // start with cwd + tmp = cwd; + + // if param != "" then add param + if (param.length()) + { + if (!tmp.endsWith(FPSTR(aSlash))) + tmp += '/'; + tmp += param; + } + // => tmp becomes cdw [ + '/' + param ] + } + + // strip filename + if (!fullname) + { + // search rightmost '/' + int lastslash = tmp.lastIndexOf(FPSTR(aSlash)); + if (lastslash >= 0) + { + tmp.remove(lastslash); + } + } + // sanetize: + // "" -> "/" + // "/some/path/" => "/some/path" + while (tmp.length() > 1 && tmp.endsWith(FPSTR(aSlash))) + tmp.remove(cwd.length() - 1); + if (tmp.length() == 0) + tmp += '/'; + return tmp; +} + +// Get the [complete] file name from cwd + parameters +// +// 3 possible cases: parameters can be absolute path, relative path or only the filename +// +// returns: +// filename or filename with complete path +String FTPServer::getFileName(const String ¶m, bool fullFilePath) +{ + // build the filename with full path + String tmp = getPathName(param, true); + + if (!fullFilePath) + { + // strip path + // search rightmost '/' + int lastslash = tmp.lastIndexOf(FPSTR(aSlash)); + if (lastslash > 0) + { + tmp.remove(0, lastslash); + } + } + + return tmp; +} + +// Formats YYYYMMDDHHMMSS from a time_t timestamp +// +// uses the buf of the FTPServer to store the date string +// +// parameters: +// timestamp +// +// return: +// pointer to buf[0] + +String FTPServer::makeDateTimeStr(time_t ft) +{ + struct tm *_tm = gmtime(&ft); + String tmp; + tmp.reserve(17); + strftime((char *)tmp.c_str(), 17, "%Y%m%d%H%M%S", _tm); + return tmp; +} + +void FTPServer::updateTimeout(uint16_t s) +{ + millisEndConnection = s; + millisEndConnection *= 60000UL; + millisEndConnection += millis(); +} diff --git a/FTPServer.h b/FTPServer.h new file mode 100644 index 0000000..fdc4078 --- /dev/null +++ b/FTPServer.h @@ -0,0 +1,103 @@ +/* + * FTP SERVER FOR ESP8266/ESP32 + * based on FTP Serveur for Arduino Due and Ethernet shield (W5100) or WIZ820io (W5200) + * based on Jean-Michel Gallego's work + * modified to work with esp8266 SPIFFS by David Paiva (david@nailbuster.com) + * modified to work with esp8266 LitteFS by Daniel Plasa dplasa@gmail.com + * Also done some code reworks and all string contants are now in flash memory + * by using F(), PSTR() ... on the string literals. + * + * 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 3 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, see <http://www.gnu.org/licenses/>. + */ + +#ifndef FTP_SERVER_H +#define FTP_SERVER_H + +/******************************************************************************* + ** ** + ** DEFINITIONS FOR FTP SERVER/CLIENT ** + ** ** + *******************************************************************************/ +#include "FTPCommon.h" +#define FTP_SERVER_VERSION "0.9.3-20200526" + +class FTPServer : public FTPCommon +{ +public: + // contruct an instance of the FTP server using a + // given FS object, e.g. SPIFFS or LittleFS + FTPServer(FS &_FSImplementation); + + // starts the FTP server with username and password, + // either one can be empty to enable anonymous ftp + void begin(const String &uname, const String &pword); + + // stops the FTP server + void stop(); + + // set the FTP server's timeout in seconds + void setTimeout(uint16_t timeout = FTP_TIME_OUT * 60); + + // needs to be called frequently (e.g. in loop() ) + // to process ftp requests + void handleFTP(); + +private: + enum internalState + { + cInit = 0, + cWait, + cCheck, + cUserId, + cPassword, + cLoginOk, + cProcess, + + tIdle, + tRetrieve, + tStore + }; + + void iniVariables(); + void disconnectClient(bool gracious = true); + int8_t processCommand(); + virtual void closeTransfer(); + void abortTransfer(); + + virtual int8_t dataConnect(); + + String getPathName(const String ¶m, bool includeLast = false); + String getFileName(const String ¶m, bool fullFilePath = false); + String makeDateTimeStr(time_t fileTime); + int8_t readChar(); + void updateTimeout(uint16_t timeout); + + // server specific + bool dataPassiveConn = true; // PASV (passive) mode is our default + String _FTP_USER; // usename + String _FTP_PASS; // password + uint32_t command; // numeric command code of command sent by the client + String cmdLine; // command line as read from client + String cmdString; // command as textual representation + String parameters; // parameters sent by client + String cwd; // the current directory + String rnFrom; // previous command was RNFR, this is the source file name + + internalState cmdState, // state of ftp control connection + transferState; // state of ftp data connection + + uint32_t millisEndConnection; +}; + +#endif // FTP_SERVER_H |