4e9f65ca3d5de8549edf4f0e182b7f0c55d1625e
[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 };
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 /* libbb candidate */
465 static
466 len_and_sockaddr* get_peer_lsa(int fd)
467 {
468         len_and_sockaddr *lsa;
469         socklen_t len = 0;
470
471         if (getpeername(fd, NULL, &len) != 0)
472                 return NULL;
473         lsa = xzalloc(LSA_LEN_SIZE + len);
474         lsa->len = len;
475         getpeername(fd, &lsa->u.sa, &lsa->len);
476         return lsa;
477 }
478
479 static void
480 handle_port(void)
481 {
482         unsigned port, port_hi;
483         char *raw, *comma;
484 #ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
485         socklen_t peer_ipv4_len;
486         struct sockaddr_in peer_ipv4;
487         struct in_addr port_ipv4_sin_addr;
488 #endif
489
490         port_pasv_cleanup();
491
492         raw = G.ftp_arg;
493
494         /* PORT command format makes sense only over IPv4 */
495         if (!raw
496 #ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
497          || G.local_addr->u.sa.sa_family != AF_INET
498 #endif
499         ) {
500  bail:
501                 WRITE_ERR(FTP_BADCMD);
502                 return;
503         }
504
505         comma = strrchr(raw, ',');
506         if (comma == NULL)
507                 goto bail;
508         *comma = '\0';
509         port = bb_strtou(&comma[1], NULL, 10);
510         if (errno || port > 0xff)
511                 goto bail;
512
513         comma = strrchr(raw, ',');
514         if (comma == NULL)
515                 goto bail;
516         *comma = '\0';
517         port_hi = bb_strtou(&comma[1], NULL, 10);
518         if (errno || port_hi > 0xff)
519                 goto bail;
520         port |= port_hi << 8;
521
522 #ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES
523         replace_char(raw, ',', '.');
524
525         /* We are verifying that PORT's IP matches getpeername().
526          * Otherwise peer can make us open data connections
527          * to other hosts (security problem!)
528          * This code would be too simplistic:
529          * lsa = xdotted2sockaddr(raw, port);
530          * if (lsa == NULL) goto bail;
531          */
532         if (!inet_aton(raw, &port_ipv4_sin_addr))
533                 goto bail;
534         peer_ipv4_len = sizeof(peer_ipv4);
535         if (getpeername(STDIN_FILENO, &peer_ipv4, &peer_ipv4_len) != 0)
536                 goto bail;
537         if (memcmp(&port_ipv4_sin_addr, &peer_ipv4.sin_addr, sizeof(struct in_addr)) != 0)
538                 goto bail;
539
540         G.port_addr = xdotted2sockaddr(raw, port);
541 #else
542         G.port_addr = get_peer_lsa(STDIN_FILENO);
543         set_nport(G.port_addr, htons(port));
544 #endif
545         WRITE_OK(FTP_PORTOK);
546 }
547
548 static void
549 handle_rest(void)
550 {
551         /* When ftp_arg == NULL simply restart from beginning */
552         G.restart_pos = G.ftp_arg ? xatoi_u(G.ftp_arg) : 0;
553         WRITE_OK(FTP_RESTOK);
554 }
555
556 static void
557 handle_retr(void)
558 {
559         struct stat statbuf;
560         off_t bytes_transferred;
561         int remote_fd;
562         int local_file_fd;
563         off_t offset = G.restart_pos;
564         char *response;
565
566         G.restart_pos = 0;
567
568         if (!port_or_pasv_was_seen())
569                 return; /* port_or_pasv_was_seen emitted error response */
570
571         /* O_NONBLOCK is useful if file happens to be a device node */
572         local_file_fd = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1;
573         if (local_file_fd < 0) {
574                 WRITE_ERR(FTP_FILEFAIL);
575                 return;
576         }
577
578         if (fstat(local_file_fd, &statbuf) != 0 || !S_ISREG(statbuf.st_mode)) {
579                 /* Note - pretend open failed */
580                 WRITE_ERR(FTP_FILEFAIL);
581                 goto file_close_out;
582         }
583         G.local_file_fd = local_file_fd;
584
585         /* Now deactive O_NONBLOCK, otherwise we have a problem
586          * on DMAPI filesystems such as XFS DMAPI.
587          */
588         ndelay_off(local_file_fd);
589
590         /* Set the download offset (from REST) if any */
591         if (offset != 0)
592                 xlseek(local_file_fd, offset, SEEK_SET);
593
594         response = xasprintf(
595                 " Opening BINARY connection for %s (%"OFF_FMT"u bytes)",
596                 G.ftp_arg, statbuf.st_size);
597         remote_fd = get_remote_transfer_fd(response);
598         free(response);
599         if (remote_fd < 0)
600                 goto file_close_out;
601
602         bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd);
603         close(remote_fd);
604         if (bytes_transferred < 0)
605                 WRITE_ERR(FTP_BADSENDFILE);
606         else
607                 WRITE_OK(FTP_TRANSFEROK);
608
609  file_close_out:
610         close(local_file_fd);
611         G.local_file_fd = 0;
612 }
613
614 /* List commands */
615
616 static int
617 popen_ls(const char *opt)
618 {
619         const char *argv[5];
620         struct fd_pair outfd;
621         pid_t pid;
622
623         argv[0] = "ftpd";
624         argv[1] = opt; /* "-l" or "-1" */
625 #if BB_MMU
626         argv[2] = "--";
627 #else
628         /* NOMMU ftpd ls helper chdirs to argv[2],
629          * preventing peer from seeing real root. */
630         argv[2] = xrealloc_getcwd_or_warn(NULL);
631 #endif
632         argv[3] = G.ftp_arg;
633         argv[4] = NULL;
634
635         xpiped_pair(outfd);
636
637         /*fflush_all(); - so far we dont use stdio on output */
638         pid = BB_MMU ? fork() : vfork();
639         if (pid < 0)
640                 bb_perror_msg_and_die(BB_MMU ? "fork" : "vfork");
641
642         if (pid == 0) {
643                 /* child */
644 #if !BB_MMU
645                 /* On NOMMU, we want to execute a child - copy of ourself.
646                  * In chroot we usually can't do it. Thus we chdir
647                  * out of the chroot back to original root,
648                  * and (see later below) execute bb_busybox_exec_path
649                  * relative to current directory */
650                 if (fchdir(G.root_fd) != 0)
651                         _exit(127);
652                 /*close(G.root_fd); - close_on_exec_on() took care of this */
653 #endif
654                 /* NB: close _first_, then move fd! */
655                 close(outfd.rd);
656                 xmove_fd(outfd.wr, STDOUT_FILENO);
657                 /* Opening /dev/null in chroot is hard.
658                  * Just making sure STDIN_FILENO is opened
659                  * to something harmless. Paranoia,
660                  * ls won't read it anyway */
661                 close(STDIN_FILENO);
662                 dup(STDOUT_FILENO); /* copy will become STDIN_FILENO */
663 #if BB_MMU
664                 /* memset(&G, 0, sizeof(G)); - ls_main does it */
665                 exit(ls_main(ARRAY_SIZE(argv) - 1, (char**) argv));
666 #else
667                 /* + 1: we must use relative path here if in chroot.
668                  * For example, execv("/proc/self/exe") will fail, since
669                  * it looks for "/proc/self/exe" _relative to chroot!_ */
670                 execv(bb_busybox_exec_path + 1, (char**) argv);
671                 _exit(127);
672 #endif
673         }
674
675         /* parent */
676         close(outfd.wr);
677 #if !BB_MMU
678         free((char*)argv[2]);
679 #endif
680         return outfd.rd;
681 }
682
683 enum {
684         USE_CTRL_CONN = 1,
685         LONG_LISTING = 2,
686 };
687
688 static void
689 handle_dir_common(int opts)
690 {
691         FILE *ls_fp;
692         char *line;
693         int ls_fd;
694
695         if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen())
696                 return; /* port_or_pasv_was_seen emitted error response */
697
698         /* -n prevents user/groupname display,
699          * which can be problematic in chroot */
700         ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1");
701         ls_fp = xfdopen_for_read(ls_fd);
702
703         if (opts & USE_CTRL_CONN) {
704                 /* STAT <filename> */
705                 cmdio_write_raw(STR(FTP_STATFILE_OK)"-File status:\r\n");
706                 while (1) {
707                         line = xmalloc_fgetline(ls_fp);
708                         if (!line)
709                                 break;
710                         /* Hack: 0 results in no status at all */
711                         /* Note: it's ok that we don't prepend space,
712                          * ftp.kernel.org doesn't do that too */
713                         cmdio_write(0, line);
714                         free(line);
715                 }
716                 WRITE_OK(FTP_STATFILE_OK);
717         } else {
718                 /* LIST/NLST [<filename>] */
719                 int remote_fd = get_remote_transfer_fd(" Directory listing");
720                 if (remote_fd >= 0) {
721                         while (1) {
722                                 line = xmalloc_fgetline(ls_fp);
723                                 if (!line)
724                                         break;
725                                 /* I've seen clients complaining when they
726                                  * are fed with ls output with bare '\n'.
727                                  * Pity... that would be much simpler.
728                                  */
729 /* TODO: need to s/LF/NUL/g here */
730                                 xwrite_str(remote_fd, line);
731                                 xwrite(remote_fd, "\r\n", 2);
732                                 free(line);
733                         }
734                 }
735                 close(remote_fd);
736                 WRITE_OK(FTP_TRANSFEROK);
737         }
738         fclose(ls_fp); /* closes ls_fd too */
739 }
740 static void
741 handle_list(void)
742 {
743         handle_dir_common(LONG_LISTING);
744 }
745 static void
746 handle_nlst(void)
747 {
748         /* NLST returns list of names, "\r\n" terminated without regard
749          * to the current binary flag. Names may start with "/",
750          * then they represent full names (we don't produce such names),
751          * otherwise names are relative to current directory.
752          * Embedded "\n" are replaced by NULs. This is safe since names
753          * can never contain NUL.
754          */
755         handle_dir_common(0);
756 }
757 static void
758 handle_stat_file(void)
759 {
760         handle_dir_common(LONG_LISTING + USE_CTRL_CONN);
761 }
762
763 /* This can be extended to handle MLST, as all info is available
764  * in struct stat for that:
765  * MLST file_name
766  * 250-Listing file_name
767  *  type=file;size=4161;modify=19970214165800; /dir/dir/file_name
768  * 250 End
769  * Nano-doc:
770  * MLST [<file or dir name, "." assumed if not given>]
771  * Returned name should be either the same as requested, or fully qualified.
772  * If there was no parameter, return "" or (preferred) fully-qualified name.
773  * Returned "facts" (case is not important):
774  *  size    - size in octets
775  *  modify  - last modification time
776  *  type    - entry type (file,dir,OS.unix=block)
777  *            (+ cdir and pdir types for MLSD)
778  *  unique  - unique id of file/directory (inode#)
779  *  perm    -
780  *      a: can be appended to (APPE)
781  *      d: can be deleted (RMD/DELE)
782  *      f: can be renamed (RNFR)
783  *      r: can be read (RETR)
784  *      w: can be written (STOR)
785  *      e: can CWD into this dir
786  *      l: this dir can be listed (dir only!)
787  *      c: can create files in this dir
788  *      m: can create dirs in this dir (MKD)
789  *      p: can delete files in this dir
790  *  UNIX.mode - unix file mode
791  */
792 static void
793 handle_size_or_mdtm(int need_size)
794 {
795         struct stat statbuf;
796         struct tm broken_out;
797         char buf[(sizeof("NNN %"OFF_FMT"u\r\n") + sizeof(off_t) * 3)
798                 | sizeof("NNN YYYYMMDDhhmmss\r\n")
799         ];
800
801         if (!G.ftp_arg
802          || stat(G.ftp_arg, &statbuf) != 0
803          || !S_ISREG(statbuf.st_mode)
804         ) {
805                 WRITE_ERR(FTP_FILEFAIL);
806                 return;
807         }
808         if (need_size) {
809                 sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size);
810         } else {
811                 gmtime_r(&statbuf.st_mtime, &broken_out);
812                 sprintf(buf, STR(FTP_STATFILE_OK)" %04u%02u%02u%02u%02u%02u\r\n",
813                         broken_out.tm_year + 1900,
814                         broken_out.tm_mon,
815                         broken_out.tm_mday,
816                         broken_out.tm_hour,
817                         broken_out.tm_min,
818                         broken_out.tm_sec);
819         }
820         cmdio_write_raw(buf);
821 }
822
823 /* Upload commands */
824
825 #if ENABLE_FEATURE_FTP_WRITE
826 static void
827 handle_mkd(void)
828 {
829         if (!G.ftp_arg || mkdir(G.ftp_arg, 0777) != 0) {
830                 WRITE_ERR(FTP_FILEFAIL);
831                 return;
832         }
833         WRITE_OK(FTP_MKDIROK);
834 }
835
836 static void
837 handle_rmd(void)
838 {
839         if (!G.ftp_arg || rmdir(G.ftp_arg) != 0) {
840                 WRITE_ERR(FTP_FILEFAIL);
841                 return;
842         }
843         WRITE_OK(FTP_RMDIROK);
844 }
845
846 static void
847 handle_dele(void)
848 {
849         if (!G.ftp_arg || unlink(G.ftp_arg) != 0) {
850                 WRITE_ERR(FTP_FILEFAIL);
851                 return;
852         }
853         WRITE_OK(FTP_DELEOK);
854 }
855
856 static void
857 handle_rnfr(void)
858 {
859         free(G.rnfr_filename);
860         G.rnfr_filename = xstrdup(G.ftp_arg);
861         WRITE_OK(FTP_RNFROK);
862 }
863
864 static void
865 handle_rnto(void)
866 {
867         int retval;
868
869         /* If we didn't get a RNFR, throw a wobbly */
870         if (G.rnfr_filename == NULL || G.ftp_arg == NULL) {
871                 cmdio_write_raw(STR(FTP_NEEDRNFR)" Use RNFR first\r\n");
872                 return;
873         }
874
875         retval = rename(G.rnfr_filename, G.ftp_arg);
876         free(G.rnfr_filename);
877         G.rnfr_filename = NULL;
878
879         if (retval) {
880                 WRITE_ERR(FTP_FILEFAIL);
881                 return;
882         }
883         WRITE_OK(FTP_RENAMEOK);
884 }
885
886 static void
887 handle_upload_common(int is_append, int is_unique)
888 {
889         struct stat statbuf;
890         char *tempname;
891         off_t bytes_transferred;
892         off_t offset;
893         int local_file_fd;
894         int remote_fd;
895
896         offset = G.restart_pos;
897         G.restart_pos = 0;
898
899         if (!port_or_pasv_was_seen())
900                 return; /* port_or_pasv_was_seen emitted error response */
901
902         tempname = NULL;
903         local_file_fd = -1;
904         if (is_unique) {
905                 tempname = xstrdup(" FILE: uniq.XXXXXX");
906                 local_file_fd = mkstemp(tempname + 7);
907         } else if (G.ftp_arg) {
908                 int flags = O_WRONLY | O_CREAT | O_TRUNC;
909                 if (is_append)
910                         flags = O_WRONLY | O_CREAT | O_APPEND;
911                 if (offset)
912                         flags = O_WRONLY | O_CREAT;
913                 local_file_fd = open(G.ftp_arg, flags, 0666);
914         }
915
916         if (local_file_fd < 0
917          || fstat(local_file_fd, &statbuf) != 0
918          || !S_ISREG(statbuf.st_mode)
919         ) {
920                 WRITE_ERR(FTP_UPLOADFAIL);
921                 if (local_file_fd >= 0)
922                         goto close_local_and_bail;
923                 return;
924         }
925         G.local_file_fd = local_file_fd;
926
927         if (offset)
928                 xlseek(local_file_fd, offset, SEEK_SET);
929
930         remote_fd = get_remote_transfer_fd(tempname ? tempname : " Ok to send data");
931         free(tempname);
932
933         if (remote_fd < 0)
934                 goto close_local_and_bail;
935
936         bytes_transferred = bb_copyfd_eof(remote_fd, local_file_fd);
937         close(remote_fd);
938         if (bytes_transferred < 0)
939                 WRITE_ERR(FTP_BADSENDFILE);
940         else
941                 WRITE_OK(FTP_TRANSFEROK);
942
943  close_local_and_bail:
944         close(local_file_fd);
945         G.local_file_fd = 0;
946 }
947
948 static void
949 handle_stor(void)
950 {
951         handle_upload_common(0, 0);
952 }
953
954 static void
955 handle_appe(void)
956 {
957         G.restart_pos = 0;
958         handle_upload_common(1, 0);
959 }
960
961 static void
962 handle_stou(void)
963 {
964         G.restart_pos = 0;
965         handle_upload_common(0, 1);
966 }
967 #endif /* ENABLE_FEATURE_FTP_WRITE */
968
969 static uint32_t
970 cmdio_get_cmd_and_arg(void)
971 {
972         size_t len;
973         uint32_t cmdval;
974         char *cmd;
975
976         alarm(G.timeout);
977
978         free(G.ftp_cmd);
979         len = 8 * 1024; /* Paranoia. Peer may send 1 gigabyte long cmd... */
980         G.ftp_cmd = cmd = xmalloc_fgets_str_len(stdin, "\r\n", &len);
981         if (!cmd)
982                 exit(0);
983
984         /* De-escape telnet: 0xff,0xff => 0xff */
985         /* RFC959 says that ABOR, STAT, QUIT may be sent even during
986          * data transfer, and may be preceded by telnet's "Interrupt Process"
987          * code (two-byte sequence 255,244) and then by telnet "Synch" code
988          * 255,242 (byte 242 is sent with TCP URG bit using send(MSG_OOB)
989          * and may generate SIGURG on our side. See RFC854).
990          * So far we don't support that (may install SIGURG handler if we'd want to),
991          * but we need to at least remove 255,xxx pairs. lftp sends those. */
992         /* Then de-escape FTP: NUL => '\n' */
993         /* Testing for \xff:
994          * Create file named '\xff': echo Hello >`echo -ne "\xff"`
995          * Try to get it:            ftpget -v 127.0.0.1 Eff `echo -ne "\xff\xff"`
996          * (need "\xff\xff" until ftpget applet is fixed to do escaping :)
997          * Testing for embedded LF:
998          * LF_HERE=`echo -ne "LF\nHERE"`
999          * echo Hello >"$LF_HERE"
1000          * ftpget -v 127.0.0.1 LF_HERE "$LF_HERE"
1001          */
1002         {
1003                 int dst, src;
1004
1005                 /* Strip "\r\n" if it is there */
1006                 if (len != 0 && cmd[len - 1] == '\n') {
1007                         len--;
1008                         if (len != 0 && cmd[len - 1] == '\r')
1009                                 len--;
1010                         cmd[len] = '\0';
1011                 }
1012                 src = strchrnul(cmd, 0xff) - cmd;
1013                 /* 99,99% there are neither NULs nor 255s and src == len */
1014                 if (src < len) {
1015                         dst = src;
1016                         do {
1017                                 if ((unsigned char)(cmd[src]) == 255) {
1018                                         src++;
1019                                         /* 255,xxx - skip 255 */
1020                                         if ((unsigned char)(cmd[src]) != 255) {
1021                                                 /* 255,!255 - skip both */
1022                                                 src++;
1023                                                 continue;
1024                                         }
1025                                         /* 255,255 - retain one 255 */
1026                                 }
1027                                 /* NUL => '\n' */
1028                                 cmd[dst++] = cmd[src] ? cmd[src] : '\n';
1029                                 src++;
1030                         } while (src < len);
1031                         cmd[dst] = '\0';
1032                 }
1033         }
1034
1035         if (G.verbose > 1)
1036                 verbose_log(cmd);
1037
1038         G.ftp_arg = strchr(cmd, ' ');
1039         if (G.ftp_arg != NULL)
1040                 *G.ftp_arg++ = '\0';
1041
1042         /* Uppercase and pack into uint32_t first word of the command */
1043         cmdval = 0;
1044         while (*cmd)
1045                 cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20);
1046
1047         return cmdval;
1048 }
1049
1050 #define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d)
1051 #define mk_const3(a,b,c)    ((a * 0x100 + b) * 0x100 + c)
1052 enum {
1053         const_ALLO = mk_const4('A', 'L', 'L', 'O'),
1054         const_APPE = mk_const4('A', 'P', 'P', 'E'),
1055         const_CDUP = mk_const4('C', 'D', 'U', 'P'),
1056         const_CWD  = mk_const3('C', 'W', 'D'),
1057         const_DELE = mk_const4('D', 'E', 'L', 'E'),
1058         const_EPSV = mk_const4('E', 'P', 'S', 'V'),
1059         const_FEAT = mk_const4('F', 'E', 'A', 'T'),
1060         const_HELP = mk_const4('H', 'E', 'L', 'P'),
1061         const_LIST = mk_const4('L', 'I', 'S', 'T'),
1062         const_MDTM = mk_const4('M', 'D', 'T', 'M'),
1063         const_MKD  = mk_const3('M', 'K', 'D'),
1064         const_MODE = mk_const4('M', 'O', 'D', 'E'),
1065         const_NLST = mk_const4('N', 'L', 'S', 'T'),
1066         const_NOOP = mk_const4('N', 'O', 'O', 'P'),
1067         const_PASS = mk_const4('P', 'A', 'S', 'S'),
1068         const_PASV = mk_const4('P', 'A', 'S', 'V'),
1069         const_PORT = mk_const4('P', 'O', 'R', 'T'),
1070         const_PWD  = mk_const3('P', 'W', 'D'),
1071         const_QUIT = mk_const4('Q', 'U', 'I', 'T'),
1072         const_REST = mk_const4('R', 'E', 'S', 'T'),
1073         const_RETR = mk_const4('R', 'E', 'T', 'R'),
1074         const_RMD  = mk_const3('R', 'M', 'D'),
1075         const_RNFR = mk_const4('R', 'N', 'F', 'R'),
1076         const_RNTO = mk_const4('R', 'N', 'T', 'O'),
1077         const_SIZE = mk_const4('S', 'I', 'Z', 'E'),
1078         const_STAT = mk_const4('S', 'T', 'A', 'T'),
1079         const_STOR = mk_const4('S', 'T', 'O', 'R'),
1080         const_STOU = mk_const4('S', 'T', 'O', 'U'),
1081         const_STRU = mk_const4('S', 'T', 'R', 'U'),
1082         const_SYST = mk_const4('S', 'Y', 'S', 'T'),
1083         const_TYPE = mk_const4('T', 'Y', 'P', 'E'),
1084         const_USER = mk_const4('U', 'S', 'E', 'R'),
1085
1086 #if !BB_MMU
1087         OPT_l = (1 << 0),
1088         OPT_1 = (1 << 1),
1089 #endif
1090         OPT_v = (1 << ((!BB_MMU) * 2 + 0)),
1091         OPT_S = (1 << ((!BB_MMU) * 2 + 1)),
1092         OPT_w = (1 << ((!BB_MMU) * 2 + 2)) * ENABLE_FEATURE_FTP_WRITE,
1093 };
1094
1095 int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
1096 #if !BB_MMU
1097 int ftpd_main(int argc, char **argv)
1098 #else
1099 int ftpd_main(int argc UNUSED_PARAM, char **argv)
1100 #endif
1101 {
1102         unsigned abs_timeout;
1103         unsigned verbose_S;
1104         smallint opts;
1105
1106         INIT_G();
1107
1108         abs_timeout = 1 * 60 * 60;
1109         verbose_S = 0;
1110         G.timeout = 2 * 60;
1111         opt_complementary = "t+:T+:vv:SS";
1112 #if BB_MMU
1113         opts = getopt32(argv,   "vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
1114 #else
1115         opts = getopt32(argv, "l1vS" IF_FEATURE_FTP_WRITE("w") "t:T:", &G.timeout, &abs_timeout, &G.verbose, &verbose_S);
1116         if (opts & (OPT_l|OPT_1)) {
1117                 /* Our secret backdoor to ls */
1118 /* TODO: pass -n? It prevents user/group resolution, whicj may not work in chroot anyway */
1119 /* TODO: pass -A? It shows dot files */
1120 /* TODO: pass --group-directories-first? would be nice, but ls don't do that yet */
1121                 xchdir(argv[2]);
1122                 argv[2] = (char*)"--";
1123                 /* memset(&G, 0, sizeof(G)); - ls_main does it */
1124                 return ls_main(argc, argv);
1125         }
1126 #endif
1127         if (G.verbose < verbose_S)
1128                 G.verbose = verbose_S;
1129         if (abs_timeout | G.timeout) {
1130                 if (abs_timeout == 0)
1131                         abs_timeout = INT_MAX;
1132                 G.end_time = monotonic_sec() + abs_timeout;
1133                 if (G.timeout > abs_timeout)
1134                         G.timeout = abs_timeout;
1135         }
1136         strcpy(G.msg_ok  + 4, MSG_OK );
1137         strcpy(G.msg_err + 4, MSG_ERR);
1138
1139         G.local_addr = get_sock_lsa(STDIN_FILENO);
1140         if (!G.local_addr) {
1141                 /* This is confusing:
1142                  * bb_error_msg_and_die("stdin is not a socket");
1143                  * Better: */
1144                 bb_show_usage();
1145                 /* Help text says that ftpd must be used as inetd service,
1146                  * which is by far the most usual cause of get_sock_lsa
1147                  * failure */
1148         }
1149
1150         if (!(opts & OPT_v))
1151                 logmode = LOGMODE_NONE;
1152         if (opts & OPT_S) {
1153                 /* LOG_NDELAY is needed since we may chroot later */
1154                 openlog(applet_name, LOG_PID | LOG_NDELAY, LOG_DAEMON);
1155                 logmode |= LOGMODE_SYSLOG;
1156         }
1157         if (logmode)
1158                 applet_name = xasprintf("%s[%u]", applet_name, (int)getpid());
1159
1160 #if !BB_MMU
1161         G.root_fd = xopen("/", O_RDONLY | O_DIRECTORY);
1162         close_on_exec_on(G.root_fd);
1163 #endif
1164
1165         if (argv[optind]) {
1166                 xchdir(argv[optind]);
1167                 chroot(".");
1168         }
1169
1170         //umask(077); - admin can set umask before starting us
1171
1172         /* Signals. We'll always take -EPIPE rather than a rude signal, thanks */
1173         signal(SIGPIPE, SIG_IGN);
1174
1175         /* Set up options on the command socket (do we need these all? why?) */
1176         setsockopt(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY, &const_int_1, sizeof(const_int_1));
1177         setsockopt(STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1));
1178         /* Telnet protocol over command link may send "urgent" data,
1179          * we prefer it to be received in the "normal" data stream: */
1180         setsockopt(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1));
1181
1182         WRITE_OK(FTP_GREET);
1183         signal(SIGALRM, timeout_handler);
1184
1185 #ifdef IF_WE_WANT_TO_REQUIRE_LOGIN
1186         {
1187                 smallint user_was_specified = 0;
1188                 while (1) {
1189                         uint32_t cmdval = cmdio_get_cmd_and_arg();
1190
1191                         if (cmdval == const_USER) {
1192                                 if (G.ftp_arg == NULL || strcasecmp(G.ftp_arg, "anonymous") != 0)
1193                                         cmdio_write_raw(STR(FTP_LOGINERR)" Server is anonymous only\r\n");
1194                                 else {
1195                                         user_was_specified = 1;
1196                                         cmdio_write_raw(STR(FTP_GIVEPWORD)" Please specify the password\r\n");
1197                                 }
1198                         } else if (cmdval == const_PASS) {
1199                                 if (user_was_specified)
1200                                         break;
1201                                 cmdio_write_raw(STR(FTP_NEEDUSER)" Login with USER\r\n");
1202                         } else if (cmdval == const_QUIT) {
1203                                 WRITE_OK(FTP_GOODBYE);
1204                                 return 0;
1205                         } else {
1206                                 cmdio_write_raw(STR(FTP_LOGINERR)" Login with USER and PASS\r\n");
1207                         }
1208                 }
1209         }
1210         WRITE_OK(FTP_LOGINOK);
1211 #endif
1212
1213         /* RFC-959 Section 5.1
1214          * The following commands and options MUST be supported by every
1215          * server-FTP and user-FTP, except in cases where the underlying
1216          * file system or operating system does not allow or support
1217          * a particular command.
1218          * Type: ASCII Non-print, IMAGE, LOCAL 8
1219          * Mode: Stream
1220          * Structure: File, Record*
1221          * (Record structure is REQUIRED only for hosts whose file
1222          *  systems support record structure).
1223          * Commands:
1224          * USER, PASS, ACCT, [bbox: ACCT not supported]
1225          * PORT, PASV,
1226          * TYPE, MODE, STRU,
1227          * RETR, STOR, APPE,
1228          * RNFR, RNTO, DELE,
1229          * CWD,  CDUP, RMD,  MKD,  PWD,
1230          * LIST, NLST,
1231          * SYST, STAT,
1232          * HELP, NOOP, QUIT.
1233          */
1234         /* ACCOUNT (ACCT)
1235          * "The argument field is a Telnet string identifying the user's account.
1236          * The command is not necessarily related to the USER command, as some
1237          * sites may require an account for login and others only for specific
1238          * access, such as storing files. In the latter case the command may
1239          * arrive at any time.
1240          * There are reply codes to differentiate these cases for the automation:
1241          * when account information is required for login, the response to
1242          * a successful PASSword command is reply code 332. On the other hand,
1243          * if account information is NOT required for login, the reply to
1244          * a successful PASSword command is 230; and if the account information
1245          * is needed for a command issued later in the dialogue, the server
1246          * should return a 332 or 532 reply depending on whether it stores
1247          * (pending receipt of the ACCounT command) or discards the command,
1248          * respectively."
1249          */
1250
1251         while (1) {
1252                 uint32_t cmdval = cmdio_get_cmd_and_arg();
1253
1254                 if (cmdval == const_QUIT) {
1255                         WRITE_OK(FTP_GOODBYE);
1256                         return 0;
1257                 }
1258                 else if (cmdval == const_USER)
1259                         /* This would mean "ok, now give me PASS". */
1260                         /*WRITE_OK(FTP_GIVEPWORD);*/
1261                         /* vsftpd can be configured to not require that,
1262                          * and this also saves one roundtrip:
1263                          */
1264                         WRITE_OK(FTP_LOGINOK);
1265                 else if (cmdval == const_PASS)
1266                         WRITE_OK(FTP_LOGINOK);
1267                 else if (cmdval == const_NOOP)
1268                         WRITE_OK(FTP_NOOPOK);
1269                 else if (cmdval == const_TYPE)
1270                         WRITE_OK(FTP_TYPEOK);
1271                 else if (cmdval == const_STRU)
1272                         WRITE_OK(FTP_STRUOK);
1273                 else if (cmdval == const_MODE)
1274                         WRITE_OK(FTP_MODEOK);
1275                 else if (cmdval == const_ALLO)
1276                         WRITE_OK(FTP_ALLOOK);
1277                 else if (cmdval == const_SYST)
1278                         cmdio_write_raw(STR(FTP_SYSTOK)" UNIX Type: L8\r\n");
1279                 else if (cmdval == const_PWD)
1280                         handle_pwd();
1281                 else if (cmdval == const_CWD)
1282                         handle_cwd();
1283                 else if (cmdval == const_CDUP) /* cd .. */
1284                         handle_cdup();
1285                 /* HELP is nearly useless, but we can reuse FEAT for it */
1286                 /* lftp uses FEAT */
1287                 else if (cmdval == const_HELP || cmdval == const_FEAT)
1288                         handle_feat(cmdval == const_HELP
1289                                         ? STRNUM32(FTP_HELP)
1290                                         : STRNUM32(FTP_STATOK)
1291                         );
1292                 else if (cmdval == const_LIST) /* ls -l */
1293                         handle_list();
1294                 else if (cmdval == const_NLST) /* "name list", bare ls */
1295                         handle_nlst();
1296                 /* SIZE is crucial for wget's download indicator etc */
1297                 /* Mozilla, lftp use MDTM (presumably for caching) */
1298                 else if (cmdval == const_SIZE || cmdval == const_MDTM)
1299                         handle_size_or_mdtm(cmdval == const_SIZE);
1300                 else if (cmdval == const_STAT) {
1301                         if (G.ftp_arg == NULL)
1302                                 handle_stat();
1303                         else
1304                                 handle_stat_file();
1305                 }
1306                 else if (cmdval == const_PASV)
1307                         handle_pasv();
1308                 else if (cmdval == const_EPSV)
1309                         handle_epsv();
1310                 else if (cmdval == const_RETR)
1311                         handle_retr();
1312                 else if (cmdval == const_PORT)
1313                         handle_port();
1314                 else if (cmdval == const_REST)
1315                         handle_rest();
1316 #if ENABLE_FEATURE_FTP_WRITE
1317                 else if (opts & OPT_w) {
1318                         if (cmdval == const_STOR)
1319                                 handle_stor();
1320                         else if (cmdval == const_MKD)
1321                                 handle_mkd();
1322                         else if (cmdval == const_RMD)
1323                                 handle_rmd();
1324                         else if (cmdval == const_DELE)
1325                                 handle_dele();
1326                         else if (cmdval == const_RNFR) /* "rename from" */
1327                                 handle_rnfr();
1328                         else if (cmdval == const_RNTO) /* "rename to" */
1329                                 handle_rnto();
1330                         else if (cmdval == const_APPE)
1331                                 handle_appe();
1332                         else if (cmdval == const_STOU) /* "store unique" */
1333                                 handle_stou();
1334                         else
1335                                 goto bad_cmd;
1336                 }
1337 #endif
1338 #if 0
1339                 else if (cmdval == const_STOR
1340                  || cmdval == const_MKD
1341                  || cmdval == const_RMD
1342                  || cmdval == const_DELE
1343                  || cmdval == const_RNFR
1344                  || cmdval == const_RNTO
1345                  || cmdval == const_APPE
1346                  || cmdval == const_STOU
1347                 ) {
1348                         cmdio_write_raw(STR(FTP_NOPERM)" Permission denied\r\n");
1349                 }
1350 #endif
1351                 else {
1352                         /* Which unsupported commands were seen in the wild?
1353                          * (doesn't necessarily mean "we must support them")
1354                          * foo 1.2.3: XXXX - comment
1355                          */
1356 #if ENABLE_FEATURE_FTP_WRITE
1357  bad_cmd:
1358 #endif
1359                         cmdio_write_raw(STR(FTP_BADCMD)" Unknown command\r\n");
1360                 }
1361         }
1362 }