| @@ -63,9 +63,24 @@ |
| #define STR1(s) #s |
| #define STR(s) STR1(s) |
| |
| + | /* Convert a constant to 3-digit string, packed into uint32_t */ |
| enum { |
| - | OPT_v = (1 << 0), |
| - | OPT_w = (1 << 1), |
| + | /* Shift for Nth decimal digit */ |
| + | SHIFT0 = 16 * BB_LITTLE_ENDIAN + 8 * BB_BIG_ENDIAN, |
| + | SHIFT1 = 8 * BB_LITTLE_ENDIAN + 16 * BB_BIG_ENDIAN, |
| + | SHIFT2 = 0 * BB_LITTLE_ENDIAN + 24 * BB_BIG_ENDIAN, |
| + | }; |
| + | #define STRNUM32(s) (uint32_t)(0 \ |
| + | | (('0' + ((s) / 1 % 10)) << SHIFT0) \ |
| + | | (('0' + ((s) / 10 % 10)) << SHIFT1) \ |
| + | | (('0' + ((s) / 100 % 10)) << SHIFT2) \ |
| + | ) |
| + | |
| + | enum { |
| + | OPT_l = (1 << 0), |
| + | OPT_1 = (1 << 1), |
| + | OPT_v = (1 << 2), |
| + | OPT_w = (1 << 3), |
| |
| #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) |
| @@ -106,78 +121,84 @@ struct globals { |
| len_and_sockaddr *local_addr; |
| len_and_sockaddr *port_addr; |
| int pasv_listen_fd; |
| - | int data_fd; |
| + | int proc_self_fd; |
| off_t restart_pos; |
| char *ftp_cmd; |
| char *ftp_arg; |
| #if ENABLE_FEATURE_FTP_WRITE |
| char *rnfr_filename; |
| #endif |
| - | smallint opts; |
| }; |
| #define G (*(struct globals*)&bb_common_bufsiz1) |
| #define INIT_G() do { } while (0) |
| |
| |
| static char * |
| - | escape_text(const char *prepend, const char *str, char from, const char *to, char append) |
| + | escape_text(const char *prepend, const char *str, unsigned escapee) |
| { |
| - | size_t retlen, remainlen, chunklen, tolen; |
| - | const char *remain; |
| + | unsigned retlen, remainlen, chunklen; |
| char *ret, *found; |
| + | char append; |
| |
| - | remain = str; |
| - | remainlen = strlen(str); |
| - | tolen = strlen(to); |
| + | append = (char)escapee; |
| + | escapee >>= 8; |
| |
| - | /* Simply alloc strlen(str)*strlen(to). "to" is max 2 so it's ok */ |
| + | remainlen = strlen(str); |
| retlen = strlen(prepend); |
| - | ret = xmalloc(retlen + remainlen * tolen + 1 + 1); |
| + | ret = xmalloc(retlen + remainlen * 2 + 1 + 1); |
| strcpy(ret, prepend); |
| |
| for (;;) { |
| - | found = strchrnul(remain, from); |
| - | chunklen = found - remain; |
| + | found = strchrnul(str, escapee); |
| + | chunklen = found - str + 1; |
| |
| - | /* Copy chunk which doesn't contain 'from' to ret */ |
| - | memcpy(&ret[retlen], remain, chunklen); |
| + | /* Copy chunk up to and including escapee (or NUL) to ret */ |
| + | memcpy(ret + retlen, str, chunklen); |
| retlen += chunklen; |
| |
| - | if (*found != '\0') { |
| - | /* Now copy 'to' instead of 'from' */ |
| - | memcpy(&ret[retlen], to, tolen); |
| - | retlen += tolen; |
| - | |
| - | remain = found + 1; |
| - | } else { |
| - | ret[retlen] = append; |
| - | ret[retlen+1] = '\0'; |
| + | if (*found == '\0') { |
| + | /* It wasn't escapee, it was NUL! */ |
| + | ret[retlen - 1] = append; /* replace NUL */ |
| + | ret[retlen] = '\0'; /* add NUL */ |
| break; |
| } |
| + | ret[retlen++] = escapee; /* duplicate escapee */ |
| + | str = found + 1; |
| } |
| return ret; |
| } |
| |
| - | static void |
| + | /* Returns strlen as a bonus */ |
| + | static unsigned |
| replace_char(char *str, char from, char to) |
| { |
| - | while ((str = strchr(str, from)) != NULL) |
| - | *str++ = to; |
| + | char *p = str; |
| + | while (*p) { |
| + | if (*p == from) |
| + | *p = to; |
| + | p++; |
| + | } |
| + | return p - str; |
| } |
| |
| static void |
| - | cmdio_write(unsigned status, const char *str) |
| + | cmdio_write(uint32_t status_str, const char *str) |
| { |
| + | union { |
| + | char buf[4]; |
| + | uint32_t v32; |
| + | } u; |
| char *response; |
| int len; |
| |
| + | u.v32 = status_str; |
| + | |
| /* FTP allegedly uses telnet protocol for command link. |
| * In telnet, 0xff is an escape char, and needs to be escaped: */ |
| - | response = escape_text(utoa(status), str, '\xff', "\xff\xff", '\r'); |
| + | response = escape_text(u.buf, str, (0xff << 8) + '\r'); |
| |
| /* ?! does FTP send embedded LFs as NULs? wow */ |
| - | len = strlen(response); |
| - | replace_char(response, '\n', '\0'); |
| + | len = replace_char(response, '\n', '\0'); |
| |
| response[len++] = '\n'; /* tack on trailing '\n' */ |
| xwrite(STDOUT_FILENO, response, len); |
| @@ -215,9 +236,9 @@ handle_pwd(void) |
| cwd = xstrdup(""); |
| |
| /* We have to promote each " to "" */ |
| - | response = escape_text(" \"", cwd, '\"', "\"\"", '\"'); |
| + | response = escape_text(" \"", cwd, ('"' << 8) + '"'); |
| free(cwd); |
| - | cmdio_write(FTP_PWDOK, response); |
| + | cmdio_write(STRNUM32(FTP_PWDOK), response); |
| free(response); |
| } |
| |
| @@ -261,21 +282,6 @@ handle_help(void) |
| |
| /* Download commands */ |
| |
| - | 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) |
| { |
| @@ -288,39 +294,10 @@ ftpdataio_get_pasv_fd(void) |
| 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 to die or print error to client? */ |
| - | remote_fd = xconnect_stream(G.port_addr); |
| - | |
| - | init_data_sock_params(remote_fd); |
| + | setsockopt(remote_fd, SOL_SOCKET, SO_KEEPALIVE, &const_int_1, sizeof(const_int_1)); |
| 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 inline int |
| port_active(void) |
| { |
| @@ -341,12 +318,12 @@ get_remote_transfer_fd(const char *p_status_msg) |
| if (pasv_active()) |
| remote_fd = ftpdataio_get_pasv_fd(); |
| else |
| - | remote_fd = ftpdataio_get_port_fd(); |
| + | remote_fd = xconnect_stream(G.port_addr); |
| |
| if (remote_fd < 0) |
| return remote_fd; |
| |
| - | cmdio_write(FTP_DATACONN, p_status_msg); |
| + | cmdio_write(STRNUM32(FTP_DATACONN), p_status_msg); |
| return remote_fd; |
| } |
| |
| @@ -374,21 +351,21 @@ port_pasv_cleanup(void) |
| static unsigned |
| bind_for_passive_mode(void) |
| { |
| + | int fd; |
| unsigned port; |
| |
| port_pasv_cleanup(); |
| |
| - | G.pasv_listen_fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0); |
| - | setsockopt_reuseaddr(G.pasv_listen_fd); |
| + | G.pasv_listen_fd = fd = xsocket(G.local_addr->u.sa.sa_family, SOCK_STREAM, 0); |
| + | setsockopt_reuseaddr(fd); |
| |
| set_nport(G.local_addr, 0); |
| - | xbind(G.pasv_listen_fd, &G.local_addr->u.sa, G.local_addr->len); |
| - | xlisten(G.pasv_listen_fd, 1); |
| - | getsockname(G.pasv_listen_fd, &G.local_addr->u.sa, &G.local_addr->len); |
| + | xbind(fd, &G.local_addr->u.sa, G.local_addr->len); |
| + | xlisten(fd, 1); |
| + | getsockname(fd, &G.local_addr->u.sa, &G.local_addr->len); |
| |
| port = get_nport(&G.local_addr->u.sa); |
| port = ntohs(port); |
| - | |
| return port; |
| } |
| |
| @@ -487,7 +464,7 @@ handle_retr(void) |
| G.restart_pos = 0; |
| |
| if (!data_transfer_checks_ok()) |
| - | return; |
| + | return; /* data_transfer_checks_ok emitted error response */ |
| |
| /* O_NONBLOCK is useful if file happens to be a device node */ |
| opened_file = G.ftp_arg ? open(G.ftp_arg, O_RDONLY | O_NONBLOCK) : -1; |
| @@ -520,7 +497,7 @@ handle_retr(void) |
| goto port_pasv_cleanup_out; |
| |
| trans_ret = bb_copyfd_eof(opened_file, remote_fd); |
| - | ftpdataio_dispose_transfer_fd(); |
| + | close(remote_fd); |
| if (trans_ret < 0) |
| cmdio_write_error(FTP_BADSENDFILE); |
| else |
| @@ -535,185 +512,105 @@ handle_retr(void) |
| |
| /* List commands */ |
| |
| - | 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; |
| - | char timestr[32]; |
| - | struct tm *tm; |
| - | |
| - | 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); |
| - | if (strftime(timestr, sizeof(timestr), "%b %d %H:%M", tm) == 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); |
| - | |
| - | xwrite_str(fd, stats); |
| - | free(stats); |
| - | if (lnkname != NULL) { |
| - | xwrite_str(fd, " -> "); |
| - | xwrite_str(fd, lnkname); |
| - | free(lnkname); |
| - | } |
| - | xwrite_str(fd, "\r\n"); |
| - | } |
| - | |
| - | static void |
| - | write_dirstats(int fd, const char *dname, int details) |
| + | static int |
| + | popen_ls(const char *opt) |
| { |
| - | DIR *dir; |
| - | struct dirent *dirent; |
| - | struct stat statbuf; |
| - | char *filename; |
| - | |
| - | dir = xopendir(dname); |
| + | char *cwd; |
| + | const char *argv[5] = { "ftpd", opt, NULL, G.ftp_arg, NULL }; |
| + | struct fd_pair outfd; |
| + | pid_t pid; |
| |
| - | for (;;) { |
| - | dirent = readdir(dir); |
| - | if (dirent == NULL) |
| - | break; |
| + | cwd = xrealloc_getcwd_or_warn(NULL); |
| |
| - | /* Ignore . and .. */ |
| - | if (dirent->d_name[0] == '.') { |
| - | if (dirent->d_name[1] == '\0' |
| - | || (dirent->d_name[1] == '.' && dirent->d_name[2] == '\0') |
| - | ) { |
| - | continue; |
| - | } |
| + | xpiped_pair(outfd); |
| + | |
| + | fflush(NULL); |
| + | pid = vfork(); |
| + | |
| + | switch (pid) { |
| + | case -1: /* failure */ |
| + | bb_perror_msg_and_die("vfork"); |
| + | case 0: /* child */ |
| + | /* NB: close _first_, then move fds! */ |
| + | close(outfd.rd); |
| + | xmove_fd(outfd.wr, STDOUT_FILENO); |
| + | close(STDIN_FILENO); |
| + | /* xopen("/dev/null", O_RDONLY); - chroot may lack it! */ |
| + | if (fchdir(G.proc_self_fd) == 0) { |
| + | close(G.proc_self_fd); |
| + | argv[2] = cwd; |
| + | /* ftpd ls helper chdirs to argv[2], |
| + | * preventing peer from seeing /proc/self |
| + | */ |
| + | execv("exe", (char**) argv); |
| } |
| - | |
| - | if (details) { |
| - | filename = xasprintf("%s/%s", dname, dirent->d_name); |
| - | if (lstat(filename, &statbuf) != 0) { |
| - | free(filename); |
| - | break; |
| - | } |
| - | } else |
| - | filename = xstrdup(dirent->d_name); |
| - | |
| - | write_filestats(fd, filename, details ? &statbuf : NULL); |
| - | free(filename); |
| + | _exit(127); |
| } |
| - | |
| - | closedir(dir); |
| + | /* parent */ |
| + | close(outfd.wr); |
| + | free(cwd); |
| + | return outfd.rd; |
| } |
| |
| static void |
| - | handle_dir_common(int full_details, int stat_cmd) |
| + | handle_dir_common(int opts) |
| { |
| - | int fd; |
| - | struct stat statbuf; |
| + | FILE *ls_fp; |
| + | char *line; |
| + | int ls_fd; |
| |
| - | if (!stat_cmd && !data_transfer_checks_ok()) |
| - | return; |
| + | if (!(opts & 1) && !data_transfer_checks_ok()) |
| + | return; /* data_transfer_checks_ok emitted error response */ |
| + | |
| + | ls_fd = popen_ls((opts & 2) ? "-l" : "-1"); |
| + | ls_fp = fdopen(ls_fd, "r"); |
| + | if (!ls_fp) /* never happens. paranoia */ |
| + | bb_perror_msg_and_die("fdopen"); |
| |
| - | if (stat_cmd) { |
| - | fd = STDIN_FILENO; |
| + | if (opts & 1) { |
| + | /* STAT <filename> */ |
| cmdio_write_raw(STR(FTP_STATFILE_OK)"-Status follows:\r\n"); |
| + | while (1) { |
| + | line = xmalloc_fgetline(ls_fp); |
| + | if (!line) |
| + | break; |
| + | cmdio_write(0, line); /* hack: 0 results in no status at all */ |
| + | free(line); |
| + | } |
| + | cmdio_write_ok(FTP_STATFILE_OK); |
| } else { |
| - | fd = get_remote_transfer_fd(" Here comes the directory listing"); |
| - | if (fd < 0) |
| - | goto bail; |
| - | } |
| - | |
| - | if (G.ftp_arg) { |
| - | if (lstat(G.ftp_arg, &statbuf) != 0) { |
| - | /* Dir doesn't exist => return ok to client */ |
| - | goto bail; |
| + | /* LIST/NLST [<filename>] */ |
| + | int remote_fd = get_remote_transfer_fd(" Here comes the directory listing"); |
| + | if (remote_fd >= 0) { |
| + | while (1) { |
| + | line = xmalloc_fgetline(ls_fp); |
| + | if (!line) |
| + | break; |
| + | xwrite_str(remote_fd, line); |
| + | xwrite(remote_fd, "\r\n", 2); |
| + | free(line); |
| + | } |
| } |
| - | 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(); |
| + | close(remote_fd); |
| port_pasv_cleanup(); |
| cmdio_write_ok(FTP_TRANSFEROK); |
| - | } else |
| - | cmdio_write_ok(FTP_STATFILE_OK); |
| + | } |
| + | fclose(ls_fp); /* closes ls_fd too */ |
| } |
| - | |
| static void |
| handle_list(void) |
| { |
| - | handle_dir_common(1, 0); |
| + | handle_dir_common(2); |
| } |
| - | |
| static void |
| handle_nlst(void) |
| { |
| - | handle_dir_common(0, 0); |
| + | handle_dir_common(0); |
| } |
| - | |
| static void |
| handle_stat_file(void) |
| { |
| - | handle_dir_common(1, 1); |
| + | handle_dir_common(3); |
| } |
| |
| static void |
| @@ -840,7 +737,7 @@ handle_upload_common(int is_append, int is_unique) |
| goto bail; |
| |
| trans_ret = bb_copyfd_eof(remote_fd, local_file_fd); |
| - | ftpdataio_dispose_transfer_fd(); |
| + | close(remote_fd); |
| |
| if (trans_ret < 0) |
| cmdio_write_error(FTP_BADSENDFILE); |
| @@ -905,8 +802,19 @@ cmdio_get_cmd_and_arg(void) |
| } |
| |
| int ftpd_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE; |
| - | int ftpd_main(int argc UNUSED_PARAM, char **argv) |
| + | int ftpd_main(int argc, char **argv) |
| { |
| + | smallint opts; |
| + | |
| + | opts = getopt32(argv, "l1v" USE_FEATURE_FTP_WRITE("w")); |
| + | |
| + | if (opts & (OPT_l|OPT_1)) { |
| + | /* Our secret backdoor to ls */ |
| + | xchdir(argv[2]); |
| + | argv[2] = (char*)"--"; |
| + | return ls_main(argc, argv); |
| + | } |
| + | |
| INIT_G(); |
| |
| G.local_addr = get_sock_lsa(STDIN_FILENO); |
| @@ -920,13 +828,13 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv) |
| * failure */ |
| } |
| |
| - | G.opts = getopt32(argv, "v" USE_FEATURE_FTP_WRITE("w")); |
| - | |
| openlog(applet_name, LOG_PID, LOG_DAEMON); |
| logmode |= LOGMODE_SYSLOG; |
| - | if (!(G.opts & OPT_v)) |
| + | if (!(opts & OPT_v)) |
| logmode = LOGMODE_SYSLOG; |
| |
| + | G.proc_self_fd = xopen("/proc/self", O_RDONLY | O_DIRECTORY); |
| + | |
| if (argv[optind]) { |
| xchdir(argv[optind]); |
| chroot("."); |
| @@ -1064,7 +972,7 @@ int ftpd_main(int argc UNUSED_PARAM, char **argv) |
| else if (cmdval == const_REST) |
| handle_rest(); |
| #if ENABLE_FEATURE_FTP_WRITE |
| - | else if (G.opts & OPT_w) { |
| + | else if (opts & OPT_w) { |
| if (cmdval == const_STOR) |
| handle_stor(); |
| else if (cmdval == const_MKD) |