#pragma save_binary /* * ftpd.c: * * This is an implementation of an ftpd daemon written in LPC using the * MudOS socket efunctions. This version of ftpd will communicate with * standard ftp clients such as the 'ftp' program found on most all UNIX * systems. This version supports _both_ ASCII _and_ BINARY transfers. * * Edit /include/net/ftpdconf.h and /include/net/ftpdsupp.h to configure * ftpd for your particular mudlib. * * Note: This object runs with ROOT_UID and depends on the correctness * of the valid_read() and valid_write() functions in the master * mudlib object. * If these two functions aren't accurate, then this object (ftpd.c) * will allow anyone to harm your mudlib. * * valid_read is called as follows: * * MASTER_OB->valid_read(filename, user, "read_file" ) * * It should return '1' only if user 'user' has permission to * read the file 'filename'. * * valid_write is called as follows: * * MASTER_OB->valid_write(filename, user, "write_file") * * It should return 1 only if user 'user' has permission to write * (or remove) the file 'filename'. */ /* * Originaly written by Pinkfish@Discworld * * 93-08-22 Bannor@newmoon * - Added DEBUG and LOGGING flags and #ifdef's * - Fixed a problem with sending files larger then 1k. * - Added support for events. * - Added support for finger ftp. * - Fixed security problems that allowed anyone to read/write * to any directory. * - Improved readability (well, I think so anyway) * - Removed all those silly "bing", "bits" and "blue" * variables. * * 93-11-21 Truilkan@Gestalt * - modified to work on tmi-2 (interface with tmi-2 security etc) * the motivation behind this port was to hopefully lessen the * number of people asking me how to make the ftpd.c written in C * work (seemingly only C/UNIX gurus and otherwise persistent * people could make it work). This LPC version of ftpd is also * easier for LPC coders to customize to fix a particular mudlib's * security code (directory access permissions etc). For use with * MudOS version 0.9.18.20 or later. * * TODO: the return code from each socket_write() should be checked and * appropriately handled. Presently only the data xfer socket_write() * calls are checking the return type. This is probably enough * to ensure that ftpd.c works correctly most of the time but in * order to be robust, the additional checking should be done. * In addition, the socket_write() return code should be checked * for EECALLBACK as well as EEWOULDBLOCK. See the socket_write * function in the MudOS driver file socket_efuns.c for more * information on the possible return codes of the socket_write() efun. * * 93-11-22 Truilkan@Gestalt * o Bumped version number to 2.0 * o Fixed check_connections() so that it actually does timeout idle ftp users. * o Added code to restrict number of users. See FTPD_MAX_USERS in ftpdconf.h. * * 93-11-28 Inspiral@Tabor * o Bumped version number to 3.0 * o Added another field to the mapping, which will hold the ftp user's * connection object. This allows master.c to work properly even if * the wizard is not currently logged into the mud. * o Above change meant altering check_password() in ftpd.h * o Above change also meant adding disable_status_check() in connection.c * This needs to be added: void disable_status_check() { * remove_call_out( "check_status" ); * } * o Added code for support of multi-worded passwords (Thanks, Domini@Ultimate) * o Removed fix in Version 2 which used reload_object() in check_password() * o Cleaned up some redundant code. * o Added socket_address() calls in login/logout logs * o Altered LOG_FILE define to specify log destination. * o Appended output of "ls" and "dir" commands with a "/" on directories. * o Added LOG_TIME define for those who like to see what time things occur. * * 93-12-15 Inspiral@Tabor * o Generalized and macro'd the socket_address() calls, * and added ip site resolve() * * 93-12-17 Inspiral@Tabor * o Added support for anonymous ftp logins ... Read the file * "/include/net/anon_ftp.doc" for instructions on installing it. * o Found/fixed a bug with "cd", which allowed you to cd into a file. * * 94-01-11 Inspiral@Tabor * o Bumped version number to 4.0 * o Incorporated fixes for valid_read/write when user not also * logged into the mud (Beek@DistortionII) * o Above fix allowed removal of LINK field from connection mapping, * as well as the disable_status_check() function in connection.c * o Added support for BINARY mode file transfers (0.9.18.20+) * o Re-installed reload_object() support in check_password() * o Added LOGIN_MSG support at login time. * * 94.04.08 - Robocoder@TMI-2 * o Bumprev'd to 4.1 * o Added 'DELE' command. * o Added file locking protection for 'APPE', 'STOR' and 'DELE'. * o Corrected some typos (and NorthAm'd the spelling of "unrecognised") * o Some minor formatting changes. * * 94.04.09 - Robocoder@TMI-2 * o Added #define check for FLOCK_D for muds that don't support file locking * o Added flock_wrapper() to give flock daemon better logging info * * 94.04.10 - Robocoder@TMI-2 * o Fixed logout() to not close the connection when it receives another * 'USER' command and is already logged in * * 94.04.25 - Robocoder@TMI-2 * o Added logout() calls in various places to solve problem of a ftp * session closing immediately after connecting (for a new user, reusing * a socket mapping not properly cleaned up) * o Slight change to resolve_callback(), cause I don't like seeing * "... connected from 0" or "... logged out from 0" * * 94.05.11 - Robocoder@TMI-2 * o Fixed query_connections() to only return the non-zero subset of the * "list" array (pre-allocat()'d) (see usage by TMI's "/cmds/std/_who.c") * o Fixed file unlocking when download aborted. * * 94.05.19 - Robocoder@TMI-2 * o Bumprev'd to 4.2 * o Added sitecheck support (TMI specific); configurable * o Made FLOCK support explicit in ftpdconf.h * o Fixed check_password() to not set uid of connection object to "Root" * o Added config option for varying read & write level access * o Added LOG_NO_CONNECT config option (for more evil admin logging) * o Moved "code" in header files to ftpdsupp.h...one more file to modify * for configuring lpc_ftpd to your mudlib *sigh* * * 94.05.21 - Robocoder@TMI-2 * o Bumprev'd to 5.0 (Warning: possible compat busters) * o Finished code for RESTRICTED_READ/RESTRICTED_WRITE; necessitated adding * a strncmp function (which may or may not already be a simul_efun with * a given mudlib) * o Cleaned up get_path() a bit...since HOME_DIR()--macro for TMI's * user_path()--returns leading and trailing slashes--should fix logs * and file locking quirks; also should now handle "./.." correctly * o "stor" checks destination is not a directory * o "appe" checks destination already exists (and is not a directory) * o Added LONG_LS to ftpdconf.h, to change non-columnar ls() to a long * "ls" (timestamp & filesize) * o Added catch()'s around rm(), read_buffer(), and write_buffer calls where * account level file permissons may not permit * o Added "stou" command (store unique) * o Added config option to ftpdconf.h for FTP_USERS * o Changed some of the error messages slightly. * o Added lose_connection() since lose_user() decrements counter of logged * in users (which is inappropriate for data connections) * o Now closes sockets before removing it from mapping * o For the sake of completeness, added a few commands which are recognized * but not implemented. * o Added "site", "mkd", and "rmd" commands * o Implemented missing part of "mdtm" command * o Moved some #includes after variable definitions in order to compile * * 94.05.21 - Robocoder@TMI-2 * o Bumprev'd to 5.1 * o Featuritus attack continues...coded "rnfr", "rnto", and "help" commands * o cmdtab for above "help" derived from Berkeley source (ftpcmd.y 5.23) * o Added CHECK_NARGS() to ftpdsupp.h, and used together with command[1] * (as opposed to tmp) to get a more consistent reply format * o If user entered "ls -l" and client translated this to "nlist -l", * it's translated to "list ." as user intended; idea from Berkeley * source (ftpd.c 5.60) * o Removed trailing text from #endif's because it was getting cluttered * in some sections; removing all instances at least keeps it uniform * o Removed the event() support (non-MudOS)...which was #if 0'd out anyways * o "nlst" also returns subdirs (1 level deeper) rather than appending * '/' on dir names * o "list" further expanded (hopefully, it doesn't max eval too often); * here the trailing '/' is retained; also LONG_LS config option removed * o Cleaning up most of the string addition (of 3 or more strings) to use * sprintf() (eg in socket_write() calls) * o Macro'd a couple of recurring socket_write() calls * o Now logs email address entered by "anonymous" ftp logins * * 94.05.23 - Robocoder@TMI-2 * o Following 4BSD, server restricts anonymous FTP to FTP_DIR; to remove * restriction, set FTP_DIR to "/" * o Code wasn't updating LAST_DATA on data connections (only on parent * control connection)--I believe it was possible for check_connections() * to abort transfers in progress...I don't recall if it was broken to begin * with or if I broke it in the midst of heavy hacking * * 94.06.17 - Robocoder@Delta.City * o Bumprev'd to 5.2 * o Finally tested the "new" stuff...fixing typos, etc * o Rewrote check_connections() to check control connections first (for * idle users) and then to check data connections for orphaned links * o Went back to "original" nlist behaviour (shorter) because it was * max eval'ing, and driver appeared to close the connection long before * the columnar ls() finished * o Return values from socket_write() are checked in in_write_callback() * o Removed CHECK_NARGS...we'll rely on client to check the number of * of parameters * * 94.06.29 - Robocoder@Delta.City * Bumprev'd to 5.3 * o in_write_callback() - lose_user() is called if !EECALLBACK, since it's * the only case we're concerned with * o in_close_callback() - removed logging here since this function is * apparently only called if connection is closed before it gets to * the login prompt * * 94.07.04 Rust@TMI-2 * o added a remove() to check to make sure there are no users before * destructing * * 94.07.25 Robocoder * o added admin check to remove() to permit it =) * o fixed Index out of bounds error when using "cdup" * o added code to in_close_callback() to handle socket closing * unexpectedly * * 94.08.15 Robocoder@TMI-2 * o Bumprev'd to 5.4 * o Added Chuck's patches for .message file support, see MESSAGE_FILES * define in ftpdconf.h; so when people cd, cwd, cdup...the .message file * is output. * o Changed login message for "anonymous" users slightly. * * 94.11.27 Robocoder@TMI-2 * o Bumprev'd to 5.5 * o Incorporated ideas from Marius@Garden's "ftpd 5.0", such as * SITE IDLE, making ASCII "get" transfers more portable with ftp clients, * and various peephole optimizations * o "nlst" output now also more portable with ftp clients to avoid the * dreaded "<n> bare linefeeds received" message * o Added support for "site help" and "site time" * * 94.11.28 Robocoder@TMI-2 * o Bumprev'd to 5.6 * o For added robustness, added sizeof(command) checks in parse_com, as * some ftp clients provide a "quote" command which doesn't validate * syntax...in which case, someone can cause the daemon to die * * 94.11.28 Robocoder * o Bumprev'd to 5.7 * o For added robustness, checks return value of get_dir()...there are * cases where it returns 0 when account level permissions are messed up. * * 94.11.28 Leto@TMI-2 * o Fixed a type error in query_connections() that caused the v20.x driver * to barf on this file. * * 94.11.29 Robocoder * o Changed error message if ftp user attempts to get/retr or delete a * non-existent file -- suggested by Leto * * 94.12.01 Robocoder * o Bumprev'd to 5.8 * o data_write_callback() now more robust -- handles EECALLBACK from * socket_write() * o Removed a useless check from resolve_callback() added sometime * around 4.1 * * 94.12.07 Robocoder * o fixed typos in ftpd.c and ftpdsupp.h...so #define'ing ANONYMOUS_FTP * loads properly * o Ack...removed "\r\n" processing to nlst output...back to * some ftp clients complaining about bare linefeeds in ls output, to * satisfy broken ftp clients that demand only "\n". * * 94.12.09 - Robocoder * o small optimization in ls() -- changed s1 = s1 + s2 -> s1 += s2 * o added some code for more robustness in in_read_callback() * * 94.12.16 Robocoder * o resolve_callback() cleaned up ... better comments to give insight * into proper use of resolve() efun * o fixed a typo in in_read_callback() which would allow * for an error to creep thru if the connection is closed while * processing ftp user commands * o small optimization to check_connections() [94.12.18] * * 95.02.11 - Leto * o Fixed for new range use ( -x to <x ) * * 95.04.23 - Robocoder * o Bumped to 5.9 * o Fixed anonymous ftp; note see FTP_DIR about changing/extending/restricting * anonymous user's path * o ANONYMOUS_FTP is now an array of valid anonymous ftp users * o Removed a redundant get_path() from "cwd" * * 95.05.13 - Robocoder * o Fixed spurious errors from orphaned data connections during callbacks */ #include <config.h> /* from tmi-2 */ #define FTPD_VERSION "5.9" /* ----------------------------------------------- Variables ----------------------------------------------- */ mapping socket_info; int our_socket; mapping temp_map; int number_of_users; /* * edit ftpdconf.h & ftpdsupp.h to configure ftpd for your mudlib */ #include <net/ftpd.h> #include <net/ftpdconf.h> #include <net/ftpdsupp.h> #include <daemons.h> #ifdef FILE_LOCKING #include <flock.h> #endif /* ----------------------------------------------- Prototypes ----------------------------------------------- */ void setup_ftp( int port ); void finish_lookup( string host, string number ); void resolve_callback( string address, string resolved, int key ); string get_path( int fd, string str ); int strncmp(string str, string sub, int i); int query_prevent_shadow() { return 1; } void lose_connection( int fd ) { socket_close( fd ); map_delete( socket_info, fd ); } void lose_user( int fd ) { socket_close( fd ); map_delete( socket_info, fd ); number_of_users--; } #ifdef FILE_LOCKING static int flock_wrapper( string fname, int flag, int fd ) { int res; seteuid( socket_info[ fd ][ USER_NAME ] ); res = file_lock( fname, flag ); seteuid( getuid() ); return res; } #endif int strncmp(string str, string sub, int i) { int j; j = strlen(str); return (i > j ? strcmp(str, sub) : strcmp(str[0..i-1], sub)); } int check_valid_read( string fname, int fd ) { int res; seteuid( socket_info[ fd ][ USER_NAME ] ); res = (int) MASTER_OB -> valid_read(fname, this_object(), "read_file"); seteuid( getuid() ); /* * This rather complicated looking section of code handles the * various read levels available. See ftpdconf.h for READ_LEVEL * definitions of these flags. */ #ifdef READ_LEVEL #if READ_LEVEL == VALID_READ return res; #elif READ_LEVEL == RESTRICTED_READ if (strncmp(fname, HOME_DIR( UNAME ), strlen(HOME_DIR( UNAME ))) == 0 #ifdef PUB_DIR || strncmp(fname, PUB_DIR, strlen(PUB_DIR)) == 0 #endif #ifdef FTP_DIR || strncmp(fname, FTP_DIR, strlen(FTP_DIR)) == 0 #endif #ifdef TMP_DIR || strncmp(fname, TMP_DIR, strlen(TMP_DIR)) == 0 #endif ) return res; else return 0; #elif READ_LEVEL == WRITE_LIMIT_READ return check_valid_write( fname, fd ); #else // default return res; #endif #else // default return res; #endif } int check_valid_write( string fname, int fd ) { int res; seteuid( socket_info[ fd ][ USER_NAME ] ); /* * prevent non-admin users from removing .message files */ #ifdef MESSAGE_FILES if (!adminp(geteuid()) && strlen(fname) >= 9 && fname[<9..<1] == "/.message") return 0; #endif res = (int) MASTER_OB -> valid_write(fname, this_object(), "write_file"); seteuid( getuid() ); /* * This rather complicated looking section of code handles the * various write levels available. See ftpdconf.h for WRITE_LEVEL * definitions of these flags. */ #ifdef WRITE_LEVEL #if WRITE_LEVEL == VALID_WRITE return res; #elif WRITE_LEVEL == RESTRICTED_WRITE if (strncmp(fname, HOME_DIR( UNAME ), strlen(HOME_DIR( UNAME ))) == 0 #ifdef PUB_DIR || strncmp(fname, PUB_DIR, strlen(PUB_DIR)) == 0 #endif #ifdef FTP_DIR || strncmp(fname, FTP_DIR, strlen(FTP_DIR)) == 0 #endif #ifdef TMP_DIR || strncmp(fname, TMP_DIR, strlen(TMP_DIR)) == 0 #endif ) return res; else return 0; #elif WRITE_LEVEL == READ_ONLY return 0; #else // default return res; #endif #else // default return res; #endif } /* query_connections returns an array of users connected to ftpd */ mapping *query_connections() { mixed *vals; mapping *list; int count, i; vals = values( socket_info ); list = allocate( sizeof( vals ) ); count = sizeof( vals ); while ( count-- ) if (!undefinedp( (vals[count])[USER_NAME] )) list[i++] = ([ USER_NAME : capitalize(( string )((vals[count])[USER_NAME])), USER_SITE : (string)((vals[count])[USER_SITE]) ]); return i ? list[0..i-1] : ({ }); } void create() { if ( !socket_info ) { socket_info = ([ ]); temp_map = ([ ]); number_of_users = 0; seteuid( ROOT_UID ); call_out( "setup_ftp", 2, FTPD_PORT ); call_out( "check_connections", 3 * 60 ); } } static void setup_ftp( int port ) { our_socket = socket_create( STREAM, "in_read_callback", "in_close_callback" ); if ( our_socket < 0 ) { TP( "Failed to create socket.\n" ); return; } if ( socket_bind( our_socket, port ) < 0 ) { TP( "Failed to bind socket.\n" ); socket_close( our_socket ); return; } if ( socket_listen( our_socket, "in_listen_callback" ) < 0 ) { TP( "Failed to listen to socket.\n" ); return; } } /* setup_ftp() */ void in_write_callback( int fd ) { string address; int res; if ( undefinedp( socket_info[fd][CONNECTED] ) ) { if ( number_of_users > FTPD_MAX_USERS ) { res = socket_write( fd, "530 Too many ftp connections active. Try again later.\n"); /* * In these cases there isn't much more that can be done */ if (res != EECALLBACK) lose_user( fd ); /* * The following blocks the code in_read_callback & * in_write_callback on this connection; we let the driver * complete the send and let the daemon timeout the connection */ if (res == EECALLBACK) { socket_info[fd][CONNECTED] = 0; } return; } res = socket_write( fd, sprintf( "220- %s FTP server (Version %s (MudOS/LPC)) ready.\n" #ifdef ANONYMOUS_FTP "220 Please login as yourself or 'anonymous'.\n", #else "220 Please login as yourself.\n", #endif THE_MUD_NAME, FTPD_VERSION ) ); /* * In these cases, assume user will receive above message */ if (res == EESUCCESS || res == EECALLBACK) { socket_info[fd][CONNECTED] = 1; if ((address = socket_address(fd)) && sscanf( address, "%s %*s", address )) { socket_info[fd][USER_SITE] = address; socket_info[fd][IDLE_SETTING] = FTPD_TIMEOUT; temp_map += ([ resolve( address, "resolve_callback" ) : fd ]); } else { socket_write( fd, "451 Error in server: socket_address() failed.\n" "221 Closing connection due to server error.\n"); lose_user( fd ); } } else if (res == EEWOULDBLOCK || res == EEALREADY) { call_out( "in_write_callback", 2, fd ); } else { lose_user( fd ); } } } /* * called for each new ftp login connection */ void in_listen_callback( int fd ) { int new_fd; if ( ( new_fd = socket_accept( fd, "in_read_callback", "in_write_callback" ) ) < 0 ) return; number_of_users++; socket_info[new_fd] = ([ ]); socket_info[new_fd][LAST_DATA] = time(); in_write_callback( new_fd ); } string ls( string path, int column, int fd ) { string *files; int i, j, s; mixed *xfiles; mixed *stats; string tmp, tmp2, creator, domain; /* if path is a directory get contents */ if ( directory_exists( path ) ) { if ( path[ strlen( path ) - 1 ] == '/' ) path += "*"; else path += "/*"; } /* begin narrow columnar "nlst" */ if (column) { files = get_dir( path ); /* can only happen if permissions are messed up at account level */ if (!files) return ""; files -= ({ ".", ".." }); if (!(i = sizeof( files ))) return ""; /* no wild cards...must have been the exact pathname to a file */ if (strsrch(path, '*') == -1 && strsrch(path, '?') == -1) { return files[0] + "\n"; } /* remove globber at end of path, leave a trailing slash */ j = strsrch(path, '/', -1); path = path[0..j]; while ( i-- ) { /* scan next level down for files */ tmp = sprintf("%s%s/", path, files[i]); if ( directory_exists( tmp ) ) { files[i] += "/"; } } return implode( files, "\n" ) + "\n"; } /* begin long "list" */ xfiles = get_dir( path, -1 ); if (!xfiles || !(s = sizeof( xfiles ))) return ""; files = allocate(s); // the Unix-like file permissions are mainly for effect...hopefully it // isn't too much, since anything more would likely be too cpu intensive // and cause it to max eval... creator = (string)MASTER_OB->creator_file(path); if (!creator) creator = ROOT_UID; domain = (string)MASTER_OB->domain_file(path); if (!domain) domain = ROOT_UID; i = strsrch(path, '/', -1); if (i >= 0) path = path[0..i]; for (i = 0; i < s; i++) { /* process timestamp */ tmp2 = ctime((xfiles[i])[2]); /* get last modified timestamp */ if ((xfiles[i])[2] + SECS_IN_YEAR < time()) { /* MMM DD YYYY */ tmp = sprintf("%s %s", tmp2[4..9], tmp2[20..23]); } else { /* MMM DD hh:mm */ tmp = tmp2[4..15]; } j = (xfiles[i])[1]; /* get filesize */ if (j == -2) { /* directory */ files[i] = sprintf("drwxrwsr-x %12s %12s <DIR> %12s %s/", creator, domain, tmp, (xfiles[i])[0]); } else { /* file */ stats = stat(path + (xfiles[i])[0]); files[i] = sprintf("-rw%crw-r-- %12s %12s %8d %12s %s", sizeof(stats) > 1 && stats[2] ? 'x' : '-', creator, domain, j, tmp, (xfiles[i])[0]); } } return sprintf( "%-#70s\n", implode( files, "\n" ) ); } void data_write_callback( int fd ) { int pfd, pos, length, ret_val; buffer tmp; if ( socket_info[fd][TYPE] == DOWNLOAD ) return; tmp = allocate_buffer( 0 ); pos = socket_info[ fd ][ POS ]; length = ( (pos+BLOCK_SIZE) >= socket_info[ fd ][ LEN ] ) ? ( socket_info[ fd ][ LEN ] - pos ) : BLOCK_SIZE; #ifdef DEBUG_SEND TP( "Entering dwc(), pos: " + pos + " length should be: " + length + ".\n" ); #endif pfd = socket_info[ fd ][ PARENT_FD ]; if (undefinedp(pfd) || undefinedp(socket_info[pfd])) return; // not a data connection, or was orphaned socket_info[ pfd ][ LAST_DATA ] = time(); if ( socket_info[ fd ][ TYPE ] == STRING ) { #ifdef DEBUG_SEND TP( "type == STRING\n" ); #endif while ( (ret_val = socket_write( fd, socket_info[fd][DATA][pos..(pos+BLOCK_SIZE-1)] )) >= 0 ) { if (ret_val == 0) { #ifdef DEBUG_SEND TP( "Ret_val == 0, continuing\n" ); #endif continue; } pos += BLOCK_SIZE; if ( pos > socket_info[ fd ][ LEN ] ) { socket_write( pfd, "226 Transfer complete.\n" ); socket_close( fd ); map_delete( socket_info[ pfd ], DATA_FD ); #ifdef DEBUG_SEND TP( "dwc() complete, exiting.\n" ); #endif return; } } } else { #ifdef DEBUG_SEND TP( "type is other then STRING\n" ); #endif if (catch( tmp = read_buffer( socket_info[ fd ][ DATA ], pos, length ) )) socket_write( pfd, "551 Error on input file.\n" ); while ( ( ret_val = socket_write( fd, tmp ) ) >= 0 ) { if ( ret_val == 0 ) { #ifdef DEBUG_SEND TP( "Ret_val == 0, continuing\n" ); #endif continue; } #ifdef DEBUG_SEND TP( "sent from " + pos + " to " + ( pos + length ) + ".\n" ); TP( "ret_val was: " + ret_val + ".\n" ); #endif pos += length; if ( pos >= socket_info[ fd ][ LEN ] ) { socket_write( pfd, "226 Transfer complete.\n" ); socket_close( fd ); map_delete( socket_info[ pfd ], DATA_FD ); #ifdef DEBUG_SEND TP( "dwc() complete, exiting.\n" ); #endif return; } length = ( (pos+BLOCK_SIZE) >= socket_info[ fd ][ LEN ] ) ? ( socket_info[ fd ][ LEN ] - pos ) : BLOCK_SIZE; tmp = allocate_buffer( 0 ); tmp = read_buffer( socket_info[ fd ][ DATA ], pos, length ); } } #ifdef DEBUG_SEND TP( "ret_val was: " + ret_val + ".\n" ); TP( "leaving dwc(), pos: " + pos + ".\n" ); #endif socket_info[ fd ][ POS ] = pos; if ( ret_val == EEWOULDBLOCK || ret_val == EEALREADY ) { /* it would block, so it's up to us to try again */ #ifdef DEBUG_SEND TP( "Adding call_out\n" ); #endif call_out( "data_write_callback", 2, fd ); } else if (ret_val != EECALLBACK) { /* not going to be called again by driver */ while ( remove_call_out( "data_write_callback" ) != -1 ) { #ifdef DEBUG_SEND TP( "Killing callout.\n" ); #endif } } } /* data_write_callback() */ static void data_conn( int fd, string mess, string name, int type ) { int new_fd, ret, data_mode; string data_mode_name; mixed tmp; if ( type == STRING || ( type == FILE && UTYPE == STRING ) ) { data_mode_name = "ASCII"; data_mode = STREAM; tmp = ""; } else { data_mode_name = "BINARY"; data_mode = STREAM_BINARY; tmp = allocate_buffer( 0 ); } if ( socket_info[ fd ][ DATA_FD ] ) { socket_write( fd, "425 Can't open data connection.\n" ); return; } new_fd = socket_create( data_mode, "data_read_callback", "data_close_callback" ); if ( new_fd < 0 ) { socket_write( fd, "425 Can't create data socket.\n" ); return; } if ( ( ret = socket_connect( new_fd, sprintf("%s %d", socket_info[ fd ][ DATA_ADDR ], socket_info[ fd ][ DATA_PORT ]), "data_read_callback", "data_write_callback" ) ) < 0 ) { TP( "Error: " + socket_info[ fd ][ DATA_ADDR ] + " " + socket_info[ fd ][ DATA_PORT ] + "\n" ); TP( socket_error( ret ) + "\n" ); socket_write( fd, "425 Can't build data connection.\n" ); socket_close( new_fd ); return; } socket_info[ new_fd ] = ([ DATA : (type == STRING ? replace_string(mess, "\n", "\r\n") : mess), POS : 0, PARENT_FD : fd, TYPE : type, LEN : (type == STRING ? strlen(mess) : file_size(mess)) ]); socket_info[ fd ][ DATA_FD ] = new_fd; socket_write( fd, sprintf("150 Opening %s mode data connection for %s " "(%d bytes).\n", data_mode_name, name, socket_info[ new_fd ][ LEN ])); data_write_callback( new_fd ); } /* data_conn() */ static void read_connection( int fd, string path, int append ) { int new_fd, ret, data_mode; string data_mode_name; mixed tmp; if ( UTYPE == BINARY ) { data_mode_name = "BINARY"; data_mode = STREAM_BINARY; tmp = allocate_buffer( 0 ); } else { data_mode_name = "ASCII"; data_mode = STREAM; tmp = ""; } if ( socket_info[ fd ][ DATA_FD ] ) { socket_write( fd, "425 Can't open data connection.\n" ); return; } new_fd = socket_create( data_mode, "data_read_callback", "data_close_callback" ); if ( new_fd < 0 ) { socket_write( fd, "425 Can't create data socket.\n" ); return; } if ( ( ret = socket_connect( new_fd, sprintf("%s %d", socket_info[ fd ][ DATA_ADDR ], socket_info[ fd ][ DATA_PORT ]), "data_read_callback", "data_write_callback" ) ) < 0 ) { TP( "Error: " + socket_info[ fd ][ DATA_ADDR ] + " " + socket_info[ fd ][ DATA_PORT ] + "\n" ); TP( socket_error( ret ) + "\n" ); socket_write( fd, "425 Can't build data connection.\n" ); socket_close( new_fd ); return; } socket_info[ new_fd ] = ([ PATH : path, PARENT_FD : fd, DATA : tmp, TYPE : DOWNLOAD, APPEND : append ]); socket_info[ fd ][ DATA_FD ] = new_fd; socket_write( fd, sprintf("150 Opening %s mode data connection for %s.\n", data_mode_name, path) ); } /* read_connection() */ void data_read_callback( int fd, mixed mess ) { int pfd; if ( socket_info[ fd ][ TYPE ] != DOWNLOAD ) return; pfd = socket_info[ fd ][ PARENT_FD ]; if (undefinedp(pfd) || undefinedp(socket_info[pfd])) return; // not a data connection, or was orphaned socket_info[ pfd ][ LAST_DATA ] = time(); if ( stringp( mess ) ) mess = replace_string( mess, "\r", "" ); socket_info[ fd ][ DATA ] += mess; } /* data_read_callback() */ void data_close_callback( int fd ) { int pos, size, res, pfd; pfd = socket_info[ fd ][ PARENT_FD ]; if ( socket_info[ fd ][ TYPE ] == DOWNLOAD ) { if ( socket_info[ fd ][ APPEND ] != 1 ) catch( rm( socket_info[ fd ][ PATH ] ) ); else pos = file_size( socket_info[ fd ][ PATH ] ); do { if (catch( res = write_buffer( UPATH, pos, UDATA[pos..(pos+(BLOCK_SIZE-1))] ) )) socket_write( pfd, "452 Error writing file.\n"); if (res) pos += BLOCK_SIZE; } while (res); size = stringp( UDATA ) ? strlen( UDATA ) : sizeof( UDATA ); #ifdef FILE_LOCKING flock_wrapper( socket_info[ fd ][ PATH ], F_UNLOCK, pfd ); #endif if ( socket_info[ fd ][ APPEND ] == -1) socket_write( pfd, sprintf("226 Transfer complete (unique file name:%s).\n", socket_info[ fd ][ PATH ]) ); else socket_write( pfd, "226 Transfer complete.\n" ); } /* * only close data connections here */ if (!undefinedp( pfd )) { socket_close( fd ); map_delete( socket_info, fd ); map_delete( socket_info[ pfd ], DATA_FD ); } } /* data_close_callback() */ static void logout( int fd ) { socket_info[ fd ][ LOGGED_IN ] = 0; if ( UNAME ) { #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_CONNECT log_file( LOG_FILE, sprintf("%s logged out from %s.\n", UNAME, USITE) ); #endif } } /* logout() */ /* * parse user command */ static void parse_comm( int fd, string str ) { string *command, tmp, tmp2; mixed *misc; int port, i, s; TP( "Parsing " + str + ".\n" ); command = explode( str, " " ); socket_info[ fd ][ LAST_DATA ] = time(); switch( lower_case( command[ 0 ] ) ) { case "port": // specify data connection port command = explode( implode( command[ 1..1000 ], " " ), "," ); if ( sizeof( command ) < 6 ) socket_write( fd, "550 Failed command.\n" ); else { socket_info[ fd ][ DATA_ADDR ] = implode( command[ 0..3 ], "." ); sscanf( command[ 4 ], "%d", i ); port = i << 8; // Modified by Dm 15 March 1996 because to_int is gone. // Added back after more checking. i = to_int( command[5] ); port += i; socket_info[ fd ][ DATA_PORT ] = port; socket_write( fd, "200 PORT command successful.\n" ); } break; case "user": // specify user name if ( socket_info[ fd ][ LOGGED_IN ] ) { logout( fd ); UNAME = 0; } CHECK_CMD(1); if ( !check_access( command[ 1 ] ) ) { socket_write( fd, sprintf("530 User %s access denied.\n", command[ 1 ]) ); #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_NO_CONNECT log_file( LOG_FILE, sprintf("%s denied access from %s.\n", command[ 1 ], USITE) ); #endif } else { #ifdef ANONYMOUS_FTP if ( member_array(command[1], ANONYMOUS_FTP) != -1) { socket_write( fd, "331 Guest login ok, send your complete E-mail address as password.\n" ); UNAME = "anonymous"; } else #endif { socket_write( fd, sprintf( "331 Password required for %s.\n", command[ 1 ]) ); UNAME = command[ 1 ]; } } break; case "pass": // specify password if ( socket_info[fd][LOGGED_IN] || !socket_info[fd][USER_NAME] ) { socket_write( fd, "503 Log in with USER first.\n" ); break; } if ( !check_password( socket_info[fd][ USER_NAME ], implode( command[ 1..<1 ], " " ) ) ) { #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") socket_write( fd, "530 Login failed. Please send your complete E-mail address as password.\n" ); else #endif socket_write( fd, "530 Login incorrect.\n" ); #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_NO_CONNECT log_file( LOG_FILE, sprintf( "%s failed/incorrect login from %s.\n", UNAME, USITE) ); #endif UNAME = 0; } else #ifdef CHECK_SITE if (!check_site( UNAME, fd )) { /* * Note: this particular response of 530 is not mentioned * as a possible response to the PASS command in RFC959, * because site checking is TMI specific. */ socket_write( fd, sprintf("530 User %s: Can't login from %s.\n", UNAME, USITE) ); #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_NO_CONNECT log_file( LOG_FILE, sprintf("%s refused login from %s.\n", UNAME, USITE) ); #endif UNAME = 0; lose_user( fd ); } else #endif { socket_info[ fd ][ LOGGED_IN ] = 1; #ifdef ANONYMOUS_FTP if ( UNAME == "anonymous" ) UCWD = FTP_DIR; else #endif UCWD = HOME_DIR( UNAME ); #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_CONNECT #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") log_file( LOG_FILE, sprintf("%s (%s) connected from %s.\n", UNAME, str[strlen(command[0]) + 1..<1], USITE) ); else #endif log_file( LOG_FILE, sprintf("%s connected from %s.\n", UNAME, USITE) ); #endif if ( !directory_exists( UCWD ) ) { socket_write( fd, "230 No directory! Logging in with home=" #ifdef GUEST_WIZARD_FTP PUB_DIR ".\n" ); socket_info[ fd ][ CWD ] = PUB_DIR; #else "/\n" ); socket_info[ fd ][ CWD ] = "/"; #endif } else { #ifdef LOGIN_MSG tmp = read_file( LOGIN_MSG ); tmp = tmp[0..<2]; tmp = sprintf("230- %s\n", replace_string( tmp, "\n", "\n230- " ) ); socket_write( fd, tmp ); #endif #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") socket_write( fd, "230 Guest login ok, access restrictions apply.\n" ); else #endif socket_write( fd, sprintf( "230 User %s logged in successfully.\n", UNAME) ); } socket_info[ fd ][ TYPE ] = STRING; } break; case "allo": // allocate storage (vacuously) socket_write( fd, "202 ALLO command ignored.\n" ); break; case "noop": // do nothing socket_write( fd, "200 NOOP command successful.\n" ); break; case "retr": // retrieve a file CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if ((i = file_size(tmp)) == -2) { socket_write( fd, sprintf("550 %s: Not a plain file.\n", command[1])); } else if (i == -1) { socket_write( fd, sprintf( "550 %s: No such file or directory.\n", command[1])); } else if ( !check_valid_read( tmp, fd ) ) { PERMISSION_DENIED550(command[ 1 ]); } else { #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s retr %s (%s).\n", UNAME, tmp, (UTYPE == BINARY ? "BINARY" : "ASCII") ) ); #endif data_conn( fd, tmp, command[ 1 ], FILE ); } break; case "stou": // store a file with a unique name CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); i = file_size(tmp); if (i >= 0 && check_valid_write( tmp, fd )) { for (i = MAX_UNIQUE_TRIES; i; i--) { if (file_size(sprintf("%s%d", tmp, i)) == -1) break; } if (i) { tmp = sprintf("%s%d", tmp, i); #ifdef FILE_LOCKING if (flock_wrapper(tmp, F_LOCK, fd)) { /* parent fd */ #endif #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s stou %s (%s).\n", UNAME, tmp, (UTYPE == BINARY ? "BINARY" : "ASCII") ) ); #endif read_connection( fd, tmp, -1 ); #ifdef FILE_LOCKING } else { PERMISSION_DENIED550(command[ 1 ]); } #endif } else { socket_write( fd, "452 Unique file name cannot be created.\n"); } } else { PERMISSION_DENIED553(command[ 1 ]); } break; case "stor": // store a file CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if ( file_size(tmp) != -2 && check_valid_write( tmp, fd ) ) { #ifdef FILE_LOCKING if (flock_wrapper(tmp, F_LOCK, fd)) { /* parent fd */ #endif #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s stor %s (%s).\n", UNAME, tmp, (UTYPE == BINARY ? "BINARY" : "ASCII") ) ); #endif read_connection( fd, tmp, 0 ); #ifdef FILE_LOCKING } else { PERMISSION_DENIED550(command[ 1 ]); } #endif } else { PERMISSION_DENIED553(command[ 1 ]); } break; case "appe": // append to a file CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if ( file_size(tmp) >= 0 && check_valid_write( tmp, fd ) ) { #ifdef FILE_LOCKING if (flock_wrapper(tmp, F_LOCK, fd)) { /* parent fd */ #endif #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s appe %s (%s).\n", UNAME, tmp, (UTYPE == BINARY ? "BINARY" : "ASCII") ) ); #endif read_connection( fd, tmp, 1 ); #ifdef FILE_LOCKING } else { PERMISSION_DENIED550(command[ 1 ]); } #endif } else { PERMISSION_DENIED553(command[ 1 ]); } break; case "dele": // delete a file CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if ((i = file_size(tmp)) == -2) { socket_write( fd, sprintf("550 %s: Not a plain file.\n", command[1])); } else if (i == -1) { socket_write( fd, sprintf( "550 %s: No such file or directory.\n", command[1])); } else if ( check_valid_write( tmp, fd ) ) { #ifdef FILE_LOCKING if (flock_wrapper(tmp, F_LOCK, fd)) { /* parent fd */ #endif #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s dele %s.\n", UNAME, tmp) ); #endif catch( i = rm(tmp) ); if (i) socket_write( fd, "250 DELE command successful.\n" ); else socket_write( fd, sprintf( "550 Permission denied to %s.\n", command[ 1 ]) ); #ifdef FILE_LOCKING flock_wrapper(tmp, F_UNLOCK, fd); /* parent fd */ } else { PERMISSION_DENIED550(command[ 1 ]); } #endif } else { PERMISSION_DENIED553(command[ 1 ]); } break; /* Supposed to return modified time in the format: YYYYMMDDHHMMSS */ case "mdtm": // show last modification time of file CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if ( check_valid_read( tmp, fd ) ) { if ((i = file_size(tmp)) == -2) socket_write( fd, sprintf("550 %s: Not a plain file.\n", command[1])); else if (i == -1) socket_write( fd, sprintf( "550 %s: No such file or directory.\n", command[1])); else { misc = stat(tmp); tmp2 = ctime(misc[1]); tmp2 = sprintf("%s%02d%c%c%s%s%s", tmp2[20..23], member_array(tmp2[4..6], MONTHS), tmp2[8] == ' ' ? '0' : tmp2[8], tmp2[11..12], tmp2[14..15], tmp2[17..18]); #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_CD_SIZE log_file( LOG_FILE, sprintf("%s mdtm %s.\n", UNAME, tmp) ); #endif socket_write( fd, sprintf("213 %s\n", tmp2) ); } } else PERMISSION_DENIED550(command[ 1 ]); break; case "size": // return size of file CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if ( check_valid_read( tmp, fd ) ) { if ( directory_exists( tmp ) ) socket_write( fd, sprintf("550 %s: Not a plain file.\n", command[ 1 ]) ); else if ( !file_exists( tmp ) ) socket_write( fd, sprintf("550 %s does not exist.\n", command[ 1 ]) ); else { #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_CD_SIZE log_file( LOG_FILE, sprintf("%s size %s.\n", UNAME, tmp) ); #endif socket_write( fd, sprintf("215 %d\n", file_size( tmp )) ); } } else PERMISSION_DENIED550(command[ 1 ]); break; case "nlst": // give name list of files in directory CHECK_LOGIN(); /* Send name list */ if ((i = sizeof(command)) > 1 && command[1] == "-l") { if (i == 2) command[1] = "."; else command = ({ command[0] }) + command[2..i-1]; // and fall through to "list" } else { /* Used by commands like "dir", "mget", and "mput" */ if ( i > 1 ) tmp = get_path( fd, command[ 1 ] ); else tmp = socket_info[ fd ][ CWD ]; if ( check_valid_read( tmp, fd ) ) { tmp2 = ls( tmp, 1, fd ); if (tmp2 == "") socket_write( fd, "550 No files found.\n" ); else data_conn( fd, tmp2, "ls", STRING ); } else PERMISSION_DENIED550(command[ 1 ]); break; } case "list": // give list files in a directory CHECK_LOGIN(); /* Send directory file list (like exec'ing "ls") */ if ( sizeof( command ) > 1 ) tmp = get_path( fd, command[ 1 ] ); else tmp = socket_info[ fd ][ CWD ]; if ( check_valid_read( tmp, fd ) ) { tmp2 = ls( tmp, 0, fd ); if (tmp2 == "") socket_write( fd, "550 No files found.\n"); else data_conn( fd, tmp2, "ls", STRING ); } else PERMISSION_DENIED550(command[ 1 ]); break; case "xpwd": // print the current working directory (deprecated) case "pwd": // print the current working directory CHECK_LOGIN(); #ifdef ANONYMOUS_FTP if ( UNAME == "anonymous" ) socket_write( fd, sprintf("257 \"%s\" is the current directory.\n", socket_info[ fd ][ CWD ][strlen(FTP_DIR) - 1..<1]) ); else #endif socket_write( fd, sprintf("257 \"%s\" is the current directory.\n", socket_info[ fd ][ CWD ]) ); break; case "xcup": // change to parent of current working directory (deprecated) case "cdup": // change to parent of current working directory if (sizeof(command) > 1) command[ 1 ] = ".."; else command += ({ ".." }); case "xcwd": // change working directory (deprecated) case "cwd": // change working directory CHECK_LOGIN(); if ( sizeof( command ) > 1 ) tmp = get_path( fd, command[ 1 ] ); else tmp = socket_info[ fd ][ CWD ]; if ( check_valid_read( tmp, fd ) ) { if ( !directory_exists( tmp ) ) { socket_write( fd, sprintf("550 %s: Not a directory.\n", command[1]) ); break; } #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_CD_SIZE log_file( LOG_FILE, sprintf("%s cd to %s.\n", UNAME, tmp) ); #endif socket_info[ fd ][ CWD ] = tmp; #ifdef MESSAGE_FILES if (file_size(tmp + "/.message") > 0) { tmp = read_file(tmp + "/.message"); tmp = tmp[0..<2]; tmp = sprintf("250- %s\n250 %s command successful.\n", replace_string(tmp, "\n", "\n250- "), command[0]); socket_write(fd, tmp); } else #endif socket_write( fd, sprintf("250 %s command successful.\n", command[0]) ); } else socket_write( fd, sprintf("550 %s: No such file or directory.\n", tmp) ); break; case "quit": // terminate session socket_write( fd, "221 Goodbye.\n" ); logout( fd ); lose_user( fd ); break; case "type": // specify data transfer type CHECK_CMD(1); if ( command[ 1 ] == "I" ) { socket_info[ fd ][ TYPE ] = BINARY; socket_write( fd, "200 Type set to I.\n" ); } else if ( command[ 1 ] == "A" ) { socket_info[ fd ][ TYPE ] = STRING; socket_write( fd, "200 Type set to A.\n" ); } else socket_write( fd, sprintf("504 Type %s not implemented.\n", command[ 1 ]) ); break; case "stat": // return status of server socket_write( fd, sprintf( "211 FTP server status: %s FTP Version %s\r\n", THE_MUD_NAME, FTPD_VERSION) ); socket_write( fd, sprintf(" Connected to %s\r\n", USITE) ); if ( socket_info[ fd ][ LOGGED_IN ] ) socket_write( fd, sprintf(" Logged in as %s\r\n", UNAME) ); else if ( UNAME ) socket_write( fd, " Waiting for password.\r\n" ); else socket_write( fd, " Waiting for user name.\r\n" ); socket_write( fd, " TYPE: ASCII, FORM: Nonprint; STRUcture: " "File; transfer MODE: Stream.\r\n" ); if ( socket_info[ fd ][ DATA_FD ] ) socket_write( fd, " Data connection open.\r\n" ); else if ( socket_info[ fd ][ DATA_ADDR ] ) socket_write( fd, sprintf(" %s %d\r\n", socket_info[ fd ][ DATA_ADDR ], socket_info[ fd ][ DATA_PORT ]) ); else socket_write( fd, " No data connection.\r\n" ); socket_write( fd, "211 End of status.\n" ); break; case "abor": // abort previous command /* Stops recvs and stors. At least thats what it is supposed to do. */ if ( socket_info[ fd ][ DATA_FD ] ) { socket_write( fd, "426 Transfer aborted. Data connection closed.\n"); #ifdef FILE_LOCKING if ( (socket_info[ fd ][ DATA_FD ])[ TYPE ] == DOWNLOAD ) flock_wrapper( (socket_info[ fd ][ DATA_FD ])[ PATH ], F_UNLOCK, socket_info[ fd ] ); #endif socket_close( socket_info[ fd ][ DATA_FD ] ); map_delete( socket_info[fd], DATA_FD ); } socket_write( fd, "226 Abort successful.\n" ); break; case "syst": // show operating system type of server system socket_write( fd, sprintf("215 MUD Type: %s\n", THE_VERSION) ); break; case "xmkd": // make a directory (deprecated) case "mkd": // make a directory CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if (file_size( tmp ) == -1) { if (check_valid_write(tmp, fd)) { if (mkdir(tmp)) { #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s mkd %s.\n", UNAME, tmp) ); #endif socket_write(fd, sprintf("257 %s command successful.\n", command[ 0 ]) ); } else socket_write(fd, sprintf("550 %s command failed.\n", command[ 0 ]) ); } else PERMISSION_DENIED550(command[ 1 ]); } else socket_write(fd, sprintf( "550 %s: Directory or file already exists.\n", command[ 1 ])); break; case "xrmd": // remove a directory (deprecated) case "rmd": // remove a directory CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[ 1 ] ); if (file_size( tmp ) == -2) { if (check_valid_write(tmp, fd)) { if (rmdir(tmp)) { #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s rmd %s.\n", UNAME, tmp) ); #endif socket_write(fd, sprintf("250 %s command successful.\n", command[ 0 ]) ); } else socket_write(fd, sprintf("550 %s command failed.\n", command[ 0 ]) ); } else PERMISSION_DENIED550(command[ 1 ]); } else socket_write(fd, sprintf("550 %s: No such file or directory.\n", command[ 1 ]) ); break; case "site": // non-standard commands CHECK_LOGIN(); CHECK_CMD(1); switch (lower_case(command[ 1 ])) { case "idle": if (sizeof(command) < 3) { socket_write(fd, sprintf("200 Current IDLE time limit is %d seconds; max %d\n", socket_info[fd][IDLE_SETTING], MAX_FTPD_IDLE)); break; } if (!sscanf(command[2], "%d", i)) { socket_write(fd, "550 SITE IDLE command failed.\n"); break; } i = (i < FTPD_TIMEOUT ? FTPD_TIMEOUT : (i > MAX_FTPD_IDLE ? MAX_FTPD_IDLE : i)); socket_info[fd][IDLE_SETTING] = i; socket_write(fd, sprintf("200 Maximum IDLE time set to %d seconds\n", i)); break; case "time": socket_write(fd, sprintf("200 Local TIME is %s.\n", ctime(time())[4..15] ) ); break; case "help": if (sizeof(command) > 1) { tmp = lower_case(command[1]); if (!undefinedp(sitecmdtab[ tmp ])) { misc = sitecmdtab[ tmp ]; if (misc[1]) socket_write(fd, sprintf("214 Syntax: %s %s.\n", misc[0], misc[2]) ); else socket_write(fd, sprintf("214 %s %s; unimplemented.\n", misc[0], misc[2]) ); } else { socket_write(fd, sprintf("502 Unknown command %s.\n", command[ 1 ]) ); } } else { socket_write(fd, "214- The following SITE commands are recognized " "(* =>'s unimplemented).\n214-\n"); misc = keys(sitecmdtab); s = sizeof(misc); tmp = "214- "; for (i = 0; i < s; i++) { tmp += sprintf("%c%-7s", sitecmdtab[misc[i]][1] ? ' ' : '*', sitecmdtab[misc[i]][0]); if (i % 8 == 7) { socket_write(fd, tmp + "\n"); tmp = "214- "; } } if (i % 8) socket_write(fd, tmp + "\n"); socket_write(fd, sprintf("214-\n214 Direct comments to %s.\n", FTP_BUGS_EMAIL)); } break; case "chmod": case "umask": socket_write( fd, sprintf("502 '%s' command not implemented.\n", command[ 0 ]) ); break; default: socket_write( fd, sprintf("500 '%s %s' command " "not understood.\n", command[ 0 ], command[ 1 ]) ); break; } break; case "rnfr": // specify rename-from file name CHECK_LOGIN(); CHECK_CMD(1); tmp = get_path( fd, command[1] ); i = file_size( tmp ); if (i == -2) { socket_write( fd, sprintf("550 %s: Not a plain file.\n", command[ 1 ]) ); } else if (i == -1) { socket_write(fd, sprintf("550 %s: No such file or directory.\n", command[ 1 ]) ); } else if (check_valid_write(tmp, fd)) { socket_info[fd][FROMNAME] = tmp; socket_write(fd, "350 File exists, ready for destination name.\n"); } else { PERMISSION_DENIED550(command[ 1 ]); } break; case "rnto": // specify rename-to file name CHECK_LOGIN(); CHECK_CMD(1); if (!socket_info[fd][FROMNAME]) { socket_write(fd, "503 Bad sequence of commands.\n"); } else { tmp = get_path( fd, command[1] ); i = file_size( tmp ); if (i != -1) { socket_write(fd, sprintf( "550 %s: Directory or file already exists.\n", command[ 1 ]) ); } else if (!check_valid_write(tmp, fd)) { PERMISSION_DENIED550(command[ 1 ]); } else { tmp2 = socket_info[fd][FROMNAME]; if (!check_valid_write(tmp2, fd)) { PERMISSION_DENIED550(tmp2); socket_info[fd][FROMNAME] = 0; } else { #ifdef FILE_LOCKING if (flock_wrapper(tmp, F_LOCK, fd)) { if (flock_wrapper(tmp2, F_LOCK, fd)) { #endif #ifdef LOG_TIME log_file( LOG_FILE, FTP_TIME ); #endif #ifdef LOG_FILE log_file( LOG_FILE, sprintf("%s rnfr %s " "rnto %s.\n", UNAME, tmp2, tmp) ); #endif if (catch( i = rename(tmp2, tmp) ) || i) socket_write(fd, "550 RNTO command failed.\n"); else socket_write(fd, "250 RNTO command successful.\n"); #ifdef FILE_LOCKING flock_wrapper(tmp2, F_UNLOCK, fd); flock_wrapper(tmp, F_UNLOCK, fd); socket_info[fd][FROMNAME] = 0; } else { PERMISSION_DENIED550(tmp2); flock_wrapper(tmp, F_UNLOCK, fd); } } else { PERMISSION_DENIED550(command[ 1 ]); } #endif } } } break; case "help": // give help information if (sizeof(command) > 1) { tmp = lower_case(command[1]); if (!undefinedp(cmdtab[ tmp ])) { misc = cmdtab[ tmp ]; if (misc[1]) socket_write(fd, sprintf("214 Syntax: %s %s.\n", misc[0], misc[2]) ); else socket_write(fd, sprintf("214 %s %s; unimplemented.\n", misc[0], misc[2]) ); } else { socket_write(fd, sprintf("502 Unknown command %s.\n", command[ 1 ]) ); } } else { socket_write(fd, "214- The following commands are recognized " "(* =>'s unimplemented).\n214-\n"); misc = keys(cmdtab); s = sizeof(misc); tmp = "214- "; for (i = 0; i < s; i++) { tmp += sprintf("%c%-7s", cmdtab[misc[i]][1] ? ' ' : '*', cmdtab[misc[i]][0]); if (i % 8 == 7) { socket_write(fd, tmp + "\n"); tmp = "214- "; } } if (i % 8) socket_write(fd, tmp + "\n"); socket_write(fd, sprintf("214-\n214 Direct comments to %s.\n", FTP_BUGS_EMAIL)); } break; case "acct": // specify account (ignored) case "mail": // mail to user case "mlfl": // mail file case "mode": // specify data transfer mode case "mrcp": // mail recipient case "mrsq": // mail recipient scheme question case "msnd": // mail send to terminal case "msam": // mail send to terminal and mailbox case "msom": // mail send to terminal or mailbox case "pasv": // prepare for server-to-server transfer case "rein": // reinitialize server state case "rest": // restart command/incomplete transfer case "smnt": // structure mount case "stru": // specify data transfer/file structure socket_write( fd, sprintf("502 '%s' command not implemented.\n", command[ 0 ]) ); break; default: socket_write( fd, sprintf("500 '%s': command not understood.\n", command[ 0 ]) ); break; } } /* parse_com() */ /* * accept user command */ void in_read_callback( int fd, string str ) { string *command; int i, j; if (undefinedp( socket_info[ fd ] ) || !socket_info[ fd ][ CONNECTED ]) return; if (!str) str = ""; str = replace_string( str, "\r", "" ); command = explode( str, "\n" ) - ({ 0 }); j = sizeof(command); for ( i = 0; i < j; i++ ) parse_comm( fd, command[i] ); } /* * Handle when the socket closes unexpectedly */ void in_close_callback( int fd ) { /* * Handle case where user aborts (eg CTRL-C) their ftp client, causing * connection to close before getting to login prompt. */ if (undefinedp(socket_info[fd][CONNECTED])) ; else { /* * Handle case where user doesn't quit properly */ logout( fd ); lose_user( fd ); } } /* * resolve path (absolute vs relative) ... does not validate path here */ static string get_path( int fd, string str ) { string *array, *array1, temp; int i, j, s; if ( !str || str == "" ) { /* no change of dir */ return socket_info[ fd ][ CWD ]; } if ( str == "/" ) { /* root of filesystem */ #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") return FTP_DIR; else #endif return "/"; } if ( str == "~" ) { /* change to home dir */ #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") return FTP_DIR; else #endif return HOME_DIR( UNAME ); } #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") { if (str[0] == '~') str = extract( str, 1 ); else if (str[0] != '/') str = sprintf("%s/%s", socket_info[ fd ][ CWD ], str)[strlen(FTP_DIR) .. <1]; else sscanf(str, FTP_DIR "%s", str); } else #endif if ( str[ 0 ] == '~' ) { /* relative to user directories */ if ( str[ 1 ] == '/' ) { /* relative to user's home directory */ temp = extract( str, 2 ); str = HOME_DIR( UNAME ) + temp; } else { /* relative to someone else's home directory */ string name; if ( sscanf( str, "~%s/%s", name, str ) != 2 ) { name = extract( str, 1 ); str = HOME_DIR( name ); } else { /* "cheat" at this point and just assume they are a wizard. */ str = sprintf("%s/%s", HOME_DIR( name ), str); } } } else if (str [0] != '/') { /* relative to current working directory */ str = sprintf("%s/%s", socket_info[ fd ][ CWD ], str); } /* else absolute path name */ /* * Assume HOME_DIR() might or might not return leading and/or trailing * slashes; process path names for consistency (always a leading slash, * and never a trailing slash); leave it up to caller to check if * path name is a directory or not */ /* * explode path name into directory components (dirs and/or file); * also remove null strings (eg "//") and single periods (eg "/./") */ array = explode( str, "/" ) - ({ "", "." }); s = sizeof(array); for (i = 0; i < s; i++) { /* * handle ".." occurrences */ if ( array[ i ] == ".." ) { /* * naive approach: if we encounter ".." when at root * just return root, ignoring rest of path; * ie assume the rest of the path is invalid */ if ( i < 1 ) { /* handle ".." at root of filesystem */ #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") return FTP_DIR; else #endif return "/"; } /* * get leading portion of path name */ if ( i == 1 ) array1 = ({ }); else array1 = array[ 0..i-2 ]; /* * get trailing portion of path name, ignoring the middle */ s -= 2; if ( i <= s ) array1 += array[ i+1 .. <1]; array = array1; i -= 2; } } #ifdef ANONYMOUS_FTP if (UNAME == "anonymous") if ( array ) str = FTP_DIR + implode( array, "/" ); else str = FTP_DIR; else #endif if ( array ) str = "/" + implode( array, "/" ); else str = "/"; return str; } static void check_connections() { int *sockets, i, limit; int pfd, fd; /* * check control connections for idle users */ if (number_of_users) { sockets = keys( socket_info ); i = sizeof( sockets ); while ( i-- ) { fd = sockets[i]; if ( !undefinedp(socket_info[ fd ][LAST_DATA]) ) { limit = time() - socket_info[ fd ][IDLE_SETTING]; if ( socket_info[ fd ][LAST_DATA] < limit ) { socket_write( fd, "421 Timeout: closing control session.\n" ); logout( fd ); lose_user( fd ); } } } } sockets = keys( socket_info ); i = sizeof( sockets ); /* * check data connections for orphans */ while ( i-- ) { fd = sockets[i]; if (!undefinedp( pfd = socket_info[ fd ][ PARENT_FD ] )) { if (undefinedp(socket_info[ pfd ])) { #ifdef FILE_LOCKING if (!undefinedp(socket_info[ fd ][ TYPE ]) && socket_info[ fd ][ TYPE ] == DOWNLOAD ) { flock_wrapper( socket_info[ fd ][ PATH ], F_UNLOCK, fd ); #endif } lose_connection( fd ); } } } call_out( "check_connections", 3 * 60 ); } void resolve_callback( string address, string resolved, int key ) { int id; if (undefinedp( ( id = temp_map[ key ] ) )) { /* * You get this error when the addr_server goes down * (or was never up to begin with) */ ; } else if ( undefinedp( socket_info[ id ] )) { /* * You get this error if the ftp user dropped the connection, * whereby the socket was closed and removed from the mapping * (see in_close_callback()) */ ; } else { if ( address && address != "" ) socket_info[id][USER_SITE] = address; /* else retain unresolved ip number address */ } } void remove() { if (number_of_users && this_player(1) && !adminp(geteuid(this_player(1)))) error( "Cannot destruct while there are active ftp sessions.\n" ); destruct( this_object() ); } /* EOF */