# -*- Mode: Python; tab-width: 4 -*-
# Author: Sam Rushing <rushing@nightmare.com>
# 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]+' <password>')
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 <byte size>
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 (
['<h2>%s</h2>' % self.SERVER_IDENT,
'<br>Listening on <b>Host:</b> %s' % self.hostname,
'<b>Port:</b> %d' % self.port,
'<br>Sessions',
'<b>Total:</b> %s' % self.total_sessions,
'<b>Current:</b> %d' % (self.total_sessions.as_long() - self.closed_sessions.as_long()),
'<br>Files',
'<b>Sent:</b> %s' % self.total_files_out,
'<b>Received:</b> %s' % self.total_files_in,
'<br>Bytes',
'<b>Sent:</b> %s' % nice_bytes (self.total_bytes_out.as_long()),
'<b>Received:</b> %s' % nice_bytes (self.total_bytes_in.as_long()),
'<br>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 '<standard unix authorizer>'
# 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)
|