################################################## # SPYCE - Python-based HTML Scripting # Copyright (c) 2002 Rimon Barr. # # Refer to spyce.py # CVS: $Id: spyceWWW.py 1159 2006-08-16 23:33:19Z ellisj $ ################################################## import sys, os, string, socket, BaseHTTPServer, SocketServer, cgi, stat, time, threading import spyce, spyceException, spyceCmd, spyceUtil, spycePreload import pcqueue __doc__ = '''Standalone Spyce web server.''' LOG = 1 def formatBytes(bytes): bytes = float(bytes) if bytes<=9999: return "%6.0f" % bytes bytes = bytes / float(1024) if bytes<=999: return "%5.1fK" % bytes bytes = bytes / float(1024) return "%5.1fM" % bytes ################################################## # Request / response handlers # class spyceHTTPRequest(spyce.spyceRequest): 'HTTP Spyce request object. (see spyce.spyceRequest)' def __init__(self, httpdHandler, documentRoot): spyce.spyceRequest.__init__(self) self._in = httpdHandler.rfile self._headers = httpdHandler.headers self._httpdHandler = httpdHandler self._documentRoot = documentRoot self._env = None def env(self, name=None): if not self._env: self._env = { 'REMOTE_ADDR': self._httpdHandler.client_address[0], 'REMOTE_PORT': self._httpdHandler.client_address[1], 'GATEWAY_INTERFACE': "CGI/1.1", 'REQUEST_METHOD': self._httpdHandler.command, 'REQUEST_URI': self._httpdHandler.path, 'PATH_INFO': self._httpdHandler.pathinfo, 'SERVER_SOFTWARE': 'spyce/%s' % spyce.__version__, 'SERVER_PROTOCOL': self._httpdHandler.request_version, 'DOCUMENT_ROOT': self._documentRoot, 'QUERY_STRING': string.join(string.split(self._httpdHandler.path, '?')[1:]) or '', 'CONTENT_LENGTH': self.getHeader('Content-Length'), 'CONTENT_TYPE': self.getHeader('Content-type'), 'HTTP_USER_AGENT': self.getHeader('User-Agent'), 'HTTP_ACCEPT': self.getHeader('Accept'), 'HTTP_ACCEPT_ENCODING': self.getHeader('Accept-Encoding'), 'HTTP_ACCEPT_LANGUAGE': self.getHeader('Accept-Language'), 'HTTP_ACCEPT_CHARSET': self.getHeader('Accept-Charset'), 'HTTP_COOKIE': self.getHeader('Cookie'), 'HTTP_REFERER': self.getHeader('Referer'), 'HTTP_HOST': self.getHeader('Host'), 'HTTP_CONNECTION': self.getHeader('Connection'), 'HTTP_KEEP_ALIVE': self.getHeader('Keep-alive'), # From ASP # AUTH_TYPE, # APPL_PHYSICAL_PATH, # REMOTE_HOST, # SERVER_PROTOCOL, # SERVER_SOFWARE } return spyceUtil.extractValue(self._env, name) def getHeader(self, type=None): if type: type=string.lower(type) return spyceUtil.extractValue(self._headers.dict, type) def getServerID(self): return os.getpid() class spyceHTTPResponse(spyceCmd.spyceCmdlineResponse): 'HTTP Spyce response object. (see spyce.spyceResponse)' def __init__(self, httpdHandler): self._httpheader = httpdHandler.request_version!='HTTP/0.9' spyceCmd.spyceCmdlineResponse.__init__(self, spyceUtil.NoCloseOut(httpdHandler.wfile), sys.stdout, self._httpheader) self._httpdHandler = httpdHandler # incidentally, this a rather expensive operation! if LOG: httpdHandler.log_request() def sendHeaders(self): if self._httpheader and not self.headersSent: resultText = self.RETURN_CODE.get(self.returncode) self.origout.write('%s %s %s\n' % (self._httpdHandler.request_version, self.returncode, resultText)) spyceCmd.spyceCmdlineResponse.sendHeaders(self) def close(self): spyceCmd.spyceCmdlineResponse.close(self) self._httpdHandler.request.close() ################################################## # Spyce web server # class myHTTPhandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_GET(self): if spyce.getServer().config.check_modules_and_restart: L = spyceUtil.scan_modules() if L: self.send_response(200) self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write("Detected changed modules %s. Restarting...\n(If you don't want Spyce to do this, turn off check_modules_and_restart in your spyceconf file.)" % L) self.request.close() os._exit(3) try: start = time.time() try: # parse pathinfo pathinfo = os.path.normpath(string.split(self.path, '?')[0]) while pathinfo and (pathinfo[0]==os.sep or pathinfo[0:2]==os.pardir): if pathinfo[0:len(os.sep)]==os.sep: pathinfo=pathinfo[len(os.sep):] if pathinfo[0:len(os.pardir)]==os.pardir: pathinfo=pathinfo[len(os.pardir):] self.pathinfo = "/"+pathinfo # convert to path path = os.path.join(self.server.documentRoot, pathinfo) if os.path.exists(path): if os.path.isdir(path): # directory listing pathparts = self.path.split('?', 1) if not pathparts[0].endswith('/'): self.send_response(spyce.spyceResponse.RETURN_MOVED_PERMANENTLY) self.send_header('Location', '?'.join([pathparts[0] + '/'] + pathparts[1:])) self.end_headers() return # check for index.spy, index.html, etc. indexFile = None for findex in spyce.getServer().config.indexFiles: p2 = os.path.join(path, findex) if os.path.exists(p2): indexFile = p2; break if indexFile is None: handler_type = '/' else: path = indexFile handler_type = os.path.splitext(path)[-1][1:] else: # regular files handler_type = os.path.splitext(path)[-1][1:] try: handler = self.server.handler[handler_type] except KeyError: handler = self.server.handler[None] else: # force 404s to spyce handler so error page can be customized handler = self.__class__.handler_spyce if spyce.DEBUG_ERROR: sys.stderr.write('handler is %s\n' % str(handler)) # process request return handler(self, path) except IOError: self.send_error(404, "Unexpected IOError") return None finally: spyce.DEBUG('total for %s was %s' % (self.path, time.time() - start)) do_POST=do_GET def handler_spyce(self, path): # process spyce request = spyceHTTPRequest(self, self.server.documentRoot) response = spyceHTTPResponse(self) result = spyce.spyceFileHandler(request, response, path) response.close() def handler_dump(self, path): # process content to dump (with correct mime type) f = None try: f = open(path, 'rb') try: _, ext = os.path.splitext(path) if ext: ext=ext[1:] mimetype = self.server.mimeTable[ext] except: mimetype = "application/octet-stream" self.send_response(200) self.send_header("Content-type", mimetype) self.end_headers() self.wfile.write(f.read()) self.request.close() finally: try: if f: f.close() except: pass def handler_directory(self, path): # process directory if(self.path[-1:]!='/'): self.send_response(301) self.send_header('Location', self.path+'/') self.end_headers() return L = os.listdir(path) L.sort(lambda a, b: cmp(a.lower(), b.lower())) def info(name, path=path): fullname = os.path.join(path, name) displayname = linkname = name = cgi.escape(name) # Append / for directories or @ for symbolic links if os.path.isdir(fullname): displayname = name + "/" linkname = name + "/" elif os.path.islink(fullname): displayname = name + "@" statinfo = os.stat(fullname) mtime = statinfo[stat.ST_MTIME] size = statinfo[stat.ST_SIZE] return linkname, displayname, mtime, size L = map(info, L) NAME_WIDTH = 30 output = ''' Index of %(title)s

Index of %(title)s

 Name%(filler)s  Date%(filler_date)s  Size
''' % { 'title' : self.pathinfo.replace(os.path.sep, '/'), 'filler': ' '*(NAME_WIDTH-len('Name')), 'filler_date': ' '*(len(time.asctime(time.localtime(0)))-len('Date')), } if L: for link, display, mtime, size in L: output = output + ' %(display)s%(filler)s %(mtime)s %(size)s\n' % { 'link': link, 'display': display[:NAME_WIDTH], 'link': link, 'filler': ' '*(NAME_WIDTH-len(display)), 'mtime': time.asctime(time.localtime(mtime)), 'size': formatBytes(size), } else: output = output + 'No files\n' output = output[:-1] + '''
Spyce-WWW/%(version)s server
''' % { 'version' : spyce.__version__, } self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(output) def buildMimeTable(files): mimetable = {} for file in files: try: f = None try: f = open(file, 'r') line = f.readline() while line: if line[0]=='#': line = f.readline(); continue line = string.strip(line) if not line: line = f.readline(); continue line = string.replace(line, '\t', ' ') items = filter(None, map(string.strip, string.split(line, ' '))) mimetype, extensions = items[0], items[1:] for ext in extensions: mimetable[ext] = mimetype line = f.readline() except IOError: pass finally: try: if f: f.close() except: pass return mimetable def buildHandlerTable(handler, server): for ext in handler.keys(): handler[ext] = eval('server.handler_'+handler[ext]) if spyce.DEBUG_ERROR: sys.stderr.write('handlers are ' + str(handler) + '\n') return handler class ThreadPooledTcpServer(SocketServer.TCPServer): allow_reuse_address = True def __init__(self, minthreads, maxthreads, qsize, *args, **kwargs): SocketServer.TCPServer.__init__(self, *args, **kwargs) self.queue = pcqueue.PCQueue(minthreads, maxthreads, qsize) self.queue.consume = self.consume def consume(self, item): request, client_address = item try: self.finish_request(request, client_address) self.close_request(request) except: self.handle_error(request, client_address) self.close_request(request) def process_request(self, request, client_address): self.queue.put((request, client_address)) def spyceHTTPserver(configFile, daemon=None): stdout = sys.stdout os.environ[spyce.SPYCE_ENTRY] = 'www' config = spycePreload.getConfigModule(configFile) if config.minthreads <= 0: raise ValueError('minthreads must be at least 1') if config.maxthreads < config.minthreads: raise ValueError('maxthreads must be at least equal to minthreads') # initialize mtime cache spyceUtil.scan_modules() # (_and_restart code inspired by WSGIkit) if config.check_modules_and_restart: reloader_environ_key = 'SPYCE_RELOADER_SHOULD_RUN' if os.environ.get(reloader_environ_key): stdout.write("# Running reloading file monitor (for check_modules_and_restart option)\n") def exit_if_modules_changed(): L = spyceUtil.scan_modules() if L: stdout.write("# (Detected changed modules %s. Restarting...\n\n(If you don't want Spyce to do this, turn off check_modules_and_restart in your spyceconf file.)\n" % L) os._exit(3) import scheduler scheduler.schedule(1, exit_if_modules_changed) else: args = spyceUtil.argsForSpawn([sys.executable, "-u"] + sys.argv) while 1: stdout.write("# Spawning another server (for check_modules_and_restart option)\n") new_environ = os.environ.copy() new_environ[reloader_environ_key] = 'true' pid = os.spawnve(os.P_NOWAIT, sys.executable, args, new_environ) # waitpid ignores keyboardinterrupt until the process it's waiting on finishes; # the threading here is a workaround for that exit_wrapper = [None] def run(): exit_wrapper[0] = os.waitpid(pid, 0) th = threading.Thread(target=run) th.setDaemon(True) th.start() try: while th.isAlive(): time.sleep(0.1) except (SystemExit, KeyboardInterrupt), e: # try to clean up child process too try: import signal os.kill(pid, signal.SIGTERM) except (ImportError, AttributeError): try: import win32api except ImportError: print "Unable to terminate child process during shutdown!\nPlease download and install the win32all package so this will work." else: win32api.TerminateProcess(pid, -1) raise e exit_code = exit_wrapper[0][1] spyce.DEBUG('RAW EXIT CODE %s' % (exit_code,)) exit_code = exit_code >> 8 # (3 == "changes detected, restart me") if exit_code != 3: return exit_code print '# Starting Spyce web server. v%s' % spyce.__version__ try: print '# Configuration - %s' % configFile server = spyce.getServer(config) except (spyceException.spyceForbidden, spyceException.spyceNotFound), e: print e return try: # initialize server try: httpd = ThreadPooledTcpServer(config.minthreads, config.maxthreads, config.maxqueuesize, (config.ipaddr, config.port), myHTTPhandler) print '# Listening on - %s:%d' % (config.ipaddr, config.port) httpd.documentRoot = os.path.abspath(config.root) print '# Document root - '+httpd.documentRoot httpd.mimeTable = buildMimeTable(config.mime) httpd.handler = buildHandlerTable(config.www_handlers, myHTTPhandler) if config.adminport: import spyceConsole spyceConsole.start_console_thread(config.adminport) print '# Admin console listening on port %d' % config.adminport except (SystemExit, KeyboardInterrupt): raise except: print 'Unable to start server on port %s' % config.port print spyceUtil.exceptionString() return -1 # daemonize if daemon: print '# Daemonizing process.' try: spyceCmd.daemonize(pidfile=daemon) except SystemExit: return 0 # expected global LOG LOG = 0 # process requests print '# Ready.' while 1: try: httpd.handle_request() except (SystemExit, KeyboardInterrupt): raise except: stdout.write('Error: %s\n' % spyceUtil.exceptionString()) except KeyboardInterrupt: print 'Break!' httpd.queue.join() return 0