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_list = u''''
{header}
{form_list}
{footer}
'''
html_form = u'''
{header}
{footer}
'''
html_submit_response = u'''
{header}
{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:
# HTTP erors are generally thrown by the webapp on purpose. Send
# error to the browser.
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()
self.wfile.write("Error {}: {}".format(e.status_code,
e.msg))
self.wfile.flush()
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'''
{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''
for error in errors:
html_errors += u'- {0}
'.format(error)
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())