// // ftpworker.cpp // // mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi // Copyright (C) 2020-2023 Dale Whinham // // This file is part of mt32-pi. // // mt32-pi 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. // // mt32-pi 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 // mt32-pi. If not, see . // //#define FTPDAEMON_DEBUG #include #include #include #include #include #include #include #include "ftpworker.h" #include "utility.h" // Use a per-instance name for the log macros #define From m_LogName constexpr u16 PassivePortBase = 9000; constexpr size_t TextBufferSize = 512; constexpr unsigned int SocketTimeout = 20; constexpr unsigned int NumRetries = 3; #ifndef MT32_PI_VERSION #define MT32_PI_VERSION "(version unknown)" #endif const char MOTDBanner[] = "Welcome to the mt32-pi " MT32_PI_VERSION " embedded FTP server!"; enum class TDirectoryListEntryType { File, Directory, }; struct TDirectoryListEntry { char Name[FF_LFN_BUF + 1]; TDirectoryListEntryType Type; u32 nSize; u16 nLastModifedDate; u16 nLastModifedTime; }; using TCommandHandler = bool (CFTPWorker::*)(const char* pArgs); struct TFTPCommand { const char* pCmdStr; TCommandHandler pHandler; }; const TFTPCommand CFTPWorker::Commands[] = { { "SYST", &CFTPWorker::System }, { "USER", &CFTPWorker::Username }, { "PASS", &CFTPWorker::Password }, { "TYPE", &CFTPWorker::Type }, { "PASV", &CFTPWorker::Passive }, { "PORT", &CFTPWorker::Port }, { "RETR", &CFTPWorker::Retrieve }, { "STOR", &CFTPWorker::Store }, { "DELE", &CFTPWorker::Delete }, { "RMD", &CFTPWorker::Delete }, { "MKD", &CFTPWorker::MakeDirectory }, { "CWD", &CFTPWorker::ChangeWorkingDirectory }, { "CDUP", &CFTPWorker::ChangeToParentDirectory }, { "PWD", &CFTPWorker::PrintWorkingDirectory }, { "LIST", &CFTPWorker::List }, { "NLST", &CFTPWorker::ListFileNames }, { "RNFR", &CFTPWorker::RenameFrom }, { "RNTO", &CFTPWorker::RenameTo }, { "BYE", &CFTPWorker::Bye }, { "QUIT", &CFTPWorker::Bye }, { "NOOP", &CFTPWorker::NoOp }, }; u8 CFTPWorker::s_nInstanceCount = 0; // Volume names from ffconf.h // TODO: Share with soundfontmanager.cpp const char* const VolumeNames[] = { FF_VOLUME_STRS }; bool ValidateVolumeName(const char* pVolumeName) { for (const auto pName : VolumeNames) { if (strcasecmp(pName, pVolumeName) == 0) return true; } return false; } // Comparator for sorting directory listings inline bool DirectoryCaseInsensitiveAscending(const TDirectoryListEntry& EntryA, const TDirectoryListEntry& EntryB) { // Directories first in ascending order if (EntryA.Type != EntryB.Type) return EntryA.Type == TDirectoryListEntryType::Directory; return strncasecmp(EntryA.Name, EntryB.Name, sizeof(TDirectoryListEntry::Name)) < 0; } CFTPWorker::CFTPWorker(CSocket* pControlSocket, const char* pExpectedUser, const char* pExpectedPassword) : CTask(TASK_STACK_SIZE), m_LogName(), m_pExpectedUser(pExpectedUser), m_pExpectedPassword(pExpectedPassword), m_pControlSocket(pControlSocket), m_pDataSocket(nullptr), m_nDataSocketPort(0), m_DataSocketIPAddress(), m_CommandBuffer{'\0'}, m_DataBuffer{0}, m_User(), m_Password(), m_DataType(TDataType::ASCII), m_TransferMode(TTransferMode::Active), m_CurrentPath(), m_RenameFrom() { ++s_nInstanceCount; m_LogName.Format("ftpd[%d]", s_nInstanceCount); } CFTPWorker::~CFTPWorker() { if (m_pControlSocket) delete m_pControlSocket; if (m_pDataSocket) delete m_pDataSocket; --s_nInstanceCount; LOGNOTE("Instance count is now %d", s_nInstanceCount); } void CFTPWorker::Run() { assert(m_pControlSocket != nullptr); const size_t nWorkerNumber = s_nInstanceCount; CScheduler* const pScheduler = CScheduler::Get(); LOGNOTE("Worker task %d spawned", nWorkerNumber); if (!SendStatus(TFTPStatus::ReadyForNewUser, MOTDBanner)) return; CTimer* const pTimer = CTimer::Get(); unsigned int nTimeout = pTimer->GetTicks(); while (m_pControlSocket) { // Block while waiting to receive #ifdef FTPDAEMON_DEBUG LOGDBG("Waiting for command"); #endif const int nReceiveBytes = m_pControlSocket->Receive(m_CommandBuffer, sizeof(m_CommandBuffer), MSG_DONTWAIT); if (nReceiveBytes == 0) { if (pTimer->GetTicks() - nTimeout >= SocketTimeout * HZ) { LOGERR("Socket timed out"); break; } pScheduler->Yield(); continue; } if (nReceiveBytes < 0) { LOGNOTE("Connection closed"); break; } // FIXME m_CommandBuffer[nReceiveBytes - 2] = '\0'; #ifdef FTPDAEMON_DEBUG const u8* pIPAddress = m_pControlSocket->GetForeignIP(); LOGDBG("<-- Received %d bytes from %d.%d.%d.%d: '%s'", nReceiveBytes, pIPAddress[0], pIPAddress[1], pIPAddress[2], pIPAddress[3], m_CommandBuffer); #endif char* pSavePtr; char* pToken = strtok_r(m_CommandBuffer, " \r\n", &pSavePtr); if (!pToken) { LOGERR("String tokenization error (received: '%s')", m_CommandBuffer); continue; } TCommandHandler pHandler = nullptr; for (size_t i = 0; i < Utility::ArraySize(Commands); ++i) { if (strcasecmp(pToken, Commands[i].pCmdStr) == 0) { pHandler = Commands[i].pHandler; break; } } if (pHandler) (this->*pHandler)(pSavePtr); else SendStatus(TFTPStatus::CommandNotImplemented, "Command not implemented."); nTimeout = pTimer->GetTicks(); } LOGNOTE("Worker task %d shutting down", nWorkerNumber); delete m_pControlSocket; m_pControlSocket = nullptr; } CSocket* CFTPWorker::OpenDataConnection() { CSocket* pDataSocket = nullptr; u8 nRetries = NumRetries; while (pDataSocket == nullptr && nRetries > 0) { // Active: Create new socket and connect to client if (m_TransferMode == TTransferMode::Active) { CNetSubSystem* const pNet = CNetSubSystem::Get(); pDataSocket = new CSocket(pNet, IPPROTO_TCP); if (pDataSocket == nullptr) { SendStatus(TFTPStatus::DataConnectionFailed, "Could not open socket."); return nullptr; } if (pDataSocket->Connect(m_DataSocketIPAddress, m_nDataSocketPort) < 0) { SendStatus(TFTPStatus::DataConnectionFailed, "Could not connect to data port."); delete pDataSocket; pDataSocket = nullptr; } } // Passive: Use previously-created socket and accept connection from client else if (m_TransferMode == TTransferMode::Passive && m_pDataSocket != nullptr) { CIPAddress ClientIPAddress; u16 nClientPort; pDataSocket = m_pDataSocket->Accept(&ClientIPAddress, &nClientPort); } --nRetries; } if (pDataSocket == nullptr) { LOGERR("Unable to open data socket after %d attempts", NumRetries); SendStatus(TFTPStatus::DataConnectionFailed, "Couldn't open data connection."); } return pDataSocket; } bool CFTPWorker::SendStatus(TFTPStatus StatusCode, const char* pMessage) { assert(m_pControlSocket != nullptr); const int nLength = snprintf(m_CommandBuffer, sizeof(m_CommandBuffer), "%d %s\r\n", StatusCode, pMessage); if (m_pControlSocket->Send(m_CommandBuffer, nLength, 0) < 0) { LOGERR("Failed to send status"); return false; } #ifdef FTPDAEMON_DEBUG else { m_CommandBuffer[nLength - 2] = '\0'; LOGDBG("--> Sent: '%s'", m_CommandBuffer); } #endif return true; } bool CFTPWorker::CheckLoggedIn() { #ifdef FTPDAEMON_DEBUG LOGDBG("Username compare: expected '%s', actual '%s'", static_cast(m_pExpectedUser), static_cast(m_User)); LOGDBG("Password compare: expected '%s', actual '%s'", static_cast(m_pExpectedPassword), static_cast(m_Password)); #endif if (m_User.Compare(m_pExpectedUser) == 0 && m_Password.Compare(m_pExpectedPassword) == 0) return true; SendStatus(TFTPStatus::NotLoggedIn, "Not logged in."); return false; } CString CFTPWorker::RealPath(const char* pInBuffer) const { assert(pInBuffer != nullptr); CString Path; const bool bAbsolute = pInBuffer[0] == '/'; if (bAbsolute) { char Buffer[TextBufferSize]; FTPPathToFatFsPath(pInBuffer, Buffer, sizeof(Buffer)); Path = Buffer; } else Path.Format("%s/%s", static_cast(m_CurrentPath), pInBuffer); return Path; } const TDirectoryListEntry* CFTPWorker::BuildDirectoryList(size_t& nOutEntries) const { DIR Dir; FILINFO FileInfo; FRESULT Result; TDirectoryListEntry* pEntries = nullptr; nOutEntries = 0; // Volume list if (m_CurrentPath.GetLength() == 0) { constexpr size_t nVolumes = Utility::ArraySize(VolumeNames); bool VolumesAvailable[nVolumes] = { false }; for (size_t i = 0; i < nVolumes; ++i) { char VolumeName[6]; strncpy(VolumeName, VolumeNames[i], sizeof(VolumeName)); strcat(VolumeName, ":"); // Returns FR_ if ((Result = f_opendir(&Dir, VolumeName)) == FR_OK) { f_closedir(&Dir); VolumesAvailable[i] = true; ++nOutEntries; } } pEntries = new TDirectoryListEntry[nOutEntries]; size_t nCurrentEntry = 0; for (size_t i = 0; i < nVolumes && nCurrentEntry < nOutEntries; ++i) { if (VolumesAvailable[i]) { TDirectoryListEntry& Entry = pEntries[nCurrentEntry++]; strncpy(Entry.Name, VolumeNames[i], sizeof(Entry.Name)); Entry.Type = TDirectoryListEntryType::Directory; Entry.nSize = 0; Entry.nLastModifedDate = 0; Entry.nLastModifedTime = 0; } } return pEntries; } // Directory list Result = f_findfirst(&Dir, &FileInfo, m_CurrentPath, "*"); if (Result == FR_OK && *FileInfo.fname) { // Count how many entries we need do { ++nOutEntries; Result = f_findnext(&Dir, &FileInfo); } while (Result == FR_OK && *FileInfo.fname); f_closedir(&Dir); if (nOutEntries && (pEntries = new TDirectoryListEntry[nOutEntries])) { size_t nCurrentEntry = 0; Result = f_findfirst(&Dir, &FileInfo, m_CurrentPath, "*"); while (Result == FR_OK && *FileInfo.fname) { TDirectoryListEntry& Entry = pEntries[nCurrentEntry++]; strncpy(Entry.Name, FileInfo.fname, sizeof(Entry.Name)); if (FileInfo.fattrib & AM_DIR) { Entry.Type = TDirectoryListEntryType::Directory; Entry.nSize = 0; } else { Entry.Type = TDirectoryListEntryType::File; Entry.nSize = FileInfo.fsize; } Entry.nLastModifedDate = FileInfo.fdate; Entry.nLastModifedTime = FileInfo.ftime; Result = f_findnext(&Dir, &FileInfo); } f_closedir(&Dir); Utility::QSort(pEntries, DirectoryCaseInsensitiveAscending, 0, nOutEntries - 1); } } return pEntries; } bool CFTPWorker::System(const char* pArgs) { // Some FTP clients (e.g. Directory Opus) will only attempt to parse LIST responses as IIS/DOS-style if we pretend to be Windows NT SendStatus(TFTPStatus::SystemType, "Windows_NT"); return true; } bool CFTPWorker::Username(const char* pArgs) { m_User = pArgs; char Buffer[TextBufferSize]; snprintf(Buffer, sizeof(Buffer), "Password required for '%s'.", static_cast(m_User)); SendStatus(TFTPStatus::PasswordRequired, Buffer); return true; } bool CFTPWorker::Port(const char* pArgs) { if (!CheckLoggedIn()) return false; char Buffer[TextBufferSize]; strncpy(Buffer, pArgs, sizeof(Buffer)); if (m_pDataSocket != nullptr) { delete m_pDataSocket; m_pDataSocket = nullptr; } m_TransferMode = TTransferMode::Active; // TODO: PORT IP Address should match original IP address u8 PortBytes[6]; char* pSavePtr; char* pToken = strtok_r(Buffer, " ,", &pSavePtr); bool bParseError = (pToken == nullptr); if (!bParseError) { PortBytes[0] = static_cast(atoi(pToken)); for (u8 i = 0; i < 5; ++i) { pToken = strtok_r(nullptr, " ,", &pSavePtr); if (pToken == nullptr) { bParseError = true; break; } PortBytes[i + 1] = static_cast(atoi(pToken)); } } if (bParseError) { SendStatus(TFTPStatus::SyntaxError, "Syntax error."); return false; } m_DataSocketIPAddress.Set(PortBytes); m_nDataSocketPort = (PortBytes[4] << 8) + PortBytes[5]; #ifdef FTPDAEMON_DEBUG CString IPAddressString; m_DataSocketIPAddress.Format(&IPAddressString); LOGDBG("PORT set to: %s:%d", static_cast(IPAddressString), m_nDataSocketPort); #endif SendStatus(TFTPStatus::Success, "Command OK."); return true; } bool CFTPWorker::Passive(const char* pArgs) { if (!CheckLoggedIn()) return false; if (m_pDataSocket == nullptr) { m_TransferMode = TTransferMode::Passive; m_nDataSocketPort = PassivePortBase + s_nInstanceCount - 1; CNetSubSystem* const pNet = CNetSubSystem::Get(); m_pDataSocket = new CSocket(pNet, IPPROTO_TCP); if (m_pDataSocket == nullptr) { SendStatus(TFTPStatus::ServiceNotAvailable, "Failed to open port for passive mode."); return false; } if (m_pDataSocket->Bind(m_nDataSocketPort) < 0) { SendStatus(TFTPStatus::DataConnectionFailed, "Could not bind to data port."); delete m_pDataSocket; m_pDataSocket = nullptr; return false; } if (m_pDataSocket->Listen() < 0) { SendStatus(TFTPStatus::DataConnectionFailed, "Could not listen on data port."); delete m_pDataSocket; m_pDataSocket = nullptr; return false; } } u8 IPAddress[IP_ADDRESS_SIZE]; CNetSubSystem::Get()->GetConfig()->GetIPAddress()->CopyTo(IPAddress); char Buffer[TextBufferSize]; snprintf(Buffer, sizeof(Buffer), "Entering passive mode (%d,%d,%d,%d,%d,%d).", IPAddress[0], IPAddress[1], IPAddress[2], IPAddress[3], (m_nDataSocketPort >> 8) & 0xFF, m_nDataSocketPort & 0xFF ); SendStatus(TFTPStatus::EnteringPassiveMode, Buffer); return true; } bool CFTPWorker::Password(const char* pArgs) { if (m_User.GetLength() == 0) { SendStatus(TFTPStatus::AccountRequired, "Need account for login."); return false; } m_Password = pArgs; if (!CheckLoggedIn()) return false; SendStatus(TFTPStatus::UserLoggedIn, "User logged in."); return true; } bool CFTPWorker::Type(const char* pArgs) { if (!CheckLoggedIn()) return false; if (strcasecmp(pArgs, "A") == 0) { m_DataType = TDataType::ASCII; SendStatus(TFTPStatus::Success, "Type set to ASCII."); return true; } if (strcasecmp(pArgs, "I") == 0) { m_DataType = TDataType::Binary; SendStatus(TFTPStatus::Success, "Type set to binary."); return true; } SendStatus(TFTPStatus::SyntaxError, "Syntax error."); return false; } bool CFTPWorker::Retrieve(const char* pArgs) { if (!CheckLoggedIn()) return false; FIL File; CString Path = RealPath(pArgs); if (f_open(&File, Path, FA_READ) != FR_OK) { SendStatus(TFTPStatus::FileActionNotTaken, "Could not open file for reading."); return false; } if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) return false; CSocket* pDataSocket = OpenDataConnection(); if (pDataSocket == nullptr) return false; size_t nSize = f_size(&File); size_t nSent = 0; while (nSent < nSize) { UINT nBytesRead; #ifdef FTPDAEMON_DEBUG LOGDBG("Sending data"); #endif if (f_read(&File, m_DataBuffer, sizeof(m_DataBuffer), &nBytesRead) != FR_OK || pDataSocket->Send(m_DataBuffer, nBytesRead, 0) < 0) { delete pDataSocket; f_close(&File); SendStatus(TFTPStatus::ActionAborted, "File action aborted, local error."); return false; } nSent += nBytesRead; assert(nSent <= nSize); } delete pDataSocket; f_close(&File); SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); return false; } bool CFTPWorker::Store(const char* pArgs) { if (!CheckLoggedIn()) return false; FIL File; CString Path = RealPath(pArgs); if (f_open(&File, Path, FA_CREATE_ALWAYS | FA_WRITE) != FR_OK) { SendStatus(TFTPStatus::FileActionNotTaken, "Could not open file for writing."); return false; } f_sync(&File); if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) return false; CSocket* pDataSocket = OpenDataConnection(); if (pDataSocket == nullptr) return false; bool bSuccess = true; CTimer* const pTimer = CTimer::Get(); unsigned int nTimeout = pTimer->GetTicks(); while (true) { #ifdef FTPDAEMON_DEBUG LOGDBG("Waiting to receive"); #endif int nReceiveResult = pDataSocket->Receive(m_DataBuffer, sizeof(m_DataBuffer), MSG_DONTWAIT); FRESULT nWriteResult; UINT nWritten; if (nReceiveResult == 0) { if (pTimer->GetTicks() - nTimeout >= SocketTimeout * HZ) { LOGERR("Socket timed out"); bSuccess = false; break; } CScheduler::Get()->Yield(); continue; } // All done if (nReceiveResult < 0) { LOGNOTE("Receive done, no more data"); break; } #ifdef FTPDAEMON_DEBUG //LOGDBG("Received %d bytes", nReceiveResult); #endif if ((nWriteResult = f_write(&File, m_DataBuffer, nReceiveResult, &nWritten)) != FR_OK) { LOGERR("Write FAILED, return code %d", nWriteResult); bSuccess = false; break; } f_sync(&File); CScheduler::Get()->Yield(); nTimeout = pTimer->GetTicks(); } if (bSuccess) SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); else SendStatus(TFTPStatus::ActionAborted, "File action aborted, local error."); #ifdef FTPDAEMON_DEBUG LOGDBG("Closing socket/file"); #endif delete pDataSocket; f_close(&File); return true; } bool CFTPWorker::Delete(const char* pArgs) { if (!CheckLoggedIn()) return false; CString Path = RealPath(pArgs); if (f_unlink(Path) != FR_OK) SendStatus(TFTPStatus::FileActionNotTaken, "File was not deleted."); else SendStatus(TFTPStatus::FileActionOk, "File deleted."); return true; } bool CFTPWorker::MakeDirectory(const char* pArgs) { if (!CheckLoggedIn()) return false; CString Path = RealPath(pArgs); if (f_mkdir(Path) != FR_OK) SendStatus(TFTPStatus::FileActionNotTaken, "Directory creation failed."); else { char Buffer[TextBufferSize]; FatFsPathToFTPPath(Path, Buffer, sizeof(Buffer)); strcat(Buffer, " directory created."); SendStatus(TFTPStatus::PathCreated, Buffer); } return true; } bool CFTPWorker::ChangeWorkingDirectory(const char* pArgs) { if (!CheckLoggedIn()) return false; char Buffer[TextBufferSize]; bool bSuccess = false; const bool bAbsolute = pArgs[0] == '/'; if (bAbsolute) { // Root if (pArgs[1] == '\0') { m_CurrentPath = ""; bSuccess = true; } else { DIR Dir; FTPPathToFatFsPath(pArgs, Buffer, sizeof(Buffer)); // f_stat() will fail if we're trying to CWD to the root of a volume, so use f_opendir() if (f_opendir(&Dir, Buffer) == FR_OK) { f_closedir(&Dir); m_CurrentPath = Buffer; bSuccess = true; } } } else { const bool bAtRoot = m_CurrentPath.GetLength() == 0; if (bAtRoot) { if (ValidateVolumeName(pArgs)) { m_CurrentPath.Format("%s:", pArgs); bSuccess = true; } } else { CString NewPath; NewPath.Format("%s/%s", static_cast(m_CurrentPath), pArgs); if (f_stat(NewPath, nullptr) == FR_OK) { m_CurrentPath = NewPath; bSuccess = true; } } } if (bSuccess) { const bool bAtRoot = m_CurrentPath.GetLength() == 0; if (bAtRoot) strncpy(Buffer, "\"/\"", sizeof(Buffer)); else FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); SendStatus(TFTPStatus::FileActionOk, Buffer); } else SendStatus(TFTPStatus::FileNotFound, "Directory unavailable."); return bSuccess; } bool CFTPWorker::ChangeToParentDirectory(const char* pArgs) { if (!CheckLoggedIn()) return false; char Buffer[TextBufferSize]; bool bSuccess = false; bool bAtRoot = m_CurrentPath.GetLength() == 0; if (!bAtRoot) { DIR Dir; FatFsParentPath(m_CurrentPath, Buffer, sizeof(Buffer)); bAtRoot = Buffer[0] == '\0'; if (bAtRoot) { m_CurrentPath = Buffer; bSuccess = true; } else if (f_opendir(&Dir, Buffer) == FR_OK) { f_closedir(&Dir); m_CurrentPath = Buffer; bSuccess = true; } } if (bSuccess) { bAtRoot = m_CurrentPath.GetLength() == 0; if (bAtRoot) strncpy(Buffer, "\"/\"", sizeof(Buffer)); else FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); SendStatus(TFTPStatus::FileActionOk, Buffer); } else SendStatus(TFTPStatus::FileNotFound, "Directory unavailable."); return false; } bool CFTPWorker::PrintWorkingDirectory(const char* pArgs) { if (!CheckLoggedIn()) return false; char Buffer[TextBufferSize]; const bool bAtRoot = m_CurrentPath.GetLength() == 0; if (bAtRoot) strncpy(Buffer, "\"/\"", sizeof(Buffer)); else FatFsPathToFTPPath(m_CurrentPath, Buffer, sizeof(Buffer)); SendStatus(TFTPStatus::PathCreated, Buffer); return true; } bool CFTPWorker::List(const char* pArgs) { if (!CheckLoggedIn()) return false; if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) return false; CSocket* pDataSocket = OpenDataConnection(); if (pDataSocket == nullptr) return false; char Buffer[TextBufferSize]; char Date[9]; char Time[8]; size_t nEntries; const TDirectoryListEntry* pDirEntries = BuildDirectoryList(nEntries); if (pDirEntries) { for (size_t i = 0; i < nEntries; ++i) { const TDirectoryListEntry& Entry = pDirEntries[i]; int nLength; // Mimic the Microsoft IIS LIST format FormatLastModifiedDate(Entry.nLastModifedDate, Date, sizeof(Date)); FormatLastModifiedTime(Entry.nLastModifedTime, Time, sizeof(Time)); if (Entry.Type == TDirectoryListEntryType::Directory) nLength = snprintf(Buffer, sizeof(Buffer), "%-9s %-13s %-14s %s\r\n", Date, Time, "", Entry.Name); else nLength = snprintf(Buffer, sizeof(Buffer), "%-9s %-13s %14d %s\r\n", Date, Time, Entry.nSize, Entry.Name); if (pDataSocket->Send(Buffer, nLength, 0) < 0) { delete[] pDirEntries; delete pDataSocket; SendStatus(TFTPStatus::DataConnectionFailed, "Transfer error."); return false; } } delete[] pDirEntries; } delete pDataSocket; SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); return true; } bool CFTPWorker::ListFileNames(const char* pArgs) { if (!CheckLoggedIn()) return false; if (!SendStatus(TFTPStatus::FileStatusOk, "Command OK.")) return false; CSocket* pDataSocket = OpenDataConnection(); if (pDataSocket == nullptr) return false; char Buffer[TextBufferSize]; size_t nEntries; const TDirectoryListEntry* pDirEntries = BuildDirectoryList(nEntries); if (pDirEntries) { for (size_t i = 0; i < nEntries; ++i) { const TDirectoryListEntry& Entry = pDirEntries[i]; if (Entry.Type == TDirectoryListEntryType::Directory) continue; const int nLength = snprintf(Buffer, sizeof(Buffer), "%s\r\n", Entry.Name); if (pDataSocket->Send(Buffer, nLength, 0) < 0) { delete[] pDirEntries; delete pDataSocket; SendStatus(TFTPStatus::DataConnectionFailed, "Transfer error."); return false; } } delete[] pDirEntries; } delete pDataSocket; SendStatus(TFTPStatus::TransferComplete, "Transfer complete."); return true; } bool CFTPWorker::RenameFrom(const char* pArgs) { if (!CheckLoggedIn()) return false; m_RenameFrom = pArgs; SendStatus(TFTPStatus::PendingFurtherInfo, "Requested file action pending further information."); return false; } bool CFTPWorker::RenameTo(const char* pArgs) { if (!CheckLoggedIn()) return false; if (m_RenameFrom.GetLength() == 0) { SendStatus(TFTPStatus::BadCommandSequence, "Bad sequence of commands."); return false; } CString SourcePath = RealPath(m_RenameFrom); CString DestPath = RealPath(pArgs); if (f_rename(SourcePath, DestPath) != FR_OK) SendStatus(TFTPStatus::FileNameNotAllowed, "File name not allowed."); else SendStatus(TFTPStatus::FileActionOk, "File renamed."); m_RenameFrom = ""; return false; } bool CFTPWorker::Bye(const char* pArgs) { SendStatus(TFTPStatus::ClosingControl, "Goodbye."); delete m_pControlSocket; m_pControlSocket = nullptr; return true; } bool CFTPWorker::NoOp(const char* pArgs) { SendStatus(TFTPStatus::Success, "Command OK."); return true; } void CFTPWorker::FatFsPathToFTPPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) { assert(pOutBuffer && nSize > 2); const char* pEnd = pOutBuffer + nSize; const char* pInChar = pInBuffer; char* pOutChar = pOutBuffer; *pOutChar++ = '"'; *pOutChar++ = '/'; while (*pInChar != '\0' && pOutChar < pEnd) { // Kill the volume colon if (*pInChar == ':') { *pOutChar++ = '/'; ++pInChar; // Kill any slashes after the colon while (*pInChar == '/') ++pInChar; continue; } // Kill duplicate slashes if (*pInChar == '/') { *pOutChar++ = *pInChar++; while (*pInChar == '/') ++pInChar; continue; } *pOutChar++ = *pInChar++; } // Kill trailing slash if (*(pOutChar - 1) == '/') --pOutChar; assert(pOutChar < pEnd - 2); *pOutChar++ = '"'; *pOutChar++ = '\0'; } void CFTPWorker::FTPPathToFatFsPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) { assert(pInBuffer && pOutBuffer); const char* pEnd = pOutBuffer + nSize; const char* pInChar = pInBuffer; char* pOutChar = pOutBuffer; // Kill leading slashes while (*pInChar == '/') ++pInChar; bool bGotVolume = false; while (*pInChar != '\0' && pOutChar < pEnd) { // Kill the volume colon if (!bGotVolume && *pInChar == '/') { bGotVolume = true; *pOutChar++ = ':'; ++pInChar; // Kill any slashes after the colon while (*pInChar == '/') ++pInChar; continue; } // Kill duplicate slashes if (*pInChar == '/') { *pOutChar++ = *pInChar++; while (*pInChar == '/') ++pInChar; continue; } *pOutChar++ = *pInChar++; } assert(pOutChar < pEnd - 2); // Kill trailing slash if (*(pOutChar - 1) == '/') --pOutChar; // Add volume colon if (!bGotVolume) *pOutChar++ = ':'; *pOutChar++ = '\0'; } void CFTPWorker::FatFsParentPath(const char* pInBuffer, char* pOutBuffer, size_t nSize) { assert(pInBuffer != nullptr && pOutBuffer != nullptr); size_t nLength = strlen(pInBuffer); assert(nLength > 0 && nSize >= nLength); const char* pLastChar = pInBuffer + nLength - 1; const char* pInChar = pLastChar; // Kill trailing slashes while (*pInChar == '/' && pInChar > pInBuffer) --pInChar; // Kill subdirectory name while (*pInChar != '/' && *pInChar != ':' && pInChar > pInBuffer) --pInChar; // Kill trailing slashes while (*pInChar == '/' && pInChar > pInBuffer) --pInChar; // Pointer didn't move (we're already at a volume root), or we reached the start of the string (path invalid) if (pInChar == pLastChar || pInChar == pInBuffer) { *pOutBuffer = '\0'; return; } // Truncate string nLength = pInChar - pInBuffer + 1; memcpy(pOutBuffer, pInBuffer, nLength); pOutBuffer[nLength] = '\0'; } void CFTPWorker::FormatLastModifiedDate(u16 nDate, char* pOutBuffer, size_t nSize) { // 2-digit year const u16 nYear = (1980 + (nDate >> 9)) % 100; u16 nMonth = (nDate >> 5) & 0x0F; u16 nDay = nDate & 0x1F; if (nMonth == 0) nMonth = 1; if (nDay == 0) nDay = 1; snprintf(pOutBuffer, nSize, "%02d-%02d-%02d", nMonth, nDay, nYear); } void CFTPWorker::FormatLastModifiedTime(u16 nDate, char* pOutBuffer, size_t nSize) { u16 nHour = (nDate >> 11) & 0x1F; const u16 nMinute = (nDate >> 5) & 0x3F; const char* pSuffix = nHour < 12 ? "AM" : "PM"; if (nHour == 0) nHour = 12; else if (nHour >= 12) nHour -= 12; snprintf(pOutBuffer, nSize, "%02d:%02d%s", nHour, nMinute, pSuffix); }