ftpd: security tightened up:
[oweals/busybox.git] / networking / ftpd.c
index 114975d021693e45b4f28be0e0d15ebf832c72d4..22cec83a821a07d15c6f6c0579362e9525f6b9b7 100644 (file)
@@ -4,6 +4,8 @@
  *
  * Author: Adam Tkac <vonsch@gmail.com>
  *
+ * Licensed under GPLv2, see file LICENSE in this tarball for details.
+ *
  * 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.
  *
@@ -32,6 +34,8 @@
 #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 STR1(s) #s
 #define STR(s) STR1(s)
 
+/* Convert a constant to 3-digit string, packed into uint32_t */
 enum {
-       OPT_v = (1 << 0),
-       OPT_w = (1 << 1),
-
-#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,
 };
+#define STRNUM32(s) (uint32_t)(0 \
+       | (('0' + ((s) / 1 % 10)) << SHIFT0) \
+       | (('0' + ((s) / 10 % 10)) << SHIFT1) \
+       | (('0' + ((s) / 100 % 10)) << SHIFT2) \
+)
 
 struct globals {
        char *p_control_line_buf;
        len_and_sockaddr *local_addr;
        len_and_sockaddr *port_addr;
        int pasv_listen_fd;
-       int data_fd;
+       int proc_self_fd;
        off_t restart_pos;
-       char *ftp_cmp;
+       char *ftp_cmd;
        char *ftp_arg;
 #if ENABLE_FEATURE_FTP_WRITE
        char *rnfr_filename;
 #endif
-       smallint opts;
 };
 #define G (*(struct globals*)&bb_common_bufsiz1)
 #define INIT_G() do { } 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;
 
-       remain = str;
-       remainlen = strlen(str);
-       tolen = strlen(to);
+       append = (char)escapee;
+       escapee >>= 8;
 
-       /* Simply alloc strlen(str)*strlen(to). "to" is max 2 so it's ok */
-       ret = xmalloc(remainlen * tolen + 1);
-       retlen = 0;
+       remainlen = strlen(str);
+       retlen = strlen(prepend);
+       ret = xmalloc(retlen + remainlen * 2 + 1 + 1);
+       strcpy(ret, prepend);
 
        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);
+               found = strchrnul(str, escapee);
+               chunklen = found - str + 1;
+
+               /* Copy chunk up to and including escapee (or NUL) to ret */
+               memcpy(ret + retlen, str, chunklen);
+               retlen += chunklen;
+
+               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)
 {
-       while ((str = strchr(str, from)) != NULL)
-               *str++ = to;
+       char *p = str;
+       while (*p) {
+               if (*p == from)
+                       *p = to;
+               p++;
+       }
+       return p - str;
 }
 
+/* NB: status_str is char[4] packed into uint32_t */
 static void
-cmdio_write(unsigned status, const char *str)
+cmdio_write(uint32_t status_str, const char *str)
 {
-       char *escaped_str, *response;
+       char *response;
        int len;
 
        /* FTP allegedly uses telnet protocol for command link.
         * In telnet, 0xff is an escape char, and needs to be escaped: */
-       escaped_str = replace_text(str, '\xff', "\xff\xff");
-
-       response = xasprintf("%u%s\r", status, escaped_str);
-       free(escaped_str);
+       response = escape_text((char *) &status_str, str, (0xff << 8) + '\r');
 
        /* ?! does FTP send embedded LFs as NULs? wow */
-       len = strlen(response);
-       replace_char(response, '\n', '\0');
+       len = replace_char(response, '\n', '\0');
 
        response[len++] = '\n'; /* tack on trailing '\n' */
        xwrite(STDOUT_FILENO, response, len);
@@ -206,18 +184,16 @@ cmdio_write_raw(const char *p_text)
 static void
 handle_pwd(void)
 {
-       char *cwd, *promoted_cwd, *response;
+       char *cwd, *response;
 
        cwd = xrealloc_getcwd_or_warn(NULL);
        if (cwd == NULL)
                cwd = xstrdup("");
 
        /* We have to promote each " to "" */
-       promoted_cwd = replace_text(cwd, '\"', "\"\"");
+       response = escape_text(" \"", cwd, ('"' << 8) + '"');
        free(cwd);
-       response = xasprintf(" \"%s\"", promoted_cwd);
-       free(promoted_cwd);
-       cmdio_write(FTP_PWDOK, response);
+       cmdio_write(STRNUM32(FTP_PWDOK), response);
        free(response);
 }
 
@@ -238,59 +214,75 @@ handle_cdup(void)
        handle_cwd();
 }
 
-//static void
-//handle_type(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_ok(FTP_TYPEOK);
-//     } else {
-//             cmdio_write_error(FTP_BADCMD);
-//     }
-//}
-
 static void
 handle_stat(void)
 {
        cmdio_write_raw(STR(FTP_STATOK)"-FTP server status:\r\n"
-                       "TYPE: BINARY\r\n"
+                       " TYPE: BINARY\r\n"
                        STR(FTP_STATOK)" Ok\r\n");
 }
 
+/* TODO: implement FEAT. Example:
+# 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
 handle_help(void)
 {
        cmdio_write_raw(STR(FTP_HELP)"-Commands:\r\n"
-                       "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"
+                       " ALLO CDUP CWD EPSV HELP LIST\r\n"
+                       " MODE NLST NOOP PASS PASV PORT PWD QUIT\r\n"
+                       " REST RETR SIZE STAT STRU SYST TYPE USER\r\n"
 #if ENABLE_FEATURE_FTP_WRITE
-                       "APPE DELE MKD RMD RNFR RNTO STOR STOU\r\n"
+                       " APPE DELE MKD RMD RNFR RNTO STOR STOU\r\n"
 #endif
                        STR(FTP_HELP)" Ok\r\n");
 }
 
 /* Download commands */
 
-static void
-init_data_sock_params(int sock_fd)
+static inline int
+port_active(void)
 {
-       struct linger linger;
-
-       G.data_fd = sock_fd;
+       return (G.port_addr != NULL);
+}
 
-       memset(&linger, 0, sizeof(linger));
-       linger.l_onoff = 1;
-       linger.l_linger = 32767;
+static inline int
+pasv_active(void)
+{
+       return (G.pasv_listen_fd > STDOUT_FILENO);
+}
 
-       setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
-       setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger));
+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
 ftpdataio_get_pasv_fd(void)
 {
@@ -303,70 +295,40 @@ ftpdataio_get_pasv_fd(void)
                return remote_fd;
        }
 
-       init_data_sock_params(remote_fd);
+       setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
        return remote_fd;
 }
 
-static int
-ftpdataio_get_port_fd(void)
-{
-       int remote_fd;
-
-       /* Do we want to die or print error to client? */
-       remote_fd = xconnect_stream(G.port_addr);
-
-       init_data_sock_params(remote_fd);
-       return remote_fd;
-}
-
-static void
-ftpdataio_dispose_transfer_fd(void)
-{
-       /* 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;
-}
-
-static inline int
-port_active(void)
-{
-       return (G.port_addr != NULL);
-}
-
-static inline int
-pasv_active(void)
-{
-       return (G.pasv_listen_fd > STDOUT_FILENO);
-}
-
+/* 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)
 {
        int remote_fd;
 
        if (pasv_active())
+               /* On error, emits error code to the peer */
                remote_fd = ftpdataio_get_pasv_fd();
        else
-               remote_fd = ftpdataio_get_port_fd();
+               /* Exits on error */
+               remote_fd = xconnect_stream(G.port_addr);
+
+       port_pasv_cleanup();
 
        if (remote_fd < 0)
                return remote_fd;
 
-       cmdio_write(FTP_DATACONN, p_status_msg);
+       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_raw(STR(FTP_BADSENDCONN)" Use PORT or PASV first\r\n");
@@ -376,98 +338,118 @@ data_transfer_checks_ok(void)
        return 1;
 }
 
-static void
-port_pasv_cleanup(void)
+/* Exits on error */
+static unsigned
+bind_for_passive_mode(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;
+       int fd;
+       unsigned port;
+
+       port_pasv_cleanup();
+
+       G.pasv_listen_fd = fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
+       setsockopt_reuseaddr(fd);
+
+       set_nport(G.local_addr, 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);
+
+       port = get_nport(&G.local_addr->u.sa);
+       port = ntohs(port);
+       return port;
 }
 
+/* Exits on error */
 static void
 handle_pasv(void)
 {
-       int bind_retries = 10;
-       unsigned short port;
-       enum { min_port = 1024, max_port = 65535 };
+       unsigned port;
        char *addr, *response;
 
-       port_pasv_cleanup();
-
-       G.pasv_listen_fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
-       setsockopt_reuseaddr(G.pasv_listen_fd);
+       port = bind_for_passive_mode();
 
-       /* TODO bind() with port == 0 and then call getsockname */
-       while (--bind_retries) {
-               port = rand() % max_port;
-               if (port < min_port) {
-                       port += min_port;
-               }
-
-               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;
-       }
-
-       if (!bind_retries)
-               bb_error_msg_and_die("can't create pasv socket");
-
-       addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa);
+       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, '.', ',');
 
-       response = xasprintf(" Entering Passive Mode (%s,%u,%u)",
+       response = xasprintf(STR(FTP_PASVOK)" Entering Passive Mode (%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
+handle_epsv(void)
+{
+       unsigned port;
+       char *response;
 
-       cmdio_write(FTP_PASVOK, response);
+       port = bind_for_passive_mode();
+       response = xasprintf(STR(FTP_EPSVOK)" EPSV Ok (|||%u|)\r\n", port);
+       cmdio_write_raw(response);
        free(response);
 }
 
 static void
 handle_port(void)
 {
-       unsigned short port;
-       char *raw, *port_part;
-       len_and_sockaddr *lsa = NULL;
+       unsigned port, port_hi;
+       char *raw, *comma;
+       socklen_t peer_ipv4_len;
+       struct sockaddr_in peer_ipv4;
+       struct in_addr port_ipv4_sin_addr;
 
        port_pasv_cleanup();
 
        raw = G.ftp_arg;
 
-       /* buglets:
-        * xatou16 will accept wrong input,
-        * xatou16 will exit instead of generating error to peer
-        */
+       /* PORT command format makes sense only over IPv4 */
+       if (!raw || G.local_addr->u.sa.sa_family != AF_INET) {
+ bail:
+               cmdio_write_error(FTP_BADCMD);
+               return;
+       }
 
-       port_part = strrchr(raw, ',');
-       if (port_part == NULL)
+       comma = strrchr(raw, ',');
+       if (comma == NULL)
+               goto bail;
+       *comma = '\0';
+       port = bb_strtou(&comma[1], NULL, 10);
+       if (errno || port > 0xff)
                goto bail;
-       port = xatou16(&port_part[1]);
-       *port_part = '\0';
 
-       port_part = strrchr(raw, ',');
-       if (port_part == NULL)
+       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 |= xatou16(&port_part[1]) << 8;
-       *port_part = '\0';
+       port |= port_hi << 8;
 
        replace_char(raw, ',', '.');
-       lsa = xdotted2sockaddr(raw, port);
 
-       if (lsa == NULL) {
- bail:
-               cmdio_write_error(FTP_BADCMD);
-               return;
-       }
+       /* 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;
 
-       G.port_addr = lsa;
+       G.port_addr = xdotted2sockaddr(raw, port);
        cmdio_write_ok(FTP_PORTOK);
 }
 
@@ -483,39 +465,38 @@ 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 */
 
        /* O_NONBLOCK is useful if file happens to be a device node */
-       opened_file = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1;
-       if (opened_file < 0) {
+       local_file_fd = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1;
+       if (local_file_fd < 0) {
                cmdio_write_error(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_error(FTP_FILEFAIL);
                goto file_close_out;
        }
 
-       /* 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)",
@@ -523,206 +504,151 @@ handle_retr(void)
        remote_fd = get_remote_transfer_fd(response);
        free(response);
        if (remote_fd < 0)
-               goto port_pasv_cleanup_out;
+               goto file_close_out;
+
+/* TODO: if we'll implement timeout, this will need more clever handling.
+ * Perhaps alarm(N) + checking that current position on local_file_fd
+ * is advancing. As of now, peer may stall us indefinitely.
+ */
 
-       trans_ret = bb_copyfd_eof(opened_file, remote_fd);
-       ftpdataio_dispose_transfer_fd();
-       if (trans_ret < 0)
+       bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd);
+       close(remote_fd);
+       if (bytes_transferred < 0)
                cmdio_write_error(FTP_BADSENDFILE);
        else
                cmdio_write_ok(FTP_TRANSFEROK);
 
- port_pasv_cleanup_out:
-       port_pasv_cleanup();
-
  file_close_out:
-       close(opened_file);
+       close(local_file_fd);
 }
 
 /* List commands */
 
-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;
-       }
-
-       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;
-}
-
-static void
-write_filestats(int fd, const char *filename,
-                               const struct stat *statbuf)
-{
-       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);
-
-       if (statbuf != NULL) {
-               size = statbuf->st_size;
-
-               if (S_ISLNK(statbuf->st_mode))
-                       /* Damn symlink... */
-                       lnkname = xmalloc_readlink(filename);
-
-               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);
-
-       xwrite_str(fd, stats);
-       free(stats);
-       if (lnkname != NULL) {
-               xwrite_str(fd, " -> ");
-               xwrite_str(fd, lnkname);
-               free(lnkname);
-       }
-       xwrite_str(fd, "\r\n");
-}
-
-static void
-write_dirstats(int fd, const char *dname, int details)
+static int
+popen_ls(const char *opt)
 {
-       DIR *dir;
-       struct dirent *dirent;
-       struct stat statbuf;
-       char *filename;
-
-       dir = xopendir(dname);
-
-       for (;;) {
-               dirent = readdir(dir);
-               if (dirent == NULL)
-                       break;
+       char *cwd;
+       const char *argv[5] = { "ftpd", opt, NULL, G.ftp_arg, NULL };
+       struct fd_pair outfd;
+       pid_t pid;
 
-               /* Ignore . and .. */
-               if (dirent->d_name[0] == '.') {
-                       if (dirent->d_name[1] == '\0'
-                        || (dirent->d_name[1] == '.' && dirent->d_name[2] == '\0')
-                       ) {
-                               continue;
-                       }
+       cwd = xrealloc_getcwd_or_warn(NULL);
+       xpiped_pair(outfd);
+
+       /*fflush(NULL); - so far we dont use stdio on output */
+       pid = vfork();
+       switch (pid) {
+       case -1:  /* failure */
+               bb_perror_msg_and_die("vfork");
+       case 0:  /* child */
+               /* NB: close _first_, then move fds! */
+               close(outfd.rd);
+               xmove_fd(outfd.wr, STDOUT_FILENO);
+               close(STDIN_FILENO);
+               /* xopen("/dev/null", O_RDONLY); - chroot may lack it! */
+               if (fchdir(G.proc_self_fd) == 0) {
+                       close(G.proc_self_fd);
+                       argv[2] = cwd;
+                       /* ftpd ls helper chdirs to argv[2],
+                        * preventing peer from seeing /proc/self
+                        */
+                       execv("exe", (char**) argv);
                }
-
-               if (details) {
-                       filename = xasprintf("%s/%s", dname, dirent->d_name);
-                       if (lstat(filename, &statbuf) != 0) {
-                               free(filename);
-                               break;
-                       }
-               } else
-                       filename = xstrdup(dirent->d_name);
-
-               write_filestats(fd, filename, details ? &statbuf : NULL);
-               free(filename);
+               _exit(127);
        }
 
-       closedir(dir);
+       /* parent */
+       close(outfd.wr);
+       free(cwd);
+       return outfd.rd;
 }
 
+enum {
+       USE_CTRL_CONN = 1,
+       LONG_LISTING = 2,
+};
+
 static void
-handle_dir_common(int full_details, int stat_cmd)
+handle_dir_common(int opts)
 {
-       int fd;
-       struct stat statbuf;
-
-       if (!stat_cmd && !data_transfer_checks_ok())
-               return;
-
-       if (stat_cmd) {
-               fd = STDIN_FILENO;
+       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 */
+
+       /* -n prevents user/groupname display,
+        * which can be problematic in chroot */
+       ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1");
+       ls_fp = fdopen(ls_fd, "r");
+       if (!ls_fp) /* never happens. paranoia */
+               bb_perror_msg_and_die("fdopen");
+
+       if (opts & USE_CTRL_CONN) {
+               /* STAT <filename> */
                cmdio_write_raw(STR(FTP_STATFILE_OK)"-Status follows:\r\n");
+               while (1) {
+                       line = xmalloc_fgetline(ls_fp);
+                       if (!line)
+                               break;
+                       cmdio_write(0, line); /* hack: 0 results in no status at all */
+                       free(line);
+               }
+               cmdio_write_ok(FTP_STATFILE_OK);
        } else {
-               fd = get_remote_transfer_fd(" Here comes the directory listing");
-               if (fd < 0)
-                       goto bail;
-       }
-
-       if (G.ftp_arg) {
-               if (lstat(G.ftp_arg, &statbuf) != 0) {
-                       /* Dir doesn't exist => return ok to client */
-                       goto bail;
+               /* LIST/NLST [<filename>] */
+               int remote_fd = get_remote_transfer_fd(" Here comes the directory listing");
+               if (remote_fd >= 0) {
+                       while (1) {
+                               line = xmalloc_fgetline(ls_fp);
+                               if (!line)
+                                       break;
+                               /* I've seen clients complaining when they
+                                * are fed with ls output with bare '\n'.
+                                * Pity... that would be much simpler.
+                                */
+                               xwrite_str(remote_fd, line);
+                               xwrite(remote_fd, "\r\n", 2);
+                               free(line);
+                       }
                }
-               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();
-               port_pasv_cleanup();
+               close(remote_fd);
                cmdio_write_ok(FTP_TRANSFEROK);
-       } else
-               cmdio_write_ok(FTP_STATFILE_OK);
+       }
+       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_nlst(void)
 {
-       handle_dir_common(0, 0);
+       handle_dir_common(0);
 }
-
 static void
 handle_stat_file(void)
 {
-       handle_dir_common(1, 1);
+       handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
+}
+
+static void
+handle_size(void)
+{
+       struct stat statbuf;
+       char buf[sizeof(STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n") + sizeof(off_t)*3];
+
+       if (!G.ftp_arg
+        || stat(G.ftp_arg, &statbuf) != 0
+        || !S_ISREG(statbuf.st_mode)
+       ) {
+               cmdio_write_error(FTP_FILEFAIL);
+               return;
+       }
+       sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size);
+       cmdio_write_raw(buf);
 }
 
 /* Upload commands */
@@ -791,18 +717,20 @@ handle_rnto(void)
 static void
 handle_upload_common(int is_append, int is_unique)
 {
-       char *tempname = NULL;
-       int trans_ret;
+       struct stat statbuf;
+       char *tempname;
+       off_t bytes_transferred;
+       off_t offset;
        int local_file_fd;
        int remote_fd;
-       off_t offset;
 
        offset = G.restart_pos;
        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 */
 
+       tempname = NULL;
        local_file_fd = -1;
        if (is_unique) {
                tempname = xstrdup(" FILE: uniq.XXXXXX");
@@ -815,13 +743,17 @@ handle_upload_common(int is_append, int is_unique)
                        flags = O_WRONLY | O_CREAT;
                local_file_fd = open(G.ftp_arg, flags, 0666);
        }
-       if (local_file_fd < 0) {
+
+       if (local_file_fd < 0
+        || fstat(local_file_fd, &statbuf) != 0
+        || !S_ISREG(statbuf.st_mode)
+       ) {
                cmdio_write_error(FTP_UPLOADFAIL);
+               if (local_file_fd >= 0)
+                       goto close_local_and_bail;
                return;
        }
 
-       /* TODO: paranoia: fstat it, refuse to do anything if it's not a regular file */
-
        if (offset)
                xlseek(local_file_fd, offset, SEEK_SET);
 
@@ -829,18 +761,21 @@ handle_upload_common(int is_append, int is_unique)
        free(tempname);
 
        if (remote_fd < 0)
-               goto bail;
+               goto close_local_and_bail;
 
-       trans_ret = bb_copyfd_eof(remote_fd, local_file_fd);
-       ftpdataio_dispose_transfer_fd();
+/* TODO: if we'll implement timeout, this will need more clever handling.
+ * Perhaps alarm(N) + checking that current position on local_file_fd
+ * is advancing. As of now, peer may stall us indefinitely.
+ */
 
-       if (trans_ret < 0)
+       bytes_transferred = bb_copyfd_eof(remote_fd, local_file_fd);
+       close(remote_fd);
+       if (bytes_transferred < 0)
                cmdio_write_error(FTP_BADSENDFILE);
        else
                cmdio_write_ok(FTP_TRANSFEROK);
 
- bail:
-       port_pasv_cleanup();
+ close_local_and_bail:
        close(local_file_fd);
 }
 
@@ -872,23 +807,24 @@ cmdio_get_cmd_and_arg(void)
        uint32_t cmdval;
        char *cmd;
 
-       free(G.ftp_cmp);
+       free(G.ftp_cmd);
        len = 8 * 1024; /* Paranoia. Peer may send 1 gigabyte long cmd... */
-       G.ftp_cmp = cmd = xmalloc_reads(STDIN_FILENO, NULL, &len);
+       G.ftp_cmd = cmd = xmalloc_reads(STDIN_FILENO, NULL, &len);
        if (!cmd)
                exit(0);
 
+       /* Trailing '\n' is already stripped, strip '\r' */
        len = strlen(cmd) - 1;
-       while (len >= 0 && cmd[len] == '\r') {
+       while ((ssize_t)len >= 0 && cmd[len] == '\r') {
                cmd[len] = '\0';
                len--;
        }
 
        G.ftp_arg = strchr(cmd, ' ');
-       if (G.ftp_arg != NULL) {
-               *G.ftp_arg = '\0';
-               G.ftp_arg++;
-       }
+       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);
@@ -896,9 +832,62 @@ cmdio_get_cmd_and_arg(void)
        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_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_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'),
+
+       OPT_l = (1 << 0),
+       OPT_1 = (1 << 1),
+       OPT_v = (1 << 2),
+       OPT_S = (1 << 3),
+       OPT_w = (1 << 4),
+};
+
 int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
-int ftpd_main(int argc UNUSED_PARAM, char **argv)
+int ftpd_main(int argc, char **argv)
 {
+       smallint opts;
+
+       opts = getopt32(argv, "l1vS" USE_FEATURE_FTP_WRITE("w"));
+
+       if (opts & (OPT_l|OPT_1)) {
+               /* Our secret backdoor to ls */
+/* TODO: pass -n too? */
+               xchdir(argv[2]);
+               argv[2] = (char*)"--";
+               return ls_main(argc, argv);
+       }
+
        INIT_G();
 
        G.local_addr = get_sock_lsa(STDIN_FILENO);
@@ -912,12 +901,15 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
                 * failure */
        }
 
-       G.opts = getopt32(argv, "v" USE_FEATURE_FTP_WRITE("w"));
+       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;
+       }
 
-       openlog(applet_name, LOG_PID, LOG_DAEMON);
-       logmode |= LOGMODE_SYSLOG;
-       if (!(G.opts & OPT_v))
-               logmode = LOGMODE_SYSLOG;
+       G.proc_self_fd = xopen("/proc/self", O_RDONLY | O_DIRECTORY);
 
        if (argv[optind]) {
                xchdir(argv[optind]);
@@ -934,7 +926,7 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
        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_raw(STR(FTP_GREET)" Welcome\r\n");
+       cmdio_write_ok(FTP_GREET);
 
 #ifdef IF_WE_WANT_TO_REQUIRE_LOGIN
        {
@@ -1009,65 +1001,54 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
                        cmdio_write_ok(FTP_GOODBYE);
                        return 0;
                }
-               if (cmdval == const_PWD)
+               else if (cmdval == const_USER)
+                       cmdio_write_ok(FTP_GIVEPWORD);
+               else if (cmdval == const_PASS)
+                       cmdio_write_ok(FTP_LOGINOK);
+               else if (cmdval == const_NOOP)
+                       cmdio_write_ok(FTP_NOOPOK);
+               else if (cmdval == const_TYPE)
+                       cmdio_write_ok(FTP_TYPEOK);
+               else if (cmdval == const_STRU)
+                       cmdio_write_ok(FTP_STRUOK);
+               else if (cmdval == const_MODE)
+                       cmdio_write_ok(FTP_MODEOK);
+               else if (cmdval == const_ALLO)
+                       cmdio_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)
                        handle_pwd();
                else if (cmdval == const_CWD)
                        handle_cwd();
                else if (cmdval == const_CDUP) /* cd .. */
                        handle_cdup();
-               else if (cmdval == const_PASV)
-                       handle_pasv();
-               else if (cmdval == const_RETR)
-                       handle_retr();
-               else if (cmdval == const_NOOP)
-                       cmdio_write_ok(FTP_NOOPOK);
-               else if (cmdval == const_SYST)
-                       cmdio_write_raw(STR(FTP_SYSTOK)" UNIX Type: L8\r\n");
                else if (cmdval == const_HELP)
                        handle_help();
                else if (cmdval == const_LIST) /* ls -l */
                        handle_list();
-               else if (cmdval == const_TYPE)
-                       //handle_type();
-                       cmdio_write_ok(FTP_TYPEOK);
-               else if (cmdval == const_PORT)
-                       handle_port();
-               else if (cmdval == const_REST)
-                       handle_rest();
                else if (cmdval == const_NLST) /* "name list", bare ls */
                        handle_nlst();
-               else if (cmdval == const_STRU) {
-                       //if (G.ftp_arg
-                       // && (G.ftp_arg[0] | 0x20) == 'f'
-                       // && G.ftp_arg[1] == '\0'
-                       //) {
-                               cmdio_write_ok(FTP_STRUOK);
-                       //} else
-                       //      cmdio_write_raw(STR(FTP_BADSTRU)" Bad STRU command\r\n");
-               } else if (cmdval == const_MODE) {
-                       //if (G.ftp_arg
-                       // && (G.ftp_arg[0] | 0x20) == 's'
-                       // && G.ftp_arg[1] == '\0'
-                       //) {
-                               cmdio_write_ok(FTP_MODEOK);
-                       //} else
-                       //      cmdio_write_raw(STR(FTP_BADMODE)" Bad MODE command\r\n");
-               }
-               else if (cmdval == const_ALLO)
-                       cmdio_write_ok(FTP_ALLOOK);
+               else if (cmdval == const_SIZE)
+                       handle_size();
                else if (cmdval == const_STAT) {
                        if (G.ftp_arg == NULL)
                                handle_stat();
                        else
                                handle_stat_file();
-               } else if (cmdval == const_USER) {
-                       /* FTP_LOGINERR confuses clients: */
-                       /* cmdio_write_raw(STR(FTP_LOGINERR)" Can't change to another user\r\n"); */
-                       cmdio_write_ok(FTP_GIVEPWORD);
-               } else if (cmdval == const_PASS)
-                       cmdio_write_ok(FTP_LOGINOK);
+               }
+               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_PORT)
+                       handle_port();
+               else if (cmdval == const_REST)
+                       handle_rest();
 #if ENABLE_FEATURE_FTP_WRITE
-               else if (G.opts & OPT_w) {
+               else if (opts & OPT_w) {
                        if (cmdval == const_STOR)
                                handle_stor();
                        else if (cmdval == const_MKD)
@@ -1102,10 +1083,8 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
                else {
                        /* Which unsupported commands were seen in the wild?
                         * (doesn't necessarily mean "we must support them")
-                        * wget 1.11.4: SIZE - todo
                         * lftp 3.6.3: FEAT - is it useful?
                         *             MDTM - works fine without it anyway
-                        * IPv6-style PASV: "EPSV 2" - todo
                         */
                        cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");
                }