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.

websocket.py 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  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 base64 import b64encode, b64decode
  27. from typing import Optional
  28. from zope.interface import implementer
  29. import txaio
  30. txaio.use_twisted()
  31. import twisted.internet.protocol
  32. from twisted.internet import endpoints
  33. from twisted.internet.interfaces import ITransport
  34. from twisted.internet.error import ConnectionDone, ConnectionAborted, \
  35. ConnectionLost
  36. from twisted.internet.defer import Deferred
  37. from twisted.python.failure import Failure
  38. from twisted.internet.protocol import connectionDone
  39. from autobahn.util import public, hltype, hlval
  40. from autobahn.util import _is_tls_error, _maybe_tls_reason
  41. from autobahn.wamp import websocket
  42. from autobahn.wamp.types import TransportDetails
  43. from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, ConnectionDeny
  44. from autobahn.websocket import protocol
  45. from autobahn.websocket.interfaces import IWebSocketClientAgent
  46. from autobahn.twisted.util import create_transport_details, transport_channel_id
  47. from autobahn.websocket.compress import PerMessageDeflateOffer, \
  48. PerMessageDeflateOfferAccept, \
  49. PerMessageDeflateResponse, \
  50. PerMessageDeflateResponseAccept
  51. __all__ = (
  52. 'create_client_agent',
  53. 'WebSocketAdapterProtocol',
  54. 'WebSocketServerProtocol',
  55. 'WebSocketClientProtocol',
  56. 'WebSocketAdapterFactory',
  57. 'WebSocketServerFactory',
  58. 'WebSocketClientFactory',
  59. 'WrappingWebSocketAdapter',
  60. 'WrappingWebSocketServerProtocol',
  61. 'WrappingWebSocketClientProtocol',
  62. 'WrappingWebSocketServerFactory',
  63. 'WrappingWebSocketClientFactory',
  64. 'listenWS',
  65. 'connectWS',
  66. 'WampWebSocketServerProtocol',
  67. 'WampWebSocketServerFactory',
  68. 'WampWebSocketClientProtocol',
  69. 'WampWebSocketClientFactory',
  70. )
  71. def create_client_agent(reactor):
  72. """
  73. :returns: an instance implementing IWebSocketClientAgent
  74. """
  75. return _TwistedWebSocketClientAgent(reactor)
  76. def check_transport_config(transport_config):
  77. """
  78. raises a ValueError if `transport_config` is invalid
  79. """
  80. # XXX move me to "autobahn.websocket.util"
  81. if not isinstance(transport_config, str):
  82. raise ValueError(
  83. "'transport_config' must be a string, found {}".format(type(transport_config))
  84. )
  85. # XXX also accept everything Crossbar has in client transport configs? e.g like:
  86. # { "type": "websocket", "endpoint": {"type": "tcp", "host": "example.com", ...}}
  87. # XXX what about TLS options? (the above point would address that too)
  88. if not transport_config.startswith("ws://") and \
  89. not transport_config.startswith("wss://"):
  90. raise ValueError(
  91. "'transport_config' must start with 'ws://' or 'wss://'"
  92. )
  93. return None
  94. def check_client_options(options):
  95. """
  96. raises a ValueError if `options` is invalid
  97. """
  98. # XXX move me to "autobahn.websocket.util"
  99. if not isinstance(options, dict):
  100. raise ValueError(
  101. "'options' must be a dict"
  102. )
  103. # anything that WebSocketClientFactory accepts (at least)
  104. valid_keys = [
  105. "origin",
  106. "protocols",
  107. "useragent",
  108. "headers",
  109. "proxy",
  110. ]
  111. for actual_k in options.keys():
  112. if actual_k not in valid_keys:
  113. raise ValueError(
  114. "'options' may not contain '{}'".format(actual_k)
  115. )
  116. def _endpoint_from_config(reactor, factory, transport_config, options):
  117. # XXX might want some Crossbar code here? e.g. if we allow
  118. # "transport_config" to be a dict etc.
  119. # ... passing in the Factory is weird, but that's what parses all
  120. # the options and the URL currently
  121. if factory.isSecure:
  122. # create default client SSL context factory when none given
  123. from twisted.internet import ssl
  124. context_factory = ssl.optionsForClientTLS(factory.host)
  125. if factory.proxy is not None:
  126. factory.contextFactory = context_factory
  127. endpoint = endpoints.HostnameEndpoint(
  128. reactor,
  129. factory.proxy['host'],
  130. factory.proxy['port'],
  131. # timeout, option?
  132. )
  133. else:
  134. if factory.isSecure:
  135. from twisted.internet import ssl
  136. endpoint = endpoints.SSL4ClientEndpoint(
  137. reactor,
  138. factory.host,
  139. factory.port,
  140. context_factory,
  141. # timeout, option?
  142. )
  143. else:
  144. endpoint = endpoints.HostnameEndpoint( # XXX right? not TCP4ClientEndpoint
  145. reactor,
  146. factory.host,
  147. factory.port,
  148. # timeout, option?
  149. # attemptDelay, option?
  150. )
  151. return endpoint
  152. class _TwistedWebSocketClientAgent(IWebSocketClientAgent):
  153. """
  154. This agent creates connections using Twisted
  155. """
  156. def __init__(self, reactor):
  157. self._reactor = reactor
  158. def open(self, transport_config, options, protocol_class=None):
  159. """
  160. Open a new connection.
  161. :param dict transport_config: valid transport configuration
  162. :param dict options: additional options for the factory
  163. :param protocol_class: a callable that returns an instance of
  164. the protocol (WebSocketClientProtocol if the default None
  165. is passed in)
  166. :returns: a Deferred that fires with an instance of
  167. `protocol_class` (or WebSocketClientProtocol by default)
  168. that has successfully shaken hands (completed the
  169. handshake).
  170. """
  171. check_transport_config(transport_config)
  172. check_client_options(options)
  173. factory = WebSocketClientFactory(
  174. url=transport_config,
  175. reactor=self._reactor,
  176. **options
  177. )
  178. factory.protocol = WebSocketClientProtocol if protocol_class is None else protocol_class
  179. # XXX might want "contextFactory" for TLS ...? (or e.g. CA etc options?)
  180. endpoint = _endpoint_from_config(self._reactor, factory, transport_config, options)
  181. rtn_d = Deferred()
  182. proto_d = endpoint.connect(factory)
  183. def failed(f):
  184. rtn_d.errback(f)
  185. def got_proto(proto):
  186. def handshake_completed(arg):
  187. rtn_d.callback(proto)
  188. return arg
  189. proto.is_open.addCallbacks(handshake_completed, failed)
  190. return proto
  191. proto_d.addCallbacks(got_proto, failed)
  192. return rtn_d
  193. class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
  194. """
  195. Adapter class for Twisted WebSocket client and server protocols.
  196. Called from Twisted:
  197. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionMade`
  198. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionLost`
  199. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.dataReceived`
  200. Called from Network-independent Code (WebSocket implementation):
  201. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onOpen`
  202. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageBegin`
  203. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageFrameData`
  204. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageFrameEnd`
  205. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageEnd`
  206. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessage`
  207. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onPing`
  208. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onPong`
  209. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onClose`
  210. FIXME:
  211. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._closeConnection`
  212. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._create_transport_details`
  213. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.registerProducer`
  214. * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.unregisterProducer`
  215. """
  216. log = txaio.make_logger()
  217. peer: Optional[str] = None
  218. is_server: Optional[bool] = None
  219. def connectionMade(self):
  220. # Twisted networking framework entry point, called by Twisted
  221. # when the connection is established (either a client or a server)
  222. # determine preliminary transport details (what is know at this point)
  223. self._transport_details = create_transport_details(self.transport, self.is_server)
  224. self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET
  225. # backward compatibility
  226. self.peer = self._transport_details.peer
  227. # try to set "Nagle" option for TCP sockets
  228. try:
  229. self.transport.setTcpNoDelay(self.tcpNoDelay)
  230. except: # don't touch this! does not work: AttributeError, OSError
  231. # eg Unix Domain sockets throw Errno 22 on this
  232. pass
  233. # ok, now forward to the networking framework independent code for websocket
  234. self._connectionMade()
  235. # ok, done!
  236. self.log.debug('{func} connection established for peer="{peer}"',
  237. func=hltype(self.connectionMade),
  238. peer=hlval(self.peer))
  239. def connectionLost(self, reason: Failure = connectionDone):
  240. # Twisted networking framework entry point, called by Twisted
  241. # when the connection is lost (either a client or a server)
  242. was_clean = False
  243. if isinstance(reason.value, ConnectionDone):
  244. self.log.debug("Connection to/from {peer} was closed cleanly",
  245. peer=self.peer)
  246. was_clean = True
  247. elif _is_tls_error(reason.value):
  248. self.log.error(_maybe_tls_reason(reason.value))
  249. elif isinstance(reason.value, ConnectionAborted):
  250. self.log.debug("Connection to/from {peer} was aborted locally",
  251. peer=self.peer)
  252. elif isinstance(reason.value, ConnectionLost):
  253. message = str(reason.value)
  254. if hasattr(reason.value, 'message'):
  255. message = reason.value.message
  256. self.log.debug(
  257. "Connection to/from {peer} was lost in a non-clean fashion: {message}",
  258. peer=self.peer,
  259. message=message,
  260. )
  261. # at least: FileDescriptorOverrun, ConnectionFdescWentAway - but maybe others as well?
  262. else:
  263. self.log.debug("Connection to/from {peer} lost ({error_type}): {error})",
  264. peer=self.peer, error_type=type(reason.value), error=reason.value)
  265. # ok, now forward to the networking framework independent code for websocket
  266. self._connectionLost(reason)
  267. # ok, done!
  268. if was_clean:
  269. self.log.debug('{func} connection lost for peer="{peer}", closed cleanly',
  270. func=hltype(self.connectionLost),
  271. peer=hlval(self.peer))
  272. else:
  273. self.log.debug('{func} connection lost for peer="{peer}", closed with error {reason}',
  274. func=hltype(self.connectionLost),
  275. peer=hlval(self.peer),
  276. reason=reason)
  277. def dataReceived(self, data: bytes):
  278. self.log.debug('{func} received {data_len} bytes for peer="{peer}"',
  279. func=hltype(self.dataReceived),
  280. peer=hlval(self.peer),
  281. data_len=hlval(len(data)))
  282. # bytes received from Twisted, forward to the networking framework independent code for websocket
  283. self._dataReceived(data)
  284. def _closeConnection(self, abort=False):
  285. if abort and hasattr(self.transport, 'abortConnection'):
  286. self.transport.abortConnection()
  287. else:
  288. # e.g. ProcessProtocol lacks abortConnection()
  289. self.transport.loseConnection()
  290. def _onOpen(self):
  291. if self._transport_details.is_secure:
  292. # now that the TLS opening handshake is complete, the actual TLS channel ID
  293. # will be available. make sure to set it!
  294. channel_id = {
  295. 'tls-unique': transport_channel_id(self.transport, self._transport_details.is_server, 'tls-unique'),
  296. }
  297. self._transport_details.channel_id = channel_id
  298. self.onOpen()
  299. def _onMessageBegin(self, isBinary):
  300. self.onMessageBegin(isBinary)
  301. def _onMessageFrameBegin(self, length):
  302. self.onMessageFrameBegin(length)
  303. def _onMessageFrameData(self, payload):
  304. self.onMessageFrameData(payload)
  305. def _onMessageFrameEnd(self):
  306. self.onMessageFrameEnd()
  307. def _onMessageFrame(self, payload):
  308. self.onMessageFrame(payload)
  309. def _onMessageEnd(self):
  310. self.onMessageEnd()
  311. def _onMessage(self, payload, isBinary):
  312. self.onMessage(payload, isBinary)
  313. def _onPing(self, payload):
  314. self.onPing(payload)
  315. def _onPong(self, payload):
  316. self.onPong(payload)
  317. def _onClose(self, wasClean, code, reason):
  318. self.onClose(wasClean, code, reason)
  319. def registerProducer(self, producer, streaming):
  320. """
  321. Register a Twisted producer with this protocol.
  322. :param producer: A Twisted push or pull producer.
  323. :type producer: object
  324. :param streaming: Producer type.
  325. :type streaming: bool
  326. """
  327. self.transport.registerProducer(producer, streaming)
  328. def unregisterProducer(self):
  329. """
  330. Unregister Twisted producer with this protocol.
  331. """
  332. self.transport.unregisterProducer()
  333. @public
  334. class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServerProtocol):
  335. """
  336. Base class for Twisted-based WebSocket server protocols.
  337. Implements :class:`autobahn.websocket.interfaces.IWebSocketChannel`.
  338. """
  339. log = txaio.make_logger()
  340. is_server = True
  341. # def onConnect(self, request: ConnectionRequest) -> Union[Optional[str], Tuple[Optional[str], Dict[str, str]]]:
  342. # pass
  343. @public
  344. class WebSocketClientProtocol(WebSocketAdapterProtocol, protocol.WebSocketClientProtocol):
  345. """
  346. Base class for Twisted-based WebSocket client protocols.
  347. Implements :class:`autobahn.websocket.interfaces.IWebSocketChannel`.
  348. """
  349. log = txaio.make_logger()
  350. is_server = False
  351. def _onConnect(self, response: ConnectionResponse):
  352. self.log.debug('{meth}(response={response})', meth=hltype(self._onConnect), response=response)
  353. return self.onConnect(response)
  354. def startTLS(self):
  355. self.log.debug("Starting TLS upgrade")
  356. self.transport.startTLS(self.factory.contextFactory)
  357. class WebSocketAdapterFactory(object):
  358. """
  359. Adapter class for Twisted-based WebSocket client and server factories.
  360. """
  361. @public
  362. class WebSocketServerFactory(WebSocketAdapterFactory, protocol.WebSocketServerFactory, twisted.internet.protocol.ServerFactory):
  363. """
  364. Base class for Twisted-based WebSocket server factories.
  365. Implements :class:`autobahn.websocket.interfaces.IWebSocketServerChannelFactory`
  366. """
  367. log = txaio.make_logger()
  368. def __init__(self, *args, **kwargs):
  369. """
  370. .. note::
  371. In addition to all arguments to the constructor of
  372. :meth:`autobahn.websocket.interfaces.IWebSocketServerChannelFactory`,
  373. you can supply a ``reactor`` keyword argument to specify the
  374. Twisted reactor to be used.
  375. """
  376. # lazy import to avoid reactor install upon module import
  377. reactor = kwargs.pop('reactor', None)
  378. if reactor is None:
  379. from twisted.internet import reactor
  380. self.reactor = reactor
  381. protocol.WebSocketServerFactory.__init__(self, *args, **kwargs)
  382. @public
  383. class WebSocketClientFactory(WebSocketAdapterFactory, protocol.WebSocketClientFactory, twisted.internet.protocol.ClientFactory):
  384. """
  385. Base class for Twisted-based WebSocket client factories.
  386. Implements :class:`autobahn.websocket.interfaces.IWebSocketClientChannelFactory`
  387. """
  388. log = txaio.make_logger()
  389. def __init__(self, *args, **kwargs):
  390. """
  391. .. note::
  392. In addition to all arguments to the constructor of
  393. :func:`autobahn.websocket.interfaces.IWebSocketClientChannelFactory`,
  394. you can supply a ``reactor`` keyword argument to specify the
  395. Twisted reactor to be used.
  396. """
  397. # lazy import to avoid reactor install upon module import
  398. reactor = kwargs.pop('reactor', None)
  399. if reactor is None:
  400. from twisted.internet import reactor
  401. self.reactor = reactor
  402. protocol.WebSocketClientFactory.__init__(self, *args, **kwargs)
  403. # we must up-call *before* we set up the contextFactory
  404. # because we need self.host etc to be set properly.
  405. if self.isSecure and self.proxy is not None:
  406. # if we have a proxy, then our factory will be used to
  407. # create the connection after CONNECT and if it's doing
  408. # TLS it needs a contextFactory
  409. from twisted.internet import ssl
  410. self.contextFactory = ssl.optionsForClientTLS(self.host)
  411. # NOTE: there's thus no way to send in our own
  412. # context-factory, nor any TLS options.
  413. # Possibly we should allow 'proxy' to contain an actual
  414. # IStreamClientEndpoint instance instead of configuration for
  415. # how to make one
  416. @implementer(ITransport)
  417. class WrappingWebSocketAdapter(object):
  418. """
  419. An adapter for stream-based transport over WebSocket.
  420. This follows `websockify <https://github.com/kanaka/websockify>`_
  421. and should be compatible with that.
  422. It uses WebSocket subprotocol negotiation and supports the
  423. following WebSocket subprotocols:
  424. - ``binary`` (or a compatible subprotocol)
  425. - ``base64``
  426. Octets are either transmitted as the payload of WebSocket binary
  427. messages when using the ``binary`` subprotocol (or an alternative
  428. binary compatible subprotocol), or encoded with Base64 and then
  429. transmitted as the payload of WebSocket text messages when using
  430. the ``base64`` subprotocol.
  431. """
  432. def onConnect(self, requestOrResponse):
  433. # Negotiate either the 'binary' or the 'base64' WebSocket subprotocol
  434. if isinstance(requestOrResponse, ConnectionRequest):
  435. request = requestOrResponse
  436. for p in request.protocols:
  437. if p in self.factory._subprotocols:
  438. self._binaryMode = (p != 'base64')
  439. return p
  440. raise ConnectionDeny(ConnectionDeny.NOT_ACCEPTABLE, 'this server only speaks {0} WebSocket subprotocols'.format(self.factory._subprotocols))
  441. elif isinstance(requestOrResponse, ConnectionResponse):
  442. response = requestOrResponse
  443. if response.protocol not in self.factory._subprotocols:
  444. self._fail_connection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, 'this client only speaks {0} WebSocket subprotocols'.format(self.factory._subprotocols))
  445. self._binaryMode = (response.protocol != 'base64')
  446. else:
  447. # should not arrive here
  448. raise Exception("logic error")
  449. def onOpen(self):
  450. self._proto.connectionMade()
  451. def onMessage(self, payload, isBinary):
  452. if isBinary != self._binaryMode:
  453. self._fail_connection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_UNSUPPORTED_DATA, 'message payload type does not match the negotiated subprotocol')
  454. else:
  455. if not isBinary:
  456. try:
  457. payload = b64decode(payload)
  458. except Exception as e:
  459. self._fail_connection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_INVALID_PAYLOAD, 'message payload base64 decoding error: {0}'.format(e))
  460. self._proto.dataReceived(payload)
  461. # noinspection PyUnusedLocal
  462. def onClose(self, wasClean, code, reason):
  463. self._proto.connectionLost(None)
  464. def write(self, data):
  465. # part of ITransport
  466. assert(type(data) == bytes)
  467. if self._binaryMode:
  468. self.sendMessage(data, isBinary=True)
  469. else:
  470. data = b64encode(data)
  471. self.sendMessage(data, isBinary=False)
  472. def writeSequence(self, data):
  473. # part of ITransport
  474. for d in data:
  475. self.write(d)
  476. def loseConnection(self):
  477. # part of ITransport
  478. self.sendClose()
  479. def getPeer(self):
  480. # part of ITransport
  481. return self.transport.getPeer()
  482. def getHost(self):
  483. # part of ITransport
  484. return self.transport.getHost()
  485. class WrappingWebSocketServerProtocol(WrappingWebSocketAdapter, WebSocketServerProtocol):
  486. """
  487. Server protocol for stream-based transport over WebSocket.
  488. """
  489. class WrappingWebSocketClientProtocol(WrappingWebSocketAdapter, WebSocketClientProtocol):
  490. """
  491. Client protocol for stream-based transport over WebSocket.
  492. """
  493. class WrappingWebSocketServerFactory(WebSocketServerFactory):
  494. """
  495. Wrapping server factory for stream-based transport over WebSocket.
  496. """
  497. def __init__(self,
  498. factory,
  499. url,
  500. reactor=None,
  501. enableCompression=True,
  502. autoFragmentSize=0,
  503. subprotocol=None):
  504. """
  505. :param factory: Stream-based factory to be wrapped.
  506. :type factory: A subclass of ``twisted.internet.protocol.Factory``
  507. :param url: WebSocket URL of the server this server factory will work for.
  508. :type url: unicode
  509. """
  510. self._factory = factory
  511. self._subprotocols = ['binary', 'base64']
  512. if subprotocol:
  513. self._subprotocols.append(subprotocol)
  514. WebSocketServerFactory.__init__(self,
  515. url=url,
  516. reactor=reactor,
  517. protocols=self._subprotocols)
  518. # automatically fragment outgoing traffic into WebSocket frames
  519. # of this size
  520. self.setProtocolOptions(autoFragmentSize=autoFragmentSize)
  521. # play nice and perform WS closing handshake
  522. self.setProtocolOptions(failByDrop=False)
  523. if enableCompression:
  524. # Enable WebSocket extension "permessage-deflate".
  525. # Function to accept offers from the client ..
  526. def accept(offers):
  527. for offer in offers:
  528. if isinstance(offer, PerMessageDeflateOffer):
  529. return PerMessageDeflateOfferAccept(offer)
  530. self.setProtocolOptions(perMessageCompressionAccept=accept)
  531. def buildProtocol(self, addr):
  532. proto = WrappingWebSocketServerProtocol()
  533. proto.factory = self
  534. proto._proto = self._factory.buildProtocol(addr)
  535. proto._proto.transport = proto
  536. return proto
  537. def startFactory(self):
  538. self._factory.startFactory()
  539. WebSocketServerFactory.startFactory(self)
  540. def stopFactory(self):
  541. self._factory.stopFactory()
  542. WebSocketServerFactory.stopFactory(self)
  543. class WrappingWebSocketClientFactory(WebSocketClientFactory):
  544. """
  545. Wrapping client factory for stream-based transport over WebSocket.
  546. """
  547. def __init__(self,
  548. factory,
  549. url,
  550. reactor=None,
  551. enableCompression=True,
  552. autoFragmentSize=0,
  553. subprotocol=None):
  554. """
  555. :param factory: Stream-based factory to be wrapped.
  556. :type factory: A subclass of ``twisted.internet.protocol.Factory``
  557. :param url: WebSocket URL of the server this client factory will connect to.
  558. :type url: unicode
  559. """
  560. self._factory = factory
  561. self._subprotocols = ['binary', 'base64']
  562. if subprotocol:
  563. self._subprotocols.append(subprotocol)
  564. WebSocketClientFactory.__init__(self,
  565. url=url,
  566. reactor=reactor,
  567. protocols=self._subprotocols)
  568. # automatically fragment outgoing traffic into WebSocket frames
  569. # of this size
  570. self.setProtocolOptions(autoFragmentSize=autoFragmentSize)
  571. # play nice and perform WS closing handshake
  572. self.setProtocolOptions(failByDrop=False)
  573. if enableCompression:
  574. # Enable WebSocket extension "permessage-deflate".
  575. # The extensions offered to the server ..
  576. offers = [PerMessageDeflateOffer()]
  577. self.setProtocolOptions(perMessageCompressionOffers=offers)
  578. # Function to accept responses from the server ..
  579. def accept(response):
  580. if isinstance(response, PerMessageDeflateResponse):
  581. return PerMessageDeflateResponseAccept(response)
  582. self.setProtocolOptions(perMessageCompressionAccept=accept)
  583. def buildProtocol(self, addr):
  584. proto = WrappingWebSocketClientProtocol()
  585. proto.factory = self
  586. proto._proto = self._factory.buildProtocol(addr)
  587. proto._proto.transport = proto
  588. return proto
  589. @public
  590. def connectWS(factory, contextFactory=None, timeout=30, bindAddress=None):
  591. """
  592. Establish WebSocket connection to a server. The connection parameters like target
  593. host, port, resource and others are provided via the factory.
  594. :param factory: The WebSocket protocol factory to be used for creating client protocol instances.
  595. :type factory: An :class:`autobahn.websocket.WebSocketClientFactory` instance.
  596. :param contextFactory: SSL context factory, required for secure WebSocket connections ("wss").
  597. :type contextFactory: A `twisted.internet.ssl.ClientContextFactory <http://twistedmatrix.com/documents/current/api/twisted.internet.ssl.ClientContextFactory.html>`_ instance.
  598. :param timeout: Number of seconds to wait before assuming the connection has failed.
  599. :type timeout: int
  600. :param bindAddress: A (host, port) tuple of local address to bind to, or None.
  601. :type bindAddress: tuple
  602. :returns: The connector.
  603. :rtype: An object which implements `twisted.interface.IConnector <http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IConnector.html>`_.
  604. """
  605. # lazy import to avoid reactor install upon module import
  606. if hasattr(factory, 'reactor'):
  607. reactor = factory.reactor
  608. else:
  609. from twisted.internet import reactor
  610. if factory.isSecure:
  611. if contextFactory is None:
  612. # create default client SSL context factory when none given
  613. from twisted.internet import ssl
  614. contextFactory = ssl.ClientContextFactory()
  615. if factory.proxy is not None:
  616. factory.contextFactory = contextFactory
  617. conn = reactor.connectTCP(factory.proxy['host'], factory.proxy['port'], factory, timeout, bindAddress)
  618. else:
  619. if factory.isSecure:
  620. conn = reactor.connectSSL(factory.host, factory.port, factory, contextFactory, timeout, bindAddress)
  621. else:
  622. conn = reactor.connectTCP(factory.host, factory.port, factory, timeout, bindAddress)
  623. return conn
  624. @public
  625. def listenWS(factory, contextFactory=None, backlog=50, interface=''):
  626. """
  627. Listen for incoming WebSocket connections from clients. The connection parameters like
  628. listening port and others are provided via the factory.
  629. :param factory: The WebSocket protocol factory to be used for creating server protocol instances.
  630. :type factory: An :class:`autobahn.websocket.WebSocketServerFactory` instance.
  631. :param contextFactory: SSL context factory, required for secure WebSocket connections ("wss").
  632. :type contextFactory: A twisted.internet.ssl.ContextFactory.
  633. :param backlog: Size of the listen queue.
  634. :type backlog: int
  635. :param interface: The interface (derived from hostname given) to bind to, defaults to '' (all).
  636. :type interface: str
  637. :returns: The listening port.
  638. :rtype: An object that implements `twisted.interface.IListeningPort <http://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IListeningPort.html>`_.
  639. """
  640. # lazy import to avoid reactor install upon module import
  641. if hasattr(factory, 'reactor'):
  642. reactor = factory.reactor
  643. else:
  644. from twisted.internet import reactor
  645. if factory.isSecure:
  646. if contextFactory is None:
  647. raise Exception("Secure WebSocket listen requested, but no SSL context factory given")
  648. listener = reactor.listenSSL(factory.port, factory, contextFactory, backlog, interface)
  649. else:
  650. listener = reactor.listenTCP(factory.port, factory, backlog, interface)
  651. return listener
  652. @public
  653. class WampWebSocketServerProtocol(websocket.WampWebSocketServerProtocol, WebSocketServerProtocol):
  654. """
  655. Twisted-based WAMP-over-WebSocket server protocol.
  656. Implements:
  657. * :class:`autobahn.wamp.interfaces.ITransport`
  658. """
  659. @public
  660. class WampWebSocketServerFactory(websocket.WampWebSocketServerFactory, WebSocketServerFactory):
  661. """
  662. Twisted-based WAMP-over-WebSocket server protocol factory.
  663. """
  664. protocol = WampWebSocketServerProtocol
  665. def __init__(self, factory, *args, **kwargs):
  666. """
  667. :param factory: A callable that produces instances that implement
  668. :class:`autobahn.wamp.interfaces.ITransportHandler`
  669. :type factory: callable
  670. :param serializers: A list of WAMP serializers to use (or ``None``
  671. for all available serializers).
  672. :type serializers: list of objects implementing
  673. :class:`autobahn.wamp.interfaces.ISerializer`
  674. """
  675. serializers = kwargs.pop('serializers', None)
  676. websocket.WampWebSocketServerFactory.__init__(self, factory, serializers)
  677. kwargs['protocols'] = self._protocols
  678. # noinspection PyCallByClass
  679. WebSocketServerFactory.__init__(self, *args, **kwargs)
  680. @public
  681. class WampWebSocketClientProtocol(websocket.WampWebSocketClientProtocol, WebSocketClientProtocol):
  682. """
  683. Twisted-based WAMP-over-WebSocket client protocol.
  684. Implements:
  685. * :class:`autobahn.wamp.interfaces.ITransport`
  686. """
  687. @public
  688. class WampWebSocketClientFactory(websocket.WampWebSocketClientFactory, WebSocketClientFactory):
  689. """
  690. Twisted-based WAMP-over-WebSocket client protocol factory.
  691. """
  692. protocol = WampWebSocketClientProtocol
  693. def __init__(self, factory, *args, **kwargs):
  694. """
  695. :param factory: A callable that produces instances that implement
  696. :class:`autobahn.wamp.interfaces.ITransportHandler`
  697. :type factory: callable
  698. :param serializer: The WAMP serializer to use (or ``None`` for
  699. "best" serializer, chosen as the first serializer available from
  700. this list: CBOR, MessagePack, UBJSON, JSON).
  701. :type serializer: object implementing :class:`autobahn.wamp.interfaces.ISerializer`
  702. """
  703. serializers = kwargs.pop('serializers', None)
  704. websocket.WampWebSocketClientFactory.__init__(self, factory, serializers)
  705. kwargs['protocols'] = self._protocols
  706. WebSocketClientFactory.__init__(self, *args, **kwargs)
  707. # Reduce the factory logs noise
  708. self.noisy = False