diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 21af65a..007c027 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -13,6 +13,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + token: ${{ secrets.DEPENDABOT_PAT }} + - name: run PlatformIO Dependabot uses: peterus/platformio_dependabot@main with: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6aa3aa3..96f4f47 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,6 +3,7 @@ name: Integration Tests on: merge_group: pull_request: + workflow_dispatch: jobs: build: @@ -85,7 +86,7 @@ jobs: - name: Run cppcheck and create html run: docker run --rm -v ${PWD}:/src facthunder/cppcheck:latest /bin/bash -c "cppcheck --xml $CPPCHECK_ARGS 2> report.xml && cppcheck-htmlreport --file=report.xml --report-dir=output" - name: Upload report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Cppcheck Report path: output diff --git a/README.md b/README.md index f372065..3e8221c 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,43 @@ # ESP-FTP-Server-Lib +This is a fork form https://github.com/peterus/ESP-FTP-Server-Lib, since there seems to be no maintanance anymore. This library will provide a simple and modern FTP server for your ESP32 or ESP8266 device. You can setup multiple users and mutliple filesystems (SD-Card, MMC-Card or/and SPIFFS). ## Examples In the example folder you can find a very simple usage of the FTP server. You just need to setup the users, add the filesystems which you want to use, and call the handle function in the loop. +With the Compileflag -DENABLE_FTP_SANITIZATION you can enable support for special-characters like ":" or "?" by URL-Encodeing of Files. ## Known Commands to the server Currently all kind of simple commands are known to the server: * CDUP +* CLNT * CWD * DELE +* FEAT * LISST * MKD +* MLSD +* NLST +* OPTS +* PASV * PORT * PWD * RETR * RMD * RNFR * RNTO +* STAT * STOR * TYPE +* USER +* PASS * SYST * QUIT * ABOR -* NLST -* STAT ## What is still missing / TODO Some commands are still missing, if you need them create a ticket :) - -Currently just the active mode is supported. For the passive mode you need to wait until version 1.0.0. diff --git a/platformio.ini b/platformio.ini index d56b186..bd653af 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,6 @@ [env:ttgo-lora32] -platform = espressif32 @ 6.3.2 +platform = espressif32 @ 7.0.1 board = ttgo-lora32-v1 framework = arduino test_build_src = yes @@ -10,7 +10,7 @@ check_flags = check_skip_packages = yes [env:ttgo-ESP8266] -platform = espressif8266 @ 4.0.0 +platform = espressif8266 @ 4.2.1 board = esp_wroom_02 framework = arduino test_build_src = yes diff --git a/src/Commands/CDUP.h b/src/Commands/CDUP.h index a88aba7..9016ed4 100644 --- a/src/Commands/CDUP.h +++ b/src/Commands/CDUP.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" class CDUP : public FTPCommand { public: @@ -12,7 +13,7 @@ class CDUP : public FTPCommand { void run(FTPPath &WorkDirectory, const std::vector &Line) override { WorkDirectory.goPathUp(); - SendResponse(250, "Ok. Current directory is " + WorkDirectory.getPath()); + SendResponse(FtpCodes::COMMAND_OK, "Ok. Current directory is " + WorkDirectory.getClearPath()); } }; diff --git a/src/Commands/CWD.h b/src/Commands/CWD.h index 5071d1c..e8f8420 100644 --- a/src/Commands/CWD.h +++ b/src/Commands/CWD.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" class CWD : public FTPCommand { public: @@ -20,9 +21,9 @@ class CWD : public FTPCommand { File dir = _Filesystem->open(path.getPath()); if (dir.isDirectory()) { WorkDirectory = path; - SendResponse(250, "Ok. Current directory is " + WorkDirectory.getPath()); + SendResponse(FtpCodes::COMMAND_OK, "Ok. Current directory is " + WorkDirectory.getClearPath()); } else { - SendResponse(550, "Directory does not exist"); + SendResponse(FtpCodes::FILE_ACTION_NOT_TAKEN, "Directory does not exist"); } } }; diff --git a/src/Commands/DELE.h b/src/Commands/DELE.h index c8ec30c..57a15a0 100644 --- a/src/Commands/DELE.h +++ b/src/Commands/DELE.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" class DELE : public FTPCommand { public: @@ -13,13 +14,13 @@ class DELE : public FTPCommand { void run(FTPPath &WorkDirectory, const std::vector &Line) override { String filepath = WorkDirectory.getFilePath(Line[1]); if (!_Filesystem->exists(filepath)) { - SendResponse(550, "File " + filepath + " not found"); + SendResponse(FtpCodes::FILE_NOT_FOUND, "File " + filepath + " not found"); return; } if (_Filesystem->remove(filepath)) { - SendResponse(250, " Deleted \"" + filepath + "\""); + SendResponse(FtpCodes::COMMAND_OK, " Deleted \"" + filepath + "\""); } else { - SendResponse(450, "Can't delete \"" + filepath + "\""); + SendResponse(FtpCodes::FILE_ACTION_ABORTED, "Can't delete \"" + filepath + "\""); } } }; diff --git a/src/Commands/LIST.h b/src/Commands/LIST.h index d291011..334464e 100644 --- a/src/Commands/LIST.h +++ b/src/Commands/LIST.h @@ -4,25 +4,43 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" #include "../common.h" class LIST : public FTPCommand { public: - explicit LIST(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort) : FTPCommand("LIST", 1, Client, Filesystem, DataAddress, DataPort) { + explicit LIST(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommand("LIST", 1, Client, Filesystem, DataAddress, DataPort, PassiveServer, PassiveMode) { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { + FTPPath listPath = WorkDirectory; + + // 1. Check if we have arguments + if (Line.size() > 1) { + String args = Line[1]; + args.trim(); // Modifies 'args' in place + + if (!args.isEmpty()) { + String path = ExtractPathFromOptions(args); + if (!path.isEmpty()) { + listPath.changePath(path); + } + } + } + if (!ConnectDataConnection()) { return; } - File dir = _Filesystem->open(WorkDirectory.getPath()); // + File dir = _Filesystem->open(listPath.getPath()); // if (!dir || !dir.isDirectory()) { CloseDataConnection(); - SendResponse(550, "Can't open directory " + WorkDirectory.getPath()); + SendResponse(FtpCodes::FILE_NOT_FOUND, "Can't open directory " + listPath.getClearPath()); return; } - int cnt = 0; - File f = dir.openNextFile(); + int cnt = 2; + data_println("drwxr-xr-x 1 owner group 0 Jan 01 1970 ."); + data_println("drwxr-xr-x 1 owner group 0 Jan 01 1970 .."); + File f = dir.openNextFile(); while (f) { String filename = f.name(); filename.remove(0, filename.lastIndexOf('/') + 1); @@ -37,13 +55,14 @@ class LIST : public FTPCommand { for (int i = 0; i < fill_cnt; i++) { data_print(" "); } + filename = listPath.reparse(filename); data_println(filesize + " Jan 01 1970 " + filename); cnt++; f.close(); f = dir.openNextFile(); } CloseDataConnection(); - SendResponse(226, String(cnt) + " matches total"); + SendResponse(FtpCodes::TRANSFER_COMPLETE, String(cnt) + " matches total"); } }; diff --git a/src/Commands/MKD.h b/src/Commands/MKD.h index e1c1765..ca2ccda 100644 --- a/src/Commands/MKD.h +++ b/src/Commands/MKD.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" class MKD : public FTPCommand { public: @@ -13,13 +14,13 @@ class MKD : public FTPCommand { void run(FTPPath &WorkDirectory, const std::vector &Line) override { String filepath = WorkDirectory.getFilePath(Line[1]); if (_Filesystem->exists(filepath)) { - SendResponse(521, "Can't create \"" + filepath + "\", Directory exists"); + SendResponse(FtpCodes::FILE_ACTION_NOT_TAKEN, "Can't create \"" + filepath + "\", Directory exists"); return; } if (_Filesystem->mkdir(filepath)) { - SendResponse(257, "\"" + filepath + "\" created"); + SendResponse(FtpCodes::PATHNAME_CREATED, "\"" + filepath + "\" created"); } else { - SendResponse(550, "Can't create \"" + filepath + "\""); + SendResponse(FtpCodes::FILE_ACTION_NOT_TAKEN, "Can't create \"" + filepath + "\""); } } }; diff --git a/src/Commands/MLSD.h b/src/Commands/MLSD.h index a88045b..32aa52b 100644 --- a/src/Commands/MLSD.h +++ b/src/Commands/MLSD.h @@ -5,25 +5,40 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" #include "../common.h" #include class MLSD : public FTPCommand { public: - explicit MLSD(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort) : FTPCommand("MLSD", 1, Client, Filesystem, DataAddress, DataPort) { + explicit MLSD(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommand("MLSD", 1, Client, Filesystem, DataAddress, DataPort, PassiveServer, PassiveMode) { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { + FTPPath listPath = WorkDirectory; + // 1. Check if we have arguments + if (Line.size() > 1) { + String args = Line[1]; + args.trim(); // Modifies 'args' in place + + if (!args.isEmpty()) { + String path = ExtractPathFromOptions(args); + if (!path.isEmpty()) { + listPath.changePath(path); + } + } + } + if (!ConnectDataConnection()) { return; } - File root = _Filesystem->open(WorkDirectory.getPath(), "r"); + File root = _Filesystem->open(listPath.getPath(), "r"); if (!root || !root.isDirectory()) { root.close(); CloseDataConnection(); - SendResponse(550, "Can't open directory " + WorkDirectory.getPath()); + SendResponse(FtpCodes::FILE_NOT_FOUND, "Can't open directory " + listPath.getClearPath()); return; } @@ -46,9 +61,9 @@ class MLSD : public FTPCommand { data_print(";"); // modify=YYYYMMDDHHMMSS; // GMT (!!!!) - char buf[128]; - time_t ft = f.getLastWrite(); - struct tm *t = localtime(&ft); + char buf[128]; + time_t ft = f.getLastWrite(); + const struct tm *t = localtime(&ft); sprintf(buf, "modify=%4d%02d%02d%02d%02d%02d;", (t->tm_year) + 1900, (t->tm_mon) + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); data_print(String(buf)); @@ -59,6 +74,7 @@ class MLSD : public FTPCommand { data_print(" "); String filename = f.name(); filename.remove(0, filename.lastIndexOf('/') + 1); + filename = listPath.reparse(filename); data_println(filename); cnt++; @@ -67,7 +83,7 @@ class MLSD : public FTPCommand { root.close(); CloseDataConnection(); - SendResponse(226, String(cnt) + " matches total"); + SendResponse(FtpCodes::TRANSFER_COMPLETE, String(cnt) + " matches total"); } }; diff --git a/src/Commands/NLST.h b/src/Commands/NLST.h index c739604..ac1762c 100644 --- a/src/Commands/NLST.h +++ b/src/Commands/NLST.h @@ -4,11 +4,12 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" #include "../common.h" class NLST : public FTPCommand { public: - explicit NLST(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort) : FTPCommand("NLST", 1, Client, Filesystem, DataAddress, DataPort) { + explicit NLST(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommand("NLST", 1, Client, Filesystem, DataAddress, DataPort, PassiveServer, PassiveMode) { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { @@ -18,20 +19,23 @@ class NLST : public FTPCommand { File dir = _Filesystem->open(WorkDirectory.getPath()); // if (!dir || !dir.isDirectory()) { CloseDataConnection(); - SendResponse(550, "Can't open directory " + WorkDirectory.getPath()); + SendResponse(FtpCodes::FILE_NOT_FOUND, "Can't open directory " + WorkDirectory.getClearPath()); return; } - int cnt = 0; - File f = dir.openNextFile(); + int cnt = 2; + data_println("."); + data_println(".."); + File f = dir.openNextFile(); while (f) { String filename = f.name(); filename.remove(0, filename.lastIndexOf('/') + 1); + data_println(WorkDirectory.reparse(filename)); cnt++; f.close(); f = dir.openNextFile(); } CloseDataConnection(); - SendResponse(226, String(cnt) + " matches total"); + SendResponse(FtpCodes::TRANSFER_COMPLETE, String(cnt) + " matches total"); } }; diff --git a/src/Commands/PASV.h b/src/Commands/PASV.h new file mode 100644 index 0000000..04e446f --- /dev/null +++ b/src/Commands/PASV.h @@ -0,0 +1,51 @@ +#ifndef PASV_H_ +#define PASV_H_ + +#include +#include +#if defined(ESP32) +#include +#elif defined(ESP8266) +#include +#endif + +#include "../FTPCommand.h" +#include "../FTPResponseCodes.h" +#include "../common.h" + +class PASV : public FTPCommand { +public: + explicit PASV(WiFiClient *const Client, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommand("PASV", 0, Client, 0, DataAddress, DataPort, PassiveServer, PassiveMode) { + } + + void run(FTPPath &WorkDirectory, const std::vector &Line) override { + if (_PassiveServer != 0 && *_PassiveServer != 0) { + (*_PassiveServer)->stop(); + delete *_PassiveServer; + *_PassiveServer = 0; + } + IPAddress localIP = WiFi.localIP(); + if (localIP == IPAddress(0, 0, 0, 0)) { + if (_PassiveMode != 0) { + *_PassiveMode = false; + } + SendResponse(FtpCodes::NO_DATA_CONNECTION, "No local IP address for passive mode"); + return; + } + int port = 20000 + random(0, 1000); + *_DataPort = port; + if (_PassiveServer != 0) { + *_PassiveServer = new WiFiServer(port); + (*_PassiveServer)->begin(); + } + if (_PassiveMode != 0) { + *_PassiveMode = true; + } + int p1 = port / 256; + int p2 = port % 256; + String response = "Entering Passive Mode (" + String(localIP[0]) + "," + String(localIP[1]) + "," + String(localIP[2]) + "," + String(localIP[3]) + "," + String(p1) + "," + String(p2) + ")"; + SendResponse(FtpCodes::ENTERING_PASV_MODE, response); + } +}; + +#endif diff --git a/src/Commands/PORT.h b/src/Commands/PORT.h index 9851c1a..90cbf09 100644 --- a/src/Commands/PORT.h +++ b/src/Commands/PORT.h @@ -4,20 +4,29 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" #include "../common.h" class PORT : public FTPCommand { public: - explicit PORT(WiFiClient *const Client, IPAddress *DataAddress, int *DataPort) : FTPCommand("PORT", 1, Client, 0, DataAddress, DataPort) { + explicit PORT(WiFiClient *const Client, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommand("PORT", 1, Client, 0, DataAddress, DataPort, PassiveServer, PassiveMode) { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { + if (_PassiveServer != 0 && *_PassiveServer != 0) { + (*_PassiveServer)->stop(); + delete *_PassiveServer; + *_PassiveServer = 0; + } + if (_PassiveMode != 0) { + *_PassiveMode = false; + } std::vector connection_details = Split>(Line[1], ','); for (int i = 0; i < 4; i++) { (*_DataAddress)[i] = connection_details[i].toInt(); } *_DataPort = connection_details[4].toInt() * 256 + connection_details[5].toInt(); - SendResponse(200, "PORT command successful"); + SendResponse(FtpCodes::COMMAND_OK, "PORT command successful"); } }; diff --git a/src/Commands/PWD.h b/src/Commands/PWD.h index 9aa8dfc..9cadb3e 100644 --- a/src/Commands/PWD.h +++ b/src/Commands/PWD.h @@ -11,7 +11,7 @@ class PWD : public FTPCommand { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { - SendResponse(257, "\"" + WorkDirectory.getPath() + "\" is your current directory"); + SendResponse(FtpCodes::PATHNAME_CREATED, "\"" + WorkDirectory.getClearPath() + "\" is your current directory"); } }; diff --git a/src/Commands/RETR.h b/src/Commands/RETR.h index a156c7a..2f4c081 100644 --- a/src/Commands/RETR.h +++ b/src/Commands/RETR.h @@ -4,13 +4,14 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" #include "../common.h" #define FTP_BUF_SIZE 4096 class RETR : public FTPCommandTransfer { public: - explicit RETR(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort) : FTPCommandTransfer("RETR", 1, Client, Filesystem, DataAddress, DataPort) { + explicit RETR(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommandTransfer("RETR", 1, Client, Filesystem, DataAddress, DataPort, PassiveServer, PassiveMode) { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { @@ -24,7 +25,7 @@ class RETR : public FTPCommandTransfer { _file = _Filesystem->open(path); if (!_file || _file.isDirectory()) { CloseDataConnection(); - SendResponse(550, "Can't open " + path); + SendResponse(FtpCodes::FILE_NOT_FOUND, "Can't open " + path); return; } workOnData(); @@ -38,7 +39,7 @@ class RETR : public FTPCommandTransfer { return; } CloseDataConnection(); - SendResponse(226, "File successfully transferred"); + SendResponse(FtpCodes::TRANSFER_COMPLETE, "File successfully transferred"); _file.close(); } diff --git a/src/Commands/RMD.h b/src/Commands/RMD.h index dad6f52..5970247 100644 --- a/src/Commands/RMD.h +++ b/src/Commands/RMD.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" class RMD : public FTPCommand { public: @@ -13,13 +14,13 @@ class RMD : public FTPCommand { void run(FTPPath &WorkDirectory, const std::vector &Line) override { String filepath = WorkDirectory.getFilePath(Line[1]); if (!_Filesystem->exists(filepath)) { - SendResponse(550, "Folder " + filepath + " not found"); + SendResponse(FtpCodes::FILE_NOT_FOUND, "Folder " + filepath + " not found"); return; } if (_Filesystem->rmdir(filepath)) { - SendResponse(250, " Deleted \"" + filepath + "\""); + SendResponse(FtpCodes::FILE_ACTION_OK, " Deleted \"" + filepath + "\""); } else { - SendResponse(450, "Can't delete \"" + filepath + "\""); + SendResponse(FtpCodes::FILE_ACTION_ABORTED, "Can't delete \"" + filepath + "\""); } } }; diff --git a/src/Commands/RNFR_RNTO.h b/src/Commands/RNFR_RNTO.h index 6a66e9d..a27b8fa 100644 --- a/src/Commands/RNFR_RNTO.h +++ b/src/Commands/RNFR_RNTO.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" class RNFR_RNTO : public FTPCommand { public: @@ -21,28 +22,28 @@ class RNFR_RNTO : public FTPCommand { void from(const FTPPath &WorkDirectory, const std::vector &Line) { String filepath = WorkDirectory.getFilePath(Line[1]); if (!_Filesystem->exists(filepath)) { - SendResponse(550, "File " + Line[1] + " not found"); + SendResponse(FtpCodes::FILE_NOT_FOUND, "File " + Line[1] + " not found"); return; } _fromSet = true; _from = filepath; - SendResponse(350, "RNFR accepted - file exists, ready for destination"); + SendResponse(FtpCodes::FILE_ACTION_PENDING, "RNFR accepted - file exists, ready for destination"); } void to(const FTPPath &WorkDirectory, const std::vector &Line) { if (!_fromSet) { - SendResponse(503, "Need RNFR before RNTO"); + SendResponse(FtpCodes::BAD_SEQUENCE, "Need RNFR before RNTO"); return; } String filepath = WorkDirectory.getFilePath(Line[1]); if (_Filesystem->exists(filepath)) { - SendResponse(553, "File " + Line[1] + " already exists"); + SendResponse(FtpCodes::FILE_NAME_NOT_ALLOWED, "File " + Line[1] + " already exists"); return; } if (_Filesystem->rename(_from, filepath)) { - SendResponse(250, "File successfully renamed or moved"); + SendResponse(FtpCodes::FILE_ACTION_OK, "File successfully renamed or moved"); } else { - SendResponse(451, "Rename/move failure"); + SendResponse(FtpCodes::FILE_ACTION_ABORTED_LOCAL_ERROR, "Rename/move failure"); } _fromSet = false; _from = ""; diff --git a/src/Commands/STAT.h b/src/Commands/STAT.h index c726bb7..2e4baef 100644 --- a/src/Commands/STAT.h +++ b/src/Commands/STAT.h @@ -4,6 +4,7 @@ #include #include "../FTPCommand.h" +#include "../FTPResponseCodes.h" #include "../common.h" class STAT : public FTPCommand { @@ -14,7 +15,7 @@ class STAT : public FTPCommand { void run(FTPPath &WorkDirectory, const std::vector &Line) override { File dir = _Filesystem->open(WorkDirectory.getPath()); // if (!dir || !dir.isDirectory()) { - SendResponse(550, "Can't open directory " + WorkDirectory.getPath()); + SendResponse(FtpCodes::FILE_NOT_FOUND, "Can't open directory " + WorkDirectory.getClearPath()); return; } int cnt = 0; @@ -33,12 +34,12 @@ class STAT : public FTPCommand { for (int i = 0; i < fill_cnt; i++) { data_print(" "); } - data_println(filesize + " Jan 01 1970 " + filename); + data_println(filesize + " Jan 01 1970 " + WorkDirectory.reparse(filename)); cnt++; f.close(); f = dir.openNextFile(); } - SendResponse(226, String(cnt) + " matches total"); + SendResponse(FtpCodes::TRANSFER_COMPLETE, String(cnt) + " matches total"); } }; diff --git a/src/Commands/STOR.h b/src/Commands/STOR.h index 9b215d6..a5be0bc 100644 --- a/src/Commands/STOR.h +++ b/src/Commands/STOR.h @@ -10,7 +10,7 @@ class STOR : public FTPCommandTransfer { public: - explicit STOR(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort) : FTPCommandTransfer("STOR", 1, Client, Filesystem, DataAddress, DataPort) { + explicit STOR(WiFiClient *const Client, FTPFilesystem *const Filesystem, IPAddress *DataAddress, int *DataPort, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommandTransfer("STOR", 1, Client, Filesystem, DataAddress, DataPort, PassiveServer, PassiveMode) { } void run(FTPPath &WorkDirectory, const std::vector &Line) override { @@ -23,7 +23,7 @@ class STOR : public FTPCommandTransfer { _ftpFsFilePath = WorkDirectory.getFilePath(Line[1]); _file = _Filesystem->open(_ftpFsFilePath, "w"); if (!_file) { - SendResponse(451, "Can't open/create " + _ftpFsFilePath); + SendResponse(FtpCodes::FILE_ACTION_ABORTED_LOCAL_ERROR, "Can't open/create " + _ftpFsFilePath); CloseDataConnection(); return; } @@ -35,18 +35,18 @@ class STOR : public FTPCommandTransfer { int nb = data_read(buffer, FTP_BUF_SIZE); if (nb > 0) { const auto wb = _file.write(buffer, nb); - if (wb != nb) { + if (wb != static_cast::type>(nb)) { _file.close(); this->_Filesystem->remove(_ftpFsFilePath.c_str()); - SendResponse(552, "Error occured while STORing"); + SendResponse(FtpCodes::EXCEEDED_STORAGE, "Error occured while STORing"); CloseDataConnection(); } return; } - SendResponse(226, "File successfully transferred"); + SendResponse(FtpCodes::TRANSFER_COMPLETE, "File successfully transferred"); CloseDataConnection(); _file.close(); } diff --git a/src/Commands/TYPE.h b/src/Commands/TYPE.h index 522e2ab..60ab12f 100644 --- a/src/Commands/TYPE.h +++ b/src/Commands/TYPE.h @@ -12,13 +12,13 @@ class TYPE : public FTPCommand { void run(FTPPath &WorkDirectory, const std::vector &Line) override { if (Line[1] == "A") { - SendResponse(200, "TYPE is now ASCII"); + SendResponse(FtpCodes::COMMAND_OK, "TYPE is now ASCII"); return; } else if (Line[1] == "I") { - SendResponse(200, "TYPE is now 8-bit binary"); + SendResponse(FtpCodes::COMMAND_OK, "TYPE is now 8-bit binary"); return; } - SendResponse(504, "Unknow TYPE"); + SendResponse(FtpCodes::COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER, "Unknown TYPE"); } }; diff --git a/src/ESP-FTP-Server-Lib.cpp b/src/ESP-FTP-Server-Lib.cpp index 752448a..c96b07d 100644 --- a/src/ESP-FTP-Server-Lib.cpp +++ b/src/ESP-FTP-Server-Lib.cpp @@ -37,7 +37,7 @@ bool isNotConnected(const std::shared_ptr &con) { void FTPServer::handle() { if (_Server.hasClient()) { - std::shared_ptr connection = std::shared_ptr(new FTPConnection(_Server.available(), _UserList, _Filesystem)); + std::shared_ptr connection = std::shared_ptr(new FTPConnection(_Server.accept(), _UserList, _Filesystem)); _Connections.push_back(connection); } for (std::shared_ptr con : _Connections) { diff --git a/src/FTPCommand.h b/src/FTPCommand.h index 0b5a088..a8753e4 100644 --- a/src/FTPCommand.h +++ b/src/FTPCommand.h @@ -3,16 +3,23 @@ #include #include +#include #include #include "FTPFilesystem.h" #include "FTPPath.h" +#include "FTPResponseCodes.h" class FTPCommand { public: - FTPCommand(String Name, int ExpectedArgumentCnt, WiFiClient *const Client, FTPFilesystem *const Filesystem = 0, IPAddress *DataAddress = 0, int *DataPort = 0) : _Name(Name), _ExpectedArgumentCnt(ExpectedArgumentCnt), _Filesystem(Filesystem), _DataAddress(DataAddress), _DataPort(DataPort), _Client(Client), _DataConnection(0) { + FTPCommand(String Name, int ExpectedArgumentCnt, WiFiClient *const Client, FTPFilesystem *const Filesystem = 0, IPAddress *DataAddress = 0, int *DataPort = 0, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : _Name(Name), _ExpectedArgumentCnt(ExpectedArgumentCnt), _Filesystem(Filesystem), _DataAddress(DataAddress), _DataPort(DataPort), _PassiveServer(PassiveServer), _PassiveMode(PassiveMode), _Client(Client), _DataConnection(0) { } virtual ~FTPCommand() { + if (_DataConnection != 0) { + _DataConnection->stop(); + delete _DataConnection; + _DataConnection = 0; + } } String getName() const { @@ -34,13 +41,43 @@ class FTPCommand { if (_DataConnection->connected()) { _DataConnection->stop(); } + if (_PassiveMode != 0 && *_PassiveMode) { + if (_PassiveServer == 0 || *_PassiveServer == 0) { + SendResponse(FtpCodes::NO_DATA_CONNECTION, "No passive server"); + return false; + } + WiFiServer *server = *_PassiveServer; + const unsigned long passiveAcceptTimeoutMs = 100; + const unsigned long passivePollDelayMs = 5; + unsigned long start = millis(); + while (!server->hasClient() && millis() - start < passiveAcceptTimeoutMs) { + yield(); + delay(passivePollDelayMs); + } + if (!server->hasClient()) { + StopPassiveServer(); + SendResponse(FtpCodes::NO_DATA_CONNECTION, "No data connection"); + return false; + } + WiFiClient client = server->accept(); + if (!client) { + StopPassiveServer(); + SendResponse(FtpCodes::NO_DATA_CONNECTION, "No data connection"); + return false; + } + *_DataConnection = client; + StopPassiveServer(); + *_PassiveMode = false; + SendResponse(FtpCodes::DATA_CONNECTION_OPEN, "Accepted data connection"); + return true; + } _DataConnection->connect(*_DataAddress, *_DataPort); if (!_DataConnection->connected()) { _DataConnection->stop(); - SendResponse(425, "No data connection"); + SendResponse(FtpCodes::NO_DATA_CONNECTION, "No data connection"); return false; } - SendResponse(150, "Accepted data connection"); + SendResponse(FtpCodes::DATA_CONNECTION_OPEN, "Accepted data connection"); return true; } @@ -69,11 +106,35 @@ class FTPCommand { if ((_DataConnection != 0) && (_DataConnection->available() > 0)) { return _DataConnection->readBytes(c, l); } + // Brief wait for initial data (max 500ms in 1ms intervals) + if (_DataConnection != 0) { + for (int i = 0; i < 500; i++) { + delay(1); + if (_DataConnection->available() > 0) { + return _DataConnection->readBytes(c, l); + } + } + } return 0; } void CloseDataConnection() { - _DataConnection->stop(); + if (_DataConnection != 0) { + _DataConnection->stop(); + } + if (_PassiveMode != 0 && *_PassiveMode) { + StopPassiveServer(); + *_PassiveMode = false; + } + } + +private: + void StopPassiveServer() { + if (_PassiveServer != 0 && *_PassiveServer != 0) { + (*_PassiveServer)->stop(); + delete *_PassiveServer; + *_PassiveServer = 0; + } } protected: @@ -82,6 +143,8 @@ class FTPCommand { FTPFilesystem *const _Filesystem; IPAddress *const _DataAddress; int *const _DataPort; + WiFiServer **const _PassiveServer; + bool *const _PassiveMode; private: WiFiClient *const _Client; @@ -90,7 +153,7 @@ class FTPCommand { class FTPCommandTransfer : public FTPCommand { public: - FTPCommandTransfer(String Name, int ExpectedArgumentCnt, WiFiClient *const Client, FTPFilesystem *const Filesystem = 0, IPAddress *DataAddress = 0, int *DataPort = 0) : FTPCommand(Name, ExpectedArgumentCnt, Client, Filesystem, DataAddress, DataPort) { + FTPCommandTransfer(String Name, int ExpectedArgumentCnt, WiFiClient *const Client, FTPFilesystem *const Filesystem = 0, IPAddress *DataAddress = 0, int *DataPort = 0, WiFiServer **PassiveServer = 0, bool *PassiveMode = 0) : FTPCommand(Name, ExpectedArgumentCnt, Client, Filesystem, DataAddress, DataPort, PassiveServer, PassiveMode) { } virtual void workOnData() = 0; @@ -102,7 +165,7 @@ class FTPCommandTransfer : public FTPCommand { void abort() { if (_file) { CloseDataConnection(); - SendResponse(426, "Transfer aborted"); + SendResponse(FtpCodes::CONNECTION_CLOSED, "Transfer aborted"); _file.close(); } } diff --git a/src/FTPConnection.cpp b/src/FTPConnection.cpp index f801b44..4140e91 100644 --- a/src/FTPConnection.cpp +++ b/src/FTPConnection.cpp @@ -9,6 +9,7 @@ #include "Commands/MKD.h" #include "Commands/MLSD.h" #include "Commands/NLST.h" +#include "Commands/PASV.h" #include "Commands/PORT.h" #include "Commands/PWD.h" #include "Commands/RETR.h" @@ -20,19 +21,20 @@ #include "ESP-FTP-Server-Lib.h" #include "common.h" -FTPConnection::FTPConnection(const WiFiClient &Client, std::list &UserList, FTPFilesystem &Filesystem) : _ClientState(Idle), _Client(Client), _Filesystem(Filesystem), _UserList(UserList), _AuthUsername("") { - std::shared_ptr retr = std::shared_ptr(new RETR(&_Client, &_Filesystem, &_DataAddress, &_DataPort)); - std::shared_ptr stor = std::shared_ptr(new STOR(&_Client, &_Filesystem, &_DataAddress, &_DataPort)); +FTPConnection::FTPConnection(const WiFiClient &Client, std::list &UserList, FTPFilesystem &Filesystem) : _ClientState(Idle), _Client(Client), _Filesystem(Filesystem), _UserList(UserList), _AuthUsername(""), _PassiveServer(nullptr), _PassiveMode(false) { + std::shared_ptr retr = std::shared_ptr(new RETR(&_Client, &_Filesystem, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode)); + std::shared_ptr stor = std::shared_ptr(new STOR(&_Client, &_Filesystem, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode)); _FTPCommands.push_back(std::shared_ptr(new CDUP(&_Client))); _FTPCommands.push_back(std::shared_ptr(new CWD(&_Client, &_Filesystem))); _FTPCommands.push_back(std::shared_ptr(new DELE(&_Client, &_Filesystem))); _FTPCommands.push_back(std::shared_ptr(new STAT(&_Client, &_Filesystem))); - _FTPCommands.push_back(std::shared_ptr(new LIST(&_Client, &_Filesystem, &_DataAddress, &_DataPort))); - _FTPCommands.push_back(std::shared_ptr(new NLST(&_Client, &_Filesystem, &_DataAddress, &_DataPort))); - _FTPCommands.push_back(std::shared_ptr(new MLSD(&_Client, &_Filesystem, &_DataAddress, &_DataPort))); + _FTPCommands.push_back(std::shared_ptr(new LIST(&_Client, &_Filesystem, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode))); + _FTPCommands.push_back(std::shared_ptr(new NLST(&_Client, &_Filesystem, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode))); + _FTPCommands.push_back(std::shared_ptr(new MLSD(&_Client, &_Filesystem, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode))); _FTPCommands.push_back(std::shared_ptr(new MKD(&_Client, &_Filesystem))); - _FTPCommands.push_back(std::shared_ptr(new PORT(&_Client, &_DataAddress, &_DataPort))); + _FTPCommands.push_back(std::shared_ptr(new PORT(&_Client, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode))); + _FTPCommands.push_back(std::shared_ptr(new PASV(&_Client, &_DataAddress, &_DataPort, &_PassiveServer, &_PassiveMode))); _FTPCommands.push_back(std::shared_ptr(new PWD(&_Client))); _FTPCommands.push_back(retr); _FTPCommands.push_back(std::shared_ptr(new RMD(&_Client, &_Filesystem))); @@ -60,6 +62,11 @@ FTPConnection::FTPConnection(const WiFiClient &Client, std::list &UserL } FTPConnection::~FTPConnection() { + if (_PassiveServer != nullptr) { + _PassiveServer->stop(); + delete _PassiveServer; + _PassiveServer = nullptr; + } #ifndef NO_GLOBAL_INSTANCES Serial.println("Connection closed!"); #else @@ -70,9 +77,18 @@ FTPConnection::~FTPConnection() { bool FTPConnection::readUntilLineEnd() { while (_Client.available()) { char c = _Client.read(); + if (c == '\r') + continue; // Ignore carriage returns if (c == '\n') { - uint32_t index_separator = _Line.indexOf(' '); - _LineSplited = {_Line.substring(0, index_separator), _Line.substring(index_separator + 1, _Line.length())}; + int index_separator = _Line.indexOf(' '); + if (index_separator != -1) { + // Correctly split into Command and Arguments + _LineSplited = {_Line.substring(0, index_separator), _Line.substring(index_separator + 1)}; + } else { + // No space found? Arguments are empty. + _LineSplited = {_Line, ""}; + } + _Line = ""; // Reset buffer for next line return true; } if (c >= 32) { @@ -101,6 +117,7 @@ bool FTPConnection::handle() { logPrintlnD(_Line); #endif String command = _LineSplited[0]; + command.toUpperCase(); // This commands are always possible: if (command == "SYST") { @@ -123,9 +140,10 @@ bool FTPConnection::handle() { _Line = ""; return true; } else if (command == "FEAT") { - _Client.println("211- Extensions suported:"); + _Client.println("211- Extensions supported:"); _Client.println(" UTF8"); _Client.println(" MLSD"); + _Client.println(" PASV"); _Client.println("211 End."); _Line = ""; return true; @@ -143,6 +161,10 @@ bool FTPConnection::handle() { _Client.println("226 Data connection closed"); _Line = ""; return true; + } else if (command == "CLNT") { + _Client.println("200 Ok"); + _Line = ""; + return true; } // Logged in? diff --git a/src/FTPConnection.h b/src/FTPConnection.h index 24e97c8..327aa04 100644 --- a/src/FTPConnection.h +++ b/src/FTPConnection.h @@ -46,8 +46,10 @@ class FTPConnection { std::list &_UserList; String _AuthUsername; - IPAddress _DataAddress; - int _DataPort; + IPAddress _DataAddress; + int _DataPort; + WiFiServer *_PassiveServer; + bool _PassiveMode; FTPPath _WorkDirectory; diff --git a/src/FTPFilesystem.h b/src/FTPFilesystem.h index 2d8836d..d5547bb 100644 --- a/src/FTPFilesystem.h +++ b/src/FTPFilesystem.h @@ -107,7 +107,9 @@ class FTPFileImpl : public fs::FileImpl { return true; }; const char *fullName() const override { - return ("/" + _Name).c_str(); + // Update the cached full path and return its pointer + _FullName = "/" + _Name; + return _FullName.c_str(); }; bool isFile() const override { return true; @@ -147,6 +149,7 @@ class FTPFileImpl : public fs::FileImpl { private: String _Name; + mutable String _FullName; // Cache for fullName() to avoid dangling pointer std::list _Filesystems; }; diff --git a/src/FTPPath.cpp b/src/FTPPath.cpp index 57749ec..95c0d55 100644 --- a/src/FTPPath.cpp +++ b/src/FTPPath.cpp @@ -12,6 +12,7 @@ FTPPath::~FTPPath() { } void FTPPath::changePath(String path) { + path = sanitize(path); std::list p = splitPath(path); if (!path.isEmpty() && path[0] == '/') { _Path.assign(p.begin(), p.end()); @@ -29,14 +30,19 @@ String FTPPath::getPath() const { return createPath(_Path); } +String FTPPath::getClearPath() const { + return reparse(createPath(_Path)); +} + String FTPPath::getFilePath(String filename) const { - if (*filename.begin() == '/') { - return filename; + String sane_filename = sanitize(filename); + if (*sane_filename.begin() == '/') { + return sane_filename; } if (_Path.size() == 0) { - return "/" + filename; + return "/" + sane_filename; } - return getPath() + "/" + filename; + return getPath() + "/" + sane_filename; } std::list FTPPath::splitPath(String path) { @@ -52,7 +58,7 @@ std::list FTPPath::splitPath(String path) { return p; } -String FTPPath::createPath(std::list path) { +String FTPPath::createPath(const std::list &path) { if (path.size() == 0) { return "/"; } @@ -63,3 +69,53 @@ String FTPPath::createPath(std::list path) { } return new_path; } + +String FTPPath::sanitize(String input) const { +#ifndef ENABLE_FTP_SANITIZATION + return input; +#else + String output = ""; + // Pre-allocate memory to prevent heap fragmentation + output.reserve(input.length()); + + for (size_t i = 0; i < input.length(); i++) { + unsigned char c = input[i]; + // Check for illegal chars or percent sign + if (getInvalidChars().indexOf(c) != -1) { + output += '%'; + output += to_hex(c >> 4); + output += to_hex(c & 0x0F); + } else { + output += (char)c; + } + } + return output; +#endif +} + +String FTPPath::reparse(String input) const { +#ifndef ENABLE_FTP_SANITIZATION + return input; +#else + String output = ""; + output.reserve(input.length()); + + for (size_t i = 0; i < input.length(); i++) { + if (input[i] == '%' && i + 2 < input.length()) { + int high = from_hex(input[i + 1]); + int low = from_hex(input[i + 2]); + + if (high != -1 && low != -1) { + char c = (char)((high << 4) | low); + if (getInvalidChars().indexOf(c) != -1) { + output += c; + i += 2; // Skip the two hex characters + continue; + } + } + } + output += input[i]; + } + return output; +#endif +} diff --git a/src/FTPPath.h b/src/FTPPath.h index 7f62ba1..70f41ee 100644 --- a/src/FTPPath.h +++ b/src/FTPPath.h @@ -14,12 +14,39 @@ class FTPPath { void goPathUp(); String getPath() const; + String getClearPath() const; String getFilePath(String filename) const; static std::list splitPath(String path); - static String createPath(std::list path); + static String createPath(const std::list &path); + + String sanitize(String input) const; + String reparse(String input) const; private: +#ifdef ENABLE_FTP_SANITIZATION + static const String &getInvalidChars() { + static const String invalidChars = ":*?\"<>|%"; + return invalidChars; + } + + // Helper to convert nibble to hex character + char to_hex(unsigned char v) const { + return v < 10 ? '0' + v : 'A' + (v - 10); + } + + // Helper to convert hex character to nibble + int from_hex(char c) const { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + return -1; + } +#endif + std::list _Path; }; diff --git a/src/FTPResponseCodes.h b/src/FTPResponseCodes.h new file mode 100644 index 0000000..e50df33 --- /dev/null +++ b/src/FTPResponseCodes.h @@ -0,0 +1,47 @@ +#ifndef FTP_RESPONSE_CODES_H_ +#define FTP_RESPONSE_CODES_H_ + +// FTP Response Code Constants +namespace FtpCodes { +// Success codes +const int DATA_CONNECTION_OPEN = 150; +const int COMMAND_OK = 200; +const int SYSTEM_STATUS = 211; +const int DIRECTORY_STATUS = 212; +const int FILE_STATUS = 213; +const int HELP_MESSAGE = 214; +const int SYSTEM_TYPE = 215; +const int READY = 220; +const int CLOSING = 221; +const int TRANSFER_COMPLETE = 226; +const int ENTERING_PASV_MODE = 227; +const int LOGGED_IN = 230; +const int FILE_ACTION_OK = 250; +const int PATHNAME_CREATED = 257; +const int USER_OK = 331; +const int NEED_PASSWORD = 331; +const int FILE_ACTION_PENDING = 350; + +// Error codes +const int SYNTAX_ERROR = 500; +const int SYNTAX_ERROR_PARAMS = 501; +const int COMMAND_NOT_IMPLEMENTED = 502; +const int BAD_SEQUENCE = 503; +const int COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER = 504; +const int NOT_LOGGED_IN = 530; +const int NEED_ACCOUNT = 532; +const int FILE_ACTION_NOT_TAKEN = 550; +const int FILE_NOT_FOUND = 550; +const int PAGE_TYPE_UNKNOWN = 551; +const int EXCEEDED_STORAGE = 552; +const int FILE_NAME_NOT_ALLOWED = 553; +const int TRANSFER_ABORTED = 426; +const int NO_DATA_CONNECTION = 425; +const int CANNOT_OPEN_CONNECTION = 425; +const int CONNECTION_CLOSED = 426; +const int FILE_ACTION_ABORTED = 450; +const int FILE_ACTION_ABORTED_LOCAL_ERROR = 451; +const int INSUFFICIENT_STORAGE = 452; +} // namespace FtpCodes + +#endif diff --git a/src/common.h b/src/common.h index 4b15edf..23b14ed 100644 --- a/src/common.h +++ b/src/common.h @@ -2,6 +2,7 @@ #define COMMON_H_ #include +#include #include #include @@ -20,4 +21,28 @@ template T Split(String str, char parser) { return str_array; } +inline String ExtractPathFromOptions(const String &args) { + if (args.isEmpty()) { + return String(); + } + std::vector tokens = Split>(args, ' '); + tokens.erase(std::remove_if(tokens.begin(), tokens.end(), + [](const String &s) { + return s.isEmpty(); + }), + tokens.end()); + + for (size_t i = 0; i < tokens.size(); ++i) { + if (!tokens[i].startsWith("-")) { + String path = tokens[i]; + for (size_t j = i + 1; j < tokens.size(); ++j) { + path += " "; + path += tokens[j]; + } + return path; + } + } + return String(); +} + #endif