Makefile.flags: restrict Wno-constant-logical-operand and Wno-string-plus-int options...
[oweals/busybox.git] / networking / ftpd.c
index ab7308be9a00122eb79512487a92f286414ae4e6..6ca231c90b203d4ee54a6fb7fe46c5188b61441b 100644 (file)
  *
  * Author: Adam Tkac <vonsch@gmail.com>
  *
+ * Licensed under GPLv2, see file LICENSE in this source tree.
+ *
  * Only subset of FTP protocol is implemented but vast majority of clients
- * should not have any problem. You have to run this daemon via inetd.
+ * should not have any problem.
  *
- * Options:
- * -w  - enable FTP write commands
+ * You have to run this daemon via inetd.
  */
+//config:config FTPD
+//config:      bool "ftpd (30 kb)"
+//config:      default y
+//config:      help
+//config:      Simple FTP daemon. You have to run it via inetd.
+//config:
+//config:config FEATURE_FTPD_WRITE
+//config:      bool "Enable -w (upload commands)"
+//config:      default y
+//config:      depends on FTPD
+//config:      help
+//config:      Enable -w option. "ftpd -w" will accept upload commands
+//config:      such as STOR, STOU, APPE, DELE, MKD, RMD, rename commands.
+//config:
+//config:config FEATURE_FTPD_ACCEPT_BROKEN_LIST
+//config:      bool "Enable workaround for RFC-violating clients"
+//config:      default y
+//config:      depends on FTPD
+//config:      help
+//config:      Some ftp clients (among them KDE's Konqueror) issue illegal
+//config:      "LIST -l" requests. This option works around such problems.
+//config:      It might prevent you from listing files starting with "-" and
+//config:      it increases the code size by ~40 bytes.
+//config:      Most other ftp servers seem to behave similar to this.
+//config:
+//config:config FEATURE_FTPD_AUTHENTICATION
+//config:      bool "Enable authentication"
+//config:      default y
+//config:      depends on FTPD
+//config:      help
+//config:      Require login, and change to logged in user's UID:GID before
+//config:      accessing any files. Option "-a USER" allows "anonymous"
+//config:      logins (treats them as if USER logged in).
+//config:
+//config:      If this option is not selected, ftpd runs with the rights
+//config:      of the user it was started under, and does not require login.
+//config:      Take care to not launch it under root.
+
+//applet:IF_FTPD(APPLET(ftpd, BB_DIR_USR_SBIN, BB_SUID_DROP))
+
+//kbuild:lib-$(CONFIG_FTPD) += ftpd.o
+
+//usage:#define ftpd_trivial_usage
+//usage:       "[-wvS]"IF_FEATURE_FTPD_AUTHENTICATION(" [-a USER]")" [-t N] [-T N] [DIR]"
+//usage:#define ftpd_full_usage "\n\n"
+//usage:       IF_NOT_FEATURE_FTPD_AUTHENTICATION(
+//usage:       "Anonymous FTP server. Client access occurs under ftpd's UID.\n"
+//usage:       )
+//usage:       IF_FEATURE_FTPD_AUTHENTICATION(
+//usage:       "FTP server. "
+//usage:       )
+//usage:       "Chroots to DIR, if this fails (run by non-root), cds to it.\n"
+//usage:       "Should be used as inetd service, inetd.conf line:\n"
+//usage:       "       21 stream tcp nowait root ftpd ftpd /files/to/serve\n"
+//usage:       "Can be run from tcpsvd:\n"
+//usage:       "       tcpsvd -vE 0.0.0.0 21 ftpd /files/to/serve"
+//usage:     "\n"
+//usage:     "\n       -w      Allow upload"
+//usage:       IF_FEATURE_FTPD_AUTHENTICATION(
+//usage:     "\n       -A      No login required, client access occurs under ftpd's UID"
+//
+// if !FTPD_AUTHENTICATION, -A is accepted too, but not shown in --help
+// since it's the only supported mode in that configuration
+//
+//usage:     "\n       -a USER Enable 'anonymous' login and map it to USER"
+//usage:       )
+//usage:     "\n       -v      Log errors to stderr. -vv: verbose log"
+//usage:     "\n       -S      Log errors to syslog. -SS: verbose log"
+//usage:     "\n       -t,-T N Idle and absolute timeout"
 
 #include "libbb.h"
+#include "common_bufsiz.h"
+#include <syslog.h>
 #include <netinet/tcp.h>
 
+#define FTP_DATACONN            150
+#define FTP_NOOPOK              200
+#define FTP_TYPEOK              200
+#define FTP_PORTOK              200
+#define FTP_STRUOK              200
+#define FTP_MODEOK              200
+#define FTP_ALLOOK              202
+#define FTP_STATOK              211
+#define FTP_STATFILE_OK         213
+#define FTP_HELP                214
+#define FTP_SYSTOK              215
+#define FTP_GREET               220
+#define FTP_GOODBYE             221
+#define FTP_TRANSFEROK          226
+#define FTP_PASVOK              227
+/*#define FTP_EPRTOK              228*/
+#define FTP_EPSVOK              229
+#define FTP_LOGINOK             230
+#define FTP_CWDOK               250
+#define FTP_RMDIROK             250
+#define FTP_DELEOK              250
+#define FTP_RENAMEOK            250
+#define FTP_PWDOK               257
+#define FTP_MKDIROK             257
+#define FTP_GIVEPWORD           331
+#define FTP_RESTOK              350
+#define FTP_RNFROK              350
+#define FTP_TIMEOUT             421
+#define FTP_BADSENDCONN         425
+#define FTP_BADSENDNET          426
+#define FTP_BADSENDFILE         451
+#define FTP_BADCMD              500
+#define FTP_COMMANDNOTIMPL      502
+#define FTP_NEEDUSER            503
+#define FTP_NEEDRNFR            503
+#define FTP_BADSTRU             504
+#define FTP_BADMODE             504
+#define FTP_LOGINERR            530
+#define FTP_FILEFAIL            550
+#define FTP_NOPERM              550
+#define FTP_UPLOADFAIL          553
+
+#define STR1(s) #s
+#define STR(s) STR1(s)
+
+/* Convert a constant to 3-digit string, packed into uint32_t */
 enum {
-       FTP_DATACONN            = 150,
-       FTP_NOOPOK              = 200,
-       FTP_TYPEOK              = 200,
-       FTP_PORTOK              = 200,
-       FTP_STRUOK              = 200,
-       FTP_MODEOK              = 200,
-       FTP_ALLOOK              = 202,
-       FTP_STATOK              = 211,
-       FTP_STATFILE_OK         = 213,
-       FTP_HELP                = 214,
-       FTP_SYSTOK              = 215,
-       FTP_GREET               = 220,
-       FTP_GOODBYE             = 221,
-       FTP_TRANSFEROK          = 226,
-       FTP_PASVOK              = 227,
-       FTP_LOGINOK             = 230,
-       FTP_CWDOK               = 250,
-#if ENABLE_FEATURE_FTP_WRITE
-       FTP_RMDIROK             = 250,
-       FTP_DELEOK              = 250,
-       FTP_RENAMEOK            = 250,
-#endif
-       FTP_PWDOK               = 257,
-#if ENABLE_FEATURE_FTP_WRITE
-       FTP_MKDIROK             = 257,
-#endif
-       FTP_GIVEPWORD           = 331,
-       FTP_RESTOK              = 350,
-#if ENABLE_FEATURE_FTP_WRITE
-       FTP_RNFROK              = 350,
-#endif
-       FTP_BADSENDCONN         = 425,
-       FTP_BADSENDNET          = 426,
-       FTP_BADSENDFILE         = 451,
-       FTP_BADCMD              = 500,
-       FTP_COMMANDNOTIMPL      = 502,
-       FTP_NEEDUSER            = 503,
-       FTP_NEEDRNFR            = 503,
-       FTP_BADSTRU             = 504,
-       FTP_BADMODE             = 504,
-       FTP_LOGINERR            = 530,
-       FTP_FILEFAIL            = 550,
-       FTP_NOPERM              = 550,
-       FTP_UPLOADFAIL          = 553,
-
-#define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d)
-#define mk_const3(a,b,c)    ((a * 0x100 + b) * 0x100 + c)
-       const_ALLO = mk_const4('A', 'L', 'L', 'O'),
-       const_APPE = mk_const4('A', 'P', 'P', 'E'),
-       const_CDUP = mk_const4('C', 'D', 'U', 'P'),
-       const_CWD  = mk_const3('C', 'W', 'D'),
-       const_DELE = mk_const4('D', 'E', 'L', 'E'),
-       const_HELP = mk_const4('H', 'E', 'L', 'P'),
-       const_LIST = mk_const4('L', 'I', 'S', 'T'),
-       const_MKD  = mk_const3('M', 'K', 'D'),
-       const_MODE = mk_const4('M', 'O', 'D', 'E'),
-       const_NLST = mk_const4('N', 'L', 'S', 'T'),
-       const_NOOP = mk_const4('N', 'O', 'O', 'P'),
-       const_PASS = mk_const4('P', 'A', 'S', 'S'),
-       const_PASV = mk_const4('P', 'A', 'S', 'V'),
-       const_PORT = mk_const4('P', 'O', 'R', 'T'),
-       const_PWD  = mk_const3('P', 'W', 'D'),
-       const_QUIT = mk_const4('Q', 'U', 'I', 'T'),
-       const_REST = mk_const4('R', 'E', 'S', 'T'),
-       const_RETR = mk_const4('R', 'E', 'T', 'R'),
-       const_RMD  = mk_const3('R', 'M', 'D'),
-       const_RNFR = mk_const4('R', 'N', 'F', 'R'),
-       const_RNTO = mk_const4('R', 'N', 'T', 'O'),
-       const_STAT = mk_const4('S', 'T', 'A', 'T'),
-       const_STOR = mk_const4('S', 'T', 'O', 'R'),
-       const_STOU = mk_const4('S', 'T', 'O', 'U'),
-       const_STRU = mk_const4('S', 'T', 'R', 'U'),
-       const_SYST = mk_const4('S', 'Y', 'S', 'T'),
-       const_TYPE = mk_const4('T', 'Y', 'P', 'E'),
-       const_USER = mk_const4('U', 'S', 'E', 'R'),
+       /* Shift for Nth decimal digit */
+       SHIFT2  =  0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN,
+       SHIFT1  =  8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN,
+       SHIFT0  = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN,
+       /* And for 4th position (space) */
+       SHIFTsp = 24 * BB_LITTLE_ENDIAN + 0 * BB_BIG_ENDIAN,
 };
+#define STRNUM32(s) (uint32_t)(0 \
+       | (('0' + ((s) / 1 % 10)) << SHIFT0) \
+       | (('0' + ((s) / 10 % 10)) << SHIFT1) \
+       | (('0' + ((s) / 100 % 10)) << SHIFT2) \
+)
+#define STRNUM32sp(s) (uint32_t)(0 \
+       | (' ' << SHIFTsp) \
+       | (('0' + ((s) / 1 % 10)) << SHIFT0) \
+       | (('0' + ((s) / 10 % 10)) << SHIFT1) \
+       | (('0' + ((s) / 100 % 10)) << SHIFT2) \
+)
+
+#define MSG_OK "Operation successful\r\n"
+#define MSG_ERR "Error\r\n"
 
 struct globals {
-       char *p_control_line_buf;
-       len_and_sockaddr *local_addr;
-       len_and_sockaddr *port_addr;
        int pasv_listen_fd;
-       int data_fd;
+#if !BB_MMU
+       int root_fd;
+#endif
+       int local_file_fd;
+       unsigned end_time;
+       unsigned timeout;
+       unsigned verbose;
+       off_t local_file_pos;
        off_t restart_pos;
-       char *ftp_cmp;
+       len_and_sockaddr *local_addr;
+       len_and_sockaddr *port_addr;
+       char *ftp_cmd;
        char *ftp_arg;
-#if ENABLE_FEATURE_FTP_WRITE
+#if ENABLE_FEATURE_FTPD_WRITE
        char *rnfr_filename;
-       smallint write_enable;
 #endif
-};
-#define G (*(struct globals*)&bb_common_bufsiz1)
-#define INIT_G() do { } while (0)
+       /* We need these aligned to uint32_t */
+       char msg_ok [(sizeof("NNN " MSG_OK ) + 3) & 0xfffc];
+       char msg_err[(sizeof("NNN " MSG_ERR) + 3) & 0xfffc];
+} FIX_ALIASING;
+#define G (*ptr_to_globals)
+/* ^^^ about 75 bytes smaller code than this: */
+//#define G (*(struct globals*)bb_common_bufsiz1)
+#define INIT_G() do { \
+       SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
+       /*setup_common_bufsiz();*/ \
+       \
+       /* Moved to main */ \
+       /*strcpy(G.msg_ok  + 4, MSG_OK );*/ \
+       /*strcpy(G.msg_err + 4, MSG_ERR);*/ \
+} while (0)
 
 
 static char *
-replace_text(const char *str, const char from, const char *to)
+escape_text(const char *prepend, const char *str, unsigned escapee)
 {
-       size_t retlen, remainlen, chunklen, tolen;
-       const char *remain;
+       unsigned retlen, remainlen, chunklen;
        char *ret, *found;
+       char append;
+
+       append = (char)escapee;
+       escapee >>= 8;
 
-       remain = str;
        remainlen = strlen(str);
+       retlen = strlen(prepend);
+       ret = xmalloc(retlen + remainlen * 2 + 1 + 1);
+       strcpy(ret, prepend);
 
-       tolen = strlen(to);
+       for (;;) {
+               found = strchrnul(str, escapee);
+               chunklen = found - str + 1;
 
-       /* simply alloc strlen(str)*strlen(to). To is max 2 so it's allowed */
-       ret = xmalloc(remainlen * strlen(to) + 1);
-       retlen = 0;
+               /* Copy chunk up to and including escapee (or NUL) to ret */
+               memcpy(ret + retlen, str, chunklen);
+               retlen += chunklen;
 
-       for (;;) {
-               found = strchr(remain, from);
-               if (found != NULL) {
-                       chunklen = found - remain;
-
-                       /* Copy chunk which doesn't contain 'from' to ret */
-                       memcpy(&ret[retlen], remain, chunklen);
-                       retlen += chunklen;
-
-                       /* Now copy 'to' instead of 'from' */
-                       memcpy(&ret[retlen], to, tolen);
-                       retlen += tolen;
-
-                       remain = found + 1;
-               } else {
-                       /*
-                        * The last chunk. We are already sure that we have enough space
-                        * so we can use strcpy.
-                        */
-                       strcpy(&ret[retlen], remain);
+               if (*found == '\0') {
+                       /* It wasn't escapee, it was NUL! */
+                       ret[retlen - 1] = append; /* replace NUL */
+                       ret[retlen] = '\0'; /* add NUL */
                        break;
                }
+               ret[retlen++] = escapee; /* duplicate escapee */
+               str = found + 1;
        }
        return ret;
 }
 
-static void
+/* Returns strlen as a bonus */
+static unsigned
 replace_char(char *str, char from, char to)
 {
-       char *ptr;
-
-       /* Don't use strchr here...*/
-       while ((ptr = strchr(str, from)) != NULL) {
-               *ptr = to;
-               str = ptr + 1;
+       char *p = str;
+       while (*p) {
+               if (*p == from)
+                       *p = to;
+               p++;
        }
+       return p - str;
 }
 
 static void
-str_netfd_write(const char *str, int fd)
+verbose_log(const char *str)
 {
-       xwrite(fd, str, strlen(str));
+       bb_error_msg("%.*s", (int)strcspn(str, "\r\n"), str);
 }
 
+/* NB: status_str is char[4] packed into uint32_t */
 static void
-ftp_write_str_common(unsigned int status, const char *str, char sep)
+cmdio_write(uint32_t status_str, const char *str)
 {
-       char *escaped_str, *response;
-       size_t len;
-
-       escaped_str = replace_text(str, '\377', "\377\377");
+       char *response;
+       int len;
 
-       response = xasprintf("%u%c%s\r\n", status, sep, escaped_str);
-       free(escaped_str);
+       /* FTP uses telnet protocol for command link.
+        * In telnet, 0xff is an escape char, and needs to be escaped: */
+       response = escape_text((char *) &status_str, str, (0xff << 8) + '\r');
 
-       len = strlen(response);
-       replace_char(escaped_str, '\n', '\0');
+       /* FTP sends embedded LFs as NULs */
+       len = replace_char(response, '\n', '\0');
 
-       /* Change trailing '\0' back to '\n' */
-       response[len - 1] = '\n';
-       xwrite(STDIN_FILENO, response, len);
+       response[len++] = '\n'; /* tack on trailing '\n' */
+       xwrite(STDOUT_FILENO, response, len);
+       if (G.verbose > 1)
+               verbose_log(response);
+       free(response);
 }
 
 static void
-cmdio_write(int status, const char *p_text)
+cmdio_write_ok(unsigned status)
 {
-       ftp_write_str_common(status, p_text, ' ');
+       *(bb__aliased_uint32_t *) G.msg_ok = status;
+       xwrite(STDOUT_FILENO, G.msg_ok, sizeof("NNN " MSG_OK) - 1);
+       if (G.verbose > 1)
+               verbose_log(G.msg_ok);
 }
+#define WRITE_OK(a) cmdio_write_ok(STRNUM32sp(a))
 
+/* TODO: output strerr(errno) if errno != 0? */
 static void
-cmdio_write_hyphen(int status, const char *p_text)
+cmdio_write_error(unsigned status)
 {
-       ftp_write_str_common(status, p_text, '-');
+       *(bb__aliased_uint32_t *) G.msg_err = status;
+       xwrite(STDOUT_FILENO, G.msg_err, sizeof("NNN " MSG_ERR) - 1);
+       if (G.verbose > 0)
+               verbose_log(G.msg_err);
 }
+#define WRITE_ERR(a) cmdio_write_error(STRNUM32sp(a))
 
 static void
 cmdio_write_raw(const char *p_text)
 {
-       str_netfd_write(p_text, STDIN_FILENO);
+       xwrite_str(STDOUT_FILENO, p_text);
+       if (G.verbose > 1)
+               verbose_log(p_text);
 }
 
-static uint32_t
-cmdio_get_cmd_and_arg(void)
+static void
+timeout_handler(int sig UNUSED_PARAM)
 {
-       int len;
-       uint32_t cmdval;
-       char *cmd;
+       off_t pos;
+       int sv_errno = errno;
 
-       free(G.ftp_cmp);
-       G.ftp_cmp = cmd = xmalloc_reads(STDIN_FILENO, NULL, NULL);
-/*
- * TODO:
- *
- * now we should change all '\0' to '\n' - xmalloc_reads will be improved,
- * probably
- */
-       len = strlen(cmd) - 1;
-       while (len >= 0 && cmd[len] == '\r') {
-               cmd[len] = '\0';
-               len--;
-       }
+       if ((int)(monotonic_sec() - G.end_time) >= 0)
+               goto timed_out;
 
-       G.ftp_arg = strchr(cmd, ' ');
-       if (G.ftp_arg != NULL) {
-               *G.ftp_arg = '\0';
-               G.ftp_arg++;
-       }
-       cmdval = 0;
-       while (*cmd)
-               cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20);
+       if (!G.local_file_fd)
+               goto timed_out;
 
-       return cmdval;
+       pos = xlseek(G.local_file_fd, 0, SEEK_CUR);
+       if (pos == G.local_file_pos)
+               goto timed_out;
+       G.local_file_pos = pos;
+
+       alarm(G.timeout);
+       errno = sv_errno;
+       return;
+
+ timed_out:
+       cmdio_write_raw(STR(FTP_TIMEOUT)" Timeout\r\n");
+/* TODO: do we need to abort (as opposed to usual shutdown) data transfer? */
+       exit(1);
 }
 
+/* Simple commands */
+
 static void
-init_data_sock_params(int sock_fd)
+handle_pwd(void)
 {
-       struct linger linger;
+       char *cwd, *response;
 
-       G.data_fd = sock_fd;
-
-       memset(&linger, 0, sizeof(linger));
-       linger.l_onoff = 1;
-       linger.l_linger = 32767;
+       cwd = xrealloc_getcwd_or_warn(NULL);
+       if (cwd == NULL)
+               cwd = xstrdup("");
 
-       setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
-       setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger));
+       /* We have to promote each " to "" */
+       response = escape_text(" \"", cwd, ('"' << 8) + '"');
+       free(cwd);
+       cmdio_write(STRNUM32(FTP_PWDOK), response);
+       free(response);
 }
 
-static int
-ftpdataio_get_pasv_fd(void)
+static void
+handle_cwd(void)
 {
-       int remote_fd;
-
-       remote_fd = accept(G.pasv_listen_fd, NULL, 0);
-
-       if (remote_fd < 0) {
-               cmdio_write(FTP_BADSENDCONN, "Can't establish connection");
-               return remote_fd;
+       if (!G.ftp_arg || chdir(G.ftp_arg) != 0) {
+               WRITE_ERR(FTP_FILEFAIL);
+               return;
        }
-
-       init_data_sock_params(remote_fd);
-       return remote_fd;
+       WRITE_OK(FTP_CWDOK);
 }
 
-static int
-ftpdataio_get_port_fd(void)
+static void
+handle_cdup(void)
 {
-       int remote_fd;
-
-       /* Do we want die or print error to client? */
-       remote_fd = xconnect_stream(G.port_addr);
+       G.ftp_arg = (char*)"..";
+       handle_cwd();
+}
 
-       init_data_sock_params(remote_fd);
-       return remote_fd;
+static void
+handle_stat(void)
+{
+       cmdio_write_raw(STR(FTP_STATOK)"-Server status:\r\n"
+                       " TYPE: BINARY\r\n"
+                       STR(FTP_STATOK)" Ok\r\n");
 }
 
+/* Examples of HELP and FEAT:
+# nc -vvv ftp.kernel.org 21
+ftp.kernel.org (130.239.17.4:21) open
+220 Welcome to ftp.kernel.org.
+FEAT
+211-Features:
+ EPRT
+ EPSV
+ MDTM
+ PASV
+ REST STREAM
+ SIZE
+ TVFS
+ UTF8
+211 End
+HELP
+214-The following commands are recognized.
+ ABOR ACCT ALLO APPE CDUP CWD  DELE EPRT EPSV FEAT HELP LIST MDTM MKD
+ MODE NLST NOOP OPTS PASS PASV PORT PWD  QUIT REIN REST RETR RMD  RNFR
+ RNTO SITE SIZE SMNT STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD
+ XPWD XRMD
+214 Help OK.
+*/
 static void
-ftpdataio_dispose_transfer_fd(void)
+handle_feat(unsigned status)
 {
-       /* This close() blocks because we set SO_LINGER */
-       if (G.data_fd > STDOUT_FILENO) {
-               if (close(G.data_fd) < 0) {
-                       /* Do it again without blocking. */
-                       struct linger linger;
-
-                       memset(&linger, 0, sizeof(linger));
-                       setsockopt(G.data_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger));
-                       close(G.data_fd);
-               }
-       }
-       G.data_fd = -1;
+       cmdio_write(status, "-Features:");
+       cmdio_write_raw(" EPSV\r\n"
+                       " PASV\r\n"
+                       " REST STREAM\r\n"
+                       " MDTM\r\n"
+                       " SIZE\r\n");
+       cmdio_write(status, " Ok");
 }
 
-static int
+/* Download commands */
+
+static inline int
 port_active(void)
 {
-       return (G.port_addr != NULL) ? 1: 0;
+       return (G.port_addr != NULL);
 }
 
-static int
+static inline int
 pasv_active(void)
 {
-       return (G.pasv_listen_fd != -1) ? 1 : 0;
+       return (G.pasv_listen_fd > STDOUT_FILENO);
+}
+
+static void
+port_pasv_cleanup(void)
+{
+       free(G.port_addr);
+       G.port_addr = NULL;
+       if (G.pasv_listen_fd > STDOUT_FILENO)
+               close(G.pasv_listen_fd);
+       G.pasv_listen_fd = -1;
 }
 
+/* On error, emits error code to the peer */
 static int
-get_remote_transfer_fd(const char *p_status_msg)
+ftpdataio_get_pasv_fd(void)
 {
        int remote_fd;
 
-       if (pasv_active())
-               remote_fd = ftpdataio_get_pasv_fd();
-       else
-               remote_fd = ftpdataio_get_port_fd();
+       remote_fd = accept(G.pasv_listen_fd, NULL, 0);
 
-       if (remote_fd < 0)
+       if (remote_fd < 0) {
+               WRITE_ERR(FTP_BADSENDCONN);
                return remote_fd;
+       }
 
-       cmdio_write(FTP_DATACONN, p_status_msg);
+       setsockopt_keepalive(remote_fd);
        return remote_fd;
 }
 
-static void
-handle_pwd(void)
+/* Clears port/pasv data.
+ * This means we dont waste resources, for example, keeping
+ * PASV listening socket open when it is no longer needed.
+ * On error, emits error code to the peer (or exits).
+ * On success, emits p_status_msg to the peer.
+ */
+static int
+get_remote_transfer_fd(const char *p_status_msg)
 {
-       char *cwd, *promoted_cwd, *response;
-
-       cwd = xrealloc_getcwd_or_warn(NULL);
-       if (cwd == NULL)
-               cwd = xstrdup("");
+       int remote_fd;
 
-       /* We _have to_ promote each " to "" */
-       promoted_cwd = replace_text(cwd, '\"', "\"\"");
-       free(cwd);
-       response = xasprintf("\"%s\"", promoted_cwd);
-       free(promoted_cwd);
-       cmdio_write(FTP_PWDOK, response);
-       free(response);
-}
+       if (pasv_active())
+               /* On error, emits error code to the peer */
+               remote_fd = ftpdataio_get_pasv_fd();
+       else
+               /* Exits on error */
+               remote_fd = xconnect_stream(G.port_addr);
 
-static void
-handle_cwd(void)
-{
-       int retval;
+       port_pasv_cleanup();
 
-       /* XXX Do we need check ftp_arg != NULL? */
-       retval = chdir(G.ftp_arg);
-       if (retval == 0)
-               cmdio_write(FTP_CWDOK, "Directory changed");
-       else
-               cmdio_write(FTP_FILEFAIL, "Can't change directory");
-}
+       if (remote_fd < 0)
+               return remote_fd;
 
-static void
-handle_cdup(void)
-{
-       G.ftp_arg = xstrdup("..");
-       handle_cwd();
-       free(G.ftp_arg);
+       cmdio_write(STRNUM32(FTP_DATACONN), p_status_msg);
+       return remote_fd;
 }
 
+/* If there were neither PASV nor PORT, emits error code to the peer */
 static int
-data_transfer_checks_ok(void)
+port_or_pasv_was_seen(void)
 {
        if (!pasv_active() && !port_active()) {
-               cmdio_write(FTP_BADSENDCONN, "Use PORT or PASV first");
+               cmdio_write_raw(STR(FTP_BADSENDCONN)" Use PORT/PASV first\r\n");
                return 0;
        }
 
        return 1;
 }
 
-static void
-port_cleanup(void)
+/* Exits on error */
+static unsigned
+bind_for_passive_mode(void)
 {
-       if (G.port_addr != NULL)
-               free(G.port_addr);
+       int fd;
+       unsigned port;
 
-       G.port_addr = NULL;
-}
+       port_pasv_cleanup();
 
-static void
-pasv_cleanup(void)
-{
-       if (G.pasv_listen_fd > STDOUT_FILENO)
-               close(G.pasv_listen_fd);
-       G.pasv_listen_fd = -1;
-}
+       G.pasv_listen_fd = fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
+       setsockopt_reuseaddr(fd);
 
-static char *
-statbuf_getperms(const struct stat *statbuf)
-{
-       char *perms;
-       enum { r = 'r', w = 'w', x = 'x', s = 's', S = 'S' };
-
-       perms = xmalloc(11);
-       memset(perms, '-', 10);
-
-       perms[0] = '?';
-       switch (statbuf->st_mode & S_IFMT) {
-       case S_IFREG: perms[0] = '-'; break;
-       case S_IFDIR: perms[0] = 'd'; break;
-       case S_IFLNK: perms[0] = 'l'; break;
-       case S_IFIFO: perms[0] = 'p'; break;
-       case S_IFSOCK: perms[0] = s; break;
-       case S_IFCHR: perms[0] = 'c'; break;
-       case S_IFBLK: perms[0] = 'b'; break;
-       }
+       set_nport(&G.local_addr->u.sa, 0);
+       xbind(fd, &G.local_addr->u.sa, G.local_addr->len);
+       xlisten(fd, 1);
+       getsockname(fd, &G.local_addr->u.sa, &G.local_addr->len);
 
-       if (statbuf->st_mode & S_IRUSR) perms[1] = r;
-       if (statbuf->st_mode & S_IWUSR) perms[2] = w;
-       if (statbuf->st_mode & S_IXUSR) perms[3] = x;
-       if (statbuf->st_mode & S_IRGRP) perms[4] = r;
-       if (statbuf->st_mode & S_IWGRP) perms[5] = w;
-       if (statbuf->st_mode & S_IXGRP) perms[6] = x;
-       if (statbuf->st_mode & S_IROTH) perms[7] = r;
-       if (statbuf->st_mode & S_IWOTH) perms[8] = w;
-       if (statbuf->st_mode & S_IXOTH) perms[9] = x;
-       if (statbuf->st_mode & S_ISUID) perms[3] = (perms[3] == x) ? s : S;
-       if (statbuf->st_mode & S_ISGID) perms[6] = (perms[6] == x) ? s : S;
-       if (statbuf->st_mode & S_ISVTX) perms[9] = (perms[9] == x) ? 't' : 'T';
-
-       perms[10] = '\0';
-
-       return perms;
+       port = get_nport(&G.local_addr->u.sa);
+       port = ntohs(port);
+       return port;
 }
 
+/* Exits on error */
 static void
-write_filestats(int fd, const char *filename,
-                               const struct stat *statbuf)
+handle_pasv(void)
 {
-       off_t size;
-       char *stats, *lnkname = NULL, *perms;
-       const char *name;
-       int retval;
-       char timestr[32];
-       struct tm *tm;
-       const char *format = "%b %d %H:%M";
-
-       name = bb_get_last_path_component_nostrip(filename);
+       unsigned port;
+       char *addr, *response;
 
-       if (statbuf != NULL) {
-               size = statbuf->st_size;
+       port = bind_for_passive_mode();
 
-               if (S_ISLNK(statbuf->st_mode))
-                       /* Damn symlink... */
-                       lnkname = xmalloc_readlink(filename);
+       if (G.local_addr->u.sa.sa_family == AF_INET)
+               addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa);
+       else /* seen this in the wild done by other ftp servers: */
+               addr = xstrdup("0.0.0.0");
+       replace_char(addr, '.', ',');
 
-               tm = gmtime(&statbuf->st_mtime);
-               retval = strftime(timestr, sizeof(timestr), format, tm);
-               if (retval == 0)
-                       bb_error_msg_and_die("strftime");
-
-               timestr[sizeof(timestr) - 1] = '\0';
-
-               perms = statbuf_getperms(statbuf);
-
-               stats = xasprintf("%s %u\tftp ftp %"OFF_FMT"u\t%s %s",
-                               perms, (int) statbuf->st_nlink,
-                               size, timestr, name);
-
-               free(perms);
-       } else
-               stats = xstrdup(name);
-
-       str_netfd_write(stats, fd);
-       free(stats);
-       if (lnkname != NULL) {
-               str_netfd_write(" -> ", fd);
-               str_netfd_write(lnkname, fd);
-               free(lnkname);
-       }
-       str_netfd_write("\r\n", fd);
+       response = xasprintf(STR(FTP_PASVOK)" PASV ok (%s,%u,%u)\r\n",
+                       addr, (int)(port >> 8), (int)(port & 255));
+       free(addr);
+       cmdio_write_raw(response);
+       free(response);
 }
 
+/* Exits on error */
 static void
-write_dirstats(int fd, const char *dname, int details)
+handle_epsv(void)
 {
-       DIR *dir;
-       struct dirent *dirent;
-       struct stat statbuf;
-       char *filename;
+       unsigned port;
+       char *response;
 
-       dir = xopendir(dname);
+       port = bind_for_passive_mode();
+       response = xasprintf(STR(FTP_EPSVOK)" EPSV ok (|||%u|)\r\n", port);
+       cmdio_write_raw(response);
+       free(response);
+}
 
-       for (;;) {
-               dirent = readdir(dir);
-               if (dirent == NULL)
-                       break;
+static void
+handle_port(void)
+{
+       unsigned port, port_hi;
+       char *raw, *comma;
+#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
+       socklen_t peer_ipv4_len;
+       struct sockaddr_in peer_ipv4;
+       struct in_addr port_ipv4_sin_addr;
+#endif
 
-               /* Ignore . and .. */
-               if (dirent->d_name[0] == '.') {
-                       if (dirent->d_name[1] == '\0'
-                        || (dirent->d_name[1] == '.' && dirent->d_name[2] == '\0')
-                       ) {
-                               continue;
-                       }
-               }
+       port_pasv_cleanup();
 
-               if (details) {
-                       filename = xasprintf("%s/%s", dname, dirent->d_name);
-                       if (lstat(filename, &statbuf) != 0) {
-                               free(filename);
-                               goto bail;
-                       }
-               } else
-                       filename = xstrdup(dirent->d_name);
+       raw = G.ftp_arg;
 
-               write_filestats(fd, filename, details ? &statbuf : NULL);
-               free(filename);
+       /* PORT command format makes sense only over IPv4 */
+       if (!raw
+#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
+        || G.local_addr->u.sa.sa_family != AF_INET
+#endif
+       ) {
+ bail:
+               WRITE_ERR(FTP_BADCMD);
+               return;
        }
 
-bail:
-       closedir(dir);
-}
-
-static void
-handle_pasv(void)
-{
-       int bind_retries = 10;
-       unsigned short port;
-       enum { min_port = 1024, max_port = 65535 };
-       char *addr, *wire_addr, *response;
-
-       pasv_cleanup();
-       port_cleanup();
-       G.pasv_listen_fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
-       setsockopt_reuseaddr(G.pasv_listen_fd);
-
-       /* TODO bind() with port == 0 and then call getsockname */
-       while (--bind_retries) {
-               port = rand() % max_port;
-               if (port < min_port) {
-                       port += min_port;
-               }
+       comma = strrchr(raw, ',');
+       if (comma == NULL)
+               goto bail;
+       *comma = '\0';
+       port = bb_strtou(&comma[1], NULL, 10);
+       if (errno || port > 0xff)
+               goto bail;
 
-               set_nport(G.local_addr, htons(port));
-               /* We don't want to use xbind, it'll die if port is in use */
-               if (bind(G.pasv_listen_fd, &G.local_addr->u.sa, G.local_addr->len) != 0) {
-                       /* do we want check if errno == EADDRINUSE ? */
-                       continue;
-               }
-               xlisten(G.pasv_listen_fd, 1);
-               break;
-       }
+       comma = strrchr(raw, ',');
+       if (comma == NULL)
+               goto bail;
+       *comma = '\0';
+       port_hi = bb_strtou(&comma[1], NULL, 10);
+       if (errno || port_hi > 0xff)
+               goto bail;
+       port |= port_hi << 8;
 
-       if (!bind_retries)
-               bb_error_msg_and_die("can't create pasv socket");
+#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
+       replace_char(raw, ',', '.');
 
-       addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa);
-       wire_addr = replace_text(addr, '.', ",");
-       free(addr);
+       /* We are verifying that PORT's IP matches getpeername().
+        * Otherwise peer can make us open data connections
+        * to other hosts (security problem!)
+        * This code would be too simplistic:
+        * lsa = xdotted2sockaddr(raw, port);
+        * if (lsa == NULL) goto bail;
+        */
+       if (!inet_aton(raw, &port_ipv4_sin_addr))
+               goto bail;
+       peer_ipv4_len = sizeof(peer_ipv4);
+       if (getpeername(STDIN_FILENO, &peer_ipv4, &peer_ipv4_len) != 0)
+               goto bail;
+       if (memcmp(&port_ipv4_sin_addr, &peer_ipv4.sin_addr, sizeof(struct in_addr)) != 0)
+               goto bail;
 
-       response = xasprintf("Entering Passive Mode (%s,%u,%u)",
-                       wire_addr, (int)(port >> 8), (int)(port & 255));
+       G.port_addr = xdotted2sockaddr(raw, port);
+#else
+       G.port_addr = get_peer_lsa(STDIN_FILENO);
+       set_nport(&G.port_addr->u.sa, htons(port));
+#endif
+       WRITE_OK(FTP_PORTOK);
+}
 
-       cmdio_write(FTP_PASVOK, response);
-       free(wire_addr);
-       free(response);
+static void
+handle_rest(void)
+{
+       /* When ftp_arg == NULL simply restart from beginning */
+       G.restart_pos = G.ftp_arg ? XATOOFF(G.ftp_arg) : 0;
+       WRITE_OK(FTP_RESTOK);
 }
 
 static void
 handle_retr(void)
 {
        struct stat statbuf;
-       int trans_ret, retval;
+       off_t bytes_transferred;
        int remote_fd;
-       int opened_file;
+       int local_file_fd;
        off_t offset = G.restart_pos;
        char *response;
 
        G.restart_pos = 0;
 
-       if (!data_transfer_checks_ok())
-               return;
+       if (!port_or_pasv_was_seen())
+               return; /* port_or_pasv_was_seen emitted error response */
 
-       /* XXX Do we need check if ftp_arg != NULL? */
-       opened_file = open(G.ftp_arg, O_RDONLY | O_NONBLOCK);
-       if (opened_file < 0) {
-               cmdio_write(FTP_FILEFAIL, "Can't open file");
+       /* O_NONBLOCK is useful if file happens to be a device node */
+       local_file_fd = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1;
+       if (local_file_fd < 0) {
+               WRITE_ERR(FTP_FILEFAIL);
                return;
        }
 
-       retval = fstat(opened_file, &statbuf);
-       if (retval < 0 || !S_ISREG(statbuf.st_mode)) {
+       if (fstat(local_file_fd, &statbuf) != 0 || !S_ISREG(statbuf.st_mode)) {
                /* Note - pretend open failed */
-               cmdio_write(FTP_FILEFAIL, "Can't open file");
+               WRITE_ERR(FTP_FILEFAIL);
                goto file_close_out;
        }
+       G.local_file_fd = local_file_fd;
 
-       /* Now deactive O_NONBLOCK, otherwise we have a problem on DMAPI filesystems
-        * such as XFS DMAPI.
+       /* Now deactive O_NONBLOCK, otherwise we have a problem
+        * on DMAPI filesystems such as XFS DMAPI.
         */
-       ndelay_off(opened_file);
+       ndelay_off(local_file_fd);
 
        /* Set the download offset (from REST) if any */
        if (offset != 0)
-               xlseek(opened_file, offset, SEEK_SET);
+               xlseek(local_file_fd, offset, SEEK_SET);
 
        response = xasprintf(
-               "Opening BINARY mode data connection for (%s %"OFF_FMT"u bytes).",
+               " Opening BINARY connection for %s (%"OFF_FMT"u bytes)",
                G.ftp_arg, statbuf.st_size);
-
        remote_fd = get_remote_transfer_fd(response);
        free(response);
        if (remote_fd < 0)
-               goto port_pasv_cleanup_out;
+               goto file_close_out;
 
-       trans_ret = bb_copyfd_eof(opened_file, remote_fd);
-       ftpdataio_dispose_transfer_fd();
-       if (trans_ret < 0)
-               cmdio_write(FTP_BADSENDFILE, "Error sending local file");
+       bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd);
+       close(remote_fd);
+       if (bytes_transferred < 0)
+               WRITE_ERR(FTP_BADSENDFILE);
        else
-               cmdio_write(FTP_TRANSFEROK, "File sent OK");
+               WRITE_OK(FTP_TRANSFEROK);
 
-port_pasv_cleanup_out:
-       port_cleanup();
-       pasv_cleanup();
-file_close_out:
-       close(opened_file);
+ file_close_out:
+       close(local_file_fd);
+       G.local_file_fd = 0;
 }
 
-static void
-handle_dir_common(int full_details, int stat_cmd)
+/* List commands */
+
+static int
+popen_ls(const char *opt)
 {
-       int fd;
-       struct stat statbuf;
+       const char *argv[5];
+       struct fd_pair outfd;
+       pid_t pid;
+
+       argv[0] = "ftpd";
+       argv[1] = opt; /* "-lA" or "-1A" */
+       argv[2] = "--";
+       argv[3] = G.ftp_arg;
+       argv[4] = NULL;
+
+       /* Improve compatibility with non-RFC conforming FTP clients
+        * which send e.g. "LIST -l", "LIST -la", "LIST -aL".
+        * See https://bugs.kde.org/show_bug.cgi?id=195578 */
+       if (ENABLE_FEATURE_FTPD_ACCEPT_BROKEN_LIST
+        && G.ftp_arg && G.ftp_arg[0] == '-'
+       ) {
+               const char *tmp = strchr(G.ftp_arg, ' ');
+               if (tmp) /* skip the space */
+                       tmp++;
+               argv[3] = tmp;
+       }
 
-       if (!stat_cmd && !data_transfer_checks_ok())
-               return;
+       xpiped_pair(outfd);
 
-       if (stat_cmd) {
-               fd = STDIN_FILENO;
-               cmdio_write_hyphen(FTP_STATFILE_OK, "Status follows:");
-       } else {
-               fd = get_remote_transfer_fd("Here comes the directory listing");
-               if (fd < 0)
-                       goto bail;
+       /*fflush_all(); - so far we dont use stdio on output */
+       pid = BB_MMU ? xfork() : xvfork();
+       if (pid == 0) {
+#if !BB_MMU
+               int cur_fd;
+#endif
+               /* child */
+               /* NB: close _first_, then move fd! */
+               close(outfd.rd);
+               xmove_fd(outfd.wr, STDOUT_FILENO);
+               /* Opening /dev/null in chroot is hard.
+                * Just making sure STDIN_FILENO is opened
+                * to something harmless. Paranoia,
+                * ls won't read it anyway */
+               close(STDIN_FILENO);
+               dup(STDOUT_FILENO); /* copy will become STDIN_FILENO */
+#if BB_MMU
+               /* memset(&G, 0, sizeof(G)); - ls_main does it */
+               exit(ls_main(/*argc_unused*/ 0, (char**) argv));
+#else
+               cur_fd = xopen(".", O_RDONLY | O_DIRECTORY);
+               /* On NOMMU, we want to execute a child - copy of ourself
+                * in order to unblock parent after vfork.
+                * In chroot we usually can't re-exec. Thus we escape
+                * out of the chroot back to original root.
+                */
+               if (G.root_fd >= 0) {
+                       if (fchdir(G.root_fd) != 0 || chroot(".") != 0)
+                               _exit(127);
+                       /*close(G.root_fd); - close_on_exec_on() took care of this */
+               }
+               /* Child expects directory to list on fd #3 */
+               xmove_fd(cur_fd, 3);
+               execv(bb_busybox_exec_path, (char**) argv);
+               _exit(127);
+#endif
        }
 
-       if (G.ftp_arg != NULL) {
-               if (lstat(G.ftp_arg, &statbuf) != 0) {
-                       /* Dir doesn't exist => return ok to client */
-                       goto bail;
-               }
-               if (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode))
-                       write_filestats(fd, G.ftp_arg, &statbuf);
-               else if (S_ISDIR(statbuf.st_mode))
-                       write_dirstats(fd, G.ftp_arg, full_details);
-       } else
-               write_dirstats(fd, ".", full_details);
-
-bail:
-       /* Well, if we can't open directory/file it doesn't matter */
-       if (!stat_cmd) {
-               ftpdataio_dispose_transfer_fd();
-               pasv_cleanup();
-               port_cleanup();
-               cmdio_write(FTP_TRANSFEROK, "OK");
-       } else
-               cmdio_write(FTP_STATFILE_OK, "End of status");
+       /* parent */
+       close(outfd.wr);
+       return outfd.rd;
 }
 
+enum {
+       USE_CTRL_CONN = 1,
+       LONG_LISTING = 2,
+};
+
+static void
+handle_dir_common(int opts)
+{
+       FILE *ls_fp;
+       char *line;
+       int ls_fd;
+
+       if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen())
+               return; /* port_or_pasv_was_seen emitted error response */
+
+       ls_fd = popen_ls((opts & LONG_LISTING) ? "-lA" : "-1A");
+       ls_fp = xfdopen_for_read(ls_fd);
+/* FIXME: filenames with embedded newlines are mishandled */
+
+       if (opts & USE_CTRL_CONN) {
+               /* STAT <filename> */
+               cmdio_write_raw(STR(FTP_STATFILE_OK)"-File status:\r\n");
+               while (1) {
+                       line = xmalloc_fgetline(ls_fp);
+                       if (!line)
+                               break;
+                       /* Hack: 0 results in no status at all */
+                       /* Note: it's ok that we don't prepend space,
+                        * ftp.kernel.org doesn't do that too */
+                       cmdio_write(0, line);
+                       free(line);
+               }
+               WRITE_OK(FTP_STATFILE_OK);
+       } else {
+               /* LIST/NLST [<filename>] */
+               int remote_fd = get_remote_transfer_fd(" Directory listing");
+               if (remote_fd >= 0) {
+                       while (1) {
+                               unsigned len;
+
+                               line = xmalloc_fgets(ls_fp);
+                               if (!line)
+                                       break;
+                               /* I've seen clients complaining when they
+                                * are fed with ls output with bare '\n'.
+                                * Replace trailing "\n\0" with "\r\n".
+                                */
+                               len = strlen(line);
+                               if (len != 0) /* paranoia check */
+                                       line[len - 1] = '\r';
+                               line[len] = '\n';
+                               xwrite(remote_fd, line, len + 1);
+                               free(line);
+                       }
+               }
+               close(remote_fd);
+               WRITE_OK(FTP_TRANSFEROK);
+       }
+       fclose(ls_fp); /* closes ls_fd too */
+}
 static void
 handle_list(void)
 {
-       handle_dir_common(1, 0);
+       handle_dir_common(LONG_LISTING);
 }
-
 static void
-handle_type(void)
+handle_nlst(void)
 {
-       if (G.ftp_arg
-        && (  ((G.ftp_arg[0] | 0x20) == 'i' && G.ftp_arg[1] == '\0')
-           || !strcasecmp(G.ftp_arg, "L8")
-           || !strcasecmp(G.ftp_arg, "L 8")
-           )
-       ) {
-               cmdio_write(FTP_TYPEOK, "Switching to Binary mode");
-       } else {
-               cmdio_write(FTP_BADCMD, "Unrecognised TYPE command");
-       }
+       /* NLST returns list of names, "\r\n" terminated without regard
+        * to the current binary flag. Names may start with "/",
+        * then they represent full names (we don't produce such names),
+        * otherwise names are relative to current directory.
+        * Embedded "\n" are replaced by NULs. This is safe since names
+        * can never contain NUL.
+        */
+       handle_dir_common(0);
 }
-
 static void
-handle_port(void)
+handle_stat_file(void)
 {
-       unsigned short port;
-       char *raw = NULL, *port_part;
-       len_and_sockaddr *lsa = NULL;
-
-       pasv_cleanup();
-       port_cleanup();
-
-       if (G.ftp_arg == NULL)
-               goto bail;
-
-       raw = replace_text(G.ftp_arg, ',', ".");
-
-       port_part = strrchr(raw, '.');
-       if (port_part == NULL)
-               goto bail;
-
-       port = xatou16(&port_part[1]);
-       *port_part = '\0';
-
-       port_part = strrchr(raw, '.');
-       if (port_part == NULL)
-               goto bail;
-
-       port |= xatou16(&port_part[1]) << 8;
-       *port_part = '\0';
-
-       lsa = xdotted2sockaddr(raw, port);
-
-bail:
-       free(raw);
-
-       if (lsa == NULL) {
-               cmdio_write(FTP_BADCMD, "Illegal PORT command");
-               return;
-       }
-
-       G.port_addr = lsa;
-       cmdio_write(FTP_PORTOK, "PORT command successful. Consider using PASV");
+       handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
 }
 
-#if ENABLE_FEATURE_FTP_WRITE
+/* This can be extended to handle MLST, as all info is available
+ * in struct stat for that:
+ * MLST file_name
+ * 250-Listing file_name
+ *  type=file;size=4161;modify=19970214165800; /dir/dir/file_name
+ * 250 End
+ * Nano-doc:
+ * MLST [<file or dir name, "." assumed if not given>]
+ * Returned name should be either the same as requested, or fully qualified.
+ * If there was no parameter, return "" or (preferred) fully-qualified name.
+ * Returned "facts" (case is not important):
+ *  size    - size in octets
+ *  modify  - last modification time
+ *  type    - entry type (file,dir,OS.unix=block)
+ *            (+ cdir and pdir types for MLSD)
+ *  unique  - unique id of file/directory (inode#)
+ *  perm    -
+ *      a: can be appended to (APPE)
+ *      d: can be deleted (RMD/DELE)
+ *      f: can be renamed (RNFR)
+ *      r: can be read (RETR)
+ *      w: can be written (STOR)
+ *      e: can CWD into this dir
+ *      l: this dir can be listed (dir only!)
+ *      c: can create files in this dir
+ *      m: can create dirs in this dir (MKD)
+ *      p: can delete files in this dir
+ *  UNIX.mode - unix file mode
+ */
 static void
-handle_upload_common(int is_append, int is_unique)
+handle_size_or_mdtm(int need_size)
 {
-       char *template = NULL;
-       int trans_ret;
-       int new_file_fd;
-       int remote_fd;
-
-       enum {
-               fileflags = O_CREAT | O_WRONLY | O_APPEND,
-       };
-
-       off_t offset = G.restart_pos;
-
-       G.restart_pos = 0;
-       if (!data_transfer_checks_ok())
-               return;
-
-       if (is_unique) {
-               template = xstrdup("uniq.XXXXXX");
-               /*
-                * XXX Use mkostemp here? vsftpd opens file with O_CREAT, O_WRONLY, 
-                * O_APPEND and O_EXCL flags...
-                */
-               new_file_fd = mkstemp(template);
-       } else {
-               /* XXX Do we need check if ftp_arg != NULL? */
-               if (!is_append && offset == 0)
-                       new_file_fd = open(G.ftp_arg, O_CREAT | O_WRONLY | O_APPEND | O_NONBLOCK | O_TRUNC, 0666);
-               else
-                       new_file_fd = open(G.ftp_arg, O_CREAT | O_WRONLY | O_APPEND | O_NONBLOCK, 0666);
-       }
-
-       if (new_file_fd < 0) {
-               cmdio_write(FTP_UPLOADFAIL, "Can't create file");
+       struct stat statbuf;
+       struct tm broken_out;
+       char buf[(sizeof("NNN %"OFF_FMT"u\r\n") + sizeof(off_t) * 3)
+               | sizeof("NNN YYYYMMDDhhmmss\r\n")
+       ];
+
+       if (!G.ftp_arg
+        || stat(G.ftp_arg, &statbuf) != 0
+        || !S_ISREG(statbuf.st_mode)
+       ) {
+               WRITE_ERR(FTP_FILEFAIL);
                return;
        }
-
-       if (!is_append && offset != 0) {
-               /* warning, allows seek past end of file! Check for seek > size? */
-               xlseek(new_file_fd, offset, SEEK_SET);
+       if (need_size) {
+               sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size);
+       } else {
+               gmtime_r(&statbuf.st_mtime, &broken_out);
+               sprintf(buf, STR(FTP_STATFILE_OK)" %04u%02u%02u%02u%02u%02u\r\n",
+                       broken_out.tm_year + 1900,
+                       broken_out.tm_mon + 1,
+                       broken_out.tm_mday,
+                       broken_out.tm_hour,
+                       broken_out.tm_min,
+                       broken_out.tm_sec);
        }
-
-       if (is_unique) {
-               char *resp = xasprintf("FILE: %s", template);
-               remote_fd = get_remote_transfer_fd(resp);
-               free(resp);
-               free(template);
-       } else
-               remote_fd = get_remote_transfer_fd("Ok to send data");
-
-       if (remote_fd < 0)
-               goto bail;
-
-       trans_ret = bb_copyfd_eof(remote_fd, new_file_fd);
-       ftpdataio_dispose_transfer_fd();
-
-       if (trans_ret < 0)
-               cmdio_write(FTP_BADSENDFILE, "Failure writing to local file");
-       else
-               cmdio_write(FTP_TRANSFEROK, "File receive OK");
-
-bail:
-       port_cleanup();
-       pasv_cleanup();
-       close(new_file_fd);
+       cmdio_write_raw(buf);
 }
 
-static void
-handle_stor(void)
-{
-       handle_upload_common(0, 0);
-}
+/* Upload commands */
 
+#if ENABLE_FEATURE_FTPD_WRITE
 static void
 handle_mkd(void)
 {
-       int retval;
-
-       /* Do we need check if ftp_arg != NULL? */
-       retval = mkdir(G.ftp_arg, 0770);
-       if (retval != 0) {
-               cmdio_write(FTP_FILEFAIL, "Create directory operation failed");
+       if (!G.ftp_arg || mkdir(G.ftp_arg, 0777) != 0) {
+               WRITE_ERR(FTP_FILEFAIL);
                return;
        }
-
-       cmdio_write(FTP_MKDIROK, "created");
+       WRITE_OK(FTP_MKDIROK);
 }
 
 static void
 handle_rmd(void)
 {
-       int retval;
-
-       /* Do we need check if ftp_arg != NULL? */
-       retval = rmdir(G.ftp_arg);
-       if (retval != 0)
-               cmdio_write(FTP_FILEFAIL, "rmdir failed");
-       else
-               cmdio_write(FTP_RMDIROK, "rmdir successful");
+       if (!G.ftp_arg || rmdir(G.ftp_arg) != 0) {
+               WRITE_ERR(FTP_FILEFAIL);
+               return;
+       }
+       WRITE_OK(FTP_RMDIROK);
 }
 
 static void
 handle_dele(void)
 {
-       int retval;
-
-       /* Do we need check if ftp_arg != NULL? */
-       retval = unlink(G.ftp_arg);
-       if (retval != 0)
-               cmdio_write(FTP_FILEFAIL, "Delete failed");
-       else
-               cmdio_write(FTP_DELEOK, "Delete successful");
+       if (!G.ftp_arg || unlink(G.ftp_arg) != 0) {
+               WRITE_ERR(FTP_FILEFAIL);
+               return;
+       }
+       WRITE_OK(FTP_DELEOK);
 }
-#endif /* ENABLE_FEATURE_FTP_WRITE */
 
 static void
-handle_rest(void)
+handle_rnfr(void)
 {
-       /* When ftp_arg == NULL simply restart from beginning */
-       G.restart_pos = xatoi_u(G.ftp_arg);
-       cmdio_write(FTP_RESTOK, "Restart OK");
+       free(G.rnfr_filename);
+       G.rnfr_filename = xstrdup(G.ftp_arg);
+       WRITE_OK(FTP_RNFROK);
 }
 
-#if ENABLE_FEATURE_FTP_WRITE
 static void
-handle_rnfr(void)
+handle_rnto(void)
 {
-       struct stat statbuf;
        int retval;
 
-       /* Clear old value */
+       /* If we didn't get a RNFR, throw a wobbly */
+       if (G.rnfr_filename == NULL || G.ftp_arg == NULL) {
+               cmdio_write_raw(STR(FTP_NEEDRNFR)" Use RNFR first\r\n");
+               return;
+       }
+
+       retval = rename(G.rnfr_filename, G.ftp_arg);
        free(G.rnfr_filename);
+       G.rnfr_filename = NULL;
 
-       /* Does it exist? Do we need check if ftp_arg != NULL? */
-       retval = stat(G.ftp_arg, &statbuf);
-       if (retval == 0) {
-               /* Yes */
-               G.rnfr_filename = xstrdup(G.ftp_arg);
-               cmdio_write(FTP_RNFROK, "Ready for RNTO");
-       } else
-               cmdio_write(FTP_FILEFAIL, "RNFR command failed");
+       if (retval) {
+               WRITE_ERR(FTP_FILEFAIL);
+               return;
+       }
+       WRITE_OK(FTP_RENAMEOK);
 }
 
 static void
-handle_rnto(void)
+handle_upload_common(int is_append, int is_unique)
 {
-       int retval;
+       struct stat statbuf;
+       char *tempname;
+       off_t bytes_transferred;
+       off_t offset;
+       int local_file_fd;
+       int remote_fd;
 
-       /* If we didn't get a RNFR, throw a wobbly */
-       if (G.rnfr_filename == NULL) {
-               cmdio_write(FTP_NEEDRNFR, "RNFR required first");
+       offset = G.restart_pos;
+       G.restart_pos = 0;
+
+       if (!port_or_pasv_was_seen())
+               return; /* port_or_pasv_was_seen emitted error response */
+
+       tempname = NULL;
+       local_file_fd = -1;
+       if (is_unique) {
+               tempname = xstrdup(" FILE: uniq.XXXXXX");
+               local_file_fd = mkstemp(tempname + 7);
+       } else if (G.ftp_arg) {
+               int flags = O_WRONLY | O_CREAT | O_TRUNC;
+               if (is_append)
+                       flags = O_WRONLY | O_CREAT | O_APPEND;
+               if (offset)
+                       flags = O_WRONLY | O_CREAT;
+               local_file_fd = open(G.ftp_arg, flags, 0666);
+       }
+
+       if (local_file_fd < 0
+        || fstat(local_file_fd, &statbuf) != 0
+        || !S_ISREG(statbuf.st_mode)
+       ) {
+               free(tempname);
+               WRITE_ERR(FTP_UPLOADFAIL);
+               if (local_file_fd >= 0)
+                       goto close_local_and_bail;
                return;
        }
+       G.local_file_fd = local_file_fd;
 
-       /* XXX Do we need check if ftp_arg != NULL? */
-       retval = rename(G.rnfr_filename, G.ftp_arg);
+       if (offset)
+               xlseek(local_file_fd, offset, SEEK_SET);
 
-       free(G.rnfr_filename);
+       remote_fd = get_remote_transfer_fd(tempname ? tempname : " Ok to send data");
+       free(tempname);
+
+       if (remote_fd < 0)
+               goto close_local_and_bail;
 
-       if (retval == 0)
-               cmdio_write(FTP_RENAMEOK, "Rename successful");
+       bytes_transferred = bb_copyfd_eof(remote_fd, local_file_fd);
+       close(remote_fd);
+       if (bytes_transferred < 0)
+               WRITE_ERR(FTP_BADSENDFILE);
        else
-               cmdio_write(FTP_FILEFAIL, "Rename failed");
+               WRITE_OK(FTP_TRANSFEROK);
+
+ close_local_and_bail:
+       close(local_file_fd);
+       G.local_file_fd = 0;
 }
-#endif /* ENABLE_FEATURE_FTP_WRITE */
 
 static void
-handle_nlst(void)
+handle_stor(void)
 {
-       handle_dir_common(0, 0);
+       handle_upload_common(0, 0);
 }
 
-#if ENABLE_FEATURE_FTP_WRITE
 static void
 handle_appe(void)
 {
+       G.restart_pos = 0;
        handle_upload_common(1, 0);
 }
-#endif
 
-static void
-handle_help(void)
-{
-       cmdio_write_hyphen(FTP_HELP, "Recognized commands:");
-       cmdio_write_raw(" ALLO CDUP CWD HELP LIST\r\n"
-                       " MODE NLST NOOP PASS PASV PORT PWD QUIT\r\n"
-                       " REST RETR STAT STRU SYST TYPE USER\r\n"
-#if ENABLE_FEATURE_FTP_WRITE
-                       " APPE DELE MKD RMD RNFR RNTO STOR STOU\r\n"
-#endif
-       );
-       cmdio_write(FTP_HELP, "Help OK");
-}
-
-#if ENABLE_FEATURE_FTP_WRITE
 static void
 handle_stou(void)
 {
+       G.restart_pos = 0;
        handle_upload_common(0, 1);
 }
-#endif
+#endif /* ENABLE_FEATURE_FTPD_WRITE */
 
-static void
-handle_stat(void)
+static uint32_t
+cmdio_get_cmd_and_arg(void)
 {
-       cmdio_write_hyphen(FTP_STATOK, "FTP server status:");
-       cmdio_write_raw(" TYPE: BINARY\r\n");
-       cmdio_write(FTP_STATOK, "End of status");
-}
+       int len;
+       uint32_t cmdval;
+       char *cmd;
 
-static void
-handle_stat_file(void)
-{
-       handle_dir_common(1, 1);
-}
+       alarm(G.timeout);
+
+       free(G.ftp_cmd);
+       {
+               /* Paranoia. Peer may send 1 gigabyte long cmd... */
+               /* Using separate len_on_stk instead of len optimizes
+                * code size (allows len to be in CPU register) */
+               size_t len_on_stk = 8 * 1024;
+               G.ftp_cmd = cmd = xmalloc_fgets_str_len(stdin, "\r\n", &len_on_stk);
+               if (!cmd)
+                       exit(0);
+               len = len_on_stk;
+       }
 
-/* TODO: libbb candidate (tftp has another copy) */
-static len_and_sockaddr *get_sock_lsa(int s)
-{
-       len_and_sockaddr *lsa;
-       socklen_t len = 0;
-
-       if (getsockname(s, NULL, &len) != 0)
-               return NULL;
-       lsa = xzalloc(LSA_LEN_SIZE + len);
-       lsa->len = len;
-       getsockname(s, &lsa->u.sa, &lsa->len);
-       return lsa;
+       /* De-escape telnet: 0xff,0xff => 0xff */
+       /* RFC959 says that ABOR, STAT, QUIT may be sent even during
+        * data transfer, and may be preceded by telnet's "Interrupt Process"
+        * code (two-byte sequence 255,244) and then by telnet "Synch" code
+        * 255,242 (byte 242 is sent with TCP URG bit using send(MSG_OOB)
+        * and may generate SIGURG on our side. See RFC854).
+        * So far we don't support that (may install SIGURG handler if we'd want to),
+        * but we need to at least remove 255,xxx pairs. lftp sends those. */
+       /* Then de-escape FTP: NUL => '\n' */
+       /* Testing for \xff:
+        * Create file named '\xff': echo Hello >`echo -ne "\xff"`
+        * Try to get it:            ftpget -v 127.0.0.1 Eff `echo -ne "\xff\xff"`
+        * (need "\xff\xff" until ftpget applet is fixed to do escaping :)
+        * Testing for embedded LF:
+        * LF_HERE=`echo -ne "LF\nHERE"`
+        * echo Hello >"$LF_HERE"
+        * ftpget -v 127.0.0.1 LF_HERE "$LF_HERE"
+        */
+       {
+               int dst, src;
+
+               /* Strip "\r\n" if it is there */
+               if (len != 0 && cmd[len - 1] == '\n') {
+                       len--;
+                       if (len != 0 && cmd[len - 1] == '\r')
+                               len--;
+                       cmd[len] = '\0';
+               }
+               src = strchrnul(cmd, 0xff) - cmd;
+               /* 99,99% there are neither NULs nor 255s and src == len */
+               if (src < len) {
+                       dst = src;
+                       do {
+                               if ((unsigned char)(cmd[src]) == 255) {
+                                       src++;
+                                       /* 255,xxx - skip 255 */
+                                       if ((unsigned char)(cmd[src]) != 255) {
+                                               /* 255,!255 - skip both */
+                                               src++;
+                                               continue;
+                                       }
+                                       /* 255,255 - retain one 255 */
+                               }
+                               /* NUL => '\n' */
+                               cmd[dst++] = cmd[src] ? cmd[src] : '\n';
+                               src++;
+                       } while (src < len);
+                       cmd[dst] = '\0';
+               }
+       }
+
+       if (G.verbose > 1)
+               verbose_log(cmd);
+
+       G.ftp_arg = strchr(cmd, ' ');
+       if (G.ftp_arg != NULL)
+               *G.ftp_arg++ = '\0';
+
+       /* Uppercase and pack into uint32_t first word of the command */
+       cmdval = 0;
+       while (*cmd)
+               cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20);
+
+       return cmdval;
 }
 
+#define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d)
+#define mk_const3(a,b,c)    ((a * 0x100 + b) * 0x100 + c)
+enum {
+       const_ALLO = mk_const4('A', 'L', 'L', 'O'),
+       const_APPE = mk_const4('A', 'P', 'P', 'E'),
+       const_CDUP = mk_const4('C', 'D', 'U', 'P'),
+       const_CWD  = mk_const3('C', 'W', 'D'),
+       const_DELE = mk_const4('D', 'E', 'L', 'E'),
+       const_EPSV = mk_const4('E', 'P', 'S', 'V'),
+       const_FEAT = mk_const4('F', 'E', 'A', 'T'),
+       const_HELP = mk_const4('H', 'E', 'L', 'P'),
+       const_LIST = mk_const4('L', 'I', 'S', 'T'),
+       const_MDTM = mk_const4('M', 'D', 'T', 'M'),
+       const_MKD  = mk_const3('M', 'K', 'D'),
+       const_MODE = mk_const4('M', 'O', 'D', 'E'),
+       const_NLST = mk_const4('N', 'L', 'S', 'T'),
+       const_NOOP = mk_const4('N', 'O', 'O', 'P'),
+       const_PASS = mk_const4('P', 'A', 'S', 'S'),
+       const_PASV = mk_const4('P', 'A', 'S', 'V'),
+       const_PORT = mk_const4('P', 'O', 'R', 'T'),
+       const_PWD  = mk_const3('P', 'W', 'D'),
+       /* Same as PWD. Reportedly used by windows ftp client */
+       const_XPWD = mk_const4('X', 'P', 'W', 'D'),
+       const_QUIT = mk_const4('Q', 'U', 'I', 'T'),
+       const_REST = mk_const4('R', 'E', 'S', 'T'),
+       const_RETR = mk_const4('R', 'E', 'T', 'R'),
+       const_RMD  = mk_const3('R', 'M', 'D'),
+       const_RNFR = mk_const4('R', 'N', 'F', 'R'),
+       const_RNTO = mk_const4('R', 'N', 'T', 'O'),
+       const_SIZE = mk_const4('S', 'I', 'Z', 'E'),
+       const_STAT = mk_const4('S', 'T', 'A', 'T'),
+       const_STOR = mk_const4('S', 'T', 'O', 'R'),
+       const_STOU = mk_const4('S', 'T', 'O', 'U'),
+       const_STRU = mk_const4('S', 'T', 'R', 'U'),
+       const_SYST = mk_const4('S', 'Y', 'S', 'T'),
+       const_TYPE = mk_const4('T', 'Y', 'P', 'E'),
+       const_USER = mk_const4('U', 'S', 'E', 'R'),
+
+#if !BB_MMU
+       OPT_l = (1 << 0),
+       OPT_1 = (1 << 1),
+#endif
+       BIT_A =        (!BB_MMU) * 2,
+       OPT_A = (1 << (BIT_A + 0)),
+       OPT_v = (1 << (BIT_A + 1)),
+       OPT_S = (1 << (BIT_A + 2)),
+       OPT_w = (1 << (BIT_A + 3)) * ENABLE_FEATURE_FTPD_WRITE,
+};
+
 int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int ftpd_main(int argc UNUSED_PARAM, char **argv)
 {
-       smallint user_was_specified = 0;
+#if ENABLE_FEATURE_FTPD_AUTHENTICATION
+       struct passwd *pw = NULL;
+       char *anon_opt = NULL;
+#endif
+       unsigned abs_timeout;
+       unsigned verbose_S;
+       smallint opts;
 
        INIT_G();
 
+       abs_timeout = 1 * 60 * 60;
+       verbose_S = 0;
+       G.timeout = 2 * 60;
+#if BB_MMU
+       opts = getopt32(argv, "^"   "AvS" IF_FEATURE_FTPD_WRITE("w")
+               "t:+T:+" IF_FEATURE_FTPD_AUTHENTICATION("a:")
+               "\0" "vv:SS",
+               &G.timeout, &abs_timeout, IF_FEATURE_FTPD_AUTHENTICATION(&anon_opt,)
+               &G.verbose, &verbose_S
+       );
+#else
+       opts = getopt32(argv, "^" "l1AvS" IF_FEATURE_FTPD_WRITE("w")
+               "t:+T:+" IF_FEATURE_FTPD_AUTHENTICATION("a:")
+               "\0" "vv:SS",
+               &G.timeout, &abs_timeout, IF_FEATURE_FTPD_AUTHENTICATION(&anon_opt,)
+               &G.verbose, &verbose_S
+       );
+       if (opts & (OPT_l|OPT_1)) {
+               /* Our secret backdoor to ls: see popen_ls() */
+               if (fchdir(3) != 0)
+                       _exit(127);
+               /* memset(&G, 0, sizeof(G)); - ls_main does it */
+               /* NB: in this case -A has a different meaning: like "ls -A" */
+               return ls_main(/*argc_unused*/ 0, argv);
+       }
+#endif
+       if (G.verbose < verbose_S)
+               G.verbose = verbose_S;
+       if (abs_timeout | G.timeout) {
+               if (abs_timeout == 0)
+                       abs_timeout = INT_MAX;
+               G.end_time = monotonic_sec() + abs_timeout;
+               if (G.timeout > abs_timeout)
+                       G.timeout = abs_timeout;
+       }
+       strcpy(G.msg_ok  + 4, MSG_OK );
+       strcpy(G.msg_err + 4, MSG_ERR);
+
        G.local_addr = get_sock_lsa(STDIN_FILENO);
        if (!G.local_addr) {
                /* This is confusing:
@@ -984,88 +1231,204 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
                 * failure */
        }
 
-       logmode = LOGMODE_SYSLOG;
-
-       USE_FEATURE_FTP_WRITE(G.write_enable =) getopt32(argv, "" USE_FEATURE_FTP_WRITE("w"));
-       if (argv[optind]) {
-               xchdir(argv[optind]);
-               chroot(".");
+       if (!(opts & OPT_v))
+               logmode = LOGMODE_NONE;
+       if (opts & OPT_S) {
+               /* LOG_NDELAY is needed since we may chroot later */
+               openlog(applet_name, LOG_PID | LOG_NDELAY, LOG_DAEMON);
+               logmode |= LOGMODE_SYSLOG;
        }
-
-//     if (G.local_addr->u.sa.sa_family != AF_INET)
-//             bb_error_msg_and_die("Only IPv4 is supported");
-
-       /* Signals. We'll always take -EPIPE rather than a rude signal, thanks */
-       signal(SIGPIPE, SIG_IGN);
-
-       /* Set up options on the command socket */
-       setsockopt(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY, &const_int_1, sizeof(const_int_1));
-       setsockopt(STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
-       setsockopt(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1));
-
-       cmdio_write(FTP_GREET, "Welcome");
-
-       while (1) {
-               uint32_t cmdval = cmdio_get_cmd_and_arg();
-
-               if (cmdval == const_USER) {
-                       if (G.ftp_arg == NULL || strcasecmp(G.ftp_arg, "anonymous") != 0)
-                               cmdio_write(FTP_LOGINERR, "Server is anonymous only");
-                       else {
-                               user_was_specified = 1;
-                               cmdio_write(FTP_GIVEPWORD, "Please specify the password");
+       if (logmode)
+               applet_name = xasprintf("%s[%u]", applet_name, (int)getpid());
+
+       //umask(077); - admin can set umask before starting us
+
+       /* Signals */
+       bb_signals(0
+               /* We'll always take EPIPE rather than a rude signal, thanks */
+               + (1 << SIGPIPE)
+               /* LIST command spawns chilren. Prevent zombies */
+               + (1 << SIGCHLD)
+               , SIG_IGN);
+
+       /* Set up options on the command socket (do we need these all? why?) */
+       setsockopt_1(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY);
+       setsockopt_keepalive(STDIN_FILENO);
+       /* Telnet protocol over command link may send "urgent" data,
+        * we prefer it to be received in the "normal" data stream: */
+       setsockopt_1(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE);
+
+       WRITE_OK(FTP_GREET);
+       signal(SIGALRM, timeout_handler);
+
+#if ENABLE_FEATURE_FTPD_AUTHENTICATION
+       if (!(opts & OPT_A)) {
+               while (1) {
+                       uint32_t cmdval = cmdio_get_cmd_and_arg();
+                       if (cmdval == const_USER) {
+                               if (anon_opt && strcmp(G.ftp_arg, "anonymous") == 0) {
+                                       pw = getpwnam(anon_opt);
+                                       if (pw)
+                                               break; /* does not even ask for password */
+                               }
+                               pw = getpwnam(G.ftp_arg);
+                               cmdio_write_raw(STR(FTP_GIVEPWORD)" Specify password\r\n");
+                       } else if (cmdval == const_PASS) {
+                               if (check_password(pw, G.ftp_arg) > 0) {
+                                       break;  /* login success */
+                               }
+                               cmdio_write_raw(STR(FTP_LOGINERR)" Login failed\r\n");
+                               pw = NULL;
+                       } else if (cmdval == const_QUIT) {
+                               WRITE_OK(FTP_GOODBYE);
+                               return 0;
+                       } else {
+                               cmdio_write_raw(STR(FTP_LOGINERR)" Login with USER+PASS\r\n");
                        }
-               } else if (cmdval == const_PASS) {
-                       if (user_was_specified)
-                               break;
-                       cmdio_write(FTP_NEEDUSER, "Login with USER");
-               } else if (cmdval == const_QUIT) {
-                       cmdio_write(FTP_GOODBYE, "Goodbye");
-                       return 0;
-               } else {
-                       cmdio_write(FTP_LOGINERR,
-                               "Login with USER and PASS");
                }
+               WRITE_OK(FTP_LOGINOK);
        }
+#endif
 
-       umask(077);
-       cmdio_write(FTP_LOGINOK, "Login successful");
+       /* Do this after auth, else /etc/passwd is not accessible */
+#if !BB_MMU
+       G.root_fd = -1;
+#endif
+       argv += optind;
+       if (argv[0]) {
+               const char *basedir = argv[0];
+#if !BB_MMU
+               G.root_fd = xopen("/", O_RDONLY | O_DIRECTORY);
+               close_on_exec_on(G.root_fd);
+#endif
+               if (chroot(basedir) == 0)
+                       basedir = "/";
+#if !BB_MMU
+               else {
+                       close(G.root_fd);
+                       G.root_fd = -1;
+               }
+#endif
+               /*
+                * If chroot failed, assume that we aren't root,
+                * and at least chdir to the specified DIR
+                * (older versions were dying with error message).
+                * If chroot worked, move current dir to new "/":
+                */
+               xchdir(basedir);
+       }
+
+#if ENABLE_FEATURE_FTPD_AUTHENTICATION
+       if (pw)
+               change_identity(pw);
+       /* else: -A is in effect */
+#endif
+
+       /* RFC-959 Section 5.1
+        * The following commands and options MUST be supported by every
+        * server-FTP and user-FTP, except in cases where the underlying
+        * file system or operating system does not allow or support
+        * a particular command.
+        * Type: ASCII Non-print, IMAGE, LOCAL 8
+        * Mode: Stream
+        * Structure: File, Record*
+        * (Record structure is REQUIRED only for hosts whose file
+        *  systems support record structure).
+        * Commands:
+        * USER, PASS, ACCT, [bbox: ACCT not supported]
+        * PORT, PASV,
+        * TYPE, MODE, STRU,
+        * RETR, STOR, APPE,
+        * RNFR, RNTO, DELE,
+        * CWD,  CDUP, RMD,  MKD,  PWD,
+        * LIST, NLST,
+        * SYST, STAT,
+        * HELP, NOOP, QUIT.
+        */
+       /* ACCOUNT (ACCT)
+        * "The argument field is a Telnet string identifying the user's account.
+        * The command is not necessarily related to the USER command, as some
+        * sites may require an account for login and others only for specific
+        * access, such as storing files. In the latter case the command may
+        * arrive at any time.
+        * There are reply codes to differentiate these cases for the automation:
+        * when account information is required for login, the response to
+        * a successful PASSword command is reply code 332. On the other hand,
+        * if account information is NOT required for login, the reply to
+        * a successful PASSword command is 230; and if the account information
+        * is needed for a command issued later in the dialogue, the server
+        * should return a 332 or 532 reply depending on whether it stores
+        * (pending receipt of the ACCounT command) or discards the command,
+        * respectively."
+        */
 
        while (1) {
                uint32_t cmdval = cmdio_get_cmd_and_arg();
 
                if (cmdval == const_QUIT) {
-                       cmdio_write(FTP_GOODBYE, "Goodbye");
+                       WRITE_OK(FTP_GOODBYE);
                        return 0;
                }
-               if (cmdval == const_PWD)
+               else if (cmdval == const_USER)
+                       /* This would mean "ok, now give me PASS". */
+                       /*WRITE_OK(FTP_GIVEPWORD);*/
+                       /* vsftpd can be configured to not require that,
+                        * and this also saves one roundtrip:
+                        */
+                       WRITE_OK(FTP_LOGINOK);
+               else if (cmdval == const_PASS)
+                       WRITE_OK(FTP_LOGINOK);
+               else if (cmdval == const_NOOP)
+                       WRITE_OK(FTP_NOOPOK);
+               else if (cmdval == const_TYPE)
+                       WRITE_OK(FTP_TYPEOK);
+               else if (cmdval == const_STRU)
+                       WRITE_OK(FTP_STRUOK);
+               else if (cmdval == const_MODE)
+                       WRITE_OK(FTP_MODEOK);
+               else if (cmdval == const_ALLO)
+                       WRITE_OK(FTP_ALLOOK);
+               else if (cmdval == const_SYST)
+                       cmdio_write_raw(STR(FTP_SYSTOK)" UNIX Type: L8\r\n");
+               else if (cmdval == const_PWD || cmdval == const_XPWD)
                        handle_pwd();
                else if (cmdval == const_CWD)
                        handle_cwd();
-               else if (cmdval == const_CDUP)
+               else if (cmdval == const_CDUP) /* cd .. */
                        handle_cdup();
+               /* HELP is nearly useless, but we can reuse FEAT for it */
+               /* lftp uses FEAT */
+               else if (cmdval == const_HELP || cmdval == const_FEAT)
+                       handle_feat(cmdval == const_HELP
+                                       ? STRNUM32(FTP_HELP)
+                                       : STRNUM32(FTP_STATOK)
+                       );
+               else if (cmdval == const_LIST) /* ls -l */
+                       handle_list();
+               else if (cmdval == const_NLST) /* "name list", bare ls */
+                       handle_nlst();
+               /* SIZE is crucial for wget's download indicator etc */
+               /* Mozilla, lftp use MDTM (presumably for caching) */
+               else if (cmdval == const_SIZE || cmdval == const_MDTM)
+                       handle_size_or_mdtm(cmdval == const_SIZE);
+               else if (cmdval == const_STAT) {
+                       if (G.ftp_arg == NULL)
+                               handle_stat();
+                       else
+                               handle_stat_file();
+               }
                else if (cmdval == const_PASV)
                        handle_pasv();
+               else if (cmdval == const_EPSV)
+                       handle_epsv();
                else if (cmdval == const_RETR)
                        handle_retr();
-               else if (cmdval == const_NOOP)
-                       cmdio_write(FTP_NOOPOK, "NOOP ok");
-               else if (cmdval == const_SYST)
-                       cmdio_write(FTP_SYSTOK, "UNIX Type: L8");
-               else if (cmdval == const_HELP)
-                       handle_help();
-               else if (cmdval == const_LIST)
-                       handle_list();
-               else if (cmdval == const_TYPE)
-                       handle_type();
                else if (cmdval == const_PORT)
                        handle_port();
                else if (cmdval == const_REST)
                        handle_rest();
-               else if (cmdval == const_NLST)
-                       handle_nlst();
-#if ENABLE_FEATURE_FTP_WRITE
-               else if (G.write_enable) {
+#if ENABLE_FEATURE_FTPD_WRITE
+               else if (opts & OPT_w) {
                        if (cmdval == const_STOR)
                                handle_stor();
                        else if (cmdval == const_MKD)
@@ -1074,45 +1437,19 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
                                handle_rmd();
                        else if (cmdval == const_DELE)
                                handle_dele();
-                       else if (cmdval == const_RNFR)
+                       else if (cmdval == const_RNFR) /* "rename from" */
                                handle_rnfr();
-                       else if (cmdval == const_RNTO)
+                       else if (cmdval == const_RNTO) /* "rename to" */
                                handle_rnto();
                        else if (cmdval == const_APPE)
                                handle_appe();
-                       else if (cmdval == const_STOU)
+                       else if (cmdval == const_STOU) /* "store unique" */
                                handle_stou();
+                       else
+                               goto bad_cmd;
                }
 #endif
-               else if (cmdval == const_STRU) {
-                       if (G.ftp_arg
-                        && (G.ftp_arg[0] | 0x20) == 'f'
-                        && G.ftp_arg[1] == '\0'
-                       ) {
-                               cmdio_write(FTP_STRUOK, "Structure set to F");
-                       } else
-                               cmdio_write(FTP_BADSTRU, "Bad STRU command");
-
-               } else if (cmdval == const_MODE) {
-                       if (G.ftp_arg
-                        && (G.ftp_arg[0] | 0x20) == 's'
-                        && G.ftp_arg[1] == '\0'
-                       ) {
-                               cmdio_write(FTP_MODEOK, "Mode set to S");
-                       } else
-                               cmdio_write(FTP_BADMODE, "Bad MODE command");
-               }
-               else if (cmdval == const_ALLO)
-                       cmdio_write(FTP_ALLOOK, "ALLO command ignored");
-               else if (cmdval == const_STAT) {
-                       if (G.ftp_arg == NULL)
-                               handle_stat();
-                       else
-                               handle_stat_file();
-               } else if (cmdval == const_USER)
-                       cmdio_write(FTP_LOGINERR, "Can't change to another user");
-               else if (cmdval == const_PASS)
-                       cmdio_write(FTP_LOGINOK, "Already logged in");
+#if 0
                else if (cmdval == const_STOR
                 || cmdval == const_MKD
                 || cmdval == const_RMD
@@ -1122,9 +1459,18 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
                 || cmdval == const_APPE
                 || cmdval == const_STOU
                ) {
-                       cmdio_write(FTP_NOPERM, "Permission denied");
-               } else {
-                       cmdio_write(FTP_BADCMD, "Unknown command");
+                       cmdio_write_raw(STR(FTP_NOPERM)" Permission denied\r\n");
+               }
+#endif
+               else {
+                       /* Which unsupported commands were seen in the wild?
+                        * (doesn't necessarily mean "we must support them")
+                        * foo 1.2.3: XXXX - comment
+                        */
+#if ENABLE_FEATURE_FTPD_WRITE
+ bad_cmd:
+#endif
+                       cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");
                }
        }
 }