def test_twisted_client(self): reactor = SelectReactor() config = { "type": "twisted", "client_string": "tcp:host=127.0.0.1:port=9876", } endpoint = create_connecting_endpoint_from_config(config, self.cbdir, reactor, self.log) self.assertTrue(isinstance(endpoint, TCP4ClientEndpoint))
def onConnect(self, request): """ Incoming (frontend) WebSocket connection accepted. Forward connect to backend WebSocket server. """ self.log.debug( 'WebSocketReverseProxyServerProtocol.onConnect(request={request})', request=request) self.backend_config = self.factory.path_config['backend'] self.backend_factory = WebSocketReverseProxyClientFactory( frontend_protocol=self, frontend_request=request, url=self.backend_config.get('url', None)) self.backend_factory.noisy = False self.backend_protocol = None # create and connect client endpoint # endpoint = create_connecting_endpoint_from_config( self.backend_config['endpoint'], None, self.factory.reactor, self.log) backend_on_connect = internet.defer.Deferred() # now, actually connect the client # d = endpoint.connect(self.backend_factory) def on_connect_success(proto): self.log.debug( 'WebSocketReverseProxyServerProtocol.onConnect(..): connected') proto.backend_on_connect = backend_on_connect self.backend_protocol = proto def on_connect_error(err): deny = ConnectionDeny( ConnectionDeny.SERVICE_UNAVAILABLE, 'WebSocket reverse proxy backend not reachable') backend_on_connect.errback(deny) d.addCallbacks(on_connect_success, on_connect_error) return backend_on_connect
def onConnect(self, request): """ Incoming (frontend) WebSocket connection accepted. Forward connect to backend WebSocket server. """ self.log.debug('WebSocketReverseProxyServerProtocol.onConnect(request={request})', request=request) self.backend_config = self.factory.path_config['backend'] self.backend_factory = WebSocketReverseProxyClientFactory(frontend_protocol=self, frontend_request=request, url=self.backend_config.get('url', None)) self.backend_factory.noisy = False self.backend_protocol = None # create and connect client endpoint # endpoint = create_connecting_endpoint_from_config(self.backend_config[u'endpoint'], None, self.factory.reactor, self.log) backend_on_connect = internet.defer.Deferred() # now, actually connect the client # d = endpoint.connect(self.backend_factory) def on_connect_success(proto): self.log.debug('WebSocketReverseProxyServerProtocol.onConnect(..): connected') proto.backend_on_connect = backend_on_connect self.backend_protocol = proto def on_connect_error(err): deny = ConnectionDeny(ConnectionDeny.SERVICE_UNAVAILABLE, u'WebSocket reverse proxy backend not reachable') backend_on_connect.errback(deny) d.addCallbacks(on_connect_success, on_connect_error) return backend_on_connect
def test_tls_auth_denied(self): """ A MQTT client offering the wrong certificate won't be authenticated. """ reactor, router, server_factory, session_factory = build_mqtt_server() real_reactor = selectreactor.SelectReactor() logger = make_logger() session, pump = connect_application_session( server_factory, ObservingSession, component_config=ComponentConfig(realm=u"mqtt")) endpoint = create_listening_endpoint_from_config({ "type": "tcp", "port": 1099, "interface": "0.0.0.0", "tls": { "certificate": "server.crt", "key": "server.key", "dhparam": "dhparam", "ca_certificates": [ "ca.cert.pem", "intermediate.cert.pem" ]}, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) client_endpoint = create_connecting_endpoint_from_config({ "type": "tcp", "host": "127.0.0.1", "port": 1099, "tls": { # BAD key: trusted by the CA, but wrong ID "certificate": "client_1.crt", "hostname": u"localhost", "key": "client_1.key", "ca_certificates": [ "ca.cert.pem", "intermediate.cert.pem" ]}, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) p = [] l = endpoint.listen(server_factory) class TestProtocol(Protocol): data = b"" expected = ( ConnACK(session_present=False, return_code=1).serialise()) def dataReceived(self_, data): self_.data = self_.data + data if len(self_.data) == len(self_.expected): self.assertEqual(self_.data, self_.expected) real_reactor.stop() @l.addCallback def _listening(factory): d = client_endpoint.connect(Factory.forProtocol(TestProtocol)) @d.addCallback def _(proto): p.append(proto) proto.transport.write( Connect(client_id=u"test123", flags=ConnectFlags(clean_session=False)).serialise()) proto.transport.write( Publish(duplicate=False, qos_level=1, retain=False, topic_name=u"test", payload=b"{}", packet_identifier=1).serialise()) lc = LoopingCall(pump.flush) lc.clock = real_reactor lc.start(0.01) def timeout(): print("Timing out :(") real_reactor.stop() print(self.logs.log_text.getvalue()) # Timeout, just in case real_reactor.callLater(10, timeout) real_reactor.run() client_protocol = p[0] # We get a CONNECT self.assertEqual(client_protocol.data, ConnACK(session_present=False, return_code=1).serialise()) client_protocol.data = b"" pump.flush() # No events! self.assertEqual(len(session.events), 0)
def start_component(self, component_id, config, reload_modules=False, details=None): """ Starts a component in this container worker. :param component_id: The ID under which to start the component. :type component_id: str :param config: Component configuration. :type config: dict :param reload_modules: If `True`, enforce reloading of modules (user code) that were modified (see: TrackingModuleReloader). :type reload_modules: bool :param details: Caller details. :type details: instance of :class:`autobahn.wamp.types.CallDetails` :returns: Component startup information. :rtype: dict """ self.log.debug(u'{klass}.start_component({component_id}, {config})', klass=self.__class__.__name__, component_id=component_id, config=config) # prohibit starting a component twice # if component_id in self.components: emsg = u'duplicate component "{}" - a component with this ID is already running (or starting)'.format(component_id) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.already_running', emsg) # check component configuration # try: self.personality.check_container_component(self.personality, config) except Exception as e: emsg = u'invalid container component configuration: {}'.format(e) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.invalid_configuration', emsg) else: self.log.debug(u'starting component "{component_id}" ..', component_id=component_id) # WAMP application component factory # realm = config.get(u'realm', None) extra = config.get(u'extra', None) controller = self if self.config.extra.expose_controller else None shared = self.components_shared if self.config.extra.expose_shared else None component_config = ComponentConfig(realm=realm, extra=extra, keyring=None, controller=controller, shared=shared) try: create_component = _appsession_loader(config) except ApplicationError as e: # for convenience, also log failed component loading self.log.error(u'component loading failed', log_failure=Failure()) if u'No module named' in str(e): self.log.error(u' Python module search paths:') for path in e.kwargs['pythonpath']: self.log.error(u' {path}', path=path) raise # force reload of modules (user code) # if reload_modules: self._module_tracker.reload() # prepare some cleanup code this connection goes away def _closed(session, was_clean): """ This is moderate hack around the fact that we don't have any way to "listen" for a close event on websocket or rawsocket objects. Also, the rawsocket implementation doesn't have "a" function we can wrap anyway (they are asyncio vs Twisted specific), so for both WebSocket and rawsocket cases, we actually listen on the WAMP session for transport close notifications. Ideally we'd listen for "close" on the transport but this works fine for cleaning up the components. """ if component_id not in self.components: self.log.warn( "Component '{id}' closed, but not in set.", id=component_id, ) return if was_clean: self.log.info( "Closed connection to '{id}'", id=component_id, ) else: self.log.error( "Lost connection to component '{id}' uncleanly", id=component_id, ) component = self.components[component_id] del self.components[component_id] self._publish_component_stop(component) component._stopped.callback(component.marshal()) del component # figure out if we need to shut down the container itself or not if not self.components: if self._exit_mode == self.SHUTDOWN_ON_LAST_COMPONENT_STOPPED: self.log.info( "Container is hosting no more components: stopping container in exit mode <{exit_mode}> ...", exit_mode=self._exit_mode, ) self.shutdown() else: self.log.info( "Container is hosting no more components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, ) else: self.log.info( "Container is still hosting {component_count} components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, component_count=len(self.components), ) # WAMP application session factory # def create_session(): try: session = create_component(component_config) # any exception spilling out from user code in onXXX handlers is fatal! def panic(fail, msg): self.log.error( "Fatal error in component: {msg} - {log_failure.value}", msg=msg, log_failure=fail, ) session.disconnect() session._swallow_error = panic # see note above, for _closed -- we should be # listening for "the transport was closed", but # "session disconnect" is close enough (since there # are no "proper events" from websocket/rawsocket # implementations). session.on('disconnect', _closed) return session except Exception: self.log.failure(u'component instantiation failed: {log_failure.value}') raise # WAMP transport factory # transport_config = config[u'transport'] if transport_config[u'type'] == u'websocket': # create a WAMP-over-WebSocket transport client factory transport_factory = WampWebSocketClientFactory(create_session, transport_config[u'url']) transport_factory.noisy = False elif transport_config[u'type'] == u'rawsocket': transport_factory = WampRawSocketClientFactory(create_session, transport_config) transport_factory.noisy = False else: # should not arrive here, since we did check the config before raise Exception(u'logic error') # create and connect client endpoint # endpoint = create_connecting_endpoint_from_config(transport_config[u'endpoint'], self.config.extra.cbdir, self._reactor, self.log) # now, actually connect the client # d = endpoint.connect(transport_factory) def on_connect_success(proto): component = ContainerComponent(component_id, config, proto, None) self.components[component_id] = component # publish event "on_component_start" to all but the caller # uri = self._uri_prefix + u'.on_component_started' component_started = { u'id': component_id, u'config': config } self.publish(uri, component_started, options=PublishOptions(exclude=details.caller)) return component_started def on_connect_error(err): # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html if isinstance(err.value, internet.error.ConnectError): emsg = u'could not connect container component to router - transport establishment failed ({})'.format(err.value) self.log.warn(emsg) raise ApplicationError(u'crossbar.error.cannot_connect', emsg) else: # should not arrive here (since all errors arriving here # should be subclasses of ConnectError) raise err d.addCallbacks(on_connect_success, on_connect_error) return d
def start_component(self, component_id, config, reload_modules=False, details=None): """ Starts a component in this container worker. :param component_id: The ID under which to start the component. :type component_id: str :param config: Component configuration. :type config: dict :param reload_modules: If `True`, enforce reloading of modules (user code) that were modified (see: TrackingModuleReloader). :type reload_modules: bool :param details: Caller details. :type details: instance of :class:`autobahn.wamp.types.CallDetails` :returns: Component startup information. :rtype: dict """ self.log.debug(u'{klass}.start_component({component_id}, {config})', klass=self.__class__.__name__, component_id=component_id, config=config) # prohibit starting a component twice # if component_id in self.components: emsg = u'duplicate component "{}" - a component with this ID is already running (or starting)'.format( component_id) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.already_running', emsg) # check component configuration # try: self.personality.check_container_component(self.personality, config) except Exception as e: emsg = u'invalid container component configuration: {}'.format(e) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.invalid_configuration', emsg) else: self.log.debug(u'starting component "{component_id}" ..', component_id=component_id) # WAMP application component factory # realm = config.get(u'realm', None) assert type(realm) == str extra = config.get(u'extra', {}) assert type(extra) == dict # forward crossbar node base directory extra['cbdir'] = self.config.extra.cbdir # allow access to controller session controller = self if self.config.extra.expose_controller else None # expose an object shared between components shared = self.components_shared if self.config.extra.expose_shared else None # this is the component configuration provided to the components ApplicationSession component_config = ComponentConfig(realm=realm, extra=extra, keyring=None, controller=controller, shared=shared) # define component ctor function try: create_component = _appsession_loader(config) except ApplicationError as e: # for convenience, also log failed component loading self.log.error(u'component loading failed', log_failure=Failure()) if u'No module named' in str(e): self.log.error(u' Python module search paths:') for path in e.kwargs['pythonpath']: self.log.error(u' {path}', path=path) raise # force reload of modules (user code) # if reload_modules: self._module_tracker.reload() # prepare some cleanup code in case this connection goes away def _closed(session, was_clean): """ This is moderate hack around the fact that we don't have any way to "listen" for a close event on websocket or rawsocket objects. Also, the rawsocket implementation doesn't have "a" function we can wrap anyway (they are asyncio vs Twisted specific), so for both WebSocket and rawsocket cases, we actually listen on the WAMP session for transport close notifications. Ideally we'd listen for "close" on the transport but this works fine for cleaning up the components. """ if component_id not in self.components: self.log.warn( "Component '{id}' closed, but not in set.", id=component_id, ) return if was_clean: self.log.info( "Closed connection to '{id}'", id=component_id, ) else: self.log.error( "Lost connection to component '{id}' uncleanly", id=component_id, ) component = self.components[component_id] del self.components[component_id] self._publish_component_stop(component) component._stopped.callback(component.marshal()) del component # figure out if we need to shut down the container itself or not if not was_clean and self._exit_mode == self.SHUTDOWN_ON_ANY_COMPONENT_FAILED: self.log.info( "A component has failed: stopping container in exit mode <{exit_mode}> ...", exit_mode=self._exit_mode, ) self.shutdown() return if self._exit_mode == self.SHUTDOWN_ON_ANY_COMPONENT_STOPPED: self.log.info( "A component has stopped: stopping container in exit mode <{exit_mode}> ...", exit_mode=self._exit_mode, ) self.shutdown() return if not self.components: if self._exit_mode == self.SHUTDOWN_ON_LAST_COMPONENT_STOPPED: self.log.info( "Container is hosting no more components: stopping container in exit mode <{exit_mode}> ...", exit_mode=self._exit_mode, ) self.shutdown() return else: self.log.info( "Container is hosting no more components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, ) else: self.log.info( "Container is still hosting {component_count} components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, component_count=len(self.components), ) # determine if we should re-start the component. Note that # we can only arrive here if we *didn't* decide to # shutdown above .. so if we have a shutdown mode of # SHUTDOWN_ON_ANY_COMPONENT_STOPPED will mean we never try # to re-start anything. if self._restart_mode and self._restart_mode != self.RESTART_NEVER: def restart_component(): # Think: if this below start_component() fails, # we'll still schedule *exactly one* new re-start # attempt for it, right? self.log.info( "Restarting component '{component_id}'", component_id=component_id, ) return self.start_component( component_id, config, reload_modules=reload_modules, details=details, ) # note we must yield to the reactor with # callLater(0, ..) to avoid infinite recurision if # we're stuck in a restart loop from twisted.internet import reactor if self._restart_mode == self.RESTART_ALWAYS: reactor.callLater(0, restart_component) elif self._restart_mode == self.RESTART_FAILED and not was_clean: reactor.callLater(0, restart_component) joined_d = Deferred() # WAMP application session factory # def create_session(): try: session = create_component(component_config) # any exception spilling out from user code in onXXX handlers is fatal! def panic(fail, msg): self.log.error( "Fatal error in component: {msg} - {log_failure.value}", msg=msg, log_failure=fail, ) session.disconnect() session._swallow_error = panic # see note above, for _closed -- we should be # listening for "the transport was closed", but # "session disconnect" is close enough (since there # are no "proper events" from websocket/rawsocket # implementations). session.on('disconnect', _closed) # note, "ready" here means: onJoin and any on('join', # ..) handlers have all completed successfully. This # is necessary for container-components (as opposed to # router-components) to work as expected def _ready(s): joined_d.callback(None) session.on('ready', _ready) def _left(s, details): if not joined_d.called: joined_d.errback( ApplicationError( details.reason, details.message, )) session.on('leave', _left) return session except Exception: self.log.failure( u'component instantiation failed: {log_failure.value}') raise # WAMP transport factory # transport_config = config[u'transport'] if transport_config[u'type'] == u'websocket': # create a WAMP-over-WebSocket transport client factory transport_factory = WampWebSocketClientFactory( create_session, transport_config[u'url']) transport_factory.noisy = False if 'options' in transport_config: set_websocket_options(transport_factory, transport_config['options']) elif transport_config[u'type'] == u'rawsocket': transport_factory = WampRawSocketClientFactory( create_session, transport_config) transport_factory.noisy = False if 'options' in transport_config: set_rawsocket_options(transport_factory, transport_config['options']) else: # should not arrive here, since we did check the config before raise Exception(u'logic error') # create and connect client endpoint # endpoint = create_connecting_endpoint_from_config( transport_config[u'endpoint'], self.config.extra.cbdir, self._reactor, self.log) # now, actually connect the client # d = endpoint.connect(transport_factory) def on_connect_success(proto): component = ContainerComponent(component_id, config, proto, None) self.components[component_id] = component # publish event "on_component_start" to all but the caller # uri = self._uri_prefix + u'.on_component_started' component_started = {u'id': component_id, u'config': config} self.publish(uri, component_started, options=PublishOptions(exclude=details.caller)) return component_started def on_connect_error(err): # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html if isinstance(err.value, internet.error.ConnectError): emsg = u'could not connect container component to router - transport establishment failed ({})'.format( err.value) self.log.warn(emsg) raise ApplicationError(u'crossbar.error.cannot_connect', emsg) else: # should not arrive here (since all errors arriving here # should be subclasses of ConnectError) raise err def await_join(arg): """ We don't want to consider this component working until its on_join has completed (see create_session() above where this is hooked up) """ return joined_d d.addCallbacks(on_connect_success, on_connect_error) d.addCallback(await_join) return d
def start_component(self, component_id, config, reload_modules=False, details=None): """ Starts a component in this container worker. :param component_id: The ID under which to start the component. :type component_id: str :param config: Component configuration. :type config: dict :param reload_modules: If `True`, enforce reloading of modules (user code) that were modified (see: TrackingModuleReloader). :type reload_modules: bool :param details: Caller details. :type details: instance of :class:`autobahn.wamp.types.CallDetails` :returns: Component startup information. :rtype: dict """ self.log.debug(u'{klass}.start_component({component_id}, {config})', klass=self.__class__.__name__, component_id=component_id, config=config) # prohibit starting a component twice # if component_id in self.components: emsg = u'duplicate component "{}" - a component with this ID is already running (or starting)'.format(component_id) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.already_running', emsg) # check component configuration # try: self.personality.check_container_component(self.personality, config) except Exception as e: emsg = u'invalid container component configuration: {}'.format(e) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.invalid_configuration', emsg) else: self.log.debug(u'starting component "{component_id}" ..', component_id=component_id) # WAMP application component factory # realm = config.get(u'realm', None) assert type(realm) == str extra = config.get(u'extra', {}) assert type(extra) == dict # forward crossbar node base directory extra['cbdir'] = self.config.extra.cbdir # allow access to controller session controller = self if self.config.extra.expose_controller else None # expose an object shared between components shared = self.components_shared if self.config.extra.expose_shared else None # this is the component configuration provided to the components ApplicationSession component_config = ComponentConfig(realm=realm, extra=extra, keyring=None, controller=controller, shared=shared) # define component ctor function try: create_component = _appsession_loader(config) except ApplicationError as e: # for convenience, also log failed component loading self.log.error(u'component loading failed', log_failure=Failure()) if u'No module named' in str(e): self.log.error(u' Python module search paths:') for path in e.kwargs['pythonpath']: self.log.error(u' {path}', path=path) raise # force reload of modules (user code) # if reload_modules: self._module_tracker.reload() # prepare some cleanup code this connection goes away def _closed(session, was_clean): """ This is moderate hack around the fact that we don't have any way to "listen" for a close event on websocket or rawsocket objects. Also, the rawsocket implementation doesn't have "a" function we can wrap anyway (they are asyncio vs Twisted specific), so for both WebSocket and rawsocket cases, we actually listen on the WAMP session for transport close notifications. Ideally we'd listen for "close" on the transport but this works fine for cleaning up the components. """ if component_id not in self.components: self.log.warn( "Component '{id}' closed, but not in set.", id=component_id, ) return if was_clean: self.log.info( "Closed connection to '{id}'", id=component_id, ) else: self.log.error( "Lost connection to component '{id}' uncleanly", id=component_id, ) component = self.components[component_id] del self.components[component_id] self._publish_component_stop(component) component._stopped.callback(component.marshal()) del component # figure out if we need to shut down the container itself or not if not self.components: if self._exit_mode == self.SHUTDOWN_ON_LAST_COMPONENT_STOPPED: self.log.info( "Container is hosting no more components: stopping container in exit mode <{exit_mode}> ...", exit_mode=self._exit_mode, ) self.shutdown() else: self.log.info( "Container is hosting no more components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, ) else: self.log.info( "Container is still hosting {component_count} components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, component_count=len(self.components), ) # WAMP application session factory # def create_session(): try: session = create_component(component_config) # any exception spilling out from user code in onXXX handlers is fatal! def panic(fail, msg): self.log.error( "Fatal error in component: {msg} - {log_failure.value}", msg=msg, log_failure=fail, ) session.disconnect() session._swallow_error = panic # see note above, for _closed -- we should be # listening for "the transport was closed", but # "session disconnect" is close enough (since there # are no "proper events" from websocket/rawsocket # implementations). session.on('disconnect', _closed) return session except Exception: self.log.failure(u'component instantiation failed: {log_failure.value}') raise # WAMP transport factory # transport_config = config[u'transport'] if transport_config[u'type'] == u'websocket': # create a WAMP-over-WebSocket transport client factory transport_factory = WampWebSocketClientFactory(create_session, transport_config[u'url']) transport_factory.noisy = False elif transport_config[u'type'] == u'rawsocket': transport_factory = WampRawSocketClientFactory(create_session, transport_config) transport_factory.noisy = False else: # should not arrive here, since we did check the config before raise Exception(u'logic error') # create and connect client endpoint # endpoint = create_connecting_endpoint_from_config(transport_config[u'endpoint'], self.config.extra.cbdir, self._reactor, self.log) # now, actually connect the client # d = endpoint.connect(transport_factory) def on_connect_success(proto): component = ContainerComponent(component_id, config, proto, None) self.components[component_id] = component # publish event "on_component_start" to all but the caller # uri = self._uri_prefix + u'.on_component_started' component_started = { u'id': component_id, u'config': config } self.publish(uri, component_started, options=PublishOptions(exclude=details.caller)) return component_started def on_connect_error(err): # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html if isinstance(err.value, internet.error.ConnectError): emsg = u'could not connect container component to router - transport establishment failed ({})'.format(err.value) self.log.warn(emsg) raise ApplicationError(u'crossbar.error.cannot_connect', emsg) else: # should not arrive here (since all errors arriving here # should be subclasses of ConnectError) raise err d.addCallbacks(on_connect_success, on_connect_error) return d
def start_link(self, link_id, link_config, caller): assert type(link_id) == str assert isinstance(link_config, RLinkConfig) assert isinstance(caller, SessionIdent) if link_id in self._links: raise ApplicationError('crossbar.error.already_running', 'router link {} already running'.format(link_id)) # setup local session # local_extra = { 'other': None, 'on_ready': Deferred(), 'rlink': link_id, 'forward_events': link_config.forward_local_events, 'forward_invocations': link_config.forward_local_invocations, } local_realm = self._realm.config['name'] local_authid = link_config.authid or util.generate_serial_number() local_authrole = 'trusted' local_config = ComponentConfig(local_realm, local_extra) local_session = RLinkLocalSession(local_config) # setup remote session # remote_extra = { 'rlink_manager': self, 'other': None, 'on_ready': Deferred(), 'authid': link_config.authid, 'exclude_authid': link_config.exclude_authid, 'forward_events': link_config.forward_remote_events, 'forward_invocations': link_config.forward_remote_invocations, } remote_realm = link_config.realm remote_config = ComponentConfig(remote_realm, remote_extra) remote_session = RLinkRemoteSession(remote_config) # cross-connect the two sessions # local_extra['other'] = remote_session remote_extra['other'] = local_session # the rlink # rlink = RLink(link_id, link_config) self._links[link_id] = rlink local_extra['tracker'] = rlink # create connecting client endpoint # connecting_endpoint = create_connecting_endpoint_from_config( link_config.transport['endpoint'], self._controller.cbdir, self._controller._reactor, self.log) try: # connect the local session # self._realm.controller._router_session_factory.add( local_session, self._realm.router, authid=local_authid, authrole=local_authrole, authextra=local_extra) yield local_extra['on_ready'] # connect the remote session # # remote connection parameters to ApplicationRunner: # # url: The WebSocket URL of the WAMP router to connect to (e.g. ws://somehost.com:8090/somepath) # realm: The WAMP realm to join the application session to. # extra: Optional extra configuration to forward to the application component. # serializers: List of :class:`autobahn.wamp.interfaces.ISerializer` (or None for default serializers). # ssl: None or :class:`twisted.internet.ssl.CertificateOptions` # proxy: Explicit proxy server to use; a dict with ``host`` and ``port`` keys # headers: Additional headers to send (only applies to WAMP-over-WebSocket). # max_retries: Maximum number of reconnection attempts. Unlimited if set to -1. # initial_retry_delay: Initial delay for reconnection attempt in seconds (Default: 1.0s). # max_retry_delay: Maximum delay for reconnection attempts in seconds (Default: 60s). # retry_delay_growth: The growth factor applied to the retry delay between reconnection attempts (Default 1.5). # retry_delay_jitter: A 0-argument callable that introduces nose into the delay. (Default random.random) # remote_runner = ApplicationRunner( url=link_config.transport['url'], realm=remote_realm, extra=remote_extra) yield remote_runner.run( remote_session, start_reactor=False, auto_reconnect=True, endpoint=connecting_endpoint, reactor=self._controller._reactor) yield remote_extra['on_ready'] except: # make sure to remove the half-initialized link from our map .. del self._links[link_id] # .. and then re-raise raise # the router link is established: store final infos rlink.started = time_ns() rlink.started_by = caller rlink.local = local_session rlink.remote = remote_session return rlink
def _test_tls_auth(self): """ A MQTT client can connect using mutually authenticated TLS authentication. """ reactor, router, server_factory, session_factory = build_mqtt_server() real_reactor = selectreactor.SelectReactor() logger = make_logger() session, pump = connect_application_session( server_factory, ObservingSession, component_config=ComponentConfig(realm="mqtt", controller=MockContainer())) endpoint = create_listening_endpoint_from_config( { "type": "tcp", "port": 1099, "interface": "0.0.0.0", "tls": { "certificate": "server.crt", "key": "server.key", "dhparam": "dhparam", "ca_certificates": ["ca.cert.pem", "intermediate.cert.pem"] }, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) client_endpoint = create_connecting_endpoint_from_config( { "type": "tcp", "host": "127.0.0.1", "port": 1099, "tls": { "certificate": "client.crt", "hostname": "localhost", "key": "client.key", "ca_certificates": ["ca.cert.pem", "intermediate.cert.pem"] }, }, FilePath(__file__).sibling('certs').path, real_reactor, logger) p = [] l = endpoint.listen(server_factory) class TestProtocol(Protocol): data = b"" expected = ( ConnACK(session_present=False, return_code=0).serialise() + PubACK(packet_identifier=1).serialise()) def dataReceived(self_, data): self_.data = self_.data + data if len(self_.data) == len(self_.expected): self.assertEqual(self_.data, self_.expected) real_reactor.stop() @l.addCallback def _listening(factory): d = client_endpoint.connect(Factory.forProtocol(TestProtocol)) @d.addCallback def _(proto): p.append(proto) proto.transport.write( Connect( client_id="test123", flags=ConnectFlags(clean_session=False)).serialise()) proto.transport.write( Publish(duplicate=False, qos_level=1, retain=False, topic_name="test", payload=b"{}", packet_identifier=1).serialise()) lc = LoopingCall(pump.flush) lc.clock = real_reactor lc.start(0.01) def timeout(): print("Timing out :(") real_reactor.stop() print(self.logs.log_text.getvalue()) # Timeout, just in case real_reactor.callLater(10, timeout) real_reactor.run() client_protocol = p[0] # We get a CONNECT self.assertEqual( client_protocol.data, ConnACK(session_present=False, return_code=0).serialise() + PubACK(packet_identifier=1).serialise()) client_protocol.data = b"" pump.flush() # This needs to be replaced with the real deal, see https://github.com/crossbario/crossbar/issues/885 self.assertEqual(len(session.events), 1) self.assertEqual(session.events, [{"args": tuple(), "kwargs": {}}])
def start_component(self, component_id, config, reload_modules=False, details=None): """ Starts a component in this container worker. :param component_id: The ID under which to start the component. :type component_id: str :param config: Component configuration. :type config: dict :param reload_modules: If `True`, enforce reloading of modules (user code) that were modified (see: TrackingModuleReloader). :type reload_modules: bool :param details: Caller details. :type details: instance of :class:`autobahn.wamp.types.CallDetails` :returns: Component startup information. :rtype: dict """ self.log.debug(u'{klass}.start_component({component_id}, {config})', klass=self.__class__.__name__, component_id=component_id, config=config) # prohibit starting a component twice # if component_id in self.components: emsg = u'duplicate component "{}" - a component with this ID is already running (or starting)'.format(component_id) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.already_running', emsg) # check component configuration # try: self.personality.check_container_component(self.personality, config) except Exception as e: emsg = u'invalid container component configuration: {}'.format(e) self.log.debug(emsg) raise ApplicationError(u'crossbar.error.invalid_configuration', emsg) else: self.log.debug(u'starting component "{component_id}" ..', component_id=component_id) # WAMP application component factory # realm = config.get(u'realm', None) extra = config.get(u'extra', None) controller = self if self.config.extra.expose_controller else None shared = self.components_shared if self.config.extra.expose_shared else None component_config = ComponentConfig(realm=realm, extra=extra, keyring=None, controller=controller, shared=shared) try: create_component = _appsession_loader(config) except ApplicationError as e: # for convenience, also log failed component loading self.log.error(u'component loading failed', log_failure=Failure()) if u'No module named' in str(e): self.log.error(u' Python module search paths:') for path in e.kwargs['pythonpath']: self.log.error(u' {path}', path=path) raise # force reload of modules (user code) # if reload_modules: self._module_tracker.reload() # WAMP application session factory # def create_session(): try: session = create_component(component_config) # any exception spilling out from user code in onXXX handlers is fatal! def panic(fail, msg): self.log.error( "Fatal error in component: {msg} - {log_failure.value}", msg=msg, log_failure=fail, ) session.disconnect() session._swallow_error = panic return session except Exception: self.log.failure(u'component instantiation failed: {log_failure.value}') raise # WAMP transport factory # transport_config = config[u'transport'] if transport_config[u'type'] == u'websocket': # create a WAMP-over-WebSocket transport client factory transport_factory = WampWebSocketClientFactory(create_session, transport_config[u'url']) transport_factory.noisy = False elif transport_config[u'type'] == u'rawsocket': transport_factory = WampRawSocketClientFactory(create_session, transport_config) transport_factory.noisy = False else: # should not arrive here, since we did check the config before raise Exception(u'logic error') # create and connect client endpoint # endpoint = create_connecting_endpoint_from_config(transport_config[u'endpoint'], self.config.extra.cbdir, self._reactor, self.log) # now, actually connect the client # d = endpoint.connect(transport_factory) def on_connect_success(proto): component = ContainerComponent(component_id, config, proto, None) self.components[component_id] = component # FIXME: this is a total hack. # def close_wrapper(orig, was_clean, code, reason): """ Wrap our protocol's onClose so we can tell when the component exits. """ r = orig(was_clean, code, reason) if component.id not in self.components: self.log.warn("Component '{id}' closed, but not in set.", id=component.id) return r if was_clean: self.log.info("Closed connection to '{id}' with code '{code}'", id=component.id, code=code) else: self.log.error("Lost connection to component '{id}' with code '{code}'.", id=component.id, code=code) if reason: self.log.warn(str(reason)) del self.components[component.id] self._publish_component_stop(component) component._stopped.callback(component.marshal()) if not self.components: if self._exit_mode == self.SHUTDOWN_ON_LAST_COMPONENT_STOPPED: self.log.info("Container is hosting no more components: stopping container in exit mode <{exit_mode}> ...", exit_mode=self._exit_mode) self.shutdown() else: self.log.info("Container is hosting no more components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode) else: self.log.info("Container is still hosting {component_count} components: continue running in exit mode <{exit_mode}>", exit_mode=self._exit_mode, component_count=len(self.components)) return r # FIXME: due to history, the following is currently the case: # ITransportHandler.onClose is implemented directly on WampWebSocketClientProtocol, # while with WampRawSocketClientProtocol, the ITransportHandler is implemented # by the object living on proto._session # if isinstance(proto, WampWebSocketClientProtocol): proto.onClose = partial(close_wrapper, proto.onClose) elif isinstance(proto, WampRawSocketClientProtocol): # FIXME: doesn't work without guard, since proto_.session is not yet there when # proto comes into existance .. if proto._session: proto._session.onClose = partial(close_wrapper, proto._session.onClose) else: raise Exception(u'logic error') # publish event "on_component_start" to all but the caller # uri = self._uri_prefix + u'.on_component_started' component_started = { u'id': component_id, u'config': config } self.publish(uri, component_started, options=PublishOptions(exclude=details.caller)) return component_started def on_connect_error(err): # https://twistedmatrix.com/documents/current/api/twisted.internet.error.ConnectError.html if isinstance(err.value, internet.error.ConnectError): emsg = u'could not connect container component to router - transport establishment failed ({})'.format(err.value) self.log.warn(emsg) raise ApplicationError(u'crossbar.error.cannot_connect', emsg) else: # should not arrive here (since all errors arriving here # should be subclasses of ConnectError) raise err d.addCallbacks(on_connect_success, on_connect_error) return d