def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures( caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True)
def test_match_observations_match_prefix(self): """ When a observer observes an URI (match prefix), the observer is returned for all uris upon lookup where the observed URI is a prefix. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, u"com.example", match=Subscribe.MATCH_PREFIX) # test matches for uri in [u"com.example.uri1.foobar.barbaz", u"com.example.uri1.foobar", u"com.example.uri1", u"com.example.topi", u"com.example.", u"com.example2", u"com.example"]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1])) # test non-matches for uri in [u"com.foobar.uri1", u"com.exampl.uri1", u"com.exampl", u"com", u""]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [])
def test_match_observations_match_wildcard_multi(self): """ Test with multiple wildcards in wildcard-matching observation. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, u"com...create", match=Subscribe.MATCH_WILDCARD) # test matches for uri in [u"com.example.foobar.create", u"com.example.1.create", u"com.myapp.foobar.create", u"com.myapp.1.create", ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1])) # test non-matches for uri in [u"com.example.foobar.delete", u"com.example.foobar.create2", u"com.example.foobar.create.barbaz" u"com.example.foobar", u"org.example.foobar.create", u"org.example.1.create", u"org.myapp.foobar.create", u"org.myapp.1.create", ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [])
def test_match_observations_match_wildcard_single(self): """ When a observer observes to a uri (wildcard prefix), the observer is returned for all uris upon lookup where the observed uri matches the wildcard pattern. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, u"com.example..create", match=Subscribe.MATCH_WILDCARD) # test matches for uri in [u"com.example.foobar.create", u"com.example.1.create" ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1])) # test non-matches for uri in [u"com.example.foobar.delete", u"com.example.foobar.create2", u"com.example.foobar.create.barbaz" u"com.example.foobar", u"com.example.create", u"com.example" ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [])
def test_match_observations_match_prefix(self): """ When a observer observes an URI (match prefix), the observer is returned for all uris upon lookup where the observed URI is a prefix. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, u"com.example", match=Subscribe.MATCH_PREFIX) # test matches for uri in [ u"com.example.uri1.foobar.barbaz", u"com.example.uri1.foobar", u"com.example.uri1", u"com.example.topi", u"com.example.", u"com.example2", u"com.example" ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1])) # test non-matches for uri in [ u"com.foobar.uri1", u"com.exampl.uri1", u"com.exampl", u"com", u"" ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [])
def test_match_observations_match_wildcard_single(self): """ When a observer observes to a uri (wildcard prefix), the observer is returned for all uris upon lookup where the observed uri matches the wildcard pattern. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer( obs1, u"com.example..create", match=Subscribe.MATCH_WILDCARD) # test matches for uri in [u"com.example.foobar.create", u"com.example.1.create"]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1])) # test non-matches for uri in [ u"com.example.foobar.delete", u"com.example.foobar.create2", u"com.example.foobar.create.barbaz" u"com.example.foobar", u"com.example.create", u"com.example" ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [])
def test_match_observations_match_wildcard_multi(self): """ Test with multiple wildcards in wildcard-matching observation. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer( obs1, u"com...create", match=Subscribe.MATCH_WILDCARD) # test matches for uri in [ u"com.example.foobar.create", u"com.example.1.create", u"com.myapp.foobar.create", u"com.myapp.1.create", ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1])) # test non-matches for uri in [ u"com.example.foobar.delete", u"com.example.foobar.create2", u"com.example.foobar.create.barbaz" u"com.example.foobar", u"org.example.foobar.create", u"org.example.1.create", u"org.myapp.foobar.create", u"org.myapp.1.create", ]: observations = obs_map.match_observations(uri) self.assertEqual(observations, [])
def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True)
def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # map: session -> in-flight invocations self._callee_to_invocations = {} # BEWARE: this map must be kept up-to-date along with the # _invocations map below! Use the helper methods # _add_invoke_request and _remove_invoke_request # map: session -> in-flight invocations self._caller_to_invocations = {} # careful here: the 'request' IDs are unique per-session # (only) so we map from (session_id, call) tuples to in-flight invocations # map: (session_id, call) -> in-flight invocations self._invocations_by_call = {} # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures(caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True, payload_transparency=True, testament_meta_api=True, payload_encryption_cryptobox=True, call_canceling=True) # store for call queues if self._router._store: self._call_store = self._router._store.call_store else: self._call_store = None
def attach_subscription_map(self, subscription_map: UriObservationMap): """ Implements :meth:`crossbar._interfaces.IRealmStore.attach_subscription_map` """ for sub in self._config.get('event-history', []): uri = sub['uri'] match = sub.get('match', u'exact') # observation, was_already_observed, was_first_observer subscription_map.add_observer(self, uri=uri, match=match)
def test_match_observations_empty(self): """ An empty observer map returns an empty observer set for any URI. """ obs_map = UriObservationMap() for uri in [u"com.example.uri1", u"com.example.uri2", u""]: obsvs = obs_map.match_observations(uri) self.assertEqual(obsvs, [])
def test_add_observer(self): """ When a observer is added, a observation is returned. """ obs_map = UriObservationMap() uri1 = u"com.example.uri1" obs1 = FakeObserver() observation, was_already_observed, is_first_observer = obs_map.add_observer(obs1, uri1) self.assertIsInstance(observation, ExactUriObservation) self.assertFalse(was_already_observed) self.assertTrue(is_first_observer)
def test_match_observations_match_multimode(self): """ When a observer is observed to multiple observations each matching a given uri looked up, the observer is returned in each observation. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer( obs1, u"com.example.product.create", match=Subscribe.MATCH_EXACT) observation2, _, _ = obs_map.add_observer(obs1, u"com.example.product", match=Subscribe.MATCH_PREFIX) observation3, _, _ = obs_map.add_observer( obs1, u"com.example..create", match=Subscribe.MATCH_WILDCARD) observations = obs_map.match_observations( u"com.example.product.create") self.assertEqual(observations, [observation1, observation2, observation3]) self.assertEqual(observations[0].observers, set([obs1])) self.assertEqual(observations[1].observers, set([obs1])) self.assertEqual(observations[2].observers, set([obs1])) observations = obs_map.match_observations(u"com.example.foobar.create") self.assertEqual(observations, [observation3]) self.assertEqual(observations[0].observers, set([obs1])) observations = obs_map.match_observations( u"com.example.product.delete") self.assertEqual(observations, [observation2]) self.assertEqual(observations[0].observers, set([obs1]))
def test_match_observations_match_exact(self): """ When a observer observes an URI (match exact), the observer is returned for the URI upon lookup. """ obs_map = UriObservationMap() uri1 = u"com.example.uri1" obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, uri1) observations = obs_map.match_observations(uri1) self.assertEqual(observations, [observation1])
def __init__(self, router, reactor, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._reactor = reactor self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures( publisher_identification=True, pattern_based_subscription=True, session_meta_api=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True, event_retention=True, payload_transparency=True, payload_encryption_cryptobox=True) # store for event history if self._router._store: self._event_store = self._router._store.event_store else: self._event_store = None # if there is a store, let the store attach itself to all the subscriptions # it is configured to track if self._event_store: self._event_store.attach_subscription_map(self._subscription_map)
def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures(caller_identification=True, pattern_based_registration=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True)
def test_add_observer_is_first_observer(self): """ When a observer is added, the ``is_first_observer`` flag in the return is correct. """ obs_map = UriObservationMap() uri1 = u"com.example.uri1" obs1 = FakeObserver() obs2 = FakeObserver() _, _, is_first_observer = obs_map.add_observer(obs1, uri1) self.assertTrue(is_first_observer) _, _, is_first_observer = obs_map.add_observer(obs2, uri1) self.assertFalse(is_first_observer)
def test_add_observer_was_already_observed(self): """ When a observer is added, the ``was_already_observed`` flag in the return is correct. """ obs_map = UriObservationMap() uri1 = u"com.example.uri1" obs1 = FakeObserver() observation1, was_already_observed, _ = obs_map.add_observer(obs1, uri1) self.assertFalse(was_already_observed) observation2, was_already_observed, _ = obs_map.add_observer(obs1, uri1) self.assertTrue(was_already_observed) self.assertEqual(observation1, observation2)
def test_match_observations_match_multimode(self): """ When a observer is observed to multiple observations each matching a given uri looked up, the observer is returned in each observation. """ obs_map = UriObservationMap() obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, u"com.example.product.create", match=Subscribe.MATCH_EXACT) observation2, _, _ = obs_map.add_observer(obs1, u"com.example.product", match=Subscribe.MATCH_PREFIX) observation3, _, _ = obs_map.add_observer(obs1, u"com.example..create", match=Subscribe.MATCH_WILDCARD) observations = obs_map.match_observations(u"com.example.product.create") self.assertEqual(observations, [observation1, observation2, observation3]) self.assertEqual(observations[0].observers, set([obs1])) self.assertEqual(observations[1].observers, set([obs1])) self.assertEqual(observations[2].observers, set([obs1])) observations = obs_map.match_observations(u"com.example.foobar.create") self.assertEqual(observations, [observation3]) self.assertEqual(observations[0].observers, set([obs1])) observations = obs_map.match_observations(u"com.example.product.delete") self.assertEqual(observations, [observation2]) self.assertEqual(observations[0].observers, set([obs1]))
def test_match_observations_match_exact_same(self): """ When multiple different observers observe the same URI (match exact), all get the same observation. """ obs_map = UriObservationMap() uri1 = u"com.example.uri1" obs1 = FakeObserver() obs2 = FakeObserver() obs3 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, uri1) observation2, _, _ = obs_map.add_observer(obs2, uri1) observation3, _, _ = obs_map.add_observer(obs3, uri1) observations = obs_map.match_observations(uri1) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1, obs2, obs3]))
def test_delete_observer(self): obs_map = UriObservationMap() uri = u"com.example.uri1" obs1 = FakeObserver() obs2 = FakeObserver() ob1, uri1, _ = obs_map.add_observer(obs1, uri) ob2, uri2, _ = obs_map.add_observer(obs2, uri) self.assertTrue(ob1 is ob2) obs_map.drop_observer(obs1, ob1) # error if we delete because there's still one observer with self.assertRaises(ValueError): obs_map.delete_observation(ob2) # drop last observer and delete obs_map.drop_observer(obs2, ob1) obs_map.delete_observation(ob2)
def test_match_observations_match_exact_multi(self): """ When the same observer is added multiple times to the same URI (match exact), the observation is only returned once, and every time the same observation ID is returned. """ obs_map = UriObservationMap() uri1 = u"com.example.uri1" obs1 = FakeObserver() observation1, _, _ = obs_map.add_observer(obs1, uri1) observation2, _, _ = obs_map.add_observer(obs1, uri1) observation3, _, _ = obs_map.add_observer(obs1, uri1) self.assertEqual(observation1, observation2) self.assertEqual(observation1, observation3) observations = obs_map.match_observations(uri1) self.assertEqual(observations, [observation1]) self.assertEqual(observations[0].observers, set([obs1]))
def attach_subscription_map(self, subscription_map: UriObservationMap): """ Implements :meth:`crossbar._interfaces.IRealmStore.attach_subscription_map` """ for sub in self._config.get('event-history', []): uri = sub['uri'] match = sub.get('match', 'exact') observation, was_already_observed, was_first_observer = subscription_map.add_observer(self, uri=uri, match=match) subscription_id = observation.id # for in-memory history, we just use a double-ended queue self._event_history[subscription_id] = (sub.get('limit', self._limit), deque())
def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # map: session -> in-flight invocations self._callee_to_invocations = {} # BEWARE: this map must be kept up-to-date along with the # _invocations map below! Use the helper methods # _add_invoke_request and _remove_invoke_request # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures(caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True, payload_transparency=True, testament_meta_api=True, payload_encryption_cryptobox=True) # store for call queues if self._router._store: self._call_store = self._router._store.call_store else: self._call_store = None
def __init__(self, router, reactor, options=None): """ :param router: The router this broker is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._reactor = reactor self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, session_meta_api=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True, event_retention=True, payload_transparency=True, payload_encryption_cryptobox=True) # store for event history if self._router._store: self._event_store = self._router._store.event_store else: self._event_store = None # if there is a store, let the store attach itself to all the subscriptions # it is configured to track if self._event_store: self._event_store.attach_subscription_map(self._subscription_map)
class Broker(FutureMixin): """ Basic WAMP broker. """ def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.attach` """ if session not in self._session_to_subscriptions: self._session_to_subscriptions[session] = set() else: raise Exception("session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.detach` """ if session in self._session_to_subscriptions: for subscription in self._session_to_subscriptions[session]: was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, 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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_last_subscriber: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) del self._session_to_subscriptions[session] else: raise Exception("session with ID {} not attached".format(session._session_id)) 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 = self._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) self._add_future_callbacks(d, on_authorize_success, on_authorize_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 other than for wildcard subscriptions # if self._option_uri_strict: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_STRICT_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) 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, ["subscribe for invalid topic URI '{0}'".format(subscribe.topic)]) session._transport.send(reply) return # authorize action # d = self._as_future(self._router.authorize, session, subscribe.topic, IRouter.ACTION_SUBSCRIBE) def on_authorize_success(authorized): if not authorized: # error reply since session is not authorized to subscribe # reply = message.Error(message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.NOT_AUTHORIZED, ["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 = { 'id': subscription.id, 'created': subscription.created, 'uri': subscription.uri, '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 # 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) # reply = message.Error(message.Subscribe.MESSAGE_TYPE, subscribe.request, ApplicationError.AUTHORIZATION_FAILED, ["failed to authorize session for subscribing to topic URI '{0}': {1}".format(subscribe.topic, err.value)]) session._transport.send(reply) self._add_future_callbacks(d, on_authorize_success, on_authorize_error) def processUnsubscribe(self, session, unsubscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processUnsubscribe` """ # get subscription by subscription ID or None (if it doesn't exist on this broker) # subscription = self._subscription_map.get_observation_by_id(unsubscribe.subscription) if subscription: if session in subscription.observers: was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) reply = message.Unsubscribed(unsubscribe.request) else: # subscription exists on this broker, but the session that wanted to unsubscribe wasn't subscribed # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) else: # subscription doesn't even exist on this broker # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) session._transport.send(reply) def _unsubscribe(self, subscription, session): # drop session from subscription observers # was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) # remove subscription from session->subscriptions map # if was_subscribed: self._session_to_subscriptions[session].discard(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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_last_subscriber: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) return was_subscribed, was_last_subscriber def removeSubscriber(self, subscription, session, reason=None): """ Actively unsubscribe a subscriber session from a subscription. """ was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) if 'subscriber' in session._session_roles and session._session_roles['subscriber'] and session._session_roles['subscriber'].subscription_revocation: reply = message.Unsubscribed(0, subscription=subscription.id, reason=reason) session._transport.send(reply) return was_subscribed, was_last_subscriber
class Dealer(object): """ Basic WAMP dealer. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # map: session -> in-flight invocations self._callee_to_invocations = {} # BEWARE: this map must be kept up-to-date along with the # _invocations map below! Use the helper methods # _add_invoke_request and _remove_invoke_request # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures( caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True, payload_transparency=True, testament_meta_api=True, payload_encryption_cryptobox=True) # store for call queues if self._router._store: self._call_store = self._router._store.call_store else: self._call_store = None def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.attach` """ if session not in self._session_to_registrations: self._session_to_registrations[session] = set() else: raise Exception("session with ID {} already attached".format( session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.detach` """ if session in self._session_to_registrations: # send out Errors for any in-flight calls we have outstanding = self._callee_to_invocations.get(session, []) for invoke in outstanding: self.log.debug( "Cancelling in-flight INVOKE with id={request} on" " session {session}", request=invoke.call.request, session=session._session_id, ) reply = message.Error( message.Call.MESSAGE_TYPE, invoke.call.request, ApplicationError.CANCELED, [u"callee disconnected from in-flight request"], ) # send this directly to the caller's session # (it is possible the caller was disconnected and thus # _transport is None before we get here though) if invoke.caller._transport: invoke.caller._transport.send(reply) for registration in self._session_to_registrations[session]: was_registered, was_last_callee = self._registration_map.drop_observer( session, registration) if was_registered and was_last_callee: self._registration_map.delete_observation(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 was_registered: service_session.publish( u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish( u'wamp.registration.on_delete', session._session_id, registration.id) del self._session_to_registrations[session] else: raise Exception(u"session with ID {} not attached".format( session._session_id)) 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) elif register.match == u"prefix": uri_is_valid = _URI_PAT_STRICT_LAST_EMPTY.match( register.procedure) elif register.match == u"exact": uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match( register.procedure) else: # should not arrive here raise Exception("logic error") else: if register.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(register.procedure) elif register.match == u"prefix": uri_is_valid = _URI_PAT_LOOSE_LAST_EMPTY.match( register.procedure) elif register.match == u"exact": uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match( register.procedure) else: # should not arrive here raise Exception("logic error") 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) ]) self._router.send(session, reply) return # disallow registration of procedures 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 = register.procedure.startswith(u"wamp.") 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) ]) self._router.send(session, 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) ]) self._router.send(session, 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) ]) self._router.send(session, reply) return # authorize REGISTER action # d = self._router.authorize(session, register.procedure, u'register', options=register.marshal_options()) def on_authorize_success(authorization): if not authorization[u'allow']: # 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_callee_extra = RegistrationCalleeExtra( register.concurrency) registration, was_already_registered, is_first_callee = self._registration_map.add_observer( session, register.procedure, register.match, registration_extra, registration_callee_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 = { u'id': registration.id, u'created': registration.created, u'uri': registration.uri, u'match': registration.match, u'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 # 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) """ self.log.failure("Authorization of 'register' for '{uri}' failed", uri=register.procedure, 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) ]) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error) def processUnregister(self, session, unregister): """ Implements :func:`crossbar.router.interfaces.IDealer.processUnregister` """ # get registration by registration ID or None (if it doesn't exist on this broker) # registration = self._registration_map.get_observation_by_id( unregister.registration) if registration: if session in registration.observers: was_registered, was_last_callee = self._unregister( registration, session) reply = message.Unregistered(unregister.request) else: # registration exists on this dealer, but the session that wanted to unregister wasn't registered # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) else: # registration doesn't even exist on this broker # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) self._router.send(session, reply) def _unregister(self, registration, session): # drop session from registration observers # was_registered, was_last_callee = self._registration_map.drop_observer( session, registration) if was_registered and was_last_callee: self._registration_map.delete_observation(registration) # remove registration from session->registrations map # if was_registered: self._session_to_registrations[session].discard(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 was_registered: service_session.publish(u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish(u'wamp.registration.on_delete', session._session_id, registration.id) return was_registered, was_last_callee def removeCallee(self, registration, session, reason=None): """ Actively unregister a callee session from a registration. """ was_registered, was_last_callee = self._unregister( registration, session) # actively inform the callee that it has been unregistered # if 'callee' in session._session_roles and session._session_roles[ 'callee'] and session._session_roles[ 'callee'].registration_revocation: reply = message.Unregistered(0, registration=registration.id, reason=reason) self._router.send(session, reply) return was_registered, was_last_callee 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) ]) self._router.send(session, reply) return # get registrations active on the procedure called # registration = self._registration_map.best_matching_observation( call.procedure) if registration: # validate payload (skip in "payload_transparency" mode) # if call.payload is None: try: self._router.validate(u'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) ]) self._router.send(session, reply) return # authorize CALL action # d = self._router.authorize(session, call.procedure, u'call', options=call.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']: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.NOT_AUTHORIZED, [ u"session is not authorized to call procedure '{0}'" .format(call.procedure) ]) self._router.send(session, reply) else: self._call(session, call, registration, authorization) 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 'call' for '{uri}' failed", uri=call.procedure, 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) ]) self._router.send(session, 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) ]) self._router.send(session, reply) def _call(self, session, call, registration, authorization, is_queued_call=False): # will hold the callee (the concrete endpoint) that we will forward the call to .. # callee = None callee_extra = None # determine callee according to invocation policy # if registration.extra.invoke in [ message.Register.INVOKE_SINGLE, message.Register.INVOKE_FIRST, message.Register.INVOKE_LAST ]: # a single endpoint is considered for forwarding the call .. 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] else: # should not arrive here raise Exception(u"logic error") # check maximum concurrency of the (single) endpoint callee_extra = registration.observers_extra.get(callee, None) if callee_extra: if callee_extra.concurrency and callee_extra.concurrency_current >= callee_extra.concurrency: if is_queued_call or ( self._call_store and self._call_store.maybe_queue_call( session, call, registration, authorization)): return False else: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, u'crossbar.error.max_concurrency_reached', [ u'maximum concurrency {} of callee/endpoint reached (on non-shared/single registration)' .format(callee_extra.concurrency) ]) self._router.send(session, reply) return False else: callee_extra.concurrency_current += 1 elif registration.extra.invoke == message.Register.INVOKE_ROUNDROBIN: # remember where we started to search for a suitable callee/endpoint in the round-robin list of callee endpoints roundrobin_start_index = registration.extra.roundrobin_current % len( registration.observers) # now search fo a suitable callee/endpoint while True: callee = registration.observers[ registration.extra.roundrobin_current % len(registration.observers)] callee_extra = registration.observers_extra.get(callee, None) registration.extra.roundrobin_current += 1 if callee_extra and callee_extra.concurrency: if callee_extra.concurrency_current >= callee_extra.concurrency: # this callee has set a maximum concurrency that has already been reached. # we need to search further .. but only if we haven't reached the beginning # of our round-robin list if registration.extra.roundrobin_current % len( registration.observers ) == roundrobin_start_index: # we've looked through the whole round-robin list, and didn't find a suitable # callee (one that hasn't it's maximum concurrency already reached). if is_queued_call or ( self._call_store and self._call_store.maybe_queue_call( session, call, registration, authorization)): return False else: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, u'crossbar.error.max_concurrency_reached', [ u'maximum concurrency of all callee/endpoints reached (on round-robin registration)' .format(callee_extra.concurrency) ]) self._router.send(session, reply) return False else: # .. search on .. pass else: # ok, we've found a callee that has set a maximum concurrency, but where the # maximum has not yet been reached break else: # ok, we've found a callee which hasn't set a maximum concurrency, and hence is always # eligible for having a call forwarded to break if callee_extra: callee_extra.concurrency_current += 1 elif registration.extra.invoke == message.Register.INVOKE_RANDOM: # FIXME: implement max. concurrency and call queueing 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 authorization[u'disclose']: disclose = True elif (call.procedure.startswith(u"wamp.") or call.procedure.startswith(u"crossbar.")): disclose = True else: disclose = False if disclose: caller = session._session_id caller_authid = session._authid caller_authrole = session._authrole else: caller = None caller_authid = None caller_authrole = 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 if call.payload: invocation = message.Invocation( invocation_request_id, registration.id, payload=call.payload, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, caller_authid=caller_authid, caller_authrole=caller_authrole, procedure=procedure, enc_algo=call.enc_algo, enc_key=call.enc_key, enc_serializer=call.enc_serializer) else: invocation = message.Invocation( invocation_request_id, registration.id, args=call.args, kwargs=call.kwargs, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, caller_authid=caller_authid, caller_authrole=caller_authrole, procedure=procedure) self._add_invoke_request(invocation_request_id, registration, session, call, callee) self._router.send(callee, invocation) return True def _add_invoke_request(self, invocation_request_id, registration, session, call, callee): """ Internal helper. Adds an InvocationRequest to both the _callee_to_invocations and _invocations maps. """ invoke_request = InvocationRequest(invocation_request_id, registration, session, call, callee) self._invocations[invocation_request_id] = invoke_request invokes = self._callee_to_invocations.get(callee, []) invokes.append(invoke_request) self._callee_to_invocations[callee] = invokes return invoke_request def _remove_invoke_request(self, invocation_request): """ Internal helper. Removes an InvocationRequest from both the _callee_to_invocations and _invocations maps. """ invokes = self._callee_to_invocations[invocation_request.callee] invokes.remove(invocation_request) if not invokes: del self._callee_to_invocations[invocation_request.callee] del self._invocations[invocation_request.id] # noinspection PyUnusedLocal def processCancel(self, session, cancel): """ Implements :func:`crossbar.router.interfaces.IDealer.processCancel` """ assert (session in self._session_to_registrations) raise Exception("not implemented") def processYield(self, session, yield_): """ Implements :func:`crossbar.router.interfaces.IDealer.processYield` """ # assert(session in self._session_to_registrations) if yield_.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[yield_.request] is_valid = True if yield_.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_result', invocation_request.call.procedure, yield_.args, yield_.kwargs) except Exception as e: is_valid = False reply = message.Error( message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [ u"call result from procedure '{0}' with invalid application payload: {1}" .format(invocation_request.call.procedure, e) ]) else: reply = message.Result(invocation_request.call.request, args=yield_.args, kwargs=yield_.kwargs, progress=yield_.progress) else: reply = message.Result(invocation_request.call.request, payload=yield_.payload, progress=yield_.progress, enc_algo=yield_.enc_algo, enc_key=yield_.enc_key, enc_serializer=yield_.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done if it's a regular call (non-progressive) or if the payload was invalid # if not yield_.progress or not is_valid: callee_extra = invocation_request.registration.observers_extra.get( session, None) if callee_extra: callee_extra.concurrency_current -= 1 self._remove_invoke_request(invocation_request) # check for any calls queued on the registration for which an # invocation just returned, and hence there is likely concurrency # free again to actually forward calls previously queued calls # that were queued because no callee endpoint concurrency was free if self._call_store: queued_call = self._call_store.get_queued_call( invocation_request.registration) if queued_call: invocation_sent = self._call(queued_call.session, queued_call.call, queued_call.registration, queued_call.authorization, True) # only actually pop the queued call when we really were # able to forward the call now if invocation_sent: self._call_store.pop_queued_call( invocation_request.registration) else: raise ProtocolError( u"Dealer.onYield(): YIELD received for non-pending request ID {0}" .format(yield_.request)) def processInvocationError(self, session, error): """ Implements :func:`crossbar.router.interfaces.IDealer.processInvocationError` """ # assert(session in self._session_to_registrations) if error.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[error.request] if error.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_error', invocation_request.call.procedure, error.args, error.kwargs) except Exception as e: reply = message.Error( message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [ u"call error from procedure '{0}' with invalid application payload: {1}" .format(invocation_request.call.procedure, e) ]) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, args=error.args, kwargs=error.kwargs) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, payload=error.payload, enc_algo=error.enc_algo, enc_key=error.enc_key, enc_serializer=error.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done # invoke = self._invocations[error.request] self._remove_invoke_request(invoke) else: raise ProtocolError( u"Dealer.onInvocationError(): ERROR received for non-pending request_type {0} and request ID {1}" .format(error.request_type, error.request))
class Broker(object): """ Basic WAMP broker. """ log = make_logger() def __init__(self, router, reactor, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._reactor = reactor self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures( publisher_identification=True, pattern_based_subscription=True, session_meta_api=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True, event_retention=True, payload_transparency=True, payload_encryption_cryptobox=True) # store for event history if self._router._store: self._event_store = self._router._store.event_store else: self._event_store = None # if there is a store, let the store attach itself to all the subscriptions # it is configured to track if self._event_store: self._event_store.attach_subscription_map(self._subscription_map) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.attach` """ if session not in self._session_to_subscriptions: self._session_to_subscriptions[session] = set() else: raise Exception(u"session with ID {} already attached".format( session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.detach` """ if session in self._session_to_subscriptions: for subscription in self._session_to_subscriptions[session]: was_subscribed, was_last_subscriber = self._subscription_map.drop_observer( session, subscription) was_deleted = False # delete it if there are no subscribers and no retained events # if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: was_deleted = True self._subscription_map.delete_observation(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.'): def _publish(): service_session = self._router._realm.session options = types.PublishOptions(correlation_id=None) if was_subscribed: service_session.publish( u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id, options=options, ) if was_deleted: service_session.publish( u'wamp.subscription.on_delete', 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) del self._session_to_subscriptions[session] else: raise Exception("session with ID {} not attached".format( session._session_id)) def _filter_publish_receivers(self, receivers, publish): """ Internal helper. Does all filtering on a candidate set of Publish receivers, based on all the white/blacklist options in 'publish'. """ # filter by "eligible" receivers # if publish.eligible: # map eligible session IDs to eligible sessions eligible = set() for session_id in publish.eligible: if session_id in self._router._session_id_to_session: eligible.add( self._router._session_id_to_session[session_id]) # filter receivers for eligible sessions receivers = eligible & receivers # if "eligible_authid" we only accept receivers that have the correct authid if publish.eligible_authid: eligible = set() for aid in publish.eligible_authid: eligible.update( self._router._authid_to_sessions.get(aid, set())) receivers = receivers & eligible # if "eligible_authrole" we only accept receivers that have the correct authrole if publish.eligible_authrole: eligible = set() for ar in publish.eligible_authrole: eligible.update( self._router._authrole_to_sessions.get(ar, set())) receivers = receivers & eligible # remove "excluded" receivers # if publish.exclude: # map excluded session IDs to excluded sessions exclude = set() for s in publish.exclude: if s in self._router._session_id_to_session: exclude.add(self._router._session_id_to_session[s]) # filter receivers for excluded sessions if exclude: receivers = receivers - exclude # remove auth-id based receivers if publish.exclude_authid: for aid in publish.exclude_authid: receivers = receivers - self._router._authid_to_sessions.get( aid, set()) # remove authrole based receivers if publish.exclude_authrole: for ar in publish.exclude_authrole: receivers = receivers - self._router._authrole_to_sessions.get( ar, set()) return receivers 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: 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) 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) ]) 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) 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 and publish.correlation_is_last is None: publish.correlation_is_last = True 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: 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) 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) ]) 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: # 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._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 ] 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: # 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: 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 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( ): self.log.debug( 'dispatching for subscription={subscription}', subscription=subscription) # 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) # 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 (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: 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: self._router.send(receiver, msg) else: # last chunk, so last receiver gets the different message for receiver in receivers_this_chunk[:-1]: self._router.send(receiver, msg) # we might have zero valid receivers if receivers_this_chunk: self._router.send( receivers_this_chunk[-1], last_msg) 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(list(receivers)) 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) ]) reply.correlation_id = publish.correlation_id reply.correlation_uri = publish.topic reply.correlation_is_anchor = False self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_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 # 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 processUnsubscribe(self, session, unsubscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processUnsubscribe` """ if self._router.is_traced: if not unsubscribe.correlation_id: unsubscribe.correlation_id = self._router.new_correlation_id() unsubscribe.correlation_is_anchor = True unsubscribe.correlation_is_last = False # get subscription by subscription ID or None (if it doesn't exist on this broker) # subscription = self._subscription_map.get_observation_by_id( unsubscribe.subscription) if subscription: if self._router.is_traced and not unsubscribe.correlation_uri: unsubscribe.correlation_uri = subscription.uri if session in subscription.observers: was_subscribed, was_last_subscriber, has_follow_up_messages = self._unsubscribe( subscription, session, unsubscribe) reply = message.Unsubscribed(unsubscribe.request) if self._router.is_traced: reply.correlation_uri = subscription.uri reply.correlation_is_last = not has_follow_up_messages else: # subscription exists on this broker, but the session that wanted to unsubscribe wasn't subscribed # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) if self._router.is_traced: reply.correlation_uri = reply.error reply.correlation_is_last = True else: # subscription doesn't even exist on this broker # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) if self._router.is_traced: reply.correlation_uri = reply.error reply.correlation_is_last = True if self._router.is_traced: reply.correlation_id = unsubscribe.correlation_id reply.correlation_is_anchor = False self._router.send(session, reply) def _unsubscribe(self, subscription, session, unsubscribe=None): # drop session from subscription observers # was_subscribed, was_last_subscriber = self._subscription_map.drop_observer( session, subscription) was_deleted = False if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: self._subscription_map.delete_observation(subscription) was_deleted = True # remove subscription from session->subscriptions map # if was_subscribed: self._session_to_subscriptions[session].discard(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 \ (was_subscribed or was_deleted): has_follow_up_messages = True def _publish(): service_session = self._router._realm.session if unsubscribe and self._router.is_traced: options = types.PublishOptions( correlation_id=unsubscribe.correlation_id, correlation_is_anchor=False, correlation_is_last=False) else: options = None if was_subscribed: service_session.publish( u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id, options=options, ) if was_deleted: if options: options.correlation_is_last = True service_session.publish( u'wamp.subscription.on_delete', 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 return was_subscribed, was_last_subscriber, has_follow_up_messages def removeSubscriber(self, subscription, session, reason=None): """ Actively unsubscribe a subscriber session from a subscription. """ was_subscribed, was_last_subscriber, _ = self._unsubscribe( subscription, session) if 'subscriber' in session._session_roles and session._session_roles[ 'subscriber'] and session._session_roles[ 'subscriber'].subscription_revocation: reply = message.Unsubscribed(0, subscription=subscription.id, reason=reason) reply.correlation_uri = subscription.uri self._router.send(session, reply) return was_subscribed, was_last_subscriber
class Broker(object): """ Basic WAMP broker. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, session_meta_api=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True, event_retention=True, payload_transparency=True, payload_encryption_cryptobox=True) # store for event history if self._router._store: self._event_store = self._router._store.event_store else: self._event_store = None # if there is a store, let the store attach itself to all the subscriptions # it is configured to track if self._event_store: self._event_store.attach_subscription_map(self._subscription_map) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.attach` """ if session not in self._session_to_subscriptions: self._session_to_subscriptions[session] = set() else: raise Exception(u"session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.detach` """ if session in self._session_to_subscriptions: for subscription in self._session_to_subscriptions[session]: was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) was_deleted = False # delete it if there are no subscribers and no retained events # if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: was_deleted = True self._subscription_map.delete_observation(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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_deleted: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) del self._session_to_subscriptions[session] else: raise Exception("session with ID {} not attached".format(session._session_id)) 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." 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 = publish.topic.startswith(u"wamp.") or publish.topic.startswith(u"crossbar.") 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 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) def processUnsubscribe(self, session, unsubscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processUnsubscribe` """ # get subscription by subscription ID or None (if it doesn't exist on this broker) # subscription = self._subscription_map.get_observation_by_id(unsubscribe.subscription) if subscription: if session in subscription.observers: was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) reply = message.Unsubscribed(unsubscribe.request) else: # subscription exists on this broker, but the session that wanted to unsubscribe wasn't subscribed # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) else: # subscription doesn't even exist on this broker # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) self._router.send(session, reply) def _unsubscribe(self, subscription, session): # drop session from subscription observers # was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) was_deleted = False if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: was_deleted = True self._subscription_map.delete_observation(subscription) # remove subscription from session->subscriptions map # if was_subscribed: self._session_to_subscriptions[session].discard(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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_deleted: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) return was_subscribed, was_last_subscriber def removeSubscriber(self, subscription, session, reason=None): """ Actively unsubscribe a subscriber session from a subscription. """ was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) if 'subscriber' in session._session_roles and session._session_roles['subscriber'] and session._session_roles['subscriber'].subscription_revocation: reply = message.Unsubscribed(0, subscription=subscription.id, reason=reason) self._router.send(session, reply) return was_subscribed, was_last_subscriber
class Dealer(object): """ Basic WAMP dealer. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures( caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True, payload_transparency=True, payload_encryption_cryptobox=True) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.attach` """ if session not in self._session_to_registrations: self._session_to_registrations[session] = set() else: raise Exception("session with ID {} already attached".format( session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.detach` """ if session in self._session_to_registrations: for registration in self._session_to_registrations[session]: was_registered, was_last_callee = self._registration_map.drop_observer( session, 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 was_registered: service_session.publish( u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish( u'wamp.registration.on_delete', session._session_id, registration.id) del self._session_to_registrations[session] else: raise Exception(u"session with ID {} not attached".format( session._session_id)) 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) elif register.match == u"prefix": uri_is_valid = _URI_PAT_STRICT_LAST_EMPTY.match( register.procedure) elif register.match == u"exact": uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match( register.procedure) else: # should not arrive here raise Exception("logic error") else: if register.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(register.procedure) elif register.match == u"prefix": uri_is_valid = _URI_PAT_LOOSE_LAST_EMPTY.match( register.procedure) elif register.match == u"exact": uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match( register.procedure) else: # should not arrive here raise Exception("logic error") 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) ]) self._router.send(session, 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) ]) self._router.send(session, 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) ]) self._router.send(session, 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) ]) self._router.send(session, reply) return # authorize REGISTER action # d = self._router.authorize(session, register.procedure, u'register') def on_authorize_success(authorization): if not authorization[u'allow']: # 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 = { u'id': registration.id, u'created': registration.created, u'uri': registration.uri, u'match': registration.match, u'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 # 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) """ 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) ]) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error) def processUnregister(self, session, unregister): """ Implements :func:`crossbar.router.interfaces.IDealer.processUnregister` """ # get registration by registration ID or None (if it doesn't exist on this broker) # registration = self._registration_map.get_observation_by_id( unregister.registration) if registration: if session in registration.observers: was_registered, was_last_callee = self._unregister( registration, session) reply = message.Unregistered(unregister.request) else: # registration exists on this dealer, but the session that wanted to unregister wasn't registered # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) else: # registration doesn't even exist on this broker # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) self._router.send(session, reply) def _unregister(self, registration, session): # drop session from registration observers # was_registered, was_last_callee = self._registration_map.drop_observer( session, registration) # remove registration from session->registrations map # if was_registered: self._session_to_registrations[session].discard(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 was_registered: service_session.publish(u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish(u'wamp.registration.on_delete', session._session_id, registration.id) return was_registered, was_last_callee def removeCallee(self, registration, session, reason=None): """ Actively unregister a callee session from a registration. """ was_registered, was_last_callee = self._unregister( registration, session) # actively inform the callee that it has been unregistered # if 'callee' in session._session_roles and session._session_roles[ 'callee'] and session._session_roles[ 'callee'].registration_revocation: reply = message.Unregistered(0, registration=registration.id, reason=reason) self._router.send(session, reply) return was_registered, was_last_callee 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) ]) self._router.send(session, reply) return # get registrations active on the procedure called # registration = self._registration_map.best_matching_observation( call.procedure) if registration: # validate payload (skip in "payload_transparency" mode) # if call.payload is None: try: self._router.validate(u'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) ]) self._router.send(session, reply) return # authorize CALL action # d = self._router.authorize(session, call.procedure, u'call') 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']: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, ApplicationError.NOT_AUTHORIZED, [ u"session is not authorized to call procedure '{0}'" .format(call.procedure) ]) self._router.send(session, 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 authorization[u'disclose']: caller = session._session_id caller_authid = session._authid caller_authrole = session._authrole else: caller = None caller_authid = None caller_authrole = 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 if call.payload: invocation = message.Invocation( invocation_request_id, registration.id, payload=call.payload, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, caller_authid=caller_authid, caller_authrole=caller_authrole, procedure=procedure, enc_algo=call.enc_algo, enc_key=call.enc_key, enc_serializer=call.enc_serializer) else: invocation = message.Invocation( invocation_request_id, registration.id, args=call.args, kwargs=call.kwargs, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, caller_authid=caller_authid, caller_authrole=caller_authrole, procedure=procedure) self._invocations[ invocation_request_id] = InvocationRequest( invocation_request_id, session, call) self._router.send(callee, 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) ]) self._router.send(session, 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) ]) self._router.send(session, reply) # noinspection PyUnusedLocal def processCancel(self, session, cancel): """ Implements :func:`crossbar.router.interfaces.IDealer.processCancel` """ assert (session in self._session_to_registrations) raise Exception("not implemented") def processYield(self, session, yield_): """ Implements :func:`crossbar.router.interfaces.IDealer.processYield` """ # assert(session in self._session_to_registrations) if yield_.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[yield_.request] is_valid = True if yield_.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_result', invocation_request.call.procedure, yield_.args, yield_.kwargs) except Exception as e: is_valid = False reply = message.Error( message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [ u"call result from procedure '{0}' with invalid application payload: {1}" .format(invocation_request.call.procedure, e) ]) else: reply = message.Result(invocation_request.call.request, args=yield_.args, kwargs=yield_.kwargs, progress=yield_.progress) else: reply = message.Result(invocation_request.call.request, payload=yield_.payload, progress=yield_.progress, enc_algo=yield_.enc_algo, enc_key=yield_.enc_key, enc_serializer=yield_.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done if it's a regular call (non-progressive) or if the payload was invalid # if not yield_.progress or not is_valid: del self._invocations[yield_.request] else: raise ProtocolError( u"Dealer.onYield(): YIELD received for non-pending request ID {0}" .format(yield_.request)) def processInvocationError(self, session, error): """ Implements :func:`crossbar.router.interfaces.IDealer.processInvocationError` """ # assert(session in self._session_to_registrations) if error.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[error.request] if error.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_error', invocation_request.call.procedure, error.args, error.kwargs) except Exception as e: reply = message.Error( message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [ u"call error from procedure '{0}' with invalid application payload: {1}" .format(invocation_request.call.procedure, e) ]) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, args=error.args, kwargs=error.kwargs) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, payload=error.payload, enc_algo=error.enc_algo, enc_key=error.enc_key, enc_serializer=error.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done # del self._invocations[error.request] else: raise ProtocolError( u"Dealer.onInvocationError(): ERROR received for non-pending request_type {0} and request ID {1}" .format(error.request_type, error.request))
class Broker(object): """ Basic WAMP broker. """ log = make_logger() def __init__(self, router, reactor, options=None): """ :param router: The router this broker is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._reactor = reactor self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, session_meta_api=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True, event_retention=True, payload_transparency=True, payload_encryption_cryptobox=True) # store for event history if self._router._store: self._event_store = self._router._store.event_store else: self._event_store = None # if there is a store, let the store attach itself to all the subscriptions # it is configured to track if self._event_store: self._event_store.attach_subscription_map(self._subscription_map) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.attach` """ if session not in self._session_to_subscriptions: self._session_to_subscriptions[session] = set() else: raise Exception(u"session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.detach` """ if session in self._session_to_subscriptions: for subscription in self._session_to_subscriptions[session]: was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) was_deleted = False # delete it if there are no subscribers and no retained events # if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: was_deleted = True self._subscription_map.delete_observation(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.'): def _publish(subscription): service_session = self._router._realm.session # FIXME: what about exclude_authid as colleced from forward_for? like we do elsewhere in this file! options = types.PublishOptions( correlation_id=None, correlation_is_anchor=True, correlation_is_last=False ) if was_subscribed: service_session.publish( u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id, options=options, ) if was_deleted: options.correlation_is_last = True service_session.publish( u'wamp.subscription.on_delete', 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, subscription) del self._session_to_subscriptions[session] else: raise Exception("session with ID {} not attached".format(session._session_id)) def _filter_publish_receivers(self, receivers, publish): """ Internal helper. Does all filtering on a candidate set of Publish receivers, based on all the white/blacklist options in 'publish'. """ # filter by "eligible" receivers # if publish.eligible: # map eligible session IDs to eligible sessions eligible = set() for session_id in publish.eligible: if session_id in self._router._session_id_to_session: eligible.add(self._router._session_id_to_session[session_id]) # filter receivers for eligible sessions receivers = eligible & receivers # if "eligible_authid" we only accept receivers that have the correct authid if publish.eligible_authid: eligible = set() for aid in publish.eligible_authid: eligible.update(self._router._authid_to_sessions.get(aid, set())) receivers = receivers & eligible # if "eligible_authrole" we only accept receivers that have the correct authrole if publish.eligible_authrole: eligible = set() for ar in publish.eligible_authrole: eligible.update(self._router._authrole_to_sessions.get(ar, set())) receivers = receivers & eligible # remove "excluded" receivers # if publish.exclude: # map excluded session IDs to excluded sessions exclude = set() for s in publish.exclude: if s in self._router._session_id_to_session: exclude.add(self._router._session_id_to_session[s]) # filter receivers for excluded sessions if exclude: receivers = receivers - exclude # remove auth-id based receivers if publish.exclude_authid: for aid in publish.exclude_authid: receivers = receivers - self._router._authid_to_sessions.get(aid, set()) # remove authrole based receivers if publish.exclude_authrole: for ar in publish.exclude_authrole: receivers = receivers - self._router._authrole_to_sessions.get(ar, set()) return receivers 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 forward_for = None if disclose: if publish.forward_for: publisher = publish.forward_for[0]['session'] publisher_authid = publish.forward_for[0]['authid'] publisher_authrole = publish.forward_for[0]['authrole'] forward_for = publish.forward_for + [ { 'session': session._session_id, 'authid': session._authid, 'authrole': session._authrole, } ] else: 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=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=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 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 exclude_authid = None if subscribe.forward_for: exclude_authid = [ff['authid'] for ff in subscribe.forward_for] def _publish(): service_session = self._router._realm.session if exclude_authid or self._router.is_traced: options = types.PublishOptions( correlation_id=subscribe.correlation_id, correlation_is_anchor=False, correlation_is_last=False, exclude_authid=exclude_authid, ) else: options = None 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: if options: 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 processUnsubscribe(self, session, unsubscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processUnsubscribe` """ if self._router.is_traced: if not unsubscribe.correlation_id: unsubscribe.correlation_id = self._router.new_correlation_id() unsubscribe.correlation_is_anchor = True unsubscribe.correlation_is_last = False # get subscription by subscription ID or None (if it doesn't exist on this broker) # subscription = self._subscription_map.get_observation_by_id(unsubscribe.subscription) if subscription: if self._router.is_traced and not unsubscribe.correlation_uri: unsubscribe.correlation_uri = subscription.uri if session in subscription.observers: was_subscribed, was_last_subscriber, has_follow_up_messages = self._unsubscribe(subscription, session, unsubscribe) reply = message.Unsubscribed(unsubscribe.request) if self._router.is_traced: reply.correlation_uri = subscription.uri reply.correlation_is_last = not has_follow_up_messages else: # subscription exists on this broker, but the session that wanted to unsubscribe wasn't subscribed # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) if self._router.is_traced: reply.correlation_uri = reply.error reply.correlation_is_last = True else: # subscription doesn't even exist on this broker # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) if self._router.is_traced: reply.correlation_uri = reply.error reply.correlation_is_last = True if self._router.is_traced: self._router._factory._worker._maybe_trace_rx_msg(session, unsubscribe) reply.correlation_id = unsubscribe.correlation_id reply.correlation_is_anchor = False self._router.send(session, reply) def _unsubscribe(self, subscription, session, unsubscribe=None): # drop session from subscription observers # was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) was_deleted = False if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: self._subscription_map.delete_observation(subscription) was_deleted = True # remove subscription from session->subscriptions map # if was_subscribed: self._session_to_subscriptions[session].discard(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 \ (was_subscribed or was_deleted): has_follow_up_messages = True exclude_authid = None if unsubscribe and unsubscribe.forward_for: exclude_authid = [ff['authid'] for ff in unsubscribe.forward_for] def _publish(): service_session = self._router._realm.session if unsubscribe and (exclude_authid or self._router.is_traced): options = types.PublishOptions( correlation_id=unsubscribe.correlation_id, correlation_is_anchor=False, correlation_is_last=False, exclude_authid=exclude_authid, ) else: options = None if was_subscribed: service_session.publish( u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id, options=options, ) if was_deleted: if options: options.correlation_is_last = True service_session.publish( u'wamp.subscription.on_delete', 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 return was_subscribed, was_last_subscriber, has_follow_up_messages def removeSubscriber(self, subscription, session, reason=None): """ Actively unsubscribe a subscriber session from a subscription. """ was_subscribed, was_last_subscriber, _ = self._unsubscribe(subscription, session) if 'subscriber' in session._session_roles and session._session_roles['subscriber'] and session._session_roles['subscriber'].subscription_revocation: reply = message.Unsubscribed(0, subscription=subscription.id, reason=reason) reply.correlation_uri = subscription.uri self._router.send(session, reply) return was_subscribed, was_last_subscriber
class Broker(object): """ Basic WAMP broker. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.attach` """ if session not in self._session_to_subscriptions: self._session_to_subscriptions[session] = set() else: raise Exception(u"session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.detach` """ if session in self._session_to_subscriptions: for subscription in self._session_to_subscriptions[session]: was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, 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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_last_subscriber: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) del self._session_to_subscriptions[session] else: raise Exception("session with ID {} not attached".format(session._session_id)) 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)]) 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, [u"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, [u"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, RouterAction.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, [u"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) """ self.log.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)] ) session._transport.send(reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_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 other than for wildcard subscriptions # if self._option_uri_strict: if subscribe.match == u"wildcard": uri_is_valid = _URI_PAT_STRICT_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) 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)]) session._transport.send(reply) return # authorize action # d = txaio.as_future(self._router.authorize, session, subscribe.topic, RouterAction.ACTION_SUBSCRIBE) def on_authorize_success(authorized): if not authorized: # 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 = { 'id': subscription.id, 'created': subscription.created, 'uri': subscription.uri, '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 # 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) """ # 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)] ) session._transport.send(reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error) def processUnsubscribe(self, session, unsubscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processUnsubscribe` """ # get subscription by subscription ID or None (if it doesn't exist on this broker) # subscription = self._subscription_map.get_observation_by_id(unsubscribe.subscription) if subscription: if session in subscription.observers: was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) reply = message.Unsubscribed(unsubscribe.request) else: # subscription exists on this broker, but the session that wanted to unsubscribe wasn't subscribed # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) else: # subscription doesn't even exist on this broker # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) session._transport.send(reply) def _unsubscribe(self, subscription, session): # drop session from subscription observers # was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) # remove subscription from session->subscriptions map # if was_subscribed: self._session_to_subscriptions[session].discard(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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_last_subscriber: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) return was_subscribed, was_last_subscriber def removeSubscriber(self, subscription, session, reason=None): """ Actively unsubscribe a subscriber session from a subscription. """ was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) if 'subscriber' in session._session_roles and session._session_roles['subscriber'] and session._session_roles['subscriber'].subscription_revocation: reply = message.Unsubscribed(0, subscription=subscription.id, reason=reason) session._transport.send(reply) return was_subscribed, was_last_subscriber
class Dealer(object): """ Basic WAMP dealer. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # map: session -> in-flight invocations self._callee_to_invocations = {} # BEWARE: this map must be kept up-to-date along with the # _invocations map below! Use the helper methods # _add_invoke_request and _remove_invoke_request # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures(caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True, payload_transparency=True, testament_meta_api=True, payload_encryption_cryptobox=True) # store for call queues if self._router._store: self._call_store = self._router._store.call_store else: self._call_store = None def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.attach` """ if session not in self._session_to_registrations: self._session_to_registrations[session] = set() else: raise Exception("session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.detach` """ if session in self._session_to_registrations: # send out Errors for any in-flight calls we have outstanding = self._callee_to_invocations.get(session, []) for invoke in outstanding: self.log.debug( "Cancelling in-flight INVOKE with id={request} on" " session {session}", request=invoke.call.request, session=session._session_id, ) reply = message.Error( message.Call.MESSAGE_TYPE, invoke.call.request, ApplicationError.CANCELED, [u"callee disconnected from in-flight request"], ) # send this directly to the caller's session invoke.caller._transport.send(reply) for registration in self._session_to_registrations[session]: was_registered, was_last_callee = self._registration_map.drop_observer(session, registration) if was_registered and was_last_callee: self._registration_map.delete_observation(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 was_registered: service_session.publish(u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish(u'wamp.registration.on_delete', session._session_id, registration.id) del self._session_to_registrations[session] else: raise Exception(u"session with ID {} not attached".format(session._session_id)) 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) elif register.match == u"prefix": uri_is_valid = _URI_PAT_STRICT_LAST_EMPTY.match(register.procedure) elif register.match == u"exact": uri_is_valid = _URI_PAT_STRICT_NON_EMPTY.match(register.procedure) else: # should not arrive here raise Exception("logic error") else: if register.match == u"wildcard": uri_is_valid = _URI_PAT_LOOSE_EMPTY.match(register.procedure) elif register.match == u"prefix": uri_is_valid = _URI_PAT_LOOSE_LAST_EMPTY.match(register.procedure) elif register.match == u"exact": uri_is_valid = _URI_PAT_LOOSE_NON_EMPTY.match(register.procedure) else: # should not arrive here raise Exception("logic error") 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)]) self._router.send(session, 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)]) self._router.send(session, 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)]) self._router.send(session, 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)]) self._router.send(session, reply) return # authorize REGISTER action # d = self._router.authorize(session, register.procedure, u'register') def on_authorize_success(authorization): if not authorization[u'allow']: # 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_callee_extra = RegistrationCalleeExtra(register.concurrency) registration, was_already_registered, is_first_callee = self._registration_map.add_observer(session, register.procedure, register.match, registration_extra, registration_callee_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 = { u'id': registration.id, u'created': registration.created, u'uri': registration.uri, u'match': registration.match, u'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 # 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) """ self.log.failure("Authorization of 'register' for '{uri}' failed", uri=register.procedure, 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)] ) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error) def processUnregister(self, session, unregister): """ Implements :func:`crossbar.router.interfaces.IDealer.processUnregister` """ # get registration by registration ID or None (if it doesn't exist on this broker) # registration = self._registration_map.get_observation_by_id(unregister.registration) if registration: if session in registration.observers: was_registered, was_last_callee = self._unregister(registration, session) reply = message.Unregistered(unregister.request) else: # registration exists on this dealer, but the session that wanted to unregister wasn't registered # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) else: # registration doesn't even exist on this broker # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) self._router.send(session, reply) def _unregister(self, registration, session): # drop session from registration observers # was_registered, was_last_callee = self._registration_map.drop_observer(session, registration) if was_registered and was_last_callee: self._registration_map.delete_observation(registration) # remove registration from session->registrations map # if was_registered: self._session_to_registrations[session].discard(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 was_registered: service_session.publish(u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish(u'wamp.registration.on_delete', session._session_id, registration.id) return was_registered, was_last_callee def removeCallee(self, registration, session, reason=None): """ Actively unregister a callee session from a registration. """ was_registered, was_last_callee = self._unregister(registration, session) # actively inform the callee that it has been unregistered # if 'callee' in session._session_roles and session._session_roles['callee'] and session._session_roles['callee'].registration_revocation: reply = message.Unregistered(0, registration=registration.id, reason=reason) self._router.send(session, reply) return was_registered, was_last_callee 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)]) self._router.send(session, reply) return # get registrations active on the procedure called # registration = self._registration_map.best_matching_observation(call.procedure) if registration: # validate payload (skip in "payload_transparency" mode) # if call.payload is None: try: self._router.validate(u'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)]) self._router.send(session, reply) return # authorize CALL action # d = self._router.authorize(session, call.procedure, u'call') 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']: reply = message.Error(message.Call.MESSAGE_TYPE, call.request, ApplicationError.NOT_AUTHORIZED, [u"session is not authorized to call procedure '{0}'".format(call.procedure)]) self._router.send(session, reply) else: self._call(session, call, registration, authorization) 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 'call' for '{uri}' failed", uri=call.procedure, 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)] ) self._router.send(session, 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)]) self._router.send(session, reply) def _call(self, session, call, registration, authorization, is_queued_call=False): # will hold the callee (the concrete endpoint) that we will forward the call to .. # callee = None callee_extra = None # determine callee according to invocation policy # if registration.extra.invoke in [message.Register.INVOKE_SINGLE, message.Register.INVOKE_FIRST, message.Register.INVOKE_LAST]: # a single endpoint is considered for forwarding the call .. 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] else: # should not arrive here raise Exception(u"logic error") # check maximum concurrency of the (single) endpoint callee_extra = registration.observers_extra.get(callee, None) if callee_extra: if callee_extra.concurrency and callee_extra.concurrency_current >= callee_extra.concurrency: if is_queued_call or (self._call_store and self._call_store.maybe_queue_call(session, call, registration, authorization)): return False else: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, u'crossbar.error.max_concurrency_reached', [u'maximum concurrency {} of callee/endpoint reached (on non-shared/single registration)'.format(callee_extra.concurrency)] ) self._router.send(session, reply) return False else: callee_extra.concurrency_current += 1 elif registration.extra.invoke == message.Register.INVOKE_ROUNDROBIN: # remember where we started to search for a suitable callee/endpoint in the round-robin list of callee endpoints roundrobin_start_index = registration.extra.roundrobin_current % len(registration.observers) # now search fo a suitable callee/endpoint while True: callee = registration.observers[registration.extra.roundrobin_current % len(registration.observers)] callee_extra = registration.observers_extra.get(callee, None) registration.extra.roundrobin_current += 1 if callee_extra and callee_extra.concurrency: if callee_extra.concurrency_current >= callee_extra.concurrency: # this callee has set a maximum concurrency that has already been reached. # we need to search further .. but only if we haven't reached the beginning # of our round-robin list if registration.extra.roundrobin_current % len(registration.observers) == roundrobin_start_index: # we've looked through the whole round-robin list, and didn't find a suitable # callee (one that hasn't it's maximum concurrency already reached). if is_queued_call or (self._call_store and self._call_store.maybe_queue_call(session, call, registration, authorization)): return False else: reply = message.Error( message.Call.MESSAGE_TYPE, call.request, u'crossbar.error.max_concurrency_reached', [u'maximum concurrency of all callee/endpoints reached (on round-robin registration)'.format(callee_extra.concurrency)] ) self._router.send(session, reply) return False else: # .. search on .. pass else: # ok, we've found a callee that has set a maximum concurrency, but where the # maximum has not yet been reached break else: # ok, we've found a callee which hasn't set a maximum concurrency, and hence is always # eligible for having a call forwarded to break if callee_extra: callee_extra.concurrency_current += 1 elif registration.extra.invoke == message.Register.INVOKE_RANDOM: # FIXME: implement max. concurrency and call queueing 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 authorization[u'disclose']: disclose = True elif (call.procedure.startswith(u"wamp.") or call.procedure.startswith(u"crossbar.")): disclose = True else: disclose = False if disclose: caller = session._session_id caller_authid = session._authid caller_authrole = session._authrole else: caller = None caller_authid = None caller_authrole = 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 if call.payload: invocation = message.Invocation(invocation_request_id, registration.id, payload=call.payload, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, caller_authid=caller_authid, caller_authrole=caller_authrole, procedure=procedure, enc_algo=call.enc_algo, enc_key=call.enc_key, enc_serializer=call.enc_serializer) else: invocation = message.Invocation(invocation_request_id, registration.id, args=call.args, kwargs=call.kwargs, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, caller_authid=caller_authid, caller_authrole=caller_authrole, procedure=procedure) self._add_invoke_request(invocation_request_id, registration, session, call, callee) self._router.send(callee, invocation) return True def _add_invoke_request(self, invocation_request_id, registration, session, call, callee): """ Internal helper. Adds an InvocationRequest to both the _callee_to_invocations and _invocations maps. """ invoke_request = InvocationRequest(invocation_request_id, registration, session, call, callee) self._invocations[invocation_request_id] = invoke_request invokes = self._callee_to_invocations.get(callee, []) invokes.append(invoke_request) self._callee_to_invocations[callee] = invokes return invoke_request def _remove_invoke_request(self, invocation_request): """ Internal helper. Removes an InvocationRequest from both the _callee_to_invocations and _invocations maps. """ invokes = self._callee_to_invocations[invocation_request.callee] invokes.remove(invocation_request) if not invokes: del self._callee_to_invocations[invocation_request.callee] del self._invocations[invocation_request.id] # noinspection PyUnusedLocal def processCancel(self, session, cancel): """ Implements :func:`crossbar.router.interfaces.IDealer.processCancel` """ assert(session in self._session_to_registrations) raise Exception("not implemented") def processYield(self, session, yield_): """ Implements :func:`crossbar.router.interfaces.IDealer.processYield` """ # assert(session in self._session_to_registrations) if yield_.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[yield_.request] is_valid = True if yield_.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_result', invocation_request.call.procedure, yield_.args, yield_.kwargs) except Exception as e: is_valid = False reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [u"call result from procedure '{0}' with invalid application payload: {1}".format(invocation_request.call.procedure, e)]) else: reply = message.Result(invocation_request.call.request, args=yield_.args, kwargs=yield_.kwargs, progress=yield_.progress) else: reply = message.Result(invocation_request.call.request, payload=yield_.payload, progress=yield_.progress, enc_algo=yield_.enc_algo, enc_key=yield_.enc_key, enc_serializer=yield_.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done if it's a regular call (non-progressive) or if the payload was invalid # if not yield_.progress or not is_valid: callee_extra = invocation_request.registration.observers_extra.get(session, None) if callee_extra: callee_extra.concurrency_current -= 1 self._remove_invoke_request(invocation_request) # check for any calls queued on the registration for which an # invocation just returned, and hence there is likely concurrency # free again to actually forward calls previously queued calls # that were queued because no callee endpoint concurrency was free if self._call_store: queued_call = self._call_store.get_queued_call(invocation_request.registration) if queued_call: invocation_sent = self._call(queued_call.session, queued_call.call, queued_call.registration, queued_call.authorization, True) # only actually pop the queued call when we really were # able to forward the call now if invocation_sent: self._call_store.pop_queued_call(invocation_request.registration) else: raise ProtocolError(u"Dealer.onYield(): YIELD received for non-pending request ID {0}".format(yield_.request)) def processInvocationError(self, session, error): """ Implements :func:`crossbar.router.interfaces.IDealer.processInvocationError` """ # assert(session in self._session_to_registrations) if error.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[error.request] if error.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_error', invocation_request.call.procedure, error.args, error.kwargs) except Exception as e: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [u"call error from procedure '{0}' with invalid application payload: {1}".format(invocation_request.call.procedure, e)]) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, args=error.args, kwargs=error.kwargs) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, payload=error.payload, enc_algo=error.enc_algo, enc_key=error.enc_key, enc_serializer=error.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done # invoke = self._invocations[error.request] self._remove_invoke_request(invoke) else: raise ProtocolError(u"Dealer.onInvocationError(): ERROR received for non-pending request_type {0} and request ID {1}".format(error.request_type, error.request))
class Dealer(object): """ Basic WAMP dealer. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # generator for WAMP request IDs self._request_id_gen = util.IdGenerator() # registration map managed by this dealer self._registration_map = UriObservationMap(ordered=True) # map: session -> set of registrations (needed for detach) self._session_to_registrations = {} # pending callee invocation requests self._invocations = {} # check all procedure URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleDealerFeatures(caller_identification=True, pattern_based_registration=True, session_meta_api=True, registration_meta_api=True, shared_registration=True, progressive_call_results=True, registration_revocation=True, payload_transparency=True, payload_encryption_cryptobox=True) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.attach` """ if session not in self._session_to_registrations: self._session_to_registrations[session] = set() else: raise Exception("session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IDealer.detach` """ if session in self._session_to_registrations: for registration in self._session_to_registrations[session]: was_registered, was_last_callee = self._registration_map.drop_observer(session, 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 was_registered: service_session.publish(u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish(u'wamp.registration.on_delete', session._session_id, registration.id) del self._session_to_registrations[session] else: raise Exception(u"session with ID {} not attached".format(session._session_id)) 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)]) self._router.send(session, 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)]) self._router.send(session, 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)]) self._router.send(session, 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)]) self._router.send(session, 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 # 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) """ 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)] ) self._router.send(session, reply) txaio.add_callbacks(d, on_authorize_success, on_authorize_error) def processUnregister(self, session, unregister): """ Implements :func:`crossbar.router.interfaces.IDealer.processUnregister` """ # get registration by registration ID or None (if it doesn't exist on this broker) # registration = self._registration_map.get_observation_by_id(unregister.registration) if registration: if session in registration.observers: was_registered, was_last_callee = self._unregister(registration, session) reply = message.Unregistered(unregister.request) else: # registration exists on this dealer, but the session that wanted to unregister wasn't registered # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) else: # registration doesn't even exist on this broker # reply = message.Error(message.Unregister.MESSAGE_TYPE, unregister.request, ApplicationError.NO_SUCH_REGISTRATION) self._router.send(session, reply) def _unregister(self, registration, session): # drop session from registration observers # was_registered, was_last_callee = self._registration_map.drop_observer(session, registration) # remove registration from session->registrations map # if was_registered: self._session_to_registrations[session].discard(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 was_registered: service_session.publish(u'wamp.registration.on_unregister', session._session_id, registration.id) if was_last_callee: service_session.publish(u'wamp.registration.on_delete', session._session_id, registration.id) return was_registered, was_last_callee def removeCallee(self, registration, session, reason=None): """ Actively unregister a callee session from a registration. """ was_registered, was_last_callee = self._unregister(registration, session) # actively inform the callee that it has been unregistered # if 'callee' in session._session_roles and session._session_roles['callee'] and session._session_roles['callee'].registration_revocation: reply = message.Unregistered(0, registration=registration.id, reason=reason) self._router.send(session, reply) return was_registered, was_last_callee 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)]) self._router.send(session, reply) return # get registrations active on the procedure called # registration = self._registration_map.best_matching_observation(call.procedure) if registration: # validate payload (skip in "payload_transparency" mode) # if call.payload is None: 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)]) self._router.send(session, 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)]) self._router.send(session, 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 if call.payload: invocation = message.Invocation(invocation_request_id, registration.id, payload=call.payload, timeout=call.timeout, receive_progress=call.receive_progress, caller=caller, procedure=procedure, enc_algo=call.enc_algo, enc_key=call.enc_key, enc_serializer=call.enc_serializer) else: 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) self._router.send(callee, 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)] ) self._router.send(session, 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)]) self._router.send(session, reply) # noinspection PyUnusedLocal def processCancel(self, session, cancel): """ Implements :func:`crossbar.router.interfaces.IDealer.processCancel` """ assert(session in self._session_to_registrations) raise Exception("not implemented") def processYield(self, session, yield_): """ Implements :func:`crossbar.router.interfaces.IDealer.processYield` """ # assert(session in self._session_to_registrations) if yield_.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[yield_.request] is_valid = True if yield_.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_result', invocation_request.call.procedure, yield_.args, yield_.kwargs) except Exception as e: is_valid = False reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [u"call result from procedure '{0}' with invalid application payload: {1}".format(invocation_request.call.procedure, e)]) else: reply = message.Result(invocation_request.call.request, args=yield_.args, kwargs=yield_.kwargs, progress=yield_.progress) else: reply = message.Result(invocation_request.call.request, payload=yield_.payload, progress=yield_.progress, enc_algo=yield_.enc_algo, enc_key=yield_.enc_key, enc_serializer=yield_.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done if it's a regular call (non-progressive) or if the payload was invalid # if not yield_.progress or not is_valid: del self._invocations[yield_.request] else: raise ProtocolError(u"Dealer.onYield(): YIELD received for non-pending request ID {0}".format(yield_.request)) def processInvocationError(self, session, error): """ Implements :func:`crossbar.router.interfaces.IDealer.processInvocationError` """ # assert(session in self._session_to_registrations) if error.request in self._invocations: # get the invocation request tracked for the caller # invocation_request = self._invocations[error.request] if error.payload is None: # validate normal args/kwargs payload try: self._router.validate('call_error', invocation_request.call.procedure, error.args, error.kwargs) except Exception as e: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, ApplicationError.INVALID_ARGUMENT, [u"call error from procedure '{0}' with invalid application payload: {1}".format(invocation_request.call.procedure, e)]) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, args=error.args, kwargs=error.kwargs) else: reply = message.Error(message.Call.MESSAGE_TYPE, invocation_request.call.request, error.error, payload=error.payload, enc_algo=error.enc_algo, enc_key=error.enc_key, enc_serializer=error.enc_serializer) # the calling session might have been lost in the meantime .. # if invocation_request.caller._transport: self._router.send(invocation_request.caller, reply) # the call is done # del self._invocations[error.request] else: raise ProtocolError(u"Dealer.onInvocationError(): ERROR received for non-pending request_type {0} and request ID {1}".format(error.request_type, error.request))
class Broker(object): """ Basic WAMP broker. """ log = make_logger() def __init__(self, router, options=None): """ :param router: The router this dealer is part of. :type router: Object that implements :class:`crossbar.router.interfaces.IRouter`. :param options: Router options. :type options: Instance of :class:`crossbar.router.types.RouterOptions`. """ self._router = router self._options = options or RouterOptions() # subscription map managed by this broker self._subscription_map = UriObservationMap() # map: session -> set of subscriptions (needed for detach) self._session_to_subscriptions = {} # check all topic URIs with strict rules self._option_uri_strict = self._options.uri_check == RouterOptions.URI_CHECK_STRICT # supported features from "WAMP Advanced Profile" self._role_features = role.RoleBrokerFeatures(publisher_identification=True, pattern_based_subscription=True, session_meta_api=True, subscription_meta_api=True, subscriber_blackwhite_listing=True, publisher_exclusion=True, subscription_revocation=True, event_retention=True, payload_transparency=True, payload_encryption_cryptobox=True) # store for event history if self._router._store: self._event_store = self._router._store.event_store else: self._event_store = None # if there is a store, let the store attach itself to all the subscriptions # it is configured to track if self._event_store: self._event_store.attach_subscription_map(self._subscription_map) def attach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.attach` """ if session not in self._session_to_subscriptions: self._session_to_subscriptions[session] = set() else: raise Exception(u"session with ID {} already attached".format(session._session_id)) def detach(self, session): """ Implements :func:`crossbar.router.interfaces.IBroker.detach` """ if session in self._session_to_subscriptions: for subscription in self._session_to_subscriptions[session]: was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) was_deleted = False # delete it if there are no subscribers and no retained events # if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: was_deleted = True self._subscription_map.delete_observation(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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_deleted: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) del self._session_to_subscriptions[session] else: raise Exception("session with ID {} not attached".format(session._session_id)) 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." 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 = publish.topic.startswith(u"wamp.") or publish.topic.startswith(u"crossbar.") 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') 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 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) def processUnsubscribe(self, session, unsubscribe): """ Implements :func:`crossbar.router.interfaces.IBroker.processUnsubscribe` """ # get subscription by subscription ID or None (if it doesn't exist on this broker) # subscription = self._subscription_map.get_observation_by_id(unsubscribe.subscription) if subscription: if session in subscription.observers: was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) reply = message.Unsubscribed(unsubscribe.request) else: # subscription exists on this broker, but the session that wanted to unsubscribe wasn't subscribed # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) else: # subscription doesn't even exist on this broker # reply = message.Error(message.Unsubscribe.MESSAGE_TYPE, unsubscribe.request, ApplicationError.NO_SUCH_SUBSCRIPTION) self._router.send(session, reply) def _unsubscribe(self, subscription, session): # drop session from subscription observers # was_subscribed, was_last_subscriber = self._subscription_map.drop_observer(session, subscription) was_deleted = False if was_subscribed and was_last_subscriber and not subscription.extra.retained_events: was_deleted = True self._subscription_map.delete_observation(subscription) # remove subscription from session->subscriptions map # if was_subscribed: self._session_to_subscriptions[session].discard(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 was_subscribed: service_session.publish(u'wamp.subscription.on_unsubscribe', session._session_id, subscription.id) if was_deleted: service_session.publish(u'wamp.subscription.on_delete', session._session_id, subscription.id) return was_subscribed, was_last_subscriber def removeSubscriber(self, subscription, session, reason=None): """ Actively unsubscribe a subscriber session from a subscription. """ was_subscribed, was_last_subscriber = self._unsubscribe(subscription, session) if 'subscriber' in session._session_roles and session._session_roles['subscriber'] and session._session_roles['subscriber'].subscription_revocation: reply = message.Unsubscribed(0, subscription=subscription.id, reason=reason) self._router.send(session, reply) return was_subscribed, was_last_subscriber