Permalink
Browse files

Component errors, component auth (#934)

* propogate errors on no_auth_method up through component

* populate self._session (for stop())

* add anonymous auth

* add ticket authentication

* basic tests for Cryptosign

* Propagate last error (if any) and improve auth error-handling

* default => event

* fail on unknown errors

* dead code

* make the serializer
  • Loading branch information...
meejah authored and oberstet committed Dec 9, 2017
1 parent d47af84 commit 6bcca8eaceef23705bc7f48e2312952f4df45993
@@ -306,7 +306,7 @@ def start(self, loop=None):
if loop is None:
self.log.warn("Using default loop")
loop = asyncio.get_default_loop()
loop = asyncio.get_event_loop()
# this future will be returned, and thus has the semantics
# specified in the docstring.
@@ -391,23 +391,16 @@ def connect_error(fail):
u'Connection failed: {error}',
error=txaio.failure_message(fail),
)
# some types of errors should probably have
# stacktraces logged immediately at error
# level, e.g. SyntaxError?
self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(fail))
return one_reconnect_loop(None)
# This is some unknown failure, e.g. could
# be SyntaxError etc so we're aborting the
# whole mission
txaio.reject(done_f, fail)
return
txaio.add_callbacks(f, session_done, connect_error)
txaio.add_callbacks(delay_f, actual_connect, error)
if False:
# check if there is any transport left we can use
# to connect
if not self._can_reconnect():
self.log.info("No remaining transports to try")
reconnect[0] = False
def error(fail):
self.log.info("Internal error {msg}", msg=txaio.failure_message(fail))
self.log.debug("{tb}", tb=txaio.failure_format_traceback(fail))
@@ -408,7 +408,7 @@ class WampRawSocketClientProtocol(WampRawSocketMixinGeneral, WampRawSocketMixinA
@property
def serializer_id(self):
if not hasattr(self, '_serializer'):
self._serializer = self.factory._serializer
self._serializer = self.factory._serializer()
return self._serializer.RAWSOCKET_SERIALIZER_ID
def get_channel_id(self, channel_id_type=u'tls-unique'):
@@ -352,6 +352,7 @@ def start(self, reactor=None):
transport_gen = itertools.cycle(self._transports)
reconnect = True
last_failure = None
self.log.debug('Entering re-connect loop')
@@ -373,12 +374,13 @@ def start(self, reactor=None):
yield self._connect_once(reactor, transport)
except Exception as e:
f = txaio.create_failure()
last_failure = f
self.log.error(u'component failed: {error}', error=txaio.failure_message(f))
self.log.debug(u'{tb}', tb=txaio.failure_format_traceback(f))
# If this is a "fatal error" that will never work,
# we bail out now
if isinstance(e, ApplicationError):
if e.error in [u'wamp.error.no_such_realm']:
if e.error in [u'wamp.error.no_such_realm', u'wamp.error.no_auth_method']:
reconnect = False
self.log.error(u"Fatal error, not reconnecting")
# The thinking here is that we really do
@@ -420,6 +422,8 @@ def start(self, reactor=None):
if not self._can_reconnect():
self.log.info("No remaining transports to try")
reconnect = False
if last_failure is not None:
last_failure.raiseException()
def stop(self):
return self._session.leave()
@@ -33,7 +33,8 @@
if os.environ.get('USE_TWISTED', False):
from autobahn.twisted.component import Component
from zope.interface import directlyProvides
from autobahn.wamp.message import Welcome, Goodbye
from autobahn.wamp.message import Welcome, Goodbye, Hello, Abort
from autobahn.wamp.exception import ApplicationError
from autobahn.wamp.serializer import JsonSerializer
from twisted.internet.interfaces import IStreamClientEndpoint
from twisted.internet.defer import inlineCallbacks, succeed
@@ -112,11 +113,72 @@ def connect(factory, **kw):
# makes this "hard".
reactor = Clock()
with replace_loop(reactor):
yield component.start()
yield component.start(reactor=reactor)
self.assertTrue(len(joins), 1)
# make sure we fire all our time-outs
reactor.advance(3600)
@patch('autobahn.twisted.component.sleep', return_value=succeed(None))
@inlineCallbacks
def test_connect_no_auth_method(self, fake_sleep):
endpoint = Mock()
directlyProvides(endpoint, IStreamClientEndpoint)
component = Component(
transports={
"type": "websocket",
"url": "ws://127.0.0.1/ws",
"endpoint": endpoint,
}
)
def connect(factory, **kw):
proto = factory.buildProtocol('boom')
proto.makeConnection(Mock())
from autobahn.websocket.protocol import WebSocketProtocol
from base64 import b64encode
from hashlib import sha1
key = proto.websocket_key + WebSocketProtocol._WS_MAGIC
proto.data = (
b"HTTP/1.1 101 Switching Protocols\x0d\x0a"
b"Upgrade: websocket\x0d\x0a"
b"Connection: upgrade\x0d\x0a"
b"Sec-Websocket-Protocol: wamp.2.json\x0d\x0a"
b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a"
)
proto.processHandshake()
from autobahn.wamp import role
subrole = role.RoleSubscriberFeatures()
msg = Hello(u"realm", roles=dict(subscriber=subrole), authmethods=[u"anonymous"])
serializer = JsonSerializer()
data, is_binary = serializer.serialize(msg)
proto.onMessage(data, is_binary)
msg = Abort(reason=u"wamp.error.no_auth_method")
proto.onMessage(*serializer.serialize(msg))
proto.onClose(False, 100, u"wamp.error.no_auth_method")
return succeed(proto)
endpoint.connect = connect
# XXX it would actually be nicer if we *could* support
# passing a reactor in here, but the _batched_timer =
# make_batched_timer() stuff (slash txaio in general)
# makes this "hard".
reactor = Clock()
with replace_loop(reactor):
with self.assertRaises(ApplicationError) as ctx:
yield component.start(reactor=reactor)
# make sure we fire all our time-outs
reactor.advance(3600)
self.assertIn(
"no_auth_method",
str(ctx.exception)
)
class InvalidTransportConfigs(unittest.TestCase):
def test_invalid_key(self):
View
@@ -67,6 +67,8 @@ def create_authenticator(name, **kwargs):
klass = {
AuthWampCra.name: AuthWampCra,
AuthCryptoSign.name: AuthCryptoSign,
AuthAnonymous.name: AuthAnonymous,
AuthTicket.name: AuthTicket,
}[name]
except KeyError:
raise ValueError(
@@ -78,6 +80,49 @@ def create_authenticator(name, **kwargs):
# experimental authentication API
class AuthAnonymous(object):
name = u'anonymous'
def __init__(self, **kw):
self._args = kw
@property
def authextra(self):
return self._args.get(u'authextra', dict())
def on_challenge(self, session, challenge):
raise RuntimeError(
"on_challenge called on anonymous authentication"
)
IAuthenticator.register(AuthAnonymous)
class AuthTicket(object):
name = u'ticket'
def __init__(self, **kw):
self._args = kw
try:
self._ticket = self._args.pop(u'ticket')
except KeyError:
raise ValueError(
"ticket authentication requires 'ticket=' kwarg"
)
@property
def authextra(self):
return self._args.get(u'authextra', dict())
def on_challenge(self, session, challenge):
assert challenge.method == u"ticket"
return self._ticket
IAuthenticator.register(AuthTicket)
class AuthCryptoSign(object):
name = u'cryptosign'
View
@@ -36,7 +36,7 @@
from autobahn.util import ObservableMixin
from autobahn.websocket.util import parse_url
from autobahn.wamp.types import ComponentConfig, SubscribeOptions, RegisterOptions
from autobahn.wamp.exception import SessionNotReady
from autobahn.wamp.exception import SessionNotReady, ApplicationError
from autobahn.wamp.auth import create_authenticator
@@ -478,7 +478,7 @@ def _connect_once(self, reactor, transport):
def create_session():
cfg = ComponentConfig(self._realm, self._extra)
try:
session = self.session_factory(cfg)
self._session = session = self.session_factory(cfg)
for auth_name, auth_config in self._authentication.items():
authenticator = create_authenticator(auth_name, **auth_config)
session.add_authenticator(authenticator)
@@ -505,8 +505,15 @@ def on_leave(session, details):
"session leaving '{details.reason}'",
details=details,
)
if self._entry and not txaio.is_called(done):
txaio.resolve(done, None)
if not txaio.is_called(done):
if details.reason in [u"wamp.error.no_auth_method"]:
txaio.resolve(done, txaio.create_failure(
ApplicationError(
u"wamp.error.no_auth_method"
)
))
else:
txaio.resolve(done, None)
session.on('leave', on_leave)
# if we were given a "main" procedure, we run through
@@ -548,11 +548,20 @@ def from_ssh_key(cls, filename):
key (from a SSH private key file) or a (public) verification key (from a SSH
public key file). A private key file must be passphrase-less.
"""
SSH_BEGIN = u'-----BEGIN OPENSSH PRIVATE KEY-----'
with open(filename, 'rb') as f:
keydata = f.read().decode('utf-8').strip()
return cls.from_ssh_data(keydata)
@util.public
@classmethod
def from_ssh_data(cls, keydata):
"""
Load an Ed25519 key from SSH key file. The key file can be a (private) signing
key (from a SSH private key file) or a (public) verification key (from a SSH
public key file). A private key file must be passphrase-less.
"""
SSH_BEGIN = u'-----BEGIN OPENSSH PRIVATE KEY-----'
if keydata.startswith(SSH_BEGIN):
# OpenSSH private key
keydata, comment = _read_ssh_ed25519_privkey(keydata)
@@ -338,7 +338,7 @@ def onOpen(self, transport):
txaio.add_callbacks(
d,
lambda _: txaio.as_future(self.onConnect),
None,
lambda fail: self._swallow_error(fail, "While calling 'onConnect'")
)
@public
@@ -511,7 +511,6 @@ def onMessage(self, msg):
)
elif isinstance(msg, message.Abort):
# fire callback and close the transport
details = types.CloseDetails(msg.reason, msg.message)
d = txaio.as_future(self.onLeave, details)
@@ -1654,7 +1653,7 @@ def at_most_one(name):
# here we check that any duplicate keys have the same values
authextra = authenticator.authextra
merged = self._merged_authextra()
for k, v in merged:
for k, v in merged.items():
if k in authextra and authextra[k] != v:
raise ValueError(
"Inconsistent authextra values for '{}': '{}' vs '{}'".format(
@@ -25,11 +25,16 @@
###############################################################################
from __future__ import absolute_import
import hashlib
from mock import Mock
from autobahn.wamp.cryptosign import _makepad, HAS_CRYPTOSIGN
from autobahn.wamp import types
from autobahn.wamp.auth import create_authenticator
if HAS_CRYPTOSIGN:
from autobahn.wamp.cryptosign import SigningKey
from nacl.encoding import HexEncoder
import tempfile
@@ -41,13 +46,48 @@
gQAAAAtzc2gtZWQyNTUxOQAAACAa38i/4dNWFuZN/72QAJbyOwZvkUyML/u2b2B1uW4RbQ
AAAEBNV9l6aPVVaWYgpthJwM5YJWhRjXKet1PcfHMt4oBFEBrfyL/h01YW5k3/vZAAlvI7
Bm+RTIwv+7ZvYHW5bhFtAAAAFXNvbWV1c2VyQGZ1bmt0aGF0LmNvbQ==
-----END OPENSSH PRIVATE KEY-----
'''
-----END OPENSSH PRIVATE KEY-----'''
pubkey = '''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJVp3hjHwIQyEladzd8mFcf0YSXcmyKS3qMLB7VqTQKm someuser@example.com
'''
@unittest.skipIf(not HAS_CRYPTOSIGN, 'nacl library not present')
class TestAuth(unittest.TestCase):
def setUp(self):
self.key = SigningKey.from_ssh_data(keybody)
self.privkey_hex = self.key._key.encode(encoder=HexEncoder)
m = hashlib.sha256()
m.update("some TLS message")
self.channel_id = m.digest()
def test_valid(self):
session = Mock()
session._transport.get_channel_id = Mock(return_value=self.channel_id)
challenge = types.Challenge(u"ticket", dict(challenge="ff" * 32))
signed = self.key.sign_challenge(session, challenge)
self.assertEqual(
u'9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00',
signed.result,
)
def test_authenticator(self):
authenticator = create_authenticator(
u"cryptosign",
authid="someone",
privkey=self.privkey_hex,
)
session = Mock()
session._transport.get_channel_id = Mock(return_value=self.channel_id)
challenge = types.Challenge(u"cryptosign", dict(challenge="ff" * 32))
reply = authenticator.on_challenge(session, challenge)
self.assertEqual(
reply.result,
u'9b6f41540c9b95b4b7b281c3042fa9c54cef43c842d62ea3fd6030fcb66e70b3e80d49d44c29d1635da9348d02ec93f3ed1ef227dfb59a07b580095c2b82f80f9d16ca518aa0c2b707f2b2a609edeca73bca8dd59817a633f35574ac6fd80d00',
)
class TestKey(unittest.TestCase):
def test_pad(self):

0 comments on commit 6bcca8e

Please sign in to comment.