""" Basic web server / framework. """ from socketserver import ThreadingMixIn from http.server import HTTPServer, BaseHTTPRequestHandler import urllib.parse import cgi class HTTPError(Exception): """ HTTPError may be thrown by routes to indicate HTTP errors such as 404, 301, etc. They are caught by the 'framework' and sent to the client's browser. """ def __init__(self, status_code, msg, headers=None): assert isinstance(status_code, int) assert isinstance(msg, str) if headers is None: headers = {} self.status_code = status_code self.msg = msg self.headers = headers Exception.__init__(self, status_code, msg, headers) class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): """ Base class for multithreaded HTTP servers. """ class RequestHandler(BaseHTTPRequestHandler): """ Basic web server request handler. Handles GET and POST requests. You should inherit from this class and implement h_ methods for handling requests. If no path is set, it dispatches to the 'index' or 'default' method. """ def log_message(self, fmt, *args): # pylint: disable=arguments-differ """Overrides BaseHTTPRequestHandler which logs to the console. We log to our log file instead""" fmt = "{0} {1}" self.scriptform.log.info(fmt.format(self.address_string(), args)) def do_GET(self): # pylint: disable=invalid-name """ Handle a GET request. """ self._call(*self._parse(self.path.lstrip('/'))) def do_POST(self): # pylint: disable=invalid-name """ Handle a POST request. """ form_values = cgi.FieldStorage( fp=self.rfile, headers=self.headers, environ={'REQUEST_METHOD': 'POST'}) self._call(self.path.strip('/'), params={'form_values': form_values}) def _parse(self, reqinfo): """ Parse information from a request. """ url_comp = urllib.parse.urlsplit(reqinfo) path = url_comp.path query_vars = urllib.parse.parse_qs(url_comp.query) # Only return the first value of each query var. E.g. for # "?foo=1&foo=2" return '1'. var_values = dict([(k, v[0]) for k, v in query_vars.items()]) return (path.strip('/'), var_values) def _call(self, path, params): """ Find a method to call on self.app_class based on `path` and call it. The method that's called is in the form 'h_'. If no path was given, it will try to call the 'index' method. If no method could be found but a `default` method exists, it is called. Otherwise 404 is sent. Methods should take care of sending proper headers and content themselves using self.send_response(), self.send_header(), self.end_header() and by writing to self.wfile. """ method_name = 'h_{0}'.format(path) method_cb = None try: if hasattr(self, method_name) and \ callable(getattr(self, method_name)): method_cb = getattr(self, method_name) elif path == '' and hasattr(self, 'index'): method_cb = getattr(self, 'index') elif hasattr(self, 'default'): method_cb = getattr(self, 'default') else: raise HTTPError(404, "Not found") method_cb(**params) except HTTPError as err: # HTTP erors are generally thrown by the webapp on purpose. Send # error to the browser. if err.status_code not in (401, ): self.scriptform.log.exception(err) self.send_response(err.status_code) for header_k, header_v in err.headers.items(): self.send_header(header_k, header_v) self.end_headers() self.wfile.write("Error {0}: {1}".format(err.status_code, err.msg).encode('utf-8')) self.wfile.flush() return False except Exception as err: self.scriptform.log.exception(err) self.send_error(500, "Internal server error") raise