This commit is contained in:
2024-12-17 14:36:15 -08:00
parent b2dbf46d28
commit 06d106de53
17731 changed files with 3037186 additions and 144 deletions

View File

@@ -0,0 +1,238 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import unittest
from unittest.mock import Mock
import platform
import re
import json
import binascii
from autobahn.wamp import auth
from autobahn.wamp import types
# these test vectors are all for HMAC-SHA1
PBKDF2_TEST_VECTORS = [
# From RFC 6070
(b'password', b'salt', 1, 20, '0c60c80f961f0e71f3a9b524af6012062fe037a6'),
(b'password', b'salt', 2, 20, 'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957'),
# From Crypt-PBKDF2
(b'password', b'ATHENA.MIT.EDUraeburn', 1, 16, 'cdedb5281bb2f801565a1122b2563515'),
(b'password', b'ATHENA.MIT.EDUraeburn', 1, 32, 'cdedb5281bb2f801565a1122b25635150ad1f7a04bb9f3a333ecc0e2e1f70837'),
(b'password', b'ATHENA.MIT.EDUraeburn', 2, 16, '01dbee7f4a9e243e988b62c73cda935d'),
(b'password', b'ATHENA.MIT.EDUraeburn', 2, 32, '01dbee7f4a9e243e988b62c73cda935da05378b93244ec8f48a99e61ad799d86'),
(b'password', b'ATHENA.MIT.EDUraeburn', 1200, 32, '5c08eb61fdf71e4e4ec3cf6ba1f5512ba7e52ddbc5e5142f708a31e2e62b1e13'),
(b'X' * 64, b'pass phrase equals block size', 1200, 32, '139c30c0966bc32ba55fdbf212530ac9c5ec59f1a452f5cc9ad940fea0598ed1'),
(b'X' * 65, b'pass phrase exceeds block size', 1200, 32, '9ccad6d468770cd51b10e6a68721be611a8b4d282601db3b36be9246915ec82a'),
]
if platform.python_implementation() != 'PyPy':
# the following fails on PyPy: "RuntimeError: maximum recursion depth exceeded"
PBKDF2_TEST_VECTORS.extend(
[
# From RFC 6070
(b'password', b'salt', 4096, 20, '4b007901b765489abead49d926f721d065a429c1'),
(b'passwordPASSWORDpassword', b'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 25, '3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038'),
(b'pass\x00word', b'sa\x00lt', 4096, 16, '56fa6aa75548099dcc37d7f03425e0c3'),
# This one is from the RFC but it just takes for ages
# (b'password', b'salt', 16777216, 20, 'eefe3d61cd4da4e4e9945b3d6ba2158c2634e984'),
]
)
class TestWampAuthHelpers(unittest.TestCase):
def test_pbkdf2(self):
for tv in PBKDF2_TEST_VECTORS:
result = auth.pbkdf2(tv[0], tv[1], tv[2], tv[3], 'sha1')
self.assertEqual(type(result), bytes)
self.assertEqual(binascii.hexlify(result).decode('ascii'), tv[4])
def test_generate_totp_secret_default(self):
secret = auth.generate_totp_secret()
self.assertEqual(type(secret), str)
self.assertEqual(len(secret), 10 * 8 / 5)
def test_generate_totp_secret_length(self):
for length in [5, 10, 20, 30, 40, 50]:
secret = auth.generate_totp_secret(length)
self.assertEqual(type(secret), str)
self.assertEqual(len(secret), length * 8 / 5)
def test_compute_totp(self):
pat = re.compile(r"\d\d\d\d\d\d")
secret = "MFRGGZDFMZTWQ2LK"
signature = auth.compute_totp(secret)
self.assertEqual(type(signature), str)
self.assertTrue(pat.match(signature) is not None)
def test_compute_totp_offset(self):
pat = re.compile(r"\d\d\d\d\d\d")
secret = "MFRGGZDFMZTWQ2LK"
for offset in range(-10, 10):
signature = auth.compute_totp(secret, offset)
self.assertEqual(type(signature), str)
self.assertTrue(pat.match(signature) is not None)
def test_derive_key(self):
secret = 'L3L1YUE8Txlw'
salt = 'salt123'
key = auth.derive_key(secret.encode('utf8'), salt.encode('utf8'))
self.assertEqual(type(key), bytes)
self.assertEqual(key, b"qzcdsr9uu/L5hnss3kjNTRe490ETgA70ZBaB5rvnJ5Y=")
def test_generate_wcs_default(self):
secret = auth.generate_wcs()
self.assertEqual(type(secret), bytes)
self.assertEqual(len(secret), 14)
def test_generate_wcs_length(self):
for length in [5, 10, 20, 30, 40, 50]:
secret = auth.generate_wcs(length)
self.assertEqual(type(secret), bytes)
self.assertEqual(len(secret), length)
def test_compute_wcs(self):
secret = 'L3L1YUE8Txlw'
challenge = json.dumps([1, 2, 3], ensure_ascii=False).encode('utf8')
signature = auth.compute_wcs(secret.encode('utf8'), challenge)
self.assertEqual(type(signature), bytes)
self.assertEqual(signature, b"1njQtmmeYO41N5EWEzD2kAjjEKRZ5kPZt/TzpYXOzR0=")
@unittest.skipIf(not auth.HAS_ARGON, 'no Argon2 library')
class TestScram(unittest.TestCase):
def test_argon2id_static(self):
# re-generate from the official argon2 tools:
# echo -n "p4ssw0rd" | argon2 '1234567890abcdef' -id -t 32 -m 9 -p 1 -l 32
expected = binascii.unhexlify('ee4a8acf9d5958354fb79a95ae20692d05e42591ba49fae85eb6700e8b0ed293')
raw_hash = auth._hash_argon2id13_secret(
b'p4ssw0rd',
binascii.b2a_base64(b'1234567890abcdef'), # ours takes base64-encoded salt
32, # this is WAY TOO SMALL; for production, use 4096 or higher
512, # note that the argon2 utility takes a "power of 2", so "-m 9" above == 512
)
decoded_hash = binascii.a2b_base64(raw_hash + b'==\n')
self.assertEqual(expected, decoded_hash)
def test_pbkdf2_static(self):
expected = binascii.unhexlify('f6991a28c75f43751e0d75499fd7b8649f659118ddc1d61cee5883af547d15f5')
# 8 iterations is WAY TOO FEW for production; this is a test
raw_hash = auth._hash_pbkdf2_secret(b'p4ssw0rd', b'1234567890abcdef', 8)
self.assertEqual(raw_hash, expected)
def test_basic(self):
scram = auth.AuthScram(
nonce='1234567890abcdef',
kdf='argon2id13',
salt=binascii.b2a_hex(b'1234567890abcdef').decode('ascii'),
iterations=32, # far too few; use 4096 or more for production
memory=512,
password='p4ssw0rd',
authid='username',
)
# thought: if we could import crossbar code here, we could
# test the "other side" of this with fewer mocks
# (i.e. hard-coding the client nonce)
scram._client_nonce = binascii.b2a_hex(b'1234567890abcdef').decode('ascii')
self.assertEqual(
{'nonce': '31323334353637383930616263646566'},
scram.authextra,
)
challenge = types.Challenge('scram', {
'nonce': '1234567890abcdeffedcba0987654321',
'kdf': 'argon2id-13',
'salt': binascii.b2a_hex(b'1234567890abcdef').decode('ascii'),
'iterations': 32,
'memory': 512,
})
reply = scram.on_challenge(Mock(), challenge)
self.assertEqual(
b'f5r3loERzGVSuimE+lvO0bWna2zyswBo0HrZkaaEy38=',
reply,
)
authextra = dict(
scram_server_signature=b'f5r3loERzGVSuimE+lvO0bWna2zyswBo0HrZkaaEy38=',
)
scram.on_welcome(Mock(), authextra)
def test_no_memory_arg(self):
scram = auth.AuthScram(
nonce='1234567890abcdef',
kdf='argon2id13',
salt=binascii.b2a_hex(b'1234567890abcdef').decode('ascii'),
iterations=4096,
memory=512,
password='p4ssw0rd',
authid='username',
)
scram.authextra
with self.assertRaises(ValueError) as ctx:
challenge = types.Challenge('scram', {
'nonce': '1234567890abcdeffedcba0987654321',
'kdf': 'argon2id-13',
'salt': binascii.b2a_hex(b'1234567890abcdef'),
'iterations': 4096,
# no 'memory' key
})
scram.on_challenge(Mock(), challenge)
self.assertIn(
"requires 'memory' parameter",
str(ctx.exception)
)
def test_unknown_arg(self):
scram = auth.AuthScram(
nonce='1234567890abcdef',
kdf='argon2id13',
salt=binascii.b2a_hex(b'1234567890abcdef'),
iterations=4096,
memory=512,
password='p4ssw0rd',
authid='username',
)
scram.authextra
with self.assertRaises(RuntimeError) as ctx:
challenge = types.Challenge('scram', {
'nonce': '1234567890abcdeffedcba0987654321',
'kdf': 'argon2id-13',
'salt': binascii.b2a_hex(b'1234567890abcdef'),
'iterations': 4096,
'memory': 512,
'an_invalid_key': None
})
scram.on_challenge(Mock(), challenge)
self.assertIn("an_invalid_key", str(ctx.exception))

View File

@@ -0,0 +1,219 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
if os.environ.get('USE_TWISTED', False):
from autobahn.twisted.util import sleep
from autobahn.twisted import wamp
from twisted.trial import unittest
from twisted.internet import defer
from twisted.application import service
class CaseComponent(wamp.ApplicationSession):
"""
Application code goes here. This is an example component that calls
a remote procedure on a WAMP peer, subscribes to a topic to receive
events, and then stops the world after some events.
"""
def __init__(self, config):
wamp.ApplicationSession.__init__(self, config)
self.test = config.extra['test']
self.stop = False
self._logline = 1
self.finished = False
def log(self, *args):
if len(args) > 1:
sargs = ", ".join(str(s) for s in args)
elif len(args) == 1:
sargs = args[0]
else:
sargs = "-"
msg = '= : {0:>3} : {1:<20} : {2}'.format(self._logline, self.__class__.__name__, sargs)
self._logline += 1
print(msg)
def finish(self):
if not self.finished:
self.test.deferred.callback(None)
self.finished = True
else:
print("already finished")
class Case1_Backend(CaseComponent):
@defer.inlineCallbacks
def onJoin(self, details):
self.log("joined")
def add2(x, y):
self.log("add2 invoked: {0}, {1}".format(x, y))
return x + y
yield self.register(add2, 'com.mathservice.add2')
self.log("add2 registered")
self.finish()
class Case1_Frontend(CaseComponent):
@defer.inlineCallbacks
def onJoin(self, details):
self.log("joined")
try:
res = yield self.call('com.mathservice.add2', 2, 3)
except Exception as e:
self.log("call error: {0}".format(e))
else:
self.log("call result: {0}".format(res))
self.finish()
class Case2_Backend(CaseComponent):
@defer.inlineCallbacks
def onJoin(self, details):
self.log("joined")
def ping():
self.log("ping() is invoked")
return
def add2(a, b):
self.log("add2() is invoked", a, b)
return a + b
def stars(nick="somebody", stars=0):
self.log("stars() is invoked", nick, stars)
return "{0} starred {1}x".format(nick, stars)
def orders(product, limit=5):
self.log("orders() is invoked", product, limit)
return ["Product {0}".format(i) for i in range(50)][:limit]
def arglen(*args, **kwargs):
self.log("arglen() is invoked", args, kwargs)
return [len(args), len(kwargs)]
yield self.register(ping, 'com.arguments.ping')
yield self.register(add2, 'com.arguments.add2')
yield self.register(stars, 'com.arguments.stars')
yield self.register(orders, 'com.arguments.orders')
yield self.register(arglen, 'com.arguments.arglen')
self.log("procedures registered")
class Case2_Frontend(CaseComponent):
@defer.inlineCallbacks
def onJoin(self, details):
self.log("joined")
yield sleep(1)
yield self.call('com.arguments.ping')
self.log("Pinged!")
res = yield self.call('com.arguments.add2', 2, 3)
self.log("Add2: {0}".format(res))
starred = yield self.call('com.arguments.stars')
self.log("Starred 1: {0}".format(starred))
starred = yield self.call('com.arguments.stars', nick='Homer')
self.log("Starred 2: {0}".format(starred))
starred = yield self.call('com.arguments.stars', stars=5)
self.log("Starred 3: {0}".format(starred))
starred = yield self.call('com.arguments.stars', nick='Homer', stars=5)
self.log("Starred 4: {0}".format(starred))
orders = yield self.call('com.arguments.orders', 'coffee')
self.log("Orders 1: {0}".format(orders))
orders = yield self.call('com.arguments.orders', 'coffee', limit=10)
self.log("Orders 2: {0}".format(orders))
arglengths = yield self.call('com.arguments.arglen')
self.log("Arglen 1: {0}".format(arglengths))
arglengths = yield self.call('com.arguments.arglen', 1, 2, 3)
self.log("Arglen 1: {0}".format(arglengths))
arglengths = yield self.call('com.arguments.arglen', a=1, b=2, c=3)
self.log("Arglen 2: {0}".format(arglengths))
arglengths = yield self.call('com.arguments.arglen', 1, 2, 3, a=1, b=2, c=3)
self.log("Arglen 3: {0}".format(arglengths))
self.log("finishing")
self.finish()
class TestRpc(unittest.TestCase):
if os.environ.get("WAMP_ROUTER_URL") is None:
skip = ("Please provide WAMP_ROUTER_URL environment with url to "
"WAMP router to run WAMP integration tests")
def setUp(self):
self.url = os.environ.get("WAMP_ROUTER_URL")
self.realm = "realm1"
@defer.inlineCallbacks
def runOneTest(self, components):
self.deferred = defer.Deferred()
app = service.MultiService()
for component in components:
c = wamp.Service(
url=self.url,
extra=dict(test=self),
realm=self.realm,
make=component,
)
c.setServiceParent(app)
app.startService()
yield self.deferred
app.stopService()
@defer.inlineCallbacks
def test_case1(self):
yield self.runOneTest([Case1_Backend, Case1_Frontend])
@defer.inlineCallbacks
def test_case2(self):
yield self.runOneTest([Case2_Backend, Case2_Frontend])

View File

@@ -0,0 +1,154 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import sys
import unittest.mock as mock
import pytest
import txaio
if os.environ.get('USE_ASYNCIO', False):
from autobahn.asyncio.component import Component
@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires Python 3.5+")
@pytest.mark.asyncio(forbid_global_loop=True)
async def test_asyncio_component(event_loop):
orig_loop = txaio.config.loop
txaio.config.loop = event_loop
comp = Component(
transports=[
{
"url": "ws://localhost:12/bogus",
"max_retries": 1,
"max_retry_delay": 0.1,
}
]
)
# if having trouble, try starting some logging (and use
# "py.test -s" to get real-time output)
# txaio.start_logging(level="debug")
f = comp.start(loop=event_loop)
txaio.config.loop = event_loop
finished = txaio.create_future()
def fail():
finished.set_exception(AssertionError("timed out"))
txaio.config.loop = orig_loop
txaio.call_later(4.0, fail)
def done(f):
try:
f.result()
finished.set_exception(AssertionError("should get an error"))
except RuntimeError as e:
if 'Exhausted all transport connect attempts' not in str(e):
finished.set_exception(AssertionError("wrong exception caught"))
finished.set_result(None)
txaio.config.loop = orig_loop
assert comp._done_f is None
f.add_done_callback(done)
await finished
@pytest.mark.skipif(sys.version_info < (3, 5), reason="requires Python 3.5+")
@pytest.mark.asyncio(forbid_global_loop=True)
async def test_asyncio_component_404(event_loop):
"""
If something connects but then gets aborted, it should still try
to re-connect (in real cases this could be e.g. wrong path,
TLS failure, WebSocket handshake failure, etc)
"""
orig_loop = txaio.config.loop
txaio.config.loop = event_loop
class FakeTransport(object):
def close(self):
pass
def write(self, data):
pass
fake_transport = FakeTransport()
actual_protocol = [None] # set in a closure below
def create_connection(protocol_factory=None, server_hostname=None, host=None, port=None, ssl=False):
if actual_protocol[0] is None:
protocol = protocol_factory()
actual_protocol[0] = protocol
protocol.connection_made(fake_transport)
return txaio.create_future_success((fake_transport, protocol))
else:
return txaio.create_future_error(RuntimeError("second connection fails completely"))
with mock.patch.object(event_loop, 'create_connection', create_connection):
event_loop.create_connection = create_connection
comp = Component(
transports=[
{
"url": "ws://localhost:12/bogus",
"max_retries": 1,
"max_retry_delay": 0.1,
}
]
)
# if having trouble, try starting some logging (and use
# "py.test -s" to get real-time output)
# txaio.start_logging(level="debug")
f = comp.start(loop=event_loop)
txaio.config.loop = event_loop
# now that we've started connecting, we *should* be able
# to connetion_lost our transport .. but we do a
# call-later to ensure we're after the setup stuff in the
# event-loop (because asyncio doesn't synchronously
# process already-completed Futures like Twisted does)
def nuke_transport():
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()
def fail():
finished.set_exception(AssertionError("timed out"))
txaio.config.loop = orig_loop
txaio.call_later(1.0, fail)
def done(f):
try:
f.result()
finished.set_exception(AssertionError("should get an error"))
except RuntimeError as e:
if 'Exhausted all transport connect attempts' not in str(e):
finished.set_exception(AssertionError("wrong exception caught"))
finished.set_result(None)
txaio.config.loop = orig_loop
f.add_done_callback(done)
await finished

View File

@@ -0,0 +1,37 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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 import cryptobox
import unittest
@unittest.skipIf(not cryptobox.HAS_CRYPTOBOX, 'no cryptobox support present')
class TestCryptoBox(unittest.TestCase):
def test_create_keyring(self):
kr = cryptobox.KeyRing()
assert kr

View File

@@ -0,0 +1,295 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import hashlib
import binascii
import unittest
from unittest.mock import Mock
import txaio
if os.environ.get('USE_TWISTED', None):
txaio.use_twisted()
elif os.environ.get('USE_ASYNCIO', None):
txaio.use_asyncio()
else:
raise RuntimeError('need either USE_TWISTED=1 or USE_ASYNCIO=1')
from autobahn.wamp import types
from autobahn.wamp.auth import create_authenticator
from autobahn.wamp.cryptosign import _makepad, HAS_CRYPTOSIGN, CryptosignAuthextra
if HAS_CRYPTOSIGN:
from autobahn.wamp.cryptosign import CryptosignKey
from nacl.encoding import HexEncoder
import tempfile
keybody = '''-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAa38i/4dNWFuZN/72QAJbyOwZvkUyML/u2b2B1uW4RbQAAAJj4FLyB+BS8
gQAAAAtzc2gtZWQyNTUxOQAAACAa38i/4dNWFuZN/72QAJbyOwZvkUyML/u2b2B1uW4RbQ
AAAEBNV9l6aPVVaWYgpthJwM5YJWhRjXKet1PcfHMt4oBFEBrfyL/h01YW5k3/vZAAlvI7
Bm+RTIwv+7ZvYHW5bhFtAAAAFXNvbWV1c2VyQGZ1bmt0aGF0LmNvbQ==
-----END OPENSSH PRIVATE KEY-----'''
pubkey = '''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJVp3hjHwIQyEladzd8mFcf0YSXcmyKS3qMLB7VqTQKm someuser@example.com
'''
# valid test vectors for WAMP-cryptosign signature testing
test_vectors_1 = [
# _WITHOUT_ channel_id
{
'channel_id': None,
'private_key': '4d57d97a68f555696620a6d849c0ce582568518d729eb753dc7c732de2804510',
'challenge': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'signature': 'b32675b221f08593213737bef8240e7c15228b07028e19595294678c90d11c0cae80a357331bfc5cc9fb71081464e6e75013517c2cf067ad566a6b7b728e5d03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
},
{
'channel_id': None,
'private_key': 'd511fe78e23934b3dadb52fcd022974b80bd92bccc7c5cf404e46cc0a8a2f5cd',
'challenge': 'b26c1f87c13fc1da14997f1b5a71995dff8fbe0a62fae8473c7bdbd05bfb607d',
'signature': 'd4209ad10d5aff6bfbc009d7e924795de138a63515efc7afc6b01b7fe5201372190374886a70207b042294af5bd64ce725cd8dceb344e6d11c09d1aaaf4d660fb26c1f87c13fc1da14997f1b5a71995dff8fbe0a62fae8473c7bdbd05bfb607d'
},
{
'channel_id': None,
'private_key': '6e1fde9cf9e2359a87420b65a87dc0c66136e66945196ba2475990d8a0c3a25b',
'challenge': 'b05e6b8ad4d69abf74aa3be3c0ee40ae07d66e1895b9ab09285a2f1192d562d2',
'signature': '7beb282184baadd08f166f16dd683b39cab53816ed81e6955def951cb2ddad1ec184e206746fd82bda075af03711d3d5658fc84a76196b0fa8d1ebc92ef9f30bb05e6b8ad4d69abf74aa3be3c0ee40ae07d66e1895b9ab09285a2f1192d562d2'
},
# _WITH_ channel_id
{
'channel_id': '62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff',
'private_key': '4d57d97a68f555696620a6d849c0ce582568518d729eb753dc7c732de2804510',
'challenge': 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
'signature': '9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00'
},
{
'channel_id': '62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff',
'private_key': 'd511fe78e23934b3dadb52fcd022974b80bd92bccc7c5cf404e46cc0a8a2f5cd',
'challenge': 'b26c1f87c13fc1da14997f1b5a71995dff8fbe0a62fae8473c7bdbd05bfb607d',
'signature': '305aaa3ac25e98f651427688b3fc43fe7d8a68a7ec1d7d61c61517c519bd4a427c3015599d83ca28b4c652333920223844ef0725eb5dc2febfd6af7677b73f01d0852a29b460fc92ec943242ac638a053bbacc200512b18b30d15083cbdc9282'
},
{
'channel_id': '62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff',
'private_key': '6e1fde9cf9e2359a87420b65a87dc0c66136e66945196ba2475990d8a0c3a25b',
'challenge': 'b05e6b8ad4d69abf74aa3be3c0ee40ae07d66e1895b9ab09285a2f1192d562d2',
'signature': 'ee3c7644fd8070532bc1fde3d70d742267da545d8c8f03e63bda63f1ad4214f4d2c4bfdb4eb9526def42deeb7e31602a6ff99eba893e0a4ad4d45892ca75e608d2b75e24a189a7f78ca776ba36fc53f6c3e31c32f251f2c524f0a44202f2902d'
},
]
class TestSigVectors(unittest.TestCase):
def test_vectors(self):
session = Mock()
for testvec in test_vectors_1:
# setup fake transport details including fake channel_id
if testvec['channel_id']:
channel_id = binascii.a2b_hex(testvec['channel_id'])
channel_id_type = 'tls-unique'
session._transport.transport_details = types.TransportDetails(channel_id={'tls-unique': channel_id})
else:
channel_id = None
channel_id_type = None
session._transport.transport_details = types.TransportDetails(channel_id=None)
# private signing key (the seed for it)
private_key = CryptosignKey.from_bytes(binascii.a2b_hex(testvec['private_key']))
# the fake challenge we've received
challenge = types.Challenge("cryptosign", dict(challenge=testvec['challenge']))
# ok, now sign the challenge
f_signed = private_key.sign_challenge(challenge,
channel_id=channel_id,
channel_id_type=channel_id_type)
def success(signed):
# the signature returned is a Hex encoded string
self.assertTrue(type(signed) == str)
# we return the concatenation of the signature and the message signed (96 bytes)
self.assertEqual(
192,
len(signed),
)
# must match the expected value in our test vector
self.assertEqual(
testvec['signature'],
signed,
)
def failed(err):
self.fail(str(err))
txaio.add_callbacks(f_signed, success, failed)
class TestAuth(unittest.TestCase):
def setUp(self):
self.key = CryptosignKey.from_ssh_bytes(keybody)
self.privkey_hex = self.key._key.encode(encoder=HexEncoder)
# all tests here fake the use of channel_id_type='tls-unique' with the following channel_id
m = hashlib.sha256()
m.update("some TLS message".encode())
# 62e935ae755f3d48f80d4d59f6121358c435722a67e859cc0caa8b539027f2ff
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.transport_details = self.transport_details
challenge = types.Challenge("cryptosign", dict(challenge="ff" * 32))
f_signed = self.key.sign_challenge(challenge,
channel_id=self.transport_details.channel_id['tls-unique'],
channel_id_type='tls-unique')
def success(signed):
self.assertEqual(
192,
len(signed),
)
self.assertEqual(
'9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00',
signed,
)
def failed(err):
self.fail(str(err))
txaio.add_callbacks(f_signed, success, failed)
def test_authenticator(self):
authenticator = create_authenticator(
"cryptosign",
authid="someone",
authextra={'channel_binding': 'tls-unique'},
privkey=self.privkey_hex,
)
session = Mock()
session._transport.transport_details = self.transport_details
challenge = types.Challenge("cryptosign", dict(challenge="ff" * 32))
f_reply = authenticator.on_challenge(session, challenge)
def success(reply):
self.assertEqual(
reply,
'9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00',
)
def failed(err):
self.fail(str(err))
txaio.add_callbacks(f_reply, success, failed)
class TestKey(unittest.TestCase):
def test_pad(self):
self.assertEqual(_makepad(0), b'')
self.assertEqual(_makepad(2), b'\x01\x02')
self.assertEqual(_makepad(3), b'\x01\x02\x03')
self.assertEqual(_makepad(30), b'\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e')
self.assertEqual(binascii.b2a_hex(_makepad(30)).decode(), '0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e')
def test_key(self):
with tempfile.NamedTemporaryFile('w+t') as fp:
fp.write(keybody)
fp.seek(0)
key = CryptosignKey.from_ssh_file(fp.name)
self.assertEqual(key.public_key(), '1adfc8bfe1d35616e64dffbd900096f23b066f914c8c2ffbb66f6075b96e116d')
def test_pubkey(self):
with tempfile.NamedTemporaryFile('w+t') as fp:
fp.write(pubkey)
fp.seek(0)
key = CryptosignKey.from_ssh_file(fp.name)
self.assertEqual(key.public_key(binary=False), '9569de18c7c0843212569dcddf2615c7f46125dc9b2292dea30b07b56a4d02a6')
self.assertEqual(key.comment, 'someuser@example.com')
class TestAuthExtra(unittest.TestCase):
def test_default_ctor(self):
ae = CryptosignAuthextra()
self.assertEqual(ae.marshal(), {})
def test_ctor(self):
ae1 = CryptosignAuthextra(pubkey=b'\xff' * 32)
self.assertEqual(ae1.marshal(), {
'pubkey': 'ff' * 32
})
ae1 = CryptosignAuthextra(pubkey=b'\xff' * 32, bandwidth=200)
self.assertEqual(ae1.marshal(), {
'pubkey': 'ff' * 32,
'reservation': {
'bandwidth': 200
}
})
def test_parse(self):
data_original = {
'pubkey': '9019a424b040859c108edee02e64c1dcb32b253686d7b5db56c306e9bdb2fe7e',
'challenge': 'fe81c84e94a75a357c259d6b37361e43966a45f57dff181bb61b2f91a0f4ac88',
'channel_binding': 'tls-unique',
'channel_id': '2e642bf991f48ece9133a0a32d15550921dda12bfebfbc941571d4b2960540bc',
'trustroot': '0xe78ea2fE1533D4beD9A10d91934e109A130D0ad8',
'reservation': {
'chain_id': 999,
'block_no': 123456789,
'realm': '0x163D58cE482560B7826b4612f40aa2A7d53310C4',
'delegate': '0x72b3486d38E9f49215b487CeAaDF27D6acf22115',
'seeder': '0x52d66f36A7927cF9612e1b40bD6549d08E0513Ff',
'bandwidth': 200
},
'signature': '747763c69394270603f64af5be3f8256a14b41ff51027e583ee81db9f1f15a01cc8e55218a76139f26dbaaa78d8a537d80d248b3fc6245ecf4602cc5fbb0f6452e',
}
ae1 = CryptosignAuthextra.parse(data_original)
data_marshalled = ae1.marshal()
# FIXME: marshal check-summed eth addresses
data_original['trustroot'] = data_original['trustroot'].lower()
for k in ['realm', 'delegate', 'seeder']:
data_original['reservation'][k] = data_original['reservation'][k].lower()
self.assertEqual(data_marshalled, data_original)

View File

@@ -0,0 +1,50 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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 unittest import TestCase
from autobahn.wamp.exception import ApplicationError
class ApplicationErrorTestCase(TestCase):
def test_unicode_str(self):
"""
Unicode arguments in ApplicationError will not raise an exception when
str()'d.
"""
error = ApplicationError("some.url", "\u2603")
self.assertIn("\u2603", str(error))
def test_unicode_errormessage(self):
"""
Unicode arguments in ApplicationError will not raise an exception when
the error_message method is called.
"""
error = ApplicationError("some.url", "\u2603")
# on py27-tw189: exceptions.UnicodeEncodeError: 'ascii' codec can't encode character '\u2603' in position 10: ordinal not in
print(error.error_message())
self.assertIn("\u2603", error.error_message())

View File

@@ -0,0 +1,98 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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.message import check_or_raise_realm_name, identify_realm_name_category
from autobahn.wamp.exception import InvalidUriError
import unittest
class TestWampIdentifiers(unittest.TestCase):
def test_valid_realm_names(self):
for name in [
'realm1',
'com.example.myapp1',
'myapp1.example.com',
'eth.wamp-proto',
'wamp-proto.eth',
'eth.wamp-proto.myapp1',
'myapp1.wamp-proto.eth',
'aaa',
'Abc',
'a00',
'A00',
'0x0000000000000000000000000000000000000000',
'0xe59C7418403CF1D973485B36660728a5f4A8fF9c',
]:
self.assertEqual(name, check_or_raise_realm_name(name))
def test_invalid_realm_names(self):
for name in [
None,
23,
{},
'',
'.realm1',
'123realm',
'0x' + '00' * 64,
'0x' + '00' * 32,
'0x' + 'zz' * 40,
'rlm$test',
'a' * 256,
]:
self.assertRaises(InvalidUriError, check_or_raise_realm_name, name)
def test_realm_name_categories(self):
for name, category in [
# valid
('realm1', 'standalone'),
('com.example.myapp1', 'standalone'),
('myapp1.example.com', 'standalone'),
('eth.wamp-proto', 'reverse_ens'),
('wamp-proto.eth', 'ens'),
('eth.wamp-proto.myapp1', 'reverse_ens'),
('myapp1.wamp-proto.eth', 'ens'),
('aaa', 'standalone'),
('Abc', 'standalone'),
('a00', 'standalone'),
('A00', 'standalone'),
('0x0000000000000000000000000000000000000000', 'eth'),
('0xe59C7418403CF1D973485B36660728a5f4A8fF9c', 'eth'),
# invalid
(None, None),
(23, None),
({}, None),
('', None),
('.realm1', None),
('123realm', None),
('0x' + '00' * 64, None),
('0x' + '00' * 32, None),
('0x' + 'zz' * 40, None),
('rlm$test', None),
('a' * 256, None),
]:
self.assertEqual(category, identify_realm_name_category(name))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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.
#
###############################################################################
# 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.
from autobahn import wamp
from autobahn.wamp import message
from autobahn.wamp import exception
from autobahn.wamp import protocol
import unittest
class TestPeerExceptions(unittest.TestCase):
def test_exception_from_message(self):
session = protocol.BaseSession()
@wamp.error("com.myapp.error1")
class AppError1(Exception):
pass
@wamp.error("com.myapp.error2")
class AppError2(Exception):
pass
session.define(AppError1)
session.define(AppError2)
# map defined errors to user exceptions
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error1')
exc = session._exception_from_message(emsg)
self.assertIsInstance(exc, AppError1)
self.assertEqual(exc.args, ())
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error2')
exc = session._exception_from_message(emsg)
self.assertIsInstance(exc, AppError2)
self.assertEqual(exc.args, ())
# map undefined error to (generic) exception
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error3')
exc = session._exception_from_message(emsg)
self.assertIsInstance(exc, exception.ApplicationError)
self.assertEqual(exc.error, 'com.myapp.error3')
self.assertEqual(exc.args, ())
self.assertEqual(exc.kwargs, {})
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error3', args=[1, 2, 'hello'])
exc = session._exception_from_message(emsg)
self.assertIsInstance(exc, exception.ApplicationError)
self.assertEqual(exc.error, 'com.myapp.error3')
self.assertEqual(exc.args, (1, 2, 'hello'))
self.assertEqual(exc.kwargs, {})
emsg = message.Error(message.Call.MESSAGE_TYPE, 123456, 'com.myapp.error3', args=[1, 2, 'hello'], kwargs={'foo': 23, 'bar': 'baz'})
exc = session._exception_from_message(emsg)
self.assertIsInstance(exc, exception.ApplicationError)
self.assertEqual(exc.error, 'com.myapp.error3')
self.assertEqual(exc.args, (1, 2, 'hello'))
self.assertEqual(exc.kwargs, {'foo': 23, 'bar': 'baz'})
def test_message_from_exception(self):
session = protocol.BaseSession()
@wamp.error("com.myapp.error1")
class AppError1(Exception):
pass
@wamp.error("com.myapp.error2")
class AppError2(Exception):
pass
session.define(AppError1)
session.define(AppError2)
exc = AppError1()
msg = session._message_from_exception(message.Call.MESSAGE_TYPE, 123456, exc)
self.assertEqual(msg.marshal(), [message.Error.MESSAGE_TYPE, message.Call.MESSAGE_TYPE, 123456, {}, "com.myapp.error1"])

View File

@@ -0,0 +1,72 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import unittest
from binascii import a2b_hex
from autobahn.wamp.auth import derive_scram_credential
TEST_VECTORS = [
{
'email': 'foobar@example.com',
'password': 'secret123',
'salt': None,
'expected': {
'iterations': 4096,
'kdf': 'argon2id-13',
'memory': 512,
'salt': '3bc3ca01dd1d501ca1c22e1c5d7d16fe',
'server-key': '8de7864c316f3c2356fd76cfdab696db55bc70e680fe5180e2f731e2345acca2',
'stored-key': 'e796c2f0a51770303ee4616bc630a66774d51a55003154aff2a54ec7c4ac0e38'
}
},
{
'email': 'foobar@example.com',
'password': 'secret123',
'salt': a2b_hex('ae1f0d2f422757809077785e660b62c6'),
'expected': {
'iterations': 4096,
'kdf': 'argon2id-13',
'memory': 512,
'salt': 'ae1f0d2f422757809077785e660b62c6',
'server-key': '0d8e7e9222a7c0e54c9e979aa342115699ff5696c45dc379b5ee241338a5861d',
'stored-key': '5f19358ff6f38e267b6ef1ea1d862514ec4e8745a84682259fd3894be09febb5'
}
},
]
class TestKey(unittest.TestCase):
def test_derive_scram_credential(self):
for tv in TEST_VECTORS:
email = tv['email']
password = tv['password']
salt = tv['salt']
expected = tv['expected']
credential = derive_scram_credential(email, password, salt)
self.assertEqual(credential, expected)

View File

@@ -0,0 +1,546 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
import unittest
import random
import decimal
from decimal import Decimal
from autobahn.wamp import message
from autobahn.wamp import role
from autobahn.wamp import serializer
def generate_test_messages():
"""
List of WAMP test message used for serializers. Expand this if you add more
options or messages.
This list of WAMP message does not contain any binary app payloads!
"""
some_bytes = os.urandom(32)
some_unicode = '\u3053\u3093\u306b\u3061\u306f\u4e16\u754c'
some_uri = 'com.myapp.foobar'
some_unicode_uri = 'com.myapp.\u4f60\u597d\u4e16\u754c.baz'
some_args = [1, 2, 3, 'hello', some_bytes, some_unicode, {'foo': 23, 'bar': 'hello', 'baz': some_bytes, 'moo': some_unicode}]
some_kwargs = {'foo': 23, 'bar': 'hello', 'baz': some_bytes, 'moo': some_unicode, 'arr': some_args}
msgs = [
message.Hello("realm1", {'subscriber': role.RoleSubscriberFeatures()}),
message.Hello("realm1", {'publisher': role.RolePublisherFeatures()}),
message.Hello("realm1", {'caller': role.RoleCallerFeatures()}),
message.Hello("realm1", {'callee': role.RoleCalleeFeatures()}),
message.Hello("realm1", {
'subscriber': role.RoleSubscriberFeatures(),
'publisher': role.RolePublisherFeatures(),
'caller': role.RoleCallerFeatures(),
'callee': role.RoleCalleeFeatures(),
}),
message.Goodbye(),
message.Yield(123456),
message.Yield(123456, args=some_args),
message.Yield(123456, args=[], kwargs=some_kwargs),
message.Yield(123456, args=some_args, kwargs=some_kwargs),
message.Yield(123456, progress=True),
message.Interrupt(123456),
message.Interrupt(123456, mode=message.Interrupt.KILL),
message.Invocation(123456, 789123),
message.Invocation(123456, 789123, args=some_args),
message.Invocation(123456, 789123, args=[], kwargs=some_kwargs),
message.Invocation(123456, 789123, args=some_args, kwargs=some_kwargs),
message.Invocation(123456, 789123, timeout=10000),
message.Result(123456),
message.Result(123456, args=some_args),
message.Result(123456, args=[], kwargs=some_kwargs),
message.Result(123456, args=some_args, kwargs=some_kwargs),
message.Result(123456, progress=True),
message.Cancel(123456),
message.Cancel(123456, mode=message.Cancel.KILL),
message.Call(123456, some_uri),
message.Call(123456, some_uri, args=some_args),
message.Call(123456, some_uri, args=[], kwargs=some_kwargs),
message.Call(123456, some_uri, args=some_args, kwargs=some_kwargs),
message.Call(123456, some_uri, timeout=10000),
message.Call(123456, some_unicode_uri),
message.Call(123456, some_unicode_uri, args=some_args),
message.Call(123456, some_unicode_uri, args=[], kwargs=some_kwargs),
message.Call(123456, some_unicode_uri, args=some_args, kwargs=some_kwargs),
message.Call(123456, some_unicode_uri, timeout=10000),
message.Unregistered(123456),
message.Unregister(123456, 789123),
message.Registered(123456, 789123),
message.Register(123456, some_uri),
message.Register(123456, some_uri, match='prefix'),
message.Register(123456, some_uri, invoke='roundrobin'),
message.Register(123456, some_unicode_uri),
message.Register(123456, some_unicode_uri, match='prefix'),
message.Register(123456, some_unicode_uri, invoke='roundrobin'),
message.Event(123456, 789123),
message.Event(123456, 789123, args=some_args),
message.Event(123456, 789123, args=[], kwargs=some_kwargs),
message.Event(123456, 789123, args=some_args, kwargs=some_kwargs),
message.Event(123456, 789123, publisher=300),
message.Published(123456, 789123),
message.Publish(123456, some_uri),
message.Publish(123456, some_uri, args=some_args),
message.Publish(123456, some_uri, args=[], kwargs=some_kwargs),
message.Publish(123456, some_uri, args=some_args, kwargs=some_kwargs),
message.Publish(123456, some_uri, exclude_me=False, exclude=[300], eligible=[100, 200, 300]),
message.Publish(123456, some_unicode_uri),
message.Publish(123456, some_unicode_uri, args=some_args),
message.Publish(123456, some_unicode_uri, args=[], kwargs=some_kwargs),
message.Publish(123456, some_unicode_uri, args=some_args, kwargs=some_kwargs),
message.Publish(123456, some_unicode_uri, exclude_me=False, exclude=[300], eligible=[100, 200, 300]),
message.Unsubscribed(123456),
message.Unsubscribe(123456, 789123),
message.Subscribed(123456, 789123),
message.Subscribe(123456, some_uri),
message.Subscribe(123456, some_uri, match=message.Subscribe.MATCH_PREFIX),
message.Subscribe(123456, some_unicode_uri),
message.Subscribe(123456, some_unicode_uri, match=message.Subscribe.MATCH_PREFIX),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri, args=some_args),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri, args=[], kwargs=some_kwargs),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_uri, args=some_args, kwargs=some_kwargs),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri, args=some_args),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri, args=[], kwargs=some_kwargs),
message.Error(message.Call.MESSAGE_TYPE, 123456, some_unicode_uri, args=some_args, kwargs=some_kwargs),
message.Result(123456),
message.Result(123456, args=some_args),
message.Result(123456, args=some_args, kwargs=some_kwargs),
]
return [(False, msg) for msg in msgs]
def generate_test_messages_binary():
"""
Generate WAMP test messages which contain binary app payloads.
With the JSON serializer, this currently only works on Python 3 (both CPython3 and PyPy3),
because even on Python 3, we need to patch the stdlib JSON, and on Python 2, the patching
would be even hackier.
"""
msgs = []
for binary in [b'',
b'\x00',
b'\30',
os.urandom(4),
os.urandom(16),
os.urandom(128),
os.urandom(256),
os.urandom(512),
os.urandom(1024)]:
msgs.append(message.Event(123456, 789123, args=[binary]))
msgs.append(message.Event(123456, 789123, args=[binary], kwargs={'foo': binary}))
return [(True, msg) for msg in msgs]
def create_serializers(decimal_support=False):
_serializers = []
_serializers.append(serializer.JsonSerializer(use_decimal_from_str=decimal_support))
_serializers.append(serializer.JsonSerializer(batched=True, use_decimal_from_str=decimal_support))
_serializers.append(serializer.CBORSerializer())
_serializers.append(serializer.CBORSerializer(batched=True))
if not decimal_support:
# builtins.OverflowError: Integer value out of range
_serializers.append(serializer.MsgPackSerializer())
_serializers.append(serializer.MsgPackSerializer(batched=True))
# roundtrip error
_serializers.append(serializer.UBJSONSerializer())
_serializers.append(serializer.UBJSONSerializer(batched=True))
# FIXME: implement full FlatBuffers serializer for WAMP
# WAMP-FlatBuffers currently only supports Python 3
# _serializers.append(serializer.FlatBuffersSerializer())
# _serializers.append(serializer.FlatBuffersSerializer(batched=True))
return _serializers
class TestFlatBuffersSerializer(unittest.TestCase):
def test_basic(self):
messages = [
message.Event(123456,
789123,
args=[1, 2, 3],
kwargs={'foo': 23, 'bar': 'hello'},
publisher=666,
retained=True),
message.Publish(123456,
'com.example.topic1',
args=[1, 2, 3],
kwargs={'foo': 23, 'bar': 'hello'},
retain=True)
]
ser = serializer.FlatBuffersSerializer()
# from pprint import pprint
for msg in messages:
# serialize message
payload, binary = ser.serialize(msg)
# unserialize message again
msg2 = ser.unserialize(payload, binary)[0]
# pprint(msg.marshal())
# pprint(msg2.marshal())
# must be equal: message roundtrips via the serializer
self.assertEqual(msg, msg2)
# self.assertEqual(msg.subscription, msg2.subscription)
# self.assertEqual(msg.publication, msg2.publication)
class TestDecimalSerializer(unittest.TestCase):
"""
binary fixed-point
binary floating-point: float (float32), double (float64)
decimal floating-point: decimal128, decimal256
decimal fixed-point: NUMERIC(precision, scale)
decimal arbitrary precision: NUMERIC, decimal.Decimal
https://developer.nvidia.com/blog/implementing-high-precision-decimal-arithmetic-with-cuda-int128/
https://github.com/johnmcfarlane/cnl
"""
def setUp(self) -> None:
self._test_serializers = create_serializers(decimal_support=True)
# enough for decimal256 precision arithmetic (76 significand decimal digits)
decimal.getcontext().prec = 76
self._test_messages_no_dec = [
(True,
{
'a': random.random(),
'b': random.randint(0, 2 ** 53),
'c': random.randint(0, 2 ** 64),
'd': random.randint(0, 2 ** 128),
'e': random.randint(0, 2 ** 256),
# float64: 52 binary digits, precision of 15-17 significant decimal digits
'f': 0.12345678901234567,
'g': 0.8765432109876545,
'y': os.urandom(8),
'z': [
-1,
0,
1,
True,
None,
0.12345678901234567,
0.8765432109876545,
os.urandom(8)
]
})
]
self._test_messages_dec = [
(True,
{
'a': random.random(),
'b': random.randint(0, 2 ** 53),
'c': random.randint(0, 2 ** 64),
'd': random.randint(0, 2 ** 128),
'e': random.randint(0, 2 ** 256),
# float64: 52 binary digits, precision of 15-17 significant decimal digits
'f': 0.12345678901234567,
'g': 0.8765432109876545,
# decimal128: precision of 38 significant decimal digits
'h': Decimal('0.1234567890123456789012345678901234567'),
'i': Decimal('0.8765432109876543210987654321098765434'),
# decimal256: precision of 76 significant decimal digits
'j': Decimal('0.123456789012345678901234567890123456701234567890123456789012345678901234567'),
'k': Decimal('0.876543210987654321098765432109876543298765432109876543210987654321098765434'),
'y': os.urandom(8),
'z': [
-1,
0,
1,
True,
None,
0.12345678901234567,
0.8765432109876545,
Decimal('0.1234567890123456789012345678901234567'),
Decimal('0.8765432109876543210987654321098765434'),
Decimal('0.123456789012345678901234567890123456701234567890123456789012345678901234567'),
Decimal('0.876543210987654321098765432109876543298765432109876543210987654321098765434'),
os.urandom(8)
]
})
]
def test_json_no_decimal(self):
"""
Test without ``use_decimal_from_str`` feature of JSON object serializer.
"""
ser = serializer.JsonObjectSerializer(use_decimal_from_str=False)
for contains_binary, obj in self._test_messages_no_dec:
_obj = ser.unserialize(ser.serialize(obj))[0]
self.assertEqual(obj, _obj)
self.assertEqual(1.0000000000000002, _obj['f'] + _obj['g'])
def test_json_decimal(self):
"""
Test ``use_decimal_from_str`` feature of JSON object serializer.
"""
ser = serializer.JsonObjectSerializer(use_decimal_from_str=True)
for contains_binary, obj in self._test_messages_dec:
_obj = ser.unserialize(ser.serialize(obj))[0]
self.assertEqual(obj, _obj)
self.assertEqual(1.0000000000000002, _obj['f'] + _obj['g'])
self.assertEqual(Decimal('1.0000000000000000000000000000000000001'), _obj['h'] + _obj['i'])
self.assertEqual(Decimal('1.000000000000000000000000000000000000000000000000000000000000000000000000001'), _obj['j'] + _obj['k'])
def test_roundtrip_msg(self):
for wamp_ser in self._test_serializers:
ser = wamp_ser._serializer
for contains_binary, msg in self._test_messages_no_dec + self._test_messages_dec:
payload = ser.serialize(msg)
msg2 = ser.unserialize(payload)
self.assertEqual(msg, msg2[0])
def test_crosstrip_msg(self):
for wamp_ser1 in self._test_serializers:
ser1 = wamp_ser1._serializer
for contains_binary, msg in self._test_messages_no_dec + self._test_messages_dec:
payload1 = ser1.serialize(msg)
msg1 = ser1.unserialize(payload1)
msg1 = msg1[0]
for wamp_ser2 in self._test_serializers:
ser2 = wamp_ser2._serializer
payload2 = ser2.serialize(msg1)
msg2 = ser2.unserialize(payload2)
msg2 = msg2[0]
self.assertEqual(msg, msg2)
# print(ser1, len(payload1), ser2, len(payload2))
class TestSerializer(unittest.TestCase):
def setUp(self):
self._test_messages = generate_test_messages() + generate_test_messages_binary()
self._test_serializers = create_serializers()
# print('Testing WAMP serializers {} with {} WAMP test messages'.format([ser.SERIALIZER_ID for ser in self._test_serializers], len(self._test_messages)))
def test_deep_equal_msg(self):
"""
Test deep object equality assert (because I am paranoid).
"""
v = os.urandom(10)
o1 = [1, 2, {'foo': 'bar', 'bar': v, 'baz': [9, 3, 2], 'goo': {'moo': [1, 2, 3]}}, v]
o2 = [1, 2, {'goo': {'moo': [1, 2, 3]}, 'bar': v, 'baz': [9, 3, 2], 'foo': 'bar'}, v]
self.assertEqual(o1, o2)
def test_roundtrip_msg(self):
"""
Test round-tripping over each serializer.
"""
for ser in self._test_serializers:
for contains_binary, msg in self._test_messages:
# serialize message
payload, binary = ser.serialize(msg)
# unserialize message again
msg2 = ser.unserialize(payload, binary)
# must be equal: message roundtrips via the serializer
self.assertEqual([msg], msg2)
def test_crosstrip_msg(self):
"""
Test cross-tripping over 2 serializers (as is done by WAMP routers).
"""
for ser1 in self._test_serializers:
for contains_binary, msg in self._test_messages:
# serialize message
payload, binary = ser1.serialize(msg)
# unserialize message again
msg1 = ser1.unserialize(payload, binary)
msg1 = msg1[0]
for ser2 in self._test_serializers:
# serialize message
payload, binary = ser2.serialize(msg1)
# unserialize message again
msg2 = ser2.unserialize(payload, binary)
# must be equal: message crosstrips via
# the serializers ser1 -> ser2
self.assertEqual([msg], msg2)
def test_cache_msg(self):
"""
Test message serialization caching.
"""
for contains_binary, msg in self._test_messages:
# message serialization cache is initially empty
self.assertEqual(msg._serialized, {})
for ser in self._test_serializers:
# verify message serialization is not yet cached
self.assertFalse(ser._serializer in msg._serialized)
payload, binary = ser.serialize(msg)
# now the message serialization must be cached
self.assertTrue(ser._serializer in msg._serialized)
self.assertEqual(msg._serialized[ser._serializer], payload)
# and after resetting the serialization cache, message
# serialization is gone
msg.uncache()
self.assertFalse(ser._serializer in msg._serialized)
def test_initial_stats(self):
"""
Test initial serializer stats are indeed empty.
"""
for ser in self._test_serializers:
stats = ser.stats(details=True)
self.assertEqual(stats['serialized']['bytes'], 0)
self.assertEqual(stats['serialized']['messages'], 0)
self.assertEqual(stats['serialized']['rated_messages'], 0)
self.assertEqual(stats['unserialized']['bytes'], 0)
self.assertEqual(stats['unserialized']['messages'], 0)
self.assertEqual(stats['unserialized']['rated_messages'], 0)
def test_serialize_stats(self):
"""
Test serializer stats are non-empty after serializing/unserializing messages.
"""
for ser in self._test_serializers:
for contains_binary, msg in self._test_messages:
# serialize message
payload, binary = ser.serialize(msg)
# unserialize message again
ser.unserialize(payload, binary)
stats = ser.stats(details=False)
self.assertTrue(stats['bytes'] > 0)
self.assertTrue(stats['messages'] > 0)
self.assertTrue(stats['rated_messages'] > 0)
def test_serialize_stats_with_details(self):
"""
Test serializer stats - with details - are non-empty after serializing/unserializing messages.
"""
for ser in self._test_serializers:
for contains_binary, msg in self._test_messages:
# serialize message
payload, binary = ser.serialize(msg)
# unserialize message again
ser.unserialize(payload, binary)
stats = ser.stats(details=True)
# {'serialized': {'bytes': 7923, 'messages': 59, 'rated_messages': 69}, 'unserialized': {'bytes': 7923, 'messages': 59, 'rated_messages': 69}}
# print(stats)
self.assertTrue(stats['serialized']['bytes'] > 0)
self.assertTrue(stats['serialized']['messages'] > 0)
self.assertTrue(stats['serialized']['rated_messages'] > 0)
self.assertTrue(stats['unserialized']['bytes'] > 0)
self.assertTrue(stats['unserialized']['messages'] > 0)
self.assertTrue(stats['unserialized']['rated_messages'] > 0)
self.assertEqual(stats['serialized']['bytes'], stats['unserialized']['bytes'])
self.assertEqual(stats['serialized']['messages'], stats['unserialized']['messages'])
self.assertEqual(stats['serialized']['rated_messages'], stats['unserialized']['rated_messages'])
def test_reset_stats(self):
"""
Test serializer stats are reset after fetching stats - depending on option.
"""
for ser in self._test_serializers:
for contains_binary, msg in self._test_messages:
# serialize message
payload, binary = ser.serialize(msg)
# unserialize message again
ser.unserialize(payload, binary)
ser.stats()
stats = ser.stats(details=True)
self.assertEqual(stats['serialized']['bytes'], 0)
self.assertEqual(stats['serialized']['messages'], 0)
self.assertEqual(stats['serialized']['rated_messages'], 0)
self.assertEqual(stats['unserialized']['bytes'], 0)
self.assertEqual(stats['unserialized']['messages'], 0)
self.assertEqual(stats['unserialized']['rated_messages'], 0)
def test_auto_stats(self):
"""
Test serializer stats are non-empty after serializing/unserializing messages.
"""
for ser in self._test_serializers:
def on_stats(stats):
self.assertTrue(stats['bytes'] > 0)
self.assertTrue(stats['messages'] > 0)
self.assertTrue(stats['rated_messages'] > 0)
ser.set_stats_autoreset(10, 0, on_stats)
for contains_binary, msg in self._test_messages:
# serialize message
payload, binary = ser.serialize(msg)
# unserialize message again
ser.unserialize(payload, binary)

View File

@@ -0,0 +1,88 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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, SessionDetails
import unittest
# from .test_wamp_transport_details import TRANSPORT_DETAILS_1
from autobahn.wamp.test.test_wamp_transport_details import TRANSPORT_DETAILS_1
class TestSessionDetails(unittest.TestCase):
def test_empty(self):
sd1 = SessionDetails()
data = sd1.marshal()
self.assertEqual(data, {
'realm': None,
'session': None,
'authid': None,
'authrole': None,
'authmethod': None,
'authprovider': None,
'authextra': None,
'serializer': None,
'transport': None,
'resumed': None,
'resumable': None,
'resume_token': None,
})
sd2 = SessionDetails.parse(data)
self.assertEqual(sd2, sd1)
def test_attributes(self):
sd1 = SessionDetails()
td1 = TransportDetails.parse(TRANSPORT_DETAILS_1)
sd1.realm = 'realm1'
sd1.session = 666
sd1.authid = 'homer'
sd1.authrole = 'user'
sd1.authmethod = 'wampcra'
sd1.authprovider = 'static'
sd1.authextra = {'foo': 'bar', 'baz': [1, 2, 3]}
sd1.serializer = 'json'
sd1.transport = td1
sd1.resumed = False
sd1.resumable = True
sd1.resume_token = '8713e25a-d4f5-48b7-9d6d-eda66603a1ab'
data = sd1.marshal()
self.assertEqual(data, {
'realm': sd1.realm,
'session': sd1.session,
'authid': sd1.authid,
'authrole': sd1.authrole,
'authmethod': sd1.authmethod,
'authprovider': sd1.authprovider,
'authextra': sd1.authextra,
'serializer': sd1.serializer,
'transport': sd1.transport.marshal(),
'resumed': sd1.resumed,
'resumable': sd1.resumable,
'resume_token': sd1.resume_token,
})
sd2 = SessionDetails.parse(data)
self.assertEqual(sd2, sd1)

View File

@@ -0,0 +1,138 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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
TRANSPORT_DETAILS_1 = {
# 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',
}
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):
td = TransportDetails.parse(TRANSPORT_DETAILS_1)
data2 = td.marshal()
self.maxDiff = None
self.assertEqual(data2, TRANSPORT_DETAILS_1)
def test_channel_typeid(self):
# test empty
td = TransportDetails()
self.assertEqual(td.channel_typeid, 'null-null-null')
# test all combinations
for channel_type in TransportDetails.CHANNEL_TYPE_TO_STR:
for channel_framing in TransportDetails.CHANNEL_FRAMING_TO_STR:
for channel_serializer in TransportDetails.CHANNEL_SERIALIZER_TO_STR:
td = TransportDetails(channel_type=channel_type, channel_framing=channel_framing, channel_serializer=channel_serializer)
channel_typeid = '{}-{}-{}'.format(TransportDetails.CHANNEL_TYPE_TO_STR[channel_type],
TransportDetails.CHANNEL_FRAMING_TO_STR[channel_framing],
TransportDetails.CHANNEL_SERIALIZER_TO_STR[channel_serializer])
self.assertEqual(td.channel_typeid, channel_typeid)

View File

@@ -0,0 +1,573 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int 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 import wamp
from autobahn.wamp.uri import Pattern, RegisterOptions, SubscribeOptions
import unittest
class TestUris(unittest.TestCase):
def test_invalid_uris(self):
for u in ["",
"com.myapp.<product:foo>.update",
"com.myapp.<123:int>.update",
"com.myapp.<:product>.update",
"com.myapp.<product:>.update",
"com.myapp.<int:>.update",
]:
self.assertRaises(Exception, Pattern, u, Pattern.URI_TARGET_ENDPOINT)
def test_valid_uris(self):
for u in ["com.myapp.proc1",
"123",
"com.myapp.<product:int>.update",
"com.myapp.<category:string>.<subcategory>.list"
"com.myapp.something..update"
]:
p = Pattern(u, Pattern.URI_TARGET_ENDPOINT)
self.assertIsInstance(p, Pattern)
def test_parse_uris(self):
tests = [
("com.myapp.<product:int>.update", [
("com.myapp.0.update", {'product': 0}),
("com.myapp.123456.update", {'product': 123456}),
("com.myapp.aaa.update", None),
("com.myapp..update", None),
("com.myapp.0.delete", None),
]),
("com.myapp.<product:string>.update", [
("com.myapp.box.update", {'product': 'box'}),
("com.myapp.123456.update", {'product': '123456'}),
("com.myapp..update", None),
]),
("com.myapp.<product>.update", [
("com.myapp.0.update", {'product': '0'}),
("com.myapp.abc.update", {'product': 'abc'}),
("com.myapp..update", None),
]),
("com.myapp.<category:string>.<subcategory:string>.list", [
("com.myapp.cosmetic.shampoo.list", {'category': 'cosmetic', 'subcategory': 'shampoo'}),
("com.myapp...list", None),
("com.myapp.cosmetic..list", None),
("com.myapp..shampoo.list", None),
]),
("eth.pydefi.tradeclock.<clock_oid:str>.get_clock_info", [
("eth.pydefi.tradeclock.ba3b1e9f-3006-4eae-ae88-cf5896b36342.get_clock_info",
{"clock_oid": "ba3b1e9f-3006-4eae-ae88-cf5896b36342"}),
]),
("eth.wamp.network.catalog.<catalog_adr:str>.owner", [
("eth.wamp.network.catalog.0xAA8Cc377db31a354137d8Bb86D0E38495dbD5266.owner",
{"catalog_adr": "0xAA8Cc377db31a354137d8Bb86D0E38495dbD5266"}),
]),
]
for test in tests:
pat = Pattern(test[0], Pattern.URI_TARGET_ENDPOINT)
for ptest in test[1]:
uri = ptest[0]
kwargs_should = ptest[1]
if kwargs_should is not None:
args_is, kwargs_is = pat.match(uri)
self.assertEqual(kwargs_is, kwargs_should)
else:
self.assertRaises(Exception, pat.match, uri)
class TestDecorators(unittest.TestCase):
def test_decorate_endpoint(self):
@wamp.register("com.calculator.square")
def square(_):
"""Do nothing."""
self.assertTrue(hasattr(square, '_wampuris'))
self.assertTrue(type(square._wampuris) == list)
self.assertEqual(len(square._wampuris), 1)
self.assertIsInstance(square._wampuris[0], Pattern)
self.assertTrue(square._wampuris[0].is_endpoint())
self.assertFalse(square._wampuris[0].is_handler())
self.assertFalse(square._wampuris[0].is_exception())
self.assertEqual(square._wampuris[0].uri(), "com.calculator.square")
self.assertEqual(square._wampuris[0]._type, Pattern.URI_TYPE_EXACT)
@wamp.register("com.myapp.product.<product:int>.update")
def update_product(product=None, label=None):
"""Do nothing."""
self.assertTrue(hasattr(update_product, '_wampuris'))
self.assertTrue(type(update_product._wampuris) == list)
self.assertEqual(len(update_product._wampuris), 1)
self.assertIsInstance(update_product._wampuris[0], Pattern)
self.assertTrue(update_product._wampuris[0].is_endpoint())
self.assertFalse(update_product._wampuris[0].is_handler())
self.assertFalse(update_product._wampuris[0].is_exception())
self.assertEqual(update_product._wampuris[0].uri(), "com.myapp.product.<product:int>.update")
self.assertEqual(update_product._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
@wamp.register("com.myapp.<category:string>.<cid:int>.update")
def update(category=None, cid=None):
"""Do nothing."""
self.assertTrue(hasattr(update, '_wampuris'))
self.assertTrue(type(update._wampuris) == list)
self.assertEqual(len(update._wampuris), 1)
self.assertIsInstance(update._wampuris[0], Pattern)
self.assertTrue(update._wampuris[0].is_endpoint())
self.assertFalse(update._wampuris[0].is_handler())
self.assertFalse(update._wampuris[0].is_exception())
self.assertEqual(update._wampuris[0].uri(), "com.myapp.<category:string>.<cid:int>.update")
self.assertEqual(update._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
@wamp.register("com.myapp.circle.<name:string>",
RegisterOptions(match="wildcard", details_arg="details"))
def circle(name=None, details=None):
""" Do nothing. """
self.assertTrue(hasattr(circle, '_wampuris'))
self.assertTrue(type(circle._wampuris) == list)
self.assertEqual(len(circle._wampuris), 1)
self.assertIsInstance(circle._wampuris[0], Pattern)
self.assertIsInstance(circle._wampuris[0].options, RegisterOptions)
self.assertEqual(circle._wampuris[0].options.match, "wildcard")
self.assertEqual(circle._wampuris[0].options.details_arg, "details")
self.assertTrue(circle._wampuris[0].is_endpoint())
self.assertFalse(circle._wampuris[0].is_handler())
self.assertFalse(circle._wampuris[0].is_exception())
self.assertEqual(circle._wampuris[0].uri(), "com.myapp.circle.<name:string>")
self.assertEqual(circle._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
@wamp.register("com.myapp.something..update",
RegisterOptions(match="wildcard", details_arg="details"))
def something(dynamic=None, details=None):
""" Do nothing. """
self.assertTrue(hasattr(something, '_wampuris'))
self.assertTrue(type(something._wampuris) == list)
self.assertEqual(len(something._wampuris), 1)
self.assertIsInstance(something._wampuris[0], Pattern)
self.assertIsInstance(something._wampuris[0].options, RegisterOptions)
self.assertEqual(something._wampuris[0].options.match, "wildcard")
self.assertEqual(something._wampuris[0].options.details_arg, "details")
self.assertTrue(something._wampuris[0].is_endpoint())
self.assertFalse(something._wampuris[0].is_handler())
self.assertFalse(something._wampuris[0].is_exception())
self.assertEqual(something._wampuris[0].uri(), "com.myapp.something..update")
self.assertEqual(something._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
def test_decorate_handler(self):
@wamp.subscribe("com.myapp.on_shutdown")
def on_shutdown():
"""Do nothing."""
self.assertTrue(hasattr(on_shutdown, '_wampuris'))
self.assertTrue(type(on_shutdown._wampuris) == list)
self.assertEqual(len(on_shutdown._wampuris), 1)
self.assertIsInstance(on_shutdown._wampuris[0], Pattern)
self.assertFalse(on_shutdown._wampuris[0].is_endpoint())
self.assertTrue(on_shutdown._wampuris[0].is_handler())
self.assertFalse(on_shutdown._wampuris[0].is_exception())
self.assertEqual(on_shutdown._wampuris[0].uri(), "com.myapp.on_shutdown")
self.assertEqual(on_shutdown._wampuris[0]._type, Pattern.URI_TYPE_EXACT)
@wamp.subscribe("com.myapp.product.<product:int>.on_update")
def on_product_update(product=None, label=None):
"""Do nothing."""
self.assertTrue(hasattr(on_product_update, '_wampuris'))
self.assertTrue(type(on_product_update._wampuris) == list)
self.assertEqual(len(on_product_update._wampuris), 1)
self.assertIsInstance(on_product_update._wampuris[0], Pattern)
self.assertFalse(on_product_update._wampuris[0].is_endpoint())
self.assertTrue(on_product_update._wampuris[0].is_handler())
self.assertFalse(on_product_update._wampuris[0].is_exception())
self.assertEqual(on_product_update._wampuris[0].uri(), "com.myapp.product.<product:int>.on_update")
self.assertEqual(on_product_update._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
@wamp.subscribe("com.myapp.<category:string>.<cid:int>.on_update")
def on_update(category=None, cid=None, label=None):
"""Do nothing."""
self.assertTrue(hasattr(on_update, '_wampuris'))
self.assertTrue(type(on_update._wampuris) == list)
self.assertEqual(len(on_update._wampuris), 1)
self.assertIsInstance(on_update._wampuris[0], Pattern)
self.assertFalse(on_update._wampuris[0].is_endpoint())
self.assertTrue(on_update._wampuris[0].is_handler())
self.assertFalse(on_update._wampuris[0].is_exception())
self.assertEqual(on_update._wampuris[0].uri(), "com.myapp.<category:string>.<cid:int>.on_update")
self.assertEqual(on_update._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
@wamp.subscribe("com.myapp.on.<event:string>",
SubscribeOptions(match="wildcard", details_arg="details"))
def on_event(event=None, details=None):
""" Do nothing. """
self.assertTrue(hasattr(on_event, '_wampuris'))
self.assertTrue(type(on_event._wampuris) == list)
self.assertEqual(len(on_event._wampuris), 1)
self.assertIsInstance(on_event._wampuris[0], Pattern)
self.assertIsInstance(on_event._wampuris[0].options, SubscribeOptions)
self.assertEqual(on_event._wampuris[0].options.match, "wildcard")
self.assertEqual(on_event._wampuris[0].options.details_arg, "details")
self.assertFalse(on_event._wampuris[0].is_endpoint())
self.assertTrue(on_event._wampuris[0].is_handler())
self.assertFalse(on_event._wampuris[0].is_exception())
self.assertEqual(on_event._wampuris[0].uri(), "com.myapp.on.<event:string>")
self.assertEqual(on_event._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
def test_decorate_exception(self):
@wamp.error("com.myapp.error")
class AppError(Exception):
"""Do nothing."""
self.assertTrue(hasattr(AppError, '_wampuris'))
self.assertTrue(type(AppError._wampuris) == list)
self.assertEqual(len(AppError._wampuris), 1)
self.assertIsInstance(AppError._wampuris[0], Pattern)
self.assertFalse(AppError._wampuris[0].is_endpoint())
self.assertFalse(AppError._wampuris[0].is_handler())
self.assertTrue(AppError._wampuris[0].is_exception())
self.assertEqual(AppError._wampuris[0].uri(), "com.myapp.error")
self.assertEqual(AppError._wampuris[0]._type, Pattern.URI_TYPE_EXACT)
@wamp.error("com.myapp.product.<product:int>.product_inactive")
class ProductInactiveError(Exception):
"""Do nothing."""
self.assertTrue(hasattr(ProductInactiveError, '_wampuris'))
self.assertTrue(type(ProductInactiveError._wampuris) == list)
self.assertEqual(len(ProductInactiveError._wampuris), 1)
self.assertIsInstance(ProductInactiveError._wampuris[0], Pattern)
self.assertFalse(ProductInactiveError._wampuris[0].is_endpoint())
self.assertFalse(ProductInactiveError._wampuris[0].is_handler())
self.assertTrue(ProductInactiveError._wampuris[0].is_exception())
self.assertEqual(ProductInactiveError._wampuris[0].uri(), "com.myapp.product.<product:int>.product_inactive")
self.assertEqual(ProductInactiveError._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
@wamp.error("com.myapp.<category:string>.<product:int>.inactive")
class ObjectInactiveError(Exception):
"""Do nothing."""
self.assertTrue(hasattr(ObjectInactiveError, '_wampuris'))
self.assertTrue(type(ObjectInactiveError._wampuris) == list)
self.assertEqual(len(ObjectInactiveError._wampuris), 1)
self.assertIsInstance(ObjectInactiveError._wampuris[0], Pattern)
self.assertFalse(ObjectInactiveError._wampuris[0].is_endpoint())
self.assertFalse(ObjectInactiveError._wampuris[0].is_handler())
self.assertTrue(ObjectInactiveError._wampuris[0].is_exception())
self.assertEqual(ObjectInactiveError._wampuris[0].uri(), "com.myapp.<category:string>.<product:int>.inactive")
self.assertEqual(ObjectInactiveError._wampuris[0]._type, Pattern.URI_TYPE_WILDCARD)
def test_match_decorated_endpoint(self):
@wamp.register("com.calculator.square")
def square(x):
return x
args, kwargs = square._wampuris[0].match("com.calculator.square")
self.assertEqual(square(666, **kwargs), 666)
@wamp.register("com.myapp.product.<product:int>.update")
def update_product(product=None, label=None):
return product, label
args, kwargs = update_product._wampuris[0].match("com.myapp.product.123456.update")
kwargs['label'] = "foobar"
self.assertEqual(update_product(**kwargs), (123456, "foobar"))
@wamp.register("com.myapp.<category:string>.<cid:int>.update")
def update(category=None, cid=None, label=None):
return category, cid, label
args, kwargs = update._wampuris[0].match("com.myapp.product.123456.update")
kwargs['label'] = "foobar"
self.assertEqual(update(**kwargs), ("product", 123456, "foobar"))
def test_match_decorated_handler(self):
@wamp.subscribe("com.myapp.on_shutdown")
def on_shutdown():
pass
args, kwargs = on_shutdown._wampuris[0].match("com.myapp.on_shutdown")
self.assertEqual(on_shutdown(**kwargs), None)
@wamp.subscribe("com.myapp.product.<product:int>.on_update")
def on_product_update(product=None, label=None):
return product, label
args, kwargs = on_product_update._wampuris[0].match("com.myapp.product.123456.on_update")
kwargs['label'] = "foobar"
self.assertEqual(on_product_update(**kwargs), (123456, "foobar"))
@wamp.subscribe("com.myapp.<category:string>.<cid:int>.on_update")
def on_update(category=None, cid=None, label=None):
return category, cid, label
args, kwargs = on_update._wampuris[0].match("com.myapp.product.123456.on_update")
kwargs['label'] = "foobar"
self.assertEqual(on_update(**kwargs), ("product", 123456, "foobar"))
def test_match_decorated_exception(self):
@wamp.error("com.myapp.error")
class AppError(Exception):
def __init__(self, msg):
Exception.__init__(self, msg)
def __eq__(self, other):
return self.__class__ == other.__class__ and self.args == other.args
args, kwargs = AppError._wampuris[0].match("com.myapp.error")
# noinspection PyArgumentList
self.assertEqual(AppError("fuck", **kwargs), AppError("fuck"))
@wamp.error("com.myapp.product.<product:int>.product_inactive")
class ProductInactiveError(Exception):
def __init__(self, msg, product=None):
Exception.__init__(self, msg)
self.product = product
def __eq__(self, other):
return self.__class__ == other.__class__ and self.args == other.args and self.product == other.product
args, kwargs = ProductInactiveError._wampuris[0].match("com.myapp.product.123456.product_inactive")
self.assertEqual(ProductInactiveError("fuck", **kwargs), ProductInactiveError("fuck", 123456))
@wamp.error("com.myapp.<category:string>.<product:int>.inactive")
class ObjectInactiveError(Exception):
def __init__(self, msg, category=None, product=None):
Exception.__init__(self, msg)
self.category = category
self.product = product
def __eq__(self, other):
return self.__class__ == other.__class__ and self.args == other.args and \
self.category == other.category and self.product == other.product
args, kwargs = ObjectInactiveError._wampuris[0].match("com.myapp.product.123456.inactive")
self.assertEqual(ObjectInactiveError("fuck", **kwargs), ObjectInactiveError("fuck", "product", 123456))
class KwException(Exception):
def __init__(self, *args, **kwargs):
Exception.__init__(self, *args)
self.kwargs = kwargs
# what if the WAMP error message received
# contains args/kwargs that cannot be
# consumed by the constructor of the exception
# class defined for the WAMP error URI?
# 1. we can bail out (but we are already signaling an error)
# 2. we can require a generic constructor
# 3. we can map only unconsumed args/kwargs to generic attributes
# 4. we can silently drop unconsumed args/kwargs
class MockSession(object):
def __init__(self):
self._ecls_to_uri_pat = {}
self._uri_to_ecls = {}
def define(self, exception, error=None):
if error is None:
assert (hasattr(exception, '_wampuris'))
self._ecls_to_uri_pat[exception] = exception._wampuris
self._uri_to_ecls[exception._wampuris[0].uri()] = exception
else:
assert (not hasattr(exception, '_wampuris'))
self._ecls_to_uri_pat[exception] = [Pattern(error, Pattern.URI_TARGET_HANDLER)]
self._uri_to_ecls[error] = exception
def map_error(self, error, args=None, kwargs=None):
# FIXME:
# 1. map to ecls based on error URI wildcard/prefix
# 2. extract additional args/kwargs from error URI
if error in self._uri_to_ecls:
ecls = self._uri_to_ecls[error]
try:
# the following might fail, eg. TypeError when
# signature of exception constructor is incompatible
# with args/kwargs or when the exception constructor raises
if kwargs:
if args:
exc = ecls(*args, **kwargs)
else:
exc = ecls(**kwargs)
else:
if args:
exc = ecls(*args)
else:
exc = ecls()
except Exception:
# FIXME: log e
exc = KwException(error, *args, **kwargs)
else:
# this never fails
args = args or []
kwargs = kwargs or {}
exc = KwException(error, *args, **kwargs)
return exc
class TestDecoratorsAdvanced(unittest.TestCase):
def test_decorate_exception_non_exception(self):
def test():
# noinspection PyUnusedLocal
@wamp.error("com.test.error")
class Foo(object):
pass
self.assertRaises(Exception, test)
def test_decorate_endpoint_multiple(self):
# noinspection PyUnusedLocal
@wamp.register("com.oldapp.oldproc")
@wamp.register("com.calculator.square")
def square(x):
"""Do nothing."""
self.assertTrue(hasattr(square, '_wampuris'))
self.assertTrue(type(square._wampuris) == list)
self.assertEqual(len(square._wampuris), 2)
for i in range(2):
self.assertIsInstance(square._wampuris[i], Pattern)
self.assertTrue(square._wampuris[i].is_endpoint())
self.assertFalse(square._wampuris[i].is_handler())
self.assertFalse(square._wampuris[i].is_exception())
self.assertEqual(square._wampuris[i]._type, Pattern.URI_TYPE_EXACT)
self.assertEqual(square._wampuris[0].uri(), "com.calculator.square")
self.assertEqual(square._wampuris[1].uri(), "com.oldapp.oldproc")
def test_marshal_decorated_exception(self):
@wamp.error("com.myapp.error")
class AppError(Exception):
pass
try:
raise AppError("fuck")
except Exception as e:
self.assertEqual(e._wampuris[0].uri(), "com.myapp.error")
@wamp.error("com.myapp.product.<product:int>.product_inactive")
class ProductInactiveError(Exception):
def __init__(self, msg, product=None):
Exception.__init__(self, msg)
self.product = product
try:
raise ProductInactiveError("fuck", 123456)
except Exception as e:
self.assertEqual(e._wampuris[0].uri(), "com.myapp.product.<product:int>.product_inactive")
session = MockSession()
session.define(AppError)
def test_define_exception_undecorated(self):
session = MockSession()
class AppError(Exception):
pass
# defining an undecorated exception requires
# an URI to be provided
self.assertRaises(Exception, session.define, AppError)
session.define(AppError, "com.myapp.error")
exc = session.map_error("com.myapp.error")
self.assertIsInstance(exc, AppError)
def test_define_exception_decorated(self):
session = MockSession()
@wamp.error("com.myapp.error")
class AppError(Exception):
pass
# when defining a decorated exception
# an URI must not be provided
self.assertRaises(Exception, session.define, AppError, "com.myapp.error")
session.define(AppError)
exc = session.map_error("com.myapp.error")
self.assertIsInstance(exc, AppError)
def test_map_exception_undefined(self):
session = MockSession()
exc = session.map_error("com.myapp.error")
self.assertIsInstance(exc, Exception)
def test_map_exception_args(self):
session = MockSession()
@wamp.error("com.myapp.error")
class AppError(Exception):
pass
@wamp.error("com.myapp.error.product_inactive")
class ProductInactiveError(Exception):
def __init__(self, product=None):
self.product = product
# define exceptions in mock session
session.define(AppError)
session.define(ProductInactiveError)
for test in [
# ("com.myapp.foo.error", [], {}, KwException),
("com.myapp.error", [], {}, AppError),
("com.myapp.error", ["you are doing it wrong"], {}, AppError),
("com.myapp.error", ["you are doing it wrong", 1, 2, 3], {}, AppError),
("com.myapp.error.product_inactive", [], {}, ProductInactiveError),
("com.myapp.error.product_inactive", [], {"product": 123456}, ProductInactiveError),
]:
error, args, kwargs, ecls = test
exc = session.map_error(error, args, kwargs)
self.assertIsInstance(exc, ecls)
self.assertEqual(list(exc.args), args)

View File

@@ -0,0 +1,406 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
if os.environ.get('USE_TWISTED', False):
from twisted.trial import unittest
from twisted.internet import defer
from twisted.python.failure import Failure
from autobahn.wamp import message, role
from autobahn.wamp.exception import ProtocolError
from autobahn.twisted.wamp import ApplicationSession
from autobahn.wamp.types import TransportDetails
class MockTransport:
def __init__(self):
self.messages = []
self._transport_details = TransportDetails()
def transport_details(self):
return self._transport_details
def send(self, msg):
self.messages.append(msg)
def close(self, *args, **kw):
pass
class MockApplicationSession(ApplicationSession):
'''
This is used by tests, which typically attach their own handler to
on*() methods. This just collects any errors from onUserError
'''
def __init__(self, *args, **kw):
ApplicationSession.__init__(self, *args, **kw)
self.errors = []
self._realm = 'dummy'
self._transport = MockTransport()
def onUserError(self, e, msg):
self.errors.append((e.value, msg))
def exception_raiser(exc):
'''
Create a method that takes any args and always raises the given
Exception instance.
'''
assert isinstance(exc, Exception), "Must derive from Exception"
def method(*args, **kw):
raise exc
return method
def async_exception_raiser(exc):
'''
Create a method that takes any args, and always returns a Deferred
that has failed.
'''
assert isinstance(exc, Exception), "Must derive from Exception"
def method(*args, **kw):
try:
raise exc
except:
return defer.fail(Failure())
return method
def create_mock_welcome():
return message.Welcome(
1234,
{
'broker': role.RoleBrokerFeatures(),
},
)
class TestSessionCallbacks(unittest.TestCase):
'''
These test that callbacks on user-overridden ApplicationSession
methods that produce errors are handled correctly.
XXX should do state-diagram documenting where we are when each
of these cases arises :/
'''
# XXX sure would be nice to use py.test @fixture to do the
# async/sync exception-raising stuff (i.e. make each test run
# 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):
session = MockApplicationSession()
exception = RuntimeError("blammo")
session.onJoin = exception_raiser(exception)
msg = create_mock_welcome()
# give the sesion a WELCOME, from which it should call onJoin
session.onMessage(msg)
# make sure we got the right error out of onUserError
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_join_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("blammo")
session.onJoin = async_exception_raiser(exception)
msg = create_mock_welcome()
# give the sesion a WELCOME, from which it should call onJoin
session.onMessage(msg)
# make sure we got the right error out of onUserError
# import traceback
# traceback.print_exception(*session.errors[0][:3])
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave(self):
session = MockApplicationSession()
exception = RuntimeError("boom")
session.onLeave = exception_raiser(exception)
msg = message.Abort("testing")
# we haven't done anything, so this is "abort before we've
# connected"
session.onMessage(msg)
# make sure we got the right error out of onUserError
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("boom")
session.onLeave = async_exception_raiser(exception)
msg = message.Abort("testing")
# we haven't done anything, so this is "abort before we've
# connected"
session.onMessage(msg)
# make sure we got the right error out of onUserError
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_valid_session(self):
'''
cover when onLeave called after we have a valid session
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onLeave = exception_raiser(exception)
# we have to get to an established connection first...
session.onMessage(create_mock_welcome())
self.assertTrue(session._session_id is not None)
# okay we have a session ("because ._session_id is not None")
msg = message.Goodbye()
session.onMessage(msg)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_valid_session_deferred(self):
'''
cover when onLeave called after we have a valid session
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onLeave = async_exception_raiser(exception)
# we have to get to an established connection first...
session.onMessage(create_mock_welcome())
self.assertTrue(session._session_id is not None)
# okay we have a session ("because ._session_id is not None")
msg = message.Goodbye()
session.onMessage(msg)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_leave_after_bad_challenge(self):
'''
onLeave raises error after onChallenge fails
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onLeave = exception_raiser(exception)
session.onChallenge = exception_raiser(exception)
# make a challenge (which will fail, and then the
# subsequent onLeave will also fail)
msg = message.Challenge("foo")
session.onMessage(msg)
self.assertEqual(2, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_via_close(self):
session = MockApplicationSession()
exception = RuntimeError("sideways")
session.onDisconnect = exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_via_close_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("sideways")
session.onDisconnect = async_exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
# XXX FIXME Probably more ways to call onLeave!
def test_on_challenge(self):
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onChallenge = exception_raiser(exception)
msg = message.Challenge("foo")
# execute
session.onMessage(msg)
# we already handle any onChallenge errors as "abort the
# connection". So make sure our error showed up in the
# fake-transport.
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
self.assertEqual(1, len(session._transport.messages))
reply = session._transport.messages[0]
self.assertIsInstance(reply, message.Abort)
self.assertEqual("such challenge", reply.message)
def test_on_challenge_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onChallenge = async_exception_raiser(exception)
msg = message.Challenge("foo")
# execute
session.onMessage(msg)
# we already handle any onChallenge errors as "abort the
# connection". So make sure our error showed up in the
# fake-transport.
self.assertEqual(1, len(session.errors))
self.assertEqual(session.errors[0][0], exception)
self.assertEqual(1, len(session._transport.messages))
reply = session._transport.messages[0]
self.assertIsInstance(reply, message.Abort)
self.assertEqual("such challenge", reply.message)
def test_no_session(self):
'''
test "all other cases" when we don't yet have a session
established, which should all raise ProtocolErrors and
*not* go through the onUserError handler. We cheat and
just test one.
'''
session = MockApplicationSession()
exception = RuntimeError("such challenge")
session.onConnect = exception_raiser(exception)
for msg in [message.Goodbye()]:
self.assertRaises(ProtocolError, session.onMessage, (msg,))
self.assertEqual(0, len(session.errors))
def test_on_disconnect(self):
session = MockApplicationSession()
exception = RuntimeError("oh sadness")
session.onDisconnect = exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("oh sadness")
session.onDisconnect = async_exception_raiser(exception)
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_with_session(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onDisconnect = exception_raiser(exception)
# create a valid session
session.onMessage(create_mock_welcome())
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_disconnect_with_session_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onDisconnect = async_exception_raiser(exception)
# create a valid session
session.onMessage(create_mock_welcome())
# we short-cut the whole state-machine traversal here by
# just calling onClose directly, which would normally be
# called via a Protocol, e.g.,
# autobahn.wamp.websocket.WampWebSocketProtocol
session.onClose(False)
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_connect(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onConnect = exception_raiser(exception)
trans = MockTransport()
# normally would be called from a Protocol?
session.onOpen(trans)
# shouldn't have done the .join()
self.assertEqual(0, len(trans.messages))
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
def test_on_connect_deferred(self):
session = MockApplicationSession()
exception = RuntimeError("the pain runs deep")
session.onConnect = async_exception_raiser(exception)
trans = MockTransport()
# normally would be called from a Protocol?
session.onOpen(trans)
# shouldn't have done the .join()
self.assertEqual(0, len(trans.messages))
self.assertEqual(1, len(session.errors))
self.assertEqual(exception, session.errors[0][0])
# XXX likely missing other ways to invoke the above. need to
# cover, for sure:
#
# onChallenge
# onJoin
# onLeave
# onDisconnect
#
# what about other ISession ones?
# onConnect
# onDisconnect
# NOTE: for Event stuff, that is publish() handlers,
# test_publish_callback_exception in test_protocol.py already
# covers exceptions coming from user-code.

View File

@@ -0,0 +1,40 @@
###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) typedef int GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################
import os
if os.environ.get('USE_TWISTED', False):
from twisted.trial import unittest
from autobahn.wamp.websocket import WampWebSocketProtocol
class TestWebsocketProtocol(unittest.TestCase):
def setUp(self):
self.protocol = WampWebSocketProtocol()
def test_close_before_open(self):
# just checking this doesn't throw an exception...
self.protocol.onClose(True, 1, "just testing")