diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 816bb4aa1..5c423a228 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -73,60 +73,60 @@ jobs: make publish_pypy_amd64 # runs on ARM64v8 (AWS Graviton) host: buildbox-arm64.crossbario.com - image_arm64: - runs-on: [self-hosted, linux, ARM64] - - # docker images are only built and published (to DockerHub) when merging - # to master (not a yet unmerged PR!) - if: github.ref == 'refs/heads/master' - - env: - DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} - AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }} - - steps: - - uses: actions/checkout@v2 - - - name: Set environment - run: | - echo AUTOBAHN_BUILD_DATE=`date -u +"%Y-%m-%d"` >> $GITHUB_ENV - echo AUTOBAHN_BUILD_ID=$(date --utc +%Y%m%d)-$(git rev-parse --short ${GITHUB_SHA}) >> $GITHUB_ENV - echo AUTOBAHN_VCS_REF=`git rev-parse --short ${GITHUB_SHA}` >> $GITHUB_ENV - echo AUTOBAHN_VERSION=$(grep -E '^(__version__)' ./autobahn/_version.py | cut -d ' ' -f3 | sed -e 's|[u"'\'']||g') >> $GITHUB_ENV - - - name: Print environment - run: | - echo "" - echo "Build environment configured:" - echo "" - echo " AUTOBAHN_BUILD_DATE = ${AUTOBAHN_BUILD_DATE}" - echo " AUTOBAHN_BUILD_ID = ${AUTOBAHN_BUILD_ID}" - echo " AUTOBAHN_VCS_REF = ${AUTOBAHN_VCS_REF}" - echo " AUTOBAHN_VERSION = ${AUTOBAHN_VERSION}" - echo "" - echo "Wheels (source):" - echo " AWS_DEFAULT_REGION = ${AWS_DEFAULT_REGION}" - echo " AWS_S3_BUCKET_NAME = ${AWS_S3_BUCKET_NAME}" - echo "" - echo "Docker image (publish):" - echo " DOCKERHUB_USER = ${DOCKERHUB_USER}" - echo "" - - - name: Build & publish Docker image for cpy-arm64 - run: | - cd ./docker && \ - make download_wheels && \ - make build_cpy_arm64v8 && \ - make test_cpy_arm64v8 - docker login -u ${{ secrets.DOCKERHUB_USER }} -p ${{ secrets.DOCKERHUB_PASSWORD }} docker.io && \ - make publish_cpy_arm64v8 - - - name: Build & publish Docker image for pypy-arm64 - run: | - cd ./docker && \ - make download_wheels && \ - make build_pypy_arm64v8 && \ - make test_pypy_arm64v8 - docker login -u ${{ secrets.DOCKERHUB_USER }} -p ${{ secrets.DOCKERHUB_PASSWORD }} docker.io && \ - make publish_pypy_arm64v8 + # image_arm64: + # runs-on: [self-hosted, linux, ARM64] + + # # docker images are only built and published (to DockerHub) when merging + # # to master (not a yet unmerged PR!) + # if: github.ref == 'refs/heads/master' + + # env: + # DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }} + # AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + # AWS_S3_BUCKET_NAME: ${{ secrets.AWS_S3_BUCKET_NAME }} + + # steps: + # - uses: actions/checkout@v2 + + # - name: Set environment + # run: | + # echo AUTOBAHN_BUILD_DATE=`date -u +"%Y-%m-%d"` >> $GITHUB_ENV + # echo AUTOBAHN_BUILD_ID=$(date --utc +%Y%m%d)-$(git rev-parse --short ${GITHUB_SHA}) >> $GITHUB_ENV + # echo AUTOBAHN_VCS_REF=`git rev-parse --short ${GITHUB_SHA}` >> $GITHUB_ENV + # echo AUTOBAHN_VERSION=$(grep -E '^(__version__)' ./autobahn/_version.py | cut -d ' ' -f3 | sed -e 's|[u"'\'']||g') >> $GITHUB_ENV + + # - name: Print environment + # run: | + # echo "" + # echo "Build environment configured:" + # echo "" + # echo " AUTOBAHN_BUILD_DATE = ${AUTOBAHN_BUILD_DATE}" + # echo " AUTOBAHN_BUILD_ID = ${AUTOBAHN_BUILD_ID}" + # echo " AUTOBAHN_VCS_REF = ${AUTOBAHN_VCS_REF}" + # echo " AUTOBAHN_VERSION = ${AUTOBAHN_VERSION}" + # echo "" + # echo "Wheels (source):" + # echo " AWS_DEFAULT_REGION = ${AWS_DEFAULT_REGION}" + # echo " AWS_S3_BUCKET_NAME = ${AWS_S3_BUCKET_NAME}" + # echo "" + # echo "Docker image (publish):" + # echo " DOCKERHUB_USER = ${DOCKERHUB_USER}" + # echo "" + + # - name: Build & publish Docker image for cpy-arm64 + # run: | + # cd ./docker && \ + # make download_wheels && \ + # make build_cpy_arm64v8 && \ + # make test_cpy_arm64v8 + # docker login -u ${{ secrets.DOCKERHUB_USER }} -p ${{ secrets.DOCKERHUB_PASSWORD }} docker.io && \ + # make publish_cpy_arm64v8 + + # - name: Build & publish Docker image for pypy-arm64 + # run: | + # cd ./docker && \ + # make download_wheels && \ + # make build_pypy_arm64v8 && \ + # make test_pypy_arm64v8 + # docker login -u ${{ secrets.DOCKERHUB_USER }} -p ${{ secrets.DOCKERHUB_PASSWORD }} docker.io && \ + # make publish_pypy_arm64v8 diff --git a/Makefile b/Makefile index 61ec8a12b..1d1411e4f 100755 --- a/Makefile +++ b/Makefile @@ -164,7 +164,7 @@ test: # test under Twisted test_twisted: USE_TWISTED=1 trial autobahn - #WAMP_ROUTER_URL="ws://127.0.0.1:8080/ws" USE_TWISTED=1 trial autobahn +# WAMP_ROUTER_URL="ws://127.0.0.1:8080/ws" USE_TWISTED=1 trial autobahn test_application_runner: USE_TWISTED=1 trial autobahn.twisted.test.test_tx_application_runner @@ -185,7 +185,8 @@ test_tx_choosereactor: USE_TWISTED=1 trial autobahn.twisted.test.test_choosereactor test_cryptosign: - USE_ASYNCIO=1 trial autobahn.wamp.test.test_wamp_cryptosign +# USE_ASYNCIO=1 trial autobahn.wamp.test.test_wamp_cryptosign + USE_ASYNCIO=1 pytest -s -v -rfA --ignore=./autobahn/twisted autobahn/wamp/test/test_wamp_cryptosign.py USE_TWISTED=1 trial autobahn.wamp.test.test_wamp_cryptosign test_wamp_scram: @@ -199,6 +200,13 @@ test_xbr_argon2: test_xbr_config: USE_TWISTED=1 trial autobahn.xbr.test.test_xbr_config +test_transport_details: + USE_ASYNCIO=1 trial autobahn.wamp.test.test_wamp_transport_details + USE_TWISTED=1 trial autobahn.wamp.test.test_wamp_transport_details + +test_tx_protocol: + USE_TWISTED=1 trial autobahn.twisted.test.test_tx_protocol + test_twisted_coverage: -rm .coverage USE_TWISTED=1 coverage run --omit=*/test/* --source=autobahn `which trial` autobahn @@ -215,8 +223,10 @@ test_coverage: # test under asyncio test_asyncio: - 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 + USE_ASYNCIO=1 pytest -s -v -rfP --ignore=./autobahn/twisted autobahn +# USE_ASYNCIO=1 pytest -s -v -rA --ignore=./autobahn/twisted ./autobahn/asyncio/test/test_aio_websocket.py +# USE_ASYNCIO=1 pytest -s -v -rA --log-cli-level=info --ignore=./autobahn/twisted ./autobahn/asyncio/test/test_aio_websocket.py + test_cs1: USE_ASYNCIO=1 python -m pytest -s -v autobahn/wamp/test/test_cryptosign.py diff --git a/autobahn/__init__.py b/autobahn/__init__.py index 6f4af3b2d..5eb13a6e2 100644 --- a/autobahn/__init__.py +++ b/autobahn/__init__.py @@ -27,3 +27,19 @@ from autobahn._version import __version__ version = __version__ + +import os +import txaio + +# this is used in the unit tests (trial/pytest), and when already done here, there +# is no risk and headaches with finding out if/where an import implies a framework +if os.environ.get('USE_TWISTED', False) and os.environ.get('USE_ASYNCIO', False): + raise RuntimeError('fatal: _both_ USE_TWISTED and USE_ASYNCIO are set!') + +if os.environ.get('USE_TWISTED', False): + txaio.use_twisted() +elif os.environ.get('USE_ASYNCIO', False): + txaio.use_asyncio() +else: + # neither USE_TWISTED nor USE_ASYNCIO selected from env var + pass diff --git a/autobahn/_version.py b/autobahn/_version.py index 66fa65be2..6c84d8182 100644 --- a/autobahn/_version.py +++ b/autobahn/_version.py @@ -24,6 +24,6 @@ # ############################################################################### -__version__ = '22.4.1.dev5' +__version__ = '22.4.1.dev6' __build__ = '00000000-0000000' diff --git a/autobahn/asyncio/component.py b/autobahn/asyncio/component.py index 6999f5649..dd0e4498c 100644 --- a/autobahn/asyncio/component.py +++ b/autobahn/asyncio/component.py @@ -30,8 +30,6 @@ from functools import wraps import txaio -txaio.use_asyncio() # noqa - from autobahn.asyncio.websocket import WampWebSocketClientFactory from autobahn.asyncio.rawsocket import WampRawSocketClientFactory diff --git a/autobahn/asyncio/rawsocket.py b/autobahn/asyncio/rawsocket.py index ef334a5fc..02277a053 100644 --- a/autobahn/asyncio/rawsocket.py +++ b/autobahn/asyncio/rawsocket.py @@ -28,12 +28,12 @@ import struct import math import copy - -from autobahn.util import public, _LazyHexFormatter -from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost -from autobahn.asyncio.util import peer2str, get_serializers, transport_channel_id +from typing import Optional import txaio +from autobahn.util import public, _LazyHexFormatter, hltype +from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost +from autobahn.asyncio.util import get_serializers, create_transport_details __all__ = ( 'WampRawSocketServerProtocol', @@ -42,8 +42,6 @@ 'WampRawSocketClientFactory' ) -txaio.use_asyncio() - FRAME_TYPE_DATA = 0 FRAME_TYPE_PING = 1 FRAME_TYPE_PONG = 2 @@ -58,12 +56,22 @@ class PrefixProtocol(asyncio.Protocol): max_length = 16 * 1024 * 1024 max_length_send = max_length log = txaio.make_logger() # @UndefinedVariable + peer: Optional[str] = None + is_server: Optional[bool] = None def connection_made(self, transport): - self.transport = transport - peer = transport.get_extra_info('peername') - self.peer = peer2str(peer) + # asyncio networking framework entry point, called by asyncio + # when the connection is established (either a client or a server) self.log.debug('RawSocker Asyncio: Connection made with peer {peer}', peer=self.peer) + + self.transport = transport + + # determine preliminary transport details (what is know at this point) + self._transport_details = create_transport_details(self.transport, self.is_server) + + # backward compatibility + self.peer = self._transport_details.peer + self._buffer = b'' self._header = None self._wait_closed = txaio.create_future() @@ -150,9 +158,6 @@ def stringReceived(self, data): class RawSocketProtocol(PrefixProtocol): - peer = None - peer_transport = None - def __init__(self): max_size = None if max_size: @@ -166,13 +171,6 @@ def __init__(self): self.max_length = 2**24 def connection_made(self, transport): - # the peer we are connected to - try: - self.peer = peer2str(transport.get_extra_info('peername')) - except: - self.peer = 'process:{}'.format(self.transport.pid) - self.peer_transport = 'rawsocket' - PrefixProtocol.connection_made(self, transport) self._handshake_done = False @@ -183,7 +181,6 @@ def parse_handshake(self): buf = bytearray(self._buffer[:4]) if buf[0] != MAGIC_BYTE: raise HandshakeError('Invalid magic byte in handshake') - return ser = buf[1] & 0x0F lexp = buf[1] >> 4 self.max_length_send = 2**(lexp + 9) @@ -232,6 +229,8 @@ def __init__(self, msg, code=0): class RawSocketClientProtocol(RawSocketProtocol): + is_server = False + def check_serializer(self, ser_id): return True @@ -259,6 +258,8 @@ def connection_made(self, transport): class RawSocketServerProtocol(RawSocketProtocol): + is_server = True + def supports_serializer(self, ser_id): raise NotImplementedError() @@ -308,7 +309,7 @@ def send(self, msg): Implements :func:`autobahn.wamp.interfaces.ITransport.send` """ if self.isOpen(): - self.log.debug("WampRawSocketProtocol: TX WAMP message: {msg}", msg=msg) + self.log.debug('{func}: TX WAMP message: {msg}', func=hltype(self.send), msg=msg) try: payload, _ = self._serializer.serialize(msg) except Exception as e: @@ -393,12 +394,6 @@ def supports_serializer(self, ser_id): self.abort() return False - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, is_server=True, channel_id_type=channel_id_type) - @public class WampRawSocketClientProtocol(WampRawSocketMixinGeneral, WampRawSocketMixinAsyncio, RawSocketClientProtocol): @@ -416,12 +411,6 @@ def serializer_id(self): self._serializer = copy.copy(self.factory._serializer) return self._serializer.RAWSOCKET_SERIALIZER_ID - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, is_server=False, channel_id_type=channel_id_type) - class WampRawSocketFactory(object): """ diff --git a/autobahn/asyncio/test/__init__.py b/autobahn/asyncio/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/asyncio/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/asyncio/test/test_aio_rawsocket.py b/autobahn/asyncio/test/test_aio_rawsocket.py index 3fa0fdad5..5045c3773 100644 --- a/autobahn/asyncio/test/test_aio_rawsocket.py +++ b/autobahn/asyncio/test/test_aio_rawsocket.py @@ -1,7 +1,6 @@ import pytest import os -from unittest import TestCase, main from unittest.mock import Mock, call from autobahn.asyncio.rawsocket import PrefixProtocol, RawSocketClientProtocol, RawSocketServerProtocol, \ WampRawSocketClientFactory, WampRawSocketServerFactory @@ -9,211 +8,227 @@ from autobahn.wamp import message -@pytest.mark.skipif(True, reason='pytest sucks') -@pytest.mark.skipif(os.environ.get('USE_ASYNCIO', False) is False, reason="Only for asyncio") -class Test(TestCase): - - def test_sers(self): - serializers = get_serializers() - self.assertTrue(len(serializers) > 0) - m = serializers[0]().serialize(message.Abort('close')) - print(m) - self.assertTrue(m) - - def test_prefix(self): - p = PrefixProtocol() - transport = Mock() - receiver = Mock() - p.stringReceived = receiver - p.connection_made(transport) - small_msg = b'\x00\x00\x00\x04abcd' - p.data_received(small_msg) - receiver.assert_called_once_with(b'abcd') - self.assertEqual(len(p._buffer), 0) - - p.sendString(b'abcd') - - # print(transport.write.call_args_list) - transport.write.assert_has_calls([call(b'\x00\x00\x00\x04'), call(b'abcd')]) - - transport.reset_mock() - receiver.reset_mock() - - big_msg = b'\x00\x00\x00\x0C' + b'0123456789AB' - - p.data_received(big_msg[0:2]) - self.assertFalse(receiver.called) - - p.data_received(big_msg[2:6]) - self.assertFalse(receiver.called) - - p.data_received(big_msg[6:11]) - self.assertFalse(receiver.called) - - p.data_received(big_msg[11:16]) - receiver.assert_called_once_with(b'0123456789AB') - - transport.reset_mock() - receiver.reset_mock() - - two_messages = b'\x00\x00\x00\x04' + b'abcd' + b'\x00\x00\x00\x05' + b'12345' + b'\x00' - p.data_received(two_messages) - receiver.assert_has_calls([call(b'abcd'), call(b'12345')]) - self.assertEqual(p._buffer, b'\x00') - - def test_is_closed(self): - class CP(RawSocketClientProtocol): - @property - def serializer_id(self): - return 1 - client = CP() - - on_hs = Mock() - transport = Mock() - receiver = Mock() - client.stringReceived = receiver - client._on_handshake_complete = on_hs - self.assertTrue(client.is_closed.done()) - client.connection_made(transport) - self.assertFalse(client.is_closed.done()) - client.connection_lost(None) - - self.assertTrue(client.is_closed.done()) - - def test_raw_socket_server1(self): - - server = RawSocketServerProtocol() - ser = Mock(return_value=True) - on_hs = Mock() - transport = Mock() - receiver = Mock() - server.supports_serializer = ser - server.stringReceived = receiver - server._on_handshake_complete = on_hs - server.stringReceived = receiver - - server.connection_made(transport) - hs = b'\x7F\xF1\x00\x00' + b'\x00\x00\x00\x04abcd' - server.data_received(hs) - - ser.assert_called_once_with(1) - on_hs.assert_called_once_with() - self.assertTrue(transport.write.called) - transport.write.assert_called_once_with(b'\x7F\xF1\x00\x00') - self.assertFalse(transport.close.called) - receiver.assert_called_once_with(b'abcd') - - def test_raw_socket_server_errors(self): - - server = RawSocketServerProtocol() - ser = Mock(return_value=True) - on_hs = Mock() - transport = Mock() - receiver = Mock() - server.supports_serializer = ser - server.stringReceived = receiver - server._on_handshake_complete = on_hs - server.stringReceived = receiver - server.connection_made(transport) - server.data_received(b'abcdef') - transport.close.assert_called_once_with() - - server = RawSocketServerProtocol() - ser = Mock(return_value=False) - on_hs = Mock() - transport = Mock(spec_set=('close', 'write', 'get_extra_info')) - receiver = Mock() - server.supports_serializer = ser - server.stringReceived = receiver - server._on_handshake_complete = on_hs - server.stringReceived = receiver - server.connection_made(transport) - server.data_received(b'\x7F\xF1\x00\x00') - transport.close.assert_called_once_with() - transport.write.assert_called_once_with(b'\x7F\x10\x00\x00') - - def test_raw_socket_client1(self): - class CP(RawSocketClientProtocol): - @property - def serializer_id(self): - return 1 - client = CP() - - on_hs = Mock() - transport = Mock() - receiver = Mock() - client.stringReceived = receiver - client._on_handshake_complete = on_hs - - client.connection_made(transport) - client.data_received(b'\x7F\xF1\x00\x00' + b'\x00\x00\x00\x04abcd') - on_hs.assert_called_once_with() - self.assertTrue(transport.write.called) - transport.write.called_one_with(b'\x7F\xF1\x00\x00') - self.assertFalse(transport.close.called) - receiver.assert_called_once_with(b'abcd') - - def test_raw_socket_client_error(self): - class CP(RawSocketClientProtocol): - @property - def serializer_id(self): - return 1 - client = CP() - - on_hs = Mock() - transport = Mock(spec_set=('close', 'write', 'get_extra_info')) - receiver = Mock() - client.stringReceived = receiver - client._on_handshake_complete = on_hs - client.connection_made(transport) - - client.data_received(b'\x7F\xF1\x00\x01') - transport.close.assert_called_once_with() - - def test_wamp(self): - transport = Mock(spec_set=('abort', 'close', 'write', 'get_extra_info')) - transport.write = Mock(side_effect=lambda m: messages.append(m)) - client = Mock(spec=['onOpen', 'onMessage']) - - def fact(): - return client - messages = [] - proto = WampRawSocketClientFactory(fact)() - proto.connection_made(transport) - self.assertTrue(proto._serializer) - s = proto._serializer.RAWSOCKET_SERIALIZER_ID - proto.data_received(bytes(bytearray([0x7F, 0xF0 | s, 0, 0]))) - client.onOpen.assert_called_once_with(proto) - proto.send(message.Abort('close')) - for d in messages[1:]: - proto.data_received(d) - self.assertTrue(client.onMessage.called) - self.assertTrue(isinstance(client.onMessage.call_args[0][0], message.Abort)) - - # server - transport = Mock(spec_set=('abort', 'close', 'write', 'get_extra_info')) - transport.write = Mock(side_effect=lambda m: messages.append(m)) - - client = None - server = Mock(spec=['onOpen', 'onMessage']) - - def fact_server(): - return server - messages = [] - proto = WampRawSocketServerFactory(fact_server)() - proto.connection_made(transport) - self.assertTrue(proto.factory._serializers) - s = proto.factory._serializers[1].RAWSOCKET_SERIALIZER_ID - proto.data_received(bytes(bytearray([0x7F, 0xF0 | s, 0, 0]))) - self.assertTrue(proto._serializer) - server.onOpen.assert_called_once_with(proto) - proto.send(message.Abort('close')) - for d in messages[1:]: - proto.data_received(d) - self.assertTrue(server.onMessage.called) - self.assertTrue(isinstance(server.onMessage.call_args[0][0], message.Abort)) - - -if __name__ == "__main__": - # import sys;sys.argv = ['', 'Test.test_prefix'] - main() +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_sers(event_loop): + serializers = get_serializers() + assert len(serializers) > 0 + m = serializers[0]().serialize(message.Abort('close')) + assert m + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_prefix(event_loop): + p = PrefixProtocol() + transport = Mock() + receiver = Mock() + p.stringReceived = receiver + p.connection_made(transport) + small_msg = b'\x00\x00\x00\x04abcd' + p.data_received(small_msg) + receiver.assert_called_once_with(b'abcd') + assert len(p._buffer) == 0 + + p.sendString(b'abcd') + + # print(transport.write.call_args_list) + transport.write.assert_has_calls([call(b'\x00\x00\x00\x04'), call(b'abcd')]) + + transport.reset_mock() + receiver.reset_mock() + + big_msg = b'\x00\x00\x00\x0C' + b'0123456789AB' + + p.data_received(big_msg[0:2]) + assert not receiver.called + + p.data_received(big_msg[2:6]) + assert not receiver.called + + p.data_received(big_msg[6:11]) + assert not receiver.called + + p.data_received(big_msg[11:16]) + receiver.assert_called_once_with(b'0123456789AB') + + transport.reset_mock() + receiver.reset_mock() + + two_messages = b'\x00\x00\x00\x04' + b'abcd' + b'\x00\x00\x00\x05' + b'12345' + b'\x00' + p.data_received(two_messages) + receiver.assert_has_calls([call(b'abcd'), call(b'12345')]) + assert p._buffer == b'\x00' + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_is_closed(event_loop): + class CP(RawSocketClientProtocol): + @property + def serializer_id(self): + return 1 + client = CP() + + on_hs = Mock() + transport = Mock() + receiver = Mock() + client.stringReceived = receiver + client._on_handshake_complete = on_hs + assert client.is_closed.done() + client.connection_made(transport) + assert not client.is_closed.done() + client.connection_lost(None) + + assert client.is_closed.done() + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_raw_socket_server1(event_loop): + + server = RawSocketServerProtocol() + ser = Mock(return_value=True) + on_hs = Mock() + transport = Mock() + receiver = Mock() + server.supports_serializer = ser + server.stringReceived = receiver + server._on_handshake_complete = on_hs + server.stringReceived = receiver + + server.connection_made(transport) + hs = b'\x7F\xF1\x00\x00' + b'\x00\x00\x00\x04abcd' + server.data_received(hs) + + ser.assert_called_once_with(1) + on_hs.assert_called_once_with() + assert transport.write.called + transport.write.assert_called_once_with(b'\x7F\xF1\x00\x00') + assert not transport.close.called + receiver.assert_called_once_with(b'abcd') + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_raw_socket_server_errors(event_loop): + + server = RawSocketServerProtocol() + ser = Mock(return_value=True) + on_hs = Mock() + transport = Mock() + receiver = Mock() + server.supports_serializer = ser + server.stringReceived = receiver + server._on_handshake_complete = on_hs + server.stringReceived = receiver + server.connection_made(transport) + server.data_received(b'abcdef') + transport.close.assert_called_once_with() + + server = RawSocketServerProtocol() + ser = Mock(return_value=False) + on_hs = Mock() + transport = Mock(spec_set=('close', 'write', 'get_extra_info')) + receiver = Mock() + server.supports_serializer = ser + server.stringReceived = receiver + server._on_handshake_complete = on_hs + server.stringReceived = receiver + server.connection_made(transport) + server.data_received(b'\x7F\xF1\x00\x00') + transport.close.assert_called_once_with() + transport.write.assert_called_once_with(b'\x7F\x10\x00\x00') + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_raw_socket_client1(event_loop): + class CP(RawSocketClientProtocol): + @property + def serializer_id(self): + return 1 + client = CP() + + on_hs = Mock() + transport = Mock() + receiver = Mock() + client.stringReceived = receiver + client._on_handshake_complete = on_hs + + client.connection_made(transport) + client.data_received(b'\x7F\xF1\x00\x00' + b'\x00\x00\x00\x04abcd') + on_hs.assert_called_once_with() + assert transport.write.called + transport.write.called_one_with(b'\x7F\xF1\x00\x00') + assert not transport.close.called + receiver.assert_called_once_with(b'abcd') + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_raw_socket_client_error(event_loop): + class CP(RawSocketClientProtocol): + @property + def serializer_id(self): + return 1 + client = CP() + + on_hs = Mock() + transport = Mock(spec_set=('close', 'write', 'get_extra_info')) + receiver = Mock() + client.stringReceived = receiver + client._on_handshake_complete = on_hs + client.connection_made(transport) + + client.data_received(b'\x7F\xF1\x00\x01') + transport.close.assert_called_once_with() + + +# FIXME: tests below + + +@pytest.mark.asyncio +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def _test_wamp_server(event_loop): + transport = Mock(spec_set=('abort', 'close', 'write', 'get_extra_info')) + transport.write = Mock(side_effect=lambda m: messages.append(m)) + server = Mock(spec=['onOpen', 'onMessage']) + + def fact_server(): + return server + + messages = [] + + proto = WampRawSocketServerFactory(fact_server)() + proto.connection_made(transport) + assert proto.factory._serializers + s = proto.factory._serializers[1].RAWSOCKET_SERIALIZER_ID + proto.data_received(bytes(bytearray([0x7F, 0xF0 | s, 0, 0]))) + assert proto._serializer + server.onOpen.assert_called_once_with(proto) + + proto.sendMessage(message.Abort('close')) + for d in messages[1:]: + proto.data_received(d) + assert server.onMessage.called + assert isinstance(server.onMessage.call_args[0][0], message.Abort) + + +@pytest.mark.asyncio +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def _test_wamp_client(event_loop): + transport = Mock(spec_set=('abort', 'close', 'write', 'get_extra_info')) + transport.write = Mock(side_effect=lambda m: messages.append(m)) + client = Mock(spec=['onOpen', 'onMessage']) + + def fact_client(): + return client + + messages = [] + + proto = WampRawSocketClientFactory(fact_client)() + proto.connection_made(transport) + assert proto._serializer + s = proto._serializer.RAWSOCKET_SERIALIZER_ID + proto.data_received(bytes(bytearray([0x7F, 0xF0 | s, 0, 0]))) + client.onOpen.assert_called_once_with(proto) + + proto.sendMessage(message.Abort('close')) + for d in messages[1:]: + proto.data_received(d) + assert client.onMessage.called + assert isinstance(client.onMessage.call_args[0][0], message.Abort) diff --git a/autobahn/asyncio/test/test_aio_websocket.py b/autobahn/asyncio/test/test_aio_websocket.py index a8c94c729..8ec38f96a 100644 --- a/autobahn/asyncio/test/test_aio_websocket.py +++ b/autobahn/asyncio/test/test_aio_websocket.py @@ -1,72 +1,73 @@ -import pytest import os -import sys +import asyncio +import pytest + +import txaio # because py.test tries to collect it as a test-case from unittest.mock import Mock from autobahn.asyncio.websocket import WebSocketServerFactory -from unittest import TestCase -import txaio +# https://medium.com/ideas-at-igenius/testing-asyncio-python-code-with-pytest-a2f3628f82bc + + +async def echo_async(what, when): + await asyncio.sleep(when) + return what + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +@pytest.mark.asyncio +async def test_echo_async(): + assert 'Hello!' == await echo_async('Hello!', 0) + + +# @pytest.mark.asyncio(forbid_global_loop=True) +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +def test_websocket_custom_loop(event_loop): + factory = WebSocketServerFactory(loop=event_loop) + server = factory() + transport = Mock() + server.connection_made(transport) + + +@pytest.mark.skipif(not os.environ.get('USE_ASYNCIO', False), reason='test runs on asyncio only') +@pytest.mark.asyncio +async def test_async_on_connect_server(event_loop): + + num = 42 + done = txaio.create_future() + values = [] + + async def foo(x): + await asyncio.sleep(1) + return x * x + + async def on_connect(req): + v = await foo(num) + values.append(v) + txaio.resolve(done, req) + + factory = WebSocketServerFactory() + server = factory() + server.onConnect = on_connect + transport = Mock() + + server.connection_made(transport) + server.data = b'\r\n'.join([ + b'GET /ws HTTP/1.1', + b'Host: www.example.com', + b'Sec-WebSocket-Version: 13', + b'Origin: http://www.example.com.malicious.com', + b'Sec-WebSocket-Extensions: permessage-deflate', + b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==', + b'Connection: keep-alive, Upgrade', + b'Upgrade: websocket', + b'\r\n', # last string doesn't get a \r\n from join() + ]) + server.processHandshake() + await done -@pytest.mark.skipif(True, reason='pytest sucks') -@pytest.mark.skipif(sys.version_info < (3, 3), reason="requires Python 3.3+") -@pytest.mark.skipif(os.environ.get('USE_ASYNCIO', False) is False, reason="only for asyncio") -@pytest.mark.usefixtures("event_loop") # ensure we have pytest_asyncio installed -class Test(TestCase): - - @pytest.mark.asyncio(forbid_global_loop=True) - def test_websocket_custom_loop(self, event_loop): - factory = WebSocketServerFactory(loop=event_loop) - server = factory() - transport = Mock() - - server.connection_made(transport) - - # not sure when this last worked, tests haven't been running - # properly under asyncio for a while it seems. - @pytest.mark.xfail - def test_async_on_connect_server(self): - # see also issue 757 - - # for python 3.5, this can be "async def foo" - def foo(x): - f = txaio.create_future() - txaio.resolve(f, x * x) - return f - - values = [] - - def on_connect(req): - f = txaio.create_future() - - def cb(x): - f = foo(42) - f.add_callbacks(f, lambda v: values.append(v), None) - return f - txaio.add_callbacks(f, cb, None) - return f - - factory = WebSocketServerFactory() - server = factory() - server.onConnect = on_connect - transport = Mock() - - server.connection_made(transport) - # need/want to insert real-fake handshake data? - server.data = b"\r\n".join([ - b'GET /ws HTTP/1.1', - b'Host: www.example.com', - b'Sec-WebSocket-Version: 13', - b'Origin: http://www.example.com.malicious.com', - b'Sec-WebSocket-Extensions: permessage-deflate', - b'Sec-WebSocket-Key: tXAxWFUqnhi86Ajj7dRY5g==', - b'Connection: keep-alive, Upgrade', - b'Upgrade: websocket', - b'\r\n', # last string doesn't get a \r\n from join() - ]) - server.processHandshake() - - self.assertEqual(1, len(values)) - self.assertEqual(42 * 42, values[0]) + assert len(values) == 1 + assert values[0] == num * num diff --git a/autobahn/asyncio/util.py b/autobahn/asyncio/util.py index 1b3e1cc16..21680c502 100644 --- a/autobahn/asyncio/util.py +++ b/autobahn/asyncio/util.py @@ -25,14 +25,18 @@ ############################################################################### import hashlib +from subprocess import Popen from typing import Optional +import asyncio from asyncio import sleep # noqa +from autobahn.wamp.types import TransportDetails __all = ( 'sleep', 'peer2str', 'transport_channel_id', + 'create_transport_details', ) @@ -55,6 +59,7 @@ def transport_channel_id(transport, is_server: bool, channel_id_type: Optional[s if channel_id_type is None: return b'\x00' * 32 + # ssl.CHANNEL_BINDING_TYPES if channel_id_type not in ['tls-unique']: raise Exception("invalid channel ID type {}".format(channel_id_type)) @@ -67,23 +72,47 @@ def transport_channel_id(transport, is_server: bool, channel_id_type: Optional[s # https://python.readthedocs.io/en/latest/library/ssl.html#ssl.SSLSocket.get_channel_binding # https://tools.ietf.org/html/rfc5929.html - tls_finished_msg = ssl_obj.get_channel_binding(cb_type='tls-unique') + tls_finished_msg: bytes = ssl_obj.get_channel_binding(cb_type='tls-unique') - m = hashlib.sha256() - m.update(tls_finished_msg) - channel_id = m.digest() - - return channel_id - - -def peer2str(peer): - if isinstance(peer, tuple): - ip_ver = 4 if len(peer) == 2 else 6 - return "tcp{2}:{0}:{1}".format(peer[0], peer[1], ip_ver) - elif isinstance(peer, str): - return "unix:{0}".format(peer) + if type(tls_finished_msg) != bytes: + return b'\x00' * 32 else: - return "?:{0}".format(peer) + m = hashlib.sha256() + m.update(tls_finished_msg) + channel_id = m.digest() + return channel_id + + +def peer2str(transport: asyncio.transports.BaseTransport) -> str: + # https://docs.python.org/3.9/library/asyncio-protocol.html?highlight=get_extra_info#asyncio.BaseTransport.get_extra_info + # https://docs.python.org/3.9/library/socket.html#socket.socket.getpeername + try: + peer = transport.get_extra_info('peername') + if isinstance(peer, tuple): + ip_ver = 4 if len(peer) == 2 else 6 + return "tcp{2}:{0}:{1}".format(peer[0], peer[1], ip_ver) + elif isinstance(peer, str): + return "unix:{0}".format(peer) + else: + return "?:{0}".format(peer) + except: + pass + + try: + proc: Popen = transport.get_extra_info('subprocess') + # return 'process:{}'.format(transport.pid) + return 'process:{}'.format(proc.pid) + except: + pass + + try: + pipe = transport.get_extra_info('pipe') + return 'pipe:{}'.format(pipe) + except: + pass + + # gracefully fallback if we can't map the peer's transport + return 'unknown' def get_serializers(): @@ -93,3 +122,26 @@ def get_serializers(): serializers = list(filter(lambda x: x, map(lambda s: getattr(serializer, s) if hasattr(serializer, s) else None, serializers))) return serializers + + +def create_transport_details(transport, is_server: bool) -> TransportDetails: + # Internal helper. Base class calls this to create a TransportDetails + peer = peer2str(transport) + + # https://docs.python.org/3.9/library/asyncio-protocol.html?highlight=get_extra_info#asyncio.BaseTransport.get_extra_info + is_secure = transport.get_extra_info('peercert', None) is not None + if is_secure: + channel_id = { + 'tls-unique': transport_channel_id(transport, is_server, 'tls-unique'), + } + channel_type = TransportDetails.CHANNEL_TYPE_TLS_TCP + peer_cert = None + else: + channel_id = {} + channel_type = TransportDetails.CHANNEL_TYPE_TCP + peer_cert = None + channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET + + return TransportDetails(channel_type=channel_type, channel_framing=channel_framing, + peer=peer, is_server=is_server, is_secure=is_secure, + channel_id=channel_id, peer_cert=peer_cert) diff --git a/autobahn/asyncio/wamp.py b/autobahn/asyncio/wamp.py index 063b72724..b9fbb16f7 100644 --- a/autobahn/asyncio/wamp.py +++ b/autobahn/asyncio/wamp.py @@ -28,8 +28,6 @@ import signal import txaio -txaio.use_asyncio() # noqa - from autobahn.util import public from autobahn.wamp import protocol from autobahn.wamp.types import ComponentConfig @@ -249,7 +247,11 @@ def accept(response): loop = asyncio.get_event_loop() if hasattr(transport_factory, 'loop'): transport_factory.loop = loop - txaio.use_asyncio() + + # assure we are using asyncio + # txaio.use_asyncio() + assert txaio._explicit_framework == 'asyncio' + txaio.config.loop = loop coro = loop.create_connection(transport_factory, host, port, ssl=ssl) diff --git a/autobahn/asyncio/websocket.py b/autobahn/asyncio/websocket.py index 733e14591..e8246f93e 100644 --- a/autobahn/asyncio/websocket.py +++ b/autobahn/asyncio/websocket.py @@ -28,15 +28,13 @@ from asyncio import iscoroutine from asyncio import Future from collections import deque +from typing import Optional import txaio -txaio.use_asyncio() - -from autobahn.util import public -from autobahn.asyncio.util import transport_channel_id, peer2str +from autobahn.util import public, hltype +from autobahn.asyncio.util import create_transport_details from autobahn.wamp import websocket from autobahn.websocket import protocol -from autobahn.wamp.types import TransportDetails __all__ = ( 'WebSocketServerProtocol', @@ -63,22 +61,28 @@ class WebSocketAdapterProtocol(asyncio.Protocol): """ Adapter class for asyncio-based WebSocket client and server protocols. """ + log = txaio.make_logger() - peer = None - peer_transport = None + peer: Optional[str] = None + is_server: Optional[bool] = None def connection_made(self, transport): + # asyncio networking framework entry point, called by asyncio + # when the connection is established (either a client or a server) + self.log.info('{func}(transport={transport})', func=hltype(self.connection_made), + transport=transport) + self.transport = transport + + # determine preliminary transport details (what is know at this point) + self._transport_details = create_transport_details(self.transport, self.is_server) + + # backward compatibility + self.peer = self._transport_details.peer + self.receive_queue = deque() self._consume() - # the peer we are connected to - try: - self.peer = peer2str(transport.get_extra_info('peername')) - except: - self.peer = 'process:{}'.format(self.transport.pid) - self.peer_transport = 'websocket' - self._connectionMade() def connection_lost(self, exc): @@ -189,14 +193,6 @@ class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServer * :class:`autobahn.websocket.interfaces.IWebSocketChannel` """ - log = txaio.make_logger() - - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, True, channel_id_type) - @public class WebSocketClientProtocol(WebSocketAdapterProtocol, protocol.WebSocketClientProtocol): @@ -208,42 +204,15 @@ class WebSocketClientProtocol(WebSocketAdapterProtocol, protocol.WebSocketClient * :class:`autobahn.websocket.interfaces.IWebSocketChannel` """ - log = txaio.make_logger() - def _onConnect(self, response): res = self.onConnect(response) + self.log.info('{func}: {res}', func=hltype(self._onConnect), res=res) if yields(res): asyncio.ensure_future(res) def startTLS(self): raise Exception("WSS over explicit proxies not implemented") - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, False, channel_id_type) - - def _create_transport_details(self): - """ - Internal helper. - Base class calls this to create a TransportDetails - """ - is_server = False - is_secure = self.transport.get_extra_info('peercert', None) is not None - if is_secure: - channel_id = { - 'tls-unique': transport_channel_id(self.transport, is_server, 'tls-unique'), - } - channel_type = TransportDetails.TRANSPORT_TYPE_TLS_TCP - peer_cert = None - else: - channel_id = {} - channel_type = TransportDetails.TRANSPORT_TYPE_TCP - peer_cert = None - return TransportDetails(channel_type=channel_type, peer=self.peer, is_server=is_server, is_secure=is_secure, - channel_id=channel_id, peer_cert=peer_cert) - class WebSocketAdapterFactory(object): """ diff --git a/autobahn/asyncio/xbr/__init__.py b/autobahn/asyncio/xbr/__init__.py index 1acff4f13..cb7a97e35 100644 --- a/autobahn/asyncio/xbr/__init__.py +++ b/autobahn/asyncio/xbr/__init__.py @@ -32,12 +32,9 @@ if HAS_XBR: + import uuid import asyncio import txaio - txaio.use_asyncio() - - import uuid - from autobahn.util import hl from autobahn.xbr._interfaces import IProvider, ISeller, IConsumer, IBuyer diff --git a/autobahn/nvx/_utf8validator.py b/autobahn/nvx/_utf8validator.py index 343933563..78d45b17a 100644 --- a/autobahn/nvx/_utf8validator.py +++ b/autobahn/nvx/_utf8validator.py @@ -68,7 +68,7 @@ def __init__(self): self.lib = lib self._vld = self.ffi.gc(self.lib.nvx_utf8vld_new(), self.lib.nvx_utf8vld_free) - print(self.lib.nvx_utf8vld_get_impl(self._vld)) + # print(self.lib.nvx_utf8vld_get_impl(self._vld)) def reset(self): self.lib.nvx_utf8vld_reset(self._vld) diff --git a/autobahn/nvx/test/__init__.py b/autobahn/nvx/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/nvx/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/nvx/test/test_nvx_utf8validator.py b/autobahn/nvx/test/test_nvx_utf8validator.py index eea857cd9..6fde51c06 100644 --- a/autobahn/nvx/test/test_nvx_utf8validator.py +++ b/autobahn/nvx/test/test_nvx_utf8validator.py @@ -306,14 +306,19 @@ def test_standard_utf8validator_incremental(self): validator = StandardUtf8Validator() return self._test_utf8_incremental(validator) - # NVX UTF8 validator lack incremental mode implementation - @unittest.expectedFailure - def test_nvx_utf8validator_incremental(self): - """ - Test NVX implementation of UTF8 validator in incremental mode. - """ - validator = NvxUtf8Validator() - return self._test_utf8_incremental(validator) + # FIXME + # see also (I think ..): https://twistedmatrix.com/trac/ticket/4811 + # + # import pytest + # + # @pytest.mark.xfail(reason='NVX UTF8 validator lacks incremental mode implementation') + # @unittest.expectedFailure + # def test_nvx_utf8validator_incremental(self): + # """ + # Test NVX implementation of UTF8 validator in incremental mode. + # """ + # validator = NvxUtf8Validator() + # return self._test_utf8_incremental(validator) def _test_utf8(self, validator): for s in self._TEST_SEQUENCES: diff --git a/autobahn/rawsocket/test/__init__.py b/autobahn/rawsocket/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/rawsocket/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/test/__init__.py b/autobahn/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/twisted/rawsocket.py b/autobahn/twisted/rawsocket.py index 92e50bd7d..8f44a59ff 100644 --- a/autobahn/twisted/rawsocket.py +++ b/autobahn/twisted/rawsocket.py @@ -26,6 +26,8 @@ import copy import math +from typing import Optional + import txaio from twisted.internet.protocol import Factory @@ -33,9 +35,9 @@ from twisted.internet.error import ConnectionDone from twisted.internet.defer import CancelledError -from autobahn.util import public -from autobahn.twisted.util import peer2str, transport_channel_id -from autobahn.util import _LazyHexFormatter +from autobahn.util import public, _LazyHexFormatter +from autobahn.twisted.util import create_transport_details +from autobahn.wamp.types import TransportDetails from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost, InvalidUriError from autobahn.exception import PayloadExceededError @@ -53,8 +55,8 @@ class WampRawSocketProtocol(Int32StringReceiver): """ log = txaio.make_logger() - peer = None - peer_transport = None + peer: Optional[str] = None + is_server: Optional[bool] = None def __init__(self): # set the RawSocket maximum message size by default @@ -67,16 +69,15 @@ def lengthLimitExceeded(self, length): raise PayloadExceededError(emsg) def connectionMade(self): - self.log.debug('{klass}.connectionMade()', klass=self.__class__.__name__) + # Twisted networking framework entry point, called by Twisted + # when the connection is established (either a client or a server) - # the peer we are connected to - # - try: - self.peer = peer2str(self.transport.getPeer()) - except AttributeError: - # ProcessProtocols lack getPeer() - self.peer = 'process:{}'.format(self.transport.pid) - self.peer_transport = 'rawsocket' + # determine preliminary transport details (what is know at this point) + self._transport_details = create_transport_details(self.transport, self.is_server) + self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET + + # backward compatibility + self.peer = self._transport_details.peer # a Future/Deferred that fires when we hit STATE_CLOSED self.is_closed = txaio.create_future() @@ -325,12 +326,6 @@ def dataReceived(self, data): if data: self.dataReceived(data) - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, is_server=True, channel_id_type=channel_id_type) - @public class WampRawSocketClientProtocol(WampRawSocketProtocol): @@ -417,12 +412,6 @@ def dataReceived(self, data): if data: self.dataReceived(data) - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, is_server=False, channel_id_type=channel_id_type) - class WampRawSocketFactory(Factory): """ diff --git a/autobahn/twisted/test/__init__.py b/autobahn/twisted/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/twisted/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/twisted/test/test_tx_protocol.py b/autobahn/twisted/test/test_tx_protocol.py index 8a49ced8d..85adfa2cf 100644 --- a/autobahn/twisted/test/test_tx_protocol.py +++ b/autobahn/twisted/test/test_tx_protocol.py @@ -26,6 +26,9 @@ from unittest.mock import Mock +import txaio +txaio.use_twisted() + from autobahn.util import wildcards2patterns from autobahn.twisted.websocket import WebSocketServerFactory from autobahn.twisted.websocket import WebSocketServerProtocol @@ -378,25 +381,27 @@ class OnConnectingTests(unittest.TestCase): def test_on_connecting_client_fails(self): + MAGIC_STR = 'bad stuff' + class TestProto(WebSocketClientProtocol): state = None wasClean = True log = Mock() def onConnecting(self, transport_details): - raise RuntimeError("bad stuff") + raise RuntimeError(MAGIC_STR) - from autobahn.testutil import FakeTransport proto = TestProto() proto.transport = FakeTransport() d = proto.startHandshake() self.successResultOf(d) # error is ignored # ... but error should be logged self.assertTrue(len(proto.log.mock_calls) > 0) - self.assertIn( - "bad stuff", - str(proto.log.mock_calls[0]), - ) + magic_found = False + for i in range(len(proto.log.mock_calls)): + if MAGIC_STR in str(proto.log.mock_calls[i]): + magic_found = True + self.assertTrue(magic_found, 'MAGIC_STR not found when expected') def test_on_connecting_client_success(self): @@ -415,7 +420,6 @@ def onConnecting(self, transport_details): resource="/ws", ) - from autobahn.test import FakeTransport proto = TestProto() proto.transport = FakeTransport() proto.factory = Mock() @@ -429,7 +433,7 @@ def onConnecting(self, transport_details): def test_str_transport(self): details = TransportDetails( - channel_type=TransportDetails.TRANSPORT_TYPE_FUNCTION, + channel_type=TransportDetails.CHANNEL_TYPE_FUNCTION, peer="example.com", is_secure=False, channel_id={}, diff --git a/autobahn/twisted/util.py b/autobahn/twisted/util.py index 2e0e3ffa3..8d896cdfd 100644 --- a/autobahn/twisted/util.py +++ b/autobahn/twisted/util.py @@ -24,11 +24,17 @@ # ############################################################################### +import os import hashlib +import threading from typing import Optional, Union from twisted.internet.defer import Deferred from twisted.internet.address import IPv4Address, UNIXAddress +from twisted.internet.interfaces import ITransport, IProcessTransport, ISSLTransport + +from autobahn.wamp.types import TransportDetails + try: from twisted.internet.stdio import PipeAddress except ImportError: @@ -45,7 +51,8 @@ __all = ( 'sleep', 'peer2str', - 'transport_channel_id' + 'transport_channel_id', + 'create_transport_details', ) @@ -69,27 +76,42 @@ def sleep(delay, reactor=None): return d -def peer2str(addr: Union[IPv4Address, IPv6Address, UNIXAddress, PipeAddress]) -> str: +def peer2str(transport: Union[ITransport, IProcessTransport]) -> str: """ - Convert a Twisted address as returned from ``self.transport.getPeer()`` to a string. + Return a *peer descriptor* given a Twisted transport, for example: - :returns: Returns a string representation of the peer on a Twisted transport. + * ``tcp4:127.0.0.1:52914``: a TCPv4 socket + * ``unix:/tmp/server.sock``: a Unix domain socket + * ``process:142092``: a Pipe originating from a spawning (parent) process + * ``pipe``: a Pipe terminating in a spawned (child) process + + :returns: Returns a string representation of the peer of the Twisted transport. """ - if isinstance(addr, IPv4Address): - res = "tcp4:{0}:{1}".format(addr.host, addr.port) - elif _HAS_IPV6 and isinstance(addr, IPv6Address): - res = "tcp6:{0}:{1}".format(addr.host, addr.port) - elif isinstance(addr, UNIXAddress): - if addr.name: - res = "unix:{0}".format(addr.name) + # IMPORTANT: we need to _first_ test for IProcessTransport + if IProcessTransport.providedBy(transport): + # note the PID of the forked process in the peer descriptor + res = "process:{}".format(transport.pid) + elif ITransport.providedBy(transport): + addr: Union[IPv4Address, IPv6Address, UNIXAddress, PipeAddress] = transport.getPeer() + if isinstance(addr, IPv4Address): + res = "tcp4:{0}:{1}".format(addr.host, addr.port) + elif _HAS_IPV6 and isinstance(addr, IPv6Address): + res = "tcp6:{0}:{1}".format(addr.host, addr.port) + elif isinstance(addr, UNIXAddress): + if addr.name: + res = "unix:{0}".format(addr.name) + else: + res = "unix" + elif isinstance(addr, PipeAddress): + # sadly, we don't have a way to get at the PID of the other side of the pipe + # res = "pipe" + res = "process:{0}".format(os.getppid()) else: - res = "unix" - elif isinstance(addr, PipeAddress): - res = "" + # gracefully fallback if we can't map the peer's address + res = "unknown" else: - # gracefully fallback if we can't map the peer's address - res = "?:{0}".format(addr) - + # gracefully fallback if we can't map the peer's transport + res = "unknown" return res @@ -164,3 +186,39 @@ def transport_channel_id(transport: object, is_server: bool, channel_id_type: Op return m.digest() else: raise NotImplementedError('should not arrive here (unhandled channel_id_type "{}")'.format(channel_id_type)) + + +def create_transport_details(transport: Union[ITransport, IProcessTransport], is_server: bool) -> TransportDetails: + """ + + :param transport: + :param is_server: + :return: + """ + peer = peer2str(transport) + + own_pid = os.getpid() + if hasattr(threading, 'get_native_id'): + # New in Python 3.8 + # https://docs.python.org/3/library/threading.html?highlight=get_native_id#threading.get_native_id + own_tid = threading.get_native_id() + else: + own_tid = threading.get_ident() + own_fd = -1 + + is_secure = ISSLTransport.providedBy(transport) + if is_secure: + channel_id = { + 'tls-unique': transport_channel_id(transport, is_server, 'tls-unique'), + } + channel_type = TransportDetails.CHANNEL_TYPE_TLS_TCP + peer_cert = None + else: + channel_id = {} + channel_type = TransportDetails.CHANNEL_TYPE_TCP + peer_cert = None + channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET + + return TransportDetails(channel_type=channel_type, channel_framing=channel_framing, peer=peer, + is_server=is_server, own_pid=own_pid, own_tid=own_tid, own_fd=own_fd, + is_secure=is_secure, channel_id=channel_id, peer_cert=peer_cert) diff --git a/autobahn/twisted/websocket.py b/autobahn/twisted/websocket.py index a9fb4cbe8..911b14bac 100644 --- a/autobahn/twisted/websocket.py +++ b/autobahn/twisted/websocket.py @@ -25,6 +25,8 @@ ############################################################################### from base64 import b64encode, b64decode +from pprint import pformat +from typing import Optional from zope.interface import implementer @@ -33,20 +35,22 @@ import twisted.internet.protocol from twisted.internet import endpoints -from twisted.internet.interfaces import ITransport, ISSLTransport +from twisted.internet.interfaces import ITransport from twisted.internet.error import ConnectionDone, ConnectionAborted, \ ConnectionLost from twisted.internet.defer import Deferred +from twisted.python.failure import Failure +from twisted.internet.protocol import connectionDone -from autobahn.util import public, hltype +from autobahn.util import public, hltype, hlval from autobahn.util import _is_tls_error, _maybe_tls_reason from autobahn.wamp import websocket -from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, ConnectionDeny from autobahn.wamp.types import TransportDetails +from autobahn.websocket.types import ConnectionRequest, ConnectionResponse, ConnectionDeny from autobahn.websocket import protocol from autobahn.websocket.interfaces import IWebSocketClientAgent -from autobahn.twisted.util import peer2str, transport_channel_id +from autobahn.twisted.util import create_transport_details from autobahn.websocket.compress import PerMessageDeflateOffer, \ PerMessageDeflateOfferAccept, \ @@ -231,36 +235,74 @@ def handshake_completed(arg): class WebSocketAdapterProtocol(twisted.internet.protocol.Protocol): """ Adapter class for Twisted WebSocket client and server protocols. + + Called from Twisted: + + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionMade` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionLost` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.dataReceived` + + Called from Network-independent Code (WebSocket implementation): + + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onOpen` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageBegin` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageFrameData` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageFrameEnd` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessageEnd` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onMessage` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onPing` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onPong` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._onClose` + + FIXME: + + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._closeConnection` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol._create_transport_details` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.registerProducer` + * :meth:`autobahn.twisted.websocket.WebSocketAdapterProtocol.unregisterProducer` """ log = txaio.make_logger() - peer = None - peer_transport = None + peer: Optional[str] = None + is_server: Optional[bool] = None def connectionMade(self): - # the peer we are connected to - try: - self.peer = peer2str(self.transport.getPeer()) - except (AttributeError, NotImplementedError): - # ProcessProtocols lack getPeer() - self.peer = 'process:{}'.format(self.transport.pid) - self.peer_transport = 'websocket' + # Twisted networking framework entry point, called by Twisted + # when the connection is established (either a client or a server) - self._connectionMade() - self.log.debug('Connection made to {peer}', peer=self.peer) + # determine preliminary transport details (what is know at this point) + self._transport_details = create_transport_details(self.transport, self.is_server) + self._transport_details.channel_framing = TransportDetails.CHANNEL_FRAMING_WEBSOCKET - # Set "Nagle" + # backward compatibility + self.peer = self._transport_details.peer + + # try to set "Nagle" option for TCP sockets try: self.transport.setTcpNoDelay(self.tcpNoDelay) except: # don't touch this! does not work: AttributeError, OSError # eg Unix Domain sockets throw Errno 22 on this pass - def connectionLost(self, reason): + # ok, now forward to the networking framework independent code for websocket + self._connectionMade() + + # ok, done! + self.log.info('{func} connection established for peer="{peer}", transport_details=\n{transport_details}', + func=hltype(self.connectionMade), + peer=hlval(self.peer), + transport_details=pformat(self._transport_details.marshal())) + + def connectionLost(self, reason: Failure = connectionDone): + # Twisted networking framework entry point, called by Twisted + # when the connection is lost (either a client or a server) + + was_clean = False if isinstance(reason.value, ConnectionDone): self.log.debug("Connection to/from {peer} was closed cleanly", peer=self.peer) + was_clean = True elif _is_tls_error(reason.value): self.log.error(_maybe_tls_reason(reason.value)) @@ -284,9 +326,27 @@ def connectionLost(self, reason): self.log.debug("Connection to/from {peer} lost ({error_type}): {error})", peer=self.peer, error_type=type(reason.value), error=reason.value) + # ok, now forward to the networking framework independent code for websocket self._connectionLost(reason) - def dataReceived(self, data): + # ok, done! + if was_clean: + self.log.info('{func} connection lost for peer="{peer}", closed cleanly', + func=hltype(self.connectionLost), + peer=hlval(self.peer)) + else: + self.log.info('{func} connection lost for peer="{peer}", closed with error {reason}', + func=hltype(self.connectionLost), + peer=hlval(self.peer), + reason=reason) + + def dataReceived(self, data: bytes): + self.log.debug('{func} received {data_len} bytes for peer="{peer}"', + func=hltype(self.dataReceived), + peer=hlval(self.peer), + data_len=hlval(len(data))) + + # bytes received from Twisted, forward to the networking framework independent code for websocket self._dataReceived(data) def _closeConnection(self, abort=False): @@ -356,12 +416,10 @@ class WebSocketServerProtocol(WebSocketAdapterProtocol, protocol.WebSocketServer """ log = txaio.make_logger() + is_server = True - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, True, channel_id_type) + # def onConnect(self, request: ConnectionRequest) -> Union[Optional[str], Tuple[Optional[str], Dict[str, str]]]: + # pass @public @@ -373,6 +431,7 @@ class WebSocketClientProtocol(WebSocketAdapterProtocol, protocol.WebSocketClient """ log = txaio.make_logger() + is_server = False def _onConnect(self, response: ConnectionResponse): self.log.debug('{meth}(response={response})', meth=hltype(self._onConnect), response=response) @@ -382,38 +441,6 @@ def startTLS(self): self.log.debug("Starting TLS upgrade") self.transport.startTLS(self.factory.contextFactory) - def get_channel_id(self, channel_id_type=None): - """ - Implements :func:`autobahn.wamp.interfaces.ITransport.get_channel_id` - """ - return transport_channel_id(self.transport, False, channel_id_type) - - def _create_transport_details(self): - """ - Internal helper. - Base class calls this to create a TransportDetails - """ - # note that ITLSTransport exists too, which is "a TCP - # transport that *can be upgraded* to TLS" .. if it *is* - # upgraded to TLS, then the transport will implement - # ISSLTransport at that point according to Twisted - # documentation - # the peer we are connected to - is_server = False - is_secure = ISSLTransport.providedBy(self.transport) - if is_secure: - channel_id = { - 'tls-unique': transport_channel_id(self.transport, is_server, 'tls-unique'), - } - channel_type = TransportDetails.TRANSPORT_TYPE_TLS_TCP - peer_cert = None - else: - channel_id = {} - channel_type = TransportDetails.TRANSPORT_TYPE_TCP - peer_cert = None - return TransportDetails(channel_type=channel_type, peer=self.peer, is_server=is_server, is_secure=is_secure, - channel_id=channel_id, peer_cert=peer_cert) - class WebSocketAdapterFactory(object): """ diff --git a/autobahn/wamp/cryptosign.py b/autobahn/wamp/cryptosign.py index 0ae81e281..13230ca4b 100644 --- a/autobahn/wamp/cryptosign.py +++ b/autobahn/wamp/cryptosign.py @@ -356,7 +356,7 @@ def _verify_signify_ed25519_signature(pubkey_file, signature_file, message): if HAS_CRYPTOSIGN: - def format_challenge(challenge: Challenge, channel_id_raw: bytes, channel_id_type: str) -> bytes: + def format_challenge(challenge: Challenge, channel_id_raw: Optional[bytes], channel_id_type: Optional[str]) -> bytes: """ Format the challenge based on provided parameters @@ -452,18 +452,18 @@ def sign_challenge(self, session: ISession, challenge: Challenge, channel_id_typ Sign WAMP-cryptosign challenge. :param session: The authenticating WAMP session. - :type session: :class:`autobahn.wamp.protocol.ApplicationSession` - :param challenge: The WAMP-cryptosign challenge object for which a signature should be computed. - :type challenge: instance of autobahn.wamp.types.Challenge - :returns: A Deferred/Future that resolves to the computed signature. - :rtype: str """ - # get the TLS channel ID of the underlying TLS connection. Could be None. - channel_id_raw = session._transport.get_channel_id(channel_id_type) + # get the TLS channel ID of the underlying TLS connection + channel_id_type = 'tls-unique' + if channel_id_type in session._transport.transport_details.channel_id: + channel_id = session._transport.transport_details.channel_id.get(channel_id_type, None) + else: + channel_id_type = None + channel_id = None - data = format_challenge(challenge, channel_id_raw, channel_id_type) + data = format_challenge(challenge, channel_id, channel_id_type) return sign_challenge(data, self.sign) diff --git a/autobahn/wamp/interfaces.py b/autobahn/wamp/interfaces.py index 6bb6df6ef..e1737e2cd 100644 --- a/autobahn/wamp/interfaces.py +++ b/autobahn/wamp/interfaces.py @@ -223,14 +223,6 @@ class ITransport(abc.ABC): message-based channel. """ - @public - @property - @abc.abstractmethod - def transport_details(self) -> Optional[TransportDetails]: - """ - Return details about the transport (when the transport is open). - """ - @public @abc.abstractmethod def send(self, message: IMessage): @@ -251,7 +243,15 @@ def isOpen(self) -> bool: Check if the transport is open for messaging. :returns: ``True``, if the transport is open. -s """ + """ + + @public + @property + @abc.abstractmethod + def transport_details(self) -> Optional[TransportDetails]: + """ + Return details about the transport (when the transport is open). + """ @public @abc.abstractmethod @@ -272,51 +272,6 @@ def abort(self): detected attacks. """ - @public - @abc.abstractmethod - def get_channel_id(self): - """ - Return the unique channel ID of the underlying transport. This is used to - mitigate credential forwarding man-in-the-middle attacks when running - application level authentication (eg WAMP-cryptosign) which are decoupled - from the underlying transport. - - The channel ID is only available when running over TLS (either WAMP-WebSocket - or WAMP-RawSocket). It is not available for non-TLS transports (plain TCP or - Unix domain sockets). It is also not available for WAMP-over-HTTP/Longpoll. - Further, it is currently unimplemented for asyncio (only works on Twisted). - - The channel ID is computed as follows: - - - for a client, the SHA256 over the "TLS Finished" message sent by the client - to the server is returned. - - - for a server, the SHA256 over the "TLS Finished" message the server expected - the client to send - - Note: this is similar to `tls-unique` as described in RFC5929, but instead - of returning the raw "TLS Finished" message, it returns a SHA256 over such a - message. The reason is that we use the channel ID mainly with WAMP-cryptosign, - which is based on Ed25519, where keys are always 32 bytes. And having a channel ID - which is always 32 bytes (independent of the TLS ciphers/hashfuns in use) allows - use to easily XOR channel IDs with Ed25519 keys and WAMP-cryptosign challenges. - - WARNING: For safe use of this (that is, for safely binding app level authentication - to the underlying transport), you MUST use TLS, and you SHOULD deactivate both - TLS session renegotiation and TLS session resumption. - - References: - - - https://tools.ietf.org/html/rfc5056 - - https://tools.ietf.org/html/rfc5929 - - http://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Connection.get_finished - - http://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Connection.get_peer_finished - - :returns: The channel ID (if available) of the underlying WAMP transport. The - channel ID is a 32 bytes value. - :rtype: binary or None - """ - @public class ITransportHandler(abc.ABC): diff --git a/autobahn/wamp/test/__init__.py b/autobahn/wamp/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/wamp/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/wamp/test/test_wamp_component_aio.py b/autobahn/wamp/test/test_wamp_component_aio.py index 0dc9b527e..8c3e6a75b 100644 --- a/autobahn/wamp/test/test_wamp_component_aio.py +++ b/autobahn/wamp/test/test_wamp_component_aio.py @@ -130,7 +130,8 @@ def create_connection(protocol_factory=None, server_hostname=None, host=None, po # process already-completed Futures like Twisted does) def nuke_transport(): - actual_protocol[0].connection_lost(None) # asyncio can call this with None + if actual_protocol[0] is not None: + actual_protocol[0].connection_lost(None) # asyncio can call this with None txaio.call_later(0.1, nuke_transport) finished = txaio.create_future() diff --git a/autobahn/wamp/test/test_wamp_cryptosign.py b/autobahn/wamp/test/test_wamp_cryptosign.py index c6fc4522b..6b5c71686 100644 --- a/autobahn/wamp/test/test_wamp_cryptosign.py +++ b/autobahn/wamp/test/test_wamp_cryptosign.py @@ -25,21 +25,11 @@ ############################################################################### import hashlib -import os import binascii +import unittest from unittest.mock import Mock import txaio - -if os.environ.get('USE_TWISTED', False): - txaio.use_twisted() - from twisted.trial import unittest -elif os.environ.get('USE_ASYNCIO', False): - txaio.use_asyncio() - import unittest -else: - raise Exception('no networking framework selected') - from autobahn.wamp.cryptosign import _makepad, HAS_CRYPTOSIGN from autobahn.wamp import types from autobahn.wamp.auth import create_authenticator @@ -89,11 +79,16 @@ def setUp(self): self.privkey_hex = self.key._key.encode(encoder=HexEncoder) m = hashlib.sha256() m.update("some TLS message".encode()) - self.channel_id = m.digest() + channel_id = m.digest() + self.transport_details = types.TransportDetails(channel_id={'tls-unique': channel_id}) + + def test_public_key(self): + self.assertEqual(self.key.public_key(binary=False), '1adfc8bfe1d35616e64dffbd900096f23b066f914c8c2ffbb66f6075b96e116d') def test_valid(self): session = Mock() - session._transport.get_channel_id = Mock(return_value=self.channel_id) + session._transport.transport_details = self.transport_details + challenge = types.Challenge("ticket", dict(challenge="ff" * 32)) f_signed = self.key.sign_challenge(session, challenge) @@ -114,7 +109,8 @@ def failed(err): def test_testvectors(self): session = Mock() - session._transport.get_channel_id = Mock(return_value=self.channel_id) + session._transport.transport_details = self.transport_details + for testvec in testvectors: priv_key = SigningKey.from_key_bytes(binascii.a2b_hex(testvec['priv_key'])) challenge = types.Challenge("ticket", dict(challenge=testvec['challenge'])) @@ -142,7 +138,7 @@ def test_authenticator(self): privkey=self.privkey_hex, ) session = Mock() - session._transport.get_channel_id = Mock(return_value=self.channel_id) + session._transport.transport_details = self.transport_details challenge = types.Challenge("cryptosign", dict(challenge="ff" * 32)) f_reply = authenticator.on_challenge(session, challenge) diff --git a/autobahn/wamp/test/test_wamp_protocol_peer.py b/autobahn/wamp/test/test_wamp_protocol_peer.py index 9c0e067fb..99ab52ec0 100644 --- a/autobahn/wamp/test/test_wamp_protocol_peer.py +++ b/autobahn/wamp/test/test_wamp_protocol_peer.py @@ -24,17 +24,9 @@ # ############################################################################### -import os - # we need to select a txaio subsystem because we're importing the base # protocol classes here for testing purposes. "normally" yo'd import # from autobahn.twisted.wamp or autobahn.asyncio.wamp explicitly. -import txaio -if os.environ.get('USE_TWISTED', False): - txaio.use_twisted() -else: - txaio.use_asyncio() - from autobahn import wamp from autobahn.wamp import message from autobahn.wamp import exception diff --git a/autobahn/wamp/test/test_wamp_scram.py b/autobahn/wamp/test/test_wamp_scram.py index 9011ef832..b4c829737 100644 --- a/autobahn/wamp/test/test_wamp_scram.py +++ b/autobahn/wamp/test/test_wamp_scram.py @@ -24,19 +24,9 @@ # ############################################################################### -import os -import txaio +import unittest from binascii import a2b_hex -if os.environ.get('USE_TWISTED', False): - txaio.use_twisted() - from twisted.trial import unittest -elif os.environ.get('USE_ASYNCIO', False): - txaio.use_asyncio() - import unittest -else: - raise Exception('no networking framework selected') - from autobahn.wamp.auth import derive_scram_credential diff --git a/autobahn/wamp/test/test_wamp_serializer.py b/autobahn/wamp/test/test_wamp_serializer.py index c4bf491e2..a0f545068 100644 --- a/autobahn/wamp/test/test_wamp_serializer.py +++ b/autobahn/wamp/test/test_wamp_serializer.py @@ -30,12 +30,6 @@ import decimal from decimal import Decimal -import txaio -if os.environ.get('USE_TWISTED', False): - txaio.use_twisted() -elif os.environ.get('USE_ASYNCIO', False): - txaio.use_asyncio() - from autobahn.wamp import message from autobahn.wamp import role from autobahn.wamp import serializer diff --git a/autobahn/wamp/test/test_wamp_transport_details.py b/autobahn/wamp/test/test_wamp_transport_details.py new file mode 100644 index 000000000..484a8a866 --- /dev/null +++ b/autobahn/wamp/test/test_wamp_transport_details.py @@ -0,0 +1,123 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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 autobahn.wamp.types import TransportDetails + +import unittest + + +class TestTransportDetails(unittest.TestCase): + + def test_ctor_empty(self): + td = TransportDetails() + data = td.marshal() + self.assertEqual(data, { + 'channel_type': None, + 'channel_framing': None, + 'channel_serializer': None, + 'own': None, + 'peer': None, + 'is_server': None, + 'own_pid': None, + 'own_tid': None, + 'own_fd': None, + 'is_secure': None, + 'channel_id': None, + 'peer_cert': None, + 'websocket_protocol': None, + 'websocket_extensions_in_use': None, + 'http_headers_received': None, + 'http_headers_sent': None, + 'http_cbtid': None, + }) + td2 = TransportDetails.parse(td.marshal()) + self.assertEqual(td2, td) + + def test_attributes(self): + td = TransportDetails() + for channel_type in TransportDetails.CHANNEL_TYPE_TO_STR: + td.channel_type = channel_type + self.assertEqual(td.channel_type, channel_type) + + for channel_framing in TransportDetails.CHANNEL_FRAMING_TO_STR: + td.channel_framing = channel_framing + self.assertEqual(td.channel_framing, channel_framing) + + for channel_serializer in TransportDetails.CHANNEL_SERIALIZER_TO_STR: + td.channel_serializer = channel_serializer + self.assertEqual(td.channel_serializer, channel_serializer) + + def test_parse(self): + data = { + # TransportDetails.CHANNEL_TYPE_TO_STR[TransportDetails.CHANNEL_TYPE_TCP] + 'channel_type': 'tcp', + + # TransportDetails.CHANNEL_FRAMING_TO_STR[TransportDetails.CHANNEL_FRAMING_WEBSOCKET] + 'channel_framing': 'websocket', + + # TransportDetails.CHANNEL_SERIALIZER_TO_STR[TransportDetails.CHANNEL_SERIALIZER_CBOR] + 'channel_serializer': 'cbor', + + # This end of the connection + 'own': 'ws://localhost:8080/ws', + 'own_pid': 9182731, + 'own_tid': 7563483, + 'own_fd': 20914571, + + # Peer of the connection + 'peer': 'tcp4:127.0.0.1:48576', + 'is_server': True, + + # TLS + 'is_secure': False, + 'channel_id': None, + 'peer_cert': None, + + # only filled when using WebSocket + 'websocket_protocol': 'wamp.2.cbor.batched', + 'websocket_extensions_in_use': None, + + # only filled when using HTTP (including regular WebSocket) + 'http_headers_received': {'cache-control': 'no-cache', + 'connection': 'Upgrade', + 'host': 'localhost:8080', + 'pragma': 'no-cache', + 'sec-websocket-extensions': 'permessage-deflate; ' + 'client_no_context_takeover; ' + 'client_max_window_bits', + 'sec-websocket-key': 'Q+t++aGQJPaFLzDW7LktEQ==', + 'sec-websocket-protocol': 'wamp.2.cbor.batched,wamp.2.cbor,wamp.2.msgpack.batched,wamp.2.msgpack,wamp.2.ubjson.batched,wamp.2.ubjson,wamp.2.json.batched,wamp.2.json', + 'sec-websocket-version': '13', + 'upgrade': 'WebSocket', + 'user-agent': 'AutobahnPython/22.4.1.dev5'}, + 'http_headers_sent': {'Set-Cookie': 'cbtid=JD27oZC18xS+O4VE9+x5iyKR;max-age=604800'}, + 'http_cbtid': 'JD27oZC18xS+O4VE9+x5iyKR', + } + + td = TransportDetails.parse(data) + data2 = td.marshal() + self.maxDiff = None + self.assertEqual(data2, data) diff --git a/autobahn/wamp/test/test_wamp_user_handler_errors.py b/autobahn/wamp/test/test_wamp_user_handler_errors.py index f7294b77d..b39d99ef9 100644 --- a/autobahn/wamp/test/test_wamp_user_handler_errors.py +++ b/autobahn/wamp/test/test_wamp_user_handler_errors.py @@ -106,6 +106,7 @@ class TestSessionCallbacks(unittest.TestCase): # twice)...but that would mean switching all test-running over # to py-test + # the whole variable must not be defined to deactivate (!) skip = True def test_on_join(self): diff --git a/autobahn/wamp/types.py b/autobahn/wamp/types.py index 5c55a315f..a666cc62d 100644 --- a/autobahn/wamp/types.py +++ b/autobahn/wamp/types.py @@ -24,7 +24,7 @@ # ############################################################################### from pprint import pformat -from typing import Optional, Any, Dict +from typing import Optional, Any, Dict, List from binascii import a2b_hex from autobahn.util import public @@ -145,26 +145,17 @@ class Accept(HelloReturn): 'authextra', ) - def __init__(self, realm=None, authid=None, authrole=None, authmethod=None, authprovider=None, authextra=None): + def __init__(self, realm: Optional[str] = None, authid: Optional[str] = None, authrole: Optional[str] = None, + authmethod: Optional[str] = None, authprovider: Optional[str] = None, + authextra: Optional[Dict[str, Any]] = None): """ :param realm: The realm the client is joined to. - :type realm: str - :param authid: The authentication ID the client is assigned, e.g. ``"joe"`` or ``"joe@example.com"``. - :type authid: str - :param authrole: The authentication role the client is assigned, e.g. ``"anonymous"``, ``"user"`` or ``"com.myapp.user"``. - :type authrole: str - :param authmethod: The authentication method that was used to authenticate the client, e.g. ``"cookie"`` or ``"wampcra"``. - :type authmethod: str - :param authprovider: The authentication provider that was used to authenticate the client, e.g. ``"mozilla-persona"``. - :type authprovider: str - :param authextra: Application-specific authextra to be forwarded to the client in `WELCOME.details.authextra`. - :type authextra: dict """ assert(realm is None or type(realm) == str) assert(authid is None or type(authid) == str) @@ -1438,63 +1429,199 @@ class TransportDetails(object): __slots__ = ( '_channel_type', + '_channel_framing', + '_channel_serializer', + '_own', '_peer', '_is_server', + '_own_pid', + '_own_tid', + '_own_fd', '_is_secure', '_channel_id', '_peer_cert', + '_websocket_protocol', + '_websocket_extensions_in_use', + '_http_headers_received', + '_http_headers_sent', + '_http_cbtid', ) - TRANSPORT_TYPE_NONE = 0 - TRANSPORT_TYPE_FUNCTION = 1 - TRANSPORT_TYPE_MEMORY = 2 - TRANSPORT_TYPE_SERIAL = 3 - TRANSPORT_TYPE_TCP = 4 - TRANSPORT_TYPE_TLS_TCP = 5 - TRANSPORT_TYPE_UDP = 6 - TRANSPORT_TYPE_DTLS_UDP = 7 - - TRANSPORT_TYPE_TO_STR = { - TRANSPORT_TYPE_NONE: 'null', - TRANSPORT_TYPE_FUNCTION: 'function', - TRANSPORT_TYPE_MEMORY: 'memory', - TRANSPORT_TYPE_SERIAL: 'serial', - TRANSPORT_TYPE_TCP: 'tcp', - TRANSPORT_TYPE_TLS_TCP: 'tcp-tls', - TRANSPORT_TYPE_UDP: 'udp', - TRANSPORT_TYPE_DTLS_UDP: 'dtls-udp', + CHANNEL_TYPE_NONE = 0 + CHANNEL_TYPE_FUNCTION = 1 + CHANNEL_TYPE_MEMORY = 2 + CHANNEL_TYPE_SERIAL = 3 + CHANNEL_TYPE_TCP = 4 + CHANNEL_TYPE_TLS_TCP = 5 + CHANNEL_TYPE_UDP = 6 + CHANNEL_TYPE_DTLS_UDP = 7 + + CHANNEL_TYPE_TO_STR = { + CHANNEL_TYPE_NONE: 'null', + CHANNEL_TYPE_FUNCTION: 'function', + CHANNEL_TYPE_MEMORY: 'memory', + CHANNEL_TYPE_SERIAL: 'serial', + CHANNEL_TYPE_TCP: 'tcp', + CHANNEL_TYPE_TLS_TCP: 'tcp-tls', + CHANNEL_TYPE_UDP: 'udp', + CHANNEL_TYPE_DTLS_UDP: 'dtls-udp', + } + + CHANNEL_TYPE_FROM_STR = { + 'null': CHANNEL_TYPE_NONE, + 'function': CHANNEL_TYPE_FUNCTION, + 'memory': CHANNEL_TYPE_MEMORY, + 'serial': CHANNEL_TYPE_SERIAL, + 'tcp': CHANNEL_TYPE_TCP, + 'tcp-tls': CHANNEL_TYPE_TLS_TCP, + 'udp': CHANNEL_TYPE_UDP, + 'dtls-udp': CHANNEL_TYPE_DTLS_UDP, + } + + CHANNEL_FRAMING_NONE = 0 + CHANNEL_FRAMING_NATIVE = 1 + CHANNEL_FRAMING_WEBSOCKET = 2 + CHANNEL_FRAMING_RAWSOCKET = 3 + + CHANNEL_FRAMING_TO_STR = { + CHANNEL_FRAMING_NONE: 'null', + CHANNEL_FRAMING_NATIVE: 'native', + CHANNEL_FRAMING_WEBSOCKET: 'websocket', + CHANNEL_FRAMING_RAWSOCKET: 'rawsocket', } - TRANSPORT_TYPE_FROM_STR = { - 'null': TRANSPORT_TYPE_NONE, - 'function': TRANSPORT_TYPE_FUNCTION, - 'memory': TRANSPORT_TYPE_MEMORY, - 'serial': TRANSPORT_TYPE_SERIAL, - 'tcp': TRANSPORT_TYPE_TCP, - 'tcp-tls': TRANSPORT_TYPE_TLS_TCP, - 'udp': TRANSPORT_TYPE_UDP, - 'dtls-udp': TRANSPORT_TYPE_DTLS_UDP, + CHANNEL_FRAMING_FROM_STR = { + 'null': CHANNEL_TYPE_NONE, + 'native': CHANNEL_FRAMING_NATIVE, + 'websocket': CHANNEL_FRAMING_WEBSOCKET, + 'rawsocket': CHANNEL_FRAMING_RAWSOCKET, } - def __init__(self, channel_type: Optional[int], peer: Optional[str] = None, is_server: Optional[bool] = None, - is_secure: Optional[bool] = None, channel_id: Optional[Dict[str, bytes]] = None, - peer_cert: Optional[Dict[str, Any]] = None): + # Keep in sync with Serializer.SERIALIZER_ID and Serializer.RAWSOCKET_SERIALIZER_ID + CHANNEL_SERIALIZER_NONE = 0 + CHANNEL_SERIALIZER_JSON = 1 + CHANNEL_SERIALIZER_MSGPACK = 2 + CHANNEL_SERIALIZER_CBOR = 3 + CHANNEL_SERIALIZER_UBJSON = 4 + CHANNEL_SERIALIZER_FLATBUFFERS = 5 + + CHANNEL_SERIALIZER_TO_STR = { + CHANNEL_SERIALIZER_NONE: 'null', + CHANNEL_SERIALIZER_JSON: 'json', + CHANNEL_SERIALIZER_MSGPACK: 'msgpack', + CHANNEL_SERIALIZER_CBOR: 'cbor', + CHANNEL_SERIALIZER_UBJSON: 'ubjson', + CHANNEL_SERIALIZER_FLATBUFFERS: 'flatbuffers', + } + + CHANNEL_SERIALIZER_FROM_STR = { + 'null': CHANNEL_SERIALIZER_NONE, + 'json': CHANNEL_SERIALIZER_JSON, + 'msgpack': CHANNEL_SERIALIZER_MSGPACK, + 'cbor': CHANNEL_SERIALIZER_CBOR, + 'ubjson': CHANNEL_SERIALIZER_UBJSON, + 'flatbuffers': CHANNEL_SERIALIZER_FLATBUFFERS, + } + + def __init__(self, + channel_type: Optional[int] = None, + channel_framing: Optional[int] = None, + channel_serializer: Optional[int] = None, + own: Optional[str] = None, + peer: Optional[str] = None, + is_server: Optional[bool] = None, + own_pid: Optional[int] = None, + own_tid: Optional[int] = None, + own_fd: Optional[int] = None, + is_secure: Optional[bool] = None, + channel_id: Optional[Dict[str, bytes]] = None, + peer_cert: Optional[Dict[str, Any]] = None, + websocket_protocol: Optional[str] = None, + websocket_extensions_in_use: Optional[List[str]] = None, + http_headers_received: Optional[Dict[str, Any]] = None, + http_headers_sent: Optional[Dict[str, Any]] = None, + http_cbtid: Optional[str] = None): self._channel_type = channel_type + self._channel_framing = channel_framing + self._channel_serializer = channel_serializer + self._own = own self._peer = peer self._is_server = is_server + self._own_pid = own_pid + self._own_tid = own_tid + self._own_fd = own_fd self._is_secure = is_secure self._channel_id = channel_id self._peer_cert = peer_cert + self._websocket_protocol = websocket_protocol + self._websocket_extensions_in_use = websocket_extensions_in_use + self._http_headers_received = http_headers_received + self._http_headers_sent = http_headers_sent + self._http_cbtid = http_cbtid + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + if other._channel_type != self._channel_type: + return False + if other._channel_framing != self._channel_framing: + return False + if other._channel_serializer != self._channel_serializer: + return False + if other._own != self._own: + return False + if other._peer != self._peer: + return False + if other._is_server != self._is_server: + return False + if other._own_pid != self._own_pid: + return False + if other._own_tid != self._own_tid: + return False + if other._own_fd != self._own_fd: + return False + if other._is_secure != self._is_secure: + return False + if other._channel_id != self._channel_id: + return False + if other._peer_cert != self._peer_cert: + return False + if other._websocket_protocol != self._websocket_protocol: + return False + if other._websocket_extensions_in_use != self._websocket_extensions_in_use: + return False + if other._http_headers_received != self._http_headers_received: + return False + if other._http_headers_sent != self._http_headers_sent: + return False + if other._http_cbtid != self._http_cbtid: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) @staticmethod - def parse(data: Dict[str, Any]): + def parse(data: Dict[str, Any]) -> 'TransportDetails': assert type(data) == dict obj = TransportDetails() if 'channel_type' in data and data['channel_type'] is not None: - if type(data['channel_type']) != int or data['channel_type'] not in TransportDetails.TRANSPORT_TYPE_FROM_STR: + if type(data['channel_type']) != str or data['channel_type'] not in TransportDetails.CHANNEL_TYPE_FROM_STR: raise ValueError('invalid "channel_type", was type {} (value {})'.format(type(data['channel_type']), data['channel_type'])) - obj.channel_type = TransportDetails.TRANSPORT_TYPE_FROM_STR[data['channel_type']] + obj.channel_type = TransportDetails.CHANNEL_TYPE_FROM_STR[data['channel_type']] + if 'channel_framing' in data and data['channel_framing'] is not None: + if type(data['channel_framing']) != str or data['channel_framing'] not in TransportDetails.CHANNEL_FRAMING_FROM_STR: + raise ValueError('invalid "channel_framing", was type {} (value {})'.format(type(data['channel_framing']), data['channel_framing'])) + obj.channel_framing = TransportDetails.CHANNEL_FRAMING_FROM_STR[data['channel_framing']] + if 'channel_serializer' in data and data['channel_serializer'] is not None: + if type(data['channel_serializer']) != str or data['channel_serializer'] not in TransportDetails.CHANNEL_SERIALIZER_FROM_STR: + raise ValueError('invalid "channel_serializer", was type {} (value {})'.format(type(data['channel_serializer']), data['channel_serializer'])) + obj.channel_serializer = TransportDetails.CHANNEL_SERIALIZER_FROM_STR[data['channel_serializer']] + if 'own' in data and data['own'] is not None: + if type(data['own']) != str: + raise ValueError('"own" must be a string, was {}'.format(type(data['own']))) + obj.own = data['own'] if 'peer' in data and data['peer'] is not None: if type(data['peer']) != str: raise ValueError('"peer" must be a string, was {}'.format(type(data['peer']))) @@ -1502,11 +1629,23 @@ def parse(data: Dict[str, Any]): if 'is_server' in data and data['is_server'] is not None: if type(data['is_server']) != bool: raise ValueError('"is_server" must be a bool, was {}'.format(type(data['is_server']))) - obj.peer = data['is_server'] + obj.is_server = data['is_server'] + if 'own_pid' in data and data['own_pid'] is not None: + if type(data['own_pid']) != int: + raise ValueError('"own_pid" must be an int, was {}'.format(type(data['own_pid']))) + obj.own_pid = data['own_pid'] + if 'own_tid' in data and data['own_tid'] is not None: + if type(data['own_tid']) != int: + raise ValueError('"own_tid" must be an int, was {}'.format(type(data['own_tid']))) + obj.own_tid = data['own_tid'] + if 'own_fd' in data and data['own_fd'] is not None: + if type(data['own_fd']) != int: + raise ValueError('"own_fd" must be an int, was {}'.format(type(data['own_fd']))) + obj.own_fd = data['own_fd'] if 'is_secure' in data and data['is_secure'] is not None: if type(data['is_secure']) != bool: raise ValueError('"is_secure" must be a bool, was {}'.format(type(data['is_secure']))) - obj.peer = data['is_secure'] + obj.is_secure = data['is_secure'] if 'channel_id' in data and data['channel_id'] is not None: if type(data['channel_id']) != Dict[str, Any]: raise ValueError('"channel_id" must be a dict, was {}'.format(type(data['channel_id']))) @@ -1520,16 +1659,47 @@ def parse(data: Dict[str, Any]): binding_id = a2b_hex(binding_id_hex) channel_id[binding_type] = binding_id obj.channel_id = channel_id + if 'websocket_protocol' in data and data['websocket_protocol'] is not None: + if type(data['websocket_protocol']) != str: + raise ValueError('"websocket_protocol" must be a string, was {}'.format(type(data['websocket_protocol']))) + obj.websocket_protocol = data['websocket_protocol'] + if 'websocket_extensions_in_use' in data and data['websocket_extensions_in_use'] is not None: + if type(data['websocket_extensions_in_use']) != List[str]: + raise ValueError('"websocket_extensions_in_use" must be a list of strings, was {}'.format(type(data['websocket_extensions_in_use']))) + obj.websocket_extensions_in_use = data['websocket_extensions_in_use'] + if 'http_headers_received' in data and data['http_headers_received'] is not None: + if type(data['http_headers_received']) != dict: + raise ValueError('"http_headers_received" must be a map of strings, was {}'.format(type(data['http_headers_received']))) + obj.http_headers_received = data['http_headers_received'] + if 'http_headers_sent' in data and data['http_headers_sent'] is not None: + if type(data['http_headers_sent']) != dict: + raise ValueError('"http_headers_sent" must be a map of strings, was {}'.format(type(data['http_headers_sent']))) + obj.http_headers_sent = data['http_headers_sent'] + if 'http_cbtid' in data and data['http_cbtid'] is not None: + if type(data['http_cbtid']) != str: + raise ValueError('"http_cbtid" must be a string, was {}'.format(type(data['http_cbtid']))) + obj.http_cbtid = data['http_cbtid'] return obj def marshal(self) -> Dict[str, Any]: return { - 'channel_type': self.TRANSPORT_TYPE_TO_STR.get(self._channel_type, None), + 'channel_type': self.CHANNEL_TYPE_TO_STR.get(self._channel_type, None), + 'channel_framing': self.CHANNEL_FRAMING_TO_STR.get(self._channel_framing, None), + 'channel_serializer': self.CHANNEL_SERIALIZER_TO_STR.get(self._channel_serializer, None), + 'own': self._own, 'peer': self._peer, 'is_server': self._is_server, + 'own_pid': self._own_pid, + 'own_tid': self._own_tid, + 'own_fd': self._own_fd, 'is_secure': self._is_secure, 'channel_id': self._channel_id, 'peer_cert': self._peer_cert, + 'websocket_protocol': self._websocket_protocol, + 'websocket_extensions_in_use': self._websocket_extensions_in_use, + 'http_headers_received': self._http_headers_received, + 'http_headers_sent': self._http_headers_sent, + 'http_cbtid': self._http_cbtid, } def __str__(self) -> str: @@ -1538,7 +1708,7 @@ def __str__(self) -> str: @property def channel_type(self) -> Optional[int]: """ - The peer this transport is connected to. + The underlying transport type, e.g. TCP. """ return self._channel_type @@ -1546,10 +1716,67 @@ def channel_type(self) -> Optional[int]: def channel_type(self, value: Optional[int]): self._channel_type = value + @property + def channel_framing(self) -> Optional[int]: + """ + The message framing used on this transport, e.g. WebSocket. + """ + return self._channel_framing + + @channel_framing.setter + def channel_framing(self, value: Optional[int]): + self._channel_framing = value + + @property + def channel_serializer(self) -> Optional[int]: + """ + The message serializer used on this transport, e.g. CBOR (batched or unbatched). + """ + return self._channel_serializer + + @channel_serializer.setter + def channel_serializer(self, value: Optional[int]): + self._channel_serializer = value + + @property + def own(self) -> Optional[str]: + """ + + https://github.com/crossbario/autobahn-python/blob/master/autobahn/websocket/test/test_websocket_url.py + https://github.com/crossbario/autobahn-python/blob/master/autobahn/rawsocket/test/test_rawsocket_url.py + + A WebSocket server URL: + + * ``ws://localhost`` + * ``wss://example.com:443/ws`` + * ``ws://62.146.25.34:80/ws`` + * ``wss://localhost:9090/ws?foo=bar`` + + A RawSocket server URL: + + * ``rs://crossbar:8081`` + * ``rss://example.com`` + * ``rs://unix:/tmp/file.sock`` + * ``rss://unix:../file.sock`` + """ + return self._own + + @own.setter + def own(self, value: Optional[str]): + self._own = value + @property def peer(self) -> Optional[str]: """ The peer this transport is connected to. + + process:12784 + pipe + + tcp4:127.0.0.1:38810 + tcp4:127.0.0.1:8080 + unix:/tmp/file.sock + """ return self._peer @@ -1560,7 +1787,8 @@ def peer(self, value: Optional[str]): @property def is_server(self) -> Optional[bool]: """ - The peer this transport is connected to. + Flag indicating whether this side of the peer is a "server" (on underlying transports that + follows a client-server approach). """ return self._is_server @@ -1568,6 +1796,46 @@ def is_server(self) -> Optional[bool]: def is_server(self, value: Optional[bool]): self._is_server = value + @property + def own_pid(self) -> Optional[int]: + """ + The process ID (PID) of this end of the connection. + """ + return self._own_pid + + @own_pid.setter + def own_pid(self, value: Optional[int]): + self._own_pid = value + + @property + def own_tid(self) -> Optional[int]: + """ + The native thread ID of this end of the connection. + + See https://docs.python.org/3/library/threading.html#threading.get_native_id. + + .. note:: + + On CPython 3.7, instead of the native thread ID, a synthetic thread ID that has no direct meaning + is used (via ``threading.get_ident()``). + """ + return self._own_tid + + @own_tid.setter + def own_tid(self, value: Optional[int]): + self._own_tid = value + + @property + def own_fd(self) -> Optional[int]: + """ + The file descriptor (FD) at this end of the connection. + """ + return self._own_fd + + @own_fd.setter + def own_fd(self, value: Optional[int]): + self._own_fd = value + @property def is_secure(self) -> Optional[bool]: """ @@ -1578,16 +1846,49 @@ def is_secure(self) -> Optional[bool]: @is_secure.setter def is_secure(self, value: Optional[bool]): - """ - Flag indicating whether this transport runs over TLS (or similar), and hence is encrypting at - the byte stream or datagram transport level (beneath WAMP payload encryption). - """ self._is_secure = value @property def channel_id(self) -> Dict[str, bytes]: """ - If this The peer this transport is connected to. + If this transport runs over a secure underlying connection, e.g. TLS, + return a map of channel binding by binding type. + + Return the unique channel ID of the underlying transport. This is used to + mitigate credential forwarding man-in-the-middle attacks when running + application level authentication (eg WAMP-cryptosign) which are decoupled + from the underlying transport. + + The channel ID is only available when running over TLS (either WAMP-WebSocket + or WAMP-RawSocket). It is not available for non-TLS transports (plain TCP or + Unix domain sockets). It is also not available for WAMP-over-HTTP/Longpoll. + Further, it is currently unimplemented for asyncio (only works on Twisted). + + The channel ID is computed as follows: + + - for a client, the SHA256 over the "TLS Finished" message sent by the client + to the server is returned. + + - for a server, the SHA256 over the "TLS Finished" message the server expected + the client to send + + Note: this is similar to `tls-unique` as described in RFC5929, but instead + of returning the raw "TLS Finished" message, it returns a SHA256 over such a + message. The reason is that we use the channel ID mainly with WAMP-cryptosign, + which is based on Ed25519, where keys are always 32 bytes. And having a channel ID + which is always 32 bytes (independent of the TLS ciphers/hashfuns in use) allows + use to easily XOR channel IDs with Ed25519 keys and WAMP-cryptosign challenges. + + WARNING: For safe use of this (that is, for safely binding app level authentication + to the underlying transport), you MUST use TLS, and you SHOULD deactivate both + TLS session renegotiation and TLS session resumption. + + References: + + - https://tools.ietf.org/html/rfc5056 + - https://tools.ietf.org/html/rfc5929 + - http://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Connection.get_finished + - http://www.pyopenssl.org/en/stable/api/ssl.html#OpenSSL.SSL.Connection.get_peer_finished """ return self._channel_id @@ -1604,8 +1905,69 @@ def peer_cert(self) -> Dict[str, Any]: See `here `_ for details about the object returned. """ - return self._peer_certificate + return self._peer_cert @peer_cert.setter def peer_cert(self, value: Dict[str, Any]): self._peer_cert = value + + @property + def websocket_protocol(self) -> Optional[str]: + """ + If the underlying connection uses a regular HTTP based WebSocket opening handshake, + the WebSocket subprotocol negotiated, e.g. ``"wamp.2.cbor.batched"``. + """ + return self._websocket_protocol + + @websocket_protocol.setter + def websocket_protocol(self, value: Optional[str]): + self._websocket_protocol = value + + @property + def websocket_extensions_in_use(self) -> Optional[List[str]]: + """ + If the underlying connection uses a regular HTTP based WebSocket opening handshake, the WebSocket extensions + negotiated, e.g. ``["permessage-deflate", "client_no_context_takeover", "client_max_window_bits"]``. + """ + return self._websocket_extensions_in_use + + @websocket_extensions_in_use.setter + def websocket_extensions_in_use(self, value: Optional[List[str]]): + self._websocket_extensions_in_use = value + + @property + def http_headers_received(self) -> Dict[str, Any]: + """ + If the underlying connection uses a regular HTTP based WebSocket opening handshake, + the HTTP request headers as received from the client on this connection. + """ + return self._http_headers_received + + @http_headers_received.setter + def http_headers_received(self, value: Dict[str, Any]): + self._http_headers_received = value + + @property + def http_headers_sent(self) -> Dict[str, Any]: + """ + If the underlying connection uses a regular HTTP based WebSocket opening handshake, + the HTTP response headers as sent from the server on this connection. + """ + return self._http_headers_sent + + @http_headers_sent.setter + def http_headers_sent(self, value: Dict[str, Any]): + self._http_headers_sent = value + + @property + def http_cbtid(self) -> Optional[str]: + """ + If the underlying connection uses a regular HTTP based WebSocket opening handshake, + the HTTP cookie value of the WAMP tracking cookie if any is associated with this + connection. + """ + return self._http_cbtid + + @http_cbtid.setter + def http_cbtid(self, value: Optional[str]): + self._http_cbtid = value diff --git a/autobahn/wamp/websocket.py b/autobahn/wamp/websocket.py index 44f0d7772..d93e219b8 100644 --- a/autobahn/wamp/websocket.py +++ b/autobahn/wamp/websocket.py @@ -31,7 +31,8 @@ from autobahn.websocket import protocol from autobahn.websocket.types import ConnectionDeny, ConnectionRequest, ConnectionResponse -from autobahn.wamp.interfaces import ITransport +from autobahn.wamp.types import TransportDetails +from autobahn.wamp.interfaces import ITransport, ISession from autobahn.wamp.exception import ProtocolError, SerializationError, TransportLost __all__ = ('WampWebSocketServerProtocol', @@ -45,7 +46,8 @@ class WampWebSocketProtocol(object): Base class for WAMP-over-WebSocket transport mixins. """ - _session = None # default; self.session is set in onOpen + _session: Optional[ISession] = None # default; self.session is set in onOpen + _transport_details: Optional[TransportDetails] = None def _bailout(self, code: int, reason: Optional[str] = None): self.log.debug('Failing WAMP-over-WebSocket transport: code={code}, reason="{reason}"', code=code, @@ -138,6 +140,13 @@ def isOpen(self): """ return self._session is not None + @property + def transport_details(self) -> Optional[TransportDetails]: + """ + Implements :func:`autobahn.wamp.interfaces.ITransport.transport_details` + """ + return self._transport_details + def close(self): """ Implements :func:`autobahn.wamp.interfaces.ITransport.close` diff --git a/autobahn/websocket/protocol.py b/autobahn/websocket/protocol.py index fd8237796..aa066cbe2 100755 --- a/autobahn/websocket/protocol.py +++ b/autobahn/websocket/protocol.py @@ -48,7 +48,7 @@ from autobahn.websocket.types import ConnectingRequest, ConnectionRequest, ConnectionResponse, ConnectionDeny from autobahn.wamp.types import TransportDetails -from autobahn.util import Stopwatch, wildcards2patterns, encode_truncate, hltype +from autobahn.util import Stopwatch, wildcards2patterns, encode_truncate, hltype, hlval from autobahn.util import _LazyHexFormatter from autobahn.util import ObservableMixin from autobahn.websocket.utf8validator import Utf8Validator @@ -370,6 +370,7 @@ class WebSocketProtocol(ObservableMixin): * :class:`autobahn.websocket.interfaces.IWebSocketChannelStreamingApi` """ + # set in WebSocketAdapterProtocol.connectionMade (Twisted) and FIXME (asyncio) peer = '' SUPPORTED_SPEC_VERSIONS = [10, 11, 12, 13, 14, 15, 16, 17, 18] @@ -558,10 +559,16 @@ def __init__(self): "message", # like onMessage (takes: payload, is_binary=) ]) - self._transport_details = None + # set in + # * autobahn.twisted.websocket.WebSocketAdapterProtocol.connectionMade + # * autobahn.asyncio.websocket.WebSocketAdapterProtocol. + self._transport_details: Optional[TransportDetails] = TransportDetails() @property def transport_details(self) -> Optional[TransportDetails]: + """ + Implements :class:`autobahn.wamp.interfaces.ITransport.transport_details`. + """ return self._transport_details def onOpen(self): @@ -1357,6 +1364,10 @@ def sendData(self, data, sync=False, chopsize=None): self._trigger() else: self.transport.write(data) + self.log.debug('{func} sent {data_len} bytes for peer {peer}', + func=hltype(self.sendData), + peer=hlval(self.peer), + data_len=hlval(len(data))) if self.state == WebSocketProtocol.STATE_OPEN: self.trafficStats.outgoingOctetsWireLevel += len(data) @@ -2552,6 +2563,7 @@ def onConnect(self, request: ConnectionRequest) -> Union[Optional[str], Tuple[Op * ``(str, dict)``: a pair of subprotocol accepted and HTTP headers to send to the client. You can also return a Deferred/Future that resolves/rejects to the above. """ + self.log.info('{func}: request={request}', func=hltype(self.onConnect), request=request) return None def _connectionMade(self): @@ -2561,9 +2573,10 @@ def _connectionMade(self): handshake. When overriding in derived class, make sure to call this base class implementation *before* your code. """ + self.log.debug('{func}: connection accepted from peer {peer}', + func=hltype(self._connectionMade), peer=self.peer) WebSocketProtocol._connectionMade(self) self.factory.countConnections += 1 - self.log.debug("connection accepted from peer {peer}", peer=self.peer) def _connectionLost(self, reason): """ @@ -2572,6 +2585,8 @@ def _connectionLost(self, reason): When overriding in derived class, make sure to call this base class implementation *after* your code. """ + self.log.info('{func}: connection lost to peer {peer}: reason={reason}', + func=hltype(self._connectionLost), peer=self.peer, reason=hlval(reason)) WebSocketProtocol._connectionLost(self, reason) self.factory.countConnections -= 1 @@ -2586,6 +2601,8 @@ def processHandshake(self): # end_of_header = self.data.find(b"\x0d\x0a\x0d\x0a") if end_of_header >= 0: + self.log.info('{func} found end of HTTP request header at byte {end_of_header}', + func=hltype(self.processHandshake), end_of_header=hlval(end_of_header)) self.http_request_data = self.data[:end_of_header + 4] self.log.debug( @@ -2892,6 +2909,7 @@ def processHandshake(self): # - return None to continue with no subprotocol # - return a pair (subprotocol, headers) # - raise a ConnectionDeny to dismiss the client + f = txaio.as_future(self.onConnect, request) def forward_error(err): @@ -3459,8 +3477,9 @@ def _connectionMade(self): When overriding in derived class, make sure to call this base class implementation _before_ your code. """ + self.log.debug('{func}: connection accepted from peer {peer}', + func=hltype(self._connectionMade), peer=self.peer) WebSocketProtocol._connectionMade(self) - self.log.debug('{meth}: connection to {peer} established', meth=hltype(self._connectionMade), peer=self.peer) if not self.factory.isServer and self.factory.proxy is not None: # start by doing a HTTP/CONNECT for explicit proxies @@ -3476,6 +3495,8 @@ def _connectionLost(self, reason): When overriding in derived class, make sure to call this base class implementation _after_ your code. """ + self.log.info('{func}: connection lost to peer {peer}: reason={reason}', + func=hltype(self._connectionLost), peer=self.peer, reason=hlval(reason)) WebSocketProtocol._connectionLost(self, reason) def startProxyConnect(self): @@ -3583,9 +3604,6 @@ def startHandshake(self): """ Start WebSocket opening handshake. """ - # extract framework-specific transport information - self._transport_details = self._create_transport_details() - self.log.debug('{meth}: starting handshake with transport_details=\n{transport_details}', meth=hltype(self.startHandshake), transport_details=pformat(self._transport_details.marshal())) @@ -3618,10 +3636,12 @@ def got_options(request_options): def options_failed(fail): self.log.error( - "onConnecting failed: {fail}", + "{meth} onConnecting failed: {fail}", fail=fail, + meth=hltype(self.startHandshake), ) self.dropConnection(abort=False) + # return fail return None txaio.add_callbacks(options_d, got_options, options_failed) return options_d diff --git a/autobahn/websocket/test/__init__.py b/autobahn/websocket/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/websocket/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/websocket/test/test_websocket_frame.py b/autobahn/websocket/test/test_websocket_frame.py index 222d972a9..bbf24b5c1 100644 --- a/autobahn/websocket/test/test_websocket_frame.py +++ b/autobahn/websocket/test/test_websocket_frame.py @@ -27,6 +27,7 @@ import os import struct + if os.environ.get('USE_TWISTED', False): from twisted.trial import unittest from twisted.internet.address import IPv4Address diff --git a/autobahn/websocket/test/test_websocket_protocol.py b/autobahn/websocket/test/test_websocket_protocol.py index 9ce7ed7ac..15c17b41a 100644 --- a/autobahn/websocket/test/test_websocket_protocol.py +++ b/autobahn/websocket/test/test_websocket_protocol.py @@ -30,6 +30,8 @@ from base64 import b64encode from unittest.mock import Mock +import txaio + from autobahn.websocket.protocol import WebSocketServerProtocol from autobahn.websocket.protocol import WebSocketServerFactory from autobahn.websocket.protocol import WebSocketClientProtocol @@ -37,8 +39,7 @@ from autobahn.websocket.protocol import WebSocketProtocol from autobahn.websocket.types import ConnectingRequest from autobahn.testutil import FakeTransport - -import txaio +from autobahn.wamp.types import TransportDetails class WebSocketClientProtocolTests(unittest.TestCase): @@ -49,7 +50,7 @@ def setUp(self): p = WebSocketClientProtocol() p.factory = f p.transport = t - p._create_transport_details = Mock() + p._transport_details = TransportDetails() p._connectionMade() p.state = p.STATE_OPEN diff --git a/autobahn/xbr/_config.py b/autobahn/xbr/_config.py index 9ed7a4612..f7cd078ea 100644 --- a/autobahn/xbr/_config.py +++ b/autobahn/xbr/_config.py @@ -349,8 +349,8 @@ def save(self, password: Optional[str] = None): with open(self._config_path, 'wb') as fp2: fp2.write(data) - self.log.info('configuration with {sections} sections, {bytes_written} bytes written to {written_to}', - sections=written, bytes_written=len(data), written_to=self._config_path) + self.log.debug('configuration with {sections} sections, {bytes_written} bytes written to {written_to}', + sections=written, bytes_written=len(data), written_to=self._config_path) return len(data) diff --git a/autobahn/xbr/test/__init__.py b/autobahn/xbr/test/__init__.py new file mode 100644 index 000000000..89caae255 --- /dev/null +++ b/autobahn/xbr/test/__init__.py @@ -0,0 +1,25 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) Crossbar.io Technologies 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. +# +############################################################################### diff --git a/autobahn/xbr/test/test_xbr_config.py b/autobahn/xbr/test/test_xbr_config.py index 0621d00d7..bfacd67b2 100644 --- a/autobahn/xbr/test/test_xbr_config.py +++ b/autobahn/xbr/test/test_xbr_config.py @@ -26,15 +26,8 @@ import os import unittest -from autobahn.xbr import HAS_XBR - -if os.environ.get('USE_TWISTED', False): - import txaio - txaio.use_twisted() -if os.environ.get('USE_ASYNCIO', False): - import txaio - txaio.use_asyncio() +from autobahn.xbr import HAS_XBR if HAS_XBR: from autobahn.xbr import Profile, UserConfig @@ -64,8 +57,6 @@ def test_load_home(self): c = UserConfig(config_path) c.load() self.assertIn(self.PROFILE_NAME, c.profiles) - else: - print('no profile file "{}" found'.format(config_path)) def test_write_default_config(self): config_dir = os.path.expanduser(self.DOTDIR) diff --git a/pytest.ini b/pytest.ini index 09dc540b0..c7a5e9b93 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,7 @@ # https://docs.pytest.org/en/stable/reference.html#ini-options-ref [pytest] -minversion = 6.2 +minversion = 7.1 addopts = -ra -q testpaths = test diff --git a/tox.ini b/tox.ini index 7edd48269..40df9429c 100644 --- a/tox.ini +++ b/tox.ini @@ -106,7 +106,7 @@ commands = # sh -c "which python && which pip && python -V" # pip install -q --no-cache --ignore-installed --force-reinstall .[twisted,asyncio,compress,serialization,encryption,scram,xbr] - asyncio: pytest -s -v --ignore=./autobahn/twisted ./autobahn + asyncio: pytest -s -v -rfP --ignore=./autobahn/twisted ./autobahn tw1910,tw203,tw221: trial ./autobahn twtrunk: python -m twisted.trial ./autobahn