Merge remote-tracking branch 'upstream/master' into unicode-exception
This commit is contained in:
commit
6c10c402b4
|
@ -70,6 +70,84 @@ The **Private Library API** is for library internal use, crossing files, classes
|
|||
|
||||
The **Private non-API** isn't an API at all: like class members which may only be used within that class, or functions which may only be used in the same module where the function is defined.
|
||||
|
||||
### Public API
|
||||
|
||||
The new rule for the public API is simple: if something is exported from the modules below, then it is public. Otherwise not.
|
||||
|
||||
* [Top](https://github.com/tavendo/AutobahnPython/blob/master/autobahn/__init__.py)
|
||||
* [WebSocket](https://github.com/tavendo/AutobahnPython/blob/master/autobahn/websocket/__init__.py)
|
||||
* [WAMP](https://github.com/tavendo/AutobahnPython/blob/master/autobahn/wamp/__init__.py)
|
||||
* [Asyncio](https://github.com/tavendo/AutobahnPython/blob/master/autobahn/asyncio/__init__.py)
|
||||
* [Twisted](https://github.com/tavendo/AutobahnPython/blob/master/autobahn/twisted/__init__.py)
|
||||
|
||||
### Cross-platform Considerations
|
||||
|
||||
Autobahn supports many different platforms and both major async frameworks. One thing that helps with this is the [txaio](https://github.com/tavendo/txaio) library. This is used for all Deferred/Future operations throughout the code and more recently for logging.
|
||||
|
||||
Here is a recommended way to do **logging**:
|
||||
|
||||
```python
|
||||
class Foo(object):
|
||||
log = txaio.make_logger()
|
||||
|
||||
def connect(self):
|
||||
try:
|
||||
self.log.info("Connecting")
|
||||
raise Exception("an error")
|
||||
except:
|
||||
fail = txaio.create_failure()
|
||||
self.log.error("Connection failed: {msg}", msg=txaio.failure_message(fail))
|
||||
self.log.debug("{traceback}", traceback=txaio.failure_format_traceback(fail))
|
||||
# Exception instance in fail.value
|
||||
```
|
||||
|
||||
Note that ``create_failure()`` can (and should) be called without arguments when inside an ``except`` block; this will give it a valid traceback instance. The only attribute you can depend on is ``fail.value`` which is the ``Exception`` instance. Otherwise use ``txaio.failre_*`` methods.
|
||||
|
||||
How to **handler async methods** with txaio:
|
||||
|
||||
```python
|
||||
f = txaio.as_future(mightReturnDeferred, 'arg0')
|
||||
|
||||
def success(result):
|
||||
print("It worked! {}".format(result))
|
||||
|
||||
def error(fail):
|
||||
print("It failed! {}".format(txaio.failure_message(fail)))
|
||||
txaio.add_callbacks(f, success, error)
|
||||
```
|
||||
|
||||
Either the success or error callback can be ``None`` (e.g. if you just need to add an error-handler). ``fail`` must implement ``txaio.IFailedFuture`` (but only that; don't depend on any other methods). You cannot use ``@asyncio.coroutine`` or ``@inlineCallbacks``.
|
||||
|
||||
|
||||
### Use of assert vs Exceptions
|
||||
|
||||
> See the discussion [here](https://github.com/tavendo/AutobahnPython/issues/99).
|
||||
|
||||
`assert` is for telling fellow programmers: "When I wrote this, I thought X could/would never really happen, and if it does, this code will very likely do the wrong thing".
|
||||
|
||||
That is, **use an assert if the following holds true: if the assert fails, it means we have a bug within the library itself**.
|
||||
|
||||
In contrast, to check e.g. for user errors, such as application code using the wrong type when calling into the library, use Exceptions:
|
||||
|
||||
```python
|
||||
import six
|
||||
|
||||
def foo(uri):
|
||||
if type(uri) != six.text_type:
|
||||
raise RuntimeError(u"URIs for foo() must be unicode - got {} instead".format(type(uri)))
|
||||
```
|
||||
|
||||
In this specific example, we also have a WAMP defined error (which would be preferred compared to the generic exception used above):
|
||||
|
||||
```python
|
||||
import six
|
||||
from autobahn.wamp import ApplicationError
|
||||
|
||||
def foo(uri):
|
||||
if type(uri) != six.text_type:
|
||||
raise ApplicationError(ApplicationError.INVALID_URI,
|
||||
u"URIs for foo() must be unicode - got {} instead".format(type(uri)))
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
|
|
7
Makefile
7
Makefile
|
@ -40,7 +40,7 @@ test_styleguide:
|
|||
|
||||
# direct test via pytest (only here because of setuptools test integration)
|
||||
test_pytest:
|
||||
python -m pytest -rsx .
|
||||
python -m pytest -rsx autobahn/
|
||||
|
||||
# test via setuptools command
|
||||
test_setuptools:
|
||||
|
@ -62,13 +62,14 @@ test_twisted_coverage:
|
|||
|
||||
test_coverage:
|
||||
-rm .coverage
|
||||
tox -e py27twisted,py27asyncio,py34asyncio
|
||||
tox -e py27-twcurrent,py27-trollius,py34-asyncio
|
||||
coverage combine
|
||||
coverage html
|
||||
coverage report --show-missing
|
||||
|
||||
# test under asyncio
|
||||
test_asyncio:
|
||||
USE_ASYNCIO=1 python -m pytest -rsx
|
||||
USE_ASYNCIO=1 python -m pytest -rsx autobahn
|
||||
#WAMP_ROUTER_URL="ws://127.0.0.1:8080/ws" USE_ASYNCIO=1 python -m pytest -rsx
|
||||
|
||||
test1:
|
||||
|
|
|
@ -34,8 +34,8 @@ Features
|
|||
- framework for `WebSocket <http://tools.ietf.org/html/rfc6455>`__ and `WAMP <http://wamp.ws/>`__ clients and servers
|
||||
- compatible with Python 2.6, 2.7, 3.3 and 3.4
|
||||
- runs on `CPython <http://python.org/>`__, `PyPy <http://pypy.org/>`__ and `Jython <http://jython.org/>`__
|
||||
- runs under `Twisted <http://twistedmatrix.com/>`__ and `asyncio <http://docs.python.org/3.4/library/asyncio.html>`__ - implements WebSocket
|
||||
`RFC6455 <http://tools.ietf.org/html/rfc6455>`__, Draft Hybi-10+, Hixie-76
|
||||
- runs under `Twisted <http://twistedmatrix.com/>`__ and `asyncio <http://docs.python.org/3.4/library/asyncio.html>`__ - implements WebSocket
|
||||
`RFC6455 <http://tools.ietf.org/html/rfc6455>`__ and Draft Hybi-10+
|
||||
- implements `WebSocket compression <http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression>`__
|
||||
- implements `WAMP <http://wamp.ws/>`__, the Web Application Messaging Protocol
|
||||
- high-performance, fully asynchronous implementation
|
||||
|
|
|
@ -24,5 +24,19 @@
|
|||
#
|
||||
###############################################################################
|
||||
|
||||
__version__ = "0.10.9"
|
||||
version = __version__ # backward compat.
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# we use the following in code examples, so it must be part of
|
||||
# out public API
|
||||
from autobahn.util import utcnow, utcstr
|
||||
|
||||
__version__ = u"0.11.0"
|
||||
"""
|
||||
AutobahnPython library version.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'utcnow',
|
||||
'utcstr',
|
||||
)
|
||||
|
|
|
@ -23,3 +23,25 @@
|
|||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# WebSocket protocol support
|
||||
from autobahn.asyncio.websocket import \
|
||||
WebSocketServerProtocol, \
|
||||
WebSocketClientProtocol, \
|
||||
WebSocketServerFactory, \
|
||||
WebSocketClientFactory
|
||||
|
||||
# WAMP support
|
||||
from autobahn.asyncio.wamp import ApplicationSession
|
||||
|
||||
|
||||
__all__ = (
|
||||
'WebSocketServerProtocol',
|
||||
'WebSocketClientProtocol',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
'ApplicationSession',
|
||||
)
|
||||
|
|
|
@ -24,11 +24,12 @@
|
|||
#
|
||||
###############################################################################
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import deque
|
||||
|
||||
from autobahn.wamp import websocket
|
||||
from autobahn.websocket import protocol
|
||||
from autobahn.websocket import http
|
||||
|
||||
try:
|
||||
import asyncio
|
||||
|
@ -41,7 +42,8 @@ except ImportError:
|
|||
from trollius import iscoroutine
|
||||
from trollius import Future
|
||||
|
||||
from autobahn._logging import make_logger
|
||||
from autobahn.websocket.types import ConnectionDeny
|
||||
import txaio
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
@ -51,7 +53,6 @@ __all__ = (
|
|||
'WebSocketAdapterFactory',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
|
||||
'WampWebSocketServerProtocol',
|
||||
'WampWebSocketClientProtocol',
|
||||
'WampWebSocketServerFactory',
|
||||
|
@ -192,10 +193,10 @@ class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServer
|
|||
res = self.onConnect(request)
|
||||
# if yields(res):
|
||||
# res = yield from res
|
||||
except http.HttpException as exc:
|
||||
self.failHandshake(exc.reason, exc.code)
|
||||
except Exception:
|
||||
self.failHandshake(http.INTERNAL_SERVER_ERROR[1], http.INTERNAL_SERVER_ERROR[0])
|
||||
except ConnectionDeny as e:
|
||||
self.failHandshake(e.reason, e.code)
|
||||
except Exception as e:
|
||||
self.failHandshake("Internal server error: {}".format(e), ConnectionDeny.http.INTERNAL_SERVER_ERROR)
|
||||
else:
|
||||
self.succeedHandshake(res)
|
||||
|
||||
|
@ -215,7 +216,7 @@ class WebSocketAdapterFactory(object):
|
|||
"""
|
||||
Adapter class for asyncio-based WebSocket client and server factories.
|
||||
"""
|
||||
log = make_logger()
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __call__(self):
|
||||
proto = self.protocol()
|
||||
|
|
|
@ -23,3 +23,18 @@
|
|||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
|
||||
class FakeTransport(object):
|
||||
_written = b""
|
||||
_open = True
|
||||
|
||||
def write(self, msg):
|
||||
if not self._open:
|
||||
raise Exception("Can't write to a closed connection")
|
||||
self._written = self._written + msg
|
||||
|
||||
def loseConnection(self):
|
||||
self._open = False
|
||||
|
|
|
@ -23,3 +23,55 @@
|
|||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# Twisted specific utilities (these should really be in Twisted, but
|
||||
# they aren't, and we use these in example code, so it must be part of
|
||||
# the public API)
|
||||
from autobahn.twisted.util import sleep
|
||||
from autobahn.twisted.choosereactor import install_reactor
|
||||
|
||||
# WebSocket protocol support
|
||||
from autobahn.twisted.websocket import \
|
||||
WebSocketServerProtocol, \
|
||||
WebSocketClientProtocol, \
|
||||
WebSocketServerFactory, \
|
||||
WebSocketClientFactory
|
||||
|
||||
# support for running Twisted stream protocols over WebSocket
|
||||
from autobahn.twisted.websocket import WrappingWebSocketServerFactory, \
|
||||
WrappingWebSocketClientFactory
|
||||
|
||||
# Twisted Web support
|
||||
from autobahn.twisted.resource import WebSocketResource, WSGIRootResource
|
||||
|
||||
# WAMP support
|
||||
from autobahn.twisted.wamp import ApplicationSession
|
||||
|
||||
|
||||
__all__ = (
|
||||
# this should really be in Twisted
|
||||
'sleep',
|
||||
'install_reactor',
|
||||
|
||||
# WebSocket
|
||||
'WebSocketServerProtocol',
|
||||
'WebSocketClientProtocol',
|
||||
'WebSocketServerFactory',
|
||||
'WebSocketClientFactory',
|
||||
|
||||
# wrapping stream protocols in WebSocket
|
||||
'WrappingWebSocketServerFactory',
|
||||
'WrappingWebSocketClientFactory',
|
||||
|
||||
# Twisted Web
|
||||
'WebSocketResource',
|
||||
|
||||
# this should really be in Twisted
|
||||
'WSGIRootResource',
|
||||
|
||||
# WAMP support
|
||||
'ApplicationSession',
|
||||
)
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from autobahn._logging import make_logger
|
||||
from txaio import make_logger
|
||||
|
||||
__all__ = (
|
||||
'install_optimal_reactor',
|
||||
|
@ -41,7 +41,7 @@ def install_optimal_reactor(verbose=False):
|
|||
:param verbose: If ``True``, print what happens.
|
||||
:type verbose: bool
|
||||
"""
|
||||
log = make_logger("twisted")
|
||||
log = make_logger()
|
||||
|
||||
import sys
|
||||
from twisted.python import reflect
|
||||
|
@ -133,7 +133,7 @@ def install_reactor(explicit_reactor=None, verbose=False):
|
|||
import txaio
|
||||
txaio.use_twisted() # just to be sure...
|
||||
|
||||
log = make_logger("twisted")
|
||||
log = make_logger()
|
||||
|
||||
if explicit_reactor:
|
||||
# install explicitly given reactor
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import itertools
|
||||
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
from twisted.internet.interfaces import IStreamClientEndpoint
|
||||
from twisted.internet.endpoints import UNIXClientEndpoint
|
||||
from twisted.internet.endpoints import TCP4ClientEndpoint
|
||||
|
||||
try:
|
||||
_TLS = True
|
||||
from twisted.internet.endpoints import SSL4ClientEndpoint
|
||||
from twisted.internet.ssl import optionsForClientTLS, CertificateOptions
|
||||
from twisted.internet.interfaces import IOpenSSLClientConnectionCreator
|
||||
except ImportError:
|
||||
_TLS = False
|
||||
|
||||
import txaio
|
||||
|
||||
from autobahn.twisted.websocket import WampWebSocketClientFactory
|
||||
from autobahn.twisted.rawsocket import WampRawSocketClientFactory
|
||||
|
||||
from autobahn.wamp import connection
|
||||
|
||||
from autobahn.twisted.util import sleep
|
||||
from autobahn.twisted.wamp import ApplicationSession
|
||||
|
||||
|
||||
__all__ = ('Connection')
|
||||
|
||||
|
||||
def _create_transport_factory(reactor, transport_config, session_factory):
|
||||
"""
|
||||
Create a WAMP-over-XXX transport factory.
|
||||
"""
|
||||
if transport_config['type'] == 'websocket':
|
||||
return WampWebSocketClientFactory(session_factory, url=transport_config['url'])
|
||||
elif transport_config['type'] == 'rawsocket':
|
||||
return WampRawSocketClientFactory(session_factory)
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
|
||||
def _create_transport_endpoint(reactor, endpoint_config):
|
||||
"""
|
||||
Create a Twisted client endpoint for a WAMP-over-XXX transport.
|
||||
"""
|
||||
if IStreamClientEndpoint.providedBy(endpoint_config):
|
||||
endpoint = IStreamClientEndpoint(endpoint_config)
|
||||
else:
|
||||
# create a connecting TCP socket
|
||||
if endpoint_config['type'] == 'tcp':
|
||||
|
||||
version = int(endpoint_config.get('version', 4))
|
||||
host = str(endpoint_config['host'])
|
||||
port = int(endpoint_config['port'])
|
||||
timeout = int(endpoint_config.get('timeout', 10)) # in seconds
|
||||
tls = endpoint_config.get('tls', None)
|
||||
|
||||
# create a TLS enabled connecting TCP socket
|
||||
if tls:
|
||||
if not _TLS:
|
||||
raise RuntimeError('TLS configured in transport, but TLS support is not installed (eg OpenSSL?)')
|
||||
|
||||
# FIXME: create TLS context from configuration
|
||||
if IOpenSSLClientConnectionCreator.providedBy(tls):
|
||||
# eg created from twisted.internet.ssl.optionsForClientTLS()
|
||||
context = IOpenSSLClientConnectionCreator(tls)
|
||||
|
||||
elif isinstance(tls, CertificateOptions):
|
||||
context = tls
|
||||
|
||||
elif tls is True:
|
||||
context = optionsForClientTLS(host)
|
||||
|
||||
else:
|
||||
raise RuntimeError('unknown type {} for "tls" configuration in transport'.format(type(tls)))
|
||||
|
||||
if version == 4:
|
||||
endpoint = SSL4ClientEndpoint(reactor, host, port, context, timeout=timeout)
|
||||
elif version == 6:
|
||||
# there is no SSL6ClientEndpoint!
|
||||
raise RuntimeError('TLS on IPv6 not implemented')
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
# create a non-TLS connecting TCP socket
|
||||
else:
|
||||
if version == 4:
|
||||
endpoint = TCP4ClientEndpoint(reactor, host, port, timeout=timeout)
|
||||
elif version == 6:
|
||||
try:
|
||||
from twisted.internet.endpoints import TCP6ClientEndpoint
|
||||
except ImportError:
|
||||
raise RuntimeError('IPv6 is not supported (please upgrade Twisted)')
|
||||
endpoint = TCP6ClientEndpoint(reactor, host, port, timeout=timeout)
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
# create a connecting Unix domain socket
|
||||
elif endpoint_config['type'] == 'unix':
|
||||
path = endpoint_config['path']
|
||||
timeout = int(endpoint_config.get('timeout', 10)) # in seconds
|
||||
endpoint = UNIXClientEndpoint(reactor, path, timeout=timeout)
|
||||
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
return endpoint
|
||||
|
||||
|
||||
class Connection(connection.Connection):
|
||||
"""
|
||||
A connection establishes a transport and attached a session
|
||||
to a realm using the transport for communication.
|
||||
|
||||
The transports a connection tries to use can be configured,
|
||||
as well as the auto-reconnect strategy.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
session = ApplicationSession
|
||||
"""
|
||||
The factory of the session we will instantiate.
|
||||
"""
|
||||
|
||||
def __init__(self, transports=u'ws://127.0.0.1:8080/ws', realm=u'realm1', extra=None):
|
||||
connection.Connection.__init__(self, None, transports, realm, extra)
|
||||
|
||||
def _connect_transport(self, reactor, transport_config, session_factory):
|
||||
"""
|
||||
Create and connect a WAMP-over-XXX transport.
|
||||
"""
|
||||
transport_factory = _create_transport_factory(reactor, transport_config, session_factory)
|
||||
transport_endpoint = _create_transport_endpoint(reactor, transport_config['endpoint'])
|
||||
return transport_endpoint.connect(transport_factory)
|
||||
|
||||
@inlineCallbacks
|
||||
def start(self, reactor=None):
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
|
||||
txaio.use_twisted()
|
||||
txaio.config.loop = reactor
|
||||
|
||||
txaio.start_logging(level='debug')
|
||||
|
||||
yield self.fire('start', reactor, self)
|
||||
|
||||
transport_gen = itertools.cycle(self._transports)
|
||||
|
||||
reconnect = True
|
||||
|
||||
while reconnect:
|
||||
transport_config = next(transport_gen)
|
||||
try:
|
||||
yield self._connect_once(reactor, transport_config)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
yield sleep(2)
|
||||
else:
|
||||
reconnect = False
|
|
@ -1,127 +0,0 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import re
|
||||
|
||||
from twisted.internet.protocol import Protocol, Factory
|
||||
|
||||
__all__ = (
|
||||
'FlashPolicyProtocol',
|
||||
'FlashPolicyFactory'
|
||||
)
|
||||
|
||||
|
||||
class FlashPolicyProtocol(Protocol):
|
||||
"""
|
||||
Flash Player 9 (version 9.0.124.0 and above) implements a strict new access
|
||||
policy for Flash applications that make Socket or XMLSocket connections to
|
||||
a remote host. It now requires the presence of a socket policy file
|
||||
on the server.
|
||||
|
||||
We want this to support the Flash WebSockets bridge which is needed for
|
||||
older browser, in particular MSIE9/8.
|
||||
|
||||
.. seealso::
|
||||
* `Autobahn WebSocket fallbacks example <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/websocket/echo_wsfallbacks>`_
|
||||
* `Flash policy files background <http://www.lightsphere.com/dev/articles/flash_socket_policy.html>`_
|
||||
"""
|
||||
|
||||
REQUESTPAT = re.compile("^\s*<policy-file-request\s*/>")
|
||||
REQUESTMAXLEN = 200
|
||||
REQUESTTIMEOUT = 5
|
||||
POLICYFILE = """<?xml version="1.0"?><cross-domain-policy><allow-access-from domain="%s" to-ports="%s" /></cross-domain-policy>"""
|
||||
|
||||
def __init__(self, allowedDomain, allowedPorts):
|
||||
"""
|
||||
|
||||
:param allowedPort: The port to which Flash player should be allowed to connect.
|
||||
:type allowedPort: int
|
||||
"""
|
||||
self._allowedDomain = allowedDomain
|
||||
self._allowedPorts = allowedPorts
|
||||
self.received = ""
|
||||
self.dropConnection = None
|
||||
|
||||
def connectionMade(self):
|
||||
# DoS protection
|
||||
##
|
||||
def dropConnection():
|
||||
self.transport.abortConnection()
|
||||
self.dropConnection = None
|
||||
self.dropConnection = self.factory.reactor.callLater(FlashPolicyProtocol.REQUESTTIMEOUT, dropConnection)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.dropConnection:
|
||||
self.dropConnection.cancel()
|
||||
self.dropConnection = None
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.received += data
|
||||
if FlashPolicyProtocol.REQUESTPAT.match(self.received):
|
||||
# got valid request: send policy file
|
||||
##
|
||||
self.transport.write(FlashPolicyProtocol.POLICYFILE % (self._allowedDomain, self._allowedPorts))
|
||||
self.transport.loseConnection()
|
||||
elif len(self.received) > FlashPolicyProtocol.REQUESTMAXLEN:
|
||||
# possible DoS attack
|
||||
##
|
||||
self.transport.abortConnection()
|
||||
else:
|
||||
# need more data
|
||||
##
|
||||
pass
|
||||
|
||||
|
||||
class FlashPolicyFactory(Factory):
|
||||
|
||||
def __init__(self, allowedDomain=None, allowedPorts=None, reactor=None):
|
||||
"""
|
||||
|
||||
:param allowedDomain: The domain from which to allow Flash to connect from.
|
||||
If ``None``, allow from anywhere.
|
||||
:type allowedDomain: str or None
|
||||
:param allowedPorts: The ports to which Flash player should be allowed to connect.
|
||||
If ``None``, allow any ports.
|
||||
:type allowedPorts: list of int or None
|
||||
:param reactor: Twisted reactor to use. If not given, autoimport.
|
||||
:type reactor: obj
|
||||
"""
|
||||
# lazy import to avoid reactor install upon module import
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
self.reactor = reactor
|
||||
|
||||
self._allowedDomain = str(allowedDomain) or "*"
|
||||
|
||||
if allowedPorts:
|
||||
self._allowedPorts = ",".join([str(port) for port in allowedPorts])
|
||||
else:
|
||||
self._allowedPorts = "*"
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
proto = FlashPolicyProtocol(self._allowedDomain, self._allowedPorts)
|
||||
proto.factory = self
|
||||
return proto
|
|
@ -1,674 +0,0 @@
|
|||
########################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
########################################
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import traceback
|
||||
import binascii
|
||||
|
||||
from collections import deque
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.web.resource import Resource, NoResource
|
||||
|
||||
# Each of the following 2 trigger a reactor import at module level
|
||||
from twisted.web import http
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
|
||||
from autobahn.util import newid
|
||||
|
||||
from autobahn.wamp.websocket import parseSubprotocolIdentifier
|
||||
|
||||
from autobahn.wamp.exception import SerializationError, \
|
||||
TransportLost
|
||||
|
||||
__all__ = (
|
||||
'WampLongPollResource',
|
||||
)
|
||||
|
||||
|
||||
class WampLongPollResourceSessionSend(Resource):
|
||||
"""
|
||||
A Web resource for sending via XHR that is part of :class:`autobahn.twisted.longpoll.WampLongPollResourceSession`.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
|
||||
:param parent: The Web parent resource for the WAMP session.
|
||||
:type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResourceSession`.
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._parent = parent
|
||||
self._debug = self._parent._parent._debug
|
||||
|
||||
def render_POST(self, request):
|
||||
"""
|
||||
A client sends a message via WAMP-over-Longpoll by HTTP/POSTing
|
||||
to this Web resource. The body of the POST should contain a batch
|
||||
of WAMP messages which are serialized according to the selected
|
||||
serializer, and delimited by a single ``\0`` byte in between two WAMP
|
||||
messages in the batch.
|
||||
"""
|
||||
payload = request.content.read()
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: receiving data for transport '{0}'\n{1}".format(self._parent._transport_id, binascii.hexlify(payload)))
|
||||
|
||||
try:
|
||||
# process (batch of) WAMP message(s)
|
||||
self._parent.onMessage(payload, None)
|
||||
|
||||
except Exception as e:
|
||||
return self._parent._parent._failRequest(request, "could not unserialize WAMP message: {0}".format(e))
|
||||
|
||||
else:
|
||||
request.setResponseCode(http.NO_CONTENT)
|
||||
self._parent._parent._setStandardHeaders(request)
|
||||
self._parent._isalive = True
|
||||
return ""
|
||||
|
||||
|
||||
class WampLongPollResourceSessionReceive(Resource):
|
||||
"""
|
||||
A Web resource for receiving via XHR that is part of :class:`autobahn.twisted.longpoll.WampLongPollResourceSession`.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
|
||||
:param parent: The Web parent resource for the WAMP session.
|
||||
:type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResourceSession`.
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._parent = parent
|
||||
self._debug = self._parent._parent._debug
|
||||
self.reactor = self._parent._parent.reactor
|
||||
|
||||
self._queue = deque()
|
||||
self._request = None
|
||||
self._killed = False
|
||||
|
||||
if self._debug:
|
||||
def logqueue():
|
||||
if not self._killed:
|
||||
log.msg("WampLongPoll: transport '{0}' - currently polled {1}, pending messages {2}".format(self._parent._transport_id, self._request is not None, len(self._queue)))
|
||||
self.reactor.callLater(1, logqueue)
|
||||
logqueue()
|
||||
|
||||
def queue(self, data):
|
||||
"""
|
||||
Enqueue data to be received by client.
|
||||
|
||||
:param data: The data to be received by the client.
|
||||
:type data: bytes
|
||||
"""
|
||||
self._queue.append(data)
|
||||
self._trigger()
|
||||
|
||||
def _kill(self):
|
||||
"""
|
||||
Kill any outstanding request.
|
||||
"""
|
||||
if self._request:
|
||||
self._request.finish()
|
||||
self._request = None
|
||||
self._killed = True
|
||||
|
||||
def _trigger(self):
|
||||
"""
|
||||
Trigger batched sending of queued messages.
|
||||
"""
|
||||
if self._request and len(self._queue):
|
||||
|
||||
if self._parent._serializer._serializer._batched:
|
||||
# in batched mode, write all pending messages
|
||||
while len(self._queue) > 0:
|
||||
msg = self._queue.popleft()
|
||||
self._request.write(msg)
|
||||
else:
|
||||
# in unbatched mode, only write 1 pending message
|
||||
msg = self._queue.popleft()
|
||||
self._request.write(msg)
|
||||
|
||||
self._request.finish()
|
||||
self._request = None
|
||||
|
||||
def render_POST(self, request):
|
||||
"""
|
||||
A client receives WAMP messages by issuing a HTTP/POST to this
|
||||
Web resource. The request will immediately return when there are
|
||||
messages pending to be received. When there are no such messages
|
||||
pending, the request will "just hang", until either a message
|
||||
arrives to be received or a timeout occurs.
|
||||
"""
|
||||
# remember request, which marks the session as being polled
|
||||
self._request = request
|
||||
|
||||
self._parent._parent._setStandardHeaders(request)
|
||||
request.setHeader('content-type', self._parent._serializer.MIME_TYPE)
|
||||
|
||||
def cancel(_):
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: poll request for transport '{0}' has gone away".format(self._parent._transport_id))
|
||||
self._request = None
|
||||
|
||||
request.notifyFinish().addErrback(cancel)
|
||||
|
||||
self._parent._isalive = True
|
||||
self._trigger()
|
||||
|
||||
return NOT_DONE_YET
|
||||
|
||||
|
||||
class WampLongPollResourceSessionClose(Resource):
|
||||
"""
|
||||
A Web resource for closing the Long-poll session WampLongPollResourceSession.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
|
||||
:param parent: The Web parent resource for the WAMP session.
|
||||
:type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResourceSession`.
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._parent = parent
|
||||
self._debug = self._parent._parent._debug
|
||||
|
||||
def render_POST(self, request):
|
||||
"""
|
||||
A client may actively close a session (and the underlying long-poll transport)
|
||||
by issuing a HTTP/POST with empty body to this resource.
|
||||
"""
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: closing transport '{0}'".format(self._parent._transport_id))
|
||||
|
||||
# now actually close the session
|
||||
self._parent.close()
|
||||
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: session ended and transport {0} closed".format(self._parent._transport_id))
|
||||
|
||||
request.setResponseCode(http.NO_CONTENT)
|
||||
self._parent._parent._setStandardHeaders(request)
|
||||
return ""
|
||||
|
||||
|
||||
class WampLongPollResourceSession(Resource):
|
||||
"""
|
||||
A Web resource representing an open WAMP session.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, transport_details):
|
||||
"""
|
||||
Create a new Web resource representing a WAMP session.
|
||||
|
||||
:param parent: The parent Web resource.
|
||||
:type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResource`.
|
||||
:param transport_details: Details on the WAMP-over-Longpoll transport session.
|
||||
:type transport_details: dict
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
|
||||
self._parent = parent
|
||||
self._debug = self._parent._debug
|
||||
self._debug_wamp = True
|
||||
self.reactor = self._parent.reactor
|
||||
|
||||
self._transport_id = transport_details['transport']
|
||||
self._serializer = transport_details['serializer']
|
||||
self._session = None
|
||||
|
||||
# session authentication information
|
||||
#
|
||||
self._authid = None
|
||||
self._authrole = None
|
||||
self._authmethod = None
|
||||
self._authprovider = None
|
||||
|
||||
self._send = WampLongPollResourceSessionSend(self)
|
||||
self._receive = WampLongPollResourceSessionReceive(self)
|
||||
self._close = WampLongPollResourceSessionClose(self)
|
||||
|
||||
self.putChild("send", self._send)
|
||||
self.putChild("receive", self._receive)
|
||||
self.putChild("close", self._close)
|
||||
|
||||
self._isalive = False
|
||||
|
||||
# kill inactive sessions after this timeout
|
||||
#
|
||||
killAfter = self._parent._killAfter
|
||||
if killAfter > 0:
|
||||
def killIfDead():
|
||||
if not self._isalive:
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: killing inactive WAMP session with transport '{0}'".format(self._transport_id))
|
||||
|
||||
self.onClose(False, 5000, "session inactive")
|
||||
self._receive._kill()
|
||||
if self._transport_id in self._parent._transports:
|
||||
del self._parent._transports[self._transport_id]
|
||||
else:
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: transport '{0}' is still alive".format(self._transport_id))
|
||||
|
||||
self._isalive = False
|
||||
self.reactor.callLater(killAfter, killIfDead)
|
||||
|
||||
self.reactor.callLater(killAfter, killIfDead)
|
||||
else:
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: transport '{0}' automatic killing of inactive session disabled".format(self._transport_id))
|
||||
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: session resource for transport '{0}' initialized)".format(self._transport_id))
|
||||
|
||||
self.onOpen()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.close`
|
||||
"""
|
||||
if self.isOpen():
|
||||
self.onClose(True, 1000, u"session closed")
|
||||
self._receive._kill()
|
||||
del self._parent._transports[self._transport_id]
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
def abort(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.abort`
|
||||
"""
|
||||
if self.isOpen():
|
||||
self.onClose(True, 1000, u"session aborted")
|
||||
self._receive._kill()
|
||||
del self._parent._transports[self._transport_id]
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
# noinspection PyUnusedLocal
|
||||
def onClose(self, wasClean, code, reason):
|
||||
"""
|
||||
Callback from :func:`autobahn.websocket.interfaces.IWebSocketChannel.onClose`
|
||||
"""
|
||||
if self._session:
|
||||
try:
|
||||
self._session.onClose(wasClean)
|
||||
except Exception:
|
||||
# silently ignore exceptions raised here ..
|
||||
if self._debug:
|
||||
traceback.print_exc()
|
||||
self._session = None
|
||||
|
||||
def onOpen(self):
|
||||
"""
|
||||
Callback from :func:`autobahn.websocket.interfaces.IWebSocketChannel.onOpen`
|
||||
"""
|
||||
self._session = self._parent._factory()
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self._session.onOpen(self)
|
||||
except Exception:
|
||||
if self._debug:
|
||||
traceback.print_exc()
|
||||
|
||||
def onMessage(self, payload, isBinary):
|
||||
"""
|
||||
Callback from :func:`autobahn.websocket.interfaces.IWebSocketChannel.onMessage`
|
||||
"""
|
||||
for msg in self._serializer.unserialize(payload, isBinary):
|
||||
if self._debug:
|
||||
print("WampLongPoll: RX {0}".format(msg))
|
||||
self._session.onMessage(msg)
|
||||
|
||||
def send(self, msg):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.send`
|
||||
"""
|
||||
if self.isOpen():
|
||||
try:
|
||||
if self._debug:
|
||||
print("WampLongPoll: TX {0}".format(msg))
|
||||
payload, isBinary = self._serializer.serialize(msg)
|
||||
except Exception as e:
|
||||
# all exceptions raised from above should be serialization errors ..
|
||||
raise SerializationError("unable to serialize WAMP application payload ({0})".format(e))
|
||||
else:
|
||||
self._receive.queue(payload)
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
def isOpen(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ITransport.isOpen`
|
||||
"""
|
||||
return self._session is not None
|
||||
|
||||
|
||||
class WampLongPollResourceOpen(Resource):
|
||||
"""
|
||||
A Web resource for creating new WAMP sessions.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
|
||||
:param parent: The parent Web resource.
|
||||
:type parent: Instance of :class:`autobahn.twisted.longpoll.WampLongPollResource`.
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._parent = parent
|
||||
self._debug = self._parent._debug
|
||||
|
||||
def render_POST(self, request):
|
||||
"""
|
||||
Request to create a new WAMP session.
|
||||
"""
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: creating new session ..")
|
||||
|
||||
payload = request.content.read()
|
||||
try:
|
||||
options = json.loads(payload)
|
||||
except Exception as e:
|
||||
return self._parent._failRequest(request, "could not parse WAMP session open request body: {0}".format(e))
|
||||
|
||||
if type(options) != dict:
|
||||
return self._parent._failRequest(request, "invalid type for WAMP session open request [was {0}, expected dictionary]".format(type(options)))
|
||||
|
||||
if 'protocols' not in options:
|
||||
return self._parent._failRequest(request, "missing attribute 'protocols' in WAMP session open request")
|
||||
|
||||
# determine the protocol to speak
|
||||
#
|
||||
protocol = None
|
||||
serializer = None
|
||||
for p in options['protocols']:
|
||||
version, serializerId = parseSubprotocolIdentifier(p)
|
||||
if version == 2 and serializerId in self._parent._serializers.keys():
|
||||
serializer = self._parent._serializers[serializerId]
|
||||
protocol = p
|
||||
break
|
||||
|
||||
if protocol is None:
|
||||
return self.__failRequest(request, "no common protocol to speak (I speak: {0})".format(["wamp.2.{0}".format(s) for s in self._parent._serializers.keys()]))
|
||||
|
||||
# make up new transport ID
|
||||
#
|
||||
if self._parent._debug_transport_id:
|
||||
# use fixed transport ID for debugging purposes
|
||||
transport = self._parent._debug_transport_id
|
||||
else:
|
||||
transport = newid()
|
||||
|
||||
# this doesn't contain all the info (when a header key appears multiple times)
|
||||
# http_headers_received = request.getAllHeaders()
|
||||
http_headers_received = {}
|
||||
for key, values in request.requestHeaders.getAllRawHeaders():
|
||||
if key not in http_headers_received:
|
||||
http_headers_received[key] = []
|
||||
http_headers_received[key].extend(values)
|
||||
|
||||
transport_details = {
|
||||
'transport': transport,
|
||||
'serializer': serializer,
|
||||
'protocol': protocol,
|
||||
'peer': request.getClientIP(),
|
||||
'http_headers_received': http_headers_received,
|
||||
'http_headers_sent': None
|
||||
}
|
||||
|
||||
# create instance of WampLongPollResourceSession or subclass thereof ..
|
||||
#
|
||||
self._parent._transports[transport] = self._parent.protocol(self._parent, transport_details)
|
||||
|
||||
# create response
|
||||
#
|
||||
self._parent._setStandardHeaders(request)
|
||||
request.setHeader('content-type', 'application/json; charset=utf-8')
|
||||
|
||||
result = {
|
||||
'transport': transport,
|
||||
'protocol': protocol
|
||||
}
|
||||
|
||||
payload = json.dumps(result)
|
||||
|
||||
if self._debug:
|
||||
log.msg("WampLongPoll: new session created on transport '{0}'".format(transport))
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
class WampLongPollResource(Resource):
|
||||
"""
|
||||
A WAMP-over-Longpoll resource for use with Twisted Web Resource trees.
|
||||
|
||||
This class provides an implementation of the
|
||||
`WAMP-over-Longpoll Transport <https://github.com/tavendo/WAMP/blob/master/spec/advanced.md#long-poll-transport>`_
|
||||
for WAMP.
|
||||
|
||||
The Resource exposes the following paths (child resources).
|
||||
|
||||
Opening a new WAMP session:
|
||||
|
||||
* ``<base-url>/open``
|
||||
|
||||
Once a transport is created and the session is opened:
|
||||
|
||||
* ``<base-url>/<transport-id>/send``
|
||||
* ``<base-url>/<transport-id>/receive``
|
||||
* ``<base-url>/<transport-id>/close``
|
||||
"""
|
||||
|
||||
protocol = WampLongPollResourceSession
|
||||
|
||||
def __init__(self,
|
||||
factory,
|
||||
serializers=None,
|
||||
timeout=10,
|
||||
killAfter=30,
|
||||
queueLimitBytes=128 * 1024,
|
||||
queueLimitMessages=100,
|
||||
debug=False,
|
||||
debug_transport_id=None,
|
||||
reactor=None):
|
||||
"""
|
||||
Create new HTTP WAMP Web resource.
|
||||
|
||||
:param factory: A (router) session factory.
|
||||
:type factory: Instance of :class:`autobahn.twisted.wamp.RouterSessionFactory`.
|
||||
:param serializers: List of WAMP serializers.
|
||||
:type serializers: list of obj (which implement :class:`autobahn.wamp.interfaces.ISerializer`)
|
||||
:param timeout: XHR polling timeout in seconds.
|
||||
:type timeout: int
|
||||
:param killAfter: Kill WAMP session after inactivity in seconds.
|
||||
:type killAfter: int
|
||||
:param queueLimitBytes: Kill WAMP session after accumulation of this many bytes in send queue (XHR poll).
|
||||
:type queueLimitBytes: int
|
||||
:param queueLimitMessages: Kill WAMP session after accumulation of this many message in send queue (XHR poll).
|
||||
:type queueLimitMessages: int
|
||||
:param debug: Enable debug logging.
|
||||
:type debug: bool
|
||||
:param debug_transport_id: If given, use this fixed transport ID.
|
||||
:type debug_transport_id: str
|
||||
:param reactor: The Twisted reactor to run under.
|
||||
:type reactor: obj
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
|
||||
# RouterSessionFactory
|
||||
self._factory = factory
|
||||
|
||||
# lazy import to avoid reactor install upon module import
|
||||
if reactor is None:
|
||||
from twisted.internet import reactor
|
||||
self.reactor = reactor
|
||||
|
||||
self._debug = debug
|
||||
self._debug_transport_id = debug_transport_id
|
||||
self._timeout = timeout
|
||||
self._killAfter = killAfter
|
||||
self._queueLimitBytes = queueLimitBytes
|
||||
self._queueLimitMessages = queueLimitMessages
|
||||
|
||||
if serializers is None:
|
||||
serializers = []
|
||||
|
||||
# try MsgPack WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import MsgPackSerializer
|
||||
serializers.append(MsgPackSerializer(batched=True))
|
||||
serializers.append(MsgPackSerializer())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# try JSON WAMP serializer
|
||||
try:
|
||||
from autobahn.wamp.serializer import JsonSerializer
|
||||
serializers.append(JsonSerializer(batched=True))
|
||||
serializers.append(JsonSerializer())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if not serializers:
|
||||
raise Exception("could not import any WAMP serializers")
|
||||
|
||||
self._serializers = {}
|
||||
for ser in serializers:
|
||||
self._serializers[ser.SERIALIZER_ID] = ser
|
||||
|
||||
self._transports = {}
|
||||
|
||||
# <Base URL>/open
|
||||
#
|
||||
self.putChild("open", WampLongPollResourceOpen(self))
|
||||
|
||||
if self._debug:
|
||||
log.msg("WampLongPollResource initialized")
|
||||
|
||||
def render_GET(self, request):
|
||||
request.setHeader('content-type', 'text/html; charset=UTF-8')
|
||||
peer = "{0}:{1}".format(request.client.host, request.client.port)
|
||||
return self.getNotice(peer=peer)
|
||||
|
||||
def getChild(self, name, request):
|
||||
"""
|
||||
Returns send/receive/close resource for transport.
|
||||
|
||||
.. seealso::
|
||||
|
||||
* :class:`twisted.web.resource.Resource`
|
||||
* :class:`zipfile.ZipFile`
|
||||
"""
|
||||
if name not in self._transports:
|
||||
return NoResource("no WAMP transport '{0}'".format(name))
|
||||
|
||||
if len(request.postpath) != 1 or request.postpath[0] not in ['send', 'receive', 'close']:
|
||||
return NoResource("invalid WAMP transport operation '{0}'".format(request.postpath))
|
||||
|
||||
return self._transports[name]
|
||||
|
||||
def _setStandardHeaders(self, request):
|
||||
"""
|
||||
Set standard HTTP response headers.
|
||||
"""
|
||||
origin = request.getHeader("origin")
|
||||
if origin is None or origin == "null":
|
||||
origin = "*"
|
||||
request.setHeader('access-control-allow-origin', origin)
|
||||
request.setHeader('access-control-allow-credentials', 'true')
|
||||
request.setHeader('cache-control', 'no-store, no-cache, must-revalidate, max-age=0')
|
||||
|
||||
headers = request.getHeader('access-control-request-headers')
|
||||
if headers is not None:
|
||||
request.setHeader('access-control-allow-headers', headers)
|
||||
|
||||
def _failRequest(self, request, msg):
|
||||
"""
|
||||
Fails a request to the long-poll service.
|
||||
"""
|
||||
self._setStandardHeaders(request)
|
||||
request.setHeader('content-type', 'text/plain; charset=UTF-8')
|
||||
request.setResponseCode(http.BAD_REQUEST)
|
||||
return msg
|
||||
|
||||
def getNotice(self, peer, redirectUrl=None, redirectAfter=0):
|
||||
"""
|
||||
Render a user notice (HTML page) when the Long-Poll root resource
|
||||
is accessed via HTTP/GET (by a user).
|
||||
|
||||
:param redirectUrl: Optional URL to redirect the user to.
|
||||
:type redirectUrl: str
|
||||
:param redirectAfter: When ``redirectUrl`` is provided, redirect after this time (seconds).
|
||||
:type redirectAfter: int
|
||||
"""
|
||||
from autobahn import __version__
|
||||
|
||||
if redirectUrl:
|
||||
redirect = """<meta http-equiv="refresh" content="%d;URL='%s'">""" % (redirectAfter, redirectUrl)
|
||||
else:
|
||||
redirect = ""
|
||||
html = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
%s
|
||||
<style>
|
||||
body {
|
||||
color: #fff;
|
||||
background-color: #027eae;
|
||||
font-family: "Segoe UI", "Lucida Grande", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a, a:visited, a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AutobahnPython %s</h1>
|
||||
<p>
|
||||
I am not Web server, but a <b>WAMP-over-LongPoll</b> transport endpoint.
|
||||
</p>
|
||||
<p>
|
||||
You can talk to me using the <a href="https://github.com/tavendo/WAMP/blob/master/spec/advanced.md#long-poll-transport">WAMP-over-LongPoll</a> protocol.
|
||||
</p>
|
||||
<p>
|
||||
For more information, please see:
|
||||
<ul>
|
||||
<li><a href="http://wamp.ws/">WAMP</a></li>
|
||||
<li><a href="http://autobahn.ws/python">AutobahnPython</a></li>
|
||||
</ul>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""" % (redirect, __version__)
|
||||
return html
|
|
@ -28,7 +28,6 @@ from __future__ import absolute_import
|
|||
|
||||
import binascii
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.protocols.basic import Int32StringReceiver
|
||||
from twisted.internet.error import ConnectionDone
|
||||
|
@ -36,6 +35,8 @@ from twisted.internet.error import ConnectionDone
|
|||
from autobahn.twisted.util import peer2str
|
||||
from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost
|
||||
|
||||
import txaio
|
||||
|
||||
__all__ = (
|
||||
'WampRawSocketServerProtocol',
|
||||
'WampRawSocketClientProtocol',
|
||||
|
@ -48,10 +49,11 @@ class WampRawSocketProtocol(Int32StringReceiver):
|
|||
"""
|
||||
Base class for Twisted-based WAMP-over-RawSocket protocols.
|
||||
"""
|
||||
log = txaio.make_logger()
|
||||
|
||||
def connectionMade(self):
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: connection made")
|
||||
self.log.debug("WampRawSocketProtocol: connection made")
|
||||
|
||||
# the peer we are connected to
|
||||
#
|
||||
|
@ -92,42 +94,42 @@ class WampRawSocketProtocol(Int32StringReceiver):
|
|||
except Exception as e:
|
||||
# Exceptions raised in onOpen are fatal ..
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: ApplicationSession constructor / onOpen raised ({0})".format(e))
|
||||
self.log.info("WampRawSocketProtocol: ApplicationSession constructor / onOpen raised ({0})".format(e))
|
||||
self.abort()
|
||||
else:
|
||||
if self.factory.debug:
|
||||
log.msg("ApplicationSession started.")
|
||||
self.log.info("ApplicationSession started.")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: connection lost: reason = '{0}'".format(reason))
|
||||
self.log.info("WampRawSocketProtocol: connection lost: reason = '{0}'".format(reason))
|
||||
try:
|
||||
wasClean = isinstance(reason.value, ConnectionDone)
|
||||
self._session.onClose(wasClean)
|
||||
except Exception as e:
|
||||
# silently ignore exceptions raised here ..
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: ApplicationSession.onClose raised ({0})".format(e))
|
||||
self.log.info("WampRawSocketProtocol: ApplicationSession.onClose raised ({0})".format(e))
|
||||
self._session = None
|
||||
|
||||
def stringReceived(self, payload):
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: RX octets: {0}".format(binascii.hexlify(payload)))
|
||||
self.log.info("WampRawSocketProtocol: RX octets: {0}".format(binascii.hexlify(payload)))
|
||||
try:
|
||||
for msg in self._serializer.unserialize(payload):
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: RX WAMP message: {0}".format(msg))
|
||||
self.log.info("WampRawSocketProtocol: RX WAMP message: {0}".format(msg))
|
||||
self._session.onMessage(msg)
|
||||
|
||||
except ProtocolError as e:
|
||||
log.msg(str(e))
|
||||
self.log.info(str(e))
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: WAMP Protocol Error ({0}) - aborting connection".format(e))
|
||||
self.log.info("WampRawSocketProtocol: WAMP Protocol Error ({0}) - aborting connection".format(e))
|
||||
self.abort()
|
||||
|
||||
except Exception as e:
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: WAMP Internal Error ({0}) - aborting connection".format(e))
|
||||
self.log.info("WampRawSocketProtocol: WAMP Internal Error ({0}) - aborting connection".format(e))
|
||||
self.abort()
|
||||
|
||||
def send(self, msg):
|
||||
|
@ -136,7 +138,7 @@ class WampRawSocketProtocol(Int32StringReceiver):
|
|||
"""
|
||||
if self.isOpen():
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: TX WAMP message: {0}".format(msg))
|
||||
self.log.info("WampRawSocketProtocol: TX WAMP message: {0}".format(msg))
|
||||
try:
|
||||
payload, _ = self._serializer.serialize(msg)
|
||||
except Exception as e:
|
||||
|
@ -145,7 +147,7 @@ class WampRawSocketProtocol(Int32StringReceiver):
|
|||
else:
|
||||
self.sendString(payload)
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: TX octets: {0}".format(binascii.hexlify(payload)))
|
||||
self.log.info("WampRawSocketProtocol: TX octets: {0}".format(binascii.hexlify(payload)))
|
||||
else:
|
||||
raise TransportLost()
|
||||
|
||||
|
@ -194,18 +196,18 @@ class WampRawSocketServerProtocol(WampRawSocketProtocol):
|
|||
if len(self._handshake_bytes) == 4:
|
||||
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: opening handshake received - {0}".format(binascii.b2a_hex(self._handshake_bytes)))
|
||||
self.log.info("WampRawSocketProtocol: opening handshake received - {0}".format(binascii.b2a_hex(self._handshake_bytes)))
|
||||
|
||||
if ord(self._handshake_bytes[0]) != 0x7f:
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{0}, but expected 0x7f".format(binascii.b2a_hex(self._handshake_bytes[0])))
|
||||
self.log.info("WampRawSocketProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{0}, but expected 0x7f".format(binascii.b2a_hex(self._handshake_bytes[0])))
|
||||
self.abort()
|
||||
|
||||
# peer requests us to send messages of maximum length 2**max_len_exp
|
||||
#
|
||||
self._max_len_send = 2 ** (9 + (ord(self._handshake_bytes[1]) >> 4))
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: client requests us to send out most {} bytes per message".format(self._max_len_send))
|
||||
self.log.info("WampRawSocketProtocol: client requests us to send out most {} bytes per message".format(self._max_len_send))
|
||||
|
||||
# client wants to speak this serialization format
|
||||
#
|
||||
|
@ -213,10 +215,10 @@ class WampRawSocketServerProtocol(WampRawSocketProtocol):
|
|||
if ser_id in self.factory._serializers:
|
||||
self._serializer = self.factory._serializers[ser_id]
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: client wants to use serializer {}".format(ser_id))
|
||||
self.log.info("WampRawSocketProtocol: client wants to use serializer {}".format(ser_id))
|
||||
else:
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: opening handshake - no suitable serializer found (client requested {0}, and we have {1})".format(ser_id, self.factory._serializers.keys()))
|
||||
self.log.info("WampRawSocketProtocol: opening handshake - no suitable serializer found (client requested {0}, and we have {1})".format(ser_id, self.factory._serializers.keys()))
|
||||
self.abort()
|
||||
|
||||
# we request the peer to send message of maximum length 2**reply_max_len_exp
|
||||
|
@ -235,7 +237,7 @@ class WampRawSocketServerProtocol(WampRawSocketProtocol):
|
|||
self._on_handshake_complete()
|
||||
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: opening handshake completed", self._serializer)
|
||||
self.log.info("WampRawSocketProtocol: opening handshake completed", self._serializer)
|
||||
|
||||
# consume any remaining data received already ..
|
||||
#
|
||||
|
@ -275,25 +277,25 @@ class WampRawSocketClientProtocol(WampRawSocketProtocol):
|
|||
if len(self._handshake_bytes) == 4:
|
||||
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: opening handshake received - {0}".format(binascii.b2a_hex(self._handshake_bytes)))
|
||||
self.log.info("WampRawSocketProtocol: opening handshake received - {0}".format(binascii.b2a_hex(self._handshake_bytes)))
|
||||
|
||||
if ord(self._handshake_bytes[0]) != 0x7f:
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{0}, but expected 0x7f".format(binascii.b2a_hex(self._handshake_bytes[0])))
|
||||
self.log.info("WampRawSocketProtocol: invalid magic byte (octet 1) in opening handshake: was 0x{0}, but expected 0x7f".format(binascii.b2a_hex(self._handshake_bytes[0])))
|
||||
self.abort()
|
||||
|
||||
# peer requests us to send messages of maximum length 2**max_len_exp
|
||||
#
|
||||
self._max_len_send = 2 ** (9 + (ord(self._handshake_bytes[1]) >> 4))
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: server requests us to send out most {} bytes per message".format(self._max_len_send))
|
||||
self.log.info("WampRawSocketProtocol: server requests us to send out most {} bytes per message".format(self._max_len_send))
|
||||
|
||||
# client wants to speak this serialization format
|
||||
#
|
||||
ser_id = ord(self._handshake_bytes[1]) & 0x0F
|
||||
if ser_id != self._serializer.RAWSOCKET_SERIALIZER_ID:
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: opening handshake - no suitable serializer found (server replied {0}, and we requested {1})".format(ser_id, self._serializer.RAWSOCKET_SERIALIZER_ID))
|
||||
self.log.info("WampRawSocketProtocol: opening handshake - no suitable serializer found (server replied {0}, and we requested {1})".format(ser_id, self._serializer.RAWSOCKET_SERIALIZER_ID))
|
||||
self.abort()
|
||||
|
||||
self._handshake_complete = True
|
||||
|
@ -301,7 +303,7 @@ class WampRawSocketClientProtocol(WampRawSocketProtocol):
|
|||
self._on_handshake_complete()
|
||||
|
||||
if self.factory.debug:
|
||||
log.msg("WampRawSocketProtocol: opening handshake completed", self._serializer)
|
||||
self.log.info("WampRawSocketProtocol: opening handshake completed", self._serializer)
|
||||
|
||||
# consume any remaining data received already ..
|
||||
#
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.protocols.policies import ProtocolWrapper
|
||||
|
@ -36,41 +39,16 @@ except ImportError:
|
|||
from twisted.web.resource import IResource, Resource
|
||||
from six import PY3
|
||||
|
||||
# The following imports reactor at module level
|
||||
# See: https://twistedmatrix.com/trac/ticket/6849
|
||||
from twisted.web.http import HTTPChannel
|
||||
|
||||
# .. and this also, since it imports t.w.http
|
||||
# The following triggers an import of reactor at module level!
|
||||
#
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
|
||||
__all__ = (
|
||||
'WebSocketResource',
|
||||
'HTTPChannelHixie76Aware',
|
||||
'WSGIRootResource',
|
||||
)
|
||||
|
||||
|
||||
class HTTPChannelHixie76Aware(HTTPChannel):
|
||||
"""
|
||||
Hixie-76 is deadly broken. It includes 8 bytes of body, but then does not
|
||||
set content-length header. This hacked HTTPChannel injects the missing
|
||||
HTTP header upon detecting Hixie-76. We need this since otherwise
|
||||
Twisted Web will silently ignore the body.
|
||||
|
||||
To use this, set ``protocol = HTTPChannelHixie76Aware`` on your
|
||||
`twisted.web.server.Site <http://twistedmatrix.com/documents/current/api/twisted.web.server.Site.html>`_ instance.
|
||||
|
||||
.. seealso: `Autobahn Twisted Web site example <https://github.com/tavendo/AutobahnPython/tree/master/examples/twisted/websocket/echo_site>`_
|
||||
"""
|
||||
|
||||
def headerReceived(self, line):
|
||||
header = line.split(':')[0].lower()
|
||||
if header == "sec-websocket-key1" and not self._transferDecoder:
|
||||
HTTPChannel.headerReceived(self, "Content-Length: 8")
|
||||
HTTPChannel.headerReceived(self, line)
|
||||
|
||||
|
||||
class WSGIRootResource(Resource):
|
||||
"""
|
||||
Root resource when you want a WSGI resource be the default serving
|
||||
|
@ -110,7 +88,6 @@ class WebSocketResource(object):
|
|||
"""
|
||||
A Twisted Web resource for WebSocket.
|
||||
"""
|
||||
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, factory):
|
||||
|
@ -180,7 +157,6 @@ class WebSocketResource(object):
|
|||
for h in request.requestHeaders.getAllRawHeaders():
|
||||
data += "%s: %s\x0d\x0a" % (h[0], ",".join(h[1]))
|
||||
data += "\x0d\x0a"
|
||||
data += request.content.read() # we need this for Hixie-76
|
||||
protocol.dataReceived(data)
|
||||
|
||||
return NOT_DONE_YET
|
||||
|
|
|
@ -26,69 +26,68 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
# t.i.reactor doesn't exist until we've imported it once, but we
|
||||
# need it to exist so we can @patch it out in the tests ...
|
||||
from twisted.internet import reactor # noqa
|
||||
from twisted.internet.defer import inlineCallbacks, succeed
|
||||
from twisted.trial import unittest
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
# t.i.reactor doesn't exist until we've imported it once, but we
|
||||
# need it to exist so we can @patch it out in the tests ...
|
||||
from twisted.internet import reactor # noqa
|
||||
from twisted.internet.defer import inlineCallbacks, succeed
|
||||
from twisted.trial import unittest
|
||||
from mock import patch, Mock
|
||||
|
||||
from mock import patch, Mock
|
||||
from autobahn.twisted.wamp import ApplicationRunner
|
||||
|
||||
from autobahn.twisted.wamp import ApplicationRunner
|
||||
|
||||
def raise_error(*args, **kw):
|
||||
raise RuntimeError("we always fail")
|
||||
def raise_error(*args, **kw):
|
||||
raise RuntimeError("we always fail")
|
||||
|
||||
class TestApplicationRunner(unittest.TestCase):
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_default(self, fakereactor):
|
||||
fakereactor.connectTCP = Mock(side_effect=raise_error)
|
||||
runner = ApplicationRunner(u'ws://fake:1234/ws', u'dummy realm')
|
||||
|
||||
# we should get "our" RuntimeError when we call run
|
||||
self.assertRaises(RuntimeError, runner.run, raise_error)
|
||||
class TestApplicationRunner(unittest.TestCase):
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_default(self, fakereactor):
|
||||
fakereactor.connectTCP = Mock(side_effect=raise_error)
|
||||
runner = ApplicationRunner(u'ws://fake:1234/ws', u'dummy realm')
|
||||
|
||||
# both reactor.run and reactor.stop should have been called
|
||||
self.assertEqual(fakereactor.run.call_count, 1)
|
||||
self.assertEqual(fakereactor.stop.call_count, 1)
|
||||
# we should get "our" RuntimeError when we call run
|
||||
self.assertRaises(RuntimeError, runner.run, raise_error)
|
||||
|
||||
@patch('twisted.internet.reactor')
|
||||
@inlineCallbacks
|
||||
def test_runner_no_run(self, fakereactor):
|
||||
fakereactor.connectTCP = Mock(side_effect=raise_error)
|
||||
runner = ApplicationRunner(u'ws://fake:1234/ws', u'dummy realm')
|
||||
# both reactor.run and reactor.stop should have been called
|
||||
self.assertEqual(fakereactor.run.call_count, 1)
|
||||
self.assertEqual(fakereactor.stop.call_count, 1)
|
||||
|
||||
try:
|
||||
yield runner.run(raise_error, start_reactor=False)
|
||||
self.fail() # should have raise an exception, via Deferred
|
||||
@patch('twisted.internet.reactor')
|
||||
@inlineCallbacks
|
||||
def test_runner_no_run(self, fakereactor):
|
||||
fakereactor.connectTCP = Mock(side_effect=raise_error)
|
||||
runner = ApplicationRunner(u'ws://fake:1234/ws', u'dummy realm')
|
||||
|
||||
except RuntimeError as e:
|
||||
# make sure it's "our" exception
|
||||
self.assertEqual(e.args[0], "we always fail")
|
||||
try:
|
||||
yield runner.run(raise_error, start_reactor=False)
|
||||
self.fail() # should have raise an exception, via Deferred
|
||||
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
except RuntimeError as e:
|
||||
# make sure it's "our" exception
|
||||
self.assertEqual(e.args[0], "we always fail")
|
||||
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_no_run_happypath(self, fakereactor):
|
||||
proto = Mock()
|
||||
fakereactor.connectTCP = Mock(return_value=succeed(proto))
|
||||
runner = ApplicationRunner(u'ws://fake:1234/ws', u'dummy realm')
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
|
||||
d = runner.run(Mock(), start_reactor=False)
|
||||
@patch('twisted.internet.reactor')
|
||||
def test_runner_no_run_happypath(self, fakereactor):
|
||||
proto = Mock()
|
||||
fakereactor.connectTCP = Mock(return_value=succeed(proto))
|
||||
runner = ApplicationRunner(u'ws://fake:1234/ws', u'dummy realm')
|
||||
|
||||
# shouldn't have actually connected to anything
|
||||
# successfully, and the run() call shouldn't have inserted
|
||||
# any of its own call/errbacks. (except the cleanup handler)
|
||||
self.assertFalse(d.called)
|
||||
self.assertEqual(1, len(d.callbacks))
|
||||
d = runner.run(Mock(), start_reactor=False)
|
||||
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
# shouldn't have actually connected to anything
|
||||
# successfully, and the run() call shouldn't have inserted
|
||||
# any of its own call/errbacks. (except the cleanup handler)
|
||||
self.assertFalse(d.called)
|
||||
self.assertEqual(1, len(d.callbacks))
|
||||
|
||||
# neither reactor.run() NOR reactor.stop() should have been called
|
||||
# (just connectTCP() will have been called)
|
||||
self.assertEqual(fakereactor.run.call_count, 0)
|
||||
self.assertEqual(fakereactor.stop.call_count, 0)
|
||||
|
|
|
@ -26,87 +26,86 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if os.environ.get('USE_TWISTED', False):
|
||||
import twisted.internet
|
||||
from twisted.trial.unittest import TestCase
|
||||
import twisted.internet
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
from mock import Mock
|
||||
from mock import Mock
|
||||
|
||||
from autobahn.twisted import choosereactor
|
||||
from autobahn.twisted import choosereactor
|
||||
|
||||
class ChooseReactorTests(TestCase):
|
||||
|
||||
def patch_reactor(self, name, new_reactor):
|
||||
"""
|
||||
Patch ``name`` so that Twisted will grab a fake reactor instead of
|
||||
a real one.
|
||||
"""
|
||||
if hasattr(twisted.internet, name):
|
||||
self.patch(twisted.internet, name, new_reactor)
|
||||
else:
|
||||
def _cleanup():
|
||||
delattr(twisted.internet, name)
|
||||
setattr(twisted.internet, name, new_reactor)
|
||||
|
||||
def patch_modules(self):
|
||||
"""
|
||||
Patch ``sys.modules`` so that Twisted believes there is no
|
||||
installed reactor.
|
||||
"""
|
||||
old_modules = dict(sys.modules)
|
||||
|
||||
new_modules = dict(sys.modules)
|
||||
del new_modules["twisted.internet.reactor"]
|
||||
class ChooseReactorTests(TestCase):
|
||||
|
||||
def patch_reactor(self, name, new_reactor):
|
||||
"""
|
||||
Patch ``name`` so that Twisted will grab a fake reactor instead of
|
||||
a real one.
|
||||
"""
|
||||
if hasattr(twisted.internet, name):
|
||||
self.patch(twisted.internet, name, new_reactor)
|
||||
else:
|
||||
def _cleanup():
|
||||
sys.modules = old_modules
|
||||
delattr(twisted.internet, name)
|
||||
setattr(twisted.internet, name, new_reactor)
|
||||
|
||||
self.addCleanup(_cleanup)
|
||||
sys.modules = new_modules
|
||||
def patch_modules(self):
|
||||
"""
|
||||
Patch ``sys.modules`` so that Twisted believes there is no
|
||||
installed reactor.
|
||||
"""
|
||||
old_modules = dict(sys.modules)
|
||||
|
||||
def test_unknown(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will use the default reactor if it is
|
||||
unable to detect the platform it is running on.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("default", reactor_mock)
|
||||
self.patch(sys, "platform", "unknown")
|
||||
new_modules = dict(sys.modules)
|
||||
del new_modules["twisted.internet.reactor"]
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
def _cleanup():
|
||||
sys.modules = old_modules
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
self.addCleanup(_cleanup)
|
||||
sys.modules = new_modules
|
||||
|
||||
def test_mac(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install KQueueReactor on
|
||||
Darwin (OS X).
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("kqreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "darwin")
|
||||
def test_unknown(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will use the default reactor if it is
|
||||
unable to detect the platform it is running on.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("default", reactor_mock)
|
||||
self.patch(sys, "platform", "unknown")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
||||
def test_linux(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install EPollReactor on Linux.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("epollreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "linux")
|
||||
def test_mac(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install KQueueReactor on
|
||||
Darwin (OS X).
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("kqreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "darwin")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
||||
def test_linux(self):
|
||||
"""
|
||||
``install_optimal_reactor`` will install EPollReactor on Linux.
|
||||
"""
|
||||
reactor_mock = Mock()
|
||||
self.patch_reactor("epollreactor", reactor_mock)
|
||||
self.patch(sys, "platform", "linux")
|
||||
|
||||
# Emulate that a reactor has not been installed
|
||||
self.patch_modules()
|
||||
|
||||
choosereactor.install_optimal_reactor()
|
||||
reactor_mock.install.assert_called_once_with()
|
||||
|
|
|
@ -22,31 +22,37 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
##############################################################################
|
||||
###############################################################################
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import logging
|
||||
import unittest2 as unittest
|
||||
|
||||
from autobahn.twisted.websocket import WebSocketServerFactory
|
||||
from autobahn.twisted.websocket import WebSocketServerProtocol
|
||||
from autobahn.test import FakeTransport
|
||||
|
||||
|
||||
# A logging handler for Trollius that prints everything out
|
||||
class PrintHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
print(record)
|
||||
class Hixie76RejectionTests(unittest.TestCase):
|
||||
"""
|
||||
Hixie-76 should not be accepted by an Autobahn server.
|
||||
"""
|
||||
def test_handshake_fails(self):
|
||||
"""
|
||||
A handshake from a client only supporting Hixie-76 will fail.
|
||||
"""
|
||||
t = FakeTransport()
|
||||
f = WebSocketServerFactory()
|
||||
p = WebSocketServerProtocol()
|
||||
p.factory = f
|
||||
p.transport = t
|
||||
|
||||
# from http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
http_request = b"GET /demo HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\nSec-WebSocket-Key2: 12998 5 Y3 1 .P00\r\nSec-WebSocket-Protocol: sample\r\nUpgrade: WebSocket\r\nSec-WebSocket-Key1: 4 @1 46546xW%0l 1 5\r\nOrigin: http://example.com\r\n\r\n^n:ds[4U"
|
||||
|
||||
h = PrintHandler()
|
||||
logging.getLogger("trollius").addHandler(h)
|
||||
|
||||
|
||||
def make_logger(logger_type=None):
|
||||
if logger_type == "twisted":
|
||||
# If we've been asked for the Twisted logger, try and get the new one
|
||||
try:
|
||||
from twisted.logger import Logger
|
||||
return Logger()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from logging import getLogger
|
||||
return getLogger()
|
||||
p.openHandshakeTimeout = 0
|
||||
p._connectionMade()
|
||||
p.data = http_request
|
||||
p.processHandshake()
|
||||
self.assertIn(b"HTTP/1.1 400", t._written)
|
||||
self.assertIn(b"Hixie76 protocol not supported", t._written)
|
|
@ -48,7 +48,7 @@ __all = (
|
|||
|
||||
def sleep(delay, reactor=None):
|
||||
"""
|
||||
Inline sleep for use in coroutines (Twisted ``inlineCallback`` decorated functions).
|
||||
Inline sleep for use in co-routines (Twisted ``inlineCallback`` decorated functions).
|
||||
|
||||
.. seealso::
|
||||
* `twisted.internet.defer.inlineCallbacks <http://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#inlineCallbacks>`__
|
||||
|
@ -68,18 +68,21 @@ def sleep(delay, reactor=None):
|
|||
|
||||
def peer2str(addr):
|
||||
"""
|
||||
Convert a Twisted address, as returned from ``self.transport.getPeer()`` to a string
|
||||
Convert a Twisted address as returned from ``self.transport.getPeer()`` to a string.
|
||||
|
||||
:returns: Returns a string representation of the peer on a Twisted transport.
|
||||
:rtype: unicode
|
||||
"""
|
||||
if isinstance(addr, IPv4Address):
|
||||
res = "tcp4:{0}:{1}".format(addr.host, addr.port)
|
||||
res = u"tcp4:{0}:{1}".format(addr.host, addr.port)
|
||||
elif _HAS_IPV6 and isinstance(addr, IPv6Address):
|
||||
res = "tcp6:{0}:{1}".format(addr.host, addr.port)
|
||||
res = u"tcp6:{0}:{1}".format(addr.host, addr.port)
|
||||
elif isinstance(addr, UNIXAddress):
|
||||
res = "unix:{0}".format(addr.name)
|
||||
res = u"unix:{0}".format(addr.name)
|
||||
elif isinstance(addr, PipeAddress):
|
||||
res = "<pipe>"
|
||||
res = u"<pipe>"
|
||||
else:
|
||||
# gracefully fallback if we can't map the peer's address
|
||||
res = "?:{0}".format(addr)
|
||||
res = u"?:{0}".format(addr)
|
||||
|
||||
return res
|
||||
|
|
|
@ -26,12 +26,10 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
import six
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.internet.defer import inlineCallbacks
|
||||
|
||||
from autobahn.wamp import protocol
|
||||
|
@ -39,6 +37,9 @@ from autobahn.wamp.types import ComponentConfig
|
|||
from autobahn.websocket.protocol import parseWsUrl
|
||||
from autobahn.twisted.websocket import WampWebSocketClientFactory
|
||||
|
||||
# new API
|
||||
# from autobahn.twisted.connection import Connection
|
||||
|
||||
import txaio
|
||||
txaio.use_twisted()
|
||||
|
||||
|
@ -48,7 +49,10 @@ __all__ = [
|
|||
'ApplicationSessionFactory',
|
||||
'ApplicationRunner',
|
||||
'Application',
|
||||
'Service'
|
||||
'Service',
|
||||
|
||||
# new API
|
||||
'Session'
|
||||
]
|
||||
|
||||
try:
|
||||
|
@ -64,16 +68,6 @@ class ApplicationSession(protocol.ApplicationSession):
|
|||
WAMP application session for Twisted-based applications.
|
||||
"""
|
||||
|
||||
def onUserError(self, e, msg):
|
||||
"""
|
||||
Override of wamp.ApplicationSession
|
||||
"""
|
||||
# see docs; will print currently-active exception to the logs,
|
||||
# which is just what we want.
|
||||
log.err(e)
|
||||
# also log the framework-provided error-message
|
||||
log.err(msg)
|
||||
|
||||
|
||||
class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
|
||||
"""
|
||||
|
@ -82,8 +76,8 @@ class ApplicationSessionFactory(protocol.ApplicationSessionFactory):
|
|||
|
||||
session = ApplicationSession
|
||||
"""
|
||||
The application session class this application session factory will use. Defaults to :class:`autobahn.twisted.wamp.ApplicationSession`.
|
||||
"""
|
||||
The application session class this application session factory will use. Defaults to :class:`autobahn.twisted.wamp.ApplicationSession`.
|
||||
"""
|
||||
|
||||
|
||||
class ApplicationRunner(object):
|
||||
|
@ -95,6 +89,8 @@ class ApplicationRunner(object):
|
|||
connecting to a WAMP router.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, url, realm, extra=None, serializers=None,
|
||||
debug=False, debug_wamp=False, debug_app=False,
|
||||
ssl=None):
|
||||
|
@ -169,9 +165,10 @@ class ApplicationRunner(object):
|
|||
|
||||
isSecure, host, port, resource, path, params = parseWsUrl(self.url)
|
||||
|
||||
# start logging to console
|
||||
if self.debug or self.debug_wamp or self.debug_app:
|
||||
log.startLogging(sys.stdout)
|
||||
txaio.start_logging(level='debug')
|
||||
else:
|
||||
txaio.start_logging(level='info')
|
||||
|
||||
# factory for use ApplicationSession
|
||||
def create():
|
||||
|
@ -181,7 +178,7 @@ class ApplicationRunner(object):
|
|||
except Exception as e:
|
||||
if start_reactor:
|
||||
# the app component could not be created .. fatal
|
||||
log.err(str(e))
|
||||
self.log.error(str(e))
|
||||
reactor.stop()
|
||||
else:
|
||||
# if we didn't start the reactor, it's up to the
|
||||
|
@ -325,6 +322,8 @@ class Application(object):
|
|||
creating, debugging and running WAMP application components.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, prefix=None):
|
||||
"""
|
||||
|
||||
|
@ -529,7 +528,7 @@ class Application(object):
|
|||
yield handler(*args, **kwargs)
|
||||
except Exception as e:
|
||||
# FIXME
|
||||
log.msg("Warning: exception in signal handler swallowed", e)
|
||||
self.log.info("Warning: exception in signal handler swallowed", e)
|
||||
|
||||
|
||||
if service:
|
||||
|
@ -610,3 +609,25 @@ if service:
|
|||
|
||||
client = clientClass(host, port, transport_factory)
|
||||
client.setServiceParent(self)
|
||||
|
||||
|
||||
# new API
|
||||
class Session(ApplicationSession):
|
||||
|
||||
def onJoin(self, details):
|
||||
return self.on_join(details)
|
||||
|
||||
def onLeave(self, details):
|
||||
return self.on_leave(details)
|
||||
|
||||
def onDisconnect(self):
|
||||
return self.on_disconnect()
|
||||
|
||||
def on_join(self):
|
||||
pass
|
||||
|
||||
def on_leave(self, details):
|
||||
self.disconnect()
|
||||
|
||||
def on_disconnect(self):
|
||||
pass
|
||||
|
|
|
@ -37,11 +37,12 @@ from twisted.internet.error import ConnectionDone, ConnectionAborted, \
|
|||
ConnectionLost
|
||||
|
||||
from autobahn.wamp import websocket
|
||||
from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, \
|
||||
ConnectionDeny
|
||||
from autobahn.websocket import protocol
|
||||
from autobahn.websocket import http
|
||||
from autobahn.twisted.util import peer2str
|
||||
|
||||
from autobahn._logging import make_logger
|
||||
import txaio
|
||||
|
||||
from autobahn.websocket.compress import PerMessageDeflateOffer, \
|
||||
PerMessageDeflateOfferAccept, \
|
||||
|
@ -78,6 +79,7 @@ class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
|
|||
Adapter class for Twisted WebSocket client and server protocols.
|
||||
"""
|
||||
peer = '<never connected>'
|
||||
log = txaio.make_logger()
|
||||
|
||||
def connectionMade(self):
|
||||
# the peer we are connected to
|
||||
|
@ -90,6 +92,7 @@ class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
|
|||
self.peer = peer2str(peer)
|
||||
|
||||
self._connectionMade()
|
||||
self.log.info('Connection made to {peer}', peer=self.peer)
|
||||
|
||||
# Set "Nagle"
|
||||
try:
|
||||
|
@ -100,12 +103,12 @@ class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
|
|||
|
||||
def connectionLost(self, reason):
|
||||
if isinstance(reason.value, ConnectionDone):
|
||||
self.factory.log.debug("Connection to/from {peer} was closed cleanly",
|
||||
peer=self.peer)
|
||||
self.log.debug("Connection to/from {peer} was closed cleanly",
|
||||
peer=self.peer)
|
||||
|
||||
elif isinstance(reason.value, ConnectionAborted):
|
||||
self.factory.log.debug("Connection to/from {peer} was aborted locally",
|
||||
peer=self.peer)
|
||||
self.log.debug("Connection to/from {peer} was aborted locally",
|
||||
peer=self.peer)
|
||||
|
||||
elif isinstance(reason.value, ConnectionLost):
|
||||
# The following is ridiculous, but the treatment of reason.value.args
|
||||
|
@ -118,16 +121,16 @@ class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
|
|||
message = None
|
||||
|
||||
if message:
|
||||
self.factory.log.debug("Connection to/from {peer} was lost in a non-clean fashion: {message}",
|
||||
peer=self.peer, message=message)
|
||||
self.log.debug("Connection to/from {peer} was lost in a non-clean fashion: {message}",
|
||||
peer=self.peer, message=message)
|
||||
else:
|
||||
self.factory.log.debug("Connection to/from {peer} was lost in a non-clean fashion",
|
||||
peer=self.peer)
|
||||
self.log.debug("Connection to/from {peer} was lost in a non-clean fashion",
|
||||
peer=self.peer)
|
||||
|
||||
# at least: FileDescriptorOverrun, ConnectionFdescWentAway - but maybe others as well?
|
||||
else:
|
||||
self.factory.log.info("Connection to/from {peer} lost ({error_type}): {error})",
|
||||
peer=self.peer, error_type=type(reason.value), error=reason.value)
|
||||
self.log.info("Connection to/from {peer} lost ({error_type}): {error})",
|
||||
peer=self.peer, error_type=type(reason.value), error=reason.value)
|
||||
|
||||
self._connectionLost(reason)
|
||||
|
||||
|
@ -178,8 +181,6 @@ class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol):
|
|||
"""
|
||||
Register a Twisted producer with this protocol.
|
||||
|
||||
Modes: Hybi, Hixie
|
||||
|
||||
:param producer: A Twisted push or pull producer.
|
||||
:type producer: object
|
||||
:param streaming: Producer type.
|
||||
|
@ -201,12 +202,11 @@ class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServer
|
|||
res.addCallback(self.succeedHandshake)
|
||||
|
||||
def forwardError(failure):
|
||||
if failure.check(http.HttpException):
|
||||
if failure.check(ConnectionDeny):
|
||||
return self.failHandshake(failure.value.reason, failure.value.code)
|
||||
else:
|
||||
if self.debug:
|
||||
self.factory._log("Unexpected exception in onConnect ['%s']" % failure.value)
|
||||
return self.failHandshake(http.INTERNAL_SERVER_ERROR[1], http.INTERNAL_SERVER_ERROR[0])
|
||||
self.log.debug("Unexpected exception in onConnect ['{failure.value}']", failure=failure)
|
||||
return self.failHandshake("Internal server error: {}".format(failure.value), ConnectionDeny.INTERNAL_SERVER_ERROR)
|
||||
|
||||
res.addErrback(forwardError)
|
||||
|
||||
|
@ -224,7 +224,6 @@ class WebSocketAdapterFactory(object):
|
|||
"""
|
||||
Adapter class for Twisted-based WebSocket client and server factories.
|
||||
"""
|
||||
log = make_logger("twisted")
|
||||
|
||||
|
||||
class WebSocketServerFactory(WebSocketAdapterFactory, protocol.WebSocketServerFactory, twisted.internet.protocol.ServerFactory):
|
||||
|
@ -293,14 +292,14 @@ class WrappingWebSocketAdapter(object):
|
|||
def onConnect(self, requestOrResponse):
|
||||
|
||||
# Negotiate either the 'binary' or the 'base64' WebSocket subprotocol
|
||||
if isinstance(requestOrResponse, protocol.ConnectionRequest):
|
||||
if isinstance(requestOrResponse, ConnectionRequest):
|
||||
request = requestOrResponse
|
||||
for p in request.protocols:
|
||||
if p in self.factory._subprotocols:
|
||||
self._binaryMode = (p != 'base64')
|
||||
return p
|
||||
raise http.HttpException(http.NOT_ACCEPTABLE[0], "this server only speaks %s WebSocket subprotocols" % self.factory._subprotocols)
|
||||
elif isinstance(requestOrResponse, protocol.ConnectionResponse):
|
||||
raise ConnectionDeny(ConnectionDeny.NOT_ACCEPTABLE, "this server only speaks %s WebSocket subprotocols" % self.factory._subprotocols)
|
||||
elif isinstance(requestOrResponse, ConnectionResponse):
|
||||
response = requestOrResponse
|
||||
if response.protocol not in self.factory._subprotocols:
|
||||
self.failConnection(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, "this client only speaks %s WebSocket subprotocols" % self.factory._subprotocols)
|
||||
|
|
|
@ -37,8 +37,10 @@ import random
|
|||
from datetime import datetime, timedelta
|
||||
from pprint import pformat
|
||||
|
||||
import txaio
|
||||
|
||||
|
||||
__all__ = ("utcnow",
|
||||
"parseutc",
|
||||
"utcstr",
|
||||
"id",
|
||||
"rid",
|
||||
|
@ -47,9 +49,29 @@ __all__ = ("utcnow",
|
|||
"Stopwatch",
|
||||
"Tracker",
|
||||
"EqualityMixin",
|
||||
"ObservableMixin",
|
||||
"IdGenerator")
|
||||
|
||||
|
||||
def utcstr(ts=None):
|
||||
"""
|
||||
Format UTC timestamp in ISO 8601 format.
|
||||
|
||||
Note: to parse an ISO 8601 formatted string, use the **iso8601**
|
||||
module instead (e.g. ``iso8601.parse_date("2014-05-23T13:03:44.123Z")``).
|
||||
|
||||
:param ts: The timestamp to format.
|
||||
:type ts: instance of :py:class:`datetime.datetime` or None
|
||||
|
||||
:returns: Timestamp formatted in ISO 8601 format.
|
||||
:rtype: unicode
|
||||
"""
|
||||
assert(ts is None or isinstance(ts, datetime))
|
||||
if ts is None:
|
||||
ts = datetime.utcnow()
|
||||
return u"{0}Z".format(ts.strftime(u"%Y-%m-%dT%H:%M:%S.%f")[:-3])
|
||||
|
||||
|
||||
def utcnow():
|
||||
"""
|
||||
Get current time in UTC as ISO 8601 string.
|
||||
|
@ -57,44 +79,7 @@ def utcnow():
|
|||
:returns: Current time as string in ISO 8601 format.
|
||||
:rtype: unicode
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
return u"{0}Z".format(now.strftime(u"%Y-%m-%dT%H:%M:%S.%f")[:-3])
|
||||
|
||||
|
||||
def utcstr(ts):
|
||||
"""
|
||||
Format UTC timestamp in ISO 8601 format.
|
||||
|
||||
:param ts: The timestamp to format.
|
||||
:type ts: instance of :py:class:`datetime.datetime`
|
||||
|
||||
:returns: Timestamp formatted in ISO 8601 format.
|
||||
:rtype: unicode
|
||||
"""
|
||||
if ts:
|
||||
return u"{0}Z".format(ts.strftime(u"%Y-%m-%dT%H:%M:%S.%f")[:-3])
|
||||
else:
|
||||
return ts
|
||||
|
||||
|
||||
def parseutc(datestr):
|
||||
"""
|
||||
Parse an ISO 8601 combined date and time string, like i.e. ``"2011-11-23T12:23:00Z"``
|
||||
into a UTC datetime instance.
|
||||
|
||||
.. deprecated:: 0.8.12
|
||||
Use the **iso8601** module instead (e.g. ``iso8601.parse_date("2014-05-23T13:03:44.123Z")``)
|
||||
|
||||
:param datestr: The datetime string to parse.
|
||||
:type datestr: unicode
|
||||
|
||||
:returns: The converted datetime object.
|
||||
:rtype: instance of :py:class:`datetime.datetime`
|
||||
"""
|
||||
try:
|
||||
return datetime.strptime(datestr, u"%Y-%m-%dT%H:%M:%SZ")
|
||||
except ValueError:
|
||||
return None
|
||||
return utcstr()
|
||||
|
||||
|
||||
class IdGenerator(object):
|
||||
|
@ -477,3 +462,35 @@ def wildcards2patterns(wildcards):
|
|||
:rtype: list of obj
|
||||
"""
|
||||
return [re.compile(wc.replace('.', '\.').replace('*', '.*')) for wc in wildcards]
|
||||
|
||||
|
||||
class ObservableMixin(object):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self._parent = parent
|
||||
self._listeners = {}
|
||||
|
||||
def on(self, event, handler):
|
||||
if event not in self._listeners:
|
||||
self._listeners[event] = set()
|
||||
self._listeners[event].add(handler)
|
||||
|
||||
def off(self, event=None, handler=None):
|
||||
if event is None:
|
||||
self._listeners = {}
|
||||
else:
|
||||
if event in self._listeners:
|
||||
if handler is None:
|
||||
del self._listeners[event]
|
||||
else:
|
||||
self._listeners[event].discard(handler)
|
||||
|
||||
def fire(self, event, *args, **kwargs):
|
||||
res = []
|
||||
if event in self._listeners:
|
||||
for handler in self._listeners[event]:
|
||||
value = txaio.as_future(handler, *args, **kwargs)
|
||||
res.append(value)
|
||||
if self._parent is not None:
|
||||
res.append(self._parent.fire(event, *args, **kwargs))
|
||||
return txaio.gather(res)
|
||||
|
|
|
@ -26,43 +26,59 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from autobahn.wamp.uri import Pattern
|
||||
from autobahn.wamp.types import \
|
||||
SessionDetails, \
|
||||
CloseDetails, \
|
||||
RegisterOptions, \
|
||||
CallOptions, \
|
||||
CallDetails, \
|
||||
CallResult, \
|
||||
SubscribeOptions, \
|
||||
PublishOptions, \
|
||||
EventDetails
|
||||
|
||||
from autobahn.wamp.exception import \
|
||||
Error, \
|
||||
SessionNotReady, \
|
||||
SerializationError, \
|
||||
ProtocolError, \
|
||||
TransportLost, \
|
||||
ApplicationError, \
|
||||
InvalidUri
|
||||
|
||||
from autobahn.wamp.interfaces import \
|
||||
ISession, \
|
||||
IApplicationSession
|
||||
|
||||
from autobahn.wamp.uri import \
|
||||
error, \
|
||||
register, \
|
||||
subscribe
|
||||
|
||||
|
||||
def register(uri):
|
||||
"""
|
||||
Decorator for WAMP procedure endpoints.
|
||||
"""
|
||||
def decorate(f):
|
||||
assert(callable(f))
|
||||
if not hasattr(f, '_wampuris'):
|
||||
f._wampuris = []
|
||||
f._wampuris.append(Pattern(uri, Pattern.URI_TARGET_ENDPOINT))
|
||||
return f
|
||||
return decorate
|
||||
__all__ = (
|
||||
'SessionDetails',
|
||||
'CloseDetails',
|
||||
'RegisterOptions',
|
||||
'CallOptions',
|
||||
'CallDetails',
|
||||
'CallResult',
|
||||
'SubscribeOptions',
|
||||
'PublishOptions',
|
||||
'EventDetails',
|
||||
|
||||
'Error',
|
||||
'SessionNotReady',
|
||||
'SerializationError',
|
||||
'ProtocolError',
|
||||
'TransportLost',
|
||||
'ApplicationError',
|
||||
'InvalidUri',
|
||||
|
||||
def subscribe(uri):
|
||||
"""
|
||||
Decorator for WAMP event handlers.
|
||||
"""
|
||||
def decorate(f):
|
||||
assert(callable(f))
|
||||
if not hasattr(f, '_wampuris'):
|
||||
f._wampuris = []
|
||||
f._wampuris.append(Pattern(uri, Pattern.URI_TARGET_HANDLER))
|
||||
return f
|
||||
return decorate
|
||||
'ISession',
|
||||
'IApplicationSession',
|
||||
|
||||
|
||||
def error(uri):
|
||||
"""
|
||||
Decorator for WAMP error classes.
|
||||
"""
|
||||
def decorate(cls):
|
||||
assert(issubclass(cls, Exception))
|
||||
if not hasattr(cls, '_wampuris'):
|
||||
cls._wampuris = []
|
||||
cls._wampuris.append(Pattern(uri, Pattern.URI_TARGET_EXCEPTION))
|
||||
return cls
|
||||
return decorate
|
||||
'error',
|
||||
'register',
|
||||
'subscribe',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
|
||||
import txaio
|
||||
|
||||
from autobahn.util import ObservableMixin
|
||||
from autobahn.websocket.protocol import parseWsUrl
|
||||
from autobahn.wamp.types import ComponentConfig
|
||||
|
||||
__all__ = ('Connection')
|
||||
|
||||
|
||||
def check_endpoint(endpoint, check_native_endpoint=None):
|
||||
"""
|
||||
Check a WAMP connecting endpoint configuration.
|
||||
"""
|
||||
if type(endpoint) != dict:
|
||||
check_native_endpoint(endpoint)
|
||||
else:
|
||||
if 'type' not in endpoint:
|
||||
raise RuntimeError('missing type in endpoint')
|
||||
if endpoint['type'] not in ['tcp', 'unix']:
|
||||
raise RuntimeError('invalid type "{}" in endpoint'.format(endpoint['type']))
|
||||
|
||||
if endpoint['type'] == 'tcp':
|
||||
pass
|
||||
elif endpoint['type'] == 'unix':
|
||||
pass
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
|
||||
def check_transport(transport, check_native_endpoint=None):
|
||||
"""
|
||||
Check a WAMP connecting transport configuration.
|
||||
"""
|
||||
if type(transport) != dict:
|
||||
raise RuntimeError('invalid type {} for transport configuration - must be a dict'.format(type(transport)))
|
||||
|
||||
if 'type' not in transport:
|
||||
raise RuntimeError('missing type in transport')
|
||||
|
||||
if transport['type'] not in ['websocket', 'rawsocket']:
|
||||
raise RuntimeError('invalid transport type {}'.format(transport['type']))
|
||||
|
||||
if transport['type'] == 'websocket':
|
||||
pass
|
||||
elif transport['type'] == 'rawsocket':
|
||||
pass
|
||||
else:
|
||||
assert(False), 'should not arrive here'
|
||||
|
||||
|
||||
class Connection(ObservableMixin):
|
||||
|
||||
session = None
|
||||
"""
|
||||
The factory of the session we will instantiate.
|
||||
"""
|
||||
|
||||
def __init__(self, main=None, transports=u'ws://127.0.0.1:8080/ws', realm=u'default', extra=None):
|
||||
ObservableMixin.__init__(self)
|
||||
|
||||
if main is not None and not callable(main):
|
||||
raise RuntimeError('"main" must be a callable if given')
|
||||
|
||||
if type(realm) != six.text_type:
|
||||
raise RuntimeError('invalid type {} for "realm" - must be Unicode'.format(type(realm)))
|
||||
|
||||
# backward compatibility / convenience: allows to provide an URL instead of a
|
||||
# list of transports
|
||||
if type(transports) == six.text_type:
|
||||
url = transports
|
||||
is_secure, host, port, resource, path, params = parseWsUrl(url)
|
||||
transport = {
|
||||
'type': 'websocket',
|
||||
'url': url,
|
||||
'endpoint': {
|
||||
'type': 'tcp',
|
||||
'host': host,
|
||||
'port': port
|
||||
}
|
||||
}
|
||||
if is_secure:
|
||||
# FIXME
|
||||
transport['endpoint']['tls'] = {}
|
||||
transports = [transport]
|
||||
|
||||
for transport in transports:
|
||||
check_transport(transport)
|
||||
|
||||
self._main = main
|
||||
self._transports = transports
|
||||
self._realm = realm
|
||||
self._extra = extra
|
||||
|
||||
def start(self, reactor):
|
||||
raise RuntimeError('not implemented')
|
||||
|
||||
def _connect_once(self, reactor, transport_config):
|
||||
|
||||
done = txaio.create_future()
|
||||
|
||||
# factory for ISession objects
|
||||
def create_session():
|
||||
cfg = ComponentConfig(self._realm, self._extra)
|
||||
try:
|
||||
session = self.session(cfg)
|
||||
except Exception:
|
||||
# couldn't instantiate session calls, which is fatal.
|
||||
# let the reconnection logic deal with that
|
||||
raise
|
||||
else:
|
||||
# let child listener bubble up event
|
||||
session._parent = self
|
||||
|
||||
# listen on leave events
|
||||
def on_leave(session, details):
|
||||
print("on_leave: {}".format(details))
|
||||
|
||||
session.on('leave', on_leave)
|
||||
|
||||
# listen on disconnect events
|
||||
def on_disconnect(session, was_clean):
|
||||
print("on_disconnect: {}".format(was_clean))
|
||||
if was_clean:
|
||||
done.callback(None)
|
||||
else:
|
||||
done.errback(RuntimeError('transport closed uncleanly'))
|
||||
|
||||
session.on('disconnect', on_disconnect)
|
||||
|
||||
# return the fresh session object
|
||||
return session
|
||||
|
||||
d = self._connect_transport(reactor, transport_config, create_session)
|
||||
|
||||
def on_connect_sucess(res):
|
||||
print('on_connect_sucess', res)
|
||||
|
||||
def on_connect_failure(err):
|
||||
print('on_connect_failure', err)
|
||||
done.errback(err)
|
||||
|
||||
txaio.add_callbacks(d, on_connect_sucess, on_connect_failure)
|
||||
|
||||
return done
|
|
@ -28,7 +28,7 @@ from __future__ import absolute_import
|
|||
|
||||
from six import PY3
|
||||
|
||||
from autobahn.wamp import error
|
||||
from autobahn.wamp.uri import error
|
||||
|
||||
__all__ = (
|
||||
'Error',
|
||||
|
@ -209,13 +209,22 @@ class ApplicationError(Error):
|
|||
self.kwargs = kwargs
|
||||
self.error = error
|
||||
|
||||
def error_message(self):
|
||||
"""
|
||||
Get the error message of this exception.
|
||||
|
||||
:return: unicode
|
||||
"""
|
||||
return u'{}: {}'.format(self.error, u' '.join(self.args))
|
||||
|
||||
def __unicode__(self):
|
||||
if self.kwargs and 'traceback' in self.kwargs:
|
||||
tb = u':\n' + u'\n'.join(self.kwargs.pop('traceback')) + u'\n'
|
||||
self.kwargs['traceback'] = u'...'
|
||||
else:
|
||||
tb = u''
|
||||
return u"ApplicationError('{0}', args = {1}, kwargs = {2}){3}".format(self.error, self.args, self.kwargs, tb)
|
||||
return u"ApplicationError('{0}', args = {1}, kwargs = {2}){3}".format(
|
||||
self.error, self.args, self.kwargs, tb)
|
||||
|
||||
def __str__(self):
|
||||
if PY3:
|
||||
|
|
|
@ -27,6 +27,15 @@
|
|||
import abc
|
||||
import six
|
||||
|
||||
__all__ = (
|
||||
'IObjectSerializer',
|
||||
'ISerializer',
|
||||
'ITransport',
|
||||
'ITransportHandler',
|
||||
'ISession',
|
||||
'IApplicationSession',
|
||||
)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IObjectSerializer(object):
|
||||
|
@ -66,78 +75,6 @@ class IObjectSerializer(object):
|
|||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IMessage(object):
|
||||
"""
|
||||
A WAMP message.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def MESSAGE_TYPE(self):
|
||||
"""
|
||||
WAMP message type code.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def marshal(self):
|
||||
"""
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
|
||||
# @abc.abstractstaticmethod ## FIXME: this is Python 3 only
|
||||
# noinspection PyMethodParameters
|
||||
def parse(wmsg):
|
||||
"""
|
||||
Factory method that parses a unserialized raw message (as returned byte
|
||||
:func:`autobahn.interfaces.ISerializer.unserialize`) into an instance
|
||||
of this class.
|
||||
|
||||
:returns: obj -- An instance of this class.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def serialize(self, serializer):
|
||||
"""
|
||||
Serialize this object into a wire level bytes representation and cache
|
||||
the resulting bytes. If the cache already contains an entry for the given
|
||||
serializer, return the cached representation directly.
|
||||
|
||||
:param serializer: The wire level serializer to use.
|
||||
:type serializer: An instance that implements :class:`autobahn.interfaces.ISerializer`
|
||||
|
||||
:returns: bytes -- The serialized bytes.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def uncache(self):
|
||||
"""
|
||||
Resets the serialization cache.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Message equality. This does an attribute-wise comparison (but skips attributes
|
||||
that start with `_`).
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __ne__(self, other):
|
||||
"""
|
||||
Message inequality (just the negate of message equality).
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __str__(self):
|
||||
"""
|
||||
Returns text representation of this message.
|
||||
|
||||
:returns: str -- Human readable representation (e.g. for logging or debugging purposes).
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ISerializer(object):
|
||||
"""
|
||||
|
@ -159,23 +96,26 @@ class ISerializer(object):
|
|||
@abc.abstractmethod
|
||||
def serialize(self, message):
|
||||
"""
|
||||
Serializes a WAMP message to bytes to be sent to a transport.
|
||||
Serializes a WAMP message to bytes for sending over a transport.
|
||||
|
||||
:param message: An instance that implements :class:`autobahn.wamp.interfaces.IMessage`
|
||||
:type message: obj
|
||||
|
||||
:returns: tuple -- A pair ``(bytes, isBinary)``.
|
||||
:returns: tuple -- A pair ``(payload, is_binary)``.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def unserialize(self, payload, isBinary):
|
||||
def unserialize(self, payload, is_binary):
|
||||
"""
|
||||
Deserialize bytes from a transport and parse into WAMP messages.
|
||||
|
||||
:param payload: Byte string from wire.
|
||||
:type payload: bytes
|
||||
:param is_binary: Type of payload. True if payload is a binary string, else
|
||||
the payload is UTF-8 encoded Unicode text.
|
||||
:type is_binary: bool
|
||||
|
||||
:returns: list -- List of objects that implement :class:`autobahn.wamp.interfaces.IMessage`.
|
||||
:returns: list -- List of ``a.w.m.Message`` objects.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -191,13 +131,18 @@ class ITransport(object):
|
|||
"""
|
||||
Send a WAMP message over the transport to the peer. If the transport is
|
||||
not open, this raises :class:`autobahn.wamp.exception.TransportLost`.
|
||||
Returns a deferred/future when the message has been processed and more
|
||||
messages may be sent. When send() is called while a previous deferred/future
|
||||
has not yet fired, the send will fail immediately.
|
||||
|
||||
:param message: An instance that implements :class:`autobahn.wamp.interfaces.IMessage`
|
||||
:type message: obj
|
||||
|
||||
:returns: obj -- A Deferred/Future
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def isOpen(self):
|
||||
def is_open(self):
|
||||
"""
|
||||
Check if the transport is open for messaging.
|
||||
|
||||
|
@ -225,31 +170,43 @@ class ITransport(object):
|
|||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ITransportHandler(object):
|
||||
|
||||
@abc.abstractmethod
|
||||
def onOpen(self, transport):
|
||||
@abc.abstractproperty
|
||||
def transport(self):
|
||||
"""
|
||||
Callback fired when transport is open.
|
||||
When the transport this handler is attached to is currently open, this property
|
||||
can be read from. The property should be considered read-only. When the transport
|
||||
is gone, this property is set to None.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def on_open(self, transport):
|
||||
"""
|
||||
Callback fired when transport is open. May run asynchronously. The transport
|
||||
is considered running and is_open() would return true, as soon as this callback
|
||||
has completed successfully.
|
||||
|
||||
:param transport: An instance that implements :class:`autobahn.wamp.interfaces.ITransport`
|
||||
:type transport: obj
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onMessage(self, message):
|
||||
def on_message(self, message):
|
||||
"""
|
||||
Callback fired when a WAMP message was received.
|
||||
Callback fired when a WAMP message was received. May run asynchronously. The callback
|
||||
should return or fire the returned deferred/future when it's done processing the message.
|
||||
In particular, an implementation of this callback must not access the message afterwards.
|
||||
|
||||
:param message: An instance that implements :class:`autobahn.wamp.interfaces.IMessage`
|
||||
:type message: obj
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onClose(self, wasClean):
|
||||
def on_close(self, was_clean):
|
||||
"""
|
||||
Callback fired when the transport has been closed.
|
||||
|
||||
:param wasClean: Indicates if the transport has been closed regularly.
|
||||
:type wasClean: bool
|
||||
:param was_clean: Indicates if the transport has been closed regularly.
|
||||
:type was_clean: bool
|
||||
"""
|
||||
|
||||
|
||||
|
@ -260,7 +217,7 @@ class ISession(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onConnect(self):
|
||||
def on_connect(self):
|
||||
"""
|
||||
Callback fired when the transport this session will run over has been established.
|
||||
"""
|
||||
|
@ -272,7 +229,7 @@ class ISession(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onChallenge(self, challenge):
|
||||
def on_challenge(self, challenge):
|
||||
"""
|
||||
Callback fired when the peer demands authentication.
|
||||
|
||||
|
@ -283,7 +240,7 @@ class ISession(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onJoin(self, details):
|
||||
def on_join(self, details):
|
||||
"""
|
||||
Callback fired when WAMP session has been established.
|
||||
|
||||
|
@ -308,7 +265,7 @@ class ISession(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onLeave(self, details):
|
||||
def on_leave(self, details):
|
||||
"""
|
||||
Callback fired when WAMP session has is closed
|
||||
|
||||
|
@ -329,11 +286,19 @@ class ISession(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onDisconnect(self):
|
||||
def on_disconnect(self):
|
||||
"""
|
||||
Callback fired when underlying transport has been closed.
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IApplicationSession(ISession):
|
||||
"""
|
||||
Interface for WAMP client peers implementing the four different
|
||||
WAMP roles (caller, callee, publisher, subscriber).
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def define(self, exception, error=None):
|
||||
"""
|
||||
|
@ -346,12 +311,6 @@ class ISession(object):
|
|||
:type error: str
|
||||
"""
|
||||
|
||||
|
||||
class ICaller(ISession):
|
||||
"""
|
||||
Interface for WAMP peers implementing role *Caller*.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def call(self, procedure, *args, **kwargs):
|
||||
"""
|
||||
|
@ -386,54 +345,6 @@ class ICaller(ISession):
|
|||
:rtype: instance of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IRegistration(object):
|
||||
"""
|
||||
Represents a registration of an endpoint.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def id(self):
|
||||
"""
|
||||
The WAMP registration ID for this registration.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def active(self):
|
||||
"""
|
||||
Flag indicating if registration is active.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def unregister(self):
|
||||
"""
|
||||
Unregister this registration that was previously created from
|
||||
:func:`autobahn.wamp.interfaces.ICallee.register`.
|
||||
|
||||
After a registration has been unregistered successfully, no calls
|
||||
will be routed to the endpoint anymore.
|
||||
|
||||
Returns an instance of :tx:`twisted.internet.defer.Deferred` (when
|
||||
running on **Twisted**) or an instance of :py:class:`asyncio.Future`
|
||||
(when running on **asyncio**).
|
||||
|
||||
- If the unregistration succeeds, the returned Deferred/Future will
|
||||
*resolve* (with no return value).
|
||||
|
||||
- If the unregistration fails, the returned Deferred/Future will be rejected
|
||||
with an instance of :class:`autobahn.wamp.exception.ApplicationError`.
|
||||
|
||||
:returns: A Deferred/Future for the unregistration
|
||||
:rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
||||
|
||||
class ICallee(ISession):
|
||||
"""
|
||||
Interface for WAMP peers implementing role *Callee*.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def register(self, endpoint, procedure=None, options=None):
|
||||
"""
|
||||
|
@ -467,25 +378,6 @@ class ICallee(ISession):
|
|||
:rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class IPublication(object):
|
||||
"""
|
||||
Represents a publication of an event. This is used with acknowledged publications.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def id(self):
|
||||
"""
|
||||
The WAMP publication ID for this publication.
|
||||
"""
|
||||
|
||||
|
||||
class IPublisher(ISession):
|
||||
"""
|
||||
Interface for WAMP peers implementing role *Publisher*.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def publish(self, topic, *args, **kwargs):
|
||||
"""
|
||||
|
@ -520,54 +412,6 @@ class IPublisher(ISession):
|
|||
:rtype: ``None`` or instance of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ISubscription(object):
|
||||
"""
|
||||
Represents a subscription to a topic.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def id(self):
|
||||
"""
|
||||
The WAMP subscription ID for this subscription.
|
||||
"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def active(self):
|
||||
"""
|
||||
Flag indicating if subscription is active.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def unsubscribe(self):
|
||||
"""
|
||||
Unsubscribe this subscription that was previously created from
|
||||
:func:`autobahn.wamp.interfaces.ISubscriber.subscribe`.
|
||||
|
||||
After a subscription has been unsubscribed successfully, no events
|
||||
will be routed to the event handler anymore.
|
||||
|
||||
Returns an instance of :tx:`twisted.internet.defer.Deferred` (when
|
||||
running on **Twisted**) or an instance of :py:class:`asyncio.Future`
|
||||
(when running on **asyncio**).
|
||||
|
||||
- If the unsubscription succeeds, the returned Deferred/Future will
|
||||
*resolve* (with no return value).
|
||||
|
||||
- If the unsubscription fails, the returned Deferred/Future will *reject*
|
||||
with an instance of :class:`autobahn.wamp.exception.ApplicationError`.
|
||||
|
||||
:returns: A Deferred/Future for the unsubscription
|
||||
:rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
||||
|
||||
class ISubscriber(ISession):
|
||||
"""
|
||||
Interface for WAMP peers implementing role *Subscriber*.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def subscribe(self, handler, topic=None, options=None):
|
||||
"""
|
||||
|
|
|
@ -32,7 +32,6 @@ import six
|
|||
import autobahn
|
||||
from autobahn import util
|
||||
from autobahn.wamp.exception import ProtocolError
|
||||
from autobahn.wamp.interfaces import IMessage
|
||||
from autobahn.wamp.role import ROLE_NAME_TO_CLASS
|
||||
|
||||
__all__ = ('Message',
|
||||
|
@ -173,24 +172,46 @@ def check_or_raise_extra(value, message=u"WAMP message invalid"):
|
|||
|
||||
class Message(util.EqualityMixin):
|
||||
"""
|
||||
WAMP message base class. Implements :class:`autobahn.wamp.interfaces.IMessage`.
|
||||
WAMP message base class.
|
||||
|
||||
.. note:: This is not supposed to be instantiated.
|
||||
"""
|
||||
|
||||
MESSAGE_TYPE = None
|
||||
"""
|
||||
WAMP message type code.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# serialization cache: mapping from ISerializer instances to serialized bytes
|
||||
self._serialized = {}
|
||||
|
||||
@staticmethod
|
||||
def parse(wmsg):
|
||||
"""
|
||||
Factory method that parses a unserialized raw message (as returned byte
|
||||
:func:`autobahn.interfaces.ISerializer.unserialize`) into an instance
|
||||
of this class.
|
||||
|
||||
:returns: obj -- An instance of this class.
|
||||
"""
|
||||
|
||||
def uncache(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.uncache`
|
||||
Resets the serialization cache.
|
||||
"""
|
||||
self._serialized = {}
|
||||
|
||||
def serialize(self, serializer):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.serialize`
|
||||
Serialize this object into a wire level bytes representation and cache
|
||||
the resulting bytes. If the cache already contains an entry for the given
|
||||
serializer, return the cached representation directly.
|
||||
|
||||
:param serializer: The wire level serializer to use.
|
||||
:type serializer: An instance that implements :class:`autobahn.interfaces.ISerializer`
|
||||
|
||||
:returns: bytes -- The serialized bytes.
|
||||
"""
|
||||
# only serialize if not cached ..
|
||||
if serializer not in self._serialized:
|
||||
|
@ -198,9 +219,6 @@ class Message(util.EqualityMixin):
|
|||
return self._serialized[serializer]
|
||||
|
||||
|
||||
IMessage.register(Message)
|
||||
|
||||
|
||||
class Hello(Message):
|
||||
"""
|
||||
A WAMP ``HELLO`` message.
|
||||
|
@ -210,8 +228,8 @@ class Hello(Message):
|
|||
|
||||
MESSAGE_TYPE = 1
|
||||
"""
|
||||
The WAMP message code for this type of message.
|
||||
"""
|
||||
The WAMP message code for this type of message.
|
||||
"""
|
||||
|
||||
def __init__(self, realm, roles, authmethods=None, authid=None):
|
||||
"""
|
||||
|
@ -317,7 +335,9 @@ class Hello(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {u'roles': {}}
|
||||
for role in self.roles.values():
|
||||
|
@ -338,7 +358,7 @@ class Hello(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Return a string representation of this message.
|
||||
"""
|
||||
return "WAMP HELLO Message (realm = {0}, roles = {1}, authmethods = {2}, authid = {3})".format(self.realm, self.roles, self.authmethods, self.authid)
|
||||
|
||||
|
@ -449,7 +469,9 @@ class Welcome(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {
|
||||
u'roles': {}
|
||||
|
@ -479,7 +501,7 @@ class Welcome(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP WELCOME Message (session = {0}, roles = {1}, authid = {2}, authrole = {3}, authmethod = {4}, authprovider = {5})".format(self.session, self.roles, self.authid, self.authrole, self.authmethod, self.authprovider)
|
||||
|
||||
|
@ -547,7 +569,9 @@ class Abort(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {}
|
||||
if self.message:
|
||||
|
@ -557,7 +581,7 @@ class Abort(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP ABORT Message (message = {0}, reason = {1})".format(self.message, self.reason)
|
||||
|
||||
|
@ -618,13 +642,15 @@ class Challenge(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Challenge.MESSAGE_TYPE, self.method, self.extra]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP CHALLENGE Message (method = {0}, extra = {1})".format(self.method, self.extra)
|
||||
|
||||
|
@ -685,13 +711,15 @@ class Authenticate(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Authenticate.MESSAGE_TYPE, self.signature, self.extra]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP AUTHENTICATE Message (signature = {0}, extra = {1})".format(self.signature, self.extra)
|
||||
|
||||
|
@ -764,7 +792,9 @@ class Goodbye(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {}
|
||||
if self.message:
|
||||
|
@ -774,7 +804,7 @@ class Goodbye(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP GOODBYE Message (message = {0}, reason = {1})".format(self.message, self.reason)
|
||||
|
||||
|
@ -876,7 +906,9 @@ class Error(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {}
|
||||
|
||||
|
@ -889,7 +921,7 @@ class Error(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP Error Message (request_type = {0}, request = {1}, error = {2}, args = {3}, kwargs = {4})".format(self.request_type, self.request, self.error, self.args, self.kwargs)
|
||||
|
||||
|
@ -1069,7 +1101,9 @@ class Publish(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -1093,7 +1127,7 @@ class Publish(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP PUBLISH Message (request = {0}, topic = {1}, args = {2}, kwargs = {3}, acknowledge = {4}, exclude_me = {5}, exclude = {6}, eligible = {7}, disclose_me = {8})".format(self.request, self.topic, self.args, self.kwargs, self.acknowledge, self.exclude_me, self.exclude, self.eligible, self.disclose_me)
|
||||
|
||||
|
@ -1151,13 +1185,15 @@ class Published(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Published.MESSAGE_TYPE, self.request, self.publication]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP PUBLISHED Message (request = {0}, publication = {1})".format(self.request, self.publication)
|
||||
|
||||
|
@ -1238,7 +1274,9 @@ class Subscribe(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -1249,7 +1287,7 @@ class Subscribe(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP SUBSCRIBE Message (request = {0}, topic = {1}, match = {2})".format(self.request, self.topic, self.match)
|
||||
|
||||
|
@ -1307,13 +1345,15 @@ class Subscribed(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Subscribed.MESSAGE_TYPE, self.request, self.subscription]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP SUBSCRIBED Message (request = {0}, subscription = {1})".format(self.request, self.subscription)
|
||||
|
||||
|
@ -1371,13 +1411,15 @@ class Unsubscribe(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Unsubscribe.MESSAGE_TYPE, self.request, self.subscription]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP UNSUBSCRIBE Message (request = {0}, subscription = {1})".format(self.request, self.subscription)
|
||||
|
||||
|
@ -1460,7 +1502,9 @@ class Unsubscribed(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
if self.reason is not None or self.subscription is not None:
|
||||
details = {}
|
||||
|
@ -1474,7 +1518,7 @@ class Unsubscribed(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP UNSUBSCRIBED Message (request = {0}, reason = {1}, subscription = {2})".format(self.request, self.reason, self.subscription)
|
||||
|
||||
|
@ -1590,7 +1634,9 @@ class Event(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {}
|
||||
|
||||
|
@ -1609,7 +1655,7 @@ class Event(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP EVENT Message (subscription = {0}, publication = {1}, args = {2}, kwargs = {3}, publisher = {4}, topic = {5})".format(self.subscription, self.publication, self.args, self.kwargs, self.publisher, self.topic)
|
||||
|
||||
|
@ -1751,7 +1797,9 @@ class Call(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -1773,7 +1821,7 @@ class Call(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP CALL Message (request = {0}, procedure = {1}, args = {2}, kwargs = {3}, timeout = {4}, receive_progress = {5}, disclose_me = {6})".format(self.request, self.procedure, self.args, self.kwargs, self.timeout, self.receive_progress, self.disclose_me)
|
||||
|
||||
|
@ -1851,7 +1899,9 @@ class Cancel(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -1862,7 +1912,7 @@ class Cancel(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP CANCEL Message (request = {0}, mode = '{1}'')".format(self.request, self.mode)
|
||||
|
||||
|
@ -1957,7 +2007,9 @@ class Result(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
details = {}
|
||||
|
||||
|
@ -1973,7 +2025,7 @@ class Result(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP RESULT Message (request = {0}, args = {1}, kwargs = {2}, progress = {3})".format(self.request, self.args, self.kwargs, self.progress)
|
||||
|
||||
|
@ -2077,7 +2129,9 @@ class Register(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -2091,7 +2145,7 @@ class Register(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP REGISTER Message (request = {0}, procedure = {1}, match = {2}, invoke = {3})".format(self.request, self.procedure, self.match, self.invoke)
|
||||
|
||||
|
@ -2149,13 +2203,15 @@ class Registered(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Registered.MESSAGE_TYPE, self.request, self.registration]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP REGISTERED Message (request = {0}, registration = {1})".format(self.request, self.registration)
|
||||
|
||||
|
@ -2213,13 +2269,15 @@ class Unregister(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
return [Unregister.MESSAGE_TYPE, self.request, self.registration]
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP UNREGISTER Message (request = {0}, registration = {1})".format(self.request, self.registration)
|
||||
|
||||
|
@ -2301,7 +2359,9 @@ class Unregistered(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
if self.reason is not None or self.registration is not None:
|
||||
details = {}
|
||||
|
@ -2315,7 +2375,7 @@ class Unregistered(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP UNREGISTERED Message (request = {0}, reason = {1}, registration = {2})".format(self.request, self.reason, self.registration)
|
||||
|
||||
|
@ -2471,7 +2531,9 @@ class Invocation(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -2496,7 +2558,7 @@ class Invocation(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP INVOCATION Message (request = {0}, registration = {1}, args = {2}, kwargs = {3}, timeout = {4}, receive_progress = {5}, caller = {6}, procedure = {7})".format(self.request, self.registration, self.args, self.kwargs, self.timeout, self.receive_progress, self.caller, self.procedure)
|
||||
|
||||
|
@ -2573,7 +2635,9 @@ class Interrupt(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -2584,7 +2648,7 @@ class Interrupt(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP INTERRUPT Message (request = {0}, mode = '{1}')".format(self.request, self.mode)
|
||||
|
||||
|
@ -2679,7 +2743,9 @@ class Yield(Message):
|
|||
|
||||
def marshal(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.marshal`
|
||||
Marshal this object into a raw message for subsequent serialization to bytes.
|
||||
|
||||
:returns: list -- The serialized raw message.
|
||||
"""
|
||||
options = {}
|
||||
|
||||
|
@ -2695,6 +2761,6 @@ class Yield(Message):
|
|||
|
||||
def __str__(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IMessage.__str__`
|
||||
Returns string representation of this message.
|
||||
"""
|
||||
return "WAMP YIELD Message (request = {0}, args = {1}, kwargs = {2}, progress = {3})".format(self.request, self.args, self.kwargs, self.progress)
|
||||
|
|
|
@ -26,19 +26,9 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import traceback
|
||||
import inspect
|
||||
import six
|
||||
from six import StringIO
|
||||
|
||||
from autobahn.wamp.interfaces import ISession, \
|
||||
IPublication, \
|
||||
IPublisher, \
|
||||
ISubscription, \
|
||||
ISubscriber, \
|
||||
ICaller, \
|
||||
IRegistration, \
|
||||
ITransportHandler
|
||||
import txaio
|
||||
import inspect
|
||||
|
||||
from autobahn import wamp
|
||||
from autobahn.wamp import uri
|
||||
|
@ -47,204 +37,30 @@ from autobahn.wamp import types
|
|||
from autobahn.wamp import role
|
||||
from autobahn.wamp import exception
|
||||
from autobahn.wamp.exception import ApplicationError, ProtocolError, SessionNotReady, SerializationError
|
||||
from autobahn.wamp.interfaces import IApplicationSession # noqa
|
||||
from autobahn.wamp.types import SessionDetails
|
||||
from autobahn.util import IdGenerator
|
||||
from autobahn.util import IdGenerator, ObservableMixin
|
||||
|
||||
import txaio
|
||||
from autobahn.wamp.request import \
|
||||
Publication, \
|
||||
Subscription, \
|
||||
Handler, \
|
||||
Registration, \
|
||||
Endpoint, \
|
||||
PublishRequest, \
|
||||
SubscribeRequest, \
|
||||
UnsubscribeRequest, \
|
||||
CallRequest, \
|
||||
InvocationRequest, \
|
||||
RegisterRequest, \
|
||||
UnregisterRequest
|
||||
|
||||
|
||||
def is_method_or_function(f):
|
||||
return inspect.ismethod(f) or inspect.isfunction(f)
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
Object representing an outstanding request, such as for subscribe/unsubscribe,
|
||||
register/unregister or call/publish.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply):
|
||||
self.request_id = request_id
|
||||
self.on_reply = on_reply
|
||||
|
||||
|
||||
class InvocationRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to invoke an endpoint.
|
||||
"""
|
||||
|
||||
|
||||
class CallRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to call a procedure.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, options):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.options = options
|
||||
|
||||
|
||||
class PublishRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to publish (acknowledged) an event.
|
||||
"""
|
||||
|
||||
|
||||
class SubscribeRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to subscribe to a topic.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, handler):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.handler = handler
|
||||
|
||||
|
||||
class UnsubscribeRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to unsubscribe a subscription.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, subscription_id):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.subscription_id = subscription_id
|
||||
|
||||
|
||||
class RegisterRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to register a procedure.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, procedure, endpoint):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.procedure = procedure
|
||||
self.endpoint = endpoint
|
||||
|
||||
|
||||
class UnregisterRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to unregister a registration.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, registration_id):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.registration_id = registration_id
|
||||
|
||||
|
||||
class Subscription(object):
|
||||
"""
|
||||
Object representing a handler subscription.
|
||||
|
||||
This class implements :class:`autobahn.wamp.interfaces.ISubscription`.
|
||||
"""
|
||||
def __init__(self, subscription_id, session, handler):
|
||||
"""
|
||||
"""
|
||||
self.id = subscription_id
|
||||
self.active = True
|
||||
self.session = session
|
||||
self.handler = handler
|
||||
|
||||
def unsubscribe(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISubscription.unsubscribe`
|
||||
"""
|
||||
if self.active:
|
||||
return self.session._unsubscribe(self)
|
||||
else:
|
||||
raise Exception("subscription no longer active")
|
||||
|
||||
def __str__(self):
|
||||
return "Subscription(id={0}, is_active={1})".format(self.id, self.active)
|
||||
|
||||
|
||||
ISubscription.register(Subscription)
|
||||
|
||||
|
||||
class Handler(object):
|
||||
"""
|
||||
Object representing an event handler attached to a subscription.
|
||||
"""
|
||||
|
||||
def __init__(self, fn, obj=None, details_arg=None):
|
||||
"""
|
||||
|
||||
:param fn: The event handler function to be called.
|
||||
:type fn: callable
|
||||
:param obj: The (optional) object upon which to call the function.
|
||||
:type obj: obj or None
|
||||
:param details_arg: The keyword argument under which event details should be provided.
|
||||
:type details_arg: str or None
|
||||
"""
|
||||
self.fn = fn
|
||||
self.obj = obj
|
||||
self.details_arg = details_arg
|
||||
|
||||
|
||||
class Publication(object):
|
||||
"""
|
||||
Object representing a publication (feedback from publishing an event when doing
|
||||
an acknowledged publish).
|
||||
|
||||
This class implements :class:`autobahn.wamp.interfaces.IPublication`.
|
||||
"""
|
||||
def __init__(self, publication_id):
|
||||
self.id = publication_id
|
||||
|
||||
def __str__(self):
|
||||
return "Publication(id={0})".format(self.id)
|
||||
|
||||
|
||||
IPublication.register(Publication)
|
||||
|
||||
|
||||
class Registration(object):
|
||||
"""
|
||||
Object representing a registration.
|
||||
|
||||
This class implements :class:`autobahn.wamp.interfaces.IRegistration`.
|
||||
"""
|
||||
def __init__(self, session, registration_id, procedure, endpoint):
|
||||
self.id = registration_id
|
||||
self.active = True
|
||||
self.session = session
|
||||
self.procedure = procedure
|
||||
self.endpoint = endpoint
|
||||
|
||||
def unregister(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IRegistration.unregister`
|
||||
"""
|
||||
if self.active:
|
||||
return self.session._unregister(self)
|
||||
else:
|
||||
raise Exception("registration no longer active")
|
||||
|
||||
|
||||
IRegistration.register(Registration)
|
||||
|
||||
|
||||
class Endpoint(object):
|
||||
"""
|
||||
Object representing an procedure endpoint attached to a registration.
|
||||
"""
|
||||
|
||||
def __init__(self, fn, obj=None, details_arg=None):
|
||||
"""
|
||||
|
||||
:param fn: The endpoint procedure to be called.
|
||||
:type fn: callable
|
||||
:param obj: The (optional) object upon which to call the function.
|
||||
:type obj: obj or None
|
||||
:param details_arg: The keyword argument under which call details should be provided.
|
||||
:type details_arg: str or None
|
||||
"""
|
||||
self.fn = fn
|
||||
self.obj = obj
|
||||
self.details_arg = details_arg
|
||||
|
||||
|
||||
class BaseSession(object):
|
||||
class BaseSession(ObservableMixin):
|
||||
"""
|
||||
WAMP session base class.
|
||||
|
||||
|
@ -255,6 +71,8 @@ class BaseSession(object):
|
|||
"""
|
||||
|
||||
"""
|
||||
ObservableMixin.__init__(self)
|
||||
|
||||
# this is for library level debugging
|
||||
self.debug = False
|
||||
|
||||
|
@ -285,26 +103,6 @@ class BaseSession(object):
|
|||
# generator for WAMP request IDs
|
||||
self._request_id_gen = IdGenerator()
|
||||
|
||||
def onConnect(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onConnect`
|
||||
"""
|
||||
|
||||
def onJoin(self, details):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onJoin`
|
||||
"""
|
||||
|
||||
def onLeave(self, details):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onLeave`
|
||||
"""
|
||||
|
||||
def onDisconnect(self):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onDisconnect`
|
||||
"""
|
||||
|
||||
def define(self, exception, error=None):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.define`
|
||||
|
@ -387,9 +185,12 @@ class BaseSession(object):
|
|||
exc = ecls(*msg.args)
|
||||
else:
|
||||
exc = ecls()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
try:
|
||||
self.onUserError(e, "While re-constructing exception")
|
||||
self.onUserError(
|
||||
txaio.create_failure(),
|
||||
"While re-constructing exception",
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -409,20 +210,13 @@ class BaseSession(object):
|
|||
return exc
|
||||
|
||||
|
||||
ISession.register(BaseSession)
|
||||
|
||||
|
||||
class ApplicationSession(BaseSession):
|
||||
"""
|
||||
WAMP endpoint session. This class implements
|
||||
|
||||
* :class:`autobahn.wamp.interfaces.IPublisher`
|
||||
* :class:`autobahn.wamp.interfaces.ISubscriber`
|
||||
* :class:`autobahn.wamp.interfaces.ICaller`
|
||||
* :class:`autobahn.wamp.interfaces.ICallee`
|
||||
* :class:`autobahn.wamp.interfaces.ITransportHandler`
|
||||
WAMP endpoint session.
|
||||
"""
|
||||
|
||||
log = txaio.make_logger()
|
||||
|
||||
def __init__(self, config=None):
|
||||
"""
|
||||
Constructor.
|
||||
|
@ -502,7 +296,7 @@ class ApplicationSession(BaseSession):
|
|||
"""
|
||||
return self._transport is not None
|
||||
|
||||
def onUserError(self, e, msg):
|
||||
def onUserError(self, fail, msg):
|
||||
"""
|
||||
This is called when we try to fire a callback, but get an
|
||||
exception from user code -- for example, a registered publish
|
||||
|
@ -513,13 +307,19 @@ class ApplicationSession(BaseSession):
|
|||
provide logging if they prefer. The Twisted implemention does
|
||||
this. (See :class:`autobahn.twisted.wamp.ApplicationSession`)
|
||||
|
||||
:param e: the Exception we caught.
|
||||
:param fail: instance implementing txaio.IFailedFuture
|
||||
|
||||
:param msg: an informative message from the library. It is
|
||||
suggested you log this immediately after the exception.
|
||||
"""
|
||||
traceback.print_exc()
|
||||
print(msg)
|
||||
if isinstance(fail.value, exception.ApplicationError):
|
||||
self.log.error(fail.value.error_message())
|
||||
else:
|
||||
self.log.error(
|
||||
'{msg}: {traceback}',
|
||||
msg=msg,
|
||||
traceback=txaio.failure_format_traceback(fail),
|
||||
)
|
||||
|
||||
def _swallow_error(self, fail, msg):
|
||||
'''
|
||||
|
@ -533,9 +333,8 @@ class ApplicationSession(BaseSession):
|
|||
chain for a Deferred/coroutine that will make it out to user
|
||||
code.
|
||||
'''
|
||||
# print("_swallow_error", typ, exc, tb)
|
||||
try:
|
||||
self.onUserError(fail.value, msg)
|
||||
self.onUserError(fail, msg)
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
@ -710,9 +509,12 @@ class ApplicationSession(BaseSession):
|
|||
try:
|
||||
# XXX what if on_progress returns a Deferred/Future?
|
||||
call_request.options.on_progress(*args, **kw)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
try:
|
||||
self.onUserError(e, "While firing on_progress")
|
||||
self.onUserError(
|
||||
txaio.create_failure(),
|
||||
"While firing on_progress",
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -809,11 +611,7 @@ class ApplicationSession(BaseSession):
|
|||
pass
|
||||
formatted_tb = None
|
||||
if self.traceback_app:
|
||||
# if asked to marshal the traceback within the WAMP error message, extract it
|
||||
# noinspection PyCallingNonCallable
|
||||
tb = StringIO()
|
||||
err.printTraceback(file=tb)
|
||||
formatted_tb = tb.getvalue().splitlines()
|
||||
formatted_tb = txaio.failure_format_traceback(err)
|
||||
|
||||
del self._invocations[msg.request]
|
||||
|
||||
|
@ -846,10 +644,13 @@ class ApplicationSession(BaseSession):
|
|||
# noinspection PyBroadException
|
||||
try:
|
||||
self._invocations[msg.request].cancel()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# XXX can .cancel() return a Deferred/Future?
|
||||
try:
|
||||
self.onUserError(e, "While cancelling call.")
|
||||
self.onUserError(
|
||||
txaio.create_failure(),
|
||||
"While cancelling call.",
|
||||
)
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
|
@ -945,7 +746,7 @@ class ApplicationSession(BaseSession):
|
|||
|
||||
self._session_id = None
|
||||
|
||||
d = txaio.as_future(self.onDisconnect)
|
||||
d = txaio.as_future(self.onDisconnect, wasClean)
|
||||
|
||||
def _error(e):
|
||||
return self._swallow_error(e, "While firing onDisconnect")
|
||||
|
@ -961,13 +762,17 @@ class ApplicationSession(BaseSession):
|
|||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onJoin`
|
||||
"""
|
||||
return self.fire('join', self, details)
|
||||
|
||||
def onLeave(self, details):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onLeave`
|
||||
"""
|
||||
if details.reason.startswith('wamp.error.'):
|
||||
print('{error}: {message}'.format(error=details.reason, message=details.message))
|
||||
self.log.error('{reason}: {wamp_message}', reason=details.reason, wamp_message=details.message)
|
||||
|
||||
self.fire('leave', self, details)
|
||||
|
||||
if self._transport:
|
||||
self.disconnect()
|
||||
# do we ever call onLeave with a valid transport?
|
||||
|
@ -991,6 +796,12 @@ class ApplicationSession(BaseSession):
|
|||
else:
|
||||
raise SessionNotReady(u"Already requested to close the session")
|
||||
|
||||
def onDisconnect(self, wasClean):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.ISession.onDisconnect`
|
||||
"""
|
||||
return self.fire('disconnect', self, wasClean)
|
||||
|
||||
def publish(self, topic, *args, **kwargs):
|
||||
"""
|
||||
Implements :func:`autobahn.wamp.interfaces.IPublisher.publish`
|
||||
|
@ -1231,11 +1042,8 @@ class ApplicationSession(BaseSession):
|
|||
return on_reply
|
||||
|
||||
|
||||
IPublisher.register(ApplicationSession)
|
||||
ISubscriber.register(ApplicationSession)
|
||||
ICaller.register(ApplicationSession)
|
||||
# ICallee.register(ApplicationSession) # FIXME: ".register" collides with the ABC "register" method
|
||||
ITransportHandler.register(ApplicationSession)
|
||||
# IApplicationSession.register collides with the abc.ABCMeta.register method
|
||||
# IApplicationSession.register(ApplicationSession)
|
||||
|
||||
|
||||
class ApplicationSessionFactory(object):
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
__all__ = (
|
||||
'Publication',
|
||||
'Subscription',
|
||||
'Handler',
|
||||
'Registration',
|
||||
'Endpoint',
|
||||
|
||||
'PublishRequest',
|
||||
'SubscribeRequest',
|
||||
'UnsubscribeRequest',
|
||||
|
||||
'CallRequest',
|
||||
'InvocationRequest',
|
||||
'RegisterRequest',
|
||||
'UnregisterRequest',
|
||||
)
|
||||
|
||||
|
||||
class Publication(object):
|
||||
"""
|
||||
Object representing a publication (feedback from publishing an event when doing
|
||||
an acknowledged publish).
|
||||
"""
|
||||
|
||||
__slots__ = ('id')
|
||||
|
||||
def __init__(self, publication_id):
|
||||
self.id = publication_id
|
||||
|
||||
def __str__(self):
|
||||
return "Publication(id={0})".format(self.id)
|
||||
|
||||
|
||||
class Subscription(object):
|
||||
"""
|
||||
Object representing a handler subscription.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'active', 'session', 'handler')
|
||||
|
||||
def __init__(self, subscription_id, session, handler):
|
||||
"""
|
||||
"""
|
||||
self.id = subscription_id
|
||||
self.active = True
|
||||
self.session = session
|
||||
self.handler = handler
|
||||
|
||||
def unsubscribe(self):
|
||||
"""
|
||||
"""
|
||||
if self.active:
|
||||
return self.session._unsubscribe(self)
|
||||
else:
|
||||
raise Exception("subscription no longer active")
|
||||
|
||||
def __str__(self):
|
||||
return "Subscription(id={0}, is_active={1})".format(self.id, self.active)
|
||||
|
||||
|
||||
class Handler(object):
|
||||
"""
|
||||
Object representing an event handler attached to a subscription.
|
||||
"""
|
||||
|
||||
__slots__ = ('fn', 'obj', 'details_arg')
|
||||
|
||||
def __init__(self, fn, obj=None, details_arg=None):
|
||||
"""
|
||||
|
||||
:param fn: The event handler function to be called.
|
||||
:type fn: callable
|
||||
:param obj: The (optional) object upon which to call the function.
|
||||
:type obj: obj or None
|
||||
:param details_arg: The keyword argument under which event details should be provided.
|
||||
:type details_arg: str or None
|
||||
"""
|
||||
self.fn = fn
|
||||
self.obj = obj
|
||||
self.details_arg = details_arg
|
||||
|
||||
|
||||
class Registration(object):
|
||||
"""
|
||||
Object representing a registration.
|
||||
"""
|
||||
|
||||
__slots__ = ('id', 'active', 'session', 'procedure', 'endpoint')
|
||||
|
||||
def __init__(self, session, registration_id, procedure, endpoint):
|
||||
self.id = registration_id
|
||||
self.active = True
|
||||
self.session = session
|
||||
self.procedure = procedure
|
||||
self.endpoint = endpoint
|
||||
|
||||
def unregister(self):
|
||||
"""
|
||||
"""
|
||||
if self.active:
|
||||
return self.session._unregister(self)
|
||||
else:
|
||||
raise Exception("registration no longer active")
|
||||
|
||||
|
||||
class Endpoint(object):
|
||||
"""
|
||||
Object representing an procedure endpoint attached to a registration.
|
||||
"""
|
||||
|
||||
__slots__ = ('fn', 'obj', 'details_arg')
|
||||
|
||||
def __init__(self, fn, obj=None, details_arg=None):
|
||||
"""
|
||||
|
||||
:param fn: The endpoint procedure to be called.
|
||||
:type fn: callable
|
||||
:param obj: The (optional) object upon which to call the function.
|
||||
:type obj: obj or None
|
||||
:param details_arg: The keyword argument under which call details should be provided.
|
||||
:type details_arg: str or None
|
||||
"""
|
||||
self.fn = fn
|
||||
self.obj = obj
|
||||
self.details_arg = details_arg
|
||||
|
||||
|
||||
class Request(object):
|
||||
"""
|
||||
Object representing an outstanding request, such as for subscribe/unsubscribe,
|
||||
register/unregister or call/publish.
|
||||
"""
|
||||
|
||||
__slots__ = ('request_id', 'on_reply')
|
||||
|
||||
def __init__(self, request_id, on_reply):
|
||||
"""
|
||||
|
||||
:param request_id: The WAMP request ID.
|
||||
:type request_id: int
|
||||
:param on_reply: The Deferred/Future to be fired when the request returns.
|
||||
:type on_reply: Deferred/Future
|
||||
"""
|
||||
self.request_id = request_id
|
||||
self.on_reply = on_reply
|
||||
|
||||
|
||||
class PublishRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to publish (acknowledged) an event.
|
||||
"""
|
||||
|
||||
|
||||
class SubscribeRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to subscribe to a topic.
|
||||
"""
|
||||
|
||||
__slots__ = ('handler',)
|
||||
|
||||
def __init__(self, request_id, on_reply, handler):
|
||||
"""
|
||||
|
||||
:param request_id: The WAMP request ID.
|
||||
:type request_id: int
|
||||
:param on_reply: The Deferred/Future to be fired when the request returns.
|
||||
:type on_reply: Deferred/Future
|
||||
:param handler: WAMP call options that are in use for this call.
|
||||
:type handler: callable
|
||||
"""
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.handler = handler
|
||||
|
||||
|
||||
class UnsubscribeRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to unsubscribe a subscription.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, subscription_id):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.subscription_id = subscription_id
|
||||
|
||||
|
||||
class CallRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to call a procedure.
|
||||
"""
|
||||
|
||||
__slots__ = ('options',)
|
||||
|
||||
def __init__(self, request_id, on_reply, options):
|
||||
"""
|
||||
|
||||
:param request_id: The WAMP request ID.
|
||||
:type request_id: int
|
||||
:param on_reply: The Deferred/Future to be fired when the request returns.
|
||||
:type on_reply: Deferred/Future
|
||||
:param options: WAMP call options that are in use for this call.
|
||||
:type options: dict
|
||||
"""
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.options = options
|
||||
|
||||
|
||||
class InvocationRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to invoke an endpoint.
|
||||
"""
|
||||
|
||||
|
||||
class RegisterRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to register a procedure.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, procedure, endpoint):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.procedure = procedure
|
||||
self.endpoint = endpoint
|
||||
|
||||
|
||||
class UnregisterRequest(Request):
|
||||
"""
|
||||
Object representing an outstanding request to unregister a registration.
|
||||
"""
|
||||
|
||||
def __init__(self, request_id, on_reply, registration_id):
|
||||
Request.__init__(self, request_id, on_reply)
|
||||
self.registration_id = registration_id
|
|
@ -41,4 +41,12 @@ class ApplicationErrorTestCase(TestCase):
|
|||
self.assertIn(u"\u2603", str(error))
|
||||
else:
|
||||
self.assertIn("\\u2603", str(error))
|
||||
print(str(error))
|
||||
|
||||
def test_unicode_errormessage(self):
|
||||
"""
|
||||
Unicode arguments in ApplicationError will not raise an exception when
|
||||
the error_message method is called.
|
||||
"""
|
||||
error = ApplicationError(u"some.url", u"\u2603")
|
||||
print(error.error_message())
|
||||
self.assertIn(u"\u2603", error.error_message())
|
||||
|
|
|
@ -32,7 +32,6 @@ if os.environ.get('USE_TWISTED', False):
|
|||
|
||||
from twisted.internet.defer import inlineCallbacks, Deferred, returnValue
|
||||
from twisted.internet.defer import succeed, DeferredList
|
||||
from twisted.python import log
|
||||
from twisted.trial import unittest
|
||||
from six import PY3
|
||||
|
||||
|
@ -463,37 +462,28 @@ if os.environ.get('USE_TWISTED', False):
|
|||
error_instance = RuntimeError("we have a problem")
|
||||
got_err_d = Deferred()
|
||||
|
||||
def observer(kw):
|
||||
if kw['isError'] and 'failure' in kw:
|
||||
fail = kw['failure']
|
||||
fail.trap(RuntimeError)
|
||||
if error_instance == fail.value:
|
||||
got_err_d.callback(True)
|
||||
log.addObserver(observer)
|
||||
def observer(e, msg):
|
||||
if error_instance == e.value:
|
||||
got_err_d.callback(True)
|
||||
handler.onUserError = observer
|
||||
|
||||
def boom():
|
||||
raise error_instance
|
||||
|
||||
try:
|
||||
sub = yield handler.subscribe(boom, u'com.myapp.topic1')
|
||||
sub = yield handler.subscribe(boom, u'com.myapp.topic1')
|
||||
|
||||
# MockTransport gives us the ack reply and then we do our
|
||||
# own event message
|
||||
publish = yield handler.publish(
|
||||
u'com.myapp.topic1',
|
||||
options=types.PublishOptions(acknowledge=True, exclude_me=False),
|
||||
)
|
||||
msg = message.Event(sub.id, publish.id)
|
||||
handler.onMessage(msg)
|
||||
# MockTransport gives us the ack reply and then we do our
|
||||
# own event message
|
||||
publish = yield handler.publish(
|
||||
u'com.myapp.topic1',
|
||||
options=types.PublishOptions(acknowledge=True, exclude_me=False),
|
||||
)
|
||||
msg = message.Event(sub.id, publish.id)
|
||||
handler.onMessage(msg)
|
||||
|
||||
# we know it worked if our observer worked and did
|
||||
# .callback on our Deferred above.
|
||||
self.assertTrue(got_err_d.called)
|
||||
# ...otherwise trial will fail the test anyway
|
||||
self.flushLoggedErrors()
|
||||
|
||||
finally:
|
||||
log.removeObserver(observer)
|
||||
# we know it worked if our observer worked and did
|
||||
# .callback on our Deferred above.
|
||||
self.assertTrue(got_err_d.called)
|
||||
|
||||
@inlineCallbacks
|
||||
def test_unsubscribe(self):
|
||||
|
@ -598,6 +588,11 @@ if os.environ.get('USE_TWISTED', False):
|
|||
handler = ApplicationSession()
|
||||
handler.traceback_app = True
|
||||
MockTransport(handler)
|
||||
errors = []
|
||||
|
||||
def log_error(e, msg):
|
||||
errors.append((e.value, msg))
|
||||
handler.onUserError = log_error
|
||||
|
||||
name_error = NameError('foo')
|
||||
|
||||
|
@ -618,9 +613,8 @@ if os.environ.get('USE_TWISTED', False):
|
|||
|
||||
# also, we should have logged the real NameError to
|
||||
# Twisted.
|
||||
errs = self.flushLoggedErrors()
|
||||
self.assertEqual(1, len(errs))
|
||||
self.assertEqual(name_error, errs[0].value)
|
||||
self.assertEqual(1, len(errors))
|
||||
self.assertEqual(name_error, errors[0][0])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_invoke_progressive_result(self):
|
||||
|
@ -674,6 +668,11 @@ if os.environ.get('USE_TWISTED', False):
|
|||
|
||||
got_progress = Deferred()
|
||||
progress_error = NameError('foo')
|
||||
logged_errors = []
|
||||
|
||||
def got_error(e, msg):
|
||||
logged_errors.append((e.value, msg))
|
||||
handler.onUserError = got_error
|
||||
|
||||
def progress(arg, something=None):
|
||||
self.assertEqual('nothing', something)
|
||||
|
@ -693,15 +692,15 @@ if os.environ.get('USE_TWISTED', False):
|
|||
options=types.CallOptions(on_progress=progress),
|
||||
key='word',
|
||||
)
|
||||
|
||||
self.assertEqual(42, res)
|
||||
# our progress handler raised an error, but not before
|
||||
# recording success.
|
||||
self.assertTrue(got_progress.called)
|
||||
self.assertEqual('life', got_progress.result)
|
||||
# make sure our progress-handler error was logged
|
||||
errs = self.flushLoggedErrors()
|
||||
self.assertEqual(1, len(errs))
|
||||
self.assertEqual(progress_error, errs[0].value)
|
||||
self.assertEqual(1, len(logged_errors))
|
||||
self.assertEqual(progress_error, logged_errors[0][0])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_invoke_progressive_result_no_args(self):
|
||||
|
|
|
@ -58,7 +58,7 @@ if os.environ.get('USE_TWISTED', False):
|
|||
self._transport = MockTransport()
|
||||
|
||||
def onUserError(self, e, msg):
|
||||
self.errors.append((e, msg))
|
||||
self.errors.append((e.value, msg))
|
||||
|
||||
def exception_raiser(exc):
|
||||
'''
|
||||
|
|
|
@ -482,3 +482,90 @@ class CallResult(object):
|
|||
|
||||
def __str__(self):
|
||||
return "CallResult(results = {0}, kwresults = {1})".format(self.results, self.kwresults)
|
||||
|
||||
|
||||
class IPublication(object):
|
||||
"""
|
||||
Represents a publication of an event. This is used with acknowledged publications.
|
||||
"""
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
The WAMP publication ID for this publication.
|
||||
"""
|
||||
|
||||
|
||||
class ISubscription(object):
|
||||
"""
|
||||
Represents a subscription to a topic.
|
||||
"""
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
The WAMP subscription ID for this subscription.
|
||||
"""
|
||||
|
||||
def active(self):
|
||||
"""
|
||||
Flag indicating if subscription is active.
|
||||
"""
|
||||
|
||||
def unsubscribe(self):
|
||||
"""
|
||||
Unsubscribe this subscription that was previously created from
|
||||
:func:`autobahn.wamp.interfaces.ISubscriber.subscribe`.
|
||||
|
||||
After a subscription has been unsubscribed successfully, no events
|
||||
will be routed to the event handler anymore.
|
||||
|
||||
Returns an instance of :tx:`twisted.internet.defer.Deferred` (when
|
||||
running on **Twisted**) or an instance of :py:class:`asyncio.Future`
|
||||
(when running on **asyncio**).
|
||||
|
||||
- If the unsubscription succeeds, the returned Deferred/Future will
|
||||
*resolve* (with no return value).
|
||||
|
||||
- If the unsubscription fails, the returned Deferred/Future will *reject*
|
||||
with an instance of :class:`autobahn.wamp.exception.ApplicationError`.
|
||||
|
||||
:returns: A Deferred/Future for the unsubscription
|
||||
:rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
||||
|
||||
class IRegistration(object):
|
||||
"""
|
||||
Represents a registration of an endpoint.
|
||||
"""
|
||||
|
||||
def id(self):
|
||||
"""
|
||||
The WAMP registration ID for this registration.
|
||||
"""
|
||||
|
||||
def active(self):
|
||||
"""
|
||||
Flag indicating if registration is active.
|
||||
"""
|
||||
|
||||
def unregister(self):
|
||||
"""
|
||||
Unregister this registration that was previously created from
|
||||
:func:`autobahn.wamp.interfaces.ICallee.register`.
|
||||
|
||||
After a registration has been unregistered successfully, no calls
|
||||
will be routed to the endpoint anymore.
|
||||
|
||||
Returns an instance of :tx:`twisted.internet.defer.Deferred` (when
|
||||
running on **Twisted**) or an instance of :py:class:`asyncio.Future`
|
||||
(when running on **asyncio**).
|
||||
|
||||
- If the unregistration succeeds, the returned Deferred/Future will
|
||||
*resolve* (with no return value).
|
||||
|
||||
- If the unregistration fails, the returned Deferred/Future will be rejected
|
||||
with an instance of :class:`autobahn.wamp.exception.ApplicationError`.
|
||||
|
||||
:returns: A Deferred/Future for the unregistration
|
||||
:rtype: instance(s) of :tx:`twisted.internet.defer.Deferred` / :py:class:`asyncio.Future`
|
||||
"""
|
||||
|
|
|
@ -24,11 +24,19 @@
|
|||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import re
|
||||
import six
|
||||
from autobahn.wamp.types import SubscribeOptions
|
||||
|
||||
__all__ = ('Pattern',)
|
||||
__all__ = (
|
||||
'Pattern',
|
||||
'register',
|
||||
'subscribe',
|
||||
'error',
|
||||
)
|
||||
|
||||
|
||||
class Pattern(object):
|
||||
|
@ -215,3 +223,42 @@ class Pattern(object):
|
|||
:rtype: bool
|
||||
"""
|
||||
return self._target == Pattern.URI_TARGET_EXCEPTION
|
||||
|
||||
|
||||
def register(uri):
|
||||
"""
|
||||
Decorator for WAMP procedure endpoints.
|
||||
"""
|
||||
def decorate(f):
|
||||
assert(callable(f))
|
||||
if not hasattr(f, '_wampuris'):
|
||||
f._wampuris = []
|
||||
f._wampuris.append(Pattern(uri, Pattern.URI_TARGET_ENDPOINT))
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
def subscribe(uri):
|
||||
"""
|
||||
Decorator for WAMP event handlers.
|
||||
"""
|
||||
def decorate(f):
|
||||
assert(callable(f))
|
||||
if not hasattr(f, '_wampuris'):
|
||||
f._wampuris = []
|
||||
f._wampuris.append(Pattern(uri, Pattern.URI_TARGET_HANDLER))
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
def error(uri):
|
||||
"""
|
||||
Decorator for WAMP error classes.
|
||||
"""
|
||||
def decorate(cls):
|
||||
assert(issubclass(cls, Exception))
|
||||
if not hasattr(cls, '_wampuris'):
|
||||
cls._wampuris = []
|
||||
cls._wampuris.append(Pattern(uri, Pattern.URI_TARGET_EXCEPTION))
|
||||
return cls
|
||||
return decorate
|
||||
|
|
|
@ -29,7 +29,7 @@ from __future__ import absolute_import, print_function
|
|||
import traceback
|
||||
|
||||
from autobahn.websocket import protocol
|
||||
from autobahn.websocket import http
|
||||
from autobahn.websocket.types import ConnectionDeny
|
||||
from autobahn.wamp.interfaces import ITransport
|
||||
from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost
|
||||
|
||||
|
@ -61,8 +61,7 @@ class WampWebSocketProtocol(object):
|
|||
self._session = self.factory._factory()
|
||||
self._session.onOpen(self)
|
||||
except Exception as e:
|
||||
if self.factory.debug_wamp:
|
||||
traceback.print_exc()
|
||||
self.log.critical(traceback.format_exc())
|
||||
# Exceptions raised in onOpen are fatal ..
|
||||
reason = "WAMP Internal Error ({0})".format(e)
|
||||
self._bailout(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_INTERNAL_ERROR, reason=reason)
|
||||
|
@ -103,8 +102,7 @@ class WampWebSocketProtocol(object):
|
|||
self._bailout(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_PROTOCOL_ERROR, reason=reason)
|
||||
|
||||
except Exception as e:
|
||||
if self.factory.debug_wamp:
|
||||
traceback.print_exc()
|
||||
self.log.critical(traceback.format_exc())
|
||||
reason = "WAMP Internal Error ({0})".format(e)
|
||||
self._bailout(protocol.WebSocketProtocol.CLOSE_STATUS_CODE_INTERNAL_ERROR, reason=reason)
|
||||
|
||||
|
@ -184,7 +182,7 @@ class WampWebSocketServerProtocol(WampWebSocketProtocol):
|
|||
return subprotocol, headers
|
||||
|
||||
if self.STRICT_PROTOCOL_NEGOTIATION:
|
||||
raise http.HttpException(http.BAD_REQUEST[0], "This server only speaks WebSocket subprotocols %s" % ', '.join(self.factory.protocols))
|
||||
raise ConnectionDeny(ConnectionDeny.BAD_REQUEST, "This server only speaks WebSocket subprotocols %s" % ', '.join(self.factory.protocols))
|
||||
else:
|
||||
# assume wamp.2.json
|
||||
self._serializer = self.factory._serializers['json']
|
||||
|
|
|
@ -23,3 +23,23 @@
|
|||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, \
|
||||
ConnectionAccept, ConnectionDeny, Message, IncomingMessage, OutgoingMessage
|
||||
from autobahn.websocket.interfaces import IWebSocketChannel
|
||||
|
||||
__all__ = (
|
||||
'IWebSocketChannel',
|
||||
|
||||
'Message',
|
||||
'IncomingMessage',
|
||||
'OutgoingMessage',
|
||||
|
||||
'ConnectionRequest',
|
||||
'ConnectionResponse',
|
||||
'ConnectionAccept',
|
||||
'ConnectionDeny',
|
||||
)
|
||||
|
|
|
@ -58,7 +58,7 @@ __all__ = [
|
|||
]
|
||||
|
||||
# class for "permessage-deflate" is always available
|
||||
##
|
||||
#
|
||||
PERMESSAGE_COMPRESSION_EXTENSION = {
|
||||
PerMessageDeflateMixin.EXTENSION_NAME: {
|
||||
'Offer': PerMessageDeflateOffer,
|
||||
|
@ -71,7 +71,7 @@ PERMESSAGE_COMPRESSION_EXTENSION = {
|
|||
|
||||
|
||||
# include "permessage-bzip2" classes if bzip2 is available
|
||||
##
|
||||
#
|
||||
try:
|
||||
import bz2
|
||||
except ImportError:
|
||||
|
@ -102,7 +102,7 @@ else:
|
|||
|
||||
|
||||
# include "permessage-snappy" classes if Snappy is available
|
||||
##
|
||||
#
|
||||
try:
|
||||
# noinspection PyPackageRequirements
|
||||
import snappy
|
||||
|
|
|
@ -1,240 +0,0 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
#
|
||||
# HTTP Status Codes
|
||||
#
|
||||
# Source: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
|
||||
# Adapted on 2011/10/11
|
||||
#
|
||||
|
||||
#
|
||||
# 1xx Informational
|
||||
#
|
||||
# Request received, continuing process.
|
||||
#
|
||||
# This class of status code indicates a provisional response, consisting only of
|
||||
# the Status-Line and optional headers, and is terminated by an empty line.
|
||||
# Since HTTP/1.0 did not define any 1xx status codes, servers must not send
|
||||
# a 1xx response to an HTTP/1.0 client except under experimental conditions.
|
||||
#
|
||||
|
||||
CONTINUE = (100, "Continue",
|
||||
"This means that the server has received the request headers, and that the client should proceed to send the request body (in the case of a request for which a body needs to be sent; for example, a POST request). If the request body is large, sending it to a server when a request has already been rejected based upon inappropriate headers is inefficient. To have a server check if the request could be accepted based on the request's headers alone, a client must send Expect: 100-continue as a header in its initial request[2] and check if a 100 Continue status code is received in response before continuing (or receive 417 Expectation Failed and not continue).")
|
||||
SWITCHING_PROTOCOLS = (101, "Switching Protocols",
|
||||
"This means the requester has asked the server to switch protocols and the server is acknowledging that it will do so.")
|
||||
PROCESSING = (102, "Processing (WebDAV) (RFC 2518)",
|
||||
"As a WebDAV request may contain many sub-requests involving file operations, it may take a long time to complete the request. This code indicates that the server has received and is processing the request, but no response is available yet.[3] This prevents the client from timing out and assuming the request was lost.")
|
||||
CHECKPOINT = (103, "Checkpoint",
|
||||
"This code is used in the Resumable HTTP Requests Proposal to resume aborted PUT or POST requests.")
|
||||
REQUEST_URI_TOO_LONG = (122, "Request-URI too long",
|
||||
"This is a non-standard IE7-only code which means the URI is longer than a maximum of 2083 characters.[5][6] (See code 414.)")
|
||||
|
||||
#
|
||||
# 2xx Success
|
||||
#
|
||||
# This class of status codes indicates the action requested by the client was
|
||||
# received, understood, accepted and processed successfully.
|
||||
#
|
||||
|
||||
OK = (200, "OK",
|
||||
"Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request the response will contain an entity describing or containing the result of the action.")
|
||||
CREATED = (201, "Created",
|
||||
"The request has been fulfilled and resulted in a new resource being created.")
|
||||
ACCEPTED = (202, "Accepted",
|
||||
"The request has been accepted for processing, but the processing has not been completed. The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place.")
|
||||
NON_AUTHORATATIVE = (203, "Non-Authoritative Information (since HTTP/1.1)",
|
||||
"The server successfully processed the request, but is returning information that may be from another source.")
|
||||
NO_CONTENT = (204, "No Content",
|
||||
"The server successfully processed the request, but is not returning any content.")
|
||||
RESET_CONTENT = (205, "Reset Content",
|
||||
"The server successfully processed the request, but is not returning any content. Unlike a 204 response, this response requires that the requester reset the document view.")
|
||||
PARTIAL_CONTENT = (206, "Partial Content",
|
||||
"The server is delivering only part of the resource due to a range header sent by the client. The range header is used by tools like wget to enable resuming of interrupted downloads, or split a download into multiple simultaneous streams.")
|
||||
MULTI_STATUS = (207, "Multi-Status (WebDAV) (RFC 4918)",
|
||||
"The message body that follows is an XML message and can contain a number of separate response codes, depending on how many sub-requests were made.")
|
||||
IM_USED = (226, "IM Used (RFC 3229)",
|
||||
"The server has fulfilled a GET request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.")
|
||||
|
||||
#
|
||||
# 3xx Redirection
|
||||
#
|
||||
# The client must take additional action to complete the request.
|
||||
#
|
||||
# This class of status code indicates that further action needs to be taken
|
||||
# by the user agent in order to fulfill the request. The action required may
|
||||
# be carried out by the user agent without interaction with the user if and
|
||||
# only if the method used in the second request is GET or HEAD. A user agent
|
||||
# should not automatically redirect a request more than five times, since such
|
||||
# redirections usually indicate an infinite loop.
|
||||
#
|
||||
|
||||
MULTIPLE_CHOICES = (300, "Multiple Choices",
|
||||
"Indicates multiple options for the resource that the client may follow. It, for instance, could be used to present different format options for video, list files with different extensions, or word sense disambiguation.")
|
||||
MOVED_PERMANENTLY = (301, "Moved Permanently",
|
||||
"This and all future requests should be directed to the given URI.")
|
||||
FOUND = (302, "Found",
|
||||
"This is an example of industrial practice contradicting the standard. HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect (the original describing phrase was 'Moved Temporarily', but popular browsers implemented 302 with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 to distinguish between the two behaviours. However, some Web applications and frameworks use the 302 status code as if it were the 303.")
|
||||
SEE_OTHER = (303, "See Other (since HTTP/1.1)",
|
||||
"The response to the request can be found under another URI using a GET method. When received in response to a POST (or PUT/DELETE), it should be assumed that the server has received the data and the redirect should be issued with a separate GET message.")
|
||||
NOT_MODIFIED = (304, "Not Modified",
|
||||
"Indicates the resource has not been modified since last requested.[2] Typically, the HTTP client provides a header like the If-Modified-Since header to provide a time against which to compare. Using this saves bandwidth and reprocessing on both the server and client, as only the header data must be sent and received in comparison to the entirety of the page being re-processed by the server, then sent again using more bandwidth of the server and client.")
|
||||
USE_PROXY = (305, "Use Proxy (since HTTP/1.1)",
|
||||
"Many HTTP clients (such as Mozilla[11] and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons.")
|
||||
SWITCH_PROXY = (306, "Switch Proxy",
|
||||
"No longer used. Originally meant 'Subsequent requests should use the specified proxy'.")
|
||||
TEMPORARY_REDIRECT = (307, "Temporary Redirect (since HTTP/1.1)",
|
||||
"In this occasion, the request should be repeated with another URI, but future requests can still use the original URI.[2] In contrast to 303, the request method should not be changed when reissuing the original request. For instance, a POST request must be repeated using another POST request.")
|
||||
RESUME_INCOMPLETE = (308, "Resume Incomplete",
|
||||
"This code is used in the Resumable HTTP Requests Proposal to resume aborted PUT or POST requests.")
|
||||
|
||||
#
|
||||
# 4xx Client Error
|
||||
#
|
||||
# The 4xx class of status code is intended for cases in which the client
|
||||
# seems to have erred. Except when responding to a HEAD request, the server
|
||||
# should include an entity containing an explanation of the error situation,
|
||||
# and whether it is a temporary or permanent condition. These status codes are
|
||||
# applicable to any request method. User agents should display any included
|
||||
# entity to the user. These are typically the most common error codes
|
||||
# encountered while online.
|
||||
#
|
||||
|
||||
BAD_REQUEST = (400, "Bad Request",
|
||||
"The request cannot be fulfilled due to bad syntax.")
|
||||
UNAUTHORIZED = (401, "Unauthorized",
|
||||
"Similar to 403 Forbidden, but specifically for use when authentication is possible but has failed or not yet been provided.[2] The response must include a WWW-Authenticate header field containing a challenge applicable to the requested resource. See Basic access authentication and Digest access authentication.")
|
||||
PAYMENT_REQUIRED = (402, "Payment Required",
|
||||
"Reserved for future use.[2] The original intention was that this code might be used as part of some form of digital cash or micropayment scheme, but that has not happened, and this code is not usually used. As an example of its use, however, Apple's MobileMe service generates a 402 error if the MobileMe account is delinquent.")
|
||||
FORBIDDEN = (403, "Forbidden",
|
||||
"The request was a legal request, but the server is refusing to respond to it.[2] Unlike a 401 Unauthorized response, authenticating will make no difference.[2]")
|
||||
NOT_FOUND = (404, "Not Found",
|
||||
"The requested resource could not be found but may be available again in the future.[2] Subsequent requests by the client are permissible.")
|
||||
METHOD_NOT_ALLOWED = (405, "Method Not Allowed",
|
||||
"A request was made of a resource using a request method not supported by that resource;[2] for example, using GET on a form which requires data to be presented via POST, or using PUT on a read-only resource.")
|
||||
NOT_ACCEPTABLE = (406, "Not Acceptable",
|
||||
"The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request.")
|
||||
PROXY_AUTH_REQUIRED = (407, "Proxy Authentication Required",
|
||||
"The client must first authenticate itself with the proxy.")
|
||||
REQUEST_TIMEOUT = (408, "Request Timeout",
|
||||
"The server timed out waiting for the request. According to W3 HTTP specifications: 'The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time.'")
|
||||
CONFLICT = (409, "Conflict",
|
||||
"Indicates that the request could not be processed because of conflict in the request, such as an edit conflict.")
|
||||
GONE = (410, "Gone",
|
||||
"Indicates that the resource requested is no longer available and will not be available again.[2] This should be used when a resource has been intentionally removed and the resource should be purged. Upon receiving a 410 status code, the client should not request the resource again in the future. Clients such as search engines should remove the resource from their indices. Most use cases do not require clients and search engines to purge the resource, and a '404 Not Found' may be used instead.")
|
||||
LENGTH_REQUIRED = (411, "Length Required",
|
||||
"The request did not specify the length of its content, which is required by the requested resource.")
|
||||
PRECONDITION_FAILED = (412, "Precondition Failed",
|
||||
"The server does not meet one of the preconditions that the requester put on the request.")
|
||||
REQUEST_ENTITY_TOO_LARGE = (413, "Request Entity Too Large",
|
||||
"The request is larger than the server is willing or able to process.")
|
||||
REQUEST_URI_TOO_LARGE = (414, "Request-URI Too Long",
|
||||
"The URI provided was too long for the server to process.")
|
||||
UNSUPPORTED_MEDIA_TYPE = (415, "Unsupported Media Type",
|
||||
"The request entity has a media type which the server or resource does not support. For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format.")
|
||||
INVALID_REQUEST_RANGE = (416, "Requested Range Not Satisfiable",
|
||||
"The client has asked for a portion of the file, but the server cannot supply that portion.[2] For example, if the client asked for a part of the file that lies beyond the end of the file.")
|
||||
EXPECTATION_FAILED = (417, "Expectation Failed",
|
||||
"The server cannot meet the requirements of the Expect request-header field.")
|
||||
TEAPOT = (418, "I'm a teapot (RFC 2324)",
|
||||
"This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, and is not expected to be implemented by actual HTTP servers.")
|
||||
UNPROCESSABLE_ENTITY = (422, "Unprocessable Entity (WebDAV) (RFC 4918)",
|
||||
"The request was well-formed but was unable to be followed due to semantic errors.")
|
||||
LOCKED = (423, "Locked (WebDAV) (RFC 4918)",
|
||||
"The resource that is being accessed is locked.")
|
||||
FAILED_DEPENDENCY = (424, "Failed Dependency (WebDAV) (RFC 4918)",
|
||||
"The request failed due to failure of a previous request (e.g. a PROPPATCH).")
|
||||
UNORDERED_COLLECTION = (425, "Unordered Collection (RFC 3648)",
|
||||
"Defined in drafts of 'WebDAV Advanced Collections Protocol', but not present in 'Web Distributed Authoring and Versioning (WebDAV) Ordered Collections Protocol'.")
|
||||
UPGRADE_REQUIRED = (426, "Upgrade Required (RFC 2817)",
|
||||
"The client should switch to a different protocol such as TLS/1.0.")
|
||||
NO_RESPONSE = (444, "No Response",
|
||||
"A Nginx HTTP server extension. The server returns no information to the client and closes the connection (useful as a deterrent for malware).")
|
||||
RETRY_WITH = (449, "Retry With",
|
||||
"A Microsoft extension. The request should be retried after performing the appropriate action.")
|
||||
PARANTAL_BLOCKED = (450, "Blocked by Windows Parental Controls",
|
||||
"A Microsoft extension. This error is given when Windows Parental Controls are turned on and are blocking access to the given webpage.")
|
||||
CLIENT_CLOSED_REQUEST = (499, "Client Closed Request",
|
||||
"An Nginx HTTP server extension. This code is introduced to log the case when the connection is closed by client while HTTP server is processing its request, making server unable to send the HTTP header back.")
|
||||
|
||||
|
||||
#
|
||||
# 5xx Server Error
|
||||
#
|
||||
# The server failed to fulfill an apparently valid request.
|
||||
#
|
||||
# Response status codes beginning with the digit "5" indicate cases in which
|
||||
# the server is aware that it has encountered an error or is otherwise incapable
|
||||
# of performing the request. Except when responding to a HEAD request, the server
|
||||
# should include an entity containing an explanation of the error situation, and
|
||||
# indicate whether it is a temporary or permanent condition. Likewise, user agents
|
||||
# should display any included entity to the user. These response codes are
|
||||
# applicable to any request method.
|
||||
#
|
||||
|
||||
INTERNAL_SERVER_ERROR = (500, "Internal Server Error",
|
||||
"A generic error message, given when no more specific message is suitable.")
|
||||
NOT_IMPLEMENTED = (501, "Not Implemented",
|
||||
"The server either does not recognize the request method, or it lacks the ability to fulfill the request.")
|
||||
BAD_GATEWAY = (502, "Bad Gateway",
|
||||
"The server was acting as a gateway or proxy and received an invalid response from the upstream server.")
|
||||
SERVICE_UNAVAILABLE = (503, "Service Unavailable",
|
||||
"The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.")
|
||||
GATEWAY_TIMEOUT = (504, "Gateway Timeout",
|
||||
"The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.")
|
||||
UNSUPPORTED_HTTP_VERSION = (505, "HTTP Version Not Supported",
|
||||
"The server does not support the HTTP protocol version used in the request.")
|
||||
VARIANT_ALSO_NEGOTIATES = (506, "Variant Also Negotiates (RFC 2295)",
|
||||
"Transparent content negotiation for the request results in a circular reference.")
|
||||
INSUFFICIENT_STORAGE = (507, "Insufficient Storage (WebDAV)(RFC 4918)",
|
||||
"The server is unable to store the representation needed to complete the request.")
|
||||
BANDWIDTH_LIMIT_EXCEEDED = (509, "Bandwidth Limit Exceeded (Apache bw/limited extension)",
|
||||
"This status code, while used by many servers, is not specified in any RFCs.")
|
||||
NOT_EXTENDED = (510, "Not Extended (RFC 2774)",
|
||||
"Further extensions to the request are required for the server to fulfill it.")
|
||||
NETWORK_READ_TIMEOUT = (598, "Network read timeout error (Informal convention)",
|
||||
"This status code is not specified in any RFCs, but is used by some HTTP proxies to signal a network read timeout behind the proxy to a client in front of the proxy.")
|
||||
NETWORK_CONNECT_TIMEOUT = (599, "Network connect timeout error (Informal convention)",
|
||||
"This status code is not specified in any RFCs, but is used by some HTTP proxies to signal a network connect timeout behind the proxy to a client in front of the proxy.")
|
||||
|
||||
|
||||
class HttpException(Exception):
|
||||
"""
|
||||
Throw an instance of this class to deny a WebSocket connection
|
||||
during handshake in :meth:`autobahn.websocket.protocol.WebSocketServerProtocol.onConnect`.
|
||||
"""
|
||||
|
||||
def __init__(self, code, reason):
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
:param code: HTTP error code.
|
||||
:type code: int
|
||||
:param reason: HTTP error reason.
|
||||
:type reason: str
|
||||
"""
|
||||
self.code = code
|
||||
self.reason = reason
|
|
@ -43,80 +43,52 @@ class IWebSocketChannel(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onConnect(self, requestOrResponse):
|
||||
def on_connect(self, request_or_response):
|
||||
"""
|
||||
Callback fired during WebSocket opening handshake when a client connects (to a server with
|
||||
request from client) or when server connection established (by a client with response from
|
||||
server).
|
||||
server). This method may run asynchronous code.
|
||||
|
||||
:param requestOrResponse: Connection request (for servers) or response (for clients).
|
||||
:type requestOrResponse: Instance of :class:`autobahn.websocket.protocol.ConnectionRequest`
|
||||
or :class:`autobahn.websocket.protocol.ConnectionResponse`.
|
||||
:param request_or_response: Connection request (for servers) or response (for clients).
|
||||
:type request_or_response: Instance of :class:`autobahn.websocket.types.ConnectionRequest`
|
||||
or :class:`autobahn.websocket.types.ConnectionResponse`.
|
||||
|
||||
:returns:
|
||||
When this callback is fired on a WebSocket server, you may return one of the
|
||||
following:
|
||||
|
||||
1. ``None``: Connection accepted (no subprotocol)
|
||||
2. ``str``: Connection accepted with given subprotocol
|
||||
3. ``(subprotocol, headers)``: Connection accepted with given ``subprotocol`` (which
|
||||
also may be ``None``) and set the given HTTP ``headers`` (e.g. cookies). ``headers``
|
||||
must be a ``dict`` with ``str`` keys and values for the HTTP header values to set.
|
||||
|
||||
If a given header value is a non-string iterable (e.g. list or tuple), a separate
|
||||
header line will be sent for each item in the iterable.
|
||||
|
||||
If the client announced one or multiple subprotocols, the server MUST select
|
||||
one of the given list.
|
||||
When this callback is fired on a WebSocket server, you may return either ``None`` (in
|
||||
which case the connection is accepted with no specific WebSocket subprotocol) or
|
||||
an instance of :class:`autobahn.websocket.types.ConnectionAccept`.
|
||||
When the callback is fired on a WebSocket client, this method must return ``None``.
|
||||
Do deny a connection, raise an Exception.
|
||||
You can also return a Deferred/Future that resolves/rejects to the above.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onOpen(self):
|
||||
def on_open(self):
|
||||
"""
|
||||
Callback fired when the initial WebSocket opening handshake was completed.
|
||||
You now can send and receive WebSocket messages.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sendMessage(self, payload, isBinary=False, fragmentSize=None, sync=False, doNotCompress=False):
|
||||
def send_message(self, message):
|
||||
"""
|
||||
Send a WebSocket message over the connection to the peer.
|
||||
|
||||
:param payload: The message payload.
|
||||
:type payload: bytes
|
||||
:param isBinary: ``True`` when payload is binary, else the payload must be UTF-8 encoded text.
|
||||
:type isBinary: bool
|
||||
:param fragmentSize: Fragment message into WebSocket fragments of this size (the last frame
|
||||
potentially being shorter).
|
||||
:type fragmentSize: int
|
||||
:param sync: If ``True``, try to force data onto the wire immediately.
|
||||
|
||||
.. warning::
|
||||
Do NOT use this feature for normal applications.
|
||||
Performance likely will suffer significantly.
|
||||
This feature is mainly here for use by Autobahn|Testsuite.
|
||||
:type sync: bool
|
||||
:param doNotCompress: Iff ``True``, never compress this message. This only applies to
|
||||
Hybi-Mode and only when WebSocket compression has been negotiated on
|
||||
the WebSocket connection. Use when you know the payload
|
||||
incompressible (e.g. encrypted or already compressed).
|
||||
:type doNotCompress: bool
|
||||
:param message: The WebSocket message to be sent.
|
||||
:type message: Instance of :class:`autobahn.websocket.types.OutgoingMessage`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onMessage(self, payload, isBinary):
|
||||
def on_message(self, message):
|
||||
"""
|
||||
Callback fired when a complete WebSocket message was received.
|
||||
|
||||
:param payload: Message payload (UTF-8 encoded text or binary). Can also be empty when
|
||||
the WebSocket message contained no payload.
|
||||
:type payload: bytes
|
||||
:param isBinary: ``True`` iff payload is binary, else the payload is UTF-8 encoded text.
|
||||
:type isBinary: bool
|
||||
:param message: The WebSocket message received.
|
||||
:type message: :class:`autobahn.websocket.types.IncomingMessage`
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sendClose(self, code=None, reason=None):
|
||||
def send_close(self, code=None, reason=None):
|
||||
"""
|
||||
Starts a WebSocket closing handshake tearing down the WebSocket connection.
|
||||
|
||||
|
@ -125,20 +97,20 @@ class IWebSocketChannel(object):
|
|||
:type code: int
|
||||
:param reason: An optional close reason (a string that when present, a status
|
||||
code MUST also be present).
|
||||
:type reason: str
|
||||
:type reason: unicode
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onClose(self, wasClean, code, reason):
|
||||
def on_close(self, was_clean, code, reason):
|
||||
"""
|
||||
Callback fired when the WebSocket connection has been closed (WebSocket closing
|
||||
handshake has been finished or the connection was closed uncleanly).
|
||||
|
||||
:param wasClean: ``True`` iff the WebSocket connection was closed cleanly.
|
||||
:type wasClean: bool
|
||||
:param code: Close status code (as sent by the WebSocket peer).
|
||||
:param code: Close status code as sent by the WebSocket peer.
|
||||
:type code: int or None
|
||||
:param reason: Close reason (as sent by the WebSocket peer).
|
||||
:param reason: Close reason as sent by the WebSocket peer.
|
||||
:type reason: unicode or None
|
||||
"""
|
||||
|
||||
|
@ -152,7 +124,7 @@ class IWebSocketChannel(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sendPing(self, payload=None):
|
||||
def send_ping(self, payload=None):
|
||||
"""
|
||||
Send a WebSocket ping to the peer.
|
||||
|
||||
|
@ -164,7 +136,7 @@ class IWebSocketChannel(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onPing(self, payload):
|
||||
def on_ping(self, payload):
|
||||
"""
|
||||
Callback fired when a WebSocket ping was received. A default implementation responds
|
||||
by sending a WebSocket pong.
|
||||
|
@ -174,7 +146,7 @@ class IWebSocketChannel(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def sendPong(self, payload=None):
|
||||
def send_pong(self, payload=None):
|
||||
"""
|
||||
Send a WebSocket pong to the peer.
|
||||
|
||||
|
@ -186,7 +158,7 @@ class IWebSocketChannel(object):
|
|||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def onPong(self, payload):
|
||||
def on_pong(self, payload):
|
||||
"""
|
||||
Callback fired when a WebSocket pong was received. A default implementation does nothing.
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -30,13 +30,7 @@ import unittest2 as unittest
|
|||
|
||||
from autobahn.websocket.protocol import WebSocketServerProtocol
|
||||
from autobahn.websocket.protocol import WebSocketServerFactory
|
||||
|
||||
|
||||
class FakeTransport(object):
|
||||
_written = b""
|
||||
|
||||
def write(self, msg):
|
||||
self._written = self._written + msg
|
||||
from autobahn.test import FakeTransport
|
||||
|
||||
|
||||
class WebSocketProtocolTests(unittest.TestCase):
|
||||
|
|
|
@ -0,0 +1,331 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import json
|
||||
import six
|
||||
|
||||
__all__ = (
|
||||
'ConnectionRequest',
|
||||
'ConnectionResponse',
|
||||
'ConnectionAccept',
|
||||
'ConnectionDeny',
|
||||
'Message',
|
||||
'IncomingMessage',
|
||||
'OutgoingMessage',
|
||||
)
|
||||
|
||||
|
||||
class ConnectionRequest(object):
|
||||
"""
|
||||
Thin-wrapper for WebSocket connection request information provided in
|
||||
:meth:`autobahn.websocket.protocol.WebSocketServerProtocol.onConnect` when
|
||||
a WebSocket client want to establish a connection to a WebSocket server.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'peer',
|
||||
'headers',
|
||||
'host',
|
||||
'path',
|
||||
'params',
|
||||
'version',
|
||||
'origin',
|
||||
'protocols',
|
||||
'extensions'
|
||||
)
|
||||
|
||||
def __init__(self, peer, headers, host, path, params, version, origin, protocols, extensions):
|
||||
"""
|
||||
|
||||
:param peer: Descriptor of the connecting client (e.g. IP address/port in case of TCP transports).
|
||||
:type peer: str
|
||||
:param headers: HTTP headers from opening handshake request.
|
||||
:type headers: dict
|
||||
:param host: Host from opening handshake HTTP header.
|
||||
:type host: str
|
||||
:param path: Path from requested HTTP resource URI. For example, a resource URI of `/myservice?foo=23&foo=66&bar=2` will be parsed to `/myservice`.
|
||||
:type path: str
|
||||
:param params: Query parameters (if any) from requested HTTP resource URI. For example, a resource URI of `/myservice?foo=23&foo=66&bar=2` will be parsed to `{'foo': ['23', '66'], 'bar': ['2']}`.
|
||||
:type params: dict of arrays of strings
|
||||
:param version: The WebSocket protocol version the client announced (and will be spoken, when connection is accepted).
|
||||
:type version: int
|
||||
:param origin: The WebSocket origin header or None. Note that this only a reliable source of information for browser clients!
|
||||
:type origin: str
|
||||
:param protocols: The WebSocket (sub)protocols the client announced. You must select and return one of those (or None) in :meth:`autobahn.websocket.WebSocketServerProtocol.onConnect`.
|
||||
:type protocols: list of str
|
||||
:param extensions: The WebSocket extensions the client requested and the server accepted (and thus will be spoken, when WS connection is established).
|
||||
:type extensions: list of str
|
||||
"""
|
||||
self.peer = peer
|
||||
self.headers = headers
|
||||
self.host = host
|
||||
self.path = path
|
||||
self.params = params
|
||||
self.version = version
|
||||
self.origin = origin
|
||||
self.protocols = protocols
|
||||
self.extensions = extensions
|
||||
|
||||
def __json__(self):
|
||||
return {'peer': self.peer,
|
||||
'headers': self.headers,
|
||||
'host': self.host,
|
||||
'path': self.path,
|
||||
'params': self.params,
|
||||
'version': self.version,
|
||||
'origin': self.origin,
|
||||
'protocols': self.protocols,
|
||||
'extensions': self.extensions}
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(self.__json__())
|
||||
|
||||
|
||||
class ConnectionResponse(object):
|
||||
"""
|
||||
Thin-wrapper for WebSocket connection response information provided in
|
||||
:meth:`autobahn.websocket.protocol.WebSocketClientProtocol.onConnect` when
|
||||
a WebSocket server has accepted a connection request by a client.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'peer',
|
||||
'headers',
|
||||
'version',
|
||||
'protocol',
|
||||
'extensions'
|
||||
)
|
||||
|
||||
def __init__(self, peer, headers, version, protocol, extensions):
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
:param peer: Descriptor of the connected server (e.g. IP address/port in case of TCP transport).
|
||||
:type peer: str
|
||||
:param headers: HTTP headers from opening handshake response.
|
||||
:type headers: dict
|
||||
:param version: The WebSocket protocol version that is spoken.
|
||||
:type version: int
|
||||
:param protocol: The WebSocket (sub)protocol in use.
|
||||
:type protocol: str
|
||||
:param extensions: The WebSocket extensions in use.
|
||||
:type extensions: list of str
|
||||
"""
|
||||
self.peer = peer
|
||||
self.headers = headers
|
||||
self.version = version
|
||||
self.protocol = protocol
|
||||
self.extensions = extensions
|
||||
|
||||
def __json__(self):
|
||||
return {'peer': self.peer,
|
||||
'headers': self.headers,
|
||||
'version': self.version,
|
||||
'protocol': self.protocol,
|
||||
'extensions': self.extensions}
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(self.__json__())
|
||||
|
||||
|
||||
class ConnectionAccept(object):
|
||||
"""
|
||||
Used by WebSocket servers to accept an incoming WebSocket connection.
|
||||
If the client announced one or multiple subprotocols, the server MUST
|
||||
select one of the subprotocols announced by the client.
|
||||
"""
|
||||
|
||||
__slots__ = ('subprotocol', 'headers')
|
||||
|
||||
def __init__(self, subprotocol=None, headers=None):
|
||||
"""
|
||||
|
||||
:param subprotocol: The WebSocket connection is accepted with the
|
||||
this WebSocket subprotocol chosen. The value must be a token
|
||||
as defined by RFC 2616.
|
||||
:type subprotocol: unicode or None
|
||||
:param headers: Additional HTTP headers to send on the WebSocket
|
||||
opening handshake reply, e.g. cookies. The keys must be unicode,
|
||||
and the values either unicode or tuple/list. In the latter case
|
||||
a separate HTTP header line will be sent for each item in
|
||||
tuple/list.
|
||||
:type headers: dict or None
|
||||
"""
|
||||
assert(subprotocol is None or type(subprotocol) == six.text_type)
|
||||
assert(headers is None or type(headers) == dict)
|
||||
if headers is not None:
|
||||
for k, v in headers.items():
|
||||
assert(type(k) == unicode)
|
||||
assert(type(v) == unicode or type(v) == list or type(v) == tuple)
|
||||
self.subprotocol = subprotocol
|
||||
self.headers = headers
|
||||
|
||||
|
||||
class ConnectionDeny(Exception):
|
||||
"""
|
||||
Throw an instance of this class to deny a WebSocket connection
|
||||
during handshake in :meth:`autobahn.websocket.protocol.WebSocketServerProtocol.onConnect`.
|
||||
"""
|
||||
|
||||
BAD_REQUEST = 400
|
||||
"""
|
||||
Bad Request. The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
|
||||
FORBIDDEN = 403
|
||||
"""
|
||||
Forbidden. The request was a legal request, but the server is refusing to respond to it.[2] Unlike a 401 Unauthorized response, authenticating will make no difference.
|
||||
"""
|
||||
|
||||
NOT_FOUND = 404
|
||||
"""
|
||||
Not Found. The requested resource could not be found but may be available again in the future.[2] Subsequent requests by the client are permissible.
|
||||
"""
|
||||
|
||||
NOT_ACCEPTABLE = 406
|
||||
"""
|
||||
Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
|
||||
REQUEST_TIMEOUT = 408
|
||||
"""
|
||||
Request Timeout. The server timed out waiting for the request. According to W3 HTTP specifications: 'The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time.
|
||||
"""
|
||||
|
||||
INTERNAL_SERVER_ERROR = 500
|
||||
"""
|
||||
Internal Server Error. A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
|
||||
NOT_IMPLEMENTED = 501
|
||||
"""
|
||||
Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request.
|
||||
"""
|
||||
|
||||
SERVICE_UNAVAILABLE = 503
|
||||
"""
|
||||
Service Unavailable. The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state.
|
||||
"""
|
||||
|
||||
def __init__(self, code, reason=None):
|
||||
"""
|
||||
Constructor.
|
||||
|
||||
:param code: HTTP error code.
|
||||
:type code: int
|
||||
:param reason: HTTP error reason.
|
||||
:type reason: unicode
|
||||
"""
|
||||
assert(type(code) == int)
|
||||
assert(reason is None or type(reason) == unicode)
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class Message(object):
|
||||
"""
|
||||
Abstract base class for WebSocket messages.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class IncomingMessage(Message):
|
||||
"""
|
||||
An incoming WebSocket message.
|
||||
"""
|
||||
|
||||
__slots__ = ('payload', 'is_binary')
|
||||
|
||||
def __init__(self, payload, is_binary=False):
|
||||
"""
|
||||
|
||||
:param payload: The WebSocket message payload, which can be UTF-8
|
||||
encoded text or a binary string.
|
||||
:type payload: bytes
|
||||
:param is_binary: ``True`` iff payload is binary, else the payload
|
||||
contains UTF-8 encoded text.
|
||||
:type is_binary: bool
|
||||
"""
|
||||
assert(type(payload) == bytes)
|
||||
assert(type(is_binary) == bool)
|
||||
|
||||
self.payload = payload
|
||||
self.is_binary = is_binary
|
||||
|
||||
|
||||
class OutgoingMessage(Message):
|
||||
"""
|
||||
An outgoing WebSocket message.
|
||||
"""
|
||||
|
||||
__slots__ = ('payload', 'is_binary', 'dont_compress')
|
||||
|
||||
def __init__(self, payload, is_binary=False, dont_compress=False):
|
||||
"""
|
||||
|
||||
:param payload: The WebSocket message payload, which can be UTF-8
|
||||
encoded text or a binary string.
|
||||
:type payload: bytes
|
||||
:param is_binary: ``True`` iff payload is binary, else the payload
|
||||
contains UTF-8 encoded text.
|
||||
:type is_binary: bool
|
||||
:param dont_compress: Iff ``True``, never compress this message.
|
||||
This only has an effect when WebSocket compression has been negotiated
|
||||
on the WebSocket connection. Use when you know the payload is
|
||||
incompressible (e.g. encrypted or already compressed).
|
||||
:type dont_compress: bool
|
||||
"""
|
||||
assert(type(payload) == bytes)
|
||||
assert(type(is_binary) == bool)
|
||||
assert(type(dont_compress) == bool)
|
||||
|
||||
self.payload = payload
|
||||
self.is_binary = is_binary
|
||||
self.dont_compress = dont_compress
|
||||
|
||||
|
||||
class Ping(object):
|
||||
"""
|
||||
A WebSocket ping message.
|
||||
"""
|
||||
|
||||
__slots__ = ('payload')
|
||||
|
||||
def __init__(self, payload=None):
|
||||
"""
|
||||
|
||||
:param payload: The WebSocket ping message payload.
|
||||
:type payload: bytes or None
|
||||
"""
|
||||
assert(payload is None or type(payload) == bytes), \
|
||||
("invalid type {} for WebSocket ping payload - must be None or bytes".format(type(payload)))
|
||||
if payload is not None:
|
||||
assert(len(payload) < 126), \
|
||||
("WebSocket ping payload too long ({} bytes) - must be <= 125 bytes".format(len(payload)))
|
||||
self.payload = payload
|
|
@ -1,310 +0,0 @@
|
|||
###############################################################################
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) Tavendo GmbH
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import re
|
||||
|
||||
__all__ = ("lookupWsSupport",)
|
||||
|
||||
UA_FIREFOX = re.compile(".*Firefox/(\d*).*")
|
||||
UA_CHROME = re.compile(".*Chrome/(\d*).*")
|
||||
UA_CHROMEFRAME = re.compile(".*chromeframe/(\d*).*")
|
||||
UA_WEBKIT = re.compile(".*AppleWebKit/([0-9+\.]*)\w*.*")
|
||||
UA_WEBOS = re.compile(".*webos/([0-9+\.]*)\w*.*")
|
||||
UA_HPWEBOS = re.compile(".*hpwOS/([0-9+\.]*)\w*.*")
|
||||
|
||||
UA_DETECT_WS_SUPPORT_DB = {}
|
||||
|
||||
|
||||
# Chrome =============================================================
|
||||
|
||||
# Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11
|
||||
|
||||
|
||||
# Chrome Frame =======================================================
|
||||
|
||||
# IE6 on Windows with Chrome Frame
|
||||
# Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; chromeframe/11.0.660.0)
|
||||
|
||||
|
||||
# Firefox ============================================================
|
||||
|
||||
# Windows 7 64 Bit
|
||||
# Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0a2) Gecko/20120227 Firefox/12.0a2
|
||||
|
||||
|
||||
# Android ============================================================
|
||||
|
||||
# Firefox Mobile
|
||||
# Mozilla/5.0 (Android; Linux armv7l; rv:10.0.2) Gecko/20120215 Firefox/10.0.2 Fennec/10.0.2
|
||||
|
||||
# Chrome for Android (on ICS)
|
||||
# Mozilla/5.0 (Linux; U; Android-4.0.3; en-us; Galaxy Nexus Build/IML74K) AppleWebKit/535.7 (KHTML, like Gecko) CrMo/16.0.912.75 Mobile Safari/535.7
|
||||
|
||||
# Android builtin browser
|
||||
|
||||
# Samsung Galaxy Tab 1
|
||||
# Mozilla/5.0 (Linux; U; Android 2.2; de-de; GT-P1000 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
|
||||
|
||||
# Samsung Galaxy S
|
||||
# Mozilla/5.0 (Linux; U; Android 2.3.3; de-de; GT-I9000 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
|
||||
|
||||
# Samsung Galaxy Note
|
||||
# Mozilla/5.0 (Linux; U; Android 2.3.6; de-de; GT-N7000 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
|
||||
|
||||
# Samsung Galaxy ACE (no Flash since ARM)
|
||||
# Mozilla/5.0 (Linux; U; Android 2.2.1; de-de; GT-S5830 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
|
||||
|
||||
|
||||
# WebOS ==============================================================
|
||||
|
||||
# HP Touchpad
|
||||
# Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.5; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) wOSBrowser/234.83 Safari/534.6 TouchPad/1.0
|
||||
# => Qt-WebKit, Hixie-76, Flash
|
||||
|
||||
|
||||
# Safari =============================================================
|
||||
|
||||
# iPod Touch, iOS 4.2.1
|
||||
# Mozilla/5.0 (iPod; U; CPU iPhone OS 4_2_1 like Mac OS X; de-de) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5
|
||||
# => Hixie-76
|
||||
|
||||
# MacBook Pro, OSX 10.5.8, Safari 5.0.6
|
||||
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.50.2 (KHTML, like Gecko) Version/5.0.6 Safari/533.22.3
|
||||
# => Hixie-76
|
||||
|
||||
# RFC6455
|
||||
# Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534+ (KHTML, like Gecko) Version/5.1.2 Safari/534.52.7
|
||||
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.24+ (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10
|
||||
|
||||
# Hixie-76
|
||||
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.53.11 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10
|
||||
|
||||
# Hixie-76
|
||||
# Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_8) AppleWebKit/534.50.2 (KHTML, like Gecko) Version/5.0.6 Safari/533.22.3
|
||||
|
||||
|
||||
# Opera ==============================================================
|
||||
|
||||
# Windows 7 32-Bit
|
||||
# Opera/9.80 (Windows NT 6.1; U; de) Presto/2.10.229 Version/11.61
|
||||
|
||||
# Windows 7 64-Bit
|
||||
# Opera/9.80 (Windows NT 6.1; WOW64; U; de) Presto/2.10.229 Version/11.62
|
||||
|
||||
# Samsung Galaxy S
|
||||
# Opera/9.80 (Android 2.3.3; Linux; Opera Mobi/ADR-1202231246; U; de) Presto/2.10.254 Version/12.00
|
||||
|
||||
# Samsung Galaxy Tab 1
|
||||
# Opera/9.80 (Android 2.2; Linux; Opera Tablet/ADR-1203051631; U; de) Presto/2.10.254 Version/12.00
|
||||
|
||||
# Samsung Galaxy ACE:
|
||||
# Opera/9.80 (Android 2.2.1; Linux; Opera Mobi/ADR-1203051631; U; de) Presto/2.10.254 Version/12.00
|
||||
|
||||
# Nokia N8, Symbian S60 5th Ed., S60 Bell
|
||||
# Opera/9.80 (S60; SymbOS; Opera Mobi/SYB-1111151949; U; de) Presto/2.9.201 Version/11.50
|
||||
|
||||
|
||||
def _lookupWsSupport(ua):
|
||||
# Internet Explorer
|
||||
##
|
||||
# FIXME: handle Windows Phone
|
||||
##
|
||||
if ua.find("MSIE") >= 0:
|
||||
# IE10 has native support
|
||||
if ua.find("MSIE 10") >= 0:
|
||||
# native Hybi-10+
|
||||
return True, False, True
|
||||
|
||||
# first, check for Google Chrome Frame
|
||||
# http://www.chromium.org/developers/how-tos/chrome-frame-getting-started/understanding-chrome-frame-user-agent
|
||||
if ua.find("chromeframe") >= 0:
|
||||
|
||||
r = UA_CHROMEFRAME.match(ua)
|
||||
try:
|
||||
v = int(r.groups()[0])
|
||||
if v >= 14:
|
||||
# native Hybi-10+
|
||||
return True, False, True
|
||||
except:
|
||||
# detection problem
|
||||
return False, False, False
|
||||
|
||||
# Flash fallback
|
||||
if ua.find("MSIE 8") >= 0 or ua.find("MSIE 9") >= 0:
|
||||
return True, True, True
|
||||
|
||||
# unsupported
|
||||
return False, False, True
|
||||
|
||||
# iOS
|
||||
##
|
||||
if ua.find("iPhone") >= 0 or ua.find("iPad") >= 0 or ua.find("iPod") >= 0:
|
||||
# native Hixie76 (as of March 2012), no Flash, no alternative browsers
|
||||
return True, False, True
|
||||
|
||||
# Android
|
||||
##
|
||||
if ua.find("Android") >= 0:
|
||||
|
||||
# Firefox Mobile
|
||||
##
|
||||
if ua.find("Firefox") >= 0:
|
||||
# Hybi-10+ for FF Mobile 8+
|
||||
return True, False, True
|
||||
|
||||
# Opera Mobile
|
||||
##
|
||||
if ua.find("Opera") >= 0:
|
||||
# Hixie76 for Opera 11+
|
||||
return True, False, True
|
||||
|
||||
# Chrome for Android
|
||||
##
|
||||
if ua.find("CrMo") >= 0:
|
||||
# http://code.google.com/chrome/mobile/docs/faq.html
|
||||
return True, False, True
|
||||
|
||||
# Android builtin Browser (ooold WebKit)
|
||||
##
|
||||
if ua.find("AppleWebKit") >= 0:
|
||||
|
||||
# Though we return WS = True, and Flash = True here, when the device has no actual Flash support, that
|
||||
# will get later detected in JS. This applies to i.e. ARMv6 devices like Samsung Galaxy ACE
|
||||
|
||||
# builtin browser, only works via Flash
|
||||
return True, True, True
|
||||
|
||||
# detection problem
|
||||
return False, False, False
|
||||
|
||||
# webOS
|
||||
##
|
||||
if ua.find("hpwOS") >= 0 or ua.find("webos") >= 0:
|
||||
try:
|
||||
if ua.find("hpwOS") >= 0:
|
||||
vv = [int(x) for x in UA_HPWEBOS.match(ua).groups()[0].split('.')]
|
||||
if vv[0] >= 3:
|
||||
return True, False, True
|
||||
elif ua.find("webos") >= 0:
|
||||
vv = [int(x) for x in UA_WEBOS.match(ua).groups()[0].split('.')]
|
||||
if vv[0] >= 2:
|
||||
return True, False, True
|
||||
except:
|
||||
# detection problem
|
||||
return False, False, False
|
||||
else:
|
||||
# unsupported
|
||||
return False, False, True
|
||||
|
||||
# Opera
|
||||
##
|
||||
if ua.find("Opera") >= 0:
|
||||
# Opera 11+ has Hixie76 (needs to be manually activated though)
|
||||
return True, False, True
|
||||
|
||||
# Firefox
|
||||
##
|
||||
if ua.find("Firefox") >= 0:
|
||||
r = UA_FIREFOX.match(ua)
|
||||
try:
|
||||
v = int(r.groups()[0])
|
||||
if v >= 7:
|
||||
# native Hybi-10+
|
||||
return True, False, True
|
||||
elif v >= 3:
|
||||
# works with Flash bridge
|
||||
return True, True, True
|
||||
else:
|
||||
# unsupported
|
||||
return False, False, True
|
||||
except:
|
||||
# detection problem
|
||||
return False, False, False
|
||||
|
||||
# Safari
|
||||
##
|
||||
if ua.find("Safari") >= 0 and not ua.find("Chrome") >= 0:
|
||||
|
||||
# rely on at least Hixie76
|
||||
return True, False, True
|
||||
|
||||
# Chrome
|
||||
##
|
||||
if ua.find("Chrome") >= 0:
|
||||
r = UA_CHROME.match(ua)
|
||||
try:
|
||||
v = int(r.groups()[0])
|
||||
if v >= 14:
|
||||
# native Hybi-10+
|
||||
return True, False, True
|
||||
elif v >= 4:
|
||||
# works with Flash bridge
|
||||
return True, True, True
|
||||
else:
|
||||
# unsupported
|
||||
return False, False, True
|
||||
except:
|
||||
# detection problem
|
||||
return False, False, False
|
||||
|
||||
# detection problem
|
||||
return False, False, False
|
||||
|
||||
|
||||
def lookupWsSupport(ua, debug=True):
|
||||
"""
|
||||
Lookup if browser supports WebSocket (Hixie76, Hybi10+, RFC6455) natively,
|
||||
and if not, whether the `web-socket-js <https://github.com/gimite/web-socket-js>`__
|
||||
Flash bridge works to polyfill that.
|
||||
|
||||
Returns a tuple of booleans ``(ws_supported, needs_flash, detected)`` where
|
||||
|
||||
* ``ws_supported``: WebSocket is supported
|
||||
* ``needs_flash``: Flash Bridge is needed for support
|
||||
* ``detected`` the code has explicitly mapped support
|
||||
|
||||
:param ua: The browser user agent string as sent in the HTTP header, e.g. provided as `flask.request.user_agent.string` in Flask.
|
||||
:type ua: str
|
||||
|
||||
:returns: tuple -- A tuple ``(ws_supported, needs_flash, detected)``.
|
||||
"""
|
||||
ws = _lookupWsSupport(ua)
|
||||
if debug:
|
||||
if ua not in UA_DETECT_WS_SUPPORT_DB:
|
||||
UA_DETECT_WS_SUPPORT_DB[ua] = ws
|
||||
|
||||
if not ws[2]:
|
||||
msg = "UNDETECTED"
|
||||
elif ws[0]:
|
||||
msg = "SUPPORTED"
|
||||
elif not ws[0]:
|
||||
msg = "UNSUPPORTED"
|
||||
else:
|
||||
msg = "ERROR"
|
||||
|
||||
print("DETECT_WS_SUPPORT: %s %s %s %s %s" % (ua, ws[0], ws[1], ws[2], msg))
|
||||
|
||||
return ws
|
|
@ -58,7 +58,7 @@ WebSocket allows `bidirectional real-time messaging <http://tavendo.com/blog/pos
|
|||
* compatible with Python 2.6, 2.7, 3.3 and 3.4
|
||||
* runs on `CPython`_, `PyPy`_ and `Jython`_
|
||||
* runs under `Twisted`_ and `asyncio`_
|
||||
* implements WebSocket `RFC6455`_ (and older versions like Hybi-10+ and Hixie-76)
|
||||
* implements WebSocket `RFC6455`_ (and draft versions Hybi-10+)
|
||||
* implements `WebSocket compression <http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression>`_
|
||||
* implements `WAMP`_, the Web Application Messaging Protocol
|
||||
* supports TLS (secure WebSocket) and proxies
|
||||
|
|
|
@ -16,7 +16,6 @@ serializer
|
|||
subprotocol
|
||||
subprotocols
|
||||
Hybi
|
||||
Hixie
|
||||
args
|
||||
kwargs
|
||||
unserialized
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
test:
|
||||
PYTHONPATH=../../.. python test_newapi3.py
|
|
@ -0,0 +1,73 @@
|
|||
from autobahn.twisted.wamp import Connection
|
||||
|
||||
|
||||
def on_join(session):
|
||||
"""
|
||||
This is user code triggered when a session was created on top of
|
||||
a connection, and the sessin has joined a realm.
|
||||
"""
|
||||
print('session connected: {}'.format(session))
|
||||
|
||||
def on_leave(details):
|
||||
print("on_leave", details)
|
||||
|
||||
session.on_leave(on_leave)
|
||||
|
||||
# explicitly leaving a realm will disconnect the connection
|
||||
# cleanly and not try to reconnect, but exit cleanly.
|
||||
session.leave()
|
||||
|
||||
|
||||
def on_create(connection):
|
||||
"""
|
||||
This is the main entry into user code. It _gets_ a connection
|
||||
instance, which it then can hook onto.
|
||||
"""
|
||||
def on_connect(session):
|
||||
session.on_join(on_join)
|
||||
session.join(u'public')
|
||||
|
||||
# we attach our listener code on the connection. whenever there
|
||||
# is a session created which has joined, our callback code is run
|
||||
connection.on_connect(on_connect)
|
||||
|
||||
|
||||
def run(on_create):
|
||||
"""
|
||||
This could be a high level "runner" tool we ship.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
# multiple, configurable transports, either via dict-like config, or
|
||||
# from native Twisted endpoints
|
||||
transports = [
|
||||
{
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1:8080/ws"
|
||||
}
|
||||
]
|
||||
|
||||
# a connection connects and automatically reconnects WAMP client
|
||||
# transports to a WAMP router. A connection has a listener system
|
||||
# where user code can hook into different events : on_join
|
||||
connection = Connection(on_create, realm=u'public',
|
||||
transports=transports, reactor=reactor)
|
||||
|
||||
# the following returns a deferred that fires when the connection is
|
||||
# finally done: either by explicit close by user code, or by error or
|
||||
# when stop reconnecting
|
||||
done = connection.connect()
|
||||
|
||||
def finish(res):
|
||||
print(res)
|
||||
reactor.stop()
|
||||
|
||||
done.addBoth(finish)
|
||||
|
||||
reactor.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# here, run() could be s.th. we ship, and a user would just
|
||||
# provide a on_create() thing and run:
|
||||
return run(on_create)
|
|
@ -0,0 +1,42 @@
|
|||
from twisted.internet.task import react
|
||||
from twisted.internet.defer import inlineCallbacks as coroutine
|
||||
from autobahn.twisted.wamp import Session
|
||||
from autobahn.twisted.connection import Connection
|
||||
|
||||
|
||||
class MySession(Session):
|
||||
|
||||
@coroutine
|
||||
def on_join(self, details):
|
||||
print("on_join: {}".format(details))
|
||||
|
||||
def add2(a, b):
|
||||
return a + b
|
||||
|
||||
yield self.register(add2, u'com.example.add2')
|
||||
|
||||
try:
|
||||
res = yield self.call(u'com.example.add2', 2, 3)
|
||||
print("result: {}".format(res))
|
||||
except Exception as e:
|
||||
print("error: {}".format(e))
|
||||
finally:
|
||||
print('leaving ..')
|
||||
#self.leave()
|
||||
|
||||
|
||||
def on_leave(self, details):
|
||||
print('on_leave xx: {}'.format(details))
|
||||
self.disconnect()
|
||||
|
||||
def on_disconnect(self):
|
||||
print('on_disconnect')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
transports = u'ws://localhost:8080/ws'
|
||||
|
||||
connection = Connection(transports=transports)
|
||||
connection.session = MySession
|
||||
react(connection.start)
|
|
@ -0,0 +1,34 @@
|
|||
from twisted.internet.task import react
|
||||
from twisted.internet.defer import inlineCallbacks as coroutine
|
||||
from autobahn.twisted.connection import Connection
|
||||
|
||||
|
||||
def main(reactor, connection):
|
||||
|
||||
@coroutine
|
||||
def on_join(session, details):
|
||||
print("on_join: {}".format(details))
|
||||
|
||||
def add2(a, b):
|
||||
print("add2() called", a, b)
|
||||
return a + b
|
||||
|
||||
yield session.register(add2, u'com.example.add2')
|
||||
|
||||
try:
|
||||
res = yield session.call(u'com.example.add2', 2, 3)
|
||||
print("result: {}".format(res))
|
||||
except Exception as e:
|
||||
print("error: {}".format(e))
|
||||
finally:
|
||||
print("leaving ..")
|
||||
session.leave()
|
||||
|
||||
connection.on('join', on_join)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
connection = Connection()
|
||||
connection.on('start', main)
|
||||
|
||||
react(connection.start)
|
|
@ -0,0 +1,35 @@
|
|||
from twisted.internet.task import react
|
||||
from twisted.internet.defer import inlineCallbacks as coroutine
|
||||
from autobahn.twisted.wamp import Session
|
||||
from autobahn.twisted.connection import Connection
|
||||
|
||||
|
||||
def make_session(config):
|
||||
|
||||
@coroutine
|
||||
def on_join(session, details):
|
||||
print("on_join: {}".format(details))
|
||||
|
||||
def add2(a, b):
|
||||
return a + b
|
||||
|
||||
yield session.register(add2, u'com.example.add2')
|
||||
|
||||
try:
|
||||
res = yield session.call(u'com.example.add2', 2, 3)
|
||||
print("result: {}".format(res))
|
||||
except Exception as e:
|
||||
print("error: {}".format(e))
|
||||
finally:
|
||||
session.leave()
|
||||
|
||||
session = Session(config=config)
|
||||
session.on('join', on_join)
|
||||
return session
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
session = make_session()
|
||||
connection = Connection()
|
||||
react(connection.start, [session])
|
|
@ -0,0 +1,21 @@
|
|||
from twisted.internet.task import react
|
||||
from twisted.internet.defer import inlineCallbacks as coroutine
|
||||
from autobahn.twisted.connection import Connection
|
||||
|
||||
|
||||
@coroutine
|
||||
def main(reactor, connection):
|
||||
|
||||
transport = yield connection.connect()
|
||||
session = yield transport.join(u'realm1')
|
||||
result = yield session.call(u'com.example.add2', 2, 3)
|
||||
yield session.leave()
|
||||
yield transport.disconnect()
|
||||
yield connection.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
connection = Connection()
|
||||
connection.on('start', main)
|
||||
|
||||
react(connection.start)
|
|
@ -0,0 +1,55 @@
|
|||
from twisted.internet import reactor
|
||||
import txaio
|
||||
from autobahn.twisted.wamp import Connection
|
||||
|
||||
|
||||
def main1(connection):
|
||||
print('main1 created', connection)
|
||||
|
||||
def on_join(session):
|
||||
print('main1 joined', session)
|
||||
session.leave()
|
||||
|
||||
connection.on_join(on_join)
|
||||
|
||||
|
||||
def main2(connection):
|
||||
print('main2 created', connection)
|
||||
|
||||
def on_join(session):
|
||||
print('main2 joined', session)
|
||||
session.leave()
|
||||
|
||||
connection.on_join(on_join)
|
||||
|
||||
|
||||
def run(entry_points):
|
||||
|
||||
transports = [
|
||||
{
|
||||
"type": "websocket",
|
||||
"url": "ws://127.0.0.1:8080/ws"
|
||||
}
|
||||
]
|
||||
|
||||
done = []
|
||||
|
||||
for main in entry_points:
|
||||
connection = Connection(main, realm=u'public',
|
||||
transports=transports, reactor=reactor)
|
||||
done.append(connection.connect())
|
||||
|
||||
# deferred that fires when all connections are done
|
||||
done = txaio.gather(done)
|
||||
|
||||
def finish(res):
|
||||
print("all connections done", res)
|
||||
reactor.stop()
|
||||
|
||||
done.addBoth(finish)
|
||||
|
||||
reactor.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
return run([main1, main2])
|
|
@ -0,0 +1,28 @@
|
|||
from twisted.internet.defer import inlineCallbacks as coroutine
|
||||
from autobahn.twisted import Client
|
||||
|
||||
|
||||
@coroutine
|
||||
def on_join(session):
|
||||
try:
|
||||
res = yield session.call(u'com.example.add2', 2, 3)
|
||||
print("Result: {}".format(res))
|
||||
except Exception as e:
|
||||
print("Error: {}".format(e))
|
||||
finally:
|
||||
session.leave()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# this is Client, a high-level API above Connection and Session
|
||||
# it's a what is nowerdays ApplicationRunner, but with a better
|
||||
# name and a listener based interface
|
||||
client = Client()
|
||||
|
||||
# "on_join" is a Session event that bubbled up via Connection
|
||||
# to Client here. this works since Connection/Session have default
|
||||
# implementations that by using WAMP defaults
|
||||
client.on_join(on_join)
|
||||
|
||||
# now make it run ..
|
||||
client.run()
|
|
@ -117,7 +117,6 @@ if __name__ == '__main__':
|
|||
debugCodePaths=debug)
|
||||
|
||||
factory.protocol = BroadcastServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True)
|
||||
listenWS(factory)
|
||||
|
||||
webdir = File(".")
|
||||
|
|
|
@ -37,8 +37,7 @@ from twisted.web.static import File
|
|||
from autobahn.websocket import WebSocketServerFactory, \
|
||||
WebSocketServerProtocol
|
||||
|
||||
from autobahn.resource import WebSocketResource, \
|
||||
HTTPChannelHixie76Aware
|
||||
from autobahn.resource import WebSocketResource
|
||||
|
||||
|
||||
class EchoServerProtocol(WebSocketServerProtocol):
|
||||
|
@ -61,9 +60,7 @@ class EchoService(service.Service):
|
|||
def startService(self):
|
||||
|
||||
factory = WebSocketServerFactory(u"ws://127.0.0.1:%d" % self.port, debug=self.debug)
|
||||
|
||||
factory.protocol = EchoServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True) # needed if Hixie76 is to be supported
|
||||
|
||||
# FIXME: Site.start/stopFactory should start/stop factories wrapped as Resources
|
||||
factory.startFactory()
|
||||
|
@ -79,7 +76,6 @@ class EchoService(service.Service):
|
|||
|
||||
# both under one Twisted Web Site
|
||||
site = Site(root)
|
||||
site.protocol = HTTPChannelHixie76Aware # needed if Hixie76 is to be supported
|
||||
|
||||
self.site = site
|
||||
self.factory = factory
|
||||
|
|
|
@ -34,8 +34,7 @@ from twisted.web.static import File
|
|||
from autobahn.twisted.websocket import WebSocketServerFactory, \
|
||||
WebSocketServerProtocol
|
||||
|
||||
from autobahn.twisted.resource import WebSocketResource, \
|
||||
HTTPChannelHixie76Aware
|
||||
from autobahn.twisted.resource import WebSocketResource
|
||||
|
||||
|
||||
class EchoServerProtocol(WebSocketServerProtocol):
|
||||
|
@ -58,9 +57,7 @@ if __name__ == '__main__':
|
|||
factory = WebSocketServerFactory(u"ws://127.0.0.1:8080",
|
||||
debug=debug,
|
||||
debugCodePaths=debug)
|
||||
|
||||
factory.protocol = EchoServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True) # needed if Hixie76 is to be supported
|
||||
|
||||
resource = WebSocketResource(factory)
|
||||
|
||||
|
@ -72,7 +69,6 @@ if __name__ == '__main__':
|
|||
|
||||
# both under one Twisted Web Site
|
||||
site = Site(root)
|
||||
site.protocol = HTTPChannelHixie76Aware # needed if Hixie76 is to be supported
|
||||
reactor.listenTCP(8080, site)
|
||||
|
||||
reactor.run()
|
||||
|
|
|
@ -34,8 +34,7 @@ from twisted.web.static import File
|
|||
from autobahn.twisted.websocket import WebSocketServerFactory, \
|
||||
WebSocketServerProtocol
|
||||
|
||||
from autobahn.twisted.resource import WebSocketResource, \
|
||||
HTTPChannelHixie76Aware
|
||||
from autobahn.twisted.resource import WebSocketResource
|
||||
|
||||
|
||||
class EchoServerProtocol(WebSocketServerProtocol):
|
||||
|
@ -58,9 +57,7 @@ if __name__ == '__main__':
|
|||
factory = WebSocketServerFactory(u"wss://127.0.0.1:8080",
|
||||
debug=debug,
|
||||
debugCodePaths=debug)
|
||||
|
||||
factory.protocol = EchoServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True) # needed if Hixie76 is to be supported
|
||||
|
||||
resource = WebSocketResource(factory)
|
||||
|
||||
|
@ -72,7 +69,6 @@ if __name__ == '__main__':
|
|||
|
||||
# both under one Twisted Web Site
|
||||
site = Site(root)
|
||||
site.protocol = HTTPChannelHixie76Aware # needed if Hixie76 is to be supported
|
||||
|
||||
reactor.listenSSL(8080, site, contextFactory)
|
||||
|
||||
|
|
|
@ -61,7 +61,6 @@ if __name__ == '__main__':
|
|||
debugCodePaths=debug)
|
||||
|
||||
factory.protocol = EchoServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True)
|
||||
listenWS(factory, contextFactory)
|
||||
|
||||
webdir = File(".")
|
||||
|
|
|
@ -63,9 +63,6 @@ if __name__ == '__main__':
|
|||
factory = WebSocketClientFactory(sys.argv[1],
|
||||
debug=debug,
|
||||
debugCodePaths=debug)
|
||||
|
||||
# uncomment to use Hixie-76 protocol
|
||||
# factory.setProtocolOptions(allowHixie76 = True, version = 0)
|
||||
factory.protocol = EchoClientProtocol
|
||||
connectWS(factory)
|
||||
|
||||
|
|
|
@ -85,9 +85,6 @@ if __name__ == '__main__':
|
|||
factory = EchoClientFactory(sys.argv[1],
|
||||
debug=debug,
|
||||
debugCodePaths=debug)
|
||||
|
||||
# uncomment to use Hixie-76 protocol
|
||||
# factory.setProtocolOptions(allowHixie76 = True, version = 0)
|
||||
connectWS(factory)
|
||||
|
||||
reactor.run()
|
||||
|
|
|
@ -71,9 +71,6 @@ if __name__ == '__main__':
|
|||
proxy=proxy,
|
||||
debug=debug,
|
||||
debugCodePaths=debug)
|
||||
|
||||
# uncomment to use Hixie-76 protocol
|
||||
# factory.setProtocolOptions(allowHixie76 = True, version = 0)
|
||||
factory.protocol = EchoClientProtocol
|
||||
connectWS(factory)
|
||||
|
||||
|
|
|
@ -55,7 +55,6 @@ if __name__ == '__main__':
|
|||
debugCodePaths=debug)
|
||||
|
||||
factory.protocol = EchoServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True)
|
||||
listenWS(factory)
|
||||
|
||||
webdir = File(".")
|
||||
|
|
|
@ -3,14 +3,14 @@ WebSocket Echo Server with Fallbacks
|
|||
|
||||
This example has the [broadest browser](http://www.tavendo.de/webmq/browsers) support currently possible with Autobahn.
|
||||
|
||||
It supports native WebSocket protocol variants Hixie-76, Hybi-10+ and RFC6455.
|
||||
It supports native WebSocket protocol variants Hybi-10+ and RFC6455.
|
||||
|
||||
On IE6-9 it uses [Google Chrome Frame](http://www.google.com/chromeframe) when available.
|
||||
|
||||
On IE8,9 it can use a [Flash-based WebSocket implementation](https://github.com/gimite/web-socket-js). This requires Adobe Flash 10+.
|
||||
|
||||
> The Flash implementation can also be used on older Android devices without Chrome Mobile, but with Flash. You need to remove the conditional comments around the Flash file includes though in this case from the `index.html`.
|
||||
>
|
||||
>
|
||||
|
||||
Running
|
||||
-------
|
||||
|
@ -51,5 +51,5 @@ Here is a typical browser log when the Flash implementation kicks in:
|
|||
Upgrade: WebSocket
|
||||
Connection: Upgrade
|
||||
Sec-WebSocket-Accept: 4wHBJpfr8P419FMUv8sJ/rT0x/4=
|
||||
|
||||
|
||||
Connected!
|
||||
|
|
|
@ -61,7 +61,6 @@ if __name__ == '__main__':
|
|||
debugCodePaths=debug)
|
||||
|
||||
factory.protocol = EchoServerProtocol
|
||||
factory.setProtocolOptions(allowHixie76=True)
|
||||
listenWS(factory)
|
||||
|
||||
# We need to start a "Flash Policy Server" on TCP/843
|
||||
|
|
|
@ -37,9 +37,7 @@ from flask import Flask, render_template
|
|||
from autobahn.twisted.websocket import WebSocketServerFactory, \
|
||||
WebSocketServerProtocol
|
||||
|
||||
from autobahn.twisted.resource import WebSocketResource, \
|
||||
WSGIRootResource, \
|
||||
HTTPChannelHixie76Aware
|
||||
from autobahn.twisted.resource import WebSocketResource, WSGIRootResource
|
||||
|
||||
|
||||
##
|
||||
|
@ -83,8 +81,6 @@ if __name__ == "__main__":
|
|||
debugCodePaths=debug)
|
||||
|
||||
wsFactory.protocol = EchoServerProtocol
|
||||
wsFactory.setProtocolOptions(allowHixie76=True) # needed if Hixie76 is to be supported
|
||||
|
||||
wsResource = WebSocketResource(wsFactory)
|
||||
|
||||
##
|
||||
|
@ -102,7 +98,6 @@ if __name__ == "__main__":
|
|||
# create a Twisted Web Site and run everything
|
||||
##
|
||||
site = Site(rootResource)
|
||||
site.protocol = HTTPChannelHixie76Aware # needed if Hixie76 is to be supported
|
||||
|
||||
reactor.listenTCP(8080, site)
|
||||
reactor.run()
|
||||
|
|
4
setup.py
4
setup.py
|
@ -50,7 +50,7 @@ LONGSDESC = open('README.rst').read()
|
|||
#
|
||||
VERSIONFILE = "autobahn/__init__.py"
|
||||
verstrline = open(VERSIONFILE, "rt").read()
|
||||
VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
|
||||
VSRE = r"^__version__ = u['\"]([^'\"]*)['\"]"
|
||||
mo = re.search(VSRE, verstrline, re.M)
|
||||
if mo:
|
||||
verstr = mo.group(1)
|
||||
|
@ -182,7 +182,7 @@ setup(
|
|||
platforms='Any',
|
||||
install_requires=[
|
||||
'six>=1.9.0', # MIT license
|
||||
'txaio>=1.1.0' # MIT license
|
||||
'txaio>=2.0.0', # MIT license
|
||||
],
|
||||
extras_require={
|
||||
'all': extras_require_all,
|
||||
|
|
3
tox.ini
3
tox.ini
|
@ -19,6 +19,7 @@ deps =
|
|||
unittest2
|
||||
coverage
|
||||
msgpack-python
|
||||
git+https://github.com/tavendo/txaio
|
||||
|
||||
; twisted dependencies
|
||||
twtrunk: https://github.com/twisted/twisted/archive/trunk.zip
|
||||
|
@ -35,7 +36,7 @@ commands =
|
|||
sh -c "which python"
|
||||
python -V
|
||||
coverage --version
|
||||
asyncio,trollius: coverage run {envbindir}/py.test autobahn
|
||||
asyncio,trollius: coverage run {envbindir}/py.test autobahn/
|
||||
twtrunk,twcurrent,tw121,tw132,twcurrent: coverage run {envbindir}/trial autobahn
|
||||
coverage report
|
||||
whitelist_externals = sh
|
||||
|
|
Loading…
Reference in New Issue