ftpd: fix the bug where >2GB file ops report errors;
make a few simplifications; add TODOs.
function old new delta
port_or_pasv_was_seen - 37 +37
get_remote_transfer_fd 104 109 +5
handle_upload_common 265 260 -5
handle_dir_common 228 223 -5
popen_ls 211 203 -8
ftpd_main 1825 1815 -10
data_transfer_checks_ok 37 - -37
------------------------------------------------------------------------------
(add/remove: 1/1 grow/shrink: 1/4 up/down: 42/-65) Total: -23 bytes
diff --git a/networking/ftpd.c b/networking/ftpd.c
index 3faa3ed..37b340e 100644
--- a/networking/ftpd.c
+++ b/networking/ftpd.c
@@ -268,6 +268,29 @@
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)
{
@@ -283,6 +306,29 @@
/* Download commands */
+static inline int
+port_active(void)
+{
+ return (G.port_addr != NULL);
+}
+
+static inline int
+pasv_active(void)
+{
+ return (G.pasv_listen_fd > STDOUT_FILENO);
+}
+
+static void
+port_pasv_cleanup(void)
+{
+ free(G.port_addr);
+ G.port_addr = NULL;
+ if (G.pasv_listen_fd > STDOUT_FILENO)
+ close(G.pasv_listen_fd);
+ G.pasv_listen_fd = -1;
+}
+
+/* On error, emits error code to the peer */
static int
ftpdataio_get_pasv_fd(void)
{
@@ -299,28 +345,26 @@
return remote_fd;
}
-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
+ /* Exits on error */
remote_fd = xconnect_stream(G.port_addr);
+ port_pasv_cleanup();
+
if (remote_fd < 0)
return remote_fd;
@@ -328,8 +372,9 @@
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");
@@ -339,16 +384,7 @@
return 1;
}
-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;
-}
-
+/* Exits on error */
static unsigned
bind_for_passive_mode(void)
{
@@ -370,6 +406,7 @@
return port;
}
+/* Exits on error */
static void
handle_pasv(void)
{
@@ -391,6 +428,7 @@
free(response);
}
+/* Exits on error */
static void
handle_epsv(void)
{
@@ -408,16 +446,16 @@
{
unsigned short port;
char *raw, *port_part;
- len_and_sockaddr *lsa = NULL;
+ len_and_sockaddr *lsa;
port_pasv_cleanup();
raw = G.ftp_arg;
- /* buglets:
- * xatou16 will accept wrong input,
- * xatou16 will exit instead of generating error to peer
- */
+/* Buglets:
+ * xatou16 will accept wrong input,
+ * xatou16 will exit instead of generating error to peer
+ */
port_part = strrchr(raw, ',');
if (port_part == NULL)
@@ -440,6 +478,11 @@
return;
}
+/* Should we verify that lsa matches getpeername(STDIN)?
+ * Otherwise peer can make us open data connections
+ * to other hosts (security problem!)
+ */
+
G.port_addr = lsa;
cmdio_write_ok(FTP_PORTOK);
}
@@ -456,38 +499,38 @@
handle_retr(void)
{
struct stat statbuf;
- int trans_ret;
+ 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; /* data_transfer_checks_ok emitted error response */
+ 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;
}
- if (fstat(opened_file, &statbuf) != 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)",
@@ -495,20 +538,22 @@
remote_fd = get_remote_transfer_fd(response);
free(response);
if (remote_fd < 0)
- goto port_pasv_cleanup_out;
+ goto file_close_out;
- trans_ret = bb_copyfd_eof(opened_file, remote_fd);
+/* 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.
+ */
+
+ bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd);
close(remote_fd);
- if (trans_ret < 0)
+ 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 */
@@ -522,12 +567,10 @@
pid_t pid;
cwd = xrealloc_getcwd_or_warn(NULL);
-
xpiped_pair(outfd);
- fflush(NULL);
+ /*fflush(NULL); - so far we dont use stdio on output */
pid = vfork();
-
switch (pid) {
case -1: /* failure */
bb_perror_msg_and_die("vfork");
@@ -547,12 +590,18 @@
}
_exit(127);
}
+
/* parent */
close(outfd.wr);
free(cwd);
return outfd.rd;
}
+enum {
+ USE_CTRL_CONN = 1,
+ LONG_LISTING = 2,
+};
+
static void
handle_dir_common(int opts)
{
@@ -560,15 +609,15 @@
char *line;
int ls_fd;
- if (!(opts & 1) && !data_transfer_checks_ok())
- return; /* data_transfer_checks_ok emitted error response */
+ if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen())
+ return; /* port_or_pasv_was_seen emitted error response */
- ls_fd = popen_ls((opts & 2) ? "-l" : "-1");
+ 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 & 1) {
+ if (opts & USE_CTRL_CONN) {
/* STAT <filename> */
cmdio_write_raw(STR(FTP_STATFILE_OK)"-Status follows:\r\n");
while (1) {
@@ -587,13 +636,16 @@
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);
}
}
close(remote_fd);
- port_pasv_cleanup();
cmdio_write_ok(FTP_TRANSFEROK);
}
fclose(ls_fp); /* closes ls_fd too */
@@ -601,7 +653,7 @@
static void
handle_list(void)
{
- handle_dir_common(2);
+ handle_dir_common(LONG_LISTING);
}
static void
handle_nlst(void)
@@ -611,7 +663,7 @@
static void
handle_stat_file(void)
{
- handle_dir_common(3);
+ handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
}
static void
@@ -698,7 +750,7 @@
handle_upload_common(int is_append, int is_unique)
{
char *tempname = NULL;
- int trans_ret;
+ off_t bytes_transferred;
int local_file_fd;
int remote_fd;
off_t offset;
@@ -706,8 +758,8 @@
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 */
local_file_fd = -1;
if (is_unique) {
@@ -726,7 +778,7 @@
return;
}
- /* TODO: paranoia: fstat it, refuse to do anything if it's not a regular file */
+/* TODO: paranoia: fstat it, refuse to do anything if it's not a regular file */
if (offset)
xlseek(local_file_fd, offset, SEEK_SET);
@@ -737,16 +789,18 @@
if (remote_fd < 0)
goto bail;
- trans_ret = bb_copyfd_eof(remote_fd, local_file_fd);
- close(remote_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_file_fd);
}