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