12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958 |
- ###############################################################################
- #
- # The MIT License (MIT)
- #
- # Copyright (c) Crossbar.io Technologies GmbH
- #
- # Permission is hereby granted, free of charge, to any person obtaining a copy
- # of this software and associated documentation files (the "Software"), to deal
- # in the Software without restriction, including without limitation the rights
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- # copies of the Software, and to permit persons to whom the Software is
- # furnished to do so, subject to the following conditions:
- #
- # The above copyright notice and this permission notice shall be included in
- # all copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- # THE SOFTWARE.
- #
- ###############################################################################
-
- from __future__ import absolute_import
-
- import six
- import txaio
- import inspect
- from functools import reduce
-
- from autobahn import wamp
- from autobahn.util import public, IdGenerator, ObservableMixin
- from autobahn.wamp import uri
- from autobahn.wamp import message
- from autobahn.wamp import types
- from autobahn.wamp import role
- from autobahn.wamp import exception
- from autobahn.wamp.exception import ApplicationError, ProtocolError, SessionNotReady, SerializationError
- from autobahn.wamp.interfaces import ISession, IPayloadCodec, IAuthenticator # noqa
- from autobahn.wamp.types import SessionDetails, CloseDetails, EncodedPayload
- from autobahn.exception import PayloadExceededError
- from autobahn.wamp.request import \
- Publication, \
- Subscription, \
- Handler, \
- Registration, \
- Endpoint, \
- PublishRequest, \
- SubscribeRequest, \
- UnsubscribeRequest, \
- CallRequest, \
- InvocationRequest, \
- RegisterRequest, \
- UnregisterRequest
-
-
- def is_method_or_function(f):
- return inspect.ismethod(f) or inspect.isfunction(f)
-
-
- class BaseSession(ObservableMixin):
- """
- WAMP session base class.
-
- This class implements :class:`autobahn.wamp.interfaces.ISession`.
- """
-
- log = txaio.make_logger()
-
- def __init__(self):
- """
-
- """
- self.set_valid_events(
- valid_events=[
- 'join', # right before onJoin runs
- 'leave', # after onLeave has run
- 'ready', # after onJoin and all 'join' listeners have completed
- 'connect', # right before onConnect
- 'disconnect', # right after onDisconnect
- ]
- )
-
- # this is for marshalling traceback from exceptions thrown in user
- # code within WAMP error messages (that is, when invoking remoted
- # procedures)
- self.traceback_app = False
-
- # mapping of exception classes to WAMP error URIs
- self._ecls_to_uri_pat = {}
-
- # mapping of WAMP error URIs to exception classes
- self._uri_to_ecls = {
- ApplicationError.INVALID_PAYLOAD: SerializationError,
- ApplicationError.PAYLOAD_SIZE_EXCEEDED: PayloadExceededError,
- }
-
- # session authentication information
- self._realm = None
- self._session_id = None
- self._authid = None
- self._authrole = None
- self._authmethod = None
- self._authprovider = None
-
- # payload transparency codec
- self._payload_codec = None
-
- # generator for WAMP request IDs
- self._request_id_gen = IdGenerator()
-
- @property
- def realm(self):
- return self._realm
-
- @property
- def session_id(self):
- return self._session_id
-
- @property
- def authid(self):
- return self._authid
-
- @property
- def authrole(self):
- return self._authrole
-
- @property
- def authmethod(self):
- return self._authmethod
-
- @property
- def authprovider(self):
- return self._authprovider
-
- def define(self, exception, error=None):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.define`
- """
- if error is None:
- assert(hasattr(exception, '_wampuris'))
- self._ecls_to_uri_pat[exception] = exception._wampuris
- self._uri_to_ecls[exception._wampuris[0].uri()] = exception
- else:
- assert(not hasattr(exception, '_wampuris'))
- self._ecls_to_uri_pat[exception] = [uri.Pattern(six.u(error), uri.Pattern.URI_TARGET_HANDLER)]
- self._uri_to_ecls[six.u(error)] = exception
-
- def _message_from_exception(self, request_type, request, exc, tb=None, enc_algo=None):
- """
- Create a WAMP error message from an exception.
-
- :param request_type: The request type this WAMP error message is for.
- :type request_type: int
-
- :param request: The request ID this WAMP error message is for.
- :type request: int
-
- :param exc: The exception.
- :type exc: Instance of :class:`Exception` or subclass thereof.
-
- :param tb: Optional traceback. If present, it'll be included with the WAMP error message.
- :type tb: list or None
- """
- args = None
- if hasattr(exc, 'args'):
- args = list(exc.args) # make sure tuples are made into lists
-
- kwargs = None
- if hasattr(exc, 'kwargs'):
- kwargs = exc.kwargs
-
- if kwargs and six.PY2:
- kwargs = {
- k.decode('utf8'): v
- for k, v in kwargs.iteritems()
- }
-
- if tb:
- if kwargs:
- kwargs[u'traceback'] = tb
- else:
- kwargs = {u'traceback': tb}
-
- if isinstance(exc, exception.ApplicationError):
- error = exc.error if type(exc.error) == six.text_type else six.u(exc.error)
- else:
- if exc.__class__ in self._ecls_to_uri_pat:
- error = self._ecls_to_uri_pat[exc.__class__][0]._uri
- else:
- error = u"wamp.error.runtime_error"
-
- encoded_payload = None
- if self._payload_codec:
- encoded_payload = self._payload_codec.encode(False, error, args, kwargs)
-
- if encoded_payload:
- msg = message.Error(request_type,
- request,
- error,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer)
- else:
- msg = message.Error(request_type,
- request,
- error,
- args,
- kwargs)
-
- return msg
-
- def _exception_from_message(self, msg):
- """
- Create a user (or generic) exception from a WAMP error message.
-
- :param msg: A WAMP error message.
- :type msg: instance of :class:`autobahn.wamp.message.Error`
- """
-
- # FIXME:
- # 1. map to ecls based on error URI wildcard/prefix
- # 2. extract additional args/kwargs from error URI
-
- exc = None
- enc_err = None
-
- if msg.enc_algo:
-
- if not self._payload_codec:
- log_msg = u"received encoded payload, but no payload codec active"
- self.log.warn(log_msg)
- enc_err = ApplicationError(ApplicationError.ENC_NO_PAYLOAD_CODEC, log_msg, enc_algo=msg.enc_algo)
- else:
- try:
- encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
- decrypted_error, msg.args, msg.kwargs = self._payload_codec.decode(True, msg.error, encoded_payload)
- except Exception as e:
- self.log.warn("failed to decrypt application payload 1: {err}", err=e)
- enc_err = ApplicationError(
- ApplicationError.ENC_DECRYPT_ERROR,
- u"failed to decrypt application payload 1: {}".format(e),
- enc_algo=msg.enc_algo,
- )
- else:
- if msg.error != decrypted_error:
- self.log.warn(
- u"URI within encrypted payload ('{decrypted_error}') does not match the envelope ('{error}')",
- decrypted_error=decrypted_error,
- error=msg.error,
- )
- enc_err = ApplicationError(
- ApplicationError.ENC_TRUSTED_URI_MISMATCH,
- u"URI within encrypted payload ('{}') does not match the envelope ('{}')".format(decrypted_error, msg.error),
- enc_algo=msg.enc_algo,
- )
-
- if enc_err:
- return enc_err
-
- if msg.error in self._uri_to_ecls:
- ecls = self._uri_to_ecls[msg.error]
- try:
- # the following might fail, eg. TypeError when
- # signature of exception constructor is incompatible
- # with args/kwargs or when the exception constructor raises
- if msg.kwargs:
- if msg.args:
- exc = ecls(*msg.args, **msg.kwargs)
- else:
- exc = ecls(**msg.kwargs)
- else:
- if msg.args:
- exc = ecls(*msg.args)
- else:
- exc = ecls()
- except Exception:
- try:
- self.onUserError(
- txaio.create_failure(),
- "While re-constructing exception",
- )
- except:
- pass
-
- if not exc:
- # the following ctor never fails ..
- if msg.kwargs:
- if msg.args:
- exc = exception.ApplicationError(msg.error, *msg.args, **msg.kwargs)
- else:
- exc = exception.ApplicationError(msg.error, **msg.kwargs)
- else:
- if msg.args:
- exc = exception.ApplicationError(msg.error, *msg.args)
- else:
- exc = exception.ApplicationError(msg.error)
-
- # FIXME: cleanup and integate into ctors above
- if hasattr(exc, 'enc_algo'):
- exc.enc_algo = msg.enc_algo
- if hasattr(exc, 'callee'):
- exc.callee = msg.callee
- if hasattr(exc, 'callee_authid'):
- exc.callee_authid = msg.callee_authid
- if hasattr(exc, 'callee_authrole'):
- exc.callee_authrole = msg.callee_authrole
- if hasattr(exc, 'forward_for'):
- exc.forward_for = msg.forward_for
-
- return exc
-
-
- @public
- class ApplicationSession(BaseSession):
- """
- WAMP endpoint session.
- """
-
- def __init__(self, config=None):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession`
- """
- BaseSession.__init__(self)
- self.config = config or types.ComponentConfig(realm=u"realm1")
-
- # set client role features supported and announced
- self._session_roles = role.DEFAULT_CLIENT_ROLES
-
- self._transport = None
- self._session_id = None
- self._realm = None
-
- self._goodbye_sent = False
- self._transport_is_closing = False
-
- # outstanding requests
- self._publish_reqs = {}
- self._subscribe_reqs = {}
- self._unsubscribe_reqs = {}
- self._call_reqs = {}
- self._register_reqs = {}
- self._unregister_reqs = {}
-
- # subscriptions in place
- self._subscriptions = {}
-
- # registrations in place
- self._registrations = {}
-
- # incoming invocations
- self._invocations = {}
-
- @public
- def set_payload_codec(self, payload_codec):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.set_payload_codec`
- """
- assert(payload_codec is None or isinstance(payload_codec, IPayloadCodec))
- self._payload_codec = payload_codec
-
- @public
- def get_payload_codec(self):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.get_payload_codec`
- """
- return self._payload_codec
-
- @public
- def onOpen(self, transport):
- """
- Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onOpen`
- """
- self._transport = transport
- d = self.fire('connect', self, transport)
- txaio.add_callbacks(
- d, None,
- lambda fail: self._swallow_error(fail, "While notifying 'connect'")
- )
- txaio.add_callbacks(
- d,
- lambda _: txaio.as_future(self.onConnect),
- lambda fail: self._swallow_error(fail, "While calling 'onConnect'")
- )
-
- @public
- def onConnect(self):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onConnect`
- """
- self.join(self.config.realm)
-
- @public
- def join(self,
- realm,
- authmethods=None,
- authid=None,
- authrole=None,
- authextra=None,
- resumable=None,
- resume_session=None,
- resume_token=None):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.join`
- """
- assert(realm is None or type(realm) == six.text_type)
- assert(authmethods is None or type(authmethods) == list)
- if type(authmethods) == list:
- for authmethod in authmethods:
- assert(type(authmethod) == six.text_type)
- assert(authid is None or type(authid) == six.text_type)
- assert(authrole is None or type(authrole) == six.text_type)
- assert(authextra is None or type(authextra) == dict)
-
- if self._session_id:
- raise Exception("already joined")
-
- # store the realm requested by client, though this might be overwritten later,
- # when realm redirection kicks in
- self._realm = realm
-
- # closing handshake state
- self._goodbye_sent = False
-
- # send HELLO message to router
- msg = message.Hello(realm=realm,
- roles=self._session_roles,
- authmethods=authmethods,
- authid=authid,
- authrole=authrole,
- authextra=authextra,
- resumable=resumable,
- resume_session=resume_session,
- resume_token=resume_token)
- self._transport.send(msg)
-
- @public
- def disconnect(self):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.disconnect`
- """
- if self._transport:
- self._transport.close()
-
- @public
- def is_connected(self):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.is_connected`
- """
- return self._transport is not None
-
- @public
- def is_attached(self):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.is_attached`
- """
- return self._transport is not None and self._session_id is not None
-
- @public
- def onUserError(self, fail, msg):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onUserError`
- """
- if isinstance(fail.value, exception.ApplicationError):
- self.log.warn('{klass}.onUserError(): "{msg}"',
- klass=self.__class__.__name__,
- msg=fail.value.error_message())
- else:
- self.log.error(
- '{klass}.onUserError(): "{msg}"\n{traceback}',
- klass=self.__class__.__name__,
- msg=msg,
- traceback=txaio.failure_format_traceback(fail),
- )
-
- def _swallow_error(self, fail, msg):
- '''
- This is an internal generic error-handler for errors encountered
- when calling down to on*() handlers that can reasonably be
- expected to be overridden in user code.
-
- Note that it *cancels* the error, so use with care!
-
- Specifically, this should *never* be added to the errback
- chain for a Deferred/coroutine that will make it out to user
- code.
- '''
- try:
- self.onUserError(fail, msg)
- except Exception:
- self.log.error(
- "Internal error: {tb}",
- tb=txaio.failure_format_traceback(txaio.create_failure()),
- )
- return None
-
- def onMessage(self, msg):
- """
- Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage`
- """
-
- if self._session_id is None:
-
- # the first message must be WELCOME, ABORT or CHALLENGE ..
- if isinstance(msg, message.Welcome):
-
- # before we let user code see the session -- that is,
- # before we fire "join" -- we give authentication
- # instances a chance to abort the session. Usually
- # this would be for "mutual authentication"
- # scenarios. For example, WAMP-SCRAM uses this to
- # confirm the server-signature
- d = txaio.as_future(self.onWelcome, msg)
-
- def success(res):
- if res is not None:
- self.log.info("Session denied by onWelcome")
- reply = message.Abort(
- u"wamp.error.cannot_authenticate", u"{0}".format(res)
- )
- self._transport.send(reply)
- return
-
- if msg.realm:
- self._realm = msg.realm
- self._session_id = msg.session
- self._authid = msg.authid
- self._authrole = msg.authrole
- self._authmethod = msg.authmethod
- self._authprovider = msg.authprovider
- self._router_roles = msg.roles
-
- details = SessionDetails(
- realm=self._realm,
- session=self._session_id,
- authid=self._authid,
- authrole=self._authrole,
- authmethod=self._authmethod,
- authprovider=self._authprovider,
- authextra=msg.authextra,
- serializer=self._transport._serializer.SERIALIZER_ID,
- resumed=msg.resumed,
- resumable=msg.resumable,
- resume_token=msg.resume_token,
- )
- # firing 'join' *before* running onJoin, so that
- # the idiom where you "do stuff" in onJoin --
- # possibly including self.leave() -- works
- # properly. Besides, there's "ready" that fires
- # after 'join' and onJoin have all completed...
- d = self.fire('join', self, details)
- # add a logging errback first, which will ignore any
- # errors from fire()
- txaio.add_callbacks(
- d, None,
- lambda e: self._swallow_error(e, "While notifying 'join'")
- )
- # this should run regardless
- txaio.add_callbacks(
- d,
- lambda _: txaio.as_future(self.onJoin, details),
- None
- )
- # ignore any errors from onJoin (XXX or, should that be fatal?)
- txaio.add_callbacks(
- d, None,
- lambda e: self._swallow_error(e, "While firing onJoin")
- )
- # this instance is now "ready"...
- txaio.add_callbacks(
- d,
- lambda _: self.fire('ready', self),
- None
- )
- # ignore any errors from 'ready'
- txaio.add_callbacks(
- d, None,
- lambda e: self._swallow_error(e, "While notifying 'ready'")
- )
-
- def error(e):
- reply = message.Abort(
- u"wamp.error.cannot_authenticate", u"Error calling onWelcome handler"
- )
- self._transport.send(reply)
- return self._swallow_error(e, "While firing onWelcome")
- txaio.add_callbacks(d, success, error)
-
- elif isinstance(msg, message.Abort):
- # fire callback and close the transport
- details = types.CloseDetails(msg.reason, msg.message)
- d = txaio.as_future(self.onLeave, details)
-
- def success(arg):
- # XXX also: handle async
- d = self.fire('leave', self, details)
-
- def return_arg(_):
- return arg
-
- def _error(e):
- return self._swallow_error(e, "While firing 'leave' event")
- txaio.add_callbacks(d, return_arg, _error)
- return d
-
- def _error(e):
- return self._swallow_error(e, "While firing onLeave")
- txaio.add_callbacks(d, success, _error)
-
- elif isinstance(msg, message.Challenge):
-
- challenge = types.Challenge(msg.method, msg.extra)
- d = txaio.as_future(self.onChallenge, challenge)
-
- def success(signature):
- if signature is None:
- raise Exception('onChallenge user callback did not return a signature')
- if type(signature) == six.binary_type:
- signature = signature.decode('utf8')
- if type(signature) != six.text_type:
- raise Exception('signature must be unicode (was {})'.format(type(signature)))
- reply = message.Authenticate(signature)
- self._transport.send(reply)
-
- def error(err):
- self.onUserError(err, "Authentication failed")
- reply = message.Abort(u"wamp.error.cannot_authenticate", u"{0}".format(err.value))
- self._transport.send(reply)
- # fire callback and close the transport
- details = types.CloseDetails(reply.reason, reply.message)
- d = txaio.as_future(self.onLeave, details)
-
- def success(arg):
- # XXX also: handle async
- self.fire('leave', self, details)
- return arg
-
- def _error(e):
- return self._swallow_error(e, "While firing onLeave")
- txaio.add_callbacks(d, success, _error)
- # switching to the callback chain, effectively
- # cancelling error (which we've now handled)
- return d
-
- txaio.add_callbacks(d, success, error)
-
- else:
- raise ProtocolError("Received {0} message, and session is not yet established".format(msg.__class__))
-
- else:
- # self._session_id != None (aka "session established")
- if isinstance(msg, message.Goodbye):
- if not self._goodbye_sent:
- # the peer wants to close: send GOODBYE reply
- reply = message.Goodbye()
- self._transport.send(reply)
-
- self._session_id = None
-
- # fire callback and close the transport
- details = types.CloseDetails(msg.reason, msg.message)
- d = txaio.as_future(self.onLeave, details)
-
- def success(arg):
- # XXX also: handle async
- self.fire('leave', self, details)
- return arg
-
- def _error(e):
- errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format(msg.reason, msg.message)
- return self._swallow_error(e, errmsg)
- txaio.add_callbacks(d, success, _error)
-
- elif isinstance(msg, message.Event):
-
- if msg.subscription in self._subscriptions:
-
- # fire all event handlers on subscription ..
- for subscription in self._subscriptions[msg.subscription]:
-
- handler = subscription.handler
- topic = msg.topic or subscription.topic
-
- if msg.enc_algo:
- # FIXME: behavior in error cases (no keyring, decrypt issues, URI mismatch, ..)
- if not self._payload_codec:
- self.log.warn("received encoded payload with enc_algo={enc_algo}, but no payload codec active - ignoring encoded payload!", enc_algo=msg.enc_algo)
- return
- else:
- try:
- encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
- decoded_topic, msg.args, msg.kwargs = self._payload_codec.decode(False, topic, encoded_payload)
- except Exception as e:
- self.log.warn("failed to decode application payload encoded with enc_algo={enc_algo}: {error}", error=e, enc_algo=msg.enc_algo)
- return
- else:
- if topic != decoded_topic:
- self.log.warn("envelope topic URI does not match encoded one")
- return
-
- invoke_args = (handler.obj,) if handler.obj else tuple()
- if msg.args:
- invoke_args = invoke_args + tuple(msg.args)
- invoke_kwargs = msg.kwargs if msg.kwargs else dict()
-
- if handler.details_arg:
- invoke_kwargs[handler.details_arg] = types.EventDetails(subscription, msg.publication, publisher=msg.publisher, publisher_authid=msg.publisher_authid, publisher_authrole=msg.publisher_authrole, topic=topic, retained=msg.retained, enc_algo=msg.enc_algo, forward_for=msg.forward_for)
-
- # FIXME: https://github.com/crossbario/autobahn-python/issues/764
- def _success(_):
- # Acknowledged Events -- only if we got the details header and
- # the broker advertised it
- if msg.x_acknowledged_delivery and self._router_roles["broker"].x_acknowledged_event_delivery:
- if self._transport:
- response = message.EventReceived(msg.publication)
- self._transport.send(response)
- else:
- self.log.warn("successfully processed event with acknowledged delivery, but could not send ACK, since the transport was lost in the meantime")
-
- def _error(e):
- errmsg = 'While firing {0} subscribed under {1}.'.format(
- handler.fn, msg.subscription)
- return self._swallow_error(e, errmsg)
-
- future = txaio.as_future(handler.fn, *invoke_args, **invoke_kwargs)
- txaio.add_callbacks(future, _success, _error)
-
- else:
- raise ProtocolError("EVENT received for non-subscribed subscription ID {0}".format(msg.subscription))
-
- elif isinstance(msg, message.Published):
-
- if msg.request in self._publish_reqs:
-
- # get and pop outstanding publish request
- publish_request = self._publish_reqs.pop(msg.request)
-
- # create a new publication object
- publication = Publication(msg.publication, was_encrypted=publish_request.was_encrypted)
-
- # resolve deferred/future for publishing successfully
- txaio.resolve(publish_request.on_reply, publication)
- else:
- raise ProtocolError("PUBLISHED received for non-pending request ID {0}".format(msg.request))
-
- elif isinstance(msg, message.Subscribed):
-
- if msg.request in self._subscribe_reqs:
-
- # get and pop outstanding subscribe request
- request = self._subscribe_reqs.pop(msg.request)
-
- # create new handler subscription list for subscription ID if not yet tracked
- if msg.subscription not in self._subscriptions:
- self._subscriptions[msg.subscription] = []
-
- subscription = Subscription(msg.subscription, request.topic, self, request.handler)
-
- # add handler to existing subscription
- self._subscriptions[msg.subscription].append(subscription)
-
- # resolve deferred/future for subscribing successfully
- txaio.resolve(request.on_reply, subscription)
- else:
- raise ProtocolError("SUBSCRIBED received for non-pending request ID {0}".format(msg.request))
-
- elif isinstance(msg, message.Unsubscribed):
-
- if msg.request in self._unsubscribe_reqs:
-
- # get and pop outstanding subscribe request
- request = self._unsubscribe_reqs.pop(msg.request)
-
- # if the subscription still exists, mark as inactive and remove ..
- if request.subscription_id in self._subscriptions:
- for subscription in self._subscriptions[request.subscription_id]:
- subscription.active = False
- del self._subscriptions[request.subscription_id]
-
- # resolve deferred/future for unsubscribing successfully
- txaio.resolve(request.on_reply, 0)
- else:
- raise ProtocolError("UNSUBSCRIBED received for non-pending request ID {0}".format(msg.request))
-
- elif isinstance(msg, message.Result):
-
- if msg.request in self._call_reqs:
-
- call_request = self._call_reqs[msg.request]
- proc = call_request.procedure
- enc_err = None
-
- if msg.enc_algo:
-
- if not self._payload_codec:
- log_msg = u"received encoded payload, but no payload codec active"
- self.log.warn(log_msg)
- enc_err = ApplicationError(ApplicationError.ENC_NO_PAYLOAD_CODEC, log_msg)
- else:
- try:
- encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
- decrypted_proc, msg.args, msg.kwargs = self._payload_codec.decode(True, proc, encoded_payload)
- except Exception as e:
- self.log.warn(
- "failed to decrypt application payload 1: {err}",
- err=e,
- )
- enc_err = ApplicationError(
- ApplicationError.ENC_DECRYPT_ERROR,
- u"failed to decrypt application payload 1: {}".format(e),
- )
- else:
- if proc != decrypted_proc:
- self.log.warn(
- "URI within encrypted payload ('{decrypted_proc}') does not match the envelope ('{proc}')",
- decrypted_proc=decrypted_proc,
- proc=proc,
- )
- enc_err = ApplicationError(
- ApplicationError.ENC_TRUSTED_URI_MISMATCH,
- u"URI within encrypted payload ('{}') does not match the envelope ('{}')".format(decrypted_proc, proc),
- )
-
- if msg.progress:
- # process progressive call result
-
- if call_request.options.on_progress:
- if enc_err:
- self.onUserError(enc_err, "could not deliver progressive call result, because payload decryption failed")
- else:
- kw = msg.kwargs or dict()
- args = msg.args or tuple()
-
- def _error(fail):
- self.onUserError(fail, "While firing on_progress")
-
- if call_request.options and call_request.options.details:
- prog_d = txaio.as_future(call_request.options.on_progress,
- types.CallResult(*msg.args,
- callee=msg.callee,
- callee_authid=msg.callee_authid,
- callee_authrole=msg.callee_authrole,
- forward_for=msg.forward_for,
- **msg.kwargs))
- else:
- prog_d = txaio.as_future(call_request.options.on_progress,
- *args,
- **kw)
-
- txaio.add_callbacks(prog_d, None, _error)
-
- else:
- # process final call result
-
- # drop original request
- del self._call_reqs[msg.request]
-
- # user callback that gets fired
- on_reply = call_request.on_reply
-
- # above might already have rejected, so we guard ..
- if enc_err:
- txaio.reject(on_reply, enc_err)
- else:
- if msg.kwargs or (call_request.options and call_request.options.details):
- kwargs = msg.kwargs or {}
- if msg.args:
- res = types.CallResult(*msg.args,
- callee=msg.callee,
- callee_authid=msg.callee_authid,
- callee_authrole=msg.callee_authrole,
- forward_for=msg.forward_for,
- **kwargs)
- else:
- res = types.CallResult(callee=msg.callee,
- callee_authid=msg.callee_authid,
- callee_authrole=msg.callee_authrole,
- forward_for=msg.forward_for,
- **kwargs)
- txaio.resolve(on_reply, res)
- else:
- if msg.args:
- if len(msg.args) > 1:
- res = types.CallResult(*msg.args)
- txaio.resolve(on_reply, res)
- else:
- txaio.resolve(on_reply, msg.args[0])
- else:
- txaio.resolve(on_reply, None)
- else:
- raise ProtocolError("RESULT received for non-pending request ID {0}".format(msg.request))
-
- elif isinstance(msg, message.Invocation):
-
- if msg.request in self._invocations:
-
- raise ProtocolError("INVOCATION received for request ID {0} already invoked".format(msg.request))
-
- else:
-
- if msg.registration not in self._registrations:
-
- raise ProtocolError("INVOCATION received for non-registered registration ID {0}".format(msg.registration))
-
- else:
- registration = self._registrations[msg.registration]
- endpoint = registration.endpoint
- proc = msg.procedure or registration.procedure
- enc_err = None
-
- if msg.enc_algo:
- if not self._payload_codec:
- log_msg = u"received encrypted INVOCATION payload, but no keyring active"
- self.log.warn(log_msg)
- enc_err = ApplicationError(ApplicationError.ENC_NO_PAYLOAD_CODEC, log_msg)
- else:
- try:
- encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
- decrypted_proc, msg.args, msg.kwargs = self._payload_codec.decode(False, proc, encoded_payload)
- except Exception as e:
- self.log.warn(
- "failed to decrypt INVOCATION payload: {err}",
- err=e,
- )
- enc_err = ApplicationError(
- ApplicationError.ENC_DECRYPT_ERROR,
- "failed to decrypt INVOCATION payload: {}".format(e),
- )
- else:
- if proc != decrypted_proc:
- self.log.warn(
- "URI within encrypted INVOCATION payload ('{decrypted_proc}') "
- "does not match the envelope ('{proc}')",
- decrypted_proc=decrypted_proc,
- proc=proc,
- )
- enc_err = ApplicationError(
- ApplicationError.ENC_TRUSTED_URI_MISMATCH,
- u"URI within encrypted INVOCATION payload ('{}') does not match the envelope ('{}')".format(decrypted_proc, proc),
- )
-
- if enc_err:
- # when there was a problem decrypting the INVOCATION payload, we obviously can't invoke
- # the endpoint, but return and
- reply = self._message_from_exception(message.Invocation.MESSAGE_TYPE, msg.request, enc_err)
- self._transport.send(reply)
-
- else:
-
- if endpoint.obj is not None:
- invoke_args = (endpoint.obj,)
- else:
- invoke_args = tuple()
-
- if msg.args:
- invoke_args = invoke_args + tuple(msg.args)
-
- invoke_kwargs = msg.kwargs if msg.kwargs else dict()
-
- if endpoint.details_arg:
-
- if msg.receive_progress:
-
- def progress(*args, **kwargs):
- assert(args is None or type(args) in (list, tuple))
- assert(kwargs is None or type(kwargs) == dict)
-
- if kwargs and six.PY2:
- kwargs = {
- k.decode('utf8'): v
- for k, v in kwargs.iteritems()
- }
-
- encoded_payload = None
- if msg.enc_algo:
- if not self._payload_codec:
- raise Exception(u"trying to send encrypted payload, but no keyring active")
- encoded_payload = self._payload_codec.encode(False, proc, args, kwargs)
-
- if encoded_payload:
- progress_msg = message.Yield(msg.request,
- payload=encoded_payload.payload,
- progress=True,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer)
- else:
- progress_msg = message.Yield(msg.request,
- args=args,
- kwargs=kwargs,
- progress=True)
-
- self._transport.send(progress_msg)
- else:
- progress = None
-
- invoke_kwargs[endpoint.details_arg] = types.CallDetails(registration,
- progress=progress,
- caller=msg.caller,
- caller_authid=msg.caller_authid,
- caller_authrole=msg.caller_authrole,
- procedure=proc,
- enc_algo=msg.enc_algo)
-
- on_reply = txaio.as_future(endpoint.fn, *invoke_args, **invoke_kwargs)
-
- def success(res):
- del self._invocations[msg.request]
-
- encoded_payload = None
- if msg.enc_algo:
- if not self._payload_codec:
- log_msg = u"trying to send encrypted payload, but no keyring active"
- self.log.warn(log_msg)
- else:
- try:
- if isinstance(res, types.CallResult):
- encoded_payload = self._payload_codec.encode(False, proc, res.results, res.kwresults)
- else:
- encoded_payload = self._payload_codec.encode(False, proc, [res])
- except Exception as e:
- self.log.warn(
- "failed to encrypt application payload: {err}",
- err=e,
- )
-
- if encoded_payload:
- if isinstance(res, types.CallResult):
- reply = message.Yield(msg.request,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer,
- callee=res.callee,
- callee_authid=res.callee_authid,
- callee_authrole=res.callee_authrole,
- forward_for=res.forward_for)
- else:
- reply = message.Yield(msg.request,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer)
- else:
- if isinstance(res, types.CallResult):
- reply = message.Yield(msg.request,
- args=res.results,
- kwargs=res.kwresults,
- callee=res.callee,
- callee_authid=res.callee_authid,
- callee_authrole=res.callee_authrole,
- forward_for=res.forward_for)
- else:
- reply = message.Yield(msg.request,
- args=[res])
-
- if self._transport is None:
- self.log.debug('Skipping result of "{}", request {} because transport disconnected.'.format(registration.procedure, msg.request))
- return
-
- try:
- self._transport.send(reply)
- except SerializationError as e:
- # the application-level payload returned from the invoked procedure can't be serialized
- reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD,
- args=[u'success return value from invoked procedure "{0}" could not be serialized: {1}'.format(registration.procedure, e)])
- self._transport.send(reply)
- except PayloadExceededError as e:
- # the application-level payload returned from the invoked procedure, when serialized and framed
- # for the transport, exceeds the transport message/frame size limit
- reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.PAYLOAD_SIZE_EXCEEDED,
- args=[u'success return value from invoked procedure "{0}" exceeds transport size limit: {1}'.format(registration.procedure, e)])
- self._transport.send(reply)
-
- def error(err):
- del self._invocations[msg.request]
-
- errmsg = txaio.failure_message(err)
-
- try:
- self.onUserError(err, errmsg)
- except:
- pass
-
- formatted_tb = None
- if self.traceback_app:
- formatted_tb = txaio.failure_format_traceback(err)
-
- reply = self._message_from_exception(
- message.Invocation.MESSAGE_TYPE,
- msg.request,
- err.value,
- formatted_tb,
- msg.enc_algo
- )
-
- try:
- self._transport.send(reply)
- except SerializationError as e:
- # the application-level payload returned from the invoked procedure can't be serialized
- reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD,
- args=[u'error return value from invoked procedure "{0}" could not be serialized: {1}'.format(registration.procedure, e)])
- self._transport.send(reply)
- except PayloadExceededError as e:
- # the application-level payload returned from the invoked procedure, when serialized and framed
- # for the transport, exceeds the transport message/frame size limit
- reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.PAYLOAD_SIZE_EXCEEDED,
- args=[u'success return value from invoked procedure "{0}" exceeds transport size limit: {1}'.format(registration.procedure, e)])
- self._transport.send(reply)
-
- # we have handled the error, so we eat it
- return None
-
- self._invocations[msg.request] = InvocationRequest(msg.request, on_reply)
-
- txaio.add_callbacks(on_reply, success, error)
-
- elif isinstance(msg, message.Interrupt):
-
- if msg.request not in self._invocations:
- # raise ProtocolError("INTERRUPT received for non-pending invocation {0}".format(msg.request))
- self.log.debug('INTERRUPT received for non-pending invocation {request}', request=msg.request)
- else:
- invoked = self._invocations[msg.request]
- # this will result in a CancelledError which will
- # be captured by the error handler around line 979
- # to delete the invocation..
- txaio.cancel(invoked.on_reply)
-
- elif isinstance(msg, message.Registered):
-
- if msg.request in self._register_reqs:
-
- # get and pop outstanding register request
- request = self._register_reqs.pop(msg.request)
-
- # create new registration if not yet tracked
- if msg.registration not in self._registrations:
- registration = Registration(self, msg.registration, request.procedure, request.endpoint)
- self._registrations[msg.registration] = registration
- else:
- raise ProtocolError("REGISTERED received for already existing registration ID {0}".format(msg.registration))
-
- txaio.resolve(request.on_reply, registration)
- else:
- raise ProtocolError("REGISTERED received for non-pending request ID {0}".format(msg.request))
-
- elif isinstance(msg, message.Unregistered):
-
- if msg.request == 0:
- # this is a forced un-register either from a call
- # to the wamp.* meta-api or the force_reregister
- # option
- try:
- reg = self._registrations[msg.registration]
- except KeyError:
- raise ProtocolError(
- "UNREGISTERED received for non-existant registration"
- " ID {0}".format(msg.registration)
- )
- self.log.info(
- u"Router unregistered procedure '{proc}' with ID {id}",
- proc=reg.procedure,
- id=msg.registration,
- )
- elif msg.request in self._unregister_reqs:
-
- # get and pop outstanding subscribe request
- request = self._unregister_reqs.pop(msg.request)
-
- # if the registration still exists, mark as inactive and remove ..
- if request.registration_id in self._registrations:
- self._registrations[request.registration_id].active = False
- del self._registrations[request.registration_id]
-
- # resolve deferred/future for unregistering successfully
- txaio.resolve(request.on_reply)
- else:
- raise ProtocolError("UNREGISTERED received for non-pending request ID {0}".format(msg.request))
-
- elif isinstance(msg, message.Error):
-
- # remove outstanding request and get the reply deferred/future
- on_reply = None
-
- # ERROR reply to CALL
- if msg.request_type == message.Call.MESSAGE_TYPE and msg.request in self._call_reqs:
- on_reply = self._call_reqs.pop(msg.request).on_reply
-
- # ERROR reply to PUBLISH
- elif msg.request_type == message.Publish.MESSAGE_TYPE and msg.request in self._publish_reqs:
- on_reply = self._publish_reqs.pop(msg.request).on_reply
-
- # ERROR reply to SUBSCRIBE
- elif msg.request_type == message.Subscribe.MESSAGE_TYPE and msg.request in self._subscribe_reqs:
- on_reply = self._subscribe_reqs.pop(msg.request).on_reply
-
- # ERROR reply to UNSUBSCRIBE
- elif msg.request_type == message.Unsubscribe.MESSAGE_TYPE and msg.request in self._unsubscribe_reqs:
- on_reply = self._unsubscribe_reqs.pop(msg.request).on_reply
-
- # ERROR reply to REGISTER
- elif msg.request_type == message.Register.MESSAGE_TYPE and msg.request in self._register_reqs:
- on_reply = self._register_reqs.pop(msg.request).on_reply
-
- # ERROR reply to UNREGISTER
- elif msg.request_type == message.Unregister.MESSAGE_TYPE and msg.request in self._unregister_reqs:
- on_reply = self._unregister_reqs.pop(msg.request).on_reply
-
- if on_reply:
- if not txaio.is_called(on_reply):
- txaio.reject(on_reply, self._exception_from_message(msg))
- else:
- raise ProtocolError("WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}".format(msg.request_type, msg.request))
-
- else:
-
- raise ProtocolError("Unexpected message {0}".format(msg.__class__))
-
- @public
- def onClose(self, wasClean):
- """
- Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onClose`
- """
- self._transport = None
-
- if self._session_id:
- # fire callback and close the transport
- details = types.CloseDetails(
- reason=types.CloseDetails.REASON_TRANSPORT_LOST,
- message=u'WAMP transport was lost without closing the session {} before'.format(self._session_id),
- )
- d = txaio.as_future(self.onLeave, details)
-
- def success(arg):
- # XXX also: handle async
- self.fire('leave', self, details)
- return arg
-
- def _error(e):
- return self._swallow_error(e, "While firing onLeave")
- txaio.add_callbacks(d, success, _error)
-
- self._session_id = None
-
- d = txaio.as_future(self.onDisconnect)
-
- def success(arg):
- # XXX do we care about returning 'arg' properly?
- return self.fire('disconnect', self, was_clean=wasClean)
-
- def _error(e):
- return self._swallow_error(e, "While firing onDisconnect")
- txaio.add_callbacks(d, success, _error)
-
- @public
- def onChallenge(self, challenge):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onChallenge`
- """
- raise Exception("received authentication challenge, but onChallenge not implemented")
-
- @public
- def onJoin(self, details):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onJoin`
- """
-
- @public
- def onWelcome(self, msg):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onWelcome`
- """
-
- def _errback_outstanding_requests(self, exc):
- """
- Errback any still outstanding requests with exc.
- """
- d = txaio.create_future_success(None)
- all_requests = [
- self._publish_reqs,
- self._subscribe_reqs,
- self._unsubscribe_reqs,
- self._call_reqs,
- self._register_reqs,
- self._unregister_reqs
- ]
- outstanding = []
- for requests in all_requests:
- outstanding.extend(requests.values())
- requests.clear()
-
- if outstanding:
- self.log.info(
- 'Cancelling {count} outstanding requests',
- count=len(outstanding),
- )
- for request in outstanding:
- self.log.debug(
- 'cleaning up outstanding {request_type} request {request_id}, '
- 'firing errback on user handler {request_on_reply}',
- request_on_reply=request.on_reply,
- request_id=request.request_id,
- request_type=request.__class__.__name__,
- )
- if not txaio.is_called(request.on_reply):
- txaio.reject(request.on_reply, exc)
-
- # wait for any async-ness in the error handlers for on_reply
- txaio.add_callbacks(d, lambda _: request.on_reply, lambda _: request.on_reply)
- return d
-
- @public
- def onLeave(self, details):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onLeave`
- """
- if details.reason != CloseDetails.REASON_DEFAULT:
- self.log.warn('session closed with reason {reason} [{message}]', reason=details.reason, message=details.message)
-
- # fire ApplicationError on any currently outstanding requests
- exc = ApplicationError(details.reason, details.message)
- d = self._errback_outstanding_requests(exc)
-
- def disconnect(_):
- if self._transport:
- self.disconnect()
- txaio.add_callbacks(d, disconnect, disconnect)
- return d
-
- @public
- def leave(self, reason=None, message=None):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.leave`
- """
- if not self._session_id:
- raise SessionNotReady(u"session hasn't joined a realm")
-
- if not self._goodbye_sent:
- if not reason:
- reason = u"wamp.close.normal"
- msg = wamp.message.Goodbye(reason=reason, message=message)
- self._transport.send(msg)
- self._goodbye_sent = True
- else:
- self.log.warn('session was already requested to leave - not sending GOODBYE again')
-
- is_closed = self._transport is None or self._transport.is_closed
-
- return is_closed
-
- @public
- def onDisconnect(self):
- """
- Implements :func:`autobahn.wamp.interfaces.ISession.onDisconnect`
- """
- # fire TransportLost on any _still_ outstanding requests
- # (these should have been already cleaned up in onLeave() - when
- # this actually has fired)
- exc = exception.TransportLost()
- self._errback_outstanding_requests(exc)
-
- @public
- def publish(self, topic, *args, **kwargs):
- """
- Implements :func:`autobahn.wamp.interfaces.IPublisher.publish`
- """
- assert(type(topic) == six.text_type)
- assert(args is None or type(args) in (list, tuple))
- assert(kwargs is None or type(kwargs) == dict)
-
- message.check_or_raise_uri(topic,
- message='{}.publish()'.format(self.__class__.__name__),
- strict=False,
- allow_empty_components=False,
- allow_none=False)
-
- options = kwargs.pop('options', None)
- if options and not isinstance(options, types.PublishOptions):
- raise Exception("options must be of type a.w.t.PublishOptions")
-
- if kwargs and six.PY2:
- kwargs = {
- k.decode('utf8'): v
- for k, v in kwargs.iteritems()
- }
-
- if not self._transport:
- raise exception.TransportLost()
-
- request_id = self._request_id_gen.next()
-
- encoded_payload = None
- if self._payload_codec:
- encoded_payload = self._payload_codec.encode(True, topic, args, kwargs)
-
- if encoded_payload:
- if options:
- msg = message.Publish(request_id,
- topic,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer,
- **options.message_attr())
- else:
- msg = message.Publish(request_id,
- topic,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer)
- else:
- if options:
- msg = message.Publish(request_id,
- topic,
- args=args,
- kwargs=kwargs,
- **options.message_attr())
- else:
- msg = message.Publish(request_id,
- topic,
- args=args,
- kwargs=kwargs)
-
- if options:
- if options.correlation_id is not None:
- msg.correlation_id = options.correlation_id
- if options.correlation_uri is not None:
- msg.correlation_uri = options.correlation_uri
- if options.correlation_is_anchor is not None:
- msg.correlation_is_anchor = options.correlation_is_anchor
- if options.correlation_is_last is not None:
- msg.correlation_is_last = options.correlation_is_last
-
- if options and options.acknowledge:
- # only acknowledged publications expect a reply ..
- on_reply = txaio.create_future()
- self._publish_reqs[request_id] = PublishRequest(request_id, on_reply, was_encrypted=(encoded_payload is not None))
- else:
- on_reply = None
-
- try:
- # Notes:
- #
- # * this might raise autobahn.wamp.exception.SerializationError
- # when the user payload cannot be serialized
- # * we have to setup a PublishRequest() in _publish_reqs _before_
- # calling transpor.send(), because a mock- or side-by-side transport
- # will immediately lead on an incoming WAMP message in onMessage()
- #
- self._transport.send(msg)
- except Exception as e:
- if request_id in self._publish_reqs:
- del self._publish_reqs[request_id]
- raise e
-
- return on_reply
-
- @public
- def subscribe(self, handler, topic=None, options=None):
- """
- Implements :func:`autobahn.wamp.interfaces.ISubscriber.subscribe`
- """
- assert((callable(handler) and topic is not None) or hasattr(handler, '__class__'))
- assert(topic is None or type(topic) == six.text_type)
- assert(options is None or isinstance(options, types.SubscribeOptions))
-
- if not self._transport:
- raise exception.TransportLost()
-
- def _subscribe(obj, fn, topic, options):
- message.check_or_raise_uri(topic,
- message='{}.subscribe()'.format(self.__class__.__name__),
- strict=False,
- allow_empty_components=True,
- allow_none=False)
-
- request_id = self._request_id_gen.next()
- on_reply = txaio.create_future()
- handler_obj = Handler(fn, obj, options.details_arg if options else None)
- self._subscribe_reqs[request_id] = SubscribeRequest(request_id, topic, on_reply, handler_obj)
-
- if options:
- msg = message.Subscribe(request_id, topic, **options.message_attr())
- else:
- msg = message.Subscribe(request_id, topic)
-
- if options:
- if options.correlation_id is not None:
- msg.correlation_id = options.correlation_id
- if options.correlation_uri is not None:
- msg.correlation_uri = options.correlation_uri
- if options.correlation_is_anchor is not None:
- msg.correlation_is_anchor = options.correlation_is_anchor
- if options.correlation_is_last is not None:
- msg.correlation_is_last = options.correlation_is_last
-
- self._transport.send(msg)
- return on_reply
-
- if callable(handler):
- # subscribe a single handler
- return _subscribe(None, handler, topic, options)
-
- else:
-
- # subscribe all methods on an object decorated with "wamp.subscribe"
- on_replies = []
- for k in inspect.getmembers(handler.__class__, is_method_or_function):
- proc = k[1]
- if "_wampuris" in proc.__dict__:
- for pat in proc.__dict__["_wampuris"]:
- if pat.is_handler():
- _uri = pat.uri()
- subopts = pat.options or options
- if subopts is None:
- if pat.uri_type == uri.Pattern.URI_TYPE_WILDCARD:
- subopts = types.SubscribeOptions(match=u"wildcard")
- else:
- subopts = types.SubscribeOptions(match=u"exact")
- on_replies.append(_subscribe(handler, proc, _uri, subopts))
-
- # XXX needs coverage
- return txaio.gather(on_replies, consume_exceptions=True)
-
- def _unsubscribe(self, subscription):
- """
- Called from :meth:`autobahn.wamp.protocol.Subscription.unsubscribe`
- """
- assert(isinstance(subscription, Subscription))
- assert subscription.active
- assert(subscription.id in self._subscriptions)
- assert(subscription in self._subscriptions[subscription.id])
-
- if not self._transport:
- raise exception.TransportLost()
-
- # remove handler subscription and mark as inactive
- self._subscriptions[subscription.id].remove(subscription)
- subscription.active = False
-
- # number of handler subscriptions left ..
- scount = len(self._subscriptions[subscription.id])
-
- if scount == 0:
- # if the last handler was removed, unsubscribe from broker ..
- request_id = self._request_id_gen.next()
-
- on_reply = txaio.create_future()
- self._unsubscribe_reqs[request_id] = UnsubscribeRequest(request_id, on_reply, subscription.id)
-
- msg = message.Unsubscribe(request_id, subscription.id)
-
- self._transport.send(msg)
- return on_reply
- else:
- # there are still handlers active on the subscription!
- return txaio.create_future_success(scount)
-
- @public
- def call(self, procedure, *args, **kwargs):
- """
- Implements :func:`autobahn.wamp.interfaces.ICaller.call`
- """
- assert(type(procedure) == six.text_type)
- assert(args is None or type(args) in (list, tuple))
- assert(kwargs is None or type(kwargs) == dict)
-
- message.check_or_raise_uri(procedure,
- message='{}.call()'.format(self.__class__.__name__),
- strict=False,
- allow_empty_components=False,
- allow_none=False)
-
- options = kwargs.pop('options', None)
- if options and not isinstance(options, types.CallOptions):
- raise Exception("options must be of type a.w.t.CallOptions")
-
- if kwargs and six.PY2:
- kwargs = {
- k.decode('utf8'): v
- for k, v in kwargs.iteritems()
- }
-
- if not self._transport:
- raise exception.TransportLost()
-
- request_id = self._request_id_gen.next()
-
- encoded_payload = None
- if self._payload_codec:
- try:
- encoded_payload = self._payload_codec.encode(True, procedure, args, kwargs)
- except:
- self.log.failure()
- raise
-
- if encoded_payload:
- if options:
- msg = message.Call(request_id,
- procedure,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer,
- **options.message_attr())
- else:
- msg = message.Call(request_id,
- procedure,
- payload=encoded_payload.payload,
- enc_algo=encoded_payload.enc_algo,
- enc_key=encoded_payload.enc_key,
- enc_serializer=encoded_payload.enc_serializer)
- else:
- if options:
- msg = message.Call(request_id,
- procedure,
- args=args,
- kwargs=kwargs,
- **options.message_attr())
- else:
- msg = message.Call(request_id,
- procedure,
- args=args,
- kwargs=kwargs)
-
- if options:
- if options.correlation_id is not None:
- msg.correlation_id = options.correlation_id
- if options.correlation_uri is not None:
- msg.correlation_uri = options.correlation_uri
- if options.correlation_is_anchor is not None:
- msg.correlation_is_anchor = options.correlation_is_anchor
- if options.correlation_is_last is not None:
- msg.correlation_is_last = options.correlation_is_last
-
- def canceller(d):
- cancel_msg = message.Cancel(request_id)
- self._transport.send(cancel_msg)
- # since we announced support for cancelling, we should
- # definitely get an Error back for our Cancel which will
- # clean up this invocation
-
- on_reply = txaio.create_future(canceller=canceller)
- self._call_reqs[request_id] = CallRequest(request_id, procedure, on_reply, options)
-
- try:
- # Notes:
- #
- # * this might raise autobahn.wamp.exception.SerializationError
- # when the user payload cannot be serialized
- # * we have to setup a CallRequest() in _call_reqs _before_
- # calling transpor.send(), because a mock- or side-by-side transport
- # will immediately lead on an incoming WAMP message in onMessage()
- #
- self._transport.send(msg)
- except:
- if request_id in self._call_reqs:
- del self._call_reqs[request_id]
- raise
-
- return on_reply
-
- @public
- def register(self, endpoint, procedure=None, options=None, prefix=None):
- """
- Implements :func:`autobahn.wamp.interfaces.ICallee.register`
- """
- assert((callable(endpoint) and procedure is not None) or hasattr(endpoint, '__class__'))
- assert(procedure is None or type(procedure) == six.text_type)
- assert(options is None or isinstance(options, types.RegisterOptions))
- assert prefix is None or isinstance(prefix, six.text_type)
-
- if not self._transport:
- raise exception.TransportLost()
-
- def _register(obj, fn, procedure, options):
- message.check_or_raise_uri(procedure,
- message='{}.register()'.format(self.__class__.__name__),
- strict=False,
- allow_empty_components=True,
- allow_none=False)
-
- request_id = self._request_id_gen.next()
- on_reply = txaio.create_future()
- endpoint_obj = Endpoint(fn, obj, options.details_arg if options else None)
- if prefix is not None:
- procedure = u"{}{}".format(prefix, procedure)
- self._register_reqs[request_id] = RegisterRequest(request_id, on_reply, procedure, endpoint_obj)
-
- if options:
- msg = message.Register(request_id, procedure, **options.message_attr())
- else:
- msg = message.Register(request_id, procedure)
-
- if options:
- if options.correlation_id is not None:
- msg.correlation_id = options.correlation_id
- if options.correlation_uri is not None:
- msg.correlation_uri = options.correlation_uri
- if options.correlation_is_anchor is not None:
- msg.correlation_is_anchor = options.correlation_is_anchor
- if options.correlation_is_last is not None:
- msg.correlation_is_last = options.correlation_is_last
-
- self._transport.send(msg)
- return on_reply
-
- if callable(endpoint):
-
- # register a single callable
- return _register(None, endpoint, procedure, options)
-
- else:
-
- # register all methods on an object decorated with "wamp.register"
- on_replies = []
- for k in inspect.getmembers(endpoint.__class__, is_method_or_function):
- proc = k[1]
- if "_wampuris" in proc.__dict__:
- for pat in proc.__dict__["_wampuris"]:
- if pat.is_endpoint():
- _uri = pat.uri()
- regopts = pat.options or options
- on_replies.append(_register(endpoint, proc, _uri, regopts))
-
- # XXX needs coverage
- return txaio.gather(on_replies, consume_exceptions=True)
-
- def _unregister(self, registration):
- """
- Called from :meth:`autobahn.wamp.protocol.Registration.unregister`
- """
- assert(isinstance(registration, Registration))
- assert registration.active
- assert(registration.id in self._registrations)
-
- if not self._transport:
- raise exception.TransportLost()
-
- request_id = self._request_id_gen.next()
-
- on_reply = txaio.create_future()
- self._unregister_reqs[request_id] = UnregisterRequest(request_id, on_reply, registration.id)
-
- msg = message.Unregister(request_id, registration.id)
-
- self._transport.send(msg)
- return on_reply
-
-
- class _SessionShim(ApplicationSession):
- """
- shim that lets us present pep8 API for user-classes to override,
- but also backwards-compatible for existing code using
- ApplicationSession "directly".
-
- **NOTE:** this is not public or intended for use; you should import
- either :class:`autobahn.asyncio.wamp.Session` or
- :class:`autobahn.twisted.wamp.Session` depending on which async
- framework you're using.
- """
-
- #: name -> IAuthenticator
- _authenticators = None
-
- def onJoin(self, details):
- return self.on_join(details)
-
- def onConnect(self):
- if self._authenticators:
- # authid, authrole *must* match across all authenticators
- # (checked in add_authenticator) so these lists are either
- # [None] or [None, 'some_authid']
- authid = [x._args.get('authid', None) for x in self._authenticators.values()][-1]
- authrole = [x._args.get('authrole', None) for x in self._authenticators.values()][-1]
- # we need a "merged" authextra here because we can send a
- # list of acceptable authmethods, but only a single
- # authextra dict
- authextra = self._merged_authextra()
- self.join(
- self.config.realm,
- authmethods=list(self._authenticators.keys()),
- authid=authid or u'public',
- authrole=authrole or u'default',
- authextra=authextra,
- )
- else:
- self.on_connect()
-
- def onChallenge(self, challenge):
- try:
- authenticator = self._authenticators[challenge.method]
- except KeyError:
- raise RuntimeError(
- "Received challenge for unknown authmethod '{}'".format(
- challenge.method
- )
- )
- return authenticator.on_challenge(self, challenge)
-
- def onWelcome(self, msg):
- if msg.authmethod is None or self._authenticators is None:
- # no authentication
- return
- try:
- authenticator = self._authenticators[msg.authmethod]
- except KeyError:
- raise RuntimeError(
- "Received onWelcome for unknown authmethod '{}'".format(
- msg.authmethod
- )
- )
- return authenticator.on_welcome(self, msg.authextra)
-
- def onLeave(self, details):
- return self.on_leave(details)
-
- def onDisconnect(self):
- return self.on_disconnect()
-
- # experimental authentication API
-
- def add_authenticator(self, authenticator):
- assert isinstance(authenticator, IAuthenticator)
- if self._authenticators is None:
- self._authenticators = {}
-
- # before adding this authenticator we need to validate that
- # it's consistent with any other authenticators we may have --
- # for example, they must all agree on "authid" etc because
- # .join() only takes one value for all of those.
-
- def at_most_one(name):
- uni = set([
- a._args[name]
- for a in list(self._authenticators.values()) + [authenticator]
- if name in a._args
- ])
- if len(uni) > 1:
- raise ValueError(
- "Inconsistent {}s: {}".format(
- name,
- ' '.join(uni),
- )
- )
-
- # all authids must match
- at_most_one('authid')
-
- # all authroles must match
- at_most_one('authrole')
-
- # can we do anything else other than merge all authextra keys?
- # here we check that any duplicate keys have the same values
- authextra = authenticator.authextra
- merged = self._merged_authextra()
- for k, v in merged.items():
- if k in authextra and authextra[k] != v:
- raise ValueError(
- "Inconsistent authextra values for '{}': '{}' vs '{}'".format(
- k, v, authextra[k],
- )
- )
-
- # validation complete, add it
- self._authenticators[authenticator.name] = authenticator
-
- def _merged_authextra(self):
- """
- internal helper
-
- :returns: a single 'authextra' dict, consisting of all keys
- from any authenticator's authextra.
-
- Note that when the authenticator was added, we already checked
- that any keys it does contain has the same value as any
- existing authextra.
- """
- authextras = [a.authextra for a in self._authenticators.values()]
-
- def extract_keys(x, y):
- return x | set(y.keys())
-
- unique_keys = reduce(extract_keys, authextras, set())
-
- def first_value_for(k):
- """
- for anything already in self._authenticators, we checked
- that it has the same value for any keys in its authextra --
- so here we just extract the first one
- """
- for authextra in authextras:
- if k in authextra:
- return authextra[k]
- # "can't" happen
- raise ValueError(
- "No values for '{}'".format(k)
- )
-
- return {
- k: first_value_for(k)
- for k in unique_keys
- }
-
- # these are the actual "new API" methods (i.e. snake_case)
- #
-
- def on_join(self, details):
- pass
-
- def on_leave(self, details):
- self.disconnect()
-
- def on_connect(self):
- self.join(self.config.realm)
-
- def on_disconnect(self):
- pass
-
-
- # ISession.register collides with the abc.ABCMeta.register method
- # ISession.register(ApplicationSession)
-
-
- class ApplicationSessionFactory(object):
- """
- WAMP endpoint session factory.
- """
-
- session = ApplicationSession
- """
- WAMP application session class to be used in this factory.
- """
-
- def __init__(self, config=None):
- """
-
- :param config: The default component configuration.
- :type config: instance of :class:`autobahn.wamp.types.ComponentConfig`
- """
- self.config = config or types.ComponentConfig(realm=u"realm1")
-
- def __call__(self):
- """
- Creates a new WAMP application session.
-
- :returns: -- An instance of the WAMP application session class as
- given by `self.session`.
- """
- session = self.session(self.config)
- session.factory = self
- return session
|