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