Separate form configuration to its own class, so it can be dynamically loaded in the future.

pull/7/head
Ferry Boender 10 years ago
parent 450bc70b72
commit 9c41bfa51b
  1. 11
      doc/MANUAL.md
  2. 165
      src/scriptform.py

@ -39,9 +39,9 @@ Configure:
Make sure the path ends in a slash! (That's what the redirect is for). Make sure the path ends in a slash! (That's what the redirect is for).
## Form definition (JSON) files ## Form config (JSON) files
Forms are defined in JSON format. They are referred to as *Form Definition* Forms are defined in JSON format. They are referred to as *Form config*
files. A single JSON file may contain multiple forms. Scriptform will show them files. A single JSON file may contain multiple forms. Scriptform will show them
on an overview page, and the user can select which form they want to fill out. on an overview page, and the user can select which form they want to fill out.
@ -76,7 +76,7 @@ Structurally, they are made up of the following elements:
- **`users`**: A dictionary of users where the key is the username and the - **`users`**: A dictionary of users where the key is the username and the
value is the plaintext password. This field is not required. **Dictionary**. value is the plaintext password. This field is not required. **Dictionary**.
For example, here's a form definition file that contains two forms: For example, here's a form config file that contains two forms:
{ {
"title": "Test server", "title": "Test server",
@ -130,8 +130,6 @@ For example, here's a form definition file that contains two forms:
} }
## Field types ## Field types
### String ### String
@ -238,6 +236,7 @@ and shown to the user in the browser.
If a script's exit code is not 0, it is assumed an error occured. Scriptform If a script's exit code is not 0, it is assumed an error occured. Scriptform
will show the script's stderr output (in red) to the user instead of stdin. will show the script's stderr output (in red) to the user instead of stdin.
FIXME:
If the form definition has a `script_raw` field, and its value is `true`, If the form definition has a `script_raw` field, and its value is `true`,
Scriptform will pass the output of the script to the browser as-is. This allows Scriptform will pass the output of the script to the browser as-is. This allows
scripts to show images, stream a file download to the browser or even show scripts to show images, stream a file download to the browser or even show
@ -265,7 +264,7 @@ things to stdout, it can be used as a callback.
Fields of the form are validated by Scriptform before the script is called. Fields of the form are validated by Scriptform before the script is called.
Exactly what is validated depends on the options specified in the Form Exactly what is validated depends on the options specified in the Form
Definition file. For more info on that, see the *Field Types* section of this Definition. For more info on that, see the *Field Types* section of this
manual. manual.
#### Field values #### Field values

@ -133,6 +133,60 @@ html_submit_response = '''
''' '''
class FormConfig:
def __init__(self, title, forms, callbacks={}, users={}):
self.title = title
self.users = users
self.forms = forms
self.callbacks = callbacks
# Validate scripts
for form_def in self.forms:
if form_def.script:
if not stat.S_IXUSR & os.stat(form_def.script)[stat.ST_MODE]:
raise Exception("{0} is not executable".format(form_def.script))
else:
if not form_name in self.callbacks:
raise Exception("No script or callback registered for '{0}'".format(form_name))
def get_form(self, form_name):
for form_def in self.forms:
if form_def.name == form_name:
return form_def
def callback(self, form_name, form_values, output_fh=None):
form = self.get_form(form_name)
if form.script:
return self.callback_script(form, form_values, output_fh)
else:
return self.callback_python(form, form_values, output_fh)
def callback_script(self, form, form_values, output_fh=None):
# Pass form values to the script through the environment as strings.
env = os.environ.copy()
for k, v in form_values.items():
env[k] = str(v)
if form.output == 'raw':
p = subprocess.Popen(form.script, shell=True, stdout=output_fh,
stderr=output_fh, env=env)
stdout, stderr = p.communicate(input)
return None
else:
p = subprocess.Popen(form.script, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env)
stdout, stderr = p.communicate()
return {
'stdout': stdout,
'stderr': stderr,
'exitcode': p.returncode
}
def callback_python(self, form, form_values, output_fh=None):
pass
class FormDefinition: class FormDefinition:
""" """
FormDefinition holds information about a single form and provides methods FormDefinition holds information about a single form and provides methods
@ -400,11 +454,13 @@ class ScriptFormWebApp(WebAppHandler):
validated. Otherwise, returns False and sends 401 HTTP back to the validated. Otherwise, returns False and sends 401 HTTP back to the
client. client.
""" """
form_config = self.scriptform.get_form_config()
self.username = None self.username = None
# If a 'users' element was present in the form configuration file, the # If a 'users' element was present in the form configuration file, the
# user must be authenticated. # user must be authenticated.
if self.scriptform.users: print form_config.users
if form_config.users:
authorized = False authorized = False
auth_header = self.headers.getheader("Authorization") auth_header = self.headers.getheader("Authorization")
if auth_header is not None: if auth_header is not None:
@ -412,8 +468,8 @@ class ScriptFormWebApp(WebAppHandler):
username, password = base64.decodestring(auth_unpw).split(":") username, password = base64.decodestring(auth_unpw).split(":")
pw_hash = hashlib.sha256(password).hexdigest() pw_hash = hashlib.sha256(password).hexdigest()
# Validate the username and password # Validate the username and password
if username in self.scriptform.users and \ if username in form_config.users and \
pw_hash == self.scriptform.users[username]: pw_hash == form_config.users[username]:
self.username = username self.username = username
authorized = True authorized = True
@ -426,11 +482,12 @@ class ScriptFormWebApp(WebAppHandler):
return True return True
def h_list(self): def h_list(self):
form_config = self.scriptform.get_form_config()
if not self.auth(): if not self.auth():
return return
h_form_list = [] h_form_list = []
for form_name, form_def in self.scriptform.forms.items(): for form_def in form_config.forms:
if form_def.allowed_users is not None and \ if form_def.allowed_users is not None and \
self.username not in form_def.allowed_users: self.username not in form_def.allowed_users:
continue # User is not allowed to run this form continue # User is not allowed to run this form
@ -444,11 +501,11 @@ class ScriptFormWebApp(WebAppHandler):
</li> </li>
'''.format(title=form_def.title, '''.format(title=form_def.title,
description=form_def.description, description=form_def.description,
name=form_name) name=form_def.name)
) )
output = html_list.format( output = html_list.format(
header=html_header.format(title=self.scriptform.title), header=html_header.format(title=form_config.title),
footer=html_footer, footer=html_footer,
form_list=''.join(h_form_list) form_list=''.join(h_form_list)
) )
@ -458,6 +515,7 @@ class ScriptFormWebApp(WebAppHandler):
self.wfile.write(output) self.wfile.write(output)
def h_form(self, form_name, errors={}): def h_form(self, form_name, errors={}):
form_config = self.scriptform.get_form_config()
if not self.auth(): if not self.auth():
return return
@ -534,7 +592,7 @@ class ScriptFormWebApp(WebAppHandler):
) )
) )
form_def = self.scriptform.get_form(form_name) form_def = form_config.get_form(form_name)
if form_def.allowed_users is not None and \ if form_def.allowed_users is not None and \
self.username not in form_def.allowed_users: self.username not in form_def.allowed_users:
raise Exception("Not authorized") raise Exception("Not authorized")
@ -547,7 +605,7 @@ class ScriptFormWebApp(WebAppHandler):
html_errors += '</ul>' html_errors += '</ul>'
output = html_form.format( output = html_form.format(
header=html_header.format(title=self.scriptform.title), header=html_header.format(title=form_config.title),
footer=html_footer, footer=html_footer,
title=form_def.title, title=form_def.title,
description=form_def.description, description=form_def.description,
@ -562,11 +620,12 @@ class ScriptFormWebApp(WebAppHandler):
self.wfile.write(output) self.wfile.write(output)
def h_submit(self, form_values): def h_submit(self, form_values):
form_config = self.scriptform.get_form_config()
if not self.auth(): if not self.auth():
return return
form_name = form_values.getfirst('form_name', None) form_name = form_values.getfirst('form_name', None)
form_def = self.scriptform.get_form(form_name) form_def = form_config.get_form(form_name)
if form_def.allowed_users is not None and \ if form_def.allowed_users is not None and \
self.username not in form_def.allowed_users: self.username not in form_def.allowed_users:
raise Exception("Not authorized") raise Exception("Not authorized")
@ -608,7 +667,7 @@ class ScriptFormWebApp(WebAppHandler):
# in some nice HTML. If no result is returned, the output was raw # in some nice HTML. If no result is returned, the output was raw
# and the callback should have written its own response to the # and the callback should have written its own response to the
# self.wfile filehandle. # self.wfile filehandle.
result = self.scriptform.callback(form_name, form_values, self.wfile) result = form_config.callback(form_name, form_values, self.wfile)
if result: if result:
if result['exitcode'] != 0: if result['exitcode'] != 0:
msg = '<span class="error">{0}</span>'.format(cgi.escape(result['stderr'])) msg = '<span class="error">{0}</span>'.format(cgi.escape(result['stderr']))
@ -619,7 +678,7 @@ class ScriptFormWebApp(WebAppHandler):
msg = result['stdout'] msg = result['stdout']
output = html_submit_response.format( output = html_submit_response.format(
header=html_header.format(title=self.scriptform.title), header=html_header.format(title=form_config.title),
footer=html_footer, footer=html_footer,
title=form_def.title, title=form_def.title,
form_name=form_def.name, form_name=form_def.name,
@ -640,41 +699,34 @@ class ScriptFormWebApp(WebAppHandler):
class ScriptForm: class ScriptForm:
""" """
'Main' class that orchestrates parsing the Form definition file 'Main' class that orchestrates parsing the Form configurations, hooking up
`config_file`, hooking up callbacks and running the webserver. callbacks and running the webserver.
""" """
def __init__(self, config_file, callbacks={}): def __init__(self, config_file, callbacks=None):
self.forms = {} self.config_file = config_file
self.callbacks = {} if callbacks:
self.title = 'ScriptForm Actions' self.callbacks = callbacks
self.users = None else:
self.callbacks = {}
self.basepath = os.path.realpath(os.path.dirname(config_file)) self.basepath = os.path.realpath(os.path.dirname(config_file))
self._load_config(config_file) def get_form_config(self):
for form_name, cb in callbacks.items(): path = self.config_file
self.callbacks[form_name] = cb config = json.load(file(path, 'r'))
# Validate scripts title = config['title']
for form_name, form_def in self.forms.items(): forms = []
if form_def.script: callbacks = self.callbacks
if not stat.S_IXUSR & os.stat(form_def.script)[stat.ST_MODE]: users = None
raise Exception("{0} is not executable".format(form_def.script))
else:
if not form_name in self.callbacks:
raise Exception("No script or callback registered for '{0}'".format(form_name))
def _load_config(self, path):
config = json.load(file(path, 'r'))
if 'title' in config:
self.title = config['title']
if 'users' in config: if 'users' in config:
self.users = config['users'] users = config['users']
for form_name, form in config['forms'].items(): for form_name, form in config['forms'].items():
if 'script' in form: if 'script' in form:
script = os.path.join(self.basepath, form['script']) script = os.path.join(self.basepath, form['script'])
else: else:
script = None script = None
self.forms[form_name] = \ forms.append(
FormDefinition(form_name, FormDefinition(form_name,
form['title'], form['title'],
form['description'], form['description'],
@ -683,45 +735,18 @@ class ScriptForm:
output=form.get('output', 'escaped'), output=form.get('output', 'escaped'),
submit_title=form.get('submit_title', None), submit_title=form.get('submit_title', None),
allowed_users=form.get('allowed_users', None)) allowed_users=form.get('allowed_users', None))
)
def get_form(self, form_name): return FormConfig(
return self.forms[form_name] config['title'],
forms,
def callback(self, form_name, form_values, output_fh=None): callbacks,
form = self.get_form(form_name) users
if form.script: )
return self.callback_script(form, form_values, output_fh)
else:
return self.callback_python(form, form_values, output_fh)
def callback_script(self, form, form_values, output_fh=None):
# Pass form values to the script through the environment as strings.
env = os.environ.copy()
for k, v in form_values.items():
env[k] = str(v)
if form.output == 'raw':
p = subprocess.Popen(form.script, shell=True, stdout=output_fh,
stderr=output_fh, env=env)
stdout, stderr = p.communicate(input)
return None
else:
p = subprocess.Popen(form.script, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=env)
stdout, stderr = p.communicate()
return {
'stdout': stdout,
'stderr': stderr,
'exitcode': p.returncode
}
def callback_python(self, form, form_values, output_fh=None):
pass
def run(self, listen_addr='0.0.0.0', listen_port=80): def run(self, listen_addr='0.0.0.0', listen_port=80):
ScriptFormWebApp.scriptform = self ScriptFormWebApp.scriptform = self
ScriptFormWebApp.callbacks = self.callbacks #ScriptFormWebApp.callbacks = self.callbacks
WebSrv(ScriptFormWebApp, listen_addr=listen_addr, listen_port=listen_port) WebSrv(ScriptFormWebApp, listen_addr=listen_addr, listen_port=listen_port)

Loading…
Cancel
Save