#!/usr/bin/env python#author: Hua Liang [ Stupid ET ]#email: et@everet.org#website: http://EverET.org#importsocket, os, stat, threading, time, struct, getoptimportsys, re, signal, select, logging, logging.handlers
host= '127.0.0.1'port= 21limit_connection_number= 5 #max client number
timeout = 60 * 3 #timeout in second
default_home_dir = os.path.normpath(os.path.abspath(os.curdir)).replace('\\', '/')
logfile= '/var/log/ftp.py.log' if os.name == 'posix' else default_home_dir + 'ftp.py.log'runas_user= 'www-data'global_options= {'run_mode':'fork'}#current working directory
account_info ={'anonymous':{'pass':'', 'home_dir':default_home_dir, 'runas_user':runas_user},
}defrunas(username):if os.name != 'posix': returnuid=get_uid(username)
os.setgid(uid)
os.setuid(uid)classFTPConnection:'''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=default_home_dir
self.curr_dir= '/'self.running=True
self.handler=dict(
[(method[7:], getattr(self, method)) \for method indir(self) \if method.startswith("handle_") andcallable(getattr(self, method))])defstart(self):try:
self.say_welcome()whileself.running:
success, command, arg=self.recv()
command=command.upper()if self.options['utf8']:
arg= unicode(arg, 'utf8').encode(sys.getfilesystemencoding())
logger.info('[' + command + ']' +arg)if notsuccess:
self.send_msg(500, "Failed")continue
if notself.handler.has_key(command):
self.send_msg(500, "Command Not Found")continue
try:
self.handler[command](arg)exceptOSError, e:
logger.error(e)
logger.error("in start")
self.send_msg(500, 'Permission denied')
self.say_bye()exceptException, e:
self.running=False
logger.error(e)
logger.error("in start")finally:
self.fd.close()
logger.info("FTP connnection done.")returnTruedefsend_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)defrecv(self):'''returns 3 tuples, success, command, arg'''
try:
success, buf, command, arg= True, '', '', ''
whileTrue:
data= self.fd.recv(4096)if not data or data <=0:
self.running=False
success=Falsebreakbuf+=dataif buf[-2:] == '\r\n': breaksplit= buf.find(' ')
command, arg= (buf[:split], buf[split + 1:].strip()) if split != -1 else (buf.strip(), '')exceptException, e:
logger.error(e)
logger.error("in recv")
self.running=False
success=Falsereturnsuccess, command, argdefsay_welcome(self):
self.send_msg(220, "Welcome to EverET.org FTP")defsay_bye(self):
self.handle_BYE('')defdata_connect(self):'''establish data connection'''
if self.data_fd ==0:
self.send_msg(500, "no data connection")returnFalseelif self.options['pasv']:
fd, addr=self.data_fd.accept()
self.data_fd.close()
self.data_fd=fdelse:try:
self.data_fd.connect((self.data_host, self.data_port))except:
self.send_msg(500, "failed to connect")returnFalsereturnTruedefclose_data_fd(self):
self.data_fd.close()
self.data_fd=0defparse_path(self, path):if path == '': path = '.'
if path[0] != '/': path = self.curr_dir + '/' +path
logger.info('parse_path' +path)
split_path= os.path.normpath(path).replace('\\', '/').split('/')
remote= ''local=self.home_dirfor item insplit_path:if item.startswith('..') or item == '': continue #ignore parent directory
remote += '/' +item
local+= '/' +itemif remote == '': remote = '/'logger.info(split_path)
logger.info('remote: %s local: %s' %(remote, local))returnremote, local#Command Handlers
defhandle_USER(self, arg):if arg inaccount_info:
self.username=argif self.username == 'anonymous':
self.send_msg(230, 'OK')else:
self.send_msg(331, "Need password")else:
self.send_msg(500, "Invalid User")
self.running=Falsedefhandle_PASS(self, arg):if arg == account_info[self.username]['pass']:
self.home_dir= account_info[self.username]['home_dir']if account_info[self.username].has_key('runas_user'):
user= account_info[self.username]['runas_user']else:
user= 'www-data'runas(user)ifos.path.isdir(self.home_dir):
self.send_msg(230, "OK")returnself.send_msg(530, "Password is not corrected")
self.running=Falsedefhandle_QUIT(self, arg):
self.handle_BYE(arg)defhandle_BYE(self, arg):
self.running=False
self.send_msg(200, "OK")defhandle_CDUP(self, arg):
self.handle_CWD('..')defhandle_XPWD(self, arg):
self.handle_PWD(arg)defhandle_PWD(self, arg):
remote, local=self.parse_path(self.curr_dir)
self.send_msg(257, '"' + remote + '"')defhandle_CWD(self, arg):
remote, local=self.parse_path(arg)try:
os.listdir(local)
self.curr_dir=remote
self.send_msg(250, "OK")exceptException, e:
logger.error(e)
logger.error("in cwd")
self.send_msg(500, "Change directory failed!")defhandle_SIZE(self, arg):
remote, local=self.parse_path(self.curr_dir)
self.send_msg(231, str(os.path.getsize(local)))defhandle_SYST(self, arg):
self.send_msg(215, "UNIX")defhandle_STOR(self, arg):
remote, local=self.parse_path(arg)if not self.data_connect(): returnself.send_msg(125, "OK")
f= open(local, 'wb')whileTrue:
data= self.data_fd.recv(8192)if len(data) == 0: breakf.write(data)
f.close()
self.close_data_fd()
self.send_msg(226, "OK")defhandle_RETR(self, arg):
remote, local=self.parse_path(arg)if not self.data_connect(): returnself.send_msg(125, "OK")
f= open(local, 'rb')whileTrue:
data= f.read(8192)if len(data) == 0: breakself.data_fd.send(data)
f.close()
self.close_data_fd()
self.send_msg(226, "OK")defhandle_TYPE(self, arg):
self.send_msg(220, "OK")defhandle_RNFR(self, arg):
remote, local=self.parse_path(arg)
self.rename_tmp_path=local
self.send_msg(350, 'rename from' +remote)defhandle_RNTO(self, arg):
remote, local=self.parse_path(arg)
os.rename(self.rename_tmp_path, local)
self.send_msg(250, 'rename to' +remote)defhandle_NLST(self, arg):if not self.data_connect(): returnself.send_msg(125, "OK")
remote, local=self.parse_path(self.curr_dir)for filename inos.listdir(local):
self.data_fd.send(filename+ '\r\n')
self.send_msg(226, "Limit")
self.close_data_fd()defhandle_XMKD(self, arg):
self.handle_MKD(arg)defhandle_MKD(self, arg):
remote, local=self.parse_path(arg)ifos.path.exists(local):
self.send_msg(500, "Folder is already existed")returnos.mkdir(local)
self.send_msg(257, "OK")defhandle_XRMD(self, arg):
self.handle_RMD(arg)defhandle_RMD(self, arg):
remote, local=self.parse_path(arg)if notos.path.exists(local):
self.send_msg(500, "Folder is not existed")returnos.rmdir(local)
self.send_msg(250, "OK")defhandle_LIST(self, arg):if not self.data_connect(): returnself.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 inos.listdir(local):
path= local + '/' +filenameif 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()defhandle_PASV(self, arg):
self.options['pasv'] =Truetry:
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)))exceptException, e:
logger.error(e)
logger.error("in pasv")
self.send_msg(500, 'passive mode failed')defhandle_PORT(self, arg):try:ifself.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")defhandle_DELE(self, arg):
remote, local=self.parse_path(arg)if notos.path.exists(local):
self.send_msg(450, "File not exist")returnos.remove(local)
self.send_msg(250, 'File deleted')defhandle_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")classFTPThread(threading.Thread):'''FTPConnection Thread Wrapper'''
def __init__(self, fd, remote_ip):
threading.Thread.__init__(self)
self.ftp=FTPConnection(fd, remote_ip)defrun(self):
self.ftp.start()
logger.info("Thread done")classFTPThreadServer:'''FTP Server which is using thread'''
defserve_forever(self):
listen_fd=socket.socket()
listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
listen_fd.bind((host, port))
listen_fd.listen(512)whileTrue:
logger.info('new server')
client_fd, client_addr=listen_fd.accept()
handler=FTPThread(client_fd, client_addr)
handler.start()classFTPForkServer:'''FTP Fork Server, use process per user'''
defchild_main(self, client_fd, client_addr, write_end):'''never return'''
for fd inself.read_fds:
os.close(fd)
self.read_fds=[]try:
handler=FTPConnection(client_fd, client_addr)
handler.start()exceptException, e:
logger.error(e)
logger.error("in child_main")
os.write(write_end, str(write_end))
sys.exit()defserve_forever(self):
listen_fd=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
listen_fd.bind((host, port))
listen_fd.listen(512)
self.read_fds=[listen_fd]whileTrue:
rlist, wlist, xlist=select.select(self.read_fds, [], [])if listen_fd inrlist:
client_fd, client_addr=listen_fd.accept()if len(self.read_fds) >limit_connection_number:
logger.error('reject client:' +str(client_addr))
client_fd.close()continue
try:
logger.info('new client:' +str(client_addr))
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)exceptException, e:
logger.error(e)
logger.error('Fork failed')for read_fd inrlist:if read_fd == listen_fd: continuedata= 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 inpwd.readlines():try:
uid= pat.search(line).group(1)except: continue
returnint(uid)def get_logger(handler =logging.StreamHandler()):
logger=logging.getLogger()
formatter= logging.Formatter('%(asctime)s %(levelname)s %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.NOTSET)returnloggerdef daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):'''becomes a daemon'''
try:
pid=os.fork()if pid >0: sys.exit(0)exceptOSError, e:
sys.stderr.write("fork #1 failed\n")
sys.exit(1)
os.umask(0)
os.setsid()try:
pid=os.fork()if pid >0: sys.exit(0)exceptOSError, e:
sys.stderr.write("fork #2 failed\n")
sys.exit(1)for f insys.stdout, sys.stderr: f.flush()
si= file(stdin, 'r')
so= file(stdout, 'a+')
se= file(stderr, 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())#0
os.dup2(so.fileno(), sys.stdout.fileno()) #1
os.dup2(se.fileno(), sys.stderr.fileno()) #2
defserve_forever():globalglobal_optionsprintglobal_optionsif global_options['run_mode'] == 'fork':
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
server=FTPForkServer()else:
server=FTPThreadServer()
server.serve_forever()defusage():print '''usage: %s [-d] [-h] [-p port] [-o] [-t]
-d become a daemon
-h help
-p listen port
-o output log to stdout, by default, it outputs to a log file.
-t thread mode, fork model by default
Waring:
The Thread Mode is not complete.
Author:
Hua Liang [ Stupid ET ]
http://EverET.org''' %os.path.basename(sys.argv[0])defparam_handler(opts):globalport, logger, global_options
be_daemon=False
logger=get_logger(logging.FileHandler(logfile))for o, a inopts:if o == '-h':
usage()
sys.exit(0)elif o == '-d':if not os.name == 'posix':print 'Only support the os with posix specifications.'sys.exit(-1)
be_daemon=Trueelif o == '-o':
logger=get_logger()elif o == '-p':try: port =int(a)exceptException, e:
usage()
sys.exit(0)elif o == '-t':
global_options['run_mode'] = 'thread'
if os.name != 'posix' and global_options['run_mode'] == 'fork':print "You can NOT run fork mode in a non posix os,\
please use -t options to run in thread mode"sys.exit(-1)ifbe_daemon: daemonize()if __name__ == '__main__':try:
opts, args= getopt.getopt(sys.argv[1:], 'hdp:ot')exceptgetopt.GetoptError:
usage()
sys.exit(2)
param_handler(opts)
socket.setdefaulttimeout(timeout)'''You can write your account_info in ftp.py.config'''
try: execfile('ftp.py.config')exceptException, e: logger.error(e)
serve_forever()