diff options
author | Will Thompson <will.thompson@collabora.co.uk> | 2013-04-12 10:59:09 +0100 |
---|---|---|
committer | Will Thompson <will.thompson@collabora.co.uk> | 2013-04-12 10:59:09 +0100 |
commit | 71f9f850be1eefc949c400a3081a7edaa336aec2 (patch) | |
tree | d95cb6787fc365f5a0309460dde2d9b160346759 | |
parent | 9ee8b96ad91af52936f5bee5b9a472de645c2363 (diff) | |
download | telepathy-haze-71f9f850be1eefc949c400a3081a7edaa336aec2.tar.gz |
Add sasl/telepathy-password.py from Gabble
This exercises the happy path of Stefan's patches to support
purple_account_request_password().
-rw-r--r-- | tests/twisted/Makefile.am | 2 | ||||
-rw-r--r-- | tests/twisted/sasl/saslutil.py | 135 | ||||
-rw-r--r-- | tests/twisted/sasl/telepathy-password.py | 53 |
3 files changed, 190 insertions, 0 deletions
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index ddf31ae..3777c89 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -11,6 +11,7 @@ TWISTED_TESTS = \ roster/publish.py \ roster/removed-from-rp-subscribe.py \ roster/subscribe.py \ + sasl/telepathy-password.py \ text/destroy.py \ text/ensure.py \ text/initiate-requestotron.py \ @@ -40,6 +41,7 @@ EXTRA_DIST = \ $(TWISTED_TESTS) \ constants.py \ hazetest.py \ + sasl/saslutil.py \ servicetest.py \ ns.py diff --git a/tests/twisted/sasl/saslutil.py b/tests/twisted/sasl/saslutil.py new file mode 100644 index 0000000..c09ea9b --- /dev/null +++ b/tests/twisted/sasl/saslutil.py @@ -0,0 +1,135 @@ +# hey, Python: encoding: utf-8 +from hazetest import XmppAuthenticator +from base64 import b64decode, b64encode +from twisted.words.xish import domish +import constants as cs +import ns +from servicetest import (ProxyWrapper, EventPattern, assertEquals, + assertLength, Event) + +class SaslChannelWrapper(ProxyWrapper): + def __init__(self, object, default=cs.CHANNEL, interfaces={ + "ServerAuthentication" : cs.CHANNEL_TYPE_SERVER_AUTHENTICATION, + "SASLAuthentication" : cs.CHANNEL_IFACE_SASL_AUTH}): + ProxyWrapper.__init__(self, object, default, interfaces) + +class SaslEventAuthenticator(XmppAuthenticator): + def __init__(self, jid, mechanisms): + XmppAuthenticator.__init__(self, jid, '') + self._mechanisms = mechanisms + + def streamSASL(self): + XmppAuthenticator.streamSASL(self) + + self.xmlstream.addObserver("/response", self._response) + self.xmlstream.addObserver("/abort", self._abort) + + def failure(self, fail_str): + reply = domish.Element((ns.NS_XMPP_SASL, 'failure')) + reply.addElement(fail_str) + self.xmlstream.send(reply) + self.xmlstream.reset() + + def abort(self): + self.failure('abort') + + def not_authorized(self): + self.failure('not-authorized') + + def success(self, data=None): + reply = domish.Element((ns.NS_XMPP_SASL, 'success')) + + if data is not None: + reply.addContent(b64encode(data)) + + self.xmlstream.send(reply) + self.authenticated=True + self.xmlstream.reset() + + def challenge(self, data): + reply = domish.Element((ns.NS_XMPP_SASL, 'challenge')) + reply.addContent(b64encode(data)) + self.xmlstream.send(reply) + + def auth(self, auth): + # Special case in XMPP: '=' means a zero-byte blob, whereas an empty + # or self-terminating XML element means no initial response. + # (RFC 3920 ยง6.2 (3)) + if str(auth) == '': + self._event_func(Event('sasl-auth', authenticator=self, + has_initial_response=False, + initial_response=None, + xml=auth)) + elif str(auth) == '=': + self._event_func(Event('sasl-auth', authenticator=self, + has_initial_response=False, + initial_response=None, + xml=auth)) + else: + self._event_func(Event('sasl-auth', authenticator=self, + has_initial_response=True, + initial_response=b64decode(str(auth)), + xml=auth)) + + def _response(self, response): + self._event_func(Event('sasl-response', authenticator=self, + response=b64decode(str(response)), + xml=response)) + + def _abort(self, abort): + self._event_func(Event('sasl-abort', authenticator=self, + xml=abort)) + +def connect_and_get_sasl_channel(q, bus, conn): + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]) + + return expect_sasl_channel(q, bus, conn) + +def expect_sasl_channel(q, bus, conn): + old_signal, new_signal = q.expect_many( + EventPattern('dbus-signal', signal='NewChannel', + predicate=lambda e: + e.args[1] == cs.CHANNEL_TYPE_SERVER_AUTHENTICATION), + EventPattern('dbus-signal', signal='NewChannels', + predicate=lambda e: + e.args[0][0][1].get(cs.CHANNEL_TYPE) == + cs.CHANNEL_TYPE_SERVER_AUTHENTICATION), + ) + + path, type, handle_type, handle, suppress_handler = old_signal.args + + chan = SaslChannelWrapper(bus.get_object(conn.bus_name, path)) + assertLength(1, new_signal.args[0]) + assertEquals(path, new_signal.args[0][0][0]) + props = new_signal.args[0][0][1] + + assertEquals(cs.CHANNEL_IFACE_SASL_AUTH, props.get(cs.AUTH_METHOD)) + return chan, props + +def abort_auth(q, chan, reason, message): + reason_err_map = { + cs.SASL_ABORT_REASON_USER_ABORT : cs.CANCELLED, + cs.SASL_ABORT_REASON_INVALID_CHALLENGE : cs.SERVICE_CONFUSED } + + mapped_error = reason_err_map.get(reason, cs.CANCELLED) + + chan.SASLAuthentication.AbortSASL(reason, message) + + ssc, ce, _ = q.expect_many( + EventPattern( + 'dbus-signal', signal='SASLStatusChanged', + interface=cs.CHANNEL_IFACE_SASL_AUTH, + predicate=lambda e: e.args[0] == cs.SASL_STATUS_CLIENT_FAILED), + EventPattern('dbus-signal', signal='ConnectionError'), + EventPattern( + 'dbus-signal', signal="StatusChanged", + args=[cs.CONN_STATUS_DISCONNECTED, + cs.CSR_AUTHENTICATION_FAILED])) + + assertEquals(cs.SASL_STATUS_CLIENT_FAILED, ssc.args[0]) + assertEquals(mapped_error, ssc.args[1]) + assertEquals(message, ssc.args[2].get('debug-message')), + assertEquals(mapped_error, ce.args[0]) diff --git a/tests/twisted/sasl/telepathy-password.py b/tests/twisted/sasl/telepathy-password.py new file mode 100644 index 0000000..f95a3d0 --- /dev/null +++ b/tests/twisted/sasl/telepathy-password.py @@ -0,0 +1,53 @@ +""" +Test the server sasl channel with the X-TELEPATHY-PASSWORD mechanism. +""" + +from servicetest import call_async, EventPattern +from hazetest import exec_test +import constants as cs +from saslutil import connect_and_get_sasl_channel + +PASSWORD = "pass" + +def test_close_straight_after_accept(q, bus, conn, stream): + chan, props = connect_and_get_sasl_channel(q, bus, conn) + + call_async(q, chan.SASLAuthentication, 'StartMechanismWithData', + 'X-TELEPATHY-PASSWORD', PASSWORD) + + # In_Progress appears before StartMechanismWithData returns + q.expect('dbus-signal', signal='SASLStatusChanged', + interface=cs.CHANNEL_IFACE_SASL_AUTH, + args=[cs.SASL_STATUS_IN_PROGRESS, '', {}]) + + # Different order to Gabble + q.expect_many( + EventPattern('dbus-return', method='StartMechanismWithData'), + EventPattern('dbus-signal', signal='SASLStatusChanged', + interface=cs.CHANNEL_IFACE_SASL_AUTH, + args=[cs.SASL_STATUS_SERVER_SUCCEEDED, '', {}]), + ) + + # fd.o#32278: + # When this was breaking, gabble received AcceptSASL and told the + # success_async GAsyncResult to complete in an idle. But, before + # the result got its callback called, Close was also received and + # the auth manager cleared its channel. When the idle function was + # finally reached it saw it no longer had a channel (it had been + # cleared in the closed callback) and thought it should be + # chaining up to the wocky auth registry but of course it should + # be calling the channel finish function. + call_async(q, chan.SASLAuthentication, 'AcceptSASL') + call_async(q, chan, 'Close') + + q.expect('dbus-signal', signal='SASLStatusChanged', + interface=cs.CHANNEL_IFACE_SASL_AUTH, + args=[cs.SASL_STATUS_SUCCEEDED, '', {}]) + + e = q.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + +if __name__ == '__main__': + exec_test(test_close_straight_after_accept, + {'password': None, 'account' : "test@example.org/Resource"}, + do_connect=False) |