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 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 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 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))