1#!/usr/local/bin/python3.8
2# vim: noexpandtab shiftwidth=4 softtabstop=4 tabstop=4
3
4import fcntl
5import errno
6import posix
7import time
8import signal
9import os
10import sys
11import time
12import getopt
13import traceback
14import datetime
15import mimetypes
16import urlparse
17import urllib
18import cStringIO
19import socket
20import select
21import pwd
22
23
24"""Http server based on recipes 511453,511454 from code.activestate.com by Pierre Quentel"""
25"""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"""
26
27# the dictionary holding one client handler for each connected client
28# key = client socket, value = instance of (a subclass of) ClientHandler
29client_handlers = {}
30
31# =======================================================================
32# The server class. Creating an instance starts a server on the specified
33# host and port
34# =======================================================================
35class Server:
36	def __init__(self,host='localhost',port=80):
37		if host=='any':
38			host=''
39		self.host,self.port = host,port
40		self.socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
41		self.socket.setblocking(0)
42		self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
43		self.socket.bind((host,port))
44		self.socket.listen(50)
45
46# =====================================================================
47# Generic client handler. An instance of this class is created for each
48# request sent by a client to the server
49# =====================================================================
50class ClientHandler:
51	blocksize = 2048
52
53	def __init__(self, server, client_socket, client_address):
54		self.server = server
55		self.client_address = client_address
56		self.client_socket = client_socket
57		self.client_socket.setblocking(0)
58		self.host = socket.getfqdn(client_address[0])
59		self.incoming = '' # receives incoming data
60		self.outgoing = ''
61		self.writable = False
62		self.close_when_done = True
63
64	def handle_error(self):
65		self.close()
66
67	def handle_read(self):
68		"""Reads the data received"""
69		try:
70			buff = self.client_socket.recv(1024)
71			if not buff:  # the connection is closed
72				self.close()
73			# buffer the data in self.incoming
74			self.incoming += buff #.write(buff)
75			self.process_incoming()
76		except socket.error:
77			self.close()
78
79	def process_incoming(self):
80		"""Test if request is complete ; if so, build the response
81		and set self.writable to True"""
82		if not self.request_complete():
83			return
84		self.response = self.make_response()
85		self.outgoing = ''
86		self.writable = True
87
88	def request_complete(self):
89		"""Return True if the request is complete, False otherwise
90		Override this method in subclasses"""
91		return True
92
93	def make_response(self):
94		"""Return the list of strings or file objects whose content will
95		be sent to the client
96		Override this method in subclasses"""
97		return ["xxx"]
98
99	def handle_write(self):
100		"""Send (a part of) the response on the socket
101		Finish the request if the whole response has been sent
102		self.response is a list of strings or file objects
103		"""
104		if self.outgoing=='' and self.response:
105			if isinstance(self.response[0],str):
106				self.outgoing = self.response.pop(0)
107			else:
108				self.outgoing = self.response[0].read(self.blocksize)  # pylint: disable=E1101
109				if not self.outgoing:
110					self.response.pop(0)
111		if self.outgoing:
112			try:
113				sent = self.client_socket.send(self.outgoing)
114			except socket.error:
115				self.close()
116				return
117			if sent < len(self.outgoing):
118				self.outgoing = self.outgoing[sent:]
119			else:
120				self.outgoing = ''
121		if self.outgoing=='' and not self.response:
122			if self.close_when_done:
123				self.close() # close socket
124			else:
125				# reset for next request
126				self.writable = False
127				self.incoming = ''
128
129	def close(self):
130		del client_handlers[self.client_socket]
131		self.client_socket.close()
132
133# ============================================================================
134# Main loop, calling the select() function on the sockets to see if new
135# clients are trying to connect, if some clients have sent data and if those
136# for which the response is complete are ready to receive it
137# For each event, call the appropriate method of the server or of the instance
138# of ClientHandler managing the dialog with the client : handle_read() or
139# handle_write()
140# ============================================================================
141def loop(server,handler,timeout=30):
142	while True:
143		k = client_handlers.keys()
144		# w = sockets to which there is something to send
145		# we must test if we can send data
146		w = [ cl for cl in client_handlers if client_handlers[cl].writable ]
147		# the heart of the program ! "r" will have the sockets that have sent
148		# data, and the server socket if a new client has tried to connect
149		r,w,e = select.select(k+[server.socket],w,k,timeout)
150		for e_socket in e:
151			client_handlers[e_socket].handle_error()
152		for r_socket in r:
153			if r_socket is server.socket:
154				# server socket readable means a new connection request
155				try:
156					client_socket,client_address = server.socket.accept()
157					client_handlers[client_socket] = handler(server,client_socket,client_address)
158				except socket.error:
159					pass
160			else:
161				# the client connected on r_socket has sent something
162				client_handlers[r_socket].handle_read()
163		w = set(w) & set(client_handlers.keys()) # remove deleted sockets
164		for w_socket in w:
165			client_handlers[w_socket].handle_write()
166
167
168# =============================================================
169# An implementation of the HTTP protocol, supporting persistent
170# connections and CGI
171# =============================================================
172
173class HTTP(ClientHandler):
174	# parameters to override if necessary
175	root = os.getcwd()                              # the directory to serve files from
176	index_files = ['index.cgi','index.html']        # index files for directories
177	logging = True                                  # print logging info for each request ?
178	blocksize = 2 << 16                             # size of blocks to read from files and send
179
180	def request_complete(self):
181		"""In the HTTP protocol, a request is complete if the "end of headers"
182		sequence ('\r\n\r\n') has been received
183		If the request is POST, stores the request body in a StringIO before
184		returning True"""
185		terminator = self.incoming.find('\r\n\r\n')
186		if terminator == -1:
187			return False
188		lines = self.incoming[:terminator].split('\r\n')
189		self.requestline = lines[0]
190		try:
191			self.method,self.url,self.protocol = lines[0].strip().split()
192			if not self.protocol.startswith("HTTP/1") or (self.protocol[7]!='0' and self.protocol[7]!='1') or len(self.protocol)!=8:
193				self.method = None
194				self.protocol = "HTTP/1.1"
195				return True
196		except:
197			self.method = None
198			self.protocol = "HTTP/1.1"
199			return True
200		# put request headers in a dictionary
201		self.headers = {}
202		for line in lines[1:]:
203			k,v = line.split(':',1)
204			self.headers[k.lower().strip()] = v.strip()
205		# persistent connection
206		close_conn = self.headers.get("connection","")
207		if (self.protocol == "HTTP/1.1" and close_conn.lower() == "keep-alive"):
208			self.close_when_done = False
209		# parse the url
210		scheme,netloc,path,params,query,fragment = urlparse.urlparse(self.url)
211		self.path,self.rest = path,(params,query,fragment)
212
213		if self.method == 'POST':
214			# for POST requests, read the request body
215			# its length must be specified in the content-length header
216			content_length = int(self.headers.get('content-length',0))
217			body = self.incoming[terminator+4:]
218			# request is incomplete if not all message body received
219			if len(body)<content_length:
220				return False
221			f_body = cStringIO.StringIO(body)
222			f_body.seek(0)
223			sys.stdin = f_body # compatibility with CGI
224
225		return True
226
227	def make_response(self):
228		try:
229			"""Build the response : a list of strings or files"""
230			if self.method is None: # bad request
231				return self.err_resp(400,'Bad request : %s' %self.requestline)
232			resp_headers, resp_body, resp_file = '','',None
233			if not self.method in ['GET','POST','HEAD']:
234				return self.err_resp(501,'Unsupported method (%s)' %self.method)
235			else:
236				file_name = self.file_name = self.translate_path()
237				if not file_name.startswith(HTTP.root+os.path.sep) and not file_name==HTTP.root:
238					return self.err_resp(403,'Forbidden')
239				elif not os.path.exists(file_name):
240					return self.err_resp(404,'File not found')
241				elif self.managed():
242					response = self.mngt_method()
243				elif not os.access(file_name,os.R_OK):
244					return self.err_resp(403,'Forbidden')
245				else:
246					fstatdata = os.stat(file_name)
247					if (fstatdata.st_mode & 0170000) == 0040000:    # directory
248						for index in self.index_files:
249							if os.path.exists(file_name+'/'+index) and os.access(file_name+'/'+index,os.R_OK):
250								return self.redirect_resp(index)
251					if (fstatdata.st_mode & 0170000) != 0100000:
252						return self.err_resp(403,'Forbidden')
253					ext = os.path.splitext(file_name)[1]
254					c_type = mimetypes.types_map.get(ext,'text/plain')
255					resp_line = "%s 200 Ok\r\n" %self.protocol
256					size = fstatdata.st_size
257					resp_headers = "Content-Type: %s\r\n" %c_type
258					resp_headers += "Content-Length: %s\r\n" %size
259					resp_headers += '\r\n'
260					if self.method == "HEAD":
261						resp_string = resp_line + resp_headers
262					elif size > HTTP.blocksize:
263						resp_string = resp_line + resp_headers
264						resp_file = open(file_name,'rb')
265					else:
266						resp_string = resp_line + resp_headers + \
267							open(file_name,'rb').read()
268					response = [resp_string]
269					if resp_file:
270						response.append(resp_file)
271			self.log(200)
272			return response
273		except:
274			return self.err_resp(500,'Internal Server Error')
275
276	def translate_path(self):
277		"""Translate URL path into a path in the file system"""
278		return os.path.realpath(os.path.join(HTTP.root,*self.path.split('/')))
279
280	def managed(self):
281		"""Test if the request can be processed by a specific method
282		If so, set self.mngt_method to the method used
283		This implementation tests if the script is in a cgi directory"""
284		if self.is_cgi():
285			self.mngt_method = self.run_cgi
286			return True
287		return False
288
289	def is_cgi(self):
290		"""Test if url points to cgi script"""
291		if self.path.endswith(".cgi"):
292			return True
293		return False
294
295	def run_cgi(self):
296		if not os.access(self.file_name,os.X_OK):
297			return self.err_resp(403,'Forbidden')
298		# set CGI environment variables
299		self.make_cgi_env()
300		# redirect print statements to a cStringIO
301		save_stdout = sys.stdout
302		output_buffer = cStringIO.StringIO()
303		sys.stdout = output_buffer
304		# run the script
305		try:
306			execfile(self.file_name, {})
307		except SystemExit:
308			pass
309		except:
310			output_buffer = cStringIO.StringIO()
311			output_buffer.write("Content-type:text/plain\r\n\r\n")
312			traceback.print_exc(file=output_buffer)
313		sys.stdout = save_stdout # restore sys.stdout
314		response = output_buffer.getvalue()
315		if self.method == "HEAD":
316			# for HEAD request, don't send message body even if the script
317			# returns one (RFC 3875)
318			head_lines = []
319			for line in response.split('\n'):
320				if not line:
321					break
322				head_lines.append(line)
323			response = '\n'.join(head_lines)
324		# close connection in case there is no content-length header
325		self.close_when_done = True
326		resp_line = "%s 200 Ok\r\n" %self.protocol
327		return [resp_line + response]
328
329	def make_cgi_env(self):
330		"""Set CGI environment variables"""
331		env = {}
332		env['SERVER_SOFTWARE'] = "AsyncServer"
333		env['SERVER_NAME'] = "AsyncServer"
334		env['GATEWAY_INTERFACE'] = 'CGI/1.1'
335		env['DOCUMENT_ROOT'] = HTTP.root
336		env['SERVER_PROTOCOL'] = "HTTP/1.1"
337		env['SERVER_PORT'] = str(self.server.port)
338
339		env['REQUEST_METHOD'] = self.method
340		env['REQUEST_URI'] = self.url
341		env['PATH_TRANSLATED'] = self.translate_path()
342		env['SCRIPT_NAME'] = self.path
343		env['PATH_INFO'] = urlparse.urlunparse(("","","",self.rest[0],"",""))
344		env['QUERY_STRING'] = self.rest[1]
345		if not self.host == self.client_address[0]:
346			env['REMOTE_HOST'] = self.host
347		env['REMOTE_ADDR'] = self.client_address[0]
348		env['CONTENT_LENGTH'] = str(self.headers.get('content-length',''))
349		for k in ['USER_AGENT','COOKIE','ACCEPT','ACCEPT_CHARSET',
350			'ACCEPT_ENCODING','ACCEPT_LANGUAGE','CONNECTION']:
351			hdr = k.lower().replace("_","-")
352			env['HTTP_%s' %k.upper()] = str(self.headers.get(hdr,''))
353		os.environ.update(env)
354
355	def redirect_resp(self,redirurl):
356		"""Return redirect message"""
357		resp_line = "%s 301 Moved Permanently\r\nLocation: %s\r\n" % (self.protocol,redirurl)
358		self.close_when_done = True
359		self.log(301)
360		return [resp_line]
361
362	def err_resp(self,code,msg):
363		"""Return an error message"""
364		resp_line = "%s %s %s\r\n" %(self.protocol,code,msg)
365		self.close_when_done = True
366		self.log(code)
367		return [resp_line]
368
369	def log(self,code):
370		"""Write a trace of the request on stderr"""
371		if HTTP.logging:
372			date_str = datetime.datetime.now().strftime('[%d/%b/%Y %H:%M:%S]')
373			sys.stderr.write('%s - - %s "%s" %s\n' %(self.host,date_str,self.requestline,code))
374
375
376# =======================================================================
377# exit_err function. Exits with error code.
378# =======================================================================
379def exit_err(msg):
380	sys.stderr.write(msg)
381	exit(1)
382
383
384# =======================================================================
385# fork function. Calls fork and exits from parent process.
386# =======================================================================
387def fork():
388	try:
389		pid = os.fork()
390		if pid > 0:
391			sys.exit(0)
392	except OSError as e:
393		exit_err("fork failed: %d (%s)" % (e.errno, e.strerror))
394
395
396# =======================================================================
397# daemonize function. Sends current process to background and manages pidfile.
398# =======================================================================
399def daemonize(pidfile, user=None):
400	# open pidfile descriptor
401	try:
402		pidf = open(pidfile, 'w+')
403	except IOError as e:
404		exit_err("could not open pidfile for writing: %s" % pidfile)
405
406	# change user from root to custom
407	if user:
408		_, _, uid, gid, _, _, _ = pwd.getpwnam(user)
409		os.setgid(gid)
410		os.setuid(uid)
411
412	# flush output buffers before forking to avoid printing something twice
413	sys.stdout.flush()
414	sys.stderr.flush()
415
416	# do first fork
417	fork()
418
419	# decouple from parent environment
420	os.chdir("/")
421	os.setsid()
422	os.umask(0)
423
424	# do second fork
425	fork()
426
427	# redirect standard file descriptors
428	nullin = open('/dev/null', 'r')
429	nullout = open('/dev/null', 'a+')
430	os.dup2(nullin.fileno(), sys.stdin.fileno())
431	os.dup2(nullout.fileno(), sys.stdout.fileno())
432	os.dup2(nullout.fileno(), sys.stderr.fileno())
433
434	# write pidfile
435	pidf.write("%d\n" % os.getpid())
436	pidf.close()
437
438
439if __name__=="__main__":
440	verbose = False
441	host = 'any'
442	port = 9425
443	rootpath="@CGI_PATH@"
444	pidfile = None
445	user = None
446
447	opts,args = getopt.getopt(sys.argv[1:],"vhH:P:R:p:u:")
448	for opt, val in opts:
449		if opt == '-h':
450			print "usage: %s [-H bind_host] [-P bind_port] [-R rootpath] [-v]\n" % sys.argv[0]
451			print "-H bind_host : local address to listen on (default: any)"
452			print "-P bind_port : port to listen on (default: 9425)"
453			print "-R rootpath : local path to use as HTTP document root (default: @CGI_PATH@)"
454			print "-v : log requests on stderr"
455			print "-p : pidfile path, setting it triggers manual daemonization"
456			print "-u : username of server owner, used in manual daemonization"
457			sys.exit(0)
458		elif opt == '-H':
459			host = val
460		elif opt == '-P':
461			port = int(val)
462		elif opt == '-R':
463			rootpath = val
464		elif opt == '-v':
465			verbose = True
466		elif opt == '-p':
467			pidfile = val
468		elif opt == '-u':
469			user = val
470
471	# launch the server on the specified port
472	server = Server(host, port)
473	if host != 'any':
474		print "Asynchronous HTTP server running on %s:%s" % (host,port)
475	else:
476		print "Asynchronous HTTP server running on port %s" % port
477	if verbose:
478		HTTP.logging = True
479	else:
480		HTTP.logging = False
481	HTTP.root = os.path.realpath(rootpath)
482	if pidfile:
483		daemonize(pidfile, user)
484	loop(server, HTTP)
485