Merge remote-tracking branch 'upstream/master' into unicode-exception

This commit is contained in:
HawkOwl 2015-09-21 17:19:04 +08:00
commit 6c10c402b4
67 changed files with 2605 additions and 3165 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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',
)

View File

@ -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',
)

View File

@ -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()

View File

@ -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

View File

@ -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',
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ..
#

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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',
)

175
autobahn/wamp/connection.py Normal file
View File

@ -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

View File

@ -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:

View File

@ -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):
"""

View File

@ -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)

View File

@ -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):

259
autobahn/wamp/request.py Normal file
View File

@ -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

View File

@ -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())

View File

@ -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):

View File

@ -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):
'''

View File

@ -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`
"""

View File

@ -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

View File

@ -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']

View File

@ -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',
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

331
autobahn/websocket/types.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -16,7 +16,6 @@ serializer
subprotocol
subprotocols
Hybi
Hixie
args
kwargs
unserialized

View File

@ -0,0 +1,2 @@
test:
PYTHONPATH=../../.. python test_newapi3.py

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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])

View File

@ -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)

View File

@ -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])

View File

@ -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()

View File

@ -117,7 +117,6 @@ if __name__ == '__main__':
debugCodePaths=debug)
factory.protocol = BroadcastServerProtocol
factory.setProtocolOptions(allowHixie76=True)
listenWS(factory)
webdir = File(".")

View 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

View File

@ -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()

View File

@ -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)

View File

@ -61,7 +61,6 @@ if __name__ == '__main__':
debugCodePaths=debug)
factory.protocol = EchoServerProtocol
factory.setProtocolOptions(allowHixie76=True)
listenWS(factory, contextFactory)
webdir = File(".")

View 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)

View File

@ -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()

View File

@ -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)

View File

@ -55,7 +55,6 @@ if __name__ == '__main__':
debugCodePaths=debug)
factory.protocol = EchoServerProtocol
factory.setProtocolOptions(allowHixie76=True)
listenWS(factory)
webdir = File(".")

View 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!

View File

@ -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

View File

@ -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()

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[pytest]
norecursedirs = autobahn/twisted/*

View File

@ -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,

View File

@ -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