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