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

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()

再次400多行Python代码实现了一个FTP服务器》有48个想法

    1. Xiaoxia 文章作者

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

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

        回复
    1. Xiaoxia 文章作者

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

      回复
  1. lzchun

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

    回复
    1. Xiaoxia 文章作者

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

      回复
      1. lzchun

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

        回复
  2. Ean

    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

    回复
  3. liuxiang

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

    回复
    1. Xiaoxia 文章作者

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

      回复
  4. poke707

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

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

    回复
      1. 晚而未完

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

        回复
  5. 大三学生

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

    回复

回复 lzchun 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据