*
* 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.
*/
+//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>
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);*/ \
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);
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 > 1)
+ if (G.verbose > 0)
verbose_log(G.msg_err);
}
#define WRITE_ERR(a) cmdio_write_error(STRNUM32sp(a))
return remote_fd;
}
- setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
+ setsockopt_keepalive(remote_fd);
return remote_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);
+ 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);
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)
{
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);
}
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);
}
static int
popen_ls(const char *opt)
{
- char *cwd;
- const char *argv[] = {
- "ftpd",
- opt,
- BB_MMU ? "--" : NULL,
- G.ftp_arg,
- NULL
- };
+ const char *argv[5];
struct fd_pair outfd;
pid_t pid;
- cwd = xrealloc_getcwd_or_warn(NULL);
- xpiped_pair(outfd);
+ 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;
+ }
- /*fflush(NULL); - so far we dont use stdio on output */
- pid = BB_MMU ? fork() : vfork();
- if (pid < 0)
- bb_perror_msg_and_die(BB_MMU ? "fork" : "vfork");
+ xpiped_pair(outfd);
+ /*fflush_all(); - so far we dont use stdio on output */
+ pid = BB_MMU ? xfork() : xvfork();
if (pid == 0) {
- /* child */
#if !BB_MMU
- if (fchdir(G.root_fd) != 0)
- _exit(127);
- close(G.root_fd);
+ int cur_fd;
#endif
+ /* child */
/* NB: close _first_, then move fd! */
close(outfd.rd);
xmove_fd(outfd.wr, STDOUT_FILENO);
* ls won't read it anyway */
close(STDIN_FILENO);
dup(STDOUT_FILENO); /* copy will become STDIN_FILENO */
-#if !BB_MMU
- /* ftpd ls helper chdirs to argv[2],
- * preventing peer from seeing real root we are in now
+#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.
*/
- argv[2] = cwd;
- /* + 1: we must use relative path here if in chroot.
- * For example, execv("/proc/self/exe") will fail, since
- * it looks for "/proc/self/exe" _relative to chroot!_ */
- execv(bb_busybox_exec_path + 1, (char**) argv);
+ 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);
-#else
- memset(&G, 0, sizeof(G));
- exit(ls_main(ARRAY_SIZE(argv) - 1, (char**) argv));
#endif
}
/* parent */
close(outfd.wr);
- free(cwd);
return outfd.rd;
}
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)"-File status:\r\n");
while (1) {
- line = xmalloc_fgetline(ls_fp);
+ line = xmalloc_fgetline(ls_fp);
if (!line)
break;
/* Hack: 0 results in no status at all */
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".
*/
-/* TODO: need to s/LF/NUL/g here */
- 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);
}
}
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,
+ broken_out.tm_mon + 1,
broken_out.tm_mday,
broken_out.tm_hour,
broken_out.tm_min,
/* Upload commands */
-#if ENABLE_FEATURE_FTP_WRITE
+#if ENABLE_FEATURE_FTPD_WRITE
static void
handle_mkd(void)
{
|| 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;
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_fgets_str_len(stdin, "\r\n", &len);
- if (!cmd)
- exit(0);
+ {
+ /* 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
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'),
OPT_l = (1 << 0),
OPT_1 = (1 << 1),
#endif
- OPT_v = (1 << ((!BB_MMU) * 2 + 0)),
- OPT_S = (1 << ((!BB_MMU) * 2 + 1)),
- OPT_w = (1 << ((!BB_MMU) * 2 + 2)) * ENABLE_FEATURE_FTP_WRITE,
+ 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;
-#if !BB_MMU
-int ftpd_main(int argc, char **argv)
-#else
int ftpd_main(int argc UNUSED_PARAM, char **argv)
-#endif
{
+#if ENABLE_FEATURE_FTPD_AUTHENTICATION
+ struct passwd *pw = NULL;
+ char *anon_opt = NULL;
+#endif
unsigned abs_timeout;
unsigned verbose_S;
smallint opts;
abs_timeout = 1 * 60 * 60;
verbose_S = 0;
G.timeout = 2 * 60;
- opt_complementary = "t+:T+:vv:SS";
#if BB_MMU
- opts = getopt32(argv, "vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
+ 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, "l1vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
+ 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 */
-/* TODO: pass -n too? */
-/* --group-directories-first would be nice, but ls don't do that yet */
- xchdir(argv[2]);
- argv[2] = (char*)"--";
- memset(&G, 0, sizeof(G));
- 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)
if (logmode)
applet_name = xasprintf("%s[%u]", applet_name, (int)getpid());
-#if !BB_MMU
- G.root_fd = xopen("/", O_RDONLY | O_DIRECTORY);
-#endif
-
- if (argv[optind]) {
- xchdir(argv[optind]);
- chroot(".");
- }
-
//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_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(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1));
+ 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
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();
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();
* (doesn't necessarily mean "we must support them")
* foo 1.2.3: XXXX - comment
*/
-#if ENABLE_FEATURE_FTP_WRITE
+#if ENABLE_FEATURE_FTPD_WRITE
bad_cmd:
#endif
cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");