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.

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902
  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. import inspect
  27. import binascii
  28. import random
  29. from typing import Optional, Dict, Any, List, Union
  30. import txaio
  31. from autobahn.websocket.protocol import WebSocketProtocol
  32. txaio.use_twisted() # noqa
  33. from twisted.internet.defer import inlineCallbacks, succeed, Deferred
  34. from twisted.application import service
  35. from twisted.internet.interfaces import IReactorCore, IStreamClientEndpoint
  36. try:
  37. from twisted.internet.ssl import CertificateOptions
  38. except ImportError:
  39. # PyOpenSSL / TLS not available
  40. CertificateOptions = Any
  41. from autobahn.util import public
  42. from autobahn.websocket.util import parse_url as parse_ws_url
  43. from autobahn.rawsocket.util import parse_url as parse_rs_url
  44. from autobahn.twisted.websocket import WampWebSocketClientFactory
  45. from autobahn.twisted.rawsocket import WampRawSocketClientFactory
  46. from autobahn.websocket.compress import PerMessageDeflateOffer, \
  47. PerMessageDeflateResponse, PerMessageDeflateResponseAccept
  48. from autobahn.wamp import protocol, auth
  49. from autobahn.wamp.interfaces import ITransportHandler, ISession, IAuthenticator, ISerializer
  50. from autobahn.wamp.types import ComponentConfig
  51. __all__ = [
  52. 'ApplicationSession',
  53. 'ApplicationSessionFactory',
  54. 'ApplicationRunner',
  55. 'Application',
  56. 'Service',
  57. # new API
  58. 'Session',
  59. # 'run', # should probably move this method to here? instead of component
  60. ]
  61. @public
  62. class ApplicationSession(protocol.ApplicationSession):
  63. """
  64. WAMP application session for Twisted-based applications.
  65. Implements:
  66. * :class:`autobahn.wamp.interfaces.ITransportHandler`
  67. * :class:`autobahn.wamp.interfaces.ISession`
  68. """
  69. log = txaio.make_logger()
  70. ITransportHandler.register(ApplicationSession)
  71. # ISession.register collides with the abc.ABCMeta.register method
  72. ISession.abc_register(ApplicationSession)
  73. class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
  74. """
  75. WAMP application session factory for Twisted-based applications.
  76. """
  77. session: ApplicationSession = ApplicationSession
  78. """
  79. The application session class this application session factory will use. Defaults to :class:`autobahn.twisted.wamp.ApplicationSession`.
  80. """
  81. log = txaio.make_logger()
  82. @public
  83. class ApplicationRunner(object):
  84. """
  85. This class is a convenience tool mainly for development and quick hosting
  86. of WAMP application components.
  87. It can host a WAMP application component in a WAMP-over-WebSocket client
  88. connecting to a WAMP router.
  89. """
  90. log = txaio.make_logger()
  91. def __init__(self,
  92. url: str,
  93. realm: Optional[str] = None,
  94. extra: Optional[Dict[str, Any]] = None,
  95. serializers: Optional[List[ISerializer]] = None,
  96. ssl: Optional[CertificateOptions] = None,
  97. proxy: Optional[Dict[str, Any]] = None,
  98. headers: Optional[Dict[str, Any]] = None,
  99. websocket_options: Optional[Dict[str, Any]] = None,
  100. max_retries: Optional[int] = None,
  101. initial_retry_delay: Optional[float] = None,
  102. max_retry_delay: Optional[float] = None,
  103. retry_delay_growth: Optional[float] = None,
  104. retry_delay_jitter: Optional[float] = None):
  105. """
  106. :param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://example.com:8080/mypath`)
  107. :param realm: The WAMP realm to join the application session to.
  108. :param extra: Optional extra configuration to forward to the application component.
  109. :param serializers: A list of WAMP serializers to use (or None for default serializers).
  110. Serializers must implement :class:`autobahn.wamp.interfaces.ISerializer`.
  111. :type serializers: list
  112. :param ssl: (Optional). If specified this should be an
  113. instance suitable to pass as ``sslContextFactory`` to
  114. :class:`twisted.internet.endpoints.SSL4ClientEndpoint`` such
  115. as :class:`twisted.internet.ssl.CertificateOptions`. Leaving
  116. it as ``None`` will use the result of calling Twisted
  117. :meth:`twisted.internet.ssl.platformTrust` which tries to use
  118. your distribution's CA certificates.
  119. :param proxy: Explicit proxy server to use; a dict with ``host`` and ``port`` keys.
  120. :param headers: Additional headers to send (only applies to WAMP-over-WebSocket).
  121. :param websocket_options: Specific WebSocket options to set (only applies to WAMP-over-WebSocket).
  122. If not provided, conservative and practical default are chosen.
  123. :param max_retries: Maximum number of reconnection attempts. Unlimited if set to -1.
  124. :param initial_retry_delay: Initial delay for reconnection attempt in seconds (Default: 1.0s).
  125. :param max_retry_delay: Maximum delay for reconnection attempts in seconds (Default: 60s).
  126. :param retry_delay_growth: The growth factor applied to the retry delay between reconnection
  127. attempts (Default 1.5).
  128. :param retry_delay_jitter: A 0-argument callable that introduces noise into the
  129. delay (Default ``random.random``).
  130. """
  131. # IMPORTANT: keep this, as it is tested in
  132. # autobahn.twisted.test.test_tx_application_runner.TestApplicationRunner.test_runner_bad_proxy
  133. assert (proxy is None or type(proxy) == dict)
  134. self.url = url
  135. self.realm = realm
  136. self.extra = extra or dict()
  137. self.serializers = serializers
  138. self.ssl = ssl
  139. self.proxy = proxy
  140. self.headers = headers
  141. self.websocket_options = websocket_options
  142. self.max_retries = max_retries
  143. self.initial_retry_delay = initial_retry_delay
  144. self.max_retry_delay = max_retry_delay
  145. self.retry_delay_growth = retry_delay_growth
  146. self.retry_delay_jitter = retry_delay_jitter
  147. # this if for auto-reconnection when Twisted ClientService is avail
  148. self._client_service = None
  149. # total number of successful connections
  150. self._connect_successes = 0
  151. @public
  152. def stop(self):
  153. """
  154. Stop reconnecting, if auto-reconnecting was enabled.
  155. """
  156. self.log.debug('{klass}.stop()', klass=self.__class__.__name__)
  157. if self._client_service:
  158. return self._client_service.stopService()
  159. else:
  160. return succeed(None)
  161. @public
  162. def run(self, make, start_reactor: bool = True, auto_reconnect: bool = False,
  163. log_level: str = 'info', endpoint: Optional[IStreamClientEndpoint] = None,
  164. reactor: Optional[IReactorCore] = None) -> Union[type(None), Deferred]:
  165. """
  166. Run the application component.
  167. :param make: A factory that produces instances of :class:`autobahn.twisted.wamp.ApplicationSession`
  168. when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
  169. :param start_reactor: When ``True`` (the default) this method starts
  170. the Twisted reactor and doesn't return until the reactor
  171. stops. If there are any problems starting the reactor or
  172. connect()-ing, we stop the reactor and raise the exception
  173. back to the caller.
  174. :param auto_reconnect:
  175. :param log_level:
  176. :param endpoint:
  177. :param reactor:
  178. :return: None is returned, unless you specify
  179. ``start_reactor=False`` in which case the Deferred that
  180. connect() returns is returned; this will callback() with
  181. an IProtocol instance, which will actually be an instance
  182. of :class:`WampWebSocketClientProtocol`
  183. """
  184. self.log.debug('{klass}.run()', klass=self.__class__.__name__)
  185. if start_reactor:
  186. # only select framework, set loop and start logging when we are asked
  187. # start the reactor - otherwise we are running in a program that likely
  188. # already tool care of all this.
  189. from twisted.internet import reactor
  190. txaio.use_twisted()
  191. txaio.config.loop = reactor
  192. txaio.start_logging(level=log_level)
  193. if callable(make):
  194. # factory for use ApplicationSession
  195. def create():
  196. cfg = ComponentConfig(self.realm, self.extra, runner=self)
  197. try:
  198. session = make(cfg)
  199. except Exception:
  200. self.log.failure('ApplicationSession could not be instantiated: {log_failure.value}')
  201. if start_reactor and reactor.running:
  202. reactor.stop()
  203. raise
  204. else:
  205. return session
  206. else:
  207. create = make
  208. if self.url.startswith('rs'):
  209. # try to parse RawSocket URL
  210. isSecure, host, port = parse_rs_url(self.url)
  211. # use the first configured serializer if any (which means, auto-choose "best")
  212. serializer = self.serializers[0] if self.serializers else None
  213. # create a WAMP-over-RawSocket transport client factory
  214. transport_factory = WampRawSocketClientFactory(create, serializer=serializer)
  215. else:
  216. # try to parse WebSocket URL
  217. isSecure, host, port, resource, path, params = parse_ws_url(self.url)
  218. # create a WAMP-over-WebSocket transport client factory
  219. transport_factory = WampWebSocketClientFactory(create, url=self.url, serializers=self.serializers, proxy=self.proxy, headers=self.headers)
  220. # client WebSocket settings - similar to:
  221. # - http://crossbar.io/docs/WebSocket-Compression/#production-settings
  222. # - http://crossbar.io/docs/WebSocket-Options/#production-settings
  223. # The permessage-deflate extensions offered to the server
  224. offers = [PerMessageDeflateOffer()]
  225. # Function to accept permessage-deflate responses from the server
  226. def accept(response):
  227. if isinstance(response, PerMessageDeflateResponse):
  228. return PerMessageDeflateResponseAccept(response)
  229. # default WebSocket options for all client connections
  230. protocol_options = {
  231. 'version': WebSocketProtocol.DEFAULT_SPEC_VERSION,
  232. 'utf8validateIncoming': True,
  233. 'acceptMaskedServerFrames': False,
  234. 'maskClientFrames': True,
  235. 'applyMask': True,
  236. 'maxFramePayloadSize': 1048576,
  237. 'maxMessagePayloadSize': 1048576,
  238. 'autoFragmentSize': 65536,
  239. 'failByDrop': True,
  240. 'echoCloseCodeReason': False,
  241. 'serverConnectionDropTimeout': 1.,
  242. 'openHandshakeTimeout': 2.5,
  243. 'closeHandshakeTimeout': 1.,
  244. 'tcpNoDelay': True,
  245. 'perMessageCompressionOffers': offers,
  246. 'perMessageCompressionAccept': accept,
  247. 'autoPingInterval': 10.,
  248. 'autoPingTimeout': 5.,
  249. 'autoPingSize': 12,
  250. # see: https://github.com/crossbario/autobahn-python/issues/1327 and
  251. # _cancelAutoPingTimeoutCall
  252. 'autoPingRestartOnAnyTraffic': True,
  253. }
  254. # let user override above default options
  255. if self.websocket_options:
  256. protocol_options.update(self.websocket_options)
  257. # set websocket protocol options on Autobahn/Twisted protocol factory, from where it will
  258. # be applied for every Autobahn/Twisted protocol instance from the factory
  259. transport_factory.setProtocolOptions(**protocol_options)
  260. # supress pointless log noise
  261. transport_factory.noisy = False
  262. if endpoint:
  263. client = endpoint
  264. else:
  265. # if user passed ssl= but isn't using isSecure, we'll never
  266. # use the ssl argument which makes no sense.
  267. context_factory = None
  268. if self.ssl is not None:
  269. if not isSecure:
  270. raise RuntimeError(
  271. 'ssl= argument value passed to %s conflicts with the "ws:" '
  272. 'prefix of the url argument. Did you mean to use "wss:"?' %
  273. self.__class__.__name__)
  274. context_factory = self.ssl
  275. elif isSecure:
  276. from twisted.internet.ssl import optionsForClientTLS
  277. context_factory = optionsForClientTLS(host)
  278. from twisted.internet import reactor
  279. if self.proxy is not None:
  280. from twisted.internet.endpoints import TCP4ClientEndpoint
  281. client = TCP4ClientEndpoint(reactor, self.proxy['host'], self.proxy['port'])
  282. transport_factory.contextFactory = context_factory
  283. elif isSecure:
  284. from twisted.internet.endpoints import SSL4ClientEndpoint
  285. assert context_factory is not None
  286. client = SSL4ClientEndpoint(reactor, host, port, context_factory)
  287. else:
  288. from twisted.internet.endpoints import TCP4ClientEndpoint
  289. client = TCP4ClientEndpoint(reactor, host, port)
  290. # as the reactor shuts down, we wish to wait until we've sent
  291. # out our "Goodbye" message; leave() returns a Deferred that
  292. # fires when the transport gets to STATE_CLOSED
  293. def cleanup(proto):
  294. if hasattr(proto, '_session') and proto._session is not None:
  295. if proto._session.is_attached():
  296. return proto._session.leave()
  297. elif proto._session.is_connected():
  298. return proto._session.disconnect()
  299. # when our proto was created and connected, make sure it's cleaned
  300. # up properly later on when the reactor shuts down for whatever reason
  301. def init_proto(proto):
  302. self._connect_successes += 1
  303. reactor.addSystemEventTrigger('before', 'shutdown', cleanup, proto)
  304. return proto
  305. use_service = False
  306. if auto_reconnect:
  307. try:
  308. # since Twisted 16.1.0
  309. from twisted.application.internet import ClientService
  310. from twisted.application.internet import backoffPolicy
  311. use_service = True
  312. except ImportError:
  313. use_service = False
  314. if use_service:
  315. # this code path is automatically reconnecting ..
  316. self.log.debug('using t.a.i.ClientService')
  317. if (self.max_retries is not None or self.initial_retry_delay is not None or self.max_retry_delay is not None or self.retry_delay_growth is not None or self.retry_delay_jitter is not None):
  318. if self.max_retry_delay > 0:
  319. kwargs = {}
  320. def _jitter():
  321. j = 1 if self.retry_delay_jitter is None else self.retry_delay_jitter
  322. return random.random() * j
  323. for key, val in [('initialDelay', self.initial_retry_delay),
  324. ('maxDelay', self.max_retry_delay),
  325. ('factor', self.retry_delay_growth),
  326. ('jitter', _jitter)]:
  327. if val is not None:
  328. kwargs[key] = val
  329. # retry policy that will only try to reconnect if we connected
  330. # successfully at least once before (so it fails on host unreachable etc ..)
  331. def retry(failed_attempts):
  332. if self._connect_successes > 0 and (self.max_retries == -1 or failed_attempts < self.max_retries):
  333. return backoffPolicy(**kwargs)(failed_attempts)
  334. else:
  335. print('hit stop')
  336. self.stop()
  337. return 100000000000000
  338. else:
  339. # immediately reconnect (zero delay)
  340. def retry(_):
  341. return 0
  342. else:
  343. retry = backoffPolicy()
  344. # https://twistedmatrix.com/documents/current/api/twisted.application.internet.ClientService.html
  345. self._client_service = ClientService(client, transport_factory, retryPolicy=retry)
  346. self._client_service.startService()
  347. d = self._client_service.whenConnected()
  348. else:
  349. # this code path is only connecting once!
  350. self.log.debug('using t.i.e.connect()')
  351. d = client.connect(transport_factory)
  352. # if we connect successfully, the arg is a WampWebSocketClientProtocol
  353. d.addCallback(init_proto)
  354. # if the user didn't ask us to start the reactor, then they
  355. # get to deal with any connect errors themselves.
  356. if start_reactor:
  357. # if an error happens in the connect(), we save the underlying
  358. # exception so that after the event-loop exits we can re-raise
  359. # it to the caller.
  360. class ErrorCollector(object):
  361. exception = None
  362. def __call__(self, failure):
  363. self.exception = failure.value
  364. reactor.stop()
  365. connect_error = ErrorCollector()
  366. d.addErrback(connect_error)
  367. # now enter the Twisted reactor loop
  368. reactor.run()
  369. # if the ApplicationSession sets an "error" key on the self.config.extra dictionary, which
  370. # has been set to the self.extra dictionary, extract the Exception from that and re-raise
  371. # it as the very last one (see below) exciting back to the caller of self.run()
  372. app_error = self.extra.get('error', None)
  373. # if we exited due to a connection error, raise that to the caller
  374. if connect_error.exception:
  375. raise connect_error.exception
  376. elif app_error:
  377. raise app_error
  378. else:
  379. # let the caller handle any errors
  380. return d
  381. class _ApplicationSession(ApplicationSession):
  382. """
  383. WAMP application session class used internally with :class:`autobahn.twisted.app.Application`.
  384. """
  385. def __init__(self, config, app):
  386. """
  387. :param config: The component configuration.
  388. :type config: Instance of :class:`autobahn.wamp.types.ComponentConfig`
  389. :param app: The application this session is for.
  390. :type app: Instance of :class:`autobahn.twisted.wamp.Application`.
  391. """
  392. # noinspection PyArgumentList
  393. ApplicationSession.__init__(self, config)
  394. self.app = app
  395. @inlineCallbacks
  396. def onConnect(self):
  397. """
  398. Implements :meth:`autobahn.wamp.interfaces.ISession.onConnect`
  399. """
  400. yield self.app._fire_signal('onconnect')
  401. self.join(self.config.realm)
  402. @inlineCallbacks
  403. def onJoin(self, details):
  404. """
  405. Implements :meth:`autobahn.wamp.interfaces.ISession.onJoin`
  406. """
  407. for uri, proc in self.app._procs:
  408. yield self.register(proc, uri)
  409. for uri, handler in self.app._handlers:
  410. yield self.subscribe(handler, uri)
  411. yield self.app._fire_signal('onjoined')
  412. @inlineCallbacks
  413. def onLeave(self, details):
  414. """
  415. Implements :meth:`autobahn.wamp.interfaces.ISession.onLeave`
  416. """
  417. yield self.app._fire_signal('onleave')
  418. self.disconnect()
  419. @inlineCallbacks
  420. def onDisconnect(self):
  421. """
  422. Implements :meth:`autobahn.wamp.interfaces.ISession.onDisconnect`
  423. """
  424. yield self.app._fire_signal('ondisconnect')
  425. class Application(object):
  426. """
  427. A WAMP application. The application object provides a simple way of
  428. creating, debugging and running WAMP application components.
  429. """
  430. log = txaio.make_logger()
  431. def __init__(self, prefix=None):
  432. """
  433. :param prefix: The application URI prefix to use for procedures and topics,
  434. e.g. ``"com.example.myapp"``.
  435. :type prefix: unicode
  436. """
  437. self._prefix = prefix
  438. # procedures to be registered once the app session has joined the router/realm
  439. self._procs = []
  440. # event handler to be subscribed once the app session has joined the router/realm
  441. self._handlers = []
  442. # app lifecycle signal handlers
  443. self._signals = {}
  444. # once an app session is connected, this will be here
  445. self.session = None
  446. def __call__(self, config):
  447. """
  448. Factory creating a WAMP application session for the application.
  449. :param config: Component configuration.
  450. :type config: Instance of :class:`autobahn.wamp.types.ComponentConfig`
  451. :returns: obj -- An object that derives of
  452. :class:`autobahn.twisted.wamp.ApplicationSession`
  453. """
  454. assert(self.session is None)
  455. self.session = _ApplicationSession(config, self)
  456. return self.session
  457. def run(self, url="ws://localhost:8080/ws", realm="realm1", start_reactor=True):
  458. """
  459. Run the application.
  460. :param url: The URL of the WAMP router to connect to.
  461. :type url: unicode
  462. :param realm: The realm on the WAMP router to join.
  463. :type realm: unicode
  464. """
  465. runner = ApplicationRunner(url, realm)
  466. return runner.run(self.__call__, start_reactor)
  467. def register(self, uri=None):
  468. """
  469. Decorator exposing a function as a remote callable procedure.
  470. The first argument of the decorator should be the URI of the procedure
  471. to register under.
  472. :Example:
  473. .. code-block:: python
  474. @app.register('com.myapp.add2')
  475. def add2(a, b):
  476. return a + b
  477. Above function can then be called remotely over WAMP using the URI `com.myapp.add2`
  478. the function was registered under.
  479. If no URI is given, the URI is constructed from the application URI prefix
  480. and the Python function name.
  481. :Example:
  482. .. code-block:: python
  483. app = Application('com.myapp')
  484. # implicit URI will be 'com.myapp.add2'
  485. @app.register()
  486. def add2(a, b):
  487. return a + b
  488. If the function `yields` (is a co-routine), the `@inlineCallbacks` decorator
  489. will be applied automatically to it. In that case, if you wish to return something,
  490. you should use `returnValue`:
  491. :Example:
  492. .. code-block:: python
  493. from twisted.internet.defer import returnValue
  494. @app.register('com.myapp.add2')
  495. def add2(a, b):
  496. res = yield stuff(a, b)
  497. returnValue(res)
  498. :param uri: The URI of the procedure to register under.
  499. :type uri: unicode
  500. """
  501. def decorator(func):
  502. if uri:
  503. _uri = uri
  504. else:
  505. assert(self._prefix is not None)
  506. _uri = "{0}.{1}".format(self._prefix, func.__name__)
  507. if inspect.isgeneratorfunction(func):
  508. func = inlineCallbacks(func)
  509. self._procs.append((_uri, func))
  510. return func
  511. return decorator
  512. def subscribe(self, uri=None):
  513. """
  514. Decorator attaching a function as an event handler.
  515. The first argument of the decorator should be the URI of the topic
  516. to subscribe to. If no URI is given, the URI is constructed from
  517. the application URI prefix and the Python function name.
  518. If the function yield, it will be assumed that it's an asynchronous
  519. process and inlineCallbacks will be applied to it.
  520. :Example:
  521. .. code-block:: python
  522. @app.subscribe('com.myapp.topic1')
  523. def onevent1(x, y):
  524. print("got event on topic1", x, y)
  525. :param uri: The URI of the topic to subscribe to.
  526. :type uri: unicode
  527. """
  528. def decorator(func):
  529. if uri:
  530. _uri = uri
  531. else:
  532. assert(self._prefix is not None)
  533. _uri = "{0}.{1}".format(self._prefix, func.__name__)
  534. if inspect.isgeneratorfunction(func):
  535. func = inlineCallbacks(func)
  536. self._handlers.append((_uri, func))
  537. return func
  538. return decorator
  539. def signal(self, name):
  540. """
  541. Decorator attaching a function as handler for application signals.
  542. Signals are local events triggered internally and exposed to the
  543. developer to be able to react to the application lifecycle.
  544. If the function yield, it will be assumed that it's an asynchronous
  545. coroutine and inlineCallbacks will be applied to it.
  546. Current signals :
  547. - `onjoined`: Triggered after the application session has joined the
  548. realm on the router and registered/subscribed all procedures
  549. and event handlers that were setup via decorators.
  550. - `onleave`: Triggered when the application session leaves the realm.
  551. .. code-block:: python
  552. @app.signal('onjoined')
  553. def _():
  554. # do after the app has join a realm
  555. :param name: The name of the signal to watch.
  556. :type name: unicode
  557. """
  558. def decorator(func):
  559. if inspect.isgeneratorfunction(func):
  560. func = inlineCallbacks(func)
  561. self._signals.setdefault(name, []).append(func)
  562. return func
  563. return decorator
  564. @inlineCallbacks
  565. def _fire_signal(self, name, *args, **kwargs):
  566. """
  567. Utility method to call all signal handlers for a given signal.
  568. :param name: The signal name.
  569. :type name: str
  570. """
  571. for handler in self._signals.get(name, []):
  572. try:
  573. # FIXME: what if the signal handler is not a coroutine?
  574. # Why run signal handlers synchronously?
  575. yield handler(*args, **kwargs)
  576. except Exception as e:
  577. # FIXME
  578. self.log.info("Warning: exception in signal handler swallowed: {err}", err=e)
  579. class Service(service.MultiService):
  580. """
  581. A WAMP application as a twisted service.
  582. The application object provides a simple way of creating, debugging and running WAMP application
  583. components inside a traditional twisted application
  584. This manages application lifecycle of the wamp connection using startService and stopService
  585. Using services also allows to create integration tests that properly terminates their connections
  586. It can host a WAMP application component in a WAMP-over-WebSocket client
  587. connecting to a WAMP router.
  588. """
  589. factory = WampWebSocketClientFactory
  590. def __init__(self, url, realm, make, extra=None, context_factory=None):
  591. """
  592. :param url: The WebSocket URL of the WAMP router to connect to (e.g. `ws://somehost.com:8090/somepath`)
  593. :type url: unicode
  594. :param realm: The WAMP realm to join the application session to.
  595. :type realm: unicode
  596. :param make: A factory that produces instances of :class:`autobahn.asyncio.wamp.ApplicationSession`
  597. when called with an instance of :class:`autobahn.wamp.types.ComponentConfig`.
  598. :type make: callable
  599. :param extra: Optional extra configuration to forward to the application component.
  600. :type extra: dict
  601. :param context_factory: optional, only for secure connections. Passed as contextFactory to
  602. the ``listenSSL()`` call; see https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IReactorSSL.connectSSL.html
  603. :type context_factory: twisted.internet.ssl.ClientContextFactory or None
  604. You can replace the attribute factory in order to change connectionLost or connectionFailed behaviour.
  605. The factory attribute must return a WampWebSocketClientFactory object
  606. """
  607. self.url = url
  608. self.realm = realm
  609. self.extra = extra or dict()
  610. self.make = make
  611. self.context_factory = context_factory
  612. service.MultiService.__init__(self)
  613. self.setupService()
  614. def setupService(self):
  615. """
  616. Setup the application component.
  617. """
  618. is_secure, host, port, resource, path, params = parse_ws_url(self.url)
  619. # factory for use ApplicationSession
  620. def create():
  621. cfg = ComponentConfig(self.realm, self.extra)
  622. session = self.make(cfg)
  623. return session
  624. # create a WAMP-over-WebSocket transport client factory
  625. transport_factory = self.factory(create, url=self.url)
  626. # setup the client from a Twisted endpoint
  627. if is_secure:
  628. from twisted.application.internet import SSLClient
  629. ctx = self.context_factory
  630. if ctx is None:
  631. from twisted.internet.ssl import optionsForClientTLS
  632. ctx = optionsForClientTLS(host)
  633. client = SSLClient(host, port, transport_factory, contextFactory=ctx)
  634. else:
  635. if self.context_factory is not None:
  636. raise Exception("context_factory specified on non-secure URI")
  637. from twisted.application.internet import TCPClient
  638. client = TCPClient(host, port, transport_factory)
  639. client.setServiceParent(self)
  640. # new API
  641. class Session(protocol._SessionShim):
  642. # XXX these methods are redundant, but put here for possibly
  643. # better clarity; maybe a bad idea.
  644. def on_welcome(self, welcome_msg):
  645. pass
  646. def on_join(self, details):
  647. pass
  648. def on_leave(self, details):
  649. self.disconnect()
  650. def on_connect(self):
  651. self.join(self.config.realm)
  652. def on_disconnect(self):
  653. pass
  654. # experimental authentication API
  655. class AuthCryptoSign(object):
  656. def __init__(self, **kw):
  657. # should put in checkconfig or similar
  658. for key in kw.keys():
  659. if key not in ['authextra', 'authid', 'authrole', 'privkey']:
  660. raise ValueError(
  661. "Unexpected key '{}' for {}".format(key, self.__class__.__name__)
  662. )
  663. for key in ['privkey']:
  664. if key not in kw:
  665. raise ValueError(
  666. "Must provide '{}' for cryptosign".format(key)
  667. )
  668. for key in kw.get('authextra', dict()):
  669. if key not in ['pubkey', 'channel_binding', 'trustroot', 'challenge']:
  670. raise ValueError(
  671. "Unexpected key '{}' in 'authextra'".format(key)
  672. )
  673. from autobahn.wamp.cryptosign import CryptosignKey
  674. self._privkey = CryptosignKey.from_bytes(
  675. binascii.a2b_hex(kw['privkey'])
  676. )
  677. if 'pubkey' in kw.get('authextra', dict()):
  678. pubkey = kw['authextra']['pubkey']
  679. if pubkey != self._privkey.public_key():
  680. raise ValueError(
  681. "Public key doesn't correspond to private key"
  682. )
  683. else:
  684. kw['authextra'] = kw.get('authextra', dict())
  685. kw['authextra']['pubkey'] = self._privkey.public_key()
  686. self._args = kw
  687. def on_challenge(self, session, challenge):
  688. # sign the challenge with our private key.
  689. channel_id_type = self._args['authextra'].get('channel_binding', None)
  690. channel_id = self.transport.transport_details.channel_id.get(channel_id_type, None)
  691. signed_challenge = self._privkey.sign_challenge(challenge, channel_id=channel_id,
  692. channel_id_type=channel_id_type)
  693. return signed_challenge
  694. IAuthenticator.register(AuthCryptoSign)
  695. class AuthWampCra(object):
  696. def __init__(self, **kw):
  697. # should put in checkconfig or similar
  698. for key in kw.keys():
  699. if key not in ['authextra', 'authid', 'authrole', 'secret']:
  700. raise ValueError(
  701. "Unexpected key '{}' for {}".format(key, self.__class__.__name__)
  702. )
  703. for key in ['secret', 'authid']:
  704. if key not in kw:
  705. raise ValueError(
  706. "Must provide '{}' for wampcra".format(key)
  707. )
  708. self._args = kw
  709. self._secret = kw.pop('secret')
  710. if not isinstance(self._secret, str):
  711. self._secret = self._secret.decode('utf8')
  712. def on_challenge(self, session, challenge):
  713. key = self._secret.encode('utf8')
  714. if 'salt' in challenge.extra:
  715. key = auth.derive_key(
  716. key,
  717. challenge.extra['salt'],
  718. challenge.extra['iterations'],
  719. challenge.extra['keylen']
  720. )
  721. signature = auth.compute_wcs(
  722. key,
  723. challenge.extra['challenge'].encode('utf8')
  724. )
  725. return signature.decode('ascii')
  726. IAuthenticator.register(AuthWampCra)