Xiaoxia[PG]

再次400多行Python代码实现了一个FTP服务器

我的代码48 条评论

Python版本
实现了比之前的xxftp更多更完善的功能 🙂
1、继续支持多用户
2、继续支持虚拟目录
3、增加支持用户根目录以及映射虚拟目录的权限设置
4、增加支持限制用户根目录或者虚拟目录的空间大小

xxftp的特点
1、开源、跨平台
2、简单、易用
3、不需要数据库
4、可扩展性超强
5、你可以免费使用xxftp假设自己的私人FTP服务器

测试地址
ftp://xiaoxia.org
匿名帐号可以使用!
匿名根目录只读,映射了一个虚拟目录,可以上传文件但不允许更改!


使用方法
跟之前用C语言写的xxftp使用方法一样:

1. Create a root directory to hold the user directories.
   Configure it in config.xml.
2. Create user directories under the root directory.
   If you want to specify a password, create a directory named ".xxftp",
   under which create a text file named "password" containing the MD5
   code of the password.
3. If you want to specify the welcome and goodbye message, write it in
   xxftp.welcome and xxftp.goodbye under the root directory.
4. Configure config.xml.

The structure of your FTP server root may be like:

-/root
	-xxftp.welcome
	-xxftp.goodbye

	-user1
		-.xxftp
			-password
		-...
	-user2
		-.xxftp
			-password
		-...
	-anonymous

源代码

import socket, threading, os, sys, time
import hashlib, platform, stat

listen_ip = "localhost"
listen_port = 21
conn_list = []
root_dir = "./home"
max_connections = 500
conn_timeout = 120

class FtpConnection(threading.Thread):
    def __init__(self, fd):
        threading.Thread.__init__(self)
        self.fd = fd
        self.running = True
        self.setDaemon(True)
        self.alive_time = time.time()
        self.option_utf8 = False
        self.identified = False
        self.option_pasv = True
        self.username = ""
    def process(self, cmd, arg):
        cmd = cmd.upper();
        if self.option_utf8:
            arg = unicode(arg, "utf8").encode(sys.getfilesystemencoding())
        print "<<", cmd, arg, self.fd
        # Ftp Command
        if cmd == "BYE" or cmd == "QUIT":
            if os.path.exists(root_dir + "/xxftp.goodbye"):
                self.message(221, open(root_dir + "/xxftp.goodbye").read())
            else:
                self.message(221, "Bye!")
            self.running = False
            return
        elif cmd == "USER":
            # Set Anonymous User
            if arg == "": arg = "anonymous"
            for c in arg:
                if not c.isalpha() and not c.isdigit() and c!="_":
                    self.message(530, "Incorrect username.")
                    return
            self.username = arg
            self.home_dir = root_dir + "/" + self.username
            self.curr_dir = "/"
            self.curr_dir, self.full_path, permission, self.vdir_list, \
                limit_size, is_virtual = self.parse_path("/")
            if not os.path.isdir(self.home_dir):
                self.message(530, "User " + self.username + " not exists.")
                return
            self.pass_path = self.home_dir + "/.xxftp/password"
            if os.path.isfile(self.pass_path):
                self.message(331, "Password required for " + self.username)
            else:
                self.message(230, "Identified!")
                self.identified = True
            return
        elif cmd == "PASS":
            if open(self.pass_path).read() == hashlib.md5(arg).hexdigest():
                self.message(230, "Identified!")
                self.identified = True
            else:
                self.message(530, "Not identified!")
                self.identified = False
            return
        elif not self.identified:
            self.message(530, "Please login with USER and PASS.")
            return

        self.alive_time = time.time()
        finish = True
        if cmd == "NOOP":
            self.message(200, "ok")
        elif cmd == "TYPE":
            self.message(200, "ok")
        elif cmd == "SYST":
            self.message(200, "UNIX")
        elif cmd == "EPSV" or cmd == "PASV":
            self.option_pasv = True
            try:
                self.data_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.data_fd.bind((listen_ip, 0))
                self.data_fd.listen(1)
                ip, port = self.data_fd.getsockname()
                if cmd == "EPSV":
                    self.message(229, "Entering Extended Passive Mode (|||" + str(port) + "|)")
                else:
                    ipnum = socket.inet_aton(ip)
                    self.message(227, "Entering Passive Mode (%s,%u,%u)." %
                        (",".join(ip.split(".")), (port>>8&0xff), (port&0xff)))
            except:
                self.message(500, "failed to create data socket.")
        elif cmd == "EPRT":
            self.message(500, "implement EPRT later...")
        elif cmd == "PORT":
            self.option_pasv = False
            self.data_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s = arg.split(",")
            self.data_ip = ".".join(s[:4])
            self.data_port = int(s[4])*256 + int(s[5])
            self.message(200, "ok")
        elif cmd == "PWD" or cmd == "XPWD":
            if self.curr_dir == "": self.curr_dir = "/"
            self.message(257, '"' + self.curr_dir + '"')
        elif cmd == "LIST" or cmd == "NLST":
            if arg != "" and arg[0] == "-": arg = "" # omit parameters
            remote, local, perm, vdir_list, limit_size, is_virtual = self.parse_path(arg)
            if not os.path.exists(local):
                self.message(550, "failed.")
                return
            if not self.establish(): return
            self.message(150, "ok")
            for v in vdir_list:
                f = v[0]
                if self.option_utf8:
                    f = unicode(f, sys.getfilesystemencoding()).encode("utf8")
                if cmd == "NLST":
                    info = f + "\r\n"
                else:
                    info = "d%s%s------- %04u %8s %8s %8lu %s %s\r\n" % (
                        "r" if "read" in perm else "-",
                        "w" if "write" in perm else "-",
                        1, "0", "0", 0,
                        time.strftime("%b %d  %Y", time.localtime(time.time())),
                        f)
                self.data_fd.send(info)
            for f in os.listdir(local):
                if f[0] == ".": continue
                path = local + "/" + f
                if self.option_utf8:
                    f = unicode(f, sys.getfilesystemencoding()).encode("utf8")
                if cmd == "NLST":
                    info = f + "\r\n"
                else:
                    st = os.stat(path)
                    info = "%s%s%s------- %04u %8s %8s %8lu %s %s\r\n" % (
                        "-" if os.path.isfile(path) else "d",
                        "r" if "read" in perm else "-",
                        "w" if "write" in perm else "-",
                        1, "0", "0", st[stat.ST_SIZE],
                        time.strftime("%b %d  %Y", time.localtime(st[stat.ST_MTIME])),
                        f)
                self.data_fd.send(info)
            self.message(226, "Limit size: " + str(limit_size))
            self.data_fd.close()
            self.data_fd = 0
        elif cmd == "REST":
            self.file_pos = int(arg)
            self.message(250, "ok")
        elif cmd == "FEAT":
            features = "211-Features:\r\nSITES\r\nEPRT\r\nEPSV\r\nMDTM\r\nPASV\r\n"\
                "REST STREAM\r\nSIZE\r\nUTF8\r\n211 End\r\n"
            self.fd.send(features)
        elif cmd == "OPTS":
            arg = arg.upper()
            if arg == "UTF8 ON":
                self.option_utf8 = True
                self.message(200, "ok")
            elif arg == "UTF8 OFF":
                self.option_utf8 = False
                self.message(200, "ok")
            else:
                self.message(500, "unrecognized option")
        elif cmd == "CDUP":
            finish = False
            arg = ".."
        else:
            finish = False
        if finish: return
        # Parse argument ( It's a path )
        if arg == "":
            self.message(500, "where's my argument?")
            return
        remote, local, permission, vdir_list, limit_size, is_virtual = \
            self.parse_path(arg)
        # can not do anything to virtual directory
        if is_virtual: permission = "none"
        can_read, can_write, can_modify = "read" in permission, "write" in permission, "modify" in permission
        newpath = local
        try:
            if cmd == "CWD":
                if(os.path.isdir(newpath)):
                    self.curr_dir = remote
                    self.full_path = newpath
                    self.message(250, '"' + remote + '"')
                else:
                    self.message(550, "failed")
            elif cmd == "MDTM":
                if os.path.exists(newpath):
                    self.message(213, time.strftime("%Y%m%d%I%M%S", time.localtime(
                        os.path.getmtime(newpath))))
                else:
                    self.message(550, "failed")
            elif cmd == "SIZE":
                self.message(231, os.path.getsize(newpath))
            elif cmd == "XMKD" or cmd == "MKD":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                os.mkdir(newpath)
                self.message(250, "ok")
            elif cmd == "RNFR":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                self.temp_path = newpath
                self.message(350, "rename from " + remote)
            elif cmd == "RNTO":
                os.rename(self.temp_path, newpath)
                self.message(250, "RNTO to " + remote)
            elif cmd == "XRMD" or cmd == "RMD":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                os.rmdir(newpath)
                self.message(250, "ok")
            elif cmd == "DELE":
                if not can_modify:
                    self.message(550, "permission denied.")
                    return
                os.remove(newpath)
                self.message(250, "ok")
            elif cmd == "RETR":
                if not os.path.isfile(newpath):
                    self.message(550, "failed")
                    return
                if not can_read:
                    self.message(550, "permission denied.")
                    return
                if not self.establish(): return
                self.message(150, "ok")
                f = open(newpath, "rb")
                while self.running:
                    self.alive_time = time.time()
                    data = f.read(8192)
                    if len(data) == 0: break
                    self.data_fd.send(data)
                f.close()
                self.data_fd.close()
                self.data_fd = 0
                self.message(226, "ok")
            elif cmd == "STOR" or cmd == "APPE":
                if not can_write:
                    self.message(550, "permission denied.")
                    return
                if os.path.exists(newpath) and not can_modify:
                    self.message(550, "permission denied.")
                    return
                # Check space size remained!
                used_size = 0
                if limit_size > 0:
                    used_size = self.get_dir_size(os.path.dirname(newpath))
                if not self.establish(): return
                self.message(150, "ok")
                f = open(newpath, ("ab" if cmd == "APPE" else "wb") )
                while self.running:
                    self.alive_time = time.time()
                    data = self.data_fd.recv(8192)
                    if len(data) == 0: break
                    if limit_size > 0:
                        used_size = used_size + len(data)
                        if used_size > limit_size: break
                    f.write(data)
                f.close()
                self.data_fd.close()
                self.data_fd = 0
                if limit_size > 0 and used_size > limit_size:
                    self.message(550, "Exceeding user space limit: " + str(limit_size) + " bytes")
                else:
                    self.message(226, "ok")
            else:
                self.message(500, cmd + " not implemented")
        except:
            self.message(550, "failed.")
    
    def establish(self):
        if self.data_fd == 0:
            self.message(500, "no data connection")
            return False
        if self.option_pasv:
            fd = self.data_fd.accept()[0]
            self.data_fd.close()
            self.data_fd = fd
        else:
            try:
                self.data_fd.connect((self.data_ip, self.data_port))
            except:
                self.message(500, "failed to establish data connection")
                return False
        return True

    def read_virtual(self, path):
        vdir_list = []
        path = path + "/.xxftp/virtual"
        if os.path.isfile(path):
            for v in open(path, "r").readlines():
                items = v.split()
                items[1] = items[1].replace("$root", root_dir)
                vdir_list.append(items)
        return vdir_list

    def get_dir_size(self, folder):
        size = 0
        for path, dirs, files in os.walk(folder):
            for f in files:
                size += os.path.getsize(os.path.join(path, f))
        return size
    
    def read_size(self, path):
        size = 0
        path = path + "/.xxftp/size"
        if os.path.isfile(path):
            size = int(open(path, "r").readline())
        return size

    def read_permission(self, path):
        permission = "read,write,modify"
        path = path + "/.xxftp/permission"
        if os.path.isfile(path):
            permission = open(path, "r").readline()
        return permission
    
    def parse_path(self, path):
        if path == "": path = "."
        if path[0] != "/":
            path = self.curr_dir + "/" + path
        s = os.path.normpath(path).replace("\\", "/").split("/")
        local = self.home_dir
        # reset directory permission
        vdir_list = self.read_virtual(local)
        limit_size = self.read_size(local)
        permission = self.read_permission(local)
        remote = ""
        is_virtual = False
        for name in s:
            name = name.lstrip(".")
            if name == "": continue
            remote = remote + "/" + name
            is_virtual = False
            for v in vdir_list:
                if v[0] == name:
                    permission = v[2]
                    local = v[1]
                    limit_size = self.read_size(local)
                    is_virtual = True
            if not is_virtual: local = local + "/" + name
            vdir_list = self.read_virtual(local)
        return (remote, local, permission, vdir_list, limit_size, is_virtual)

    def run(self):
        ''' Connection Process '''
        try:
            if len(conn_list) > max_connections:
                self.message(500, "too many connections!")
                self.fd.close()
                self.running = False
                return
            # Welcome Message
            if os.path.exists(root_dir + "/xxftp.welcome"):
                self.message(220, open(root_dir + "/xxftp.welcome").read())
            else:
                self.message(220, "xxftp(Python) www.xiaoxia.org")
            # Command Loop
            line = ""
            while self.running:
                data = self.fd.recv(4096)
                if len(data) == 0: break
                line += data
                if line[-2:] != "\r\n": continue
                line = line[:-2]
                space = line.find(" ")
                if space == -1:
                    self.process(line, "")
                else:
                    self.process(line[:space], line[space+1:])
                line = ""
        except:
            print "error", sys.exc_info()
        self.running = False
        self.fd.close()
        print "connection end", self.fd, "user", self.username

    def message(self, code, s):
        ''' Send Ftp Message '''
        s = str(s).replace("\r", "")
        ss = s.split("\n")
        if len(ss) > 1:
            r = (str(code) + "-") + ("\r\n" + str(code) + "-").join(ss[:-1])
            r += "\r\n" + str(code) + " " + ss[-1] + "\r\n"
        else:
            r = str(code) + " " + ss[0] + "\r\n"
        if self.option_utf8:
            r = unicode(r, sys.getfilesystemencoding()).encode("utf8")
        self.fd.send(r)

def server_listen():
    global conn_list
    listen_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_fd.bind((listen_ip, listen_port))
    listen_fd.listen(1024)
    conn_lock = threading.Lock()
    print "ftpd is listening on ", listen_ip + ":" + str(listen_port)

    while True:
        conn_fd, remote_addr = listen_fd.accept()
        print "connection from ", remote_addr, "conn_list", len(conn_list)
        conn = FtpConnection(conn_fd)
        conn.start()

        conn_lock.acquire()
        conn_list.append(conn)
        # check timeout
        try:
            curr_time = time.time()
            for conn in conn_list:
                if int(curr_time - conn.alive_time) > conn_timeout:
                    if conn.running == True:
                        conn.fd.shutdown(socket.SHUT_RDWR)
                    conn.running = False
            conn_list = [conn for conn in conn_list if conn.running]
        except:
            print sys.exc_info()
        conn_lock.release()


def main():
    server_listen()
    
if __name__ == "__main__":
    main()

48 条评论

  1. 小虾神牛~PY都学这么快~

  2. Python特点就是易学易用!!!

    1. Python有句话是“人生苦短”

      1. 嗯,把更多时间留给爱!

  3. 好快啊,我要学python 🙁

  4. 这个版本很占cpu啊,在火狐下打开刷几下, python 立马 99%

    1. 嗯,因为没有使用到数据库和数据结构来保存账户信息。大多数情况下每次发送命令操作都需要读取文件系统上的文件内容,所以在效率上会受到限制。

      1. 原来昨天我下载的是 ftp://xiaoxia.org/upload/ftpd.py 这里的程序,今天复制这里的代码运行之后,cpu正常。

        1. 嗯,你之前下的那个未写完,是有问题的!

          1. 请教下哦~这个代码我看了一段时间了 有部分始终没有明白 是怎么实现的 想请教下~

            1. 哪个部分?

      2. 占用cpu与读取文件系统文件无关,数据库的存储本质也是datafile,总归是基于文件,性能瓶颈不应在于此。

  5. 怎么没用twisted?? 怎么做到非阻塞和异步的??

    1. 嗯,用的是多线程阻塞socket。

      1. 期待你的python版Myqq..

        1. 嗯,有时间再说。

          1. python版Myqq?期待
            正在考虑python的webqq

            1. 我也期待python版本的webqq,支持!!!

  6. 牛啊 ftp服务端都搞了好几个

  7. 在linux下不能用,提示permission denied.错误

  8. 已搞定,自己不太熟linux...

    1. 嗯!Windows、Linux都可以使用的,能运行python脚本即可!
      有问题可以在这里提出来!

  9. 用了两天还不错,主要是小啊,不过有点问题希望解决下。
    1 就是不能删除目录
    2 不能双击反点上层目录

    1. 可以删除目录的呀!!!你是用root方式运行程序的吗?
      可以删除文件但是不能删除目录???
      目录名字是英文的吗?

      1. 我在windows下也试了还是不行,能删文件不能删文件夹。
        还有不能在ftp软件里双击返回上层目录

  10. 不错,交个朋友!

  11. 这个有没有人完全看懂了的?

  12. Traceback (most recent call last):
    File "/home/a508/desk/python.py", line 427, in
    main()
    请教啊 为什么我的提示这个呢?
    File "/home/a508/desk/python.py", line 424, in main
    server_listen()
    File "/home/a508/desk/python.py", line 396, in server_listen
    listen_fd.bind((listen_ip, listen_port))
    File "/usr/lib/python2.7/socket.py", line 224, in meth
    return getattr(self._sock,name)(*args)
    error: [Errno 13] Permission denied

    1. 要用root权限监听21端口。你需要使用sudo。

      1. 谢谢啊 明白了 以后有不懂的请教你,希望不吝赐教啊

  13. 还是我,虾哥,我想问下FtpServer发给Client的目录是什么样的格式,什么样的类型

    1. 你可以抓包看看,或者随便找一个客户端工具,都会有显示的。

  14. 高人啊,学习了,能不能给写FTP协议的东西,学习学习啊

    1. RFC文档吧,挺实在的!

  15. 请问这个虚拟目录的怎么设置啊
    我是windows,看代码下好像要在用户目录建.xxftp文件夹,在里面建立virtual文件,windows不能建立.xxftp,我代码改成_xxftp,建立好了。
    可是文件里面怎么写路径呢,如我要共享c:/ 和D:/
    一直没有成功,求解

    1. 我记得好像不能跨分区的,这个简单的应该是共享当前home目录下的吧。当然可以指定home的位置在别的分区上。

  16. 先看了监听的函数,这句是什么语法??
    conn_list = [conn for conn in conn_list if conn.running]

    不知道文字表述google不好弄 :<

    1. 这是python的list的固有语法。

      1. 原来叫list推导式:)

  17. virtual文件是什么文件呢

    1. 虚拟目录来的~~

      1. 刚刚查了下,虚拟目录指的是Internet服务发布的宿主目录以外的其他目录。
        那是不是说,在这里的话,虚拟目录就是映射到客户端上文件的目录列表呢?

        1. 嗯,可以映射其他地方的文件夹。

  18. 客户端怎么和服务器进行连接,我们要多一个ftp的服务器和客户端,但是网上这方面资料好少,现在是完全摸不着头脑,求大神指点下,谢谢了

    1. 不就是TCP/IP的socket么?

  19. 如何使用你的这个ftpserver呢 ,怎么建立文件夹 ?还望指点一下,

    1. 的确需要写一个帮助文档,可能是我太懒了。

  20. 其他的一切正常,就是不能断点续传。

发表评论

评论会经过自动审核,链接请勿超过 3 个。