From 920cd96c679da15b4194128a933f08a21c6d562b Mon Sep 17 00:00:00 2001 From: Michael Theall Date: Thu, 21 Jan 2016 19:25:22 -0600 Subject: [PATCH] Remove coloring from linux build. Terminate loop when canceling waiting for wifi. Implement ABOR, HELP, and STAT. - STAT (no argument) during a transfer gives the current file's offset. - STAT (no argument) outside of transfer will return server uptime. - STAT (with argument) is the same as LIST but with the data traveling over the command socket. Allow ABOR, STAT, and QUIT to occur during transfer. Support telnet interrupt (discard data up to Data Mark past TCP urgent mark). Support arguments for LIST and STAT. Escape \r in response as per telnet standard. Some clients don't like this but they are out of spec. Unescape \r\0 in request as per telnet standard. Some clients don't properly escape this but they are out of spec. Support commands broken across multiple recv(). Escape quotes on PWD response. Added much more documentation. --- Makefile.3ds | 4 +- Makefile.linux | 2 +- README.md | 9 +- include/console.h | 13 + source/ftp.c | 1375 ++++++++++++++++++++++++++++++++++++++------- source/main.c | 2 + 6 files changed, 1204 insertions(+), 201 deletions(-) diff --git a/Makefile.3ds b/Makefile.3ds index 60ada03..fdccf0f 100644 --- a/Makefile.3ds +++ b/Makefile.3ds @@ -33,7 +33,7 @@ DATA := data INCLUDES := include APP_TITLE := Super ftBRONY II Turbo -APP_DESCRIPTION := v2.1 +APP_DESCRIPTION := v2.2 APP_AUTHOR := mtheall ICON := ftbrony.png @@ -44,7 +44,7 @@ ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard CFLAGS := -g -Wall -O3 -mword-relocations \ $(ARCH) \ - -DSTATUS_STRING="\"ftbrony v2.1\"" + -DSTATUS_STRING="\"ftbrony v2.2\"" CFLAGS += $(INCLUDE) -DARM11 -D_3DS diff --git a/Makefile.linux b/Makefile.linux index 1dc9966..129a8a3 100644 --- a/Makefile.linux +++ b/Makefile.linux @@ -3,7 +3,7 @@ TARGET := $(notdir $(CURDIR)) CFILES := $(wildcard source/*.c) OFILES := $(patsubst source/%,build.linux/%,$(CFILES:.c=.o)) -CFLAGS := -g -Wall -Iinclude -DSTATUS_STRING="\"ftpd v2.1\"" +CFLAGS := -g -Wall -Iinclude -DSTATUS_STRING="\"ftpd v2.2\"" LDFLAGS := .PHONY: all clean diff --git a/README.md b/README.md index 1e9808e..4888e1b 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,14 @@ Copy the `ftbrony.3dsx` file to your SD card and launch it. Supported Commands ------------------ +- ABOR +- ALLO (no-op) - APPE - CDUP - CWD - DELE -- FEAT (no-op) +- FEAT +- HELP - LIST - MKD - MODE (no-op) @@ -42,7 +45,8 @@ Supported Commands - RETR - RMD - RNFR -- RNTO (rename syscall is broken?) +- RNTO +- STAT - STOR - STRU (no-op) - SYST @@ -56,5 +60,4 @@ Supported Commands Planned Commands ---------------- -- ALLO - STOU diff --git a/include/console.h b/include/console.h index 77a5884..7f4156a 100644 --- a/include/console.h +++ b/include/console.h @@ -1,5 +1,6 @@ #pragma once +#ifdef _3DS #define ESC(x) "\x1b[" #x #define RESET ESC(0m) #define BLACK ESC(30m) @@ -10,6 +11,18 @@ #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); diff --git a/source/ftp.c b/source/ftp.c index 9a204b8..612e576 100644 --- a/source/ftp.c +++ b/source/ftp.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #ifdef _3DS #include <3ds.h> @@ -24,12 +26,12 @@ #endif #include "console.h" -#define POLL_UNKNOWN (~(POLLIN|POLLOUT)) +#define POLL_UNKNOWN (~(POLLIN|POLLPRI|POLLOUT)) #define XFER_BUFFERSIZE 32768 #define SOCK_BUFFERSIZE 32768 #define FILE_BUFFERSIZE 65536 -#define CMD_BUFFERSIZE 1024 +#define CMD_BUFFERSIZE 4096 #define SOCU_ALIGN 0x1000 #define SOCU_BUFFERSIZE 0x100000 #define LISTEN_PORT 5000 @@ -42,12 +44,14 @@ typedef struct ftp_session_t ftp_session_t; #define FTP_DECLARE(x) static int 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(MKD); FTP_DECLARE(MODE); @@ -64,6 +68,7 @@ FTP_DECLARE(RETR); FTP_DECLARE(RMD); FTP_DECLARE(RNFR); FTP_DECLARE(RNTO); +FTP_DECLARE(STAT); FTP_DECLARE(STOR); FTP_DECLARE(STOU); FTP_DECLARE(STRU); @@ -96,12 +101,14 @@ typedef enum SESSION_SEND = BIT(4), /*!< data transfer in sink mode */ SESSION_RENAME = BIT(5), /*!< last command was RNFR and buffer contains path */ SESSION_NLST = BIT(6), /*!< list command is NLST */ + SESSION_URGENT = BIT(7), /*!< in telnet urgent mode */ } session_flags_t; /*! ftp session */ struct ftp_session_t { char cwd[4096]; /*!< 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 */ @@ -112,12 +119,14 @@ struct ftp_session_t ftp_session_t *next; /*!< link to next session */ ftp_session_t *prev; /*!< link to prev session */ - int (*transfer)(ftp_session_t*); /*! data transfer callback */ + loop_status_t (*transfer)(ftp_session_t*); /*! data transfer callback */ char buffer[XFER_BUFFERSIZE]; /*! persistent data between callbacks */ char tmp_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 */ @@ -131,16 +140,21 @@ typedef struct ftp_command int (*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(MKD), FTP_COMMAND(MODE), @@ -157,6 +171,7 @@ static ftp_command_t ftp_commands[] = FTP_COMMAND(RMD), FTP_COMMAND(RNFR), FTP_COMMAND(RNTO), + FTP_COMMAND(STAT), FTP_COMMAND(STOR), FTP_COMMAND(STOU), FTP_COMMAND(STRU), @@ -164,6 +179,7 @@ static ftp_command_t ftp_commands[] = 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), @@ -208,6 +224,8 @@ static in_port_t data_port = DATA_PORT; 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 * @@ -236,6 +254,7 @@ ftp_set_socket_nonblocking(int fd) { int rc, flags; + /* get the socket flags */ flags = fcntl(fd, F_GETFL, 0); if(flags == -1) { @@ -243,6 +262,7 @@ ftp_set_socket_nonblocking(int fd) return -1; } + /* add O_NONBLOCK to the socket flags */ rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK); if(rc != 0) { @@ -264,6 +284,7 @@ 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) @@ -272,6 +293,7 @@ ftp_set_socket_options(int fd) return -1; } + /* increase send buffer size */ rc = setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sock_buffersize, sizeof(sock_buffersize)); if(rc != 0) @@ -365,12 +387,13 @@ ftp_session_close_pasv(ftp_session_t *session) /*! close data socket on ftp session * - * @param[in] session 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) + if(session->data_fd >= 0 && session->data_fd != session->cmd_fd) ftp_closesocket(session->data_fd, true); session->data_fd = -1; @@ -510,8 +533,10 @@ ftp_session_open_file_write(ftp_session_t *session, 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) { @@ -618,6 +643,12 @@ ftp_session_set_state(ftp_session_t *session, } } +/*! 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) { @@ -628,43 +659,99 @@ ftp_session_transfer(ftp_session_t *session) } while(rc == 0); } -__attribute__((format(printf,3,4))) -/*! send ftp response to ftp session's peer +/*! escape a buffer * - * @param[in] session ftp session - * @param[in] code response code - * @param[in] fmt format string - * @param[in] ... format arguments + * @param[in] buffer buffer to escape + * @param[in,out] len buffer length + * @param[in] quotes whether to escape quotes * - * returns bytes send to peer + * @returns escaped buffer + * + * @note The caller must free the returned buffer */ -static ssize_t -ftp_send_response(ftp_session_t *session, - int code, - const char *fmt, ...) +static char* +escape_buffer(const char *buffer, + size_t *len, + bool quotes) { - static char buffer[CMD_BUFFERSIZE]; - ssize_t rc, to_send; - va_list ap; + size_t i, diff = 0; + char *out, *p = (char*)buffer; - /* print response code and message to buffer */ - va_start(ap, fmt); - if(code != 211) - 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)) + /* check for \r that needs to be escaped */ + do { - /* couldn't fit message; just send code */ - console_print(RED "%s: buffersize too small\n" RESET, __func__); - rc = sprintf(buffer, "%d\r\n", code); + p = memchr(p, '\r', buffer + *len - p); + if(p != NULL) + { + ++p; + ++diff; + } + } while(p != NULL); + + if(quotes) + { + /* check for " that needs to be escaped */ + p = (char*)buffer; + do + { + p = memchr(p, '"', buffer + *len - p); + if(p != NULL) + { + ++p; + ++diff; + } + } while(p != NULL); } + /* check if an escape was needed */ + if(diff == 0) + return strdup(buffer); + + /* escape \r */ + p = out = (char*)malloc(*len + diff); + if(out == NULL) + return NULL; + + /* copy the buffer while performing escapes */ + for(i = 0; i < *len; ++i) + { + if(*buffer == '\r') + { + /* escaped \r is \r\0 */ + *p++ = *buffer++; + *p++ = 0; + } + else if(quotes && *buffer == '"') + { + /* escaped " is "" */ + *p++ = *buffer++; + *p++ = '"'; + } + else + *p++ = *buffer++; + } + + *len += diff; + return out; +} + +/*! send a response on the command socket + * + * @param[in] session ftp session + * @param[in] buffer buffer to send + * @param[in] len buffer length + * + * @returns bytes sent to peer + */ +static ssize_t +ftp_send_response_buffer(ftp_session_t *session, + const char *buffer, + size_t len) +{ + ssize_t rc, to_send; + /* send response */ - to_send = rc; + to_send = len; console_print(GREEN "%s" RESET, buffer); rc = send(session->cmd_fd, buffer, to_send, 0); if(rc < 0) @@ -676,9 +763,52 @@ ftp_send_response(ftp_session_t *session, return rc; } +__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 + * + * returns bytes sent to peer + */ +static ssize_t +ftp_send_response(ftp_session_t *session, + int code, + const char *fmt, ...) +{ + static char buffer[CMD_BUFFERSIZE]; + ssize_t rc; + va_list ap; + + /* 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); + } + + return 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) @@ -736,6 +866,7 @@ ftp_session_new(int listen_fd) memset(host, 0, sizeof(host)); memset(serv, 0, sizeof(serv)); + /* reverse dns lookup */ rc = getnameinfo((struct sockaddr*)&addr, addrlen, host, 15, serv, 2, 0); if(rc != 0) @@ -825,6 +956,7 @@ ftp_session_accept(ftp_session_t *session) return -1; } + /* set the socket to non-blocking */ rc = ftp_set_socket_nonblocking(new_fd); if(rc != 0) { @@ -837,6 +969,7 @@ ftp_session_accept(ftp_session_t *session) 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; @@ -892,6 +1025,7 @@ ftp_session_connect(ftp_session_t *session) return -1; } + /* set socket to non-blocking */ rc = ftp_set_socket_nonblocking(session->data_fd); if(rc != 0) return -1; @@ -903,22 +1037,115 @@ ftp_session_connect(ftp_session_t *session) return 0; } -/*! read command for ftp session +/*! unescape a command * * @param[in] session ftp session */ static void -ftp_session_read_command(ftp_session_t *session) +ftp_session_unescape_command(ftp_session_t *session) { - static char buffer[CMD_BUFFERSIZE]; + size_t in, out; + size_t diff = 0; + + /* escape \r\0 from the first command */ + for(in = out = 0; in < session->cmd_buffersize && session->cmd_buffer[in] != 0; ++in) + { + if(session->cmd_buffer[in] == '\r' + && in < session->cmd_buffersize - 1 + && session->cmd_buffer[in+1] == 0) + { + /* this is an escaped \r */ + session->cmd_buffer[out++] = session->cmd_buffer[in++]; + ++diff; + } + else + { + session->cmd_buffer[out++] = session->cmd_buffer[in]; + } + } + + /* copy remaining buffer */ + if(diff > 0) + memmove(session->cmd_buffer + out, session->cmd_buffer + in, session->cmd_buffersize - in); + + /* adjust the buffer size */ + session->cmd_buffersize -= diff; +} + +/*! 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; - char *args; ftp_command_t key, *command; - memset(buffer, 0, sizeof(buffer)); + /* check out-of-band data */ + if(events & POLLPRI) + { + session->flags |= SESSION_URGENT; - /* retrieve command */ - rc = recv(session->cmd_fd, buffer, sizeof(buffer), 0); + /* 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 */ @@ -934,39 +1161,99 @@ ftp_session_read_command(ftp_session_t *session) } else { - /* split into command and arguments */ - /* TODO: support partial transfers */ - buffer[sizeof(buffer)-1] = 0; + session->cmd_buffersize += rc; + len = sizeof(session->cmd_buffer) - session->cmd_buffersize; - args = buffer; - while(*args && *args != '\r' && *args != '\n') - ++args; - *args = 0; - - args = 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); - - /* execute the command */ - if(command == NULL) + if(session->flags & SESSION_URGENT) { - ftp_send_response(session, 502, "invalid command -> %s %s\r\n", - key.name, args); + /* 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; + } + } } - else + + /* loop through commands */ + while(true) { - /* clear RENAME flag for all commands except RNTO */ - if(strcasecmp(command->name, "RNTO") != 0) - session->flags &= ~SESSION_RENAME; - command->handler(session, args); + /* must have at least enough data for the delimiter */ + if(session->cmd_buffersize < 2) + return; + + /* look for \r\n delimiter */ + for(i = 0; i < session->cmd_buffersize-1; ++i) + { + if(session->cmd_buffer[i] == '\r' + && session->cmd_buffer[i+1] == '\n') + { + /* we found a delimiter */ + session->cmd_buffer[i] = 0; + next = &session->cmd_buffer[i+2]; + break; + } + } + + /* check if a delimiter was found */ + if(i == session->cmd_buffersize-1) + return; + + /* unescape the command */ + ftp_session_unescape_command(session); + + /* 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); + + /* execute the command */ + if(command == NULL) + { + ftp_send_response(session, 502, "invalid command\r\n"); + } + 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; } } } @@ -981,37 +1268,42 @@ static ftp_session_t* ftp_session_poll(ftp_session_t *session) { int rc; - struct pollfd pollinfo; + 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 */ - pollinfo.fd = session->cmd_fd; - pollinfo.events = POLLIN; - pollinfo.revents = 0; break; case DATA_CONNECT_STATE: /* we are waiting for a PASV connection */ - pollinfo.fd = session->pasv_fd; - pollinfo.events = POLLIN; - pollinfo.revents = 0; + pollinfo[1].fd = session->pasv_fd; + pollinfo[1].events = POLLIN; + pollinfo[1].revents = 0; + nfds = 2; break; case DATA_TRANSFER_STATE: /* we need to transfer data */ - pollinfo.fd = session->data_fd; + pollinfo[1].fd = session->data_fd; if(session->flags & SESSION_RECV) - pollinfo.events = POLLIN; + pollinfo[1].events = POLLIN; else - pollinfo.events = POLLOUT; - pollinfo.revents = 0; + pollinfo[1].events = POLLOUT; + pollinfo[1].revents = 0; + nfds = 2; break; } - /* poll the selected socket */ - rc = poll(&pollinfo, 1, 0); + /* poll the selected sockets */ + rc = poll(pollinfo, nfds, 0); if(rc < 0) { console_print(RED "poll: %d %s\n" RESET, errno, strerror(errno)); @@ -1019,33 +1311,40 @@ ftp_session_poll(ftp_session_t *session) } else if(rc > 0) { - if(pollinfo.revents != 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)) + 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) { - /* handle event */ switch(session->state) { case COMMAND_STATE: - if(pollinfo.revents & POLL_UNKNOWN) - console_print(YELLOW "cmd_fd: revents=0x%08X\n" RESET, pollinfo.revents); - - /* we need to read a new command */ - if(pollinfo.revents & (POLLERR|POLLHUP)) - ftp_session_close_cmd(session); - else if(pollinfo.revents & POLLIN) - ftp_session_read_command(session); + /* this shouldn't happen? */ break; case DATA_CONNECT_STATE: - if(pollinfo.revents & POLL_UNKNOWN) - console_print(YELLOW "pasv_fd: revents=0x%08X\n" RESET, pollinfo.revents); + 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.revents & (POLLERR|POLLHUP)) + 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.revents & POLLIN) + else if(pollinfo[1].revents & POLLIN) { if(ftp_session_accept(session) != 0) ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); @@ -1053,16 +1352,16 @@ ftp_session_poll(ftp_session_t *session) break; case DATA_TRANSFER_STATE: - if(pollinfo.revents & POLL_UNKNOWN) - console_print(YELLOW "data_fd: revents=0x%08X\n" RESET, pollinfo.revents); + 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.revents & (POLLERR|POLLHUP)) + 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.revents & (POLLIN|POLLOUT)) + else if(pollinfo[1].revents & (POLLIN|POLLOUT)) ftp_session_transfer(session); break; } @@ -1083,6 +1382,8 @@ ftp_init(void) { int rc; + start_time = time(NULL); + #ifdef _3DS Result ret = 0; u32 wifi = 0; @@ -1093,21 +1394,27 @@ ftp_init(void) /* 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; } + /* 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; @@ -1286,6 +1593,10 @@ ftp_exit(void) #endif } +/*! ftp look + * + * @returns whether to keep looping + */ loop_status_t ftp_loop(void) { @@ -1293,13 +1604,16 @@ ftp_loop(void) 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; @@ -1310,6 +1624,7 @@ ftp_loop(void) { if(pollinfo.revents & POLLIN) { + /* we got a new client */ ftp_session_new(listenfd); } else @@ -1318,11 +1633,13 @@ ftp_loop(void) } } + /* poll each session */ session = sessions; while(session != NULL) session = ftp_session_poll(session); #ifdef _3DS + /* check if the user wants to exit */ hidScanInput(); if(hidKeysDown() & KEY_B) return LOOP_EXIT; @@ -1331,11 +1648,16 @@ ftp_loop(void) 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 == '/') @@ -1346,6 +1668,10 @@ cd_up(ftp_session_t *session) strcat(session->cwd, "/"); } +/*! validate a path + * + * @param[in] args path to validate + */ static int validate_path(const char *args) { @@ -1366,8 +1692,19 @@ validate_path(const char *args) 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; @@ -1375,6 +1712,7 @@ build_path(ftp_session_t *session, memset(session->buffer, 0, sizeof(session->buffer)); + /* make sure the input is a valid path */ if(validate_path(args) != 0) { errno = EINVAL; @@ -1383,6 +1721,7 @@ build_path(ftp_session_t *session, if(args[0] == '/') { + /* this is an absolute path */ if(strlen(args) > sizeof(session->buffer)-1) { errno = ENAMETOOLONG; @@ -1392,12 +1731,13 @@ build_path(ftp_session_t *session, } else { - if(strcmp(session->cwd, "/") == 0) + /* 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", - session->cwd, args); + cwd, args); if(rc >= sizeof(session->buffer)) { @@ -1406,49 +1746,90 @@ build_path(ftp_session_t *session, } } + /* remove trailing / */ p = session->buffer + strlen(session->buffer); while(p > session->buffer && *--p == '/') *p = 0; + /* if we ended with an empty path, it is the root directory */ if(strlen(session->buffer) == 0) strcpy(session->buffer, "/"); return 0; } -static int +/*! 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; + 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) { - struct stat st; - struct dirent *dent = readdir(session->dp); - if(dent == NULL) + /* check if this was a STAT */ + if(session->data_fd == session->cmd_fd) + rc = 213; + else + rc = 226; + + /* 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, 226, "OK\r\n"); - return -1; + ftp_send_response(session, rc, "OK\r\n"); + return LOOP_EXIT; } - if(strcmp(dent->d_name, ".") == 0 || strcmp(dent->d_name, "..") == 0) - return 0; + /* 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; + } - if(strcmp(session->cwd, "/") == 0) - snprintf(session->buffer, sizeof(session->buffer), - "/%s", dent->d_name); - else - snprintf(session->buffer, sizeof(session->buffer), - "%s/%s", session->cwd, dent->d_name); + /* 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->flags & SESSION_NLST) { - session->buffersize = - sprintf(session->buffer, "%s\r\n", dent->d_name); + /* NLST gives the whole path name */ + session->buffersize = 0; + if(build_path(session, session->lwd, dent->d_name) == 0) + { + /* escape \r as per telnet standard */ + len = strlen(session->buffer); + buffer = escape_buffer(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 */ sdmc_dir_t *dir = (sdmc_dir_t*)session->dp->dirData->dirStruct; if(dir->entry_data.attributes & FS_ATTRIBUTE_DIRECTORY) @@ -1458,35 +1839,64 @@ list_transfer(ftp_session_t *session) st.st_size = dir->entry_data.fileSize; #else - rc = lstat(session->buffer, &st); + /* 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) { - console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); + /* an error occurred */ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); ftp_send_response(session, 550, "unavailable\r\n"); - return -1; + return LOOP_EXIT; } #endif - session->buffersize = - sprintf(session->buffer, - "%crwxrwxrwx 1 3DS 3DS %llu Jan 1 1970 %s\r\n", - S_ISDIR(st.st_mode) ? 'd' : - S_ISLNK(st.st_mode) ? 'l' : '-', - (unsigned long long)st.st_size, - dent->d_name); + /* escape \r as per telnet standard */ + len = strlen(dent->d_name); + buffer = escape_buffer(dent->d_name, &len, false); + if(buffer != NULL) + { + /* copy to the session buffer to send */ + session->buffersize = + sprintf(session->buffer, + "%crwxrwxrwx 1 3DS 3DS %llu Jan 1 1970 ", + S_ISDIR(st.st_mode) ? 'd' : + S_ISLNK(st.st_mode) ? 'l' : '-', + (unsigned long long)st.st_size); + if(session->buffersize + len + 2 > sizeof(session->buffer)) + { + /* buffer will overflow */ + free(buffer); + ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); + ftp_send_response(session, 425, "%s\r\n", strerror(EOVERFLOW)); + return LOOP_EXIT; + } + memcpy(session->buffer+session->buffersize, buffer, len); + free(buffer); + len = session->buffersize + len; + session->buffer[len++] = '\r'; + session->buffer[len++] = '\n'; + session->buffersize = len; + } + 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 -1; + return LOOP_EXIT; console_print(RED "send: %d %s\n" RESET, errno, strerror(errno)); } else @@ -1494,43 +1904,55 @@ list_transfer(ftp_session_t *session) ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); ftp_send_response(session, 426, "Connection broken during transfer\r\n"); - return -1; + return LOOP_EXIT; } + /* we can try to send more data */ session->bufferpos += rc; - return 0; + return LOOP_CONTINUE; } -static int +/*! 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"); - return -1; + 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 -1; + return LOOP_EXIT; console_print(RED "send: %d %s\n" RESET, errno, strerror(errno)); } else @@ -1538,27 +1960,36 @@ retrieve_transfer(ftp_session_t *session) ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); ftp_send_response(session, 426, "Connection broken during transfer\r\n"); - return -1; + return LOOP_EXIT; } + /* we can try to send more data */ session->bufferpos += rc; - return 0; + return LOOP_CONTINUE; } -static int +/*! 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 -1; + return LOOP_EXIT; console_print(RED "recv: %d %s\n" RESET, errno, strerror(errno)); } @@ -1568,9 +1999,10 @@ store_transfer(ftp_session_t *session) ftp_send_response(session, 226, "OK\r\n"); else ftp_send_response(session, 426, "Connection broken during transfer\r\n"); - return -1; + return LOOP_EXIT; } + /* we received some data so reset the session buffer to write */ session->bufferpos = 0; session->buffersize = rc; } @@ -1578,13 +2010,15 @@ store_transfer(ftp_session_t *session) 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 -1; + return LOOP_EXIT; } + /* we can try to receive more data */ session->bufferpos += rc; - return 0; + return LOOP_CONTINUE; } /*! ftp_xfer_file mode */ @@ -1610,13 +2044,15 @@ ftp_xfer_file(ftp_session_t *session, { int rc; - if(build_path(session, args) != 0) + /* 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); return ftp_send_response(session, 553, "%s\r\n", strerror(rc)); } + /* open the file for retrieving or storing */ if(mode == XFER_FILE_RETR) rc = ftp_session_open_file_read(session); else @@ -1624,22 +2060,25 @@ ftp_xfer_file(ftp_session_t *session, if(rc != 0) { + /* error opening the file */ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); return ftp_send_response(session, 450, "failed to open file\r\n"); } if(session->flags & SESSION_PORT) { + /* connect to the client */ ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV); rc = ftp_session_connect(session); if(rc != 0) { + /* error connecting */ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); return ftp_send_response(session, 425, "can't open data connection\r\n"); } + /* set up the transfer */ session->flags &= ~(SESSION_RECV|SESSION_SEND); - if(mode == XFER_FILE_RETR) { session->flags |= SESSION_SEND; @@ -1658,8 +2097,8 @@ ftp_xfer_file(ftp_session_t *session, } else if(session->flags & SESSION_PASV) { + /* set up the transfer */ session->flags &= ~(SESSION_RECV|SESSION_SEND); - if(mode == XFER_FILE_RETR) { session->flags |= SESSION_SEND; @@ -1687,6 +2126,7 @@ typedef enum { XFER_DIR_LIST, /*!< Long list */ XFER_DIR_NLST, /*!< Short list */ + XFER_DIR_STAT, /*!< Stat command */ } xfer_dir_mode_t; /*! Transfer a directory @@ -1702,16 +2142,99 @@ ftp_xfer_dir(ftp_session_t *session, const char *args, xfer_dir_mode_t mode) { - ssize_t rc; + ssize_t rc; + size_t len; + struct stat st; + const char *base; + char *buffer; - if(ftp_session_open_cwd(session) != 0) + /* set up the transfer */ + session->flags &= ~(SESSION_RECV|SESSION_NLST); + 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); + return ftp_send_response(session, 550, "%s\r\n", strerror(errno)); + } + + args = session->buffer; + + /* check if this is a directory */ + session->dp = opendir(args); + if(session->dp == NULL) + { + /* not a directory; check if it is a file */ + rc = stat(args, &st); + if(rc != 0) + { + /* error getting stat */ + ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); + return ftp_send_response(session, 550, "%s\r\n", strerror(errno)); + } + else + { + /* get the base name */ + base = strrchr(args, '/') + 1; + + /* escape \r as per telnet standard */ + len = strlen(base); + buffer = escape_buffer(base, &len, false); + if(buffer != NULL) + { + /* copy to the session buffer to send */ + session->buffersize = + sprintf(session->buffer, + "-rwxrwxrwx 1 3DS 3DS %llu Jan 1 1970 ", + (unsigned long long)st.st_size); + if(session->buffersize + len + 2 > sizeof(session->buffer)) + { + /* buffer will overflow */ + free(buffer); + ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); + ftp_send_response(session, 425, "%s\r\n", strerror(EOVERFLOW)); + return LOOP_EXIT; + } + memcpy(session->buffer + session->buffersize, buffer, len); + free(buffer); + len = session->buffersize + len; + session->buffer[len++] = '\r'; + session->buffer[len++] = '\n'; + session->buffersize = len; + } + else + session->buffersize = 0; + } + } + else + { + /* it was a directory, so set it as the lwd */ + strcpy(session->lwd, session->buffer); + } + } + 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); - return ftp_send_response(session, 550, "unavailable\r\n"); + return ftp_send_response(session, 550, "%s\r\n", strerror(errno)); + } + else + { + /* set the lwd as the cwd */ + strcpy(session->lwd, session->cwd); } if(session->flags & SESSION_PORT) { + /* connect to the client */ ftp_session_set_state(session, DATA_TRANSFER_STATE, CLOSE_PASV); rc = ftp_session_connect(session); if(rc != 0) @@ -1720,38 +2243,41 @@ ftp_xfer_dir(ftp_session_t *session, return ftp_send_response(session, 425, "can't open data connection\r\n"); } - session->flags &= ~(SESSION_RECV|SESSION_SEND); - session->flags |= SESSION_SEND; - - if(mode == XFER_DIR_LIST) - session->flags &= ~SESSION_NLST; - else + /* set up the transfer */ + if(mode == XFER_DIR_NLST) session->flags |= SESSION_NLST; - - session->transfer = list_transfer; - session->bufferpos = 0; - session->buffersize = 0; + else if(mode != XFER_DIR_LIST) + { + ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); + return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + } return ftp_send_response(session, 150, "Ready\r\n"); } else if(session->flags & SESSION_PASV) { - session->flags &= ~(SESSION_RECV|SESSION_SEND); - session->flags |= SESSION_SEND; - - if(mode == XFER_DIR_LIST) - session->flags &= ~SESSION_NLST; - else + /* set up the transfer */ + if(mode == XFER_DIR_NLST) session->flags |= SESSION_NLST; - - session->transfer = list_transfer; - session->bufferpos = 0; - session->buffersize = 0; + else if(mode != XFER_DIR_LIST) + { + ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); + return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + } ftp_session_set_state(session, DATA_CONNECT_STATE, CLOSE_DATA); return 0; } + else if(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; + return ftp_send_response(session, -213, "Status\r\n"); + } + /* we must have got LIST or NLST without a preceding PORT or PASV */ ftp_session_set_state(session, COMMAND_STATE, CLOSE_PASV | CLOSE_DATA); return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); } @@ -1762,6 +2288,41 @@ ftp_xfer_dir(ftp_session_t *session, * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/*! @fn static int ABOR(ftp_session_t *session, const char *args) + * + * @brief abort a transfer + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ +FTP_DECLARE(ABOR) +{ + console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); + + if(session->state == COMMAND_STATE) + return ftp_send_response(session, 225, "No transfer to abort\r\n"); + + /* 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 */ + return ftp_send_response(session, 425, "Transfer aborted\r\n"); +} + +/*! @fn static int ALLO(ftp_session_t *session, const char *args) + * + * @brief allocate space + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(ALLO) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); @@ -1771,59 +2332,102 @@ FTP_DECLARE(ALLO) return ftp_send_response(session, 202, "superfluous command\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(APPE) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); + /* open the file in append mode */ return ftp_xfer_file(session, args, XFER_FILE_APPE); } +/*! @fn static int CDUP(ftp_session_t *session, const char *args) + * + * @brief CWD to parent directory + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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); return ftp_send_response(session, 200, "OK\r\n"); } +/*! @fn static int CWD(ftp_session_t *session, const char *args) + * + * @brief change working directory + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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); return ftp_send_response(session, 200, "OK\r\n"); } - if(build_path(session, args) != 0) + /* build the new cwd path */ + if(build_path(session, session->cwd, args) != 0) return ftp_send_response(session, 553, "%s\r\n", strerror(errno)); + /* get the path status */ + rc = stat(session->buffer, &st); + if(rc != 0) { - struct stat st; - int rc; - - rc = stat(session->buffer, &st); - if(rc != 0) - { - console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); - return ftp_send_response(session, 550, "unavailable\r\n"); - } - - if(!S_ISDIR(st.st_mode)) - return ftp_send_response(session, 553, "not a directory\r\n"); + console_print(RED "stat '%s': %d %s\n" RESET, session->buffer, errno, strerror(errno)); + return ftp_send_response(session, 550, "unavailable\r\n"); } + /* make sure it is a directory */ + if(!S_ISDIR(st.st_mode)) + return ftp_send_response(session, 553, "not a directory\r\n"); + + /* copy the path into the cwd */ strncpy(session->cwd, session->buffer, sizeof(session->cwd)); return ftp_send_response(session, 200, "OK\r\n"); } +/*! @fn static int DELE(ftp_session_t *session, const char *args) + * + * @brief delete a file + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(DELE) { int rc; @@ -1832,12 +2436,15 @@ FTP_DECLARE(DELE) ftp_session_set_state(session, COMMAND_STATE, 0); - if(build_path(session, args) != 0) + /* build the file path */ + if(build_path(session, session->cwd, args) != 0) return ftp_send_response(session, 553, "%s\r\n", strerror(errno)); + /* 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)); return ftp_send_response(session, 550, "failed to delete file\r\n"); } @@ -1845,22 +2452,77 @@ FTP_DECLARE(DELE) return ftp_send_response(session, 250, "OK\r\n"); } +/*! @fn static int FEAT(ftp_session_t *session, const char *args) + * + * @brief list server features + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(FEAT) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); ftp_session_set_state(session, COMMAND_STATE, 0); - return ftp_send_response(session, 211, "\r\n UTF8\r\n211 End\r\n"); + /* our only feature is UTF-8 */ + return ftp_send_response(session, -211, "\r\n UTF8\r\n211 End\r\n"); } +/*! @fn static int HELP(ftp_session_t *session, const char *args) + * + * @brief print server help + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ +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 */ + return ftp_send_response(session, -214, + "The following commands are recognized\r\n" + " ABOR ALLO APPE CDUP CWD DELE FEAT HELP LIST MKD MODE NLST NOOP OPTS\r\n" + " PASS PASV PORT PWD QUIT REST RETR RMD RNFR RNTO STAT STOR STOU STRU\r\n" + " SYST TYPE USER XCUP XCWD XMKD XPWD XRMD\r\n" + "214 End\r\n"); +} + +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(LIST) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); + /* open the path in LIST mode */ return ftp_xfer_dir(session, args, XFER_DIR_LIST); } +/*! @fn static int MKD(ftp_session_t *session, const char *args) + * + * @brief create a directory + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(MKD) { int rc; @@ -1869,12 +2531,15 @@ FTP_DECLARE(MKD) ftp_session_set_state(session, COMMAND_STATE, 0); - if(build_path(session, args) != 0) + /* build the path */ + if(build_path(session, session->cwd, args) != 0) return ftp_send_response(session, 553, "%s\r\n", strerror(errno)); + /* 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)); return ftp_send_response(session, 550, "failed to create directory\r\n"); } @@ -1882,37 +2547,80 @@ FTP_DECLARE(MKD) return ftp_send_response(session, 250, "OK\r\n"); } +/*! @fn static int MODE(ftp_session_t *session, const char *args) + * + * @brief set transfer mode + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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) return ftp_send_response(session, 200, "OK\r\n"); return ftp_send_response(session, 504, "unavailable\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ 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); } +/*! @fn static int NOOP(ftp_session_t *session, const char *args) + * + * @brief no-op + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(NOOP) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); + + /* this is a no-op */ return ftp_send_response(session, 200, "OK\r\n"); } +/*! @fn static int OPTS(ftp_session_t *session, const char *args) + * + * @brief set options + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(OPTS) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); ftp_session_set_state(session, COMMAND_STATE, 0); + /* we only accept the following options */ if(strcasecmp(args, "UTF8") == 0 || strcasecmp(args, "UTF8 ON") == 0 || strcasecmp(args, "UTF8 NLST") == 0) @@ -1921,15 +2629,34 @@ FTP_DECLARE(OPTS) return ftp_send_response(session, 504, "invalid argument\r\n"); } +/*! @fn static int PASS(ftp_session_t *session, const char *args) + * + * @brief provide password + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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); return ftp_send_response(session, 230, "OK\r\n"); } +/*! @fn static int PASV(ftp_session_t *session, const char *args) + * + * @brief request an address to connect to + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(PASV) { int rc; @@ -1941,10 +2668,11 @@ FTP_DECLARE(PASV) 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) { @@ -1952,13 +2680,16 @@ FTP_DECLARE(PASV) return ftp_send_response(session, 451, "\r\n"); } + /* 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); return ftp_send_response(session, 451, "\r\n"); } + /* grab a new port */ session->pasv_addr.sin_port = htons(next_data_port()); #ifdef _3DS @@ -1966,18 +2697,23 @@ FTP_DECLARE(PASV) 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); return ftp_send_response(session, 451, "\r\n"); } + /* 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); return ftp_send_response(session, 451, "\r\n"); @@ -1985,11 +2721,13 @@ FTP_DECLARE(PASV) #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); return ftp_send_response(session, 451, "\r\n"); @@ -1997,12 +2735,13 @@ FTP_DECLARE(PASV) } #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", @@ -2016,6 +2755,15 @@ FTP_DECLARE(PASV) return ftp_send_response(session, 227, "%s\r\n", buffer); } +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(PORT) { char *addrstr, *p, *portstr; @@ -2026,14 +2774,16 @@ FTP_DECLARE(PORT) 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) return ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM)); + /* replace a,b,c,d,e,f with a.b.c.d\0e.f */ for(p = addrstr; *p; ++p) { if(*p == ',') @@ -2049,12 +2799,14 @@ FTP_DECLARE(PORT) } } + /* make sure we got the right number of values */ if(commas != 5) { free(addrstr); return ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); } + /* parse the address */ rc = inet_aton(addrstr, &addr.sin_addr); if(rc == 0) { @@ -2062,6 +2814,7 @@ FTP_DECLARE(PORT) return ftp_send_response(session, 501, "%s\r\n", strerror(EINVAL)); } + /* parse the port */ val = 0; port = 0; for(p = portstr; *p; ++p) @@ -2084,6 +2837,7 @@ FTP_DECLARE(PORT) } } + /* validate the port */ if(val > 0xFF || port > 0xFF) { free(addrstr); @@ -2092,6 +2846,7 @@ FTP_DECLARE(PORT) port <<= 8; port += val; + /* fill in the address port and family */ addr.sin_family = AF_INET; addr.sin_port = htons(port); @@ -2099,30 +2854,87 @@ FTP_DECLARE(PORT) memcpy(&session->peer_addr, &addr, sizeof(addr)); + /* we are ready to connect to the client */ session->flags |= SESSION_PORT; - return ftp_send_response(session, 200, "OK\r\n"); } +/*! @fn static int PWD(ftp_session_t *session, const char *args) + * + * @brief print working directory + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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); - return ftp_send_response(session, 257, "\"%s\"\r\n", session->cwd); + /* escape the cwd */ + len = strlen(session->cwd); + path = escape_buffer(session->cwd, &len, true); + if(path != NULL) + { + i = sprintf(buffer, "256 \""); + 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"); + return ftp_send_response(session, 425, "%s\r\n", strerror(EOVERFLOW)); + } + memcpy(buffer+i, path, len); + free(path); + len += i; + buffer[len++] = '"'; + buffer[len++] = '\r'; + buffer[len++] = '\n'; + + return ftp_send_response_buffer(session, buffer, len); + } + return ftp_send_response(session, 425, "%s\r\n", strerror(ENOMEM)); } +/*! @fn static int QUIT(ftp_session_t *session, const char *args) + * + * @brief terminate ftp session + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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); return 0; } +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(REST) { const char *p; @@ -2132,9 +2944,11 @@ FTP_DECLARE(REST) ftp_session_set_state(session, COMMAND_STATE, 0); + /* make sure an argument is provided */ if(args == NULL) return ftp_send_response(session, 504, "invalid argument\r\n"); + /* parse the offset */ for(p = args; *p; ++p) { if(!isdigit((int)*p)) @@ -2151,18 +2965,39 @@ FTP_DECLARE(REST) pos += (*p - '0'); } + /* set the restart offset */ session->filepos = pos; - return ftp_send_response(session, 200, "OK\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(RETR) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); + /* open the file to retrieve */ return ftp_xfer_file(session, args, XFER_FILE_RETR); } +/*! @fn static int RMD(ftp_session_t *session, const char *args) + * + * @brief remove a directory + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ FTP_DECLARE(RMD) { int rc; @@ -2171,12 +3006,15 @@ FTP_DECLARE(RMD) ftp_session_set_state(session, COMMAND_STATE, 0); - if(build_path(session, args) != 0) + /* build the path to remove */ + if(build_path(session, session->cwd, args) != 0) return ftp_send_response(session, 553, "%s\r\n", strerror(errno)); + /* remove the directory */ rc = rmdir(session->buffer); if(rc != 0) { + /* rmdir error */ console_print(RED "rmdir: %d %s\n" RESET, errno, strerror(errno)); return ftp_send_response(session, 550, "failed to delete directory\r\n"); } @@ -2184,6 +3022,17 @@ FTP_DECLARE(RMD) return ftp_send_response(session, 250, "OK\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(RNFR) { int rc; @@ -2192,21 +3041,35 @@ FTP_DECLARE(RNFR) ftp_session_set_state(session, COMMAND_STATE, 0); - if(build_path(session, args) != 0) + /* build the path to rename from */ + if(build_path(session, session->cwd, args) != 0) return ftp_send_response(session, 553, "%s\r\n", strerror(errno)); + /* 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)); return ftp_send_response(session, 450, "no such file or directory\r\n"); } + /* we are ready for RNTO */ session->flags |= SESSION_RENAME; - return ftp_send_response(session, 350, "OK\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(RNTO) { int rc; @@ -2215,19 +3078,25 @@ FTP_DECLARE(RNTO) ftp_session_set_state(session, COMMAND_STATE, 0); + /* make sure the previous command was RNFR */ if(!(session->flags & SESSION_RENAME)) return ftp_send_response(session, 503, "Bad sequence of commands\r\n"); + /* clear the rename state */ session->flags &= ~SESSION_RENAME; + /* copy the RNFR path */ memcpy(session->tmp_buffer, session->buffer, XFER_BUFFERSIZE); - if(build_path(session, args) != 0) + /* build the path to rename to */ + if(build_path(session, session->cwd, args) != 0) return ftp_send_response(session, 554, "%s\r\n", strerror(errno)); + /* rename the file */ rc = rename(session->tmp_buffer, session->buffer); if(rc != 0) { + /* rename failure */ console_print(RED "rename: %d %s\n" RESET, errno, strerror(errno)); return ftp_send_response(session, 550, "failed to rename file/directory\r\n"); } @@ -2235,58 +3104,174 @@ FTP_DECLARE(RNTO) return ftp_send_response(session, 250, "OK\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ +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 */ + return ftp_send_response(session, -211, "FTP server status\r\n" + " Waiting for data connection\r\n" + "211 End\r\n"); + } + else if(session->state == DATA_TRANSFER_STATE) + { + /* we are in the middle of a transfer */ + return ftp_send_response(session, -211, "FTP server status\r\n" + " Transferred %" PRIu64 " bytes\r\n" + "211 End\r\n", + session->filepos); + } + + if(strlen(args) == 0) + { + /* no argument provided, send the server status */ + return ftp_send_response(session, -211, "FTP server status\r\n" + " Uptime: %02d:%02d:%02d\r\n" + "211 End\r\n", + hours, minutes, seconds); + } + + /* argument provided, open the path in STAT mode */ + return ftp_xfer_dir(session, args, XFER_DIR_STAT); +} + +/*! @fn static int 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 + * + * @returns error + */ FTP_DECLARE(STOR) { console_print(CYAN "%s %s\n" RESET, __func__, args ? args : ""); + /* open the file to store */ return ftp_xfer_file(session, args, XFER_FILE_STOR); } - +/*! @fn static int STOU(ftp_session_t *session, const char *args) + * + * @brief store a unique file + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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); return ftp_send_response(session, 502, "unavailable\r\n"); } +/*! @fn static int STRU(ftp_session_t *session, const char *args) + * + * @brief set file structure + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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) return ftp_send_response(session, 200, "OK\r\n"); return ftp_send_response(session, 504, "unavailable\r\n"); } +/*! @fn static int SYST(ftp_session_t *session, const char *args) + * + * @brief identify system + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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 */ return ftp_send_response(session, 215, "UNIX Type: L8\r\n"); } +/*! @fn static int 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 + * + * @returns error + */ 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 */ return ftp_send_response(session, 200, "OK\r\n"); } +/*! @fn static int USER(ftp_session_t *session, const char *args) + * + * @brief provide user name + * + * @param[in] session ftp session + * @param[in] args arguments + * + * @returns error + */ 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 */ return ftp_send_response(session, 230, "OK\r\n"); } diff --git a/source/main.c b/source/main.c index 241ba9e..46c76ea 100644 --- a/source/main.c +++ b/source/main.c @@ -93,6 +93,8 @@ main(int argc, /* done with ftp */ ftp_exit(); } + else + status = LOOP_EXIT; } console_print("Press B to exit\n");