From 87242a9e9b40c215c829ccfbe0dfd7f330417d5d Mon Sep 17 00:00:00 2001 From: Daniel Plasa Date: Thu, 28 May 2020 22:12:01 +0200 Subject: Add FTP Client --- FTPClient.cpp | 316 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ FTPClient.h | 119 ++++++++++++++++++++++ FTPCommon.cpp | 146 +++++++++++++++++++++++++++ FTPCommon.h | 131 ++++++++++++++++++++++++ 4 files changed, 712 insertions(+) create mode 100644 FTPClient.cpp create mode 100644 FTPClient.h create mode 100644 FTPCommon.cpp create mode 100644 FTPCommon.h diff --git a/FTPClient.cpp b/FTPClient.cpp new file mode 100644 index 0000000..c6cccc1 --- /dev/null +++ b/FTPClient.cpp @@ -0,0 +1,316 @@ +#include "FTPClient.h" + +// helper macro +#define CLIENT_SEND(fmt, ...) \ + do \ + { \ + FTP_DEBUG_MSG(">>> " fmt, ##__VA_ARGS__); \ + control.printf_P(PSTR(fmt "\n"), ##__VA_ARGS__); \ + } while (0) + +FTPClient::FTPClient(FS &_FSImplementation) : FTPCommon(_FSImplementation) +{ +} + +void FTPClient::begin(const ServerInfo &theServer) +{ + _server = &theServer; +} + +const FTPClient::Status &FTPClient::transfer(const String &localFileName, const String &remoteFileName, TransferType direction) +{ + _serverStatus.result = PROGRESS; + if (ftpState >= cIdle) + { + _remoteFileName = remoteFileName; + _direction = direction; + + if (direction & FTP_GET) + file = THEFS.open(localFileName, "w"); + else if (direction & FTP_PUT) + file = THEFS.open(localFileName, "r"); + + if (!file) + { + _serverStatus.result = ERROR; + _serverStatus.code = 65530; + _serverStatus.desc = F("Local file error"); + } + else + { + ftpState = cConnect; + if (direction & 0x80) + { + while (ftpState <= cQuit) + { + handleFTP(); + delay(25); + } + } + } + } + else + { + // return error code with status "in PROGRESS" + _serverStatus.code = 65529; + } + return _serverStatus; +} + +const FTPClient::Status &FTPClient::check() +{ + return _serverStatus; +} + +void FTPClient::handleFTP() +{ + if (_server == nullptr) + { + _serverStatus.result = TransferResult::ERROR; + _serverStatus.code = 65535; + _serverStatus.desc = F("begin() not called"); + } + else if (ftpState > cIdle) + { + _serverStatus.result = TransferResult::ERROR; + } + else if (cConnect == ftpState) + { + _serverStatus.code = 65534; + _serverStatus.desc = F("No connection to FTP server"); + if (controlConnect()) + { + FTP_DEBUG_MSG("Connection to %s:%u established", control.remoteIP().toString().c_str(), control.remotePort()); + _serverStatus.result = TransferResult::PROGRESS; + ftpState = cGreet; + } + else + { + ftpState = cError; + } + } + else if (cGreet == ftpState) + { + if (waitFor(220 /* 220 (vsFTPd version) */, F("No server greeting"))) + { + CLIENT_SEND("USER %s", _server->login.c_str()); + ftpState = cUser; + } + } + else if (cUser == ftpState) + { + if (waitFor(331 /* 331 Password */)) + { + CLIENT_SEND("PASS %s", _server->password.c_str()); + ftpState = cPassword; + } + } + else if (cPassword == ftpState) + { + if (waitFor(230 /* 230 Login successful*/)) + { + CLIENT_SEND("PASV"); + ftpState = cPassive; + } + } + else if (cPassive == ftpState) + { + if (waitFor(227 /* 227 Entering Passive Mode (ip,ip,ip,ip,port,port) */)) + { + bool parseOK = false; + // find () + uint8_t bracketOpen = _serverStatus.desc.indexOf(F("(")); + uint8_t bracketClose = _serverStatus.desc.indexOf(F(")")); + if (bracketOpen && (bracketClose > bracketOpen)) + { + FTP_DEBUG_MSG("Parsing PASV response %s", _serverStatus.desc.c_str()); + _serverStatus.desc[bracketClose] = '\0'; + if (parseDataIpPort(_serverStatus.desc.c_str() + bracketOpen + 1)) + { + // catch ip=0.0.0.0 and replace with the control.remoteIP() + if (dataIP.toString() == F("0.0.0.0")) + { + dataIP = control.remoteIP(); + } + parseOK = true; + ftpState = cData; + } + } + if (!parseOK) + { + _serverStatus.code = 65533; + _serverStatus.desc = F("FTP server response not understood."); + } + } + } + else if (cData == ftpState) + { + // open data connection + if (dataConnect() < 0) + { + _serverStatus.code = 65532; + _serverStatus.desc = F("No data connection to FTP server"); + ftpState = cError; + } + else + { + FTP_DEBUG_MSG("Data connection to %s:%u established", data.remoteIP().toString().c_str(), data.remotePort()); + millisBeginTrans = millis(); + bytesTransfered = 0; + ftpState = cTransfer; + if (_direction & FTP_PUT_NONBLOCKING) + { + CLIENT_SEND("STOR %s", _remoteFileName.c_str()); + allocateBuffer(file.size()); + } + else if (_direction & FTP_GET_NONBLOCKING) + { + CLIENT_SEND("RETR %s", _remoteFileName.c_str()); + allocateBuffer(2048); + } + } + } + else if (cTransfer == ftpState) + { + bool res = true; + if (_direction & FTP_PUT_NONBLOCKING) + { + res = doFiletoNetwork(); + } + else + { + res = doNetworkToFile(); + } + if (!res || !data.connected()) + { + ftpState = cFinish; + } + } + else if (cFinish == ftpState) + { + closeTransfer(); + ftpState = cQuit; + } + else if (cQuit == ftpState) + { + CLIENT_SEND("QUIT"); + _serverStatus.result = OK; + ftpState = cIdle; + } + else if (cIdle == ftpState) + { + stop(); + } +} + +int8_t FTPClient::controlConnect() +{ + if (_server->validateCA) + { + FTP_DEBUG_MSG("Ignoring CA verification - FTP only"); + } + control.connect(_server->servername, _server->port); + FTP_DEBUG_MSG("Connection to %s:%d ... %S", _server->servername.c_str(), _server->port, control.connected() ? PSTR("OK") : PSTR("failed")); + if (control.connected()) + return 1; + return -1; +} + +bool FTPClient::waitFor(const uint16_t respCode, const __FlashStringHelper *errorString, uint16_t timeOut) +{ + // initalize waiting + if (0 == waitUntil) + { + waitUntil = millis(); + waitUntil += timeOut; + _serverStatus.desc.clear(); + } + else + { + // timeout + if ((int32_t)(millis() - waitUntil) >= 0) + { + FTP_DEBUG_MSG("Waiting for code %u - timeout!", respCode); + _serverStatus.code = 65535; + if (errorString) + { + _serverStatus.desc = errorString; + } + else + { + _serverStatus.desc = F("timeout"); + } + ftpState = cTimeout; + waitUntil = 0; + return false; + } + + // check for bytes from the client + while (control.available()) + { + char c = control.read(); + //FTP_DEBUG_MSG("readChar() line='%s' <= %c", _serverStatus.desc.c_str(), c); + if (c == '\n' || c == '\r') + { + // filter out empty lines + _serverStatus.desc.trim(); + if (0 == _serverStatus.desc.length()) + continue; + + // line complete, evaluate code + _serverStatus.code = strtol(_serverStatus.desc.c_str(), NULL, 0); + if (respCode != _serverStatus.code) + { + ftpState = cError; + FTP_DEBUG_MSG("Waiting for code %u but SMTP server replies: %s", respCode, _serverStatus.desc.c_str()); + } + else + { + FTP_DEBUG_MSG("Waiting for code %u success, SMTP server replies: %s", respCode, _serverStatus.desc.c_str()); + } + + waitUntil = 0; + return (respCode == _serverStatus.code); + } + else + { + // just add the char + _serverStatus.desc += c; + } + } + } + return false; +} + +/* +bool SMTPSSender::connect() +{ + client = new WiFiClientSecure(); + if (NULL == client) + return false; + + DEBUG_MSG("%SCA validation!", _server->validateCA ? PSTR("") : PSTR("NO ")); + + if (_server->validateCA == false) + { + // disable CA checks + reinterpret_cast(client)->setInsecure(); + } + + // Determine if MFLN is supported by a server + // if it returns true, use the ::setBufferSizes(rx, tx) to shrink + // the needed BearSSL memory while staying within protocol limits. + bool mfln = reinterpret_cast(client)->probeMaxFragmentLength(_server->servername, _server->port, 512); + + DEBUG_MSG("MFLN %Ssupported", mfln ? PSTR("") : PSTR("un")); + + if (mfln) + { + reinterpret_cast(client)->setBufferSizes(512, 512); + } + + reinterpret_cast(client)->connect(_server->servername, _server->port); + return reinterpret_cast(client)->connected(); +} + +*/ \ No newline at end of file diff --git a/FTPClient.h b/FTPClient.h new file mode 100644 index 0000000..d252add --- /dev/null +++ b/FTPClient.h @@ -0,0 +1,119 @@ +/** \mainpage FTPClient library + * + * MIT license + * written by Daniel Plasa: + * 1. split into a plain FTP and a FTPS class to save code space in case only FTP is needed. + * 2. Supply two ways of getting/putting files: + * a) blocking, returns only after transfer complete (or error) + * b) non-blocking, returns immedeate. call check() for status of process + * + * When using non-blocking mode, be sure to call update() frequently, e.g. in loop(). + */ + +#ifndef FTP_CLIENT_H +#define FTP_CLIENT_H + +/******************************************************************************* + ** ** + ** DEFINITIONS FOR FTP SERVER/CLIENT ** + ** ** + *******************************************************************************/ +#include +#include "FTPCommon.h" + +class FTPClient : public FTPCommon +{ +public: + struct ServerInfo + { + ServerInfo(const String &_l, const String &_pw, const String &_sn, uint16_t _p = 21, bool v = false) : login(_l), password(_pw), servername(_sn), port(_p), validateCA(v) {} + ServerInfo() = default; + String login; + String password; + String servername; + uint16_t port; + bool authTLS = false; + bool validateCA = false; + }; + + typedef enum + { + OK, + PROGRESS, + ERROR, + } TransferResult; + + typedef struct + { + TransferResult result; + uint16_t code; + String desc; + } Status; + + typedef enum + { + FTP_PUT = 1 | 0x80, + FTP_GET = 2 | 0x80, + FTP_PUT_NONBLOCKING = FTP_PUT & 0x7f, + FTP_GET_NONBLOCKING = FTP_GET & 0x7f, + } TransferType; + + // contruct an instance of the FTP Client using a + // given FS object, e.g. SPIFFS or LittleFS + FTPClient(FS &_FSImplementation); + + // initialize FTP Client with the ftp server's credentials + void begin(const ServerInfo &server); + + // transfer a file (nonblocking via handleFTP() ) + const Status &transfer(const String &localFileName, const String &remoteFileName, TransferType direction = FTP_GET); + + // check status + const Status &check(); + + // call freqently (e.g. in loop()), when using non-blocking mode + void handleFTP(); + +protected: + typedef enum + { + cConnect = 0, + cGreet, + cUser, + cPassword, + cPassive, + cData, + cTransfer, + cFinish, + cQuit, + cIdle, + cTimeout, + cError + } internalState; + internalState ftpState = cIdle; + Status _serverStatus; + uint32_t waitUntil = 0; + const ServerInfo *_server = nullptr; + + String _remoteFileName; + TransferType _direction; + + int8_t controlConnect(); // connects to ServerInfo, returns -1: no connection possible, +1: connection established + + bool waitFor(const uint16_t respCode, const __FlashStringHelper *errorString = nullptr, uint16_t timeOut = 10000); +}; + +// basically just the same as FTPClient but has a different connect() method to account for SSL/TLS +// connection stuff +/* +class FTPSClient : public FTPClient +{ +public: + FTPSClient() = default; + +private: + virtual bool connect(); +}; +*/ + +#endif // FTP_CLIENT_H diff --git a/FTPCommon.cpp b/FTPCommon.cpp new file mode 100644 index 0000000..66211a8 --- /dev/null +++ b/FTPCommon.cpp @@ -0,0 +1,146 @@ +#include "FTPCommon.h" + +FTPCommon::FTPCommon(FS &_FSImplementation) : THEFS(_FSImplementation) +{ +} + +FTPCommon::~FTPCommon() +{ + stop(); +} + +void FTPCommon::stop() +{ + control.stop(); + data.stop(); + file.close(); + freeBuffer(); +} + +void FTPCommon::setTimeout(uint16_t timeout) +{ + sTimeOut = timeout; +} + +uint16_t FTPCommon::allocateBuffer(uint16_t desiredBytes) +{ + // allocate a big buffer for file transfers + uint16_t maxBlock = ESP.getMaxFreeBlockSize() / 2; + + if (desiredBytes > maxBlock) + desiredBytes = maxBlock; + + while (fileBuffer == NULL && desiredBytes > 0) + { + fileBuffer = (uint8_t *)malloc(desiredBytes); + if (NULL == fileBuffer) + { + FTP_DEBUG_MSG("Cannot allocate buffer for file transfer, re-trying"); + // try with less bytes + desiredBytes--; + } + else + { + fileBufferSize = desiredBytes; + } + } + return fileBufferSize; +} + +void FTPCommon::freeBuffer() +{ + free(fileBuffer); + fileBuffer = NULL; +} + +int8_t FTPCommon::dataConnect() +{ + // open our own data connection + data.stop(); + FTP_DEBUG_MSG("Open data connection to %s:%u", dataIP.toString().c_str(), dataPort); + data.connect(dataIP, dataPort); + return data.connected() ? 1 : -1; +} + +bool FTPCommon::parseDataIpPort(const char *p) +{ + // parse IP and data port of "ip,ip,ip,ip,port,port" + uint8_t parsecount = 0; + uint8_t tmp[6]; + while (parsecount < sizeof(tmp)) + { + tmp[parsecount++] = atoi(p); + p = strchr(p, ','); + if (NULL == p || *(++p) == '\0') + break; + } + if (parsecount >= sizeof(tmp)) + { + // copy first 4 bytes = IP + for (uint8_t i = 0; i < 4; ++i) + dataIP[i] = tmp[i]; + // data port is 5,6 + dataPort = tmp[4] * 256 + tmp[5]; + return true; + } + return false; +} + +bool FTPCommon::doFiletoNetwork() +{ + // data connection lost or no more bytes to transfer? + if (!data.connected() || (bytesTransfered >= file.size())) + { + return false; + } + + // how many bytes to transfer left? + uint32_t nb = (file.size() - bytesTransfered); + if (nb > fileBufferSize) + nb = fileBufferSize; + + // transfer the file + FTP_DEBUG_MSG("Transfer %d bytes fs->net", nb); + nb = file.readBytes((char *)fileBuffer, nb); + if (nb > 0) + { + data.write(fileBuffer, nb); + bytesTransfered += nb; + } + + return (nb > 0); +} + +bool FTPCommon::doNetworkToFile() +{ + // Avoid blocking by never reading more bytes than are available + int16_t navail = data.available(); + + if (navail > 0) + { + if (navail > fileBufferSize) + navail = fileBufferSize; + FTP_DEBUG_MSG("Transfer %d bytes net->FS", navail); + navail = data.read(fileBuffer, navail); + file.write(fileBuffer, navail); + bytesTransfered += navail; + } + + if (!data.connected() && (navail <= 0)) + { + // connection closed or no more bytes to read + return false; + } + else + { + // inidcate, we need to be called again + return true; + } +} + +void FTPCommon::closeTransfer() +{ + freeBuffer(); + file.close(); + data.stop(); +} diff --git a/FTPCommon.h b/FTPCommon.h new file mode 100644 index 0000000..55844f7 --- /dev/null +++ b/FTPCommon.h @@ -0,0 +1,131 @@ +#ifndef FTP_COMMON_H +#define FTP_COMMON_H + +#include +#include +#include +#include + +#define FTP_CTRL_PORT 21 // Command port on wich server is listening +#define FTP_DATA_PORT_PASV 50009 // Data port in passive mode +#define FTP_TIME_OUT 5 // Disconnect client after 5 minutes of inactivity +#define FTP_CMD_SIZE 127 // allow max. 127 chars in a received command + +// Use ESP8266 Core Debug functionality +#ifdef DEBUG_ESP_PORT +#define FTP_DEBUG_MSG(fmt, ...) \ + do \ + { \ + DEBUG_ESP_PORT.printf_P(PSTR("[FTP] " fmt "\n"), ##__VA_ARGS__); \ + yield(); \ + } while (0) +#else +#define FTP_DEBUG_MSG(...) +#endif + +#define FTP_CMD(CMD) (FTP_CMD_LE_##CMD) // make command +#define FTP_CMD_LE_USER 0x52455355 // "USER" as uint32_t (little endian) +#define FTP_CMD_BE_USER 0x55534552 // "USER" as uint32_t (big endian) +#define FTP_CMD_LE_PASS 0x53534150 // "PASS" as uint32_t (little endian) +#define FTP_CMD_BE_PASS 0x50415353 // "PASS" as uint32_t (big endian) +#define FTP_CMD_LE_QUIT 0x54495551 // "QUIT" as uint32_t (little endian) +#define FTP_CMD_BE_QUIT 0x51554954 // "QUIT" as uint32_t (big endian) +#define FTP_CMD_LE_CDUP 0x50554443 // "CDUP" as uint32_t (little endian) +#define FTP_CMD_BE_CDUP 0x43445550 // "CDUP" as uint32_t (big endian) +#define FTP_CMD_LE_CWD 0x00445743 // "CWD" as uint32_t (little endian) +#define FTP_CMD_BE_CWD 0x43574400 // "CWD" as uint32_t (big endian) +#define FTP_CMD_LE_PWD 0x00445750 // "PWD" as uint32_t (little endian) +#define FTP_CMD_BE_PWD 0x50574400 // "PWD" as uint32_t (big endian) +#define FTP_CMD_LE_MODE 0x45444f4d // "MODE" as uint32_t (little endian) +#define FTP_CMD_BE_MODE 0x4d4f4445 // "MODE" as uint32_t (big endian) +#define FTP_CMD_LE_PASV 0x56534150 // "PASV" as uint32_t (little endian) +#define FTP_CMD_BE_PASV 0x50415356 // "PASV" as uint32_t (big endian) +#define FTP_CMD_LE_PORT 0x54524f50 // "PORT" as uint32_t (little endian) +#define FTP_CMD_BE_PORT 0x504f5254 // "PORT" as uint32_t (big endian) +#define FTP_CMD_LE_STRU 0x55525453 // "STRU" as uint32_t (little endian) +#define FTP_CMD_BE_STRU 0x53545255 // "STRU" as uint32_t (big endian) +#define FTP_CMD_LE_TYPE 0x45505954 // "TYPE" as uint32_t (little endian) +#define FTP_CMD_BE_TYPE 0x54595045 // "TYPE" as uint32_t (big endian) +#define FTP_CMD_LE_ABOR 0x524f4241 // "ABOR" as uint32_t (little endian) +#define FTP_CMD_BE_ABOR 0x41424f52 // "ABOR" as uint32_t (big endian) +#define FTP_CMD_LE_DELE 0x454c4544 // "DELE" as uint32_t (little endian) +#define FTP_CMD_BE_DELE 0x44454c45 // "DELE" as uint32_t (big endian) +#define FTP_CMD_LE_LIST 0x5453494c // "LIST" as uint32_t (little endian) +#define FTP_CMD_BE_LIST 0x4c495354 // "LIST" as uint32_t (big endian) +#define FTP_CMD_LE_MLSD 0x44534c4d // "MLSD" as uint32_t (little endian) +#define FTP_CMD_BE_MLSD 0x4d4c5344 // "MLSD" as uint32_t (big endian) +#define FTP_CMD_LE_NLST 0x54534c4e // "NLST" as uint32_t (little endian) +#define FTP_CMD_BE_NLST 0x4e4c5354 // "NLST" as uint32_t (big endian) +#define FTP_CMD_LE_NOOP 0x504f4f4e // "NOOP" as uint32_t (little endian) +#define FTP_CMD_BE_NOOP 0x4e4f4f50 // "NOOP" as uint32_t (big endian) +#define FTP_CMD_LE_RETR 0x52544552 // "RETR" as uint32_t (little endian) +#define FTP_CMD_BE_RETR 0x52455452 // "RETR" as uint32_t (big endian) +#define FTP_CMD_LE_STOR 0x524f5453 // "STOR" as uint32_t (little endian) +#define FTP_CMD_BE_STOR 0x53544f52 // "STOR" as uint32_t (big endian) +#define FTP_CMD_LE_MKD 0x00444b4d // "MKD" as uint32_t (little endian) +#define FTP_CMD_BE_MKD 0x4d4b4400 // "MKD" as uint32_t (big endian) +#define FTP_CMD_LE_RMD 0x00444d52 // "RMD" as uint32_t (little endian) +#define FTP_CMD_BE_RMD 0x524d4400 // "RMD" as uint32_t (big endian) +#define FTP_CMD_LE_RNFR 0x52464e52 // "RNFR" as uint32_t (little endian) +#define FTP_CMD_BE_RNFR 0x524e4652 // "RNFR" as uint32_t (big endian) +#define FTP_CMD_LE_RNTO 0x4f544e52 // "RNTO" as uint32_t (little endian) +#define FTP_CMD_BE_RNTO 0x524e544f // "RNTO" as uint32_t (big endian) +#define FTP_CMD_LE_FEAT 0x54414546 // "FEAT" as uint32_t (little endian) +#define FTP_CMD_BE_FEAT 0x46454154 // "FEAT" as uint32_t (big endian) +#define FTP_CMD_LE_MDTM 0x4d54444d // "MDTM" as uint32_t (little endian) +#define FTP_CMD_BE_MDTM 0x4d44544d // "MDTM" as uint32_t (big endian) +#define FTP_CMD_LE_SIZE 0x455a4953 // "SIZE" as uint32_t (little endian) +#define FTP_CMD_BE_SIZE 0x53495a45 // "SIZE" as uint32_t (big endian) +#define FTP_CMD_LE_SITE 0x45544953 // "SITE" as uint32_t (little endian) +#define FTP_CMD_BE_SITE 0x53495445 // "SITE" as uint32_t (big endian) +#define FTP_CMD_LE_SYST 0x54535953 // "SYST" as uint32_t (little endian) +#define FTP_CMD_BE_SYST 0x53595354 // "SYST" as uint32_t (big endian) + +class FTPCommon +{ +public: + // contruct an instance of the FTP Server or Client using a + // given FS object, e.g. SPIFFS or LittleFS + FTPCommon(FS &_FSImplementation); + virtual ~FTPCommon(); + + // stops the FTP Server or Client + virtual void stop(); + + // set a 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 + virtual void handleFTP() = 0; + +protected: + WiFiClient control; + WiFiClient data; + + File file; + FS &THEFS; + + IPAddress dataIP; // IP address for PORT (active) mode + uint16_t dataPort = // holds our PASV port number or the port number provided by PORT + FTP_DATA_PORT_PASV; + virtual int8_t dataConnect(); // connects to dataIP:dataPort, returns -1: no data connection possible, +1: data connection established + bool parseDataIpPort(const char *p); + + uint16_t sTimeOut = // disconnect after 5 min of inactivity + FTP_TIME_OUT * 60; + + bool doFiletoNetwork(); + bool doNetworkToFile(); + virtual void closeTransfer(); + + uint16_t allocateBuffer(uint16_t desiredBytes); // allocate buffer for transfer + void freeBuffer(); + uint8_t *fileBuffer = NULL; // pointer to buffer for file transfer (by allocateBuffer) + uint16_t fileBufferSize; // size of buffer + + uint32_t millisBeginTrans; // store time of beginning of a transaction + uint32_t bytesTransfered; // bytes transfered +}; + +#endif // FTP_COMMON_H -- cgit v1.2.3