# -*- Mode: Python; tab-width: 4 -*- # Author: Sam Rushing # Copyright 1996-2000 by Sam Rushing # All Rights Reserved. # RCS_ID = '$Id: ftp_server.py,v 1.21 2002/05/09 16:58:38 andreasjung Exp $' # An extensible, configurable, asynchronous FTP server. # # All socket I/O is non-blocking, however file I/O is currently # blocking. Eventually file I/O may be made non-blocking, too, if it # seems necessary. Currently the only CPU-intensive operation is # getting and formatting a directory listing. [this could be moved # into another process/directory server, or another thread?] # # Only a subset of RFC 959 is implemented, but much of that RFC is # vestigial anyway. I've attempted to include the most commonly-used # commands, using the feature set of wu-ftpd as a guide. import asyncore import asynchat import os import socket import stat import string import sys import time # TODO: implement a directory listing cache. On very-high-load # servers this could save a lot of disk abuse, and possibly the # work of computing emulated unix ls output. # Potential security problem with the FTP protocol? I don't think # there's any verification of the origin of a data connection. Not # really a problem for the server (since it doesn't send the port # command, except when in PASV mode) But I think a data connection # could be spoofed by a program with access to a sniffer - it could # watch for a PORT command to go over a command channel, and then # connect to that port before the server does. # Unix user id's: # In order to support assuming the id of a particular user, # it seems there are two options: # 1) fork, and seteuid in the child # 2) carefully control the effective uid around filesystem accessing # methods, using try/finally. [this seems to work] if RCS_ID.startswith('$Id: '): VERSION = string.split(RCS_ID)[2] else: VERSION = '0.0' from counter import counter import producers import status_handler import logger import string class ftp_channel (asynchat.async_chat): # defaults for a reliable __repr__ addr = ('unknown','0') # unset this in a derived class in order # to enable the commands in 'self.write_commands' read_only = 1 write_commands = ['appe','dele','mkd','rmd','rnfr','rnto','stor','stou'] restart_position = 0 # comply with (possibly troublesome) RFC959 requirements # This is necessary to correctly run an active data connection # through a firewall that triggers on the source port (expected # to be 'L-1', or 20 in the normal case). bind_local_minus_one = 0 def __init__ (self, server, conn, addr): self.server = server self.current_mode = 'a' self.addr = addr asynchat.async_chat.__init__ (self, conn) self.set_terminator ('\r\n') # client data port. Defaults to 'the same as the control connection'. self.client_addr = (addr[0], 21) self.client_dc = None self.in_buffer = '' self.closing = 0 self.passive_acceptor = None self.passive_connection = None self.filesystem = None self.authorized = 0 # send the greeting self.respond ( '220 %s FTP server (Medusa Async V%s [experimental]) ready.' % ( self.server.hostname, VERSION ) ) # def __del__ (self): # print 'ftp_channel.__del__()' # -------------------------------------------------- # async-library methods # -------------------------------------------------- def handle_expt (self): # this is handled below. not sure what I could # do here to make that code less kludgish. pass def collect_incoming_data (self, data): self.in_buffer = self.in_buffer + data if len(self.in_buffer) > 4096: # silently truncate really long lines # (possible denial-of-service attack) self.in_buffer = '' def found_terminator (self): line = self.in_buffer if not len(line): return sp = string.find (line, ' ') if sp != -1: line = [line[:sp], line[sp+1:]] else: line = [line] command = string.lower (line[0]) # watch especially for 'urgent' abort commands. if string.find (command, 'abor') != -1: # strip off telnet sync chars and the like... while command and command[0] not in string.letters: command = command[1:] fun_name = 'cmd_%s' % command if command != 'pass': self.log ('<== %s' % repr(self.in_buffer)[1:-1]) else: self.log ('<== %s' % line[0]+' ') self.in_buffer = '' if not hasattr (self, fun_name): self.command_not_understood (line[0]) return fun = getattr (self, fun_name) if (not self.authorized) and (command not in ('user', 'pass', 'help', 'quit')): self.respond ('530 Please log in with USER and PASS') elif (not self.check_command_authorization (command)): self.command_not_authorized (command) else: try: result = apply (fun, (line,)) except: self.server.total_exceptions.increment() (file, fun, line), t,v, tbinfo = asyncore.compact_traceback() if self.client_dc: try: self.client_dc.close() except: pass self.respond ( '451 Server Error: %s, %s: file: %s line: %s' % ( t,v,file,line, ) ) closed = 0 def close (self): if not self.closed: self.closed = 1 if self.passive_acceptor: self.passive_acceptor.close() if self.client_dc: self.client_dc.close() self.server.closed_sessions.increment() asynchat.async_chat.close (self) # -------------------------------------------------- # filesystem interface functions. # override these to provide access control or perform # other functions. # -------------------------------------------------- def cwd (self, line): return self.filesystem.cwd (line[1]) def cdup (self, line): return self.filesystem.cdup() def open (self, path, mode): return self.filesystem.open (path, mode) # returns a producer def listdir (self, path, long=0): return self.filesystem.listdir (path, long) def get_dir_list (self, line, long=0): # we need to scan the command line for arguments to '/bin/ls'... args = line[1:] path_args = [] for arg in args: if arg[0] != '-': path_args.append (arg) else: # ignore arguments pass if len(path_args) < 1: dir = '.' else: dir = path_args[0] return self.listdir (dir, long) # -------------------------------------------------- # authorization methods # -------------------------------------------------- def check_command_authorization (self, command): if command in self.write_commands and self.read_only: return 0 else: return 1 # -------------------------------------------------- # utility methods # -------------------------------------------------- def log (self, message): self.server.logger.log ( self.addr[0], '%d %s' % ( self.addr[1], message ) ) def respond (self, resp): self.log ('==> %s' % resp) self.push (resp + '\r\n') def command_not_understood (self, command): self.respond ("500 '%s': command not understood." % command) def command_not_authorized (self, command): self.respond ( "530 You are not authorized to perform the '%s' command" % ( command ) ) def make_xmit_channel (self): # In PASV mode, the connection may or may _not_ have been made # yet. [although in most cases it is... FTP Explorer being # the only exception I've yet seen]. This gets somewhat confusing # because things may happen in any order... pa = self.passive_acceptor if pa: if pa.ready: # a connection has already been made. conn, addr = self.passive_acceptor.ready cdc = xmit_channel (self, addr) cdc.set_socket (conn) cdc.connected = 1 self.passive_acceptor.close() self.passive_acceptor = None else: # we're still waiting for a connect to the PASV port. cdc = xmit_channel (self) else: # not in PASV mode. ip, port = self.client_addr cdc = xmit_channel (self, self.client_addr) cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM) if self.bind_local_minus_one: cdc.bind (('', self.server.port - 1)) try: cdc.connect ((ip, port)) except socket.error, why: self.respond ("425 Can't build data connection") self.client_dc = cdc # pretty much the same as xmit, but only right on the verge of # being worth a merge. def make_recv_channel (self, fd): pa = self.passive_acceptor if pa: if pa.ready: # a connection has already been made. conn, addr = pa.ready cdc = recv_channel (self, addr, fd) cdc.set_socket (conn) cdc.connected = 1 self.passive_acceptor.close() self.passive_acceptor = None else: # we're still waiting for a connect to the PASV port. cdc = recv_channel (self, None, fd) else: # not in PASV mode. ip, port = self.client_addr cdc = recv_channel (self, self.client_addr, fd) cdc.create_socket (socket.AF_INET, socket.SOCK_STREAM) try: cdc.connect ((ip, port)) except socket.error, why: self.respond ("425 Can't build data connection") self.client_dc = cdc type_map = { 'a':'ASCII', 'i':'Binary', 'e':'EBCDIC', 'l':'Binary' } type_mode_map = { 'a':'t', 'i':'b', 'e':'b', 'l':'b' } # -------------------------------------------------- # command methods # -------------------------------------------------- def cmd_type (self, line): 'specify data transfer type' # ascii, ebcdic, image, local t = string.lower (line[1]) # no support for EBCDIC # if t not in ['a','e','i','l']: if t not in ['a','i','l']: self.command_not_understood (string.join (line)) elif t == 'l' and (len(line) > 2 and line[2] != '8'): self.respond ('504 Byte size must be 8') else: self.current_mode = t self.respond ('200 Type set to %s.' % self.type_map[t]) def cmd_quit (self, line): 'terminate session' self.respond ('221 Goodbye.') self.close_when_done() def cmd_port (self, line): 'specify data connection port' info = string.split (line[1], ',') ip = string.join (info[:4], '.') port = string.atoi(info[4])*256 + string.atoi(info[5]) # how many data connections at a time? # I'm assuming one for now... # TODO: we should (optionally) verify that the # ip number belongs to the client. [wu-ftpd does this?] self.client_addr = (ip, port) self.respond ('200 PORT command successful.') def new_passive_acceptor (self): # ensure that only one of these exists at a time. if self.passive_acceptor is not None: self.passive_acceptor.close() self.passive_acceptor = None self.passive_acceptor = passive_acceptor (self) return self.passive_acceptor def cmd_pasv (self, line): 'prepare for server-to-server transfer' pc = self.new_passive_acceptor() port = pc.addr[1] ip_addr = pc.control_channel.getsockname()[0] self.respond ( '227 Entering Passive Mode (%s,%d,%d)' % ( string.join (string.split (ip_addr, '.'), ','), port/256, port%256 ) ) self.client_dc = None def cmd_nlst (self, line): 'give name list of files in directory' # ncftp adds the -FC argument for the user-visible 'nlist' # command. We could try to emulate ls flags, but not just yet. if '-FC' in line: line.remove ('-FC') try: dir_list_producer = self.get_dir_list (line, 0) except os.error, why: self.respond ('550 Could not list directory: %s' % repr(why)) return self.respond ( '150 Opening %s mode data connection for file list' % ( self.type_map[self.current_mode] ) ) self.make_xmit_channel() self.client_dc.push_with_producer (dir_list_producer) self.client_dc.close_when_done() def cmd_list (self, line): 'give list files in a directory' try: dir_list_producer = self.get_dir_list (line, 1) except os.error, why: self.respond ('550 Could not list directory: %s' % repr(why)) return self.respond ( '150 Opening %s mode data connection for file list' % ( self.type_map[self.current_mode] ) ) self.make_xmit_channel() self.client_dc.push_with_producer (dir_list_producer) self.client_dc.close_when_done() def cmd_cwd (self, line): 'change working directory' if self.cwd (line): self.respond ('250 CWD command successful.') else: self.respond ('550 No such directory.') def cmd_cdup (self, line): 'change to parent of current working directory' if self.cdup(line): self.respond ('250 CDUP command successful.') else: self.respond ('550 No such directory.') def cmd_pwd (self, line): 'print the current working directory' self.respond ( '257 "%s" is the current directory.' % ( self.filesystem.current_directory() ) ) # modification time # example output: # 213 19960301204320 def cmd_mdtm (self, line): 'show last modification time of file' filename = line[1] if not self.filesystem.isfile (filename): self.respond ('550 "%s" is not a file' % filename) else: mtime = time.gmtime(self.filesystem.stat(filename)[stat.ST_MTIME]) self.respond ( '213 %4d%02d%02d%02d%02d%02d' % ( mtime[0], mtime[1], mtime[2], mtime[3], mtime[4], mtime[5] ) ) def cmd_noop (self, line): 'do nothing' self.respond ('200 NOOP command successful.') def cmd_size (self, line): 'return size of file' filename = line[1] if not self.filesystem.isfile (filename): self.respond ('550 "%s" is not a file' % filename) else: self.respond ( '213 %d' % (self.filesystem.stat(filename)[stat.ST_SIZE]) ) def cmd_retr (self, line): 'retrieve a file' if len(line) < 2: self.command_not_understood (string.join (line)) else: file = line[1] if not self.filesystem.isfile (file): self.log_info ('checking %s' % file) self.respond ('550 No such file') else: try: # FIXME: for some reason, 'rt' isn't working on win95 mode = 'r'+self.type_mode_map[self.current_mode] fd = self.open (file, mode) except IOError, why: self.respond ('553 could not open file for reading: %s' % (repr(why))) return self.respond ( "150 Opening %s mode data connection for file '%s'" % ( self.type_map[self.current_mode], file ) ) self.make_xmit_channel() if self.restart_position: # try to position the file as requested, but # give up silently on failure (the 'file object' # may not support seek()) try: fd.seek (self.restart_position) except: pass self.restart_position = 0 self.client_dc.push_with_producer ( file_producer (self, self.client_dc, fd) ) self.client_dc.close_when_done() def cmd_stor (self, line, mode='wb'): 'store a file' if len (line) < 2: self.command_not_understood (string.join (line)) else: if self.restart_position: restart_position = 0 self.respond ('553 restart on STOR not yet supported') return file = line[1] # todo: handle that type flag try: fd = self.open (file, mode) except IOError, why: self.respond ('553 could not open file for writing: %s' % (repr(why))) return self.respond ( '150 Opening %s connection for %s' % ( self.type_map[self.current_mode], file ) ) self.make_recv_channel (fd) def cmd_abor (self, line): 'abort operation' if self.client_dc: self.client_dc.close() self.respond ('226 ABOR command successful.') def cmd_appe (self, line): 'append to a file' return self.cmd_stor (line, 'ab') def cmd_dele (self, line): if len (line) != 2: self.command_not_understood (string.join (line)) else: file = line[1] if self.filesystem.isfile (file): try: self.filesystem.unlink (file) self.respond ('250 DELE command successful.') except: self.respond ('550 error deleting file.') else: self.respond ('550 %s: No such file.' % file) def cmd_mkd (self, line): if len (line) != 2: self.command.not_understood (string.join (line)) else: path = line[1] try: self.filesystem.mkdir (path) self.respond ('257 MKD command successful.') except: self.respond ('550 error creating directory.') def cmd_rmd (self, line): if len (line) != 2: self.command.not_understood (string.join (line)) else: path = line[1] try: self.filesystem.rmdir (path) self.respond ('250 RMD command successful.') except: self.respond ('550 error removing directory.') def cmd_user (self, line): 'specify user name' if len(line) > 1: self.user = line[1] self.respond ('331 Password required.') else: self.command_not_understood (string.join (line)) def cmd_pass (self, line): 'specify password' if len(line) < 2: pw = '' else: pw = line[1] result, message, fs = self.server.authorizer.authorize (self, self.user, pw) if result: self.respond ('230 %s' % message) self.filesystem = fs self.authorized = 1 self.log_info('Successful login: Filesystem=%s' % repr(fs)) else: self.respond ('530 %s' % message) def cmd_rest (self, line): 'restart incomplete transfer' try: pos = string.atoi (line[1]) except ValueError: self.command_not_understood (string.join (line)) self.restart_position = pos self.respond ( '350 Restarting at %d. Send STORE or RETRIEVE to initiate transfer.' % pos ) def cmd_stru (self, line): 'obsolete - set file transfer structure' if line[1] in 'fF': # f == 'file' self.respond ('200 STRU F Ok') else: self.respond ('504 Unimplemented STRU type') def cmd_mode (self, line): 'obsolete - set file transfer mode' if line[1] in 'sS': # f == 'file' self.respond ('200 MODE S Ok') else: self.respond ('502 Unimplemented MODE type') # The stat command has two personalities. Normally it returns status # information about the current connection. But if given an argument, # it is equivalent to the LIST command, with the data sent over the # control connection. Strange. But wuftpd, ftpd, and nt's ftp server # all support it. # ## def cmd_stat (self, line): ## 'return status of server' ## pass def cmd_syst (self, line): 'show operating system type of server system' # Replying to this command is of questionable utility, because # this server does not behave in a predictable way w.r.t. the # output of the LIST command. We emulate Unix ls output, but # on win32 the pathname can contain drive information at the front # Currently, the combination of ensuring that os.sep == '/' # and removing the leading slash when necessary seems to work. # [cd'ing to another drive also works] # # This is how wuftpd responds, and is probably # the most expected. The main purpose of this reply is so that # the client knows to expect Unix ls-style LIST output. self.respond ('215 UNIX Type: L8') # one disadvantage to this is that some client programs # assume they can pass args to /bin/ls. # a few typical responses: # 215 UNIX Type: L8 (wuftpd) # 215 Windows_NT version 3.51 # 215 VMS MultiNet V3.3 # 500 'SYST': command not understood. (SVR4) def cmd_help (self, line): 'give help information' # find all the methods that match 'cmd_xxxx', # use their docstrings for the help response. attrs = dir(self.__class__) help_lines = [] for attr in attrs: if attr[:4] == 'cmd_': x = getattr (self, attr) if type(x) == type(self.cmd_help): if x.__doc__: help_lines.append ('\t%s\t%s' % (attr[4:], x.__doc__)) if help_lines: self.push ('214-The following commands are recognized\r\n') self.push_with_producer (producers.lines_producer (help_lines)) self.push ('214 \r\n') else: self.push ('214-\r\n\tHelp Unavailable\r\n214 \r\n') class ftp_server (asyncore.dispatcher): # override this to spawn a different FTP channel class. ftp_channel_class = ftp_channel SERVER_IDENT = 'FTP Server (V%s)' % VERSION def __init__ ( self, authorizer, hostname =None, ip ='', port =21, resolver =None, logger_object=logger.file_logger (sys.stdout) ): self.ip = ip self.port = port self.authorizer = authorizer if hostname is None: self.hostname = socket.gethostname() else: self.hostname = hostname # statistics self.total_sessions = counter() self.closed_sessions = counter() self.total_files_out = counter() self.total_files_in = counter() self.total_bytes_out = counter() self.total_bytes_in = counter() self.total_exceptions = counter() # asyncore.dispatcher.__init__ (self) self.create_socket (socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind ((self.ip, self.port)) self.listen (5) if not logger_object: logger_object = sys.stdout if resolver: self.logger = logger.resolving_logger (resolver, logger_object) else: self.logger = logger.unresolving_logger (logger_object) self.log_info('FTP server started at %s\n\tAuthorizer:%s\n\tHostname: %s\n\tPort: %d' % ( time.ctime(time.time()), repr (self.authorizer), self.hostname, self.port) ) def writable (self): return 0 def handle_read (self): pass def handle_connect (self): pass def handle_accept (self): conn, addr = self.accept() self.total_sessions.increment() self.log_info('Incoming connection from %s:%d' % (addr[0], addr[1])) self.ftp_channel_class (self, conn, addr) # return a producer describing the state of the server def status (self): def nice_bytes (n): return string.join (status_handler.english_bytes (n)) return producers.lines_producer ( ['

%s

' % self.SERVER_IDENT, '
Listening on Host: %s' % self.hostname, 'Port: %d' % self.port, '
Sessions', 'Total: %s' % self.total_sessions, 'Current: %d' % (self.total_sessions.as_long() - self.closed_sessions.as_long()), '
Files', 'Sent: %s' % self.total_files_out, 'Received: %s' % self.total_files_in, '
Bytes', 'Sent: %s' % nice_bytes (self.total_bytes_out.as_long()), 'Received: %s' % nice_bytes (self.total_bytes_in.as_long()), '
Exceptions: %s' % self.total_exceptions, ] ) # ====================================================================== # Data Channel Classes # ====================================================================== # This socket accepts a data connection, used when the server has been # placed in passive mode. Although the RFC implies that we ought to # be able to use the same acceptor over and over again, this presents # a problem: how do we shut it off, so that we are accepting # connections only when we expect them? [we can't] # # wuftpd, and probably all the other servers, solve this by allowing # only one connection to hit this acceptor. They then close it. Any # subsequent data-connection command will then try for the default # port on the client side [which is of course never there]. So the # 'always-send-PORT/PASV' behavior seems required. # # Another note: wuftpd will also be listening on the channel as soon # as the PASV command is sent. It does not wait for a data command # first. # --- we need to queue up a particular behavior: # 1) xmit : queue up producer[s] # 2) recv : the file object # # It would be nice if we could make both channels the same. Hmmm.. # class passive_acceptor (asyncore.dispatcher): ready = None def __init__ (self, control_channel): # connect_fun (conn, addr) asyncore.dispatcher.__init__ (self) self.control_channel = control_channel self.create_socket (socket.AF_INET, socket.SOCK_STREAM) # bind to an address on the interface that the # control connection is coming from. self.bind (( self.control_channel.getsockname()[0], 0 )) self.addr = self.getsockname() self.listen (1) # def __del__ (self): # print 'passive_acceptor.__del__()' def log (self, *ignore): pass def handle_accept (self): conn, addr = self.accept() conn.setblocking(0) dc = self.control_channel.client_dc if dc is not None: dc.set_socket (conn) dc.addr = addr dc.connected = 1 self.control_channel.passive_acceptor = None else: self.ready = conn, addr self.close() class xmit_channel (asynchat.async_chat): # for an ethernet, you want this to be fairly large, in fact, it # _must_ be large for performance comparable to an ftpd. [64k] we # ought to investigate automatically-sized buffers... ac_out_buffer_size = 16384 bytes_out = 0 def __init__ (self, channel, client_addr=None): self.channel = channel self.client_addr = client_addr asynchat.async_chat.__init__ (self) # def __del__ (self): # print 'xmit_channel.__del__()' def log (*args): pass def readable (self): return not self.connected def writable (self): return 1 def send (self, data): result = asynchat.async_chat.send (self, data) self.bytes_out = self.bytes_out + result return result def handle_error (self): # usually this is to catch an unexpected disconnect. self.log_info ('unexpected disconnect on data xmit channel', 'error') try: self.close() except: pass # TODO: there's a better way to do this. we need to be able to # put 'events' in the producer fifo. to do this cleanly we need # to reposition the 'producer' fifo as an 'event' fifo. # dummy function to suppress warnings caused by some FTP clients def handle_connect(self): pass def close (self): c = self.channel s = c.server c.client_dc = None s.total_files_out.increment() s.total_bytes_out.increment (self.bytes_out) if not len(self.producer_fifo): c.respond ('226 Transfer complete') elif not c.closed: c.respond ('426 Connection closed; transfer aborted') del c del s del self.channel asynchat.async_chat.close (self) class recv_channel (asyncore.dispatcher): def __init__ (self, channel, client_addr, fd): self.channel = channel self.client_addr = client_addr self.fd = fd asyncore.dispatcher.__init__ (self) self.bytes_in = counter() def log (self, *ignore): pass def handle_connect (self): pass def writable (self): return 0 def recv (*args): result = apply (asyncore.dispatcher.recv, args) self = args[0] self.bytes_in.increment(len(result)) return result buffer_size = 8192 def handle_read (self): block = self.recv (self.buffer_size) if block: try: self.fd.write (block) except IOError: self.log_info ('got exception writing block...', 'error') def handle_close (self): s = self.channel.server s.total_files_in.increment() s.total_bytes_in.increment(self.bytes_in.as_long()) self.fd.close() self.channel.respond ('226 Transfer complete.') self.close() import filesys # not much of a doorman! 8^) class dummy_authorizer: def __init__ (self, root='/'): self.root = root def authorize (self, channel, username, password): channel.persona = -1, -1 channel.read_only = 1 return 1, 'Ok.', filesys.os_filesystem (self.root) class anon_authorizer: def __init__ (self, root='/'): self.root = root def authorize (self, channel, username, password): if username in ('ftp', 'anonymous'): channel.persona = -1, -1 channel.read_only = 1 return 1, 'Ok.', filesys.os_filesystem (self.root) else: return 0, 'Password invalid.', None # =========================================================================== # Unix-specific improvements # =========================================================================== if os.name == 'posix': class unix_authorizer: # return a trio of (success, reply_string, filesystem) def authorize (self, channel, username, password): import crypt import pwd try: info = pwd.getpwnam (username) except KeyError: return 0, 'No such user.', None mangled = info[1] if crypt.crypt (password, mangled[:2]) == mangled: channel.read_only = 0 fs = filesys.schizophrenic_unix_filesystem ( '/', info[5], persona = (info[2], info[3]) ) return 1, 'Login successful.', fs else: return 0, 'Password invalid.', None def __repr__ (self): return '' # simple anonymous ftp support class unix_authorizer_with_anonymous (unix_authorizer): def __init__ (self, root=None, real_users=0): self.root = root self.real_users = real_users def authorize (self, channel, username, password): if string.lower(username) in ['anonymous', 'ftp']: import pwd try: # ok, here we run into lots of confusion. # on some os', anon runs under user 'nobody', # on others as 'ftp'. ownership is also critical. # need to investigate. # linux: new linuxen seem to have nobody's UID=-1, # which is an illegal value. Use ftp. ftp_user_info = pwd.getpwnam ('ftp') if string.lower(os.uname()[0]) == 'linux': nobody_user_info = pwd.getpwnam ('ftp') else: nobody_user_info = pwd.getpwnam ('nobody') channel.read_only = 1 if self.root is None: self.root = ftp_user_info[5] fs = filesys.unix_filesystem (self.root, '/') return 1, 'Anonymous Login Successful', fs except KeyError: return 0, 'Anonymous account not set up', None elif self.real_users: return unix_authorizer.authorize ( self, channel, username, password ) else: return 0, 'User logins not allowed', None class file_producer: block_size = 16384 def __init__ (self, server, dc, fd): self.fd = fd self.done = 0 def more (self): if self.done: return '' else: block = self.fd.read (self.block_size) if not block: self.fd.close() self.done = 1 return block # usage: ftp_server /PATH/TO/FTP/ROOT PORT # for example: # $ ftp_server /home/users/ftp 8021 if os.name == 'posix': def test (port='8021'): import sys fs = ftp_server ( unix_authorizer(), port=string.atoi (port) ) try: asyncore.loop() except KeyboardInterrupt: self.log_info('FTP server shutting down. (received SIGINT)', 'warning') # close everything down on SIGINT. # of course this should be a cleaner shutdown. asyncore.close_all() if __name__ == '__main__': test (sys.argv[1]) # not unix else: def test (): fs = ftp_server (dummy_authorizer()) if __name__ == '__main__': test () # this is the command list from the wuftpd man page # '*' means we've implemented it. # '!' requires write access # command_documentation = { 'abor': 'abort previous command', #* 'acct': 'specify account (ignored)', 'allo': 'allocate storage (vacuously)', 'appe': 'append to a file', #*! 'cdup': 'change to parent of current working directory', #* 'cwd': 'change working directory', #* 'dele': 'delete a file', #! 'help': 'give help information', #* 'list': 'give list files in a directory', #* 'mkd': 'make a directory', #! 'mdtm': 'show last modification time of file', #* 'mode': 'specify data transfer mode', 'nlst': 'give name list of files in directory', #* 'noop': 'do nothing', #* 'pass': 'specify password', #* 'pasv': 'prepare for server-to-server transfer', #* 'port': 'specify data connection port', #* 'pwd': 'print the current working directory', #* 'quit': 'terminate session', #* 'rest': 'restart incomplete transfer', #* 'retr': 'retrieve a file', #* 'rmd': 'remove a directory', #! 'rnfr': 'specify rename-from file name', #! 'rnto': 'specify rename-to file name', #! 'site': 'non-standard commands (see next section)', 'size': 'return size of file', #* 'stat': 'return status of server', #* 'stor': 'store a file', #*! 'stou': 'store a file with a unique name', #! 'stru': 'specify data transfer structure', 'syst': 'show operating system type of server system', #* 'type': 'specify data transfer type', #* 'user': 'specify user name', #* 'xcup': 'change to parent of current working directory (deprecated)', 'xcwd': 'change working directory (deprecated)', 'xmkd': 'make a directory (deprecated)', #! 'xpwd': 'print the current working directory (deprecated)', 'xrmd': 'remove a directory (deprecated)', #! } # debugging aid (linux) def get_vm_size (): return string.atoi (string.split(open ('/proc/self/stat').readline())[22]) def print_vm(): print 'vm: %8dk' % (get_vm_size()/1024)