You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MiniDexed/src/net/ftpworker.cpp

1206 lines
27 KiB

//
// ftpworker.cpp
//
// mt32-pi - A baremetal MIDI synthesizer for Raspberry Pi
// Copyright (C) 2020-2023 Dale Whinham <daleyo@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
//#define FTPDAEMON_DEBUG
#include <circle/logger.h>
#include <circle/net/in.h>
#include <circle/net/netsubsystem.h>
#include <circle/sched/scheduler.h>
#include <circle/timer.h>
#include <fatfs/ff.h>
#include <cstdio>
#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<const char*>(m_pExpectedUser), static_cast<const char*>(m_User));
LOGDBG("Password compare: expected '%s', actual '%s'", static_cast<const char*>(m_pExpectedPassword), static_cast<const char*>(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<const char*>(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<const char*>(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<u8>(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<u8>(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<const char*>(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<const char*>(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, "<DIR>", 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);
}