/* FIXME: 1. Parse command is a hack. We can do better. * 2. OSV: hooks support seems to be bad, as it requires storing of * entire input file in memory. Seem to be better to change it to * something more reasonable, like having * 'hook_write(void const *buf, int count)' routine that will be * called multiple times while file is being received. * 3. OSV: Remove hack with "/dev/null"? * * FTP Server Daemon * * Submitted by: Jake Janovetz * * Changed by: Sergei Organov (OSV) * Arnout Vandecappelle (AV) * Sebastien Bourdeauducq (MM) * * * Changes: * * 2010-12-02 Sebastien Bourdeauducq * * * Support spaces in filenames * * 2010-04-29 Arnout Vandecappelle (Essensium/Mind) * * * Added USER/PASS authentication. * * 2001-01-31 Sergei Organov * * * Hacks with current dir and root dir removed in favor of new libio * support for task-local current and root directories. * * 2001-01-30 Sergei Organov * * * Bug in `close_data_socket()' introduced by previous change fixed. * * `command_pasv()' changed to set timeout on socket we are listening on * and code fixed to don't close socket twice on error. * * `serr()' changed to clear `errno'. * * `data_socket()' changed to clear `errno' before `bind()'. * * `session()' changed to clear `errno' before processing session. * * 2001-01-29 Sergei Organov * * * `close_data_socket()' fixed to close both active and passive sockets * * Initialize info->data_socket to -1 in `daemon()' * * Initialize `fname' to empty string in `exec_command()' * * 2001-01-22 Sergei Organov * * * Timeouts on sockets implemented. 'idle' field added to * configuration. No timeout by default to keep backward compatibility. * Note: SITE IDLE command not implemented yet. * * Basic global access control implemented. 'access' field added to * configuration. No access limitations by default to keep backward * compatibility. * * 2001-01-17 Sergei Organov * * * Anchor data socket for active mode (using self IP and port 20.) * * Fixed default data port support (still not tested). * * Don't allow IP address different from originating host in * PORT command to improve security. * * Fixed bug in MDTM command. * * Check for correctness of parsing of argument in command_port(). * * Fixed squeeze_path() to don't allow names like 'NAME/smth' where * 'NAME' is not a directory. * * Command parsing a little bit improved: command names are now * converted to upper-case to be more compatible with RFC (command * names are not case-sensitive.) * * Reformat comments so that they have RTEMS look-and-feel. * * 2001-01-16 Sergei Organov * * * Fixed DELE, SITE CHMOD, RMD, MKD broken by previous changes * * True ASCII mode implemented (doesn't work for hooks and /dev/null) * * Passive mode implemented, PASV command added. * * Default port for data connection could be used (untested, can't find * ftp client that doesn't send PORT command) * * SYST reply changed to UNIX, as former RTEMS isn't registered name. * * Reply codes reviewed and fixed. * * 2001-01-08 Sergei Organov * * * use pool of pre-created threads to handle sessions * * LIST output now similar to what "/bin/ls -al" would output, thus * FTP clients could parse it. * * LIST NAME now works (both for files and directories) * * keep track of CWD for every session separately * * ability to specify root directory name in configuration table * * options sent in commands are ignored, thus LIST -al FILE works * * added support for NLST, CDUP and MDTM commands * * buffers are allocated on stack instead of heap where possible * * drop using of task notepad to pass parameters - use function * arguments instead * * various bug-fixes, e.g., use of PF_INET in socket() instead of * AF_INET, use snprintf() instead of sprintf() everywhere for safety, * etc. */ /************************************************************************* * ftpd.c ************************************************************************* * Description: * * This file contains the daemon which services requests that appear * on the FTP port. This server is compatible with FTP, but it * also provides 'hooks' to make it usable in situations where files * are not used/necessary. Once the server is started, it runs * forever. * * * Organization: * * The FTP daemon is started upon boot along with a (configurable) * number of tasks to handle sessions. It runs all the time and * waits for connections on the known FTP port (21). When * a connection is made, it wakes-up a 'session' task. That * session then interacts with the remote host. When the session * is complete, the session task goes to sleep. The daemon still * runs, however. * * * Supported commands are: * * RETR xxx - Sends a file from the client. * STOR xxx - Receives a file from the client. xxx = filename. * LIST xxx - Sends a file list to the client. * NLST xxx - Sends a file list to the client. * USER - Does nothing. * PASS - Does nothing. * SYST - Replies with the system type (`RTEMS'). * DELE xxx - Delete file xxx. * MKD xxx - Create directory xxx. * RMD xxx - Remove directory xxx. * PWD - Print working directory. * CWD xxx - Change working directory. * CDUP - Change to upper directory. * SITE CHMOD xxx yyy - Change permissions on file yyy to xxx. * PORT a,b,c,d,x,y - Setup for a data port to IP address a.b.c.d * and port (x*256 + y). * MDTM xxx - Send file modification date/time to the client. * xxx = filename. * PASV - Use passive mode data connection. * * * The public routines contained in this file are: * * rtems_initialize_ftpd - Initializes and starts the server daemon, * then returns to its caller. * *------------------------------------------------------------------------ * Jake Janovetz * University of Illinois * 1406 West Green Street * Urbana IL 61801 ************************************************************************* * Change History: * 12/01/97 - Creation (JWJ) * 2001-01-08 - Changes by OSV * 2010-04-29 - Authentication added by AV *************************************************************************/ /************************************************************************* * Meanings of first and second digits of reply codes: * * Reply: Description: *-------- -------------- * 1yz Positive preliminary reply. The action is being started but * expect another reply before sending another command. * 2yz Positive completion reply. A new command can be sent. * 3yz Positive intermediate reply. The command has been accepted * but another command must be sent. * 4yz Transient negative completion reply. The requested action did * not take place, but the error condition is temporary so the * command can be reissued later. * 5yz Permanent negative completion reply. The command was not * accepted and should not be retried. *------------------------------------------------------------------------- * x0z Syntax errors. * x1z Information. * x2z Connections. Replies referring to the control or data * connections. * x3z Authentication and accounting. Replies for the login or * accounting commands. * x4z Unspecified. * x5z Filesystem status. *************************************************************************/ #if HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNUC__ /* change to #if 1 to disable syslog entirely */ #if 0 #undef syslog #define syslog(a, b, ...) while(0){} #endif #endif #define FTPD_SERVER_MESSAGE "RTEMS FTP server (Version 1.1-JWJ) ready." #define FTPD_SYSTYPE "UNIX Type: L8" /* Seem to be unused */ #if 0 #define FTPD_WELCOME_MESSAGE \ "Welcome to the RTEMS FTP server.\n" \ "\n" \ "Login accepted.\n" #endif /* Event to be used by session tasks for waiting */ enum { FTPD_RTEMS_EVENT = RTEMS_EVENT_1 }; /* Configuration table */ static struct rtems_ftpd_configuration *ftpd_config; /* this is not prototyped in strict ansi mode */ FILE *fdopen (int fildes, const char *mode); /*SessionInfo structure. * * The following structure is allocated for each session. */ typedef struct { struct sockaddr_in ctrl_addr; /* Control connection self address */ struct sockaddr_in data_addr; /* Data address set by PORT command */ struct sockaddr_in def_addr; /* Default address for data */ int use_default; /* 1 - use default address for data */ FILE *ctrl_fp; /* File pointer for control connection */ int ctrl_socket; /* Socket for ctrl connection */ int pasv_socket; /* Socket for PASV connection */ int data_socket; /* Socket for data connection */ int idle; /* Timeout in seconds */ int xfer_mode; /* Transfer mode (ASCII/binary) */ rtems_id tid; /* Task id */ char *user; /* user name (0 if not supplied) */ char user_buf[256]; /* user name buffer */ bool auth; /* true if user/pass was valid, false if not or not supplied */ } FTPD_SessionInfo_t; /* * TaskPool structure. */ typedef struct { FTPD_SessionInfo_t *info; FTPD_SessionInfo_t **queue; int count; int head; int tail; rtems_mutex mutex; rtems_counting_semaphore sem; } FTPD_TaskPool_t; /* * Task pool instance. */ static FTPD_TaskPool_t task_pool; /* * Root directory */ static char const* ftpd_root = "/"; /* * Default idle timeout for sockets in seconds. */ static int ftpd_timeout = 0; /* * Global access flags. */ static int ftpd_access = 0; static void yield(void) { /* * If we build not for the legacy network stack, then we use the libbsd. In * the libbsd there is no global network stack semaphore which provides round * robin fairness for threads of equal priority. */ #ifndef RTEMS_NETWORKING sched_yield(); #endif } /* * serr * * Return error string corresponding to current 'errno'. * */ static char const* serr(void) { int err = errno; errno = 0; return strerror(err); } /* * Utility routines for access control. * */ static int can_read(void) { return (ftpd_access & FTPD_NO_READ) == 0; } static int can_write(void) { return (ftpd_access & FTPD_NO_WRITE) == 0; } /* * Task pool management routines * */ /* * task_pool_done * * Cleanup task pool. * * Input parameters: * count - number of entries in task pool to cleanup * * Output parameters: * NONE * */ static void task_pool_done(int count) { int i; for(i = 0; i < count; ++i) rtems_task_delete(task_pool.info[i].tid); free(task_pool.info); free(task_pool.queue); rtems_mutex_destroy(&task_pool.mutex); rtems_counting_semaphore_destroy(&task_pool.sem); task_pool.info = 0; task_pool.queue = 0; task_pool.count = 0; } /* * task_pool_init * * Initialize task pool. * * Input parameters: * count - number of entries in task pool to create * priority - priority tasks are started with * * Output parameters: * returns 1 on success, 0 on failure. * */ static void session(rtems_task_argument arg); /* Forward declare */ static int task_pool_init(int count, rtems_task_priority priority) { int i; rtems_status_code sc; char id = 'a'; task_pool.count = 0; task_pool.head = task_pool.tail = 0; rtems_mutex_init(&task_pool.mutex, "FTPD"); rtems_counting_semaphore_init(&task_pool.sem, "FTPD", (unsigned int) count); task_pool.info = (FTPD_SessionInfo_t*) malloc(sizeof(FTPD_SessionInfo_t) * count); task_pool.queue = (FTPD_SessionInfo_t**) malloc(sizeof(FTPD_SessionInfo_t*) * count); if (NULL == task_pool.info || NULL == task_pool.queue) { task_pool_done(0); syslog(LOG_ERR, "ftpd: Not enough memory"); return 0; } for(i = 0; i < count; ++i) { FTPD_SessionInfo_t *info = &task_pool.info[i]; sc = rtems_task_create(rtems_build_name('F', 'T', 'P', id), priority, FTPD_STACKSIZE, RTEMS_PREEMPT | RTEMS_NO_TIMESLICE | RTEMS_NO_ASR | RTEMS_INTERRUPT_LEVEL(0), RTEMS_FLOATING_POINT | RTEMS_LOCAL, &info->tid); if (sc == RTEMS_SUCCESSFUL) { sc = rtems_task_start( info->tid, session, (rtems_task_argument)info); if (sc != RTEMS_SUCCESSFUL) task_pool_done(i); } else task_pool_done(i + 1); if (sc != RTEMS_SUCCESSFUL) { syslog(LOG_ERR, "ftpd: Could not create/start FTPD session: %s", rtems_status_text(sc)); return 0; } task_pool.queue[i] = task_pool.info + i; if (++id > 'z') id = 'a'; } task_pool.count = count; return 1; } /* * task_pool_obtain * * Obtain free task from task pool. * * Input parameters: * NONE * * Output parameters: * returns pointer to the corresponding SessionInfo structure on success, * NULL if there are no free tasks in the pool. * */ static FTPD_SessionInfo_t* task_pool_obtain(void) { FTPD_SessionInfo_t* info = 0; rtems_counting_semaphore_wait(&task_pool.sem); rtems_mutex_lock(&task_pool.mutex); info = task_pool.queue[task_pool.head]; if(++task_pool.head >= task_pool.count) task_pool.head = 0; rtems_mutex_unlock(&task_pool.mutex); return info; } /* * task_pool_release * * Return task obtained by 'obtain()' back to the task pool. * * Input parameters: * info - pointer to corresponding SessionInfo structure. * * Output parameters: * NONE * */ static void task_pool_release(FTPD_SessionInfo_t* info) { rtems_mutex_lock(&task_pool.mutex); task_pool.queue[task_pool.tail] = info; if(++task_pool.tail >= task_pool.count) task_pool.tail = 0; rtems_mutex_unlock(&task_pool.mutex); rtems_counting_semaphore_post(&task_pool.sem); } /* * End of task pool routines */ /* * Function: send_reply * * * This procedure sends a reply to the client via the control * connection. * * * Input parameters: * code - 3-digit reply code. * text - Reply text. * * Output parameters: * NONE */ static void send_reply(FTPD_SessionInfo_t *info, int code, char *text) { text = text != NULL ? text : ""; fprintf(info->ctrl_fp, "%d %.70s\r\n", code, text); fflush(info->ctrl_fp); } /* * close_socket * * Close socket. * * Input parameters: * s - socket descriptor. * seconds - number of seconds the timeout should be, * if >= 0 - infinite timeout (no timeout). * * Output parameters: * returns 1 on success, 0 on failure. */ static int set_socket_timeout(int s, int seconds) { int res = 0; struct timeval tv; int len = sizeof(tv); if(seconds < 0) seconds = 0; tv.tv_usec = 0; tv.tv_sec = seconds; if(0 != setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, len)) syslog(LOG_ERR, "ftpd: Can't set send timeout on socket: %s.", serr()); else if(0 != setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, len)) syslog(LOG_ERR, "ftpd: Can't set receive timeout on socket: %s.", serr()); else res = 1; return res; } /* * close_socket * * Close socket. * * Input parameters: * s - socket descriptor to be closed. * * Output parameters: * returns 1 on success, 0 on failure */ static int close_socket(int s) { if (0 <= s) { if (0 != close(s)) { shutdown(s, 2); if (0 != close(s)) return 0; } } return 1; } /* * data_socket * * Create data socket for session. * * Input parameters: * info - corresponding SessionInfo structure * * Output parameters: * returns socket descriptor, or -1 if failure * */ static int data_socket(FTPD_SessionInfo_t *info) { int s = info->pasv_socket; if(0 > s) { int on = 1; s = socket(PF_INET, SOCK_STREAM, 0); if(0 > s) send_reply(info, 425, "Can't create data socket."); else if(0 > setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) { close_socket(s); s = -1; } else { struct sockaddr_in data_source; int tries; /* anchor socket to avoid multi-homing problems */ data_source = info->ctrl_addr; data_source.sin_port = htons(20); /* ftp-data port */ for(tries = 1; tries < 10; ++tries) { errno = 0; if(bind(s, (struct sockaddr *)&data_source, sizeof(data_source)) >= 0) break; if (errno != EADDRINUSE) tries = 10; else rtems_task_wake_after(tries * 10); } if(tries >= 10) { send_reply(info, 425, "Can't bind data socket."); close_socket(s); s = -1; } else { struct sockaddr_in *data_dest = (info->use_default) ? &info->def_addr : &info->data_addr; if(0 > connect(s, (struct sockaddr *)data_dest, sizeof(*data_dest))) { send_reply(info, 425, "Can't connect data socket."); close_socket(s); s = -1; } } } } info->data_socket = s; info->use_default = 1; if(s >= 0) set_socket_timeout(s, info->idle); return s; } /* * close_data_socket * * Close data socket for session. * * Input parameters: * info - corresponding SessionInfo structure * * Output parameters: * NONE * */ static void close_data_socket(FTPD_SessionInfo_t *info) { /* As at most one data socket could be open simultaneously and in some cases data_socket == pasv_socket, we select socket to close, then close it. */ int s = info->data_socket; if(0 > s) s = info->pasv_socket; if(!close_socket(s)) syslog(LOG_ERR, "ftpd: Error closing data socket."); info->data_socket = -1; info->pasv_socket = -1; info->use_default = 1; } /* * close_stream * * Close control stream of session. * * Input parameters: * info - corresponding SessionInfo structure * * Output parameters: * NONE * */ static void close_stream(FTPD_SessionInfo_t* info) { if (NULL != info->ctrl_fp) { if (0 != fclose(info->ctrl_fp)) { syslog(LOG_ERR, "ftpd: Could not close control stream: %s", serr()); } else info->ctrl_socket = -1; } if (!close_socket(info->ctrl_socket)) syslog(LOG_ERR, "ftpd: Could not close control socket: %s", serr()); info->ctrl_fp = NULL; info->ctrl_socket = -1; } /* * send_mode_reply * * Sends BINARY/ASCII reply string depending on current transfer mode. * * Input parameters: * info - corresponding SessionInfo structure * * Output parameters: * NONE * */ static void send_mode_reply(FTPD_SessionInfo_t *info) { if(info->xfer_mode == TYPE_I) send_reply(info, 150, "Opening BINARY mode data connection."); else send_reply(info, 150, "Opening ASCII mode data connection."); } /* * command_retrieve * * Perform the "RETR" command (send file to client). * * Input parameters: * info - corresponding SessionInfo structure * char *filename - source filename. * * Output parameters: * NONE * */ static void command_retrieve(FTPD_SessionInfo_t *info, char const *filename) { int s = -1; int fd = -1; char buf[FTPD_DATASIZE]; struct stat stat_buf; int res = 0; if(!can_read() || !info->auth) { send_reply(info, 550, "Access denied."); return; } if (0 > (fd = open(filename, O_RDONLY))) { send_reply(info, 550, "Error opening file."); return; } if (fstat(fd, &stat_buf) == 0 && S_ISDIR(stat_buf.st_mode)) { if (-1 != fd) close(fd); send_reply(info, 550, "Is a directory."); return; } send_mode_reply(info); s = data_socket(info); if (0 <= s) { int n = -1; if(info->xfer_mode == TYPE_I) { while ((n = read(fd, buf, FTPD_DATASIZE)) > 0) { if(send(s, buf, n, 0) != n) break; yield(); } } else if (info->xfer_mode == TYPE_A) { int rest = 0; while (rest == 0 && (n = read(fd, buf, FTPD_DATASIZE)) > 0) { char const* e = buf; char const* b; int i; rest = n; do { char lf = '\0'; b = e; for(i = 0; i < rest; ++i, ++e) { if(*e == '\n') { lf = '\n'; break; } } if(send(s, b, i, 0) != i) break; if(lf == '\n') { if(send(s, "\r\n", 2, 0) != 2) break; ++e; ++i; } } while((rest -= i) > 0); yield(); } } if (0 == n) { if (0 == close(fd)) { fd = -1; res = 1; } } } if (-1 != fd) close(fd); if (0 == res) send_reply(info, 451, "File read error."); else send_reply(info, 226, "Transfer complete."); close_data_socket(info); return; } /* * discard * * Analog of `write' routine that just discards passed data * * Input parameters: * fd - file descriptor (ignored) * buf - data to write (ignored) * count - number of bytes in `buf' * * Output parameters: * returns `count' * */ static ssize_t discard(int fd, void const* buf, size_t count) { (void)fd; (void)buf; return count; } /* * command_store * * Performs the "STOR" command (receive data from client). * * Input parameters: * info - corresponding SessionInfo structure * char *filename - Destination filename. * * Output parameters: * NONE */ static void command_store(FTPD_SessionInfo_t *info, char const *filename) { int s; int n; unsigned long size = 0; struct rtems_ftpd_hook *usehook = NULL; char buf[FTPD_DATASIZE]; int res = 1; int bare_lfs = 0; int null = 0; typedef ssize_t (*WriteProc)(int, void const*, size_t); WriteProc wrt = &write; if(!can_write() || !info->auth) { send_reply(info, 550, "Access denied."); return; } send_mode_reply(info); s = data_socket(info); if(0 > s) return; null = !strcmp("/dev/null", filename); if (null) { /* File "/dev/null" just throws data away. * FIXME: this is hack. Using `/dev/null' filesystem entry would be * better. */ wrt = &discard; } if (!null && ftpd_config->hooks != NULL) { /* Search our list of hooks to see if we need to do something special. */ struct rtems_ftpd_hook *hook; int i; i = 0; hook = &ftpd_config->hooks[i++]; while (hook->filename != NULL) { if (!strcmp(hook->filename, filename)) { usehook = hook; break; } hook = &ftpd_config->hooks[i++]; } } if (usehook != NULL) { /* * OSV: FIXME: Small buffer could be used and hook routine * called multiple times instead. Alternatively, the support could be * removed entirely in favor of configuring RTEMS pseudo-device with * given name. */ char *bigBufr; size_t filesize = ftpd_config->max_hook_filesize + 1; /* * Allocate space for our "file". */ bigBufr = (char *)malloc(filesize); if (bigBufr == NULL) { send_reply(info, 451, "Local resource failure: malloc."); close_data_socket(info); return; } /* * Retrieve the file into our buffer space. */ size = 0; while ((n = recv(s, bigBufr + size, filesize - size, 0)) > 0) { size += n; } if (size >= filesize) { send_reply(info, 451, "File too long: buffer size exceeded."); free(bigBufr); close_data_socket(info); return; } /* * Call our hook. */ res = (usehook->hook_function)(bigBufr, size) == 0; free(bigBufr); if(!res) { send_reply(info, 451, "File processing failed."); close_data_socket(info); return; } } else { /* Data transfer to regular file or /dev/null. */ int fd = 0; if(!null) fd = creat(filename, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); if (0 > fd) { send_reply(info, 550, "Error creating file."); close_data_socket(info); return; } if(info->xfer_mode == TYPE_I) { while ((n = recv(s, buf, FTPD_DATASIZE, 0)) > 0) { if (wrt(fd, buf, n) != n) { res = 0; break; } yield(); } } else if(info->xfer_mode == TYPE_A) { int rest = 0; int pended_cr = 0; while (res && rest == 0 && (n = recv(s, buf, FTPD_DATASIZE, 0)) > 0) { char const* e = buf; char const* b; int i; rest = n; if(pended_cr && *e != '\n') { char const lf = '\r'; pended_cr = 0; if(wrt(fd, &lf, 1) != 1) { res = 0; break; } } do { int count; int sub = 0; b = e; for(i = 0; i < rest; ++i, ++e) { int pcr = pended_cr; pended_cr = 0; if(*e == '\r') { pended_cr = 1; } else if(*e == '\n') { if(pcr) { sub = 2; ++i; ++e; break; } ++bare_lfs; } } if(res == 0) break; count = i - sub - pended_cr; if(count > 0 && wrt(fd, b, count) != count) { res = 0; break; } if(sub == 2 && wrt(fd, e - 1, 1) != 1) res = 0; } while((rest -= i) > 0); yield(); } } if (0 > close(fd) || res == 0) { send_reply(info, 452, "Error writing file."); close_data_socket(info); return; } } if (bare_lfs > 0) { snprintf(buf, FTPD_BUFSIZE, "Transfer complete. WARNING! %d bare linefeeds received in ASCII mode.", bare_lfs); send_reply(info, 226, buf); } else send_reply(info, 226, "Transfer complete."); close_data_socket(info); } /* * send_dirline * * Sends one line of LIST command reply corresponding to single file. * * Input parameters: * s - socket descriptor to send data to * wide - if 0, send only file name. If not 0, send 'stat' info as well in * "ls -l" format. * curTime - current time * path - path to be prepended to what is given by 'add' * add - path to be appended to what is given by 'path', the resulting path * is then passed to 'stat()' routine * name - file name to be reported in output * buf - buffer for temporary data * * Output parameters: * returns 0 on failure, 1 on success * */ static int send_dirline(int s, int wide, time_t curTime, char const* path, char const* add, char const* fname, char* buf) { if(wide) { struct stat stat_buf; int plen = strlen(path); int alen = strlen(add); if(plen == 0) { buf[plen++] = '/'; buf[plen] = '\0'; } else { strcpy(buf, path); if(alen > 0 && buf[plen - 1] != '/') { buf[plen++] = '/'; if(plen >= FTPD_BUFSIZE) return 0; buf[plen] = '\0'; } } if(plen + alen >= FTPD_BUFSIZE) return 0; strcpy(buf + plen, add); if (stat(buf, &stat_buf) == 0) { int len; struct tm bt; time_t tf = stat_buf.st_mtime; enum { SIZE = 80 }; time_t SIX_MONTHS = (365L*24L*60L*60L)/2L; char timeBuf[SIZE]; gmtime_r(&tf, &bt); if(curTime > tf + SIX_MONTHS || tf > curTime + SIX_MONTHS) strftime (timeBuf, SIZE, "%b %d %Y", &bt); else strftime (timeBuf, SIZE, "%b %d %H:%M", &bt); len = snprintf(buf, FTPD_BUFSIZE, "%c%c%c%c%c%c%c%c%c%c 1 %5d %5d %11u %s %s\r\n", (S_ISLNK(stat_buf.st_mode)?('l'): (S_ISDIR(stat_buf.st_mode)?('d'):('-'))), (stat_buf.st_mode & S_IRUSR)?('r'):('-'), (stat_buf.st_mode & S_IWUSR)?('w'):('-'), (stat_buf.st_mode & S_IXUSR)?('x'):('-'), (stat_buf.st_mode & S_IRGRP)?('r'):('-'), (stat_buf.st_mode & S_IWGRP)?('w'):('-'), (stat_buf.st_mode & S_IXGRP)?('x'):('-'), (stat_buf.st_mode & S_IROTH)?('r'):('-'), (stat_buf.st_mode & S_IWOTH)?('w'):('-'), (stat_buf.st_mode & S_IXOTH)?('x'):('-'), (int)stat_buf.st_uid, (int)stat_buf.st_gid, (int)stat_buf.st_size, timeBuf, fname ); if(send(s, buf, len, 0) != len) return 0; } } else { int len = snprintf(buf, FTPD_BUFSIZE, "%s\r\n", fname); if(send(s, buf, len, 0) != len) return 0; } return 1; } /* * command_list * * Send file list to client. * * Input parameters: * info - corresponding SessionInfo structure * char *fname - File (or directory) to list. * * Output parameters: * NONE */ static void command_list(FTPD_SessionInfo_t *info, char const *fname, int wide) { int s; DIR *dirp = 0; struct dirent *dp = 0; struct stat stat_buf; char buf[FTPD_BUFSIZE]; time_t curTime; int sc = 1; if(!info->auth) { send_reply(info, 550, "Access denied."); return; } send_reply(info, 150, "Opening ASCII mode data connection for LIST."); s = data_socket(info); if(0 > s) { syslog(LOG_ERR, "ftpd: Error connecting to data socket."); return; } if(fname[0] == '\0') fname = "."; if (0 > stat(fname, &stat_buf)) { snprintf(buf, FTPD_BUFSIZE, "%s: No such file or directory.\r\n", fname); send(s, buf, strlen(buf), 0); } else if (S_ISDIR(stat_buf.st_mode) && (NULL == (dirp = opendir(fname)))) { snprintf(buf, FTPD_BUFSIZE, "%s: Can not open directory.\r\n", fname); send(s, buf, strlen(buf), 0); } else { time(&curTime); if(!dirp && *fname) sc = sc && send_dirline(s, wide, curTime, fname, "", fname, buf); else { /* FIXME: need "." and ".." only when '-a' option is given */ sc = sc && send_dirline(s, wide, curTime, fname, "", ".", buf); sc = sc && send_dirline(s, wide, curTime, fname, (strcmp(fname, ftpd_root) ? ".." : ""), "..", buf); while (sc && (dp = readdir(dirp)) != NULL) sc = sc && send_dirline(s, wide, curTime, fname, dp->d_name, dp->d_name, buf); } } if(dirp) closedir(dirp); close_data_socket(info); if(sc) send_reply(info, 226, "Transfer complete."); else send_reply(info, 426, "Connection aborted."); } /* * command_cwd * * Change current working directory. * * Input parameters: * info - corresponding SessionInfo structure * dir - directory name passed in CWD command * * Output parameters: * NONE * */ static void command_cwd(FTPD_SessionInfo_t *info, char *dir) { if(!info->auth) { send_reply(info, 550, "Access denied."); return; } if(chdir(dir) == 0) send_reply(info, 250, "CWD command successful."); else send_reply(info, 550, "CWD command failed."); } /* * command_pwd * * Send current working directory to client. * * Input parameters: * info - corresponding SessionInfo structure * * Output parameters: * NONE */ static void command_pwd(FTPD_SessionInfo_t *info) { char buf[FTPD_BUFSIZE]; char const* cwd; errno = 0; buf[0] = '"'; if(!info->auth) { send_reply(info, 550, "Access denied."); return; } cwd = getcwd(buf + 1, FTPD_BUFSIZE - 4); if(cwd) { int len = strlen(cwd); static char const txt[] = "\" is the current directory."; int size = sizeof(txt); if(len + size + 1 >= FTPD_BUFSIZE) size = FTPD_BUFSIZE - len - 2; memcpy(buf + len + 1, txt, size); buf[len + size] = '\0'; send_reply(info, 250, buf); } else { snprintf(buf, FTPD_BUFSIZE, "Error: %s.", serr()); send_reply(info, 452, buf); } } /* * command_mdtm * * Handle FTP MDTM command (send file modification time to client)/ * * Input parameters: * info - corresponding SessionInfo structure * fname - file name passed in MDTM command * * Output parameters: * info->cwd is set to new CWD value. */ static void command_mdtm(FTPD_SessionInfo_t *info, char const* fname) { struct stat stbuf; char buf[FTPD_BUFSIZE]; if(!info->auth) { send_reply(info, 550, "Access denied."); return; } if (0 > stat(fname, &stbuf)) { snprintf(buf, FTPD_BUFSIZE, "%s: %s.", fname, serr()); send_reply(info, 550, buf); } else { struct tm *t = gmtime(&stbuf.st_mtime); snprintf(buf, FTPD_BUFSIZE, "%04d%02d%02d%02d%02d%02d", 1900 + t->tm_year, t->tm_mon+1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); send_reply(info, 213, buf); } } static void command_size(FTPD_SessionInfo_t *info, char const* fname) { struct stat stbuf; char buf[FTPD_BUFSIZE]; if(!info->auth) { send_reply(info, 550, "Access denied."); return; } if (info->xfer_mode != TYPE_I || 0 > stat(fname, &stbuf) || stbuf.st_size < 0) { send_reply(info, 550, "Could not get file size."); } else { snprintf(buf, FTPD_BUFSIZE, "%" PRIuMAX, (uintmax_t) stbuf.st_size); send_reply(info, 213, buf); } } /* * command_port * * This procedure fills address for data connection given the IP address and * port of the remote machine. * * Input parameters: * info - corresponding SessionInfo structure * args - arguments to the "PORT" command. * * Output parameters: * info->data_addr is set according to arguments of the PORT command. * info->use_default is set to 0 on success, 1 on failure. */ static void command_port(FTPD_SessionInfo_t *info, char const *args) { enum { NUM_FIELDS = 6 }; unsigned int a[NUM_FIELDS]; int n; close_data_socket(info); n = sscanf(args, "%u,%u,%u,%u,%u,%u", a+0, a+1, a+2, a+3, a+4, a+5); if(NUM_FIELDS == n) { int i; union { uint8_t b[NUM_FIELDS]; struct { uint32_t ip; uint16_t port; } u ; } ip_info; for(i = 0; i < NUM_FIELDS; ++i) { if(a[i] > 255) break; ip_info.b[i] = (uint8_t)a[i]; } if(i == NUM_FIELDS) { /* Note: while it contradicts with RFC959, we don't allow PORT command * to specify IP address different than those of the originating client * for the sake of safety. */ if (ip_info.u.ip == info->def_addr.sin_addr.s_addr) { info->data_addr.sin_addr.s_addr = ip_info.u.ip; info->data_addr.sin_port = ip_info.u.port; info->data_addr.sin_family = AF_INET; memset(info->data_addr.sin_zero, 0, sizeof(info->data_addr.sin_zero)); info->use_default = 0; send_reply(info, 200, "PORT command successful."); return; /* success */ } else { send_reply(info, 425, "Address doesn't match peer's IP."); return; } } } send_reply(info, 501, "Syntax error."); } /* * command_pasv * * Handle FTP PASV command. * Open socket, listen for and accept connection on it. * * Input parameters: * info - corresponding SessionInfo structure * * Output parameters: * info->pasv_socket is set to the descriptor of the data socket */ static void command_pasv(FTPD_SessionInfo_t *info) { int s = -1; int err = 1; close_data_socket(info); s = socket(PF_INET, SOCK_STREAM, 0); if (s < 0) syslog(LOG_ERR, "ftpd: Error creating PASV socket: %s", serr()); else { struct sockaddr_in addr; socklen_t addrLen = sizeof(addr); addr = info->ctrl_addr; addr.sin_port = htons(0); if (0 > bind(s, (struct sockaddr *)&addr, addrLen)) syslog(LOG_ERR, "ftpd: Error binding PASV socket: %s", serr()); else if (0 > listen(s, 1)) syslog(LOG_ERR, "ftpd: Error listening on PASV socket: %s", serr()); else if(set_socket_timeout(s, info->idle)) { char buf[FTPD_BUFSIZE]; unsigned char const *ip, *p; getsockname(s, (struct sockaddr *)&addr, &addrLen); ip = (unsigned char const*)&(addr.sin_addr); p = (unsigned char const*)&(addr.sin_port); snprintf(buf, FTPD_BUFSIZE, "Entering passive mode (%u,%u,%u,%u,%u,%u).", ip[0], ip[1], ip[2], ip[3], p[0], p[1]); send_reply(info, 227, buf); info->pasv_socket = accept(s, (struct sockaddr *)&addr, &addrLen); if (0 > info->pasv_socket) syslog(LOG_ERR, "ftpd: Error accepting PASV connection: %s", serr()); else { close_socket(s); s = -1; err = 0; } } } if(err) { /* (OSV) The note is from FreeBSD FTPD. * Note: a response of 425 is not mentioned as a possible response to * the PASV command in RFC959. However, it has been blessed as a * legitimate response by Jon Postel in a telephone conversation * with Rick Adams on 25 Jan 89. */ send_reply(info, 425, "Can't open passive connection."); close_socket(s); } } /* * skip_options * * Utility routine to skip options (if any) from input command. * * Input parameters: * p - pointer to pointer to command * * Output parameters: * p - is changed to point to first non-option argument */ static void skip_options(char **p) { char* buf = *p; char* last = NULL; while(1) { while(isspace((unsigned char)*buf)) ++buf; if(*buf == '-') { if(*++buf == '-') { /* `--' should terminate options */ if(isspace((unsigned char)*++buf)) { last = buf; do ++buf; while(isspace((unsigned char)*buf)); break; } } while(*buf && !isspace((unsigned char)*buf)) ++buf; last = buf; } else break; } if(last) *last = '\0'; *p = buf; } /* * split_command * * Split command into command itself, options, and arguments. Command itself * is converted to upper case. * * Input parameters: * buf - initial command string * * Output parameter: * buf - is modified by inserting '\0' at ends of split entities * cmd - upper-cased command code * opts - string containing all the options * args - string containing all the arguments */ static void split_command(char *buf, char **cmd, char **opts, char **args) { char* eoc; char* p = buf; while(isspace((unsigned char)*p)) ++p; *cmd = p; while(*p && !isspace((unsigned char)*p)) { *p = toupper((unsigned char)*p); ++p; } eoc = p; if(*p) *p++ = '\0'; while(isspace((unsigned char)*p)) ++p; *opts = p; skip_options(&p); *args = p; if(*opts == p) *opts = eoc; while(*p && *p != '\r' && *p != '\n') ++p; if(*p) *p++ = '\0'; } /* * exec_command * * Parse and execute FTP command. * * FIXME: This section is somewhat of a hack. We should have a better * way to parse commands. * * Input parameters: * info - corresponding SessionInfo structure * cmd - command to be executed (upper-case) * args - arguments of the command * * Output parameters: * NONE */ static void exec_command(FTPD_SessionInfo_t *info, char* cmd, char* args) { char fname[FTPD_BUFSIZE]; int wrong_command = 0; fname[0] = '\0'; if (!strcmp("PORT", cmd)) { command_port(info, args); } else if (!strcmp("PASV", cmd)) { command_pasv(info); } else if (!strcmp("RETR", cmd)) { strncpy(fname, args, 254); command_retrieve(info, fname); } else if (!strcmp("STOR", cmd)) { strncpy(fname, args, 254); command_store(info, fname); } else if (!strcmp("LIST", cmd)) { strncpy(fname, args, 254); command_list(info, fname, 1); } else if (!strcmp("NLST", cmd)) { strncpy(fname, args, 254); command_list(info, fname, 0); } else if (!strcmp("MDTM", cmd)) { strncpy(fname, args, 254); command_mdtm(info, fname); } else if (!strcmp("SIZE", cmd)) { strncpy(fname, args, 254); command_size(info, fname); } else if (!strcmp("SYST", cmd)) { send_reply(info, 215, FTPD_SYSTYPE); } else if (!strcmp("TYPE", cmd)) { if (args[0] == 'I') { info->xfer_mode = TYPE_I; send_reply(info, 200, "Type set to I."); } else if (args[0] == 'A') { info->xfer_mode = TYPE_A; send_reply(info, 200, "Type set to A."); } else { info->xfer_mode = TYPE_I; send_reply(info, 504, "Type not implemented. Set to I."); } } else if (!strcmp("USER", cmd)) { strlcpy(info->user_buf, args, sizeof(info->user_buf)); info->user = info->user_buf; if (ftpd_config->login && !ftpd_config->login(info->user, NULL)) { info->auth = false; send_reply(info, 331, "User name okay, need password."); } else { info->auth = true; send_reply(info, 230, "User logged in."); } } else if (!strcmp("PASS", cmd)) { if (!info->user) { send_reply(info, 332, "Need account to log in"); } else { if (ftpd_config->login && !ftpd_config->login(info->user, args)) { info->auth = false; send_reply(info, 530, "Not logged in."); } else { info->auth = true; send_reply(info, 230, "User logged in."); } } } else if (!strcmp("DELE", cmd)) { if(!can_write() || !info->auth) { send_reply(info, 550, "Access denied."); } else if ( strncpy(fname, args, 254) && unlink(fname) == 0) { send_reply(info, 257, "DELE successful."); } else { send_reply(info, 550, "DELE failed."); } } else if (!strcmp("SITE", cmd)) { char* opts; split_command(args, &cmd, &opts, &args); if(!strcmp("CHMOD", cmd)) { int mask; if(!can_write() || !info->auth) { send_reply(info, 550, "Access denied."); } else { char *c; c = strchr(args, ' '); if((c != NULL) && (sscanf(args, "%o", &mask) == 1) && strncpy(fname, c+1, 254) && (chmod(fname, (mode_t)mask) == 0)) send_reply(info, 257, "CHMOD successful."); else send_reply(info, 550, "CHMOD failed."); } } else wrong_command = 1; } else if (!strcmp("RMD", cmd)) { if(!can_write() || !info->auth) { send_reply(info, 550, "Access denied."); } else if ( strncpy(fname, args, 254) && rmdir(fname) == 0) { send_reply(info, 257, "RMD successful."); } else { send_reply(info, 550, "RMD failed."); } } else if (!strcmp("MKD", cmd)) { if(!can_write() || !info->auth) { send_reply(info, 550, "Access denied."); } else if ( strncpy(fname, args, 254) && mkdir(fname, S_IRWXU | S_IRWXG | S_IRWXO) == 0) { send_reply(info, 257, "MKD successful."); } else { send_reply(info, 550, "MKD failed."); } } else if (!strcmp("CWD", cmd)) { strncpy(fname, args, 254); command_cwd(info, fname); } else if (!strcmp("CDUP", cmd)) { command_cwd(info, ".."); } else if (!strcmp("PWD", cmd)) { command_pwd(info); } else wrong_command = 1; if(wrong_command) send_reply(info, 500, "Command not understood."); } /* * session * * This task handles single session. It is waked up when the FTP daemon gets a * service request from a remote machine. Here, we watch for commands that * will come through the control connection. These commands are then parsed * and executed until the connection is closed, either unintentionally or * intentionally with the "QUIT" command. * * Input parameters: * arg - pointer to corresponding SessionInfo. * * Output parameters: * NONE */ static void session(rtems_task_argument arg) { FTPD_SessionInfo_t *const info = (FTPD_SessionInfo_t *)arg; int chroot_made = 0; rtems_libio_set_private_env(); /* chroot() can fail here because the directory may not exist yet. */ chroot_made = chroot(ftpd_root) == 0; while(1) { rtems_event_set set; int rv; rtems_event_receive(FTPD_RTEMS_EVENT, RTEMS_EVENT_ANY, RTEMS_NO_TIMEOUT, &set); chroot_made = chroot_made || chroot(ftpd_root) == 0; rv = chroot_made ? chdir("/") : -1; errno = 0; if (rv == 0) { send_reply(info, 220, FTPD_SERVER_MESSAGE); while (1) { char buf[FTPD_BUFSIZE]; char *cmd, *opts, *args; if (fgets(buf, FTPD_BUFSIZE, info->ctrl_fp) == NULL) { syslog(LOG_INFO, "ftpd: Connection aborted."); break; } split_command(buf, &cmd, &opts, &args); if (!strcmp("QUIT", cmd)) { send_reply(info, 221, "Goodbye."); break; } else { exec_command(info, cmd, args); } } } else { send_reply(info, 421, "Service not available, closing control connection."); } /* * Go back to the root directory. A use case is to release a current * directory in a mounted file system on dynamic media, e.g. USB stick. * The return value can be ignored since the next session will try do the * operation again and an error check is performed in this case. */ chdir("/"); /* Close connection and put ourselves back into the task pool. */ close_data_socket(info); close_stream(info); task_pool_release(info); } } /* * ftpd_daemon * * This task runs forever. It waits for service requests on the FTP port * (port 21 by default). When a request is received, it opens a new session * to handle those requests until the connection is closed. * * Input parameters: * NONE * * Output parameters: * NONE */ static void ftpd_daemon(rtems_task_argument args RTEMS_UNUSED) { int s; socklen_t addrLen; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(ftpd_config->port); addr.sin_addr.s_addr = htonl(INADDR_ANY); s = socket(PF_INET, SOCK_STREAM, 0); if (s < 0) syslog(LOG_ERR, "ftpd: Error creating control socket: %s", serr()); else if (0 > bind(s, (struct sockaddr *)&addr, sizeof(addr))) syslog(LOG_ERR, "ftpd: Error binding control socket: %s", serr()); else if (0 > listen(s, 1)) syslog(LOG_ERR, "ftpd: Error listening on control socket: %s", serr()); else while (1) { int ss; addrLen = sizeof(addr); ss = accept(s, (struct sockaddr *)&addr, &addrLen); if (0 > ss) syslog(LOG_ERR, "ftpd: Error accepting control connection: %s", serr()); else if(!set_socket_timeout(ss, ftpd_timeout)) close_socket(ss); else { FTPD_SessionInfo_t *info; info = task_pool_obtain(); if (NULL == info) { close_socket(ss); } else { info->ctrl_socket = ss; if ((info->ctrl_fp = fdopen(info->ctrl_socket, "r+")) == NULL) { syslog(LOG_ERR, "ftpd: fdopen() on socket failed: %s", serr()); close_stream(info); task_pool_release(info); } else { /* Initialize corresponding SessionInfo structure */ info->def_addr = addr; if(0 > getsockname(ss, (struct sockaddr *)&addr, &addrLen)) { syslog(LOG_ERR, "ftpd: getsockname(): %s", serr()); close_stream(info); task_pool_release(info); } else { info->use_default = 1; info->ctrl_addr = addr; info->pasv_socket = -1; info->data_socket = -1; info->xfer_mode = TYPE_A; info->data_addr.sin_port = htons(ntohs(info->ctrl_addr.sin_port) - 1); info->idle = ftpd_timeout; info->user = NULL; if (ftpd_config->login) info->auth = false; else info->auth = true; /* Wakeup the session task. The task will call task_pool_release after it closes connection. */ rtems_event_send(info->tid, FTPD_RTEMS_EVENT); } } } } } close(s); rtems_task_exit(); } /* * rtems_ftpd_start * * Here, we start the FTPD task which waits for FTP requests and services * them. This procedure returns to its caller once the task is started. * * * Input parameters: * config: constant initial setup. * Output parameters: * returns RTEMS_SUCCESSFUL on successful start of the daemon. */ int rtems_ftpd_start(const struct rtems_ftpd_configuration* config) { rtems_status_code sc; rtems_id tid; rtems_task_priority priority; int count; if (ftpd_config != NULL) return RTEMS_RESOURCE_IN_USE; ftpd_config = malloc(sizeof(*ftpd_config)); if (ftpd_config == NULL) return RTEMS_NO_MEMORY; *ftpd_config = *config; if (ftpd_config->port == 0) { ftpd_config->port = FTPD_CONTROL_PORT; } if (ftpd_config->priority == 0) { ftpd_config->priority = 40; } priority = ftpd_config->priority; ftpd_timeout = ftpd_config->idle; if (ftpd_timeout < 0) ftpd_timeout = 0; ftpd_config->idle = ftpd_timeout; ftpd_access = ftpd_config->access; ftpd_root = "/"; if (ftpd_config->root && ftpd_config->root[0] == '/' ) ftpd_root = ftpd_config->root; ftpd_config->root = ftpd_root; if (ftpd_config->tasks_count <= 0) ftpd_config->tasks_count = 1; count = ftpd_config->tasks_count; if (!task_pool_init(count, priority)) { syslog(LOG_ERR, "ftpd: Could not initialize task pool."); return RTEMS_UNSATISFIED; } sc = rtems_task_create(rtems_build_name('F', 'T', 'P', 'D'), priority, RTEMS_MINIMUM_STACK_SIZE, RTEMS_PREEMPT | RTEMS_NO_TIMESLICE | RTEMS_NO_ASR | RTEMS_INTERRUPT_LEVEL(0), RTEMS_FLOATING_POINT | RTEMS_LOCAL, &tid); if (sc == RTEMS_SUCCESSFUL) { sc = rtems_task_start(tid, ftpd_daemon, 0); if (sc != RTEMS_SUCCESSFUL) rtems_task_delete(tid); } if (sc != RTEMS_SUCCESSFUL) { task_pool_done(count); syslog(LOG_ERR, "ftpd: Could not create/start FTP daemon: %s", rtems_status_text(sc)); return RTEMS_UNSATISFIED; } if (ftpd_config->verbose) syslog(LOG_INFO, "ftpd: FTP daemon started (%d session%s max)", count, ((count > 1) ? "s" : "")); return RTEMS_SUCCESSFUL; }