import http.server import http.cookies import socketserver import logging import os.path import posixpath import urllib from collections import namedtuple # Logging logger = logging.getLogger(__name__) # Hard-coded default error message DEFAULT_ERROR_MESSAGE = """\ %(code)d - %(message)s

%(message)s

%(explain)s
""" class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): """ This request handler routes requests to a specialised handler. Handling a request is roughly done in two steps: 1) Requests are first passed through matching registered filters 2) Request is passed to the matching handler. Responsibility for selecting the handler is left to the server class. """ error_message_format = DEFAULT_ERROR_MESSAGE server_version = 'EcaHTTP/2' default_request_version = 'HTTP/1.1' def send_header(self, key, value): """Buffer headers until they can be sent.""" if not self.response_sent: if not hasattr(self, '_cached_headers'): self._cached_headers = [] self._cached_headers.append((key,value)) else: super().send_header(key, value) def send_response(self, *args, **kwargs): """Sends the necessary response, and appends buffered headers.""" super().send_response(*args, **kwargs) self.response_sent = True if hasattr(self, '_cached_headers'): for h in self._cached_headers: self.send_header(*h) self._cached_headers = [] def dispatch(self): """Dispatch incoming requests.""" self.handler = None self.response_sent = False # the method we will be looking for # (uses HTTP method name to build Python method name) method_name = "handle_{}".format(self.command) # let server determine specialised handler factory, and call it handler_factory = self.server.get_handler(self.command, self.path) if not handler_factory: self.send_error(404) return # instantiate handler self.handler = handler_factory(self) # check for necessary HTTP method if not hasattr(self.handler, method_name): self.send_error(501, "Unsupported method ({})".format(self.command)) return # apply filters to request # note: filters are applied in order of registration for filter_factory in self.server.get_filters(self.command, self.path): filter = filter_factory(self) if not hasattr(filter, method_name): self.send_error(501, "Unsupported method ({})".format(self.command)) return filter_method = getattr(filter, method_name) filter_method() # select and invoke actual method method = getattr(self.handler, method_name) method() def translate_path(self, path): """ Translate a /-separated PATH to the local filename syntax. This method is unelegantly 'borrowed' from SimpleHTTPServer.py to change the original so that it has the `path = self.server.static_path' line. """ # abandon query parameters path = path.split('?',1)[0] path = path.split('#',1)[0] path = path[len(self.url_path):] # Don't forget explicit trailing slash when normalizing. Issue17324 trailing_slash = path.rstrip().endswith('/') path = posixpath.normpath(urllib.parse.unquote(path)) words = path.split('/') words = filter(None, words) # server content from static_path, instead of os.getcwd() path = self.local_path for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) if trailing_slash: path += '/' return path # Standard HTTP verbs bound to dispatch method def do_GET(self): self.dispatch() def do_POST(self): self.dispatch() def do_PUT(self): self.dispatch() def do_DELETE(self): self.dispatch() def do_HEAD(self): self.dispatch() # Fallback handlers for static content # (These invoke the original SimpleHTTPRequestHandler behaviour) def handle_GET(self): super().do_GET() def handle_HEAD(self): super().do_HEAD() # handle logging def _log_data(self): path = getattr(self, 'path','') command = getattr(self, 'command', '') return { 'address': self.client_address, 'location': path, 'method': command } def _get_message_format(self, format, args): log_data = self._log_data() message_format = "[{}, {} {}] {}".format(self.client_address[0], log_data['method'], log_data['location'], format%args) return message_format #overload logging methods def log_message(self, format, *args): logger.debug(self._get_message_format(format, args), extra=self._log_data()) def log_error(self, format, *args): logger.warn(self._get_message_format(format, args), extra=self._log_data()) HandlerRegistration = namedtuple('HandlerRegistration',['methods','path','handler']) class HTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): """ HTTP Server with path/method registration functionality to allow simple configuration of served content. """ def __init__(self, server_address, RequestHandlerClass=HTTPRequestHandler): self.handlers = [] self.filters = [] super().__init__(server_address, RequestHandlerClass) def get_handler(self, method, path): """Selects the best matching handler.""" # Select handlers for the given method, that match any path or a prefix of the given path matches = [m for m in self.handlers if (not m.methods or method in m.methods) and path.startswith(m.path)] # if there are matches, we select the one with the longest matching prefix if matches: best = max(matches, key=lambda e: len(e.path)) return best.handler else: return None def get_filters(self, method, path): """Selects all applicable filters.""" # Select all filters that the given method, that match any path or a suffix of the given path return [f.handler for f in self.filters if (not f.methods or method in f.methods) and path.startswith(f.path)] def _log_registration(self, kind, registration): message_format = "Adding HTTP request {} {} for ({} {})" message = message_format.format(kind, registration.handler, registration.methods, registration.path) logger.info(message) def add_route(self, path, handler_factory, methods=["GET"]): """ Adds a request handler to the server. The handler can be specialised in in or more request methods by providing a comma separated list of methods. Handlers are matched longest-matching-prefix with regards to paths. """ reg = HandlerRegistration(methods, path, handler_factory) self._log_registration('handler', reg) self.handlers.append(reg) def add_content(self, path, local_path, methods=['GET','HEAD']): """ Adds a StaticContent handler to the server. This method is shorthand for self.add_route(path, StaticContent(path, local_path), methods) """ if not path.endswith('/'): logger.warn("Static content configured without trailing '/'. "+ "This is different from traditional behaviour.") logger.info("Serving static content for {} under '{}' from '{}'".format(methods,path,local_path)) self.add_route(path, StaticContent(path, local_path), methods) def add_filter(self, path, filter_factory, methods=[]): """ Adds a filter to the server. Like handlers, filters can be specialised on in or more request methods by providing a comma-separated list of methods. Filters are selected on match prefix with regards to paths. Filters are applied in order of registration. """ reg = HandlerRegistration(methods, path, filter_factory) self._log_registration('filter', reg) self.filters.append(reg) def serve_forever(self): logger.info("Server is running...") super().serve_forever() class Handler: """ Handler base class. """ def __init__(self, request): self.request = request class Filter(Handler): """ Filter base class that does nothing. """ def handle_GET(self): self.handle() def handle_POST(self): self.handle() def handle_HEAD(self): self.handle() def handle(self): pass # static content handler defined here, because of intrinsice coupling with # request handler. def StaticContent(url_path, local_path): class StaticContent(Handler): """ Explicit fallback handler. """ def set_paths(self): self.request.local_path = local_path self.request.url_path = url_path def handle_GET(self): self.set_paths() self.request.handle_GET() def handle_HEAD(self): self.set_paths() self.request.handle_HEAD() # return class so that it can be constructed return StaticContent