# encoding: utf-8
"""
server.py

Created by David Farrar  on 2011-11-30.
Copyright (c) 2011-2013  Exa Networks. All rights reserved.
"""

import socket
import errno

from exaproxy.network.functions import isipv4
from exaproxy.network.errno_list import errno_block

from exaproxy.util.proxy import ProxyProtocol

def ishex (s):
	return bool(s) and not bool(s.strip('0123456789abcdefABCDEF'))

def count_quotes (data):
	return data.count('"') - data.count('\\"')

class HTTPClient (object):
	eor = ['\r\n\r\n', '\n\n']
	proxy_protocol = ProxyProtocol()

	__slots__ = ['name', 'ipv4', 'sock', 'accept_addr', 'accept_port', 'peer', 'reader', 'writer', 'w_buffer', 'log']

	def __init__(self, name, sock, peer, logger, max_buffer, proxied):
		addr, port = sock.getsockname()[:2]

		self.name = name
		self.sock = sock
		self.peer = peer
		self.accept_addr = addr
		self.accept_port = port
		self.ipv4 = isipv4(addr)
		self.reader = self._read(sock, max_buffer, proxied=proxied)
		self.writer = self._write(sock)
		self.w_buffer = ''

		self.log = logger

		# start the _read coroutine
		self.reader.next()

	def checkRequest (self, r_buffer, size, seek=0):
		for eor in self.eor:
			pos = r_buffer[seek:].find(eor)
			if pos == -1: continue

			buff = r_buffer[:seek+pos]
			if not buff: continue

			if not count_quotes(buff) % 2:  # we have matching pairs
				return buff + eor, r_buffer[seek+pos+len(eor):], seek

			seek += pos + len(eor)

		if size and len(r_buffer) > size:
			return None,None,None

		return '', r_buffer, seek


	def checkChunkSize (self, r_buffer):
		# return a tuple : bool, length
		# * the bool is : is there more chunk to come
		# * the len contains the size of the chunk(s) extracted
		#   a size of None means that we could not decode as it is invalid

		total_len = 0

		while r_buffer:
			if not '\n' in r_buffer:
				if len(r_buffer) > 6:  # len('FFFF') + len(\r\n)
					return True, None
				return True, 0

			header,r_buffer = r_buffer.split('\n', 1)
			len_header = len(header) + 1

			if header.endswith('\r'):
				header = header[:-1]
				len_eol = 2
			else:
				len_eol = 1

			if ';' in header:
				header = header.split(';',1)[0]

			if not ishex(header):
				return True,None

			len_chunk = int(header, 16)

			# 0xFFFF is not enough - coad is complaining :p
			if len_chunk > 0x100000:
				return True,None

			if len_chunk == 0:
				total_len += len_header
				return False, total_len
			else:
				total = len_chunk + len_eol
				total_len += total + len_header
				r_buffer = r_buffer[total:]

		return True,total_len


	def _read (self, sock, max_buffer, read_size=64*1024, proxied=False):
		"""Coroutine managing data read from the client"""
		# yield request, content
		# request is the text that form the request header
		# content any text which is related to the current request after the headers

		yield ''

		r_buffer = ''
		nb_to_send = 0
		seek = 0
		masquerade = None

		# mode can be one of : request, chunk, extension, relay
		# request : we are reading the request (read all you can until a separator)
		# extra-headers : we are reading data until a separator
		# chunked : we are reading chunk-encoded darta
		# transfer : we are reading as much as requested in remaining
		# passthrough : read as much as can to be relayed

		mode = 'proxy' if proxied else 'http'
		http_request = ''
		data = ''

		while True:
			try:
				while True:
					if mode != 'new-request' and (mode != 'transfer' or r_buffer == ''):
						new_data = sock.recv(read_size)
						if not new_data:
							break # read failed so we abort

						r_buffer += new_data

					elif mode == 'new-request':
						mode = 'http'

					if mode == 'proxy':
						masquerade, r_buffer, mode = self.processProxyHeader(r_buffer, mode)
						if masquerade is None:
							break

						elif not masquerade:
							continue

						self.setPeer(masquerade)

					# check for a new http header
					if mode == 'http':
						r_buffer = r_buffer.lstrip('\r\n')

						http_request, r_buffer, seek = self.checkRequest(r_buffer, max_buffer, seek)
						if http_request is None:
							break

					# all modes that are not directly related to reading a new request
					if mode != 'http':
						data, r_buffer, mode, nb_to_send, seek = self.process(r_buffer, mode, nb_to_send, max_buffer, seek)
						if data is None:
							break

					if mode == 'http' and http_request:
						http_response, http_request = [http_request], ''
						seek = 0

						mode, nb_to_send = yield http_response, ['']

					elif data:
						data_response, data = [data], ''
						yield [''], data_response

					else:
						yield [''], ['']


				# break out of the outer loop as soon as we leave the inner loop
				# through normal execution
				break

			except socket.error, e:
				if e.args[0] in errno_block:
					yield [''], ['']
				else:
					break

		yield [None], [None]


	def processProxyHeader (self, r_buffer, mode):
		r_buffer = r_buffer.lstrip('\r\n')

		for eor in self.eor:
			if eor in r_buffer:
				client_ip, r_buffer = self.proxy_protocol.parse(r_buffer)
				mode = 'http'
				break

			else:
				client_ip, r_buffer = '', r_buffer

		return client_ip, r_buffer, mode

	def process (self, r_buffer, mode, nb_to_send, max_buffer, seek):
		if mode == 'passthrough':
			data, r_buffer, new_mode, nb_to_send, seek = r_buffer, '', mode, 0, 0

		elif mode == 'transfer':
			data, r_buffer, new_mode, nb_to_send, seek = self._transfer(r_buffer, mode, nb_to_send)

		elif mode == 'chunked':
			data, r_buffer, new_mode, nb_to_send, seek = self._chunked(r_buffer, mode, nb_to_send)

		elif mode == 'end-chunk':
			data, r_buffer, new_mode, nb_to_send, seek = self._endChunk(r_buffer, mode, nb_to_send)

		elif mode == 'extra-headers':
			data, r_buffer, new_mode, nb_to_send, seek = self._headers(r_buffer, mode, nb_to_send, max_buffer, seek)

		else:
			data, r_buffer, new_mode, nb_to_send, seek = None, r_buffer, None, seek

		return data, r_buffer, new_mode, nb_to_send, seek

	def _transfer (self, r_buffer, mode, nb_to_send):
		r_len = len(r_buffer)
		length = min(r_len, nb_to_send)

		r_buffer, data = r_buffer[length:], r_buffer[:length]
		nb_to_send = nb_to_send - length

		if nb_to_send == 0:
			mode = 'new-request'

		return data, r_buffer, mode, nb_to_send, 0

	def _chunked (self, r_buffer, mode, nb_to_send):
		r_len = len(r_buffer)
		length = min(r_len, nb_to_send)

		r_buffer, data = r_buffer[length:], r_buffer[:length]
		nb_to_send = nb_to_send - length

		# sum of the sizes of all chunks in our buffer
		chunked, new_to_send = self.checkChunkSize(r_buffer[nb_to_send:])

		if new_to_send is not None:
			nb_to_send += new_to_send

		else:
			# data is invalid
			data = None

		if not chunked:
			mode = 'end-chunk' if nb_to_send > 0 else 'new-request'

		return data, r_buffer, mode, nb_to_send, 0

	def _endChunk (self, r_buffer, mode, nb_to_send):
		header_buffer = r_buffer[nb_to_send:]

		if header_buffer.startswith('\r\n'):
			nb_to_send += 2
			seek = 0
			mode = 'new-request'

		elif header_buffer.startswith('\n'):
			nb_to_send += 1
			seek = 0
			mode = 'new-request'

		elif header_buffer and header_buffer != '\r':
			mode = 'extra-headers'
			seek = nb_to_send

		return '', r_buffer, mode, nb_to_send, seek

	def _headers (self, r_buffer, mode, nb_to_send, max_buffer, seek):
		related, r_buffer, seek = self.checkRequest(r_buffer, max_buffer, seek)

		if related:
			mode = 'new-request'
			seek = 0

		return related, r_buffer, mode, nb_to_send, seek


	def setPeer (self, peer):
		"""Set the claimed ip address for this client.
		Does not effect the ip address we try sending data to."""
		self.peer = peer

	def readData(self):
		# pop data from lists to free memory held by the coroutine
		request_l, content_l = self.reader.send(('new-request',0))
		request = request_l.pop()
		content = content_l.pop()

		return self.name, self.accept_addr, self.accept_port, self.peer, request, '', content

	def readRelated(self, mode, remaining):
		# pop data from lists to free memory held by the coroutine
		mode = mode or 'new-request'
		request_l, content_l = self.reader.send((mode,remaining))
		request = request_l.pop()
		content = content_l.pop()

		return self.name, self.accept_addr, self.peer, request, '', content

	def _write(self, sock):
		"""Coroutine managing data sent to the client"""
		filename = yield None

		# check to see if we are returning data directly from a local file
		if filename is not None:
			try:
				# NOTE: we must read from the file on demand rather than doing this
				with open(filename) as fd:
					file_data = fd.read()

				found = True, False, 0, 0
			except IOError:
				found = None

			data = yield found
			self.w_buffer += data + file_data
			file_data = ''

		else:
			found = None

		data = yield found
		finished = False
		w_buffer = self.w_buffer

		while True:
			try:
				while True:
					w_buffer = self.w_buffer
					had_buffer = bool(w_buffer)

					if data is not None:
						w_buffer += data
					else:
						# We've finished downloading, even if the client hasn't yet
						finished = True

					if finished:
						if not w_buffer:
							break	  # terminate the client connection
						elif data:
							self.log.error('Tried to send data to client after we told it to close. Dropping it.')

					if not had_buffer or data == '':
						sent = sock.send(w_buffer)
						#if sent:
						#	self.log.debug(">> [%s]" % w_buffer[:sent].replace('\t','\\t').replace('\r','\\r').replace('\n','\\n'))
						w_buffer = w_buffer[sent:]
					else:
						sent = 0

					self.w_buffer = w_buffer
					buffered = bool(w_buffer) or finished
					data = yield buffered, had_buffer, sent if self.ipv4 else 0, 0 if self.ipv4 else sent

				# break out of the outer loop as soon as we leave the inner loop
				# through normal execution
				yield None
				break

			except socket.error, e:
				self.w_buffer = w_buffer
				if e.args[0] in errno_block:
					self.log.debug('interrupted when trying to sent %d bytes, fine, will retry' % len(data))
					self.log.debug('reason: errno %d: %s' % (e.args[0], errno.errorcode.get(e.args[0], '<no errno name>')))
					data = yield bool(w_buffer) or finished, had_buffer, 0, 0
				else:
					self.log.debug('handled an unexpected error writing on socket')
					self.log.debug('reason, errno %d: %s' % (e.args[0], errno.errorcode.get(e.args[0], '<no errno name>')))
					yield None  # stop the client connection
					break  # and don't come back

		yield None

	def writeData(self, data):
		return self.writer.send(data)

	def startData(self, command, data):
		# start the _write coroutine
		self.writer = self._write(self.sock)
		self.writer.next()

		if command == 'stream':
			self.writer.send(None)  # no local file
			res = self.writer.send(data)

		elif command == 'close':
			self.writer.send(None)  # no local file
			res = self.writer.send(data)
			self.writer.send(None)  # close the connection once the buffer is empty

		elif command == 'file':
			header, filename = data
			res = self.writer.send(filename)  # use local file
			self.writer.send(header)  # write the response headers before the file

			self.writer.send(None)  # close the connection once the buffer is empty

		else:
			res = None

		# buffered, had_buffer
		return self.name, self.peer, res

	def shutdown(self):
		try:
			self.sock.shutdown(socket.SHUT_RDWR)
		except socket.error:
			pass
		finally:
			self.sock.close()

		self.writer.close()
		self.reader.close()
