Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

protocol.py 90KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047
  1. ###############################################################################
  2. #
  3. # The MIT License (MIT)
  4. #
  5. # Copyright (c) typedef int GmbH
  6. #
  7. # Permission is hereby granted, free of charge, to any person obtaining a copy
  8. # of this software and associated documentation files (the "Software"), to deal
  9. # in the Software without restriction, including without limitation the rights
  10. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. # copies of the Software, and to permit persons to whom the Software is
  12. # furnished to do so, subject to the following conditions:
  13. #
  14. # The above copyright notice and this permission notice shall be included in
  15. # all copies or substantial portions of the Software.
  16. #
  17. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. # THE SOFTWARE.
  24. #
  25. ###############################################################################
  26. from typing import Union, Optional, Dict, Any, ClassVar, List, Callable
  27. import txaio
  28. import inspect
  29. from functools import reduce
  30. from autobahn import wamp
  31. from autobahn.util import public, IdGenerator, ObservableMixin
  32. from autobahn.wamp import uri
  33. from autobahn.wamp import message
  34. from autobahn.wamp import types
  35. from autobahn.wamp import role
  36. from autobahn.wamp import exception
  37. from autobahn.wamp.exception import ApplicationError, ProtocolError, SerializationError, TypeCheckError
  38. from autobahn.wamp.interfaces import ISession, IPayloadCodec, IAuthenticator, ITransport, IMessage # noqa
  39. from autobahn.wamp.types import Challenge, SessionDetails, CloseDetails, EncodedPayload, SubscribeOptions, \
  40. CallResult, RegisterOptions
  41. from autobahn.exception import PayloadExceededError
  42. from autobahn.wamp.request import \
  43. Publication, \
  44. Subscription, \
  45. Handler, \
  46. Registration, \
  47. Endpoint, \
  48. PublishRequest, \
  49. SubscribeRequest, \
  50. UnsubscribeRequest, \
  51. CallRequest, \
  52. InvocationRequest, \
  53. RegisterRequest, \
  54. UnregisterRequest
  55. def is_method_or_function(f):
  56. return inspect.ismethod(f) or inspect.isfunction(f)
  57. class BaseSession(ObservableMixin):
  58. """
  59. WAMP session base class.
  60. This class implements :class:`autobahn.wamp.interfaces.ISession`.
  61. """
  62. log = None
  63. def __init__(self):
  64. self.log = txaio.make_logger()
  65. self.set_valid_events(
  66. valid_events=[
  67. 'join', # right before onJoin runs
  68. 'leave', # after onLeave has run
  69. 'ready', # after onJoin and all 'join' listeners have completed
  70. 'connect', # right before onConnect
  71. 'disconnect', # right after onDisconnect
  72. ]
  73. )
  74. # this is for marshalling traceback from exceptions thrown in user
  75. # code within WAMP error messages (that is, when invoking remoted
  76. # procedures)
  77. self.traceback_app = False
  78. # mapping of exception classes to list of WAMP error URI patterns
  79. self._ecls_to_uri_pat: Dict[ClassVar, List[uri.Pattern]] = {}
  80. # mapping of WAMP error URIs to exception classes
  81. self._uri_to_ecls: Dict[str, ClassVar] = {
  82. ApplicationError.INVALID_PAYLOAD: SerializationError,
  83. ApplicationError.PAYLOAD_SIZE_EXCEEDED: PayloadExceededError,
  84. }
  85. # WAMP ITransport (_not_ a Twisted protocol, which is self.transport - when using Twisted)
  86. self._transport: Optional[ITransport] = None
  87. # session authentication information
  88. self._realm: Optional[str] = None
  89. self._session_id: Optional[int] = None
  90. self._authid: Optional[str] = None
  91. self._authrole: Optional[str] = None
  92. self._authmethod: Optional[str] = None
  93. self._authprovider: Optional[str] = None
  94. self._authextra: Optional[Dict[str, Any]] = None
  95. # set client role features supported and announced
  96. self._session_roles: Dict[str, role.RoleFeatures] = role.DEFAULT_CLIENT_ROLES
  97. # complete session details
  98. self._session_details: Optional[SessionDetails] = None
  99. # payload transparency codec
  100. self._payload_codec: Optional[IPayloadCodec] = None
  101. # generator for WAMP request IDs
  102. self._request_id_gen = IdGenerator()
  103. @property
  104. def transport(self) -> Optional[ITransport]:
  105. """
  106. Implements :func:`autobahn.wamp.interfaces.ITransportHandler.transport`
  107. """
  108. # Note: self._transport (which is a WAMP ITransport) is different from self.transport (which
  109. # is a Twisted protocol when using Twisted)!
  110. return self._transport
  111. @public
  112. def is_connected(self) -> bool:
  113. """
  114. Implements :func:`autobahn.wamp.interfaces.ISession.is_connected`
  115. """
  116. return self._transport is not None and self._transport.isOpen()
  117. @public
  118. def is_attached(self) -> bool:
  119. """
  120. Implements :func:`autobahn.wamp.interfaces.ISession.is_attached`
  121. """
  122. return self._session_id is not None and self.is_connected()
  123. @property
  124. def session_details(self) -> Optional[SessionDetails]:
  125. """
  126. Implements :func:`autobahn.wamp.interfaces.ISession.session_details`
  127. """
  128. return self._session_details
  129. @property
  130. def realm(self) -> Optional[str]:
  131. return self._realm
  132. @property
  133. def session_id(self) -> Optional[int]:
  134. return self._session_id
  135. @property
  136. def authid(self) -> Optional[str]:
  137. return self._authid
  138. @property
  139. def authrole(self) -> Optional[str]:
  140. return self._authrole
  141. @property
  142. def authmethod(self) -> Optional[str]:
  143. return self._authmethod
  144. @property
  145. def authprovider(self) -> Optional[str]:
  146. return self._authprovider
  147. @property
  148. def authextra(self) -> Optional[Dict[str, Any]]:
  149. return self._authextra
  150. def define(self, exception: Exception, error: Optional[str] = None):
  151. """
  152. Implements :func:`autobahn.wamp.interfaces.ISession.define`
  153. """
  154. if error is None:
  155. if hasattr(exception, '_wampuris'):
  156. self._ecls_to_uri_pat[exception] = exception._wampuris
  157. self._uri_to_ecls[exception._wampuris[0].uri()] = exception
  158. else:
  159. raise RuntimeError('cannot define WAMP exception from class with no decoration ("_wampuris" unset)')
  160. else:
  161. if not hasattr(exception, '_wampuris'):
  162. self._ecls_to_uri_pat[exception] = [uri.Pattern(error, uri.Pattern.URI_TARGET_HANDLER)]
  163. self._uri_to_ecls[error] = exception
  164. else:
  165. raise RuntimeError('cannot define WAMP exception: error URI is explicit, but class is decorated ("_wampuris" set')
  166. def _message_from_exception(self, request_type, request, exc, tb=None, enc_algo=None):
  167. """
  168. Create a WAMP error message from an exception.
  169. :param request_type: The request type this WAMP error message is for.
  170. :type request_type: int
  171. :param request: The request ID this WAMP error message is for.
  172. :type request: int
  173. :param exc: The exception.
  174. :type exc: Instance of :class:`Exception` or subclass thereof.
  175. :param tb: Optional traceback. If present, it'll be included with the WAMP error message.
  176. :type tb: list or None
  177. """
  178. args = None
  179. if hasattr(exc, 'args'):
  180. args = list(exc.args) # make sure tuples are made into lists
  181. kwargs = None
  182. if hasattr(exc, 'kwargs'):
  183. kwargs = exc.kwargs
  184. if tb:
  185. if kwargs:
  186. kwargs['traceback'] = tb
  187. else:
  188. kwargs = {'traceback': tb}
  189. if isinstance(exc, exception.ApplicationError):
  190. error = exc.error if type(exc.error) == str else exc.error
  191. else:
  192. if exc.__class__ in self._ecls_to_uri_pat:
  193. error = self._ecls_to_uri_pat[exc.__class__][0]._uri
  194. else:
  195. error = "wamp.error.runtime_error"
  196. encoded_payload = None
  197. if self._payload_codec:
  198. encoded_payload = self._payload_codec.encode(False, error, args, kwargs)
  199. if encoded_payload:
  200. msg = message.Error(request_type,
  201. request,
  202. error,
  203. payload=encoded_payload.payload,
  204. enc_algo=encoded_payload.enc_algo,
  205. enc_key=encoded_payload.enc_key,
  206. enc_serializer=encoded_payload.enc_serializer)
  207. else:
  208. msg = message.Error(request_type,
  209. request,
  210. error,
  211. args,
  212. kwargs)
  213. return msg
  214. def _exception_from_message(self, msg):
  215. """
  216. Create a user (or generic) exception from a WAMP error message.
  217. :param msg: A WAMP error message.
  218. :type msg: instance of :class:`autobahn.wamp.message.Error`
  219. """
  220. # FIXME:
  221. # 1. map to ecls based on error URI wildcard/prefix
  222. # 2. extract additional args/kwargs from error URI
  223. exc = None
  224. enc_err = None
  225. if msg.enc_algo:
  226. if not self._payload_codec:
  227. log_msg = "received encoded payload, but no payload codec active"
  228. self.log.warn(log_msg)
  229. enc_err = ApplicationError(ApplicationError.ENC_NO_PAYLOAD_CODEC, log_msg, enc_algo=msg.enc_algo)
  230. else:
  231. try:
  232. encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
  233. decrypted_error, msg.args, msg.kwargs = self._payload_codec.decode(True, msg.error, encoded_payload)
  234. except Exception as e:
  235. self.log.warn("failed to decrypt application payload 1: {err}", err=e)
  236. enc_err = ApplicationError(
  237. ApplicationError.ENC_DECRYPT_ERROR,
  238. "failed to decrypt application payload 1: {}".format(e),
  239. enc_algo=msg.enc_algo,
  240. )
  241. else:
  242. if msg.error != decrypted_error:
  243. self.log.warn(
  244. "URI within encrypted payload ('{decrypted_error}') does not match the envelope ('{error}')",
  245. decrypted_error=decrypted_error,
  246. error=msg.error,
  247. )
  248. enc_err = ApplicationError(
  249. ApplicationError.ENC_TRUSTED_URI_MISMATCH,
  250. "URI within encrypted payload ('{}') does not match the envelope ('{}')".format(decrypted_error, msg.error),
  251. enc_algo=msg.enc_algo,
  252. )
  253. if enc_err:
  254. return enc_err
  255. if msg.error in self._uri_to_ecls:
  256. ecls = self._uri_to_ecls[msg.error]
  257. try:
  258. # the following might fail, eg. TypeError when
  259. # signature of exception constructor is incompatible
  260. # with args/kwargs or when the exception constructor raises
  261. if msg.kwargs:
  262. if msg.args:
  263. exc = ecls(*msg.args, **msg.kwargs)
  264. else:
  265. exc = ecls(**msg.kwargs)
  266. else:
  267. if msg.args:
  268. exc = ecls(*msg.args)
  269. else:
  270. exc = ecls()
  271. except Exception:
  272. try:
  273. self.onUserError(
  274. txaio.create_failure(),
  275. "While re-constructing exception",
  276. )
  277. except:
  278. pass
  279. if not exc:
  280. # the following ctor never fails ..
  281. if msg.kwargs:
  282. if msg.args:
  283. exc = exception.ApplicationError(msg.error, *msg.args, **msg.kwargs)
  284. else:
  285. exc = exception.ApplicationError(msg.error, **msg.kwargs)
  286. else:
  287. if msg.args:
  288. exc = exception.ApplicationError(msg.error, *msg.args)
  289. else:
  290. exc = exception.ApplicationError(msg.error)
  291. # FIXME: cleanup and integate into ctors above
  292. if hasattr(exc, 'enc_algo'):
  293. exc.enc_algo = msg.enc_algo
  294. if hasattr(exc, 'callee'):
  295. exc.callee = msg.callee
  296. if hasattr(exc, 'callee_authid'):
  297. exc.callee_authid = msg.callee_authid
  298. if hasattr(exc, 'callee_authrole'):
  299. exc.callee_authrole = msg.callee_authrole
  300. if hasattr(exc, 'forward_for'):
  301. exc.forward_for = msg.forward_for
  302. return exc
  303. @public
  304. class ApplicationSession(BaseSession):
  305. """
  306. WAMP application session for applications (networking framework agnostic parts).
  307. Implements (partially):
  308. * :class:`autobahn.wamp.interfaces.ITransportHandler`
  309. * :class:`autobahn.wamp.interfaces.ISession`
  310. """
  311. def __init__(self, config: Optional[types.ComponentConfig] = None):
  312. """
  313. Implements :func:`autobahn.wamp.interfaces.ISession`
  314. """
  315. BaseSession.__init__(self)
  316. self._config: types.ComponentConfig = config or types.ComponentConfig(realm="realm1")
  317. # for keeping transport-closing state
  318. self._goodbye_sent: bool = False
  319. self._transport_is_closing: bool = False
  320. # WAMP application payload codec (e.g. for WAMP-cryptobox E2E)
  321. self._payload_codec: Optional[IPayloadCodec] = None
  322. # outstanding requests
  323. self._publish_reqs = {}
  324. self._subscribe_reqs = {}
  325. self._unsubscribe_reqs = {}
  326. self._call_reqs = {}
  327. self._register_reqs = {}
  328. self._unregister_reqs = {}
  329. # subscriptions in place
  330. self._subscriptions = {}
  331. # registrations in place
  332. self._registrations = {}
  333. # incoming invocations
  334. self._invocations = {}
  335. @property
  336. def config(self) -> types.ComponentConfig:
  337. return self._config
  338. @public
  339. def set_payload_codec(self, payload_codec: Optional[IPayloadCodec]):
  340. """
  341. Implements :func:`autobahn.wamp.interfaces.ISession.set_payload_codec`
  342. """
  343. self._payload_codec = payload_codec
  344. @public
  345. def get_payload_codec(self) -> Optional[IPayloadCodec]:
  346. """
  347. Implements :func:`autobahn.wamp.interfaces.ISession.get_payload_codec`
  348. """
  349. return self._payload_codec
  350. @public
  351. def onOpen(self, transport: ITransport):
  352. """
  353. Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onOpen`
  354. """
  355. self.log.debug('{func}(transport={transport})', func=self.onOpen, transport=transport)
  356. # The WAMP transport (e.g. WebSocket connection)
  357. self._transport = transport
  358. # FIXME: the observer API gets "transport" as argument, but _not_ the onConnect callback below?
  359. d = self.fire('connect', self, transport)
  360. txaio.add_callbacks(
  361. d, None,
  362. lambda fail: self._swallow_error(fail, "While notifying 'connect'")
  363. )
  364. txaio.add_callbacks(
  365. d,
  366. lambda _: txaio.as_future(self.onConnect),
  367. lambda fail: self._swallow_error(fail, "While calling 'onConnect'")
  368. )
  369. @public
  370. def onConnect(self):
  371. """
  372. Implements :func:`autobahn.wamp.interfaces.ISession.onConnect`
  373. """
  374. self.log.debug('{func}()', func=self.onConnect)
  375. self.join(self.config.realm)
  376. @public
  377. def join(self,
  378. realm: str,
  379. authmethods: Optional[List[str]] = None,
  380. authid: Optional[str] = None,
  381. authrole: Optional[str] = None,
  382. authextra: Optional[Dict[str, Any]] = None,
  383. resumable: Optional[bool] = None,
  384. resume_session: Optional[int] = None,
  385. resume_token: Optional[str] = None):
  386. """
  387. Implements :func:`autobahn.wamp.interfaces.ISession.join`
  388. """
  389. if self._session_id:
  390. raise Exception("session already joined")
  391. if not self._transport:
  392. raise Exception("no transport set for session")
  393. # store the realm requested by client, though this might be overwritten later,
  394. # when realm redirection kicks in
  395. self._realm = realm
  396. # closing handshake state
  397. self._goodbye_sent = False
  398. # send HELLO message to router
  399. msg = message.Hello(realm=realm,
  400. roles=self._session_roles,
  401. authmethods=authmethods,
  402. authid=authid,
  403. authrole=authrole,
  404. authextra=authextra,
  405. resumable=resumable,
  406. resume_session=resume_session,
  407. resume_token=resume_token)
  408. self._transport.send(msg)
  409. @public
  410. def disconnect(self):
  411. """
  412. Implements :func:`autobahn.wamp.interfaces.ISession.disconnect`
  413. """
  414. if self._transport:
  415. self._transport.close()
  416. @public
  417. def onUserError(self, fail, msg):
  418. """
  419. Implements :func:`autobahn.wamp.interfaces.ISession.onUserError`
  420. """
  421. if hasattr(fail, 'value') and isinstance(fail.value, exception.ApplicationError):
  422. self.log.warn('{klass}.onUserError(): "{msg}"',
  423. klass=self.__class__.__name__,
  424. msg=fail.value.error_message())
  425. else:
  426. self.log.error(
  427. '{klass}.onUserError(): "{msg}"\n{traceback}',
  428. klass=self.__class__.__name__,
  429. msg=msg,
  430. traceback=txaio.failure_format_traceback(fail),
  431. )
  432. def _swallow_error(self, fail, msg):
  433. '''
  434. This is an internal generic error-handler for errors encountered
  435. when calling down to on*() handlers that can reasonably be
  436. expected to be overridden in user code.
  437. Note that it *cancels* the error, so use with care!
  438. Specifically, this should *never* be added to the errback
  439. chain for a Deferred/coroutine that will make it out to user
  440. code.
  441. '''
  442. try:
  443. self.onUserError(fail, msg)
  444. except Exception:
  445. self.log.error(
  446. "Internal error: {tb}",
  447. tb=txaio.failure_format_traceback(txaio.create_failure()),
  448. )
  449. return None
  450. def type_check(self, func):
  451. """
  452. Does parameter type checking and validation against type hints
  453. and appropriately tells the user code and the caller (through router).
  454. """
  455. async def _type_check(*args, **kwargs):
  456. # Converge both args and kwargs into a dictionary
  457. arguments = inspect.getcallargs(func, *args, **kwargs)
  458. response = []
  459. for name, kind in func.__annotations__.items():
  460. if name in arguments:
  461. # if a "type" has __origin__ attribute it is a
  462. # parameterized generic
  463. if getattr(kind, "__origin__", None) == Union:
  464. expected_types = [arg.__name__ for arg in kind.__args__]
  465. if not isinstance(arguments[name], kind.__args__):
  466. response.append(
  467. "'{}' expected types={} got={}".format(name, expected_types,
  468. type(arguments[name]).__name__))
  469. elif not isinstance(arguments[name], kind):
  470. response.append(
  471. "'{}' expected type={} got={}".format(name, kind.__name__, type(arguments[name]).__name__))
  472. if response:
  473. raise TypeCheckError(', '.join(response))
  474. return await txaio.as_future(func, *args, **kwargs)
  475. return _type_check
  476. def onMessage(self, msg: IMessage):
  477. """
  478. Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onMessage`
  479. """
  480. if self._session_id is None:
  481. # the first message must be WELCOME, ABORT or CHALLENGE ..
  482. if isinstance(msg, message.Welcome):
  483. # before we let user code see the session -- that is,
  484. # before we fire "join" -- we give authentication
  485. # instances a chance to abort the session. Usually
  486. # this would be for "mutual authentication"
  487. # scenarios. For example, WAMP-SCRAM uses this to
  488. # confirm the server-signature
  489. d = txaio.as_future(self.onWelcome, msg)
  490. def success(res):
  491. if res is not None:
  492. self.log.debug("Session denied by onWelcome: {res}", res=res)
  493. reply = message.Abort(
  494. "wamp.error.cannot_authenticate", "{0}".format(res)
  495. )
  496. self._transport.send(reply)
  497. return
  498. if msg.realm:
  499. self._realm = msg.realm
  500. self._session_id = msg.session
  501. self._authid = msg.authid
  502. self._authrole = msg.authrole
  503. self._authmethod = msg.authmethod
  504. self._authprovider = msg.authprovider
  505. self._authextra = msg.authextra
  506. self._router_roles = msg.roles
  507. self._session_details = SessionDetails(
  508. realm=self._realm,
  509. session=self._session_id,
  510. authid=self._authid,
  511. authrole=self._authrole,
  512. authmethod=self._authmethod,
  513. authprovider=self._authprovider,
  514. authextra=msg.authextra,
  515. serializer=self._transport._serializer.SERIALIZER_ID,
  516. transport=self._transport.transport_details,
  517. # FIXME
  518. resumed=False, # msg.resumed,
  519. resumable=False, # msg.resumable,
  520. resume_token=None, # msg.resume_token,
  521. )
  522. # firing 'join' *before* running onJoin, so that
  523. # the idiom where you "do stuff" in onJoin --
  524. # possibly including self.leave() -- works
  525. # properly. Besides, there's "ready" that fires
  526. # after 'join' and onJoin have all completed...
  527. d = self.fire('join', self, self._session_details)
  528. # add a logging errback first, which will ignore any
  529. # errors from fire()
  530. txaio.add_callbacks(
  531. d, None,
  532. lambda e: self._swallow_error(e, "While notifying 'join'")
  533. )
  534. # this should run regardless
  535. txaio.add_callbacks(
  536. d,
  537. lambda _: txaio.as_future(self.onJoin, self._session_details),
  538. None
  539. )
  540. # ignore any errors from onJoin (XXX or, should that be fatal?)
  541. txaio.add_callbacks(
  542. d, None,
  543. lambda e: self._swallow_error(e, "While firing onJoin")
  544. )
  545. # this instance is now "ready"...
  546. txaio.add_callbacks(
  547. d,
  548. lambda _: self.fire('ready', self),
  549. None
  550. )
  551. # ignore any errors from 'ready'
  552. txaio.add_callbacks(
  553. d, None,
  554. lambda e: self._swallow_error(e, "While notifying 'ready'")
  555. )
  556. def error(e):
  557. reply = message.Abort(
  558. "wamp.error.cannot_authenticate", "Error calling onWelcome handler"
  559. )
  560. self._transport.send(reply)
  561. return self._swallow_error(e, "While firing onWelcome")
  562. txaio.add_callbacks(d, success, error)
  563. elif isinstance(msg, message.Abort):
  564. # fire callback and close the transport
  565. details = types.CloseDetails(msg.reason, msg.message)
  566. d = txaio.as_future(self.onLeave, details)
  567. def success(arg):
  568. # XXX also: handle async
  569. d = self.fire('leave', self, details)
  570. def return_arg(_):
  571. return arg
  572. def _error(e):
  573. return self._swallow_error(e, "While firing 'leave' event")
  574. txaio.add_callbacks(d, return_arg, _error)
  575. return d
  576. def _error(e):
  577. return self._swallow_error(e, "While firing onLeave")
  578. txaio.add_callbacks(d, success, _error)
  579. elif isinstance(msg, message.Challenge):
  580. challenge = types.Challenge(msg.method, msg.extra)
  581. d = txaio.as_future(self.onChallenge, challenge)
  582. def success(signature):
  583. if signature is None:
  584. raise Exception('onChallenge user callback did not return a signature')
  585. if type(signature) == bytes:
  586. signature = signature.decode('utf8')
  587. if type(signature) != str:
  588. raise Exception('signature must be unicode (was {})'.format(type(signature)))
  589. reply = message.Authenticate(signature)
  590. self._transport.send(reply)
  591. def error(err):
  592. self.onUserError(err, "Authentication failed")
  593. reply = message.Abort("wamp.error.cannot_authenticate", "{0}".format(err.value))
  594. self._transport.send(reply)
  595. # fire callback and close the transport
  596. details = types.CloseDetails(reply.reason, reply.message)
  597. d = txaio.as_future(self.onLeave, details)
  598. def success(arg):
  599. # XXX also: handle async
  600. self.fire('leave', self, details)
  601. return arg
  602. def _error(e):
  603. return self._swallow_error(e, "While firing onLeave")
  604. txaio.add_callbacks(d, success, _error)
  605. # switching to the callback chain, effectively
  606. # cancelling error (which we've now handled)
  607. return d
  608. txaio.add_callbacks(d, success, error)
  609. else:
  610. raise ProtocolError("Received {0} message, and session is not yet established".format(msg.__class__))
  611. else:
  612. # self._session_id != None (aka "session established")
  613. if isinstance(msg, message.Goodbye):
  614. if not self._goodbye_sent:
  615. # the peer wants to close: send GOODBYE reply
  616. reply = message.Goodbye()
  617. self._transport.send(reply)
  618. self._session_id = None
  619. # fire callback and close the transport
  620. details = types.CloseDetails(msg.reason, msg.message)
  621. d = txaio.as_future(self.onLeave, details)
  622. def success(arg):
  623. # XXX also: handle async
  624. self.fire('leave', self, details)
  625. return arg
  626. def _error(e):
  627. errmsg = 'While firing onLeave for reason "{0}" and message "{1}"'.format(msg.reason, msg.message)
  628. return self._swallow_error(e, errmsg)
  629. txaio.add_callbacks(d, success, _error)
  630. elif isinstance(msg, message.Event):
  631. if msg.subscription in self._subscriptions:
  632. # fire all event handlers on subscription ..
  633. for subscription in self._subscriptions[msg.subscription]:
  634. handler = subscription.handler
  635. topic = msg.topic or subscription.topic
  636. if msg.enc_algo:
  637. # FIXME: behavior in error cases (no keyring, decrypt issues, URI mismatch, ..)
  638. if not self._payload_codec:
  639. self.log.warn("received encoded payload with enc_algo={enc_algo}, but no payload codec active - ignoring encoded payload!", enc_algo=msg.enc_algo)
  640. return
  641. else:
  642. try:
  643. encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
  644. decoded_topic, msg.args, msg.kwargs = self._payload_codec.decode(False, topic, encoded_payload)
  645. except Exception as e:
  646. self.log.warn("failed to decode application payload encoded with enc_algo={enc_algo}: {error}", error=e, enc_algo=msg.enc_algo)
  647. return
  648. else:
  649. if topic != decoded_topic:
  650. self.log.warn("envelope topic URI does not match encoded one")
  651. return
  652. invoke_args = (handler.obj,) if handler.obj else tuple()
  653. if msg.args:
  654. invoke_args = invoke_args + tuple(msg.args)
  655. invoke_kwargs = msg.kwargs if msg.kwargs else dict()
  656. if handler.details_arg:
  657. 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, transaction_hash=msg.transaction_hash, retained=msg.retained, enc_algo=msg.enc_algo, forward_for=msg.forward_for)
  658. # FIXME: https://github.com/crossbario/autobahn-python/issues/764
  659. def _success(_):
  660. # Acknowledged Events -- only if we got the details header and
  661. # the broker advertised it
  662. if msg.x_acknowledged_delivery and self._router_roles["broker"].x_acknowledged_event_delivery:
  663. if self._transport:
  664. response = message.EventReceived(msg.publication)
  665. self._transport.send(response)
  666. else:
  667. self.log.warn("successfully processed event with acknowledged delivery, but could not send ACK, since the transport was lost in the meantime")
  668. def _error(e):
  669. errmsg = 'While firing {0} subscribed under {1}.'.format(
  670. handler.fn, msg.subscription)
  671. return self._swallow_error(e, errmsg)
  672. future = txaio.as_future(handler.fn, *invoke_args, **invoke_kwargs)
  673. txaio.add_callbacks(future, _success, _error)
  674. else:
  675. raise ProtocolError("EVENT received for non-subscribed subscription ID {0}".format(msg.subscription))
  676. elif isinstance(msg, message.Published):
  677. if msg.request in self._publish_reqs:
  678. # get and pop outstanding publish request
  679. publish_request = self._publish_reqs.pop(msg.request)
  680. if txaio.is_future(publish_request.on_reply) and txaio.is_called(publish_request.on_reply):
  681. return
  682. # create a new publication object
  683. publication = Publication(msg.publication, was_encrypted=publish_request.was_encrypted)
  684. # resolve deferred/future for publishing successfully
  685. txaio.resolve(publish_request.on_reply, publication)
  686. else:
  687. raise ProtocolError("PUBLISHED received for non-pending request ID {0}".format(msg.request))
  688. elif isinstance(msg, message.Subscribed):
  689. if msg.request in self._subscribe_reqs:
  690. # get and pop outstanding subscribe request
  691. request = self._subscribe_reqs.pop(msg.request)
  692. if txaio.is_future(request.on_reply) and txaio.is_called(request.on_reply):
  693. return
  694. # create new handler subscription list for subscription ID if not yet tracked
  695. if msg.subscription not in self._subscriptions:
  696. self._subscriptions[msg.subscription] = []
  697. subscription = Subscription(msg.subscription, request.topic, self, request.handler)
  698. # add handler to existing subscription
  699. self._subscriptions[msg.subscription].append(subscription)
  700. # resolve deferred/future for subscribing successfully
  701. txaio.resolve(request.on_reply, subscription)
  702. else:
  703. raise ProtocolError("SUBSCRIBED received for non-pending request ID {0}".format(msg.request))
  704. elif isinstance(msg, message.Unsubscribed):
  705. if msg.request in self._unsubscribe_reqs:
  706. # get and pop outstanding subscribe request
  707. request = self._unsubscribe_reqs.pop(msg.request)
  708. if txaio.is_future(request.on_reply) and txaio.is_called(request.on_reply):
  709. return
  710. # if the subscription still exists, mark as inactive and remove ..
  711. if request.subscription_id in self._subscriptions:
  712. for subscription in self._subscriptions[request.subscription_id]:
  713. subscription.active = False
  714. del self._subscriptions[request.subscription_id]
  715. # resolve deferred/future for unsubscribing successfully
  716. txaio.resolve(request.on_reply, 0)
  717. else:
  718. raise ProtocolError("UNSUBSCRIBED received for non-pending request ID {0}".format(msg.request))
  719. elif isinstance(msg, message.Result):
  720. if msg.request in self._call_reqs:
  721. call_request = self._call_reqs[msg.request]
  722. proc = call_request.procedure
  723. enc_err = None
  724. if msg.enc_algo:
  725. if not self._payload_codec:
  726. log_msg = "received encoded payload, but no payload codec active"
  727. self.log.warn(log_msg)
  728. enc_err = ApplicationError(ApplicationError.ENC_NO_PAYLOAD_CODEC, log_msg)
  729. else:
  730. try:
  731. encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
  732. decrypted_proc, msg.args, msg.kwargs = self._payload_codec.decode(True, proc, encoded_payload)
  733. except Exception as e:
  734. self.log.warn(
  735. "failed to decrypt application payload 1: {err}",
  736. err=e,
  737. )
  738. enc_err = ApplicationError(
  739. ApplicationError.ENC_DECRYPT_ERROR,
  740. "failed to decrypt application payload 1: {}".format(e),
  741. )
  742. else:
  743. if proc != decrypted_proc:
  744. self.log.warn(
  745. "URI within encrypted payload ('{decrypted_proc}') does not match the envelope ('{proc}')",
  746. decrypted_proc=decrypted_proc,
  747. proc=proc,
  748. )
  749. enc_err = ApplicationError(
  750. ApplicationError.ENC_TRUSTED_URI_MISMATCH,
  751. "URI within encrypted payload ('{}') does not match the envelope ('{}')".format(decrypted_proc, proc),
  752. )
  753. if msg.progress:
  754. # process progressive call result
  755. if call_request.options.on_progress:
  756. if enc_err:
  757. self.onUserError(enc_err, "could not deliver progressive call result, because payload decryption failed")
  758. else:
  759. kw = msg.kwargs or dict()
  760. args = msg.args or tuple()
  761. def _error(fail):
  762. self.onUserError(fail, "While firing on_progress")
  763. if call_request.options and call_request.options.details:
  764. prog_d = txaio.as_future(call_request.options.on_progress,
  765. types.CallResult(*msg.args,
  766. callee=msg.callee,
  767. callee_authid=msg.callee_authid,
  768. callee_authrole=msg.callee_authrole,
  769. forward_for=msg.forward_for,
  770. **msg.kwargs))
  771. else:
  772. prog_d = txaio.as_future(call_request.options.on_progress,
  773. *args,
  774. **kw)
  775. txaio.add_callbacks(prog_d, None, _error)
  776. else:
  777. # process final call result
  778. # drop original request
  779. del self._call_reqs[msg.request]
  780. # user callback that gets fired
  781. on_reply = call_request.on_reply
  782. if txaio.is_future(on_reply) and txaio.is_called(on_reply):
  783. return
  784. # above might already have rejected, so we guard ..
  785. if enc_err:
  786. txaio.reject(on_reply, enc_err)
  787. else:
  788. if msg.kwargs or (call_request.options and call_request.options.details):
  789. kwargs = msg.kwargs or {}
  790. if msg.args:
  791. res = types.CallResult(*msg.args,
  792. callee=msg.callee,
  793. callee_authid=msg.callee_authid,
  794. callee_authrole=msg.callee_authrole,
  795. forward_for=msg.forward_for,
  796. **kwargs)
  797. else:
  798. res = types.CallResult(callee=msg.callee,
  799. callee_authid=msg.callee_authid,
  800. callee_authrole=msg.callee_authrole,
  801. forward_for=msg.forward_for,
  802. **kwargs)
  803. txaio.resolve(on_reply, res)
  804. else:
  805. if msg.args:
  806. if len(msg.args) > 1:
  807. res = types.CallResult(*msg.args)
  808. txaio.resolve(on_reply, res)
  809. else:
  810. txaio.resolve(on_reply, msg.args[0])
  811. else:
  812. txaio.resolve(on_reply, None)
  813. else:
  814. raise ProtocolError("RESULT received for non-pending request ID {0}".format(msg.request))
  815. elif isinstance(msg, message.Invocation):
  816. if msg.request in self._invocations:
  817. raise ProtocolError("INVOCATION received for request ID {0} already invoked".format(msg.request))
  818. else:
  819. if msg.registration not in self._registrations:
  820. raise ProtocolError("INVOCATION received for non-registered registration ID {0}".format(msg.registration))
  821. else:
  822. registration = self._registrations[msg.registration]
  823. endpoint = registration.endpoint
  824. proc = msg.procedure or registration.procedure
  825. enc_err = None
  826. if msg.enc_algo:
  827. if not self._payload_codec:
  828. log_msg = "received encrypted INVOCATION payload, but no keyring active"
  829. self.log.warn(log_msg)
  830. enc_err = ApplicationError(ApplicationError.ENC_NO_PAYLOAD_CODEC, log_msg)
  831. else:
  832. try:
  833. encoded_payload = EncodedPayload(msg.payload, msg.enc_algo, msg.enc_serializer, msg.enc_key)
  834. decrypted_proc, msg.args, msg.kwargs = self._payload_codec.decode(False, proc, encoded_payload)
  835. except Exception as e:
  836. self.log.warn(
  837. "failed to decrypt INVOCATION payload: {err}",
  838. err=e,
  839. )
  840. enc_err = ApplicationError(
  841. ApplicationError.ENC_DECRYPT_ERROR,
  842. "failed to decrypt INVOCATION payload: {}".format(e),
  843. )
  844. else:
  845. if proc != decrypted_proc:
  846. self.log.warn(
  847. "URI within encrypted INVOCATION payload ('{decrypted_proc}') "
  848. "does not match the envelope ('{proc}')",
  849. decrypted_proc=decrypted_proc,
  850. proc=proc,
  851. )
  852. enc_err = ApplicationError(
  853. ApplicationError.ENC_TRUSTED_URI_MISMATCH,
  854. "URI within encrypted INVOCATION payload ('{}') does not match the envelope ('{}')".format(decrypted_proc, proc),
  855. )
  856. if enc_err:
  857. # when there was a problem decrypting the INVOCATION payload, we obviously can't invoke
  858. # the endpoint, but return and
  859. reply = self._message_from_exception(message.Invocation.MESSAGE_TYPE, msg.request, enc_err)
  860. self._transport.send(reply)
  861. else:
  862. if endpoint.obj is not None:
  863. invoke_args = (endpoint.obj,)
  864. else:
  865. invoke_args = tuple()
  866. if msg.args:
  867. invoke_args = invoke_args + tuple(msg.args)
  868. invoke_kwargs = msg.kwargs if msg.kwargs else dict()
  869. if endpoint.details_arg:
  870. if msg.receive_progress:
  871. def progress(*args, **kwargs):
  872. assert(args is None or type(args) in (list, tuple))
  873. assert(kwargs is None or type(kwargs) == dict)
  874. encoded_payload = None
  875. if msg.enc_algo:
  876. if not self._payload_codec:
  877. raise Exception("trying to send encrypted payload, but no keyring active")
  878. encoded_payload = self._payload_codec.encode(False, proc, args, kwargs)
  879. if encoded_payload:
  880. progress_msg = message.Yield(msg.request,
  881. payload=encoded_payload.payload,
  882. progress=True,
  883. enc_algo=encoded_payload.enc_algo,
  884. enc_key=encoded_payload.enc_key,
  885. enc_serializer=encoded_payload.enc_serializer)
  886. else:
  887. progress_msg = message.Yield(msg.request,
  888. args=args,
  889. kwargs=kwargs,
  890. progress=True)
  891. self._transport.send(progress_msg)
  892. else:
  893. progress = None
  894. invoke_kwargs[endpoint.details_arg] = types.CallDetails(registration,
  895. progress=progress,
  896. caller=msg.caller,
  897. caller_authid=msg.caller_authid,
  898. caller_authrole=msg.caller_authrole,
  899. procedure=proc,
  900. transaction_hash=msg.transaction_hash,
  901. enc_algo=msg.enc_algo)
  902. on_reply = txaio.as_future(endpoint.fn, *invoke_args, **invoke_kwargs)
  903. def success(res):
  904. del self._invocations[msg.request]
  905. encoded_payload = None
  906. if msg.enc_algo:
  907. if not self._payload_codec:
  908. log_msg = "trying to send encrypted payload, but no keyring active"
  909. self.log.warn(log_msg)
  910. else:
  911. try:
  912. if isinstance(res, types.CallResult):
  913. encoded_payload = self._payload_codec.encode(False, proc, res.results, res.kwresults)
  914. else:
  915. encoded_payload = self._payload_codec.encode(False, proc, [res])
  916. except Exception as e:
  917. self.log.warn(
  918. "failed to encrypt application payload: {err}",
  919. err=e,
  920. )
  921. if encoded_payload:
  922. if isinstance(res, types.CallResult):
  923. reply = message.Yield(msg.request,
  924. payload=encoded_payload.payload,
  925. enc_algo=encoded_payload.enc_algo,
  926. enc_key=encoded_payload.enc_key,
  927. enc_serializer=encoded_payload.enc_serializer,
  928. callee=res.callee,
  929. callee_authid=res.callee_authid,
  930. callee_authrole=res.callee_authrole,
  931. forward_for=res.forward_for)
  932. else:
  933. reply = message.Yield(msg.request,
  934. payload=encoded_payload.payload,
  935. enc_algo=encoded_payload.enc_algo,
  936. enc_key=encoded_payload.enc_key,
  937. enc_serializer=encoded_payload.enc_serializer)
  938. else:
  939. if isinstance(res, types.CallResult):
  940. reply = message.Yield(msg.request,
  941. args=res.results,
  942. kwargs=res.kwresults,
  943. callee=res.callee,
  944. callee_authid=res.callee_authid,
  945. callee_authrole=res.callee_authrole,
  946. forward_for=res.forward_for)
  947. else:
  948. reply = message.Yield(msg.request,
  949. args=[res])
  950. if self._transport is None:
  951. self.log.debug('Skipping result of "{}", request {} because transport disconnected.'.format(registration.procedure, msg.request))
  952. return
  953. try:
  954. self._transport.send(reply)
  955. except SerializationError as e:
  956. # the application-level payload returned from the invoked procedure can't be serialized
  957. error_reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD,
  958. args=['success return value (args={}, kwargs={}) from invoked procedure "{}" could not be serialized: {}'.format(reply.args,
  959. reply.kwargs,
  960. registration.procedure,
  961. e)])
  962. self._transport.send(error_reply)
  963. except PayloadExceededError as e:
  964. # the application-level payload returned from the invoked procedure, when serialized and framed
  965. # for the transport, exceeds the transport message/frame size limit
  966. error_reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.PAYLOAD_SIZE_EXCEEDED,
  967. args=['success return value (args={}, kwargs={}) from invoked procedure "{}" exceeds transport size limit: {}'.format(reply.args,
  968. reply.kwargs,
  969. registration.procedure,
  970. e)])
  971. self._transport.send(error_reply)
  972. def error(err):
  973. del self._invocations[msg.request]
  974. errmsg = txaio.failure_message(err)
  975. try:
  976. self.onUserError(err, errmsg)
  977. except:
  978. pass
  979. formatted_tb = None
  980. if self.traceback_app:
  981. formatted_tb = txaio.failure_format_traceback(err)
  982. reply = self._message_from_exception(
  983. message.Invocation.MESSAGE_TYPE,
  984. msg.request,
  985. err.value,
  986. formatted_tb,
  987. msg.enc_algo
  988. )
  989. try:
  990. self._transport.send(reply)
  991. except SerializationError as e:
  992. # the application-level payload returned from the invoked procedure can't be serialized
  993. reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.INVALID_PAYLOAD,
  994. args=['error return value from invoked procedure "{0}" could not be serialized: {1}'.format(registration.procedure, e)])
  995. self._transport.send(reply)
  996. except PayloadExceededError as e:
  997. # the application-level payload returned from the invoked procedure, when serialized and framed
  998. # for the transport, exceeds the transport message/frame size limit
  999. reply = message.Error(message.Invocation.MESSAGE_TYPE, msg.request, ApplicationError.PAYLOAD_SIZE_EXCEEDED,
  1000. args=['success return value from invoked procedure "{0}" exceeds transport size limit: {1}'.format(registration.procedure, e)])
  1001. self._transport.send(reply)
  1002. # we have handled the error, so we eat it
  1003. return None
  1004. self._invocations[msg.request] = InvocationRequest(msg.request, on_reply)
  1005. txaio.add_callbacks(on_reply, success, error)
  1006. elif isinstance(msg, message.Interrupt):
  1007. if msg.request not in self._invocations:
  1008. # raise ProtocolError("INTERRUPT received for non-pending invocation {0}".format(msg.request))
  1009. self.log.debug('INTERRUPT received for non-pending invocation {request}', request=msg.request)
  1010. else:
  1011. invoked = self._invocations[msg.request]
  1012. # this will result in a CancelledError which will
  1013. # be captured by the error handler around line 979
  1014. # to delete the invocation..
  1015. txaio.cancel(invoked.on_reply)
  1016. elif isinstance(msg, message.Registered):
  1017. if msg.request in self._register_reqs:
  1018. # get and pop outstanding register request
  1019. request = self._register_reqs.pop(msg.request)
  1020. if txaio.is_future(request.on_reply) and txaio.is_called(request.on_reply):
  1021. return
  1022. # create new registration if not yet tracked
  1023. if msg.registration not in self._registrations:
  1024. registration = Registration(self, msg.registration, request.procedure, request.endpoint)
  1025. self._registrations[msg.registration] = registration
  1026. else:
  1027. raise ProtocolError("REGISTERED received for already existing registration ID {0}".format(msg.registration))
  1028. txaio.resolve(request.on_reply, registration)
  1029. else:
  1030. raise ProtocolError("REGISTERED received for non-pending request ID {0}".format(msg.request))
  1031. elif isinstance(msg, message.Unregistered):
  1032. if msg.request == 0:
  1033. # this is a forced un-register either from a call
  1034. # to the wamp.* meta-api or the force_reregister
  1035. # option
  1036. try:
  1037. reg = self._registrations[msg.registration]
  1038. except KeyError:
  1039. raise ProtocolError(
  1040. "UNREGISTERED received for non-existant registration"
  1041. " ID {0}".format(msg.registration)
  1042. )
  1043. self.log.debug(
  1044. "Router unregistered procedure '{proc}' with ID {id}",
  1045. proc=reg.procedure,
  1046. id=msg.registration,
  1047. )
  1048. elif msg.request in self._unregister_reqs:
  1049. # get and pop outstanding subscribe request
  1050. request = self._unregister_reqs.pop(msg.request)
  1051. if txaio.is_future(request.on_reply) and txaio.is_called(request.on_reply):
  1052. return
  1053. # if the registration still exists, mark as inactive and remove ..
  1054. if request.registration_id in self._registrations:
  1055. self._registrations[request.registration_id].active = False
  1056. del self._registrations[request.registration_id]
  1057. # resolve deferred/future for unregistering successfully
  1058. txaio.resolve(request.on_reply)
  1059. else:
  1060. raise ProtocolError("UNREGISTERED received for non-pending request ID {0}".format(msg.request))
  1061. elif isinstance(msg, message.Error):
  1062. # remove outstanding request and get the reply deferred/future
  1063. on_reply = None
  1064. # ERROR reply to CALL
  1065. if msg.request_type == message.Call.MESSAGE_TYPE and msg.request in self._call_reqs:
  1066. on_reply = self._call_reqs.pop(msg.request).on_reply
  1067. # ERROR reply to PUBLISH
  1068. elif msg.request_type == message.Publish.MESSAGE_TYPE and msg.request in self._publish_reqs:
  1069. on_reply = self._publish_reqs.pop(msg.request).on_reply
  1070. # ERROR reply to SUBSCRIBE
  1071. elif msg.request_type == message.Subscribe.MESSAGE_TYPE and msg.request in self._subscribe_reqs:
  1072. on_reply = self._subscribe_reqs.pop(msg.request).on_reply
  1073. # ERROR reply to UNSUBSCRIBE
  1074. elif msg.request_type == message.Unsubscribe.MESSAGE_TYPE and msg.request in self._unsubscribe_reqs:
  1075. on_reply = self._unsubscribe_reqs.pop(msg.request).on_reply
  1076. # ERROR reply to REGISTER
  1077. elif msg.request_type == message.Register.MESSAGE_TYPE and msg.request in self._register_reqs:
  1078. on_reply = self._register_reqs.pop(msg.request).on_reply
  1079. # ERROR reply to UNREGISTER
  1080. elif msg.request_type == message.Unregister.MESSAGE_TYPE and msg.request in self._unregister_reqs:
  1081. on_reply = self._unregister_reqs.pop(msg.request).on_reply
  1082. if on_reply:
  1083. if not txaio.is_called(on_reply):
  1084. txaio.reject(on_reply, self._exception_from_message(msg))
  1085. else:
  1086. raise ProtocolError("WampAppSession.onMessage(): ERROR received for non-pending request_type {0} and request ID {1}".format(msg.request_type, msg.request))
  1087. else:
  1088. raise ProtocolError("Unexpected message {0}".format(msg.__class__))
  1089. @public
  1090. def onClose(self, wasClean):
  1091. """
  1092. Implements :func:`autobahn.wamp.interfaces.ITransportHandler.onClose`
  1093. """
  1094. self._transport = None
  1095. if self._session_id:
  1096. # fire callback and close the transport
  1097. details = types.CloseDetails(
  1098. reason=types.CloseDetails.REASON_TRANSPORT_LOST,
  1099. message='WAMP transport was lost without closing the session {} before'.format(self._session_id),
  1100. )
  1101. d = txaio.as_future(self.onLeave, details)
  1102. def success(arg):
  1103. # XXX also: handle async
  1104. self.fire('leave', self, details)
  1105. return arg
  1106. def _error(e):
  1107. return self._swallow_error(e, "While firing onLeave")
  1108. txaio.add_callbacks(d, success, _error)
  1109. self._session_id = None
  1110. d = txaio.as_future(self.onDisconnect)
  1111. def success(arg):
  1112. # XXX do we care about returning 'arg' properly?
  1113. return self.fire('disconnect', self, was_clean=wasClean)
  1114. def _error(e):
  1115. return self._swallow_error(e, "While firing onDisconnect")
  1116. txaio.add_callbacks(d, success, _error)
  1117. @public
  1118. def onChallenge(self, challenge: Challenge) -> str:
  1119. """
  1120. Implements :func:`autobahn.wamp.interfaces.ISession.onChallenge`
  1121. """
  1122. # Note: if we use NotImplementedError in below, the type checking in PyCharm
  1123. # will recognize that, and complain about "not implemented abstract method"
  1124. # in e.g. autobahn.twisted.wamp.ApplicationSession even though the latter
  1125. # derives from this base class! IOW: don't change it;)
  1126. raise RuntimeError('received authentication challenge, but onChallenge not implemented')
  1127. @public
  1128. def onJoin(self, details: SessionDetails):
  1129. """
  1130. Implements :meth:`autobahn.wamp.interfaces.ISession.onJoin`
  1131. """
  1132. @public
  1133. def onWelcome(self, welcome: message.Welcome) -> Optional[str]:
  1134. """
  1135. Implements :meth:`autobahn.wamp.interfaces.ISession.onWelcome`
  1136. """
  1137. def _errback_outstanding_requests(self, exc):
  1138. """
  1139. Errback any still outstanding requests with exc.
  1140. """
  1141. d = txaio.create_future_success(None)
  1142. all_requests = [
  1143. self._publish_reqs,
  1144. self._subscribe_reqs,
  1145. self._unsubscribe_reqs,
  1146. self._call_reqs,
  1147. self._register_reqs,
  1148. self._unregister_reqs
  1149. ]
  1150. outstanding = []
  1151. for requests in all_requests:
  1152. outstanding.extend(requests.values())
  1153. requests.clear()
  1154. if outstanding:
  1155. self.log.info(
  1156. 'Cancelling {count} outstanding requests',
  1157. count=len(outstanding),
  1158. )
  1159. for request in outstanding:
  1160. self.log.debug(
  1161. 'cleaning up outstanding {request_type} request {request_id}, '
  1162. 'firing errback on user handler {request_on_reply}',
  1163. request_on_reply=request.on_reply,
  1164. request_id=request.request_id,
  1165. request_type=request.__class__.__name__,
  1166. )
  1167. if not txaio.is_called(request.on_reply):
  1168. txaio.reject(request.on_reply, exc)
  1169. # wait for any async-ness in the error handlers for on_reply
  1170. txaio.add_callbacks(d, lambda _: request.on_reply, lambda _: request.on_reply)
  1171. return d
  1172. @public
  1173. def onLeave(self, details: CloseDetails):
  1174. """
  1175. Implements :meth:`autobahn.wamp.interfaces.ISession.onLeave`
  1176. """
  1177. if details.reason != CloseDetails.REASON_DEFAULT:
  1178. self.log.warn('session closed with reason {reason} [{message}]', reason=details.reason, message=details.message)
  1179. # fire ApplicationError on any currently outstanding requests
  1180. exc = ApplicationError(details.reason, details.message)
  1181. d = self._errback_outstanding_requests(exc)
  1182. def disconnect(_):
  1183. if self._transport:
  1184. self.disconnect()
  1185. txaio.add_callbacks(d, disconnect, disconnect)
  1186. return d
  1187. @public
  1188. def leave(self, reason: Optional[str] = None, message: Optional[str] = None):
  1189. """
  1190. Implements :meth:`autobahn.wamp.interfaces.ISession.leave`
  1191. """
  1192. if not self._session_id:
  1193. self.log.warn('session is not joined on a realm - no session to leave')
  1194. return
  1195. if not self._goodbye_sent:
  1196. if not reason:
  1197. reason = "wamp.close.normal"
  1198. msg = wamp.message.Goodbye(reason=reason, message=message)
  1199. self._transport.send(msg)
  1200. self._goodbye_sent = True
  1201. else:
  1202. self.log.warn('session was already requested to leave - not sending GOODBYE again')
  1203. is_closed = self._transport is None or self._transport.is_closed
  1204. return is_closed
  1205. @public
  1206. def onDisconnect(self):
  1207. """
  1208. Implements :meth:`autobahn.wamp.interfaces.ISession.onDisconnect`
  1209. """
  1210. # fire TransportLost on any _still_ outstanding requests
  1211. # (these should have been already cleaned up in onLeave() - when
  1212. # this actually has fired)
  1213. exc = exception.TransportLost()
  1214. self._errback_outstanding_requests(exc)
  1215. # FIXME:
  1216. # def publish(self, topic: str, *args: Optional[List[Any]], **kwargs: Optional[Dict[str, Any]]) -> Optional[Publication]:
  1217. @public
  1218. def publish(self, topic: str, *args, **kwargs) -> Optional[Publication]:
  1219. """
  1220. Implements :meth:`autobahn.wamp.interfaces.IPublisher.publish`
  1221. """
  1222. assert(type(topic) == str)
  1223. assert(args is None or type(args) in (list, tuple))
  1224. assert(kwargs is None or type(kwargs) == dict)
  1225. message.check_or_raise_uri(topic,
  1226. message='{}.publish()'.format(self.__class__.__name__),
  1227. strict=False,
  1228. allow_empty_components=False,
  1229. allow_none=False)
  1230. options = kwargs.pop('options', None)
  1231. if options and not isinstance(options, types.PublishOptions):
  1232. raise Exception("options must be of type a.w.t.PublishOptions")
  1233. if not self._transport:
  1234. raise exception.TransportLost()
  1235. request_id = self._request_id_gen.next()
  1236. encoded_payload = None
  1237. if self._payload_codec:
  1238. encoded_payload = self._payload_codec.encode(True, topic, args, kwargs)
  1239. if encoded_payload:
  1240. if options:
  1241. msg = message.Publish(request_id,
  1242. topic,
  1243. payload=encoded_payload.payload,
  1244. enc_algo=encoded_payload.enc_algo,
  1245. enc_key=encoded_payload.enc_key,
  1246. enc_serializer=encoded_payload.enc_serializer,
  1247. **options.message_attr())
  1248. else:
  1249. msg = message.Publish(request_id,
  1250. topic,
  1251. payload=encoded_payload.payload,
  1252. enc_algo=encoded_payload.enc_algo,
  1253. enc_key=encoded_payload.enc_key,
  1254. enc_serializer=encoded_payload.enc_serializer)
  1255. else:
  1256. if options:
  1257. msg = message.Publish(request_id,
  1258. topic,
  1259. args=args,
  1260. kwargs=kwargs,
  1261. **options.message_attr())
  1262. else:
  1263. msg = message.Publish(request_id,
  1264. topic,
  1265. args=args,
  1266. kwargs=kwargs)
  1267. if options:
  1268. if options.correlation_id is not None:
  1269. msg.correlation_id = options.correlation_id
  1270. if options.correlation_uri is not None:
  1271. msg.correlation_uri = options.correlation_uri
  1272. if options.correlation_is_anchor is not None:
  1273. msg.correlation_is_anchor = options.correlation_is_anchor
  1274. if options.correlation_is_last is not None:
  1275. msg.correlation_is_last = options.correlation_is_last
  1276. if options and options.acknowledge:
  1277. # only acknowledged publications expect a reply ..
  1278. on_reply = txaio.create_future()
  1279. self._publish_reqs[request_id] = PublishRequest(request_id, on_reply, was_encrypted=(encoded_payload is not None))
  1280. else:
  1281. on_reply = None
  1282. try:
  1283. # Notes:
  1284. #
  1285. # * this might raise autobahn.wamp.exception.SerializationError
  1286. # when the user payload cannot be serialized
  1287. # * we have to setup a PublishRequest() in _publish_reqs _before_
  1288. # calling transpor.send(), because a mock- or side-by-side transport
  1289. # will immediately lead on an incoming WAMP message in onMessage()
  1290. #
  1291. self._transport.send(msg)
  1292. except Exception as e:
  1293. if request_id in self._publish_reqs:
  1294. del self._publish_reqs[request_id]
  1295. raise e
  1296. return on_reply
  1297. @public
  1298. def subscribe(self, handler: Union[Callable, Any], topic: Optional[str] = None,
  1299. options: Optional[SubscribeOptions] = None, check_types: Optional[bool] = None) -> \
  1300. Union[Subscription, List[Subscription]]:
  1301. """
  1302. Implements :meth:`autobahn.wamp.interfaces.ISubscriber.subscribe`
  1303. """
  1304. assert((callable(handler) and topic is not None) or (hasattr(handler, '__class__') and not check_types))
  1305. assert(topic is None or type(topic) == str)
  1306. assert(options is None or isinstance(options, types.SubscribeOptions))
  1307. if not self._transport:
  1308. raise exception.TransportLost()
  1309. def _subscribe(obj, fn, topic, options, check_types):
  1310. message.check_or_raise_uri(topic,
  1311. message='{}.subscribe()'.format(self.__class__.__name__),
  1312. strict=False,
  1313. allow_empty_components=True,
  1314. allow_none=False)
  1315. request_id = self._request_id_gen.next()
  1316. on_reply = txaio.create_future()
  1317. if check_types:
  1318. fn = self.type_check(fn)
  1319. handler_obj = Handler(fn, obj, options.details_arg if options else None)
  1320. self._subscribe_reqs[request_id] = SubscribeRequest(request_id, topic, on_reply, handler_obj)
  1321. if options:
  1322. msg = message.Subscribe(request_id, topic, **options.message_attr())
  1323. else:
  1324. msg = message.Subscribe(request_id, topic)
  1325. if options:
  1326. if options.correlation_id is not None:
  1327. msg.correlation_id = options.correlation_id
  1328. if options.correlation_uri is not None:
  1329. msg.correlation_uri = options.correlation_uri
  1330. if options.correlation_is_anchor is not None:
  1331. msg.correlation_is_anchor = options.correlation_is_anchor
  1332. if options.correlation_is_last is not None:
  1333. msg.correlation_is_last = options.correlation_is_last
  1334. self._transport.send(msg)
  1335. return on_reply
  1336. if callable(handler):
  1337. # subscribe a single handler
  1338. return _subscribe(None, handler, topic, options, check_types)
  1339. else:
  1340. # subscribe all methods on an object decorated with "wamp.subscribe"
  1341. on_replies = []
  1342. for k in inspect.getmembers(handler.__class__, is_method_or_function):
  1343. proc = k[1]
  1344. if "_wampuris" in proc.__dict__:
  1345. for pat in proc.__dict__["_wampuris"]:
  1346. if pat.is_handler():
  1347. _uri = pat.uri()
  1348. subopts = pat.options or options
  1349. if subopts is None:
  1350. if pat.uri_type == uri.Pattern.URI_TYPE_WILDCARD:
  1351. subopts = types.SubscribeOptions(match="wildcard")
  1352. else:
  1353. subopts = types.SubscribeOptions(match="exact")
  1354. on_replies.append(_subscribe(handler, proc, _uri, subopts, pat._check_types))
  1355. return txaio.gather(on_replies, consume_exceptions=True)
  1356. def _unsubscribe(self, subscription):
  1357. """
  1358. Called from :meth:`autobahn.wamp.protocol.Subscription.unsubscribe`
  1359. """
  1360. assert(isinstance(subscription, Subscription))
  1361. assert subscription.active
  1362. assert(subscription.id in self._subscriptions)
  1363. assert(subscription in self._subscriptions[subscription.id])
  1364. if not self._transport:
  1365. raise exception.TransportLost()
  1366. # remove handler subscription and mark as inactive
  1367. self._subscriptions[subscription.id].remove(subscription)
  1368. subscription.active = False
  1369. # number of handler subscriptions left ..
  1370. scount = len(self._subscriptions[subscription.id])
  1371. if scount == 0:
  1372. # if the last handler was removed, unsubscribe from broker ..
  1373. request_id = self._request_id_gen.next()
  1374. on_reply = txaio.create_future()
  1375. self._unsubscribe_reqs[request_id] = UnsubscribeRequest(request_id, on_reply, subscription.id)
  1376. msg = message.Unsubscribe(request_id, subscription.id)
  1377. self._transport.send(msg)
  1378. return on_reply
  1379. else:
  1380. # there are still handlers active on the subscription!
  1381. return txaio.create_future_success(scount)
  1382. @public
  1383. def call(self, procedure: str, *args, **kwargs) -> Union[Any, CallResult]:
  1384. """
  1385. Implements :meth:`autobahn.wamp.interfaces.ICaller.call`
  1386. .. note::
  1387. Regarding type hints for ``*args`` and ``**kwargs``, doesn't work as we
  1388. can receive any Python types as list items or dict values, and because
  1389. of what is discussed here
  1390. https://adamj.eu/tech/2021/05/11/python-type-hints-args-and-kwargs/
  1391. """
  1392. assert(type(procedure) == str)
  1393. assert(args is None or type(args) in (list, tuple))
  1394. assert(kwargs is None or type(kwargs) == dict)
  1395. message.check_or_raise_uri(procedure,
  1396. message='{}.call()'.format(self.__class__.__name__),
  1397. strict=False,
  1398. allow_empty_components=False,
  1399. allow_none=False)
  1400. options = kwargs.pop('options', None)
  1401. if options and not isinstance(options, types.CallOptions):
  1402. raise Exception("options must be of type a.w.t.CallOptions")
  1403. if not self._transport:
  1404. raise exception.TransportLost()
  1405. request_id = self._request_id_gen.next()
  1406. encoded_payload = None
  1407. if self._payload_codec:
  1408. try:
  1409. encoded_payload = self._payload_codec.encode(True, procedure, args, kwargs)
  1410. except:
  1411. self.log.failure()
  1412. raise
  1413. if encoded_payload:
  1414. if options:
  1415. msg = message.Call(request_id,
  1416. procedure,
  1417. payload=encoded_payload.payload,
  1418. enc_algo=encoded_payload.enc_algo,
  1419. enc_key=encoded_payload.enc_key,
  1420. enc_serializer=encoded_payload.enc_serializer,
  1421. **options.message_attr())
  1422. else:
  1423. msg = message.Call(request_id,
  1424. procedure,
  1425. payload=encoded_payload.payload,
  1426. enc_algo=encoded_payload.enc_algo,
  1427. enc_key=encoded_payload.enc_key,
  1428. enc_serializer=encoded_payload.enc_serializer)
  1429. else:
  1430. if options:
  1431. msg = message.Call(request_id,
  1432. procedure,
  1433. args=args,
  1434. kwargs=kwargs,
  1435. **options.message_attr())
  1436. else:
  1437. msg = message.Call(request_id,
  1438. procedure,
  1439. args=args,
  1440. kwargs=kwargs)
  1441. if options:
  1442. if options.correlation_id is not None:
  1443. msg.correlation_id = options.correlation_id
  1444. if options.correlation_uri is not None:
  1445. msg.correlation_uri = options.correlation_uri
  1446. if options.correlation_is_anchor is not None:
  1447. msg.correlation_is_anchor = options.correlation_is_anchor
  1448. if options.correlation_is_last is not None:
  1449. msg.correlation_is_last = options.correlation_is_last
  1450. def canceller(d):
  1451. cancel_msg = message.Cancel(request_id)
  1452. self._transport.send(cancel_msg)
  1453. # since we announced support for cancelling, we should
  1454. # definitely get an Error back for our Cancel which will
  1455. # clean up this invocation
  1456. on_reply = txaio.create_future(canceller=canceller)
  1457. self._call_reqs[request_id] = CallRequest(request_id, procedure, on_reply, options)
  1458. try:
  1459. # Notes:
  1460. #
  1461. # * this might raise autobahn.wamp.exception.SerializationError
  1462. # when the user payload cannot be serialized
  1463. # * we have to setup a CallRequest() in _call_reqs _before_
  1464. # calling transpor.send(), because a mock- or side-by-side transport
  1465. # will immediately lead on an incoming WAMP message in onMessage()
  1466. #
  1467. self._transport.send(msg)
  1468. except:
  1469. if request_id in self._call_reqs:
  1470. del self._call_reqs[request_id]
  1471. raise
  1472. return on_reply
  1473. @public
  1474. def register(self, endpoint: Union[Callable, Any], procedure: Optional[str] = None,
  1475. options: Optional[RegisterOptions] = None, prefix: Optional[str] = None,
  1476. check_types: Optional[bool] = None) -> Union[Registration, List[Registration]]:
  1477. """
  1478. Implements :meth:`autobahn.wamp.interfaces.ICallee.register`
  1479. """
  1480. assert((callable(endpoint) and procedure is not None) or (hasattr(endpoint, '__class__') and not check_types))
  1481. if not self._transport:
  1482. raise exception.TransportLost()
  1483. def _register(obj, fn, procedure, options, check_types):
  1484. message.check_or_raise_uri(procedure,
  1485. message='{}.register()'.format(self.__class__.__name__),
  1486. strict=False,
  1487. allow_empty_components=True,
  1488. allow_none=False)
  1489. request_id = self._request_id_gen.next()
  1490. on_reply = txaio.create_future()
  1491. if check_types:
  1492. fn = self.type_check(fn)
  1493. endpoint_obj = Endpoint(fn, obj, options.details_arg if options else None)
  1494. if prefix is not None:
  1495. procedure = "{}{}".format(prefix, procedure)
  1496. self._register_reqs[request_id] = RegisterRequest(request_id, on_reply, procedure, endpoint_obj)
  1497. if options:
  1498. msg = message.Register(request_id, procedure, **options.message_attr())
  1499. else:
  1500. msg = message.Register(request_id, procedure)
  1501. if options:
  1502. if options.correlation_id is not None:
  1503. msg.correlation_id = options.correlation_id
  1504. if options.correlation_uri is not None:
  1505. msg.correlation_uri = options.correlation_uri
  1506. if options.correlation_is_anchor is not None:
  1507. msg.correlation_is_anchor = options.correlation_is_anchor
  1508. if options.correlation_is_last is not None:
  1509. msg.correlation_is_last = options.correlation_is_last
  1510. self._transport.send(msg)
  1511. return on_reply
  1512. if callable(endpoint):
  1513. # register a single callable
  1514. return _register(None, endpoint, procedure, options, check_types)
  1515. else:
  1516. # register all methods on an object decorated with "wamp.register"
  1517. on_replies = []
  1518. for k in inspect.getmembers(endpoint.__class__, is_method_or_function):
  1519. proc = k[1]
  1520. if "_wampuris" in proc.__dict__:
  1521. for pat in proc.__dict__["_wampuris"]:
  1522. if pat.is_endpoint():
  1523. _uri = pat.uri()
  1524. regopts = pat.options or options
  1525. on_replies.append(_register(endpoint, proc, _uri, regopts, pat._check_types))
  1526. return txaio.gather(on_replies, consume_exceptions=True)
  1527. def _unregister(self, registration):
  1528. """
  1529. Called from :meth:`autobahn.wamp.protocol.Registration.unregister`
  1530. """
  1531. assert(isinstance(registration, Registration))
  1532. assert registration.active
  1533. assert(registration.id in self._registrations)
  1534. if not self._transport:
  1535. raise exception.TransportLost()
  1536. request_id = self._request_id_gen.next()
  1537. on_reply = txaio.create_future()
  1538. self._unregister_reqs[request_id] = UnregisterRequest(request_id, on_reply, registration.id)
  1539. msg = message.Unregister(request_id, registration.id)
  1540. self._transport.send(msg)
  1541. return on_reply
  1542. class _SessionShim(ApplicationSession):
  1543. """
  1544. shim that lets us present pep8 API for user-classes to override,
  1545. but also backwards-compatible for existing code using
  1546. ApplicationSession "directly".
  1547. **NOTE:** this is not public or intended for use; you should import
  1548. either autobahn.asyncio.wamp.Session or
  1549. autobahn.twisted.wamp.Session depending on which async
  1550. framework yo're using.
  1551. """
  1552. #: name -> IAuthenticator
  1553. _authenticators = None
  1554. def onJoin(self, details):
  1555. return self.on_join(details)
  1556. def onConnect(self):
  1557. if self._authenticators:
  1558. # authid, authrole *must* match across all authenticators
  1559. # (checked in add_authenticator) so these lists are either
  1560. # [None] or [None, 'some_authid']
  1561. authid = [x._args.get('authid', None) for x in self._authenticators.values()][-1]
  1562. authrole = [x._args.get('authrole', None) for x in self._authenticators.values()][-1]
  1563. # we need a "merged" authextra here because we can send a
  1564. # list of acceptable authmethods, but only a single
  1565. # authextra dict
  1566. authextra = self._merged_authextra()
  1567. self.join(
  1568. self.config.realm,
  1569. authmethods=list(self._authenticators.keys()),
  1570. authid=authid,
  1571. authrole=authrole,
  1572. authextra=authextra,
  1573. )
  1574. else:
  1575. self.on_connect()
  1576. def onChallenge(self, challenge):
  1577. try:
  1578. authenticator = self._authenticators[challenge.method]
  1579. except KeyError:
  1580. raise RuntimeError(
  1581. "Received challenge for unknown authmethod '{}' [authenticators={}]".format(
  1582. challenge.method,
  1583. str(sorted(self._authenticators.keys()))
  1584. )
  1585. )
  1586. return authenticator.on_challenge(self, challenge)
  1587. def onWelcome(self, msg):
  1588. if msg.authmethod is None or self._authenticators is None:
  1589. # no authentication
  1590. return
  1591. try:
  1592. authenticator = self._authenticators[msg.authmethod]
  1593. except KeyError:
  1594. raise RuntimeError(
  1595. "Received onWelcome for unknown authmethod '{}' [authenticators={}]".format(
  1596. msg.authmethod,
  1597. str(sorted(self._authenticators.keys()))
  1598. )
  1599. )
  1600. return authenticator.on_welcome(self, msg.authextra)
  1601. def onLeave(self, details):
  1602. return self.on_leave(details)
  1603. def onDisconnect(self):
  1604. return self.on_disconnect()
  1605. # experimental authentication API
  1606. def add_authenticator(self, authenticator):
  1607. assert isinstance(authenticator, IAuthenticator)
  1608. if self._authenticators is None:
  1609. self._authenticators = {}
  1610. # before adding this authenticator we need to validate that
  1611. # it's consistent with any other authenticators we may have --
  1612. # for example, they must all agree on "authid" etc because
  1613. # .join() only takes one value for all of those.
  1614. def at_most_one(name):
  1615. uni = set([
  1616. a._args[name]
  1617. for a in list(self._authenticators.values()) + [authenticator]
  1618. if name in a._args
  1619. ])
  1620. if len(uni) > 1:
  1621. raise ValueError(
  1622. "Inconsistent {}s: {}".format(
  1623. name,
  1624. ' '.join(uni),
  1625. )
  1626. )
  1627. # all authids must match
  1628. at_most_one('authid')
  1629. # all authroles must match
  1630. at_most_one('authrole')
  1631. # can we do anything else other than merge all authextra keys?
  1632. # here we check that any duplicate keys have the same values
  1633. authextra = authenticator.authextra
  1634. merged = self._merged_authextra()
  1635. for k, v in merged.items():
  1636. if k in authextra and authextra[k] != v:
  1637. raise ValueError(
  1638. "Inconsistent authextra values for '{}': '{}' vs '{}'".format(
  1639. k, v, authextra[k],
  1640. )
  1641. )
  1642. # validation complete, add it
  1643. self._authenticators[authenticator.name] = authenticator
  1644. def _merged_authextra(self):
  1645. """
  1646. internal helper
  1647. :returns: a single 'authextra' dict, consisting of all keys
  1648. from any authenticator's authextra.
  1649. Note that when the authenticator was added, we already checked
  1650. that any keys it does contain has the same value as any
  1651. existing authextra.
  1652. """
  1653. authextras = [a.authextra for a in self._authenticators.values()]
  1654. def extract_keys(x, y):
  1655. return x | set(y.keys())
  1656. unique_keys = reduce(extract_keys, authextras, set())
  1657. def first_value_for(k):
  1658. """
  1659. for anything already in self._authenticators, we checked
  1660. that it has the same value for any keys in its authextra --
  1661. so here we just extract the first one
  1662. """
  1663. for authextra in authextras:
  1664. if k in authextra:
  1665. return authextra[k]
  1666. # "can't" happen
  1667. raise ValueError(
  1668. "No values for '{}'".format(k)
  1669. )
  1670. return {
  1671. k: first_value_for(k)
  1672. for k in unique_keys
  1673. }
  1674. # these are the actual "new API" methods (i.e. snake_case)
  1675. #
  1676. def on_join(self, details):
  1677. pass
  1678. def on_leave(self, details):
  1679. self.disconnect()
  1680. def on_connect(self):
  1681. self.join(self.config.realm)
  1682. def on_disconnect(self):
  1683. pass
  1684. # ISession.register collides with the abc.ABCMeta.register method
  1685. ISession.abc_register(ApplicationSession)
  1686. class ApplicationSessionFactory(object):
  1687. """
  1688. WAMP endpoint session factory.
  1689. """
  1690. session = ApplicationSession
  1691. """
  1692. WAMP application session class to be used in this factory.
  1693. """
  1694. def __init__(self, config=None):
  1695. """
  1696. :param config: The default component configuration.
  1697. :type config: instance of :class:`autobahn.wamp.types.ComponentConfig`
  1698. """
  1699. self.config = config or types.ComponentConfig(realm="realm1")
  1700. def __call__(self):
  1701. """
  1702. Creates a new WAMP application session.
  1703. :returns: -- An instance of the WAMP application session class as
  1704. given by `self.session`.
  1705. """
  1706. session = self.session(self.config)
  1707. session.factory = self
  1708. return session