#!/usr/bin/python
import SocketServer
import sys
import socket
import os
import threading
import getopt

try:
	import dbus
	import avahi
	has_avahi = True
except:
	has_avahi = False
	

_socket = socket
_pasv_ports = {}
_portmap_lock = threading.Lock()
def acquirePort():
	_portmap_lock.acquire()
	usePort = 0
	for port, active in _pasv_ports.items():
		if active == False:
			usePort = port
			break
	if usePort in _pasv_ports:
		_pasv_ports[usePort] = True
	_portmap_lock.release()
	return usePort

class FTPHandler(SocketServer.StreamRequestHandler):
	def build_data(self):
		if self.connect_to == "PASV":
			srv = socket.socket()
			srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
			srv.bind(('', self.pasv_port))
			srv.listen(1)
			data = srv.accept()[0]
			srv.shutdown(socket.SHUT_RDWR)
			srv.close()
			del srv
			return data
		else:
			data = socket.socket()
			data.connect(self.connect_to)
			return data
	data_socket = False
	def finish(self):
		if self.pasv_port != 0:
			_pasv_ports[self.pasv_port] = False
	def handle(self):
		print "Connection from %s" % self.connection.getpeername()[0]
		self.wfile.write("200 Hello out there\r\n")
		self.pasv_port = acquirePort()
		debug("Using port %d for passive ftp " % self.pasv_port)
		self.path = "/"
		self.rest = 0
		self.type = "I"
		self.renameFrom = None
		self.login = False
		while True:
			try:
				line = self.rfile.readline().strip().split()
			except:
				# Connection lost
				break
			if debug_mode:
				print "Received command: ", line
				try:
					arg = " ".join(line[1:]).replace('"', '')
					pwd = os.getcwd()
					if arg[0] == "/":
						newpath = os.path.abspath(pwd + arg)
					else:
						newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
					print " Interpreted as file ", newpath
				except:
					pass
			if not line:
				break
			# Allow pre login
			if line[0] == "SYST":
				self.wfile.write("215 UNIX Type: L8\r\n")
				continue
			elif line[0] == "FEAT":
				self.wfile.write("211-Features:\r\nUTF8\r\nPASV\r\n211 End\r\n")
				continue
			elif line[0] == "NOOP":
				self.wfile.write("200 Pong\r\n")
				continue
			# Login
			if require_login and self.login != 2:
				if line[0] == "USER" and line[1] == require_login[0] and self.login == False:
					self.login = 1
					self.wfile.write("331 Ok\r\n")
					continue
				if line[0] == "USER":
					self.wfile.write("331 Ok\r\n")
					continue
				elif line[0] == "PASS" and len(line) > 1 and line[1] == require_login[1] and self.login == 1:
					self.login = 2
					self.wfile.write("200 Ok\r\n")
					continue
				self.wfile.write("530 Bah. Wrong. Authenticate yourself!\r\n")
				continue
			# Post login
			if line[0] == "USER" or line[0] == "PASS":
				self.wfile.write("230 Ill accept whatever you say\r\n")
			elif line[0] == "PORT":
				data = line[1].split(',')
				if len(data) != 6:
					self.wfile.write("500 Oops\r\n")
				else:
					ip = ".".join(data[0:4])
					port = (int(data[4]) << 8) + int(data[5])
					self.connect_to = (ip, port)
					self.wfile.write("200 Ok Ill connect to %s:%d\r\n" % (ip, port))
			elif line[0] == "PASV":
				if self.pasv_port == 0:
					self.wfile.write("425 Out of resources, sorry\r\n")
				else:
					if my_ip:
						the_ip = my_ip
					else:
						the_ip = self.connection.getsockname()[0]
					self.wfile.write("227 Entering passive mode (%s,%d,%d)\r\n" % (",".join(the_ip.split(".")),
						(self.pasv_port & 0xFF00) >> 8, (self.pasv_port & 0x00FF)))
					self.connect_to = "PASV"
					try:
						self.data_socket = self.build_data()
					except _socket.error:
						debug("Failed to build data connection")
						self.wfile.write("425 Failed to build data connection\r\n")
						continue
			elif line[0] == "QUIT":
				self.wfile.write("200 Bye\r\n")
				self.wfile.close()
				break
			elif line[0] == "RNFR":
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
				elif not os.access(newpath, os.F_OK):
					self.wfile.write("400 Not found\r\n")
				else:
					self.renameFrom = newpath
					self.wfile.write("350 Ok. To which file?\r\n")
			elif line[0] == "RNTO":
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				if not self.renameFrom:
					self.wfile.write("500 Specify RNFR first\r\n")
					continue
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
				else:
					debug("Renamed " + self.renameFrom + " to " + newpath)
					os.rename(self.renameFrom, newpath)
					self.renameFrom = None
					self.wfile.write("250 Ok\r\n")
			elif line[0] == "DELE":
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
				else:
					os.unlink(newpath)
					self.wfile.write("250 Ok\r\n")
					debug("Removed file " + newpath)
			elif line[0] == "SITE":
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				if line[1] != "CHMOD":
					self.wfile.write("500 I dont know anything but chmod\r\n")
					continue
				mode = line[2]
				arg = " ".join(line[3:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
				elif not os.access(newpath, os.F_OK):
					self.wfile.write("400 Not found\r\n")
				else:
					os.chmod(newpath, int(mode, 8))
					self.wfile.write("200 Ok\r\n")
			elif line[0] == "CWD":
				if line[1] == "/":
					self.path = "/"
					self.wfile.write("200 Ok\r\n")
					continue
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
				elif not os.access(newpath, os.F_OK):
					self.wfile.write("400 Not found\r\n")
				else:
					self.path = newpath[len(pwd):]
					self.wfile.write("200 Ok\r\n")
			elif line[0] == "CDUP":
				pwd = os.getcwd()
				newpath = os.path.abspath(pwd + "/" + self.path + "/..")
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
				else:
					self.path = newpath[len(pwd):]
					self.wfile.write("200 Ok\r\n")
			elif line[0] == "PWD":
				self.wfile.write("257 \"%s\"\r\n" % self.path)
			elif line[0] == "TYPE":
				self.type = line[1]
				self.wfile.write("200 Ok\r\n")
			elif line[0] == "REST":
				self.rest = abs(int(line[1]))
				self.wfile.write("350 Position accepted")
			elif line[0] == "RMD":
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
					continue
				else:
					try:
						os.rmdir(newpath)
						debug("Removed directory %s" % newpath)
						self.wfile.write("200 Ok\r\n")
					except:
						self.wfile.write("400 Failure\r\n")
			elif line[0] == "MKD":
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
					continue
				else:
					try:
						os.mkdir(newpath)
						self.wfile.write("257 Ok\r\n")
						debug("Made dir " + newpath)
					except:
						self.wfile.write("400 Failure\r\n")
			elif line[0] == "STOR" or line[0] == "APPE":
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
					continue
				if not allow_store:
					self.wfile.write("550 Naa. Not here.\r\n")
					continue
				try:
					if self.data_socket:
						socket = self.data_socket
						self.data_socket = False
					else:
						socket = self.build_data()
				except _socket.error:
					self.wfile.write("425 Failed to build data connection\r\n")
					continue
				writeto = open(newpath, "w" if line[0] == "STOR" else "a")
				self.wfile.write("150 Here we go\r\n")
				print "Receiving " + newpath
				
				while True:
					data = socket.recv(1024)
					if self.type == "A":
						if len(data) > 0 and data[-1] == "r": data = data + socket.recv(1)
						data = data.replace("\r\n", "\n")
					if data == "":
						break
					writeto.write(data)
				debug("Done")

				writeto.close()				
				socket.close()
				self.wfile.write("226 Ok.\r\n")
			elif line[0] == "RETR":
				arg = " ".join(line[1:]).replace('"', '')
				pwd = os.getcwd()
				if arg[0] == "/":
					newpath = os.path.abspath(pwd + arg)
				else:
					newpath = os.path.abspath(pwd + "/" + self.path + "/" + arg)
				if newpath[0:len(pwd)] != pwd:
					self.wfile.write("553 You just tried to leave root. I cant let you do that\r\n")
					continue
				elif not os.access(newpath, os.F_OK):
					self.wfile.write("400 Not found\r\n")
				print "Sending " + newpath

				try:
					if self.data_socket:
						socket = self.data_socket
						self.data_socket = False
					else:
						socket = self.build_data()
				except _socket.error:
					debug("Failed to build data connection")
					self.wfile.write("425 Failed to build data connection\r\n")
					continue
				self.wfile.write("150 Here we go\r\n")


				fcont = open(newpath)
				fcont.seek(self.rest)
				self.rest = 0
				while True:
					content = fcont.read(1024)
					if self.type == "A":
						content = content.replace("\n", "\r\n")
					if content == "": break
					try:
						socket.send(content)
					except _socket.error:
						# Connection lost
						break
				fcont.close()
				socket.close()
				self.wfile.write("226 Ok what now?\r\n")
			elif line[0] == "LIST":
				try:
					if self.data_socket:
						socket = self.data_socket
						self.data_socket = False
					else:
						socket = self.build_data()
				except _socket.error:
					self.wfile.write("425 Failed to build data connection\r\n")
					continue
				self.wfile.write("150 Here we go\r\n")
				args = ""
				if len(line) > 1 and line[1] == "-a":
					args = "a"
				os.environ["LC_ALL"] = "en_US.utf8"
				socket.send(os.popen("ls -ln%s --time-style='+%%b %%d %%Y' .%r" %
					(args, self.path)).read().replace("\n", "\r\n"))
				socket.close()
				self.wfile.write("226 Ok what now?\r\n")
			else:
				self.wfile.write("500 I dont know about that as Im dumb (You said: %s)\r\n" % line[0])

					
my_ip = ""
allow_store = False
baseport = 12000
trange = 20
add = 0
debug_mode = False
daemon_mode = False
require_login = False
use_avahi = False

try:
	(options, rest) = getopt.getopt(sys.argv[1:], "sp:r:h:dDl:a")
except:
	print "iftpd - Instant FTPD"
	print "Copyright (c) 2009-2010, Phillip Berndt"
	print
	print "Creates a FTP daemon in ./"
	print
	print "Syntax: iftpd -s -p <port> -r <range> -h <host>"
	print 
	print " -s            Allow users to modify data"
	print " -p            Listen on port <port>          (Default: 12000)"
	print " -r            Use <port> - <port> + <range>"
	print "                for passive mode FTP          (Default: 20)"
	print " -h            Send <host> for passive mode"
	print "                connection host"
	print " -d            Debug mode"
	print " -D            Daemon mode"
	print " -l            Require <user>:<pass> login    (Default: Allow all)"
	print " -a            Announce service to local network via Avahi"
	print
	sys.exit(1)

for option, arg in options:
	if option == "-s":
		print "WARNING: Activating store mode"
		allow_store = True
	if option == "-p":
		baseport = int(arg)
	if option == "-r":
		trange = int(arg)
	if option == "-h":
		my_ip = socket.gethostbyname(arg)
	if option == "-d":
		debug_mode = True
	if option == "-D":
		daemon_mode = True
	if option == "-l":
		require_login = arg.split(":", 2)
	if option == "-a":
		if not has_avahi:
			print "python-dbus or python-avahi not available. Ignoring -a."
		else:
			use_avahi = True

if debug_mode:
	def debug(string):
		print string
else:
	def debug(string):
		pass

while True:
	try:
		server = SocketServer.ThreadingTCPServer(('', 12000 + add), FTPHandler, False)
		server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
		server.server_bind()
		server.server_activate()
		print "Serving on port %d" % (baseport + add)
	except:
		add = add + 1
		if add > 20:
			print "Failed to find a port :/"
			sys.exit(1)
		continue
	break
_pasv_ports = dict(zip(range(baseport + add + 1, baseport + add + trange + 1), [ False ] * trange))
for port in _pasv_ports.keys():
	srv = socket.socket()
	srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	try:
		srv.bind(('', port))
		srv.close()
		del srv
	except _socket.error:
		del _pasv_ports[port]
		del srv
print "Using ports %d to %d for passive mode (%d are useable)" % (baseport + 1 + add, baseport + 1 + add + trange, len(_pasv_ports))
if use_avahi:
	debug("Announcing service via Avahi")
	bus = dbus.SystemBus()
	dbserver = dbus.Interface(bus.get_object(avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER)
	group = dbus.Interface(bus.get_object(avahi.DBUS_NAME, dbserver.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP)
	group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), "iftpd on " + socket.gethostname(), "_ftp._tcp", "", "", dbus.UInt16(baseport + add), "")
	group.Commit()

if daemon_mode:
	debug("Switching to daemon mode")
	if os.fork() != 0:
		os._exit(0)
	os.setsid()
	if os.fork() != 0:
		os._exit(0)
	for i in range(3): os.close(i)
	os.open(os.devnull if hasattr(os, "devnull") else "/dev/null", os.O_RDWR)
	os.dup2(0, 1)
	os.dup2(0, 2)
try:
	server.serve_forever()
except KeyboardInterrupt:
	if use_avahi:
		group.Reset()
	os._exit(0)
finally:
	if use_avahi:
		group.Reset()
	
