#
# FtpCube
# Copyright (C) 2001 Michael Gilfix
#
# This file is part of FtpCube.
#
# You should have received a file COPYING containing license terms
# along with this program; if not, write to Michael Gilfix
# (mgilfix@eecs.tufts.edu) for a copy.
#
# This version of FtpCube is open source; you can redistribute it and/or
# modify it under the terms listed in the file COPYING.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#

import gtk
import string # For backwards compatibility with 1.5.x
import socket, threading, time, re, os
import exceptions
import main

CRLF = '\r\n'

DOWNLOAD = 0
UPLOAD = 1

class FtpException (exceptions.Exception):

	def __init__ (self, args=None):
		self.args = args

class FtpSeriousException (exceptions.Exception):

	def __init__ (self, errno, msg):
		self.args = (errno, msg)
		self.errno = errno
		self.errmsg = msg

class FtpControlConnection (threading.Thread):

	def __init__ (self, silence=0):

		threading.Thread.__init__ (self)
		self.setDaemon (1)
		self.die = 0
		self.silence = silence

		# Set initial instance variables
		self.sock = None
		self.file = None
		self.multiline_code = None
		self.incoming = [ ]
		self.outgoing = [ ]
		self.outgoing_event = threading.Event ()
		self.cmdobj = None
		self.exception_handler = None

		# Create the socket
		proto = socket.getprotobyname ('tcp')
		try:
			self.sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM, proto)
		except socket.error, (errno, strerror):
			raise FtpSeriousException (errno, "Error opening control socket: %s" %strerror)

		host = main.app['host']
		port = main.app['port']
		for i in range (main.app['retries']):
			try:
				if not silence: main.app.ftp_local_tell_user ("Connecting to %s on port %d." %(host, port))
				self.sock.connect ((host, port))
				if not silence: main.app.ftp_local_tell_user ("Connected to %s." %host)
				self.file = self.sock.makefile ('rb')
				break
			except socket.error, (errno, strerror):
				# Check if we've finished the retry delay and issue the exception is we're
				# tapped out
				if i == main.app['retries']:
					raise FtpSeriousException ("Error connecting to host %s: %s" %(host, strerror))
				else:
					# Wait a bit
					delay = main.app['delay']
					if not silence: main.app.ftp_local_tell_user ("Waiting %d secs before re-attempting connection..." %delay)
					time.sleep (delay)

	def destroy (self):

		# This might need some revising at some point
		if not self.silence: main.app.ftp_local_tell_user ("Closing connection to %s..." %(main.app['host']))
		self.die = 1
		self.outgoing_event.set ()
		self.file.close ()
		self.sock.close ()

	def reset_queue (self):

		self.outgoing = [ ]
		self.outgoing_event.set ()

	def wait_for_response (self):

		while 1:
			line = self.file.readline ()

			if not line:
				gtk.threads_enter ()
				if not self.silence: main.app.ftp_local_tell_user ("Connection to %s terminated..." %main.app['host'])
				gtk.threads_leave ()
				self.sock.close ()
				return

			# Clean up the line
			if line[-2:] == CRLF: line = line[:-2]
			elif line[-1:] in CRLF: line = line[:-1]

			data = self.process_data (line)
			if data is not None:
				return data

	def process_data (self, line):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_remote_tell_user (line)
			gtk.threads_leave ()

		self.incoming.append (line)

		if line[3:4] == '-' or self.multiline_code is not None:
			if self.multiline_code is None:
				self.multiline_code = line[:3]
				return
			elif line[:3] == self.multiline_code and line[3:4] != '-':
				self.multiline_code = None
				data = self.incoming
				self.incoming = [ ]
				return data
		else:
			data = self.incoming
			self.incoming = [ ]
			return data

	def send (self, msg):
		return self.sock.send (msg + CRLF)

	def send_oob (self, msg):
		return self.sock.send (msg + CRLF, socket.MSG_OOB)

	def run (self):

		while not self.die:
			if self.outgoing == [ ]:
				self.outgoing_event.clear ()
				self.outgoing_event.wait ()

			if not self.outgoing == [ ]:
				self.cmdobj = self.outgoing.pop (0)
				try:
					# Call the execute function directly so
					# it runs in our thread
					self.cmdobj.execute ()
				except Exception, strerror:
					if self.exception_handler is not None:
						self.exception_handler (strerror)
					else:
						raise Exception (strerror)

	def set_exception_handler (self, callback):
		self.exception_handler = callback

	def clear_exception_handler (self):
		self.exception_handler = None

	def get_executing_obj (self):
		return self.cmdobj

	def busy (self):
		return self.outgoing_event.isSet ()

	def add_outgoing (self, cmdobj):

		self.outgoing.append (cmdobj)
		if not self.outgoing_event.isSet ():
			self.outgoing_event.set ()

class FTPGetWelcomeMsg:

	def __init__ (self, queue):
		self.control_queue = queue

	def execute (self):

		response = self.control_queue.wait_for_response ()
		c = response[-1][:1]
		if c not in '123':
			raise FtpException ("Error connecting to server: Welcome message not received correctly")

class FTPAbortCmd:

	def __init__ (self, queue, silence=0):

		self.control_queue = queue
		self.silence = silence

	def execute (self):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user ("ABOR")
			gtk.threads_leave ()

		self.control_queue.send_oob ("ABOR")
		response = self.control_queue.wait_for_response ()
		if response[-1][:3] not in ('426', '226'):
			raise FtpException (response[-1])

class FTPVoidCmd:

	def __init__ (self, queue, cmd, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.cmd = cmd
		self.response = None

	def execute (self):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user (self.cmd)
			gtk.threads_leave ()

		self.control_queue.send (self.cmd)
		self.response = self.control_queue.wait_for_response ()
		if self.response[-1][0] != '2':
			raise FtpException (self.response[-1])

class FTPBasicCmd (FTPVoidCmd):

	def __init__ (self, queue, cmd, silence=0):
		FTPVoidCmd.__init__ (self, queue, cmd, silence)

	def get_response (self):
		return self.response

class FTPLogin:

	def __init__ (self, queue, user, pw, acct, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.user = user
		self.pw = pw
		self.acct = acct

	def execute (self):

		string = "USER " + self.user

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user (string)
			gtk.threads_leave ()

		self.control_queue.send (string)
		response = self.control_queue.wait_for_response ()

		if response[-1][0] == '3':
			string = "PASS " + self.pw

			if not self.silence:
				gtk.threads_enter ()
				main.app.ftp_local_tell_user (string)
				gtk.threads_leave ()

			self.control_queue.send (string)
			response = self.control_queue.wait_for_response ()

		if response[-1][0] == '3':
			# For now, assume ACCT to be ''
			string = "ACCT "

			if not self.silence:
				gtk.threads_enter ()
				main.app.ftp_local_tell_user (string)
				gtk.threads_leave ()

			self.control_queue.send (string)
			response = self.control_queue.wait_for_response ()

		if response[-1][0] != '2':
			raise FtpException ("Error logging in to %s" %main.app['host'])

class FTPTransferConnection:

	def __init__ (self, control, host, port, passive=1):

		self.control_queue = control

		# Set instance variables
		self.sock = None
		self.control_queue = None

		if passive:
			try:
				self.sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
				self.sock.connect ((host, port))
			except socket.error, (strerror, msg):
				raise FtpException ("Error creating connection for transfer: %s" %msg)
		else:
			host, port = self.create_server_sock ()
			try:
				self.sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
				self.sock.bind (('', 0))
				self.sock.listen (1)
				dummyhost, port = sock.getsockname ()
				host, dummyport = control.getsockname ()
			except socket.error, (strerror, msg):
				raise FtpException ("Error creating socket for transfar: %s" %msg)

			self.send_port (host, port)
			self.sock, sockaddr = self.sock.accept ()

	def send_port (self, host, port):

		hbytes = string.split (host, '.')
		pbytes = [ `port/256`, `port%256` ]
		bytes = hbytes + pbytes
		cmd = 'PORT ' + string.join (bytes, ',')
		return self.control.basic_cmd (cmd)

	def create_server_sock (self):
		pass

	def get_sock (self):
		return self.sock

	def get_file (self):
		return self.sock.makefile ('rb')

	def close (self):
		self.sock.close ()

class FTPTransferCmd:

	# For parsing server return codes
	re150 = re.compile ("150 .* \((\d+) bytes\)", re.IGNORECASE)

	def __init__ (self, queue, cmd, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.cmd = cmd

	def execute (self):

		if not self.silence:
			gtk.threads_enter ()
			main.app.ftp_local_tell_user (self.cmd)
			gtk.threads_leave ()

		self.control_queue.send (self.cmd)
		response = self.control_queue.wait_for_response ()

		if response[-1][0] != '1':
			raise FtpException ("Error initiating transfer: %s" %response[-1])
		if response[-1][:3] == '150':
			self.size = self.parse150 (response[-1])

	def parse150 (self, resp):

		# Sanity checking
		if resp[:3] != '150':
			raise FtpException ("Error parsing 150 response: 150 response not given")

		matched = self.re150.match (resp)
		if matched:
			return int (matched.group (1))
		return None

	def get_size (self):
		return self.size


class FTPTransfer:

	def __init__ (self, queue, rest=None, passive=1, silence=0):

		self.control_queue = queue
		self.silence = silence
		self.rest = rest
		self.passive = passive
		self.transfer = None

	def initiate_transfer (self):

		response = None

		if self.passive:
			cmd = FTPBasicCmd (self.control_queue, 'PASV', silence=self.silence)
			cmd.execute ()
			host, port = self.parse227 ( cmd.get_response()[-1] )

			self.transfer = FTPTransferConnection (self.control_queue, host, port, self.passive)

			if self.rest is not None:
				cmd = FTPVoidCmd (self.control_queue, "REST %s" %self.rest, silence=self.silence)
				cmd.execute ()

			cmd = FTPTransferCmd (self.control_queue, self.cmd, silence=self.silence)
			cmd.execute ()

			self.size = cmd.get_size ()
		else:
			# Non passive stuff will go here
			pass

	def parse227 (self, resp):

		# Sanity checking
		if resp[:3] != '227':
			raise FtpException ("Error parsing 227 response: 227 response not given")

		left_bracket = string.find (resp, '(')
		right_bracket = string.find (resp, ')')
		if left_bracket < 0 or right_bracket < 0:
			raise FtpException ("Error parsing 227 response: Format should be (h1,h2,h3,h4,p1,p2) and given: %s" %resp)
		fields = string.split (resp[left_bracket + 1: right_bracket], ',')
		if len (fields) != 6:
			raise FtpException ("Error parsing 227 response: Unable to pack appropriate number of fields")
		host = string.join (fields[:4], '.')
		port = (int (fields[4]) << 8) + int (fields[5])
		return host, port

class FTPList (FTPTransfer):

	def __init__ (self, queue, cmd, callback=None, rest=None, passive=1, silence=0):

		FTPTransfer.__init__ (self, queue, rest=rest, passive=passive, silence=silence)
		self.control_queue = queue
		self.silence = silence
		self.callback = callback
		self.cmd = cmd

		self.abort_flag = 0
		# Local copy of data
		self.data = [ ]

	def execute (self):

		# Set the appropriate transfer type
		type = FTPBasicCmd (self.control_queue, 'TYPE A', silence=self.silence)
		type.execute ()

		# Now prepare for the transfer
		self.initiate_transfer ()

		self.sock = self.transfer.get_sock ()
		self.file = self.transfer.get_file ()

		while not self.abort_flag:
			line = self.file.readline ()
			if not line:
				break
			if line[-2:] == CRLF:
				line = line[:-2]
			elif line[-1:] in CRLF:
				line = line[:-1]
			self.data.append (line)

		# Wait for the finished transfer and then get the response
		self.control_queue.wait_for_response ()

		if self.callback is None:
			gtk.threads_enter ()
			main.app.update_remote_listing (self.get_data ())
			gtk.threads_leave ()
		else:
			self.callback (self.get_data ())

	def get_data (self):
		return self.data

	def abort (self):
		self.abort_flag = 1

class FTPBinaryTransfer (FTPTransfer):

	def __init__ (self, queue, cmd, local_file, direction, progress=None, blocksize=8192, rest=None, passive=1, silence=0):

		FTPTransfer.__init__ (self, queue, rest, passive)
		self.control_queue = queue
		self.cmd = cmd
		self.local_file = local_file
		self.direction = direction
		self.silence = silence
		self.progress = progress
		self.blocksize = blocksize
		self.rest = rest
		self.passive = passive

		# For aborting the transfer
		self.abort_flag = 0

	def execute (self):

		# Set the appropriate transfer type
		type = FTPBasicCmd (self.control_queue, 'TYPE I', silence=self.silence)
		type.execute ()

		# Now prepare for the transfer
		self.initiate_transfer ()

		self.sock = self.transfer.get_sock ()

		if self.direction == DOWNLOAD:
			file = open (self.local_file, 'w')
			while not self.abort_flag:
				data = self.sock.recv (self.blocksize)
				if not data:
					break
				gtk.threads_enter ()
				self.progress.update_transfer (len (data))
				gtk.threads_leave ()
				file.write (data)
		elif self.direction == UPLOAD:
			file = open (self.local_file, 'r')
			while not self.abort_flag:
				data = file.read (self.blocksize)
				if not data:
					break
				written = self.sock.send (data)
				gtk.threads_enter ()
				self.progress.update_transfer (len (data))
				gtk.threads_leave ()

		file.close ()
		self.sock.close ()

		if self.direction == DOWNLOAD:
			# Wait for the finished transfer and then get the response
			self.control_queue.wait_for_response ()
			gtk.threads_enter ()
			main.app.add_to_download_list (self.local_file)
			main.app.update_local_listing ('.')
			gtk.threads_leave ()
		elif self.direction == UPLOAD:
			self.control_queue.wait_for_response ()
			gtk.threads_enter ()
			main.app.main_thread.list ()
			gtk.threads_leave ()

	def abort (self):
		self.abort_flag = 1

class FTP:

	def __init__ (self, silence=0):

		self.silence = silence

		# Set initial variables
		self.control_connect = None
		self.passive_server = 1

		self.control_connect = FtpControlConnection (silence=self.silence)
		self.control_connect.start ()

		# Sanity checking on our options
		if not main.app['username']:
			raise FtpException ("Error opening FTP connection: No username given")
		if not main.app['password']:
			raise FtpException ("Error opening FTP connection: No password given")
		if not main.app['host']:
			raise FtpException ("Error opening FTP connection: No host given")
		if not (main.app['port'] > 0 and main.app['port'] < 65534):
			raise FtpException ("Error opening FTP connection: Invalid port given")
		if not main.app['delay'] > 0:
			raise FtpException ("Error opening FTP connection: No delay given")

	def destroy (self):
		self.control_connect.destroy ()

	def set_exception_handler (self, callback):
		self.control_connect.set_exception_handler (callback)

	def clear_exception_handler (self):
		self.control_connect.clear_exception_handler ()

	def reset (self):
		self.control_connect.reset_queue ()

	def ftp_busy (self):
		return self.control_connect.busy ()

	def get_welcome_msg (self):

		welcome = FTPGetWelcomeMsg (self.control_connect)
		self.control_connect.add_outgoing (welcome)

	def perform_login (self):

		login = FTPLogin (self.control_connect, main.app['username'], main.app['password'], '', silence=self.silence)
		self.control_connect.add_outgoing (login)

	def cwd (self, dir):

		# Default to '/' if dir is blank
		if not dir:
			dir = '/'

		if dir == '..':
			cmd = "CDUP"
		elif dir == ".":
			# Actually do nothing and return
			return
		else:
			cmd = "CWD " + dir
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.control_connect.add_outgoing (ftpcmd)

	def delete (self, file):

		cmd = 'DELE ' + file
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.control_connect.add_outgoing (ftpcmd)

	def mkdir (self, directory):

		cmd = 'MKD ' + directory
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.control_connect.add_outgoing (ftpcmd)

	def rmdir (self, directory):

		cmd = 'RMD ' + directory
		ftpcmd = FTPVoidCmd (self.control_connect, cmd, silence=self.silence)
		self.control_connect.add_outgoing (ftpcmd)

	def abort (self):

		self.control_connect.reset_queue ()
		aborcmd = FTPAbortCmd (self.control_connect, silence=self.silence)
		self.control_connect.add_outgoing (aborcmd)

		# Check if we're cancelling an outstanding transfer such as a binary
		# transfer or list
		executing = self.control_connect.get_executing_obj ()
		if isinstance (executing, FTPTransfer):
			executing.abort ()

	def list (self, args=None, callback=None, rest=None):

		cmd = 'LIST'
		if args is not None:
			for i in args:
				if i:
					cmd = cmd + (' ' + i)
		listcmd = FTPList (self.control_connect, cmd, callback=callback, silence=self.silence)
		self.control_connect.add_outgoing (listcmd)

	def retrieve_binary (self, file, local_dir, progress, blocksize=8192, passive=1, rest=None):

		cmd = 'RETR ' + file
		local_file = local_dir + os.sep + file
		binarycmd = FTPBinaryTransfer (self.control_connect, cmd, local_file, DOWNLOAD, progress, blocksize, rest, passive, silence=self.silence)
		self.control_connect.add_outgoing (binarycmd)

	def upload_binary (self, file, local_dir, progress, blocksize=8192, passive=1, rest=None):

		cmd = 'STOR ' + file
		local_file = local_dir + os.sep + file
		binarycmd = FTPBinaryTransfer (self.control_connect, cmd, local_file, UPLOAD, progress, blocksize, rest, passive, silence=self.silence)
		self.control_connect.add_outgoing (binarycmd)
