ftpd: work around LIST -aXYZ too, not only LIST -lXYZ
[oweals/busybox.git] / networking / ftpd.c
1 /* vi: set sw=4 ts=4: */
2 /*
3  * Simple FTP daemon, based on vsftpd 2.0.7 (written by Chris Evans)
4  *
5  * Author: Adam Tkac <vonsch@gmail.com>
6  *
7  * Licensed under GPLv2, see file LICENSE in this tarball for details.
8  *
9  * Only subset of FTP protocol is implemented but vast majority of clients
10  * should not have any problem.
11  *
12  * You have to run this daemon via inetd.
13  */
14
15 #include "libbb.h"
16 #include <syslog.h>
17 #include <netinet/tcp.h>
18
19 #define FTP_DATACONN            150
20 #define FTP_NOOPOK              200
21 #define FTP_TYPEOK              200
22 #define FTP_PORTOK              200
23 #define FTP_STRUOK              200
24 #define FTP_MODEOK              200
25 #define FTP_ALLOOK              202
26 #define FTP_STATOK              211
27 #define FTP_STATFILE_OK         213
28 #define FTP_HELP                214
29 #define FTP_SYSTOK              215
30 #define FTP_GREET               220
31 #define FTP_GOODBYE             221
32 #define FTP_TRANSFEROK          226
33 #define FTP_PASVOK              227
34 /*#define FTP_EPRTOK              228*/
35 #define FTP_EPSVOK              229
36 #define FTP_LOGINOK             230
37 #define FTP_CWDOK               250
38 #define FTP_RMDIROK             250
39 #define FTP_DELEOK              250
40 #define FTP_RENAMEOK            250
41 #define FTP_PWDOK               257
42 #define FTP_MKDIROK             257
43 #define FTP_GIVEPWORD           331
44 #define FTP_RESTOK              350
45 #define FTP_RNFROK              350
46 #define FTP_TIMEOUT             421
47 #define FTP_BADSENDCONN         425
48 #define FTP_BADSENDNET          426
49 #define FTP_BADSENDFILE         451
50 #define FTP_BADCMD              500
51 #define FTP_COMMANDNOTIMPL      502
52 #define FTP_NEEDUSER            503
53 #define FTP_NEEDRNFR            503
54 #define FTP_BADSTRU             504
55 #define FTP_BADMODE             504
56 #define FTP_LOGINERR            530
57 #define FTP_FILEFAIL            550
58 #define FTP_NOPERM              550
59 #define FTP_UPLOADFAIL          553
60
61 #define STR1(s) #s
62 #define STR(s) STR1(s)
63
64 /* Convert a constant to 3-digit string, packed into uint32_t */
65 enum {
66         /* Shift for Nth decimal digit */
67         SHIFT2  =  0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN,
68         SHIFT1  =  8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN,
69         SHIFT0  = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN,
70         /* And for 4th position (space) */
71         SHIFTsp = 24 * BB_LITTLE_ENDIAN + 0 * BB_BIG_ENDIAN,
72 };
73 #define STRNUM32(s) (uint32_t)(0 \
74         | (('0' + ((s) / 1 % 10)) << SHIFT0) \
75         | (('0' + ((s) / 10 % 10)) << SHIFT1) \
76         | (('0' + ((s) / 100 % 10)) << SHIFT2) \
77 )
78 #define STRNUM32sp(s) (uint32_t)(0 \
79         | (' ' << SHIFTsp) \
80         | (('0' + ((s) / 1 % 10)) << SHIFT0) \
81         | (('0' + ((s) / 10 % 10)) << SHIFT1) \
82         | (('0' + ((s) / 100 % 10)) << SHIFT2) \
83 )
84
85 #define MSG_OK "Operation successful\r\n"
86 #define MSG_ERR "Error\r\n"
87
88 struct globals {
89         int pasv_listen_fd;
90 #if !BB_MMU
91         int root_fd;
92 #endif
93         int local_file_fd;
94         unsigned end_time;
95         unsigned timeout;
96         unsigned verbose;
97         off_t local_file_pos;
98         off_t restart_pos;
99         len_and_sockaddr *local_addr;
100         len_and_sockaddr *port_addr;
101         char *ftp_cmd;
102         char *ftp_arg;
103 #if ENABLE_FEATURE_FTP_WRITE
104         char *rnfr_filename;
105 #endif
106         /* We need these aligned to uint32_t */
107         char msg_ok [(sizeof("NNN " MSG_OK ) + 3) & 0xfffc];
108         char msg_err[(sizeof("NNN " MSG_ERR) + 3) & 0xfffc];
109 } FIX_ALIASING;
110 #define G (*(struct globals*)&bb_common_bufsiz1)
111 #define INIT_G() do { \
112         /* Moved to main */ \
113         /*strcpy(G.msg_ok  + 4, MSG_OK );*/ \
114         /*strcpy(G.msg_err + 4, MSG_ERR);*/ \
115 } while (0)
116
117
118 static char *
119 escape_text(const char *prepend, const char *str, unsigned escapee)
120 {
121         unsigned retlen, remainlen, chunklen;
122         char *ret, *found;
123         char append;
124
125         append = (char)escapee;
126         escapee >>= 8;
127
128         remainlen = strlen(str);
129         retlen = strlen(prepend);
130         ret = xmalloc(retlen + remainlen * 2 + 1 + 1);
131         strcpy(ret, prepend);
132
133         for (;;) {
134                 found = strchrnul(str, escapee);
135                 chunklen = found - str + 1;
136
137                 /* Copy chunk up to and including escapee (or NUL) to ret */
138                 memcpy(ret + retlen, str, chunklen);
139                 retlen += chunklen;
140
141                 if (*found == '\0') {
142                         /* It wasn't escapee, it was NUL! */
143                         ret[retlen - 1] = append; /* replace NUL */
144                         ret[retlen] = '\0'; /* add NUL */
145                         break;
146                 }
147                 ret[retlen++] = escapee; /* duplicate escapee */
148                 str = found + 1;
149         }
150         return ret;
151 }
152
153 /* Returns strlen as a bonus */
154 static unsigned
155 replace_char(char *str, char from, char to)
156 {
157         char *p = str;
158         while (*p) {
159                 if (*p == from)
160                         *p = to;
161                 p++;
162         }
163         return p - str;
164 }
165
166 static void
167 verbose_log(const char *str)
168 {
169         bb_error_msg("%.*s", (int)strcspn(str, "\r\n"), str);
170 }
171
172 /* NB: status_str is char[4] packed into uint32_t */
173 static void
174 cmdio_write(uint32_t status_str, const char *str)
175 {
176         char *response;
177         int len;
178
179         /* FTP uses telnet protocol for command link.
180          * In telnet, 0xff is an escape char, and needs to be escaped: */
181         response = escape_text((char *) &status_str, str, (0xff << 8) + '\r');
182
183         /* FTP sends embedded LFs as NULs */
184         len = replace_char(response, '\n', '\0');
185
186         response[len++] = '\n'; /* tack on trailing '\n' */
187         xwrite(STDOUT_FILENO, response, len);
188         if (G.verbose > 1)
189                 verbose_log(response);
190         free(response);
191 }
192
193 static void
194 cmdio_write_ok(unsigned status)
195 {
196         *(uint32_t *) G.msg_ok = status;
197         xwrite(STDOUT_FILENO, G.msg_ok, sizeof("NNN " MSG_OK) - 1);
198         if (G.verbose > 1)
199                 verbose_log(G.msg_ok);
200 }
201 #define WRITE_OK(a) cmdio_write_ok(STRNUM32sp(a))
202
203 /* TODO: output strerr(errno) if errno != 0? */
204 static void
205 cmdio_write_error(unsigned status)
206 {
207         *(uint32_t *) G.msg_err = status;
208         xwrite(STDOUT_FILENO, G.msg_err, sizeof("NNN " MSG_ERR) - 1);
209         if (G.verbose > 1)
210                 verbose_log(G.msg_err);
211 }
212 #define WRITE_ERR(a) cmdio_write_error(STRNUM32sp(a))
213
214 static void
215 cmdio_write_raw(const char *p_text)
216 {
217         xwrite_str(STDOUT_FILENO, p_text);
218         if (G.verbose > 1)
219                 verbose_log(p_text);
220 }
221
222 static void
223 timeout_handler(int sig UNUSED_PARAM)
224 {
225         off_t pos;
226         int sv_errno = errno;
227
228         if ((int)(monotonic_sec() - G.end_time) >= 0)
229                 goto timed_out;
230
231         if (!G.local_file_fd)
232                 goto timed_out;
233
234         pos = xlseek(G.local_file_fd, 0, SEEK_CUR);
235         if (pos == G.local_file_pos)
236                 goto timed_out;
237         G.local_file_pos = pos;
238
239         alarm(G.timeout);
240         errno = sv_errno;
241         return;
242
243  timed_out:
244         cmdio_write_raw(STR(FTP_TIMEOUT)" Timeout\r\n");
245 /* TODO: do we need to abort (as opposed to usual shutdown) data transfer? */
246         exit(1);
247 }
248
249 /* Simple commands */
250
251 static void
252 handle_pwd(void)
253 {
254         char *cwd, *response;
255
256         cwd = xrealloc_getcwd_or_warn(NULL);
257         if (cwd == NULL)
258                 cwd = xstrdup("");
259
260         /* We have to promote each " to "" */
261         response = escape_text(" \"", cwd, ('"' << 8) + '"');
262         free(cwd);
263         cmdio_write(STRNUM32(FTP_PWDOK), response);
264         free(response);
265 }
266
267 static void
268 handle_cwd(void)
269 {
270         if (!G.ftp_arg || chdir(G.ftp_arg) != 0) {
271                 WRITE_ERR(FTP_FILEFAIL);
272                 return;
273         }
274         WRITE_OK(FTP_CWDOK);
275 }
276
277 static void
278 handle_cdup(void)
279 {
280         G.ftp_arg = (char*)"..";
281         handle_cwd();
282 }
283
284 static void
285 handle_stat(void)
286 {
287         cmdio_write_raw(STR(FTP_STATOK)"-Server status:\r\n"
288                         " TYPE: BINARY\r\n"
289                         STR(FTP_STATOK)" Ok\r\n");
290 }
291
292 /* Examples of HELP and FEAT:
293 # nc -vvv ftp.kernel.org 21
294 ftp.kernel.org (130.239.17.4:21) open
295 220 Welcome to ftp.kernel.org.
296 FEAT
297 211-Features:
298  EPRT
299  EPSV
300  MDTM
301  PASV
302  REST STREAM
303  SIZE
304  TVFS
305  UTF8
306 211 End
307 HELP
308 214-The following commands are recognized.
309  ABOR ACCT ALLO APPE CDUP CWD  DELE EPRT EPSV FEAT HELP LIST MDTM MKD
310  MODE NLST NOOP OPTS PASS PASV PORT PWD  QUIT REIN REST RETR RMD  RNFR
311  RNTO SITE SIZE SMNT STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD
312  XPWD XRMD
313 214 Help OK.
314 */
315 static void
316 handle_feat(unsigned status)
317 {
318         cmdio_write(status, "-Features:");
319         cmdio_write_raw(" EPSV\r\n"
320                         " PASV\r\n"
321                         " REST STREAM\r\n"
322                         " MDTM\r\n"
323                         " SIZE\r\n");
324         cmdio_write(status, " Ok");
325 }
326
327 /* Download commands */
328
329 static inline int
330 port_active(void)
331 {
332         return (G.port_addr != NULL);
333 }
334
335 static inline int
336 pasv_active(void)
337 {
338         return (G.pasv_listen_fd > STDOUT_FILENO);
339 }
340
341 static void
342 port_pasv_cleanup(void)
343 {
344         free(G.port_addr);
345         G.port_addr = NULL;
346         if (G.pasv_listen_fd > STDOUT_FILENO)
347                 close(G.pasv_listen_fd);
348         G.pasv_listen_fd = -1;
349 }
350
351 /* On error, emits error code to the peer */
352 static int
353 ftpdataio_get_pasv_fd(void)
354 {
355         int remote_fd;
356
357         remote_fd = accept(G.pasv_listen_fd, NULL, 0);
358
359         if (remote_fd < 0) {
360                 WRITE_ERR(FTP_BADSENDCONN);
361                 return remote_fd;
362         }
363
364         setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
365         return remote_fd;
366 }
367
368 /* Clears port/pasv data.
369  * This means we dont waste resources, for example, keeping
370  * PASV listening socket open when it is no longer needed.
371  * On error, emits error code to the peer (or exits).
372  * On success, emits p_status_msg to the peer.
373  */
374 static int
375 get_remote_transfer_fd(const char *p_status_msg)
376 {
377         int remote_fd;
378
379         if (pasv_active())
380                 /* On error, emits error code to the peer */
381                 remote_fd = ftpdataio_get_pasv_fd();
382         else
383                 /* Exits on error */
384                 remote_fd = xconnect_stream(G.port_addr);
385
386         port_pasv_cleanup();
387
388         if (remote_fd < 0)
389                 return remote_fd;
390
391         cmdio_write(STRNUM32(FTP_DATACONN), p_status_msg);
392         return remote_fd;
393 }
394
395 /* If there were neither PASV nor PORT, emits error code to the peer */
396 static int
397 port_or_pasv_was_seen(void)
398 {
399         if (!pasv_active() && !port_active()) {
400                 cmdio_write_raw(STR(FTP_BADSENDCONN)" Use PORT/PASV first\r\n");
401                 return 0;
402         }
403
404         return 1;
405 }
406
407 /* Exits on error */
408 static unsigned
409 bind_for_passive_mode(void)
410 {
411         int fd;
412         unsigned port;
413
414         port_pasv_cleanup();
415
416         G.pasv_listen_fd = fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0);
417         setsockopt_reuseaddr(fd);
418
419         set_nport(G.local_addr, 0);
420         xbind(fd, &G.local_addr->u.sa, G.local_addr->len);
421         xlisten(fd, 1);
422         getsockname(fd, &G.local_addr->u.sa, &G.local_addr->len);
423
424         port = get_nport(&G.local_addr->u.sa);
425         port = ntohs(port);
426         return port;
427 }
428
429 /* Exits on error */
430 static void
431 handle_pasv(void)
432 {
433         unsigned port;
434         char *addr, *response;
435
436         port = bind_for_passive_mode();
437
438         if (G.local_addr->u.sa.sa_family == AF_INET)
439                 addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa);
440         else /* seen this in the wild done by other ftp servers: */
441                 addr = xstrdup("0.0.0.0");
442         replace_char(addr, '.', ',');
443
444         response = xasprintf(STR(FTP_PASVOK)" PASV ok (%s,%u,%u)\r\n",
445                         addr, (int)(port >> 8), (int)(port & 255));
446         free(addr);
447         cmdio_write_raw(response);
448         free(response);
449 }
450
451 /* Exits on error */
452 static void
453 handle_epsv(void)
454 {
455         unsigned port;
456         char *response;
457
458         port = bind_for_passive_mode();
459         response = xasprintf(STR(FTP_EPSVOK)" EPSV ok (|||%u|)\r\n", port);
460         cmdio_write_raw(response);
461         free(response);
462 }
463
464 static void
465 handle_port(void)
466 {
467         unsigned port, port_hi;
468         char *raw, *comma;
469 #ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
470         socklen_t peer_ipv4_len;
471         struct sockaddr_in peer_ipv4;
472         struct in_addr port_ipv4_sin_addr;
473 #endif
474
475         port_pasv_cleanup();
476
477         raw = G.ftp_arg;
478
479         /* PORT command format makes sense only over IPv4 */
480         if (!raw
481 #ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
482          || G.local_addr->u.sa.sa_family != AF_INET
483 #endif
484         ) {
485  bail:
486                 WRITE_ERR(FTP_BADCMD);
487                 return;
488         }
489
490         comma = strrchr(raw, ',');
491         if (comma == NULL)
492                 goto bail;
493         *comma = '\0';
494         port = bb_strtou(&comma[1], NULL, 10);
495         if (errno || port > 0xff)
496                 goto bail;
497
498         comma = strrchr(raw, ',');
499         if (comma == NULL)
500                 goto bail;
501         *comma = '\0';
502         port_hi = bb_strtou(&comma[1], NULL, 10);
503         if (errno || port_hi > 0xff)
504                 goto bail;
505         port |= port_hi << 8;
506
507 #ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
508         replace_char(raw, ',', '.');
509
510         /* We are verifying that PORT's IP matches getpeername().
511          * Otherwise peer can make us open data connections
512          * to other hosts (security problem!)
513          * This code would be too simplistic:
514          * lsa = xdotted2sockaddr(raw, port);
515          * if (lsa == NULL) goto bail;
516          */
517         if (!inet_aton(raw, &port_ipv4_sin_addr))
518                 goto bail;
519         peer_ipv4_len = sizeof(peer_ipv4);
520         if (getpeername(STDIN_FILENO, &peer_ipv4, &peer_ipv4_len) != 0)
521                 goto bail;
522         if (memcmp(&port_ipv4_sin_addr, &peer_ipv4.sin_addr, sizeof(struct in_addr)) != 0)
523                 goto bail;
524
525         G.port_addr = xdotted2sockaddr(raw, port);
526 #else
527         G.port_addr = get_peer_lsa(STDIN_FILENO);
528         set_nport(G.port_addr, htons(port));
529 #endif
530         WRITE_OK(FTP_PORTOK);
531 }
532
533 static void
534 handle_rest(void)
535 {
536         /* When ftp_arg == NULL simply restart from beginning */
537         G.restart_pos = G.ftp_arg ? xatoi_u(G.ftp_arg) : 0;
538         WRITE_OK(FTP_RESTOK);
539 }
540
541 static void
542 handle_retr(void)
543 {
544         struct stat statbuf;
545         off_t bytes_transferred;
546         int remote_fd;
547         int local_file_fd;
548         off_t offset = G.restart_pos;
549         char *response;
550
551         G.restart_pos = 0;
552
553         if (!port_or_pasv_was_seen())
554                 return; /* port_or_pasv_was_seen emitted error response */
555
556         /* O_NONBLOCK is useful if file happens to be a device node */
557         local_file_fd = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1;
558         if (local_file_fd < 0) {
559                 WRITE_ERR(FTP_FILEFAIL);
560                 return;
561         }
562
563         if (fstat(local_file_fd, &statbuf) != 0 || !S_ISREG(statbuf.st_mode)) {
564                 /* Note - pretend open failed */
565                 WRITE_ERR(FTP_FILEFAIL);
566                 goto file_close_out;
567         }
568         G.local_file_fd = local_file_fd;
569
570         /* Now deactive O_NONBLOCK, otherwise we have a problem
571          * on DMAPI filesystems such as XFS DMAPI.
572          */
573         ndelay_off(local_file_fd);
574
575         /* Set the download offset (from REST) if any */
576         if (offset != 0)
577                 xlseek(local_file_fd, offset, SEEK_SET);
578
579         response = xasprintf(
580                 " Opening BINARY connection for %s (%"OFF_FMT"u bytes)",
581                 G.ftp_arg, statbuf.st_size);
582         remote_fd = get_remote_transfer_fd(response);
583         free(response);
584         if (remote_fd < 0)
585                 goto file_close_out;
586
587         bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd);
588         close(remote_fd);
589         if (bytes_transferred < 0)
590                 WRITE_ERR(FTP_BADSENDFILE);
591         else
592                 WRITE_OK(FTP_TRANSFEROK);
593
594  file_close_out:
595         close(local_file_fd);
596         G.local_file_fd = 0;
597 }
598
599 /* List commands */
600
601 static int
602 popen_ls(const char *opt)
603 {
604         const char *argv[5];
605         struct fd_pair outfd;
606         pid_t pid;
607
608         argv[0] = "ftpd";
609         argv[1] = opt; /* "-l" or "-1" */
610 #if BB_MMU
611         argv[2] = "--";
612 #else
613         /* NOMMU ftpd ls helper chdirs to argv[2],
614          * preventing peer from seeing real root. */
615         argv[2] = xrealloc_getcwd_or_warn(NULL);
616 #endif
617         argv[3] = G.ftp_arg;
618         argv[4] = NULL;
619
620         /* Improve compatibility with non-RFC conforming FTP clients
621          * which send e.g. "LIST -l", "LIST -la", "LIST -aL".
622          * See https://bugs.kde.org/show_bug.cgi?id=195578 */
623         if (ENABLE_FEATURE_FTPD_ACCEPT_BROKEN_LIST
624          && G.ftp_arg && G.ftp_arg[0] == '-'
625         ) {
626                 const char *tmp = strchr(G.ftp_arg, ' ');
627                 if (tmp) /* skip the space */
628                         tmp++;
629                 argv[3] = tmp;
630         }
631
632         xpiped_pair(outfd);
633
634         /*fflush_all(); - so far we dont use stdio on output */
635         pid = BB_MMU ? fork() : vfork();
636         if (pid < 0)
637                 bb_perror_msg_and_die(BB_MMU ? "fork" : "vfork");
638
639         if (pid == 0) {
640                 /* child */
641 #if !BB_MMU
642                 /* On NOMMU, we want to execute a child - copy of ourself.
643                  * In chroot we usually can't do it. Thus we chdir
644                  * out of the chroot back to original root,
645                  * and (see later below) execute bb_busybox_exec_path
646                  * relative to current directory */
647                 if (fchdir(G.root_fd) != 0)
648                         _exit(127);
649                 /*close(G.root_fd); - close_on_exec_on() took care of this */
650 #endif
651                 /* NB: close _first_, then move fd! */
652                 close(outfd.rd);
653                 xmove_fd(outfd.wr, STDOUT_FILENO);
654                 /* Opening /dev/null in chroot is hard.
655                  * Just making sure STDIN_FILENO is opened
656                  * to something harmless. Paranoia,
657                  * ls won't read it anyway */
658                 close(STDIN_FILENO);
659                 dup(STDOUT_FILENO); /* copy will become STDIN_FILENO */
660 #if BB_MMU
661                 /* memset(&G, 0, sizeof(G)); - ls_main does it */
662                 exit(ls_main(ARRAY_SIZE(argv) - 1, (char**) argv));
663 #else
664                 /* + 1: we must use relative path here if in chroot.
665                  * For example, execv("/proc/self/exe") will fail, since
666                  * it looks for "/proc/self/exe" _relative to chroot!_ */
667                 execv(bb_busybox_exec_path + 1, (char**) argv);
668                 _exit(127);
669 #endif
670         }
671
672         /* parent */
673         close(outfd.wr);
674 #if !BB_MMU
675         free((char*)argv[2]);
676 #endif
677         return outfd.rd;
678 }
679
680 enum {
681         USE_CTRL_CONN = 1,
682         LONG_LISTING = 2,
683 };
684
685 static void
686 handle_dir_common(int opts)
687 {
688         FILE *ls_fp;
689         char *line;
690         int ls_fd;
691
692         if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen())
693                 return; /* port_or_pasv_was_seen emitted error response */
694
695         /* -n prevents user/groupname display,
696          * which can be problematic in chroot */
697         ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1");
698         ls_fp = xfdopen_for_read(ls_fd);
699
700         if (opts & USE_CTRL_CONN) {
701                 /* STAT <filename> */
702                 cmdio_write_raw(STR(FTP_STATFILE_OK)"-File status:\r\n");
703                 while (1) {
704                         line = xmalloc_fgetline(ls_fp);
705                         if (!line)
706                                 break;
707                         /* Hack: 0 results in no status at all */
708                         /* Note: it's ok that we don't prepend space,
709                          * ftp.kernel.org doesn't do that too */
710                         cmdio_write(0, line);
711                         free(line);
712                 }
713                 WRITE_OK(FTP_STATFILE_OK);
714         } else {
715                 /* LIST/NLST [<filename>] */
716                 int remote_fd = get_remote_transfer_fd(" Directory listing");
717                 if (remote_fd >= 0) {
718                         while (1) {
719                                 line = xmalloc_fgetline(ls_fp);
720                                 if (!line)
721                                         break;
722                                 /* I've seen clients complaining when they
723                                  * are fed with ls output with bare '\n'.
724                                  * Pity... that would be much simpler.
725                                  */
726 /* TODO: need to s/LF/NUL/g here */
727                                 xwrite_str(remote_fd, line);
728                                 xwrite(remote_fd, "\r\n", 2);
729                                 free(line);
730                         }
731                 }
732                 close(remote_fd);
733                 WRITE_OK(FTP_TRANSFEROK);
734         }
735         fclose(ls_fp); /* closes ls_fd too */
736 }
737 static void
738 handle_list(void)
739 {
740         handle_dir_common(LONG_LISTING);
741 }
742 static void
743 handle_nlst(void)
744 {
745         /* NLST returns list of names, "\r\n" terminated without regard
746          * to the current binary flag. Names may start with "/",
747          * then they represent full names (we don't produce such names),
748          * otherwise names are relative to current directory.
749          * Embedded "\n" are replaced by NULs. This is safe since names
750          * can never contain NUL.
751          */
752         handle_dir_common(0);
753 }
754 static void
755 handle_stat_file(void)
756 {
757         handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
758 }
759
760 /* This can be extended to handle MLST, as all info is available
761  * in struct stat for that:
762  * MLST file_name
763  * 250-Listing file_name
764  *  type=file;size=4161;modify=19970214165800; /dir/dir/file_name
765  * 250 End
766  * Nano-doc:
767  * MLST [<file or dir name, "." assumed if not given>]
768  * Returned name should be either the same as requested, or fully qualified.
769  * If there was no parameter, return "" or (preferred) fully-qualified name.
770  * Returned "facts" (case is not important):
771  *  size    - size in octets
772  *  modify  - last modification time
773  *  type    - entry type (file,dir,OS.unix=block)
774  *            (+ cdir and pdir types for MLSD)
775  *  unique  - unique id of file/directory (inode#)
776  *  perm    -
777  *      a: can be appended to (APPE)
778  *      d: can be deleted (RMD/DELE)
779  *      f: can be renamed (RNFR)
780  *      r: can be read (RETR)
781  *      w: can be written (STOR)
782  *      e: can CWD into this dir
783  *      l: this dir can be listed (dir only!)
784  *      c: can create files in this dir
785  *      m: can create dirs in this dir (MKD)
786  *      p: can delete files in this dir
787  *  UNIX.mode - unix file mode
788  */
789 static void
790 handle_size_or_mdtm(int need_size)
791 {
792         struct stat statbuf;
793         struct tm broken_out;
794         char buf[(sizeof("NNN %"OFF_FMT"u\r\n") + sizeof(off_t) * 3)
795                 | sizeof("NNN YYYYMMDDhhmmss\r\n")
796         ];
797
798         if (!G.ftp_arg
799          || stat(G.ftp_arg, &statbuf) != 0
800          || !S_ISREG(statbuf.st_mode)
801         ) {
802                 WRITE_ERR(FTP_FILEFAIL);
803                 return;
804         }
805         if (need_size) {
806                 sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size);
807         } else {
808                 gmtime_r(&statbuf.st_mtime, &broken_out);
809                 sprintf(buf, STR(FTP_STATFILE_OK)" %04u%02u%02u%02u%02u%02u\r\n",
810                         broken_out.tm_year + 1900,
811                         broken_out.tm_mon,
812                         broken_out.tm_mday,
813                         broken_out.tm_hour,
814                         broken_out.tm_min,
815                         broken_out.tm_sec);
816         }
817         cmdio_write_raw(buf);
818 }
819
820 /* Upload commands */
821
822 #if ENABLE_FEATURE_FTP_WRITE
823 static void
824 handle_mkd(void)
825 {
826         if (!G.ftp_arg || mkdir(G.ftp_arg, 0777) != 0) {
827                 WRITE_ERR(FTP_FILEFAIL);
828                 return;
829         }
830         WRITE_OK(FTP_MKDIROK);
831 }
832
833 static void
834 handle_rmd(void)
835 {
836         if (!G.ftp_arg || rmdir(G.ftp_arg) != 0) {
837                 WRITE_ERR(FTP_FILEFAIL);
838                 return;
839         }
840         WRITE_OK(FTP_RMDIROK);
841 }
842
843 static void
844 handle_dele(void)
845 {
846         if (!G.ftp_arg || unlink(G.ftp_arg) != 0) {
847                 WRITE_ERR(FTP_FILEFAIL);
848                 return;
849         }
850         WRITE_OK(FTP_DELEOK);
851 }
852
853 static void
854 handle_rnfr(void)
855 {
856         free(G.rnfr_filename);
857         G.rnfr_filename = xstrdup(G.ftp_arg);
858         WRITE_OK(FTP_RNFROK);
859 }
860
861 static void
862 handle_rnto(void)
863 {
864         int retval;
865
866         /* If we didn't get a RNFR, throw a wobbly */
867         if (G.rnfr_filename == NULL || G.ftp_arg == NULL) {
868                 cmdio_write_raw(STR(FTP_NEEDRNFR)" Use RNFR first\r\n");
869                 return;
870         }
871
872         retval = rename(G.rnfr_filename, G.ftp_arg);
873         free(G.rnfr_filename);
874         G.rnfr_filename = NULL;
875
876         if (retval) {
877                 WRITE_ERR(FTP_FILEFAIL);
878                 return;
879         }
880         WRITE_OK(FTP_RENAMEOK);
881 }
882
883 static void
884 handle_upload_common(int is_append, int is_unique)
885 {
886         struct stat statbuf;
887         char *tempname;
888         off_t bytes_transferred;
889         off_t offset;
890         int local_file_fd;
891         int remote_fd;
892
893         offset = G.restart_pos;
894         G.restart_pos = 0;
895
896         if (!port_or_pasv_was_seen())
897                 return; /* port_or_pasv_was_seen emitted error response */
898
899         tempname = NULL;
900         local_file_fd = -1;
901         if (is_unique) {
902                 tempname = xstrdup(" FILE: uniq.XXXXXX");
903                 local_file_fd = mkstemp(tempname + 7);
904         } else if (G.ftp_arg) {
905                 int flags = O_WRONLY | O_CREAT | O_TRUNC;
906                 if (is_append)
907                         flags = O_WRONLY | O_CREAT | O_APPEND;
908                 if (offset)
909                         flags = O_WRONLY | O_CREAT;
910                 local_file_fd = open(G.ftp_arg, flags, 0666);
911         }
912
913         if (local_file_fd < 0
914          || fstat(local_file_fd, &statbuf) != 0
915          || !S_ISREG(statbuf.st_mode)
916         ) {
917                 WRITE_ERR(FTP_UPLOADFAIL);
918                 if (local_file_fd >= 0)
919                         goto close_local_and_bail;
920                 return;
921         }
922         G.local_file_fd = local_file_fd;
923
924         if (offset)
925                 xlseek(local_file_fd, offset, SEEK_SET);
926
927         remote_fd = get_remote_transfer_fd(tempname ? tempname : " Ok to send data");
928         free(tempname);
929
930         if (remote_fd < 0)
931                 goto close_local_and_bail;
932
933         bytes_transferred = bb_copyfd_eof(remote_fd, local_file_fd);
934         close(remote_fd);
935         if (bytes_transferred < 0)
936                 WRITE_ERR(FTP_BADSENDFILE);
937         else
938                 WRITE_OK(FTP_TRANSFEROK);
939
940  close_local_and_bail:
941         close(local_file_fd);
942         G.local_file_fd = 0;
943 }
944
945 static void
946 handle_stor(void)
947 {
948         handle_upload_common(0, 0);
949 }
950
951 static void
952 handle_appe(void)
953 {
954         G.restart_pos = 0;
955         handle_upload_common(1, 0);
956 }
957
958 static void
959 handle_stou(void)
960 {
961         G.restart_pos = 0;
962         handle_upload_common(0, 1);
963 }
964 #endif /* ENABLE_FEATURE_FTP_WRITE */
965
966 static uint32_t
967 cmdio_get_cmd_and_arg(void)
968 {
969         int len;
970         uint32_t cmdval;
971         char *cmd;
972
973         alarm(G.timeout);
974
975         free(G.ftp_cmd);
976         {
977                 /* Paranoia. Peer may send 1 gigabyte long cmd... */
978                 /* Using separate len_on_stk instead of len optimizes
979                  * code size (allows len to be in CPU register) */
980                 size_t len_on_stk = 8 * 1024;
981                 G.ftp_cmd = cmd = xmalloc_fgets_str_len(stdin, "\r\n", &len_on_stk);
982                 if (!cmd)
983                         exit(0);
984                 len = len_on_stk;
985         }
986
987         /* De-escape telnet: 0xff,0xff => 0xff */
988         /* RFC959 says that ABOR, STAT, QUIT may be sent even during
989          * data transfer, and may be preceded by telnet's "Interrupt Process"
990          * code (two-byte sequence 255,244) and then by telnet "Synch" code
991          * 255,242 (byte 242 is sent with TCP URG bit using send(MSG_OOB)
992          * and may generate SIGURG on our side. See RFC854).
993          * So far we don't support that (may install SIGURG handler if we'd want to),
994          * but we need to at least remove 255,xxx pairs. lftp sends those. */
995         /* Then de-escape FTP: NUL => '\n' */
996         /* Testing for \xff:
997          * Create file named '\xff': echo Hello >`echo -ne "\xff"`
998          * Try to get it:            ftpget -v 127.0.0.1 Eff `echo -ne "\xff\xff"`
999          * (need "\xff\xff" until ftpget applet is fixed to do escaping :)
1000          * Testing for embedded LF:
1001          * LF_HERE=`echo -ne "LF\nHERE"`
1002          * echo Hello >"$LF_HERE"
1003          * ftpget -v 127.0.0.1 LF_HERE "$LF_HERE"
1004          */
1005         {
1006                 int dst, src;
1007
1008                 /* Strip "\r\n" if it is there */
1009                 if (len != 0 && cmd[len - 1] == '\n') {
1010                         len--;
1011                         if (len != 0 && cmd[len - 1] == '\r')
1012                                 len--;
1013                         cmd[len] = '\0';
1014                 }
1015                 src = strchrnul(cmd, 0xff) - cmd;
1016                 /* 99,99% there are neither NULs nor 255s and src == len */
1017                 if (src < len) {
1018                         dst = src;
1019                         do {
1020                                 if ((unsigned char)(cmd[src]) == 255) {
1021                                         src++;
1022                                         /* 255,xxx - skip 255 */
1023                                         if ((unsigned char)(cmd[src]) != 255) {
1024                                                 /* 255,!255 - skip both */
1025                                                 src++;
1026                                                 continue;
1027                                         }
1028                                         /* 255,255 - retain one 255 */
1029                                 }
1030                                 /* NUL => '\n' */
1031                                 cmd[dst++] = cmd[src] ? cmd[src] : '\n';
1032                                 src++;
1033                         } while (src < len);
1034                         cmd[dst] = '\0';
1035                 }
1036         }
1037
1038         if (G.verbose > 1)
1039                 verbose_log(cmd);
1040
1041         G.ftp_arg = strchr(cmd, ' ');
1042         if (G.ftp_arg != NULL)
1043                 *G.ftp_arg++ = '\0';
1044
1045         /* Uppercase and pack into uint32_t first word of the command */
1046         cmdval = 0;
1047         while (*cmd)
1048                 cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20);
1049
1050         return cmdval;
1051 }
1052
1053 #define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d)
1054 #define mk_const3(a,b,c)    ((a * 0x100 + b) * 0x100 + c)
1055 enum {
1056         const_ALLO = mk_const4('A', 'L', 'L', 'O'),
1057         const_APPE = mk_const4('A', 'P', 'P', 'E'),
1058         const_CDUP = mk_const4('C', 'D', 'U', 'P'),
1059         const_CWD  = mk_const3('C', 'W', 'D'),
1060         const_DELE = mk_const4('D', 'E', 'L', 'E'),
1061         const_EPSV = mk_const4('E', 'P', 'S', 'V'),
1062         const_FEAT = mk_const4('F', 'E', 'A', 'T'),
1063         const_HELP = mk_const4('H', 'E', 'L', 'P'),
1064         const_LIST = mk_const4('L', 'I', 'S', 'T'),
1065         const_MDTM = mk_const4('M', 'D', 'T', 'M'),
1066         const_MKD  = mk_const3('M', 'K', 'D'),
1067         const_MODE = mk_const4('M', 'O', 'D', 'E'),
1068         const_NLST = mk_const4('N', 'L', 'S', 'T'),
1069         const_NOOP = mk_const4('N', 'O', 'O', 'P'),
1070         const_PASS = mk_const4('P', 'A', 'S', 'S'),
1071         const_PASV = mk_const4('P', 'A', 'S', 'V'),
1072         const_PORT = mk_const4('P', 'O', 'R', 'T'),
1073         const_PWD  = mk_const3('P', 'W', 'D'),
1074         const_QUIT = mk_const4('Q', 'U', 'I', 'T'),
1075         const_REST = mk_const4('R', 'E', 'S', 'T'),
1076         const_RETR = mk_const4('R', 'E', 'T', 'R'),
1077         const_RMD  = mk_const3('R', 'M', 'D'),
1078         const_RNFR = mk_const4('R', 'N', 'F', 'R'),
1079         const_RNTO = mk_const4('R', 'N', 'T', 'O'),
1080         const_SIZE = mk_const4('S', 'I', 'Z', 'E'),
1081         const_STAT = mk_const4('S', 'T', 'A', 'T'),
1082         const_STOR = mk_const4('S', 'T', 'O', 'R'),
1083         const_STOU = mk_const4('S', 'T', 'O', 'U'),
1084         const_STRU = mk_const4('S', 'T', 'R', 'U'),
1085         const_SYST = mk_const4('S', 'Y', 'S', 'T'),
1086         const_TYPE = mk_const4('T', 'Y', 'P', 'E'),
1087         const_USER = mk_const4('U', 'S', 'E', 'R'),
1088
1089 #if !BB_MMU
1090         OPT_l = (1 << 0),
1091         OPT_1 = (1 << 1),
1092 #endif
1093         OPT_v = (1 << ((!BB_MMU) * 2 + 0)),
1094         OPT_S = (1 << ((!BB_MMU) * 2 + 1)),
1095         OPT_w = (1 << ((!BB_MMU) * 2 + 2)) * ENABLE_FEATURE_FTP_WRITE,
1096 };
1097
1098 int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
1099 #if !BB_MMU
1100 int ftpd_main(int argc, char **argv)
1101 #else
1102 int ftpd_main(int argc UNUSED_PARAM, char **argv)
1103 #endif
1104 {
1105         unsigned abs_timeout;
1106         unsigned verbose_S;
1107         smallint opts;
1108
1109         INIT_G();
1110
1111         abs_timeout = 1 * 60 * 60;
1112         verbose_S = 0;
1113         G.timeout = 2 * 60;
1114         opt_complementary = "t+:T+:vv:SS";
1115 #if BB_MMU
1116         opts = getopt32(argv,   "vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
1117 #else
1118         opts = getopt32(argv, "l1vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
1119         if (opts & (OPT_l|OPT_1)) {
1120                 /* Our secret backdoor to ls */
1121 /* TODO: pass -n? It prevents user/group resolution, which may not work in chroot anyway */
1122 /* TODO: pass -A? It shows dot files */
1123 /* TODO: pass --group-directories-first? would be nice, but ls doesn't do that yet */
1124                 xchdir(argv[2]);
1125                 argv[2] = (char*)"--";
1126                 /* memset(&G, 0, sizeof(G)); - ls_main does it */
1127                 return ls_main(argc, argv);
1128         }
1129 #endif
1130         if (G.verbose < verbose_S)
1131                 G.verbose = verbose_S;
1132         if (abs_timeout | G.timeout) {
1133                 if (abs_timeout == 0)
1134                         abs_timeout = INT_MAX;
1135                 G.end_time = monotonic_sec() + abs_timeout;
1136                 if (G.timeout > abs_timeout)
1137                         G.timeout = abs_timeout;
1138         }
1139         strcpy(G.msg_ok  + 4, MSG_OK );
1140         strcpy(G.msg_err + 4, MSG_ERR);
1141
1142         G.local_addr = get_sock_lsa(STDIN_FILENO);
1143         if (!G.local_addr) {
1144                 /* This is confusing:
1145                  * bb_error_msg_and_die("stdin is not a socket");
1146                  * Better: */
1147                 bb_show_usage();
1148                 /* Help text says that ftpd must be used as inetd service,
1149                  * which is by far the most usual cause of get_sock_lsa
1150                  * failure */
1151         }
1152
1153         if (!(opts & OPT_v))
1154                 logmode = LOGMODE_NONE;
1155         if (opts & OPT_S) {
1156                 /* LOG_NDELAY is needed since we may chroot later */
1157                 openlog(applet_name, LOG_PID | LOG_NDELAY, LOG_DAEMON);
1158                 logmode |= LOGMODE_SYSLOG;
1159         }
1160         if (logmode)
1161                 applet_name = xasprintf("%s[%u]", applet_name, (int)getpid());
1162
1163 #if !BB_MMU
1164         G.root_fd = xopen("/", O_RDONLY | O_DIRECTORY);
1165         close_on_exec_on(G.root_fd);
1166 #endif
1167
1168         if (argv[optind]) {
1169                 xchdir(argv[optind]);
1170                 chroot(".");
1171         }
1172
1173         //umask(077); - admin can set umask before starting us
1174
1175         /* Signals. We'll always take -EPIPE rather than a rude signal, thanks */
1176         signal(SIGPIPE, SIG_IGN);
1177
1178         /* Set up options on the command socket (do we need these all? why?) */
1179         setsockopt(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY, &const_int_1, sizeof(const_int_1));
1180         setsockopt(STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
1181         /* Telnet protocol over command link may send "urgent" data,
1182          * we prefer it to be received in the "normal" data stream: */
1183         setsockopt(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1));
1184
1185         WRITE_OK(FTP_GREET);
1186         signal(SIGALRM, timeout_handler);
1187
1188 #ifdef IF_WE_WANT_TO_REQUIRE_LOGIN
1189         {
1190                 smallint user_was_specified = 0;
1191                 while (1) {
1192                         uint32_t cmdval = cmdio_get_cmd_and_arg();
1193
1194                         if (cmdval == const_USER) {
1195                                 if (G.ftp_arg == NULL || strcasecmp(G.ftp_arg, "anonymous") != 0)
1196                                         cmdio_write_raw(STR(FTP_LOGINERR)" Server is anonymous only\r\n");
1197                                 else {
1198                                         user_was_specified = 1;
1199                                         cmdio_write_raw(STR(FTP_GIVEPWORD)" Please specify the password\r\n");
1200                                 }
1201                         } else if (cmdval == const_PASS) {
1202                                 if (user_was_specified)
1203                                         break;
1204                                 cmdio_write_raw(STR(FTP_NEEDUSER)" Login with USER\r\n");
1205                         } else if (cmdval == const_QUIT) {
1206                                 WRITE_OK(FTP_GOODBYE);
1207                                 return 0;
1208                         } else {
1209                                 cmdio_write_raw(STR(FTP_LOGINERR)" Login with USER and PASS\r\n");
1210                         }
1211                 }
1212         }
1213         WRITE_OK(FTP_LOGINOK);
1214 #endif
1215
1216         /* RFC-959 Section 5.1
1217          * The following commands and options MUST be supported by every
1218          * server-FTP and user-FTP, except in cases where the underlying
1219          * file system or operating system does not allow or support
1220          * a particular command.
1221          * Type: ASCII Non-print, IMAGE, LOCAL 8
1222          * Mode: Stream
1223          * Structure: File, Record*
1224          * (Record structure is REQUIRED only for hosts whose file
1225          *  systems support record structure).
1226          * Commands:
1227          * USER, PASS, ACCT, [bbox: ACCT not supported]
1228          * PORT, PASV,
1229          * TYPE, MODE, STRU,
1230          * RETR, STOR, APPE,
1231          * RNFR, RNTO, DELE,
1232          * CWD,  CDUP, RMD,  MKD,  PWD,
1233          * LIST, NLST,
1234          * SYST, STAT,
1235          * HELP, NOOP, QUIT.
1236          */
1237         /* ACCOUNT (ACCT)
1238          * "The argument field is a Telnet string identifying the user's account.
1239          * The command is not necessarily related to the USER command, as some
1240          * sites may require an account for login and others only for specific
1241          * access, such as storing files. In the latter case the command may
1242          * arrive at any time.
1243          * There are reply codes to differentiate these cases for the automation:
1244          * when account information is required for login, the response to
1245          * a successful PASSword command is reply code 332. On the other hand,
1246          * if account information is NOT required for login, the reply to
1247          * a successful PASSword command is 230; and if the account information
1248          * is needed for a command issued later in the dialogue, the server
1249          * should return a 332 or 532 reply depending on whether it stores
1250          * (pending receipt of the ACCounT command) or discards the command,
1251          * respectively."
1252          */
1253
1254         while (1) {
1255                 uint32_t cmdval = cmdio_get_cmd_and_arg();
1256
1257                 if (cmdval == const_QUIT) {
1258                         WRITE_OK(FTP_GOODBYE);
1259                         return 0;
1260                 }
1261                 else if (cmdval == const_USER)
1262                         /* This would mean "ok, now give me PASS". */
1263                         /*WRITE_OK(FTP_GIVEPWORD);*/
1264                         /* vsftpd can be configured to not require that,
1265                          * and this also saves one roundtrip:
1266                          */
1267                         WRITE_OK(FTP_LOGINOK);
1268                 else if (cmdval == const_PASS)
1269                         WRITE_OK(FTP_LOGINOK);
1270                 else if (cmdval == const_NOOP)
1271                         WRITE_OK(FTP_NOOPOK);
1272                 else if (cmdval == const_TYPE)
1273                         WRITE_OK(FTP_TYPEOK);
1274                 else if (cmdval == const_STRU)
1275                         WRITE_OK(FTP_STRUOK);
1276                 else if (cmdval == const_MODE)
1277                         WRITE_OK(FTP_MODEOK);
1278                 else if (cmdval == const_ALLO)
1279                         WRITE_OK(FTP_ALLOOK);
1280                 else if (cmdval == const_SYST)
1281                         cmdio_write_raw(STR(FTP_SYSTOK)" UNIX Type: L8\r\n");
1282                 else if (cmdval == const_PWD)
1283                         handle_pwd();
1284                 else if (cmdval == const_CWD)
1285                         handle_cwd();
1286                 else if (cmdval == const_CDUP) /* cd .. */
1287                         handle_cdup();
1288                 /* HELP is nearly useless, but we can reuse FEAT for it */
1289                 /* lftp uses FEAT */
1290                 else if (cmdval == const_HELP || cmdval == const_FEAT)
1291                         handle_feat(cmdval == const_HELP
1292                                         ? STRNUM32(FTP_HELP)
1293                                         : STRNUM32(FTP_STATOK)
1294                         );
1295                 else if (cmdval == const_LIST) /* ls -l */
1296                         handle_list();
1297                 else if (cmdval == const_NLST) /* "name list", bare ls */
1298                         handle_nlst();
1299                 /* SIZE is crucial for wget's download indicator etc */
1300                 /* Mozilla, lftp use MDTM (presumably for caching) */
1301                 else if (cmdval == const_SIZE || cmdval == const_MDTM)
1302                         handle_size_or_mdtm(cmdval == const_SIZE);
1303                 else if (cmdval == const_STAT) {
1304                         if (G.ftp_arg == NULL)
1305                                 handle_stat();
1306                         else
1307                                 handle_stat_file();
1308                 }
1309                 else if (cmdval == const_PASV)
1310                         handle_pasv();
1311                 else if (cmdval == const_EPSV)
1312                         handle_epsv();
1313                 else if (cmdval == const_RETR)
1314                         handle_retr();
1315                 else if (cmdval == const_PORT)
1316                         handle_port();
1317                 else if (cmdval == const_REST)
1318                         handle_rest();
1319 #if ENABLE_FEATURE_FTP_WRITE
1320                 else if (opts & OPT_w) {
1321                         if (cmdval == const_STOR)
1322                                 handle_stor();
1323                         else if (cmdval == const_MKD)
1324                                 handle_mkd();
1325                         else if (cmdval == const_RMD)
1326                                 handle_rmd();
1327                         else if (cmdval == const_DELE)
1328                                 handle_dele();
1329                         else if (cmdval == const_RNFR) /* "rename from" */
1330                                 handle_rnfr();
1331                         else if (cmdval == const_RNTO) /* "rename to" */
1332                                 handle_rnto();
1333                         else if (cmdval == const_APPE)
1334                                 handle_appe();
1335                         else if (cmdval == const_STOU) /* "store unique" */
1336                                 handle_stou();
1337                         else
1338                                 goto bad_cmd;
1339                 }
1340 #endif
1341 #if 0
1342                 else if (cmdval == const_STOR
1343                  || cmdval == const_MKD
1344                  || cmdval == const_RMD
1345                  || cmdval == const_DELE
1346                  || cmdval == const_RNFR
1347                  || cmdval == const_RNTO
1348                  || cmdval == const_APPE
1349                  || cmdval == const_STOU
1350                 ) {
1351                         cmdio_write_raw(STR(FTP_NOPERM)" Permission denied\r\n");
1352                 }
1353 #endif
1354                 else {
1355                         /* Which unsupported commands were seen in the wild?
1356                          * (doesn't necessarily mean "we must support them")
1357                          * foo 1.2.3: XXXX - comment
1358                          */
1359 #if ENABLE_FEATURE_FTP_WRITE
1360  bad_cmd:
1361 #endif
1362                         cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");
1363                 }
1364         }
1365 }