From 3f31371193a457aa22f3b60e857f4e52f2b0fec5 Mon Sep 17 00:00:00 2001 From: Michael Theall Date: Fri, 24 Apr 2020 15:51:43 -0500 Subject: [PATCH] Add configuration --- .clang-format | 1 + Makefile.3ds | 2 +- Makefile.linux | 3 +- Makefile.nds | 2 +- Makefile.switch | 1 + README.md | 10 ++ include/fs.h | 10 ++ include/ftpConfig.h | 100 +++++++++++++ include/ftpServer.h | 36 ++++- include/ftpSession.h | 24 ++- source/3ds/imgui_ctru.cpp | 54 +++++++ source/fs.cpp | 30 +++- source/ftpConfig.cpp | 257 +++++++++++++++++++++++++++++++ source/ftpServer.cpp | 141 +++++++++++++++-- source/ftpSession.cpp | 299 +++++++++++++++++++++++++++++++++++-- source/main.cpp | 2 +- source/switch/imgui_nx.cpp | 42 ++++++ source/switch/init.c | 2 +- 18 files changed, 978 insertions(+), 38 deletions(-) create mode 100644 include/ftpConfig.h create mode 100644 source/ftpConfig.cpp diff --git a/.clang-format b/.clang-format index 30f66ed..cba56ee 100644 --- a/.clang-format +++ b/.clang-format @@ -21,6 +21,7 @@ AlwaysBreakTemplateDeclarations: true BinPackArguments: false BinPackParameters: false BraceWrapping: + AfterCaseLabel: true AfterClass: true AfterControlStatement: true AfterEnum: true diff --git a/Makefile.3ds b/Makefile.3ds index 2070cc8..d8b1971 100644 --- a/Makefile.3ds +++ b/Makefile.3ds @@ -71,7 +71,7 @@ CFLAGS := -g -Wall $(OPTIMIZE) -mword-relocations \ CFLAGS += $(INCLUDE) -DARM11 -D_3DS \ -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ - -DNO_IPV6 + -DNO_IPV6 -DFTPDCONFIG="\"/config/ftpd/ftpd.cfg\"" CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 diff --git a/Makefile.linux b/Makefile.linux index bd484a1..cb41a61 100644 --- a/Makefile.linux +++ b/Makefile.linux @@ -11,7 +11,8 @@ CPPFLAGS := -g -Wall -pthread -Iinclude -Isource/linux \ `pkg-config --cflags gl glfw3` \ -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ - -DIMGUI_IMPL_OPENGL_LOADER_GLAD=1 + -DIMGUI_IMPL_OPENGL_LOADER_GLAD=1 \ + -DFTPDCONFIG="\"ftpd.cfg\"" CFLAGS := $(CPPFLAGS) CXXFLAGS := $(CPPFLAGS) -std=gnu++17 LDFLAGS := -pthread `pkg-config --libs gl glfw3` -ldl diff --git a/Makefile.nds b/Makefile.nds index f8c8899..6a31484 100644 --- a/Makefile.nds +++ b/Makefile.nds @@ -47,7 +47,7 @@ CFLAGS := -g -Wall $(OPTIMIZE) \ $(ARCH) $(INCLUDE) -DARM9 -DNDS \ -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ - -DNO_IPV6 -DCLASSIC + -DNO_IPV6 -DCLASSIC -DFTPDCONFIG="\"/config/ftpd/ftpd.cfg\"" CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 ASFLAGS := -g $(ARCH) LDFLAGS = -specs=ds_arm9.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) $(OPTIMIZE) diff --git a/Makefile.switch b/Makefile.switch index 1f9005f..b49eb66 100644 --- a/Makefile.switch +++ b/Makefile.switch @@ -74,6 +74,7 @@ CFLAGS := -g -Wall -Wno-narrowing $(OPTIMIZE) \ CFLAGS += $(INCLUDE) -D__SWITCH__ \ -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ + -DFTPDCONFIG="\"/config/ftpd/ftpd.cfg\"" \ `$(PREFIX)pkg-config --cflags libzstd` CXXFLAGS := $(CFLAGS) -std=gnu++17 -fno-exceptions -fno-rtti diff --git a/README.md b/README.md index e11bc15..c09e069 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Build `switch/ftpd.nro`: - RMD - RNFR - RNTO +- SITE - SIZE - STAT - STOR @@ -121,3 +122,12 @@ Build `switch/ftpd.nro`: ## Planned Commands - STOU + +## SITE commands + +- Show help: SITE HELP +- Set username: SITE USER +- Set password: SITE PASS +- Set port: SITE PORT +- Set getMTime: SITE MTIME [0|1] +- Save config: SITE SAVE diff --git a/include/fs.h b/include/fs.h index 7f8c0ec..3739453 100644 --- a/include/fs.h +++ b/include/fs.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace fs { @@ -82,6 +83,9 @@ public: /// \note Can return partial reads ssize_t read (void *data_, std::size_t size_); + /// \brief Read line + std::string_view readLine (); + /// \brief Read data /// \param data_ Output buffer /// \param size_ Size to read @@ -109,6 +113,12 @@ private: /// \brief Buffer size std::size_t m_bufferSize = 0; + + /// \brief Line buffer + char *m_lineBuffer = nullptr; + + /// \brief Line buffer size + std::size_t m_lineBufferSize = 0; }; /// Directory object diff --git a/include/ftpConfig.h b/include/ftpConfig.h new file mode 100644 index 0000000..58df89e --- /dev/null +++ b/include/ftpConfig.h @@ -0,0 +1,100 @@ +// 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) 2020 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 . + +#pragma once + +#include +#include +#include + +class FtpConfig; +using UniqueFtpConfig = std::unique_ptr; + +/// \brief FTP config +class FtpConfig +{ +public: + ~FtpConfig (); + + /// \brief Create config + static UniqueFtpConfig create (); + + /// \brief Load config + /// \param path_ Path to config file + static UniqueFtpConfig load (char const *path_); + + /// \brief Save config + /// \param path_ Path to config file + bool save (char const *path_); + + /// \brief Get user + std::string const &user () const; + + /// \brief Get password + std::string const &pass () const; + + /// \brief Get port + std::uint16_t port () const; + +#ifdef _3DS + /// \brief Whether to get mtime + /// \note only effective on 3DS + bool getMTime () const; +#endif + + /// \brief Set user + /// \param user_ User + void setUser (std::string const &user_); + + /// \brief Set password + /// \param pass_ Password + void setPass (std::string const &pass_); + + /// \brief Set listen port + /// \param port_ Listen port + bool setPort (std::string const &port_); + + /// \brief Set listen port + /// \param port_ Listen port + bool setPort (std::uint16_t port_); + +#ifdef _3DS + /// \brief Set whether to get mtime + /// \param getMTime_ Whether to get mtime + void setGetMTime (bool getMTime_); +#endif + +private: + FtpConfig (); + + /// \brief Username + std::string m_user; + + /// \brief Password + std::string m_pass; + + /// \brief Listen port + std::uint16_t m_port; + +#ifdef _3DS + /// \brief Whether to get mtime + bool m_getMTime = true; +#endif +}; diff --git a/include/ftpServer.h b/include/ftpServer.h index 13f5c68..c2ea971 100644 --- a/include/ftpServer.h +++ b/include/ftpServer.h @@ -20,6 +20,7 @@ #pragma once +#include "ftpConfig.h" #include "ftpSession.h" #include "platform.h" #include "socket.h" @@ -44,8 +45,7 @@ public: void draw (); /// \brief Create server - /// \param port_ Port to listen on - static UniqueFtpServer create (std::uint16_t port_); + static UniqueFtpServer create (); /// \brief Get free space static std::string getFreeSpace (); @@ -58,8 +58,8 @@ public: private: /// \brief Paramterized constructor - /// \param port_ Port to listen on - FtpServer (std::uint16_t port_); + /// \param config_ FTP config + FtpServer (UniqueFtpConfig config_); /// \brief Handle when network is found void handleNetworkFound (); @@ -67,6 +67,9 @@ private: /// \brief Handle when network is lost void handleNetworkLost (); + /// \brief Show menu in the current window + void showMenu (); + /// \brief Server loop void loop (); @@ -81,6 +84,9 @@ private: platform::Mutex m_lock; #endif + /// \brief Config + UniqueFtpConfig m_config; + /// \brief Listen socket UniqueSocket m_socket; @@ -90,9 +96,25 @@ private: /// \brief Sessions std::vector m_sessions; - /// \brief Port to listen on - std::uint16_t const m_port; - /// \brief Whether thread should quit std::atomic m_quit; + +#ifndef CLASSIC + /// \brief Whether to show settings menu + bool m_showSettings = false; + + /// \brief User name setting + std::string m_userSetting; + + /// \brief Password setting + std::string m_passSetting; + + /// \brief Port setting + std::uint16_t m_portSetting; + +#ifdef _3DS + /// \brief getMTime setting + bool m_getMTimeSetting; +#endif +#endif }; diff --git a/include/ftpSession.h b/include/ftpSession.h index 237656a..cf8b8b3 100644 --- a/include/ftpSession.h +++ b/include/ftpSession.h @@ -21,6 +21,7 @@ #pragma once #include "fs.h" +#include "ftpConfig.h" #include "ioBuffer.h" #include "platform.h" #include "socket.h" @@ -47,8 +48,9 @@ public: void draw (); /// \brief Create session + /// \param config_ FTP config /// \param commandSocket_ Command socket - static UniqueFtpSession create (UniqueSocket commandSocket_); + static UniqueFtpSession create (FtpConfig &config_, UniqueSocket commandSocket_); /// \brief Poll for activity /// \param sessions_ Sessions to poll @@ -122,8 +124,12 @@ private: }; /// \brief Parameterized constructor + /// \param config_ FTP config /// \param commandSocket_ Command socket - FtpSession (UniqueSocket commandSocket_); + FtpSession (FtpConfig &config_, UniqueSocket commandSocket_); + + /// \brief Whether session is authorized + bool authorized () const; /// \brief Set session state /// \param state_ State to set @@ -203,6 +209,9 @@ private: platform::Mutex m_lock; #endif + /// \brief FTP config + FtpConfig &m_config; + /// \brief Command socket SharedSocket m_commandSocket; @@ -278,6 +287,13 @@ private: /// \brief Directory transfer mode XferDirMode m_xferDirMode; + /// \brief Last command timestamp + time_t m_timestamp; + + /// \brief Whether user has been authorized + bool m_authorizedUser : 1; + /// \brief Whether password has been authorized + bool m_authorizedPass : 1; /// \brief Whether previous command was PASV bool m_pasv : 1; /// \brief Whether previous command was PORT @@ -409,6 +425,10 @@ private: /// \param args_ Command arguments void RNTO (char const *args_); + /// \brief Site command + /// \param args_ Command arguments + void SITE (char const *args_); + /// \brief Get file size /// \param args_ Command arguments void SIZE (char const *args_); diff --git a/source/3ds/imgui_ctru.cpp b/source/3ds/imgui_ctru.cpp index 1812874..136a53d 100644 --- a/source/3ds/imgui_ctru.cpp +++ b/source/3ds/imgui_ctru.cpp @@ -23,6 +23,8 @@ #include "imgui.h" +#include "../imgui/imgui_internal.h" + #include "fs.h" #include "platform.h" @@ -136,6 +138,57 @@ void updateGamepads (ImGuiIO &io_) io_.NavInputs[out] = std::clamp ((value - min) / (max - min), 0.0f, 1.0f); } } + +/// \brief Update keyboard inputs +/// \param io_ ImGui IO +void updateKeyboard (ImGuiIO &io_) +{ + static enum { + INACTIVE, + KEYBOARD, + CLEARED, + } state = INACTIVE; + + switch (state) + { + case INACTIVE: + { + if (!io_.WantTextInput) + return; + + auto &textState = ImGui::GetCurrentContext ()->InputTextState; + + SwkbdState kbd; + + swkbdInit (&kbd, SWKBD_TYPE_NORMAL, 2, -1); + swkbdSetButton (&kbd, SWKBD_BUTTON_LEFT, "Cancel", false); + swkbdSetButton (&kbd, SWKBD_BUTTON_RIGHT, "OK", true); + swkbdSetInitialText ( + &kbd, std::string (textState.InitialTextA.Data, textState.InitialTextA.Size).c_str ()); + + if (textState.UserFlags & ImGuiInputTextFlags_Password) + swkbdSetPasswordMode (&kbd, SWKBD_PASSWORD_HIDE_DELAY); + + char buffer[32] = {0}; + auto const button = swkbdInputText (&kbd, buffer, sizeof (buffer)); + if (button == SWKBD_BUTTON_RIGHT) + io_.AddInputCharactersUTF8 (buffer); + + state = KEYBOARD; + break; + } + + case KEYBOARD: + // need to skip a frame for active id to really be cleared + ImGui::ClearActiveID (); + state = CLEARED; + break; + + case CLEARED: + state = INACTIVE; + break; + } +} } bool imgui::ctru::init () @@ -180,5 +233,6 @@ void imgui::ctru::newFrame () updateTouch (io); updateGamepads (io); + updateKeyboard (io); } #endif diff --git a/source/fs.cpp b/source/fs.cpp index f580f46..9c59d76 100644 --- a/source/fs.cpp +++ b/source/fs.cpp @@ -23,6 +23,10 @@ #include #include +#if defined(NDS) || defined(_3DS) || defined(__SWITCH__) +#define getline __getline +#endif + std::string fs::printSize (std::uint64_t const size_) { constexpr std::uint64_t const KiB = 1024; @@ -77,7 +81,10 @@ std::string fs::printSize (std::uint64_t const size_) } /////////////////////////////////////////////////////////////////////////// -fs::File::~File () = default; +fs::File::~File () +{ + std::free (m_lineBuffer); +} fs::File::File () = default; @@ -136,6 +143,27 @@ ssize_t fs::File::read (void *const data_, std::size_t const size_) return std::fread (data_, 1, size_, m_fp.get ()); } +std::string_view fs::File::readLine () +{ + while (true) + { + auto rc = ::getline (&m_lineBuffer, &m_lineBufferSize, m_fp.get ()); + if (rc < 0) + return {}; + + while (rc > 0) + { + if (m_lineBuffer[rc - 1] != '\r' && m_lineBuffer[rc - 1] != '\n') + break; + + m_lineBuffer[--rc] = 0; + } + + if (rc > 0) + return std::string_view (m_lineBuffer, rc); + } +} + bool fs::File::readAll (void *const data_, std::size_t const size_) { auto p = static_cast (data_); diff --git a/source/ftpConfig.cpp b/source/ftpConfig.cpp new file mode 100644 index 0000000..00c0db3 --- /dev/null +++ b/source/ftpConfig.cpp @@ -0,0 +1,257 @@ +// 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) 2020 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 "ftpConfig.h" + +#include "fs.h" +#include "log.h" + +#include + +//#include + +namespace +{ +constexpr std::uint16_t DEFAULT_PORT = 5000; + +bool mkdirParent (std::string const &path_) +{ + auto pos = path_.find_first_of ('/'); + while (pos != std::string::npos) + { + auto const dir = path_.substr (0, pos); + + struct stat st; + auto const rc = ::stat (dir.c_str (), &st); + if (rc < 0 && errno != ENOENT) + return false; + + if (rc < 0 && errno == ENOENT) + { + auto const rc = ::mkdir (dir.c_str (), 0755); + if (rc < 0) + return false; + } + + pos = path_.find_first_of ('/', pos + 1); + } + + return true; +} + +std::string strip (std::string const &str_) +{ + auto const start = str_.find_first_not_of (" \t"); + if (start == std::string::npos) + return {}; + + auto const end = str_.find_last_not_of (" \t"); + + if (end == std::string::npos) + return str_.substr (start); + + return str_.substr (start, end + 1 - start); +} + +template +bool parseInt (T &out_, std::string const &val_) +{ + T val = 0; + + for (auto const &c : val_) + { + if (!std::isdigit (c)) + { + errno = EINVAL; + return false; + } + + if (std::numeric_limits::max () / 10 < val) + { + errno = EOVERFLOW; + return false; + } + + val *= 10; + + auto const v = c - '0'; + if (std::numeric_limits::max () - v < val) + { + errno = EOVERFLOW; + return false; + } + + val += v; + } + + out_ = val; + return true; +} +} + +/////////////////////////////////////////////////////////////////////////// +FtpConfig::~FtpConfig () = default; + +FtpConfig::FtpConfig () : m_port (DEFAULT_PORT) +{ +} + +UniqueFtpConfig FtpConfig::create () +{ + return UniqueFtpConfig (new FtpConfig ()); +} + +UniqueFtpConfig FtpConfig::load (char const *const path_) +{ + auto config = create (); + + auto fp = fs::File (); + if (!fp.open (path_)) + return config; + + std::uint16_t port = DEFAULT_PORT; + + std::string line; + while (!(line = fp.readLine ()).empty ()) + { + auto const pos = line.find_first_of ('='); + if (pos == std::string::npos) + { + error ("Ignoring '%s'\n", line.c_str ()); + continue; + } + + auto const key = strip (line.substr (0, pos)); + auto const val = strip (line.substr (pos + 1)); + if (key.empty () || val.empty ()) + { + error ("Ignoring '%s'\n", line.c_str ()); + continue; + } + + if (key == "user") + config->m_user = val; + else if (key == "pass") + config->m_pass = val; + else if (key == "port") + parseInt (port, val); +#ifdef _3DS + else if (key == "mtime") + { + if (val == "0") + config->m_getMTime = false; + else if (val == "1") + config->m_getMTime = true; + else + error ("Invalid value for mtime: %s\n", val.c_str ()); + } +#endif + } + + config->setPort (port); + + return config; +} + +bool FtpConfig::save (char const *const path_) +{ + if (!mkdirParent (path_)) + return false; + + auto fp = fs::File (); + if (!fp.open (path_, "wb")) + return false; + + if (!m_user.empty ()) + std::fprintf (fp, "user=%s\n", m_user.c_str ()); + if (!m_pass.empty ()) + std::fprintf (fp, "pass=%s\n", m_pass.c_str ()); + std::fprintf (fp, "port=%u\n", m_port); + +#ifdef _3DS + std::fprintf (fp, "mtime=%u\n", m_getMTime); +#endif + + return true; +} + +std::string const &FtpConfig::user () const +{ + return m_user; +} + +std::string const &FtpConfig::pass () const +{ + return m_pass; +} + +std::uint16_t FtpConfig::port () const +{ + return m_port; +} + +#ifdef _3DS +bool FtpConfig::getMTime () const +{ + return m_getMTime; +} +#endif + +void FtpConfig::setUser (std::string const &user_) +{ + m_user = user_.substr (0, user_.find_first_of ('\0')); +} + +void FtpConfig::setPass (std::string const &pass_) +{ + m_pass = pass_.substr (0, pass_.find_first_of ('\0')); +} + +bool FtpConfig::setPort (std::string const &port_) +{ + std::uint16_t parsed; + if (!parseInt (parsed, port_)) + return false; + + return setPort (parsed); +} + +bool FtpConfig::setPort (std::uint16_t const port_) +{ + if (port_ < 1024 +#if !defined(NDS) && !defined(_3DS) + && port_ != 0 +#endif + ) + { + errno = EPERM; + return false; + } + + m_port = port_; + return true; +} + +#ifdef _3DS +void FtpConfig::setGetMTime (bool const getMTime_) +{ + m_getMTime = getMTime_; +} +#endif diff --git a/source/ftpServer.cpp b/source/ftpServer.cpp index 0f97023..f471e68 100644 --- a/source/ftpServer.cpp +++ b/source/ftpServer.cpp @@ -77,7 +77,7 @@ FtpServer::~FtpServer () #endif } -FtpServer::FtpServer (std::uint16_t const port_) : m_port (port_), m_quit (false) +FtpServer::FtpServer (UniqueFtpConfig config_) : m_config (std::move (config_)), m_quit (false) { #ifndef NDS m_thread = platform::Thread (std::bind (&FtpServer::threadFunc, this)); @@ -172,12 +172,20 @@ void FtpServer::draw () ImGui::Begin (title, nullptr, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize); + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize +#ifndef _3DS + | ImGuiWindowFlags_MenuBar +#endif + ); } +#ifndef _3DS + showMenu (); +#endif + #ifndef _3DS ImGui::BeginChild ( - "Logs", ImVec2 (0, 0.55f * height), false, ImGuiWindowFlags_HorizontalScrollbar); + "Logs", ImVec2 (0, 0.5f * height), false, ImGuiWindowFlags_HorizontalScrollbar); #endif drawLog (); #ifndef _3DS @@ -192,7 +200,10 @@ void FtpServer::draw () ImGui::SetNextWindowPos (ImVec2 (width * 0.1f, height * 0.5f), ImGuiCond_FirstUseEver); ImGui::Begin ("Sessions", nullptr, - ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize); + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_MenuBar); + + showMenu (); #else ImGui::Separator (); #endif @@ -207,10 +218,13 @@ void FtpServer::draw () #endif } -UniqueFtpServer FtpServer::create (std::uint16_t const port_) +UniqueFtpServer FtpServer::create () { updateFreeSpace (); - return UniqueFtpServer (new FtpServer (port_)); + + auto config = FtpConfig::load (FTPDCONFIG); + + return UniqueFtpServer (new FtpServer (std::move (config))); } std::string FtpServer::getFreeSpace () @@ -256,13 +270,13 @@ void FtpServer::handleNetworkFound () #else addr.sin_addr.s_addr = INADDR_ANY; #endif - addr.sin_port = htons (m_port); + addr.sin_port = htons (m_config->port ()); auto socket = Socket::create (); if (!socket) return; - if (m_port != 0 && !socket->setReuseAddress (true)) + if (m_config->port () != 0 && !socket->setReuseAddress (true)) return; if (!socket->bind (addr)) @@ -292,6 +306,115 @@ void FtpServer::handleNetworkLost () info ("Stopped server at %s\n", m_name.c_str ()); } +void FtpServer::showMenu () +{ +#ifndef CLASSIC + auto const prevShowSettings = m_showSettings; + + if (ImGui::BeginMenuBar ()) + { +#if defined(_3DS) || defined(__SWITCH__) + if (ImGui::BeginMenu (u8"Menu \xee\x80\x83")) // Y Button +#else + if (ImGui::BeginMenu ("Menu")) +#endif + { + if (ImGui::MenuItem ("Settings")) + m_showSettings = true; + + ImGui::EndMenu (); + } + ImGui::EndMenuBar (); + } + + if (!prevShowSettings && m_showSettings) + { + m_userSetting = m_config->user (); + m_userSetting.resize (32); + + m_passSetting = m_config->pass (); + m_passSetting.resize (32); + + m_portSetting = m_config->port (); + +#ifdef _3DS + m_getMTimeSetting = m_config->getMTime (); +#endif + + ImGui::OpenPopup ("Settings"); + } + +#ifdef _3DS + auto const &io = ImGui::GetIO (); + auto const width = io.DisplaySize.x; + auto const height = io.DisplaySize.y; + + ImGui::SetNextWindowSize (ImVec2 (width * 0.8f, height * 0.5f)); + ImGui::SetNextWindowPos (ImVec2 (width * 0.1f, height * 0.5f)); + if (ImGui::BeginPopupModal ("Settings", + nullptr, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) +#else + if (ImGui::BeginPopupModal ("Settings", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) +#endif + { + ImGui::InputText ( + "User", &m_userSetting[0], m_userSetting.size (), ImGuiInputTextFlags_AutoSelectAll); + + ImGui::InputText ("Pass", + &m_passSetting[0], + m_passSetting.size (), + ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_Password); + + ImGui::InputScalar ("Port", + ImGuiDataType_U16, + &m_portSetting, + nullptr, + nullptr, + "%u", + ImGuiInputTextFlags_AutoSelectAll); + +#ifdef _3DS + ImGui::Checkbox ("Get mtime", &m_getMTimeSetting); +#endif + + auto const apply = ImGui::Button ("Apply", ImVec2 (100, 0)); + ImGui::SameLine (); + auto const save = ImGui::Button ("Save", ImVec2 (100, 0)); + ImGui::SameLine (); + auto const cancel = ImGui::Button ("Cancel", ImVec2 (100, 0)); + + if (apply || save) + { + m_showSettings = false; + ImGui::CloseCurrentPopup (); + + m_config->setUser (m_userSetting); + m_config->setPass (m_passSetting); + m_config->setPort (m_portSetting); + +#ifdef _3DS + m_config->setGetMTime (m_getMTimeSetting); +#endif + + UniqueSocket socket; + LOCKED (socket = std::move (m_socket)); + } + + if (save && !m_config->save (FTPDCONFIG)) + error ("Failed to save config\n"); + + if (apply || save || cancel) + { + m_showSettings = false; + ImGui::CloseCurrentPopup (); + } + + ImGui::EndPopup (); + } +#endif +} + void FtpServer::loop () { if (!m_socket) @@ -309,7 +432,7 @@ void FtpServer::loop () auto socket = m_socket->accept (); if (socket) { - auto session = FtpSession::create (std::move (socket)); + auto session = FtpSession::create (*m_config, std::move (socket)); LOCKED (m_sessions.emplace_back (std::move (session))); } else diff --git a/source/ftpSession.cpp b/source/ftpSession.cpp index 8da0c00..8d36acb 100644 --- a/source/ftpSession.cpp +++ b/source/ftpSession.cpp @@ -262,11 +262,14 @@ FtpSession::~FtpSession () closeData (); } -FtpSession::FtpSession (UniqueSocket commandSocket_) - : m_commandSocket (std::move (commandSocket_)), +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), @@ -278,6 +281,11 @@ FtpSession::FtpSession (UniqueSocket commandSocket_) m_mlstPerm (true), m_mlstUnixMode (false) { + 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; @@ -373,9 +381,9 @@ void FtpSession::draw () #endif } -UniqueFtpSession FtpSession::create (UniqueSocket commandSocket_) +UniqueFtpSession FtpSession::create (FtpConfig &config_, UniqueSocket commandSocket_) { - return UniqueFtpSession (new FtpSession (std::move (commandSocket_))); + return UniqueFtpSession (new FtpSession (config_, std::move (commandSocket_))); } bool FtpSession::poll (std::vector const &sessions_) @@ -557,8 +565,11 @@ bool FtpSession::poll (std::vector const &sessions_) } else if (i.revents & (POLLIN | POLLOUT)) { - while (((*session).*(session->m_transfer)) ()) - ; + for (unsigned i = 0; i < 10; ++i) + { + if (!((*session).*(session->m_transfer)) ()) + break; + } } } } @@ -568,6 +579,11 @@ bool FtpSession::poll (std::vector const &sessions_) 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_; @@ -989,8 +1005,10 @@ int FtpSession::fillDirent (struct stat const &st_, std::string_view const path_ if (!tm) return errno; - auto fmt = "%b %e %H:%M "; - rc = std::strftime (&buffer[pos], size - pos, fmt, tm); + auto fmt = "%b %e %Y "; + if (m_timestamp > st_.st_mtime && m_timestamp - st_.st_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) @@ -1009,6 +1027,7 @@ int FtpSession::fillDirent (struct stat const &st_, std::string_view const path_ buffer[pos++] = '\n'; m_xferBuffer.markUsed (pos); + LOCKED (m_filePosition += pos); return 0; } @@ -1145,6 +1164,7 @@ void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool m_recv = false; m_send = true; + m_filePosition = 0; m_xferBuffer.clear (); m_transfer = &FtpSession::listTransfer; @@ -1233,7 +1253,7 @@ void FtpSession::xferDir (char const *const args_, XferDirMode const mode_, bool // everything else uses basename auto const pos = path.find_last_of ('/'); assert (pos != std::string::npos); - name = encodePath (std::string_view (path).substr (pos)); + name = encodePath (std::string_view (path).substr (pos + 1)); } auto const rc = fillDirent (st, name); @@ -1420,6 +1440,7 @@ void FtpSession::readCommand (int const events_) return ::strcasecmp (lhs_.first.data (), rhs_) < 0; }); + m_timestamp = std::time (nullptr); if (it == std::end (handlers) || ::strcasecmp (it->first.data (), command) != 0) { std::string response = "502 Invalid command \""; @@ -1585,13 +1606,17 @@ bool FtpSession::listTransfer () if (m_xferDirMode == XferDirMode::NLST) { // NLST gives the whole path name - auto const path = encodePath (buildPath (m_lwd, dent->d_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 { @@ -1629,7 +1654,7 @@ bool FtpSession::listTransfer () else if (m_xferDirMode == XferDirMode::NLST) getmtime = false; - if (getmtime) + if (getmtime && m_config.getMTime ()) { std::uint64_t mtime = 0; auto const rc = archive_getmtime (fullPath.c_str (), &mtime); @@ -1801,6 +1826,13 @@ void FtpSession::ALLO (char const *args_) 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); } @@ -1809,6 +1841,12 @@ void FtpSession::CDUP (char const *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)); @@ -1822,6 +1860,12 @@ 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)); @@ -1835,6 +1879,12 @@ 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 ()) @@ -1885,6 +1935,13 @@ void FtpSession::HELP (char const *args_) 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); } @@ -1892,6 +1949,13 @@ void FtpSession::LIST (char const *args_) void FtpSession::MDTM (char const *args_) { setState (State::COMMAND, false, false); + + if (!authorized ()) + { + sendResponse ("530 Not logged in\r\n"); + return; + } + sendResponse ("502 Command not implemented\r\n"); } @@ -1899,6 +1963,12 @@ 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 ()) @@ -1920,6 +1990,13 @@ void FtpSession::MKD (char const *args_) 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, true); } @@ -1977,6 +2054,13 @@ void FtpSession::MODE (char const *args_) void FtpSession::NLST (char const *args_) { + if (!authorized ()) + { + setState (State::COMMAND, false, false); + sendResponse ("530 Not logged in\r\n"); + return; + } + // open the path in NLST mode xferDir (args_, XferDirMode::NLST, false); } @@ -2048,11 +2132,34 @@ void FtpSession::OPTS (char const *args_) void FtpSession::PASS (char const *args_) { setState (State::COMMAND, false, false); - sendResponse ("230 OK\r\n"); + + m_authorizedPass = false; + + if (!m_config.user ().empty () && !m_authorizedUser) + { + sendResponse ("430 User not authorized\r\n"); + return; + } + + if (m_config.pass ().empty () || m_config.pass () == args_) + { + m_authorizedPass = true; + sendResponse ("230 OK\r\n"); + return; + } + + sendResponse ("430 Invalid password\r\n"); } void FtpSession::PASV (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; @@ -2117,6 +2224,13 @@ void FtpSession::PASV (char const *args_) 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; @@ -2203,6 +2317,12 @@ void FtpSession::PWD (char const *args_) { setState (State::COMMAND, false, false); + if (!authorized ()) + { + sendResponse ("530 Not logged in\r\n"); + return; + } + auto const path = encodePath (m_cwd); std::string response = "257 \""; @@ -2222,6 +2342,12 @@ 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) @@ -2250,6 +2376,13 @@ void FtpSession::REST (char const *args_) 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); } @@ -2258,6 +2391,12 @@ 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 ()) @@ -2281,6 +2420,12 @@ 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 ()) @@ -2306,6 +2451,12 @@ 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 ()) { @@ -2337,10 +2488,99 @@ void FtpSession::RNTO (char const *args_) sendResponse ("250 OK\r\n"); } +void FtpSession::SITE (char const *args_) +{ + setState (State::COMMAND, false, false); + + auto const str = std::string (args_); + auto const pos = str.find_first_of (' '); + + auto const command = str.substr (0, pos); + auto const arg = pos == std::string::npos ? std::string () : str.substr (pos + 1); + + if (::strcasecmp (command.c_str (), "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" +#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 (::strcasecmp (command.c_str (), "USER") == 0) + { + m_config.setUser (arg); + sendResponse ("200 OK\r\n"); + return; + } + else if (::strcasecmp (command.c_str (), "PASS") == 0) + { + m_config.setPass (arg); + sendResponse ("200 OK\r\n"); + return; + } + else if (::strcasecmp (command.c_str (), "PORT") == 0) + { + if (!m_config.setPort (arg)) + { + sendResponse ("550 %s\r\n", std::strerror (errno)); + return; + } + + sendResponse ("200 OK\r\n"); + return; + } +#ifdef _3DS + else if (::strcasecmp (command.c_str (), "MTIME") == 0) + { + if (arg == "0") + m_config.setGetMTime (false); + else if (arg == "1") + m_config.setGetMTime (true); + else + { + sendResponse ("550 %s\r\n", std::strerror (EINVAL)); + return; + } + } +#endif + else if (::strcasecmp (command.c_str (), "SAVE") == 0) + { + if (!m_config.save (FTPDCONFIG)) + { + 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 ()) @@ -2371,7 +2611,7 @@ void FtpSession::STAT (char const *args_) if (m_state == State::DATA_CONNECT) { sendResponse ("211-FTP server status\r\n" - " Waitin for data connection\r\n" + " Waiting for data connection\r\n" "211 End\r\n"); return; } @@ -2404,11 +2644,25 @@ void FtpSession::STAT (char const *args_) 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); } @@ -2450,7 +2704,23 @@ void FtpSession::TYPE (char const *args_) void FtpSession::USER (char const *args_) { setState (State::COMMAND, false, false); - sendResponse ("230 OK\r\n"); + + m_authorizedUser = false; + + if (m_config.user ().empty () || m_config.user () == args_) + { + m_authorizedUser = true; + if (m_config.pass ().empty ()) + { + sendResponse ("230 OK\r\n"); + return; + } + + sendResponse ("331 Need password\r\n"); + return; + } + + sendResponse ("502 Invalid user\r\n"); } // clang-format off @@ -2484,6 +2754,7 @@ std::vector> co {"RMD", &FtpSession::RMD}, {"RNFR", &FtpSession::RNFR}, {"RNTO", &FtpSession::RNTO}, + {"SITE", &FtpSession::SITE}, {"SIZE", &FtpSession::SIZE}, {"STAT", &FtpSession::STAT}, {"STOR", &FtpSession::STOR}, diff --git a/source/main.cpp b/source/main.cpp index 96318ed..def262b 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -49,7 +49,7 @@ int main (int argc_, char *argv_[]) style.WindowRounding = 0.0f; #endif - auto server = FtpServer::create (5000); + auto server = FtpServer::create (); while (platform::loop ()) { diff --git a/source/switch/imgui_nx.cpp b/source/switch/imgui_nx.cpp index 2ae3186..24c1f8a 100644 --- a/source/switch/imgui_nx.cpp +++ b/source/switch/imgui_nx.cpp @@ -23,6 +23,8 @@ #include "imgui.h" +#include "../imgui/imgui_internal.h" + #include "fs.h" #include "platform.h" @@ -1376,6 +1378,46 @@ void updateKeyboard (ImGuiIO &io_) for (int i = 0; i < 256; ++i) io_.KeysDown[i] = hidKeyboardHeld (static_cast (i)); + + static enum { + INACTIVE, + KEYBOARD, + CLEARED, + } state = INACTIVE; + + switch (state) + { + case INACTIVE: + { + if (!io_.WantTextInput) + return; + + auto &textState = ImGui::GetCurrentContext ()->InputTextState; + + SwkbdConfig kbd; + swkbdCreate (&kbd, 0); + swkbdConfigMakePresetDefault (&kbd); + swkbdConfigSetInitialText ( + &kbd, std::string (textState.InitialTextA.Data, textState.InitialTextA.Size).c_str ()); + + char buffer[32]; + if (R_SUCCEEDED (swkbdShow (&kbd, buffer, sizeof (buffer)))) + io_.AddInputCharactersUTF8 (buffer); + + state = KEYBOARD; + break; + } + + case KEYBOARD: + // need to skip a frame for active id to really be cleared + ImGui::ClearActiveID (); + state = CLEARED; + break; + + case CLEARED: + state = INACTIVE; + break; + } } } diff --git a/source/switch/init.c b/source/switch/init.c index 9e64420..ae9c6d9 100644 --- a/source/switch/init.c +++ b/source/switch/init.c @@ -41,7 +41,7 @@ static SocketInitConfig const s_socketInitConfig = { .sb_efficiency = 8, - .num_bsd_sessions = 1, + .num_bsd_sessions = 2, .bsd_service_type = BsdServiceType_User, };