diff --git a/networking/ftpd.c b/networking/ftpd.c new file mode 100644 index 000000000..ab7308be9 --- /dev/null +++ b/networking/ftpd.c @@ -0,0 +1,1130 @@ +/* vi: set sw=4 ts=4: */ +/* + * Simple FTP daemon, based on vsftpd 2.0.7 (written by Chris Evans) + * + * Author: Adam Tkac + * + * Only subset of FTP protocol is implemented but vast majority of clients + * should not have any problem. You have to run this daemon via inetd. + * + * Options: + * -w - enable FTP write commands + */ + +#include "libbb.h" +#include + +enum { + FTP_DATACONN = 150, + FTP_NOOPOK = 200, + FTP_TYPEOK = 200, + FTP_PORTOK = 200, + FTP_STRUOK = 200, + FTP_MODEOK = 200, + FTP_ALLOOK = 202, + FTP_STATOK = 211, + FTP_STATFILE_OK = 213, + FTP_HELP = 214, + FTP_SYSTOK = 215, + FTP_GREET = 220, + FTP_GOODBYE = 221, + FTP_TRANSFEROK = 226, + FTP_PASVOK = 227, + FTP_LOGINOK = 230, + FTP_CWDOK = 250, +#if ENABLE_FEATURE_FTP_WRITE + FTP_RMDIROK = 250, + FTP_DELEOK = 250, + FTP_RENAMEOK = 250, +#endif + FTP_PWDOK = 257, +#if ENABLE_FEATURE_FTP_WRITE + FTP_MKDIROK = 257, +#endif + FTP_GIVEPWORD = 331, + FTP_RESTOK = 350, +#if ENABLE_FEATURE_FTP_WRITE + FTP_RNFROK = 350, +#endif + FTP_BADSENDCONN = 425, + FTP_BADSENDNET = 426, + FTP_BADSENDFILE = 451, + FTP_BADCMD = 500, + FTP_COMMANDNOTIMPL = 502, + FTP_NEEDUSER = 503, + FTP_NEEDRNFR = 503, + FTP_BADSTRU = 504, + FTP_BADMODE = 504, + FTP_LOGINERR = 530, + FTP_FILEFAIL = 550, + FTP_NOPERM = 550, + FTP_UPLOADFAIL = 553, + +#define mk_const4(a,b,c,d) (((a * 0x100 + b) * 0x100 + c) * 0x100 + d) +#define mk_const3(a,b,c) ((a * 0x100 + b) * 0x100 + c) + const_ALLO = mk_const4('A', 'L', 'L', 'O'), + const_APPE = mk_const4('A', 'P', 'P', 'E'), + const_CDUP = mk_const4('C', 'D', 'U', 'P'), + const_CWD = mk_const3('C', 'W', 'D'), + const_DELE = mk_const4('D', 'E', 'L', 'E'), + const_HELP = mk_const4('H', 'E', 'L', 'P'), + const_LIST = mk_const4('L', 'I', 'S', 'T'), + const_MKD = mk_const3('M', 'K', 'D'), + const_MODE = mk_const4('M', 'O', 'D', 'E'), + const_NLST = mk_const4('N', 'L', 'S', 'T'), + const_NOOP = mk_const4('N', 'O', 'O', 'P'), + const_PASS = mk_const4('P', 'A', 'S', 'S'), + const_PASV = mk_const4('P', 'A', 'S', 'V'), + const_PORT = mk_const4('P', 'O', 'R', 'T'), + const_PWD = mk_const3('P', 'W', 'D'), + const_QUIT = mk_const4('Q', 'U', 'I', 'T'), + const_REST = mk_const4('R', 'E', 'S', 'T'), + const_RETR = mk_const4('R', 'E', 'T', 'R'), + const_RMD = mk_const3('R', 'M', 'D'), + const_RNFR = mk_const4('R', 'N', 'F', 'R'), + const_RNTO = mk_const4('R', 'N', 'T', 'O'), + const_STAT = mk_const4('S', 'T', 'A', 'T'), + const_STOR = mk_const4('S', 'T', 'O', 'R'), + const_STOU = mk_const4('S', 'T', 'O', 'U'), + const_STRU = mk_const4('S', 'T', 'R', 'U'), + const_SYST = mk_const4('S', 'Y', 'S', 'T'), + const_TYPE = mk_const4('T', 'Y', 'P', 'E'), + const_USER = mk_const4('U', 'S', 'E', 'R'), +}; + +struct globals { + char *p_control_line_buf; + len_and_sockaddr *local_addr; + len_and_sockaddr *port_addr; + int pasv_listen_fd; + int data_fd; + off_t restart_pos; + char *ftp_cmp; + char *ftp_arg; +#if ENABLE_FEATURE_FTP_WRITE + char *rnfr_filename; + smallint write_enable; +#endif +}; +#define G (*(struct globals*)&bb_common_bufsiz1) +#define INIT_G() do { } while (0) + + +static char * +replace_text(const char *str, const char from, const char *to) +{ + size_t retlen, remainlen, chunklen, tolen; + const char *remain; + char *ret, *found; + + remain = str; + remainlen = strlen(str); + + tolen = strlen(to); + + /* simply alloc strlen(str)*strlen(to). To is max 2 so it's allowed */ + ret = xmalloc(remainlen * strlen(to) + 1); + retlen = 0; + + for (;;) { + found = strchr(remain, from); + if (found != NULL) { + chunklen = found - remain; + + /* Copy chunk which doesn't contain 'from' to ret */ + memcpy(&ret[retlen], remain, chunklen); + retlen += chunklen; + + /* Now copy 'to' instead of 'from' */ + memcpy(&ret[retlen], to, tolen); + retlen += tolen; + + remain = found + 1; + } else { + /* + * The last chunk. We are already sure that we have enough space + * so we can use strcpy. + */ + strcpy(&ret[retlen], remain); + break; + } + } + return ret; +} + +static void +replace_char(char *str, char from, char to) +{ + char *ptr; + + /* Don't use strchr here...*/ + while ((ptr = strchr(str, from)) != NULL) { + *ptr = to; + str = ptr + 1; + } +} + +static void +str_netfd_write(const char *str, int fd) +{ + xwrite(fd, str, strlen(str)); +} + +static void +ftp_write_str_common(unsigned int status, const char *str, char sep) +{ + char *escaped_str, *response; + size_t len; + + escaped_str = replace_text(str, '\377', "\377\377"); + + response = xasprintf("%u%c%s\r\n", status, sep, escaped_str); + free(escaped_str); + + len = strlen(response); + replace_char(escaped_str, '\n', '\0'); + + /* Change trailing '\0' back to '\n' */ + response[len - 1] = '\n'; + xwrite(STDIN_FILENO, response, len); +} + +static void +cmdio_write(int status, const char *p_text) +{ + ftp_write_str_common(status, p_text, ' '); +} + +static void +cmdio_write_hyphen(int status, const char *p_text) +{ + ftp_write_str_common(status, p_text, '-'); +} + +static void +cmdio_write_raw(const char *p_text) +{ + str_netfd_write(p_text, STDIN_FILENO); +} + +static uint32_t +cmdio_get_cmd_and_arg(void) +{ + int len; + uint32_t cmdval; + char *cmd; + + free(G.ftp_cmp); + G.ftp_cmp = cmd = xmalloc_reads(STDIN_FILENO, NULL, NULL); +/* + * TODO: + * + * now we should change all '\0' to '\n' - xmalloc_reads will be improved, + * probably + */ + len = strlen(cmd) - 1; + while (len >= 0 && cmd[len] == '\r') { + cmd[len] = '\0'; + len--; + } + + G.ftp_arg = strchr(cmd, ' '); + if (G.ftp_arg != NULL) { + *G.ftp_arg = '\0'; + G.ftp_arg++; + } + cmdval = 0; + while (*cmd) + cmdval = (cmdval << 8) + ((unsigned char)*cmd++ & (unsigned char)~0x20); + + return cmdval; +} + +static void +init_data_sock_params(int sock_fd) +{ + struct linger linger; + + G.data_fd = sock_fd; + + memset(&linger, 0, sizeof(linger)); + linger.l_onoff = 1; + linger.l_linger = 32767; + + setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1)); + setsockopt(sock_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger)); +} + +static int +ftpdataio_get_pasv_fd(void) +{ + int remote_fd; + + remote_fd = accept(G.pasv_listen_fd, NULL, 0); + + if (remote_fd < 0) { + cmdio_write(FTP_BADSENDCONN, "Can't establish connection"); + return remote_fd; + } + + init_data_sock_params(remote_fd); + return remote_fd; +} + +static int +ftpdataio_get_port_fd(void) +{ + int remote_fd; + + /* Do we want die or print error to client? */ + remote_fd = xconnect_stream(G.port_addr); + + init_data_sock_params(remote_fd); + return remote_fd; +} + +static void +ftpdataio_dispose_transfer_fd(void) +{ + /* This close() blocks because we set SO_LINGER */ + if (G.data_fd > STDOUT_FILENO) { + if (close(G.data_fd) < 0) { + /* Do it again without blocking. */ + struct linger linger; + + memset(&linger, 0, sizeof(linger)); + setsockopt(G.data_fd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger)); + close(G.data_fd); + } + } + G.data_fd = -1; +} + +static int +port_active(void) +{ + return (G.port_addr != NULL) ? 1: 0; +} + +static int +pasv_active(void) +{ + return (G.pasv_listen_fd != -1) ? 1 : 0; +} + +static int +get_remote_transfer_fd(const char *p_status_msg) +{ + int remote_fd; + + if (pasv_active()) + remote_fd = ftpdataio_get_pasv_fd(); + else + remote_fd = ftpdataio_get_port_fd(); + + if (remote_fd < 0) + return remote_fd; + + cmdio_write(FTP_DATACONN, p_status_msg); + return remote_fd; +} + +static void +handle_pwd(void) +{ + char *cwd, *promoted_cwd, *response; + + cwd = xrealloc_getcwd_or_warn(NULL); + if (cwd == NULL) + cwd = xstrdup(""); + + /* We _have to_ promote each " to "" */ + promoted_cwd = replace_text(cwd, '\"', "\"\""); + free(cwd); + response = xasprintf("\"%s\"", promoted_cwd); + free(promoted_cwd); + cmdio_write(FTP_PWDOK, response); + free(response); +} + +static void +handle_cwd(void) +{ + int retval; + + /* XXX Do we need check ftp_arg != NULL? */ + retval = chdir(G.ftp_arg); + if (retval == 0) + cmdio_write(FTP_CWDOK, "Directory changed"); + else + cmdio_write(FTP_FILEFAIL, "Can't change directory"); +} + +static void +handle_cdup(void) +{ + G.ftp_arg = xstrdup(".."); + handle_cwd(); + free(G.ftp_arg); +} + +static int +data_transfer_checks_ok(void) +{ + if (!pasv_active() && !port_active()) { + cmdio_write(FTP_BADSENDCONN, "Use PORT or PASV first"); + return 0; + } + + return 1; +} + +static void +port_cleanup(void) +{ + if (G.port_addr != NULL) + free(G.port_addr); + + G.port_addr = NULL; +} + +static void +pasv_cleanup(void) +{ + if (G.pasv_listen_fd > STDOUT_FILENO) + close(G.pasv_listen_fd); + G.pasv_listen_fd = -1; +} + +static char * +statbuf_getperms(const struct stat *statbuf) +{ + char *perms; + enum { r = 'r', w = 'w', x = 'x', s = 's', S = 'S' }; + + perms = xmalloc(11); + memset(perms, '-', 10); + + perms[0] = '?'; + switch (statbuf->st_mode & S_IFMT) { + case S_IFREG: perms[0] = '-'; break; + case S_IFDIR: perms[0] = 'd'; break; + case S_IFLNK: perms[0] = 'l'; break; + case S_IFIFO: perms[0] = 'p'; break; + case S_IFSOCK: perms[0] = s; break; + case S_IFCHR: perms[0] = 'c'; break; + case S_IFBLK: perms[0] = 'b'; break; + } + + if (statbuf->st_mode & S_IRUSR) perms[1] = r; + if (statbuf->st_mode & S_IWUSR) perms[2] = w; + if (statbuf->st_mode & S_IXUSR) perms[3] = x; + if (statbuf->st_mode & S_IRGRP) perms[4] = r; + if (statbuf->st_mode & S_IWGRP) perms[5] = w; + if (statbuf->st_mode & S_IXGRP) perms[6] = x; + if (statbuf->st_mode & S_IROTH) perms[7] = r; + if (statbuf->st_mode & S_IWOTH) perms[8] = w; + if (statbuf->st_mode & S_IXOTH) perms[9] = x; + if (statbuf->st_mode & S_ISUID) perms[3] = (perms[3] == x) ? s : S; + if (statbuf->st_mode & S_ISGID) perms[6] = (perms[6] == x) ? s : S; + if (statbuf->st_mode & S_ISVTX) perms[9] = (perms[9] == x) ? 't' : 'T'; + + perms[10] = '\0'; + + return perms; +} + +static void +write_filestats(int fd, const char *filename, + const struct stat *statbuf) +{ + off_t size; + char *stats, *lnkname = NULL, *perms; + const char *name; + int retval; + char timestr[32]; + struct tm *tm; + const char *format = "%b %d %H:%M"; + + name = bb_get_last_path_component_nostrip(filename); + + if (statbuf != NULL) { + size = statbuf->st_size; + + if (S_ISLNK(statbuf->st_mode)) + /* Damn symlink... */ + lnkname = xmalloc_readlink(filename); + + tm = gmtime(&statbuf->st_mtime); + retval = strftime(timestr, sizeof(timestr), format, tm); + if (retval == 0) + bb_error_msg_and_die("strftime"); + + timestr[sizeof(timestr) - 1] = '\0'; + + perms = statbuf_getperms(statbuf); + + stats = xasprintf("%s %u\tftp ftp %"OFF_FMT"u\t%s %s", + perms, (int) statbuf->st_nlink, + size, timestr, name); + + free(perms); + } else + stats = xstrdup(name); + + str_netfd_write(stats, fd); + free(stats); + if (lnkname != NULL) { + str_netfd_write(" -> ", fd); + str_netfd_write(lnkname, fd); + free(lnkname); + } + str_netfd_write("\r\n", fd); +} + +static void +write_dirstats(int fd, const char *dname, int details) +{ + DIR *dir; + struct dirent *dirent; + struct stat statbuf; + char *filename; + + dir = xopendir(dname); + + for (;;) { + dirent = readdir(dir); + if (dirent == NULL) + break; + + /* Ignore . and .. */ + if (dirent->d_name[0] == '.') { + if (dirent->d_name[1] == '\0' + || (dirent->d_name[1] == '.' && dirent->d_name[2] == '\0') + ) { + continue; + } + } + + if (details) { + filename = xasprintf("%s/%s", dname, dirent->d_name); + if (lstat(filename, &statbuf) != 0) { + free(filename); + goto bail; + } + } else + filename = xstrdup(dirent->d_name); + + write_filestats(fd, filename, details ? &statbuf : NULL); + free(filename); + } + +bail: + closedir(dir); +} + +static void +handle_pasv(void) +{ + int bind_retries = 10; + unsigned short port; + enum { min_port = 1024, max_port = 65535 }; + char *addr, *wire_addr, *response; + + pasv_cleanup(); + port_cleanup(); + G.pasv_listen_fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0); + setsockopt_reuseaddr(G.pasv_listen_fd); + + /* TODO bind() with port == 0 and then call getsockname */ + while (--bind_retries) { + port = rand() % max_port; + if (port < min_port) { + port += min_port; + } + + set_nport(G.local_addr, htons(port)); + /* We don't want to use xbind, it'll die if port is in use */ + if (bind(G.pasv_listen_fd, &G.local_addr->u.sa, G.local_addr->len) != 0) { + /* do we want check if errno == EADDRINUSE ? */ + continue; + } + xlisten(G.pasv_listen_fd, 1); + break; + } + + if (!bind_retries) + bb_error_msg_and_die("can't create pasv socket"); + + addr = xmalloc_sockaddr2dotted_noport(&G.local_addr->u.sa); + wire_addr = replace_text(addr, '.', ","); + free(addr); + + response = xasprintf("Entering Passive Mode (%s,%u,%u)", + wire_addr, (int)(port >> 8), (int)(port & 255)); + + cmdio_write(FTP_PASVOK, response); + free(wire_addr); + free(response); +} + +static void +handle_retr(void) +{ + struct stat statbuf; + int trans_ret, retval; + int remote_fd; + int opened_file; + off_t offset = G.restart_pos; + char *response; + + G.restart_pos = 0; + + if (!data_transfer_checks_ok()) + return; + + /* XXX Do we need check if ftp_arg != NULL? */ + opened_file = open(G.ftp_arg, O_RDONLY | O_NONBLOCK); + if (opened_file < 0) { + cmdio_write(FTP_FILEFAIL, "Can't open file"); + return; + } + + retval = fstat(opened_file, &statbuf); + if (retval < 0 || !S_ISREG(statbuf.st_mode)) { + /* Note - pretend open failed */ + cmdio_write(FTP_FILEFAIL, "Can't open file"); + goto file_close_out; + } + + /* Now deactive O_NONBLOCK, otherwise we have a problem on DMAPI filesystems + * such as XFS DMAPI. + */ + ndelay_off(opened_file); + + /* Set the download offset (from REST) if any */ + if (offset != 0) + xlseek(opened_file, offset, SEEK_SET); + + response = xasprintf( + "Opening BINARY mode data connection for (%s %"OFF_FMT"u bytes).", + G.ftp_arg, statbuf.st_size); + + remote_fd = get_remote_transfer_fd(response); + free(response); + if (remote_fd < 0) + goto port_pasv_cleanup_out; + + trans_ret = bb_copyfd_eof(opened_file, remote_fd); + ftpdataio_dispose_transfer_fd(); + if (trans_ret < 0) + cmdio_write(FTP_BADSENDFILE, "Error sending local file"); + else + cmdio_write(FTP_TRANSFEROK, "File sent OK"); + +port_pasv_cleanup_out: + port_cleanup(); + pasv_cleanup(); +file_close_out: + close(opened_file); +} + +static void +handle_dir_common(int full_details, int stat_cmd) +{ + int fd; + struct stat statbuf; + + if (!stat_cmd && !data_transfer_checks_ok()) + return; + + if (stat_cmd) { + fd = STDIN_FILENO; + cmdio_write_hyphen(FTP_STATFILE_OK, "Status follows:"); + } else { + fd = get_remote_transfer_fd("Here comes the directory listing"); + if (fd < 0) + goto bail; + } + + if (G.ftp_arg != NULL) { + if (lstat(G.ftp_arg, &statbuf) != 0) { + /* Dir doesn't exist => return ok to client */ + goto bail; + } + if (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode)) + write_filestats(fd, G.ftp_arg, &statbuf); + else if (S_ISDIR(statbuf.st_mode)) + write_dirstats(fd, G.ftp_arg, full_details); + } else + write_dirstats(fd, ".", full_details); + +bail: + /* Well, if we can't open directory/file it doesn't matter */ + if (!stat_cmd) { + ftpdataio_dispose_transfer_fd(); + pasv_cleanup(); + port_cleanup(); + cmdio_write(FTP_TRANSFEROK, "OK"); + } else + cmdio_write(FTP_STATFILE_OK, "End of status"); +} + +static void +handle_list(void) +{ + handle_dir_common(1, 0); +} + +static void +handle_type(void) +{ + if (G.ftp_arg + && ( ((G.ftp_arg[0] | 0x20) == 'i' && G.ftp_arg[1] == '\0') + || !strcasecmp(G.ftp_arg, "L8") + || !strcasecmp(G.ftp_arg, "L 8") + ) + ) { + cmdio_write(FTP_TYPEOK, "Switching to Binary mode"); + } else { + cmdio_write(FTP_BADCMD, "Unrecognised TYPE command"); + } +} + +static void +handle_port(void) +{ + unsigned short port; + char *raw = NULL, *port_part; + len_and_sockaddr *lsa = NULL; + + pasv_cleanup(); + port_cleanup(); + + if (G.ftp_arg == NULL) + goto bail; + + raw = replace_text(G.ftp_arg, ',', "."); + + port_part = strrchr(raw, '.'); + if (port_part == NULL) + goto bail; + + port = xatou16(&port_part[1]); + *port_part = '\0'; + + port_part = strrchr(raw, '.'); + if (port_part == NULL) + goto bail; + + port |= xatou16(&port_part[1]) << 8; + *port_part = '\0'; + + lsa = xdotted2sockaddr(raw, port); + +bail: + free(raw); + + if (lsa == NULL) { + cmdio_write(FTP_BADCMD, "Illegal PORT command"); + return; + } + + G.port_addr = lsa; + cmdio_write(FTP_PORTOK, "PORT command successful. Consider using PASV"); +} + +#if ENABLE_FEATURE_FTP_WRITE +static void +handle_upload_common(int is_append, int is_unique) +{ + char *template = NULL; + int trans_ret; + int new_file_fd; + int remote_fd; + + enum { + fileflags = O_CREAT | O_WRONLY | O_APPEND, + }; + + off_t offset = G.restart_pos; + + G.restart_pos = 0; + if (!data_transfer_checks_ok()) + return; + + if (is_unique) { + template = xstrdup("uniq.XXXXXX"); + /* + * XXX Use mkostemp here? vsftpd opens file with O_CREAT, O_WRONLY, + * O_APPEND and O_EXCL flags... + */ + new_file_fd = mkstemp(template); + } else { + /* XXX Do we need check if ftp_arg != NULL? */ + if (!is_append && offset == 0) + new_file_fd = open(G.ftp_arg, O_CREAT | O_WRONLY | O_APPEND | O_NONBLOCK | O_TRUNC, 0666); + else + new_file_fd = open(G.ftp_arg, O_CREAT | O_WRONLY | O_APPEND | O_NONBLOCK, 0666); + } + + if (new_file_fd < 0) { + cmdio_write(FTP_UPLOADFAIL, "Can't create file"); + return; + } + + if (!is_append && offset != 0) { + /* warning, allows seek past end of file! Check for seek > size? */ + xlseek(new_file_fd, offset, SEEK_SET); + } + + if (is_unique) { + char *resp = xasprintf("FILE: %s", template); + remote_fd = get_remote_transfer_fd(resp); + free(resp); + free(template); + } else + remote_fd = get_remote_transfer_fd("Ok to send data"); + + if (remote_fd < 0) + goto bail; + + trans_ret = bb_copyfd_eof(remote_fd, new_file_fd); + ftpdataio_dispose_transfer_fd(); + + if (trans_ret < 0) + cmdio_write(FTP_BADSENDFILE, "Failure writing to local file"); + else + cmdio_write(FTP_TRANSFEROK, "File receive OK"); + +bail: + port_cleanup(); + pasv_cleanup(); + close(new_file_fd); +} + +static void +handle_stor(void) +{ + handle_upload_common(0, 0); +} + +static void +handle_mkd(void) +{ + int retval; + + /* Do we need check if ftp_arg != NULL? */ + retval = mkdir(G.ftp_arg, 0770); + if (retval != 0) { + cmdio_write(FTP_FILEFAIL, "Create directory operation failed"); + return; + } + + cmdio_write(FTP_MKDIROK, "created"); +} + +static void +handle_rmd(void) +{ + int retval; + + /* Do we need check if ftp_arg != NULL? */ + retval = rmdir(G.ftp_arg); + if (retval != 0) + cmdio_write(FTP_FILEFAIL, "rmdir failed"); + else + cmdio_write(FTP_RMDIROK, "rmdir successful"); +} + +static void +handle_dele(void) +{ + int retval; + + /* Do we need check if ftp_arg != NULL? */ + retval = unlink(G.ftp_arg); + if (retval != 0) + cmdio_write(FTP_FILEFAIL, "Delete failed"); + else + cmdio_write(FTP_DELEOK, "Delete successful"); +} +#endif /* ENABLE_FEATURE_FTP_WRITE */ + +static void +handle_rest(void) +{ + /* When ftp_arg == NULL simply restart from beginning */ + G.restart_pos = xatoi_u(G.ftp_arg); + cmdio_write(FTP_RESTOK, "Restart OK"); +} + +#if ENABLE_FEATURE_FTP_WRITE +static void +handle_rnfr(void) +{ + struct stat statbuf; + int retval; + + /* Clear old value */ + free(G.rnfr_filename); + + /* Does it exist? Do we need check if ftp_arg != NULL? */ + retval = stat(G.ftp_arg, &statbuf); + if (retval == 0) { + /* Yes */ + G.rnfr_filename = xstrdup(G.ftp_arg); + cmdio_write(FTP_RNFROK, "Ready for RNTO"); + } else + cmdio_write(FTP_FILEFAIL, "RNFR command failed"); +} + +static void +handle_rnto(void) +{ + int retval; + + /* If we didn't get a RNFR, throw a wobbly */ + if (G.rnfr_filename == NULL) { + cmdio_write(FTP_NEEDRNFR, "RNFR required first"); + return; + } + + /* XXX Do we need check if ftp_arg != NULL? */ + retval = rename(G.rnfr_filename, G.ftp_arg); + + free(G.rnfr_filename); + + if (retval == 0) + cmdio_write(FTP_RENAMEOK, "Rename successful"); + else + cmdio_write(FTP_FILEFAIL, "Rename failed"); +} +#endif /* ENABLE_FEATURE_FTP_WRITE */ + +static void +handle_nlst(void) +{ + handle_dir_common(0, 0); +} + +#if ENABLE_FEATURE_FTP_WRITE +static void +handle_appe(void) +{ + handle_upload_common(1, 0); +} +#endif + +static void +handle_help(void) +{ + cmdio_write_hyphen(FTP_HELP, "Recognized commands:"); + cmdio_write_raw(" ALLO CDUP CWD HELP LIST\r\n" + " MODE NLST NOOP PASS PASV PORT PWD QUIT\r\n" + " REST RETR STAT STRU SYST TYPE USER\r\n" +#if ENABLE_FEATURE_FTP_WRITE + " APPE DELE MKD RMD RNFR RNTO STOR STOU\r\n" +#endif + ); + cmdio_write(FTP_HELP, "Help OK"); +} + +#if ENABLE_FEATURE_FTP_WRITE +static void +handle_stou(void) +{ + handle_upload_common(0, 1); +} +#endif + +static void +handle_stat(void) +{ + cmdio_write_hyphen(FTP_STATOK, "FTP server status:"); + cmdio_write_raw(" TYPE: BINARY\r\n"); + cmdio_write(FTP_STATOK, "End of status"); +} + +static void +handle_stat_file(void) +{ + handle_dir_common(1, 1); +} + +/* TODO: libbb candidate (tftp has another copy) */ +static len_and_sockaddr *get_sock_lsa(int s) +{ + len_and_sockaddr *lsa; + socklen_t len = 0; + + if (getsockname(s, NULL, &len) != 0) + return NULL; + lsa = xzalloc(LSA_LEN_SIZE + len); + lsa->len = len; + getsockname(s, &lsa->u.sa, &lsa->len); + return lsa; +} + +int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; +int ftpd_main(int argc UNUSED_PARAM, char **argv) +{ + smallint user_was_specified = 0; + + INIT_G(); + + G.local_addr = get_sock_lsa(STDIN_FILENO); + if (!G.local_addr) { + /* This is confusing: + * bb_error_msg_and_die("stdin is not a socket"); + * Better: */ + bb_show_usage(); + /* Help text says that ftpd must be used as inetd service, + * which is by far the most usual cause of get_sock_lsa + * failure */ + } + + logmode = LOGMODE_SYSLOG; + + USE_FEATURE_FTP_WRITE(G.write_enable =) getopt32(argv, "" USE_FEATURE_FTP_WRITE("w")); + if (argv[optind]) { + xchdir(argv[optind]); + chroot("."); + } + +// if (G.local_addr->u.sa.sa_family != AF_INET) +// bb_error_msg_and_die("Only IPv4 is supported"); + + /* Signals. We'll always take -EPIPE rather than a rude signal, thanks */ + signal(SIGPIPE, SIG_IGN); + + /* Set up options on the command socket */ + setsockopt(STDIN_FILENO, IPPROTO_TCP, TCP_NODELAY, &const_int_1, sizeof(const_int_1)); + setsockopt(STDIN_FILENO, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1)); + setsockopt(STDIN_FILENO, SOL_SOCKET, SO_OOBINLINE, &const_int_1, sizeof(const_int_1)); + + cmdio_write(FTP_GREET, "Welcome"); + + while (1) { + uint32_t cmdval = cmdio_get_cmd_and_arg(); + + if (cmdval == const_USER) { + if (G.ftp_arg == NULL || strcasecmp(G.ftp_arg, "anonymous") != 0) + cmdio_write(FTP_LOGINERR, "Server is anonymous only"); + else { + user_was_specified = 1; + cmdio_write(FTP_GIVEPWORD, "Please specify the password"); + } + } else if (cmdval == const_PASS) { + if (user_was_specified) + break; + cmdio_write(FTP_NEEDUSER, "Login with USER"); + } else if (cmdval == const_QUIT) { + cmdio_write(FTP_GOODBYE, "Goodbye"); + return 0; + } else { + cmdio_write(FTP_LOGINERR, + "Login with USER and PASS"); + } + } + + umask(077); + cmdio_write(FTP_LOGINOK, "Login successful"); + + while (1) { + uint32_t cmdval = cmdio_get_cmd_and_arg(); + + if (cmdval == const_QUIT) { + cmdio_write(FTP_GOODBYE, "Goodbye"); + return 0; + } + if (cmdval == const_PWD) + handle_pwd(); + else if (cmdval == const_CWD) + handle_cwd(); + else if (cmdval == const_CDUP) + handle_cdup(); + else if (cmdval == const_PASV) + handle_pasv(); + else if (cmdval == const_RETR) + handle_retr(); + else if (cmdval == const_NOOP) + cmdio_write(FTP_NOOPOK, "NOOP ok"); + else if (cmdval == const_SYST) + cmdio_write(FTP_SYSTOK, "UNIX Type: L8"); + else if (cmdval == const_HELP) + handle_help(); + else if (cmdval == const_LIST) + handle_list(); + else if (cmdval == const_TYPE) + handle_type(); + else if (cmdval == const_PORT) + handle_port(); + else if (cmdval == const_REST) + handle_rest(); + else if (cmdval == const_NLST) + handle_nlst(); +#if ENABLE_FEATURE_FTP_WRITE + else if (G.write_enable) { + if (cmdval == const_STOR) + handle_stor(); + else if (cmdval == const_MKD) + handle_mkd(); + else if (cmdval == const_RMD) + handle_rmd(); + else if (cmdval == const_DELE) + handle_dele(); + else if (cmdval == const_RNFR) + handle_rnfr(); + else if (cmdval == const_RNTO) + handle_rnto(); + else if (cmdval == const_APPE) + handle_appe(); + else if (cmdval == const_STOU) + handle_stou(); + } +#endif + else if (cmdval == const_STRU) { + if (G.ftp_arg + && (G.ftp_arg[0] | 0x20) == 'f' + && G.ftp_arg[1] == '\0' + ) { + cmdio_write(FTP_STRUOK, "Structure set to F"); + } else + cmdio_write(FTP_BADSTRU, "Bad STRU command"); + + } else if (cmdval == const_MODE) { + if (G.ftp_arg + && (G.ftp_arg[0] | 0x20) == 's' + && G.ftp_arg[1] == '\0' + ) { + cmdio_write(FTP_MODEOK, "Mode set to S"); + } else + cmdio_write(FTP_BADMODE, "Bad MODE command"); + } + else if (cmdval == const_ALLO) + cmdio_write(FTP_ALLOOK, "ALLO command ignored"); + else if (cmdval == const_STAT) { + if (G.ftp_arg == NULL) + handle_stat(); + else + handle_stat_file(); + } else if (cmdval == const_USER) + cmdio_write(FTP_LOGINERR, "Can't change to another user"); + else if (cmdval == const_PASS) + cmdio_write(FTP_LOGINOK, "Already logged in"); + else if (cmdval == const_STOR + || cmdval == const_MKD + || cmdval == const_RMD + || cmdval == const_DELE + || cmdval == const_RNFR + || cmdval == const_RNTO + || cmdval == const_APPE + || cmdval == const_STOU + ) { + cmdio_write(FTP_NOPERM, "Permission denied"); + } else { + cmdio_write(FTP_BADCMD, "Unknown command"); + } + } +}