#!/usr/bin/python2
# vim: noexpandtab shiftwidth=4 softtabstop=4 tabstop=4

import fcntl
import errno
import posix
import time
import signal
import os
import sys
import time
import getopt
import traceback
import datetime
import mimetypes
import urlparse
import urllib
import cStringIO
import socket
import select
import pwd


"""Http server based on recipes 511453,511454 from code.activestate.com by Pierre Quentel"""
"""Added support for indexes, access tests, proper handle of SystemExit exception, fixed couple of errors and vulnerbilities, getopt, lockfiles, daemonize etc. by Jakub Kruszona-Zawadzki"""

# the dictionary holding one client handler for each connected client
# key = client socket, value = instance of (a subclass of) ClientHandler
client_handlers = {}

# =======================================================================
# The server class. Creating an instance starts a server on the specified
# host and port
# =======================================================================
class Server:
	def __init__(self,host='localhost',port=80):
		if host=='any':
			host=''
		self.host,self.port = host,port
		self.socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
		self.socket.setblocking(0)
		self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
		self.socket.bind((host,port))
		self.socket.listen(50)

# =====================================================================
# Generic client handler. An instance of this class is created for each
# request sent by a client to the server
# =====================================================================
class ClientHandler:
	blocksize = 2048

	def __init__(self, server, client_socket, client_address):
		self.server = server
		self.client_address = client_address
		self.client_socket = client_socket
		self.client_socket.setblocking(0)
		self.host = socket.getfqdn(client_address[0])
		self.incoming = '' # receives incoming data
		self.outgoing = ''
		self.writable = False
		self.close_when_done = True

	def handle_error(self):
		self.close()

	def handle_read(self):
		"""Reads the data received"""
		try:
			buff = self.client_socket.recv(1024)
			if not buff:  # the connection is closed
				self.close()
			# buffer the data in self.incoming
			self.incoming += buff #.write(buff)
			self.process_incoming()
		except socket.error:
			self.close()

	def process_incoming(self):
		"""Test if request is complete ; if so, build the response
		and set self.writable to True"""
		if not self.request_complete():
			return
		self.response = self.make_response()
		self.outgoing = ''
		self.writable = True

	def request_complete(self):
		"""Return True if the request is complete, False otherwise
		Override this method in subclasses"""
		return True

	def make_response(self):
		"""Return the list of strings or file objects whose content will
		be sent to the client
		Override this method in subclasses"""
		return ["xxx"]

	def handle_write(self):
		"""Send (a part of) the response on the socket
		Finish the request if the whole response has been sent
		self.response is a list of strings or file objects
		"""
		if self.outgoing=='' and self.response:
			if isinstance(self.response[0],str):
				self.outgoing = self.response.pop(0)
			else:
				self.outgoing = self.response[0].read(self.blocksize)  # pylint: disable=E1101
				if not self.outgoing:
					self.response.pop(0)
		if self.outgoing:
			try:
				sent = self.client_socket.send(self.outgoing)
			except socket.error:
				self.close()
				return
			if sent < len(self.outgoing):
				self.outgoing = self.outgoing[sent:]
			else:
				self.outgoing = ''
		if self.outgoing=='' and not self.response:
			if self.close_when_done:
				self.close() # close socket
			else:
				# reset for next request
				self.writable = False
				self.incoming = ''

	def close(self):
		del client_handlers[self.client_socket]
		self.client_socket.close()

# ============================================================================
# Main loop, calling the select() function on the sockets to see if new
# clients are trying to connect, if some clients have sent data and if those
# for which the response is complete are ready to receive it
# For each event, call the appropriate method of the server or of the instance
# of ClientHandler managing the dialog with the client : handle_read() or
# handle_write()
# ============================================================================
def loop(server,handler,timeout=30):
	while True:
		k = client_handlers.keys()
		# w = sockets to which there is something to send
		# we must test if we can send data
		w = [ cl for cl in client_handlers if client_handlers[cl].writable ]
		# the heart of the program ! "r" will have the sockets that have sent
		# data, and the server socket if a new client has tried to connect
		r,w,e = select.select(k+[server.socket],w,k,timeout)
		for e_socket in e:
			client_handlers[e_socket].handle_error()
		for r_socket in r:
			if r_socket is server.socket:
				# server socket readable means a new connection request
				try:
					client_socket,client_address = server.socket.accept()
					client_handlers[client_socket] = handler(server,client_socket,client_address)
				except socket.error:
					pass
			else:
				# the client connected on r_socket has sent something
				client_handlers[r_socket].handle_read()
		w = set(w) & set(client_handlers.keys()) # remove deleted sockets
		for w_socket in w:
			client_handlers[w_socket].handle_write()


# =============================================================
# An implementation of the HTTP protocol, supporting persistent
# connections and CGI
# =============================================================

class HTTP(ClientHandler):
	# parameters to override if necessary
	root = os.getcwd()                              # the directory to serve files from
	index_files = ['index.cgi','index.html']        # index files for directories
	logging = True                                  # print logging info for each request ?
	blocksize = 2 << 16                             # size of blocks to read from files and send

	def request_complete(self):
		"""In the HTTP protocol, a request is complete if the "end of headers"
		sequence ('\r\n\r\n') has been received
		If the request is POST, stores the request body in a StringIO before
		returning True"""
		terminator = self.incoming.find('\r\n\r\n')
		if terminator == -1:
			return False
		lines = self.incoming[:terminator].split('\r\n')
		self.requestline = lines[0]
		try:
			self.method,self.url,self.protocol = lines[0].strip().split()
			if not self.protocol.startswith("HTTP/1") or (self.protocol[7]!='0' and self.protocol[7]!='1') or len(self.protocol)!=8:
				self.method = None
				self.protocol = "HTTP/1.1"
				return True
		except:
			self.method = None
			self.protocol = "HTTP/1.1"
			return True
		# put request headers in a dictionary
		self.headers = {}
		for line in lines[1:]:
			k,v = line.split(':',1)
			self.headers[k.lower().strip()] = v.strip()
		# persistent connection
		close_conn = self.headers.get("connection","")
		if (self.protocol == "HTTP/1.1" and close_conn.lower() == "keep-alive"):
			self.close_when_done = False
		# parse the url
		scheme,netloc,path,params,query,fragment = urlparse.urlparse(self.url)
		self.path,self.rest = path,(params,query,fragment)

		if self.method == 'POST':
			# for POST requests, read the request body
			# its length must be specified in the content-length header
			content_length = int(self.headers.get('content-length',0))
			body = self.incoming[terminator+4:]
			# request is incomplete if not all message body received
			if len(body)<content_length:
				return False
			f_body = cStringIO.StringIO(body)
			f_body.seek(0)
			sys.stdin = f_body # compatibility with CGI

		return True

	def make_response(self):
		try:
			"""Build the response : a list of strings or files"""
			if self.method is None: # bad request
				return self.err_resp(400,'Bad request : %s' %self.requestline)
			resp_headers, resp_body, resp_file = '','',None
			if not self.method in ['GET','POST','HEAD']:
				return self.err_resp(501,'Unsupported method (%s)' %self.method)
			else:
				file_name = self.file_name = self.translate_path()
				if not file_name.startswith(HTTP.root+os.path.sep) and not file_name==HTTP.root:
					return self.err_resp(403,'Forbidden')
				elif not os.path.exists(file_name):
					return self.err_resp(404,'File not found')
				elif self.managed():
					response = self.mngt_method()
				elif not os.access(file_name,os.R_OK):
					return self.err_resp(403,'Forbidden')
				else:
					fstatdata = os.stat(file_name)
					if (fstatdata.st_mode & 0170000) == 0040000:    # directory
						for index in self.index_files:
							if os.path.exists(file_name+'/'+index) and os.access(file_name+'/'+index,os.R_OK):
								return self.redirect_resp(index)
					if (fstatdata.st_mode & 0170000) != 0100000:
						return self.err_resp(403,'Forbidden')
					ext = os.path.splitext(file_name)[1]
					c_type = mimetypes.types_map.get(ext,'text/plain')
					resp_line = "%s 200 Ok\r\n" %self.protocol
					size = fstatdata.st_size
					resp_headers = "Content-Type: %s\r\n" %c_type
					resp_headers += "Content-Length: %s\r\n" %size
					resp_headers += '\r\n'
					if self.method == "HEAD":
						resp_string = resp_line + resp_headers
					elif size > HTTP.blocksize:
						resp_string = resp_line + resp_headers
						resp_file = open(file_name,'rb')
					else:
						resp_string = resp_line + resp_headers + \
							open(file_name,'rb').read()
					response = [resp_string]
					if resp_file:
						response.append(resp_file)
			self.log(200)
			return response
		except:
			return self.err_resp(500,'Internal Server Error')

	def translate_path(self):
		"""Translate URL path into a path in the file system"""
		return os.path.realpath(os.path.join(HTTP.root,*self.path.split('/')))

	def managed(self):
		"""Test if the request can be processed by a specific method
		If so, set self.mngt_method to the method used
		This implementation tests if the script is in a cgi directory"""
		if self.is_cgi():
			self.mngt_method = self.run_cgi
			return True
		return False

	def is_cgi(self):
		"""Test if url points to cgi script"""
		if self.path.endswith(".cgi"):
			return True
		return False

	def run_cgi(self):
		if not os.access(self.file_name,os.X_OK):
			return self.err_resp(403,'Forbidden')
		# set CGI environment variables
		self.make_cgi_env()
		# redirect print statements to a cStringIO
		save_stdout = sys.stdout
		output_buffer = cStringIO.StringIO()
		sys.stdout = output_buffer
		# run the script
		try:
			execfile(self.file_name, {})
		except SystemExit:
			pass
		except:
			output_buffer = cStringIO.StringIO()
			output_buffer.write("Content-type:text/plain\r\n\r\n")
			traceback.print_exc(file=output_buffer)
		sys.stdout = save_stdout # restore sys.stdout
		response = output_buffer.getvalue()
		if self.method == "HEAD":
			# for HEAD request, don't send message body even if the script
			# returns one (RFC 3875)
			head_lines = []
			for line in response.split('\n'):
				if not line:
					break
				head_lines.append(line)
			response = '\n'.join(head_lines)
		# close connection in case there is no content-length header
		self.close_when_done = True
		resp_line = "%s 200 Ok\r\n" %self.protocol
		return [resp_line + response]

	def make_cgi_env(self):
		"""Set CGI environment variables"""
		env = {}
		env['SERVER_SOFTWARE'] = "AsyncServer"
		env['SERVER_NAME'] = "AsyncServer"
		env['GATEWAY_INTERFACE'] = 'CGI/1.1'
		env['DOCUMENT_ROOT'] = HTTP.root
		env['SERVER_PROTOCOL'] = "HTTP/1.1"
		env['SERVER_PORT'] = str(self.server.port)

		env['REQUEST_METHOD'] = self.method
		env['REQUEST_URI'] = self.url
		env['PATH_TRANSLATED'] = self.translate_path()
		env['SCRIPT_NAME'] = self.path
		env['PATH_INFO'] = urlparse.urlunparse(("","","",self.rest[0],"",""))
		env['QUERY_STRING'] = self.rest[1]
		if not self.host == self.client_address[0]:
			env['REMOTE_HOST'] = self.host
		env['REMOTE_ADDR'] = self.client_address[0]
		env['CONTENT_LENGTH'] = str(self.headers.get('content-length',''))
		for k in ['USER_AGENT','COOKIE','ACCEPT','ACCEPT_CHARSET',
			'ACCEPT_ENCODING','ACCEPT_LANGUAGE','CONNECTION']:
			hdr = k.lower().replace("_","-")
			env['HTTP_%s' %k.upper()] = str(self.headers.get(hdr,''))
		os.environ.update(env)

	def redirect_resp(self,redirurl):
		"""Return redirect message"""
		resp_line = "%s 301 Moved Permanently\r\nLocation: %s\r\n" % (self.protocol,redirurl)
		self.close_when_done = True
		self.log(301)
		return [resp_line]

	def err_resp(self,code,msg):
		"""Return an error message"""
		resp_line = "%s %s %s\r\n" %(self.protocol,code,msg)
		self.close_when_done = True
		self.log(code)
		return [resp_line]

	def log(self,code):
		"""Write a trace of the request on stderr"""
		if HTTP.logging:
			date_str = datetime.datetime.now().strftime('[%d/%b/%Y %H:%M:%S]')
			sys.stderr.write('%s - - %s "%s" %s\n' %(self.host,date_str,self.requestline,code))


# =======================================================================
# exit_err function. Exits with error code.
# =======================================================================
def exit_err(msg):
	sys.stderr.write(msg)
	exit(1)


# =======================================================================
# fork function. Calls fork and exits from parent process.
# =======================================================================
def fork():
	try:
		pid = os.fork()
		if pid > 0:
			sys.exit(0)
	except OSError as e:
		exit_err("fork failed: %d (%s)" % (e.errno, e.strerror))


# =======================================================================
# daemonize function. Sends current process to background and manages pidfile.
# =======================================================================
def daemonize(pidfile, user=None):
	# open pidfile descriptor
	try:
		pidf = open(pidfile, 'w+')
	except IOError as e:
		exit_err("could not open pidfile for writing: %s" % pidfile)

	# change user from root to custom
	if user:
		_, _, uid, gid, _, _, _ = pwd.getpwnam(user)
		os.setgid(gid)
		os.setuid(uid)

	# flush output buffers before forking to avoid printing something twice
	sys.stdout.flush()
	sys.stderr.flush()

	# do first fork
	fork()

	# decouple from parent environment
	os.chdir("/")
	os.setsid()
	os.umask(0)

	# do second fork
	fork()

	# redirect standard file descriptors
	nullin = open('/dev/null', 'r')
	nullout = open('/dev/null', 'a+')
	os.dup2(nullin.fileno(), sys.stdin.fileno())
	os.dup2(nullout.fileno(), sys.stdout.fileno())
	os.dup2(nullout.fileno(), sys.stderr.fileno())

	# write pidfile
	pidf.write("%d\n" % os.getpid())
	pidf.close()


if __name__=="__main__":
	verbose = False
	host = 'any'
	port = 9425
	rootpath="/usr/share/mfscgi"
	pidfile = None
	user = None

	opts,args = getopt.getopt(sys.argv[1:],"vhH:P:R:p:u:")
	for opt, val in opts:
		if opt == '-h':
			print "usage: %s [-H bind_host] [-P bind_port] [-R rootpath] [-v]\n" % sys.argv[0]
			print "-H bind_host : local address to listen on (default: any)"
			print "-P bind_port : port to listen on (default: 9425)"
			print "-R rootpath : local path to use as HTTP document root (default: /usr/share/mfscgi)"
			print "-v : log requests on stderr"
			print "-p : pidfile path, setting it triggers manual daemonization"
			print "-u : username of server owner, used in manual daemonization"
			sys.exit(0)
		elif opt == '-H':
			host = val
		elif opt == '-P':
			port = int(val)
		elif opt == '-R':
			rootpath = val
		elif opt == '-v':
			verbose = True
		elif opt == '-p':
			pidfile = val
		elif opt == '-u':
			user = val

	# launch the server on the specified port
	server = Server(host, port)
	if host != 'any':
		print "Asynchronous HTTP server running on %s:%s" % (host,port)
	else:
		print "Asynchronous HTTP server running on port %s" % port
	if verbose:
		HTTP.logging = True
	else:
		HTTP.logging = False
	HTTP.root = os.path.realpath(rootpath)
	if pidfile:
		daemonize(pidfile, user)
	loop(server, HTTP)
