// 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) 2023 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 "ftpServer.h" #include "fs.h" #ifndef __WIIU__ #include "licenses.h" #endif #include "log.h" #include "platform.h" #include "socket.h" #ifndef __WIIU__ #include "imgui.h" #endif #ifdef NDS #include #endif #include #include #include #include #include #include #include #include #include #include #include using namespace std::chrono_literals; #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 Application start time auto const s_startTime = std::time (nullptr); #ifndef NDS /// \brief Mutex for s_freeSpace platform::Mutex s_lock; #endif /// \brief Free space string std::string s_freeSpace; #ifndef CLASSIC #ifndef NDEBUG std::string printable (char *const data_, std::size_t const size_) { std::string result; result.reserve (size_); for (std::size_t i = 0; i < size_; ++i) { if (std::isprint (data_[i]) || std::isspace (data_[i])) result.push_back (data_[i]); else { char buffer[5]; std::snprintf ( buffer, sizeof (buffer), "%%%02u", static_cast (data_[i])); result += buffer; } } return result; } int curlDebug (CURL *const handle_, curl_infotype const type_, char *const data_, std::size_t const size_, void *const user_) { (void)user_; auto const text = printable (data_, size_); switch (type_) { case CURLINFO_TEXT: info ("== Info: %s", text.c_str ()); break; case CURLINFO_HEADER_OUT: info ("=> Send header: %s", text.c_str ()); break; case CURLINFO_DATA_OUT: info ("=> Send data: %s", text.c_str ()); break; case CURLINFO_SSL_DATA_OUT: info ("=> Send SSL data: %s", text.c_str ()); break; case CURLINFO_HEADER_IN: info ("<= Receive header: %s", text.c_str ()); break; case CURLINFO_DATA_IN: info ("<= Receive data: %s", text.c_str ()); break; case CURLINFO_SSL_DATA_IN: info ("<= Receive SSL data: %s", text.c_str ()); break; default: break; } return 0; } #endif std::size_t curlCallback (void *const contents_, std::size_t const size_, std::size_t const count_, void *const user_) { auto const total = size_ * count_; auto const start = static_cast (contents_); auto const end = start + total; auto &result = *static_cast (user_); result.insert (std::end (result), start, end); return total; } #endif } /////////////////////////////////////////////////////////////////////////// FtpServer::~FtpServer () { m_quit = true; #ifndef NDS m_thread.join (); #endif #ifndef CLASSIC if (m_uploadLogCurl) { curl_multi_remove_handle (m_uploadLogCurlM, m_uploadLogCurl); curl_easy_cleanup (m_uploadLogCurl); curl_mime_free (m_uploadLogMime); } if (m_uploadLogCurlM) curl_multi_cleanup (m_uploadLogCurlM); #endif } FtpServer::FtpServer (UniqueFtpConfig config_) : m_config (std::move (config_)), m_quit (false) { #ifndef NDS m_thread = platform::Thread (std::bind (&FtpServer::threadFunc, this)); #endif } void FtpServer::draw () { #ifdef NDS loop (); #endif #ifdef CLASSIC { char port[7]; #ifndef NDS auto const lock = std::scoped_lock (m_lock); #endif if (m_socket) std::sprintf (port, ":%u", m_socket->sockName ().port ()); #ifndef NO_CONSOLE consoleSelect (&g_statusConsole); std::printf ("\x1b[0;0H\x1b[32;1m%s \x1b[36;1m%s%s", STATUS_STRING, m_socket ? m_socket->sockName ().name () : "Waiting on WiFi", m_socket ? port : ""); #endif #ifndef NDS char timeBuffer[16]; auto const now = std::time (nullptr); std::strftime (timeBuffer, sizeof (timeBuffer), "%H:%M:%S", std::localtime (&now)); std::printf (" \x1b[37;1m%s", timeBuffer); #endif std::fputs ("\x1b[K", stdout); std::fflush (stdout); } { #ifndef NDS auto const lock = std::scoped_lock (s_lock); #endif if (!s_freeSpace.empty ()) { #ifndef NO_CONSOLE consoleSelect (&g_statusConsole); std::printf ("\x1b[0;%uH\x1b[32;1m%s", static_cast (g_statusConsole.windowWidth - s_freeSpace.size () + 1), s_freeSpace.c_str ()); std::fflush (stdout); #endif } } { #ifndef NDS auto const lock = std::scoped_lock (m_lock); #endif #ifndef NO_CONSOLE consoleSelect (&g_sessionConsole); std::fputs ("\x1b[2J", stdout); for (auto &session : m_sessions) { session->draw (); if (&session != &m_sessions.back ()) std::fputc ('\n', stdout); } std::fflush (stdout); #endif } drawLog (); #else auto const &io = ImGui::GetIO (); auto const width = io.DisplaySize.x; auto const height = io.DisplaySize.y; ImGui::SetNextWindowPos (ImVec2 (0, 0), ImGuiCond_FirstUseEver); #ifdef __3DS__ // top screen ImGui::SetNextWindowSize (ImVec2 (width, height * 0.5f)); #else ImGui::SetNextWindowSize (ImVec2 (width, height)); #endif { char title[64]; { auto const serverLock = std::scoped_lock (m_lock); std::snprintf (title, sizeof (title), STATUS_STRING " %s###ftpd", m_socket ? m_name.c_str () : "Waiting for WiFi..."); } ImGui::Begin (title, nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize #ifndef __3DS__ | ImGuiWindowFlags_MenuBar #endif ); } #ifndef __3DS__ showMenu (); #endif #ifndef __3DS__ ImGui::BeginChild ( "Logs", ImVec2 (0, 0.5f * height), false, ImGuiWindowFlags_HorizontalScrollbar); #endif drawLog (); #ifndef __3DS__ ImGui::EndChild (); #endif #ifdef __3DS__ ImGui::End (); // bottom screen ImGui::SetNextWindowSize (ImVec2 (width * 0.8f, height * 0.5f)); ImGui::SetNextWindowPos (ImVec2 (width * 0.1f, height * 0.5f), ImGuiCond_FirstUseEver); ImGui::Begin ("Sessions", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_MenuBar); showMenu (); #else ImGui::Separator (); #endif { auto const lock = std::scoped_lock (m_lock); for (auto &session : m_sessions) session->draw (); } ImGui::End (); #endif } UniqueFtpServer FtpServer::create () { updateFreeSpace (); auto config = FtpConfig::load (FTPDCONFIG); return UniqueFtpServer (new FtpServer (std::move (config))); } std::string FtpServer::getFreeSpace () { #ifndef NDS auto const lock = std::scoped_lock (s_lock); #endif return s_freeSpace; } void FtpServer::updateFreeSpace () { struct statvfs st; #if defined(NDS) || defined(__3DS__) || defined(__SWITCH__) if (::statvfs ("sdmc:/", &st) != 0) #else if (::statvfs ("/", &st) != 0) #endif return; auto freeSpace = fs::printSize (static_cast (st.f_bsize) * st.f_bfree); #ifndef NDS auto const lock = std::scoped_lock (s_lock); #endif if (freeSpace != s_freeSpace) s_freeSpace = std::move (freeSpace); } std::time_t FtpServer::startTime () { return s_startTime; } void FtpServer::handleNetworkFound () { SockAddr addr; if (!platform::networkAddress (addr)) return; std::uint16_t port; { #ifndef NDS auto const lock = m_config->lockGuard (); #endif port = m_config->port (); } addr.setPort (port); auto socket = Socket::create (); if (!socket) return; if (port != 0 && !socket->setReuseAddress (true)) return; if (!socket->bind (addr)) return; if (!socket->listen (10)) return; auto const &sockName = socket->sockName (); auto const name = sockName.name (); m_name.resize (std::strlen (name) + 3 + 5); m_name.resize (std::sprintf (m_name.data (), "[%s]:%u", name, sockName.port ())); info ("Started server at %s\n", m_name.c_str ()); LOCKED (m_socket = std::move (socket)); } void FtpServer::handleNetworkLost () { { // destroy sessions std::vector sessions; LOCKED (sessions = std::move (m_sessions)); } { // destroy command socket UniqueSocket sock; LOCKED (sock = std::move (m_socket)); } info ("Stopped server at %s\n", m_name.c_str ()); } #ifndef CLASSIC void FtpServer::showMenu () { auto const prevShowSettings = m_showSettings; auto const prevShowAbout = m_showAbout; if (ImGui::BeginMenuBar ()) { #if defined(__3DS__) || defined(__SWITCH__) if (ImGui::BeginMenu ("Menu \xee\x80\x83")) // Y Button #else if (ImGui::BeginMenu ("Menu")) #endif { if (ImGui::MenuItem ("Settings")) m_showSettings = true; if (ImGui::MenuItem ("Upload Log")) { #ifndef NDS auto const lock = std::scoped_lock (m_lock); #endif if (!m_uploadLogCurlM) m_uploadLogCurlM = curl_multi_init (); if (m_uploadLogCurlM && !m_uploadLogCurl.load (std::memory_order_relaxed)) { m_uploadLogData = getLog (); auto const handle = curl_easy_init (); #ifdef __3DS__ // 3DS CA fails peer verification, so add CA here curl_easy_setopt (handle, CURLOPT_CAINFO, "romfs:/sni.cloudflaressl.com.ca"); #endif #ifndef NDEBUG curl_easy_setopt (handle, CURLOPT_DEBUGFUNCTION, &curlDebug); curl_easy_setopt (handle, CURLOPT_DEBUGDATA, nullptr); curl_easy_setopt (handle, CURLOPT_VERBOSE, 1L); #endif // write result into string m_uploadLogResult.clear (); curl_easy_setopt (handle, CURLOPT_WRITEFUNCTION, &curlCallback); curl_easy_setopt (handle, CURLOPT_WRITEDATA, &m_uploadLogResult); // set headers static char contentType[] = "Content-Type: multipart/form-data"; static curl_slist const headers = {contentType, nullptr}; curl_easy_setopt (handle, CURLOPT_URL, "https://hastebin.com/documents"); curl_easy_setopt (handle, CURLOPT_HTTPHEADER, &headers); // set form data auto const mime = curl_mime_init (handle); auto const part = curl_mime_addpart (mime); curl_mime_name (part, "data"); curl_mime_data (part, m_uploadLogData.data (), m_uploadLogData.size ()); curl_easy_setopt (handle, CURLOPT_MIMEPOST, mime); // add to multi handle curl_multi_add_handle (m_uploadLogCurlM, handle); // signal network thread to process m_uploadLogMime = mime; m_uploadLogCurl.store (handle, std::memory_order_relaxed); } } if (ImGui::MenuItem ("About")) m_showAbout = true; ImGui::EndMenu (); } ImGui::EndMenuBar (); } if (m_showSettings) { if (!prevShowSettings) { #ifndef NDS auto const lock = m_config->lockGuard (); #endif 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 #ifdef __SWITCH__ m_enableAPSetting = m_config->enableAP (); m_ssidSetting = m_config->ssid (); m_ssidSetting.resize (19); m_passphraseSetting = m_config->passphrase (); m_passphraseSetting.resize (63); #endif ImGui::OpenPopup ("Settings"); } showSettings (); } if (m_showAbout) { if (!prevShowAbout) ImGui::OpenPopup ("About"); showAbout (); } } void FtpServer::showSettings () { #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.data (), m_userSetting.size (), ImGuiInputTextFlags_AutoSelectAll); ImGui::InputText ("Pass", m_passSetting.data (), 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 #ifdef __SWITCH__ ImGui::Checkbox ("Enable Access Point", &m_enableAPSetting); ImGui::InputText ("SSID", m_ssidSetting.data (), m_ssidSetting.size (), ImGuiInputTextFlags_AutoSelectAll); auto const ssidError = platform::validateSSID (m_ssidSetting); if (ssidError) ImGui::TextColored (ImVec4 (1.0f, 0.4f, 0.4f, 1.0f), ssidError); ImGui::InputText ("Passphrase", m_passphraseSetting.data (), m_passphraseSetting.size (), ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_Password); auto const passphraseError = platform::validatePassphrase (m_passphraseSetting); if (passphraseError) ImGui::TextColored (ImVec4 (1.0f, 0.4f, 0.4f, 1.0f), passphraseError); #endif static ImVec2 const sizes[] = { ImGui::CalcTextSize ("Apply"), ImGui::CalcTextSize ("Save"), ImGui::CalcTextSize ("Reset"), ImGui::CalcTextSize ("Cancel"), }; static auto const maxWidth = std::max_element ( std::begin (sizes), std::end (sizes), [] (auto const &lhs_, auto const &rhs_) { return lhs_.x < rhs_.x; })->x; static auto const maxHeight = std::max_element ( std::begin (sizes), std::end (sizes), [] (auto const &lhs_, auto const &rhs_) { return lhs_.y < rhs_.y; })->y; auto const &style = ImGui::GetStyle (); auto const width = maxWidth + 2 * style.FramePadding.x; auto const height = maxHeight + 2 * style.FramePadding.y; auto const apply = ImGui::Button ("Apply", ImVec2 (width, height)); ImGui::SameLine (); auto const save = ImGui::Button ("Save", ImVec2 (width, height)); ImGui::SameLine (); auto const reset = ImGui::Button ("Reset", ImVec2 (width, height)); ImGui::SameLine (); auto const cancel = ImGui::Button ("Cancel", ImVec2 (width, height)); if (apply || save) { m_showSettings = false; ImGui::CloseCurrentPopup (); #ifndef NDS auto const lock = m_config->lockGuard (); #endif m_config->setUser (m_userSetting); m_config->setPass (m_passSetting); m_config->setPort (m_portSetting); #ifdef __3DS__ m_config->setGetMTime (m_getMTimeSetting); #endif #ifdef __SWITCH__ m_config->setEnableAP (m_enableAPSetting); m_config->setSSID (m_ssidSetting); m_config->setPassphrase (m_passphraseSetting); m_apError = false; #endif UniqueSocket socket; LOCKED (socket = std::move (m_socket)); } if (save) { #ifndef NDS auto const lock = m_config->lockGuard (); #endif if (!m_config->save (FTPDCONFIG)) error ("Failed to save config\n"); } if (reset) { static auto const defaults = FtpConfig::create (); m_userSetting = defaults->user (); m_passSetting = defaults->pass (); m_portSetting = defaults->port (); #ifdef __3DS__ m_getMTimeSetting = defaults->getMTime (); #endif #ifdef __SWITCH__ m_enableAPSetting = defaults->enableAP (); m_ssidSetting = defaults->ssid (); m_passphraseSetting = defaults->passphrase (); #endif } if (apply || save || cancel) { m_showSettings = false; ImGui::CloseCurrentPopup (); } ImGui::EndPopup (); } } void FtpServer::showAbout () { auto const &io = ImGui::GetIO (); auto const width = io.DisplaySize.x; auto const height = io.DisplaySize.y; #ifdef __3DS__ ImGui::SetNextWindowSize (ImVec2 (width * 0.8f, height * 0.5f)); ImGui::SetNextWindowPos (ImVec2 (width * 0.1f, height * 0.5f)); #else ImGui::SetNextWindowSize (ImVec2 (width * 0.8f, height * 0.8f)); ImGui::SetNextWindowPos (ImVec2 (width * 0.1f, height * 0.1f)); #endif if (ImGui::BeginPopupModal ("About", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize)) { ImGui::TextUnformatted (STATUS_STRING); ImGui::TextWrapped ("Copyright © 2023 Michael Theall, Dave Murphy, TuxSH"); ImGui::Separator (); ImGui::Text ("Platform: %s", io.BackendPlatformName); ImGui::Text ("Renderer: %s", io.BackendRendererName); if (ImGui::Button ("OK", ImVec2 (100, 0))) { m_showAbout = false; ImGui::CloseCurrentPopup (); } ImGui::Separator (); if (ImGui::TreeNode (g_dearImGuiVersion)) { ImGui::TextWrapped ("%s", g_dearImGuiCopyright); ImGui::Separator (); ImGui::TextWrapped ("%s", g_mitLicense); ImGui::TreePop (); } #if defined(NDS) #elif defined(__3DS__) if (ImGui::TreeNode (g_libctruVersion)) { ImGui::TextWrapped ("%s", g_zlibLicense); ImGui::Separator (); ImGui::TextWrapped ("%s", g_zlibLicense); ImGui::TreePop (); } if (ImGui::TreeNode (g_citro3dVersion)) { ImGui::TextWrapped ("%s", g_citro3dCopyright); ImGui::Separator (); ImGui::TextWrapped ("%s", g_zlibLicense); ImGui::TreePop (); } #elif defined(__SWITCH__) if (ImGui::TreeNode (g_libnxVersion)) { ImGui::TextWrapped ("%s", g_libnxCopyright); ImGui::Separator (); ImGui::TextWrapped ("%s", g_libnxLicense); ImGui::TreePop (); } if (ImGui::TreeNode (g_deko3dVersion)) { ImGui::TextWrapped ("%s", g_deko3dCopyright); ImGui::Separator (); ImGui::TextWrapped ("%s", g_zlibLicense); ImGui::TreePop (); } if (ImGui::TreeNode (g_zstdVersion)) { ImGui::TextWrapped ("%s", g_zstdCopyright); ImGui::Separator (); ImGui::TextWrapped ("%s", g_bsdLicense); ImGui::TreePop (); } #else if (ImGui::TreeNode (g_glfwVersion)) { ImGui::TextWrapped ("%s", g_glfwCopyright); ImGui::Separator (); ImGui::TextWrapped ("%s", g_zlibLicense); ImGui::TreePop (); } #endif ImGui::EndPopup (); } } #endif void FtpServer::loop () { if (!m_socket) { #ifndef CLASSIC #ifdef __SWITCH__ if (!m_apError) { bool enable; std::string ssid; std::string passphrase; { auto const lock = m_config->lockGuard (); enable = m_config->enableAP (); ssid = m_config->ssid (); passphrase = m_config->passphrase (); } m_apError = !platform::enableAP (enable, ssid, passphrase); } #endif #endif if (platform::networkVisible ()) handleNetworkFound (); } #ifndef CLASSIC { auto const lock = std::scoped_lock (m_lock); if (m_uploadLogCurl.load (std::memory_order_relaxed)) { int busy = 0; auto const mc = curl_multi_perform (m_uploadLogCurlM, &busy); if (mc != CURLM_OK) { error ("curl_multi_perform: %d %s\n", mc, curl_multi_strerror (mc)); curl_multi_remove_handle (m_uploadLogCurlM, m_uploadLogCurl); curl_easy_cleanup (m_uploadLogCurl); curl_mime_free (m_uploadLogMime); m_uploadLogMime = nullptr; m_uploadLogCurl.store (nullptr, std::memory_order_relaxed); } else { int count = 0; auto const msg = curl_multi_info_read (m_uploadLogCurlM, &count); if (msg && msg->msg == CURLMSG_DONE && msg->easy_handle == m_uploadLogCurl) { if (msg->data.result != CURLE_OK) info ("cURL finished with status %d\n", msg->data.result); if (m_uploadLogResult.starts_with ("{\"key\":\"")) { auto const key = m_uploadLogResult.substr (8, 10); info ("https://hastebin.com/%s\n", key.c_str ()); } curl_multi_remove_handle (m_uploadLogCurlM, m_uploadLogCurl); curl_easy_cleanup (m_uploadLogCurl); curl_mime_free (m_uploadLogMime); m_uploadLogMime = nullptr; m_uploadLogCurl.store (nullptr, std::memory_order_relaxed); } } } } #endif // poll listen socket if (m_socket) { Socket::PollInfo info{*m_socket, POLLIN, 0}; auto const rc = Socket::poll (&info, 1, 0ms); if (rc < 0) { handleNetworkLost (); return; } if (rc > 0 && (info.revents & POLLIN)) { auto socket = m_socket->accept (); if (socket) { auto session = FtpSession::create (*m_config, std::move (socket)); LOCKED (m_sessions.emplace_back (std::move (session))); } else { handleNetworkLost (); return; } } } { std::vector deadSessions; { // remove dead sessions #ifndef NDS auto const lock = std::scoped_lock (m_lock); #endif auto it = std::begin (m_sessions); while (it != std::end (m_sessions)) { auto &session = *it; if (session->dead ()) { deadSessions.emplace_back (std::move (session)); it = m_sessions.erase (it); } else ++it; } } } // poll sessions if (!m_sessions.empty ()) { if (!FtpSession::poll (m_sessions)) handleNetworkLost (); } #ifndef NDS // avoid busy polling in background thread else platform::Thread::sleep (16ms); #endif } void FtpServer::threadFunc () { while (!m_quit) loop (); }