mirror of
https://github.com/avatao-content/baseimage-tutorial-framework
synced 2025-06-29 13:16:23 +00:00
Rework whole TFW networking model
This commit is contained in:
@ -1,114 +1,28 @@
|
||||
# Copyright (C) 2018 Avatao.com Innovative Learning Kft.
|
||||
# All Rights Reserved. See LICENSE file for details.
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import suppress
|
||||
|
||||
from tornado.web import Application
|
||||
|
||||
from tfw.networking.server.zmq_websocket_proxy import ZMQWebSocketProxy
|
||||
from tfw.networking.event_handlers.server_connector import ServerUplinkConnector
|
||||
from tfw.networking.server.event_handler_connector import EventHandlerConnector
|
||||
from tfw.networking.message_sender import MessageSender
|
||||
from tfw.networking.fsm_aware import FSMAware
|
||||
from tfw.crypto import KeyManager, verify_message, sign_message
|
||||
from tfw.config.logs import logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TFWServer(FSMAware):
|
||||
class TFWServer:
|
||||
"""
|
||||
This class handles the proxying of messages between the frontend and event handers.
|
||||
It proxies messages from the "/ws" route to all event handlers subscribed to a ZMQ
|
||||
SUB socket.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._event_handler_connector = EventHandlerConnector()
|
||||
self._uplink_connector = ServerUplinkConnector()
|
||||
self._auth_key = KeyManager().auth_key
|
||||
|
||||
self.application = Application([(
|
||||
r'/ws', ZMQWebSocketProxy, {
|
||||
'event_handler_connector': self._event_handler_connector,
|
||||
'proxy_filters_and_callbacks': {
|
||||
'message_handlers': [
|
||||
self.handle_trigger,
|
||||
self.handle_recover,
|
||||
self.handle_fsm_update
|
||||
],
|
||||
'frontend_message_handlers': [self.save_frontend_messages]
|
||||
}
|
||||
}
|
||||
)])
|
||||
|
||||
self._frontend_messages = FrontendMessageStorage()
|
||||
|
||||
def handle_trigger(self, message):
|
||||
if 'trigger' in message:
|
||||
LOG.debug('Executing handler for trigger "%s"', message.get('trigger', ''))
|
||||
fsm_eh_command = {
|
||||
'key': 'fsm',
|
||||
'data': {
|
||||
'command': 'trigger',
|
||||
'value': message['trigger']
|
||||
}
|
||||
}
|
||||
if verify_message(self._auth_key, message):
|
||||
sign_message(self._auth_key, fsm_eh_command)
|
||||
self._uplink_connector.send_to_eventhandler(fsm_eh_command)
|
||||
|
||||
def handle_recover(self, message):
|
||||
if message['key'] == 'recover':
|
||||
self._frontend_messages.replay_messages(self._uplink_connector)
|
||||
self._frontend_messages.clear()
|
||||
|
||||
def handle_fsm_update(self, message):
|
||||
self.update_fsm_data(message)
|
||||
|
||||
def save_frontend_messages(self, message):
|
||||
self._frontend_messages.save_message(message)
|
||||
|
||||
def listen(self, port):
|
||||
self.application.listen(port)
|
||||
|
||||
|
||||
class MessageStorage(ABC):
|
||||
def __init__(self):
|
||||
self.saved_messages = []
|
||||
|
||||
def save_message(self, message):
|
||||
with suppress(KeyError, AttributeError):
|
||||
if self.filter_message(message):
|
||||
self.saved_messages.extend(self.transform_message(message))
|
||||
|
||||
@abstractmethod
|
||||
def filter_message(self, message):
|
||||
raise NotImplementedError
|
||||
|
||||
def transform_message(self, message): # pylint: disable=no-self-use
|
||||
yield message
|
||||
|
||||
def clear(self):
|
||||
self.saved_messages.clear()
|
||||
|
||||
|
||||
class FrontendMessageStorage(MessageStorage):
|
||||
def filter_message(self, message):
|
||||
key = message['key']
|
||||
command = message.get('data', {}).get('command')
|
||||
return (
|
||||
key in ('message', 'dashboard', 'queueMessages')
|
||||
or key == 'ide' and command in ('select', 'read')
|
||||
)
|
||||
|
||||
def transform_message(self, message):
|
||||
if message['key'] == 'queueMessages':
|
||||
yield from MessageSender.generate_messages_from_queue(message)
|
||||
else:
|
||||
yield message
|
||||
|
||||
def replay_messages(self, connector):
|
||||
for message in self.saved_messages:
|
||||
connector.send(message)
|
||||
|
@ -5,7 +5,7 @@ import json
|
||||
|
||||
from tornado.websocket import WebSocketHandler
|
||||
|
||||
from tfw.mixins.callback_mixin import CallbackMixin
|
||||
from tfw.networking.event_handlers.server_connector import Scope
|
||||
from tfw.config.logs import logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -14,38 +14,18 @@ LOG = logging.getLogger(__name__)
|
||||
class ZMQWebSocketProxy(WebSocketHandler):
|
||||
# pylint: disable=abstract-method
|
||||
instances = set()
|
||||
sequence_number = 0
|
||||
|
||||
def initialize(self, **kwargs): # pylint: disable=arguments-differ
|
||||
self._event_handler_connector = kwargs['event_handler_connector']
|
||||
self._proxy_filters_and_callbacks = kwargs.get('proxy_filters_and_callbacks', {})
|
||||
self.event_handler_connector = kwargs['event_handler_connector']
|
||||
self.tfw_router = TFWRouter(self.send_to_zmq, self.send_to_websockets)
|
||||
|
||||
self.proxy_eventhandler_to_websocket = TFWProxy(
|
||||
self.send_eventhandler_message,
|
||||
self.send_websocket_message
|
||||
)
|
||||
self.proxy_websocket_to_eventhandler = TFWProxy(
|
||||
self.send_websocket_message,
|
||||
self.send_eventhandler_message
|
||||
)
|
||||
def send_to_zmq(self, message):
|
||||
self.event_handler_connector.send_message(message)
|
||||
|
||||
self.subscribe_proxy_callbacks()
|
||||
|
||||
def subscribe_proxy_callbacks(self):
|
||||
eventhandler_message_handlers = self._proxy_filters_and_callbacks.get('eventhandler_message_handlers', [])
|
||||
frontend_message_handlers = self._proxy_filters_and_callbacks.get('frontend_message_handlers', [])
|
||||
message_handlers = self._proxy_filters_and_callbacks.get('message_handlers', [])
|
||||
proxy_filters = self._proxy_filters_and_callbacks.get('proxy_filters', [])
|
||||
|
||||
self.proxy_websocket_to_eventhandler.subscribe_proxy_callbacks_and_filters(
|
||||
eventhandler_message_handlers + message_handlers,
|
||||
proxy_filters
|
||||
)
|
||||
|
||||
self.proxy_eventhandler_to_websocket.subscribe_proxy_callbacks_and_filters(
|
||||
frontend_message_handlers + message_handlers,
|
||||
proxy_filters
|
||||
)
|
||||
@staticmethod
|
||||
def send_to_websockets(message):
|
||||
for instance in ZMQWebSocketProxy.instances:
|
||||
instance.write_message(message)
|
||||
|
||||
def prepare(self):
|
||||
ZMQWebSocketProxy.instances.add(self)
|
||||
@ -54,102 +34,39 @@ class ZMQWebSocketProxy(WebSocketHandler):
|
||||
ZMQWebSocketProxy.instances.remove(self)
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
LOG.debug('WebSocket connection initiated')
|
||||
self._event_handler_connector.register_callback(self.eventhander_callback)
|
||||
LOG.debug('WebSocket connection initiated!')
|
||||
self.event_handler_connector.register_callback(self.zmq_callback)
|
||||
|
||||
def eventhander_callback(self, message):
|
||||
"""
|
||||
Invoked on ZMQ messages from event handlers.
|
||||
"""
|
||||
self.sequence_message(message)
|
||||
LOG.debug('Received on pull socket: %s', message)
|
||||
self.proxy_eventhandler_to_websocket(message)
|
||||
|
||||
@classmethod
|
||||
def sequence_message(cls, message):
|
||||
cls.sequence_number += 1
|
||||
message['seq'] = cls.sequence_number
|
||||
def zmq_callback(self, message):
|
||||
LOG.debug('Received on ZMQ pull socket: %s', message)
|
||||
self.tfw_router.route(message)
|
||||
|
||||
def on_message(self, message):
|
||||
"""
|
||||
Invoked on WS messages from frontend.
|
||||
"""
|
||||
message = json.loads(message)
|
||||
self.sequence_message(message)
|
||||
LOG.debug('Received on WebSocket: %s', message)
|
||||
self.proxy_websocket_to_eventhandler(message)
|
||||
|
||||
def send_eventhandler_message(self, message):
|
||||
self._event_handler_connector.send_message(message)
|
||||
|
||||
@staticmethod
|
||||
def send_websocket_message(message):
|
||||
for instance in ZMQWebSocketProxy.instances:
|
||||
instance.write_message(message)
|
||||
self.tfw_router.route(message)
|
||||
|
||||
# much secure, very cors, wow
|
||||
def check_origin(self, origin):
|
||||
return True
|
||||
|
||||
|
||||
class TFWProxy:
|
||||
# pylint: disable=protected-access
|
||||
def __init__(self, to_source, to_destination):
|
||||
self.to_source = to_source
|
||||
self.to_destination = to_destination
|
||||
class TFWRouter:
|
||||
def __init__(self, send_to_zmq, send_to_websockets):
|
||||
self.send_to_zmq = send_to_zmq
|
||||
self.send_to_websockets = send_to_websockets
|
||||
|
||||
self.proxy_filters = CallbackMixin()
|
||||
self.proxy_callbacks = CallbackMixin()
|
||||
def route(self, message):
|
||||
scope = Scope(message.get('scope', 'zmq'))
|
||||
|
||||
self.proxy_filters.subscribe_callback(self.validate_message)
|
||||
|
||||
self.keyhandlers = {
|
||||
'mirror': self.mirror,
|
||||
'broadcast': self.broadcast
|
||||
routing_table = {
|
||||
Scope.ZMQ: self.send_to_zmq,
|
||||
Scope.WEBSOCKET: self.send_to_websockets,
|
||||
Scope.BROADCAST: self.broadcast
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def validate_message(message):
|
||||
if 'key' not in message:
|
||||
raise ValueError('Invalid TFW message format!')
|
||||
|
||||
def __call__(self, message):
|
||||
if not self.filter_and_execute_callbacks(message):
|
||||
return
|
||||
|
||||
if message['key'] not in self.keyhandlers:
|
||||
self.to_destination(message)
|
||||
else:
|
||||
handler = self.keyhandlers[message['key']]
|
||||
try:
|
||||
handler(message)
|
||||
except KeyError:
|
||||
LOG.error('Invalid "%s" message format! Ignoring.', handler.__name__)
|
||||
|
||||
def filter_and_execute_callbacks(self, message):
|
||||
try:
|
||||
self.proxy_filters._execute_callbacks(message)
|
||||
self.proxy_callbacks._execute_callbacks(message)
|
||||
return True
|
||||
except ValueError:
|
||||
LOG.exception('Invalid TFW message received!')
|
||||
return False
|
||||
|
||||
def mirror(self, message):
|
||||
message = message['data']
|
||||
if not self.filter_and_execute_callbacks(message):
|
||||
return
|
||||
LOG.debug('Mirroring message: %s', message)
|
||||
self.to_source(message)
|
||||
action = routing_table[scope]
|
||||
action(message)
|
||||
|
||||
def broadcast(self, message):
|
||||
message = message['data']
|
||||
if not self.filter_and_execute_callbacks(message):
|
||||
return
|
||||
LOG.debug('Broadcasting message: %s', message)
|
||||
self.to_source(message)
|
||||
self.to_destination(message)
|
||||
|
||||
def subscribe_proxy_callbacks_and_filters(self, proxy_callbacks, proxy_filters):
|
||||
self.proxy_callbacks.subscribe_callbacks(*proxy_callbacks)
|
||||
self.proxy_filters.subscribe_callbacks(*proxy_filters)
|
||||
self.send_to_zmq(message)
|
||||
self.send_to_websockets(message)
|
||||
|
Reference in New Issue
Block a user