1#!@PYTHON@
2
3import fcntl
4import errno
5import posix
6import time
7import signal
8import os
9import sys
10import getopt
11import traceback
12import datetime
13import mimetypes
14try:
15	from urllib.parse import urlparse
16	from urllib.parse import urlunparse
17except ImportError:
18	from urlparse import urlparse
19	from urlparse import urlunparse
20import socket
21import select
22import subprocess
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
31def emptybuff():
32	if sys.version<'3':
33		return ''
34	else:
35		return bytes(0)
36
37if sys.version<'3':
38	buff_type = str
39else:
40	buff_type = bytes
41
42# =======================================================================
43# The server class. Creating an instance starts a server on the specified
44# host and port
45# =======================================================================
46class Server:
47	def __init__(self,host='localhost',port=80):
48		if host=='any':
49			host=''
50		self.host,self.port = host,port
51		self.socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
52		self.socket.setblocking(0)
53		self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
54		self.socket.bind((host,port))
55		self.socket.listen(50)
56
57# =====================================================================
58# Generic client handler. An instance of this class is created for each
59# request sent by a client to the server
60# =====================================================================
61class ClientHandler:
62	blocksize = 2048
63
64	def __init__(self, server, client_socket, client_address):
65		self.server = server
66		self.client_address = client_address
67		self.client_socket = client_socket
68		self.client_socket.setblocking(0)
69		self.host = socket.getfqdn(client_address[0])
70		self.incoming = emptybuff() # receives incoming data
71		self.outgoing = emptybuff()
72		self.writable = False
73		self.close_when_done = True
74
75	def handle_error(self):
76		self.close()
77
78	def handle_read(self):
79		"""Reads the data received"""
80		try:
81			buff = self.client_socket.recv(1024)
82			if not buff:  # the connection is closed
83				self.close()
84			# buffer the data in self.incoming
85			self.incoming += buff #.write(buff)
86			self.process_incoming()
87		except socket.error:
88			self.close()
89
90	def process_incoming(self):
91		"""Test if request is complete ; if so, build the response
92		and set self.writable to True"""
93		if not self.request_complete():
94			return
95		self.response = self.make_response()
96		self.outgoing = emptybuff()
97		self.writable = True
98
99	def request_complete(self):
100		"""Return True if the request is complete, False otherwise
101		Override this method in subclasses"""
102		return True
103
104	def make_response(self):
105		"""Return the list of strings or file objects whose content will
106		be sent to the client
107		Override this method in subclasses"""
108		return ["xxx"]
109
110	def handle_write(self):
111		"""Send (a part of) the response on the socket
112		Finish the request if the whole response has been sent
113		self.response is a list of strings or file objects
114		"""
115		if len(self.outgoing)==0 and self.response:
116			if isinstance(self.response[0],buff_type):
117				self.outgoing = self.response.pop(0)
118			else:
119				self.outgoing = self.response[0].read(self.blocksize)
120				if not self.outgoing:
121					self.response.pop(0)
122		if self.outgoing:
123			try:
124				sent = self.client_socket.send(self.outgoing)
125			except socket.error:
126				self.close()
127				return
128			if sent < len(self.outgoing):
129				self.outgoing = self.outgoing[sent:]
130			else:
131				self.outgoing = emptybuff()
132		if len(self.outgoing)==0 and not self.response:
133			if self.close_when_done:
134				self.close() # close socket
135			else:
136				# reset for next request
137				self.writable = False
138				self.incoming = emptybuff()
139
140	def close(self):
141		del client_handlers[self.client_socket]
142		self.client_socket.close()
143
144# ============================================================================
145# Main loop, calling the select() function on the sockets to see if new
146# clients are trying to connect, if some clients have sent data and if those
147# for which the response is complete are ready to receive it
148# For each event, call the appropriate method of the server or of the instance
149# of ClientHandler managing the dialog with the client : handle_read() or
150# handle_write()
151# ============================================================================
152def loop(server,handler,timeout=30):
153	while True:
154		k = list(client_handlers.keys())
155		# w = sockets to which there is something to send
156		# we must test if we can send data
157		w = [ cl for cl in client_handlers if client_handlers[cl].writable ]
158		# the heart of the program ! "r" will have the sockets that have sent
159		# data, and the server socket if a new client has tried to connect
160		r,w,e = select.select(k+[server.socket],w,k,timeout)
161		for e_socket in e:
162			client_handlers[e_socket].handle_error()
163		for r_socket in r:
164			if r_socket is server.socket:
165				# server socket readable means a new connection request
166				try:
167					client_socket,client_address = server.socket.accept()
168					client_handlers[client_socket] = handler(server,client_socket,client_address)
169				except socket.error:
170					pass
171			else:
172				# the client connected on r_socket has sent something
173				client_handlers[r_socket].handle_read()
174		w = set(w) & set(client_handlers.keys()) # remove deleted sockets
175		for w_socket in w:
176			client_handlers[w_socket].handle_write()
177
178
179# =============================================================
180# An implementation of the HTTP protocol, supporting persistent
181# connections and CGI
182# =============================================================
183
184class HTTP(ClientHandler):
185	# parameters to override if necessary
186	root = os.getcwd()				# the directory to serve files from
187	index_files = ['index.cgi','index.html']	# index files for directories
188	logging = True					# print logging info for each request ?
189	blocksize = 2 << 16				# size of blocks to read from files and send
190
191	def request_complete(self):
192		"""In the HTTP protocol, a request is complete if the "end of headers"
193		sequence ('\r\n\r\n') has been received
194		If the request is POST, stores the request body in a StringIO before
195		returning True"""
196		term = '\r\n\r\n'
197		if sys.version>='3':
198			term = term.encode('ascii')
199		terminator = self.incoming.find(term)
200		if terminator == -1:
201			return False
202		if sys.version>='3':
203			lines = self.incoming[:terminator].decode('ascii').split('\r\n')
204		else:
205			lines = self.incoming[:terminator].split('\r\n')
206		self.requestline = lines[0]
207		try:
208			self.method,self.url,self.protocol = lines[0].strip().split()
209			if not self.protocol.startswith("HTTP/1") or ( self.protocol[7]!='0' and self.protocol[7]!='1') or len(self.protocol)!=8:
210				self.method = None
211				self.protocol = "HTTP/1.1"
212				self.postbody = None
213				return True
214		except:
215			self.method = None
216			self.protocol = "HTTP/1.1"
217			self.postbody = None
218			return True
219		# put request headers in a dictionary
220		self.headers = {}
221		for line in lines[1:]:
222			k,v = line.split(':',1)
223			self.headers[k.lower().strip()] = v.strip()
224		# persistent connection
225		close_conn = self.headers.get("connection","")
226		if (self.protocol == "HTTP/1.1" and close_conn.lower() == "keep-alive"):
227			self.close_when_done = False
228		# parse the url
229		scheme,netloc,path,params,query,fragment = urlparse(self.url)
230		self.path,self.rest = path,(params,query,fragment)
231
232		if self.method == 'POST':
233			# for POST requests, read the request body
234			# its length must be specified in the content-length header
235			content_length = int(self.headers.get('content-length',0))
236			body = self.incoming[terminator+4:]
237			# request is incomplete if not all message body received
238			if len(body)<content_length:
239				return False
240			self.postbody = body
241		else:
242			self.postbody = None
243
244		return True
245
246	def make_response(self):
247#		try:
248			"""Build the response : a list of strings or files"""
249			if self.method is None: # bad request
250				return self.err_resp(400,'Bad request : %s' %self.requestline)
251			resp_headers, resp_body, resp_file = '','',None
252			if not self.method in ['GET','POST','HEAD']:
253				return self.err_resp(501,'Unsupported method (%s)' %self.method)
254			else:
255				file_name = self.file_name = self.translate_path()
256				if not file_name.startswith(HTTP.root+os.path.sep) and not file_name==HTTP.root:
257					return self.err_resp(403,'Forbidden')
258				elif not os.path.exists(file_name):
259					return self.err_resp(404,'File not found')
260				elif self.managed():
261					response = self.mngt_method()
262				elif not os.access(file_name,os.R_OK):
263					return self.err_resp(403,'Forbidden')
264				else:
265					fstatdata = os.stat(file_name)
266					if (fstatdata.st_mode & 0xF000) == 0x4000:	# directory
267						for index in self.index_files:
268							if os.path.exists(file_name+'/'+index) and os.access(file_name+'/'+index,os.R_OK):
269								return self.redirect_resp(index)
270					if (fstatdata.st_mode & 0xF000) != 0x8000:
271						return self.err_resp(403,'Forbidden')
272					ext = os.path.splitext(file_name)[1]
273					c_type = mimetypes.types_map.get(ext,'text/plain')
274					resp_line = "%s 200 Ok\r\n" %self.protocol
275					size = fstatdata.st_size
276					resp_headers = "Content-Type: %s\r\n" %c_type
277					resp_headers += "Content-Length: %s\r\n" %size
278					resp_headers += '\r\n'
279					if sys.version>='3':
280						resp_line = resp_line.encode('ascii')
281						resp_headers = resp_headers.encode('ascii')
282					if self.method == "HEAD":
283						resp_string = resp_line + resp_headers
284					elif size > HTTP.blocksize:
285						resp_string = resp_line + resp_headers
286						resp_file = open(file_name,'rb')
287					else:
288						resp_string = resp_line + resp_headers + \
289							open(file_name,'rb').read()
290					response = [resp_string]
291					if resp_file:
292						response.append(resp_file)
293			self.log(200)
294			return response
295#		except:
296#			return self.err_resp(500,'Internal Server Error')
297
298	def translate_path(self):
299		"""Translate URL path into a path in the file system"""
300		return os.path.realpath(os.path.join(HTTP.root,*self.path.split('/')))
301
302	def managed(self):
303		"""Test if the request can be processed by a specific method
304		If so, set self.mngt_method to the method used
305		This implementation tests if the script is in a cgi directory"""
306		if self.is_cgi():
307			self.mngt_method = self.run_cgi
308			return True
309		return False
310
311	def is_cgi(self):
312		"""Test if url points to cgi script"""
313		if self.path.endswith(".cgi"):
314			return True
315		return False
316
317	def run_cgi(self):
318		if not os.access(self.file_name,os.X_OK):
319			return self.err_resp(403,'Forbidden')
320		# set CGI environment variables
321		e = self.make_cgi_env()
322		self.close_when_done = True
323		if self.method == "HEAD":
324			try:
325				proc = subprocess.Popen(self.file_name, env=e, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
326				cgiout, cgierr = proc.communicate()
327				response = cgiout + cgierr
328				if sys.version>='3':
329					response = response.decode('latin-1')
330			except:
331				response = "Content-type: text/plain\r\n\r\n" + traceback.format_exc()
332			# for HEAD request, don't send message body even if the script
333			# returns one (RFC 3875)
334			head_lines = []
335			for line in response.split('\n'):
336				if not line:
337					break
338				head_lines.append(line)
339			response = '\n'.join(head_lines)
340			if sys.version>='3':
341				response = response.encode('latin-1')
342			resp_line = "%s 200 Ok\r\n" %self.protocol
343			if sys.version>='3':
344				resp_line = resp_line.encode('ascii')
345			return [resp_line + response]
346		else:
347			try:
348				if self.postbody != None:
349					proc = subprocess.Popen(self.file_name, env=e, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
350					cgiout, cgierr = proc.communicate(self.postbody)
351					response = cgiout + cgierr
352					if sys.version>='3':
353						response = response.decode('latin-1')
354					resp_line = "%s 200 Ok\r\n" %self.protocol
355					if sys.version>='3':
356						resp_line = resp_line.encode('ascii')
357					return [resp_line + response]
358				else:
359					proc = subprocess.Popen(self.file_name, env=e, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
360					resp_line = "%s 200 Ok\r\n" %self.protocol
361					if sys.version>='3':
362						resp_line = resp_line.encode('ascii')
363					return [resp_line,proc.stdout,proc.stderr]
364			except:
365				response = "Content-type: text/plain\r\n\r\n" + traceback.format_exc()
366				resp_line = "%s 200 Ok\r\n" %self.protocol
367				if sys.version>='3':
368					resp_line = resp_line.encode('ascii')
369					response = response.encode('latin-1')
370				return [resp_line + response]
371		# sys.stdout = save_stdout # restore sys.stdout
372		# close connection in case there is no content-length header
373		# resp_line = "%s 200 Ok\r\n" %self.protocol
374		# if sys.version>='3':
375		#	resp_line = resp_line.encode('ascii')
376		# return [resp_line + response]
377
378	def make_cgi_env(self):
379		"""Set CGI environment variables"""
380		env = {}
381		env['SERVER_SOFTWARE'] = "AsyncServer"
382		env['SERVER_NAME'] = "AsyncServer"
383		env['GATEWAY_INTERFACE'] = 'CGI/1.1'
384		env['DOCUMENT_ROOT'] = HTTP.root
385		env['SERVER_PROTOCOL'] = "HTTP/1.1"
386		env['SERVER_PORT'] = str(self.server.port)
387
388		env['REQUEST_METHOD'] = self.method
389		env['REQUEST_URI'] = self.url
390		env['PATH_TRANSLATED'] = self.translate_path()
391		env['SCRIPT_NAME'] = self.path
392		env['PATH_INFO'] = urlunparse(("","","",self.rest[0],"",""))
393		env['QUERY_STRING'] = self.rest[1]
394		if not self.host == self.client_address[0]:
395			env['REMOTE_HOST'] = self.host
396		env['REMOTE_ADDR'] = self.client_address[0]
397		env['CONTENT_LENGTH'] = str(self.headers.get('content-length',''))
398		for k in ['USER_AGENT','COOKIE','ACCEPT','ACCEPT_CHARSET',
399			'ACCEPT_ENCODING','ACCEPT_LANGUAGE','CONNECTION']:
400			hdr = k.lower().replace("_","-")
401			env['HTTP_%s' %k.upper()] = str(self.headers.get(hdr,''))
402		return env
403
404	def redirect_resp(self,redirurl):
405		"""Return redirect message"""
406		resp_line = "%s 301 Moved Permanently\r\nLocation: %s\r\n" % (self.protocol,redirurl)
407		if sys.version>='3':
408			resp_line = resp_line.encode('ascii')
409		self.close_when_done = True
410		self.log(301)
411		return [resp_line]
412
413	def err_resp(self,code,msg):
414		"""Return an error message"""
415		resp_line = "%s %s %s\r\n" %(self.protocol,code,msg)
416		if sys.version>='3':
417			resp_line = resp_line.encode('ascii')
418		self.close_when_done = True
419		self.log(code)
420		return [resp_line]
421
422	def log(self,code):
423		"""Write a trace of the request on stderr"""
424		if HTTP.logging:
425			date_str = datetime.datetime.now().strftime('[%d/%b/%Y %H:%M:%S]')
426			sys.stderr.write('%s - - %s "%s" %s\n' %(self.host,date_str,self.requestline,code))
427
428
429def mylock(filename):
430	try:
431		fd = posix.open(filename,posix.O_RDWR|posix.O_CREAT,438) # 438 = 0o666
432	except IOError:
433		return -1
434	try:
435		fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
436	except IOError:
437		ex = sys.exc_info()[1]
438		if ex.errno != errno.EAGAIN:
439			posix.close(fd)
440			return -1
441		try:
442			pid = int(posix.read(fd,100).strip())
443			posix.close(fd)
444			return pid
445		except ValueError:
446			posix.close(fd)
447			return -2
448	posix.ftruncate(fd,0)
449	if sys.version_info[0]<3:
450		posix.write(fd,"%u" % posix.getpid())
451	else:
452		posix.write(fd,("%u" % posix.getpid()).encode('utf-8'))
453	return 0
454
455def wdlock(fname,runmode,timeout):
456	killed = 0
457	for i in range(timeout):
458		l = mylock(fname)
459		if l==0:
460			if runmode==2:
461				if killed:
462					return 0
463				else:
464					print("can't find process to terminate")
465					return -1
466			if runmode==3:
467				print("mfscgiserv is not running")
468				return 0
469			print("lockfile created and locked")
470			return 1
471		elif l<0:
472			if l<-1:
473				print("lockfile is damaged (can't obtain pid - kill prevoius instance manually)")
474			else:
475				print("lockfile error")
476			return -1
477		else:
478			if runmode==3:
479				print("mfscgiserv pid:%u" % l)
480				return 0
481			if runmode==1:
482				print("can't start: lockfile is already locked by another process")
483				return -1
484			if killed!=l:
485				print("sending SIGTERM to lock owner (pid:%u)" % l)
486				posix.kill(l,signal.SIGTERM)
487				killed = l
488			if (i%10)==0 and i>0:
489				print("about %u seconds passed and lock still exists" % i)
490			time.sleep(1)
491	print("about %u seconds passed and lockfile is still locked - giving up" % timeout)
492	return -1
493
494if __name__=="__main__":
495	locktimeout = 60
496	daemonize = 1
497	verbose = 0
498	host = 'any'
499	port = @DEFAULT_CGISERV_HTTP_PORT@
500	rootpath="%%CGIDIR%%"
501	datapath="%%DATAPATH%%"
502
503	opts,args = getopt.getopt(sys.argv[1:],"hH:P:R:t:fv")
504	for opt,val in opts:
505		if opt=='-h':
506			print("usage: %s [-H bind_host] [-P bind_port] [-R rootpath] [-t locktimeout] [-f [-v]] [start|stop|restart|test]\n" % sys.argv[0])
507			print("-H bind_host : local address to listen on (default: any)\n-P bind_port : port to listen on (default: @DEFAULT_CGISERV_HTTP_PORT@)\n-R rootpath : local path to use as HTTP document root (default: %%CGIDIR%%)\n-t locktimeout : how long to wait for lockfile (default: 60s)\n-f : run in foreground\n-v : log requests on stderr")
508			os._exit(0)
509		elif opt=='-H':
510			host = val
511		elif opt=='-P':
512			port = int(val)
513		elif opt=='-R':
514			rootpath = val
515		elif opt=='t':
516			locktimeout = int(val)
517		elif opt=='-f':
518			daemonize = 0
519		elif opt=='-v':
520			verbose = 1
521
522	lockfname = datapath + os.path.sep + '.mfscgiserv.lock'
523
524	try:
525		mode = args[0]
526		if mode=='start':
527			mode = 1
528		elif mode=='stop':
529			mode = 2
530		elif mode=='test':
531			mode = 3
532		else:
533			mode = 0
534	except:
535		mode = 0
536
537	rootpath = os.path.realpath(rootpath)
538
539	pipefd = posix.pipe()
540
541	if (mode==1 or mode==0) and daemonize:
542# daemonize
543		try:
544			pid = os.fork()
545		except OSError:
546			e = sys.exc_info()[1]
547			raise Exception("fork error: %s [%d]" % (e.strerror, e.errno))
548		if pid>0:
549			posix.read(pipefd[0],1)
550			os._exit(0)
551		os.setsid()
552		try:
553			pid = os.fork()
554		except OSError:
555			e = sys.exc_info()[1]
556			raise Exception("fork error: %s [%d]" % (e.strerror, e.errno))
557			if sys.version_info[0]<3:
558				posix.write(pipefd[1],'0')
559			else:
560				posix.write(pipefd[1],bytes(1))
561		if pid>0:
562			os._exit(0)
563
564	if wdlock(lockfname,mode,locktimeout)==1:
565
566		print("starting simple cgi server (host: %s , port: %u , rootpath: %s)" % (host,port,rootpath))
567
568		if daemonize:
569			os.close(0)
570			os.close(1)
571			os.close(2)
572			if os.open("/dev/null",os.O_RDWR)!=0:
573				raise Exception("can't open /dev/null as 0 descriptor")
574			os.dup2(0,1)
575			os.dup2(0,2)
576
577			if sys.version_info[0]<3:
578				posix.write(pipefd[1],'0')
579			else:
580				posix.write(pipefd[1],bytes(1))
581
582		posix.close(pipefd[0])
583		posix.close(pipefd[1])
584
585		server = Server(host, port)
586
587# launch the server on the specified port
588		if not daemonize:
589			if host!='any':
590				print("Asynchronous HTTP server running on %s:%s" % (host,port))
591			else:
592				print("Asynchronous HTTP server running on port %s" % port)
593		if not daemonize and verbose:
594			HTTP.logging = True
595		else:
596			HTTP.logging = False
597		HTTP.root = rootpath
598		loop(server,HTTP)
599
600	else:
601		if sys.version_info[0]<3:
602			posix.write(pipefd[1],'0')
603		else:
604			posix.write(pipefd[1],bytes(1))
605		os._exit(0)
606