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