diff --git a/src/scriptform.py b/src/scriptform.py index 2ff45df..8d06951 100755 --- a/src/scriptform.py +++ b/src/scriptform.py @@ -47,7 +47,8 @@ import socket from daemon import Daemon from formdefinition import FormDefinition from formconfig import FormConfig -from webapp import ThreadedHTTPServer, ScriptFormWebApp +from webserver import ThreadedHTTPServer +from webapp import ScriptFormWebApp class ScriptFormError(Exception): diff --git a/src/webapp.py b/src/webapp.py index 7f30ffd..145346e 100644 --- a/src/webapp.py +++ b/src/webapp.py @@ -3,10 +3,6 @@ The webapp part of Scriptform, which takes care of serving requests and handling them. """ -from SocketServer import ThreadingMixIn -import BaseHTTPServer -from BaseHTTPServer import BaseHTTPRequestHandler -import urlparse import cgi import logging import tempfile @@ -15,6 +11,7 @@ import base64 import hashlib from formrender import FormRender +from webserver import HTTPError, RequestHandler HTML_HEADER = u''' @@ -161,115 +158,9 @@ HTML_SUBMIT_RESPONSE = u''' ''' -class HTTPError(Exception): +class ScriptFormWebApp(RequestHandler): """ - 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): - 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, BaseHTTPServer.HTTPServer): - """ - Base class for multithreaded HTTP servers. - """ - pass - - -class WebAppHandler(BaseHTTPRequestHandler): - """ - Basic web server request handler. Handles GET and POST requests. This class - should be extended with methods (starting with 'h_') to handle the actual - requests. If no path is set, it dispatches to the 'index' or 'default' - method. - """ - def log_message(self, fmt, *args): - """Overrides BaseHTTPRequestHandler which logs to the console. We log - to our log file instead""" - fmt = "{} {}" - self.scriptform.log.info(fmt.format(self.address_string(), args)) - - def do_GET(self): - """ - Handle a GET request. - """ - self._call(*self._parse(self.path)) - - def do_POST(self): - """ - 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 = urlparse.urlsplit(reqinfo) - path = url_comp.path - query_vars = urlparse.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, 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 {}: {}".format(err.status_code, - err.msg)) - self.wfile.flush() - return False - except Exception, err: - self.scriptform.log.exception(err) - self.send_error(500, "Internal server error") - raise - - -class ScriptFormWebApp(WebAppHandler): - """ - This class is a request handler for WebSrv. + This class is a request handler for the webserver. """ def index(self): """ @@ -459,8 +350,8 @@ class ScriptFormWebApp(WebAppHandler): def h_submit(self, form_values): """ Handle the submitting of a form by validating the values and then doing - a callback to a script. How the output is - handled depends on settings in the form definition. + a callback to a script. How the output is handled depends on settings + in the form definition. """ username = self.auth() @@ -503,14 +394,13 @@ class ScriptFormWebApp(WebAppHandler): # Field is a normal form field. Store its value. values[field_name] = form_values.getfirst(field_name, None) - # Validate the form values form_errors, form_values = form_def.validate(values) if not form_errors: - # Call user's callback. If a result is returned, we wrap its output - # in some nice HTML. If no result is returned, the output was raw - # and the callback should have written its own response to the - # self.wfile filehandle. + # Call script. If a result is returned, we wrap its output in some + # nice HTML. If no result is returned, the output was raw and the + # callback should have written its own response to the self.wfile + # filehandle. # Log the callback and its parameters for auditing purposes. log = logging.getLogger('CALLBACK_AUDIT') diff --git a/src/webserver.py b/src/webserver.py new file mode 100644 index 0000000..25a3492 --- /dev/null +++ b/src/webserver.py @@ -0,0 +1,114 @@ +""" +Basic web server / framework. +""" + +from SocketServer import ThreadingMixIn +import BaseHTTPServer +from BaseHTTPServer import BaseHTTPRequestHandler +import urlparse +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): + 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, BaseHTTPServer.HTTPServer): + """ + Base class for multithreaded HTTP servers. + """ + pass + + +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): + """Overrides BaseHTTPRequestHandler which logs to the console. We log + to our log file instead""" + fmt = "{} {}" + self.scriptform.log.info(fmt.format(self.address_string(), args)) + + def do_GET(self): + """ + Handle a GET request. + """ + self._call(*self._parse(self.path)) + + def do_POST(self): + """ + 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 = urlparse.urlsplit(reqinfo) + path = url_comp.path + query_vars = urlparse.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, 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 {}: {}".format(err.status_code, + err.msg)) + self.wfile.flush() + return False + except Exception, err: + self.scriptform.log.exception(err) + self.send_error(500, "Internal server error") + raise