327 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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 = """\
 | |
| <!DOCTYPE html>
 | |
| <html>
 | |
| <head>
 | |
| <style type="text/css">
 | |
| * { /* Reset the worst style breakers */
 | |
|     padding: 0;
 | |
|     margin: 0;
 | |
| }
 | |
| 
 | |
| html { /* We always want at least this height */
 | |
|     min-height: 100%%;
 | |
| }
 | |
| 
 | |
| body#error {
 | |
|     font-family: sans-serif;
 | |
|     height: 100%%;
 | |
|     background: #3378c6;
 | |
|     background: -webkit-radial-gradient(center, ellipse cover, #3378c6 0%%,#23538a 100%%);
 | |
|     background: radial-gradient(ellipse at center, #3378c6 0%%,#23538a 100%%);
 | |
|     background-size: 100%% 100%%;
 | |
|     background-repeat: no-repeat;
 | |
| }
 | |
| 
 | |
| #error #message {
 | |
|     position: absolute;
 | |
|     width: 34em;
 | |
|     height: 4em;
 | |
| 
 | |
|     top: 50%%;
 | |
|     margin-top: -2em;
 | |
|     left: 50%%;
 | |
|     margin-left: -17em;
 | |
| 
 | |
|     text-align: center;
 | |
|     color: #114;
 | |
|     text-shadow: 0 1px 0 #88d;
 | |
| }
 | |
| </style>
 | |
| <title>%(code)d - %(message)s</title>
 | |
| </head>
 | |
| <body id='error'>
 | |
| <div id='message'>
 | |
| <h1>%(message)s</h1>
 | |
| <span>%(explain)s</span>
 | |
| </div>
 | |
| </body>
 | |
| </html>
 | |
| """
 | |
| 
 | |
| 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','<unknown path>')
 | |
|         command = getattr(self, 'command', '<unknown 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
 | |
| 
 |