|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
"""
|
|
|
|
Main ScriptForm program
|
|
|
|
"""
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import argparse
|
|
|
|
import os
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import threading
|
|
|
|
import hashlib
|
|
|
|
import getpass
|
|
|
|
|
|
|
|
if hasattr(sys, 'dont_write_bytecode'):
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
|
|
|
|
# pylint: disable=wrong-import-position
|
|
|
|
from daemon import Daemon
|
|
|
|
from formdefinition import FormDefinition
|
|
|
|
from formconfig import FormConfig
|
|
|
|
from webserver import ThreadedHTTPServer
|
|
|
|
from webapp import ScriptFormWebApp
|
|
|
|
|
|
|
|
|
|
|
|
class ScriptForm(object):
|
|
|
|
"""
|
|
|
|
'Main' class that orchestrates parsing the Form configurations and running
|
|
|
|
the webserver.
|
|
|
|
"""
|
|
|
|
def __init__(self, config_file, cache=True):
|
|
|
|
self.config_file = config_file
|
|
|
|
self.cache = cache
|
|
|
|
self.log = logging.getLogger('SCRIPTFORM')
|
|
|
|
self.form_config_singleton = None
|
|
|
|
self.websrv = None
|
|
|
|
self.running = False
|
|
|
|
self.httpd = None
|
|
|
|
|
|
|
|
# Init form config so it can raise errors about problems.
|
|
|
|
self.get_form_config()
|
|
|
|
|
|
|
|
def get_form_config(self):
|
|
|
|
"""
|
|
|
|
Read and return the form configuration in the form of a FormConfig
|
|
|
|
instance. If it has already been read, a cached version is returned.
|
|
|
|
"""
|
|
|
|
# Cache
|
|
|
|
if self.cache and self.form_config_singleton is not None:
|
|
|
|
return self.form_config_singleton
|
|
|
|
|
|
|
|
with open(self.config_file, "r") as fh:
|
|
|
|
file_contents = fh.read()
|
|
|
|
try:
|
|
|
|
config = json.loads(file_contents)
|
|
|
|
except ValueError as err:
|
|
|
|
sys.stderr.write("Error in form configuration '{}': {}\n".format(
|
|
|
|
self.config_file, err))
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
static_dir = None
|
|
|
|
custom_css = None
|
|
|
|
users = None
|
|
|
|
forms = []
|
|
|
|
|
|
|
|
if 'static_dir' in config:
|
|
|
|
static_dir = config['static_dir']
|
|
|
|
if 'custom_css' in config:
|
|
|
|
with open(config["custom_css"], "r") as fh:
|
|
|
|
custom_css = fh.read()
|
|
|
|
if 'users' in config:
|
|
|
|
users = config['users']
|
|
|
|
for form in config['forms']:
|
|
|
|
form_name = form['name']
|
|
|
|
if not form['script'].startswith('/'):
|
|
|
|
# Script is relative to the current dir
|
|
|
|
script = os.path.join(os.path.realpath(os.curdir),
|
|
|
|
form['script'])
|
|
|
|
else:
|
|
|
|
# Absolute path to the script
|
|
|
|
script = form['script']
|
|
|
|
forms.append(
|
|
|
|
FormDefinition(form_name,
|
|
|
|
form['title'],
|
|
|
|
form['description'],
|
|
|
|
form.get('fields', None),
|
|
|
|
script,
|
|
|
|
fields_from=form.get("fields_from", None),
|
|
|
|
default_value=form.get('default_value', ""),
|
|
|
|
output=form.get('output', 'escaped'),
|
|
|
|
hidden=form.get('hidden', False),
|
|
|
|
submit_title=form.get('submit_title', 'Submit'),
|
|
|
|
allowed_users=form.get('allowed_users', None),
|
|
|
|
run_as=form.get('run_as', None))
|
|
|
|
)
|
|
|
|
|
|
|
|
form_config = FormConfig(
|
|
|
|
config['title'],
|
|
|
|
forms,
|
|
|
|
users,
|
|
|
|
static_dir,
|
|
|
|
custom_css
|
|
|
|
)
|
|
|
|
self.form_config_singleton = form_config
|
|
|
|
return form_config
|
|
|
|
|
|
|
|
def run(self, listen_addr='0.0.0.0', listen_port=8081):
|
|
|
|
"""
|
|
|
|
Start the webserver on address `listen_addr` and port `listen_port`.
|
|
|
|
This call is blocking until the user hits Ctrl-c, the shutdown() method
|
|
|
|
is called or something like SystemExit is raised in a handler.
|
|
|
|
"""
|
|
|
|
ScriptFormWebApp.scriptform = self
|
|
|
|
self.httpd = ThreadedHTTPServer((listen_addr, listen_port),
|
|
|
|
ScriptFormWebApp)
|
|
|
|
self.httpd.daemon_threads = True
|
|
|
|
self.log.info("Listening on %s:%s", listen_addr, listen_port)
|
|
|
|
self.running = True
|
|
|
|
try:
|
|
|
|
self.httpd.serve_forever()
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
pass
|
|
|
|
self.running = False
|
|
|
|
|
|
|
|
def shutdown(self):
|
|
|
|
"""
|
|
|
|
Shutdown the server. This interupts the run() method and must thus be
|
|
|
|
run in a seperate thread.
|
|
|
|
"""
|
|
|
|
self.log.info("Attempting server shutdown")
|
|
|
|
|
|
|
|
def t_shutdown(scriptform_instance):
|
|
|
|
"""
|
|
|
|
Callback for when the server is shutdown.
|
|
|
|
"""
|
|
|
|
scriptform_instance.log.info(self.websrv)
|
|
|
|
# Undocumented feature to shutdow the server.
|
|
|
|
scriptform_instance.httpd.socket.close()
|
|
|
|
scriptform_instance.httpd.shutdown()
|
|
|
|
|
|
|
|
# We need to spawn a new thread in which the server is shut down,
|
|
|
|
# because doing it from the main thread blocks, since the server is
|
|
|
|
# waiting for connections..
|
|
|
|
thread = threading.Thread(target=t_shutdown, args=(self,))
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
|
|
|
def main(): # pragma: no cover
|
|
|
|
"""
|
|
|
|
main method
|
|
|
|
"""
|
|
|
|
desc = """A stand-alone webserver that automatically generates forms """ \
|
|
|
|
"""from JSON to serve as frontends to scripts."""
|
|
|
|
parser = argparse.ArgumentParser(description=desc)
|
|
|
|
parser.add_argument('--version',
|
|
|
|
action='version',
|
|
|
|
version='%(prog)s %%VERSION%%')
|
|
|
|
parser.add_argument('-g', '--generate-pw',
|
|
|
|
action='store_true',
|
|
|
|
default=False,
|
|
|
|
help='Generate password')
|
|
|
|
parser.add_argument('-p', '--port',
|
|
|
|
metavar='PORT',
|
|
|
|
dest='port',
|
|
|
|
type=int,
|
|
|
|
default=8081,
|
|
|
|
help='Port to listen on (default=8081)')
|
|
|
|
parser.add_argument('-f', '--foreground',
|
|
|
|
dest='foreground',
|
|
|
|
action='store_true',
|
|
|
|
default=False,
|
|
|
|
help='Run in foreground (debugging)')
|
|
|
|
parser.add_argument('-r', '--reload',
|
|
|
|
dest='reload',
|
|
|
|
action='store_true',
|
|
|
|
default=False,
|
|
|
|
help='Reload form config on every request (DEV)')
|
|
|
|
parser.add_argument('--pid-file',
|
|
|
|
metavar='PATH',
|
|
|
|
dest='pid_file',
|
|
|
|
type=str,
|
|
|
|
default=None,
|
|
|
|
help='Pid file')
|
|
|
|
parser.add_argument('--log-file',
|
|
|
|
metavar='PATH',
|
|
|
|
dest='log_file',
|
|
|
|
type=str,
|
|
|
|
default=None,
|
|
|
|
help='Log file')
|
|
|
|
parser.add_argument('--stop',
|
|
|
|
dest='action_stop',
|
|
|
|
action='store_true',
|
|
|
|
default=None,
|
|
|
|
help='Stop daemon')
|
|
|
|
parser.add_argument(dest='config',
|
|
|
|
metavar="CONFIG_FILE",
|
|
|
|
help="Path to form definition config",
|
|
|
|
)
|
|
|
|
options = parser.parse_args()
|
|
|
|
|
|
|
|
if options.generate_pw:
|
|
|
|
# Generate a password for use in the `users` section
|
|
|
|
plain_pw = getpass.getpass()
|
|
|
|
if plain_pw != getpass.getpass('Repeat password: '):
|
|
|
|
sys.stderr.write("Passwords do not match.\n")
|
|
|
|
sys.exit(1)
|
|
|
|
sha = hashlib.sha256(plain_pw.encode('utf8')).hexdigest()
|
|
|
|
sys.stdout.write("{}\n".format(sha))
|
|
|
|
sys.exit(0)
|
|
|
|
else:
|
|
|
|
# Switch to dir of form definition configuration
|
|
|
|
formconfig_path = os.path.realpath(options.config)
|
|
|
|
os.chdir(os.path.dirname(formconfig_path))
|
|
|
|
|
|
|
|
# Initialize daemon so we can start or stop it
|
|
|
|
daemon = Daemon(options.pid_file, options.log_file,
|
|
|
|
foreground=options.foreground)
|
|
|
|
|
|
|
|
if options.action_stop:
|
|
|
|
daemon.stop()
|
|
|
|
sys.exit(0)
|
|
|
|
else:
|
|
|
|
cache = not options.reload
|
|
|
|
scriptform_instance = ScriptForm(formconfig_path, cache=cache)
|
|
|
|
daemon.register_shutdown_callback(scriptform_instance.shutdown)
|
|
|
|
daemon.start()
|
|
|
|
scriptform_instance.run(listen_port=options.port)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
|
|
main()
|