from SocketServer import ThreadingMixIn import BaseHTTPServer from BaseHTTPServer import BaseHTTPRequestHandler import urlparse import cgi import logging import tempfile import os import base64 import hashlib from formrender import FormRender html_header = u'''

{title}

''' html_footer = u'''
Powered by Scriptform v%%VERSION%%
''' html_list = u'''' {header}
{form_list}
{footer} ''' html_form = u''' {header}

{title}

{description}

{footer} ''' html_submit_response = u''' {header}

{title}

Result

{msg}
{footer} ''' class HTTPError(Exception): 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): 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): self._call(*self._parse(self.path)) def do_POST(self): 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): url_comp = urlparse.urlsplit(reqinfo) path = url_comp.path qs = 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 qs.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, e: if e.status_code not in (401, ): self.scriptform.log.exception(e) self.send_response(e.status_code) for header_k, header_v in e.headers.items(): self.send_header(header_k, header_v) self.end_headers() return False except Exception, e: self.scriptform.log.exception(e) self.send_error(500, "Internal server error") raise class ScriptFormWebApp(WebAppHandler): """ This class is a request handler for WebSrv. """ def index(self): """ Index handler. If there's only one form defined, render that form. Otherwise render a list of available forms. """ form_config = self.scriptform.get_form_config() visible_forms = form_config.get_visible_forms(getattr(self, 'username', None)) if len(visible_forms) == 1: first_form = visible_forms[0] return self.h_form(first_form.name) else: return self.h_list() def auth(self): """ Verify that the user is authenticated. This is required if the form definition contains a 'users' field. Returns the username if the user is validated or None if no validation is required.. Otherwise, raises a 401 HTTP back to the client. """ form_config = self.scriptform.get_form_config() self.username = None # If a 'users' element was present in the form configuration file, the # user must be authenticated. if form_config.users: authorized = False auth_header = self.headers.getheader("Authorization") if auth_header is not None: auth_unpw = auth_header.split(' ', 1)[1] username, password = base64.decodestring(auth_unpw).split(":") pw_hash = hashlib.sha256(password).hexdigest() # Validate the username and password if username in form_config.users and \ pw_hash == form_config.users[username]: self.username = username authorized = True if not authorized: headers = { "WWW-Authenticate": 'Basic realm="Private Area"' } raise HTTPError(401, 'Authenticate', headers) return True def h_list(self): """ Render a list of available forms. """ if not self.auth(): return form_config = self.scriptform.get_form_config() h_form_list = [] for form_def in form_config.get_visible_forms(getattr(self, 'username', None)): h_form_list.append(u'''
  • {title}

    {description}

    {title}
  • '''.format(title=form_def.title, description=form_def.description, name=form_def.name) ) output = html_list.format( header=html_header.format(title=form_config.title, custom_css=form_config.custom_css), footer=html_footer, form_list=u''.join(h_form_list) ) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(output.encode('utf8')) def h_form(self, form_name, errors=None, **form_values): """ Render a form. """ if errors is None: errors = {} if not self.auth(): return form_config = self.scriptform.get_form_config() fr = FormRender(None) def render_field(field, errors): params = { 'name': field['name'], 'classes': [], } if field.get('hidden', None): params['classes'].append('hidden') params["style"] = field.get("style", "") if field['type'] not in ('file', 'checkbox'): params['value'] = form_values.get(field['name'], '') if field['type'] not in ('radio', 'checkbox', 'select'): params['required'] = field.get('required', False) if field['type'] in ('string'): params['size'] = field.get('size', '') if field['type'] in ('number', 'integer', 'float', 'password'): params['minval'] = field.get("min", '') if field['type'] in ('number', 'integer', 'float'): params['maxval'] = field.get("max", '') if field['type'] in ('text'): params['rows'] = field.get("rows", '') params['cols'] = field.get("cols", '') if field['type'] == 'radio': if not form_values.get(field['name'], None): params['value'] = field['options'][0][0] params['options'] = field['options'] if field['type'] in ('radio', 'select'): params['options'] = field['options'] if field['type'] == 'checkbox': params['checked'] = False if field['name'] in form_values and form_values[field['name']] == 'on': params['checked'] = True h_input = fr.r_field(field['type'], **params) return fr.r_form_line(field['type'], field['title'], h_input, params['classes'], errors) # Make sure the user is allowed to access this form. form_def = form_config.get_form_def(form_name) if form_def.allowed_users is not None and \ self.username not in form_def.allowed_users: # FIXME: Raise HTTPError instead? self.send_error(401, "You're not authorized to view this form") return html_errors = u'' if errors: html_errors = u'' output = html_form.format( header=html_header.format(title=form_config.title, custom_css=form_config.custom_css), footer=html_footer, title=form_def.title, description=form_def.description, errors=html_errors, name=form_def.name, fields=u''.join([render_field(f, errors.get(f['name'], [])) for f in form_def.fields]), submit_title=form_def.submit_title ) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(output.encode('utf8')) 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. """ if not self.auth(): return form_config = self.scriptform.get_form_config() form_name = form_values.getfirst('form_name', None) form_def = form_config.get_form_def(form_name) if form_def.allowed_users is not None and \ self.username not in form_def.allowed_users: # FIXME: Raise HTTPError instead? self.send_error(401, "You're not authorized to view this form") return # Convert FieldStorage to a simple dict, because we're not allowd to # add items to it. For normal fields, the form field name becomes the # key and the value becomes the field value. For file upload fields, we # stream the uploaded file to a temp file and then put the temp file in # the destination dict. We also add an extra field with the originally # uploaded file's name. values = {} tmp_files = [] for field_name in form_values: field = form_values[field_name] if field.filename is not None: # Field is an uploaded file. Stream it to a temp file if # something was actually uploaded if field.filename == '': continue tmpfile = tempfile.mktemp(prefix="scriptform_") f = file(tmpfile, 'w') while True: buf = field.file.read(1024 * 16) if not buf: break f.write(buf) f.close() field.file.close() tmp_files.append(tmpfile) # For later cleanup values[field_name] = tmpfile values['{0}__name'.format(field_name)] = field.filename else: # 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. # Log the callback and its parameters for auditing purposes. log = logging.getLogger('CALLBACK_AUDIT') log.info("Calling script {0}".format(form_def.script)) log.info("User: {0}".format(getattr(self.request, 'username', 'None'))) log.info("Variables: {0}".format(dict(form_values.items()))) result = form_config.callback(form_name, form_values, self.wfile, self.wfile) if form_def.output != 'raw': # Ignore everything if we're doing raw output, since it's the # scripts responsibility. if result['exitcode'] != 0: msg = u'{0}'.format(cgi.escape(result['stderr'].decode('utf8'))) else: if form_def.output == 'escaped': msg = u'
    {0}
    '.format(cgi.escape(result['stdout'].decode('utf8'))) else: # Non-escaped output (html, usually) msg = result['stdout'].decode('utf8') output = html_submit_response.format( header=html_header.format(title=form_config.title, custom_css=form_config.custom_css), footer=html_footer, title=form_def.title, form_name=form_def.name, msg=msg, ) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(output.encode('utf8')) else: # Form had errors form_values.pop('form_name') self.h_form(form_name, form_errors, **form_values) # Clean up uploaded files for file_name in tmp_files: if os.path.exists(file_name): os.unlink(file_name) def h_static(self, fname): """Serve static files""" if not self.auth(): return form_config = self.scriptform.get_form_config() if not form_config.static_dir: # FIXME: Raise Error self.send_error(501, "Static file serving not enabled") return if '..' in fname: # FIXME: Raise Error self.send_error(403, "Invalid file name") return path = os.path.join(form_config.static_dir, fname) if not os.path.exists(path): # FIXME: Raise Error self.send_error(404, "Not found") return f = file(path, 'r') self.send_response(200) self.end_headers() self.wfile.write(f.read())