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