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} FIX_ALIASING; 110#define G (*(struct globals*)&bb_common_bufsiz1) 111#define INIT_G() do { \ 112 /* Moved to main */ \ 113 /*strcpy(G.msg_ok + 4, MSG_OK );*/ \ 114 /*strcpy(G.msg_err + 4, MSG_ERR);*/ \ 115} while (0) 116 117 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 464static void 465handle_port(void) 466{ 467 unsigned port, port_hi; 468 char *raw, *comma; 469#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES 470 socklen_t peer_ipv4_len; 471 struct sockaddr_in peer_ipv4; 472 struct in_addr port_ipv4_sin_addr; 473#endif 474 475 port_pasv_cleanup(); 476 477 raw = G.ftp_arg; 478 479 /* PORT command format makes sense only over IPv4 */ 480 if (!raw 481#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES 482 || G.local_addr->u.sa.sa_family != AF_INET 483#endif 484 ) { 485 bail: 486 WRITE_ERR(FTP_BADCMD); 487 return; 488 } 489 490 comma = strrchr(raw, ','); 491 if (comma == NULL) 492 goto bail; 493 *comma = '\0'; 494 port = bb_strtou(&comma[1], NULL, 10); 495 if (errno || port > 0xff) 496 goto bail; 497 498 comma = strrchr(raw, ','); 499 if (comma == NULL) 500 goto bail; 501 *comma = '\0'; 502 port_hi = bb_strtou(&comma[1], NULL, 10); 503 if (errno || port_hi > 0xff) 504 goto bail; 505 port |= port_hi << 8; 506 507#ifdef WHY_BOTHER_WE_CAN_ASSUME_IP_MATCHES 508 replace_char(raw, ',', '.'); 509 510 /* We are verifying that PORT's IP matches getpeername(). 511 * Otherwise peer can make us open data connections 512 * to other hosts (security problem!) 513 * This code would be too simplistic: 514 * lsa = xdotted2sockaddr(raw, port); 515 * if (lsa == NULL) goto bail; 516 */ 517 if (!inet_aton(raw, &port_ipv4_sin_addr)) 518 goto bail; 519 peer_ipv4_len = sizeof(peer_ipv4); 520 if (getpeername(STDIN_FILENO, &peer_ipv4, &peer_ipv4_len) != 0) 521 goto bail; 522 if (memcmp(&port_ipv4_sin_addr, &peer_ipv4.sin_addr, sizeof(struct in_addr)) != 0) 523 goto bail; 524 525 G.port_addr = xdotted2sockaddr(raw, port); 526#else 527 G.port_addr = get_peer_lsa(STDIN_FILENO); 528 set_nport(G.port_addr, htons(port)); 529#endif 530 WRITE_OK(FTP_PORTOK); 531} 532 533static void 534handle_rest(void) 535{ 536 /* When ftp_arg == NULL simply restart from beginning */ 537 G.restart_pos = G.ftp_arg ? xatoi_u(G.ftp_arg) : 0; 538 WRITE_OK(FTP_RESTOK); 539} 540 541static void 542handle_retr(void) 543{ 544 struct stat statbuf; 545 off_t bytes_transferred; 546 int remote_fd; 547 int local_file_fd; 548 off_t offset = G.restart_pos; 549 char *response; 550 551 G.restart_pos = 0; 552 553 if (!port_or_pasv_was_seen()) 554 return; /* port_or_pasv_was_seen emitted error response */ 555 556 /* O_NONBLOCK is useful if file happens to be a device node */ 557 local_file_fd = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1; 558 if (local_file_fd < 0) { 559 WRITE_ERR(FTP_FILEFAIL); 560 return; 561 } 562 563 if (fstat(local_file_fd, &statbuf) != 0 || !S_ISREG(statbuf.st_mode)) { 564 /* Note - pretend open failed */ 565 WRITE_ERR(FTP_FILEFAIL); 566 goto file_close_out; 567 } 568 G.local_file_fd = local_file_fd; 569 570 /* Now deactive O_NONBLOCK, otherwise we have a problem 571 * on DMAPI filesystems such as XFS DMAPI. 572 */ 573 ndelay_off(local_file_fd); 574 575 /* Set the download offset (from REST) if any */ 576 if (offset != 0) 577 xlseek(local_file_fd, offset, SEEK_SET); 578 579 response = xasprintf( 580 " Opening BINARY connection for %s (%"OFF_FMT"u bytes)", 581 G.ftp_arg, statbuf.st_size); 582 remote_fd = get_remote_transfer_fd(response); 583 free(response); 584 if (remote_fd < 0) 585 goto file_close_out; 586 587 bytes_transferred = bb_copyfd_eof(local_file_fd, remote_fd); 588 close(remote_fd); 589 if (bytes_transferred < 0) 590 WRITE_ERR(FTP_BADSENDFILE); 591 else 592 WRITE_OK(FTP_TRANSFEROK); 593 594 file_close_out: 595 close(local_file_fd); 596 G.local_file_fd = 0; 597} 598 599/* List commands */ 600 601static int 602popen_ls(const char *opt) 603{ 604 const char *argv[5]; 605 struct fd_pair outfd; 606 pid_t pid; 607 608 argv[0] = "ftpd"; 609 argv[1] = opt; /* "-l" or "-1" */ 610#if BB_MMU 611 argv[2] = "--"; 612#else 613 /* NOMMU ftpd ls helper chdirs to argv[2], 614 * preventing peer from seeing real root. */ 615 argv[2] = xrealloc_getcwd_or_warn(NULL); 616#endif 617 argv[3] = G.ftp_arg; 618 argv[4] = NULL; 619 620 /* Improve compatibility with non-RFC conforming FTP clients 621 * which send e.g. "LIST -l", "LIST -la", "LIST -aL". 622 * See https://bugs.kde.org/show_bug.cgi?id=195578 */ 623 if (ENABLE_FEATURE_FTPD_ACCEPT_BROKEN_LIST 624 && G.ftp_arg && G.ftp_arg[0] == '-' 625 ) { 626 const char *tmp = strchr(G.ftp_arg, ' '); 627 if (tmp) /* skip the space */ 628 tmp++; 629 argv[3] = tmp; 630 } 631 632 xpiped_pair(outfd); 633 634 /*fflush_all(); - so far we dont use stdio on output */ 635 pid = BB_MMU ? xfork() : xvfork(); 636 if (pid == 0) { 637 /* child */ 638#if !BB_MMU 639 /* On NOMMU, we want to execute a child - copy of ourself. 640 * In chroot we usually can't do it. Thus we chdir 641 * out of the chroot back to original root, 642 * and (see later below) execute bb_busybox_exec_path 643 * relative to current directory */ 644 if (fchdir(G.root_fd) != 0) 645 _exit(127); 646 /*close(G.root_fd); - close_on_exec_on() took care of this */ 647#endif 648 /* NB: close _first_, then move fd! */ 649 close(outfd.rd); 650 xmove_fd(outfd.wr, STDOUT_FILENO); 651 /* Opening /dev/null in chroot is hard. 652 * Just making sure STDIN_FILENO is opened 653 * to something harmless. Paranoia, 654 * ls won't read it anyway */ 655 close(STDIN_FILENO); 656 dup(STDOUT_FILENO); /* copy will become STDIN_FILENO */ 657#if BB_MMU 658 /* memset(&G, 0, sizeof(G)); - ls_main does it */ 659 exit(ls_main(ARRAY_SIZE(argv) - 1, (char**) argv)); 660#else 661 /* + 1: we must use relative path here if in chroot. 662 * For example, execv("/proc/self/exe") will fail, since 663 * it looks for "/proc/self/exe" _relative to chroot!_ */ 664 execv(bb_busybox_exec_path + 1, (char**) argv); 665 _exit(127); 666#endif 667 } 668 669 /* parent */ 670 close(outfd.wr); 671#if !BB_MMU 672 free((char*)argv[2]); 673#endif 674 return outfd.rd; 675} 676 677enum { 678 USE_CTRL_CONN = 1, 679 LONG_LISTING = 2, 680}; 681 682static void 683handle_dir_common(int opts) 684{ 685 FILE *ls_fp; 686 char *line; 687 int ls_fd; 688 689 if (!(opts & USE_CTRL_CONN) && !port_or_pasv_was_seen()) 690 return; /* port_or_pasv_was_seen emitted error response */ 691 692 /* -n prevents user/groupname display, 693 * which can be problematic in chroot */ 694 ls_fd = popen_ls((opts & LONG_LISTING) ? "-l" : "-1"); 695 ls_fp = xfdopen_for_read(ls_fd); 696 697 if (opts & USE_CTRL_CONN) { 698 /* STAT <filename> */ 699 cmdio_write_raw(STR(FTP_STATFILE_OK)"-File status:\r\n"); 700 while (1) { 701 line = xmalloc_fgetline(ls_fp); 702 if (!line) 703 break; 704 /* Hack: 0 results in no status at all */ 705 /* Note: it's ok that we don't prepend space, 706 * ftp.kernel.org doesn't do that too */ 707 cmdio_write(0, line); 708 free(line); 709 } 710 WRITE_OK(FTP_STATFILE_OK); 711 } else { 712 /* LIST/NLST [<filename>] */ 713 int remote_fd = get_remote_transfer_fd(" Directory listing"); 714 if (remote_fd >= 0) { 715 while (1) { 716 line = xmalloc_fgetline(ls_fp); 717 if (!line) 718 break; 719 /* I've seen clients complaining when they 720 * are fed with ls output with bare '\n'. 721 * Pity... that would be much simpler. 722 */ 723/* TODO: need to s/LF/NUL/g here */ 724 xwrite_str(remote_fd, line); 725 xwrite(remote_fd, "\r\n", 2); 726 free(line); 727 } 728 } 729 close(remote_fd); 730 WRITE_OK(FTP_TRANSFEROK); 731 } 732 fclose(ls_fp); /* closes ls_fd too */ 733} 734static void 735handle_list(void) 736{ 737 handle_dir_common(LONG_LISTING); 738} 739static void 740handle_nlst(void) 741{ 742 /* NLST returns list of names, "\r\n" terminated without regard 743 * to the current binary flag. Names may start with "/", 744 * then they represent full names (we don't produce such names), 745 * otherwise names are relative to current directory. 746 * Embedded "\n" are replaced by NULs. This is safe since names 747 * can never contain NUL. 748 */ 749 handle_dir_common(0); 750} 751static void 752handle_stat_file(void) 753{ 754 handle_dir_common(LONG_LISTING + USE_CTRL_CONN); 755} 756 757/* This can be extended to handle MLST, as all info is available 758 * in struct stat for that: 759 * MLST file_name 760 * 250-Listing file_name 761 * type=file;size=4161;modify=19970214165800; /dir/dir/file_name 762 * 250 End 763 * Nano-doc: 764 * MLST [<file or dir name, "." assumed if not given>] 765 * Returned name should be either the same as requested, or fully qualified. 766 * If there was no parameter, return "" or (preferred) fully-qualified name. 767 * Returned "facts" (case is not important): 768 * size - size in octets 769 * modify - last modification time 770 * type - entry type (file,dir,OS.unix=block) 771 * (+ cdir and pdir types for MLSD) 772 * unique - unique id of file/directory (inode#) 773 * perm - 774 * a: can be appended to (APPE) 775 * d: can be deleted (RMD/DELE) 776 * f: can be renamed (RNFR) 777 * r: can be read (RETR) 778 * w: can be written (STOR) 779 * e: can CWD into this dir 780 * l: this dir can be listed (dir only!) 781 * c: can create files in this dir 782 * m: can create dirs in this dir (MKD) 783 * p: can delete files in this dir 784 * UNIX.mode - unix file mode 785 */ 786static void 787handle_size_or_mdtm(int need_size) 788{ 789 struct stat statbuf; 790 struct tm broken_out; 791 char buf[(sizeof("NNN %"OFF_FMT"u\r\n") + sizeof(off_t) * 3) 792 | sizeof("NNN YYYYMMDDhhmmss\r\n") 793 ]; 794 795 if (!G.ftp_arg 796 || stat(G.ftp_arg, &statbuf) != 0 797 || !S_ISREG(statbuf.st_mode) 798 ) { 799 WRITE_ERR(FTP_FILEFAIL); 800 return; 801 } 802 if (need_size) { 803 sprintf(buf, STR(FTP_STATFILE_OK)" %"OFF_FMT"u\r\n", statbuf.st_size); 804 } else { 805 gmtime_r(&statbuf.st_mtime, &broken_out); 806 sprintf(buf, STR(FTP_STATFILE_OK)" %04u%02u%02u%02u%02u%02u\r\n", 807 broken_out.tm_year + 1900, 808 broken_out.tm_mon, 809 broken_out.tm_mday, 810 broken_out.tm_hour, 811 broken_out.tm_min, 812 broken_out.tm_sec); 813 } 814 cmdio_write_raw(buf); 815} 816 817/* Upload commands */ 818 819#if ENABLE_FEATURE_FTP_WRITE 820static void 821handle_mkd(void) 822{ 823 if (!G.ftp_arg || mkdir(G.ftp_arg, 0777) != 0) { 824 WRITE_ERR(FTP_FILEFAIL); 825 return; 826 } 827 WRITE_OK(FTP_MKDIROK); 828} 829 830static void 831handle_rmd(void) 832{ 833 if (!G.ftp_arg || rmdir(G.ftp_arg) != 0) { 834 WRITE_ERR(FTP_FILEFAIL); 835 return; 836 } 837 WRITE_OK(FTP_RMDIROK); 838} 839 840static void 841handle_dele(void) 842{ 843 if (!G.ftp_arg || unlink(G.ftp_arg) != 0) { 844 WRITE_ERR(FTP_FILEFAIL); 845 return; 846 } 847 WRITE_OK(FTP_DELEOK); 848} 849 850static void 851handle_rnfr(void) 852{ 853 free(G.rnfr_filename); 854 G.rnfr_filename = xstrdup(G.ftp_arg); 855 WRITE_OK(FTP_RNFROK); 856} 857 858static void 859handle_rnto(void) 860{ 861 int retval; 862 863 /* If we didn't get a RNFR, throw a wobbly */ 864 if (G.rnfr_filename == NULL || G.ftp_arg == NULL) { 865 cmdio_write_raw(STR(FTP_NEEDRNFR)" Use RNFR first\r\n"); 866 return; 867 } 868 869 retval = rename(G.rnfr_filename, G.ftp_arg); 870 free(G.rnfr_filename); 871 G.rnfr_filename = NULL; 872 873 if (retval) { 874 WRITE_ERR(FTP_FILEFAIL); 875 return; 876 } 877 WRITE_OK(FTP_RENAMEOK); 878} 879 880static void 881handle_upload_common(int is_append, int is_unique) 882{ 883 struct stat statbuf; 884 char *tempname; 885 off_t bytes_transferred; 886 off_t offset; 887 int local_file_fd; 888 int remote_fd; 889 890 offset = G.restart_pos; 891 G.restart_pos = 0; 892 893 if (!port_or_pasv_was_seen()) 894 return; /* port_or_pasv_was_seen emitted error response */ 895 896 tempname = NULL; 897 local_file_fd = -1; 898 if (is_unique) { 899 tempname = xstrdup(" FILE: uniq.XXXXXX"); 900 local_file_fd = mkstemp(tempname + 7); 901 } else if (G.ftp_arg) { 902 int flags = O_WRONLY | O_CREAT | O_TRUNC; 903 if (is_append) 904 flags = O_WRONLY | O_CREAT | O_APPEND; 905 if (offset) 906 flags = O_WRONLY | O_CREAT; 907 local_file_fd = open(G.ftp_arg, flags, 0666); 908 } 909 910 if (local_file_fd < 0 911 || fstat(local_file_fd, &statbuf) != 0 912 || !S_ISREG(statbuf.st_mode) 913 ) { 914 WRITE_ERR(FTP_UPLOADFAIL); 915 if (local_file_fd >= 0) 916 goto close_local_and_bail; 917 return; 918 } 919 G.local_file_fd = local_file_fd; 920 921 if (offset) 922 xlseek(local_file_fd, offset, SEEK_SET); 923 924 remote_fd = get_remote_transfer_fd(tempname ? tempname : " Ok to send data"); 925 free(tempname); 926 927 if (remote_fd < 0) 928 goto close_local_and_bail; 929 930 bytes_transferred = bb_copyfd_eof(remote_fd, local_file_fd); 931 close(remote_fd); 932 if (bytes_transferred < 0) 933 WRITE_ERR(FTP_BADSENDFILE); 934 else 935 WRITE_OK(FTP_TRANSFEROK); 936 937 close_local_and_bail: 938 close(local_file_fd); 939 G.local_file_fd = 0; 940} 941 942static void 943handle_stor(void) 944{ 945 handle_upload_common(0, 0); 946} 947 948static void 949handle_appe(void) 950{ 951 G.restart_pos = 0; 952 handle_upload_common(1, 0); 953} 954 955static void 956handle_stou(void) 957{ 958 G.restart_pos = 0; 959 handle_upload_common(0, 1); 960} 961#endif /* ENABLE_FEATURE_FTP_WRITE */ 962 963static uint32_t 964cmdio_get_cmd_and_arg(void) 965{ 966 int len; 967 uint32_t cmdval; 968 char *cmd; 969 970 alarm(G.timeout); 971 972 free(G.ftp_cmd); 973 { 974 /* Paranoia. Peer may send 1 gigabyte long cmd... */ 975 /* Using separate len_on_stk instead of len optimizes 976 * code size (allows len to be in CPU register) */ 977 size_t len_on_stk = 8 * 1024; 978 G.ftp_cmd = cmd = xmalloc_fgets_str_len(stdin, "\r\n", &len_on_stk); 979 if (!cmd) 980 exit(0); 981 len = len_on_stk; 982 } 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) 1052enum { 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 1095int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; 1096#if !BB_MMU 1097int ftpd_main(int argc, char **argv) 1098#else 1099int 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, which 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 doesn'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} 1363