From c41cba5a578b278dc7d57ef97234a2a67e076d20 Mon Sep 17 00:00:00 2001
From: Denis Vlasenko <vda.linux@googlemail.com>
Date: Mon, 9 Mar 2009 15:46:07 +0000
Subject: ftpd: reuse ls applet for LIST/NLST/STAT generation

function                                             old     new   delta
popen_ls                                               -     211    +211
ftpd_main                                           1760    1826     +66
handle_dir_common                                    199     228     +29
get_remote_transfer_fd                                89     104     +15
replace_char                                          30      34      +4
handle_upload_common                                 263     265      +2
bind_for_passive_mode                                129     121      -8
cmdio_write                                           84      62     -22
escape_text                                          166     136     -30
init_data_sock_params                                 81       -     -81
ftpdataio_dispose_transfer_fd                         87       -     -87
write_dirstats                                       149       -    -149
write_filestats                                      603       -    -603
------------------------------------------------------------------------------
(add/remove: 1/4 grow/shrink: 11/5 up/down: 384/-986)        Total: -602 bytes
   text    data     bss     dec     hex filename
 808804     476    7864  817144   c77f8 busybox_old
 808156     476    7864  816496   c7570 busybox_unstripped
---
 coreutils/Kbuild  |   1 +
 coreutils/ls.c    |   1 -
 include/libbb.h   |   2 +
 networking/ftpd.c | 400 +++++++++++++++++++++---------------------------------
 4 files changed, 157 insertions(+), 247 deletions(-)

diff --git a/coreutils/Kbuild b/coreutils/Kbuild
index 67c56588a..57100a9cf 100644
--- a/coreutils/Kbuild
+++ b/coreutils/Kbuild
@@ -44,6 +44,7 @@ lib-$(CONFIG_LENGTH)    += length.o
 lib-$(CONFIG_LN)        += ln.o
 lib-$(CONFIG_LOGNAME)   += logname.o
 lib-$(CONFIG_LS)        += ls.o
+lib-$(CONFIG_FTPD)      += ls.o
 lib-$(CONFIG_MD5SUM)    += md5_sha1_sum.o
 lib-$(CONFIG_MKDIR)     += mkdir.o
 lib-$(CONFIG_MKFIFO)    += mkfifo.o
diff --git a/coreutils/ls.c b/coreutils/ls.c
index 7b65d049e..f4f2724c5 100644
--- a/coreutils/ls.c
+++ b/coreutils/ls.c
@@ -864,7 +864,6 @@ static const char ls_color_opt[] ALIGN1 =
 #endif
 
 
-int ls_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int ls_main(int argc UNUSED_PARAM, char **argv)
 {
 	struct dnode **dnd;
diff --git a/include/libbb.h b/include/libbb.h
index 7d6ea9029..80a1c912c 100644
--- a/include/libbb.h
+++ b/include/libbb.h
@@ -917,6 +917,8 @@ int test_main(int argc, char **argv) USE_TEST(MAIN_EXTERNALLY_VISIBLE);
 int kill_main(int argc, char **argv) USE_KILL(MAIN_EXTERNALLY_VISIBLE);
 /* Similar, but used by chgrp, not shell */
 int chown_main(int argc, char **argv) USE_CHOWN(MAIN_EXTERNALLY_VISIBLE);
+/* Used by ftpd */
+int ls_main(int argc, char **argv) USE_LS(MAIN_EXTERNALLY_VISIBLE);
 /* Don't need USE_xxx() guard for these */
 int gunzip_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int bunzip2_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
diff --git a/networking/ftpd.c b/networking/ftpd.c
index e83066032..2a7d8006b 100644
--- a/networking/ftpd.c
+++ b/networking/ftpd.c
@@ -63,9 +63,24 @@
 #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),
+	/* Shift for Nth decimal digit */
+	SHIFT0 = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN,
+	SHIFT1 =  8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN,
+	SHIFT2 =  0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN,
+};
+#define STRNUM32(s) (uint32_t)(0 \
+	| (('0' + ((s) / 1 % 10)) << SHIFT0) \
+	| (('0' + ((s) / 10 % 10)) << SHIFT1) \
+	| (('0' + ((s) / 100 % 10)) << SHIFT2) \
+)
+
+enum {
+	OPT_l = (1 << 0),
+	OPT_1 = (1 << 1),
+	OPT_v = (1 << 2),
+	OPT_w = (1 << 3),
 
 #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)
@@ -106,78 +121,84 @@ struct globals {
 	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_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 *
-escape_text(const char *prepend, const char *str, char from, const char *to, char append)
+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 */
+	remainlen = strlen(str);
 	retlen = strlen(prepend);
-	ret = xmalloc(retlen + remainlen * tolen + 1 + 1);
+	ret = xmalloc(retlen + remainlen * 2 + 1 + 1);
 	strcpy(ret, prepend);
 
 	for (;;) {
-		found = strchrnul(remain, from);
-		chunklen = found - remain;
+		found = strchrnul(str, escapee);
+		chunklen = found - str + 1;
 
-		/* Copy chunk which doesn't contain 'from' to ret */
-		memcpy(&ret[retlen], remain, chunklen);
+		/* Copy chunk up to and including escapee (or NUL) to ret */
+		memcpy(ret + retlen, str, chunklen);
 		retlen += chunklen;
 
-		if (*found != '\0') {
-			/* Now copy 'to' instead of 'from' */
-			memcpy(&ret[retlen], to, tolen);
-			retlen += tolen;
-
-			remain = found + 1;
-		} else {
-			ret[retlen] = append;
-			ret[retlen+1] = '\0';
+		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;
 }
 
 static void
-cmdio_write(unsigned status, const char *str)
+cmdio_write(uint32_t status_str, const char *str)
 {
+	union {
+		char buf[4];
+		uint32_t v32;
+	} u;
 	char *response;
 	int len;
 
+	u.v32 = status_str;
+
 	/* FTP allegedly uses telnet protocol for command link.
 	 * In telnet, 0xff is an escape char, and needs to be escaped: */
-	response = escape_text(utoa(status), str, '\xff', "\xff\xff", '\r');
+	response = escape_text(u.buf, 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);
@@ -215,9 +236,9 @@ handle_pwd(void)
 		cwd = xstrdup("");
 
 	/* We have to promote each " to "" */
-	response = escape_text(" \"", cwd, '\"', "\"\"", '\"');
+	response = escape_text(" \"", cwd, ('"' << 8) + '"');
 	free(cwd);
-	cmdio_write(FTP_PWDOK, response);
+	cmdio_write(STRNUM32(FTP_PWDOK), response);
 	free(response);
 }
 
@@ -261,21 +282,6 @@ handle_help(void)
 
 /* Download commands */
 
-static void
-init_data_sock_params(int sock_fd)
-{
-	struct linger linger;
-
-	G.data_fd = sock_fd;
-
-	memset(&linger, 0, sizeof(linger));
-	linger.l_onoff = 1;
-	linger.l_linger = 32767;
-
-	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 int
 ftpdataio_get_pasv_fd(void)
 {
@@ -288,39 +294,10 @@ ftpdataio_get_pasv_fd(void)
 		return remote_fd;
 	}
 
-	init_data_sock_params(remote_fd);
-	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);
+	setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
 	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)
 {
@@ -341,12 +318,12 @@ get_remote_transfer_fd(const char *p_status_msg)
 	if (pasv_active())
 		remote_fd = ftpdataio_get_pasv_fd();
 	else
-		remote_fd = ftpdataio_get_port_fd();
+		remote_fd = xconnect_stream(G.port_addr);
 
 	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;
 }
 
@@ -374,21 +351,21 @@ port_pasv_cleanup(void)
 static unsigned
 bind_for_passive_mode(void)
 {
+	int fd;
 	unsigned port;
 
 	port_pasv_cleanup();
 
-	G.pasv_listen_fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
-	setsockopt_reuseaddr(G.pasv_listen_fd);
+	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(G.pasv_listen_fd, &G.local_addr->u.sa, G.local_addr->len);
-	xlisten(G.pasv_listen_fd, 1);
-	getsockname(G.pasv_listen_fd, &G.local_addr->u.sa, &G.local_addr->len);
+	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;
 }
 
@@ -487,7 +464,7 @@ handle_retr(void)
 	G.restart_pos = 0;
 
 	if (!data_transfer_checks_ok())
-		return;
+		return; /* data_transfer_checks_ok 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;
@@ -520,7 +497,7 @@ handle_retr(void)
 		goto port_pasv_cleanup_out;
 
 	trans_ret = bb_copyfd_eof(opened_file, remote_fd);
-	ftpdataio_dispose_transfer_fd();
+	close(remote_fd);
 	if (trans_ret < 0)
 		cmdio_write_error(FTP_BADSENDFILE);
 	else
@@ -535,185 +512,105 @@ handle_retr(void)
 
 /* 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;
-	char timestr[32];
-	struct tm *tm;
-
-	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);
-		if (strftime(timestr, sizeof(timestr), "%b %d %H:%M", tm) == 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);
+	char *cwd;
+	const char *argv[5] = { "ftpd", opt, NULL, G.ftp_arg, NULL };
+	struct fd_pair outfd;
+	pid_t pid;
 
-	for (;;) {
-		dirent = readdir(dir);
-		if (dirent == NULL)
-			break;
+	cwd = xrealloc_getcwd_or_warn(NULL);
 
-		/* Ignore . and .. */
-		if (dirent->d_name[0] == '.') {
-			if (dirent->d_name[1] == '\0'
-			 || (dirent->d_name[1] == '.' && dirent->d_name[2] == '\0')
-			) {
-				continue;
-			}
+	xpiped_pair(outfd);
+
+	fflush(NULL);
+	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;
 }
 
 static void
-handle_dir_common(int full_details, int stat_cmd)
+handle_dir_common(int opts)
 {
-	int fd;
-	struct stat statbuf;
+	FILE *ls_fp;
+	char *line;
+	int ls_fd;
 
-	if (!stat_cmd && !data_transfer_checks_ok())
-		return;
+	if (!(opts & 1) && !data_transfer_checks_ok())
+		return; /* data_transfer_checks_ok emitted error response */
+
+	ls_fd = popen_ls((opts & 2) ? "-l" : "-1");
+	ls_fp = fdopen(ls_fd, "r");
+	if (!ls_fp) /* never happens. paranoia */
+		bb_perror_msg_and_die("fdopen");
 
-	if (stat_cmd) {
-		fd = STDIN_FILENO;
+	if (opts & 1) {
+		/* 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;
+				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();
+		close(remote_fd);
 		port_pasv_cleanup();
 		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(2);
 }
-
 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(3);
 }
 
 static void
@@ -840,7 +737,7 @@ handle_upload_common(int is_append, int is_unique)
 		goto bail;
 
 	trans_ret = bb_copyfd_eof(remote_fd, local_file_fd);
-	ftpdataio_dispose_transfer_fd();
+	close(remote_fd);
 
 	if (trans_ret < 0)
 		cmdio_write_error(FTP_BADSENDFILE);
@@ -905,8 +802,19 @@ cmdio_get_cmd_and_arg(void)
 }
 
 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, "l1v" USE_FEATURE_FTP_WRITE("w"));
+
+	if (opts & (OPT_l|OPT_1)) {
+		/* Our secret backdoor to ls */
+		xchdir(argv[2]);
+		argv[2] = (char*)"--";
+		return ls_main(argc, argv);
+	}
+
 	INIT_G();
 
 	G.local_addr = get_sock_lsa(STDIN_FILENO);
@@ -920,13 +828,13 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
 		 * failure */
 	}
 
-	G.opts = getopt32(argv, "v" USE_FEATURE_FTP_WRITE("w"));
-
 	openlog(applet_name, LOG_PID, LOG_DAEMON);
 	logmode |= LOGMODE_SYSLOG;
-	if (!(G.opts & OPT_v))
+	if (!(opts & OPT_v))
 		logmode = LOGMODE_SYSLOG;
 
+	G.proc_self_fd = xopen("/proc/self", O_RDONLY | O_DIRECTORY);
+
 	if (argv[optind]) {
 		xchdir(argv[optind]);
 		chroot(".");
@@ -1064,7 +972,7 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv)
 		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)
-- 
cgit v1.2.3-55-g6feb