Makefile.flags: restrict Wno-constant-logical-operand and Wno-string-plus-int options...
[oweals/busybox.git] / networking / ftpd.c
index 583d7b387a468a502fc279a3c92bbfa700f342d8..6ca231c90b203d4ee54a6fb7fe46c5188b61441b 100644 (file)
@@ -4,13 +4,85 @@
  *
  * Author: Adam Tkac <vonsch@gmail.com>
  *
- * Licensed under GPLv2, see file LICENSE in this tarball for details.
+ * 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.
+ *
+ * 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>
 
@@ -85,25 +157,33 @@ enum {
 
 struct globals {
        int pasv_listen_fd;
-       int proc_self_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;
        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;
 #endif
        /* 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];
-};
-#define G (*(struct globals*)&bb_common_bufsiz1)
+} 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);*/ \
@@ -158,6 +238,12 @@ replace_char(char *str, char from, char to)
        return p - str;
 }
 
+static void
+verbose_log(const char *str)
+{
+       bb_error_msg("%.*s", (int)strcspn(str, "\r\n"), str);
+}
+
 /* NB: status_str is char[4] packed into uint32_t */
 static void
 cmdio_write(uint32_t status_str, const char *str)
@@ -165,23 +251,27 @@ cmdio_write(uint32_t status_str, const char *str)
        char *response;
        int len;
 
-       /* FTP allegedly uses telnet protocol for command link.
+       /* 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');
 
-       /* ?! does FTP send embedded LFs as NULs? wow */
+       /* FTP sends embedded LFs as NULs */
        len = replace_char(response, '\n', '\0');
 
        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_ok(unsigned status)
 {
-       *(uint32_t *) G.msg_ok = status;
+       *(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))
 
@@ -189,8 +279,10 @@ cmdio_write_ok(unsigned status)
 static void
 cmdio_write_error(unsigned status)
 {
-       *(uint32_t *) G.msg_err = status;
+       *(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))
 
@@ -198,6 +290,8 @@ static void
 cmdio_write_raw(const char *p_text)
 {
        xwrite_str(STDOUT_FILENO, p_text);
+       if (G.verbose > 1)
+               verbose_log(p_text);
 }
 
 static void
@@ -265,12 +359,12 @@ handle_cdup(void)
 static void
 handle_stat(void)
 {
-       cmdio_write_raw(STR(FTP_STATOK)"-FTP server status:\r\n"
+       cmdio_write_raw(STR(FTP_STATOK)"-Server status:\r\n"
                        " TYPE: BINARY\r\n"
                        STR(FTP_STATOK)" Ok\r\n");
 }
 
-/* TODO: implement FEAT. Example:
+/* 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.
@@ -294,16 +388,15 @@ HELP
 214 Help OK.
 */
 static void
-handle_help(void)
+handle_feat(unsigned status)
 {
-       cmdio_write_raw(STR(FTP_HELP)"-Commands:\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"
-#endif
-                       STR(FTP_HELP)" Ok\r\n");
+       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");
 }
 
 /* Download commands */
@@ -343,7 +436,7 @@ ftpdataio_get_pasv_fd(void)
                return remote_fd;
        }
 
-       setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
+       setsockopt_keepalive(remote_fd);
        return remote_fd;
 }
 
@@ -379,7 +472,7 @@ static int
 port_or_pasv_was_seen(void)
 {
        if (!pasv_active() && !port_active()) {
-               cmdio_write_raw(STR(FTP_BADSENDCONN)" Use PORT or PASV first\r\n");
+               cmdio_write_raw(STR(FTP_BADSENDCONN)" Use PORT/PASV first\r\n");
                return 0;
        }
 
@@ -398,7 +491,7 @@ bind_for_passive_mode(void)
        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);
+       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);
@@ -423,7 +516,7 @@ handle_pasv(void)
                addr = xstrdup("0.0.0.0");
        replace_char(addr, '.', ',');
 
-       response = xasprintf(STR(FTP_PASVOK)" Entering Passive Mode (%s,%u,%u)\r\n",
+       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);
@@ -438,26 +531,11 @@ handle_epsv(void)
        char *response;
 
        port = bind_for_passive_mode();
-       response = xasprintf(STR(FTP_EPSVOK)" EPSV Ok (|||%u|)\r\n", port);
+       response = xasprintf(STR(FTP_EPSVOK)" EPSV ok (|||%u|)\r\n", port);
        cmdio_write_raw(response);
        free(response);
 }
 
-/* libbb candidate */
-static
-len_and_sockaddr* get_peer_lsa(int fd)
-{
-       len_and_sockaddr *lsa;
-       socklen_t len = 0;
-
-       if (getpeername(fd, NULL, &len) != 0)
-               return NULL;
-       lsa = xzalloc(LSA_LEN_SIZE + len);
-       lsa->len = len;
-       getpeername(fd, &lsa->u.sa, &lsa->len);
-       return lsa;
-}
-
 static void
 handle_port(void)
 {
@@ -522,7 +600,7 @@ handle_port(void)
        G.port_addr = xdotted2sockaddr(raw, port);
 #else
        G.port_addr = get_peer_lsa(STDIN_FILENO);
-       set_nport(G.port_addr, htons(port));
+       set_nport(&G.port_addr->u.sa, htons(port));
 #endif
        WRITE_OK(FTP_PORTOK);
 }
@@ -531,7 +609,7 @@ static void
 handle_rest(void)
 {
        /* When ftp_arg == NULL simply restart from beginning */
-       G.restart_pos = G.ftp_arg ? xatoi_u(G.ftp_arg) : 0;
+       G.restart_pos = G.ftp_arg ? XATOOFF(G.ftp_arg) : 0;
        WRITE_OK(FTP_RESTOK);
 }
 
@@ -574,7 +652,7 @@ handle_retr(void)
                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);
@@ -598,39 +676,70 @@ handle_retr(void)
 static int
 popen_ls(const char *opt)
 {
-       char *cwd;
-       const char *argv[5] = { "ftpd", opt, NULL, G.ftp_arg, NULL };
+       const char *argv[5];
        struct fd_pair outfd;
        pid_t pid;
 
-       cwd = xrealloc_getcwd_or_warn(NULL);
+       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;
+       }
+
        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! */
+       /*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);
-               /* 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);
+               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
        }
 
        /* parent */
        close(outfd.wr);
-       free(cwd);
        return outfd.rd;
 }
 
@@ -649,38 +758,43 @@ handle_dir_common(int opts)
        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");
+       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)"-Status follows:\r\n");
+               cmdio_write_raw(STR(FTP_STATFILE_OK)"-File status:\r\n");
                while (1) {
-                       line = xmalloc_fgetline(ls_fp);
+                       line = xmalloc_fgetline(ls_fp);
                        if (!line)
                                break;
-                       cmdio_write(0, line); /* hack: 0 results in no status at all */
+                       /* 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(" Here comes the directory listing");
+               int remote_fd = get_remote_transfer_fd(" Directory listing");
                if (remote_fd >= 0) {
                        while (1) {
-                               line = xmalloc_fgetline(ls_fp);
+                               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'.
-                                * Pity... that would be much simpler.
+                                * Replace trailing "\n\0" with "\r\n".
                                 */
-                               xwrite_str(remote_fd, line);
-                               xwrite(remote_fd, "\r\n", 2);
+                               len = strlen(line);
+                               if (len != 0) /* paranoia check */
+                                       line[len - 1] = '\r';
+                               line[len] = '\n';
+                               xwrite(remote_fd, line, len + 1);
                                free(line);
                        }
                }
@@ -697,6 +811,13 @@ handle_list(void)
 static void
 handle_nlst(void)
 {
+       /* 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
@@ -705,11 +826,43 @@ handle_stat_file(void)
        handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
 }
 
+/* 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_size(void)
+handle_size_or_mdtm(int need_size)
 {
        struct stat statbuf;
-       char buf[sizeof(STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n") + sizeof(off_t)*3];
+       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
@@ -718,13 +871,24 @@ handle_size(void)
                WRITE_ERR(FTP_FILEFAIL);
                return;
        }
-       sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size);
+       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);
+       }
        cmdio_write_raw(buf);
 }
 
 /* Upload commands */
 
-#if ENABLE_FEATURE_FTP_WRITE
+#if ENABLE_FEATURE_FTPD_WRITE
 static void
 handle_mkd(void)
 {
@@ -770,7 +934,7 @@ handle_rnto(void)
 
        /* 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)" RNFR required first\r\n");
+               cmdio_write_raw(STR(FTP_NEEDRNFR)" Use RNFR first\r\n");
                return;
        }
 
@@ -819,6 +983,7 @@ handle_upload_common(int is_append, int is_unique)
         || 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;
@@ -866,30 +1031,83 @@ handle_stou(void)
        G.restart_pos = 0;
        handle_upload_common(0, 1);
 }
-#endif /* ENABLE_FEATURE_FTP_WRITE */
+#endif /* ENABLE_FEATURE_FTPD_WRITE */
 
 static uint32_t
 cmdio_get_cmd_and_arg(void)
 {
-       size_t len;
+       int len;
        uint32_t cmdval;
        char *cmd;
 
        alarm(G.timeout);
 
        free(G.ftp_cmd);
-       len = 8 * 1024; /* Paranoia. Peer may send 1 gigabyte long cmd... */
-       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 ((ssize_t)len >= 0 && cmd[len] == '\r') {
-               cmd[len] = '\0';
-               len--;
+       {
+               /* 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;
+       }
+
+       /* 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';
@@ -911,8 +1129,10 @@ enum {
        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'),
@@ -921,6 +1141,8 @@ enum {
        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'),
@@ -936,37 +1158,65 @@ enum {
        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),
-       OPT_v = (1 << 2),
-       OPT_S = (1 << 3),
-       OPT_w = (1 << 4),
+#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, char **argv)
+int ftpd_main(int argc UNUSED_PARAM, char **argv)
 {
+#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;
-       opt_complementary = "t+:T+";
-       opts = getopt32(argv, "l1vS" USE_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout);
+#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 */
-               memset(&G, 0, sizeof(G));
-/* TODO: pass -n too? */
-/* --group-directories-first would be nice, but ls don't do that yet */
-               xchdir(argv[2]);
-               argv[2] = (char*)"--";
-               return ls_main(argc, argv);
+               /* 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;
        }
-       G.end_time = monotonic_sec() + abs_timeout;
-       if (G.timeout > abs_timeout)
-               G.timeout = abs_timeout + 1;
        strcpy(G.msg_ok  + 4, MSG_OK );
        strcpy(G.msg_err + 4, MSG_ERR);
 
@@ -988,53 +1238,90 @@ int ftpd_main(int argc, char **argv)
                openlog(applet_name, LOG_PID | LOG_NDELAY, LOG_DAEMON);
                logmode |= LOGMODE_SYSLOG;
        }
-
-       G.proc_self_fd = xopen("/proc/self", O_RDONLY | O_DIRECTORY);
-
-       if (argv[optind]) {
-               xchdir(argv[optind]);
-               chroot(".");
-       }
+       if (logmode)
+               applet_name = xasprintf("%s[%u]", applet_name, (int)getpid());
 
        //umask(077); - admin can set umask before starting us
 
-       /* Signals. We'll always take -EPIPE rather than a rude signal, thanks */
-       signal(SIGPIPE, SIG_IGN);
+       /* 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(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));
+       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);
 
-#ifdef IF_WE_WANT_TO_REQUIRE_LOGIN
-       {
-               smallint user_was_specified = 0;
+#if ENABLE_FEATURE_FTPD_AUTHENTICATION
+       if (!(opts & OPT_A)) {
                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_raw(STR(FTP_LOGINERR)" Server is anonymous only\r\n");
-                               else {
-                                       user_was_specified = 1;
-                                       cmdio_write_raw(STR(FTP_GIVEPWORD)" Please specify the password\r\n");
+                               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 (user_was_specified)
-                                       break;
-                               cmdio_write_raw(STR(FTP_NEEDUSER)" Login with USER\r\n");
+                               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 and PASS\r\n");
+                               cmdio_write_raw(STR(FTP_LOGINERR)" Login with USER+PASS\r\n");
                        }
                }
+               WRITE_OK(FTP_LOGINOK);
        }
-       WRITE_OK(FTP_LOGINOK);
+#endif
+
+       /* 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
@@ -1083,7 +1370,12 @@ int ftpd_main(int argc, char **argv)
                        return 0;
                }
                else if (cmdval == const_USER)
-                       WRITE_OK(FTP_GIVEPWORD);
+                       /* 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)
@@ -1098,20 +1390,27 @@ int ftpd_main(int argc, char **argv)
                        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)
+               else if (cmdval == const_PWD || cmdval == const_XPWD)
                        handle_pwd();
                else if (cmdval == const_CWD)
                        handle_cwd();
                else if (cmdval == const_CDUP) /* cd .. */
                        handle_cdup();
-               else if (cmdval == const_HELP)
-                       handle_help();
+               /* 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();
-               else if (cmdval == const_SIZE)
-                       handle_size();
+               /* 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();
@@ -1128,7 +1427,7 @@ int ftpd_main(int argc, char **argv)
                        handle_port();
                else if (cmdval == const_REST)
                        handle_rest();
-#if ENABLE_FEATURE_FTP_WRITE
+#if ENABLE_FEATURE_FTPD_WRITE
                else if (opts & OPT_w) {
                        if (cmdval == const_STOR)
                                handle_stor();
@@ -1146,6 +1445,8 @@ int ftpd_main(int argc, char **argv)
                                handle_appe();
                        else if (cmdval == const_STOU) /* "store unique" */
                                handle_stou();
+                       else
+                               goto bad_cmd;
                }
 #endif
 #if 0
@@ -1164,9 +1465,11 @@ int ftpd_main(int argc, char **argv)
                else {
                        /* Which unsupported commands were seen in the wild?
                         * (doesn't necessarily mean "we must support them")
-                        * lftp 3.6.3: FEAT - is it useful?
-                        *             MDTM - works fine without it anyway
+                        * foo 1.2.3: XXXX - comment
                         */
+#if ENABLE_FEATURE_FTPD_WRITE
+ bad_cmd:
+#endif
                        cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");
                }
        }