mirror of
https://github.com/wiiu-env/ftpiiu_plugin.git
synced 2024-12-26 20:51:47 +01:00
a3bbc1265e
The test for additional switches to the LIST command must be performed before the path is joined. Otherwise, the path will never be empty and the test will be skipped. Fixes ENODEV error (FTP/550) on clients using "LIST -a".
3208 lines
67 KiB
C++
3208 lines
67 KiB
C++
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
#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 <arpa/inet.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#if FTPD_HAS_GLOB
|
|
#include <glob.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <cinttypes>
|
|
#include <cstdarg>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <mutex>
|
|
#include <string>
|
|
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<int> (lhs_.size ()) - gsl::narrow_cast<int> (rhs_.size ());
|
|
}
|
|
|
|
/// \brief Parse command
|
|
/// \param buffer_ Buffer to parse
|
|
/// \param size_ Size of buffer
|
|
/// \returns {delimiterPos, nextPos}
|
|
std::pair<char *, char *> 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<char const *> (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<std::string_view> 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<unsigned> (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<float> (diff) / std::chrono::duration<float> (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<int> (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<UniqueFtpSession> const &sessions_)
|
|
{
|
|
// poll for pending close sockets first
|
|
std::vector<Socket::PollInfo> 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::microseconds> (
|
|
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<std::size_t> (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<unsigned long long> (st_.st_size));
|
|
if (rc < 0)
|
|
return errno;
|
|
if (static_cast<std::size_t> (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<unsigned long> (st_.st_mode & mask));
|
|
if (rc < 0)
|
|
return errno;
|
|
if (static_cast<std::size_t> (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<unsigned long> (st_.st_nlink),
|
|
owner,
|
|
group,
|
|
static_cast<unsigned long long> (st_.st_size));
|
|
if (rc < 0)
|
|
return errno;
|
|
|
|
if (static_cast<std::size_t> (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<std::size_t> (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<char const *> (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<std::size_t> (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 <NAME>\r\n"
|
|
" Set password: SITE PASS <PASS>\r\n"
|
|
" Set port: SITE PORT <PORT>\r\n"
|
|
#ifndef __NDS__
|
|
" Set hostname: SITE HOST <HOSTNAME>\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<std::uint64_t> (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<std::pair<std::string_view, void (FtpSession::*) (char const *)>> 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
|