From a0ee0481eca9df0bc40eb9684a307319af726435 Mon Sep 17 00:00:00 2001
From: VoltZ <47382115+SuperSaiyajinVoltZ@users.noreply.github.com>
Date: Fri, 8 Nov 2019 03:20:30 +0100
Subject: [PATCH] Add FTP.
---
include/screens/ftpScreen.hpp | 37 +
include/utils/console.h | 39 +
include/utils/ftp.h | 18 +
romfs/lang/en/app.json | 9 +-
source/main.cpp | 2 +
source/screens/ftpScreen.cpp | 96 +
source/screens/mainMenu.cpp | 5 +
source/utils/console.c | 237 ++
source/utils/ftp.c | 4042 +++++++++++++++++++++++++++++++++
9 files changed, 4484 insertions(+), 1 deletion(-)
create mode 100644 include/screens/ftpScreen.hpp
create mode 100644 include/utils/console.h
create mode 100644 include/utils/ftp.h
create mode 100644 source/screens/ftpScreen.cpp
create mode 100644 source/utils/console.c
create mode 100644 source/utils/ftp.c
diff --git a/include/screens/ftpScreen.hpp b/include/screens/ftpScreen.hpp
new file mode 100644
index 0000000..a871cee
--- /dev/null
+++ b/include/screens/ftpScreen.hpp
@@ -0,0 +1,37 @@
+/*
+* This file is part of Universal-Updater
+* Copyright (C) 2019 VoltZ, Epicpkmn11, Flame, RocketRobz, TotallyNotGuy
+*
+* 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 .
+*
+* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
+* * Requiring preservation of specified reasonable legal notices or
+* author attributions in that material or in the Appropriate Legal
+* Notices displayed by works containing it.
+* * Prohibiting misrepresentation of the origin of that material,
+* or requiring that modified versions of such material be marked in
+* reasonable ways as different from the original version.
+*/
+
+#include "screens/screen.hpp"
+
+class FTPScreen : public Screen
+{
+public:
+ void Draw(void) const override;
+ void Logic(u32 hDown, u32 hHeld, touchPosition touch) override;
+
+private:
+ int ftpEnabled = 1;
+};
\ No newline at end of file
diff --git a/include/utils/console.h b/include/utils/console.h
new file mode 100644
index 0000000..78aef52
--- /dev/null
+++ b/include/utils/console.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#ifdef _3DS
+#include <3ds.h>
+#define ESC(x) "\x1b[" #x
+#define RESET ESC(0m)
+#define BLACK ESC(30m)
+#define RED ESC(31;1m)
+#define GREEN ESC(32;1m)
+#define YELLOW ESC(33;1m)
+#define BLUE ESC(34;1m)
+#define MAGENTA ESC(35;1m)
+#define CYAN ESC(36;1m)
+#define WHITE ESC(37;1m)
+#else
+#define ESC(x)
+#define RESET
+#define BLACK
+#define RED
+#define GREEN
+#define YELLOW
+#define BLUE
+#define MAGENTA
+#define CYAN
+#define WHITE
+#endif
+
+void console_init(void);
+
+__attribute__((format(printf,1,2)))
+void console_set_status(const char *fmt, ...);
+
+__attribute__((format(printf,1,2)))
+void console_print(const char *fmt, ...);
+
+__attribute__((format(printf,1,2)))
+void debug_print(const char *fmt, ...);
+
+void console_render(void);
diff --git a/include/utils/ftp.h b/include/utils/ftp.h
new file mode 100644
index 0000000..cafb236
--- /dev/null
+++ b/include/utils/ftp.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <3ds.h>
+
+/*! Loop status */
+typedef enum
+{
+ LOOP_CONTINUE, /*!< Continue looping */
+ LOOP_RESTART, /*!< Reinitialize */
+ LOOP_EXIT, /*!< Terminate looping */
+} loop_status_t;
+
+bool isTransfering;
+char ftp_accepted_connection[50], ftp_file_transfer[100];
+
+int ftp_init(void);
+loop_status_t ftp_loop(void);
+void ftp_exit(void);
diff --git a/romfs/lang/en/app.json b/romfs/lang/en/app.json
index 63c4c9d..8a37d38 100644
--- a/romfs/lang/en/app.json
+++ b/romfs/lang/en/app.json
@@ -44,5 +44,12 @@
"FUTURE_SCRIPT": "This Script is a Future Script.",
"OUTDATED_SCRIPT": "This Script is outdated.",
- "UP-TO-DATE": "This Script is Up-To-Date."
+ "UP-TO-DATE": "This Script is Up-To-Date.",
+
+ "FTP_MODE": "FTP Mode",
+ "FTP_INITIALIZED": "FTP Initialized.",
+ "FAILED_GET_IP": "Failed to get IP.",
+ "FAILED_INITIALIZE_FTP": "Failed to initialize FTP.",
+ "B_FTP_EXIT": "Press B to exit from FTP.",
+ "WIFI_NOT_ENABLED": "WiFi not enabled."
}
diff --git a/source/main.cpp b/source/main.cpp
index 8887792..e264aff 100644
--- a/source/main.cpp
+++ b/source/main.cpp
@@ -57,6 +57,7 @@ int main()
romfsInit();
sdmcInit();
cfguInit();
+ acInit();
// Create Folder if missing.
mkdir("sdmc:/3ds", 0777);
mkdir("sdmc:/3ds/Universal-Updater", 0777);
@@ -90,6 +91,7 @@ int main()
Gui::exit();
gfxExit();
cfguExit();
+ acExit();
romfsExit();
sdmcExit();
return 0;
diff --git a/source/screens/ftpScreen.cpp b/source/screens/ftpScreen.cpp
new file mode 100644
index 0000000..ad8776e
--- /dev/null
+++ b/source/screens/ftpScreen.cpp
@@ -0,0 +1,96 @@
+/*
+* This file is part of Universal-Updater
+* Copyright (C) 2019 VoltZ, Epicpkmn11, Flame, RocketRobz, TotallyNotGuy
+*
+* 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 .
+*
+* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
+* * Requiring preservation of specified reasonable legal notices or
+* author attributions in that material or in the Appropriate Legal
+* Notices displayed by works containing it.
+* * Prohibiting misrepresentation of the origin of that material,
+* or requiring that modified versions of such material be marked in
+* reasonable ways as different from the original version.
+*/
+
+#include "lang/lang.hpp"
+
+#include "screens/ftpScreen.hpp"
+#include "screens/screenCommon.hpp"
+
+#include "utils/config.hpp"
+
+#include
+#include
+#include
+
+extern "C" {
+ #include "ftp.h"
+}
+
+void FTPScreen::Draw(void) const
+{
+ ftp_init();
+ Result ret = 0;
+ char buf[137], hostname[128];
+ u32 wifiStatus = 0;
+ ret = gethostname(hostname, sizeof(hostname));
+
+ while(ftpEnabled == 1) {
+ ftp_loop();
+ Gui::clearTextBufs();
+ C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
+ Gui::DrawTop();
+ Gui::DrawString((400-Gui::GetStringWidth(0.8f, Lang::get("FTP_MODE")))/2, 2, 0.8f, Config::TxtColor, Lang::get("FTP_MODE"), 400);
+ Gui::DrawBottom();
+ ret = ACU_GetWifiStatus(&wifiStatus);
+
+ if ((wifiStatus != 0) && R_SUCCEEDED(ret)) {
+ Gui::DrawStringCentered(0, 40, 0.48f, Config::TxtColor, Lang::get("FTP_INITIALIZED"), 320);
+ snprintf(buf, 137, "IP: %s:5000", R_FAILED(ret)? Lang::get("FAILED_GET_IP").c_str() : hostname);
+
+ if (strlen(ftp_accepted_connection) != 0)
+ Gui::DrawStringCentered(0, 80, 0.45f, Config::TxtColor, ftp_accepted_connection, 320);
+
+ if (strlen(ftp_file_transfer) != 0)
+ Gui::DrawStringCentered(0, 150, 0.45f, Config::TxtColor, ftp_file_transfer, 320);
+ }
+ else {
+ Gui::DrawStringCentered(0, 40, 0.48f, Config::TxtColor, Lang::get("FAILED_INITIALIZE_FTP"), 320);
+ snprintf(buf, 18, Lang::get("WIFI_NOT_ENABLED").c_str());
+ }
+
+ Gui::DrawStringCentered(0, 60, 0.48, Config::TxtColor, buf, 320);
+ Gui::DrawStringCentered(0, 220, 0.48f, Config::TxtColor, Lang::get("B_FTP_EXIT"), 320);
+
+ Gui::clearTextBufs();
+ C3D_FrameEnd(0);
+ hidScanInput();
+ u32 hDown = hidKeysDown();
+
+ if (hDown & KEY_B)
+ break;
+ }
+ memset(ftp_accepted_connection, 0, 20); // Empty accepted connection address.
+ memset(ftp_file_transfer, 0, 50); // Empty transfer status.
+ ftp_exit();
+
+ Gui::screenBack();
+ return;
+}
+
+// Needed here, otherwise it won't compile.
+void FTPScreen::Logic(u32 hDown, u32 hHeld, touchPosition touch)
+{
+}
\ No newline at end of file
diff --git a/source/screens/mainMenu.cpp b/source/screens/mainMenu.cpp
index cbcf54a..e8de14d 100644
--- a/source/screens/mainMenu.cpp
+++ b/source/screens/mainMenu.cpp
@@ -26,6 +26,7 @@
#include "download/download.hpp"
+#include "screens/ftpScreen.hpp"
#include "screens/mainMenu.hpp"
#include "screens/scriptBrowse.hpp"
#include "screens/scriptlist.hpp"
@@ -92,6 +93,10 @@ void MainMenu::Logic(u32 hDown, u32 hHeld, touchPosition touch) {
}
}
+ if (hDown & KEY_X) {
+ Gui::setScreen(std::make_unique());
+ }
+
if (hDown & KEY_TOUCH) {
if (touching(touch, mainButtons[0])) {
Gui::setScreen(std::make_unique());
diff --git a/source/utils/console.c b/source/utils/console.c
new file mode 100644
index 0000000..d00e0f2
--- /dev/null
+++ b/source/utils/console.c
@@ -0,0 +1,237 @@
+#include "utils/console.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef _3DS
+#include <3ds.h>
+
+static PrintConsole status_console;
+static PrintConsole main_console;
+static PrintConsole tcp_console;
+#if ENABLE_LOGGING
+static bool disable_logging = false;
+#endif
+
+/*! initialize console subsystem */
+void
+console_init(void)
+{
+ consoleInit(GFX_TOP, &status_console);
+ consoleSetWindow(&status_console, 0, 0, 50, 1);
+
+ consoleInit(GFX_TOP, &main_console);
+ consoleSetWindow(&main_console, 0, 1, 50, 29);
+
+ consoleInit(GFX_BOTTOM, &tcp_console);
+
+ consoleSelect(&main_console);
+}
+
+/*! set status bar contents
+ *
+ * @param[in] fmt format string
+ * @param[in] ... format arguments
+ */
+void
+console_set_status(const char *fmt, ...)
+{
+ va_list ap;
+
+ consoleSelect(&status_console);
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+#ifdef ENABLE_LOGGING
+ vfprintf(stderr, fmt, ap);
+#endif
+ va_end(ap);
+ consoleSelect(&main_console);
+}
+
+/*! add text to the console
+ *
+ * @param[in] fmt format string
+ * @param[in] ... format arguments
+ */
+void
+console_print(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+#ifdef ENABLE_LOGGING
+ if (!disable_logging)
+ vfprintf(stderr, fmt, ap);
+#endif
+ va_end(ap);
+}
+
+/*! print debug message
+ *
+ * @param[in] fmt format string
+ * @param[in] ... format arguments
+ */
+void
+debug_print(const char *fmt, ...)
+{
+#ifdef ENABLE_LOGGING
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+#endif
+}
+
+/*! print tcp tables */
+static void
+print_tcp_table(void)
+{
+ static SOCU_TCPTableEntry tcp_entries[32];
+ socklen_t optlen;
+ size_t i;
+ int rc, lines = 0;
+
+#ifdef ENABLE_LOGGING
+ disable_logging = true;
+#endif
+
+ consoleSelect(&tcp_console);
+ console_print("\x1b[0;0H\x1b[K");
+ optlen = sizeof(tcp_entries);
+ rc = SOCU_GetNetworkOpt(SOL_CONFIG, NETOPT_TCP_TABLE, tcp_entries, &optlen);
+ if(rc != 0 && errno != ENODEV)
+ console_print(RED "tcp table: %d %s\n\x1b[J\n" RESET, errno, strerror(errno));
+ else if(rc == 0)
+ {
+ for(i = 0; lines < 30 && i < optlen / sizeof(SOCU_TCPTableEntry); ++i)
+ {
+ SOCU_TCPTableEntry *entry = &tcp_entries[i];
+ struct sockaddr_in *local = (struct sockaddr_in*)&entry->local;
+ struct sockaddr_in *remote = (struct sockaddr_in*)&entry->remote;
+
+ console_print(GREEN "%stcp[%zu]: ", i == 0 ? "" : "\n", i);
+ switch(entry->state)
+ {
+ case TCP_STATE_CLOSED:
+ console_print("CLOSED\x1b[K");
+ local = remote = NULL;
+ break;
+
+ case TCP_STATE_LISTEN:
+ console_print("LISTEN\x1b[K");
+ remote = NULL;
+ break;
+
+ case TCP_STATE_ESTABLISHED:
+ console_print("ESTABLISHED\x1b[K");
+ break;
+
+ case TCP_STATE_FINWAIT1:
+ console_print("FINWAIT1\x1b[K");
+ break;
+
+ case TCP_STATE_FINWAIT2:
+ console_print("FINWAIT2\x1b[K");
+ break;
+
+ case TCP_STATE_CLOSE_WAIT:
+ console_print("CLOSE_WAIT\x1b[K");
+ break;
+
+ case TCP_STATE_LAST_ACK:
+ console_print("LAST_ACK\x1b[K");
+ break;
+
+ case TCP_STATE_TIME_WAIT:
+ console_print("TIME_WAIT\x1b[K");
+ break;
+
+ default:
+ console_print("State %lu\x1b[K", entry->state);
+ break;
+ }
+
+ ++lines;
+
+ if(local && (lines++ < 30))
+ console_print("\n Local %s:%u\x1b[K", inet_ntoa(local->sin_addr),
+ ntohs(local->sin_port));
+
+ if(remote && (lines++ < 30))
+ console_print("\n Peer %s:%u\x1b[K", inet_ntoa(remote->sin_addr),
+ ntohs(remote->sin_port));
+ }
+
+ console_print(RESET "\x1b[J");
+ }
+ else
+ console_print("\x1b[2J");
+
+ consoleSelect(&main_console);
+
+#ifdef ENABLE_LOGGING
+ disable_logging = false;
+#endif
+}
+
+/*! draw console to screen */
+void
+console_render(void)
+{
+ /* print tcp table */
+ print_tcp_table();
+
+ /* flush framebuffer */
+ gfxFlushBuffers();
+ gspWaitForVBlank();
+ gfxSwapBuffers();
+}
+#else
+
+/* this is a lot easier when you have a real console */
+
+void
+console_init(void)
+{
+}
+
+void
+console_set_status(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ va_end(ap);
+ fputc('\n', stdout);
+}
+
+void
+console_print(const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ va_end(ap);
+}
+
+void
+debug_print(const char *fmt, ...)
+{
+#ifdef ENABLE_LOGGING
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+#endif
+}
+
+void console_render(void)
+{
+}
+#endif
\ No newline at end of file
diff --git a/source/utils/ftp.c b/source/utils/ftp.c
new file mode 100644
index 0000000..7109759
--- /dev/null
+++ b/source/utils/ftp.c
@@ -0,0 +1,4042 @@
+/* This FTP server implementation is based on RFC 959,
+ * (https://tools.ietf.org/html/rfc959), RFC 3659
+ * (https://tools.ietf.org/html/rfc3659) and suggested implementation details
+ * from https://cr.yp.to/ftp/filesystem.html
+ */
+
+#include "utils/console.h"
+#include "utils/ftp.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#ifdef _3DS
+#define lstat stat
+#else
+#include
+#define BIT(x) (1<<(x))
+#endif
+
+#define POLL_UNKNOWN (~(POLLIN|POLLPRI|POLLOUT))
+
+#define XFER_BUFFERSIZE 32768
+#define SOCK_BUFFERSIZE 32768
+#define FILE_BUFFERSIZE 65536
+#define CMD_BUFFERSIZE 4096
+#define SOCU_ALIGN 0x1000
+#define SOCU_BUFFERSIZE 0x100000
+#define LISTEN_PORT 5000
+#ifdef _3DS
+#define DATA_PORT (LISTEN_PORT+1)
+#else
+#define DATA_PORT 0 /* ephemeral port */
+#endif
+
+typedef struct ftp_session_t ftp_session_t;
+
+#define FTP_DECLARE(x) static void x(ftp_session_t *session, const char *args)
+FTP_DECLARE(ABOR);
+FTP_DECLARE(ALLO);
+FTP_DECLARE(APPE);
+FTP_DECLARE(CDUP);
+FTP_DECLARE(CWD);
+FTP_DECLARE(DELE);
+FTP_DECLARE(FEAT);
+FTP_DECLARE(HELP);
+FTP_DECLARE(LIST);
+FTP_DECLARE(MDTM);
+FTP_DECLARE(MKD);
+FTP_DECLARE(MLSD);
+FTP_DECLARE(MLST);
+FTP_DECLARE(MODE);
+FTP_DECLARE(NLST);
+FTP_DECLARE(NOOP);
+FTP_DECLARE(OPTS);
+FTP_DECLARE(PASS);
+FTP_DECLARE(PASV);
+FTP_DECLARE(PORT);
+FTP_DECLARE(PWD);
+FTP_DECLARE(QUIT);
+FTP_DECLARE(REST);
+FTP_DECLARE(RETR);
+FTP_DECLARE(RMD);
+FTP_DECLARE(RNFR);
+FTP_DECLARE(RNTO);
+FTP_DECLARE(SIZE);
+FTP_DECLARE(STAT);
+FTP_DECLARE(STOR);
+FTP_DECLARE(STOU);
+FTP_DECLARE(STRU);
+FTP_DECLARE(SYST);
+FTP_DECLARE(TYPE);
+FTP_DECLARE(USER);
+
+/*! session state */
+typedef enum
+{
+ COMMAND_STATE, /*!< waiting for a command */
+ DATA_CONNECT_STATE, /*!< waiting for connection after PASV command */
+ DATA_TRANSFER_STATE, /*!< data transfer in progress */
+} session_state_t;
+
+/*! ftp_session_set_state flags */
+typedef enum
+{
+ CLOSE_PASV = BIT(0), /*!< Close the pasv_fd */
+ CLOSE_DATA = BIT(1), /*!< Close the data_fd */
+} set_state_flags_t;
+
+/*! ftp_session_t flags */
+typedef enum
+{
+ SESSION_BINARY = BIT(0), /*!< data transfers in binary mode */
+ SESSION_PASV = BIT(1), /*!< have pasv_addr ready for data transfer command */
+ SESSION_PORT = BIT(2), /*!< have peer_addr ready for data transfer command */
+ SESSION_RECV = BIT(3), /*!< data transfer in source mode */
+ SESSION_SEND = BIT(4), /*!< data transfer in sink mode */
+ SESSION_RENAME = BIT(5), /*!< last command was RNFR and buffer contains path */
+ SESSION_URGENT = BIT(6), /*!< in telnet urgent mode */
+} session_flags_t;
+
+/*! ftp_xfer_dir mode */
+typedef enum
+{
+ XFER_DIR_LIST, /*!< Long list */
+ XFER_DIR_MLSD, /*!< Machine list directory */
+ XFER_DIR_MLST, /*!< Machine list */
+ XFER_DIR_NLST, /*!< Short list */
+ XFER_DIR_STAT, /*!< Stat command */
+} xfer_dir_mode_t;
+
+typedef enum
+{
+ SESSION_MLST_TYPE = BIT(0),
+ SESSION_MLST_SIZE = BIT(1),
+ SESSION_MLST_MODIFY = BIT(2),
+ SESSION_MLST_PERM = BIT(3),
+ SESSION_MLST_UNIX_MODE = BIT(4),
+} session_mlst_flags_t;
+
+/*! ftp session */
+struct ftp_session_t
+{
+ char cwd[32768]; /*!< current working directory */
+ char lwd[4096]; /*!< list working directory */
+ struct sockaddr_in peer_addr; /*!< peer address for data connection */
+ struct sockaddr_in pasv_addr; /*!< listen address for PASV connection */
+ int cmd_fd; /*!< socket for command connection */
+ int pasv_fd; /*!< listen socket for PASV */
+ int data_fd; /*!< socket for data transfer */
+ time_t timestamp; /*!< time from last command */
+ session_flags_t flags; /*!< session flags */
+ xfer_dir_mode_t dir_mode; /*!< dir transfer mode */
+ session_mlst_flags_t mlst_flags; /*!< session MLST flags */
+ session_state_t state; /*!< session state */
+ ftp_session_t *next; /*!< link to next session */
+ ftp_session_t *prev; /*!< link to prev session */
+
+ loop_status_t (*transfer)(ftp_session_t*); /*! data transfer callback */
+ char buffer[XFER_BUFFERSIZE]; /*! persistent data between callbacks */
+ char file_buffer[FILE_BUFFERSIZE]; /*! stdio file buffer */
+ char cmd_buffer[CMD_BUFFERSIZE]; /*! command buffer */
+ size_t bufferpos; /*! persistent buffer position between callbacks */
+ size_t buffersize; /*! persistent buffer size between callbacks */
+ size_t cmd_buffersize;
+ uint64_t filepos; /*! persistent file position between callbacks */
+ uint64_t filesize; /*! persistent file size between callbacks */
+ FILE *fp; /*! persistent open file pointer between callbacks */
+ DIR *dp; /*! persistent open directory pointer between callbacks */
+};
+
+/*! ftp command descriptor */
+typedef struct ftp_command
+{
+ const char *name; /*!< command name */
+ void (*handler)(ftp_session_t*, const char*); /*!< command callback */
+} ftp_command_t;
+
+/*! ftp command list */
+static ftp_command_t ftp_commands[] =
+{
+/*! ftp command */
+#define FTP_COMMAND(x) { #x, x, }
+/*! ftp alias */
+#define FTP_ALIAS(x,y) { #x, y, }
+ FTP_COMMAND(ABOR),
+ FTP_COMMAND(ALLO),
+ FTP_COMMAND(APPE),
+ FTP_COMMAND(CDUP),
+ FTP_COMMAND(CWD),
+ FTP_COMMAND(DELE),
+ FTP_COMMAND(FEAT),
+ FTP_COMMAND(HELP),
+ FTP_COMMAND(LIST),
+ FTP_COMMAND(MDTM),
+ FTP_COMMAND(MKD),
+ FTP_COMMAND(MLSD),
+ FTP_COMMAND(MLST),
+ FTP_COMMAND(MODE),
+ FTP_COMMAND(NLST),
+ FTP_COMMAND(NOOP),
+ FTP_COMMAND(OPTS),
+ FTP_COMMAND(PASS),
+ FTP_COMMAND(PASV),
+ FTP_COMMAND(PORT),
+ FTP_COMMAND(PWD),
+ FTP_COMMAND(QUIT),
+ FTP_COMMAND(REST),
+ FTP_COMMAND(RETR),
+ FTP_COMMAND(RMD),
+ FTP_COMMAND(RNFR),
+ FTP_COMMAND(RNTO),
+ FTP_COMMAND(SIZE),
+ FTP_COMMAND(STAT),
+ FTP_COMMAND(STOR),
+ FTP_COMMAND(STOU),
+ FTP_COMMAND(STRU),
+ FTP_COMMAND(SYST),
+ FTP_COMMAND(TYPE),
+ FTP_COMMAND(USER),
+ FTP_ALIAS(XCUP, CDUP),
+ FTP_ALIAS(XCWD, CWD),
+ FTP_ALIAS(XMKD, MKD),
+ FTP_ALIAS(XPWD, PWD),
+ FTP_ALIAS(XRMD, RMD),
+};
+/*! number of ftp commands */
+static const size_t num_ftp_commands = sizeof(ftp_commands)/sizeof(ftp_commands[0]);
+
+static void update_free_space(void);
+
+/*! compare ftp command descriptors
+ *
+ * @param[in] p1 left side of comparison (ftp_command_t*)
+ * @param[in] p2 right side of comparison (ftp_command_t*)
+ *
+ * @returns <0 if p1 < p2
+ * @returns 0 if p1 == p2
+ * @returns >0 if p1 > p2
+ */
+static int
+ftp_command_cmp(const void *p1,
+ const void *p2)
+{
+ ftp_command_t *c1 = (ftp_command_t*)p1;
+ ftp_command_t *c2 = (ftp_command_t*)p2;
+
+ /* ordered by command name */
+ return strcasecmp(c1->name, c2->name);
+}
+
+#ifdef _3DS
+/*! SOC service buffer */
+static u32 *SOCU_buffer = NULL;
+
+/*! Whether LCD is powered */
+static bool lcd_power = true;
+
+/*! aptHook cookie */
+static aptHookCookie cookie;
+#endif
+
+/*! server listen address */
+static struct sockaddr_in serv_addr;
+/*! listen file descriptor */
+static int listenfd = -1;
+#ifdef _3DS
+/*! current data port */
+static in_port_t data_port = DATA_PORT;
+#endif
+/*! list of ftp sessions */
+static ftp_session_t *sessions = NULL;
+/*! socket buffersize */
+static int sock_buffersize = SOCK_BUFFERSIZE;
+/*! server start time */
+static time_t start_time = 0;
+
+/*! Allocate a new data port
+ *
+ * @returns next data port
+ */
+static in_port_t
+next_data_port(void)
+{
+#ifdef _3DS
+ if(++data_port >= 10000)
+ data_port = DATA_PORT;
+ return data_port;
+#else
+ return 0; /* ephemeral port */
+#endif
+}
+
+/*! set a socket to non-blocking
+ *
+ * @param[in] fd socket
+ *
+ * @returns error
+ */
+static int
+ftp_set_socket_nonblocking(int fd)
+{
+ int rc, flags;
+
+ /* get the socket flags */
+ flags = fcntl(fd, F_GETFL, 0);
+ if(flags == -1)
+ {
+ console_print(RED "fcntl: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ /* add O_NONBLOCK to the socket flags */
+ rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK);
+ if(rc != 0)
+ {
+ console_print(RED "fcntl: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+/*! set socket options
+ *
+ * @param[in] fd socket
+ *
+ * @returns failure
+ */
+static int
+ftp_set_socket_options(int fd)
+{
+ int rc;
+
+ /* increase receive buffer size */
+ rc = setsockopt(fd, SOL_SOCKET, SO_RCVBUF,
+ &sock_buffersize, sizeof(sock_buffersize));
+ if(rc != 0)
+ {
+ console_print(RED "setsockopt: SO_RCVBUF %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ /* increase send buffer size */
+ rc = setsockopt(fd, SOL_SOCKET, SO_SNDBUF,
+ &sock_buffersize, sizeof(sock_buffersize));
+ if(rc != 0)
+ {
+ console_print(RED "setsockopt: SO_SNDBUF %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+/*! close a socket
+ *
+ * @param[in] fd socket to close
+ * @param[in] connected whether this socket is connected
+ */
+static void
+ftp_closesocket(int fd,
+ bool connected)
+{
+ int rc;
+ struct sockaddr_in addr;
+ socklen_t addrlen = sizeof(addr);
+ struct pollfd pollinfo;
+
+ if(connected)
+ {
+ /* get peer address and print */
+ rc = getpeername(fd, (struct sockaddr*)&addr, &addrlen);
+ if(rc != 0)
+ {
+ console_print(RED "getpeername: %d %s\n" RESET, errno, strerror(errno));
+ console_print(YELLOW "closing connection to fd=%d\n" RESET, fd);
+ }
+ else
+ console_print(YELLOW "closing connection to %s:%u\n" RESET,
+ inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
+
+ /* shutdown connection */
+ rc = shutdown(fd, SHUT_WR);
+ if(rc != 0)
+ console_print(RED "shutdown: %d %s\n" RESET, errno, strerror(errno));
+
+ /* wait for client to close connection */
+ pollinfo.fd = fd;
+ pollinfo.events = POLLIN;
+ pollinfo.revents = 0;
+ rc = poll(&pollinfo, 1, 250);
+ if(rc < 0)
+ console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno));
+ }
+
+ /* set linger to 0 */
+ struct linger linger;
+ linger.l_onoff = 1;
+ linger.l_linger = 0;
+ rc = setsockopt(fd, SOL_SOCKET, SO_LINGER,
+ &linger, sizeof(linger));
+ if(rc != 0)
+ console_print(RED "setsockopt: SO_LINGER %d %s\n" RESET,
+ errno, strerror(errno));
+
+ /* close socket */
+ rc = close(fd);
+ if(rc != 0)
+ console_print(RED "close: %d %s\n" RESET, errno, strerror(errno));
+}
+
+/*! close command socket on ftp session
+ *
+ * @param[in] session ftp session
+ */
+static void
+ftp_session_close_cmd(ftp_session_t *session)
+{
+ /* close command socket */
+ if(session->cmd_fd >= 0)
+ ftp_closesocket(session->cmd_fd, true);
+ session->cmd_fd = -1;
+}
+
+/*! close listen socket on ftp session
+ *
+ * @param[in] session ftp session
+ */
+static void
+ftp_session_close_pasv(ftp_session_t *session)
+{
+ /* close pasv socket */
+ if(session->pasv_fd >= 0)
+ {
+ console_print(YELLOW "stop listening on %s:%u\n" RESET,
+ inet_ntoa(session->pasv_addr.sin_addr),
+ ntohs(session->pasv_addr.sin_port));
+
+ ftp_closesocket(session->pasv_fd, false);
+ }
+
+ session->pasv_fd = -1;
+}
+
+/*! close data socket on ftp session
+ *
+ * @param[in] session ftp session
+ */
+static void
+ftp_session_close_data(ftp_session_t *session)
+{
+ /* close data connection */
+ if(session->data_fd >= 0 && session->data_fd != session->cmd_fd)
+ ftp_closesocket(session->data_fd, true);
+ session->data_fd = -1;
+
+ /* clear send/recv flags */
+ session->flags &= ~(SESSION_RECV|SESSION_SEND);
+}
+
+/*! close open file for ftp session
+ *
+ * @param[in] session ftp session
+ */
+static void
+ftp_session_close_file(ftp_session_t *session)
+{
+ int rc;
+
+ if(session->fp != NULL)
+ {
+ rc = fclose(session->fp);
+ if(rc != 0)
+ console_print(RED "fclose: %d %s\n" RESET, errno, strerror(errno));
+ }
+
+ session->fp = NULL;
+ session->filepos = 0;
+}
+
+/*! open file for reading for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns -1 for error
+ */
+static int
+ftp_session_open_file_read(ftp_session_t *session)
+{
+ int rc;
+ struct stat st;
+
+ /* open file in read mode */
+ session->fp = fopen(session->buffer, "rb");
+ if(session->fp == NULL)
+ {
+ console_print(RED "fopen '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+ return -1;
+ }
+
+ /* it's okay if this fails */
+ errno = 0;
+ rc = setvbuf(session->fp, session->file_buffer, _IOFBF, FILE_BUFFERSIZE);
+ if(rc != 0)
+ {
+ console_print(RED "setvbuf: %d %s\n" RESET, errno, strerror(errno));
+ }
+
+ /* get the file size */
+ rc = fstat(fileno(session->fp), &st);
+ if(rc != 0)
+ {
+ console_print(RED "fstat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+ return -1;
+ }
+ session->filesize = st.st_size;
+
+ if(session->filepos != 0)
+ {
+ rc = fseek(session->fp, session->filepos, SEEK_SET);
+ if(rc != 0)
+ {
+ console_print(RED "fseek '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/*! read from an open file for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns bytes read
+ */
+static ssize_t
+ftp_session_read_file(ftp_session_t *session)
+{
+ ssize_t rc;
+
+ /* read file at current position */
+ rc = fread(session->buffer, 1, sizeof(session->buffer), session->fp);
+ if(rc < 0)
+ {
+ console_print(RED "fread: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ /* adjust file position */
+ session->filepos += rc;
+
+ return rc;
+}
+
+/*! open file for writing for ftp session
+ *
+ * @param[in] session ftp session
+ * @param[in] append whether to append
+ *
+ * @returns -1 for error
+ *
+ * @note truncates file
+ */
+static int
+ftp_session_open_file_write(ftp_session_t *session,
+ bool append)
+{
+ int rc;
+ const char *mode = "wb";
+
+ if(append)
+ mode = "ab";
+ else if(session->filepos != 0)
+ mode = "r+b";
+
+ /* open file in write mode */
+ session->fp = fopen(session->buffer, mode);
+ if(session->fp == NULL)
+ {
+ console_print(RED "fopen '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+ return -1;
+ }
+
+ update_free_space();
+
+ /* it's okay if this fails */
+ errno = 0;
+ rc = setvbuf(session->fp, session->file_buffer, _IOFBF, FILE_BUFFERSIZE);
+ if(rc != 0)
+ {
+ console_print(RED "setvbuf: %d %s\n" RESET, errno, strerror(errno));
+ }
+
+ /* check if this had REST but not APPE */
+ if(session->filepos != 0 && !append)
+ {
+ /* seek to the REST offset */
+ rc = fseek(session->fp, session->filepos, SEEK_SET);
+ if(rc != 0)
+ {
+ console_print(RED "fseek '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+/*! write to an open file for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns bytes written
+ */
+static ssize_t
+ftp_session_write_file(ftp_session_t *session)
+{
+ ssize_t rc;
+
+ /* write to file at current position */
+ rc = fwrite(session->buffer + session->bufferpos,
+ 1, session->buffersize - session->bufferpos,
+ session->fp);
+ if(rc < 0)
+ {
+ console_print(RED "fwrite: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+ else if(rc == 0)
+ console_print(RED "fwrite: wrote 0 bytes\n" RESET);
+
+ /* adjust file position */
+ session->filepos += rc;
+
+ update_free_space();
+ return rc;
+}
+
+/*! close current working directory for ftp session
+ *
+ * @param[in] session ftp session
+ */
+static void
+ftp_session_close_cwd(ftp_session_t *session)
+{
+ int rc;
+
+ /* close open directory pointer */
+ if(session->dp != NULL)
+ {
+ rc = closedir(session->dp);
+ if(rc != 0)
+ console_print(RED "closedir: %d %s\n" RESET, errno, strerror(errno));
+ }
+ session->dp = NULL;
+}
+
+/*! open current working directory for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @return -1 for failure
+ */
+static int
+ftp_session_open_cwd(ftp_session_t *session)
+{
+ /* open current working directory */
+ session->dp = opendir(session->cwd);
+ if(session->dp == NULL)
+ {
+ console_print(RED "opendir '%s': %d %s\n" RESET, session->cwd, errno, strerror(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+/*! set state for ftp session
+ *
+ * @param[in] session ftp session
+ * @param[in] state state to set
+ * @param[in] flags flags
+ */
+static void
+ftp_session_set_state(ftp_session_t *session,
+ session_state_t state,
+ set_state_flags_t flags)
+{
+ session->state = state;
+
+ /* close pasv and data sockets */
+ if(flags & CLOSE_PASV)
+ ftp_session_close_pasv(session);
+ if(flags & CLOSE_DATA)
+ ftp_session_close_data(session);
+
+ if(state == COMMAND_STATE)
+ {
+ /* close file/cwd */
+ ftp_session_close_file(session);
+ ftp_session_close_cwd(session);
+ }
+}
+
+/*! fill directory entry
+ *
+ * @param[in] session ftp session
+ * @param[in] st stat data
+ * @param[in] path path to fill
+ * @param[in] len path length
+ * @param[in] type type fact
+ *
+ * @returns errno
+ */
+static int
+ftp_session_fill_dirent_type(ftp_session_t *session, const struct stat *st,
+ const char *path, size_t len, const char *type)
+{
+ session->buffersize = 0;
+
+ if(session->dir_mode == XFER_DIR_MLSD
+ || session->dir_mode == XFER_DIR_MLST)
+ {
+ if(session->dir_mode == XFER_DIR_MLST)
+ session->buffer[session->buffersize++] = ' ';
+
+ if(session->mlst_flags & SESSION_MLST_TYPE)
+ {
+ /* type fact */
+ if(!type)
+ {
+ type = "???";
+ if(S_ISREG(st->st_mode))
+ type = "file";
+ else if(S_ISDIR(st->st_mode))
+ type = "dir";
+#ifndef _3DS
+ 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
+ }
+
+ session->buffersize +=
+ sprintf(session->buffer + session->buffersize, "Type=%s;", type);
+ }
+
+ if(session->mlst_flags & SESSION_MLST_SIZE)
+ {
+ /* size fact */
+ session->buffersize +=
+ sprintf(session->buffer + session->buffersize, "Size=%lld;",
+ (signed long long)st->st_size);
+ }
+
+ if(session->mlst_flags & SESSION_MLST_MODIFY)
+ {
+ /* mtime fact */
+ struct tm *tm = gmtime(&st->st_mtime);
+ if(tm == NULL)
+ return errno;
+
+ session->buffersize +=
+ strftime(session->buffer + session->buffersize,
+ sizeof(session->buffer) - session->buffersize,
+ "Modify=%Y%m%d%H%M%S;", tm);
+ if(session->buffersize == 0)
+ return EOVERFLOW;
+ }
+
+ if(session->mlst_flags & SESSION_MLST_PERM)
+ {
+ /* permission fact */
+ strcpy(session->buffer + session->buffersize, "Perm=");
+ session->buffersize += strlen("Perm=");
+
+ /* append permission */
+ if(S_ISREG(st->st_mode) && (st->st_mode & S_IWUSR))
+ session->buffer[session->buffersize++] = 'a';
+
+ /* create permission */
+ if(S_ISDIR(st->st_mode) && (st->st_mode & S_IWUSR))
+ session->buffer[session->buffersize++] = 'c';
+
+ /* delete permission */
+ session->buffer[session->buffersize++] = 'd';
+
+ /* chdir permission */
+ if(S_ISDIR(st->st_mode) && (st->st_mode & S_IXUSR))
+ session->buffer[session->buffersize++] = 'e';
+
+ /* rename permission */
+ session->buffer[session->buffersize++] = 'f';
+
+ /* list permission */
+ if(S_ISDIR(st->st_mode) && (st->st_mode & S_IRUSR))
+ session->buffer[session->buffersize++] = 'l';
+
+ /* mkdir permission */
+ if(S_ISDIR(st->st_mode) && (st->st_mode & S_IWUSR))
+ session->buffer[session->buffersize++] = 'm';
+
+ /* delete permission */
+ if(S_ISDIR(st->st_mode) && (st->st_mode & S_IWUSR))
+ session->buffer[session->buffersize++] = 'p';
+
+ /* read permission */
+ if(S_ISREG(st->st_mode) && (st->st_mode & S_IRUSR))
+ session->buffer[session->buffersize++] = 'r';
+
+ /* write permission */
+ if(S_ISREG(st->st_mode) && (st->st_mode & S_IWUSR))
+ session->buffer[session->buffersize++] = 'w';
+
+ session->buffer[session->buffersize++] = ';';
+ }
+
+ if(session->mlst_flags & SESSION_MLST_UNIX_MODE)
+ {
+ /* unix mode fact */
+ mode_t mask = S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX | S_ISGID | S_ISUID;
+ session->buffersize +=
+ sprintf(session->buffer + session->buffersize, "UNIX.mode=0%lo;",
+ (unsigned long)(st->st_mode & mask));
+ }
+
+ /* make sure space precedes name */
+ if(session->buffer[session->buffersize-1] != ' ')
+ session->buffer[session->buffersize++] = ' ';
+ }
+ else if(session->dir_mode != XFER_DIR_NLST)
+ {
+ if(session->dir_mode == XFER_DIR_STAT)
+ session->buffer[session->buffersize++] = ' ';
+
+ /* perms nlinks owner group size */
+ session->buffersize +=
+ sprintf(session->buffer + session->buffersize,
+ "%c%c%c%c%c%c%c%c%c%c %lu 3DS 3DS %lld ",
+ S_ISREG(st->st_mode) ? '-' :
+ S_ISDIR(st->st_mode) ? 'd' :
+#ifndef _3DS
+ 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
+ '?',
+ 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' : '-',
+ (unsigned long)st->st_nlink,
+ (signed long long)st->st_size);
+
+ /* timestamp */
+ struct tm *tm = gmtime(&st->st_mtime);
+ if(tm)
+ {
+ const char *fmt = "%b %e %Y ";
+ if(session->timestamp > st->st_mtime
+ && session->timestamp - st->st_mtime < (60*60*24*365/2))
+ {
+ fmt = "%b %e %H:%M ";
+ }
+
+ session->buffersize +=
+ strftime(session->buffer + session->buffersize,
+ sizeof(session->buffer) - session->buffersize,
+ fmt, tm);
+ }
+ else
+ {
+ session->buffersize +=
+ sprintf(session->buffer + session->buffersize, "Jan 1 1970 ");
+ }
+ }
+
+ if(session->buffersize + len + 2 > sizeof(session->buffer))
+ {
+ /* buffer will overflow */
+ return EOVERFLOW;
+ }
+
+ /* copy path */
+ memcpy(session->buffer+session->buffersize, path, len);
+ len = session->buffersize + len;
+ session->buffer[len++] = '\r';
+ session->buffer[len++] = '\n';
+ session->buffersize = len;
+
+ return 0;
+}
+
+/*! fill directory entry
+ *
+ * @param[in] session ftp session
+ * @param[in] st stat data
+ * @param[in] path path to fill
+ * @param[in] len path length
+ *
+ * @returns errno
+ */
+static int
+ftp_session_fill_dirent(ftp_session_t *session, const struct stat *st,
+ const char *path, size_t len)
+{
+ return ftp_session_fill_dirent_type(session, st, path, len, NULL);
+}
+
+/*! transfer loop
+ *
+ * Try to transfer as much data as the sockets will allow without blocking
+ *
+ * @param[in] session ftp session
+ */
+static void
+ftp_session_transfer(ftp_session_t *session)
+{
+ int rc;
+ do
+ {
+ rc = session->transfer(session);
+ } while(rc == 0);
+}
+
+/*! encode a path
+ *
+ * @param[in] path path to encode
+ * @param[in,out] len path length
+ * @param[in] quotes whether to encode quotes
+ *
+ * @returns encoded path
+ *
+ * @note The caller must free the returned path
+ */
+static char*
+encode_path(const char *path,
+ size_t *len,
+ bool quotes)
+{
+ bool enc = false;
+ size_t i, diff = 0;
+ char *out, *p = (char*)path;
+
+ /* check for \n that needs to be encoded */
+ if(memchr(p, '\n', *len) != NULL)
+ enc = true;
+
+ if(quotes)
+ {
+ /* check for " that needs to be encoded */
+ p = (char*)path;
+ do
+ {
+ p = memchr(p, '"', path + *len - p);
+ if(p != NULL)
+ {
+ ++p;
+ ++diff;
+ }
+ } while(p != NULL);
+ }
+
+ /* check if an encode was needed */
+ if(!enc && diff == 0)
+ return strdup(path);
+
+ /* allocate space for encoded path */
+ p = out = (char*)malloc(*len + diff);
+ if(out == NULL)
+ return NULL;
+
+ /* copy the path while performing encoding */
+ for(i = 0; i < *len; ++i)
+ {
+ if(*path == '\n')
+ {
+ /* encoded \n is \0 */
+ *p++ = 0;
+ }
+ else if(quotes && *path == '"')
+ {
+ /* encoded " is "" */
+ *p++ = '"';
+ *p++ = '"';
+ }
+ else
+ *p++ = *path;
+ ++path;
+ }
+
+ *len += diff;
+ return out;
+}
+
+/*! decode a path
+ *
+ * @param[in] session ftp session
+ * @param[in] len command length
+ */
+static void
+decode_path(ftp_session_t *session,
+ size_t len)
+{
+ size_t i;
+
+ /* decode \0 from the first command */
+ for(i = 0; i < len; ++i)
+ {
+ /* this is an encoded \n */
+ if(session->cmd_buffer[i] == 0)
+ session->cmd_buffer[i] = '\n';
+ }
+}
+
+/*! fill cdir directory entry
+ *
+ * @param[in] session ftp session
+ * @param[in] path path to fill
+ *
+ * @returns errno
+ */
+static int
+ftp_session_fill_dirent_cdir(ftp_session_t *session, const char *path)
+{
+ int rc;
+ struct stat st;
+ char *buffer;
+ size_t len;
+
+ rc = stat(path, &st);
+ /* double-check this was a directory */
+ if(rc == 0 && !S_ISDIR(st.st_mode))
+ {
+ /* shouldn't happen but just in case */
+ rc = -1;
+ errno = ENOTDIR;
+ }
+ if(rc != 0)
+ return errno;
+
+ /* encode \n in path */
+ len = strlen(path);
+ buffer = encode_path(path, &len, false);
+ if(!buffer)
+ return ENOMEM;
+
+ /* fill dirent with listed directory as type=cdir */
+ rc = ftp_session_fill_dirent_type(session, &st, buffer, len, "cdir");
+ free(buffer);
+
+ return rc;
+}
+
+/*! send a response on the command socket
+ *
+ * @param[in] session ftp session
+ * @param[in] buffer buffer to send
+ * @param[in] len buffer length
+ */
+static void
+ftp_send_response_buffer(ftp_session_t *session,
+ const char *buffer,
+ size_t len)
+{
+ ssize_t rc, to_send;
+
+ if(session->cmd_fd < 0)
+ return;
+
+ /* send response */
+ to_send = len;
+ console_print(GREEN "%s" RESET, buffer);
+ rc = send(session->cmd_fd, buffer, to_send, 0);
+ if(rc < 0)
+ {
+ console_print(RED "send: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_cmd(session);
+ }
+ else if(rc != to_send)
+ {
+ console_print(RED "only sent %u/%u bytes\n" RESET,
+ (unsigned int)rc, (unsigned int)to_send);
+ ftp_session_close_cmd(session);
+ }
+}
+
+__attribute__((format(printf,3,4)))
+/*! send ftp response to ftp session's peer
+ *
+ * @param[in] session ftp session
+ * @param[in] code response code
+ * @param[in] fmt format string
+ * @param[in] ... format arguments
+ */
+static void
+ftp_send_response(ftp_session_t *session,
+ int code,
+ const char *fmt, ...)
+{
+ static char buffer[CMD_BUFFERSIZE];
+ ssize_t rc;
+ va_list ap;
+
+ if(session->cmd_fd < 0)
+ return;
+
+ /* print response code and message to buffer */
+ va_start(ap, fmt);
+ if(code > 0)
+ rc = sprintf(buffer, "%d ", code);
+ else
+ rc = sprintf(buffer, "%d-", -code);
+ rc += vsnprintf(buffer+rc, sizeof(buffer)-rc, fmt, ap);
+ va_end(ap);
+
+ if(rc >= sizeof(buffer))
+ {
+ /* couldn't fit message; just send code */
+ console_print(RED "%s: buffersize too small\n" RESET, __func__);
+ if(code > 0)
+ rc = sprintf(buffer, "%d \r\n", code);
+ else
+ rc = sprintf(buffer, "%d-\r\n", -code);
+ }
+
+ ftp_send_response_buffer(session, buffer, rc);
+}
+
+/*! destroy ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns the next session in the list
+ */
+static ftp_session_t*
+ftp_session_destroy(ftp_session_t *session)
+{
+ ftp_session_t *next = session->next;
+
+ /* close all sockets/files */
+ ftp_session_close_cmd(session);
+ ftp_session_close_pasv(session);
+ ftp_session_close_data(session);
+ ftp_session_close_file(session);
+ ftp_session_close_cwd(session);
+
+ /* unlink from sessions list */
+ if(session->next)
+ session->next->prev = session->prev;
+ if(session == sessions)
+ sessions = session->next;
+ else
+ {
+ session->prev->next = session->next;
+ if(session == sessions->prev)
+ sessions->prev = session->prev;
+ }
+
+ /* deallocate */
+ free(session);
+
+ return next;
+}
+
+/*! allocate new ftp session
+ *
+ * @param[in] listen_fd socket to accept connection from
+ */
+static void
+ftp_session_new(int listen_fd)
+{
+ ssize_t rc;
+ int new_fd;
+ ftp_session_t *session;
+ struct sockaddr_in addr;
+ socklen_t addrlen = sizeof(addr);
+
+ /* accept connection */
+ new_fd = accept(listen_fd, (struct sockaddr*)&addr, &addrlen);
+ if(new_fd < 0)
+ {
+ console_print(RED "accept: %d %s\n" RESET, errno, strerror(errno));
+ return;
+ }
+
+ console_print(CYAN "accepted connection from %s:%u\n" RESET,
+ inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
+
+ snprintf(ftp_accepted_connection, sizeof(ftp_accepted_connection), "Accepted connection: %s:%u", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
+
+ /* allocate a new session */
+ session = (ftp_session_t*)calloc(1, sizeof(ftp_session_t));
+ if(session == NULL)
+ {
+ console_print(RED "failed to allocate session\n" RESET);
+ ftp_closesocket(new_fd, true);
+ return;
+ }
+
+ /* initialize session */
+ strcpy(session->cwd, "/");
+ session->peer_addr.sin_addr.s_addr = INADDR_ANY;
+ session->cmd_fd = new_fd;
+ session->pasv_fd = -1;
+ session->data_fd = -1;
+ session->mlst_flags = SESSION_MLST_TYPE
+ | SESSION_MLST_SIZE
+ | SESSION_MLST_MODIFY
+ | SESSION_MLST_PERM;
+ session->state = COMMAND_STATE;
+
+ /* link to the sessions list */
+ if(sessions == NULL)
+ {
+ sessions = session;
+ session->prev = session;
+ }
+ else
+ {
+ sessions->prev->next = session;
+ session->prev = sessions->prev;
+ sessions->prev = session;
+ }
+
+ /* copy socket address to pasv address */
+ addrlen = sizeof(session->pasv_addr);
+ rc = getsockname(new_fd, (struct sockaddr*)&session->pasv_addr, &addrlen);
+ if(rc != 0)
+ {
+ console_print(RED "getsockname: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 451, "Failed to get connection info\r\n");
+ ftp_session_destroy(session);
+ return;
+ }
+
+ session->cmd_fd = new_fd;
+
+ /* send initiator response */
+ ftp_send_response(session, 220, "Hello!\r\n");
+}
+
+/*! accept PASV connection for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns -1 for failure
+ */
+static int
+ftp_session_accept(ftp_session_t *session)
+{
+ int rc, new_fd;
+ struct sockaddr_in addr;
+ socklen_t addrlen = sizeof(addr);
+
+ if(session->flags & SESSION_PASV)
+ {
+ /* clear PASV flag */
+ session->flags &= ~SESSION_PASV;
+
+ /* tell the peer that we're ready to accept the connection */
+ ftp_send_response(session, 150, "Ready\r\n");
+
+ /* accept connection from peer */
+ new_fd = accept(session->pasv_fd, (struct sockaddr*)&addr, &addrlen);
+ if(new_fd < 0)
+ {
+ console_print(RED "accept: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 425, "Failed to establish connection\r\n");
+ return -1;
+ }
+
+ /* set the socket to non-blocking */
+ rc = ftp_set_socket_nonblocking(new_fd);
+ if(rc != 0)
+ {
+ ftp_closesocket(new_fd, true);
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 425, "Failed to establish connection\r\n");
+ return -1;
+ }
+
+ console_print(CYAN "accepted connection from %s:%u\n" RESET,
+ inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
+
+ /* we are ready to transfer data */
+ ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV);
+ session->data_fd = new_fd;
+
+ return 0;
+ }
+ else
+ {
+ /* peer didn't send PASV command */
+ ftp_send_response(session, 503, "Bad sequence of commands\r\n");
+ return -1;
+ }
+}
+
+/*! connect to peer for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns -1 for failure
+ */
+static int
+ftp_session_connect(ftp_session_t *session)
+{
+ int rc;
+
+ /* clear PORT flag */
+ session->flags &= ~SESSION_PORT;
+
+ /* create a new socket */
+ session->data_fd = socket(AF_INET, SOCK_STREAM, 0);
+ if(session->data_fd < 0)
+ {
+ console_print(RED "socket: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ /* set socket options */
+ rc = ftp_set_socket_options(session->data_fd);
+ if(rc != 0)
+ {
+ ftp_closesocket(session->data_fd, false);
+ session->data_fd = -1;
+ return -1;
+ }
+
+ /* set socket to non-blocking */
+ rc = ftp_set_socket_nonblocking(session->data_fd);
+ if(rc != 0)
+ return -1;
+
+ /* connect to peer */
+ rc = connect(session->data_fd, (struct sockaddr*)&session->peer_addr,
+ sizeof(session->peer_addr));
+ if(rc != 0)
+ {
+ if(errno != EINPROGRESS)
+ {
+ console_print(RED "connect: %d %s\n" RESET, errno, strerror(errno));
+ ftp_closesocket(session->data_fd, false);
+ session->data_fd = -1;
+ return -1;
+ }
+ }
+ else
+ {
+ console_print(CYAN "connected to %s:%u\n" RESET,
+ inet_ntoa(session->peer_addr.sin_addr),
+ ntohs(session->peer_addr.sin_port));
+
+ ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV);
+ ftp_send_response(session, 150, "Ready\r\n");
+ }
+
+ return 0;
+}
+
+/*! read command for ftp session
+ *
+ * @param[in] session ftp session
+ * @param[in] events poll events
+ */
+static void
+ftp_session_read_command(ftp_session_t *session,
+ int events)
+{
+ char *buffer, *args, *next = NULL;
+ size_t i, len;
+ int atmark;
+ ssize_t rc;
+ ftp_command_t key, *command;
+
+ /* check out-of-band data */
+ if(events & POLLPRI)
+ {
+ session->flags |= SESSION_URGENT;
+
+ /* check if we are at the urgent marker */
+ atmark = sockatmark(session->cmd_fd);
+ if(atmark < 0)
+ {
+ console_print(RED "sockatmark: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_cmd(session);
+ return;
+ }
+
+ if(!atmark)
+ {
+ /* discard in-band data */
+ rc = recv(session->cmd_fd, session->cmd_buffer, sizeof(session->cmd_buffer), 0);
+ if(rc < 0 && errno != EWOULDBLOCK)
+ {
+ console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_cmd(session);
+ }
+
+ return;
+ }
+
+ /* retrieve the urgent data */
+ rc = recv(session->cmd_fd, session->cmd_buffer, sizeof(session->cmd_buffer), MSG_OOB);
+ if(rc < 0)
+ {
+ /* EWOULDBLOCK means out-of-band data is on the way */
+ if(errno == EWOULDBLOCK)
+ return;
+
+ /* error retrieving out-of-band data */
+ console_print(RED "recv (oob): %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_cmd(session);
+ return;
+ }
+
+ /* reset the command buffer */
+ session->cmd_buffersize = 0;
+ return;
+ }
+
+ /* prepare to receive data */
+ buffer = session->cmd_buffer + session->cmd_buffersize;
+ len = sizeof(session->cmd_buffer) - session->cmd_buffersize;
+ if(len == 0)
+ {
+ /* error retrieving command */
+ console_print(RED "Exceeded command buffer size\n" RESET);
+ ftp_session_close_cmd(session);
+ return;
+ }
+
+ /* retrieve command data */
+ rc = recv(session->cmd_fd, buffer, len, 0);
+ if(rc < 0)
+ {
+ /* error retrieving command */
+ console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_cmd(session);
+ return;
+ }
+ if(rc == 0)
+ {
+ /* peer closed connection */
+ debug_print("peer closed connection\n");
+ ftp_session_close_cmd(session);
+ return;
+ }
+ else
+ {
+ session->cmd_buffersize += rc;
+ len = sizeof(session->cmd_buffer) - session->cmd_buffersize;
+
+ if(session->flags & SESSION_URGENT)
+ {
+ /* look for telnet data mark */
+ for(i = 0; i < session->cmd_buffersize; ++i)
+ {
+ if((unsigned char)session->cmd_buffer[i] == 0xF2)
+ {
+ /* ignore all data that precedes the data mark */
+ if(i < session->cmd_buffersize - 1)
+ memmove(session->cmd_buffer, session->cmd_buffer + i + 1, len - i - 1);
+ session->cmd_buffersize -= i + 1;
+ session->flags &= ~SESSION_URGENT;
+ break;
+ }
+ }
+ }
+
+ /* loop through commands */
+ while(true)
+ {
+ /* must have at least enough data for the delimiter */
+ if(session->cmd_buffersize < 1)
+ return;
+
+ /* look for \r\n or \n delimiter */
+ for(i = 0; i < session->cmd_buffersize; ++i)
+ {
+ if(i < session->cmd_buffersize-1
+ && session->cmd_buffer[i] == '\r'
+ && session->cmd_buffer[i+1] == '\n')
+ {
+ /* we found a \r\n delimiter */
+ session->cmd_buffer[i] = 0;
+ next = &session->cmd_buffer[i+2];
+ break;
+ }
+ else if(session->cmd_buffer[i] == '\n')
+ {
+ /* we found a \n delimiter */
+ session->cmd_buffer[i] = 0;
+ next = &session->cmd_buffer[i+1];
+ break;
+ }
+ }
+
+ /* check if a delimiter was found */
+ if(i == session->cmd_buffersize)
+ return;
+
+ /* decode the command */
+ decode_path(session, i);
+
+ /* split command from arguments */
+ args = buffer = session->cmd_buffer;
+ while(*args && !isspace((int)*args))
+ ++args;
+ if(*args)
+ *args++ = 0;
+
+ /* look up the command */
+ key.name = buffer;
+ command = bsearch(&key, ftp_commands,
+ num_ftp_commands, sizeof(ftp_command_t),
+ ftp_command_cmp);
+
+ /* update command timestamp */
+ session->timestamp = time(NULL);
+
+ /* execute the command */
+ if(command == NULL)
+ {
+ /* send header */
+ ftp_send_response(session, 502, "Invalid command \"");
+
+ /* send command */
+ len = strlen(buffer);
+ buffer = encode_path(buffer, &len, false);
+ if(buffer != NULL)
+ ftp_send_response_buffer(session, buffer, len);
+ else
+ ftp_send_response_buffer(session, key.name, strlen(key.name));
+ free(buffer);
+
+ /* send args (if any) */
+ if(*args != 0)
+ {
+ ftp_send_response_buffer(session, " ", 1);
+
+ len = strlen(args);
+ buffer = encode_path(args, &len, false);
+ if(buffer != NULL)
+ ftp_send_response_buffer(session, buffer, len);
+ else
+ ftp_send_response_buffer(session, args, strlen(args));
+ free(buffer);
+ }
+
+ /* send footer */
+ ftp_send_response_buffer(session, "\"\r\n", 3);
+ }
+ else if(session->state != COMMAND_STATE)
+ {
+ /* only some commands are available during data transfer */
+ if(strcasecmp(command->name, "ABOR") != 0
+ && strcasecmp(command->name, "STAT") != 0
+ && strcasecmp(command->name, "QUIT") != 0)
+ {
+ ftp_send_response(session, 503, "Invalid command during transfer\r\n");
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_session_close_cmd(session);
+ }
+ else
+ command->handler(session, args);
+ }
+ else
+ {
+ /* clear RENAME flag for all commands except RNTO */
+ if(strcasecmp(command->name, "RNTO") != 0)
+ session->flags &= ~SESSION_RENAME;
+
+ command->handler(session, args);
+ }
+
+ /* remove executed command from the command buffer */
+ len = session->cmd_buffer + session->cmd_buffersize - next;
+ if(len > 0)
+ memmove(session->cmd_buffer, next, len);
+ session->cmd_buffersize = len;
+ }
+ }
+}
+
+/*! poll sockets for ftp session
+ *
+ * @param[in] session ftp session
+ *
+ * @returns next session
+ */
+static ftp_session_t*
+ftp_session_poll(ftp_session_t *session)
+{
+ int rc;
+ struct pollfd pollinfo[2];
+ nfds_t nfds = 1;
+
+ /* the first pollfd is the command socket */
+ pollinfo[0].fd = session->cmd_fd;
+ pollinfo[0].events = POLLIN | POLLPRI;
+ pollinfo[0].revents = 0;
+
+ switch(session->state)
+ {
+ case COMMAND_STATE:
+ /* we are waiting to read a command */
+ break;
+
+ case DATA_CONNECT_STATE:
+ if(session->flags & SESSION_PASV)
+ {
+ /* we are waiting for a PASV connection */
+ pollinfo[1].fd = session->pasv_fd;
+ pollinfo[1].events = POLLIN;
+ }
+ else
+ {
+ /* we are waiting to complete a PORT connection */
+ pollinfo[1].fd = session->data_fd;
+ pollinfo[1].events = POLLOUT;
+ }
+ pollinfo[1].revents = 0;
+ nfds = 2;
+ break;
+
+ case DATA_TRANSFER_STATE:
+ /* we need to transfer data */
+ pollinfo[1].fd = session->data_fd;
+ if(session->flags & SESSION_RECV)
+ pollinfo[1].events = POLLIN;
+ else
+ pollinfo[1].events = POLLOUT;
+ pollinfo[1].revents = 0;
+ nfds = 2;
+ break;
+ }
+
+ /* poll the selected sockets */
+ rc = poll(pollinfo, nfds, 0);
+ if(rc < 0)
+ {
+ console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_cmd(session);
+ }
+ else if(rc > 0)
+ {
+ /* check the command socket */
+ if(pollinfo[0].revents != 0)
+ {
+ /* handle command */
+ if(pollinfo[0].revents & POLL_UNKNOWN)
+ console_print(YELLOW "cmd_fd: revents=0x%08X\n" RESET, pollinfo[0].revents);
+
+ /* we need to read a new command */
+ if(pollinfo[0].revents & (POLLERR|POLLHUP))
+ {
+ debug_print("cmd revents=0x%x\n", pollinfo[0].revents);
+ ftp_session_close_cmd(session);
+ }
+ else if(pollinfo[0].revents & (POLLIN | POLLPRI))
+ ftp_session_read_command(session, pollinfo[0].revents);
+ }
+
+ /* check the data/pasv socket */
+ if(nfds > 1 && pollinfo[1].revents != 0)
+ {
+ switch(session->state)
+ {
+ case COMMAND_STATE:
+ /* this shouldn't happen? */
+ break;
+
+ case DATA_CONNECT_STATE:
+ if(pollinfo[1].revents & POLL_UNKNOWN)
+ console_print(YELLOW "pasv_fd: revents=0x%08X\n" RESET, pollinfo[1].revents);
+
+ /* we need to accept the PASV connection */
+ if(pollinfo[1].revents & (POLLERR|POLLHUP))
+ {
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 426, "Data connection failed\r\n");
+ }
+ else if(pollinfo[1].revents & POLLIN)
+ {
+ if(ftp_session_accept(session) != 0)
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ }
+ else if(pollinfo[1].revents & POLLOUT)
+ {
+
+ console_print(CYAN "connected to %s:%u\n" RESET,
+ inet_ntoa(session->peer_addr.sin_addr),
+ ntohs(session->peer_addr.sin_port));
+
+ ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV);
+ ftp_send_response(session, 150, "Ready\r\n");
+ }
+ break;
+
+ case DATA_TRANSFER_STATE:
+ if(pollinfo[1].revents & POLL_UNKNOWN)
+ console_print(YELLOW "data_fd: revents=0x%08X\n" RESET, pollinfo[1].revents);
+
+ /* we need to transfer data */
+ if(pollinfo[1].revents & (POLLERR|POLLHUP))
+ {
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 426, "Data connection failed\r\n");
+ }
+ else if(pollinfo[1].revents & (POLLIN|POLLOUT))
+ ftp_session_transfer(session);
+ break;
+ }
+ }
+ }
+
+ /* still connected to peer; return next session */
+ if(session->cmd_fd >= 0)
+ return session->next;
+
+ /* disconnected from peer; destroy it and return next session */
+ debug_print("disconnected from peer\n");
+ return ftp_session_destroy(session);
+}
+
+/* Update free space in status bar */
+static void
+update_free_space(void)
+{
+#ifdef _3DS
+#define KiB (1024.0)
+#define MiB (1024.0*KiB)
+#define GiB (1024.0*MiB)
+ char buffer[16];
+ struct statvfs st;
+ double bytes_free;
+ int rc, len;
+
+ rc = statvfs("sdmc:/", &st);
+ if(rc != 0)
+ console_print(RED "statvfs: %d %s\n" RESET, errno, strerror(errno));
+ else
+ {
+ bytes_free = (double)st.f_bsize * st.f_bfree;
+
+ if (bytes_free < 1000.0)
+ len = snprintf(buffer, sizeof(buffer), "%.0lfB", bytes_free);
+ else if(bytes_free < 10.0*KiB)
+ len = snprintf(buffer, sizeof(buffer), "%.2lfKiB", floor((bytes_free*100.0)/KiB)/100.0);
+ else if(bytes_free < 100.0*KiB)
+ len = snprintf(buffer, sizeof(buffer), "%.1lfKiB", floor((bytes_free*10.0)/KiB)/10.0);
+ else if(bytes_free < 1000.0*KiB)
+ len = snprintf(buffer, sizeof(buffer), "%.0lfKiB", floor(bytes_free/KiB));
+ else if(bytes_free < 10.0*MiB)
+ len = snprintf(buffer, sizeof(buffer), "%.2lfMiB", floor((bytes_free*100.0)/MiB)/100.0);
+ else if(bytes_free < 100.0*MiB)
+ len = snprintf(buffer, sizeof(buffer), "%.1lfMiB", floor((bytes_free*10.0)/MiB)/10.0);
+ else if(bytes_free < 1000.0*MiB)
+ len = snprintf(buffer, sizeof(buffer), "%.0lfMiB", floor(bytes_free/MiB));
+ else if(bytes_free < 10.0*GiB)
+ len = snprintf(buffer, sizeof(buffer), "%.2lfGiB", floor((bytes_free*100.0)/GiB)/100.0);
+ else if(bytes_free < 100.0*GiB)
+ len = snprintf(buffer, sizeof(buffer), "%.1lfGiB", floor((bytes_free*10.0)/GiB)/10.0);
+ else
+ len = snprintf(buffer, sizeof(buffer), "%.0lfGiB", floor(bytes_free/GiB));
+
+ console_set_status("\x1b[0;%dH" GREEN "%s", 50-len, buffer);
+ }
+#endif
+}
+
+/*! Update status bar */
+static int
+update_status(void)
+{
+ update_free_space();
+ char hostname[128];
+ socklen_t addrlen = sizeof(serv_addr);
+ int rc;
+
+ rc = getsockname(listenfd, (struct sockaddr*)&serv_addr, &addrlen);
+ if(rc != 0)
+ {
+ console_print(RED "getsockname: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ rc = gethostname(hostname, sizeof(hostname));
+ if(rc != 0)
+ {
+ console_print(RED "gethostname: %d %s\n" RESET, errno, strerror(errno));
+ return -1;
+ }
+
+ return 0;
+}
+
+#ifdef _3DS
+/*! Handle apt events
+ *
+ * @param[in] type Event type
+ * @param[in] closure Callback closure
+ */
+static void
+apt_hook(APT_HookType type,
+ void *closure)
+{
+ switch(type)
+ {
+ case APTHOOK_ONSUSPEND:
+ case APTHOOK_ONSLEEP:
+ /* turn on backlight, or you can't see the home menu! */
+ if(R_SUCCEEDED(gspLcdInit()))
+ {
+ GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTH);
+ gspLcdExit();
+ }
+ break;
+
+ case APTHOOK_ONRESTORE:
+ case APTHOOK_ONWAKEUP:
+ /* restore backlight power state */
+ if(R_SUCCEEDED(gspLcdInit()))
+ {
+ (lcd_power ? GSPLCD_PowerOnBacklight : GSPLCD_PowerOffBacklight)(GSPLCD_SCREEN_BOTH);
+ gspLcdExit();
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+#endif
+
+/*! initialize ftp subsystem */
+int
+ftp_init(void)
+{
+ int rc;
+
+ start_time = time(NULL);
+
+#ifdef _3DS
+ Result ret = 0;
+ u32 wifi = 0;
+ bool loop = true;
+
+ /* register apt hook */
+ aptHook(&cookie, apt_hook, NULL);
+
+ console_print(GREEN "Waiting for wifi...\n" RESET);
+
+ /* wait for wifi to be available */
+ /*while((loop = aptMainLoop()) && !wifi && (ret == 0 || ret == 0xE0A09D2E))
+ {
+ ret = 0;
+
+ hidScanInput();
+ if(hidKeysDown() & KEY_B)
+ {
+ // user canceled
+ loop = false;
+ break;
+ }
+
+ // update the wifi status
+ ret = ACU_GetWifiStatus(&wifi);
+ if(ret != 0)
+ wifi = 0;
+ }*/
+
+ ret = 0;
+
+ // update the wifi status
+ ret = ACU_GetWifiStatus(&wifi);
+ if(ret != 0)
+ wifi = 0;
+
+ /* check if there was a wifi error */
+ if(ret != 0)
+ console_print(RED "ACU_GetWifiStatus returns 0x%lx\n" RESET, ret);
+
+ /* check if we need to exit */
+ if(!loop || ret != 0)
+ return -1;
+
+ console_print(GREEN "Ready!\n" RESET);
+
+ /* allocate buffer for SOC service */
+ SOCU_buffer = (u32*)memalign(SOCU_ALIGN, SOCU_BUFFERSIZE);
+ if(SOCU_buffer == NULL)
+ {
+ console_print(RED "memalign: failed to allocate\n" RESET);
+ goto memalign_fail;
+ }
+
+ /* initialize SOC service */
+ ret = socInit(SOCU_buffer, SOCU_BUFFERSIZE);
+ if(ret != 0)
+ {
+ console_print(RED "socInit: 0x%08X\n" RESET, (unsigned int)ret);
+ goto soc_fail;
+ }
+#endif
+
+ /* allocate socket to listen for clients */
+ listenfd = socket(AF_INET, SOCK_STREAM, 0);
+ if(listenfd < 0)
+ {
+ console_print(RED "socket: %d %s\n" RESET, errno, strerror(errno));
+ ftp_exit();
+ return -1;
+ }
+
+ /* get address to listen on */
+ serv_addr.sin_family = AF_INET;
+#ifdef _3DS
+ serv_addr.sin_addr.s_addr = gethostid();
+ serv_addr.sin_port = htons(LISTEN_PORT);
+#else
+ serv_addr.sin_addr.s_addr = INADDR_ANY;
+ serv_addr.sin_port = htons(LISTEN_PORT);
+#endif
+
+ /* reuse address */
+ {
+ int yes = 1;
+ rc = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+ if(rc != 0)
+ {
+ console_print(RED "setsockopt: %d %s\n" RESET, errno, strerror(errno));
+ ftp_exit();
+ return -1;
+ }
+ }
+
+ /* bind socket to listen address */
+ rc = bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
+ if(rc != 0)
+ {
+ console_print(RED "bind: %d %s\n" RESET, errno, strerror(errno));
+ ftp_exit();
+ return -1;
+ }
+
+ /* listen on socket */
+ rc = listen(listenfd, 5);
+ if(rc != 0)
+ {
+ console_print(RED "listen: %d %s\n" RESET, errno, strerror(errno));
+ ftp_exit();
+ return -1;
+ }
+
+ /* print server address */
+ rc = update_status();
+ if(rc != 0)
+ {
+ ftp_exit();
+ return -1;
+ }
+
+ return 0;
+
+#ifdef _3DS
+soc_fail:
+ free(SOCU_buffer);
+ SOCU_buffer = NULL;
+
+memalign_fail:
+ return -1;
+#endif
+}
+
+/*! deinitialize ftp subsystem */
+void
+ftp_exit(void)
+{
+#ifdef _3DS
+ Result ret;
+#endif
+
+ /* unhook aptHookCookie */
+ aptUnhook(&cookie);
+
+ debug_print("exiting ftp server\n");
+
+ /* clean up all sessions */
+ while(sessions != NULL)
+ ftp_session_destroy(sessions);
+
+ /* stop listening for new clients */
+ if(listenfd >= 0)
+ ftp_closesocket(listenfd, false);
+
+#ifdef _3DS
+ /* deinitialize SOC service */
+ console_render();
+ console_print(CYAN "Waiting for socExit()...\n" RESET);
+
+ if(SOCU_buffer != NULL)
+ {
+ ret = socExit();
+ if(ret != 0)
+ console_print(RED "socExit: 0x%08X\n" RESET, (unsigned int)ret);
+ free(SOCU_buffer);
+ }
+#endif
+}
+
+/*! ftp look
+ *
+ * @returns whether to keep looping
+ */
+loop_status_t
+ftp_loop(void)
+{
+ int rc;
+ struct pollfd pollinfo;
+ ftp_session_t *session;
+
+ /* we will poll for new client connections */
+ pollinfo.fd = listenfd;
+ pollinfo.events = POLLIN;
+ pollinfo.revents = 0;
+
+ /* poll for a new client */
+ rc = poll(&pollinfo, 1, 0);
+ if(rc < 0)
+ {
+ /* wifi got disabled */
+ if(errno == ENETDOWN)
+ return LOOP_RESTART;
+
+ console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno));
+ return LOOP_EXIT;
+ }
+ else if(rc > 0)
+ {
+ if(pollinfo.revents & POLLIN)
+ {
+ /* we got a new client */
+ ftp_session_new(listenfd);
+ }
+ else
+ {
+ console_print(YELLOW "listenfd: revents=0x%08X\n" RESET, pollinfo.revents);
+ }
+ }
+
+ /* poll each session */
+ session = sessions;
+ while(session != NULL)
+ session = ftp_session_poll(session);
+
+#ifdef _3DS
+ /* check if the user wants to exit */
+ hidScanInput();
+ u32 down = hidKeysDown();
+
+ if(down & KEY_B)
+ return LOOP_EXIT;
+
+ /* check if the user wants to toggle the LCD power */
+ if(down & KEY_START)
+ {
+ lcd_power = !lcd_power;
+ apt_hook(APTHOOK_ONRESTORE, NULL);
+ }
+#endif
+
+ return LOOP_CONTINUE;
+}
+
+/*! change to parent directory
+ *
+ * @param[in] session ftp session
+ */
+static void
+cd_up(ftp_session_t *session)
+{
+ char *slash = NULL, *p;
+
+ /* remove basename from cwd */
+ for(p = session->cwd; *p; ++p)
+ {
+ if(*p == '/')
+ slash = p;
+ }
+ *slash = 0;
+ if(strlen(session->cwd) == 0)
+ strcat(session->cwd, "/");
+}
+
+/*! validate a path
+ *
+ * @param[in] args path to validate
+ */
+static int
+validate_path(const char *args)
+{
+ const char *p;
+
+ /* make sure no path components are '..' */
+ p = args;
+ while((p = strstr(p, "/..")) != NULL)
+ {
+ if(p[3] == 0 || p[3] == '/')
+ return -1;
+ }
+
+ /* make sure there are no '//' */
+ if(strstr(args, "//") != NULL)
+ return -1;
+
+ return 0;
+}
+
+/*! get a path relative to cwd
+ *
+ * @param[in] session ftp session
+ * @param[in] cwd working directory
+ * @param[in] args path to make
+ *
+ * @returns error
+ *
+ * @note the output goes to session->buffer
+ */
+static int
+build_path(ftp_session_t *session,
+ const char *cwd,
+ const char *args)
+{
+ int rc;
+ char *p;
+
+ session->buffersize = 0;
+ memset(session->buffer, 0, sizeof(session->buffer));
+
+ /* make sure the input is a valid path */
+ if(validate_path(args) != 0)
+ {
+ errno = EINVAL;
+ return -1;
+ }
+
+ if(args[0] == '/')
+ {
+ /* this is an absolute path */
+ size_t len = strlen(args);
+ if(len > sizeof(session->buffer)-1)
+ {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+
+ memcpy(session->buffer, args, len);
+ session->buffersize = len;
+ }
+ else
+ {
+ /* this is a relative path */
+ if(strcmp(cwd, "/") == 0)
+ rc = snprintf(session->buffer, sizeof(session->buffer), "/%s",
+ args);
+ else
+ rc = snprintf(session->buffer, sizeof(session->buffer), "%s/%s",
+ cwd, args);
+
+ if(rc >= sizeof(session->buffer))
+ {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+
+ session->buffersize = rc;
+ }
+
+ /* remove trailing / */
+ p = session->buffer + session->buffersize;
+ while(p > session->buffer && *--p == '/')
+ {
+ *p = 0;
+ --session->buffersize;
+ }
+
+ /* if we ended with an empty path, it is the root directory */
+ if(session->buffersize == 0)
+ session->buffer[session->buffersize++] = '/';
+
+ return 0;
+}
+
+/*! transfer a directory listing
+ *
+ * @param[in] session ftp session
+ *
+ * @returns whether to call again
+ */
+static loop_status_t
+list_transfer(ftp_session_t *session)
+{
+ ssize_t rc;
+ size_t len;
+ char *buffer;
+ struct stat st;
+ struct dirent *dent;
+
+ /* check if we sent all available data */
+ if(session->bufferpos == session->buffersize)
+ {
+ /* check xfer dir type */
+ if(session->dir_mode == XFER_DIR_STAT)
+ rc = 213;
+ else
+ {
+ rc = 226;
+ memset(ftp_file_transfer, 0, 50); // Empty transfer status
+ isTransfering = false;
+ }
+
+ /* check if this was for a file */
+ if(session->dp == NULL)
+ {
+ /* we already sent the file's listing */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, rc, "OK\r\n");
+ return LOOP_EXIT;
+ }
+
+ /* get the next directory entry */
+ dent = readdir(session->dp);
+ if(dent == NULL)
+ {
+ /* we have exhausted the directory listing */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, rc, "OK\r\n");
+ return LOOP_EXIT;
+ }
+
+ /* TODO I think we are supposed to return entries for . and .. */
+ if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0)
+ return LOOP_CONTINUE;
+
+ /* check if this was a NLST */
+ if(session->dir_mode == XFER_DIR_NLST)
+ {
+ /* NLST gives the whole path name */
+ session->buffersize = 0;
+ if(build_path(session, session->lwd, dent->d_name) == 0)
+ {
+ /* encode \n in path */
+ len = session->buffersize;
+ buffer = encode_path(session->buffer, &len, false);
+ if(buffer != NULL)
+ {
+ /* copy to the session buffer to send */
+ memcpy(session->buffer, buffer, len);
+ free(buffer);
+ session->buffer[len++] = '\r';
+ session->buffer[len++] = '\n';
+ session->buffersize = len;
+ }
+ }
+ }
+ else
+ {
+#ifdef _3DS
+ /* the sdmc directory entry already has the type and size, so no need to do a slow stat */
+ u32 magic = *(u32*)session->dp->dirData->dirStruct;
+
+ if(magic == SDMC_DIRITER_MAGIC)
+ {
+ sdmc_dir_t *dir = (sdmc_dir_t*)session->dp->dirData->dirStruct;
+ FS_DirectoryEntry *entry = &dir->entry_data[dir->index];
+
+ if(entry->attributes & FS_ATTRIBUTE_DIRECTORY)
+ st.st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH;
+ else
+ st.st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
+
+ if(!(entry->attributes & FS_ATTRIBUTE_READ_ONLY))
+ st.st_mode |= S_IWUSR | S_IWGRP | S_IWOTH;
+
+ st.st_size = entry->fileSize;
+ st.st_mtime = 0;
+
+ bool getmtime = true;
+ if(session->dir_mode == XFER_DIR_MLSD
+ || session->dir_mode == XFER_DIR_MLST)
+ {
+ if(!(session->mlst_flags & SESSION_MLST_MODIFY))
+ getmtime = false;
+ }
+ else if(session->dir_mode == XFER_DIR_NLST)
+ getmtime = false;
+
+ if((rc = build_path(session, session->lwd, dent->d_name)) != 0)
+ console_print(RED "build_path: %d %s\n" RESET, errno, strerror(errno));
+ else if(getmtime)
+ {
+ uint64_t mtime = 0;
+ if((rc = sdmc_getmtime(session->buffer, &mtime)) != 0)
+ console_print(RED "sdmc_getmtime '%s': 0x%x\n" RESET, session->buffer, rc);
+ else
+ st.st_mtime = mtime;
+ }
+ }
+ else
+ {
+ /* lstat the entry */
+ if((rc = build_path(session, session->lwd, dent->d_name)) != 0)
+ console_print(RED "build_path: %d %s\n" RESET, errno, strerror(errno));
+ else if((rc = lstat(session->buffer, &st)) != 0)
+ console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+
+ if(rc != 0)
+ {
+ /* an error occurred */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "unavailable\r\n");
+ return LOOP_EXIT;
+ }
+ }
+#else
+ /* lstat the entry */
+ if((rc = build_path(session, session->lwd, dent->d_name)) != 0)
+ console_print(RED "build_path: %d %s\n" RESET, errno, strerror(errno));
+ else if((rc = lstat(session->buffer, &st)) != 0)
+ console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+
+ if(rc != 0)
+ {
+ /* an error occurred */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "unavailable\r\n");
+ return LOOP_EXIT;
+ }
+#endif
+ /* encode \n in path */
+ len = strlen(dent->d_name);
+ buffer = encode_path(dent->d_name, &len, false);
+ if(buffer != NULL)
+ {
+ rc = ftp_session_fill_dirent(session, &st, buffer, len);
+ free(buffer);
+ if(rc != 0)
+ {
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 425, "%s\r\n", strerror(rc));
+ return LOOP_EXIT;
+ }
+ }
+ else
+ session->buffersize = 0;
+ }
+ session->bufferpos = 0;
+ }
+
+ /* send any pending data */
+ rc = send(session->data_fd, session->buffer + session->bufferpos,
+ session->buffersize - session->bufferpos, 0);
+ if(rc <= 0)
+ {
+ /* error sending data */
+ if(rc < 0)
+ {
+ if(errno == EWOULDBLOCK)
+ return LOOP_EXIT;
+ console_print(RED "send: %d %s\n" RESET, errno, strerror(errno));
+ }
+ else
+ console_print(YELLOW "send: %d %s\n" RESET, ECONNRESET, strerror(ECONNRESET));
+
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 426, "Connection broken during transfer\r\n");
+ return LOOP_EXIT;
+ }
+
+ /* we can try to send more data */
+ session->bufferpos += rc;
+ return LOOP_CONTINUE;
+}
+
+/*! send a file to the client
+ *
+ * @param[in] session ftp session
+ *
+ * @returns whether to call again
+ */
+static loop_status_t
+retrieve_transfer(ftp_session_t *session)
+{
+ ssize_t rc;
+
+ if(session->bufferpos == session->buffersize)
+ {
+ /* we have sent all the data so read some more */
+ rc = ftp_session_read_file(session);
+ if(rc <= 0)
+ {
+ /* can't read any more data */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ if(rc < 0)
+ ftp_send_response(session, 451, "Failed to read file\r\n");
+ else
+ {
+ ftp_send_response(session, 226, "OK\r\n");
+ memset(ftp_file_transfer, 0, 50); // Empty transfer status
+ isTransfering = false;
+ }
+ return LOOP_EXIT;
+ }
+
+ /* we read some data so reset the session buffer to send */
+ session->bufferpos = 0;
+ session->buffersize = rc;
+ }
+
+ /* send any pending data */
+ rc = send(session->data_fd, session->buffer + session->bufferpos,
+ session->buffersize - session->bufferpos, 0);
+ if(rc <= 0)
+ {
+ /* error sending data */
+ if(rc < 0)
+ {
+ if(errno == EWOULDBLOCK)
+ return LOOP_EXIT;
+ console_print(RED "send: %d %s\n" RESET, errno, strerror(errno));
+ }
+ else
+ console_print(YELLOW "send: %d %s\n" RESET, ECONNRESET, strerror(ECONNRESET));
+
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 426, "Connection broken during transfer\r\n");
+ return LOOP_EXIT;
+ }
+
+ /* we can try to send more data */
+ session->bufferpos += rc;
+ return LOOP_CONTINUE;
+}
+
+/*! send a file to the client
+ *
+ * @param[in] session ftp session
+ *
+ * @returns whether to call again
+ */
+static loop_status_t
+store_transfer(ftp_session_t *session)
+{
+ ssize_t rc;
+
+ if(session->bufferpos == session->buffersize)
+ {
+ /* we have written all the received data, so try to get some more */
+ rc = recv(session->data_fd, session->buffer, sizeof(session->buffer), 0);
+ if(rc <= 0)
+ {
+ /* can't read any more data */
+ if(rc < 0)
+ {
+ if(errno == EWOULDBLOCK)
+ return LOOP_EXIT;
+ console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno));
+ }
+
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+
+ if(rc == 0)
+ ftp_send_response(session, 226, "OK\r\n");
+ else
+ ftp_send_response(session, 426, "Connection broken during transfer\r\n");
+ return LOOP_EXIT;
+ }
+
+ /* we received some data so reset the session buffer to write */
+ session->bufferpos = 0;
+ session->buffersize = rc;
+ }
+
+ rc = ftp_session_write_file(session);
+ if(rc <= 0)
+ {
+ /* error writing data */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 451, "Failed to write file\r\n");
+ return LOOP_EXIT;
+ }
+
+ /* we can try to receive more data */
+ session->bufferpos += rc;
+ return LOOP_CONTINUE;
+}
+
+/*! ftp_xfer_file mode */
+typedef enum
+{
+ XFER_FILE_RETR, /*!< Retrieve a file */
+ XFER_FILE_STOR, /*!< Store a file */
+ XFER_FILE_APPE, /*!< Append a file */
+} xfer_file_mode_t;
+
+/*! Transfer a file
+ *
+ * @param[in] session ftp session
+ * @param[in] args ftp arguments
+ * @param[in] mode transfer mode
+ *
+ * @returns failure
+ */
+static void
+ftp_xfer_file(ftp_session_t *session,
+ const char *args,
+ xfer_file_mode_t mode)
+{
+ int rc;
+
+ /* build the path of the file to transfer */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ rc = errno;
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 553, "%s\r\n", strerror(rc));
+ return;
+ }
+
+ /* open the file for retrieving or storing */
+ if(mode == XFER_FILE_RETR)
+ rc = ftp_session_open_file_read(session);
+ else
+ rc = ftp_session_open_file_write(session, mode == XFER_FILE_APPE);
+
+ if(rc != 0)
+ {
+ /* error opening the file */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 450, "failed to open file\r\n");
+ return;
+ }
+
+ if(session->flags & (SESSION_PORT|SESSION_PASV))
+ {
+ ftp_session_set_state(session, DATA_CONNECT_STATE, CLOSE_DATA);
+
+ if(session->flags & SESSION_PORT)
+ {
+ /* setup connection */
+ rc = ftp_session_connect(session);
+ if(rc != 0)
+ {
+ /* error connecting */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 425, "can't open data connection\r\n");
+ return;
+ }
+ }
+
+ /* set up the transfer */
+ session->flags &= ~(SESSION_RECV|SESSION_SEND);
+ if(mode == XFER_FILE_RETR)
+ {
+ session->flags |= SESSION_SEND;
+ session->transfer = retrieve_transfer;
+ }
+ else
+ {
+ session->flags |= SESSION_RECV;
+ session->transfer = store_transfer;
+ }
+
+ session->bufferpos = 0;
+ session->buffersize = 0;
+
+ return;
+ }
+
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 503, "Bad sequence of commands\r\n");
+}
+
+/*! Transfer a directory
+ *
+ * @param[in] session ftp session
+ * @param[in] args ftp arguments
+ * @param[in] mode transfer mode
+ * @param[in] workaround whether to workaround LIST -a
+ */
+static void
+ftp_xfer_dir(ftp_session_t *session,
+ const char *args,
+ xfer_dir_mode_t mode,
+ bool workaround)
+{
+ ssize_t rc;
+ size_t len;
+ struct stat st;
+ char *buffer;
+
+ /* set up the transfer */
+ session->dir_mode = mode;
+ session->flags &= ~SESSION_RECV;
+ session->flags |= SESSION_SEND;
+
+ session->transfer = list_transfer;
+ session->buffersize = 0;
+ session->bufferpos = 0;
+
+ if(strlen(args) > 0)
+ {
+ /* an argument was provided */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ /* error building path */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* check if this is a directory */
+ session->dp = opendir(session->buffer);
+ if(session->dp == NULL)
+ {
+ /* not a directory; check if it is a file */
+ rc = stat(session->buffer, &st);
+ if(rc != 0)
+ {
+ /* error getting stat */
+ rc = errno;
+
+ /* work around broken clients that think LIST -a is valid */
+ if(workaround && mode == XFER_DIR_LIST)
+ {
+ if(args[0] == '-' && (args[1] == 'a' || args[1] == 'l'))
+ {
+ if(args[2] == 0)
+ buffer = strdup(args+2);
+ else
+ buffer = strdup(args+3);
+
+ if(buffer != NULL)
+ {
+ ftp_xfer_dir(session, buffer, mode, false);
+ free(buffer);
+ return;
+ }
+
+ rc = ENOMEM;
+ }
+ }
+
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "%s\r\n", strerror(rc));
+ return;
+ }
+ else if(mode == XFER_DIR_MLSD)
+ {
+ /* specified file instead of directory for MLSD */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL));
+ return;
+ }
+ else if(mode == XFER_DIR_NLST)
+ {
+ /* NLST uses full path name */
+ len = session->buffersize;
+ buffer = encode_path(session->buffer, &len, false);
+ }
+ else
+ {
+ /* everything else uses base name */
+ const char *base = strrchr(session->buffer, '/') + 1;
+
+ len = strlen(base);
+ buffer = encode_path(base, &len, false);
+ }
+
+ if(buffer)
+ {
+ rc = ftp_session_fill_dirent(session, &st, buffer, len);
+ free(buffer);
+ }
+ else
+ rc = ENOMEM;
+
+ if(rc != 0)
+ {
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "%s\r\n", strerror(rc));
+ return;
+ }
+ }
+ else
+ {
+ /* it was a directory, so set it as the lwd */
+ memcpy(session->lwd, session->buffer, session->buffersize);
+ session->lwd[session->buffersize] = 0;
+ session->buffersize = 0;
+
+ if(session->dir_mode == XFER_DIR_MLSD
+ && (session->mlst_flags & SESSION_MLST_TYPE))
+ {
+ /* send this directory as type=cdir */
+ rc = ftp_session_fill_dirent_cdir(session, session->lwd);
+ if(rc != 0)
+ {
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "%s\r\n", strerror(rc));
+ return;
+ }
+ }
+ }
+ }
+ else if(ftp_session_open_cwd(session) != 0)
+ {
+ /* no argument, but opening cwd failed */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "%s\r\n", strerror(errno));
+ return;
+ }
+ else
+ {
+ /* set the cwd as the lwd */
+ strcpy(session->lwd, session->cwd);
+ session->buffersize = 0;
+
+ if(session->dir_mode == XFER_DIR_MLSD
+ && (session->mlst_flags & SESSION_MLST_TYPE))
+ {
+ /* send this directory as type=cdir */
+ rc = ftp_session_fill_dirent_cdir(session, session->lwd);
+ if(rc != 0)
+ {
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "%s\r\n", strerror(rc));
+ return;
+ }
+ }
+ }
+
+ if(mode == XFER_DIR_MLST || mode == XFER_DIR_STAT)
+ {
+ /* this is a little different; we have to send the data over the command socket */
+ ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV | CLOSE_DATA);
+ session->data_fd = session->cmd_fd;
+ session->flags |= SESSION_SEND;
+ ftp_send_response(session, -213, "Status\r\n");
+ return;
+ }
+ else if(session->flags & (SESSION_PORT|SESSION_PASV))
+ {
+ ftp_session_set_state(session, DATA_CONNECT_STATE, CLOSE_DATA);
+
+ if(session->flags & SESSION_PORT)
+ {
+ /* setup connection */
+ rc = ftp_session_connect(session);
+ if(rc != 0)
+ {
+ /* error connecting */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 425, "can't open data connection\r\n");
+ }
+ }
+
+ return;
+ }
+
+ /* we must have got LIST/MLSD/MLST/NLST without a preceding PORT or PASV */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 503, "Bad sequence of commands\r\n");
+}
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * *
+ * F T P C O M M A N D S *
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+/*! @fn static void ABOR(ftp_session_t *session, const char *args)
+ *
+ * @brief abort a transfer
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(ABOR)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ if(session->state == COMMAND_STATE)
+ {
+ ftp_send_response(session, 225, "No transfer to abort\r\n");
+ return;
+ }
+
+ /* abort the transfer */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+
+ /* send response for this request */
+ ftp_send_response(session, 225, "Aborted\r\n");
+
+ /* send response for transfer */
+ ftp_send_response(session, 425, "Transfer aborted\r\n");
+}
+
+/*! @fn static void ALLO(ftp_session_t *session, const char *args)
+ *
+ * @brief allocate space
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(ALLO)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ ftp_send_response(session, 202, "superfluous command\r\n");
+}
+
+/*! @fn static void APPE(ftp_session_t *session, const char *args)
+ *
+ * @brief append data to a file
+ *
+ * @note requires a PASV or PORT connection
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(APPE)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* open the file in append mode */
+ ftp_xfer_file(session, args, XFER_FILE_APPE);
+}
+
+/*! @fn static void CDUP(ftp_session_t *session, const char *args)
+ *
+ * @brief CWD to parent directory
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(CDUP)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* change to parent directory */
+ cd_up(session);
+
+ ftp_send_response(session, 200, "OK\r\n");
+}
+
+/*! @fn static void CWD(ftp_session_t *session, const char *args)
+ *
+ * @brief change working directory
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(CWD)
+{
+ struct stat st;
+ int rc;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* .. is equivalent to CDUP */
+ if(strcmp(args, "..") == 0)
+ {
+ cd_up(session);
+ ftp_send_response(session, 200, "OK\r\n");
+ return;
+ }
+
+ /* build the new cwd path */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* get the path status */
+ rc = stat(session->buffer, &st);
+ if(rc != 0)
+ {
+ console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno));
+ ftp_send_response(session, 550, "unavailable\r\n");
+ return;
+ }
+
+ /* make sure it is a directory */
+ if(!S_ISDIR(st.st_mode))
+ {
+ ftp_send_response(session, 553, "not a directory\r\n");
+ return;
+ }
+
+ /* copy the path into the cwd */
+ strncpy(session->cwd, session->buffer, sizeof(session->cwd));
+
+ ftp_send_response(session, 200, "OK\r\n");
+}
+
+/*! @fn static void DELE(ftp_session_t *session, const char *args)
+ *
+ * @brief delete a file
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(DELE)
+{
+ int rc;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the file path */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* try to unlink the path */
+ rc = unlink(session->buffer);
+ if(rc != 0)
+ {
+ /* error unlinking the file */
+ console_print(RED "unlink: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 550, "failed to delete file\r\n");
+ return;
+ }
+
+ update_free_space();
+ ftp_send_response(session, 250, "OK\r\n");
+}
+
+/*! @fn static void FEAT(ftp_session_t *session, const char *args)
+ *
+ * @brief list server features
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(FEAT)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* list our features */
+ ftp_send_response(session, -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",
+ session->mlst_flags & SESSION_MLST_TYPE ? "*" : "",
+ session->mlst_flags & SESSION_MLST_SIZE ? "*" : "",
+ session->mlst_flags & SESSION_MLST_MODIFY ? "*" : "",
+ session->mlst_flags & SESSION_MLST_PERM ? "*" : "",
+ session->mlst_flags & SESSION_MLST_UNIX_MODE ? "*" : "");
+}
+
+/*! @fn static void HELP(ftp_session_t *session, const char *args)
+ *
+ * @brief print server help
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(HELP)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* list our accepted commands */
+ ftp_send_response(session, -214,
+ "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 STAT\r\n"
+ " STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD XPWD XRMD\r\n"
+ "214 End\r\n");
+}
+
+/*! @fn static void LIST(ftp_session_t *session, const char *args)
+ *
+ * @brief retrieve a directory listing
+ *
+ * @note Requires a PORT or PASV connection
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(LIST)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* open the path in LIST mode */
+ ftp_xfer_dir(session, args, XFER_DIR_LIST, true);
+}
+
+/*! @fn static void MDTM(ftp_session_t *session, const char *args)
+ *
+ * @brief get last modification time
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(MDTM)
+{
+ int rc;
+#ifdef _3DS
+ uint64_t mtime;
+#else
+ struct stat st;
+#endif
+ time_t t_mtime;
+ struct tm *tm;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the path */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+#ifdef _3DS
+ rc = sdmc_getmtime(session->buffer, &mtime);
+ if(rc != 0)
+ {
+ ftp_send_response(session, 550, "Error getting mtime\r\n");
+ return;
+ }
+ t_mtime = mtime;
+#else
+ rc = stat(session->buffer, &st);
+ if(rc != 0)
+ {
+ ftp_send_response(session, 550, "Error getting mtime\r\n");
+ return;
+ }
+ t_mtime = st.st_mtime;
+#endif
+
+ tm = gmtime(&t_mtime);
+ if(tm == NULL)
+ {
+ ftp_send_response(session, 550, "Error getting mtime\r\n");
+ return;
+ }
+
+ session->buffersize = strftime(session->buffer, sizeof(session->buffer), "%Y%m%d%H%M%S", tm);
+ if(session->buffersize == 0)
+ {
+ ftp_send_response(session, 550, "Error getting mtime\r\n");
+ return;
+ }
+
+ session->buffer[session->buffersize] = 0;
+
+ ftp_send_response(session, 213, "%s\r\n", session->buffer);
+}
+/*! @fn static void MKD(ftp_session_t *session, const char *args)
+ *
+ * @brief create a directory
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(MKD)
+{
+ int rc;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the path */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* try to create the directory */
+ rc = mkdir(session->buffer, 0755);
+ if(rc != 0 && errno != EEXIST)
+ {
+ /* mkdir failure */
+ console_print(RED "mkdir: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 550, "failed to create directory\r\n");
+ return;
+ }
+
+ update_free_space();
+ ftp_send_response(session, 250, "OK\r\n");
+}
+
+/*! @fn static void MLSD(ftp_session_t *session, const char *args)
+ *
+ * @brief set transfer mode
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(MLSD)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* open the path in MLSD mode */
+ ftp_xfer_dir(session, args, XFER_DIR_MLSD, true);
+}
+
+/*! @fn static void MLST(ftp_session_t *session, const char *args)
+ *
+ * @brief set transfer mode
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(MLST)
+{
+ struct stat st;
+ int rc;
+ char *path;
+ size_t len;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the path */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 501, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* stat path */
+ rc = lstat(session->buffer, &st);
+ if(rc != 0)
+ {
+ ftp_send_response(session, 550, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* encode \n in path */
+ len = session->buffersize;
+ path = encode_path(session->buffer, &len, true);
+ if(!path)
+ {
+ ftp_send_response(session, 550, "%s\r\n", strerror(ENOMEM));
+ return;
+ }
+
+ session->dir_mode = XFER_DIR_MLST;
+ rc = ftp_session_fill_dirent(session, &st, path, len);
+ free(path);
+ if(rc != 0)
+ {
+ ftp_send_response(session, 550, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ path = malloc(session->buffersize + 1);
+ if(!path)
+ {
+ ftp_send_response(session, 550, "%s\r\n", strerror(ENOMEM));
+ return;
+ }
+
+ memcpy(path, session->buffer, session->buffersize);
+ path[session->buffersize] = 0;
+ ftp_send_response(session, -250, "Status\r\n%s250 End\r\n", path);
+ free(path);
+}
+
+/*! @fn static void MODE(ftp_session_t *session, const char *args)
+ *
+ * @brief set transfer mode
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(MODE)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* we only accept S (stream) mode */
+ if(strcasecmp(args, "S") == 0)
+ {
+ ftp_send_response(session, 200, "OK\r\n");
+ return;
+ }
+
+ ftp_send_response(session, 504, "unavailable\r\n");
+}
+
+/*! @fn static void NLST(ftp_session_t *session, const char *args)
+ *
+ * @brief retrieve a name list
+ *
+ * @note Requires a PASV or PORT connection
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(NLST)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* open the path in NLST mode */
+ return ftp_xfer_dir(session, args, XFER_DIR_NLST, false);
+}
+
+/*! @fn static void NOOP(ftp_session_t *session, const char *args)
+ *
+ * @brief no-op
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(NOOP)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* this is a no-op */
+ ftp_send_response(session, 200, "OK\r\n");
+}
+
+/*! @fn static void OPTS(ftp_session_t *session, const char *args)
+ *
+ * @brief set options
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(OPTS)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* we accept the following UTF8 options */
+ if(strcasecmp(args, "UTF8") == 0
+ || strcasecmp(args, "UTF8 ON") == 0
+ || strcasecmp(args, "UTF8 NLST") == 0)
+ {
+ ftp_send_response(session, 200, "OK\r\n");
+ return;
+ }
+
+ /* check MLST options */
+ if(strncasecmp(args, "MLST ", 5) == 0)
+ {
+ static const struct
+ {
+ const char *name;
+ session_mlst_flags_t flag;
+ } mlst_flags[] =
+ {
+ { "Type;", SESSION_MLST_TYPE, },
+ { "Size;", SESSION_MLST_SIZE, },
+ { "Modify;", SESSION_MLST_MODIFY, },
+ { "Perm;", SESSION_MLST_PERM, },
+ { "UNIX.mode;", SESSION_MLST_UNIX_MODE, },
+ };
+ static const size_t num_mlst_flags = sizeof(mlst_flags)/sizeof(mlst_flags[0]);
+
+ session_mlst_flags_t flags = 0;
+ args += 5;
+ const char *p = args;
+ while(*p)
+ {
+ for(size_t i = 0; i < num_mlst_flags; ++i)
+ {
+ if(strncasecmp(mlst_flags[i].name, p, strlen(mlst_flags[i].name)) == 0)
+ {
+ flags |= mlst_flags[i].flag;
+ p += strlen(mlst_flags[i].name)-1;
+ break;
+ }
+ }
+
+ while(*p && *p != ';')
+ ++p;
+
+ if(*p == ';')
+ ++p;
+ }
+
+ session->mlst_flags = flags;
+ ftp_send_response(session, 200, "MLST OPTS%s%s%s%s%s%s\r\n",
+ flags ? " " : "",
+ flags & SESSION_MLST_TYPE ? "Type;" : "",
+ flags & SESSION_MLST_SIZE ? "Size;" : "",
+ flags & SESSION_MLST_MODIFY ? "Modify;" : "",
+ flags & SESSION_MLST_PERM ? "Perm;" : "",
+ flags & SESSION_MLST_UNIX_MODE ? "UNIX.mode;" : "");
+ return;
+ }
+
+ ftp_send_response(session, 504, "invalid argument\r\n");
+}
+
+/*! @fn static void PASS(ftp_session_t *session, const char *args)
+ *
+ * @brief provide password
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(PASS)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* we accept any password */
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ ftp_send_response(session, 230, "OK\r\n");
+}
+
+/*! @fn static void PASV(ftp_session_t *session, const char *args)
+ *
+ * @brief request an address to connect to
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(PASV)
+{
+ int rc;
+ char buffer[INET_ADDRSTRLEN + 10];
+ char *p;
+ in_port_t port;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ memset(buffer, 0, sizeof(buffer));
+
+ /* reset the state */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ session->flags &= ~(SESSION_PASV|SESSION_PORT);
+
+ /* create a socket to listen on */
+ session->pasv_fd = socket(AF_INET, SOCK_STREAM, 0);
+ if(session->pasv_fd < 0)
+ {
+ console_print(RED "socket: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 451, "\r\n");
+ return;
+ }
+
+ /* set the socket options */
+ rc = ftp_set_socket_options(session->pasv_fd);
+ if(rc != 0)
+ {
+ /* failed to set socket options */
+ ftp_session_close_pasv(session);
+ ftp_send_response(session, 451, "\r\n");
+ return;
+ }
+
+ /* grab a new port */
+ session->pasv_addr.sin_port = htons(next_data_port());
+
+#ifdef _3DS
+ console_print(YELLOW "binding to %s:%u\n" RESET,
+ inet_ntoa(session->pasv_addr.sin_addr),
+ ntohs(session->pasv_addr.sin_port));
+#endif
+
+ /* bind to the port */
+ rc = bind(session->pasv_fd, (struct sockaddr*)&session->pasv_addr,
+ sizeof(session->pasv_addr));
+ if(rc != 0)
+ {
+ /* failed to bind */
+ console_print(RED "bind: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_pasv(session);
+ ftp_send_response(session, 451, "\r\n");
+ return;
+ }
+
+ /* listen on the socket */
+ rc = listen(session->pasv_fd, 1);
+ if(rc != 0)
+ {
+ /* failed to listen */
+ console_print(RED "listen: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_pasv(session);
+ ftp_send_response(session, 451, "\r\n");
+ return;
+ }
+
+#ifndef _3DS
+ {
+ /* get the socket address since we requested an ephemeral port */
+ socklen_t addrlen = sizeof(session->pasv_addr);
+ rc = getsockname(session->pasv_fd, (struct sockaddr*)&session->pasv_addr,
+ &addrlen);
+ if(rc != 0)
+ {
+ /* failed to get socket address */
+ console_print(RED "getsockname: %d %s\n" RESET, errno, strerror(errno));
+ ftp_session_close_pasv(session);
+ ftp_send_response(session, 451, "\r\n");
+ return;
+ }
+ }
+#endif
+
+ /* we are now listening on the socket */
+ console_print(YELLOW "listening on %s:%u\n" RESET,
+ inet_ntoa(session->pasv_addr.sin_addr),
+ ntohs(session->pasv_addr.sin_port));
+ session->flags |= SESSION_PASV;
+
+ /* print the address in the ftp format */
+ port = ntohs(session->pasv_addr.sin_port);
+ strcpy(buffer, inet_ntoa(session->pasv_addr.sin_addr));
+ sprintf(buffer+strlen(buffer), ",%u,%u",
+ port >> 8, port & 0xFF);
+ for(p = buffer; *p; ++p)
+ {
+ if(*p == '.')
+ *p = ',';
+ }
+
+ ftp_send_response(session, 227, "%s\r\n", buffer);
+}
+
+/*! @fn static void PORT(ftp_session_t *session, const char *args)
+ *
+ * @brief provide an address for the server to connect to
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(PORT)
+{
+ char *addrstr, *p, *portstr;
+ int commas = 0, rc;
+ short port = 0;
+ unsigned long val;
+ struct sockaddr_in addr;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* reset the state */
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ session->flags &= ~(SESSION_PASV|SESSION_PORT);
+
+ /* dup the args since they are const and we need to change it */
+ addrstr = strdup(args);
+ if(addrstr == NULL)
+ {
+ ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM));
+ return;
+ }
+
+ /* replace a,b,c,d,e,f with a.b.c.d\0e.f */
+ for(p = addrstr; *p; ++p)
+ {
+ if(*p == ',')
+ {
+ if(commas != 3)
+ *p = '.';
+ else
+ {
+ *p = 0;
+ portstr = p+1;
+ }
+ ++commas;
+ }
+ }
+
+ /* make sure we got the right number of values */
+ if(commas != 5)
+ {
+ free(addrstr);
+ ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL));
+ return;
+ }
+
+ /* parse the address */
+ rc = inet_aton(addrstr, &addr.sin_addr);
+ if(rc == 0)
+ {
+ free(addrstr);
+ ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL));
+ return;
+ }
+
+ /* parse the port */
+ val = 0;
+ port = 0;
+ for(p = portstr; *p; ++p)
+ {
+ if(!isdigit((int)*p))
+ {
+ if(p == portstr || *p != '.' || val > 0xFF)
+ {
+ free(addrstr);
+ ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL));
+ return;
+ }
+ port <<= 8;
+ port += val;
+ val = 0;
+ }
+ else
+ {
+ val *= 10;
+ val += *p - '0';
+ }
+ }
+
+ /* validate the port */
+ if(val > 0xFF || port > 0xFF)
+ {
+ free(addrstr);
+ ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL));
+ return;
+ }
+ port <<= 8;
+ port += val;
+
+ /* fill in the address port and family */
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+
+ free(addrstr);
+
+ memcpy(&session->peer_addr, &addr, sizeof(addr));
+
+ /* we are ready to connect to the client */
+ session->flags |= SESSION_PORT;
+ ftp_send_response(session, 200, "OK\r\n");
+}
+
+/*! @fn static void PWD(ftp_session_t *session, const char *args)
+ *
+ * @brief print working directory
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(PWD)
+{
+ static char buffer[CMD_BUFFERSIZE];
+ size_t len = sizeof(buffer), i;
+ char *path;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* encode the cwd */
+ len = strlen(session->cwd);
+ path = encode_path(session->cwd, &len, true);
+ if(path != NULL)
+ {
+ i = sprintf(buffer, "257 \"");
+ if(i + len + 3 > sizeof(buffer))
+ {
+ /* buffer will overflow */
+ free(path);
+ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA);
+ ftp_send_response(session, 550, "unavailable\r\n");
+ ftp_send_response(session, 425, "%s\r\n", strerror(EOVERFLOW));
+ return;
+ }
+ memcpy(buffer+i, path, len);
+ free(path);
+ len += i;
+ buffer[len++] = '"';
+ buffer[len++] = '\r';
+ buffer[len++] = '\n';
+
+ ftp_send_response_buffer(session, buffer, len);
+ return;
+ }
+
+ ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM));
+}
+
+/*! @fn static void QUIT(ftp_session_t *session, const char *args)
+ *
+ * @brief terminate ftp session
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(QUIT)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* disconnect from the client */
+ ftp_send_response(session, 221, "disconnecting\r\n");
+ ftp_session_close_cmd(session);
+}
+
+/*! @fn static void REST(ftp_session_t *session, const char *args)
+ *
+ * @brief restart a transfer
+ *
+ * @note sets file position for a subsequent STOR operation
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(REST)
+{
+ const char *p;
+ uint64_t pos = 0;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* make sure an argument is provided */
+ if(args == NULL)
+ {
+ ftp_send_response(session, 504, "invalid argument\r\n");
+ return;
+ }
+
+ /* parse the offset */
+ for(p = args; *p; ++p)
+ {
+ if(!isdigit((int)*p))
+ {
+ ftp_send_response(session, 504, "invalid argument\r\n");
+ return;
+ }
+
+ if(UINT64_MAX / 10 < pos)
+ {
+ ftp_send_response(session, 504, "invalid argument\r\n");
+ return;
+ }
+
+ pos *= 10;
+
+ if(UINT64_MAX - (*p - '0') < pos)
+ {
+ ftp_send_response(session, 504, "invalid argument\r\n");
+ return;
+ }
+
+ pos += (*p - '0');
+ }
+
+ /* set the restart offset */
+ session->filepos = pos;
+ ftp_send_response(session, 200, "OK\r\n");
+}
+
+/*! @fn static void RETR(ftp_session_t *session, const char *args)
+ *
+ * @brief retrieve a file
+ *
+ * @note Requires a PASV or PORT connection
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(RETR)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+ snprintf(ftp_file_transfer, sizeof(ftp_file_transfer), "Sending: %s", args ? args : "");
+ isTransfering = true;
+
+ /* open the file to retrieve */
+ return ftp_xfer_file(session, args, XFER_FILE_RETR);
+}
+
+/*! @fn static void RMD(ftp_session_t *session, const char *args)
+ *
+ * @brief remove a directory
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(RMD)
+{
+ int rc;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the path to remove */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* remove the directory */
+ rc = rmdir(session->buffer);
+ if(rc != 0)
+ {
+ /* rmdir error */
+ console_print(RED "rmdir: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 550, "failed to delete directory\r\n");
+ return;
+ }
+
+ update_free_space();
+ ftp_send_response(session, 250, "OK\r\n");
+}
+
+/*! @fn static void RNFR(ftp_session_t *session, const char *args)
+ *
+ * @brief rename from
+ *
+ * @note Must be followed by RNTO
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(RNFR)
+{
+ int rc;
+ struct stat st;
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the path to rename from */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* make sure the path exists */
+ rc = lstat(session->buffer, &st);
+ if(rc != 0)
+ {
+ /* error getting path status */
+ console_print(RED "lstat: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 450, "no such file or directory\r\n");
+ return;
+ }
+
+ /* we are ready for RNTO */
+ session->flags |= SESSION_RENAME;
+ ftp_send_response(session, 350, "OK\r\n");
+}
+
+/*! @fn static void RNTO(ftp_session_t *session, const char *args)
+ *
+ * @brief rename to
+ *
+ * @note Must be preceded by RNFR
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(RNTO)
+{
+ static char rnfr[XFER_BUFFERSIZE]; // rename-from buffer
+ int rc;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* make sure the previous command was RNFR */
+ if(!(session->flags & SESSION_RENAME))
+ {
+ ftp_send_response(session, 503, "Bad sequence of commands\r\n");
+ return;
+ }
+
+ /* clear the rename state */
+ session->flags &= ~SESSION_RENAME;
+
+ /* copy the RNFR path */
+ memcpy(rnfr, session->buffer, XFER_BUFFERSIZE);
+
+ /* build the path to rename to */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 554, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ /* rename the file */
+ rc = rename(rnfr, session->buffer);
+ if(rc != 0)
+ {
+ /* rename failure */
+ console_print(RED "rename: %d %s\n" RESET, errno, strerror(errno));
+ ftp_send_response(session, 550, "failed to rename file/directory\r\n");
+ return;
+ }
+
+ update_free_space();
+ ftp_send_response(session, 250, "OK\r\n");
+}
+
+/*! @fn static void SIZE(ftp_session_t *session, const char *args)
+ *
+ * @brief get file size
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(SIZE)
+{
+ int rc;
+ struct stat st;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* build the path to stat */
+ if(build_path(session, session->cwd, args) != 0)
+ {
+ ftp_send_response(session, 553, "%s\r\n", strerror(errno));
+ return;
+ }
+
+ rc = stat(session->buffer, &st);
+ if(rc != 0 || !S_ISREG(st.st_mode))
+ {
+ ftp_send_response(session, 550, "Could not get file size.\r\n");
+ return;
+ }
+
+ ftp_send_response(session, 213, "%" PRIu64 "\r\n",
+ (uint64_t)st.st_size);
+}
+
+/*! @fn static void STAT(ftp_session_t *session, const char *args)
+ *
+ * @brief get status
+ *
+ * @note If no argument is supplied, and a transfer is occurring, get the
+ * current transfer status. If no argument is supplied, and no transfer
+ * is occurring, get the server status. If an argument is supplied, this
+ * is equivalent to LIST, except the data is sent over the command
+ * socket.
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(STAT)
+{
+ time_t uptime = time(NULL) - start_time;
+ int hours = uptime / 3600;
+ int minutes = (uptime / 60) % 60;
+ int seconds = uptime % 60;
+
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ if(session->state == DATA_CONNECT_STATE)
+ {
+ /* we are waiting to connect to the client */
+ ftp_send_response(session, -211, "FTP server status\r\n"
+ " Waiting for data connection\r\n"
+ "211 End\r\n");
+ return;
+ }
+ else if(session->state == DATA_TRANSFER_STATE)
+ {
+ /* we are in the middle of a transfer */
+ ftp_send_response(session, -211, "FTP server status\r\n"
+ " Transferred %" PRIu64 " bytes\r\n"
+ "211 End\r\n",
+ session->filepos);
+ return;
+ }
+
+ if(strlen(args) == 0)
+ {
+ /* no argument provided, send the server status */
+ ftp_send_response(session, -211, "FTP server status\r\n"
+ " Uptime: %02d:%02d:%02d\r\n"
+ "211 End\r\n",
+ hours, minutes, seconds);
+ return;
+ }
+
+ /* argument provided, open the path in STAT mode */
+ ftp_xfer_dir(session, args, XFER_DIR_STAT, false);
+}
+
+/*! @fn static void STOR(ftp_session_t *session, const char *args)
+ *
+ * @brief store a file
+ *
+ * @note Requires a PASV or PORT connection
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(STOR)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+ snprintf(ftp_file_transfer, sizeof(ftp_file_transfer), "Receiving: %s", args ? args : "");
+ isTransfering = true;
+
+ /* open the file to store */
+ return ftp_xfer_file(session, args, XFER_FILE_STOR);
+}
+
+/*! @fn static void STOU(ftp_session_t *session, const char *args)
+ *
+ * @brief store a unique file
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(STOU)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ /* we do not support this yet */
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ ftp_send_response(session, 502, "unavailable\r\n");
+}
+
+/*! @fn static void STRU(ftp_session_t *session, const char *args)
+ *
+ * @brief set file structure
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(STRU)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* we only support F (no structure) mode */
+ if(strcasecmp(args, "F") == 0)
+ {
+ ftp_send_response(session, 200, "OK\r\n");
+ return;
+ }
+
+ ftp_send_response(session, 504, "unavailable\r\n");
+}
+
+/*! @fn static void SYST(ftp_session_t *session, const char *args)
+ *
+ * @brief identify system
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(SYST)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* we are UNIX compliant with 8-bit characters */
+ ftp_send_response(session, 215, "UNIX Type: L8\r\n");
+}
+
+/*! @fn static void TYPE(ftp_session_t *session, const char *args)
+ *
+ * @brief set transfer mode
+ *
+ * @note transfer mode is always binary
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(TYPE)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* we always transfer in binary mode */
+ ftp_send_response(session, 200, "OK\r\n");
+}
+
+/*! @fn static void USER(ftp_session_t *session, const char *args)
+ *
+ * @brief provide user name
+ *
+ * @param[in] session ftp session
+ * @param[in] args arguments
+ */
+FTP_DECLARE(USER)
+{
+ console_print(CYAN "%s %s\n" RESET, __func__, args ? args : "");
+
+ ftp_session_set_state(session, COMMAND_STATE, 0);
+
+ /* we accept any user name */
+ ftp_send_response(session, 230, "OK\r\n");
+}