Merge branch 'eventhandler-rework' into chausie

This commit is contained in:
Kristóf Tóth 2019-07-25 12:12:01 +02:00
commit 7698ff430e
9 changed files with 122 additions and 423 deletions

View File

@ -2,8 +2,8 @@ FROM avatao/controller:debian-buster
USER root USER root
ENV PYTHONPATH="/usr/local/lib" \ ENV PYTHONPATH="/usr/local/lib" \
TFW_PUBLISHER_PORT=7654 \ TFW_PUB_PORT=7654 \
TFW_RECEIVER_PORT=8765 \ TFW_PULL_PORT=8765 \
TFW_AUTH_KEY="/tmp/tfw-auth.key" \ TFW_AUTH_KEY="/tmp/tfw-auth.key" \
CONTROLLER_PORT=5555 CONTROLLER_PORT=5555

View File

@ -4,12 +4,13 @@ import json
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, Application from tornado.web import RequestHandler, Application
from tfw.builtins import FSMAwareEventHandler from tfw.event_handlers import FSMAwareEventHandler
from tfw.main import EventHandlerFactory, setup_signal_handlers
class ControllerPostHandler(RequestHandler): class ControllerPostHandler(RequestHandler):
# pylint: disable=abstract-method # pylint: disable=abstract-method,attribute-defined-outside-init,unused-argument
def initialize(self, **kwargs): # pylint: disable=arguments-differ def initialize(self, **kwargs):
self.controller = kwargs['controller'] self.controller = kwargs['controller']
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
@ -19,21 +20,18 @@ class ControllerPostHandler(RequestHandler):
})) }))
class ControllerEventHandler(FSMAwareEventHandler):
def handle_event(self, message):
pass
if __name__ == '__main__': if __name__ == '__main__':
controller = ControllerEventHandler('controller') controller_eh = EventHandlerFactory().build(
lambda *_: None,
event_handler_type=FSMAwareEventHandler
)
application = Application([( application = Application([(
f'/{os.environ["SECRET"]}', f'/{os.environ["SECRET"]}',
ControllerPostHandler, ControllerPostHandler,
{'controller': controller} {'controller': controller_eh}
)]) )])
application.listen(os.environ['CONTROLLER_PORT']) application.listen(os.environ['CONTROLLER_PORT'])
try: setup_signal_handlers()
IOLoop.instance().start() IOLoop.instance().start()
finally:
controller.cleanup()

View File

@ -1,71 +0,0 @@
import logging
from ast import literal_eval
from tfw.components import MessageSender
from tfw.builtins import TFWServerUplinkConnector
from tfw.builtins import EventHandler, FSMAwareEventHandler, TerminalCommandsEventHandler
LOG = logging.getLogger(__name__)
class CenatorEventHandler(EventHandler):
"""
Logs commands executed in terminal to messages and invokes an
additional callback function to handle special commands.
!! Please remove from production code. !!
"""
def __init__(self, key):
super().__init__(key)
def handle_event(self, message):
command = message['value']
LOG.debug('User executed command: "%s"', command)
MessageSender(self.server_connector).send('JOHN CENA', f'You\'ve executed "{command}"')
class TestCommandsEventHandler(TerminalCommandsEventHandler):
"""
Some example commands useful for debugging.
!! Please remove from production code !! and inherit your own
class from TerminalCommands if you need to define custom
commands in your challenge.
"""
# pylint: disable=unused-argument, attribute-defined-outside-init, no-self-use
def command_sendmessage(self, *args):
"""
Insert TFW message template as first argument if executed without args.
Evaluate first argumen as a dict and send it to the frontend.
This is useful for playing around with frontend APIs.
"""
if not args:
message_template = """'{"key": "", "data": {"command": ""}}'"""
TFWServerUplinkConnector().send_message({
'key': 'shell',
'data': {
'command': 'write',
'value': f'sendmessage {message_template}'
}
})
else:
TFWServerUplinkConnector().send_message(literal_eval(args[0]))
class MessageFSMStepsEventHandler(FSMAwareEventHandler):
"""
This example EventHandler is capable of detecting FSM state.
!! Please remove from production code !!
"""
def handle_event(self, message):
pass
def handle_fsm_step(self, **kwargs):
"""
When the FSM steps this method is invoked.
Receives a 'data' field from an fsm_update message as kwargs.
"""
MessageSender(self.server_connector).send(
'FSM info',
f'FSM has stepped from state "{kwargs["last_event"]["from_state"]}" '
f'to state "{kwargs["current_state"]}" in response to trigger "{kwargs["last_event"]["trigger"]}"'
)

View File

@ -0,0 +1,45 @@
import logging
from ast import literal_eval
from tfw.components.frontend import MessageSender
from tfw.components.terminal import TerminalCommandsHandler
from tfw.main import TFWUplinkConnector
LOG = logging.getLogger(__name__)
class CenatorHandler:
keys = ['history.bash']
def handle_event(self, message, server_connector): # pylint: disable=no-self-use
command = message['value']
LOG.debug('User executed command: "%s"', command)
MessageSender(server_connector).send('JOHN CENA', f'You\'ve executed "{command}"')
class TestCommandsHandler(TerminalCommandsHandler):
# pylint: disable=unused-argument,attribute-defined-outside-init,no-self-use
def command_sendmessage(self, *args):
if not args:
message_template = """'{"key": "", "data": {"command": ""}}'"""
TFWUplinkConnector().send_message({
'key': 'shell',
'data': {
'command': 'write',
'value': f'sendmessage {message_template}'
}
})
else:
TFWUplinkConnector().send_message(literal_eval(args[0]))
def messageFSMStepsHandler(message, server_connector):
"""
When the FSM steps this method is invoked.
Receives a 'data' field from an fsm_update message as kwargs.
"""
MessageSender(server_connector).send(
'FSM info',
f'FSM has stepped from state "{message["last_event"]["from_state"]}" '
f'to state "{message["current_state"]}" in response to trigger "{message["last_event"]["trigger"]}"'
)

View File

@ -5,16 +5,18 @@ from functools import partial
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tfw.fsm import YamlFSM from tfw.fsm import YamlFSM
from tfw.builtins import IdeEventHandler, TerminalEventHandler, FrontendEventHandler from tfw.event_handlers import FSMAwareEventHandler
from tfw.builtins import LogMonitoringEventHandler, ProcessManagingEventHandler from tfw.components.ide import IdeHandler
from tfw.builtins import DirectorySnapshottingEventHandler, FSMManagingEventHandler from tfw.components.terminal import TerminalHandler
from tfw.config import TFWENV from tfw.components.frontend import FrontendHandler
from tfw.components.process_management import ProcessHandler, ProcessLogHandler
from tfw.components.snapshots import SnapshotHandler
from tfw.components.fsm import FSMHandler
from tfw.main import EventHandlerFactory, setup_signal_handlers
from tfw.logging import Log, Logger, LogFormatter, VerboseLogFormatter from tfw.logging import Log, Logger, LogFormatter, VerboseLogFormatter
from tao.config import TAOENV from tfw.config import TFWENV, TAOENV
from custom_event_handlers import MessageFSMStepsEventHandler from custom_handlers import CenatorHandler, TestCommandsHandler, messageFSMStepsHandler
from custom_event_handlers import CenatorEventHandler, TestCommandsEventHandler
from signal_handling import setup_signal_handlers
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -26,51 +28,63 @@ def main():
Log(TFWENV.LOGFILE, VerboseLogFormatter()) Log(TFWENV.LOGFILE, VerboseLogFormatter())
]).start() ]).start()
# TFW component EventHandlers (builtins, required for their respective functionalities) eh_factory = EventHandlerFactory()
fsm = FSMManagingEventHandler( # TFW FSM # TFW builtin EventHandlers (required for their respective functionalities)
key='fsm', # TFW FSM
fsm_eh = eh_factory.build(FSMHandler(
fsm_type=partial( fsm_type=partial(
YamlFSM, YamlFSM,
'test_fsm.yml', 'test_fsm.yml',
{} # jinja2 variables, use empty dict to enable jinja2 parsing without any variables {} # jinja2 variables, empty dict enables jinja2 without any variables
) )
) ))
ide = IdeEventHandler( # Web IDE backend # Web IDE backend
key='ide', ide_eh = eh_factory.build(IdeHandler(
allowed_directories=[TFWENV.IDE_WD, TFWENV.WEBSERVICE_DIR], allowed_directories=[TFWENV.IDE_WD, TFWENV.WEBSERVICE_DIR],
directory=TFWENV.IDE_WD, directory=TFWENV.IDE_WD,
exclude=['*.pyc'] exclude=['*.pyc']
) ))
terminal = TerminalEventHandler( # Web shell backend # Web shell backend
key='shell' terminal_eh = eh_factory.build(TerminalHandler(
) port=TFWENV.TERMINADO_PORT,
cenator = CenatorEventHandler('history.bash') # Reacts to terminal commands user=TAOENV.USER,
commands = TestCommandsEventHandler( # Catches special commands workind_directory=TFWENV.TERMINADO_WD,
key='history.bash', histfile=TFWENV.HISTFILE
bashrc=f'/home/{TAOENV.USER}/.bashrc' ))
) # Handles 'deploy' button clicks
processmanager = ProcessManagingEventHandler( # Handles 'deploy' button clicks processmanager_eh = eh_factory.build(ProcessHandler(
key='processmanager', supervisor_uri=TFWENV.SUPERVISOR_HTTP_URI,
log_tail=2000 log_tail=2000,
) ))
logmonitor = LogMonitoringEventHandler( # Sends live logs of webservice process to frontend # Sends live logs of webservice process to frontend
key='logmonitor', logmonitor_eh = eh_factory.build(ProcessLogHandler(
process_name='webservice', process_name='webservice',
supervisor_uri=TFWENV.SUPERVISOR_HTTP_URI,
log_tail=2000 log_tail=2000
) ))
snapshot = DirectorySnapshottingEventHandler( # Manages filesystem snapshots of directories # Manages filesystem snapshots of directories
key='snapshot', snapshot_eh = eh_factory.build(SnapshotHandler(
directories=[ directories=[
TFWENV.IDE_WD, TFWENV.IDE_WD,
TFWENV.WEBSERVICE_DIR TFWENV.WEBSERVICE_DIR
] ],
) snapshots_dir=TFWENV.SNAPSHOTS_DIR
frontend = FrontendEventHandler() # Proxies frontend API calls to frontend ))
# Proxies frontend API calls to frontend
frontend_eh = eh_factory.build(FrontendHandler())
# Your custom event handlers # Replace these with your custom event handlers
message_fsm_steps_eh = MessageFSMStepsEventHandler( # Echoes executed commands to messages
key='test' cenator_eh = eh_factory.build(CenatorHandler())
# Echoes FSM steps
message_fsm_steps_eh = eh_factory.build(
messageFSMStepsHandler,
event_handler_type=FSMAwareEventHandler
) )
# Catches special commands
commands_eh = eh_factory.build(TestCommandsHandler(
bashrc=f'/home/{TAOENV.USER}/.bashrc'
))
setup_signal_handlers() setup_signal_handlers()
IOLoop.instance().start() IOLoop.instance().start()

View File

@ -1,235 +0,0 @@
from json import dumps, loads
from tfw.crypto import KeyManager, sign_message, verify_message
from tfw.builtins import PipeIOEventHandlerBase
from tfw.builtins.pipe_io_event_handler import DEFAULT_PERMISSIONS
from tfw.networking.scope import Scope
class SignMessagePipeIOEventHandler(PipeIOEventHandlerBase):
"""
Signs a valid TFW message with HMAC.
Note that the running process needs root permissions in order to read
the authentication key.
When forwarding is true, it will send the signed message to the TFW
server before writing it into the output pipe.
"""
def __init__(
self, in_pipe_path, out_pipe_path,
permissions=DEFAULT_PERMISSIONS,
forwarding=True
):
self.forwarding = forwarding
self.auth_key = KeyManager().auth_key
super().__init__(None, in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
pass
def handle_pipe_event(self, message_bytes):
message = loads(message_bytes)
sign_message(self.auth_key, message)
if self.forwarding:
self.server_connector.send_message(message)
self.pipe_io.send_message(dumps(message).encode())
class VerifyMessagePipeIOEventHandler(PipeIOEventHandlerBase):
"""
Verifies a signed TFW message.
This pipe also needs root permissions. Send the serialized JSON object
to the pipe, then wait for its boolean response.
"""
def __init__(self, in_pipe_path, out_pipe_path, permissions=DEFAULT_PERMISSIONS):
self.auth_key = KeyManager().auth_key
super().__init__(None, in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
pass
def handle_pipe_event(self, message_bytes):
message = loads(message_bytes)
validity = verify_message(self.auth_key, message)
self.pipe_io.send_message(str(validity).lower().encode())
class BotPipeIOEventHandler(PipeIOEventHandlerBase):
"""
Sends bot messages to the frontend.
If you assign @originator, it will be the default message sender.
When you write a line to the pipe, it will be considered as a single
message and gets appended to the queue until an empty line is received,
which triggers forwarding the messages to the TFW server.
"""
def __init__(
self, in_pipe_path, out_pipe_path, permissions=DEFAULT_PERMISSIONS,
originator='avataobot'
):
self.queue = []
self.originator = originator
super().__init__(None, in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
pass
def handle_pipe_event(self, message_bytes):
if message_bytes == b"":
if self.queue:
self.server_connector.send_message({
'key': 'queueMessages',
'data': {
'messages': self.queue
}
})
self.queue = []
else:
self.queue.append({
'originator': self.originator,
'message': message_bytes.decode().replace('\\n', '\n')
})
class DeployPipeIOEventHandler(PipeIOEventHandlerBase):
"""
Manages deployment in the IDE.
When you receive "deploy", then you have to answer with a "true" or
"false" depending whether you are satisfied with the result or not.
The @process parameter is the name of the supervised service.
"""
# pylint: disable=too-many-arguments
def __init__(
self, in_pipe_path, out_pipe_path, process,
permissions=DEFAULT_PERMISSIONS
):
self.expected = False
self.process = process
self.onsuccess = {
'key': 'processmanager',
'data': {
'process_name': process,
'command': 'restart'
}
}
self.onerror = {
'key': 'processmanager',
'data': {
'process_name': process,
'error': True
}
}
super().__init__('processmanager', in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
if message == self.onsuccess:
self.expected = True
self.pipe_io.send_message(b'deploy')
def handle_pipe_event(self, message_bytes):
if not self.expected:
raise ValueError(
f'{self.pipe_io.in_pipe}: There is nothing to deploy.'
)
self.expected = False
if message_bytes == b'true':
self.server_connector.send_message(self.onsuccess, scope=Scope.WEBSOCKET)
elif message_bytes == b'false':
self.server_connector.send_message(self.onerror, scope=Scope.WEBSOCKET)
else:
raise ValueError(
f'{self.pipe_io.in_pipe}: Expected "true" or "false".'
)
class IdePipeIOEventHandler(PipeIOEventHandlerBase):
"""
Manipulates the content of the IDE.
You can observe a file, and when the user edits it, you will receive
the new contents where newlines are escaped as "\\n".
In order to overwrite the file, send an escaped text back to the pipe.
Since the pipe doesn't know if the file is selected initially in the IDE,
you have to provide this information by yourself with @selected,
but it will track it later on.
"""
# pylint: disable=too-many-arguments
def __init__(
self, in_pipe_path, out_pipe_path, filename,
permissions=DEFAULT_PERMISSIONS,
selected=True
):
self.selected = selected
self.filename = filename
super().__init__('ide', in_pipe_path, out_pipe_path, permissions)
def handle_event(self, message):
data = message['data']
if data['command'] == 'select':
self.selected = data['filename'] == self.filename
elif data['command'] == 'write' and self.selected:
clean = data['content'].replace('\n', '\\n')
self.pipe_io.send_message(clean.encode())
def handle_pipe_event(self, message_bytes):
if not self.selected:
self.server_connector.send_message({
'key': 'mirror',
'data': {
'key': 'ide',
'data': {
'command': 'select',
'filename': self.filename
}
}
})
self.server_connector.send_message({
'key': 'mirror',
'data': {
'key': 'ide',
'data': {
'command': 'write',
'content': message_bytes.decode().replace('\\n', '\n')
}
}
})
self.server_connector.send_message({
'key': 'mirror',
'data': {
'key': 'ide',
'data': {
'command': 'read'
}
}
})
class FSMPipeIOEventHandler(PipeIOEventHandlerBase):
"""
Handles FSM steps.
When the FSM enters the next state, you will receive a line containing
its name. To trigger a state change, send the name of the transition to
the pipe.
"""
def __init__(self, in_pipe_path, out_pipe_path, permissions=DEFAULT_PERMISSIONS):
super().__init__(
['fsm', 'fsm_update'],
in_pipe_path, out_pipe_path, permissions
)
def handle_event(self, message):
if 'current_state' in message['data']:
self.pipe_io.send_message(message['data']['current_state'].encode())
def handle_pipe_event(self, message_bytes):
self.server_connector.send_message({
'key': '',
'trigger': message_bytes.decode()
})

View File

@ -3,17 +3,10 @@ from sys import stderr
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tfw.builtins import PipeIOEventHandler from tfw.components.pipe_io import PipeIOHandler
from tfw.config import TFWENV from tfw.config import TFWENV
from tfw.logging import Log, Logger, LogFormatter, VerboseLogFormatter from tfw.logging import Log, Logger, LogFormatter, VerboseLogFormatter
from tfw.main import EventHandlerFactory, setup_signal_handlers
from pipe_io_auxlib import (
SignMessagePipeIOEventHandler, VerifyMessagePipeIOEventHandler,
BotPipeIOEventHandler,
DeployPipeIOEventHandler, IdePipeIOEventHandler,
FSMPipeIOEventHandler
)
from signal_handling import setup_signal_handlers
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -25,46 +18,12 @@ def main():
Log(TFWENV.LOGFILE, VerboseLogFormatter()) Log(TFWENV.LOGFILE, VerboseLogFormatter())
]).start() ]).start()
json_pipe = PipeIOEventHandler( eh_factory = EventHandlerFactory()
'',
'/tmp/tfw_json_send',
'/tmp/tfw_json_recv'
)
sign_pipe = SignMessagePipeIOEventHandler( json_pipe_eh = eh_factory.build(PipeIOHandler(
'/tmp/tfw_sign_send', '/tmp/tfw_send',
'/tmp/tfw_sign_recv', '/tmp/tfw_recv'
forwarding=True ))
)
verify_pipe = VerifyMessagePipeIOEventHandler(
'/tmp/tfw_verify_send',
'/tmp/tfw_verify_recv'
)
bot_pipe = BotPipeIOEventHandler(
'/tmp/tfw_bot_send',
'/tmp/tfw_bot_recv',
permissions=0o666
)
deploy_pipe = DeployPipeIOEventHandler(
'/tmp/tfw_deploy_send',
'/tmp/tfw_deploy_recv',
'webservice'
)
ide_pipe = IdePipeIOEventHandler(
'/tmp/tfw_ide_send',
'/tmp/tfw_ide_recv',
'user_ops.py',
selected=True
)
fsm_pipe = FSMPipeIOEventHandler(
'/tmp/tfw_fsm_send',
'/tmp/tfw_fsm_recv'
)
setup_signal_handlers() setup_signal_handlers()
IOLoop.instance().start() IOLoop.instance().start()

View File

@ -1,11 +0,0 @@
from signal import signal, SIGTERM, SIGINT
from tfw.builtins import EventHandler
def setup_signal_handlers():
def stop(*_):
EventHandler.stop_all_instances()
exit(0)
signal(SIGTERM, stop)
signal(SIGINT, stop)

View File

@ -3,8 +3,8 @@
from os.path import exists from os.path import exists
from tfw.fsm import LinearFSM from tfw.fsm import LinearFSM
from tfw.components import MessageSender from tfw.components.frontend import MessageSender
from tfw.builtins import TFWServerUplinkConnector from tfw.main import TFWUplinkConnector
class TestFSM(LinearFSM): class TestFSM(LinearFSM):
@ -12,7 +12,7 @@ class TestFSM(LinearFSM):
def __init__(self): def __init__(self):
super().__init__(6) super().__init__(6)
self.uplink = TFWServerUplinkConnector() self.uplink = TFWUplinkConnector()
self.message_sender = MessageSender(self.uplink) self.message_sender = MessageSender(self.uplink)
self.subscribe_predicate('step_3', self.step_3_allowed) self.subscribe_predicate('step_3', self.step_3_allowed)