我的FTP Server——ftp.py

原创 2012年03月26日 23:35:10

在上文中,我们简要地学习了下FTP协议,链接 http://everet.org/2012/03/ftp-protocol.html

有兴趣的同学们可以去围观下。

因为最近偶看了下FTP协议,所以决定写个FTP Server玩玩。毕竟一直写的都是应用程序,于是乎想写下服务器端的程序。

结果就有了ftp.py,名字灵感来源于web.py。

ftp.py

ftp.py支持多线程模式和多进程模式,支持虚拟用户。

其中多进程模式支持以其他用户身份运行,加强了安全性。

其中用户设置写在ftp.py.config中,如果文件不存在则默认使用anonymous账户。

用户设置文件ftp.py.config格式为:

account_info = {
‘et’:{‘pass’:’123456789′, ‘home_dir’:'/root/’},
‘lst’:{‘pass’:’987654321′, ‘home_dir’:'/tmp/’}
}

而切换多进程模式或者多线程模式只要新建不同的Server就好。很容易就加个什么参数选择,不过有空再弄了。我一般都是用多进程模式。

#server = FTPThreadServer()
server = FTPForkServer()

源码如下:

最新的源码请见Git: https://github.com/cedricporter/et-python/tree/master/ftp

#!/usr/bin/env python
# author:  Hua Liang [ Stupid ET ]
# email:   et@everet.org
# website: http://EverET.org
#
import socket, os, stat, threading, time, sys, re, signal, select

host = '0.0.0.0'
port = 21
limit_connection_number = 3

runas_user = 'www-data'

account_info = {
    'anonymous':{'pass':'', 'home_dir':'/tmp/'},
    } 
try:
    '''You can write your account_info in ftp.py.config'''
    execfile('ftp.py.config')
except Exception, e:
    print e

class FTPConnection:
    '''You can add handle func by startswith handle_ prefix.
    When the connection receives CWD command, it'll use handle_CWD to handle it.
    '''
    def __init__(self, fd, remote_ip):
        self.fd = fd
        self.data_fd = 0
        self.options = {'pasv': False, 'utf8': False}
        self.data_host = ''
        self.data_port = 0
        self.localhost = fd.getsockname()[0]
        self.home_dir = '/tmp/'
        self.curr_dir = '/'
        self.running = True
        self.handler = dict(
            [(method[7:], getattr(self, method)) \
            for method in dir(self) \
            if method.startswith("handle_") and callable(getattr(self, method))])

    def start(self): 
        try:
            self.say_welcome()
            while self.running:
                success, command, arg = self.recv()
                command = command.upper()
                if self.options['utf8']:
                    arg = unicode(arg, 'utf8').encode(sys.getfilesystemencoding())
                print '[', command, ']', arg
                if not success: 
                    self.send_msg(500, "Failed")
                    continue
                if not self.handler.has_key(command):
                    self.send_msg(500, "Command Not Found")
                    continue
                try:
                    self.handler[command](arg)
                except OSError, e:
                    print e
                    self.send_msg(500, 'Permission denied')
            self.say_bye()
            self.fd.close()
        except Exception, e:
            self.running = False
            print e

        return True

    def send_msg(self, code, msg):
        if self.options['utf8']:
            msg = unicode(msg, sys.getfilesystemencoding()).encode('utf8')
        message = str(code) + ' ' + msg + '\r\n'
        self.fd.send(message)

    def recv(self):
        '''returns 3 tuples, success, command, arg'''
        try:
            success, buf, command, arg = True, '', '', ''
            while True:
                data = self.fd.recv(4096)
                if not data:
                    self.running = False
                    break
                buf += data
                if buf[-2:] == '\r\n': break
            split = buf.find(' ')
            command, arg = (buf[:split], buf[split + 1:].strip()) if split != -1 else (buf.strip(), '')
        except:
            success = False

        return success, command, arg


    def say_welcome(self):
        self.send_msg(220, "Welcome to EverET.org FTP")

    def say_bye(self):
        self.handle_BYE('')

    def data_connect(self):
        '''establish data connection'''
        if self.data_fd == 0:
            self.send_msg(500, "no data connection")
            return False
        elif self.options['pasv']:
            fd, addr = self.data_fd.accept()
            self.data_fd.close()
            self.data_fd = fd
        else:
            try:
                self.data_fd.connect((self.data_host, self.data_port))
            except:
                self.send_msg(500, "failed to connect")
                return False
        return True

    def close_data_fd(self):
        self.data_fd.close()
        self.data_fd = 0

    def parse_path(self, path):
        if path == '': path = '.'
        if path[0] != '/': path = self.curr_dir + '/' + path
        print 'parse_path', path
        split_path = os.path.normpath(path).replace('\\', '/').split('/')
        remote = '' 
        local = self.home_dir
        print split_path
        for item in split_path:
            if item.startswith('..') or item == '': continue # ignore parent directory
            remote += '/' + item
            local += '/' + item
        if remote == '': remote = '/'
        print 'remote', remote, 'local', local
        return remote, local

    # Command Handlers
    def handle_USER(self, arg):
        if arg in account_info:
            self.username = arg
            self.send_msg(331, "Need password")
        else:
            self.send_msg(500, "Invalid User")
            self.running = False
    def handle_PASS(self, arg):
        if arg == account_info[self.username]['pass']: 
            self.home_dir = account_info[self.username]['home_dir']
            if os.path.isdir(self.home_dir):
                self.send_msg(230, "OK")
                return
        self.send_msg(530, "Password is not corrected")
        self.running = False
    def handle_QUIT(self, arg):
        self.handle_BYE(arg)
    def handle_BYE(self, arg):
        self.running = False
        self.send_msg(200, "OK")
    def handle_CDUP(self, arg):
        self.handle_CWD('..')
    def handle_XPWD(self, arg):
        self.handle_PWD(arg)
    def handle_PWD(self, arg):
        remote, local = self.parse_path(self.curr_dir)
        self.send_msg(257, '"' + remote + '"')
    def handle_CWD(self, arg):
        remote, local = self.parse_path(arg)
        try:
            os.listdir(local)
            self.curr_dir = remote
            self.send_msg(250, "OK")
        except Exception, e:
            print e
            self.send_msg(500, "Change directory failed!")
    def handle_SIZE(self, arg):
        remote, local = self.parse_path(self.curr_dir)
        self.send_msg(231, str(os.path.getsize(local)))
    def handle_SYST(self, arg):
        self.send_msg(215, "UNIX")
    def handle_STOR(self, arg):
        remote, local = self.parse_path(arg)
        if not self.data_connect(): return
        self.send_msg(125, "OK")
        f = open(local, 'wb')
        print f, local
        while True:
            data = self.data_fd.recv(8192)
            if len(data) == 0: break
            f.write(data)
        f.close()
        self.close_data_fd()
        self.send_msg(226, "OK")
    def handle_RETR(self, arg):
        print 'in RETR'
        remote, local = self.parse_path(arg)
        if not self.data_connect(): return
        self.send_msg(125, "OK")
        f = open(local, 'rb')
        while True:
            data = f.read(8192)
            if len(data) == 0: break
            self.data_fd.send(data)
        f.close()
        self.close_data_fd()
        self.send_msg(226, "OK")
    def handle_TYPE(self, arg):
        self.send_msg(220, "OK")
    def handle_RNFR(self, arg):
        remote, local = self.parse_path(arg)
        self.rename_tmp_path = local
        self.send_msg(350, 'rename from ' + remote)
    def handle_RNTO(self, arg):
        remote, local = self.parse_path(arg)
        os.rename(self.rename_tmp_path, local)
        self.send_msg(250, 'rename to ' + remote)
    def handle_NLST(self, arg):
        if not self.data_connect(): return
        self.send_msg(125, "OK")
        remote, local = self.parse_path(self.curr_dir)
        for filename in os.listdir(local):
            self.data_fd.send(filename + '\r\n')
        self.send_msg(226, "Limit")
        self.close_data_fd()
    def handle_XMKD(self, arg):
        self.handle_MKD(arg)
    def handle_MKD(self, arg):
        remote, local = self.parse_path(arg)
        if os.path.exists(local):
            self.send_msg(500, "Folder is already existed")
            return
        os.mkdir(local)
        self.send_msg(257, "OK")
    def handle_XRMD(self, arg):
        self.handle_RMD(arg)
    def handle_RMD(self, arg):
        remote, local = self.parse_path(arg)
        if not os.path.exists(local):
            self.send_msg(500, "Folder is not existed")
            return
        os.rmdir(local)
        self.send_msg(250, "OK")
    def handle_LIST(self, arg):
        if not self.data_connect(): return 
        self.send_msg(125, "OK")
        template = "%s%s%s------- %04u %8s %8s %8lu %s %s\r\n"
        remote, local = self.parse_path(self.curr_dir)
        for filename in os.listdir(local):
            path = local + '/' + filename
            if os.path.isfile(path) or os.path.isdir(path): # ignores link or block file
                status = os.stat(path)
                msg = template % (
                    'd' if os.path.isdir(path) else '-',
                    'r', 'w', 1, '0', '0', 
                    status[stat.ST_SIZE], 
                    time.strftime("%b %d  %Y", time.localtime(status[stat.ST_MTIME])), 
                    filename)
                if self.options['utf8']: msg = unicode(msg, sys.getfilesystemencoding()).encode('utf8')
                self.data_fd.send(msg)
        self.send_msg(226, "Limit")
        self.close_data_fd()
    def handle_PASV(self, arg):
        self.options['pasv'] = True
        try:
            self.data_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.data_fd.bind((self.localhost, 0))
            self.data_fd.listen(1)
            ip, port = self.data_fd.getsockname()
            self.send_msg(227, 'Enter Passive Mode (%s,%u,%u).' %
                    (','.join(ip.split('.')), (port >> 8 & 0xff), (port & 0xff)))
        except Exception, e:
            print e
            self.send_msg(500, 'passive mode failed')
    def handle_PORT(self, arg):
        try:
            if self.data_fd:
                self.data_fd.close()
            t = arg.split(',')
            self.data_host = '.'.join(t[:4])
            self.data_port = int(t[4]) * 256 + int(t[5])
            self.data_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        except:
            self.send_msg(500, "PORT failed")
        self.send_msg(200, "OK")
    def handle_DELE(self, arg):
        remote, local = self.parse_path(arg)
        if not os.path.exists(local):
            self.send_msg(450, "File not exist")
            return
        os.remove(local)
        self.send_msg(250, 'File deleted')
    def handle_OPTS(self, arg):
        if arg.upper() == "UTF8 ON":
            self.options['utf8'] = True
            self.send_msg(200, "OK")
        elif arg.upper() == "UTF8 OFF":
            self.options['utf8'] = False
            self.send_msg(200, "OK")
        else:
            self.send_msg(500, "Invalid argument")
            


class FTPThread(threading.Thread):
    '''FTPConnection Thread Wrapper'''
    def __init__(self, fd, remote_ip):
        threading.Thread.__init__(self)
        self.ftp = FTPConnection(fd, remote_ip)

    def run(self):
        self.ftp.start()
        print "Thread done"

class FTPThreadServer:
    '''FTP Server which is using thread'''
    def serve_forever(self):
        listen_fd = socket.socket()
        listen_fd.bind((host, port))
        listen_fd.listen(512)
        while True:
            print 'new server'
            client_fd, client_addr = listen_fd.accept()
            handler = FTPThread(client_fd, client_addr)
            handler.start()

class FTPForkServer:
    '''FTP Fork Server, use process per user'''
    def child_main(self, client_fd, client_addr, write_end):
        '''never return'''
        for fd in self.read_fds:
            os.close(fd)
        self.read_fds = []

        uid = get_uid(runas_user)
        os.setgid(uid)
        os.setuid(uid)
        try:
            handler = FTPConnection(client_fd, client_addr)
            handler.start()
        except: pass

        os.write(write_end, str(write_end))
        sys.exit()

    def serve_forever(self):
        listen_fd = socket.socket()
        listen_fd.bind((host, port))
        listen_fd.listen(512)
        self.read_fds = [listen_fd]
        while True:
            rlist, wlist, xlist = select.select(self.read_fds, [], [])

            if listen_fd in rlist:
                print 'new server' 
                print self.read_fds
                client_fd, client_addr = listen_fd.accept()
                if len(self.read_fds) > limit_connection_number:
                    print 'read_fds length = ', len(self.read_fds)
                    client_fd.close()
                    continue
                try:
                    read_end, write_end = os.pipe()
                    self.read_fds.append(read_end)
                    fork_result = os.fork()
                    if fork_result == 0: # child process
                        listen_fd.close()
                        self.read_fds.remove(listen_fd)
                        self.child_main(client_fd, client_addr, write_end) # never return
                    else:
                        os.close(write_end)
                except Exception, e:
                    print e
                    print 'Fork failed'
                    
            for read_fd in rlist:
                if read_fd == listen_fd: continue
                data = os.read(read_fd, 32)
                self.read_fds.remove(read_fd)
                os.close(read_fd)

                        

def get_uid(username = 'www-data'):
    '''get uid by username, I don't know whether there's a
    function can get it, so I wrote this function.'''
    pwd = open('/etc/passwd', 'r')
    pat = re.compile(username + ':.*?:(.*?):.*?')
    for line in pwd.readlines():
        try:
            uid = pat.search(line).group(1)
        except: continue
        return int(uid)


def main():
    #server = FTPThreadServer()
    server = FTPForkServer()
    server.serve_forever()

if __name__ == '__main__':
    import sys
    #sys.stdout = open('/var/log/ftp.py.log', 'w')
    signal.signal(signal.SIGCHLD, signal.SIG_IGN)

    main()


本文链接: http://everet.org/2012/03/ftp-server.html


ubuntu14.04 server ftp 服务安装配置详解

ubuntu14.04 server ftp 服务安装配置详解cheungmine2016-01-270 安装好vsftpd服务安装$ sudo apt-get install vsftpd关闭,启动...
  • cheungmine
  • cheungmine
  • 2016年01月27日 19:04
  • 7190

使用FileZilla Server轻松搭建个人FTP服务器

FileZilla server配置 双击运行“FileZilla Server Interface.exe”,会提示你连接到服务器,什么都不用设置,直接点确定进入运行界面,会提示你已成功连...
  • hbuxiaofei
  • hbuxiaofei
  • 2015年05月25日 19:45
  • 6706

linux下ftp的server/client的部署及使用

1.首先在一台linux机器上安装ftp服务端软件,例如proftpd,vsftpd等等 2.ftp客户端使用 (1)ftp remoteserver      通过ip或者主机名来来通过ftp协...
  • wangjianno2
  • wangjianno2
  • 2015年07月12日 02:43
  • 2013

关于FTP server的开发进展

关于FTP server与microPYTHON的进展
  • unsv29
  • unsv29
  • 2017年12月15日 18:29
  • 53

Windows7下利用FileZilla Server搭建ftp(小白版)

来自:http://ce.sysu.edu.cn/hope/Item.aspx?id=77421 作者:曾珊珊 来源:本站原创 发布时间:2012年05月12日 点击数: 232 ...
  • loveaborn
  • loveaborn
  • 2012年07月10日 22:05
  • 12809

FileZilla_Server快速搭建FTP服务器

本拆分工具的主要用途:将一个excel文件中的若干相同格式的工作表中的相同数据保存到一个新的excel文件中,实现一个文件拆分到多个文件的效果。        注意:如果需要这个宏工具及这个工...
  • chen289251809
  • chen289251809
  • 2017年01月09日 20:23
  • 7167

Windows Server 2008 阿里云服务器(ECS)配置FTP传输

作为一只服务器小白,一个月前刚刚租了一个windows系统的阿里云服务器,折腾半天网址终于备案好了,高高兴兴地想要把自己亲手搭建的网站发布到服务器上,但是发现本地电脑怎么都无法连接FTP服务器进行文件...
  • coffeesweet
  • coffeesweet
  • 2015年12月21日 19:35
  • 7805

Windows7下利用FileZilla Server搭建ftp(小白版)

写在前面 本文仅适合吾等技术小白,大神们请果断忽略之。 1、需求分析 为了提高效率,更好地完成期末网站作业,搭建ftp,实现组内资源共享。 2、所需软件 FileZilla Server 网...
  • Life_is_so
  • Life_is_so
  • 2013年05月21日 09:38
  • 768

关于使用github上开源类库 ios-ftp-server 的心得

笔者最近在实现app上一个通过ftp协议与电脑端共享文件的功能。  这里附上使用的第三方开源代码的连接,以节省读者的时间。 https://github.com/loleh43/ios-ftp-ser...
  • weng_Sky
  • weng_Sky
  • 2015年08月03日 11:54
  • 1484

linux下ftp的server/client的部署及使用

1.首先在一台linux机器上安装ftp服务端软件,例如proftpd,vsftpd等等 2.ftp客户端使用 (1)ftp remoteserver      通过ip或者主机名来来通过ftp协...
  • wangjianno2
  • wangjianno2
  • 2015年07月12日 02:43
  • 2013
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:我的FTP Server——ftp.py
举报原因:
原因补充:

(最多只允许输入30个字)