Files
twitter-project/eca/httpd.py

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