/* cmds.c: BetaFTPD command handlers Copyright (C) 1999-2000 Steinar H. Gunderson This program is is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2 if the License as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #define _GNU_SOURCE #include #if HAVE_CONFIG_H #include #endif #if HAVE_STROPTS_H #include #endif #if HAVE_SYS_CONF_H #include #endif #if HAVE_DIRENT_H #include #endif #if HAVE_CRYPT_H #include #endif #if HAVE_ERRNO_H #include #endif #if HAVE_GLOB_H #include #endif #if HAVE_SYS_POLL_H #include #endif #if HAVE_STDARG_H #include #endif #if HAVE_STDIO_H #include #endif #if HAVE_STDLIB_H #include #endif #if HAVE_STRING_H #include #endif #if HAVE_STRINGS_H #include #endif #if HAVE_UNISTD_H #include #endif #if HAVE_TIME_H #include #endif #if HAVE_FCNTL_H #include #endif #if HAVE_GRP_H #include #endif #if HAVE_SYS_IOCTL_H #include #endif #if HAVE_SYS_STAT_H #include #endif #if HAVE_SYS_PARAM_H #include #endif #if HAVE_STROPTS_H #include #endif #if HAVE_SYS_CONF_H #include #endif #if HAVE_SHADOW_H #include #endif #if HAVE_SYS_FILIO_H #include #endif #if WANT_NONROOT #define NO_SETUID #define DO_SETUID #else #define NO_SETUID ,0 #define DO_SETUID ,1 #endif #ifndef NBBY #define NBBY 8 #endif #include "ftpd.h" #include "conn.h" #include "nonroot.h" #if WANT_DCACHE #include "dcache.h" #endif #define lstat stat /* * TRAP_ERROR: This is a quick way of doing a test for an error condition. * if an error occurs (or more precisely, if the value supplied is * true), an error message is sent back to the client. This is _not_ * a debugging macro. The condition is allowed to have side effects, * and is only evaluated once. * * The second argument is the FTP error code to send back, in case * of an error (the error message itself will be supplied by the * system). The third argument is code to execute after the FTP error * has been sent (typically `return' or `return -1'). * * If TRAP_ERROR_DEBUG is defined, some extra debugging info is * sent. Don't enable this for a normal server, it could be a * security risk. */ #undef TRAP_ERROR_DEBUG /* #define TRAP_ERROR_DEBUG 1 */ #ifdef TRAP_ERROR_DEBUG #define TRAP_ERROR(x, num, y) TRAP_ERROR_INTERNAL(x, num, y, __FILE__, __LINE__) #define TRAP_ERROR_INTERNAL(x, num, y, fl, ln) \ if (x) { \ numeric(num, "%s (%s:%d)", strerror(errno), fl, ln); \ y; \ } #else #define TRAP_ERROR(x, num, y) \ if (x) { \ numeric(num, strerror(errno)); \ y; \ } #endif extern struct conn *first_conn; #if WANT_DCACHE extern struct dcache *first_dcache; #endif struct handler { char cmd_name[6]; char add_cmlen; /* =1 if the command takes an argument */ int (conn::*callback)(); char min_auth; #if !WANT_NONROOT char do_setuid; /* =1 if root is not *really* needed */ #endif }; void prepare_for_transfer(struct ftran *f); static char decode_mode(mode_t mode); static char classify(const mode_t mode); const struct handler conn::handler_table[] = { { "user ", 1, &conn::cmd_user, 0 NO_SETUID }, { "pass ", 1, &conn::cmd_pass, 1 NO_SETUID }, { "retr ", 1, &conn::cmd_retr, 3 DO_SETUID }, { "acct ", 1, &conn::cmd_acct, 0 NO_SETUID }, { "port ", 1, &conn::cmd_port, 3 DO_SETUID }, { "pasv" , 0, &conn::cmd_pasv, 3 DO_SETUID }, { "pwd" , 0, &conn::cmd_pwd, 3 DO_SETUID }, { "cwd " , 1, &conn::cmd_cwd, 3 DO_SETUID }, { "cdup" , 0, &conn::cmd_cdup, 3 DO_SETUID }, { "rest ", 1, &conn::cmd_rest, 3 DO_SETUID }, { "list" , 0, &conn::cmd_list, 3 DO_SETUID }, { "nlst" , 0, &conn::cmd_nlst, 3 DO_SETUID }, { "type ", 1, &conn::cmd_type, 3 DO_SETUID }, { "mode ", 1, &conn::cmd_mode, 3 DO_SETUID }, { "stru ", 1, &conn::cmd_stru, 3 DO_SETUID }, { "size ", 1, &conn::cmd_size, 3 DO_SETUID }, { "mdtm ", 1, &conn::cmd_mdtm, 3 DO_SETUID }, { "abor" , 0, &conn::cmd_abor, 3 DO_SETUID }, { "dele ", 1, &conn::cmd_dele, 3 DO_SETUID }, { "rnfr ", 1, &conn::cmd_rnfr, 3 DO_SETUID }, { "rnto ", 1, &conn::cmd_rnto, 3 DO_SETUID }, { "mkd " , 1, &conn::cmd_mkd, 3 DO_SETUID }, { "rmd " , 1, &conn::cmd_rmd, 3 DO_SETUID }, { "allo ", 1, &conn::cmd_allo, 3 DO_SETUID }, { "stat" , 0, &conn::cmd_stat, 0 NO_SETUID }, { "noop" , 0, &conn::cmd_noop, 0 DO_SETUID }, { "syst" , 0, &conn::cmd_syst, 0 DO_SETUID }, { "help" , 0, &conn::cmd_help, 0 NO_SETUID }, { "quit" , 0, &conn::cmd_quit, 0 DO_SETUID }, { "rein" , 0, &conn::cmd_rein, 0 DO_SETUID }, /* deprecated forms */ { "xcup" , 0, &conn::cmd_cdup, 3 DO_SETUID }, { "xcwd ", 1, &conn::cmd_cwd, 3 DO_SETUID }, { "xpwd" , 0, &conn::cmd_pwd, 3 DO_SETUID }, { "xmkd ", 1, &conn::cmd_mkd, 3 DO_SETUID }, { "xrmd ", 1, &conn::cmd_rmd, 3 DO_SETUID }, #if WANT_UPLOAD { "stor ", 1, &conn::cmd_stor, 3 DO_SETUID }, { "appe ", 1, &conn::cmd_appe, 3 DO_SETUID }, #endif #if DOING_PROFILING #warning Use DOING_PROFILING with caution, and NEVER on a production server! :-) { "exit", 0, &conn::cmd_exit, 0 NO_SETUID }, #endif { "" , 0, NULL, 0 NO_SETUID } }; /* * init(): * Allocates a new control connection (type `struct conn'), * initializes it, and adds it to the linked list. The connection * operates on the socket SOCK. */ struct conn *conn::init(FtpServer *container, const int sock) { const unsigned int one = 1; struct conn *c = this; if (c == NULL) return c; if (sock != -1) { ioctl(sock, FIONBIO, &one); printf("poller.add(fd %d, c %p)\n", sock, c); if (poller.add(sock, c, POLLIN) != 0) { /* temp unavail */ send(sock, "230 Server too busy, please try again later.\r\n", 46, 0); close(sock); return NULL; } add_to_linked_list((struct list_element *)container->m_first_conn, (struct list_element *)c); } else { /* this is the bogus head of the list */ c->c_next_conn = NULL; c->c_prev_conn = NULL; } c->c_transfer = NULL; c->c_sock = sock; c->c_buf_len = c->c_auth = c->c_rest_pos = 0; #if WANT_ASCII c->c_ascii_mode = 0; #endif /* * equals: * strcpy(c->c_curr_dir, "/"); * strcpy(c->c_last_cmd, ""); * strcpy(c->c_rename_from, "") */ c->c_curr_dir[0] = '/'; #if WANT_FULLSCREEN c->c_curr_dir[1] = c->c_last_cmd[0] = c->c_rename_from[0] = '\0'; #else c->c_curr_dir[1] = c->c_rename_from[0] = '\0'; #endif time(&(c->c_last_transfer)); /*list_clients();*/ return c; } /* * shutdown(): * Destroy a control connection, remove it from the linked * list, and clean up after it. */ void conn::shutdown() { poller.del(c_sock); close(c_sock); destroy_ftran(c_transfer); remove_from_linked_list((struct list_element *)this); /* FIXME: memory leak */ /* add_to_linked_list(things-to-reap, this); */ } /* * do_chdir(): Does a chdir() to newd on c, staying inside the * limits of root_dir. Use this instead of a chdir() whenever * you can, and possibly even when you can't :-) * * This command quirks around some problems in the rest of * the code (namely translate_path()), so a blank newdir is * interpreted as the root directory. */ int conn::do_chdir(const char * const newd) { char chd[512], temp[512]; TRAP_ERROR(chdir(c_curr_dir) == -1, 550, return -1); /* handle `absolute' paths */ if (newd[0] == '/' || newd[0] == '\0') { strcpy(temp, c_root_dir); /* * is this the root directory? if not, remove the trailing `/' * and concatenate the new directory on */ if (newd[1] != '\0' && newd[0] != '\0') { temp[strlen(temp) - 1] = 0; strcat(temp, newd); } } else { strcpy(temp, newd); } #if WANT_NONROOT if (nr_check_permission(c_uid, temp, 1, 1, NULL) == -1) { numeric(550, "Permission denied"); return -1; } #endif TRAP_ERROR(chdir(temp) == -1, 550, return -1); getcwd(chd, 254); if (chd[strlen(chd) - 1] != '/') { strcat(chd, "/"); } if (strncmp(chd, c_root_dir, strlen(c_root_dir)) != 0) { numeric(550, "No such file or directory."); return -1; } return 0; } /* * cmd_user(): Handles the USER command, and does most of the initial * authentication work. User names are limited to 16 * characters, by force... */ int conn::cmd_user(void) { strncpy(c_username, c_recv_buf, 16); c_username[16] = 0; if (strcasecmp(c_username, "anonymous") == 0) { strcpy(c_username, "ftp"); } if (strcasecmp(c_username, "ftp") == 0) { numeric(331, "Login OK, send password (your e-mail)."); c_auth = 1; } else { numeric(331, "Password required for %s.", c_username); c_auth = 2; } return 1; } /* * cmd_pass(): Handles the PASS command, and checks the password. * This function is rather long and complicated, mostly * because there are so many ways of doing users * (including my nonroot system) out there... And we * don't even support PAM or real shadow passwords (with * expiry etc) yet... */ int conn::cmd_pass(void) { #if WANT_NONROOT c_auth = nr_userinfo(c_username, &c_uid, c_curr_dir, c_root_dir, c_recv_buf); #else /* !WANT_NONROOT */ #if WANT_SHADOW && HAVE_SHADOW_H struct spwd *s; #endif struct passwd *p; p = getpwnam(c_username); #if WANT_SHADOW && HAVE_SHADOW_H s = getspnam(c_username); #endif if (p == NULL) { c_auth = 0; } else { c_uid = p->pw_uid; strncpy(c_curr_dir, p->pw_dir, 254); c_curr_dir[254] = 0; } if (c_auth == 1) { if (c_curr_dir[strlen(c_curr_dir) - 1] != '/') { strcat(c_curr_dir, "/"); } strcpy(c_root_dir, c_curr_dir); c_auth = 3; } else if (c_auth != 0) { strcpy(c_root_dir, "/"); if (strcmp(crypt(c_recv_buf, p->pw_passwd), p->pw_passwd) != 0 #if WANT_SHADOW && HAVE_SHADOW_H && (s == NULL || strcmp(crypt(c_recv_buf, s->sp_pwdp), s->sp_pwdp) != 0) #endif ) { c_auth = 0; } else { c_auth = 3; } } #endif /* !WANT_NONROOT */ /* root should not be allowed to FTP */ if (c_uid == 0) { c_auth = 0; } if (c_auth == 0) { numeric(530, "Login incorrect."); } else { #if WANT_MESSAGE chdir(c_curr_dir); dump_file(this, 230, "welcome.msg"); #endif numeric(230, "User logged in."); } return 1; } /* * cmd_acct(): Handle (ignore) the ACCT command. I don't see how we * could make use of this command... wu-ftpd doesn't, either. * However, wu-ftpd (at least the version I have here) uses * 502, which isn't a legal error code according to RFC959. * 202, on the other hand, is, and seems to be applicable. * * I'm unsure if this one should require setuid or not, but * I feel that the RFC959 intention is having it _before_ * USER/PASS. Therefore, this one runs with root privilegies :-) */ int conn::cmd_acct(void) { numeric(202, "ACCT ignored OK -- not applicable on this system."); return 1; } /* * cmd_port(): Handles the PORT command, and sets up the data socket. * Making a brief uid=0 (root) appearance (to bind the socket) -- * I feel it's safer that way (instead of running as root * the whole way), in case there are some weird overflows * somewhere. */ int conn::cmd_port(void) { short int a0, a1, a2, a3, p0, p1; int i, sock, err; struct ftran *f; struct sockaddr_in sin; if ((c_transfer != NULL) && (c_transfer->state >= 4)) { numeric(500, "Sorry, only one transfer at a time."); return 1; } sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); TRAP_ERROR(sock == -1, 500, return 1); destroy_ftran(c_transfer); c_transfer = f = alloc_new_ftran(sock, this); i = sscanf(c_recv_buf, "%3hu,%3hu,%3hu,%3hu,%3hu,%3hu", &a0, &a1, &a2, &a3, &p0, &p1); if (i < 6) { numeric(501, "Parse error."); } else { const int one = 1; socklen_t tmp; /* bind to own address, port 20 (FTP data) */ tmp = sizeof(sin); err = getsockname(c_sock, (struct sockaddr *)&sin, &tmp); TRAP_ERROR(err == -1, 500, return 1); sin.sin_port = FTP_PORT - 1; numeric(200, "PORT command OK."); /* note that bind() might well fail, so we don't error check */ #if !WANT_NONROOT /* need root privilegies for a short while */ seteuid(getuid()); #endif bind(sock, (struct sockaddr *)&sin, sizeof(sin)); #if !WANT_NONROOT seteuid(c_uid); #endif f->sin.sin_family = AF_INET; f->sin.sin_addr.s_addr = htonl( ((unsigned char)(a0) << 24) + ((unsigned char)(a1) << 16) + ((unsigned char)(a2) << 8) + ((unsigned char)(a3) )); f->sin.sin_port = htons( ((unsigned char)(p0) << 8) + ((unsigned char)(p1) )); f->sock = sock; f->state = 3; i = 1; ioctl(f->sock, FIONBIO, &one); } return 1; } /* * cmd_pasv(): Handles the PASV command, and sets up the data socket. * Uses port numbers > 1024, since it doesn't run as root. */ int conn::cmd_pasv(void) { struct ftran *f; socklen_t tmp; int sock, err; unsigned int one = 1; struct sockaddr_in addr; if ((c_transfer != NULL) && (c_transfer->state >= 4)) { numeric(503, "Sorry, only one transfer at once."); return 1; } destroy_ftran(c_transfer); sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); TRAP_ERROR(sock == -1, 500, return 1); c_transfer = f = alloc_new_ftran(sock, this); err = poller.add(sock, f, POLLIN); TRAP_ERROR(err != 0, 501, return 1); ioctl(sock, FIONBIO, &one); /* setup socket */ tmp = sizeof(addr); err = getsockname(c_sock, (struct sockaddr *)&addr, &tmp); TRAP_ERROR(err == -1, 500, return 1); addr.sin_port = 0; /* let the system choose */ err = bind(sock, (struct sockaddr *)&addr, sizeof(struct sockaddr)); TRAP_ERROR(err == -1, 500, return 1); tmp = sizeof(addr); err = getsockname(sock, (struct sockaddr *)&addr, &tmp); TRAP_ERROR(err == -1, 500, return 1); err = listen(f->sock, 1); TRAP_ERROR(err == -1, 500, return 1); f->state = 1; numeric(227, "Entering passive mode (%u,%u,%u,%u,%u,%u)", (htonl(addr.sin_addr.s_addr) & 0xff000000) >> 24, (htonl(addr.sin_addr.s_addr) & 0x00ff0000) >> 16, (htonl(addr.sin_addr.s_addr) & 0x0000ff00) >> 8, (htonl(addr.sin_addr.s_addr) & 0x000000ff), (htons(addr.sin_port) & 0xff00) >> 8, (htons(addr.sin_port) & 0x00ff)); return 1; } /* * cmd_pwd(): Handles PWD command (print working directory). * * Note that if somebody contacts you with the message `the server * says curr_dir() is outside root_dir()', you should fix your * /betaftpd.users file, if you use nonroot. If not, it's a bug. * Try to get it _reproducible_, and mail it to me. */ int conn::cmd_pwd(void) { char temp[512], *cdir = NULL; cdir = do_pwd(temp, c_curr_dir); if (cdir != NULL) { numeric(257, "\"%s\" is current working directory.", cdir); } return 1; } /* * do_pwd(): Translates an absolute path to a path suitable for viewing * to the user (ie. removes the root_dir, and removes a trailing * slash if it exists). Note that the retbuf is only used as a * storage place -- the pointer to the right place within retbuf * is _returned_. */ char *conn::do_pwd(char * const retbuf, const char * const dir) { char *cdir = NULL; strcpy(retbuf, dir); if (strncmp(retbuf, c_root_dir, strlen(c_root_dir)) != 0) { numeric(550, "curr_dir is outside root_dir, please contact site administrator."); return NULL; } cdir = retbuf + strlen(c_root_dir) - 1; if (cdir[strlen(cdir) - 1] == '/' && strlen(cdir) > 1) { cdir[strlen(cdir) - 1] = 0; } else if (strlen(cdir) == 0) { strcpy(cdir, "/"); } return cdir; } /* * cmd_cwd(): Handles CWD command (change working directory). Uses * cmd_cwd_internal() (see below). */ int conn::cmd_cwd(void) { cmd_cwd_internal(c_recv_buf); return 1; } /* * cmd_cdup(): Handles a CDUP command (identical to `CWD ..'). Note that * RFC959 gives two different response codes (250 and 200) -- * 250 is the same as CWD gives, which sounds logical to me. * wu-ftpd uses it as well. * * Note that using a CDUP to try to get outside root_dir returns * an error, instead of just staying in the root directory (as * the OS and thus wu-ftpd does). */ int conn::cmd_cdup(void) { cmd_cwd_internal(".."); return 1; } /* * cmd_cwd_internal(): * Does the work for CWD and CDUP (modularized to save some * space and have clearer code). Mostly, it just uses do_chdir(), * and sees where that takes us. It adds a trailing slash if needed. */ void conn::cmd_cwd_internal(const char * const newd) { if (do_chdir(newd) != -1) { int i; getcwd(c_curr_dir, 254); i = strlen(c_curr_dir); if (c_curr_dir[i - 1] != '/') { c_curr_dir[i++] = '/'; c_curr_dir[i] = '\0'; } #if WANT_MESSAGE dump_file(this, 250, ".message"); list_readmes(this); #endif numeric(250, "CWD successful."); } } /* * cmd_rest(): Handles the REST command. All it does is tell the file * sending functions to start at the correct number. We should * perhaps add some better error checking to this? */ int conn::cmd_rest(void) { c_rest_pos = abs(atoi(c_recv_buf)); numeric(350, "Setting resume at %u bytes.", c_rest_pos); return 1; } /* * cmd_retr(): Handles the RETR command. This command doesn't send the * file, but it opens it and tells the socket handling code * to check for activity on the data socket. When the * connection occurs (or succeeds, if we're using PORT mode), * the actual file transfer begins. */ int conn::cmd_retr(void) { struct ftran *f = c_transfer; if ((f == NULL) || ((f->state != 1) && (f->state != 3))) { numeric(425, "No data connection set up; please use PASV or PORT."); return 1; } #if WANT_ASCII if ((c_rest_pos > 0) && (c_ascii_mode == 1)) { numeric(500, "Cannot resume while in ASCII mode."); return 1; } #endif f->local_file = do_openfile(c_recv_buf, f->filename, O_RDONLY #if WANT_NONROOT , 4 #endif ); f->dir_listing = 0; assert(f->owner == this); if (f->local_file == -1) { numeric(550, strerror(errno)); destroy_ftran(f); } else if (f->local_file == -2) { f->local_file = -1; destroy_ftran(f); } else { #if WANT_UPLOAD f->upload = 0; #endif prepare_for_transfer(f); } return 1; } #if WANT_UPLOAD /* * cmd_stor(): Handles the STOR command (upload file). Pushes the * work down to do_store(), below. */ int conn::cmd_stor(void) { do_store(0); return 1; } /* * cmd_appe(): Handles the APPE command (append to file). Pushes * the work down to do_store(), below. */ int conn::cmd_appe(void) { do_store(1); return 1; } /* * do_store(): Initiate an upload. Most of the comments to do_retr() * (above) apply to this one as well. */ void conn::do_store(const int append) { struct ftran *f = c_transfer; if ((f == NULL) || ((f->state != 1) && (f->state != 3))) { numeric(425, "No data connection set up; please use PASV or PORT."); return; } #if WANT_ASCII if ((c_rest_pos > 0) && (c_ascii_mode == 1)) { numeric(500, "Cannot resume while in ASCII mode."); return; } #endif f->local_file = do_openfile(this, c_recv_buf, f->filename, O_WRONLY | O_CREAT | ((append || c_rest_pos > 0) ? 0 : O_TRUNC) #if WANT_NONROOT , 2 #endif ); f->dir_listing = 0; if (f->local_file == -1) { numeric(f->owner, 550, strerror(errno)); } else if (f->local_file == -2) { f->local_file = -1; } else { f->upload = 1; f->append = append; #if WANT_ASCII f->ascii_mode = c_ascii_mode; #endif prepare_for_transfer(f); } } #endif /* WANT_UPLOAD */ /* * cmd_size(): Handle the SIZE command -- returns the size of a * file. Note that this command is not part of RFC959, * and thus there is no clear specification (except * for some ftpext documents, which we try to follow * as closely as we can). BetaFTPD deviates from wu-ftpd * in that it lets you check the `size' of directories * as well (instead of giving 550). This is _not_ the * size of all the files in the directory, rather how * much space the directory inode uses. */ int conn::cmd_size(void) { #if WANT_ASCII if (c_ascii_mode) { numeric(550, "SIZE not available in ASCII mode."); return 1; } #endif { const char * const fname = translate_path(c_recv_buf); struct stat buf; TRAP_ERROR(fname == NULL || lstat(fname, &buf) == -1, 550, return 1); numeric(213, "%lu", (unsigned long)(buf.st_size)); return 1; } } /* * cmd_mdtm(): Handle the MDTM command -- returns the modification * date/time of a file. See the comments on cmd_size(), * above. */ int conn::cmd_mdtm(void) { const char * const fname = translate_path(c_recv_buf); struct stat buf; struct tm *m; TRAP_ERROR(fname == NULL || lstat(fname, &buf) == -1, 550, return 1); m = gmtime(&(buf.st_mtime)); /* at least wu-ftpd does it in GMT */ numeric(213, "%u%02u%02u%02u%02u%02u", m->tm_year + 1900, m->tm_mon + 1, m->tm_mday, m->tm_hour, m->tm_min, m->tm_sec); return 1; } /* * cmd_abor(): Handle the ABOR command (abort a file transfer). This should * be clean enough, but isn't tested extensively. */ int conn::cmd_abor(void) { if (c_transfer != NULL) { numeric(426, "File transfer aborted."); destroy_ftran(c_transfer); } numeric(226, "ABOR command processed OK."); return 1; } /* * cmd_dele(): Handle the DELE command (delete a file). */ int conn::cmd_dele(void) { const char * const fname = translate_path(c_recv_buf); TRAP_ERROR(fname == NULL || unlink(fname) == -1, 550, return 1); numeric(250, "File deleted OK."); return 1; } /* * cmd_rnfr(): Handle the RNFR command (take a filename to rename from). */ int conn::cmd_rnfr(void) { const char * const fname = translate_path(c_recv_buf); char cwd[256]; struct stat buf; c_rename_from[0] = '\0'; if (fname == NULL) return 1; getcwd(cwd, 256); snprintf(c_rename_from, 256, "%s/%s", cwd, fname); /* Just check that the file exists. */ TRAP_ERROR(lstat(c_rename_from, &buf) == -1, 550, c_rename_from[0] = '\0'; return 1); numeric(350, "File exists, send RNTO."); return 1; } /* * cmd_rnto(): Handle the RNTO command (do the actual renaming). */ int conn::cmd_rnto(void) { const char * const fname = translate_path(c_recv_buf); if (fname == NULL) return 1; if (c_rename_from[0] == '\0') { numeric(503, "Please send RNFR first."); return 1; } TRAP_ERROR(rename(c_rename_from, fname) == -1, 550, c_rename_from[0] = '\0'; return 1); c_rename_from[0] = '\0'; numeric(250, "File renamed successfully."); return 1; } /* * cmd_mkd(): Handle the MKD/XMKD command (create a new directory). * RFC959 is not clear on the error codes for this command -- * one place, 521 is cited as the correct error, but is * mentioned nowhere else. Different FTP servers differ here * as well. Thus, I've followed what appears to be the intention * (having `analogous' errors with STOR), and use 550 instead. * * Making directories is probably the topic covered most * extensively by RFC959 (and in the most confusing way as * well). I try to follow the conventions, but it isn't always * easy :-) (This code isn't quite easy to understand, because * temp2 is used twice, in two different roles.) */ int conn::cmd_mkd(void) { const char * const fname = translate_path(c_recv_buf); char temp[512], temp2[1024], *cdir; int i, j; TRAP_ERROR(fname == NULL || mkdir(fname, 0755) == -1, 550, return 1); chdir(fname); getcwd(temp2, 512); cdir = do_pwd(temp, temp2); /* double the quotes in the output */ for (i = 0, j = 0; i <= strlen(cdir); i++, j++) { temp2[j] = cdir[i]; if (cdir[i] == '"') { temp2[++j] = '"'; } } numeric(257, "\"%s\" created.", temp2); return 1; } /* * cmd_rmd(): Handle the RMD/XRMD command. Works just like DELE, only for * directories. */ int conn::cmd_rmd(void) { const char * const fname = translate_path(c_recv_buf); TRAP_ERROR(fname == NULL || rmdir(fname) == -1, 550, return 1); numeric(250, "Directory deleted."); return 1; } /* * cmd_allo(): Handle the ALLO command. The command does not do anything, except * sit around and play compliant. Some Windows FTP servers (Serv-U, * for instance), verifies that there is enough space on the disk, * but since we have no idea on what the filesystem will be stored on, * we just ignore the command. * * We could theoretically use this information to give more information * to the full-screen mode, but close to no FTP clients send this * command, and it would touch too much code. */ int conn::cmd_allo(void) { numeric(202, "No storage allocation necessary."); return 1; } /* * cmd_stat(): Handle the STAT command. Please see README for more details. * Note that this command is run with euid=root, since it has * to be able to run before USER. * * Note that we need to bypass numeric(), to get a multi-line * reply. */ #if WANT_STAT char conn_state[5][27] = { "Not logged in", "Waiting for e-mail address", "Waiting for password", "Logged in", "Waiting for password", /* actually non-existant user */ }; char ftran_state[6][42] = { "Not initialized", "Decided PASV address/port", "Waiting on PASV socket", "Got PORT address/port", "Connecting on PORT address/port", "Transferring file (or connecting on PORT)" }; #endif int conn::cmd_stat(void) { #if WANT_STAT char buf[1024]; int i, err; struct ftran *f = c_transfer; snprintf(buf, 1024, "211- FTP server status:\r\n" " dkBetaFTPD version " VERSION " (http://betaftpd.sf.net)\r\n" " Connected to %s\r\n" " Control connection state: %s\r\n" #if WANT_ASCII " TYPE: %s; STRUcture: File; transfer MODE: Stream\r\n" #else " TYPE: Image; STRUcture: File; transfer MODE: Stream\r\n" #endif " Data connection state: %s\r\n" "211 End of status\r\n", inet_ntoa(((struct sockaddr_in *)(&(c_addr)))->sin_addr), conn_state[c_auth], #if WANT_ASCII (c_ascii_mode == 1) ? "ASCII, FORM: Nonprint" : "Image", #endif (f) ? ftran_state[f->state] : ftran_state[0]); i = strlen(buf); err = send(c_sock, buf, i, 0); if (err == -1 && errno == EPIPE) { shutdown(); return 0; } #else numeric(502, "STAT command disabled for security reasons."); #endif return 1; } #if HAVE_MMAP /* * _mwrite(): This define is for mmap-listing. It works as a write() * (not in parameter, but in function), and is used in * cmd_list() and cmd_nlst() only. * * Note that this function returns the new position in the * `file'. The caller is expected to send this information * back in `pos' at the next call to _mwrite(). */ int _mwrite(const char * const buf, const struct ftran * const f, const int pos, const int count, const int size) { if (pos + count >= size) return size; /* out of space */ memcpy(f->file_data + pos, buf, count); return pos + count; } #endif /* * mwrite: This is a short_hand define, making calls to _mwrite() very * similiar to calls to write(). It works both with and without * mmap(). */ #if HAVE_MMAP #define mwrite(buf, count) pos = _mwrite((buf), (f), (pos), (count), (size)); #else #define mwrite(buf, count) write(f->local_file, buf, count); #endif /* * long_listing(): * Formats output in `ls -l' style. It returns one line for the * file PATHNAME, and returns it in retbuf. Setting do_classify * to nonzero has the same effect as `ls -F'. * * This command is so long, because simply there is so much to * be done. GNU ls has some extra functions, but it's close to * 3000 lines too... */ int long_listing(char * const retbuf, const char * const pathname, const int do_classify) { int i, year; char newd[512], temp[1026]; struct stat buf; struct tm *t; time_t now; char username[17], groupname[17]; time(&now); year = localtime(&now)->tm_year; { #if !WANT_NONROOT struct passwd *p; struct group *g; #endif if (lstat(pathname, &buf) == -1) return 0; #if WANT_NONROOT strcpy(username, nr_get_uname(buf.st_uid)); strcpy(groupname, nr_get_gname(buf.st_gid)); #else p = getpwuid(buf.st_uid); if (p != NULL) { strncpy(username, p->pw_name, 16); username[16] = 0; } else { snprintf(username, 16, "%u", buf.st_uid); } g = getgrgid(buf.st_gid); if (g != NULL) { strncpy(groupname, g->gr_name, 16); groupname[16] = 0; } else { snprintf(groupname, 16, "%u", buf.st_gid); } #endif } /* * This POSIX approximation is based on GNU ls code (and obfuscated * a bit...), to be compatible with `real' ls implementations. */ t = localtime(&(buf.st_mtime)); strftime(newd, 512, ((now > buf.st_mtime + 6L * 30L * 24L * 60L * 60L) || (now < buf.st_mtime - 60L * 60L)) ? "%b %e %Y" : "%b %e %H:%M", t); { #if WANT_NONROOT char rights[16]; if (nr_check_permission(0, pathname, 0, (S_ISDIR(buf.st_mode)), rights) == -1) { /* no permission to even see this file */ return 0; } snprintf(temp, 1024, "%c%s %3u %-8s %-8s %8lu %12s %s\r\n", #else snprintf(temp, 1024, "%c%c%c%c%c%c%c%c%c%c %3u %-8s %-8s %8lu %12s %s", #endif decode_mode(buf.st_mode), #if WANT_NONROOT rights, #else (buf.st_mode & S_IRUSR) ? 'r' : '-', (buf.st_mode & S_IWUSR) ? 'w' : '-', (buf.st_mode & S_IXUSR) ? ((buf.st_mode & S_ISUID) ? 's' : 'x') : '-', (buf.st_mode & S_IRGRP) ? 'r' : '-', (buf.st_mode & S_IWGRP) ? 'w' : '-', (buf.st_mode & S_IXGRP) ? ((buf.st_mode & S_ISGID) ? 's' : 'x') : '-', (buf.st_mode & S_IROTH) ? 'r' : '-', (buf.st_mode & S_IWOTH) ? 'w' : '-', (buf.st_mode & S_IXOTH) ? ((buf.st_mode & S_ISVTX) ? 't' : 'x') : '-', #endif buf.st_nlink, username, groupname, (unsigned long)(buf.st_size), newd, pathname); i = strlen(temp); #if 0 /* * vim needs this extra character for some reason... It's too * bad I'll have to do it this way, but syntax colouring * that works properly is almost a `must' for me :-) */ ) #endif /* add an extra classification `sign' if we got -F */ if (do_classify) { int len = strlen(temp); temp[len] = classify(buf.st_mode); temp[len + 1] = '\0'; } } strcpy(retbuf, temp); return 1; } /* * cmd_list(): Handles the LIST command (directory listing). Does a * long listing (of type `ls -l'). The listing work is * done by do_listing(), below. */ int conn::cmd_list(void) { struct list_options lo; lo.recursive = 0; lo.long_listing = 1; lo.classify = 0; do_listing(&lo); return 1; } /* * cmd_nlst(): Handles the NLST command (plain directory listing). * Does a plain listing (no dates etc.), unless overridden * by the `-l' or `-L' flag (case insensitivity because most * FTP clients don't have a clue about what they send out). * The listing work is done by do_listing(), below. */ int conn::cmd_nlst(void) { struct list_options lo; lo.recursive = 0; lo.long_listing = 0; lo.classify = 0; do_listing(&lo); return 1; } /* * do_listing(): * Prepares any listing buffers, temp files, etc., before * pushing the work one step further :-) * * If the directory listing cache is enabled, the cache * is checked first, to see if we still have a valid entry. */ void conn::do_listing(struct list_options * const lo) { int i; char *ptr; #if HAVE_MMAP int size; #endif struct ftran * const f = c_transfer; #if WANT_DCACHE char cwd[256]; #endif #if WANT_NONROOT #warning No nonroot checking for list_core() yet #endif i = prepare_for_listing(&ptr, lo); if (i == -1) { destroy_ftran(c_transfer); return; } #if WANT_DCACHE getcwd(cwd, 256); #endif #if HAVE_MMAP strcpy(f->filename, "(directory listing)"); #endif #if WANT_DCACHE { struct dcache *d = find_dcache(cwd, ptr, lo); if (d != NULL) { d->use_count++; f->dir_cache = d; f->file_data = d->dir_data; f->size = d->dir_size; f->dir_listing = 1; f->pos = 0; prepare_for_transfer(f); return; } } #endif #if HAVE_MMAP { int num_files = get_num_files(ptr, lo); if (num_files == -1) return; size = num_files * 160; f->file_data = (char *)malloc(size + 1); TRAP_ERROR(f->file_data == NULL, 550, return); list_core(ptr, "", lo, size, 0); } #else list_core(ptr, "", lo); #endif #if WANT_DCACHE populate_dcache(f, cwd, ptr, lo); #endif #if HAVE_MMAP f->pos = 0; #endif prepare_for_transfer(f); } /* * get_num_files(): * Get the number of files in PATHNAME (optionally matching * a pattern). Note that c is needed for TRAP_ERROR. */ int conn::get_num_files(const char * const pathname, struct list_options * const lo) { int num_files; glob_t pglob; /* * glob() fails to set errno correctly, so we simply guess on * `permission denied'... The others are far less likely to happen. */ switch (glob(pathname, 0, NULL, &pglob)) { #ifdef GLOB_NOMATCH case GLOB_NOMATCH: return 0; #endif case 0: num_files = pglob.gl_pathc; break; default: numeric(550, strerror(EACCES)); return -1; } if (lo->recursive) { int i; for (i = 0; i < pglob.gl_pathc; i++) { char *temp = pglob.gl_pathv[i]; struct stat buf; lstat(temp, &buf); if (S_ISDIR(buf.st_mode)) { chdir(temp); num_files += get_num_files("*", lo); chdir(".."); } } } globfree(&pglob); return num_files; } /* * list_core(): Enumerate all the files in PATHNAME, and formats them * according to list_options (calling format functions if * required). * * Note that we don't do any realloc() yet, so if your * _average_ file name length is over a certain size (a little * under 80 for long listings, and a little under 160 for * short listings), the list will be truncated. Fix... * * The return value only makes sense if mmap()'ing, since it * returns the number of bytes written into the buffer. * * This function is rather long. */ int conn::list_core(const char * const pathname, char * const disp_pathname, struct list_options * const lo #if HAVE_MMAP , const int size, int pos #endif ) { int i; glob_t pglob; struct ftran * const f = c_transfer; /* * glob() fails to set errno correctly, so we simply guess on * `permission denied'... The others are far less likely to happen. */ switch (glob(pathname, GLOB_MARK, NULL, &pglob)) { case 0: #ifdef GLOB_NOMATCH case GLOB_NOMATCH: #endif break; /* note: break, not return */ default: numeric(550, strerror(EACCES)); #if HAVE_MMAP return pos; #else return 0; #endif } if (lo->recursive) { if (disp_pathname[0] == '\0') { mwrite(".:\r\n", 4); } else { char temp[1024]; int i; snprintf(temp, 1024, "%s:\r\n", disp_pathname); i = strlen(temp); mwrite(temp, i); } } if (lo->long_listing) { /* FIX: we may get too high total number if we are running nonroot! */ struct stat buf; long unsigned int total = 0; char temp[1024]; for (i = 0; i < pglob.gl_pathc; i++) { if (lstat(pglob.gl_pathv[i], &buf) != -1) { total += buf.st_blocks; } } snprintf(temp, 1024, "total %lu\r\n", total >> 1); i = strlen(temp); mwrite(temp, i); } for (i = 0; i < pglob.gl_pathc; i++) { char * const temp = pglob.gl_pathv[i]; char buf[2048]; /* strip `/' away from the pathname -- add it later if -F */ { int len = strlen(temp); if (temp[len - 1] == '/') { temp[len - 1] = '\0'; } } if (lo->long_listing) { if (long_listing(buf, temp, lo->classify) == 0) continue; } else { strcpy(buf, temp); if (lo->classify) { struct stat statbuf; if (lstat(buf, &statbuf) != -1) { const int len = strlen(buf); buf[len] = classify(statbuf.st_mode); buf[len + 1] = 0; } } } mwrite(buf, strlen(buf)); mwrite("\r\n", 2); } /* * If recursion is on, dive into any subdirectories now -- note * that each entry is stat()'ed twice, hopefully the OS will manage, * and we've got our own dcache anyways -- this could be fixed at * the expense of some memory, consider for later inclusion. */ if (lo->recursive) { for (i = 0; i < pglob.gl_pathc; i++) { struct stat buf; const char * const temp = pglob.gl_pathv[i]; /* don't dive into `.' or `..' */ if (lstat(temp, &buf) != -1 && S_ISDIR(buf.st_mode) && (temp[0] != '.' || (temp[1] != '.' && temp[1] != '\0'))) { char tmp2[1024]; mwrite("\r\n", 2); /* attach the pathname to the end of the displayed path */ if (disp_pathname[0] == '\0') { snprintf(tmp2, 1024, "%s", temp); } else { snprintf(tmp2, 1024, "%s/%s", disp_pathname, temp); } chdir(temp); pos = list_core("*", tmp2, lo, #if HAVE_MMAP size, pos); #endif chdir(".."); } } } #if HAVE_MMAP f->size = pos; #else lseek(f->local_file, 0, SEEK_SET); #endif globfree(&pglob); #if HAVE_MMAP return pos; #else return 0; #endif } /* * cmd_noop(): Handles the NOOP command. Does nothing, doesn't even * reset the timeout. */ int conn::cmd_noop(void) { numeric(200, "NOOP command successful."); return 1; } /* * cmd_syst(): Handles the SYST command. Returns the system identification. */ int conn::cmd_syst(void) { numeric(215, "UNIX Type: L%u", NBBY); return 1; } /* * cmd_type(): Handles the TYPE command. */ int conn::cmd_type(void) { #if WANT_ASCII c_recv_buf[0] &= (255-32); /* convert to upper case */ if (c_recv_buf[0] == 'A') { c_ascii_mode = 1; numeric(200, "Type is ASCII."); } else if (c_recv_buf[0] == 'I') { c_ascii_mode = 0; numeric(200, "Type is IMAGE."); } else { numeric(504, "Unknown type."); } #else numeric(200, "TYPE ignored (always I)"); #endif return 1; } /* * cmd_mode(): Handles the MODE command. Only stream mode is supported. */ int conn::cmd_mode(void) { c_recv_buf[0] &= (255-32); /* convert to upper case */ if (c_recv_buf[0] == 'S') { numeric(200, "Mode is STREAM."); } else { numeric(504, "Unknown mode."); } return 1; } /* * cmd_stru(): Handles the STRU command. Only file mode is supported. */ int conn::cmd_stru(void) { c_recv_buf[0] &= (255-32); /* convert to upper case */ if (c_recv_buf[0] == 'F') { numeric(200, "Structure is FILE."); } else { numeric(504, "Unknown structure."); } return 1; } /* * cmd_help(): Handle the HELP command. I'm sorry, but I'm unwilling * to use a lot of space to explain the RFCs in such a message, * and BetaFTPD doesn't have any special things that should * be noted anywhere. Thus, this message is close to empty. I * feel that a 5xx entry would have been better, but that is * disallowed. * * As with ACCT, this command is supposed to be executed from * everywhere, so we have to run without setuid. I don't like * it, but at the same time I have to idea what could go * wrong... * * Perhaps I should make this message sound a little less * like an error, since the error code is intended for helpful * messages? :-) */ int conn::cmd_help(void) { numeric(414, "Sorry, no detailed help; use standard FTP commands."); return 1; } /* * cmd_quit(): Handles the QUIT command, which shuts down the control * and data sockets. */ int conn::cmd_quit(void) { numeric(221, "Have a nice day!"); shutdown(); return 0; } /* * cmd_rein(): Handle the REIN command, which does close to a full reset * of the connection. Much of the code here is intentionally * copied directly from init() -- perhaps we should * modularize this? */ int conn::cmd_rein(void) { destroy_ftran(c_transfer); c_buf_len = c_auth = c_rest_pos = 0; /* equals: strcpy(c_curr_dir, "/") ; strcpy(c_last_cmd, ""); */ c_curr_dir[0] = '/'; #if WANT_FULLSCREEN c_curr_dir[1] = c_last_cmd[0] = '\0'; #else c_curr_dir[1] = '\0'; #endif time(&(c_last_transfer)); numeric(220, "BetaFTPD " VERSION " ready."); return 1; } #if DOING_PROFILING /* * cmd_exit(): Handles the EXIT command, my own `extension' to the * FTP protocol... IMPORTANT: Only to be used for profiling * purposes!! (It's needed to get some profiling data out * of the server after compiling it with -pg, since such data * is only written on a clear exit()). Any user (even those * not logged in) can issue an EXIT, and make the server shut * down without clearing any sockets etc. In other words: * Don't use it on a production site. */ int conn::cmd_exit(void) { while (first_conn->c_next_conn) { shutdown(first_conn->c_next_conn); delete first_conn->c_next_conn; } exit(0); } #endif /* * parse_command(): * Gets a command from c_recv_buf, determines which command * it is, sets proper effective user-ID and calls the command * handler. Finally, it cleans up. * * To me, this command seems optimizable, but I'm not really * sure where :-) */ void conn::parse_command() { int cmlen; if (this == NULL) return; /* strip any leading non-ASCII characters (including CR/LFs) */ while (c_buf_len > 0 && (c_recv_buf[0] < 'a' || c_recv_buf[0] > 'z') && (c_recv_buf[0] < 'A' || c_recv_buf[0] > 'Z')) { remove_bytes(1); /* not good */ } /* scan, searching for CR or LF */ cmlen = strcspn(c_recv_buf, "\r\n"); if (cmlen >= c_buf_len) return; #if WANT_FULLSCREEN strncpy(c_last_cmd, c_recv_buf, cmlen); c_last_cmd[cmlen] = 0; #endif const struct handler *h; for (h=handler_table; h->cmd_name[0]; h++) { if ((cmlen >= (strlen(h->cmd_name) + h->add_cmlen)) && (strncasecmp(c_recv_buf, h->cmd_name, strlen(h->cmd_name)) == 0)) { if (c_auth < h->min_auth) { numeric(503, "Please login with USER and PASS."); while (c_recv_buf[0] != '\n') remove_bytes(1); } else { char schar; #if !WANT_NONROOT if (h->do_setuid) { seteuid(c_uid); } else { seteuid(0); } #endif remove_bytes(strlen(h->cmd_name)); cmlen -= strlen(h->cmd_name); while (c_recv_buf[0] == ' ') { remove_bytes(1); cmlen--; } schar = c_recv_buf[cmlen]; c_recv_buf[cmlen] = 0; /* result of zero means the connection is freed */ /* ptr-to-member-function syntax looks odd to newbies like me */ if ((this->*h->callback)()) { c_recv_buf[cmlen] = schar; #if !WANT_NONROOT if (h->do_setuid) seteuid(getuid()); #endif remove_bytes(cmlen); } } return; } }; numeric(500, "Sorry, no such command."); remove_bytes(cmlen); } /* * prepare_for_transfer(): * Prepares an ftran object for a file transfer, setting * file size, opening sockets etc. * * nonroot notice: prepare_for_transfer() assumes all access * checks are already done. */ void prepare_for_transfer(struct ftran *f) { #if WANT_NONROOT #warning No nonroot checking for prepare_for_transfer() yet #endif #if HAVE_MMAP /* mmap doesn't make temp files for dir listings */ if (!f->dir_listing) { #endif f->size = lseek(f->local_file, 0, SEEK_END); errno = 0; #if WANT_UPLOAD if (f->upload == 0 || f->append == 0 || f->owner->c_rest_pos != 0) #endif lseek(f->local_file, f->owner->c_rest_pos, SEEK_SET); #if HAVE_MMAP } #endif if (f->state == 1) { /* PASV connection */ f->state = 2; /* waiting */ } else if (f->state == 3) { /* PORT connection */ f->state = 4; connect(f->sock, (struct sockaddr *)&f->sin, sizeof(f->sin)); poller.add(f->sock, f, POLLOUT); } time(&(f->tran_start)); } /* * decode_mode(): * Takes a mode_t argument (from a `struct stat'), and * returns the proper dirlist letter for that type. * * Note: S_IFLNK seems to be broken, or perhaps I just have * missed something (S_IFLNK is always set for all *files* on * my glibc 2.0.111 system). * * The most common cases are put first, for speed :-) */ static char decode_mode(mode_t mode) { if (S_ISREG(mode)) return '-'; if (S_ISDIR(mode)) return 'd'; if (S_ISLNK(mode)) return 'l'; if (S_ISBLK(mode)) return 'b'; if (S_ISCHR(mode)) return 'c'; if (S_ISSOCK(mode)) return 's'; if (S_ISFIFO(mode)) return 'f'; return '-'; } /* * translate_path(): * Take an FTP path, do all neccessary root_dir checks, * change to the correct directory and return the proper * file name to open/stat/whatever. The path returned is * relative to the current directory (NOT absolute). chdir() * in any way will `destroy' this argument. * * Note that `path' will be _changed_, and used as a return pointer * base. Do not attempt to free the result from this function -- * if you need to, free path instead. */ char *conn::translate_path(char * const path) { char *ptr = NULL; /* chdir to the right dir, then chop it off */ chdir(c_curr_dir); ptr = strrchr(path, '/'); if (ptr != NULL) { char save_char = ptr[0]; ptr[0] = 0; if (do_chdir(path) == -1) { return NULL; } ptr[0] = save_char; ptr++; } else { ptr = path; } return ptr; } /* * do_openfile(): * Opens the file PATH with access parameters FLAGS, translating * paths and checking permissions as neccessary. Generally, this * should be used whenever you need an open(). * * The parameters might be a bit confusing. To clarify them a bit: * c: IN/OUT (will be changed) * path: IN (but _will_ be changed) * filename: OUT * flags: IN * check_perm: IN */ int conn::do_openfile(char * const path, char * const filename, const int flags #if WANT_NONROOT , const int check_permission #endif ) { char *ptr; struct stat buf; #if WANT_NONROOT if (nr_check_permission(c_uid, path, check_permission, 0, NULL) == -1) { return -1; } #endif ptr = translate_path(c_recv_buf); if (ptr == NULL) return -1; #if WANT_UPLOAD if ((flags & O_CREAT) == 0) { #endif TRAP_ERROR(stat(ptr, &buf) == -1, 550, return -2); if (!S_ISREG(buf.st_mode)) { numeric(550, "Not a plain file.", ptr); return -2; } #if WANT_UPLOAD } #endif if (filename != NULL) { /* filename should always be != NULL */ strcpy(filename, ptr); } return open(ptr, flags, 0666); } /* * prepare_for_listing(): * Parse list options, put them back into the list_options * structure lo, and make temporary room for the list. */ int conn::prepare_for_listing(char ** const ptr, struct list_options * const lo) { #if !HAVE_MMAP char *tfname; #endif struct ftran *f = c_transfer; char *tmp; char *optr = NULL, *fptr = NULL; #if WANT_NONROOT char chd[512]; #endif #if WANT_NONROOT #warning No nonroot checking for prepare_for_listing() yet #endif if ((f == NULL) || ((f->state != 1) && (f->state != 3))) { numeric(425, "No data connection set up; please use PASV or PORT."); return -1; } /* * A little parameter scanning is required here. There can only * be two parts: the directory name, and any options. We'll find * any options first. */ if (c_recv_buf[0] == '-') { optr = c_recv_buf; } else { optr = strstr(c_recv_buf, " -"); } /* Then see if there are any options to parse. */ if (optr != NULL) { while (*++optr) { switch (*optr & (255-32)) { /* uppercase */ case 'R': /* actually case sensitive... */ lo->recursive = 1; break; case 'L': lo->long_listing = 1; break; case 'F': lo->classify = 1; break; case ' ': fptr = optr + 1; *(optr--) = 0; break; default: break; } } } else { fptr = c_recv_buf; } /* then we chdir to the dir in fptr (if any) */ tmp = fptr ? strrchr(fptr, '/') : NULL; if (tmp != NULL) { tmp[0] = 0; if (do_chdir(fptr) == -1) return -1; fptr = tmp + 1; } else { /* current directory */ TRAP_ERROR(chdir(c_curr_dir) == -1, 550, return -1); } /* if no argument, choose all files */ if (fptr == NULL || fptr[0] == 0) { fptr = "*"; } else { /* we need to check if the last part is a directory (no -d switch) */ struct stat buf; if (stat(fptr, &buf) == 0 && S_ISDIR(buf.st_mode)) { TRAP_ERROR(chdir(fptr) == -1, 550, return -1); fptr = "*"; } } *ptr = fptr; #if WANT_NONROOT getcwd(chd, 512); if (nr_check_permission(c_uid, chd, 4, 1, NULL) == -1) { numeric(550, "Permission denied"); return -1; } #endif #if !HAVE_MMAP tfname = tempnam(NULL, "ftp"); #if WANT_NONROOT if (tfname == NULL) tfname = tempnam("/", "ftp"); #endif TRAP_ERROR(tfname == NULL, 550, return -1); strcpy(f->filename, tfname); free(tfname); f->local_file = open(f->filename, O_RDWR | O_CREAT | O_TRUNC, 0666); TRAP_ERROR(f->local_file == -1, 550, return -1); #endif f->dir_listing = 1; #if WANT_UPLOAD f->upload = 0; #endif return 0; } /* * classify(): Takes a mode_t argument (from `struct stat'), and returns * the parameter to be used in an `ls -F'-style listing. */ static char classify(const mode_t mode) { if (S_ISREG(mode)) { if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) { return '*'; } else { return '\0'; } } if (S_ISDIR(mode)) return '/'; if (S_ISLNK(mode)) return '@'; if (S_ISSOCK(mode)) return '='; if (S_ISFIFO(mode)) return '|'; return '\0'; } int conn::notifyPollEvent(Poller::PollEvent *e) { int bytes_avail; if ((e->revents & (POLLIN|POLLERR|POLLHUP|POLLNVAL)) == 0) return 0; bytes_avail = recv(c_sock, c_recv_buf + c_buf_len, 255 - c_buf_len, 0); printf("recv(fd %d, buf, %d, 0) returns %d\n", c_sock, 255 - c_buf_len, bytes_avail); if (bytes_avail <= 0) { /* * select() has already told us there's something about * this socket, so if we get a return value of zero, the * client has closed the socket. If we get a return value * of -1 (error), we close the socket ourselves. * * We do the same for poll(), even though we actually have * bits that tell us what is happening (in case of new * input AND error/hangup at the same time, we do an * explicit check at the bottom of the loop as well). */ printf("%s %d: bytes_avail %d\n", __FILE__, __LINE__, bytes_avail); shutdown(); return 1; } /* overrun = disconnect */ if (c_buf_len + bytes_avail > 254) { numeric(503, "Buffer overrun; disconnecting."); shutdown(); return 1; } c_buf_len += bytes_avail; parse_command(); if (e->revents & (POLLERR|POLLHUP|POLLNVAL)) { printf("%s %d: revents %x\n", __FILE__, __LINE__, e->revents); shutdown(); } return 0; } /* * numeric(): Sends a numeric FTP reply to the client. Note that * you can use this command much the same way as you * would use a printf() (with all the normal %s, %d, * etc.), since it actually uses printf() internally. */ void conn::numeric(const int numeric, const char * const format, ...) { struct conn * const c = this; char buf[256], fmt[256]; va_list args; int i, err; snprintf(fmt, 256, "%03u %s\r\n", numeric, format); va_start(args, format); i = vsnprintf(buf, 256, fmt, args); va_end(args); err = send(c->c_sock, buf, i, 0); if (err == -1 && errno == EPIPE) { shutdown(); } } /* * remove_bytes(): * Remove some bytes from the incoming buffer. This gives * room for new data on the control connection, and should * be called when the code has finished using the data. * (This is done automatically for all commands, so you * normally need not worry about it.) */ void conn::remove_bytes(const int num) { if (c_buf_len <= num) { c_buf_len = 0; } else { c_buf_len -= num; memmove(c_recv_buf, c_recv_buf + num, c_buf_len); } }