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()
小虾神牛~PY都学这么快~
Python特点就是易学易用!!!
Python有句话是“人生苦短”
嗯,把更多时间留给爱!
好快啊,我要学python 🙁
这个版本很占cpu啊,在火狐下打开刷几下, python 立马 99%
嗯,因为没有使用到数据库和数据结构来保存账户信息。大多数情况下每次发送命令操作都需要读取文件系统上的文件内容,所以在效率上会受到限制。
原来昨天我下载的是 ftp://xiaoxia.org/upload/ftpd.py 这里的程序,今天复制这里的代码运行之后,cpu正常。
嗯,你之前下的那个未写完,是有问题的!
请教下哦~这个代码我看了一段时间了 有部分始终没有明白 是怎么实现的 想请教下~
哪个部分?
占用cpu与读取文件系统文件无关,数据库的存储本质也是datafile,总归是基于文件,性能瓶颈不应在于此。
怎么没用twisted?? 怎么做到非阻塞和异步的??
嗯,用的是多线程阻塞socket。
期待你的python版Myqq..
嗯,有时间再说。
python版Myqq?期待
正在考虑python的webqq
我也期待python版本的webqq,支持!!!
牛啊 ftp服务端都搞了好几个
在linux下不能用,提示permission denied.错误
已搞定,自己不太熟linux…
嗯!Windows、Linux都可以使用的,能运行python脚本即可!
有问题可以在这里提出来!
用了两天还不错,主要是小啊,不过有点问题希望解决下。
1 就是不能删除目录
2 不能双击反点上层目录
可以删除目录的呀!!!你是用root方式运行程序的吗?
可以删除文件但是不能删除目录???
目录名字是英文的吗?
我在windows下也试了还是不行,能删文件不能删文件夹。
还有不能在ftp软件里双击返回上层目录
不错,交个朋友!
这个有没有人完全看懂了的?
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
要用root权限监听21端口。你需要使用sudo。
谢谢啊 明白了 以后有不懂的请教你,希望不吝赐教啊
还是我,虾哥,我想问下FtpServer发给Client的目录是什么样的格式,什么样的类型
你可以抓包看看,或者随便找一个客户端工具,都会有显示的。
高人啊,学习了,能不能给写FTP协议的东西,学习学习啊
RFC文档吧,挺实在的!
请问这个虚拟目录的怎么设置啊
我是windows,看代码下好像要在用户目录建.xxftp文件夹,在里面建立virtual文件,windows不能建立.xxftp,我代码改成_xxftp,建立好了。
可是文件里面怎么写路径呢,如我要共享c:/ 和D:/
一直没有成功,求解
我记得好像不能跨分区的,这个简单的应该是共享当前home目录下的吧。当然可以指定home的位置在别的分区上。
先看了监听的函数,这句是什么语法??
conn_list = [conn for conn in conn_list if conn.running]
不知道文字表述google不好弄 :<
这是python的list的固有语法。
原来叫list推导式:)
virtual文件是什么文件呢
虚拟目录来的~~
刚刚查了下,虚拟目录指的是Internet服务发布的宿主目录以外的其他目录。
那是不是说,在这里的话,虚拟目录就是映射到客户端上文件的目录列表呢?
嗯,可以映射其他地方的文件夹。
客户端怎么和服务器进行连接,我们要多一个ftp的服务器和客户端,但是网上这方面资料好少,现在是完全摸不着头脑,求大神指点下,谢谢了
不就是TCP/IP的socket么?
如何使用你的这个ftpserver呢 ,怎么建立文件夹 ?还望指点一下,
的确需要写一个帮助文档,可能是我太懒了。
其他的一切正常,就是不能断点续传。