#! /usr/bin/env python """Provide simple support for Java-style servlets. The code in this module provides an incomplete port of Java's API for servlets. Only essential classes and methods are implemented here.""" ################################################################################ __author__ = 'Stephen "Zero" Chappell ' __date__ = '11 February 2010' __version__ = '$Revision: 3 $' ################################################################################ import urllib.parse import socketserver import http.server import webbrowser import socket import cgitb import cgi import sys import io ################################################################################ class HttpServlet(http.server.BaseHTTPRequestHandler): """Provide a base class for implementing a servlet. The HttpServlet class should be inherited by any class that is to be run as a servlet. A few basic HTTP methods are automatically handled by this class and passed on to the service method to be written by the servlet designer. Request and response systems are handled here.""" # These variables properly identify and setup the handler. server_version = 'HttpServlet/' + http.server.__version__ protocol_version = 'HTTP/1.1' __debug = False # Determines how exceptions are reported. @classmethod def debug(cls, value): """Set the debugging status of servlet applications. This determines what happens when code raises an exception. if True: traceback is sent back to the browser for analysis. if False: the browser receives an ambiguous 500 error code.""" cls.__debug = value def do_GET(self): """Handle the GET method sent via HTTP.""" if self.path == '/favicon.ico': self.send_error(404) return self.__call_service(self.path) def do_POST(self): """Handle the POST method sent via HTTP.""" try: length = int(self.headers.get('content-length')) except: self.send_error(411) else: self.__call_service('?' + self.rfile.read(length).decode()) def __call_service(self, query): """Execute service method implemented by child class. When either a GET or POST request is received, this method is called with a string representing the query. Request and response objects are created for the child's service method, and an answer is sent back to the client with errors automatically being caught.""" request = _HttpServletRequest(query) response = _HttpServletResponse() try: self.service(request, response) except Exception: if self.__debug: self.send_response(500) self.send_header('Content-Type', 'text/html') self.send_header('Connection', 'close') self.end_headers() klass, value, trace = sys.exc_info() # The next function call may raise an exception. html = cgitb.html((klass, value, trace.tb_next)) self.wfile.write(html.encode()) else: self.send_error(500) else: response_value = response._value self.send_response(200) self.send_header('Content-Type', response._type) self.send_header('Content-Length', str(len(response_value))) self.end_headers() self.wfile.write(response_value) def service(self, request, response): """Process the client's request and send back a response.""" raise NotImplementedError() ################################################################################ class _HttpServletRequest: """Store the query from the client's request. Instances of this class are automatically created to store query variables obtained by parsing the path or post data sent from the client to the servlet.""" def __init__(self, path): """Initialize the request object from the path.""" query = urllib.parse.urlparse(path).query self.__dict = urllib.parse.parse_qs(query, True) def getParameter(self, name): """Compute the value of the named parameter. The parameter that has the specified name is found, and the first value associated with that name is returned. If the name cannot be found, then None is returned to the caller instead of an exception.""" return self.__dict.get(name, [None])[0] ################################################################################ class _HttpServletResponse: """Allow servlet to send response to client. Servlet child classes have their service methods called with an instance of this class. It allows the responses to be configured for both the type of data and the content of the data being sent.""" def __init__(self): """Initialize a blank, generic response object.""" self.__content_type = 'text/plain' self.__print_writer = _PrintWriter() def setContentType(self, content_type): """Set the type of data being sent to client. This is the argument to the "Content-Type" header sent to the client. By default, it is set to "text/plain" but may be set to whatever is appropriate for the data type being sent. Note the data alterations in the _value property.""" self.__content_type = content_type def getWriter(self): """Produce a writer object for printing. This method returns a PrintWriter object that caches the servlet's response to the client. Once the service method returns, the PrintWriter's value is sent back to the client.""" return self.__print_writer @property def _type(self): """Read-only content-type property for __call_service.""" return self.__content_type @property def _value(self): """Read-only response-value property for __call_service.""" value = self.__print_writer.getvalue() lines = value.replace('\r\n', '\n').replace('\r', '\n') return lines.replace('\n', '\r\n').encode() ################################################################################ class _PrintWriter(io.StringIO): """Cache the response generated for the client. An instance of the class is automatically built by HTTP Servlet Response objects to cache the data being sent back to the client.""" print = io.StringIO.write def println(self, string): """Print a line of data to the internal representation.""" self.write(string + '\r\n') ################################################################################ class HttpServer(socketserver.ThreadingMixIn, http.server.HTTPServer): """Create a server with specified address and handler. A generic web server can be instantiated with this class. It will listen on the address given to its constructor and will use the handler class to process all incoming traffic. Running a server is greatly simplified.""" # We should not be binding to an # address that is already in use. allow_reuse_address = False @classmethod def main(cls, RequestHandlerClass, port=80): """Start server with handler on given port. This static method provides an easy way to start, run, and exit a HttpServer instance. The server will be executed if possible, and the computer's web browser will be directed to the address.""" try: server = cls(('', port), RequestHandlerClass) active = True except socket.error: active = False else: addr, port = server.socket.getsockname() print('Serving HTTP on', addr, 'port', port, '...') finally: port = '' if port == 80 else ':' + str(port) addr = 'http://localhost' + port + '/' webbrowser.open(addr) if active: try: server.serve_forever() except KeyboardInterrupt: print('Keyboard interrupt received: EXITING') finally: server.server_close() def handle_error(self, request, client_address): """Process exceptions raised by the RequestHandlerClass. Overriding this method is necessary for two different reasons: (1) SystemExit exceptions are incorrectly caught otherwise and (2) Socket errors should be silently passed in the server code""" klass, value = sys.exc_info()[:2] if klass is SystemExit: self.__exit = value self._BaseServer__serving = None elif issubclass(klass, socket.error): pass else: super().handle_error(request, client_address) def serve_forever(self, poll_interval=0.5): """Handle all incoming client requests forever. This method has been overridden so that SystemExit exceptions raised in the RequestHandlerClass can be re-raised after being caught in the handle_error method above. This allows servlet code to terminate server execution if so desired or required.""" super().serve_forever(poll_interval) if self._BaseServer__serving is None: raise self.__exit