diff --git a/solvable/src/pipe_io_auxlib.py b/solvable/src/pipe_io_auxlib.py new file mode 100644 index 0000000..37bb1a8 --- /dev/null +++ b/solvable/src/pipe_io_auxlib.py @@ -0,0 +1,154 @@ +from json import dumps, loads +from tfw.crypto import KeyManager, sign_message, verify_message +from tfw.components import PipeIOEventHandlerBase +from tfw.components.pipe_io_event_handler import DEFAULT_PERMISSIONS + +class DeployPipeIOEventHandler(PipeIOEventHandlerBase): + # 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(self.onsuccess) + elif message_bytes == b'false': + self.server_connector.send(self.onerror) + else: + raise ValueError( + f'{self.pipe_io.in_pipe}: Expected "true" or "false".' + ) + +class IdePipeIOEventHandler(PipeIOEventHandlerBase): + # 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({ + 'key': 'mirror', + 'data': { + 'key': 'ide', + 'data': { + 'command': 'select', + 'filename': self.filename + } + } + }) + + self.server_connector.send({ + 'key': 'mirror', + 'data': { + 'key': 'ide', + 'data': { + 'command': 'write', + 'content': message_bytes.decode().replace('\\n', '\n') + } + } + }) + self.server_connector.send({ + 'key': 'mirror', + 'data': { + 'key': 'ide', + 'data': { + 'command': 'read' + } + } + }) + +class FSMPipeIOEventHandler(PipeIOEventHandlerBase): + 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({ + 'key': '', + 'trigger': message_bytes.decode() + }) + +class MsgSignPipeIOEventHandler(PipeIOEventHandlerBase): + 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): + return + + 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) + self.pipe_io.send_message(dumps(message).encode()) + +class MsgVerifyPipeIOEventHandler(PipeIOEventHandlerBase): + 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): + return + + 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()) diff --git a/solvable/src/pipe_io_main.py b/solvable/src/pipe_io_main.py index 26e7349..31e3271 100644 --- a/solvable/src/pipe_io_main.py +++ b/solvable/src/pipe_io_main.py @@ -5,12 +5,85 @@ from tornado.ioloop import IOLoop from tfw import EventHandlerBase from tfw.components import PipeIOEventHandler +from pipe_io_auxlib import ( + MsgSignPipeIOEventHandler, MsgVerifyPipeIOEventHandler, + DeployPipeIOEventHandler, IdePipeIOEventHandler, + FSMPipeIOEventHandler +) + if __name__ == '__main__': - pipe_io = PipeIOEventHandler( + ''' + Creates general purpose pipes. + You can send/receive JSON messages to/from the TFW server as any user. + ''' + json_pipe = PipeIOEventHandler( '', - '/tmp/tfw_send', - '/tmp/tfw_recv' + '/tmp/tfw_json_send', + '/tmp/tfw_json_recv', + permissions=0o666 + ) + + ''' + Signs a valid TFW message with HMAC. + Note that this 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. + ''' + sign_pipe = MsgSignPipeIOEventHandler( + '/tmp/tfw_sign_send', + '/tmp/tfw_sign_recv', + forwarding=True + ) + + ''' + 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. + ''' + verify_pipe = MsgVerifyPipeIOEventHandler( + '/tmp/tfw_verify_send', + '/tmp/tfw_verify_recv' + ) + + ''' + 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 third parameter is the name of the supervised service. + ''' + deploy_pipe = DeployPipeIOEventHandler( + '/tmp/tfw_deploy_send', + '/tmp/tfw_deploy_recv', + 'webservice' + ) + + ''' + 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, but it will track it + later on. + ''' + ide_pipe = IdePipeIOEventHandler( + '/tmp/tfw_ide_send', + '/tmp/tfw_ide_recv', + 'user_ops.py', + selected=True + ) + + ''' + 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. + ''' + fsm_pipe = FSMPipeIOEventHandler( + '/tmp/tfw_fsm_send', + '/tmp/tfw_fsm_recv' ) event_handlers = EventHandlerBase.get_local_instances()