// ftpd is a server implementation based on the following:
// - RFC 959 (https://tools.ietf.org/html/rfc959)
// - RFC 3659 (https://tools.ietf.org/html/rfc3659)
// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html
//
// Copyright (C) 2024 Michael Theall
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
#include "ftpSession.h"
#include "IOAbstraction.h"
#include "ftpServer.h"
#include "log.h"
#include "mdns.h"
#include "platform.h"
#if !defined(__WIIU__) && !defined(CLASSIC)
#include "imgui.h"
#endif
#include
#include
#include
#if FTPD_HAS_GLOB
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std::chrono_literals;
#if defined(__NDS__) || defined(__3DS__) || defined(__SWITCH__)
#define lstat stat
#endif
#ifdef __NDS__
#define LOCKED(x) x
#else
#define LOCKED(x) \
do \
{ \
auto const lock = std::scoped_lock (m_lock); \
x; \
} while (0)
#endif
namespace
{
/// \brief Idle timeout
constexpr auto IDLE_TIMEOUT = 60;
/// \brief Check if string view is a C string
/// \param str_ String to check
bool isCString (std::string_view const str_)
{
return str_.find_first_of ('\0') != std::string_view::npos;
}
/// \brief Case-insensitive string compare
/// \param lhs_ Left string
/// \param rhs_ Right string
int compare (std::string_view const lhs_, std::string_view const rhs_)
{
if (isCString (lhs_) && isCString (rhs_))
return ::strcasecmp (lhs_.data (), rhs_.data ());
auto const maxLen = std::min (lhs_.size (), rhs_.size ());
for (unsigned i = 0; i < maxLen; ++i)
{
auto const l = std::tolower (lhs_[i]);
auto const r = std::tolower (rhs_[i]);
if (l != r)
return l - r;
}
return gsl::narrow_cast (lhs_.size ()) - gsl::narrow_cast (rhs_.size ());
}
/// \brief Parse command
/// \param buffer_ Buffer to parse
/// \param size_ Size of buffer
/// \returns {delimiterPos, nextPos}
std::pair parseCommand (char *const buffer_, std::size_t const size_)
{
// look for \r\n or \n delimiter
auto const end = &buffer_[size_];
for (auto p = buffer_; p < end; ++p)
{
if (p[0] == '\r' && p < end - 1 && p[1] == '\n')
return {p, &p[2]};
if (p[0] == '\n')
return {p, &p[1]};
}
return {nullptr, nullptr};
}
/// \brief Decode path
/// \param buffer_ Buffer to decode
/// \param size_ Size of buffer
void decodePath (char *const buffer_, std::size_t const size_)
{
auto const end = &buffer_[size_];
for (auto p = buffer_; p < end; ++p)
{
// this is an encoded \n
if (*p == '\0')
*p = '\n';
}
}
/// \brief Encode path
/// \param buffer_ Buffer to encode
/// \param quotes_ Whether to encode quotes
std::string encodePath (std::string_view const buffer_, bool const quotes_ = false)
{
// check if the buffer has \n
bool const lf = std::memchr (buffer_.data (), '\n', buffer_.size ());
auto end = std::end (buffer_);
std::size_t numQuotes = 0;
if (quotes_)
{
// check for \" that needs to be encoded
auto p = buffer_.data ();
do
{
p = static_cast (std::memchr (p, '"', end - p));
if (p)
{
++p;
++numQuotes;
}
} while (p);
}
// if nothing needs escaping, return it as-is
if (!lf && !numQuotes)
return std::string (buffer_);
// reserve output buffer
std::string path (buffer_.size () + numQuotes, '\0');
auto in = buffer_.data ();
auto out = path.data ();
// encode into the output buffer
while (in < end)
{
if (*in == '\n')
{
// encoded \n is \0
*out++ = '\0';
}
else if (quotes_ && *in == '"')
{
// encoded \" is \"\"
*out++ = '"';
*out++ = '"';
}
else
*out++ = *in;
++in;
}
return path;
}
/// \brief Get parent directory name of a path
/// \param path_ Path to get parent of
std::string dirName (std::string_view const path_)
{
// remove last path component
auto const dir = std::string (path_.substr (0, path_.rfind ('/')));
if (dir.empty ())
return "/";
return dir;
}
/// \brief Resolve path
/// \param path_ Path to resolve
std::string resolvePath (std::string_view const path_)
{
assert (!path_.empty ());
assert (path_[0] == '/');
// make sure parent is a directory
struct stat st;
if (IOAbstraction::stat (dirName (path_).c_str (), &st) != 0)
return {};
if (!S_ISDIR (st.st_mode))
{
errno = ENOTDIR;
return {};
}
// split path components
std::vector components;
std::size_t pos = 1;
auto next = path_.find ('/', pos);
while (next != std::string::npos)
{
if (next != pos)
components.emplace_back (path_.substr (pos, next - pos));
pos = next + 1;
next = path_.find ('/', pos);
}
if (pos != path_.size ())
components.emplace_back (path_.substr (pos));
// collapse . and ..
auto it = std::begin (components);
while (it != std::end (components))
{
if (*it == ".")
{
it = components.erase (it);
continue;
}
if (*it == "..")
{
if (it != std::begin (components))
it = components.erase (std::prev (it));
it = components.erase (it);
continue;
}
++it;
}
// join path components
std::string outPath = "/";
for (auto const &component : components)
{
outPath += component;
outPath.push_back ('/');
}
if (outPath.size () > 1)
outPath.pop_back ();
return outPath;
}
/// \brief Build path from a parent and child
/// \param cwd_ Parent directory
/// \param args_ Child component
std::string buildPath (std::string_view const cwd_, std::string_view const args_)
{
std::string path;
// absolute path
if (args_[0] == '/')
path = std::string (args_);
// relative path
else
path = std::string (cwd_) + '/' + std::string (args_);
// coalesce consecutive slashes
auto it = std::begin (path);
while (it != std::end (path))
{
if (it != std::begin (path) && *it == '/' && *std::prev (it) == '/')
it = path.erase (it);
else
++it;
}
return path;
}
/// \brief Build resolved path from a parent and child
/// \param cwd_ Parent directory
/// \param args_ Child component
std::string buildResolvedPath (std::string_view const cwd_, std::string_view const args_)
{
return resolvePath (buildPath (cwd_, args_));
}
}
///////////////////////////////////////////////////////////////////////////
#if FTPD_HAS_GLOB
FtpSession::Glob::~Glob () noexcept
{
clear ();
}
FtpSession::Glob::Glob () noexcept = default;
bool FtpSession::Glob::glob (char const *const pattern_) noexcept
{
if (!m_glob.has_value ())
m_glob.emplace ();
else
::globfree (&m_glob.value ());
std::memset (&m_glob.value (), 0, sizeof (glob_t));
auto const rc = ::glob (pattern_, GLOB_NOSORT, nullptr, &m_glob.value ());
if (rc == GLOB_NOSPACE)
{
clear ();
errno = ENOMEM;
return false;
}
else if (rc != 0)
{
clear ();
errno = EIO;
return false;
}
m_offset = 0;
return true;
}
char const *FtpSession::Glob::next () noexcept
{
if (!m_glob.has_value ())
return nullptr;
if (m_glob->gl_pathc <= 0 || m_offset >= static_cast (m_glob->gl_pathc))
{
clear ();
return nullptr;
}
return m_glob->gl_pathv[m_offset++];
}
void FtpSession::Glob::clear () noexcept
{
if (!m_glob.has_value ())
return;
::globfree (&m_glob.value ());
m_glob.reset ();
}
#endif
///////////////////////////////////////////////////////////////////////////
FtpSession::~FtpSession ()
{
closeCommand ();
closePasv ();
closeData ();
}
FtpSession::FtpSession (FtpConfig &config_, UniqueSocket commandSocket_)
: m_config (config_),
m_commandSocket (std::move (commandSocket_)),
m_commandBuffer (COMMAND_BUFFERSIZE),
m_responseBuffer (RESPONSE_BUFFERSIZE),
m_xferBuffer (XFER_BUFFERSIZE),
m_authorizedUser (false),
m_authorizedPass (false),
m_pasv (false),
m_port (false),
m_recv (false),
m_send (false),
m_urgent (false),
m_mlstType (true),
m_mlstSize (true),
m_mlstModify (true),
m_mlstPerm (true),
m_mlstUnixMode (false),
m_devZero (false)
{
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
if (m_config.user ().empty ())
m_authorizedUser = true;
if (m_config.pass ().empty ())
m_authorizedPass = true;
}
char buffer[32];
std::sprintf (buffer, "Session#%p", this);
m_windowName = buffer;
std::sprintf (buffer, "Plot#%p", this);
m_plotName = buffer;
m_commandSocket->setNonBlocking ();
sendResponse ("220 Hello!\r\n");
}
bool FtpSession::dead ()
{
#ifndef __NDS__
auto const lock = std::scoped_lock (m_lock);
#endif
if (m_commandSocket || m_pasvSocket || m_dataSocket)
return false;
return true;
}
void FtpSession::draw ()
{
#ifndef __NDS__
auto const lock = std::scoped_lock (m_lock);
#endif
#ifdef CLASSIC
if (m_filePosition)
{
std::fputs (fs::printSize (m_filePosition).c_str (), stdout);
std::fputc (' ', stdout);
}
std::fputs (m_workItem.empty () ? m_cwd.c_str () : m_workItem.c_str (), stdout);
#else
#ifdef __3DS__
ImGui::BeginChild (m_windowName.c_str (), ImVec2 (0.0f, 45.0f), true);
#else
ImGui::BeginChild (m_windowName.c_str (), ImVec2 (0.0f, 80.0f), true);
#endif
if (!m_workItem.empty ())
ImGui::TextUnformatted (m_workItem.c_str ());
else
ImGui::TextUnformatted (m_cwd.c_str ());
if (m_fileSize)
ImGui::Text (
"%s/%s", fs::printSize (m_filePosition).c_str (), fs::printSize (m_fileSize).c_str ());
else if (m_filePosition)
ImGui::Text ("%s/???", fs::printSize (m_filePosition).c_str ());
if (m_fileSize || m_filePosition)
{
// MiB/s plot lines
for (std::size_t i = 0; i < POSITION_HISTORY - 1; ++i)
{
m_filePositionDeltas[i] = m_filePositionHistory[i + 1] - m_filePositionHistory[i];
m_filePositionHistory[i] = m_filePositionHistory[i + 1];
}
auto const diff = m_filePosition - m_filePositionHistory[POSITION_HISTORY - 1];
m_filePositionDeltas[POSITION_HISTORY - 1] = diff;
m_filePositionHistory[POSITION_HISTORY - 1] = m_filePosition;
if (m_xferRate == -1.0f)
{
m_xferRate = 0.0f;
m_filePositionTime = platform::steady_clock::now ();
}
else
{
auto const now = platform::steady_clock::now ();
auto const timeDiff = now - m_filePositionTime;
m_filePositionTime = now;
auto const rate =
gsl::narrow_cast (diff) / std::chrono::duration (timeDiff).count ();
auto const alpha = 0.01f;
m_xferRate = alpha * rate + (1.0f - alpha) * m_xferRate;
}
auto const rateString = fs::printSize (m_xferRate) + "/s";
ImGui::SameLine ();
ImGui::PlotLines (
"", m_filePositionDeltas, IM_ARRAYSIZE (m_filePositionDeltas), 0, rateString.c_str ());
}
ImGui::EndChild ();
#endif
}
void FtpSession::drawConnections ()
{
#ifndef CLASSIC
#ifdef NO_IPV6
char peerName[INET_ADDRSTRLEN];
char sockName[INET_ADDRSTRLEN];
#else
char peerName[INET6_ADDRSTRLEN];
char sockName[INET6_ADDRSTRLEN];
#endif
static char const *const stateStrings[] = {
"Command",
"Data Connect",
"Data Transfer",
};
ImGui::TextWrapped ("State: %s", stateStrings[static_cast (m_state)]);
if (m_commandSocket)
{
m_commandSocket->peerName ().name (peerName, sizeof (peerName));
m_commandSocket->sockName ().name (sockName, sizeof (sockName));
if (m_commandSocket == m_dataSocket)
ImGui::TextWrapped ("Command/Data %s -> %s", peerName, sockName);
else
ImGui::TextWrapped ("Command %s -> %s", peerName, sockName);
}
if (m_pasvSocket)
{
m_pasvSocket->sockName ().name (sockName, sizeof (sockName));
ImGui::TextWrapped ("PASV %s", sockName);
}
if (m_dataSocket && m_dataSocket != m_commandSocket)
{
m_dataSocket->peerName ().name (peerName, sizeof (peerName));
m_dataSocket->sockName ().name (sockName, sizeof (sockName));
ImGui::TextWrapped ("Data %s -> %s", peerName, sockName);
}
for (auto const &sock : m_pendingCloseSocket)
{
if (!sock)
continue;
sock->peerName ().name (peerName, sizeof (peerName));
sock->sockName ().name (sockName, sizeof (sockName));
ImGui::TextWrapped ("Closing %s -> %s", peerName, sockName);
}
#endif
}
UniqueFtpSession FtpSession::create (FtpConfig &config_, UniqueSocket commandSocket_)
{
return UniqueFtpSession (new FtpSession (config_, std::move (commandSocket_)));
}
bool FtpSession::poll (std::vector const &sessions_)
{
// poll for pending close sockets first
std::vector pollInfo;
for (auto &session : sessions_)
{
for (auto &pending : session->m_pendingCloseSocket)
{
assert (pending.unique ());
pollInfo.emplace_back (*pending, POLLIN, 0);
}
}
if (!pollInfo.empty ())
{
auto const rc = Socket::poll (pollInfo.data (), pollInfo.size (), 0ms);
if (rc < 0)
{
error ("poll: %s\n", std::strerror (errno));
return false;
}
else
{
for (auto const &i : pollInfo)
{
if (!i.revents)
continue;
for (auto &session : sessions_)
{
for (auto it = std::begin (session->m_pendingCloseSocket);
it != std::end (session->m_pendingCloseSocket);)
{
auto &socket = *it;
if (&i.socket.get () != socket.get ())
{
++it;
continue;
}
it = session->m_pendingCloseSocket.erase (it);
}
}
}
}
}
// poll for everything else
pollInfo.clear ();
for (auto &session : sessions_)
{
if (session->m_commandSocket)
{
pollInfo.emplace_back (*session->m_commandSocket, POLLIN | POLLPRI, 0);
if (session->m_responseBuffer.usedSize () != 0)
pollInfo.back ().events |= POLLOUT;
}
switch (session->m_state)
{
case State::COMMAND:
// we are waiting to read a command
break;
case State::DATA_CONNECT:
if (session->m_pasv)
{
assert (!session->m_port);
// we are waiting for a PASV connection
pollInfo.emplace_back (*session->m_pasvSocket, POLLIN, 0);
}
else
{
// we are waiting to complete a PORT connection
pollInfo.emplace_back (*session->m_dataSocket, POLLOUT, 0);
}
break;
case State::DATA_TRANSFER:
// we need to transfer data
if (session->m_recv)
{
assert (!session->m_send);
pollInfo.emplace_back (*session->m_dataSocket, POLLIN, 0);
}
else
{
assert (session->m_send);
pollInfo.emplace_back (*session->m_dataSocket, POLLOUT, 0);
}
break;
}
}
if (pollInfo.empty ())
return true;
// poll for activity
auto const rc = Socket::poll (pollInfo.data (), pollInfo.size (), 100ms);
if (rc < 0)
{
error ("poll: %s\n", std::strerror (errno));
return false;
}
auto const now = std::time (nullptr);
for (auto &session : sessions_)
{
bool handled = false;
for (auto const &i : pollInfo)
{
if (!i.revents)
continue;
handled = true;
// check command socket
if (&i.socket.get () == session->m_commandSocket.get ())
{
if (i.revents & ~(POLLIN | POLLPRI | POLLOUT))
debug ("Command revents 0x%X\n", i.revents);
if (!session->m_dataSocket && (i.revents & POLLOUT))
session->writeResponse ();
if (i.revents & (POLLIN | POLLPRI))
session->readCommand (i.revents);
if (i.revents & (POLLERR | POLLHUP))
session->closeCommand ();
}
// check the data socket
if (&i.socket.get () == session->m_pasvSocket.get () ||
&i.socket.get () == session->m_dataSocket.get ())
{
switch (session->m_state)
{
case State::COMMAND:
std::abort ();
break;
case State::DATA_CONNECT:
if (i.revents & ~(POLLIN | POLLPRI | POLLOUT))
debug ("Data revents 0x%X\n", i.revents);
if (i.revents & (POLLERR | POLLHUP))
{
session->sendResponse ("426 Data connection failed\r\n");
session->setState (State::COMMAND, true, true);
}
else if (i.revents & POLLIN)
{
// we need to accept the PASV connection
session->dataAccept ();
}
else if (i.revents & POLLOUT)
{
// PORT connection completed
auto const &sockName = session->m_dataSocket->peerName ();
info ("Connected to [%s]:%u\n", sockName.name (), sockName.port ());
session->sendResponse ("150 Ready\r\n");
session->setState (State::DATA_TRANSFER, true, false);
}
break;
case State::DATA_TRANSFER:
if (i.revents & ~(POLLIN | POLLPRI | POLLOUT))
debug ("Data revents 0x%X\n", i.revents);
// we need to transfer data
if (i.revents & (POLLERR | POLLHUP))
{
session->sendResponse ("426 Data connection failed\r\n");
session->setState (State::COMMAND, true, true);
}
else if (i.revents & (POLLIN | POLLOUT))
{
auto start_time = std::chrono::high_resolution_clock::now ();
while (true)
{
if (!((*session).*(session->m_transfer)) ())
{
break;
}
if (std::chrono::duration_cast (
std::chrono::high_resolution_clock::now () - start_time) >
5000ms)
{
break;
}
}
}
break;
}
}
}
if (!handled && now - session->m_timestamp >= IDLE_TIMEOUT)
{
session->closeCommand ();
session->closePasv ();
session->closeData ();
}
}
return true;
}
bool FtpSession::authorized () const
{
return m_authorizedUser && m_authorizedPass;
}
void FtpSession::setState (State const state_, bool const closePasv_, bool const closeData_)
{
m_state = state_;
m_timestamp = std::time (nullptr);
if (closePasv_)
closePasv ();
if (closeData_)
closeData ();
if (state_ == State::COMMAND)
{
{
#ifndef __NDS__
auto const lock = std::scoped_lock (m_lock);
#endif
m_restartPosition = 0;
m_fileSize = 0;
m_filePosition = 0;
for (auto &pos : m_filePositionHistory)
pos = 0;
m_xferRate = -1.0f;
m_workItem.clear ();
}
m_devZero = false;
m_file.close ();
m_dir.close ();
}
}
void FtpSession::closeSocket (SharedSocket &socket_)
{
if (socket_ && socket_.unique ())
{
socket_->shutdown (SHUT_WR);
#ifndef __WIIU__
socket_->setLinger (true, 0s);
#endif
LOCKED (m_pendingCloseSocket.emplace_back (std::move (socket_)));
}
else
LOCKED (socket_.reset ());
}
void FtpSession::closeCommand ()
{
closeSocket (m_commandSocket);
}
void FtpSession::closePasv ()
{
UniqueSocket pasv;
LOCKED (pasv = std::move (m_pasvSocket));
}
void FtpSession::closeData ()
{
closeSocket (m_dataSocket);
m_recv = false;
m_send = false;
}
bool FtpSession::changeDir (char const *const args_)
{
if (std::strcmp (args_, "..") == 0)
{
// cd up
auto const pos = m_cwd.find_last_of ('/');
assert (pos != std::string::npos);
if (pos == 0)
LOCKED (m_cwd = "/");
else
LOCKED (m_cwd = m_cwd.substr (0, pos));
return true;
}
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
return false;
stat_t st;
if (tzStat (path.c_str (), &st) != 0)
return false;
if (!S_ISDIR (st.st_mode))
{
errno = ENOTDIR;
return false;
}
LOCKED (m_cwd = path);
return true;
}
bool FtpSession::dataAccept ()
{
if (!m_pasv)
{
sendResponse ("503 Bad sequence of commands\r\n");
setState (State::COMMAND, true, true);
return false;
}
m_pasv = false;
auto peer = m_pasvSocket->accept ();
LOCKED (m_dataSocket = std::move (peer));
if (!m_dataSocket)
{
sendResponse ("425 Failed to establish connection\r\n");
setState (State::COMMAND, true, true);
return false;
}
#ifndef __3DS__
#ifdef __WIIU__
m_dataSocket->setWinScale (1);
#endif
m_dataSocket->setRecvBufferSize (SOCK_BUFFERSIZE);
m_dataSocket->setSendBufferSize (SOCK_BUFFERSIZE);
#endif
if (!m_dataSocket->setNonBlocking ())
{
sendResponse ("425 Failed to establish connection\r\n");
setState (State::COMMAND, true, true);
return false;
}
// we are ready to transfer data
sendResponse ("150 Ready\r\n");
setState (State::DATA_TRANSFER, true, false);
return true;
}
bool FtpSession::dataConnect ()
{
assert (m_port);
m_port = false;
auto data = Socket::create (Socket::eStream);
LOCKED (m_dataSocket = std::move (data));
if (!m_dataSocket)
return false;
#ifdef __WIIU__
m_dataSocket->setWinScale (1);
#endif
m_dataSocket->setRecvBufferSize (SOCK_BUFFERSIZE);
m_dataSocket->setSendBufferSize (SOCK_BUFFERSIZE);
if (!m_dataSocket->setNonBlocking ())
return false;
if (!m_dataSocket->connect (m_portAddr))
{
if (errno != EINPROGRESS)
return false;
return true;
}
// we are ready to transfer data
sendResponse ("150 Ready\r\n");
setState (State::DATA_TRANSFER, true, false);
return true;
}
int FtpSession::tzStat (char const *const path_, stat_t *st_)
{
auto const rc = IOAbstraction::stat (path_, st_);
if (rc != 0)
return rc;
#ifdef __3DS__
if (m_config.getMTime ())
{
std::uint64_t mtime = 0;
auto const rc = archive_getmtime (path_, &mtime);
if (rc != 0)
error ("sdmc_getmtime %s 0x%lx\n", path_, rc);
else
st_->st_mtime = mtime - FtpServer::tzOffset ();
}
#endif
return 0;
}
int FtpSession::tzLStat (char const *const path_, stat_t *st_)
{
auto const rc = IOAbstraction::lstat (path_, st_);
if (rc != 0)
return rc;
#ifdef __3DS__
if (m_config.getMTime ())
{
std::uint64_t mtime = 0;
auto const rc = archive_getmtime (path_, &mtime);
if (rc != 0)
error ("sdmc_getmtime %s 0x%lx\n", path_, rc);
else
st_->st_mtime = mtime - FtpServer::tzOffset ();
}
#endif
return 0;
}
int FtpSession::fillDirent (stat_t const &st_, std::string_view const path_, char const *type_)
{
auto const buffer = m_xferBuffer.freeArea ();
auto const size = m_xferBuffer.freeSize ();
std::size_t pos = 0;
if (m_xferDirMode == XferDirMode::MLSD || m_xferDirMode == XferDirMode::MLST)
{
if (m_xferDirMode == XferDirMode::MLST)
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = ' ';
}
// type fact
if (m_mlstType)
{
if (!type_)
{
type_ = "???";
if (S_ISREG (st_.st_mode))
type_ = "file";
else if (S_ISDIR (st_.st_mode))
type_ = "dir";
#if !defined(__3DS__) && !defined(__SWITCH__) && !defined(__WIIU__)
else if (S_ISLNK (st_.st_mode))
type_ = "os.unix=symlink";
else if (S_ISCHR (st_.st_mode))
type_ = "os.unix=character";
else if (S_ISBLK (st_.st_mode))
type_ = "os.unix=block";
else if (S_ISFIFO (st_.st_mode))
type_ = "os.unix=fifo";
else if (S_ISSOCK (st_.st_mode))
type_ = "os.unix=socket";
#endif
}
auto const rc = std::snprintf (&buffer[pos], size - pos, "Type=%s;", type_);
if (rc < 0)
return errno;
if (static_cast (rc) > size - pos)
return EAGAIN;
pos += rc;
}
// size fact
if (m_mlstSize)
{
auto const rc = std::snprintf (&buffer[pos],
size - pos,
"Size=%llu;",
static_cast (st_.st_size));
if (rc < 0)
return errno;
if (static_cast (rc) > size - pos)
return EAGAIN;
pos += rc;
}
auto mtime = st_.st_mtime;
if (mtime > 0x2208985200L)
{
mtime = time (0);
}
// mtime fact
if (m_mlstModify)
{
auto const tm = std::gmtime (&mtime);
if (!tm)
return errno;
auto const rc = std::strftime (&buffer[pos], size - pos, "Modify=%Y%m%d%H%M%S;", tm);
if (rc == 0)
return EAGAIN;
pos += rc;
}
// permission fact
if (m_mlstPerm)
{
auto const header = "Perm=";
if (size - pos < std::strlen (header))
return EAGAIN;
std::strcpy (&buffer[pos], header);
pos += std::strlen (header);
// append permission
if (S_ISREG (st_.st_mode) && (st_.st_mode & S_IWUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'a';
}
// create permission
if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IWUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'c';
}
// delete permission
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'd';
// chdir permission
if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IXUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'e';
}
// rename permission
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'f';
// list permission
if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IRUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'l';
}
// mkdir permission
if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IWUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'm';
}
// purge permission
if (S_ISDIR (st_.st_mode) && (st_.st_mode & S_IWUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'p';
}
// read permission
if (S_ISREG (st_.st_mode) && (st_.st_mode & S_IRUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'r';
}
// write permission
if (S_ISREG (st_.st_mode) && (st_.st_mode & S_IWUSR))
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = 'w';
}
if (pos >= size)
return EAGAIN;
buffer[pos++] = ';';
}
// unix mode fact
if (m_mlstUnixMode)
{
auto const mask = S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX | S_ISGID | S_ISUID;
auto const rc = std::snprintf (&buffer[pos],
size - pos,
"UNIX.mode=0%lo;",
static_cast (st_.st_mode & mask));
if (rc < 0)
return errno;
if (static_cast (rc) > size - pos)
return EAGAIN;
pos += rc;
}
// make sure space precedes name
if (buffer[pos - 1] != ' ')
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = ' ';
}
}
else if (m_xferDirMode != XferDirMode::NLST)
{
if (m_xferDirMode == XferDirMode::STAT)
{
if (pos >= size)
return EAGAIN;
buffer[pos++] = ' ';
}
#ifdef __3DS__
auto const owner = "3DS";
auto const group = "3DS";
#elif defined(__SWITCH__)
auto const owner = "Switch";
auto const group = "Switch";
#elif defined(__WIIU__)
auto const owner = "WiiU";
auto const group = "WiiU";
#else
char owner[32];
char group[32];
std::sprintf (owner, "%d", st_.st_uid);
std::sprintf (group, "%d", st_.st_gid);
#endif
// perms nlinks owner group size
auto rc = std::snprintf (&buffer[pos],
size - pos,
"%c%c%c%c%c%c%c%c%c%c %lu %s %s %llu ",
// clang-format off
S_ISREG (st_.st_mode) ? '-' :
S_ISDIR (st_.st_mode) ? 'd' :
#if !defined(__3DS__) && !defined(__SWITCH__) && !defined(__WIIU__)
S_ISLNK (st_.st_mode) ? 'l' :
S_ISCHR (st_.st_mode) ? 'c' :
S_ISBLK (st_.st_mode) ? 'b' :
S_ISFIFO (st_.st_mode) ? 'p' :
S_ISSOCK (st_.st_mode) ? 's' :
#endif
'?',
// clang-format on
st_.st_mode & S_IRUSR ? 'r' : '-',
st_.st_mode & S_IWUSR ? 'w' : '-',
st_.st_mode & S_IXUSR ? 'x' : '-',
st_.st_mode & S_IRGRP ? 'r' : '-',
st_.st_mode & S_IWGRP ? 'w' : '-',
st_.st_mode & S_IXGRP ? 'x' : '-',
st_.st_mode & S_IROTH ? 'r' : '-',
st_.st_mode & S_IWOTH ? 'w' : '-',
st_.st_mode & S_IXOTH ? 'x' : '-',
static_cast (st_.st_nlink),
owner,
group,
static_cast (st_.st_size));
if (rc < 0)
return errno;
if (static_cast (rc) > size - pos)
return EAGAIN;
pos += rc;
auto mtime = st_.st_mtime;
if (mtime > 0x2208985200L)
{
mtime = time (0);
}
// timestamp
auto const tm = std::gmtime (&mtime);
if (!tm)
return errno;
auto fmt = "%b %e %Y ";
if (m_timestamp > mtime && m_timestamp - mtime < (60 * 60 * 24 * 365 / 2))
fmt = "%b %e %H:%M ";
rc = std::strftime (&buffer[pos], size - pos, fmt, tm);
if (rc < 0)
return errno;
if (static_cast (rc) > size - pos)
return EAGAIN;
pos += rc;
}
if (size - pos < path_.size () + 2)
return EAGAIN;
// path
std::memcpy (&buffer[pos], path_.data (), path_.size ());
pos += path_.size ();
buffer[pos++] = '\r';
buffer[pos++] = '\n';
m_xferBuffer.markUsed (pos);
LOCKED (m_filePosition += pos);
return 0;
}
int FtpSession::fillDirent (std::string const &path_, char const *type_)
{
stat_t st;
if (tzStat (path_.c_str (), &st) != 0)
return errno;
return fillDirent (st, encodePath (path_), type_);
}
void FtpSession::xferFile (char const *const args_, XferFileMode const mode_)
{
m_xferBuffer.clear ();
// build the path of the file to transfer
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("553 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return;
}
if (path == "/devZero")
{
m_devZero = true;
}
else if (mode_ == XferFileMode::RETR)
{
// stat the file
stat_t st;
if (tzStat (path.c_str (), &st) != 0)
{
sendResponse ("450 %s\r\n", std::strerror (errno));
return;
}
// open the file in read mode
if (!m_file.open (path.c_str (), "rb"))
{
sendResponse ("450 %s\r\n", std::strerror (errno));
return;
}
LOCKED (m_fileSize = st.st_size);
m_file.setBufferSize (FILE_BUFFERSIZE);
if (m_restartPosition != 0)
{
if (m_file.seek (m_restartPosition, SEEK_SET) != 0)
{
sendResponse ("450 %s\r\n", std::strerror (errno));
return;
}
}
LOCKED (m_filePosition = m_restartPosition);
}
else
{
auto const append = mode_ == XferFileMode::APPE;
char const *mode = "wb";
if (append)
mode = "ab";
else if (m_restartPosition != 0)
mode = "r+b";
// open file in write mode
if (!m_file.open (path.c_str (), mode))
{
sendResponse ("450 %s\r\n", std::strerror (errno));
return;
}
FtpServer::updateFreeSpace ();
m_file.setBufferSize (FILE_BUFFERSIZE);
// check if this had REST but not APPE
if (m_restartPosition != 0 && !append)
{
// seek to the REST offset
if (m_file.seek (m_restartPosition, SEEK_SET) != 0)
{
sendResponse ("450 %s\r\n", std::strerror (errno));
return;
}
}
LOCKED (m_filePosition = m_restartPosition);
}
if (!m_port && !m_pasv)
{
sendResponse ("503 Bad sequence of commands\r\n");
setState (State::COMMAND, true, true);
return;
}
setState (State::DATA_CONNECT, false, true);
// setup connection
if (m_port && !dataConnect ())
{
sendResponse ("425 Can't open data connection\r\n");
setState (State::COMMAND, true, true);
return;
}
// set up the transfer
if (mode_ == XferFileMode::RETR)
{
m_recv = false;
m_send = true;
m_transfer = &FtpSession::retrieveTransfer;
}
else
{
m_recv = true;
m_send = false;
m_transfer = &FtpSession::storeTransfer;
}
LOCKED (m_workItem = path);
}
void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool const workaround_)
{
// set up the transfer
m_xferDirMode = mode_;
m_recv = false;
m_send = true;
m_filePosition = 0;
m_xferBuffer.clear ();
m_transfer = &FtpSession::listTransfer;
if (std::strlen (args_) > 0)
{
// an argument was provided
// work around broken clients that think LIST -a/-l is valid
if (workaround_)
{
if (args_[0] == '-' && (args_[1] == 'a' || args_[1] == 'l'))
{
char const *args = &args_[2];
if (*args == '\0' || *args == ' ')
{
if (*args == ' ')
++args;
xferDir (args, mode_, false);
return;
}
}
}
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("550 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return;
}
stat_t st;
if (tzStat (path.c_str (), &st) != 0)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return;
}
if (mode_ == XferDirMode::MLST)
{
auto const rc = fillDirent (st, path);
if (rc != 0)
{
sendResponse ("550 %s\r\n", std::strerror (rc));
setState (State::COMMAND, true, true);
return;
}
LOCKED (m_workItem = path);
}
else if (S_ISDIR (st.st_mode))
{
if (!m_dir.open (path.c_str ()))
{
sendResponse ("550 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return;
}
// set as lwd
m_lwd = std::move (path);
if (mode_ == XferDirMode::MLSD && m_mlstType)
{
// send this directory as type=cdir
auto const rc = fillDirent (st, m_lwd, "cdir");
if (rc != 0)
{
sendResponse ("550 %s\r\n", std::strerror (rc));
setState (State::COMMAND, true, true);
return;
}
}
LOCKED (m_workItem = m_lwd);
}
else if (mode_ == XferDirMode::MLSD)
{
// specified file instead of directory for MLSD
sendResponse ("501 %s\r\n", std::strerror (ENOTDIR));
setState (State::COMMAND, true, true);
return;
}
else
{
std::string name;
if (mode_ == XferDirMode::NLST)
{
// NLST uses full path name
name = encodePath (path);
}
else
{
// everything else uses basename
auto const pos = path.find_last_of ('/');
assert (pos != std::string::npos);
name = encodePath (std::string_view (path).substr (pos + 1));
}
auto const rc = fillDirent (st, name);
if (rc != 0)
{
sendResponse ("550 %s\r\n", std::strerror (rc));
setState (State::COMMAND, true, true);
return;
}
LOCKED (m_workItem = path);
}
}
else if (mode_ == XferDirMode::MLST)
{
auto const rc = fillDirent (m_cwd);
if (rc != 0)
{
sendResponse ("550 %s\r\n", std::strerror (rc));
setState (State::COMMAND, true, true);
return;
}
LOCKED (m_workItem = m_cwd);
}
else if (!m_dir.open (m_cwd.c_str ()))
{
// no argument, but opening cwd failed
sendResponse ("550 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return;
}
else
{
// set the cwd as the lwd
m_lwd = m_cwd;
if (mode_ == XferDirMode::MLSD && m_mlstType)
{
// send this directory as type=cdir
auto const rc = fillDirent (m_lwd, "cdir");
if (rc != 0)
{
sendResponse ("550 %s\r\n", std::strerror (rc));
setState (State::COMMAND, true, true);
return;
}
}
LOCKED (m_workItem = m_lwd);
}
if (mode_ == XferDirMode::MLST || mode_ == XferDirMode::STAT)
{
// this is a little different; we have to send the data over the command socket
sendResponse ("250-Status\r\n");
setState (State::DATA_TRANSFER, true, true);
LOCKED (m_dataSocket = m_commandSocket);
m_send = true;
return;
}
if (!m_port && !m_pasv)
{
// Prior PORT or PASV required
sendResponse ("503 Bad sequence of commands\r\n");
setState (State::COMMAND, true, true);
return;
}
setState (State::DATA_CONNECT, false, true);
m_send = true;
// setup connection
if (m_port && !dataConnect ())
{
sendResponse ("425 Can't open data connection\r\n");
setState (State::COMMAND, true, true);
}
}
void FtpSession::readCommand (int const events_)
{
#ifndef __NDS__
// check out-of-band data
if (events_ & POLLPRI)
{
m_urgent = true;
// check if we are at the urgent marker
auto const atMark = m_commandSocket->atMark ();
if (atMark < 0)
{
closeCommand ();
return;
}
if (!atMark)
{
// discard in-band data
m_commandBuffer.clear ();
auto const rc = m_commandSocket->read (m_commandBuffer);
if (rc < 0 && errno != EWOULDBLOCK)
closeCommand ();
else
m_timestamp = std::time (nullptr);
return;
}
// retrieve the urgent data
m_commandBuffer.clear ();
auto const rc = m_commandSocket->read (m_commandBuffer, true);
if (rc < 0)
{
// EWOULDBLOCK means out-of-band data is on the way
if (errno != EWOULDBLOCK)
closeCommand ();
else
m_timestamp = std::time (nullptr);
return;
}
else
m_timestamp = std::time (nullptr);
// reset the command buffer
m_commandBuffer.clear ();
return;
}
#endif
if (events_ & POLLIN)
{
// prepare to receive data
if (m_commandBuffer.freeSize () == 0)
{
error ("Exceeded command buffer size\n");
closeCommand ();
return;
}
auto const rc = m_commandSocket->read (m_commandBuffer);
if (rc < 0)
{
closeCommand ();
return;
}
if (rc == 0)
{
// peer closed connection
info ("Peer closed connection\n");
closeCommand ();
return;
}
m_timestamp = std::time (nullptr);
if (m_urgent)
{
// look for telnet data mark
auto const buffer = m_commandBuffer.usedArea ();
auto const size = m_commandBuffer.usedSize ();
auto const mark = static_cast (std::memchr (buffer, 0xF2, size));
if (!mark)
return;
// ignore all data that precedes the data mark
m_commandBuffer.markFree (mark + 1 - buffer);
m_commandBuffer.coalesce ();
m_urgent = false;
}
}
// loop through commands
while (true)
{
// must have at least enough data for the delimiter
auto const size = m_commandBuffer.usedSize ();
if (size < 1)
return;
auto const buffer = m_commandBuffer.usedArea ();
auto const [delim, next] = parseCommand (buffer, size);
if (!next)
return;
*delim = '\0';
decodePath (buffer, delim - buffer);
if (::strncasecmp ("USER ", buffer, 5) == 0 || ::strncasecmp ("PASS ", buffer, 5) == 0)
command ("%.*s ******\n", 5, buffer);
else
command ("%s\n", buffer);
char const *const command = buffer;
char *args = buffer;
while (*args && !std::isspace (*args))
++args;
if (*args)
*args++ = 0;
auto const it = std::lower_bound (std::begin (handlers),
std::end (handlers),
command,
[] (auto const &lhs_, auto const &rhs_) { return compare (lhs_.first, rhs_) < 0; });
m_timestamp = std::time (nullptr);
if (it == std::end (handlers) || compare (it->first, command) != 0)
{
std::string response = "502 Invalid command \"";
response += encodePath (command);
if (*args)
{
response.push_back (' ');
response += encodePath (args);
}
response += "\"\r\n";
sendResponse (response);
}
else if (m_state != State::COMMAND)
{
// only some commands are available during data transfer
if (compare (command, "ABOR") != 0 && compare (command, "NOOP") != 0 &&
compare (command, "PWD") != 0 && compare (command, "QUIT") != 0 &&
compare (command, "STAT") != 0 && compare (command, "XPWD") != 0)
{
sendResponse ("503 Invalid command during transfer\r\n");
setState (State::COMMAND, true, true);
closeCommand ();
}
else
{
auto const handler = it->second;
(this->*handler) (args);
}
}
else
{
// clear rename for all commands except RNTO
if (compare (command, "RNTO") != 0)
m_rename.clear ();
auto const handler = it->second;
(this->*handler) (args);
}
m_commandBuffer.markFree (next - buffer);
m_commandBuffer.coalesce ();
}
}
void FtpSession::writeResponse ()
{
auto const rc = m_commandSocket->write (m_responseBuffer);
if (rc <= 0)
{
closeCommand ();
return;
}
m_timestamp = std::time (nullptr);
m_responseBuffer.coalesce ();
}
void FtpSession::sendResponse (char const *fmt_, ...)
{
if (!m_commandSocket)
return;
auto const buffer = m_responseBuffer.freeArea ();
auto const size = m_responseBuffer.freeSize ();
va_list ap;
va_start (ap, fmt_);
addLog (RESPONSE, fmt_, ap);
va_end (ap);
va_start (ap, fmt_);
auto const rc = std::vsnprintf (buffer, size, fmt_, ap);
va_end (ap);
if (rc < 0)
{
error ("vsnprintf: %s\n", std::strerror (errno));
closeCommand ();
return;
}
if (static_cast (rc) > size)
{
error ("Not enough space for response\n");
closeCommand ();
return;
}
m_responseBuffer.markUsed (rc);
// try to write data immediately
assert (m_commandSocket);
auto const bytes = m_commandSocket->write (m_responseBuffer);
if (bytes <= 0)
{
if (bytes == 0 || errno != EWOULDBLOCK)
closeCommand ();
}
else
{
m_timestamp = std::time (nullptr);
m_responseBuffer.coalesce ();
}
}
void FtpSession::sendResponse (std::string_view const response_)
{
if (!m_commandSocket)
return;
addLog (RESPONSE, response_);
auto const buffer = m_responseBuffer.freeArea ();
auto const size = m_responseBuffer.freeSize ();
if (response_.size () > size)
{
error ("Not enough space for response\n");
closeCommand ();
return;
}
std::memcpy (buffer, response_.data (), response_.size ());
m_responseBuffer.markUsed (response_.size ());
}
bool FtpSession::listTransfer ()
{
// check if we sent all available data
while (m_xferBuffer.empty ())
{
m_xferBuffer.clear ();
// check xfer dir type
int rc = 226;
if (m_xferDirMode == XferDirMode::MLST || m_xferDirMode == XferDirMode::STAT)
rc = 250;
// check if this was for a file/MLST
if (!m_dir)
{
// we already sent the file's listing
sendResponse ("%d OK\r\n", rc);
setState (State::COMMAND, true, true);
return false;
}
// get the next directory entry
auto const dent = m_dir.read ();
if (!dent)
{
// we have exhausted the directory listing
sendResponse ("%d OK\r\n", rc);
setState (State::COMMAND, true, true);
return false;
}
// I think we are supposed to return entries for . and ..
if (std::strcmp (dent->d_name, ".") == 0 || std::strcmp (dent->d_name, "..") == 0)
continue; // just skip it
// check if this was NLST
if (m_xferDirMode == XferDirMode::NLST)
{
// NLST gives the whole path name
auto const path = encodePath (buildPath (m_lwd, dent->d_name)) + "\r\n";
if (m_xferBuffer.freeSize () < path.size ())
{
sendResponse ("501 %s\r\n", std::strerror (ENOMEM));
setState (State::COMMAND, true, true);
return false;
}
std::memcpy (m_xferBuffer.freeArea (), path.data (), path.size ());
m_xferBuffer.markUsed (path.size ());
LOCKED (m_filePosition += path.size ());
}
else
{
// build the path
auto const fullPath = buildPath (m_lwd, dent->d_name);
auto const path = encodePath (dent->d_name);
#ifdef _DIRENT_HAVE_D_STAT
auto const rc = fillDirent (dent->d_stat, path);
#else
struct stat st = {};
// lstat the entry
if (IOAbstraction::lstat (fullPath.c_str (), &st) != 0)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return false;
}
auto const rc = fillDirent (st, path);
#endif
if (rc != 0)
{
sendResponse ("425 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return false;
}
}
}
// send any pending data
auto const rc = m_dataSocket->write (m_xferBuffer);
if (rc <= 0)
{
// error sending data
if (rc < 0 && errno == EWOULDBLOCK)
return false;
sendResponse ("426 Connection broken during transfer\r\n");
setState (State::COMMAND, true, true);
return false;
}
m_timestamp = std::time (nullptr);
// we can try to send more data
return true;
}
bool FtpSession::globTransfer ()
{
#if FTPD_HAS_GLOB
// check if we sent all available data
if (m_xferBuffer.empty ())
{
m_xferBuffer.clear ();
auto const entry = m_glob.next ();
if (!entry)
{
// we have exhausted the glob listing
sendResponse ("226 OK\r\n");
setState (State::COMMAND, true, true);
return false;
}
// NLST gives the whole path name
auto const path = encodePath (entry) + "\r\n";
if (m_xferBuffer.freeSize () < path.size ())
{
sendResponse ("501 %s\r\n", std::strerror (ENOMEM));
setState (State::COMMAND, true, true);
return false;
}
std::memcpy (m_xferBuffer.freeArea (), path.data (), path.size ());
m_xferBuffer.markUsed (path.size ());
LOCKED (m_filePosition += path.size ());
}
// send any pending data
auto const rc = m_dataSocket->write (m_xferBuffer);
if (rc <= 0)
{
// error sending data
if (rc < 0 && errno == EWOULDBLOCK)
return false;
sendResponse ("426 Connection broken during transfer\r\n");
setState (State::COMMAND, true, true);
return false;
}
m_timestamp = std::time (nullptr);
// we can try to send more data
return true;
#else
/// \todo error code?
sendResponse ("451 Glob unsupported\r\n");
setState (State::COMMAND, true, true);
return false;
#endif
}
bool FtpSession::retrieveTransfer ()
{
if (m_xferBuffer.empty ())
{
m_xferBuffer.clear ();
if (!m_devZero)
{
// we have sent all the data, so read some more
auto const rc = m_file.read (m_xferBuffer);
if (rc < 0)
{
// failed to read data
sendResponse ("451 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return false;
}
if (rc == 0)
{
// reached end of file
sendResponse ("226 OK\r\n");
setState (State::COMMAND, true, true);
return false;
}
}
else
{
auto const buffer = m_xferBuffer.freeArea ();
auto const size = m_xferBuffer.freeSize ();
std::memset (buffer, 0, size);
m_xferBuffer.markUsed (size);
}
}
// send any pending data
auto const rc = m_dataSocket->write (m_xferBuffer);
if (rc <= 0)
{
// error sending data
if (rc < 0 && errno == EWOULDBLOCK)
return false;
sendResponse ("426 Connection broken during transfer\r\n");
setState (State::COMMAND, true, true);
return false;
}
m_timestamp = std::time (nullptr);
// we can try to read/send more data
LOCKED (m_filePosition += rc);
return true;
}
bool FtpSession::storeTransfer ()
{
if (m_xferBuffer.empty ())
{
m_xferBuffer.clear ();
// we have written all the received data, so try to get some more
auto const rc = m_dataSocket->read (m_xferBuffer);
if (rc < 0)
{
// failed to read data
if (errno == EWOULDBLOCK)
return false;
sendResponse ("451 %s\r\n", std::strerror (errno));
setState (State::COMMAND, true, true);
return false;
}
if (rc == 0)
{
// reached end of file
sendResponse ("226 OK\r\n");
setState (State::COMMAND, true, true);
return false;
}
m_timestamp = std::time (nullptr);
}
if (!m_devZero)
{
// write any pending data
auto const rc = m_file.write (m_xferBuffer);
if (rc <= 0)
{
// error writing data
sendResponse ("426 %s\r\n", rc < 0 ? std::strerror (errno) : "Failed to write data");
setState (State::COMMAND, true, true);
return false;
}
// we can try to recv/write more data
LOCKED (m_filePosition += rc);
}
else
{
LOCKED (m_filePosition += m_xferBuffer.usedSize ());
m_xferBuffer.clear ();
}
return true;
}
///////////////////////////////////////////////////////////////////////////
void FtpSession::ABOR (char const *args_)
{
(void)args_;
if (m_state == State::COMMAND)
{
sendResponse ("225 No transfer to abort\r\n");
return;
}
// abort the transfer
sendResponse ("225 Aborted\r\n");
sendResponse ("425 Transfer aborted\r\n");
setState (State::COMMAND, true, true);
}
void FtpSession::ALLO (char const *args_)
{
(void)args_;
sendResponse ("202 Superfluous command\r\n");
setState (State::COMMAND, false, false);
}
void FtpSession::APPE (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// open the file in append mode
xferFile (args_, XferFileMode::APPE);
}
void FtpSession::CDUP (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
if (!changeDir (".."))
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
sendResponse ("200 OK\r\n");
}
void FtpSession::CWD (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
if (!changeDir (args_))
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
sendResponse ("200 OK\r\n");
}
void FtpSession::DELE (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// build the path to remove
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("553 %s\r\n", std::strerror (errno));
return;
}
// unlink the path
if (IOAbstraction::unlink (path.c_str ()) != 0)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
FtpServer::updateFreeSpace ();
sendResponse ("250 OK\r\n");
}
void FtpSession::FEAT (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
sendResponse ("211-\r\n"
" MDTM\r\n"
" MLST Type%s;Size%s;Modify%s;Perm%s;UNIX.mode%s;\r\n"
" PASV\r\n"
" SIZE\r\n"
" TVFS\r\n"
" UTF8\r\n"
"\r\n"
"211 End\r\n",
m_mlstType ? "*" : "",
m_mlstSize ? "*" : "",
m_mlstModify ? "*" : "",
m_mlstPerm ? "*" : "",
m_mlstUnixMode ? "*" : "");
}
void FtpSession::HELP (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
sendResponse ("214-\r\n"
"The following commands are recognized\r\n"
" ABOR ALLO APPE CDUP CWD DELE FEAT HELP LIST MDTM MKD MLSD MLST MODE\r\n"
" NLST NOOP OPTS PASS PASV PORT PWD QUIT REST RETR RMD RNFR RNTO SITE\r\n"
" SIZE STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD XPWD XRMD\r\n"
"214 End\r\n");
}
void FtpSession::LIST (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// open the path in LIST mode
xferDir (args_, XferDirMode::LIST, true);
}
void FtpSession::MDTM (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
sendResponse ("502 Command not implemented\r\n");
}
void FtpSession::MKD (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// build the path to create
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("553 %s\r\n", std::strerror (errno));
return;
}
// create the directory
if (IOAbstraction::mkdir (path.c_str (), 0755) != 0)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
FtpServer::updateFreeSpace ();
sendResponse ("250 OK\r\n");
}
void FtpSession::MLSD (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// open the path in MLSD mode
xferDir (args_, XferDirMode::MLSD, false);
}
void FtpSession::MLST (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// open the path in MLST mode
xferDir (args_, XferDirMode::MLST, false);
}
void FtpSession::MODE (char const *args_)
{
setState (State::COMMAND, false, false);
// we only accept S (stream) mode
if (compare (args_, "S") == 0)
{
sendResponse ("200 OK\r\n");
return;
}
sendResponse ("504 Unavailable\r\n");
}
void FtpSession::NLST (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
#if FTPD_HAS_GLOB
if (std::strchr (args_, '*'))
{
if (::chdir (m_cwd.c_str ()) != 0 || !m_glob.glob (args_))
{
sendResponse ("501 %s\r\n", std::strerror (errno));
setState (State::COMMAND, false, false);
return;
}
m_transfer = &FtpSession::globTransfer;
if (!m_port && !m_pasv)
{
// Prior PORT or PASV required
sendResponse ("503 Bad sequence of commands\r\n");
setState (State::COMMAND, true, true);
return;
}
setState (State::DATA_CONNECT, false, true);
m_send = true;
// setup connection
if (m_port && !dataConnect ())
{
sendResponse ("425 Can't open data connection\r\n");
setState (State::COMMAND, true, true);
}
return;
}
#endif
xferDir (args_, XferDirMode::NLST, false);
}
void FtpSession::NOOP (char const *args_)
{
(void)args_;
sendResponse ("200 OK\r\n");
}
void FtpSession::OPTS (char const *args_)
{
setState (State::COMMAND, false, false);
// check UTF8 options
if (compare (args_, "UTF8") == 0 || compare (args_, "UTF8 ON") == 0 ||
compare (args_, "UTF8 NLST") == 0)
{
sendResponse ("200 OK\r\n");
return;
}
// check MLST options
if (::strncasecmp (args_, "MLST ", 5) == 0)
{
m_mlstType = false;
m_mlstSize = false;
m_mlstModify = false;
m_mlstPerm = false;
m_mlstUnixMode = false;
auto p = args_ + 5;
while (*p)
{
auto const match = [] (auto const &name_, auto const &arg_) {
return ::strncasecmp (name_, arg_, std::strlen (name_)) == 0;
};
if (match ("Type;", p))
m_mlstType = true;
else if (match ("Size;", p))
m_mlstSize = true;
else if (match ("Modify;", p))
m_mlstModify = true;
else if (match ("Perm;", p))
m_mlstPerm = true;
else if (match ("UNIX.mode;", p))
m_mlstUnixMode = true;
p = std::strchr (p, ';');
if (!p)
break;
++p;
}
sendResponse ("200 MLST OPTS%s%s%s%s%s%s\r\n",
m_mlstType || m_mlstSize || m_mlstModify || m_mlstPerm || m_mlstUnixMode ? " " : "",
m_mlstType ? "Type;" : "",
m_mlstSize ? "Size;" : "",
m_mlstModify ? "Modify;" : "",
m_mlstPerm ? "Perm;" : "",
m_mlstUnixMode ? "UNIX.mode;" : "");
return;
}
sendResponse ("504 %s\r\n", std::strerror (EINVAL));
}
void FtpSession::PASS (char const *args_)
{
setState (State::COMMAND, false, false);
m_authorizedPass = false;
std::string user;
std::string pass;
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
user = m_config.user ();
pass = m_config.pass ();
}
if (!user.empty () && !m_authorizedUser)
{
sendResponse ("430 User not authorized\r\n");
return;
}
if (pass.empty () || pass == args_)
{
m_authorizedPass = true;
sendResponse ("230 OK\r\n");
return;
}
sendResponse ("430 Invalid password\r\n");
}
void FtpSession::PASV (char const *args_)
{
(void)args_;
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// reset state
setState (State::COMMAND, true, true);
m_pasv = false;
m_port = false;
// create a socket to listen on
auto pasv = Socket::create (Socket::eStream);
LOCKED (m_pasvSocket = std::move (pasv));
if (!m_pasvSocket)
{
sendResponse ("451 Failed to create listening socket\r\n");
return;
}
// set the socket option
#ifdef __WIIU__
m_pasvSocket->setWinScale (1);
#endif
m_pasvSocket->setRecvBufferSize (SOCK_BUFFERSIZE);
m_pasvSocket->setSendBufferSize (SOCK_BUFFERSIZE);
// create an address to bind
sockaddr_in addr = m_commandSocket->sockName ();
#if defined(__NDS__) || defined(__3DS__)
static std::uint16_t ephemeralPort = 5001;
if (ephemeralPort > 10000)
ephemeralPort = 5001;
addr.sin_port = htons (ephemeralPort++);
#else
addr.sin_port = htons (0);
#endif
// bind to the address
if (!m_pasvSocket->bind (addr))
{
closePasv ();
sendResponse ("451 Failed to bind address\r\n");
return;
}
// listen on the socket
if (!m_pasvSocket->listen (1))
{
closePasv ();
sendResponse ("451 Failed to listen on socket\r\n");
return;
}
// we are now listening on the socket
auto const &sockName = m_pasvSocket->sockName ();
std::string name = sockName.name ();
auto const port = sockName.port ();
info ("Listening on [%s]:%u\n", name.c_str (), port);
// send the address in the ftp format
for (auto &c : name)
{
if (c == '.')
c = ',';
}
m_pasv = true;
sendResponse (
"227 Entering Passive Mode (%s,%u,%u).\r\n", name.c_str (), port >> 8, port & 0xFF);
}
void FtpSession::PORT (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// reset state
setState (State::COMMAND, true, true);
m_pasv = false;
m_port = false;
std::string addrString = args_;
// convert a,b,c,d,e,f with a.b.c.d\0e.f
unsigned commas = 0;
char const *portString = nullptr;
for (auto &p : addrString)
{
if (p == ',')
{
if (commas++ != 3)
p = '.';
else
{
p = '\0';
portString = &p + 1;
}
}
}
// check for the expected number of fields
if (commas != 5)
{
sendResponse ("501 %s\r\n", std::strerror (EINVAL));
return;
}
sockaddr_in addr{};
// parse the address
if (!inet_aton (addrString.data (), &addr.sin_addr))
{
sendResponse ("501 %s\r\n", std::strerror (EINVAL));
return;
}
// parse the port
int val = 0;
std::uint16_t port = 0;
for (auto p = portString; *p; ++p)
{
if (!std::isdigit (*p))
{
if (p == portString || *p != '.' || val > 0xFF)
{
sendResponse ("501 %s\r\n", std::strerror (EINVAL));
return;
}
port <<= 8;
port += val;
val = 0;
}
else
{
val *= 10;
val += *p - '0';
}
}
if (val > 0xFF || port > 0xFF)
{
sendResponse ("501 %s\r\n", std::strerror (EINVAL));
return;
}
port <<= 8;
port += val;
addr.sin_family = AF_INET;
addr.sin_port = htons (port);
// we are ready to connect to the client
m_portAddr = addr;
m_port = true;
sendResponse ("200 OK\r\n");
}
void FtpSession::PWD (char const *args_)
{
(void)args_;
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
auto const path = encodePath (m_cwd);
std::string response = "257 \"";
response += encodePath (m_cwd, true);
response += "\"\r\n";
sendResponse (response);
}
void FtpSession::QUIT (char const *args_)
{
(void)args_;
sendResponse ("221 Disconnecting\r\n");
closeCommand ();
}
void FtpSession::REST (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// parse the offset
std::uint64_t pos = 0;
for (auto p = args_; *p; ++p)
{
if (!std::isdigit (*p) || UINT64_MAX / 10 < pos)
{
sendResponse ("504 %s\r\n", std::strerror (errno));
return;
}
pos *= 10;
if (UINT64_MAX - (*p - '0') < pos)
{
sendResponse ("504 %s\r\n", std::strerror (errno));
return;
}
pos += (*p - '0');
}
// set the restart offset
m_restartPosition = pos;
sendResponse ("350 OK\r\n");
}
void FtpSession::RETR (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// open the file to retrieve
xferFile (args_, XferFileMode::RETR);
}
void FtpSession::RMD (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// build the path to remove
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("553 %s\r\n", std::strerror (errno));
return;
}
// remove the directory
if (IOAbstraction::rmdir (path.c_str ()) != 0)
{
sendResponse ("550 %d %s\r\n", __LINE__, std::strerror (errno));
return;
}
FtpServer::updateFreeSpace ();
sendResponse ("250 OK\r\n");
}
void FtpSession::RNFR (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// build the path to rename from
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("553 %s\r\n", std::strerror (errno));
return;
}
// make sure the path exists
stat_t st;
if (tzLStat (path.c_str (), &st) != 0)
{
sendResponse ("450 %s\r\n", std::strerror (errno));
return;
}
// we are ready for RNTO
m_rename = path;
sendResponse ("350 OK\r\n");
}
void FtpSession::RNTO (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// make sure the previous command was RNFR
if (m_rename.empty ())
{
sendResponse ("503 Bad sequence of commands\r\n");
return;
}
// build the path to rename to
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
m_rename.clear ();
sendResponse ("554 %s\r\n", std::strerror (errno));
return;
}
// rename the file
if (IOAbstraction::rename (m_rename.c_str (), path.c_str ()) != 0)
{
m_rename.clear ();
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
// clear the rename state
m_rename.clear ();
FtpServer::updateFreeSpace ();
sendResponse ("250 OK\r\n");
}
void FtpSession::SITE (char const *args_)
{
setState (State::COMMAND, false, false);
auto const str = std::string_view (args_);
auto const pos = str.find_first_of (' ');
auto const command = str.substr (0, pos);
auto const arg = pos == std::string::npos ? std::string_view () : str.substr (pos + 1);
if (compare (command.data (), "HELP") == 0)
{
sendResponse ("211-\r\n"
" Show this help: SITE HELP\r\n"
" Set username: SITE USER \r\n"
" Set password: SITE PASS \r\n"
" Set port: SITE PORT \r\n"
#ifndef __NDS__
" Set hostname: SITE HOST \r\n"
#endif
#ifdef __3DS__
" Set getMTime: SITE MTIME [0|1]\r\n"
#endif
" Save config: SITE SAVE\r\n"
"211 End\r\n");
return;
}
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
if (compare (command, "USER") == 0)
{
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
m_config.setUser (std::string (arg));
}
sendResponse ("200 OK\r\n");
return;
}
else if (compare (command, "PASS") == 0)
{
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
m_config.setPass (std::string (arg));
}
sendResponse ("200 OK\r\n");
return;
}
else if (compare (command, "PORT") == 0)
{
bool error = false;
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
error = !m_config.setPort (arg);
}
if (error)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
sendResponse ("200 OK\r\n");
return;
}
#ifndef __NDS__
else if (compare (command, "HOST") == 0)
{
{
auto const lock = m_config.lockGuard ();
m_config.setHostname (std::string (arg));
mdns::setHostname (std::string (arg));
}
}
#endif
#ifdef __3DS__
else if (compare (command, "MTIME") == 0)
{
if (arg == "0")
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
m_config.setGetMTime (false);
}
else if (arg == "1")
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
m_config.setGetMTime (true);
}
else
{
sendResponse ("550 %s\r\n", std::strerror (EINVAL));
return;
}
}
#endif
else if (compare (command, "SAVE") == 0)
{
bool error;
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
error = !m_config.save (FTPDCONFIG);
}
if (error)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
sendResponse ("200 OK\r\n");
return;
}
sendResponse ("550 Invalid command\r\n");
}
void FtpSession::SIZE (char const *args_)
{
setState (State::COMMAND, false, false);
if (!authorized ())
{
sendResponse ("530 Not logged in\r\n");
return;
}
// build the path to stat
auto const path = buildResolvedPath (m_cwd, args_);
if (path.empty ())
{
sendResponse ("553 %s\r\n", std::strerror (errno));
return;
}
// stat the path
stat_t st;
if (tzStat (path.c_str (), &st) != 0)
{
sendResponse ("550 %s\r\n", std::strerror (errno));
return;
}
if (!S_ISREG (st.st_mode))
{
sendResponse ("550 Not a file\r\n");
return;
}
sendResponse ("213 %" PRIu64 "\r\n", static_cast (st.st_size));
}
void FtpSession::STAT (char const *args_)
{
if (m_state == State::DATA_CONNECT)
{
sendResponse ("211-FTP server status\r\n"
" Waiting for data connection\r\n"
"211 End\r\n");
return;
}
if (m_state == State::DATA_TRANSFER)
{
sendResponse ("211-FTP server status\r\n"
" Transferred %" PRIu64 " bytes\r\n"
"211 End\r\n",
m_filePosition);
return;
}
if (std::strlen (args_) == 0)
{
// TODO keep track of start time
auto const uptime =
std::chrono::system_clock::to_time_t (std::chrono::system_clock::now ()) -
FtpServer::startTime ();
unsigned const hours = uptime / 3600;
unsigned const minutes = (uptime / 60) % 60;
unsigned const seconds = uptime % 60;
sendResponse ("211-FTP server status\r\n"
" Uptime: %02u:%02u:%02u\r\n"
"211 End\r\n",
hours,
minutes,
seconds);
return;
}
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
xferDir (args_, XferDirMode::STAT, false);
}
void FtpSession::STOR (char const *args_)
{
if (!authorized ())
{
setState (State::COMMAND, false, false);
sendResponse ("530 Not logged in\r\n");
return;
}
// open the file to store
xferFile (args_, XferFileMode::STOR);
}
void FtpSession::STOU (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
sendResponse ("502 Command not implemented\r\n");
}
void FtpSession::STRU (char const *args_)
{
setState (State::COMMAND, false, false);
// we only support F (no structure) mode
if (compare (args_, "F") == 0)
{
sendResponse ("200 OK\r\n");
return;
}
sendResponse ("504 Unavailable\r\n");
}
void FtpSession::SYST (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
sendResponse ("215 UNIX Type: L8\r\n");
}
void FtpSession::TYPE (char const *args_)
{
(void)args_;
setState (State::COMMAND, false, false);
// we always transfer in binary mode
sendResponse ("200 OK\r\n");
}
void FtpSession::USER (char const *args_)
{
setState (State::COMMAND, false, false);
m_authorizedUser = false;
std::string user;
std::string pass;
{
#ifndef __NDS__
auto const lock = m_config.lockGuard ();
#endif
user = m_config.user ();
pass = m_config.pass ();
}
if (user.empty () || user == args_)
{
m_authorizedUser = true;
if (pass.empty ())
{
sendResponse ("230 OK\r\n");
return;
}
sendResponse ("331 Need password\r\n");
return;
}
sendResponse ("430 Invalid user\r\n");
}
// clang-format off
std::vector> const
FtpSession::handlers =
{
{"ABOR", &FtpSession::ABOR},
{"ALLO", &FtpSession::ALLO},
{"APPE", &FtpSession::APPE},
{"CDUP", &FtpSession::CDUP},
{"CWD", &FtpSession::CWD},
{"DELE", &FtpSession::DELE},
{"FEAT", &FtpSession::FEAT},
{"HELP", &FtpSession::HELP},
{"LIST", &FtpSession::LIST},
{"MDTM", &FtpSession::MDTM},
{"MKD", &FtpSession::MKD},
{"MLSD", &FtpSession::MLSD},
{"MLST", &FtpSession::MLST},
{"MODE", &FtpSession::MODE},
{"NLST", &FtpSession::NLST},
{"NOOP", &FtpSession::NOOP},
{"OPTS", &FtpSession::OPTS},
{"PASS", &FtpSession::PASS},
{"PASV", &FtpSession::PASV},
{"PORT", &FtpSession::PORT},
{"PWD", &FtpSession::PWD},
{"QUIT", &FtpSession::QUIT},
{"REST", &FtpSession::REST},
{"RETR", &FtpSession::RETR},
{"RMD", &FtpSession::RMD},
{"RNFR", &FtpSession::RNFR},
{"RNTO", &FtpSession::RNTO},
{"SITE", &FtpSession::SITE},
{"SIZE", &FtpSession::SIZE},
{"STAT", &FtpSession::STAT},
{"STOR", &FtpSession::STOR},
{"STOU", &FtpSession::STOU},
{"STRU", &FtpSession::STRU},
{"SYST", &FtpSession::SYST},
{"TYPE", &FtpSession::TYPE},
{"USER", &FtpSession::USER},
{"XCUP", &FtpSession::CDUP},
{"XCWD", &FtpSession::CWD},
{"XMKD", &FtpSession::MKD},
{"XPWD", &FtpSession::PWD},
{"XRMD", &FtpSession::RMD},
};
// clang-format on