You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
scriptform/src/scriptform.py

233 lines
8.2 KiB

#!/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()