""" A minimal FTP server in Python. """ import os, sys, posixpath, stat, time from StringIO import StringIO import socket import greensock2 class FTPConnexion(object): WELCOME = 'Welcome from ftpd.py' def __init__(self, conn, addr, sdata, rootdir=os.curdir): self.conn = conn self.addr = addr self.sdata = sdata self.rootdir = os.path.abspath(rootdir) self.workingdir = '/' self.dataport = None self.logtty = sys.platform != "win32" and sys.stdout.isatty() def translate_path(self, path): path = posixpath.join(self.workingdir, path) components = path.split('/') i = 0 while i < len(components): p = components[i] if not p or p == '.': del components[i] elif p == '..': if i > 0: i -= 1 del components[i:i+2] else: del components[i] else: i += 1 result = os.path.join(self.rootdir, *components) return result def reply(self, code, msg=None): if msg is None: msg = ERRCODES.get(code, 'xxx') reply = '%d %s\r\n' % (code, msg) print '\t' + reply[:-2] greensock2.sendall(self.conn, reply) def withquotes(self, s): return '"%s"' % (s.replace('"', '""'),) def run(self): self.reply(220, self.WELCOME) greensock2.autogreenlet(self.accept_commands) def accept_commands(self): try: buf = '' while 1: t = greensock2.recv(self.conn, 2048) buf += t.replace('\r', '') lines = buf.split('\n') for line in lines[:-1]: self.process_command(line) buf = lines[-1] except Exception, e: msg = str(e) if msg: msg = ': ' + msg print '* %s%s' % (e.__class__.__name__, msg) print '* disconnected from', self.addr def process_command(self, line): msg = line if self.logtty: msg = '\x1b[1m%s\x1b[0m' % (msg,) else: msg = '>> ' + msg print '\t' + msg args = line.split(None, 1) cmd = args[0].upper() if len(args) == 1: arg = '' else: arg = args[1] try: meth = getattr(self, 'do_' + cmd) except AttributeError: self.reply(502) else: meth(arg) def do_USER(self, username): self.reply(331, 'Any password will work') def do_PASS(self, passwd): self.reply(230) def do_PWD(self, ignored): self.reply(257, '%s is the current working dir' % ( self.withquotes(self.workingdir),)) def do_CWD(self, p): p = posixpath.join(self.workingdir, p) p = posixpath.normpath(p) if not os.path.isdir(self.translate_path(p)): self.reply(431, 'No such directory.') else: if not p.endswith('/'): p += '/' self.workingdir = p self.reply(250, '%s is now the working dir' % ( self.withquotes(self.workingdir),)) def do_HELP(self, ignored): self.reply(211, 'Ready.') def do_NOOP(self, ignored): self.reply(200) ## def do_PASV(self, ignored): ## self.sdata = socket.socket() ## self.sdata.bind(('', socket.INADDR_ANY)) ## self.sdata.listen(3) ## 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2). def do_PORT(self, addr): h1,h2,h3,h4,p1,p2 = [int(n.strip()) for n in addr.split(',')] addr = ('%d.%d.%d.%d' % (h1, h2, h3, h4), (p1<<8) | p2) self.dataport = addr self.reply(200) def do_QUIT(self, ignored): self.reply(221) self.conn.shutdown(socket.SHUT_RDWR) def do_LIST(self, path): locpath = self.translate_path(path) try: f = self.listdir(locpath) except OSError, e: self.reply(450, str(e)) else: self.sendfile(f) def listdir(self, locpath): namelist = os.listdir(locpath) f = StringIO() for name in namelist: if name.startswith('.'): continue # hidden try: st = os.lstat(os.path.join(locpath, name)) except OSError: continue # cannot stat print >> f, self.format_stat(locpath, name, st) f.seek(0) return f def format_stat(self, locdir, name, st): if stat.S_ISLNK(st.st_mode): kind = 'l' elif stat.S_ISDIR(st.st_mode): kind = 'd' else: kind = '-' mode = '' for bit, char in enumerate('rwxrwxrwx'): if st.st_mode & (0400 >> bit): mode += char else: mode += '-' mtime = time.strftime("%b %2d %Y", time.localtime(st.st_mtime)) return '%s%s %2d %5d %5d %5d %s %s' % ( kind, mode, st.st_nlink, st.st_uid, st.st_gid, st.st_size, mtime, name) def do_RETR(self, path): locpath = self.translate_path(path) try: f = open(locpath, 'rb') except IOError, e: self.reply(450, str(e)) else: self.sendfile(f) def do_SIZE(self, path): locpath = self.translate_path(path) try: st = os.stat(locpath) except OSError, e: self.reply(450, str(e)) else: self.reply(213, str(st.st_size)) def do_TYPE(self, type): self.reply(200) def sendfile(self, f): try: if self.dataport is None: self.reply(425) return self.reply(150) s = socket.socket() try: try: s.connect(self.dataport) except socket.error, e: self.reply(425, str(e)) return try: buf = '' while 1: if len(buf) < 32768: t = f.read(65536) if not t: break buf += t greensock2.wait_output(s) count = s.send(buf) buf = buf[count:] if buf: greensock2.sendall(s, buf) s.shutdown(socket.SHUT_RDWR) except socket.error, e: self.reply(426, str(e)) return finally: s.close() self.reply(250) finally: f.close() ERRCODES = { 150: "File status okay; about to open data connection.", 200: "Command okay.", 221: "Service closing control connection.", 230: "User logged in, proceed.", 250: "Requested file action okay, completed.", 331: "User name okay, need password.", 425: "Can't open data connection.", 502: "Command not implemented.", } def run_server(port, data_port): srv = socket.socket() srv.bind(('', port)) sdata = socket.socket() sdata.bind(('', data_port)) sdata.listen(5) srv.listen(5) print '* listening on %r, %r' % (srv.getsockname(), sdata.getsockname()) while 1: greensock2.wait_input(srv) conn, addr = srv.accept() print '* connected by', addr FTPConnexion(conn, addr, sdata).run() if __name__ == '__main__': if len(sys.argv) > 1: port = int(sys.argv[1]) else: port = 21 run_server(port, port-1)