/* * $Id: ftp_session.c,v 1.47 2004/03/25 20:46:35 shane Exp $ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if TIME_WITH_SYS_TIME # include # include #else # if HAVE_SYS_TIME_H # include # else # include # endif #endif #include "daemon_assert.h" #include "telnet_session.h" #include "ftp_command.h" #include "file_list.h" #include "ftp_session.h" #include "watchdog.h" #include "oftpd.h" #include "af_portability.h" /* space requirements */ #define ADDRPORT_STRLEN 58 /* prototypes */ static int invariant(const ftp_session_t *f); static void reply(ftp_session_t *f, int code, const char *fmt, ...); static void change_dir(ftp_session_t *f, const char *new_dir); static int open_connection(ftp_session_t *f); static int write_fully(int fd, const char *buf, int buflen); static void init_passive_port(); static int get_passive_port(); static int convert_newlines(char *dst, const char *src, int srclen); static void get_addr_str(const sockaddr_storage_t *s, char *buf, int bufsiz); static void send_readme(const ftp_session_t *f, int code); static void netscape_hack(int fd); static void set_port(ftp_session_t *f, const sockaddr_storage_t *host_port); static int set_pasv(ftp_session_t *f, sockaddr_storage_t *host_port); static int ip_equal(const sockaddr_storage_t *a, const sockaddr_storage_t *b); static void get_absolute_fname(char *fname, int fname_len, const char *dir, const char *file); /* command handlers */ static void do_user(ftp_session_t *f, const ftp_command_t *cmd); static void do_pass(ftp_session_t *f, const ftp_command_t *cmd); static void do_cwd(ftp_session_t *f, const ftp_command_t *cmd); static void do_cdup(ftp_session_t *f, const ftp_command_t *cmd); static void do_quit(ftp_session_t *f, const ftp_command_t *cmd); static void do_port(ftp_session_t *f, const ftp_command_t *cmd); static void do_pasv(ftp_session_t *f, const ftp_command_t *cmd); static void do_type(ftp_session_t *f, const ftp_command_t *cmd); static void do_stru(ftp_session_t *f, const ftp_command_t *cmd); static void do_mode(ftp_session_t *f, const ftp_command_t *cmd); static void do_retr(ftp_session_t *f, const ftp_command_t *cmd); static void do_stor(ftp_session_t *f, const ftp_command_t *cmd); static void do_pwd(ftp_session_t *f, const ftp_command_t *cmd); static void do_nlst(ftp_session_t *f, const ftp_command_t *cmd); static void do_list(ftp_session_t *f, const ftp_command_t *cmd); static void do_syst(ftp_session_t *f, const ftp_command_t *cmd); static void do_noop(ftp_session_t *f, const ftp_command_t *cmd); static void do_rest(ftp_session_t *f, const ftp_command_t *cmd); static void do_lprt(ftp_session_t *f, const ftp_command_t *cmd); static void do_lpsv(ftp_session_t *f, const ftp_command_t *cmd); static void do_eprt(ftp_session_t *f, const ftp_command_t *cmd); static void do_epsv(ftp_session_t *f, const ftp_command_t *cmd); static void do_size(ftp_session_t *f, const ftp_command_t *cmd); static void do_mdtm(ftp_session_t *f, const ftp_command_t *cmd); static struct { char *name; void (*func)(ftp_session_t *f, const ftp_command_t *cmd); } command_func[] = { { "USER", do_user }, { "PASS", do_pass }, { "CWD", do_cwd }, { "CDUP", do_cdup }, { "QUIT", do_quit }, { "PORT", do_port }, { "PASV", do_pasv }, { "LPRT", do_lprt }, { "LPSV", do_lpsv }, { "EPRT", do_eprt }, { "EPSV", do_epsv }, { "TYPE", do_type }, { "STRU", do_stru }, { "MODE", do_mode }, { "RETR", do_retr }, { "STOR", do_stor }, { "PWD", do_pwd }, { "NLST", do_nlst }, { "LIST", do_list }, { "SYST", do_syst }, { "NOOP", do_noop }, { "REST", do_rest }, { "SIZE", do_size }, { "MDTM", do_mdtm } }; #define NUM_COMMAND_FUNC (sizeof(command_func) / sizeof(command_func[0])) int ftp_session_init(ftp_session_t *f, const sockaddr_storage_t *client_addr, const sockaddr_storage_t *server_addr, telnet_session_t *t, const char *dir, error_t *err) { daemon_assert(f != NULL); daemon_assert(client_addr != NULL); daemon_assert(server_addr != NULL); daemon_assert(t != NULL); daemon_assert(dir != NULL); daemon_assert(strlen(dir) <= PATH_MAX); daemon_assert(err != NULL); #ifdef INET6 /* if the control connection is on IPv6, we need to get an IPv4 address */ /* to bind the socket to */ if (SSFAM(server_addr) == AF_INET6) { struct addrinfo hints; struct addrinfo *res; int errcode; /* getaddrinfo() does the job nicely */ memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_INET; hints.ai_flags = AI_PASSIVE; if (getaddrinfo(NULL, "ftp", &hints, &res) != 0) { error_init(err, 0, "unable to determing IPv4 address; %s", gai_strerror(errcode)); return 0; } /* let's sanity check */ daemon_assert(res != NULL); daemon_assert(sizeof(f->server_ipv4_addr) >= res->ai_addrlen); daemon_assert(SSFAM(host_port) == AF_INET); /* copy the result and free memory as necessary */ memcpy(&f->server_ipv4_addr, res->ai_addr, res->ai_addrlen); freeaddrinfo(res); } else { daemon_assert(SSFAM(host_port) == AF_INET); f->server_ipv4_addr = *server_addr; } #else f->server_ipv4_addr = *server_addr; #endif f->session_active = 1; f->command_number = 0; f->data_type = TYPE_ASCII; f->file_structure = STRU_FILE; f->file_offset = 0; f->file_offset_command_number = ULONG_MAX; f->epsv_all_set = 0; f->client_addr = *client_addr; get_addr_str(client_addr, f->client_addr_str, sizeof(f->client_addr_str)); f->server_addr = *server_addr; f->telnet_session = t; daemon_assert(strlen(dir) < sizeof(f->dir)); strcpy(f->dir, dir); f->data_channel = DATA_PORT; f->data_port = *client_addr; f->server_fd = -1; daemon_assert(invariant(f)); return 1; } void ftp_session_drop(ftp_session_t *f, const char *reason) { daemon_assert(invariant(f)); daemon_assert(reason != NULL); /* say goodbye */ reply(f, 421, "%s.", reason); daemon_assert(invariant(f)); } void ftp_session_run(ftp_session_t *f, watched_t *watched) { char buf[2048]; int len; ftp_command_t cmd; int i; daemon_assert(invariant(f)); daemon_assert(watched != NULL); /* record our watchdog */ f->watched = watched; /* say hello */ send_readme(f, 220); reply(f, 220, "Service ready for new user."); /* process commands */ while (f->session_active && telnet_session_readln(f->telnet_session, buf, sizeof(buf))) { /* delay our timeout based on this input */ watchdog_defer_watched(f->watched); /* increase our command count */ if (f->command_number == ULONG_MAX) { f->command_number = 0; } else { f->command_number++; } /* make sure we read a whole line */ len = strlen(buf); if (buf[len-1] != '\n') { reply(f, 500, "Command line too long."); while (telnet_session_readln(f->telnet_session, buf, sizeof(buf))) { len = strlen(buf); if (buf[len-1] == '\n') { break; } } goto next_command; } syslog(LOG_DEBUG, "%s %s", f->client_addr_str, buf); /* parse the line */ if (!ftp_command_parse(buf, &cmd)) { reply(f, 500, "Syntax error, command unrecognized."); goto next_command; } /* dispatch the command */ for (i=0; iserver_fd != -1) { close(f->server_fd); f->server_fd = -1; } } #ifndef NDEBUG static int invariant(const ftp_session_t *f) { int len; if (f == NULL) { return 0; } if ((f->session_active != 0) && (f->session_active != 1)) { return 0; } if ((f->data_type != TYPE_ASCII) && (f->data_type != TYPE_IMAGE)) { return 0; } if ((f->file_structure != STRU_FILE) && (f->file_structure != STRU_RECORD)){ return 0; } if (f->file_offset < 0) { return 0; } if ((f->epsv_all_set != 0) && (f->epsv_all_set != 1)) { return 0; } len = strlen(f->client_addr_str); if ((len <= 0) || (len >= ADDRPORT_STRLEN)) { return 0; } if (f->telnet_session == NULL) { return 0; } switch (f->data_channel) { case DATA_PORT: /* If the client specifies a port, verify that it is from the */ /* host the client connected from. This prevents a client from */ /* using the server to open connections to arbritrary hosts. */ if (!ip_equal(&f->client_addr, &f->data_port)) { return 0; } if (f->server_fd != -1) { return 0; } break; case DATA_PASSIVE: if (f->server_fd < 0) { return 0; } break; default: return 0; } return 1; } #endif /* NDEBUG */ static void reply(ftp_session_t *f, int code, const char *fmt, ...) { char buf[256]; va_list ap; daemon_assert(invariant(f)); daemon_assert(code >= 100); daemon_assert(code <= 559); daemon_assert(fmt != NULL); /* prepend our code to the buffer */ sprintf(buf, "%d", code); buf[3] = ' '; /* add the formatted output of the caller to the buffer */ va_start(ap, fmt); vsnprintf(buf+4, sizeof(buf)-4, fmt, ap); va_end(ap); /* log our reply */ syslog(LOG_DEBUG, "%s %s", f->client_addr_str, buf); /* send the output to the other side */ telnet_session_println(f->telnet_session, buf); daemon_assert(invariant(f)); } static void do_user(ftp_session_t *f, const ftp_command_t *cmd) { const char *user; char addr_port[ADDRPORT_STRLEN]; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); user = cmd->arg[0].string; if (strcasecmp(user, "ftp") && strcasecmp(user, "anonymous")) { syslog(LOG_WARNING, "%s attempted to log in as \"%s\"", f->client_addr_str, user); reply(f, 530, "Only anonymous FTP supported."); } else { reply(f, 331, "Send e-mail address as password."); } daemon_assert(invariant(f)); } static void do_pass(ftp_session_t *f, const ftp_command_t *cmd) { const char *password; char addr_port[ADDRPORT_STRLEN]; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); password = cmd->arg[0].string; syslog(LOG_INFO, "%s reports e-mail address \"%s\"", f->client_addr_str, password); reply(f, 230, "User logged in, proceed."); daemon_assert(invariant(f)); } #ifdef INET6 static void get_addr_str(const sockaddr_storage_t *s, char *buf, int bufsiz) { int port; int error; int len; daemon_assert(s != NULL); daemon_assert(buf != NULL); /* buf must be able to contain (at least) a string representing an * ipv4 addr, followed by the string " port " (6 chars) and the port * number (which is 5 chars max), plus the '\0' character. */ daemon_assert(bufsiz >= (INET_ADDRSTRLEN + 12)); error = getnameinfo(client_addr, sizeof(sockaddr_storage_t), buf, bufsiz, NULL, 0, NI_NUMERICHOST); /* getnameinfo() should never fail when called with NI_NUMERICHOST */ daemon_assert(error == 0); len = strlen(buf); daemon_assert(bufsiz >= len+12); snprintf(buf+len, bufsiz-len, " port %d", ntohs(SINPORT(&f->client_addr))); } #else static void get_addr_str(const sockaddr_storage_t *s, char *buf, int bufsiz) { unsigned int addr; int port; daemon_assert(s != NULL); daemon_assert(buf != NULL); /* buf must be able to contain (at least) a string representing an * ipv4 addr, followed by the string " port " (6 chars) and the port * number (which is 5 chars max), plus the '\0' character. */ daemon_assert(bufsiz >= (INET_ADDRSTRLEN + 12)); addr = ntohl(s->sin_addr.s_addr); port = ntohs(s->sin_port); snprintf(buf, bufsiz, "%d.%d.%d.%d port %d", (addr >> 24) & 0xff, (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff, port); } #endif static void do_cwd(ftp_session_t *f, const ftp_command_t *cmd) { const char *new_dir; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); new_dir = cmd->arg[0].string; change_dir(f, new_dir); daemon_assert(invariant(f)); } static void do_cdup(ftp_session_t *f, const ftp_command_t *cmd) { daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 0); change_dir(f, ".."); daemon_assert(invariant(f)); } static void change_dir(ftp_session_t *f, const char *new_dir) { char target[PATH_MAX+1]; const char *p, *n; int len; char *prev_dir; char *target_end; struct stat stat_buf; int dir_okay; daemon_assert(invariant(f)); daemon_assert(new_dir != NULL); daemon_assert(strlen(new_dir) <= PATH_MAX); /* set up our "base" directory that we build from */ p = new_dir; if (*p == '/') { /* if this starts with a '/' it is an absolute path */ strcpy(target, "/"); do { p++; } while (*p == '/'); } else { /* otherwise it's a relative path */ daemon_assert(strlen(f->dir) < sizeof(target)); strcpy(target, f->dir); } /* add on each directory, handling "." and ".." */ while (*p != '\0') { /* find the end of the next directory (either at '/' or '\0') */ n = strchr(p, '/'); if (n == NULL) { n = strchr(p, '\0'); } len = n - p; if ((len == 1) && (p[0] == '.')) { /* do nothing with "." */ } else if ((len == 2) && (p[0] == '.') && (p[1] == '.')) { /* change to previous directory with ".." */ prev_dir = strrchr(target, '/'); daemon_assert(prev_dir != NULL); *prev_dir = '\0'; if (prev_dir == target) { strcpy(target, "/"); } } else { /* otherwise add to current directory */ if ((strlen(target) + 1 + len) > PATH_MAX) { reply(f, 550, "Error changing directory; path is too long."); return; } /* append a '/' unless we were at the root directory */ target_end = strchr(target, '\0'); if (target_end != target+1) { *target_end++ = '/'; } /* add the directory itself */ while (p != n) { *target_end++ = *p++; } *target_end = '\0'; } /* advance to next directory to check */ p = n; /* skip '/' characters */ while (*p == '/') { p++; } } /* see if this is a directory we can change into */ dir_okay = 0; if (stat(target, &stat_buf) == 0) { #ifndef STAT_MACROS_BROKEN if (!S_ISDIR(stat_buf.st_mode)) { #else if (S_ISDIR(stat_buf.st_mode)) { #endif reply(f, 550,"Directory change failed; target is not a directory."); } else { if (S_IXOTH & stat_buf.st_mode) { dir_okay = 1; } else if ((stat_buf.st_gid == getegid()) && (S_IXGRP & stat_buf.st_mode)) { dir_okay = 1; } else if ((stat_buf.st_uid == geteuid()) && (S_IXUSR & stat_buf.st_mode)) { dir_okay = 1; } else { reply(f, 550, "Directory change failed; permission denied."); } } } else { reply(f, 550, "Directory change failed; directory does not exist."); } /* if everything is okay, change into the directory */ if (dir_okay) { daemon_assert(strlen(target) < sizeof(f->dir)); /* send a readme unless we changed to our current directory */ if (strcmp(f->dir, target) != 0) { strcpy(f->dir, target); send_readme(f, 250); } else { strcpy(f->dir, target); } reply(f, 250, "Directory change successful."); } daemon_assert(invariant(f)); } static void do_quit(ftp_session_t *f, const ftp_command_t *cmd) { daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 0); reply(f, 221, "Service closing control connection."); f->session_active = 0; daemon_assert(invariant(f)); } /* support for the various port setting functions */ static void set_port(ftp_session_t *f, const sockaddr_storage_t *host_port) { daemon_assert(invariant(f)); daemon_assert(host_port != NULL); if (f->epsv_all_set) { reply(f, 500, "After EPSV ALL, only EPSV allowed."); } else if (!ip_equal(&f->client_addr, host_port)) { reply(f, 500, "Port must be on command channel IP."); } else if (ntohs(SINPORT(host_port)) < IPPORT_RESERVED) { reply(f, 500, "Port may not be less than 1024, which is reserved."); } else { /* close any outstanding PASSIVE port */ if (f->data_channel == DATA_PASSIVE) { close(f->server_fd); f->server_fd = -1; } f->data_channel = DATA_PORT; f->data_port = *host_port; reply(f, 200, "Command okay."); } daemon_assert(invariant(f)); } /* set IP and port for client to receive data on */ static void do_port(ftp_session_t *f, const ftp_command_t *cmd) { const sockaddr_storage_t *host_port; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); host_port = &cmd->arg[0].host_port; daemon_assert(SSFAM(host_port) == AF_INET); set_port(f, host_port); daemon_assert(invariant(f)); } /* set IP and port for client to receive data on, transport independent */ static void do_lprt(ftp_session_t *f, const ftp_command_t *cmd) { const sockaddr_storage_t *host_port; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); host_port = &cmd->arg[0].host_port; #ifdef INET6 if ((SSFAM(host_port) != AF_INET) && (SSFAM(host_port) != AF_INET6)) { reply(f, 521, "Only IPv4 and IPv6 supported, address families (4,6)"); } #else if (SSFAM(host_port) != AF_INET) { reply(f, 521, "Only IPv4 supported, address family (4)"); } #endif set_port(f, host_port); daemon_assert(invariant(f)); } /* set IP and port for the client to receive data on, IPv6 extension */ /* */ /* RFC 2428 specifies that if the data connection is going to be on */ /* the same IP as the control connection, EPSV must be used. Since */ /* that is the only mode of transfer we support, we reject all EPRT */ /* requests. */ static void do_eprt(ftp_session_t *f, const ftp_command_t *cmd) { const sockaddr_storage_t *host_port; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); reply(f, 500, "EPRT not supported, use EPSV."); daemon_assert(invariant(f)); } /* support for the various pasv setting functions */ /* returns the file descriptor of the bound port, or -1 on error */ /* note: the "host_port" parameter will be modified, having its port set */ static int set_pasv(ftp_session_t *f, sockaddr_storage_t *bind_addr) { int socket_fd; int port; daemon_assert(invariant(f)); daemon_assert(bind_addr != NULL); socket_fd = socket(SSFAM(bind_addr), SOCK_STREAM, 0); if (socket_fd == -1) { reply(f, 500, "Error creating server socket; %s.", strerror(errno)); return -1; } for (;;) { port = get_passive_port(); SINPORT(bind_addr) = htons(port); if (bind(socket_fd, (struct sockaddr *)bind_addr, sizeof(struct sockaddr)) == 0) { break; } if (errno != EADDRINUSE) { reply(f, 500, "Error binding server port; %s.", strerror(errno)); close(socket_fd); return -1; } } if (listen(socket_fd, 1) != 0) { reply(f, 500, "Error listening on server port; %s.", strerror(errno)); close(socket_fd); return -1; } return socket_fd; } /* pick a server port to listen for connection on */ static void do_pasv(ftp_session_t *f, const ftp_command_t *cmd) { int socket_fd; unsigned int addr; int port; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 0); if (f->epsv_all_set) { reply(f, 500, "After EPSV ALL, only EPSV allowed."); goto exit_pasv; } socket_fd = set_pasv(f, &f->server_ipv4_addr); if (socket_fd == -1) { goto exit_pasv; } /* report port to client */ addr = ntohl(f->server_ipv4_addr.sin_addr.s_addr); port = ntohs(f->server_ipv4_addr.sin_port); reply(f, 227, "Entering Passive Mode (%d,%d,%d,%d,%d,%d).", addr >> 24, (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff, port >> 8, port & 0xff); /* close any outstanding PASSIVE port */ if (f->data_channel == DATA_PASSIVE) { close(f->server_fd); } f->data_channel = DATA_PASSIVE; f->server_fd = socket_fd; exit_pasv: daemon_assert(invariant(f)); } /* pick a server port to listen for connection on, including IPv6 */ static void do_lpsv(ftp_session_t *f, const ftp_command_t *cmd) { int socket_fd; char addr[80]; uint8_t *a; uint8_t *p; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 0); if (f->epsv_all_set) { reply(f, 500, "After EPSV ALL, only EPSV allowed."); goto exit_lpsv; } socket_fd = set_pasv(f, &f->server_addr); if (socket_fd == -1) { goto exit_lpsv; } /* report address and port to client */ #ifdef INET6 if (SSFAM(&f->server_addr) == AF_INET6) { a = (uint8_t *)&SIN6ADDR(&f->server_addr); p = (uint8_t *)&SIN6PORT(&f->server_addr); snprintf(addr, sizeof(addr), "(6,16,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,2,%d,%d)", a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15], p[0], p[1]); } else #endif { a = (uint8_t *)&SIN4ADDR(&f->server_addr); p = (uint8_t *)&SIN4PORT(&f->server_addr); snprintf(addr, sizeof(addr), "(4,4,%d,%d,%d,%d,2,%d,%d)", a[0], a[1], a[2], a[3], p[0], p[1]); } reply(f, 228, "Entering Long Passive Mode %s", addr); /* close any outstanding PASSIVE port */ if (f->data_channel == DATA_PASSIVE) { close(f->server_fd); } f->data_channel = DATA_PASSIVE; f->server_fd = socket_fd; exit_lpsv: daemon_assert(invariant(f)); } /* pick a server port to listen for connection on, new IPv6 method */ static void do_epsv(ftp_session_t *f, const ftp_command_t *cmd) { int socket_fd; sockaddr_storage_t *addr; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert((cmd->num_arg == 0) || (cmd->num_arg == 1)); /* check our argument, if any, and use the appropriate address */ if (cmd->num_arg == 0) { addr = &f->server_addr; } else { switch (cmd->arg[0].num) { /* EPSV_ALL is a special number indicating the client sent */ /* the command "EPSV ALL" - this is not a request to assign */ /* a new passive port, but rather to deny all future port */ /* assignment requests other than EPSV */ case EPSV_ALL: f->epsv_all_set = 1; reply(f, 200, "EPSV ALL command successful."); goto exit_epsv; case 1: addr = (sockaddr_storage_t *)&f->server_ipv4_addr; break; #ifdef INET6 case 2: addr = &f->server_addr; break; default: reply(f, 522, "Only IPv4 and IPv6 supported, use (1,2)"); goto exit_epsv; #else default: reply(f, 522, "Only IPv4 supported, use (1)"); goto exit_epsv; #endif } } /* bind port and so on */ socket_fd = set_pasv(f, addr); if (socket_fd == -1) { goto exit_epsv; } /* report port to client */ reply(f, 229, "Entering Extended Passive Mode (|||%d|)", ntohs(SINPORT(&f->server_addr))); /* close any outstanding PASSIVE port */ if (f->data_channel == DATA_PASSIVE) { close(f->server_fd); } f->data_channel = DATA_PASSIVE; f->server_fd = socket_fd; exit_epsv: daemon_assert(invariant(f)); } /* seed the random number generator used to pick a port */ static void init_passive_port() { struct timeval tv; unsigned short int seed[3]; gettimeofday(&tv, NULL); seed[0] = (tv.tv_sec >> 16) & 0xFFFF; seed[1] = tv.tv_sec & 0xFFFF; seed[2] = tv.tv_usec & 0xFFFF; seed48(seed); } /* pick a port to try to bind() for passive FTP connections */ static int get_passive_port() { static pthread_once_t once_control = PTHREAD_ONCE_INIT; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int port; /* initialize the random number generator the first time we're called */ pthread_once(&once_control, init_passive_port); /* pick a random port between 1024 and 65535, inclusive */ pthread_mutex_lock(&mutex); port = (lrand48() % 64512) + 1024; pthread_mutex_unlock(&mutex); return port; } static void do_type(ftp_session_t *f, const ftp_command_t *cmd) { char type; char form; int cmd_okay; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg >= 1); daemon_assert(cmd->num_arg <= 2); type = cmd->arg[0].string[0]; if (cmd->num_arg == 2) { form = cmd->arg[1].string[0]; } else { form = 0; } cmd_okay = 0; if (type == 'A') { if ((cmd->num_arg == 1) || ((cmd->num_arg == 2) && (form == 'N'))) { f->data_type = TYPE_ASCII; cmd_okay = 1; } } else if (type == 'I') { f->data_type = TYPE_IMAGE; cmd_okay = 1; } if (cmd_okay) { reply(f, 200, "Command okay."); } else { reply(f, 504, "Command not implemented for that parameter."); } daemon_assert(invariant(f)); } static void do_stru(ftp_session_t *f, const ftp_command_t *cmd) { char structure; int cmd_okay; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); structure = cmd->arg[0].string[0]; cmd_okay = 0; if (structure == 'F') { f->file_structure = STRU_FILE; cmd_okay = 1; } else if (structure == 'R') { f->file_structure = STRU_RECORD; cmd_okay = 1; } if (cmd_okay) { reply(f, 200, "Command okay."); } else { reply(f, 504, "Command not implemented for that parameter."); } daemon_assert(invariant(f)); } static void do_mode(ftp_session_t *f, const ftp_command_t *cmd) { char mode; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); mode = cmd->arg[0].string[0]; if (mode == 'S') { reply(f, 200, "Command okay."); } else { reply(f, 504, "Command not implemented for that parameter."); } daemon_assert(invariant(f)); } /* convert the user-entered file name into a full path on our local drive */ static void get_absolute_fname(char *fname, int fname_len, const char *dir, const char *file) { daemon_assert(fname != NULL); daemon_assert(dir != NULL); daemon_assert(file != NULL); if (*file == '/') { /* absolute path, use as input */ daemon_assert(strlen(file) < fname_len); strcpy(fname, file); } else { /* construct a file name based on our current directory */ daemon_assert(strlen(dir) + 1 + strlen(file) < fname_len); strcpy(fname, dir); /* add a seperating '/' if we're not at the root */ if (fname[1] != '\0') { strcat(fname, "/"); } /* and of course the actual file name */ strcat(fname, file); } } static void do_retr(ftp_session_t *f, const ftp_command_t *cmd) { const char *file_name; char full_path[PATH_MAX+1+MAX_STRING_LEN]; int file_fd; struct stat stat_buf; int socket_fd; int read_ret; char buf[4096]; char converted_buf[8192]; int converted_buflen; char addr_port[ADDRPORT_STRLEN]; struct timeval start_timestamp; struct timeval end_timestamp; struct timeval transfer_time; off_t file_size; off_t offset; off_t amt_to_send; int sendfile_ret; off_t amt_sent; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); /* set up for exit */ file_fd = -1; socket_fd = -1; /* create an absolute name for our file */ file_name = cmd->arg[0].string; get_absolute_fname(full_path, sizeof(full_path), f->dir, file_name); /* open file */ file_fd = open(full_path, O_RDONLY); if (file_fd == -1) { reply(f, 550, "Error opening file; %s.", strerror(errno)); goto exit_retr; } if (fstat(file_fd, &stat_buf) != 0) { reply(f, 550, "Error getting file information; %s.", strerror(errno)); goto exit_retr; } #ifndef STATS_MACRO_BROKEN if (S_ISDIR(stat_buf.st_mode)) { #else if (!S_ISDIR(stat_buf.st_mode)) { #endif reply(f, 550, "Error, file is a directory."); goto exit_retr; } /* if the last command was a REST command, restart at the */ /* requested position in the file */ if ((f->file_offset_command_number == (f->command_number - 1)) && (f->file_offset > 0)) { if (lseek(file_fd, f->file_offset, SEEK_SET) == -1) { reply(f, 550, "Error seeking to restart position; %s.", strerror(errno)); goto exit_retr; } } /* ready to transfer */ reply(f, 150, "About to open data connection."); /* mark start time */ gettimeofday(&start_timestamp, NULL); /* open data path */ socket_fd = open_connection(f); if (socket_fd == -1) { goto exit_retr; } /* we're golden, send the file */ file_size = 0; if (f->data_type == TYPE_ASCII) { for (;;) { read_ret = read(file_fd, buf, sizeof(buf)); if (read_ret == -1) { reply(f, 550, "Error reading from file; %s.", strerror(errno)); goto exit_retr; } if (read_ret == 0) { break; } converted_buflen = convert_newlines(converted_buf, buf, read_ret); if (write_fully(socket_fd, converted_buf, converted_buflen) == -1) { reply(f, 550, "Error writing to data connection; %s.", strerror(errno)); goto exit_retr; } file_size += converted_buflen; } } else { daemon_assert(f->data_type == TYPE_IMAGE); /* for sendfile(), we still have to use a loop to avoid having our watchdog time us out on large files - it does allow us to avoid an extra copy to/from user space */ /* #ifdef HAVE_SENDFILE offset = f->file_offset; file_size = stat_buf.st_size - offset; while (offset < stat_buf.st_size) { amt_to_send = stat_buf.st_size - offset; if (amt_to_send > 65536) { amt_to_send = 65536; } #ifdef HAVE_LINUX_SENDFILE sendfile_ret = sendfile(socket_fd, file_fd, &offset, amt_to_send); if (sendfile_ret != amt_to_send) { reply(f, 550, "Error sending file; %s.", strerror(errno)); goto exit_retr; } #elif HAVE_FREEBSD_SENDFILE sendfile_ret = sendfile(file_fd, socket_fd, offset, amt_to_send, NULL, &amt_sent, 0); if (sendfile_ret != 0) { reply(f, 550, "Error sending file; %s.", strerror(errno)); goto exit_retr; } offset += amt_sent; #endif watchdog_defer_watched(f->watched); } #else */ for (;;) { read_ret = read(file_fd, buf, sizeof(buf)); if (read_ret == -1) { reply(f, 550, "Error reading from file; %s.", strerror(errno)); goto exit_retr; } if (read_ret == 0) { break; } if (write_fully(socket_fd, buf, read_ret) == -1) { reply(f, 550, "Error writing to data connection; %s.", strerror(errno)); goto exit_retr; } file_size += read_ret; watchdog_defer_watched(f->watched); } /* #endif */ /* HAVE_SENDFILE */ } /* disconnect */ close(socket_fd); socket_fd = -1; /* hey, it worked, let the other side know */ reply(f, 226, "File transfer complete."); /* mark end time */ gettimeofday(&end_timestamp, NULL); /* calculate transfer rate */ transfer_time.tv_sec = end_timestamp.tv_sec - start_timestamp.tv_sec; transfer_time.tv_usec = end_timestamp.tv_usec - start_timestamp.tv_usec; while (transfer_time.tv_usec >= 1000000) { transfer_time.tv_sec++; transfer_time.tv_usec -= 1000000; } while (transfer_time.tv_usec < 0) { transfer_time.tv_sec--; transfer_time.tv_usec += 1000000; } /* note the transfer */ syslog(LOG_INFO, "%s retrieved \"%s\", %ld bytes in %d.%06d seconds", f->client_addr_str, full_path, file_size, transfer_time.tv_sec, transfer_time.tv_usec); exit_retr: f->file_offset = 0; if (socket_fd != -1) { close(socket_fd); } if (file_fd != -1) { close(file_fd); } daemon_assert(invariant(f)); } static void do_stor(ftp_session_t *f, const ftp_command_t *cmd) { daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); reply(f, 553, "Server will not store files."); daemon_assert(invariant(f)); } static int open_connection(ftp_session_t *f) { int socket_fd; struct sockaddr_in addr; unsigned addr_len; daemon_assert((f->data_channel == DATA_PORT) || (f->data_channel == DATA_PASSIVE)); if (f->data_channel == DATA_PORT) { socket_fd = socket(SSFAM(&f->data_port), SOCK_STREAM, 0); if (socket_fd == -1) { reply(f, 425, "Error creating socket; %s.", strerror(errno)); return -1; } if (connect(socket_fd, (struct sockaddr *)&f->data_port, sizeof(sockaddr_storage_t)) != 0) { reply(f, 425, "Error connecting; %s.", strerror(errno)); close(socket_fd); return -1; } } else { daemon_assert(f->data_channel == DATA_PASSIVE); addr_len = sizeof(struct sockaddr_in); socket_fd = accept(f->server_fd, (struct sockaddr *)&addr, &addr_len); if (socket_fd == -1) { reply(f, 425, "Error accepting connection; %s.", strerror(errno)); return -1; } #ifdef INET6 /* in IPv6, the client can connect to a channel using a different */ /* protocol - in that case, we'll just blindly let the connection */ /* through, otherwise verify addresses match */ if (SAFAM(addr) == SSFAM(&f->client_addr)) { if (memcmp(&SINADDR(&f->client_addr), &SINADDR(&addr), sizeof(SINADDR(&addr)))) { reply(f, 425, "Error accepting connection; connection from invalid IP."); close(socket_fd); return -1; } } #else if (memcmp(&f->client_addr.sin_addr, &addr.sin_addr, sizeof(struct in_addr))) { reply(f, 425, "Error accepting connection; connection from invalid IP."); close(socket_fd); return -1; } #endif } return socket_fd; } /* convert any '\n' to '\r\n' */ /* destination should be twice the size of the source for safety */ static int convert_newlines(char *dst, const char *src, int srclen) { int i; int dstlen; daemon_assert(dst != NULL); daemon_assert(src != NULL); dstlen = 0; for (i=0; inum_arg == 0); reply(f, 257, "\"%s\" is current directory.", f->dir); daemon_assert(invariant(f)); } #if 0 /* because oftpd uses glob(), it is possible for users to launch a denial-of-service attack by sending certain wildcard expressions that create extremely large lists of files, e.g. "*/../*/../*/../*/../*/../*" in order to prevent this, a user may pass wildcards, or paths, but not both as arguments to LIST or NLST - at most all the files from a single directory will be returned */ #endif /* check if a filespec has a wildcard in it */ static int filespec_has_wildcard(const char *filespec) { daemon_assert(filespec != NULL); /* check each character for wildcard */ while (*filespec != '\0') { switch (*filespec) { /* wildcards */ case '*': case '?': case '[': return 1; /* backslash escapes next character unless at end of string */ case '\\': if (*(filespec+1) != '\0') { filespec++; } break; } filespec++; } return 0; } /* filespec includes path separator, i.e. '/' */ static int filespec_has_path_separator(const char *filespec) { daemon_assert(filespec != NULL); /* check each character for path separator */ if (strchr(filespec, '/') != NULL) { return 1; } else { return 0; } } /* returns whether filespec is legal or not */ static int filespec_is_legal(const char *filespec) { daemon_assert(filespec != NULL); if (filespec_has_wildcard(filespec)) { if (filespec_has_path_separator(filespec)) { return 0; } } return 1; } static void do_nlst(ftp_session_t *f, const ftp_command_t *cmd) { int fd; const char *param; int send_ok; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert((cmd->num_arg == 0) || (cmd->num_arg == 1)); /* set up for exit */ fd = -1; /* figure out what parameters to use */ if (cmd->num_arg == 0) { param = "*"; } else { daemon_assert(cmd->num_arg == 1); /* ignore attempts to send options to "ls" by silently dropping */ if (cmd->arg[0].string[0] == '-') { param = "*"; } else { param = cmd->arg[0].string; } } /* check spec passed */ if (!filespec_is_legal(param)) { reply(f, 550, "Illegal filename passed."); goto exit_nlst; } /* ready to list */ reply(f, 150, "About to send name list."); /* open our data connection */ fd = open_connection(f); if (fd == -1) { goto exit_nlst; } /* send any files */ send_ok = file_nlst(fd, f->dir, param); /* strange handshake for Netscape's benefit */ netscape_hack(fd); if (send_ok) { reply(f, 226, "Transfer complete."); } else { reply(f, 451, "Error sending name list."); } /* clean up and exit */ exit_nlst: if (fd != -1) { close(fd); } daemon_assert(invariant(f)); } static void do_list(ftp_session_t *f, const ftp_command_t *cmd) { int fd; const char *param; int send_ok; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert((cmd->num_arg == 0) || (cmd->num_arg == 1)); /* set up for exit */ fd = -1; /* figure out what parameters to use */ if (cmd->num_arg == 0) { param = "*"; } else { daemon_assert(cmd->num_arg == 1); /* ignore attempts to send options to "ls" by silently dropping */ if (cmd->arg[0].string[0] == '-') { param = "*"; } else { param = cmd->arg[0].string; } } /* check spec passed */ if (!filespec_is_legal(param)) { reply(f, 550, "Illegal filename passed."); goto exit_list; } /* ready to list */ reply(f, 150, "About to send file list."); /* open our data connection */ fd = open_connection(f); if (fd == -1) { goto exit_list; } /* send any files */ send_ok = file_list(fd, f->dir, param); /* strange handshake for Netscape's benefit */ netscape_hack(fd); if (send_ok) { reply(f, 226, "Transfer complete."); } else { reply(f, 451, "Error sending file list."); } /* clean up and exit */ exit_list: if (fd != -1) { close(fd); } daemon_assert(invariant(f)); } static void do_syst(ftp_session_t *f, const ftp_command_t *cmd) { daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 0); reply(f, 215, "UNIX"); daemon_assert(invariant(f)); } static void do_noop(ftp_session_t *f, const ftp_command_t *cmd) { daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 0); reply(f, 200, "Command okay."); daemon_assert(invariant(f)); } static void do_rest(ftp_session_t *f, const ftp_command_t *cmd) { daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); if (f->data_type != TYPE_IMAGE) { reply(f, 555, "Restart not possible in ASCII mode."); } else if (f->file_structure != STRU_FILE) { reply(f, 555, "Restart only possible with FILE structure."); } else { f->file_offset = cmd->arg[0].offset; f->file_offset_command_number = f->command_number; reply(f, 350, "Restart okay, awaiting file retrieval request."); } daemon_assert(invariant(f)); } static void do_size(ftp_session_t *f, const ftp_command_t *cmd) { const char *file_name; char full_path[PATH_MAX+1+MAX_STRING_LEN]; struct stat stat_buf; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); if (f->data_type != TYPE_IMAGE) { reply(f, 550, "Size cannot be determined in ASCII mode."); } else if (f->file_structure != STRU_FILE) { reply(f, 550, "Size cannot be determined with FILE structure."); } else { /* create an absolute name for our file */ file_name = cmd->arg[0].string; get_absolute_fname(full_path, sizeof(full_path), f->dir, file_name); /* get the file information */ if (stat(full_path, &stat_buf) != 0) { reply(f, 550, "Error getting file status; %s.", strerror(errno)); } else { /* verify that the file is not a directory */ if (S_ISDIR(stat_buf.st_mode)) { reply(f, 550, "File is a directory, SIZE command not valid."); } else { /* output the size */ if (sizeof(off_t) == 8) { reply(f, 213, "%llu", stat_buf.st_size); } else { reply(f, 213, "%lu", stat_buf.st_size); } } } } daemon_assert(invariant(f)); } /* if no gmtime_r() is available, provide one */ #ifndef HAVE_GMTIME_R struct tm *gmtime_r(const time_t *timep, struct tm *timeptr) { static pthread_mutex_t time_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&time_lock); *timeptr = *(gmtime(timep)); pthread_mutex_unlock(&time_lock); return timeptr; } #endif /* HAVE_GMTIME_R */ static void do_mdtm(ftp_session_t *f, const ftp_command_t *cmd) { const char *file_name; char full_path[PATH_MAX+1+MAX_STRING_LEN]; struct stat stat_buf; struct tm mtime; char time_buf[16]; daemon_assert(invariant(f)); daemon_assert(cmd != NULL); daemon_assert(cmd->num_arg == 1); /* create an absolute name for our file */ file_name = cmd->arg[0].string; get_absolute_fname(full_path, sizeof(full_path), f->dir, file_name); /* get the file information */ if (stat(full_path, &stat_buf) != 0) { reply(f, 550, "Error getting file status; %s.", strerror(errno)); } else { gmtime_r(&stat_buf.st_mtime, &mtime); strftime(time_buf, sizeof(time_buf), "%Y%m%d%H%M%S", &mtime); reply(f, 213, time_buf); } daemon_assert(invariant(f)); } static void send_readme(const ftp_session_t *f, int code) { char file_name[PATH_MAX+1]; int dir_len; struct stat stat_buf; int fd; int read_ret; char buf[4096]; char code_str[8]; char *p; int len; char *nl; int line_len; daemon_assert(invariant(f)); daemon_assert(code >= 100); daemon_assert(code <= 559); /* set up for early exit */ fd = -1; /* verify our README wouldn't be too long */ dir_len = strlen(f->dir); if ((dir_len + 1 + sizeof(README_FILE_NAME)) > sizeof(file_name)) { goto exit_send_readme; } /* create a README file name */ strcpy(file_name, f->dir); strcat(file_name, "/"); strcat(file_name, README_FILE_NAME); /* open our file */ fd = open(file_name, O_RDONLY); if (fd == -1) { goto exit_send_readme; } /* verify this isn't a directory */ if (fstat(fd, &stat_buf) != 0) { goto exit_send_readme; } #ifndef STATS_MACRO_BROKEN if (S_ISDIR(stat_buf.st_mode)) { #else if (!S_ISDIR(stat_buf.st_mode)) { #endif goto exit_send_readme; } /* convert our code to a buffer */ daemon_assert(code >= 100); daemon_assert(code <= 999); sprintf(code_str, "%03d-", code); /* read and send */ read_ret = read(fd, buf, sizeof(buf)); if (read_ret > 0) { telnet_session_print(f->telnet_session, code_str); while (read_ret > 0) { p = buf; len = read_ret; nl = memchr(p, '\n', len); while ((len > 0) && (nl != NULL)) { *nl = '\0'; telnet_session_println(f->telnet_session, p); line_len = nl - p; len -= line_len + 1; if (len > 0) { telnet_session_print(f->telnet_session, code_str); } p = nl+1; nl = memchr(p, '\n', len); } if (len > 0) { telnet_session_print(f->telnet_session, p); } read_ret = read(fd, buf, sizeof(buf)); } } /* cleanup and exit */ exit_send_readme: if (fd != -1) { close(fd); } daemon_assert(invariant(f)); } /* hack which prevents Netscape error in file list */ static void netscape_hack(int fd) { fd_set readfds; struct timeval timeout; int select_ret; char c; daemon_assert(fd >= 0); shutdown(fd, 1); FD_ZERO(&readfds); FD_SET(fd, &readfds); timeout.tv_sec = 15; timeout.tv_usec = 0; select_ret = select(fd+1, &readfds, NULL, NULL, &timeout); if (select_ret > 0) { read(fd, &c, 1); } } /* compare two addresses to see if they contain the same IP address */ static int ip_equal(const sockaddr_storage_t *a, const sockaddr_storage_t *b) { daemon_assert(a != NULL); daemon_assert(b != NULL); daemon_assert((SSFAM(a) == AF_INET) || (SSFAM(a) == AF_INET6)); daemon_assert((SSFAM(b) == AF_INET) || (SSFAM(b) == AF_INET6)); if (SSFAM(a) != SSFAM(b)) { return 0; } if (memcmp(&SINADDR(a), &SINADDR(b), sizeof(SINADDR(a))) != 0) { return 0; } return 1; }