def transport_check(_): self.log.debug('Entering re-connect loop') if not self._can_reconnect(): err_msg = u"Component failed: Exhausted all transport connect attempts" self.log.info(err_msg) try: raise RuntimeError(err_msg) except RuntimeError as e: txaio.reject(self._done_f, e) return while True: transport = next(transport_gen) if transport.can_reconnect(): transport_candidate[0] = transport break delay = transport.next_delay() self.log.debug( 'trying transport {transport_idx} using connect delay {transport_delay}', transport_idx=transport.idx, transport_delay=delay, ) self._delay_f = txaio.sleep(delay) txaio.add_callbacks(self._delay_f, attempt_connect, error)
def test_immediate_failure(framework): exception = RuntimeError("it failed") try: raise exception except: f0 = txaio.create_future_error() fail = txaio.create_failure() errors = [] results = [] f1 = txaio.create_future_error(fail) def cb(x): results.append(x) def errback(f): errors.append(f) txaio.add_callbacks(f0, cb, errback) txaio.add_callbacks(f1, cb, errback) run_once() run_once() run_once() assert len(results) == 0 assert len(errors) == 2 assert isinstance(errors[0], txaio.IFailedFuture) assert isinstance(errors[1], txaio.IFailedFuture) assert errors[0].value == exception assert errors[1].value == exception # should be distinct FailedPromise instances assert id(errors[0]) != id(errors[1])
def test_as_future_recursive(framework): ''' Returns another Future from as_future ''' errors = [] results = [] calls = [] f1 = txaio.create_future_success(42) def method(*args, **kw): calls.append((args, kw)) return f1 f0 = txaio.as_future(method, 1, 2, 3, key='word') def cb(x): results.append(x) def errback(f): errors.append(f) txaio.add_callbacks(f0, cb, errback) run_once() assert len(results) == 1 assert len(errors) == 0 assert results[0] == 42 assert calls[0] == ((1, 2, 3), dict(key='word'))
def test_as_future_exception(framework): ''' Raises an exception from as_future ''' errors = [] results = [] calls = [] exception = RuntimeError("sadness") def method(*args, **kw): calls.append((args, kw)) raise exception f = txaio.as_future(method, 1, 2, 3, key='word') def cb(x): results.append(x) def errback(f): errors.append(f) txaio.add_callbacks(f, cb, errback) run_once() assert len(results) == 0 assert len(errors) == 1 assert errors[0].value == exception assert calls[0] == ((1, 2, 3), dict(key='word'))
def onClose(self, wasClean): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onClose` """ self._transport = None if self._session_id: # fire callback and close the transport d = txaio.as_future( self.onLeave, types.CloseDetails( reason=types.CloseDetails.REASON_TRANSPORT_LOST, message="WAMP transport was lost without closing the session before", ), ) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) self._session_id = None d = txaio.as_future(self.onDisconnect, wasClean) def _error(e): return self._swallow_error(e, "While firing onDisconnect") txaio.add_callbacks(d, None, _error)
def test_as_future_immediate_none(framework): ''' Returning None immediately from as_future ''' errors = [] results = [] calls = [] def method(*args, **kw): calls.append((args, kw)) return None f = txaio.as_future(method, 1, 2, 3, key='word') def cb(x): results.append(x) def errback(f): errors.append(f) txaio.add_callbacks(f, cb, errback) run_once() assert len(results) == 1 assert len(errors) == 0 assert results[0] is None assert calls[0] == ((1, 2, 3), dict(key='word'))
def introspectRemoteObject(self, busName, objectPath, replaceKnownInterfaces=False): """ Calls org.freedesktop.DBus.Introspectable.Introspect @type busName: C{string} @param busName: Name of the bus containing the object @type objectPath: C{string} @param objectPath: Object Path to introspect @type replaceKnownInterfaces: C{bool} @param replaceKnownInterfaces: If True (defaults to False), the content of the introspected XML will override any pre-existing definitions of the contained interfaces. @rtype: L{twisted.internet.defer.Deferred} @returns: a Deferred to a list of L{interface.DBusInterface} instances created from the content of the introspected XML description of the object's interface. """ d = self.callRemote(objectPath, 'Introspect', interface = 'org.freedesktop.DBus.Introspectable', destination = busName) def ok(xml_str): return introspection.getInterfacesFromXML(xml_str, replaceKnownInterfaces) def err(e): raise error.IntrospectionFailed('Introspection Failed: ' + e.getErrorMessage()) txaio.add_callbacks(d, ok, err) return d
def test_errback_reject_no_args(): """ txaio.reject() with no args """ f = txaio.create_future() exception = RuntimeError("it failed") errors = [] def err(f): errors.append(f) txaio.add_callbacks(f, None, err) try: raise exception except: txaio.reject(f) run_once() assert len(errors) == 1 assert isinstance(errors[0], txaio.IFailedFuture) assert exception == errors[0].value tb = txaio.failure_format_traceback(errors[0]) assert 'RuntimeError' in tb assert 'it failed' in tb assert txaio.failure_message(errors[0]) == 'RuntimeError: it failed' assert 'it failed' in str(errors[0])
def test_errback(framework): f = txaio.create_future() exception = RuntimeError("it failed") errors = [] def err(f): errors.append(f) txaio.add_callbacks(f, None, err) try: raise exception except: fail = txaio.create_failure() txaio.reject(f, fail) run_once() assert len(errors) == 1 assert isinstance(errors[0], txaio.IFailedFuture) assert exception == errors[0].value assert txaio.failure_traceback(errors[0]) is not None tb = txaio.failure_format_traceback(errors[0]) assert 'RuntimeError' in tb assert 'it failed' in tb assert txaio.failure_message(errors[0]) == 'RuntimeError: it failed' assert 'it failed' in str(errors[0])
def test_create_error(): f = txaio.create_future(error=RuntimeError("test")) if txaio.using_twisted: assert f.called else: assert f.done() # cancel the error; we expected it txaio.add_callbacks(f, None, lambda _: None)
def on_connect(req): f = txaio.create_future() def cb(x): f = foo(42) f.add_callbacks(f, lambda v: values.append(v), None) return f txaio.add_callbacks(f, cb, None) return f
def actual_connect(_): f = self._connect_once(loop, transport) def session_done(x): txaio.resolve(done_f, None) def connect_error(fail): if isinstance(fail.value, asyncio.CancelledError): reconnect[0] = False txaio.reject(done_f, fail) return self.log.debug(u'component failed: {error}', error=txaio.failure_message(fail)) self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(fail)) # If this is a "fatal error" that will never work, # we bail out now if isinstance(fail.value, ApplicationError): if fail.value.error in [u'wamp.error.no_such_realm']: reconnect[0] = False self.log.error(u"Fatal error, not reconnecting") txaio.reject(done_f, fail) return self.log.error(u"{msg}", msg=fail.value.error_message()) return one_reconnect_loop(None) elif isinstance(fail.value, OSError): # failed to connect entirely, like nobody # listening etc. self.log.info(u"Connection failed: {msg}", msg=txaio.failure_message(fail)) return one_reconnect_loop(None) elif _is_ssl_error(fail.value): # Quoting pyOpenSSL docs: "Whenever # [SSL.Error] is raised directly, it has a # list of error messages from the OpenSSL # error queue, where each item is a tuple # (lib, function, reason). Here lib, function # and reason are all strings, describing where # and what the problem is. See err(3) for more # information." self.log.error(u"TLS failure: {reason}", reason=fail.value.args[1]) self.log.error(u"Marking this transport as failed") transport.failed() else: self.log.error( u'Connection failed: {error}', error=txaio.failure_message(fail), ) # some types of errors should probably have # stacktraces logged immediately at error # level, e.g. SyntaxError? self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(fail)) return one_reconnect_loop(None) txaio.add_callbacks(f, session_done, connect_error)
def addMatch(self, callback, mtype=None, sender=None, interface=None, member=None, path=None, path_namespace=None, destination=None, arg=None, arg_path=None, arg0namespace=None): """ Creates a message matching rule, associates it with the specified callback function, and sends the match rule to the DBus daemon. The arguments to this function are exactly follow the DBus specification. Refer to the \"Message Bus Message Routing\" section of the DBus specification for details. @rtype: C{int} @returns: a L{Deferred} to an integer id that may be used to unregister the match rule """ l = list() def add( k,v ): if v is not None: l.append( "%s='%s'" % (k,v) ) add('type', mtype) add('sender', sender) add('interface', interface) add('member', member) add('path', path) add('path_namespace', path_namespace) add('destination', destination) if arg: for idx, v in arg: add('arg%d' % (idx,), v) if arg_path: for idx, v in arg_path: add('arg%dpath' % (idx,), v) add('arg0namespace', arg0namespace) rule = ','.join(l) d = self.callRemote('/org/freedesktop/DBus', 'AddMatch', interface = 'org.freedesktop.DBus', destination = 'org.freedesktop.DBus', body = [rule], signature = 's') def ok(_): rule_id = self.router.addMatch(callback, mtype, sender, interface, member, path, path_namespace, destination, arg, arg_path, arg0namespace) self.match_rules[rule_id] = rule return rule_id txaio.add_callbacks(d, ok, None) return d
def onJoin(self, details): d2 = self.subscribe(lambda: None, u'com.example.topic1') def ok(_): txaio.resolve(d, None) def error(err): txaio.reject(d, err) txaio.add_callbacks(d2, ok, error)
def onOpen(self, transport): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onOpen` """ self._transport = transport d = txaio.as_future(self.onConnect) def _error(e): return self._swallow_error(e, "While firing onConnect") txaio.add_callbacks(d, None, _error)
def component_start(comp): # the future from start() errbacks if we fail, or callbacks # when the component is considered "done" (so maybe never) d = txaio.as_future(comp.start, reactor) txaio.add_callbacks( d, partial(component_success, comp), partial(component_failure, comp), ) return d
def on_join(session, details): self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def setup_success(_): self.log.debug("setup_success") def setup_error(err): self.log.debug("setup_error", err) txaio.add_callbacks(d, setup_success, setup_error)
def test_immediate_result(framework): f = txaio.create_future_success("it worked") results = [] def cb(f): results.append(f) txaio.add_callbacks(f, cb, None) run_once() assert len(results) == 1 assert results[0] == "it worked"
def test_callback(framework): f = txaio.create_future() results = [] def cb(f): results.append(f) txaio.add_callbacks(f, cb, None) txaio.resolve(f, "it worked") run_once() assert len(results) == 1 assert results[0] == "it worked"
def error(err): reply = message.Abort(u"wamp.error.cannot_authenticate", u"{0}".format(err.value)) self._transport.send(reply) # fire callback and close the transport details = types.CloseDetails(reply.reason, reply.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) # switching to the callback chain, effectively # cancelling error (which we've now handled) return d
def on_join(session, details): self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") txaio.resolve(done, None) def main_error(err): self.log.debug("main_error", err) txaio.reject(done, err) txaio.add_callbacks(d, main_success, main_error)
def notify_connect_error(fail): chain_f = txaio.create_future() # hmm, if connectfailure took a _Transport instead of # (or in addition to?) self it could .failed() the # transport and we could do away with the is_fatal # listener? handler_f = self.fire('connectfailure', self, fail.value) txaio.add_callbacks( handler_f, lambda _: txaio.reject(chain_f, fail), lambda _: txaio.reject(chain_f, fail) ) return chain_f
def onJoin(self, details): # noinspection PyUnusedLocal def on_event(*arg, **kwargs): pass d2 = self.subscribe(on_event, u'com.example.topic1') def ok(_): txaio.resolve(d, None) def error(err): txaio.reject(d, err) txaio.add_callbacks(d2, ok, error)
def sign_challenge(self, session, challenge): """ Sign WAMP-cryptosign challenge. :param challenge: The WAMP-cryptosign challenge object for which a signature should be computed. :type challenge: instance of autobahn.wamp.types.Challenge :returns: A Deferred/Future that resolves to the computed signature. :rtype: str """ if not isinstance(challenge, Challenge): raise Exception("challenge must be instance of autobahn.wamp.types.Challenge, not {}".format(type(challenge))) if u'challenge' not in challenge.extra: raise Exception("missing challenge value in challenge.extra") # the challenge sent by the router (a 32 bytes random value) challenge_hex = challenge.extra[u'challenge'] # the challenge for WAMP-cryptosign is a 32 bytes random value in Hex encoding (that is, a unicode string) challenge_raw = binascii.a2b_hex(challenge_hex) # if the transport has a channel ID, the message to be signed by the client actually # is the XOR of the challenge and the channel ID channel_id_raw = session._transport.get_channel_id() if channel_id_raw: data = util.xor(challenge_raw, channel_id_raw) else: data = challenge_raw # a raw byte string is signed, and the signature is also a raw byte string d1 = self.sign(data) # asyncio lacks callback chaining (and we cannot use co-routines, since we want # to support older Pythons), hence we need d2 d2 = txaio.create_future() def process(signature_raw): # convert the raw signature into a hex encode value (unicode string) signature_hex = binascii.b2a_hex(signature_raw).decode('ascii') # we return the concatenation of the signature and the message signed (96 bytes) data_hex = binascii.b2a_hex(data).decode('ascii') sig = signature_hex + data_hex txaio.resolve(d2, sig) txaio.add_callbacks(d1, process, None) return d2
def _connect_transport(self, reactor, transport, session_factory, done): """ Create and connect a WAMP-over-XXX transport. :param done: is a Deferred/Future from the parent which we should signal upon error if it is not done yet (XXX maybe an "on_error" callable instead?) """ transport_factory = _create_transport_factory(reactor, transport, session_factory) if transport.proxy: transport_endpoint = _create_transport_endpoint( reactor, { "type": "tcp", "host": transport.proxy["host"], "port": transport.proxy["port"], } ) else: transport_endpoint = _create_transport_endpoint(reactor, transport.endpoint) d = transport_endpoint.connect(transport_factory) def on_connect_success(proto): # if e.g. an SSL handshake fails, we will have # successfully connected (i.e. get here) but need to # 'listen' for the "connectionLost" from the underlying # protocol in case of handshake failure .. so we wrap # it. Also, we don't increment transport.success_count # here on purpose (because we might not succeed). orig = proto.connectionLost @wraps(orig) def lost(fail): rtn = orig(fail) if not txaio.is_called(done): txaio.reject(done, fail) return rtn proto.connectionLost = lost def on_connect_failure(err): transport.connect_failures += 1 # failed to establish a connection in the first place txaio.reject(done, err) txaio.add_callbacks(d, on_connect_success, None) txaio.add_callbacks(d, None, on_connect_failure) return d
def _run(reactor, components): if isinstance(components, Component): components = [components] if type(components) != list: raise ValueError( '"components" must be a list of Component objects - encountered' ' {0}'.format(type(components)) ) for c in components: if not isinstance(c, Component): raise ValueError( '"components" must be a list of Component objects - encountered' 'item of type {0}'.format(type(c)) ) log = txaio.make_logger() def component_success(c, arg): log.debug("Component {c} successfully completed: {arg}", c=c, arg=arg) return arg def component_failure(f): log.error("Component error: {msg}", msg=txaio.failure_message(f)) log.debug("Component error: {tb}", tb=txaio.failure_format_traceback(f)) return None # all components are started in parallel dl = [] for c in components: # a component can be of type MAIN or SETUP d = c.start(reactor) txaio.add_callbacks(d, partial(component_success, c), component_failure) dl.append(d) d = txaio.gather(dl, consume_exceptions=False) def all_done(arg): log.debug("All components ended; stopping reactor") try: reactor.stop() except ReactorNotRunning: pass txaio.add_callbacks(d, all_done, all_done) return d
def connectionAuthenticated(self): """ Called by L{protocol.BasicDBusProtocol} when the DBus authentication has completed successfully. """ self.router = router.MessageRouter() self.match_rules = dict() self.objHandler = objects.DBusObjectHandler(self) self._pendingCalls = dict() # serial_number => (deferred, delayed_timeout_cb | None) self._dcCallbacks = list() d = self.callRemote( '/Hello', 'Hello', interface = 'org.freedesktop.DBus', destination = 'org.freedesktop.DBus' ) txaio.add_callbacks(d, self._cbGotHello, lambda err: self.doNotifyStartupError(err))
def on_join(session, details): self.log.debug("session on_join: {details}", details=details) self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.config['url'], ) d = txaio.as_future(self._entry, reactor, session) def setup_success(_): self.log.debug("setup_success") def setup_error(err): self.log.debug("setup_error: {err}", err=err) txaio.reject(done, err) txaio.add_callbacks(d, setup_success, setup_error)
def delMatch(self, rule_id): """ Removes a message matching rule previously registered with addMatch """ rule = self.match_rules[rule_id] d = self.callRemote('/org/freedesktop/DBus', 'RemoveMatch', interface = 'org.freedesktop.DBus', destination = 'org.freedesktop.DBus', body = [rule], signature = 's') def ok(_): del self.match_rules[rule_id] self.router.delMatch(rule_id) txaio.add_callbacks(d, ok, None) return d
def on_join(session, details): self.log.debug("session on_join: {details}", details=details) transport.connect_sucesses += 1 self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.config['url'], ) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") session.leave() def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) # I guess .leave() here too...? txaio.add_callbacks(d, main_success, main_error)
def processSubscribe(self, session, subscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processSubscribe` """ # check topic URI: for SUBSCRIBE, must be valid URI (either strict or loose), and all # URI components must be non-empty for normal subscriptions, may be empty for # wildcard subscriptions and must be non-empty for all but the last component for # prefix subscriptions # if self._option_uri_strict: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_STRICT_EMPTY.match(subscribe.topic) elif subscribe.match == u"prefix": uri_is_valid = _URI_PAT_STRICT_LAST_EMPTY.match(subscribe.topic) else: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(subscribe.topic) else: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(subscribe.topic) elif subscribe.match == u"prefix": uri_is_valid = _URI_PAT_LOOSE_LAST_EMPTY.match(subscribe.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(subscribe.topic) if not uri_is_valid: reply = message.Error(message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.INVALID_URI, [u"subscribe for invalid topic URI '{0}'".format(subscribe.topic)]) self._router.send(session, reply) return # authorize SUBSCRIBE action # d = self._router.authorize(session, subscribe.topic, u'subscribe') def on_authorize_success(authorization): if not authorization[u'allow']: # error reply since session is not authorized to subscribe # reply = message.Error(message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.NOT_AUTHORIZED, [u"session is not authorized to subscribe to topic '{0}'".format(subscribe.topic)]) else: # ok, session authorized to subscribe. now get the subscription # subscription, was_already_subscribed, is_first_subscriber = self._subscription_map.add_observer(session, subscribe.topic, subscribe.match) if not was_already_subscribed: self._session_to_subscriptions[session].add(subscription) # publish WAMP meta events # if self._router._realm: service_session = self._router._realm.session if service_session and not subscription.uri.startswith(u'wamp.'): if is_first_subscriber: subscription_details = { u'id': subscription.id, u'created': subscription.created, u'uri': subscription.uri, u'match': subscription.match, } service_session.publish(u'wamp.subscription.on_create', session._session_id, subscription_details) if not was_already_subscribed: service_session.publish(u'wamp.subscription.on_subscribe', session._session_id, subscription.id) # acknowledge subscribe with subscription ID # reply = message.Subscribed(subscribe.request, subscription.id) # send out reply to subscribe requestor # self._router.send(session, reply) def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ # XXX same as another code-block, can we collapse? self.log.failure(err) reply = message.Error( message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.AUTHORIZATION_FAILED, [u"failed to authorize session for subscribing to topic URI '{0}': {1}".format(subscribe.topic, err.value)] ) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def send(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransport.send` """ if isinstance(msg, message.Hello): self._router = self._routerFactory.get(msg.realm) # fake session ID assignment (normally done in WAMP opening handshake) self._session._session_id = util.id() # set fixed/trusted authentication information self._session._authid = self._trusted_authid self._session._authrole = self._trusted_authrole self._session._authmethod = None # FIXME: the following does blow up # self._session._authmethod = u'trusted' self._session._authprovider = None # add app session to router self._router.attach(self._session) # fake app session open # details = SessionDetails(self._session._realm, self._session._session_id, self._session._authid, self._session._authrole, self._session._authmethod, self._session._authprovider) d = txaio.as_future(self._session.onJoin, details) def _log_error_and_close(fail): self.log.failure("Internal error: {log_failure.value}", failure=fail) self.close() txaio.add_callbacks(d, None, _log_error_and_close) # app-to-router # elif isinstance(msg, message.Publish) or \ isinstance(msg, message.Subscribe) or \ isinstance(msg, message.Unsubscribe) or \ isinstance(msg, message.Call) or \ isinstance(msg, message.Yield) or \ isinstance(msg, message.Register) or \ isinstance(msg, message.Unregister) or \ isinstance(msg, message.Cancel) or \ (isinstance(msg, message.Error) and msg.request_type == message.Invocation.MESSAGE_TYPE): # deliver message to router # self._router.process(self._session, msg) # router-to-app # elif isinstance(msg, message.Event) or \ isinstance(msg, message.Invocation) or \ isinstance(msg, message.Result) or \ isinstance(msg, message.Published) or \ isinstance(msg, message.Subscribed) or \ isinstance(msg, message.Unsubscribed) or \ isinstance(msg, message.Registered) or \ isinstance(msg, message.Unregistered) or \ (isinstance(msg, message.Error) and ( msg.request_type == message.Call.MESSAGE_TYPE or msg.request_type == message.Cancel.MESSAGE_TYPE or msg.request_type == message.Register.MESSAGE_TYPE or msg.request_type == message.Unregister.MESSAGE_TYPE or msg.request_type == message.Publish.MESSAGE_TYPE or msg.request_type == message.Subscribe.MESSAGE_TYPE or msg.request_type == message.Unsubscribe.MESSAGE_TYPE)): # deliver message to app session # self._session.onMessage(msg) else: # should not arrive here # raise Exception( "RouterApplicationSession.send: unhandled message {0}".format( msg))
def sign_challenge(self, session, challenge, channel_id_type='tls-unique'): """ Sign WAMP-cryptosign challenge. :param session: The authenticating WAMP session. :type session: :class:`autobahn.wamp.protocol.ApplicationSession` :param challenge: The WAMP-cryptosign challenge object for which a signature should be computed. :type challenge: instance of autobahn.wamp.types.Challenge :returns: A Deferred/Future that resolves to the computed signature. :rtype: str """ if not isinstance(challenge, Challenge): raise Exception( "challenge must be instance of autobahn.wamp.types.Challenge, not {}" .format(type(challenge))) if 'challenge' not in challenge.extra: raise Exception("missing challenge value in challenge.extra") # the challenge sent by the router (a 32 bytes random value) challenge_hex = challenge.extra['challenge'] if type(challenge_hex) != str: raise Exception( "invalid type {} for challenge (expected a hex string)". format(type(challenge_hex))) if len(challenge_hex) != 64: raise Exception( "unexpected challenge (hex) length: was {}, but expected 64" .format(len(challenge_hex))) # the challenge for WAMP-cryptosign is a 32 bytes random value in Hex encoding (that is, a unicode string) challenge_raw = binascii.a2b_hex(challenge_hex) if channel_id_type == 'tls-unique': # get the TLS channel ID of the underlying TLS connection channel_id_raw = session._transport.get_channel_id() assert len( channel_id_raw ) == 32, 'unexpected TLS transport channel ID length (was {}, but expected 32)'.format( len(channel_id_raw)) # with TLS channel binding of type "tls-unique", the message to be signed by the client actually # is the XOR of the challenge and the TLS channel ID data = util.xor(challenge_raw, channel_id_raw) elif channel_id_type is None: # when no channel binding was requested, the message to be signed by the client is the challenge only data = challenge_raw else: assert False, 'invalid channel_id_type "{}"'.format( channel_id_type) # a raw byte string is signed, and the signature is also a raw byte string d1 = self.sign(data) # asyncio lacks callback chaining (and we cannot use co-routines, since we want # to support older Pythons), hence we need d2 d2 = txaio.create_future() def process(signature_raw): # convert the raw signature into a hex encode value (unicode string) signature_hex = binascii.b2a_hex(signature_raw).decode('ascii') # we return the concatenation of the signature and the message signed (96 bytes) data_hex = binascii.b2a_hex(data).decode('ascii') sig = signature_hex + data_hex txaio.resolve(d2, sig) txaio.add_callbacks(d1, process, None) return d2
def processPublish(self, session, publish): """ Implements :func:`crossbar.router.interfaces.IBroker.processPublish` """ if self._router.is_traced: if not publish.correlation_id: publish.correlation_id = self._router.new_correlation_id() publish.correlation_is_anchor = True if not publish.correlation_uri: publish.correlation_uri = publish.topic # check topic URI: for PUBLISH, must be valid URI (either strict or loose), and # all URI components must be non-empty if self._option_uri_strict: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(publish.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(publish.topic) if not uri_is_valid: if publish.acknowledge: if self._router.is_traced: publish.correlation_is_last = False self._router._factory._worker._maybe_trace_rx_msg(session, publish) reply = message.Error(message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [u"publish with invalid topic URI '{0}' (URI strict checking {1})".format(publish.topic, self._option_uri_strict)]) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False reply.correlation_is_last = True self._router.send(session, reply) else: if self._router.is_traced: publish.correlation_is_last = True self._router._factory._worker._maybe_trace_rx_msg(session, publish) return # disallow publication to topics starting with "wamp." other than for # trusted sessions (that are sessions built into Crossbar.io routing core) # if session._authrole is not None and session._authrole != u"trusted": is_restricted = publish.topic.startswith(u"wamp.") if is_restricted: if publish.acknowledge: if self._router.is_traced: publish.correlation_is_last = False self._router._factory._worker._maybe_trace_rx_msg(session, publish) reply = message.Error(message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [u"publish with restricted topic URI '{0}'".format(publish.topic)]) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False reply.correlation_is_last = True self._router.send(session, reply) else: if self._router.is_traced: publish.correlation_is_last = True self._router._factory._worker._maybe_trace_rx_msg(session, publish) return # get subscriptions active on the topic published to # subscriptions = self._subscription_map.match_observations(publish.topic) # check if the event is being persisted by checking if we ourself are among the observers # on _any_ matching subscription # we've been previously added to observer lists on subscriptions ultimately from # node configuration and during the broker starts up. store_event = False if self._event_store: for subscription in subscriptions: if self._event_store in subscription.observers: store_event = True break if store_event: self.log.debug('Persisting event on topic "{topic}"', topic=publish.topic) # check if the event is to be retained by inspecting the 'retain' flag retain_event = False if publish.retain: retain_event = True # go on if (otherwise there isn't anything to do anyway): # # - there are any active subscriptions OR # - the publish is to be acknowledged OR # - the event is to be persisted OR # - the event is to be retained # if not (subscriptions or publish.acknowledge or store_event or retain_event): # the received PUBLISH message is the only one received/sent # for this WAMP action, so mark it as "last" (there is another code path below!) if self._router.is_traced: if publish.correlation_is_last is None: publish.correlation_is_last = True self._router._factory._worker._maybe_trace_rx_msg(session, publish) else: # validate payload # if publish.payload is None: try: self._router.validate('event', publish.topic, publish.args, publish.kwargs) except Exception as e: if publish.acknowledge: if self._router.is_traced: publish.correlation_is_last = False self._router._factory._worker._maybe_trace_rx_msg(session, publish) reply = message.Error(message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_ARGUMENT, [u"publish to topic URI '{0}' with invalid application payload: {1}".format(publish.topic, e)]) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False reply.correlation_is_last = True self._router.send(session, reply) else: if self._router.is_traced: publish.correlation_is_last = True self._router._factory._worker._maybe_trace_rx_msg(session, publish) return # authorize PUBLISH action # d = self._router.authorize(session, publish.topic, u'publish', options=publish.marshal_options()) def on_authorize_success(authorization): # the call to authorize the action _itself_ succeeded. now go on depending on whether # the action was actually authorized or not .. # if not authorization[u'allow']: if publish.acknowledge: if self._router.is_traced: publish.correlation_is_last = False self._router._factory._worker._maybe_trace_rx_msg(session, publish) reply = message.Error(message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.NOT_AUTHORIZED, [u"session not authorized to publish to topic '{0}'".format(publish.topic)]) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False reply.correlation_is_last = True self._router.send(session, reply) else: if self._router.is_traced: publish.correlation_is_last = True self._router._factory._worker._maybe_trace_rx_msg(session, publish) else: # new ID for the publication # publication = util.id() # publisher disclosure # if authorization[u'disclose']: disclose = True elif (publish.topic.startswith(u"wamp.") or publish.topic.startswith(u"crossbar.")): disclose = True else: disclose = False if disclose: publisher = session._session_id publisher_authid = session._authid publisher_authrole = session._authrole else: publisher = None publisher_authid = None publisher_authrole = None # skip publisher # if publish.exclude_me is None or publish.exclude_me: me_also = False else: me_also = True # persist event (this is done only once, regardless of the number of subscriptions # the event matches on) # if store_event: self._event_store.store_event(session, publication, publish) # retain event on the topic # if retain_event: retained_event = RetainedEvent(publish, publisher, publisher_authid, publisher_authrole) observation = self._subscription_map.get_observation(publish.topic) if not observation: # No observation, lets make a new one observation = self._subscription_map.create_observation(publish.topic, extra=SubscriptionExtra()) else: # this can happen if event-history is # enabled on the topic: the event-store # creates an observation before any client # could possible hit the code above if observation.extra is None: observation.extra = SubscriptionExtra() elif not isinstance(observation.extra, SubscriptionExtra): raise Exception( "incorrect 'extra' for '{}'".format(publish.topic) ) if observation.extra.retained_events: if not publish.eligible and not publish.exclude: observation.extra.retained_events = [retained_event] else: observation.extra.retained_events.append(retained_event) else: observation.extra.retained_events = [retained_event] subscription_to_receivers = {} total_receivers_cnt = 0 # iterate over all subscriptions and determine actual receivers of the event # under the respective subscription. also persist events (independent of whether # there is any actual receiver right now on the subscription) # for subscription in subscriptions: # initial list of receivers are all subscribers on a subscription .. # receivers = subscription.observers receivers = self._filter_publish_receivers(receivers, publish) # if receivers is non-empty, dispatch event .. # receivers_cnt = len(receivers) - (1 if self in receivers else 0) if receivers_cnt: total_receivers_cnt += receivers_cnt subscription_to_receivers[subscription] = receivers # send publish acknowledge before dispatching # if publish.acknowledge: if self._router.is_traced: publish.correlation_is_last = False self._router._factory._worker._maybe_trace_rx_msg(session, publish) reply = message.Published(publish.request, publication) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False reply.correlation_is_last = total_receivers_cnt == 0 self._router.send(session, reply) else: if self._router.is_traced and publish.correlation_is_last is None: if total_receivers_cnt == 0: publish.correlation_is_last = True else: publish.correlation_is_last = False # now actually dispatch the events! # for chunked dispatching, this will be filled with deferreds for each chunk # processed. when the complete list of deferreds is done, that means the # event has been sent out to all applicable receivers all_dl = [] if total_receivers_cnt: # list of receivers that should have received the event, but we could not # send the event, since the receiver has disappeared in the meantime vanished_receivers = [] for subscription, receivers in subscription_to_receivers.items(): storing_event = store_event and self._event_store in subscription.observers self.log.debug('dispatching for subscription={subscription}, storing_event={storing_event}', subscription=subscription, storing_event=storing_event) # for pattern-based subscriptions, the EVENT must contain # the actual topic being published to # if subscription.match != message.Subscribe.MATCH_EXACT: topic = publish.topic else: topic = None if publish.payload: msg = message.Event(subscription.id, publication, payload=publish.payload, publisher=publisher, publisher_authid=publisher_authid, publisher_authrole=publisher_authrole, topic=topic, enc_algo=publish.enc_algo, enc_key=publish.enc_key, enc_serializer=publish.enc_serializer, forward_for=publish.forward_for) else: msg = message.Event(subscription.id, publication, args=publish.args, kwargs=publish.kwargs, publisher=publisher, publisher_authid=publisher_authid, publisher_authrole=publisher_authrole, topic=topic, forward_for=publish.forward_for) # if the publish message had a correlation ID, this will also be the # correlation ID of the event message sent out msg.correlation_id = publish.correlation_id msg.correlation_uri = publish.topic msg.correlation_is_anchor = False msg.correlation_is_last = False chunk_size = self._options.event_dispatching_chunk_size if chunk_size and len(receivers) > chunk_size: self.log.debug('chunked dispatching to {receivers_size} with chunk_size={chunk_size}', receivers_size=len(receivers), chunk_size=chunk_size) else: self.log.debug('unchunked dispatching to {receivers_size} receivers', receivers_size=len(receivers)) # note that we're using one code-path for both chunked and unchunked # dispatches; the *first* chunk is always done "synchronously" (before # the first call-later) so "un-chunked mode" really just means we know # we'll be done right now and NOT do a call_later... # a Deferred that fires when all chunks are done all_d = txaio.create_future() all_dl.append(all_d) # all the event messages are the same except for the last one, which # needs to have the "is_last" flag set if we're doing a trace if self._router.is_traced: last_msg = copy.deepcopy(msg) last_msg.correlation_id = msg.correlation_id last_msg.correlation_uri = msg.correlation_uri last_msg.correlation_is_anchor = False last_msg.correlation_is_last = True def _notify_some(receivers): # we do a first pass over the proposed chunk of receivers # because not all of them will have a transport, and if this # will be the last chunk of receivers we need to figure out # which event is last... receivers_this_chunk = [] for receiver in receivers[:chunk_size]: if receiver._session_id and receiver._transport: receivers_this_chunk.append(receiver) else: vanished_receivers.append(receiver) receivers = receivers[chunk_size:] # XXX note there's still going to be some edge-cases here .. if # we are NOT the last chunk, but all the next chunk's receivers # (could be only 1 in that chunk!) vanish before we run our next # batch, then a "last" event will never go out ... # we now actually do the deliveries, but now we know which # receiver is the last one if receivers or not self._router.is_traced: # NOT the last chunk (or we're not traced so don't care) for receiver in receivers_this_chunk: # send out WAMP msg to peer self._router.send(receiver, msg) if self._event_store or storing_event: self._event_store.store_event_history(publication, subscription.id, receiver) else: # last chunk, so last receiver gets the different message for receiver in receivers_this_chunk[:-1]: self._router.send(receiver, msg) if self._event_store or storing_event: self._event_store.store_event_history(publication, subscription.id, receiver) # FIXME: I don't get the following comment and code path. when, how? and what to # do about event store? => storing_event # # we might have zero valid receivers if receivers_this_chunk: self._router.send(receivers_this_chunk[-1], last_msg) # FIXME: => storing_event if receivers: # still more to do .. return txaio.call_later(0, _notify_some, receivers) else: # all done! resolve all_d, which represents all receivers # to a single subscription matching the event txaio.resolve(all_d, None) _notify_some([ recv for recv in receivers if (me_also or recv != session) and recv != self._event_store ]) return txaio.gather(all_dl) def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ self.log.failure("Authorization failed", failure=err) if publish.acknowledge: if self._router.is_traced: publish.correlation_is_last = False self._router._factory._worker._maybe_trace_rx_msg(session, publish) reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.AUTHORIZATION_FAILED, [u"failed to authorize session for publishing to topic URI '{0}': {1}".format(publish.topic, err.value)] ) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False self._router.send(session, reply) else: if self._router.is_traced: publish.correlation_is_last = True self._router._factory._worker._maybe_trace_rx_msg(session, publish) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def onMessage(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage` """ if self._session_id is None: # the first message must be WELCOME, ABORT or CHALLENGE .. if isinstance(msg, message.Welcome): self._session_id = msg.session details = SessionDetails(self._realm, self._session_id, msg.authid, msg.authrole, msg.authmethod) d = txaio.as_future(self.onJoin, details) def _error(e): return self._swallow_error(e, "While firing onJoin") txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Abort): # fire callback and close the transport details = types.CloseDetails(msg.reason, msg.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Challenge): challenge = types.Challenge(msg.method, msg.extra) d = txaio.as_future(self.onChallenge, challenge) def success(signature): reply = message.Authenticate(signature) self._transport.send(reply) def error(err): reply = message.Abort(u"wamp.error.cannot_authenticate", u"{0}".format(err.value)) self._transport.send(reply) # fire callback and close the transport details = types.CloseDetails(reply.reason, reply.message) d = txaio.as_future(self.onLeave, details) def _error(e): return self._swallow_error(e, "While firing onLeave") txaio.add_callbacks(d, None, _error) # switching to the callback chain, effectively # cancelling error (which we've now handled) return d txaio.add_callbacks(d, success, error) else: raise ProtocolError( "Received {0} message, and session is not yet established". format(msg.__class__)) else: # self._session_id != None (aka "session established") if isinstance(msg, message.Goodbye): if not self._goodbye_sent: # the peer wants to close: send GOODBYE reply reply = message.Goodbye() self._transport.send(reply) self._session_id = None # fire callback and close the transport details = types.CloseDetails(msg.reason, msg.message) d = txaio.as_future(self.onLeave, details) def _error(e): errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format( msg.reason, msg.message) return self._swallow_error(e, errmsg) txaio.add_callbacks(d, None, _error) elif isinstance(msg, message.Event): if msg.subscription in self._subscriptions: # fire all event handlers on subscription .. for subscription in self._subscriptions[msg.subscription]: handler = subscription.handler invoke_args = ( handler.obj, ) if handler.obj else tuple() if msg.args: invoke_args = invoke_args + tuple(msg.args) invoke_kwargs = msg.kwargs if msg.kwargs else dict() if handler.details_arg: invoke_kwargs[ handler.details_arg] = types.EventDetails( publication=msg.publication, publisher=msg.publisher, topic=msg.topic) try: handler.fn(*invoke_args, **invoke_kwargs) except Exception as e: msg = 'While firing {0} subscribed under {1}.'.format( handler.fn, msg.subscription) try: self.onUserError(e, msg) except: pass else: raise ProtocolError( "EVENT received for non-subscribed subscription ID {0}" .format(msg.subscription)) elif isinstance(msg, message.Published): if msg.request in self._publish_reqs: # get and pop outstanding publish request publish_request = self._publish_reqs.pop(msg.request) # create a new publication object publication = Publication(msg.publication) # resolve deferred/future for publishing successfully txaio.resolve(publish_request.on_reply, publication) else: raise ProtocolError( "PUBLISHED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Subscribed): if msg.request in self._subscribe_reqs: # get and pop outstanding subscribe request request = self._subscribe_reqs.pop(msg.request) # create new handler subscription list for subscription ID if not yet tracked if msg.subscription not in self._subscriptions: self._subscriptions[msg.subscription] = [] subscription = Subscription(msg.subscription, self, request.handler) # add handler to existing subscription self._subscriptions[msg.subscription].append(subscription) # resolve deferred/future for subscribing successfully txaio.resolve(request.on_reply, subscription) else: raise ProtocolError( "SUBSCRIBED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Unsubscribed): if msg.request in self._unsubscribe_reqs: # get and pop outstanding subscribe request request = self._unsubscribe_reqs.pop(msg.request) # if the subscription still exists, mark as inactive and remove .. if request.subscription_id in self._subscriptions: for subscription in self._subscriptions[ request.subscription_id]: subscription.active = False del self._subscriptions[request.subscription_id] # resolve deferred/future for unsubscribing successfully txaio.resolve(request.on_reply, 0) else: raise ProtocolError( "UNSUBSCRIBED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Result): if msg.request in self._call_reqs: if msg.progress: # progressive result call_request = self._call_reqs[msg.request] if call_request.options.on_progress: kw = msg.kwargs or dict() args = msg.args or tuple() try: # XXX what if on_progress returns a Deferred/Future? call_request.options.on_progress(*args, **kw) except Exception as e: try: self.onUserError( e, "While firing on_progress") except: pass else: # silently ignore progressive results pass else: # final result # call_request = self._call_reqs.pop(msg.request) on_reply = call_request.on_reply if msg.kwargs: if msg.args: res = types.CallResult(*msg.args, **msg.kwargs) else: res = types.CallResult(**msg.kwargs) txaio.resolve(on_reply, res) else: if msg.args: if len(msg.args) > 1: res = types.CallResult(*msg.args) txaio.resolve(on_reply, res) else: txaio.resolve(on_reply, msg.args[0]) else: txaio.resolve(on_reply, None) else: raise ProtocolError( "RESULT received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Invocation): if msg.request in self._invocations: raise ProtocolError( "INVOCATION received for request ID {0} already invoked" .format(msg.request)) else: if msg.registration not in self._registrations: raise ProtocolError( "INVOCATION received for non-registered registration ID {0}" .format(msg.registration)) else: registration = self._registrations[msg.registration] endpoint = registration.endpoint if endpoint.obj: invoke_args = (endpoint.obj, ) else: invoke_args = tuple() if msg.args: invoke_args = invoke_args + tuple(msg.args) invoke_kwargs = msg.kwargs if msg.kwargs else dict() if endpoint.details_arg: if msg.receive_progress: def progress(*args, **kwargs): progress_msg = message.Yield(msg.request, args=args, kwargs=kwargs, progress=True) self._transport.send(progress_msg) else: progress = None invoke_kwargs[ endpoint.details_arg] = types.CallDetails( progress, caller=msg.caller, procedure=msg.procedure) on_reply = txaio.as_future(endpoint.fn, *invoke_args, **invoke_kwargs) def success(res): del self._invocations[msg.request] if isinstance(res, types.CallResult): reply = message.Yield(msg.request, args=res.results, kwargs=res.kwresults) else: reply = message.Yield(msg.request, args=[res]) try: self._transport.send(reply) except SerializationError as e: # the application-level payload returned from the invoked procedure can't be serialized reply = message.Error( message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD, args=[ u'success return value from invoked procedure "{0}" could not be serialized: {1}' .format(registration.procedure, e) ]) self._transport.send(reply) def error(err): errmsg = 'Failure while invoking procedure {0} registered under "{1}".'.format( endpoint.fn, registration.procedure) try: self.onUserError(err, errmsg) except: pass formatted_tb = None if self.traceback_app: # if asked to marshal the traceback within the WAMP error message, extract it # noinspection PyCallingNonCallable tb = StringIO() err.printTraceback(file=tb) formatted_tb = tb.getvalue().splitlines() del self._invocations[msg.request] if hasattr(err, 'value'): exc = err.value else: exc = err reply = self._message_from_exception( message.Invocation.MESSAGE_TYPE, msg.request, exc, formatted_tb) try: self._transport.send(reply) except SerializationError as e: # the application-level payload returned from the invoked procedure can't be serialized reply = message.Error( message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD, args=[ u'error return value from invoked procedure "{0}" could not be serialized: {1}' .format(registration.procedure, e) ]) self._transport.send(reply) # we have handled the error, so we eat it return None self._invocations[msg.request] = InvocationRequest( msg.request, on_reply) txaio.add_callbacks(on_reply, success, error) elif isinstance(msg, message.Interrupt): if msg.request not in self._invocations: raise ProtocolError( "INTERRUPT received for non-pending invocation {0}". format(msg.request)) else: # noinspection PyBroadException try: self._invocations[msg.request].cancel() except Exception as e: # XXX can .cancel() return a Deferred/Future? try: self.onUserError(e, "While cancelling call.") except: pass finally: del self._invocations[msg.request] elif isinstance(msg, message.Registered): if msg.request in self._register_reqs: # get and pop outstanding register request request = self._register_reqs.pop(msg.request) # create new registration if not yet tracked if msg.registration not in self._registrations: registration = Registration(self, msg.registration, request.procedure, request.endpoint) self._registrations[msg.registration] = registration else: raise ProtocolError( "REGISTERED received for already existing registration ID {0}" .format(msg.registration)) txaio.resolve(request.on_reply, registration) else: raise ProtocolError( "REGISTERED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Unregistered): if msg.request in self._unregister_reqs: # get and pop outstanding subscribe request request = self._unregister_reqs.pop(msg.request) # if the registration still exists, mark as inactive and remove .. if request.registration_id in self._registrations: self._registrations[ request.registration_id].active = False del self._registrations[request.registration_id] # resolve deferred/future for unregistering successfully txaio.resolve(request.on_reply) else: raise ProtocolError( "UNREGISTERED received for non-pending request ID {0}". format(msg.request)) elif isinstance(msg, message.Error): # remove outstanding request and get the reply deferred/future on_reply = None # ERROR reply to CALL if msg.request_type == message.Call.MESSAGE_TYPE and msg.request in self._call_reqs: on_reply = self._call_reqs.pop(msg.request).on_reply # ERROR reply to PUBLISH elif msg.request_type == message.Publish.MESSAGE_TYPE and msg.request in self._publish_reqs: on_reply = self._publish_reqs.pop(msg.request).on_reply # ERROR reply to SUBSCRIBE elif msg.request_type == message.Subscribe.MESSAGE_TYPE and msg.request in self._subscribe_reqs: on_reply = self._subscribe_reqs.pop(msg.request).on_reply # ERROR reply to UNSUBSCRIBE elif msg.request_type == message.Unsubscribe.MESSAGE_TYPE and msg.request in self._unsubscribe_reqs: on_reply = self._unsubscribe_reqs.pop(msg.request).on_reply # ERROR reply to REGISTER elif msg.request_type == message.Register.MESSAGE_TYPE and msg.request in self._register_reqs: on_reply = self._register_reqs.pop(msg.request).on_reply # ERROR reply to UNREGISTER elif msg.request_type == message.Unregister.MESSAGE_TYPE and msg.request in self._unregister_reqs: on_reply = self._unregister_reqs.pop(msg.request).on_reply if on_reply: txaio.reject(on_reply, self._exception_from_message(msg)) else: raise ProtocolError( "WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}" .format(msg.request_type, msg.request)) else: raise ProtocolError("Unexpected message {0}".format( msg.__class__))
def _connect_once(self, reactor, transport): self.log.info( 'connecting once using transport type "{transport_type}" ' 'over endpoint "{endpoint_desc}"', transport_type=transport.type, endpoint_desc=transport.describe_endpoint(), ) done = txaio.create_future() # factory for ISession objects def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: session = self.session_factory(cfg) for auth_name, auth_config in self._authentication.items(): session.add_authenticator(auth_name, **auth_config) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # listen on leave events; if we get errors # (e.g. no_such_realm), an on_leave can happen without # an on_join before def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if self._entry: txaio.resolve(done, None) session.on('leave', on_leave) # if we were given a "main" procedure, we run through # it completely (i.e. until its Deferred fires) and # then disconnect this session def on_join(session, details): self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") def leave(): try: session.leave() except SessionNotReady: # someone may have already called # leave() pass txaio.call_later(0, leave) def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) txaio.add_callbacks(d, main_success, main_error) if self._entry is not None: session.on('join', on_join) # listen on disconnect events. Note that in case we # had a "main" procedure, we could have already # resolve()'d our "done" future def on_disconnect(session, was_clean): self.log.debug( "session on_disconnect: was_clean={was_clean}", was_clean=was_clean, ) if not txaio.is_called(done): if was_clean: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) else: txaio.reject( done, RuntimeError('transport closed uncleanly')) session.on('disconnect', on_disconnect) # return the fresh session object return session transport.connect_attempts += 1 d = self._connect_transport(reactor, transport, create_session) def on_connect_sucess(proto): # if e.g. an SSL handshake fails, we will have # successfully connected (i.e. get here) but need to # 'listen' for the "connectionLost" from the underlying # protocol in case of handshake failure .. so we wrap # it. Also, we don't increment transport.success_count # here on purpose (because we might not succeed). orig = proto.connectionLost @wraps(orig) def lost(fail): rtn = orig(fail) if not txaio.is_called(done): txaio.reject(done, fail) return rtn proto.connectionLost = lost def on_connect_failure(err): transport.connect_failures += 1 # failed to establish a connection in the first place done.errback(err) txaio.add_callbacks(d, on_connect_sucess, None) txaio.add_callbacks(d, None, on_connect_failure) return done
def _connect_once(self, reactor, transport): self.log.info( 'connecting once using transport type "{transport_type}" ' 'over endpoint "{endpoint_desc}"', transport_type=transport.type, endpoint_desc=transport.describe_endpoint(), ) done = txaio.create_future() # factory for ISession objects def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: session = self.session_factory(cfg) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # the only difference bewteen MAIN and SETUP-type # entry-points is that we want to shut down the # component when a MAIN-type entrypoint's Deferred is # done. if self._entry_type == Component.TYPE_MAIN: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) transport.connect_sucesses += 1 self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.url, ) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") session.leave() def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) # I guess .leave() here too...? txaio.add_callbacks(d, main_success, main_error) session.on('join', on_join) elif self._entry_type == Component.TYPE_SETUP: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) self.log.info( 'Successfully connected to transport #{transport_idx}: url={url}', transport_idx=transport.idx, url=transport.url, ) d = txaio.as_future(self._entry, reactor, session) def setup_success(_): self.log.debug("setup_success") def setup_error(err): self.log.debug("setup_error: {err}", err=err) txaio.reject(done, err) txaio.add_callbacks(d, setup_success, setup_error) session.on('join', on_join) else: assert (False), 'logic error' # listen on leave events def on_leave(session, details): self.log.debug("session on_leave: {details}", details=details) # this could be a "leave" that's expected e.g. our # main() exited, or it could be an error if not txaio.is_called(done): if details.reason.startswith('wamp.error.'): txaio.reject( done, ApplicationError(details.reason, details.message)) else: txaio.resolve(done, None) session.on('leave', on_leave) # listen on disconnect events def on_disconnect(session, was_clean): self.log.debug("session on_disconnect: {was_clean}", was_clean=was_clean) if was_clean: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) else: txaio.reject( done, RuntimeError('transport closed uncleanly')) session.on('disconnect', on_disconnect) # return the fresh session object return session transport.connect_attempts += 1 d = self._connect_transport(reactor, transport, create_session) def on_connect_sucess(proto): # if e.g. an SSL handshake fails, we will have # successfully connected (here) but need to listen for the # "connectionLost" from the underlying protocol in case of # handshake failure .. so we wrap it. Also, we don't # increment transport.success_count here. orig = proto.connectionLost @wraps(orig) def lost(fail): rtn = orig(fail) if not txaio.is_called(done): txaio.reject(done, fail) return rtn proto.connectionLost = lost def on_connect_failure(err): transport.connect_failures += 1 # failed to establish a connection in the first place done.errback(err) txaio.add_callbacks(d, on_connect_sucess, on_connect_failure) return done
def _run(reactor, components, done_callback): """ Internal helper. Use "run" method from autobahn.twisted.wamp or autobahn.asyncio.wamp This is the generic parts of the run() method so that there's very little code in the twisted/asyncio specific run() methods. This is called by react() (or run_until_complete() so any errors coming out of this should be handled properly. Logging will already be started. """ # let user pass a single component to run, too # XXX probably want IComponent? only demand it, here and below? if isinstance(components, Component): components = [components] if type(components) != list: raise ValueError( '"components" must be a list of Component objects - encountered' ' {0}'.format(type(components))) for c in components: if not isinstance(c, Component): raise ValueError( '"components" must be a list of Component objects - encountered' 'item of type {0}'.format(type(c))) # validation complete; proceed with startup log = txaio.make_logger() def component_success(comp, arg): log.debug("Component '{c}' successfully completed: {arg}", c=comp, arg=arg) return arg def component_failure(comp, f): log.error("Component '{c}' error: {msg}", c=comp, msg=txaio.failure_message(f)) log.debug("Component error: {tb}", tb=txaio.failure_format_traceback(f)) # double-check: is a component-failure still fatal to the # startup process (because we passed consume_exception=False # to gather() below?) return None def component_start(comp): # the future from start() errbacks if we fail, or callbacks # when the component is considered "done" (so maybe never) d = txaio.as_future(comp.start, reactor) txaio.add_callbacks( d, partial(component_success, comp), partial(component_failure, comp), ) return d # note that these are started in parallel -- maybe we want to add # a "connected" signal to components so we could start them in the # order they're given to run() as "a" solution to dependencies. dl = [] for comp in components: d = component_start(comp) dl.append(d) done_d = txaio.gather(dl, consume_exceptions=False) def all_done(arg): log.debug("All components ended; stopping reactor") done_callback(reactor, arg) txaio.add_callbacks(done_d, all_done, all_done) return done_d
def send(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransport.send` """ if isinstance(msg, message.Hello): self._router = self._routerFactory.get(msg.realm) # fake session ID assignment (normally done in WAMP opening handshake) self._session._session_id = util.id() # set fixed/trusted authentication information self._session._authid = self._trusted_authid self._session._authrole = self._trusted_authrole self._session._authmethod = None # FIXME: the following does blow up # self._session._authmethod = u'trusted' self._session._authprovider = None # add app session to router self._router.attach(self._session) # fake app session open # details = SessionDetails(self._session._realm, self._session._session_id, self._session._authid, self._session._authrole, self._session._authmethod, self._session._authprovider) # fire onOpen callback and handle any exception escaping from there d = txaio.as_future(self._session.onJoin, details) txaio.add_callbacks(d, None, lambda fail: self._swallow_error(fail, "While firing onJoin")) # app-to-router # elif isinstance(msg, (message.Publish, message.Subscribe, message.Unsubscribe, message.Call, message.Yield, message.Register, message.Unregister, message.Cancel)) or \ (isinstance(msg, message.Error) and msg.request_type == message.Invocation.MESSAGE_TYPE): # deliver message to router # self._router.process(self._session, msg) # router-to-app # elif isinstance(msg, (message.Event, message.Invocation, message.Result, message.Published, message.Subscribed, message.Unsubscribed, message.Registered, message.Unregistered)) or \ (isinstance(msg, message.Error) and (msg.request_type in { message.Call.MESSAGE_TYPE, message.Cancel.MESSAGE_TYPE, message.Register.MESSAGE_TYPE, message.Unregister.MESSAGE_TYPE, message.Publish.MESSAGE_TYPE, message.Subscribe.MESSAGE_TYPE, message.Unsubscribe.MESSAGE_TYPE})): # deliver message to app session # self._session.onMessage(msg) # ignore messages # elif isinstance(msg, message.Goodbye): # fire onClose callback and handle any exception escaping from there d = txaio.as_future(self._session.onClose, None) txaio.add_callbacks(d, None, lambda fail: self._swallow_error(fail, "While firing onClose")) else: # should not arrive here # raise Exception("RouterApplicationSession.send: unhandled message {0}".format(msg))
def onMessage(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage` """ if self._session_id is None: if not self._pending_session_id: self._pending_session_id = util.id() def welcome(realm, authid=None, authrole=None, authmethod=None, authprovider=None): self._session_id = self._pending_session_id self._pending_session_id = None self._goodbye_sent = False self._router = self._router_factory.get(realm) if not self._router: raise Exception("no such realm") self._authid = authid self._authrole = authrole self._authmethod = authmethod self._authprovider = authprovider roles = self._router.attach(self) msg = message.Welcome(self._session_id, roles, authid=authid, authrole=authrole, authmethod=authmethod, authprovider=authprovider) self._transport.send(msg) self.onJoin( SessionDetails(self._realm, self._session_id, self._authid, self._authrole, self._authmethod, self._authprovider)) # the first message MUST be HELLO if isinstance(msg, message.Hello): self._realm = msg.realm self._session_roles = msg.roles details = types.HelloDetails(msg.roles, msg.authmethods, msg.authid, self._pending_session_id) d = txaio.as_future(self.onHello, self._realm, details) def success(res): msg = None if isinstance(res, types.Accept): welcome(self._realm, res.authid, res.authrole, res.authmethod, res.authprovider) elif isinstance(res, types.Challenge): msg = message.Challenge(res.method, res.extra) elif isinstance(res, types.Deny): msg = message.Abort(res.reason, res.message) else: pass if msg: self._transport.send(msg) txaio.add_callbacks(d, success, self._swallow_error_and_abort) elif isinstance(msg, message.Authenticate): d = txaio.as_future(self.onAuthenticate, msg.signature, {}) def success(res): msg = None if isinstance(res, types.Accept): welcome(self._realm, res.authid, res.authrole, res.authmethod, res.authprovider) elif isinstance(res, types.Deny): msg = message.Abort(res.reason, res.message) else: pass if msg: self._transport.send(msg) txaio.add_callbacks(d, success, self._swallow_error_and_abort) elif isinstance(msg, message.Abort): # fire callback and close the transport self.onLeave(types.CloseDetails(msg.reason, msg.message)) self._session_id = None self._pending_session_id = None # self._transport.close() else: raise ProtocolError( "Received {0} message, and session is not yet established". format(msg.__class__)) else: if isinstance(msg, message.Hello): raise ProtocolError( u"HELLO message received, while session is already established" ) elif isinstance(msg, message.Goodbye): if not self._goodbye_sent: # the peer wants to close: send GOODBYE reply reply = message.Goodbye() self._transport.send(reply) # fire callback and close the transport self.onLeave(types.CloseDetails(msg.reason, msg.message)) self._router.detach(self) self._session_id = None self._pending_session_id = None # self._transport.close() else: self._router.process(self, msg)
def onMessage(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage` """ if self._session_id is None: if not self._pending_session_id: self._pending_session_id = util.id() def welcome(realm, authid=None, authrole=None, authmethod=None, authprovider=None): self._session_id = self._pending_session_id self._pending_session_id = None self._goodbye_sent = False self._router = self._router_factory.get(realm) if not self._router: raise Exception("no such realm") self._authid = authid self._authrole = authrole self._authmethod = authmethod self._authprovider = authprovider roles = self._router.attach(self) msg = message.Welcome(self._session_id, roles, authid=authid, authrole=authrole, authmethod=authmethod, authprovider=authprovider) self._transport.send(msg) self.onJoin(SessionDetails(self._realm, self._session_id, self._authid, self._authrole, self._authmethod, self._authprovider)) # the first message MUST be HELLO if isinstance(msg, message.Hello): self._realm = msg.realm self._session_roles = msg.roles details = types.HelloDetails(msg.roles, msg.authmethods, msg.authid, self._pending_session_id) d = txaio.as_future(self.onHello, self._realm, details) def success(res): msg = None if isinstance(res, types.Accept): welcome(self._realm, res.authid, res.authrole, res.authmethod, res.authprovider) elif isinstance(res, types.Challenge): msg = message.Challenge(res.method, res.extra) elif isinstance(res, types.Deny): msg = message.Abort(res.reason, res.message) else: pass if msg: self._transport.send(msg) txaio.add_callbacks(d, success, self._swallow_error_and_abort) elif isinstance(msg, message.Authenticate): d = txaio.as_future(self.onAuthenticate, msg.signature, {}) def success(res): msg = None if isinstance(res, types.Accept): welcome(self._realm, res.authid, res.authrole, res.authmethod, res.authprovider) elif isinstance(res, types.Deny): msg = message.Abort(res.reason, res.message) else: pass if msg: self._transport.send(msg) txaio.add_callbacks(d, success, self._swallow_error_and_abort) elif isinstance(msg, message.Abort): # fire callback and close the transport self.onLeave(types.CloseDetails(msg.reason, msg.message)) self._session_id = None self._pending_session_id = None # self._transport.close() else: raise ProtocolError("Received {0} message, and session is not yet established".format(msg.__class__)) else: if isinstance(msg, message.Hello): raise ProtocolError(u"HELLO message received, while session is already established") elif isinstance(msg, message.Goodbye): if not self._goodbye_sent: # The peer wants to close: answer with GOODBYE reply. # Note: We MUST NOT send any WAMP message _after_ GOODBYE reply = message.Goodbye() self._transport.send(reply) self._goodbye_sent = True else: # This is the peer's GOODBYE reply to our own earlier GOODBYE pass # We need to first detach the session from the router before # erasing the session ID below .. self._router.detach(self) # In order to send wamp.session.on_leave properly # (i.e. *with* the proper session_id) we save it previous_session_id = self._session_id # At this point, we've either sent GOODBYE already earlier, # or we have just responded with GOODBYE. In any case, we MUST NOT # send any WAMP message from now on: # clear out session ID, so that anything that might be triggered # in the onLeave below is prohibited from sending WAMP stuff. # E.g. the client might have been subscribed to meta events like # wamp.session.on_leave - and we must not send that client's own # leave to itself! self._session_id = None self._pending_session_id = None # publish event, *after* self._session_id is None so # that we don't publish to ourselves as well (if this # session happens to be subscribed to wamp.session.on_leave) if self._service_session: self._service_session.publish( u'wamp.session.on_leave', previous_session_id, ) # fire callback and close the transport self.onLeave(types.CloseDetails(msg.reason, msg.message)) # don't close the transport, as WAMP allows to reattach a session # to the same or a different realm without closing the transport # self._transport.close() else: self._router.process(self, msg)
def processCall(self, session, call): """ Implements :func:`crossbar.router.interfaces.IDealer.processCall` """ # check procedure URI: for CALL, must be valid URI (either strict or loose), and # all URI components must be non-empty if self._option_uri_strict: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(call.procedure) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(call.procedure) if not uri_is_valid: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.INVALID_URI, [ u"call with invalid procedure URI '{0}' (URI strict checking {1})" .format(call.procedure, self._option_uri_strict) ]) session._transport.send(reply) return # get registrations active on the procedure called # registration = self._registration_map.best_matching_observation( call.procedure) if registration: # validate payload # try: self._router.validate('call', call.procedure, call.args, call.kwargs) except Exception as e: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.INVALID_ARGUMENT, [ u"call of procedure '{0}' with invalid application payload: {1}" .format(call.procedure, e) ]) session._transport.send(reply) return # authorize CALL action # d = txaio.as_future(self._router.authorize, session, call.procedure, RouterAction.ACTION_CALL) def on_authorize_success(authorized): # the call to authorize the action _itself_ succeeded. now go on depending on whether # the action was actually authorized or not .. # if not authorized: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.NOT_AUTHORIZED, [ u"session is not authorized to call procedure '{0}'" .format(call.procedure) ]) session._transport.send(reply) else: # determine callee according to invocation policy # if registration.extra.invoke == message.Register.INVOKE_SINGLE: callee = registration.observers[0] elif registration.extra.invoke == message.Register.INVOKE_FIRST: callee = registration.observers[0] elif registration.extra.invoke == message.Register.INVOKE_LAST: callee = registration.observers[ len(registration.observers) - 1] elif registration.extra.invoke == message.Register.INVOKE_ROUNDROBIN: callee = registration.observers[ registration.extra.roundrobin_current % len(registration.observers)] registration.extra.roundrobin_current += 1 elif registration.extra.invoke == message.Register.INVOKE_RANDOM: callee = registration.observers[random.randint( 0, len(registration.observers) - 1)] else: # should not arrive here raise Exception(u"logic error") # new ID for the invocation # invocation_request_id = self._request_id_gen.next() # caller disclosure # if call.disclose_me: caller = session._session_id else: caller = None # for pattern-based registrations, the INVOCATION must contain # the actual procedure being called # if registration.match != message.Register.MATCH_EXACT: procedure = call.procedure else: procedure = None invocation = message.Invocation( invocation_request_id, registration.id, args=call.args, kwargs=call.kwargs, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, procedure=procedure) self._invocations[ invocation_request_id] = InvocationRequest( invocation_request_id, session, call) callee._transport.send(invocation) def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ self.log.failure(err) reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.AUTHORIZATION_FAILED, [ u"failed to authorize session for calling procedure '{0}': {1}" .format(call.procedure, err.value) ]) session._transport.send(reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error) else: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.NO_SUCH_PROCEDURE, [ u"no callee registered for procedure '{0}'".format( call.procedure) ]) session._transport.send(reply)
def processPublish(self, session, publish): """ Implements :func:`crossbar.router.interfaces.IBroker.processPublish` """ # check topic URI: for PUBLISH, must be valid URI (either strict or loose), and # all URI components must be non-empty if self._option_uri_strict: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(publish.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(publish.topic) if not uri_is_valid: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [ "publish with invalid topic URI '{0}' (URI strict checking {1})" .format(publish.topic, self._option_uri_strict) ]) session._transport.send(reply) return # disallow publication to topics starting with "wamp." and # "crossbar." other than for trusted session (that are sessions # built into Crossbar.io) if session._authrole is not None and session._authrole != u"trusted": if publish.topic.startswith(u"wamp.") or publish.topic.startswith( u"crossbar."): if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [ "publish with restricted topic URI '{0}'".format( publish.topic) ]) session._transport.send(reply) return # get subscriptions active on the topic published to # subscriptions = self._subscription_map.match_observations( publish.topic) # go on if there are any active subscriptions or the publish is to be acknowledged # otherwise there isn't anything to do anyway. # if subscriptions or publish.acknowledge: # validate payload # try: self._router.validate('event', publish.topic, publish.args, publish.kwargs) except Exception as e: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_ARGUMENT, [ "publish to topic URI '{0}' with invalid application payload: {1}" .format(publish.topic, e) ]) session._transport.send(reply) return # authorize PUBLISH action # d = txaio.as_future(self._router.authorize, session, publish.topic, IRouter.ACTION_PUBLISH) def on_authorize_success(authorized): # the call to authorize the action _itself_ succeeded. now go on depending on whether # the action was actually authorized or not .. # if not authorized: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.NOT_AUTHORIZED, [ "session not authorized to publish to topic '{0}'" .format(publish.topic) ]) session._transport.send(reply) else: # new ID for the publication # publication = util.id() # send publish acknowledge immediately when requested # if publish.acknowledge: msg = message.Published(publish.request, publication) session._transport.send(msg) # publisher disclosure # if publish.disclose_me: publisher = session._session_id else: publisher = None # skip publisher # if publish.exclude_me is None or publish.exclude_me: me_also = False else: me_also = True # iterate over all subscriptions .. # for subscription in subscriptions: # initial list of receivers are all subscribers on a subscription .. # receivers = subscription.observers # filter by "eligible" receivers # if publish.eligible: # map eligible session IDs to eligible sessions eligible = [] for session_id in publish.eligible: if session_id in self._router._session_id_to_session: eligible.append( self._router. _session_id_to_session[session_id]) # filter receivers for eligible sessions receivers = set(eligible) & receivers # remove "excluded" receivers # if publish.exclude: # map excluded session IDs to excluded sessions exclude = [] for s in publish.exclude: if s in self._router._session_id_to_session: exclude.append( self._router._session_id_to_session[s]) # filter receivers for excluded sessions if exclude: receivers = receivers - set(exclude) # if receivers is non-empty, dispatch event .. # if receivers: # for pattern-based subscriptions, the EVENT must contain # the actual topic being published to # if subscription.match != message.Subscribe.MATCH_EXACT: topic = publish.topic else: topic = None msg = message.Event(subscription.id, publication, args=publish.args, kwargs=publish.kwargs, publisher=publisher, topic=topic) for receiver in receivers: if me_also or receiver != session: # the receiving subscriber session might have been lost in the meantime .. if receiver._transport: receiver._transport.send(msg) def on_authorize_error(err): # the call to authorize the action _itself_ failed (note this is different from the # call to authorize succeed, but the authorization being denied) # if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.AUTHORIZATION_FAILED, [ "failed to authorize session for publishing to topic URI '{0}': {1}" .format(publish.topic, err.value) ]) session._transport.send(reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def processRegister(self, session, register): """ Implements :func:`crossbar.router.interfaces.IDealer.processRegister` """ # check topic URI: for SUBSCRIBE, must be valid URI (either strict or loose), and all # URI components must be non-empty other than for wildcard subscriptions # if self._option_uri_strict: if register.match == u"wildcard": uri_is_valid = _URI_PAT_STRICT_EMPTY.match(register.procedure) else: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match( register.procedure) else: if register.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(register.procedure) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match( register.procedure) if not uri_is_valid: reply = message.Error( message.Register.MESSAGE_TYPE, register.request, ApplicationError.INVALID_URI, [ u"register for invalid procedure URI '{0}' (URI strict checking {1})" .format(register.procedure, self._option_uri_strict) ]) session._transport.send(reply) return # disallow registration of procedures starting with "wamp." and "crossbar." other than for # trusted sessions (that are sessions built into Crossbar.io) # if session._authrole is not None and session._authrole != u"trusted": is_restricted = register.procedure.startswith( u"wamp.") or register.procedure.startswith(u"crossbar.") if is_restricted: reply = message.Error( message.Register.MESSAGE_TYPE, register.request, ApplicationError.INVALID_URI, [ u"register for restricted procedure URI '{0}')".format( register.procedure) ]) session._transport.send(reply) return # get existing registration for procedure / matching strategy - if any # registration = self._registration_map.get_observation( register.procedure, register.match) if registration: # there is an existing registration, and that has an invocation strategy that only allows a single callee # on a the given registration # if registration.extra.invoke == message.Register.INVOKE_SINGLE: reply = message.Error( message.Register.MESSAGE_TYPE, register.request, ApplicationError.PROCEDURE_ALREADY_EXISTS, [ u"register for already registered procedure '{0}'". format(register.procedure) ]) session._transport.send(reply) return # there is an existing registration, and that has an invokation strategy different from the one # requested by the new callee # if registration.extra.invoke != register.invoke: reply = message.Error( message.Register.MESSAGE_TYPE, register.request, ApplicationError. PROCEDURE_EXISTS_INVOCATION_POLICY_CONFLICT, [ u"register for already registered procedure '{0}' with conflicting invocation policy (has {1} and {2} was requested)" .format(register.procedure, registration.extra.invoke, register.invoke) ]) session._transport.send(reply) return # authorize action # d = txaio.as_future(self._router.authorize, session, register.procedure, RouterAction.ACTION_REGISTER) def on_authorize_success(authorized): if not authorized: # error reply since session is not authorized to register # reply = message.Error( message.Register.MESSAGE_TYPE, register.request, ApplicationError.NOT_AUTHORIZED, [ u"session is not authorized to register procedure '{0}'" .format(register.procedure) ]) else: # ok, session authorized to register. now get the registration # registration_extra = RegistrationExtra(register.invoke) registration, was_already_registered, is_first_callee = self._registration_map.add_observer( session, register.procedure, register.match, registration_extra) if not was_already_registered: self._session_to_registrations[session].add(registration) # publish WAMP meta events # if self._router._realm: service_session = self._router._realm.session if service_session and not registration.uri.startswith( u'wamp.'): if is_first_callee: registration_details = { 'id': registration.id, 'created': registration.created, 'uri': registration.uri, 'match': registration.match, 'invoke': registration.extra.invoke, } service_session.publish( u'wamp.registration.on_create', session._session_id, registration_details) if not was_already_registered: service_session.publish( u'wamp.registration.on_register', session._session_id, registration.id) # acknowledge register with registration ID # reply = message.Registered(register.request, registration.id) # send out reply to register requestor # session._transport.send(reply) def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ self.log.failure(err) reply = message.Error( message.Register.MESSAGE_TYPE, register.request, ApplicationError.AUTHORIZATION_FAILED, [ u"failed to authorize session for registering procedure '{0}': {1}" .format(register.procedure, err.value) ]) session._transport.send(reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def processPublish(self, session, publish): """ Implements :func:`crossbar.router.interfaces.IBroker.processPublish` """ # check topic URI: for PUBLISH, must be valid URI (either strict or loose), and # all URI components must be non-empty if self._option_uri_strict: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(publish.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(publish.topic) if not uri_is_valid: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [ u"publish with invalid topic URI '{0}' (URI strict checking {1})" .format(publish.topic, self._option_uri_strict) ]) self._router.send(session, reply) return # disallow publication to topics starting with "wamp." other than for # trusted sessions (that are sessions built into Crossbar.io routing core) # if session._authrole is not None and session._authrole != u"trusted": is_restricted = publish.topic.startswith(u"wamp.") if is_restricted: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [ u"publish with restricted topic URI '{0}'".format( publish.topic) ]) self._router.send(session, reply) return # get subscriptions active on the topic published to # subscriptions = self._subscription_map.match_observations( publish.topic) # check if the event is being persisted by checking if we ourself are among the observers # on _any_ matching subscription # we've been previously added to observer lists on subscriptions ultimately from # node configuration and during the broker starts up. store_event = False if self._event_store: for subscription in subscriptions: if self._event_store in subscription.observers: store_event = True break if store_event: self.log.debug('Persisting event on topic "{topic}"', topic=publish.topic) # check if the event is to be retained by inspecting the 'retain' flag retain_event = False if publish.retain: retain_event = True # go on if (otherwise there isn't anything to do anyway): # # - there are any active subscriptions OR # - the publish is to be acknowledged OR # - the event is to be persisted OR # - the event is to be retained # if subscriptions or publish.acknowledge or store_event or retain_event: # If it's a MQTT publish, we need to adjust the arguments. if getattr(publish, "_mqtt_publish", False): from crossbar.adapter.mqtt.wamp import mqtt_payload_transform tfd = mqtt_payload_transform(self._router._mqtt_payload_format, publish.payload) if not tfd: # If we have no message to give, drop it entirely return else: publish.payload = None publish.args, publish.kwargs = tfd # validate payload # if publish.payload is None: try: self._router.validate('event', publish.topic, publish.args, publish.kwargs) except Exception as e: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_ARGUMENT, [ u"publish to topic URI '{0}' with invalid application payload: {1}" .format(publish.topic, e) ]) self._router.send(session, reply) return # authorize PUBLISH action # d = self._router.authorize(session, publish.topic, u'publish') def on_authorize_success(authorization): # the call to authorize the action _itself_ succeeded. now go on depending on whether # the action was actually authorized or not .. # if not authorization[u'allow']: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.NOT_AUTHORIZED, [ u"session not authorized to publish to topic '{0}'" .format(publish.topic) ]) self._router.send(session, reply) else: # new ID for the publication # publication = util.id() # persist event (this is done only once, regardless of the number of subscriptions # the event matches on) # if store_event: self._event_store.store_event(session._session_id, publication, publish.topic, publish.args, publish.kwargs) # retain event on the topic # if retain_event: observation = self._subscription_map.get_observation( publish.topic) if not observation: # No observation, lets make a new one observation = self._subscription_map.create_observation( publish.topic, extra=SubscriptionExtra()) if observation.extra.retained_events: if not publish.eligible and not publish.exclude: observation.extra.retained_events = [publish] else: observation.extra.retained_events.append( publish) else: observation.extra.retained_events = [publish] # send publish acknowledge immediately when requested # if publish.acknowledge: reply = message.Published(publish.request, publication) self._router.send(session, reply) # publisher disclosure # if authorization[u'disclose']: disclose = True elif (publish.topic.startswith(u"wamp.") or publish.topic.startswith(u"crossbar.")): disclose = True else: disclose = False if disclose: publisher = session._session_id publisher_authid = session._authid publisher_authrole = session._authrole else: publisher = None publisher_authid = None publisher_authrole = None # skip publisher # if publish.exclude_me is None or publish.exclude_me: me_also = False else: me_also = True # iterate over all subscriptions .. # for subscription in subscriptions: # persist event history, but check if it is persisted on the individual subscription! # if store_event and self._event_store in subscription.observers: self._event_store.store_event_history( publication, subscription.id) # initial list of receivers are all subscribers on a subscription .. # receivers = subscription.observers # filter by "eligible" receivers # if publish.eligible: # map eligible session IDs to eligible sessions eligible = [] for session_id in publish.eligible: if session_id in self._router._session_id_to_session: eligible.append( self._router. _session_id_to_session[session_id]) # filter receivers for eligible sessions receivers = set(eligible) & receivers # remove "excluded" receivers # if publish.exclude: # map excluded session IDs to excluded sessions exclude = [] for s in publish.exclude: if s in self._router._session_id_to_session: exclude.append( self._router._session_id_to_session[s]) # filter receivers for excluded sessions if exclude: receivers = receivers - set(exclude) # if receivers is non-empty, dispatch event .. # receivers_cnt = len(receivers) - (1 if self in receivers else 0) if receivers_cnt: # for pattern-based subscriptions, the EVENT must contain # the actual topic being published to # if subscription.match != message.Subscribe.MATCH_EXACT: topic = publish.topic else: topic = None if publish.payload: msg = message.Event( subscription.id, publication, payload=publish.payload, publisher=publisher, publisher_authid=publisher_authid, publisher_authrole=publisher_authrole, topic=topic, enc_algo=publish.enc_algo, enc_key=publish.enc_key, enc_serializer=publish.enc_serializer) else: msg = message.Event( subscription.id, publication, args=publish.args, kwargs=publish.kwargs, publisher=publisher, publisher_authid=publisher_authid, publisher_authrole=publisher_authrole, topic=topic) for receiver in receivers: if (me_also or receiver != session ) and receiver != self._event_store: # the receiving subscriber session # might have no transport, or no # longer be joined if receiver._session_id and receiver._transport: self._router.send(receiver, msg) def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ self.log.failure("Authorization failed", failure=err) if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.AUTHORIZATION_FAILED, [ u"failed to authorize session for publishing to topic URI '{0}': {1}" .format(publish.topic, err.value) ]) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def processPublish(self, session, publish): """ Implements :func:`crossbar.router.interfaces.IBroker.processPublish` """ # check topic URI: for PUBLISH, must be valid URI (either strict or loose), and # all URI components must be non-empty if self._option_uri_strict: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(publish.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(publish.topic) if not uri_is_valid: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [ u"publish with invalid topic URI '{0}' (URI strict checking {1})" .format(publish.topic, self._option_uri_strict) ]) self._router.send(session, reply) return # disallow publication to topics starting with "wamp." other than for # trusted sessions (that are sessions built into Crossbar.io routing core) # if session._authrole is not None and session._authrole != u"trusted": is_restricted = publish.topic.startswith(u"wamp.") if is_restricted: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_URI, [ u"publish with restricted topic URI '{0}'".format( publish.topic) ]) self._router.send(session, reply) return # get subscriptions active on the topic published to # subscriptions = self._subscription_map.match_observations( publish.topic) # check if the event is being persisted by checking if we ourself are among the observers # on _any_ matching subscription # we've been previously added to observer lists on subscriptions ultimately from # node configuration and during the broker starts up. store_event = False if self._event_store: for subscription in subscriptions: if self._event_store in subscription.observers: store_event = True break if store_event: self.log.debug('Persisting event on topic "{topic}"', topic=publish.topic) # check if the event is to be retained by inspecting the 'retain' flag retain_event = False if publish.retain: retain_event = True # go on if (otherwise there isn't anything to do anyway): # # - there are any active subscriptions OR # - the publish is to be acknowledged OR # - the event is to be persisted OR # - the event is to be retained # if subscriptions or publish.acknowledge or store_event or retain_event: # validate payload # if publish.payload is None: try: self._router.validate('event', publish.topic, publish.args, publish.kwargs) except Exception as e: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.INVALID_ARGUMENT, [ u"publish to topic URI '{0}' with invalid application payload: {1}" .format(publish.topic, e) ]) self._router.send(session, reply) return # authorize PUBLISH action # d = self._router.authorize(session, publish.topic, u'publish', options=publish.marshal_options()) def on_authorize_success(authorization): # the call to authorize the action _itself_ succeeded. now go on depending on whether # the action was actually authorized or not .. # if not authorization[u'allow']: if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.NOT_AUTHORIZED, [ u"session not authorized to publish to topic '{0}'" .format(publish.topic) ]) self._router.send(session, reply) else: # new ID for the publication # publication = util.id() # send publish acknowledge immediately when requested # if publish.acknowledge: reply = message.Published(publish.request, publication) self._router.send(session, reply) # publisher disclosure # if authorization[u'disclose']: disclose = True elif (publish.topic.startswith(u"wamp.") or publish.topic.startswith(u"crossbar.")): disclose = True else: disclose = False if disclose: publisher = session._session_id publisher_authid = session._authid publisher_authrole = session._authrole else: publisher = None publisher_authid = None publisher_authrole = None # skip publisher # if publish.exclude_me is None or publish.exclude_me: me_also = False else: me_also = True # persist event (this is done only once, regardless of the number of subscriptions # the event matches on) # if store_event: self._event_store.store_event(session._session_id, publication, publish.topic, publish.args, publish.kwargs) # retain event on the topic # if retain_event: retained_event = RetainedEvent(publish, publisher, publisher_authid, publisher_authrole) observation = self._subscription_map.get_observation( publish.topic) if not observation: # No observation, lets make a new one observation = self._subscription_map.create_observation( publish.topic, extra=SubscriptionExtra()) else: # this can happen if event-history is # enabled on the topic: the event-store # creates an observation before any client # could possible hit the code above if observation.extra is None: observation.extra = SubscriptionExtra() elif not isinstance(observation.extra, SubscriptionExtra): raise Exception( "incorrect 'extra' for '{}'".format( publish.topic)) if observation.extra.retained_events: if not publish.eligible and not publish.exclude: observation.extra.retained_events = [ retained_event ] else: observation.extra.retained_events.append( retained_event) else: observation.extra.retained_events = [ retained_event ] all_dl = [] # iterate over all subscriptions .. # for subscription in subscriptions: self.log.debug( 'dispatching for subscription={subscription}', subscription=subscription) # persist event history, but check if it is persisted on the individual subscription! # if store_event and self._event_store in subscription.observers: self._event_store.store_event_history( publication, subscription.id) # initial list of receivers are all subscribers on a subscription .. # receivers = subscription.observers receivers = self._filter_publish_receivers( receivers, publish) # if receivers is non-empty, dispatch event .. # receivers_cnt = len(receivers) - (1 if self in receivers else 0) if receivers_cnt: # for pattern-based subscriptions, the EVENT must contain # the actual topic being published to # if subscription.match != message.Subscribe.MATCH_EXACT: topic = publish.topic else: topic = None if publish.payload: msg = message.Event( subscription.id, publication, payload=publish.payload, publisher=publisher, publisher_authid=publisher_authid, publisher_authrole=publisher_authrole, topic=topic, enc_algo=publish.enc_algo, enc_key=publish.enc_key, enc_serializer=publish.enc_serializer) else: msg = message.Event( subscription.id, publication, args=publish.args, kwargs=publish.kwargs, publisher=publisher, publisher_authid=publisher_authid, publisher_authrole=publisher_authrole, topic=topic) chunk_size = self._options.event_dispatching_chunk_size if chunk_size: self.log.debug( 'chunked dispatching to {receivers_size} with chunk_size={chunk_size}', receivers_size=len(receivers), chunk_size=chunk_size) # a Deferred that fires when all chunks are done all_d = txaio.create_future() all_dl.append(all_d) def _notify_some(receivers): for receiver in receivers[:chunk_size]: if ( me_also or receiver != session ) and receiver != self._event_store: # the receiving subscriber session # might have no transport, or no # longer be joined if receiver._session_id and receiver._transport: self._router.send( receiver, msg) receivers = receivers[chunk_size:] if len(receivers) > 0: # still more to do .. return txaio.call_later( 0, _notify_some, receivers) else: # all done! resolve all_d, which represents all receivers # to a single subscription matching the event txaio.resolve(all_d, None) _notify_some(list(receivers)) else: self.log.debug( 'unchunked dispatching to {receivers_size} receivers', receivers_size=len(receivers)) for receiver in receivers: if (me_also or receiver != session ) and receiver != self._event_store: # the receiving subscriber session # might have no transport, or no # longer be joined if receiver._session_id and receiver._transport: self._router.send(receiver, msg) return txaio.gather(all_dl) def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ self.log.failure("Authorization failed", failure=err) if publish.acknowledge: reply = message.Error( message.Publish.MESSAGE_TYPE, publish.request, ApplicationError.AUTHORIZATION_FAILED, [ u"failed to authorize session for publishing to topic URI '{0}': {1}" .format(publish.topic, err.value) ]) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def _connect_once(self, reactor, transport): self.log.debug( 'connecting once using transport type "{transport_type}" ' 'over endpoint "{endpoint_desc}"', transport_type=transport.type, endpoint_desc=transport.describe_endpoint(), ) done = txaio.create_future() # factory for ISession objects def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: self._session = session = self.session_factory(cfg) for auth_name, auth_config in self._authentication.items(): if isinstance(auth_config, IAuthenticator): session.add_authenticator(auth_config) else: authenticator = create_authenticator( auth_name, **auth_config) session.add_authenticator(authenticator) except Exception as e: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that f = txaio.create_failure(e) txaio.reject(done, f) raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the # connection. This lets you do component.on('join', # cb) which will work just as if you called # session.on('join', cb) for every session created. session._parent = self # listen on leave events; if we get errors # (e.g. no_such_realm), an on_leave can happen without # an on_join before def on_leave(session, details): self.log.info( "session leaving '{details.reason}'", details=details, ) if not txaio.is_called(done): if details.reason in [ "wamp.close.normal", "wamp.close.goodbye_and_out" ]: txaio.resolve(done, None) else: f = txaio.create_failure( ApplicationError(details.reason, details.message)) txaio.reject(done, f) session.on('leave', on_leave) # if we were given a "main" procedure, we run through # it completely (i.e. until its Deferred fires) and # then disconnect this session def on_join(session, details): transport.connect_sucesses += 1 self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") def leave(): try: session.leave() except SessionNotReady: # someone may have already called # leave() pass txaio.call_later(0, leave) def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) session.disconnect() txaio.add_callbacks(d, main_success, main_error) if self._entry is not None: session.on('join', on_join) # listen on disconnect events. Note that in case we # had a "main" procedure, we could have already # resolve()'d our "done" future def on_disconnect(session, was_clean): self.log.debug( "session on_disconnect: was_clean={was_clean}", was_clean=was_clean, ) if not txaio.is_called(done): if not was_clean: self.log.warn("Session disconnected uncleanly") else: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) session.on('disconnect', on_disconnect) # return the fresh session object return session transport.connect_attempts += 1 d = txaio.as_future( self._connect_transport, reactor, transport, create_session, done, ) def on_error(err): """ this may seem redundant after looking at _connect_transport, but it will handle a case where something goes wrong in _connect_transport itself -- as the only connect our caller has is the 'done' future """ transport.connect_failures += 1 # something bad has happened, and maybe didn't get caught # upstream yet if not txaio.is_called(done): txaio.reject(done, err) txaio.add_callbacks(d, None, on_error) return done
def _connect_once(self, reactor, transport_config): self.log.info( 'connecting once using transport type "{transport_type}" ' 'over endpoint type "{endpoint_type}"', transport_type=transport_config['type'], endpoint_type=transport_config['endpoint']['type'], ) done = txaio.create_future() # factory for ISession objects def create_session(): cfg = ComponentConfig(self._realm, self._extra) try: session = self.session(cfg) except Exception: # couldn't instantiate session calls, which is fatal. # let the reconnection logic deal with that raise else: # hook up the listener to the parent so we can bubble # up events happning on the session onto the connection session._parent = self if self._entry_type == Component.TYPE_MAIN: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def main_success(_): self.log.debug("main_success") txaio.resolve(done, None) def main_error(err): self.log.debug("main_error: {err}", err=err) txaio.reject(done, err) txaio.add_callbacks(d, main_success, main_error) session.on('join', on_join) elif self._entry_type == Component.TYPE_SETUP: def on_join(session, details): self.log.debug("session on_join: {details}", details=details) d = txaio.as_future(self._entry, reactor, session) def setup_success(_): self.log.debug("setup_success") def setup_error(err): self.log.debug("setup_error: {err}", err=err) txaio.add_callbacks(d, setup_success, setup_error) session.on('join', on_join) else: assert (False), 'logic error' # listen on leave events def on_leave(session, details): self.log.debug("session on_leave: {details}", details=details) session.on('leave', on_leave) # listen on disconnect events def on_disconnect(session, was_clean): self.log.debug("session on_disconnect: {was_clean}", was_clean=was_clean) if was_clean: # eg the session has left the realm, and the transport was properly # shut down. successfully finish the connection txaio.resolve(done, None) else: txaio.reject( done, RuntimeError('transport closed uncleanly')) session.on('disconnect', on_disconnect) # return the fresh session object return session d = self._connect_transport(reactor, transport_config, create_session) def on_connect_sucess(proto): # FIXME: leave / cleanup proto when reactor stops? pass def on_connect_failure(err): # failed to establish a connection in the first place done.errback(err) txaio.add_callbacks(d, on_connect_sucess, on_connect_failure) return done
def connect_error(fail): notify_f = notify_connect_error(fail) txaio.add_callbacks(notify_f, None, handle_connect_error)
def processSubscribe(self, session, subscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processSubscribe` """ if self._router.is_traced: if not subscribe.correlation_id: subscribe.correlation_id = self._router.new_correlation_id() subscribe.correlation_is_anchor = True subscribe.correlation_is_last = False if not subscribe.correlation_uri: subscribe.correlation_uri = subscribe.topic self._router._factory._worker._maybe_trace_rx_msg(session, subscribe) # check topic URI: for SUBSCRIBE, must be valid URI (either strict or loose), and all # URI components must be non-empty for normal subscriptions, may be empty for # wildcard subscriptions and must be non-empty for all but the last component for # prefix subscriptions # if self._option_uri_strict: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_STRICT_EMPTY.match(subscribe.topic) elif subscribe.match == u"prefix": uri_is_valid = _URI_PAT_STRICT_LAST_EMPTY.match(subscribe.topic) else: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(subscribe.topic) else: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(subscribe.topic) elif subscribe.match == u"prefix": uri_is_valid = _URI_PAT_LOOSE_LAST_EMPTY.match(subscribe.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(subscribe.topic) if not uri_is_valid: reply = message.Error(message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.INVALID_URI, [u"subscribe for invalid topic URI '{0}'".format(subscribe.topic)]) reply.correlation_id = subscribe.correlation_id reply.correlation_uri = subscribe.topic reply.correlation_is_anchor = False reply.correlation_is_last = True self._router.send(session, reply) return # authorize SUBSCRIBE action # d = self._router.authorize(session, subscribe.topic, u'subscribe', options=subscribe.marshal_options()) def on_authorize_success(authorization): if not authorization[u'allow']: # error reply since session is not authorized to subscribe # replies = [message.Error(message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.NOT_AUTHORIZED, [u"session is not authorized to subscribe to topic '{0}'".format(subscribe.topic)])] replies[0].correlation_id = subscribe.correlation_id replies[0].correlation_uri = subscribe.topic replies[0].correlation_is_anchor = False replies[0].correlation_is_last = True else: # ok, session authorized to subscribe. now get the subscription # subscription, was_already_subscribed, is_first_subscriber = self._subscription_map.add_observer(session, subscribe.topic, subscribe.match, extra=SubscriptionExtra()) if not was_already_subscribed: self._session_to_subscriptions[session].add(subscription) # publish WAMP meta events, if we have a service session, but # not for the meta API itself! # if self._router._realm and \ self._router._realm.session and \ not subscription.uri.startswith(u'wamp.') and \ (is_first_subscriber or not was_already_subscribed): has_follow_up_messages = True def _publish(): service_session = self._router._realm.session options = types.PublishOptions( correlation_id=subscribe.correlation_id, correlation_is_anchor=False, correlation_is_last=False, ) if is_first_subscriber: subscription_details = { u'id': subscription.id, u'created': subscription.created, u'uri': subscription.uri, u'match': subscription.match, } service_session.publish( u'wamp.subscription.on_create', session._session_id, subscription_details, options=options, ) if not was_already_subscribed: options.correlation_is_last = True service_session.publish( u'wamp.subscription.on_subscribe', session._session_id, subscription.id, options=options, ) # we postpone actual sending of meta events until we return to this client session self._reactor.callLater(0, _publish) else: has_follow_up_messages = False # check for retained events # def _get_retained_event(): if subscription.extra.retained_events: retained_events = list(subscription.extra.retained_events) retained_events.reverse() for retained_event in retained_events: authorized = False if not retained_event.publish.exclude and not retained_event.publish.eligible: authorized = True elif session._session_id in retained_event.publish.eligible and session._session_id not in retained_event.publish.exclude: authorized = True if authorized: publication = util.id() if retained_event.publish.payload: msg = message.Event(subscription.id, publication, payload=retained_event.publish.payload, enc_algo=retained_event.publish.enc_algo, enc_key=retained_event.publish.enc_key, enc_serializer=retained_event.publish.enc_serializer, publisher=retained_event.publisher, publisher_authid=retained_event.publisher_authid, publisher_authrole=retained_event.publisher_authrole, retained=True) else: msg = message.Event(subscription.id, publication, args=retained_event.publish.args, kwargs=retained_event.publish.kwargs, publisher=retained_event.publisher, publisher_authid=retained_event.publisher_authid, publisher_authrole=retained_event.publisher_authrole, retained=True) msg.correlation_id = subscribe.correlation_id msg.correlation_uri = subscribe.topic msg.correlation_is_anchor = False msg.correlation_is_last = False return [msg] return [] # acknowledge subscribe with subscription ID # replies = [message.Subscribed(subscribe.request, subscription.id)] replies[0].correlation_id = subscribe.correlation_id replies[0].correlation_uri = subscribe.topic replies[0].correlation_is_anchor = False replies[0].correlation_is_last = False if subscribe.get_retained: replies.extend(_get_retained_event()) replies[-1].correlation_is_last = not has_follow_up_messages # send out reply to subscribe requestor # [self._router.send(session, reply) for reply in replies] def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ self.log.failure("Authorization of 'subscribe' for '{uri}' failed", uri=subscribe.topic, failure=err) reply = message.Error( message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.AUTHORIZATION_FAILED, [u"failed to authorize session for subscribing to topic URI '{0}': {1}".format(subscribe.topic, err.value)] ) reply.correlation_id = subscribe.correlation_id reply.correlation_uri = subscribe.topic reply.correlation_is_anchor = False reply.correlation_is_last = True self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)
def attempt_connect(_): self._delay_f = None def handle_connect_error(fail): # FIXME - make txaio friendly # Can connect_f ever be in a cancelled state? # if txaio.using_asyncio and isinstance(fail.value, asyncio.CancelledError): # unrecoverable_error = True self.log.debug('component failed: {error}', error=txaio.failure_message(fail)) self.log.debug('{tb}', tb=txaio.failure_format_traceback(fail)) # If this is a "fatal error" that will never work, # we bail out now if isinstance(fail.value, ApplicationError): self.log.error("{msg}", msg=fail.value.error_message()) elif isinstance(fail.value, OSError): # failed to connect entirely, like nobody # listening etc. self.log.info("Connection failed: {msg}", msg=txaio.failure_message(fail)) elif self._is_ssl_error(fail.value): # Quoting pyOpenSSL docs: "Whenever # [SSL.Error] is raised directly, it has a # list of error messages from the OpenSSL # error queue, where each item is a tuple # (lib, function, reason). Here lib, function # and reason are all strings, describing where # and what the problem is. See err(3) for more # information." # (and 'args' is a 1-tuple containing the above # 3-tuple...) ssl_lib, ssl_func, ssl_reason = fail.value.args[0][0] self.log.error("TLS failure: {reason}", reason=ssl_reason) else: self.log.error( 'Connection failed: {error}', error=txaio.failure_message(fail), ) if self._is_fatal is None: is_fatal = False else: is_fatal = self._is_fatal(fail.value) if is_fatal: self.log.info("Error was fatal; failing transport") transport_candidate[0].failed() txaio.call_later(0, transport_check, None) return def notify_connect_error(fail): chain_f = txaio.create_future() # hmm, if connectfailure took a _Transport instead of # (or in addition to?) self it could .failed() the # transport and we could do away with the is_fatal # listener? handler_f = self.fire('connectfailure', self, fail.value) txaio.add_callbacks(handler_f, lambda _: txaio.reject(chain_f, fail), lambda _: txaio.reject(chain_f, fail)) return chain_f def connect_error(fail): notify_f = notify_connect_error(fail) txaio.add_callbacks(notify_f, None, handle_connect_error) def session_done(x): txaio.resolve(self._done_f, None) connect_f = txaio.as_future( self._connect_once, loop, transport_candidate[0], ) txaio.add_callbacks(connect_f, session_done, connect_error)
def cb(value): print("Callback:", value) return value # should always return input arg def eb(fail): # fail will implement txaio.IFailedPromise print("Errback:", fail) # fail.printTraceback() return fail # should always return input arg f0 = txaio.create_future() f1 = txaio.create_future() txaio.add_callbacks(f0, cb, eb) txaio.add_callbacks(f1, cb, eb) # ... txaio.reject(f0, RuntimeError("it failed")) # or can just "txaio.reject(f0)" if inside an except: block txaio.resolve(f1, "The answer is: 42") if txaio.using_asyncio: # for twisted, we don't need to enter the event-loop for this # simple example (since all results are already available), but # you'd simply use reactor.run()/.stop() or task.react() as normal import asyncio asyncio.get_event_loop().run_until_complete(f1)
def _start(self, loop=None): """ This starts the Component, which means it will start connecting (and re-connecting) to its configured transports. A Component runs until it is "done", which means one of: - There was a "main" function defined, and it completed successfully; - Something called ``.leave()`` on our session, and we left successfully; - ``.stop()`` was called, and completed successfully; - none of our transports were able to connect successfully (failure); :returns: a Future/Deferred which will resolve (to ``None``) when we are "done" or with an error if something went wrong. """ # we can only be "start()ed" once before we stop .. but that # doesn't have to be an error we can give back another future # that fires when our "real" _done_f is completed. if self._done_f is not None: d = txaio.create_future() def _cb(arg): txaio.resolve(d, arg) txaio.add_callbacks(self._done_f, _cb, _cb) return d # this future will be returned, and thus has the semantics # specified in the docstring. self._done_f = txaio.create_future() def _reset(arg): """ if the _done_f future is resolved (good or bad), we want to set it to None in our class """ self._done_f = None return arg txaio.add_callbacks(self._done_f, _reset, _reset) # Create a generator of transports that .can_reconnect() transport_gen = itertools.cycle(self._transports) # this is a 1-element list so we can set it from closures in # this function transport_candidate = [0] def error(fail): self._delay_f = None if self._stopping: # might be better to add framework-specific checks in # subclasses to see if this is CancelledError (for # Twisted) and whatever asyncio does .. but tracking # if we're in the shutdown path is fine too txaio.resolve(self._done_f, None) else: self.log.info("Internal error {msg}", msg=txaio.failure_message(fail)) self.log.debug("{tb}", tb=txaio.failure_format_traceback(fail)) txaio.reject(self._done_f, fail) def attempt_connect(_): self._delay_f = None def handle_connect_error(fail): # FIXME - make txaio friendly # Can connect_f ever be in a cancelled state? # if txaio.using_asyncio and isinstance(fail.value, asyncio.CancelledError): # unrecoverable_error = True self.log.debug('component failed: {error}', error=txaio.failure_message(fail)) self.log.debug('{tb}', tb=txaio.failure_format_traceback(fail)) # If this is a "fatal error" that will never work, # we bail out now if isinstance(fail.value, ApplicationError): self.log.error("{msg}", msg=fail.value.error_message()) elif isinstance(fail.value, OSError): # failed to connect entirely, like nobody # listening etc. self.log.info("Connection failed: {msg}", msg=txaio.failure_message(fail)) elif self._is_ssl_error(fail.value): # Quoting pyOpenSSL docs: "Whenever # [SSL.Error] is raised directly, it has a # list of error messages from the OpenSSL # error queue, where each item is a tuple # (lib, function, reason). Here lib, function # and reason are all strings, describing where # and what the problem is. See err(3) for more # information." # (and 'args' is a 1-tuple containing the above # 3-tuple...) ssl_lib, ssl_func, ssl_reason = fail.value.args[0][0] self.log.error("TLS failure: {reason}", reason=ssl_reason) else: self.log.error( 'Connection failed: {error}', error=txaio.failure_message(fail), ) if self._is_fatal is None: is_fatal = False else: is_fatal = self._is_fatal(fail.value) if is_fatal: self.log.info("Error was fatal; failing transport") transport_candidate[0].failed() txaio.call_later(0, transport_check, None) return def notify_connect_error(fail): chain_f = txaio.create_future() # hmm, if connectfailure took a _Transport instead of # (or in addition to?) self it could .failed() the # transport and we could do away with the is_fatal # listener? handler_f = self.fire('connectfailure', self, fail.value) txaio.add_callbacks(handler_f, lambda _: txaio.reject(chain_f, fail), lambda _: txaio.reject(chain_f, fail)) return chain_f def connect_error(fail): notify_f = notify_connect_error(fail) txaio.add_callbacks(notify_f, None, handle_connect_error) def session_done(x): txaio.resolve(self._done_f, None) connect_f = txaio.as_future( self._connect_once, loop, transport_candidate[0], ) txaio.add_callbacks(connect_f, session_done, connect_error) def transport_check(_): self.log.debug('Entering re-connect loop') if not self._can_reconnect(): err_msg = "Component failed: Exhausted all transport connect attempts" self.log.info(err_msg) try: raise RuntimeError(err_msg) except RuntimeError as e: txaio.reject(self._done_f, e) return while True: transport = next(transport_gen) if transport.can_reconnect(): transport_candidate[0] = transport break delay = transport.next_delay() self.log.debug( 'trying transport {transport_idx} using connect delay {transport_delay}', transport_idx=transport.idx, transport_delay=delay, ) self._delay_f = txaio.sleep(delay) txaio.add_callbacks(self._delay_f, attempt_connect, error) # issue our first event, then start the reconnect loop start_f = self.fire('start', loop, self) txaio.add_callbacks(start_f, transport_check, error) return self._done_f
def onMessage(self, msg): """ Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage` """ if self._session_id is None: if not self._pending_session_id: self._pending_session_id = util.id() def welcome(realm, authid=None, authrole=None, authmethod=None, authprovider=None, authextra=None, custom=None): self._realm = realm self._session_id = self._pending_session_id self._pending_session_id = None self._goodbye_sent = False self._router = self._router_factory.get(realm) if not self._router: # should not arrive here raise Exception( "logic error (no realm at a stage were we should have one)" ) self._authid = authid self._authrole = authrole self._authmethod = authmethod self._authprovider = authprovider self._authextra = authextra or {} self._authextra[ u'x_cb_node_id'] = self._router_factory._node_id self._authextra[u'x_cb_peer'] = str(self._transport.peer) self._authextra[u'x_cb_pid'] = os.getpid() roles = self._router.attach(self) msg = message.Welcome(self._session_id, roles, realm=realm, authid=authid, authrole=authrole, authmethod=authmethod, authprovider=authprovider, authextra=self._authextra, custom=custom) self._transport.send(msg) self.onJoin( SessionDetails(self._realm, self._session_id, self._authid, self._authrole, self._authmethod, self._authprovider, self._authextra)) # the first message MUST be HELLO if isinstance(msg, message.Hello): self._session_roles = msg.roles details = types.HelloDetails( realm=msg.realm, authmethods=msg.authmethods, authid=msg.authid, authrole=msg.authrole, authextra=msg.authextra, session_roles=msg.roles, pending_session=self._pending_session_id) d = txaio.as_future(self.onHello, msg.realm, details) def success(res): msg = None # it is possible this session has disconnected # while onHello was taking place if self._transport is None: self.log.info( "Client session disconnected during authentication", ) return if isinstance(res, types.Accept): custom = { u'x_cb_node_id': self._router_factory._node_id } welcome(res.realm, res.authid, res.authrole, res.authmethod, res.authprovider, res.authextra, custom) elif isinstance(res, types.Challenge): msg = message.Challenge(res.method, res.extra) elif isinstance(res, types.Deny): msg = message.Abort(res.reason, res.message) else: pass if msg: self._transport.send(msg) txaio.add_callbacks(d, success, self._swallow_error_and_abort) elif isinstance(msg, message.Authenticate): d = txaio.as_future(self.onAuthenticate, msg.signature, {}) def success(res): msg = None # it is possible this session has disconnected # while authentication was taking place if self._transport is None: self.log.info( "Client session disconnected during authentication", ) return if isinstance(res, types.Accept): custom = { u'x_cb_node_id': self._router_factory._node_id } welcome(res.realm, res.authid, res.authrole, res.authmethod, res.authprovider, res.authextra, custom) elif isinstance(res, types.Deny): msg = message.Abort(res.reason, res.message) else: pass if msg: self._transport.send(msg) txaio.add_callbacks(d, success, self._swallow_error_and_abort) elif isinstance(msg, message.Abort): # fire callback and close the transport self.onLeave(types.CloseDetails(msg.reason, msg.message)) self._session_id = None self._pending_session_id = None # self._transport.close() else: # raise ProtocolError(u"PReceived {0} message while session is not joined".format(msg.__class__)) # self.log.warn('Protocol state error - received {message} while session is not joined') # swallow all noise like still getting PUBLISH messages from log event forwarding - maybe FIXME pass else: if isinstance(msg, message.Hello): raise ProtocolError( u"HELLO message received, while session is already established" ) elif isinstance(msg, message.Goodbye): if not self._goodbye_sent: # The peer wants to close: answer with GOODBYE reply. # Note: We MUST NOT send any WAMP message _after_ GOODBYE reply = message.Goodbye() self._transport.send(reply) self._goodbye_sent = True else: # This is the peer's GOODBYE reply to our own earlier GOODBYE pass # We need to first detach the session from the router before # erasing the session ID below .. try: self._router.detach(self) except Exception: self.log.failure("Internal error") # In order to send wamp.session.on_leave properly # (i.e. *with* the proper session_id) we save it previous_session_id = self._session_id # At this point, we've either sent GOODBYE already earlier, # or we have just responded with GOODBYE. In any case, we MUST NOT # send any WAMP message from now on: # clear out session ID, so that anything that might be triggered # in the onLeave below is prohibited from sending WAMP stuff. # E.g. the client might have been subscribed to meta events like # wamp.session.on_leave - and we must not send that client's own # leave to itself! self._session_id = None self._pending_session_id = None # publish event, *after* self._session_id is None so # that we don't publish to ourselves as well (if this # session happens to be subscribed to wamp.session.on_leave) if self._service_session: self._service_session.publish( u'wamp.session.on_leave', previous_session_id, ) # fire callback and close the transport self.onLeave(types.CloseDetails(msg.reason, msg.message)) # don't close the transport, as WAMP allows to reattach a session # to the same or a different realm without closing the transport # self._transport.close() else: self._router.process(self, msg)
def processSubscribe(self, session, subscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processSubscribe` """ # check topic URI: for SUBSCRIBE, must be valid URI (either strict or loose), and all # URI components must be non-empty for normal subscriptions, may be empty for # wildcard subscriptions and must be non-empty for all but the last component for # prefix subscriptions # if self._option_uri_strict: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_STRICT_EMPTY.match(subscribe.topic) elif subscribe.match == u"prefix": uri_is_valid = _URI_PAT_STRICT_LAST_EMPTY.match( subscribe.topic) else: uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(subscribe.topic) else: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(subscribe.topic) elif subscribe.match == u"prefix": uri_is_valid = _URI_PAT_LOOSE_LAST_EMPTY.match(subscribe.topic) else: uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(subscribe.topic) if not uri_is_valid: reply = message.Error( message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.INVALID_URI, [ u"subscribe for invalid topic URI '{0}'".format( subscribe.topic) ]) self._router.send(session, reply) return # authorize SUBSCRIBE action # d = self._router.authorize(session, subscribe.topic, u'subscribe') def on_authorize_success(authorization): if not authorization[u'allow']: # error reply since session is not authorized to subscribe # replies = [ message.Error( message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.NOT_AUTHORIZED, [ u"session is not authorized to subscribe to topic '{0}'" .format(subscribe.topic) ]) ] else: # ok, session authorized to subscribe. now get the subscription # subscription, was_already_subscribed, is_first_subscriber = self._subscription_map.add_observer( session, subscribe.topic, subscribe.match, extra=SubscriptionExtra()) if not was_already_subscribed: self._session_to_subscriptions[session].add(subscription) # publish WAMP meta events # if self._router._realm: service_session = self._router._realm.session if service_session and not subscription.uri.startswith( u'wamp.'): if is_first_subscriber: subscription_details = { u'id': subscription.id, u'created': subscription.created, u'uri': subscription.uri, u'match': subscription.match, } service_session.publish( u'wamp.subscription.on_create', session._session_id, subscription_details) if not was_already_subscribed: service_session.publish( u'wamp.subscription.on_subscribe', session._session_id, subscription.id) # check for retained events # def _get_retained_event(): if subscription.extra.retained_events: retained_events = list( subscription.extra.retained_events) retained_events.reverse() for publish in retained_events: authorised = False if not publish.exclude and not publish.eligible: authorised = True elif session._session_id in publish.eligible and session._session_id not in publish.exclude: authorised = True if authorised: publication = util.id() if publish.payload: msg = message.Event( subscription.id, publication, payload=publish.payload, retained=True, enc_algo=publish.enc_algo, enc_key=publish.enc_key, enc_serializer=publish.enc_serializer) else: msg = message.Event(subscription.id, publication, args=publish.args, kwargs=publish.kwargs, retained=True) return [msg] return [] # acknowledge subscribe with subscription ID # replies = [ message.Subscribed(subscribe.request, subscription.id) ] if subscribe.get_retained: replies.extend(_get_retained_event()) # send out reply to subscribe requestor # [self._router.send(session, reply) for reply in replies] def on_authorize_error(err): """ the call to authorize the action _itself_ failed (note this is different from the call to authorize succeed, but the authorization being denied) """ # XXX same as another code-block, can we collapse? self.log.failure("Authorization failed", failure=err) reply = message.Error( message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.AUTHORIZATION_FAILED, [ u"failed to authorize session for subscribing to topic URI '{0}': {1}" .format(subscribe.topic, err.value) ]) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error)