Changeset 66:b713f442b222 in ralphm-patches for client_listen_authenticator.patch


Ignore:
Timestamp:
Sep 1, 2012, 12:40:53 PM (8 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Add many tests, docstrings for authenticator, make example functional.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • client_listen_authenticator.patch

    r65 r66  
    11# HG changeset patch
    2 # Parent ad0f4165244b1c661023f03d8f7557fe5352337f
     2# Parent a1648553ea06b7f0b38fec775db71ac03782e3d6
    33Add authenticator for accepting XMPP client connections.
    44
    55The new authenticator XMPPClientListenAuthenticator is to be used together
    6 with an `XmlStream` created for an incoming XMPP stream. It handles the
    7 SASL PLAIN mechanism only.
     6with an `XmlStream` created for an incoming XMPP stream. It uses the
     7new initializers for SASL (PLAIN only), resource binding and session
     8establishement.
    89
    910This authenticator needs at least one Twisted Cred portal to hold the
     
    1314logout callback is called.
    1415
    15 TODO:
    16 
    17  * Add tests.
    18  * Add docstrings.
    19 
    2016diff --git a/wokkel/client.py b/wokkel/client.py
    2117--- a/wokkel/client.py
    2218+++ b/wokkel/client.py
    23 @@ -10,14 +10,28 @@
     19@@ -10,14 +10,27 @@
    2420 that should probably eventually move there.
    2521 """
    2622 
    2723+import base64
    28 +
    29 +from zope.interface import Interface
    3024+
    3125 from twisted.application import service
     
    4034 
    4135 from wokkel import generic
     36+from wokkel.iwokkel import IUserSession
    4237 from wokkel.subprotocols import StreamManager
    4338 
     
    5247     """
    5348     Check what authentication methods are available.
    54 @@ -51,7 +65,7 @@
     49@@ -51,7 +64,7 @@
    5550     autentication.
    5651     """
     
    6156     def __init__(self, jid, password):
    6257         xmlstream.ConnectAuthenticator.__init__(self, jid.host)
    63 @@ -186,3 +200,210 @@
     58@@ -186,3 +199,246 @@
    6459     c = XMPPClientConnector(reactor, domain, factory)
    6560     c.connect()
     
    7469+
    7570+
    76 +class IAccount(Interface):
    77 +    pass
    78 +
    79 +
    80 +
    81 +class SASLReceivingInitializer(object):
     71+
     72+class AuthorizationIdentifierNotSupported(Exception):
     73+    """
     74+    Authorization Identifiers are not supported.
     75+    """
     76+
     77+
     78+
     79+class SASLReceivingInitializer(generic.BaseReceivingInitializer):
     80+    """
     81+    Stream initializer for SASL authentication, receiving side.
     82+    """
     83+
    8284+    required = True
    8385+
    84 +    def __init__(self, xs, portal):
    85 +        self.xmlstream = xs
     86+    def __init__(self, name, xs, portal):
     87+        generic.BaseReceivingInitializer.__init__(self, name, xs)
    8688+        self.portal = portal
    87 +        self.deferred = defer.Deferred()
    8889+        self.failureGrace = 3
    8990+
     
    9697+
    9798+    def initialize(self):
    98 +        self.xmlstream.addObserver(XPATH_AUTH, self.onAuth)
     99+        self.xmlstream.avatar = None
     100+        self.xmlstream.addObserver(XPATH_AUTH, self._onAuth)
    99101+        return self.deferred
    100102+
    101103+
    102 +    def onAuth(self, auth):
     104+    def _onAuth(self, auth):
     105+        """
     106+        Called when the start of the SASL negotiation is received.
     107+
     108+        @type auth: L{domish.Element}.
     109+        """
    103110+        auth.handled = True
    104111+
     
    107114+            self.xmlstream.send(response)
    108115+            self.xmlstream.reset()
     116+            self.deferred.callback(xmlstream.Reset)
    109117+
    110118+        def eb(failure):
     
    113121+            elif failure.check(InvalidMechanism):
    114122+                condition = 'invalid-mechanism'
     123+            elif failure.check(AuthorizationIdentifierNotSupported):
     124+                condition = 'invalid-authz'
    115125+            else:
    116126+                log.err(failure)
     
    124134+            self.failureGrace -= 1
    125135+            if self.failureGrace == 0:
    126 +                raise error.StreamError('policy-violation')
    127 +                #self.xmlstream.sendStreamError(exc)
    128 +
    129 +        d = defer.maybeDeferred(self.doAuth, auth)
     136+                self.deferred.errback(error.StreamError('policy-violation'))
     137+            else:
     138+                return
     139+
     140+        d = defer.maybeDeferred(self._doAuth, auth)
    130141+        d.addCallbacks(cb, eb)
    131 +        d.chainDeferred(self.deferred)
    132 +
    133 +
    134 +    def credentialsFromPlain(self, auth):
     142+
     143+
     144+    def _credentialsFromPlain(self, auth):
     145+        """
     146+        Create credentials from the initial response for PLAIN.
     147+        """
    135148+        initialResponse = base64.b64decode(unicode(auth))
    136149+        authzid, authcid, passwd = initialResponse.split('\x00')
    137150+
    138 +        # FIXME: bail if authzid is set
    139 +
    140 +        creds = credentials.UsernamePassword(username=authcid.encode('utf-8'),
     151+        if authzid:
     152+            raise AuthorizationIdentifierNotSupported()
     153+
     154+        creds = credentials.UsernamePassword(username=authcid,
    141155+                                             password=passwd)
    142156+        return creds
    143157+
    144158+
    145 +    def doAuth(self, auth):
    146 +
     159+    def _doAuth(self, auth):
     160+        """
     161+        Start authentication.
     162+        """
    147163+        if auth.getAttribute('mechanism') != 'PLAIN':
    148164+            raise InvalidMechanism()
    149165+
    150 +        creds = self.credentialsFromPlain(auth)
     166+        creds = self._credentialsFromPlain(auth)
    151167+
    152168+        def cb((iface, avatar, logout)):
     
    155171+                                       lambda _: logout())
    156172+
    157 +        d = self.portal.login(creds, None, IAccount)
     173+        d = self.portal.login(creds, self.xmlstream, IUserSession)
    158174+        d.addCallback(cb)
    159175+        return d
     
    161177+
    162178+
    163 +class BindReceivingInitializer(object):
     179+class BindReceivingInitializer(generic.BaseReceivingInitializer):
     180+    """
     181+    Stream initializer for resource binding, receiving side.
     182+    """
     183+
    164184+    required = True
    165 +
    166 +    def __init__(self, xs):
    167 +        self.xmlstream = xs
    168 +        self.deferred = defer.Deferred()
    169 +
    170185+
    171186+    def getFeatures(self):
     
    186201+            response.addElement((client.NS_XMPP_BIND, 'bind'))
    187202+            response.bind.addElement((client.NS_XMPP_BIND, 'jid'),
    188 +                                  content=boundJID.full())
     203+                                     content=boundJID.full())
    189204+
    190205+            return response
    191 +
    192 +        def eb(failure):
    193 +            if not isinstance(failure, error.StanzaError):
    194 +                log.msg(failure)
    195 +                exc = error.StanzaError('internal-server-error')
    196 +            else:
    197 +                exc = failure.value
    198 +
    199 +            return exc.toResponse(iq)
    200206+
    201207+        iq.handled = True
     
    203209+        d = self.xmlstream.avatar.bindResource(resource)
    204210+        d.addCallback(cb)
    205 +        d.addErrback(eb)
    206211+        d.addCallback(self.xmlstream.send)
    207212+        d.chainDeferred(self.deferred)
     
    209214+
    210215+
    211 +class SessionReceivingInitializer(object):
     216+class SessionReceivingInitializer(generic.BaseReceivingInitializer):
     217+    """
     218+    Stream initializer for session establishment, receiving side.
     219+
     220+    This is mostly a no-op and just returns a result stanza. If resource
     221+    binding hasn't yet completed, this will return a stanza error with the
     222+    condition C{'forbidden'}.
     223+
     224+    Note that RFC 6120 deprecated the session establishment protocol. This
     225+    is provided for backwards compatibility.
     226+    """
     227+
    212228+    required = False
    213 +
    214 +    def __init__(self, xs):
    215 +        self.xmlstream = xs
    216 +        self.deferred = defer.Deferred()
    217 +
    218229+
    219230+    def getFeatures(self):
     
    233244+
    234245+        if self.xmlstream.otherEntity:
    235 +            reply = xmlstream.toResponse(iq)
     246+            reply = xmlstream.toResponse(iq, 'result')
    236247+        else:
    237248+            reply = error.StanzaError('forbidden').toResponse(iq)
     
    242253+
    243254+class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator):
     255+    """
     256+    XML Stream authenticator for XMPP clients, server side.
     257+
     258+    @ivar portals: Mapping of server JIDs to Cred Portals.
     259+    @type portals: C{dict} of L{twisted.words.protocols.jabber.jid.JID} to
     260+        L{twisted.cred.portal.Portal}.
     261+    """
     262+
    244263+    namespace = NS_CLIENT
    245264+
     
    251270+
    252271+    def getInitializers(self):
     272+        """
     273+        Return initializers based on previously completed initializers.
     274+
     275+        This has three stages: 1. SASL, 2. Resource binding and session
     276+        establishment. 3. Completed. Note that session establishment
     277+        is optional.
     278+        """
    253279+        if not self.completedInitializers:
    254 +            return [SASLReceivingInitializer(self.xmlstream, self.portal)]
    255 +        elif isinstance(self.completedInitializers[-1],
    256 +                        SASLReceivingInitializer):
    257 +            return [BindReceivingInitializer(self.xmlstream),
    258 +                    SessionReceivingInitializer(self.xmlstream)]
     280+            return [SASLReceivingInitializer('sasl', self.xmlstream, self.portal)]
     281+        elif self.completedInitializers[-1] == 'sasl':
     282+            return [BindReceivingInitializer('bind', self.xmlstream),
     283+                    SessionReceivingInitializer('session', self.xmlstream)]
    259284+        else:
    260285+            return []
     
    262287+
    263288+    def checkStream(self):
     289+        """
     290+        Check that the stream header has proper addressing.
     291+
     292+        The C{'to'} attribute must be present and there should have a matching
     293+        portal in L{portals}.
     294+        """
    264295+        generic.FeatureListenAuthenticator.checkStream(self)
    265296+
     
    272303+        except KeyError:
    273304+            raise error.StreamError('host-unknown')
     305diff --git a/wokkel/generic.py b/wokkel/generic.py
     306--- a/wokkel/generic.py
     307+++ b/wokkel/generic.py
     308@@ -465,6 +465,7 @@
     309 
     310     def __init__(self):
     311         self.completedInitializers = []
     312+        self._initialized = False
     313 
     314 
     315     def _onElementFallback(self, element):
     316@@ -556,11 +557,12 @@
     317 
     318         self.xmlstream.send(features)
     319 
     320-        if not required:
     321+        if not required and not self._initialized:
     322             # There are no required initializers anymore. This stream is
     323             # now ready for the exchange of stanzas.
     324             self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
     325             self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
     326+            self._initialized = True
     327 
     328         if ds:
     329             d = defer.DeferredList(ds, fireOnOneCallback=True,
     330diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
     331--- a/wokkel/iwokkel.py
     332+++ b/wokkel/iwokkel.py
     333@@ -985,6 +985,45 @@
     334 
     335 
     336 
     337+class IUserSession(Interface):
     338+    def loggedIn(realm, mind):
     339+        """
     340+        Called by the realm when login occurs.
     341+
     342+        @param realm: The realm though which login is occurring.
     343+        @param mind: The mind object.
     344+        """
     345+
     346+
     347+    def bindResource(resource):
     348+        """
     349+        Bind a resource to this session.
     350+
     351+        @type resource: C{unicode}.
     352+        """
     353+
     354+
     355+    def logout():
     356+        """
     357+        End this session.
     358+
     359+        This is called when the stream is disconnected.
     360+        """
     361+
     362+
     363+    def send(element):
     364+        """
     365+        Called when the client sends a stanza.
     366+        """
     367+
     368+
     369+    def receive(element):
     370+        """
     371+        Have the client receive a stanza.
     372+        """
     373+
     374+
     375+
     376 class IReceivingInitializer(Interface):
     377     """
     378     Interface for XMPP stream initializers for receiving entities.
    274379diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
    275380--- a/wokkel/test/test_client.py
    276381+++ b/wokkel/test/test_client.py
    277 @@ -5,14 +5,21 @@
     382@@ -5,16 +5,29 @@
    278383 Tests for L{wokkel.client}.
    279384 """
    280385 
     386+from base64 import b64encode
     387+
    281388+from zope.interface import implements
    282389+
     
    284391+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
    285392 from twisted.internet import defer
     393+from twisted.test import proto_helpers
    286394 from twisted.trial import unittest
    287  from twisted.words.protocols.jabber import xmlstream
     395-from twisted.words.protocols.jabber import xmlstream
     396+from twisted.words.protocols.jabber import error, xmlstream
     397+from twisted.words.protocols.jabber.client import NS_XMPP_BIND
     398+from twisted.words.protocols.jabber.client import NS_XMPP_SESSION
    288399 from twisted.words.protocols.jabber.client import XMPPAuthenticator
    289400 from twisted.words.protocols.jabber.jid import JID
     
    296407+from twisted.words.xish import xpath
    297408 
    298  from wokkel import client
    299  
    300 @@ -155,3 +162,167 @@
     409-from wokkel import client
     410+from wokkel import client, iwokkel
     411+from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator
     412 
     413 class XMPPClientTest(unittest.TestCase):
     414     """
     415@@ -155,3 +168,505 @@
    301416         self.assertEqual(factory.deferred, d2)
    302417 
     
    304419+
    305420+
    306 +class TestAccount(object):
    307 +    implements(client.IAccount)
     421+
     422+class TestSession(object):
     423+    implements(iwokkel.IUserSession)
     424+
     425+    def __init__(self, domain, user):
     426+        self.domain = domain
     427+        self.user = user
     428+
     429+
     430+    def bindResource(self, resource):
     431+        return defer.succeed(JID(tuple=(self.user, self.domain, resource)))
     432+
    308433+
    309434+
     
    314439+    logoutCalled = False
    315440+
     441+    def __init__(self, domain):
     442+        self.domain = domain
     443+
     444+
    316445+    def requestAvatar(self, avatarId, mind, *interfaces):
    317 +        return (client.IAccount, TestAccount(), self.logout)
     446+        return (iwokkel.IUserSession,
     447+                TestSession(self.domain, avatarId.decode('utf-8')),
     448+                self.logout)
    318449+
    319450+
     
    322453+
    323454+
    324 +class TestableXmlStream(xmlstream.XmlStream):
    325 +
    326 +    def __init__(self, authenticator):
    327 +        xmlstream.XmlStream.__init__(self, authenticator)
    328 +        self.headerSent = False
    329 +        self.footerSent = False
    330 +        self.streamErrors = []
    331 +        self.output = []
    332 +
    333 +
    334 +    def reset(self):
    335 +        xmlstream.XmlStream.reset(self)
    336 +        self.headerSent = False
    337 +
    338 +
    339 +    def sendHeader(self):
    340 +        self.headerSent = True
    341 +
    342 +
    343 +    def sendFooter(self):
    344 +        self.footerSent = True
    345 +
    346 +
    347 +    def sendStreamError(self, streamError):
    348 +        self.streamErrors.append(streamError)
    349 +
    350 +
    351 +    def send(self, obj):
    352 +        self.output.append(obj)
     455+
     456+class TestableFeatureListenAuthenticator(FeatureListenAuthenticator):
     457+    namespace = 'jabber:client'
     458+
     459+    initialized = None
     460+
     461+    def __init__(self, getInitializers):
     462+        """
     463+        Set up authenticator.
     464+
     465+        @param getInitializers: Function to override the getInitializers
     466+            method. It will receive C{self} as the only argument.
     467+        """
     468+        FeatureListenAuthenticator.__init__(self)
     469+
     470+        import types
     471+        self.getInitializers = types.MethodType(getInitializers, self)
     472+
     473+        xs = TestableXmlStream(self)
     474+        xs.makeConnection(proto_helpers.StringTransport())
     475+
     476+
     477+    def streamStarted(self, rootElement):
     478+        """
     479+        Set up observers for authentication events.
     480+        """
     481+        def authenticated(_):
     482+            self.initialized = True
     483+
     484+        self.xmlstream.addObserver(STREAM_AUTHD_EVENT, authenticated)
     485+        FeatureListenAuthenticator.streamStarted(self, rootElement)
     486+
     487+
     488+
     489+class SASLReceivingInitializerTest(unittest.TestCase):
     490+    """
     491+    Tests for L{client.SASLReceivingInitializer}.
     492+    """
     493+
     494+    def setUp(self):
     495+        realm = TestRealm(u'example.org')
     496+        checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret')
     497+        self.portal = portal = Portal(realm, (checker,))
     498+
     499+        def getInitializers(self):
     500+            self.initializer = client.SASLReceivingInitializer('sasl',
     501+                                                               self.xmlstream,
     502+                                                               portal)
     503+            return [self.initializer]
     504+
     505+        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
     506+        self.xmlstream = self.authenticator.xmlstream
     507+
     508+
     509+    def test_getFeatures(self):
     510+        """
     511+        The stream features list SASL with the PLAIN mechanism.
     512+        """
     513+        xs = self.xmlstream
     514+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     515+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     516+                         "to='example.org' "
     517+                         "version='1.0'>")
     518+
     519+        self.assertTrue(xs.headerSent)
     520+
     521+        # Check SASL mechanisms
     522+        features = xs.output[-1]
     523+        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
     524+                                          "/mechanisms[@xmlns='%s']"
     525+                                          "/mechanism[@xmlns='%s' and "
     526+                                                     "text()='PLAIN']" %
     527+                                          (NS_STREAMS, NS_XMPP_SASL, NS_XMPP_SASL),
     528+                                      features))
     529+
     530+
     531+    def test_auth(self):
     532+        """
     533+        Authenticating causes an avatar to be set on the authenticator.
     534+        """
     535+        xs = self.xmlstream
     536+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     537+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     538+                         "to='example.org' "
     539+                         "version='1.0'>")
     540+        xs.output = []
     541+        response = b64encode('\x00'.join(['', 'test', 'secret']))
     542+        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     543+                         "mechanism='PLAIN'>%s</auth>" % response)
     544+        self.assertTrue(iwokkel.IUserSession.providedBy(self.xmlstream.avatar))
     545+        self.assertFalse(xs.headerSent)
     546+        self.assertEqual(1, len(xs.output))
     547+        self.assertFalse(self.authenticator.initialized)
     548+
     549+
     550+    def test_authInvalidMechanism(self):
     551+        """
     552+        Authenticating with an invalid SASL mechanism causes a streamError.
     553+        """
     554+        xs = self.xmlstream
     555+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     556+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     557+                         "to='example.org' "
     558+                         "version='1.0'>")
     559+        xs.output = []
     560+        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     561+                         "mechanism='unknown'/>")
     562+        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     563+                                       "/invalid-mechanism[@xmlns='%s']" %
     564+                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
     565+                                      xs.output[-1]))
     566+
     567+
     568+    def test_authFail(self):
     569+        """
     570+        Authenticating causes an avatar to be set on the authenticator.
     571+        """
     572+        xs = self.xmlstream
     573+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     574+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     575+                         "to='example.org' "
     576+                         "version='1.0'>")
     577+        xs.output = []
     578+        response = b64encode('\x00'.join(['', 'test', 'bad']))
     579+        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     580+                         "mechanism='PLAIN'>%s</auth>" % response)
     581+        self.assertIdentical(None, self.xmlstream.avatar)
     582+        self.assertTrue(xs.headerSent)
     583+
     584+        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     585+                                       "/not-authorized[@xmlns='%s']" %
     586+                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
     587+                                      xs.output[-1]))
     588+
     589+        self.assertFalse(self.authenticator.initialized)
     590+
     591+
     592+    def test_authFailMultiple(self):
     593+        """
     594+        Authenticating causes an avatar to be set on the authenticator.
     595+        """
     596+        xs = self.xmlstream
     597+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     598+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     599+                         "to='example.org' "
     600+                         "version='1.0'>")
     601+
     602+        xs.output = []
     603+        response = b64encode('\x00'.join(['', 'test', 'bad']))
     604+
     605+        attempts = self.authenticator.initializer.failureGrace
     606+        for attempt in xrange(attempts):
     607+            xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     608+                             "mechanism='PLAIN'>%s</auth>" % response)
     609+            self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     610+                                           "/not-authorized[@xmlns='%s']" %
     611+                                           (NS_XMPP_SASL, NS_XMPP_SASL)),
     612+                                          xs.output[-1]))
     613+        self.xmlstream.assertStreamError(self, condition='policy-violation')
     614+        self.assertFalse(self.authenticator.initialized)
     615+
     616+
     617+    def test_authException(self):
     618+        """
     619+        Other authentication exceptions yield temporary-auth-failure.
     620+        """
     621+        class Error(Exception):
     622+            pass
     623+
     624+        def login(credentials, mind, *interfaces):
     625+            raise Error()
     626+
     627+        self.portal.login = login
     628+
     629+        xs = self.xmlstream
     630+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     631+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     632+                         "to='example.org' "
     633+                         "version='1.0'>")
     634+        xs.output = []
     635+        response = b64encode('\x00'.join(['', 'test', 'bad']))
     636+        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     637+                         "mechanism='PLAIN'>%s</auth>" % response)
     638+        self.assertIdentical(None, self.xmlstream.avatar)
     639+        self.assertTrue(xs.headerSent)
     640+
     641+        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     642+                                       "/temporary-auth-failure[@xmlns='%s']" %
     643+                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
     644+                                      xs.output[-1]))
     645+        self.assertFalse(self.authenticator.initialized)
     646+        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
     647+
     648+
     649+    def test_authNonAsciiUsername(self):
     650+        """
     651+        Authenticating causes an avatar to be set on the authenticator.
     652+        """
     653+        xs = self.xmlstream
     654+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     655+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     656+                         "to='example.org' "
     657+                         "version='1.0'>")
     658+        xs.output = []
     659+        response = b64encode('\x00'.join(['', 'test\xa1', 'secret']))
     660+        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     661+                         "mechanism='PLAIN'>%s</auth>" % response)
     662+        self.assertIdentical(None, self.xmlstream.avatar)
     663+        self.assertTrue(xs.headerSent)
     664+
     665+        self.assertEqual(1, len(xs.output))
     666+        failure = xs.output[-1]
     667+        condition = failure.elements().next()
     668+        self.assertEqual('not-authorized', condition.name)
     669+
     670+
     671+    def test_authAuthorizationIdentifier(self):
     672+        """
     673+        Authorization Identifiers are not supported.
     674+        """
     675+        xs = self.xmlstream
     676+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     677+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     678+                         "to='example.org' "
     679+                         "version='1.0'>")
     680+        xs.output = []
     681+        response = b64encode('\x00'.join(['other', 'test', 'secret']))
     682+        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     683+                         "mechanism='PLAIN'>%s</auth>" % response)
     684+        self.assertIdentical(None, self.xmlstream.avatar)
     685+        self.assertTrue(xs.headerSent)
     686+
     687+        self.assertEqual(1, len(xs.output))
     688+        failure = xs.output[-1]
     689+        condition = failure.elements().next()
     690+        self.assertEqual('invalid-authz', condition.name)
     691+
     692+
     693+
     694+class BindReceivingInitializerTest(unittest.TestCase):
     695+    """
     696+    Tests for L{client.BindReceivingInitializer}.
     697+    """
     698+
     699+    def setUp(self):
     700+        def getInitializers(self):
     701+            self.initializer = client.BindReceivingInitializer('bind',
     702+                                                               self.xmlstream)
     703+            return [self.initializer]
     704+
     705+        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
     706+        self.xmlstream = self.authenticator.xmlstream
     707+        self.xmlstream.avatar = TestSession('example.org', 'test')
     708+
     709+
     710+    def test_getFeatures(self):
     711+        """
     712+        The stream features include resource binding.
     713+        """
     714+        xs = self.xmlstream
     715+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     716+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     717+                         "to='example.org' "
     718+                         "version='1.0'>")
     719+
     720+        features = xs.output[-1]
     721+        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
     722+                                          "/bind[@xmlns='%s']" %
     723+                                          (NS_STREAMS, NS_XMPP_BIND),
     724+                                      features))
     725+
     726+
     727+    def test_bind(self):
     728+        """
     729+        To bind a resource, the avatar is requested one and a JID is returned.
     730+        """
     731+        xs = self.xmlstream
     732+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     733+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     734+                         "to='example.org' "
     735+                         "version='1.0'>")
     736+
     737+        # This initializer is required.
     738+        self.assertFalse(self.authenticator.initialized)
     739+
     740+        xs.output = []
     741+        xs.dataReceived("""<iq type='set'>
     742+                             <bind xmlns='%s'>Home</bind>
     743+                           </iq>""" % NS_XMPP_BIND)
     744+
     745+        self.assertTrue(xs.headerSent, "Unexpected stream restart")
     746+
     747+        # In response to the bind request, a result iq and the new stream
     748+        # features are sent
     749+        response = xs.output[-2]
     750+        self.assertTrue(xpath.matches("/iq[@type='result']"
     751+                                          "/bind[@xmlns='%s']"
     752+                                          "/jid[@xmlns='%s' and "
     753+                                               "text()='%s']" %
     754+                                          (NS_XMPP_BIND,
     755+                                           NS_XMPP_BIND,
     756+                                           'test@example.org/Home'),
     757+                                      response))
     758+
     759+        self.assertTrue(self.authenticator.initialized)
     760+
     761+
     762+
     763+class SessionReceivingInitializerTest(unittest.TestCase):
     764+    """
     765+    Tests for L{client.SessionReceivingInitializer}.
     766+    """
     767+
     768+    def setUp(self):
     769+        def getInitializers(self):
     770+            self.initializer = client.SessionReceivingInitializer('session',
     771+                                                                  self.xmlstream)
     772+            return [self.initializer]
     773+
     774+        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
     775+        self.xmlstream = self.authenticator.xmlstream
     776+
     777+
     778+    def test_getFeatures(self):
     779+        """
     780+        The stream features include session establishment.
     781+        """
     782+        xs = self.xmlstream
     783+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     784+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     785+                         "to='example.org' "
     786+                         "version='1.0'>")
     787+
     788+        features = xs.output[-1]
     789+        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
     790+                                          "/session[@xmlns='%s']" %
     791+                                          (NS_STREAMS, NS_XMPP_SESSION),
     792+                                      features))
     793+
     794+    def test_session(self):
     795+        """
     796+        Session establishment is a no-op iq exchange.
     797+        """
     798+        xs = self.xmlstream
     799+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     800+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     801+                         "to='example.org' "
     802+                         "version='1.0'>")
     803+
     804+        # This initializer is not required.
     805+        self.assertTrue(self.authenticator.initialized)
     806+
     807+        # If resource binding has completed, xs.otherEntity has been set.
     808+        xs.otherEntity = JID('test@example.org/Home')
     809+
     810+        xs.output = []
     811+        xs.dataReceived("""<iq type='set'>
     812+                             <session xmlns='%s'/>
     813+                           </iq>""" % NS_XMPP_SESSION)
     814+
     815+        self.assertTrue(xs.headerSent, "Unexpected stream restart")
     816+
     817+        # In response to the session request, a result iq and the new stream
     818+        # features are sent
     819+        response = xs.output[-2]
     820+        self.assertTrue(xpath.matches("/iq[@type='result']", response))
     821+
     822+
     823+
     824+    def test_sessionNoBind(self):
     825+        """
     826+        Session establishment requires resource binding being completed.
     827+        """
     828+        xs = self.xmlstream
     829+        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     830+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     831+                         "to='example.org' "
     832+                         "version='1.0'>")
     833+
     834+        # This initializer is not required.
     835+        self.assertTrue(self.authenticator.initialized)
     836+
     837+        xs.output = []
     838+        xs.dataReceived("""<iq type='set'>
     839+                             <session xmlns='%s'/>
     840+                           </iq>""" % NS_XMPP_SESSION)
     841+
     842+        self.assertTrue(xs.headerSent, "Unexpected stream restart")
     843+
     844+        # In response to the session request, a result iq and the new stream
     845+        # features are sent
     846+        response = xs.output[-2]
     847+        stanzaError = error.exceptionFromStanza(response)
     848+        self.assertEqual('forbidden', stanzaError.condition)
     849+
    353850+
    354851+
     
    359856+
    360857+    def setUp(self):
    361 +        self.output = []
    362 +        realm = TestRealm()
    363 +        checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret')
    364 +        portal = Portal(realm, (checker,))
    365 +        portals = {JID('example.org'): portal}
     858+        portals = {JID('example.org'): None}
    366859+        self.authenticator = client.XMPPClientListenAuthenticator(portals)
    367860+        self.xmlstream = TestableXmlStream(self.authenticator)
     
    369862+
    370863+
    371 +    def loseConnection(self):
    372 +        """
    373 +        Stub loseConnection because we are a transport.
    374 +        """
    375 +        self.xmlstream.connectionLost("no reason")
    376 +
    377 +
    378 +    def test_streamStarted(self):
    379 +        xs = self.xmlstream
    380 +        xs.dataReceived("<stream:stream xmlns='jabber:client' "
    381 +                         "xmlns:stream='http://etherx.jabber.org/streams' "
    382 +                         "to='example.org' "
    383 +                         "version='1.0'>")
    384 +
    385 +        self.assertTrue(xs.headerSent)
    386 +
    387 +        # Extract SASL mechanisms
    388 +        features = xs.output[-1]
    389 +        self.assertEquals(NS_STREAMS, features.uri)
    390 +        self.assertEquals('features', features.name)
    391 +        parent = features.elements(NS_XMPP_SASL, 'mechanisms').next()
    392 +        mechanisms = set()
    393 +        for child in parent.elements(NS_XMPP_SASL, 'mechanism'):
    394 +            mechanisms.add(unicode(child))
    395 +
    396 +        self.assertIn('PLAIN', mechanisms)
     864+    def test_getInitializersStart(self):
     865+        """
     866+        Upon the start of negotation, only the SASL initializer is available.
     867+        """
     868+        inits = self.authenticator.getInitializers()
     869+        (init,) = inits
     870+        self.assertEqual('sasl', init.name)
     871+        self.assertIsInstance(init, client.SASLReceivingInitializer)
     872+
     873+
     874+    def test_getInitializersPostSASL(self):
     875+        """
     876+        After SASL, the resource binding and session establishment initializers
     877+        are available.
     878+        """
     879+        self.authenticator.completedInitializers = ['sasl']
     880+        inits = self.authenticator.getInitializers()
     881+        (bind, session) = inits
     882+        self.assertEqual('bind', bind.name)
     883+        self.assertIsInstance(bind, client.BindReceivingInitializer)
     884+        self.assertEqual('session', session.name)
     885+        self.assertIsInstance(session, client.SessionReceivingInitializer)
    397886+
    398887+
     
    406895+                         "to='example.org' "
    407896+                         "version='1.0'>")
    408 +        streamError = xs.streamErrors[-1]
    409 +        self.assertEquals('invalid-namespace', streamError.condition)
     897+        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
    410898+
    411899+
     
    418906+                         "xmlns:stream='http://etherx.jabber.org/streams' "
    419907+                         "version='1.0'>")
    420 +        streamError = xs.streamErrors[-1]
    421 +        self.assertEquals('improper-addressing', streamError.condition)
     908+        self.xmlstream.assertStreamError(self, condition='improper-addressing')
    422909+
    423910+
     
    431918+                         "to='example.com' "
    432919+                         "version='1.0'>")
    433 +        streamError = xs.streamErrors[-1]
    434 +        self.assertEquals('host-unknown', streamError.condition)
    435 +
    436 +
    437 +    def test_auth(self):
    438 +        """
    439 +        Authenticating causes an avatar to be set on the authenticator.
    440 +        """
    441 +        xs = self.xmlstream
    442 +        xs.dataReceived("<stream:stream xmlns='jabber:client' "
    443 +                         "xmlns:stream='http://etherx.jabber.org/streams' "
    444 +                         "to='example.org' "
    445 +                         "version='1.0'>")
    446 +        xs.output = []
    447 +        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
    448 +                         "mechanism='PLAIN'>AHRlc3QAc2VjcmV0</auth>")
    449 +        self.assertTrue(client.IAccount.providedBy(self.xmlstream.avatar))
    450 +
    451 +
    452 +    def test_authInvalidMechanism(self):
    453 +        """
    454 +        Authenticating with an invalid SASL mechanism causes a streamError.
    455 +        """
    456 +        xs = self.xmlstream
    457 +        xs.dataReceived("<stream:stream xmlns='jabber:client' "
    458 +                         "xmlns:stream='http://etherx.jabber.org/streams' "
    459 +                         "to='example.org' "
    460 +                         "version='1.0'>")
    461 +        xs.output = []
    462 +        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
    463 +                         "mechanism='unknown'/>")
    464 +        xpath.matches(("/failure[@xmlns='%s']"
    465 +                       "/invalid-mechanism[@xmlns='%s']" %
    466 +                       (NS_XMPP_SASL, NS_XMPP_SASL)),
    467 +                      xs.output[-1])
     920+        self.xmlstream.assertStreamError(self, condition='host-unknown')
     921diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
     922--- a/wokkel/test/test_generic.py
     923+++ b/wokkel/test/test_generic.py
     924@@ -331,15 +331,12 @@
     925     """
     926 
     927     def setUp(self):
     928-        self.gotAuthenticated = False
     929-        self.initFailure = None
     930+        self.gotAuthenticated = 0
     931         self.authenticator = generic.FeatureListenAuthenticator()
     932         self.authenticator.namespace = 'jabber:server'
     933         self.xmlstream = generic.TestableXmlStream(self.authenticator)
     934         self.xmlstream.addObserver('//event/stream/authd',
     935                                    self.onAuthenticated)
     936-        self.xmlstream.addObserver('//event/xmpp/initfailed',
     937-                                   self.onInitFailed)
     938 
     939         self.init = TestableReceivingInitializer('init', self.xmlstream,
     940                                                  'testns', 'test')
     941@@ -351,11 +348,7 @@
     942 
     943 
     944     def onAuthenticated(self, obj):
     945-        self.gotAuthenticated = True
     946-
     947-
     948-    def onInitFailed(self, failure):
     949-        self.initFailure = failure
     950+        self.gotAuthenticated += 1
     951 
     952 
     953     def test_getInitializers(self):
     954@@ -537,6 +530,38 @@
     955                         "  <query xmlns='jabber:iq:version'/>"
     956                         "</iq>")
     957 
     958+    def test_streamStartedInitializerNotRequired(self):
     959+        """
     960+        If no initializers are required, initialization is done.
     961+        """
     962+        self.init.required = False
     963+        xs = self.xmlstream
     964+        xs.makeConnection(proto_helpers.StringTransport())
     965+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     966+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     967+                         "from='example.com' to='example.org' id='12345' "
     968+                         "version='1.0'>")
     969+
     970+        self.assertEqual(1, self.gotAuthenticated)
     971+
     972+
     973+    def test_streamStartedInitializerNotRequiredDoneOnce(self):
     974+        """
     975+        If no initializers are required, the authd event is not sent again.
     976+        """
     977+        self.init.required = False
     978+        xs = self.xmlstream
     979+        xs.makeConnection(proto_helpers.StringTransport())
     980+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     981+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     982+                         "from='example.com' to='example.org' id='12345' "
     983+                         "version='1.0'>")
     984+
     985+        self.assertEqual(1, self.gotAuthenticated)
     986+        xs.output = []
     987+        self.init.deferred.callback(None)
     988+        self.assertEqual(1, self.gotAuthenticated)
     989+
     990 
     991     def test_streamStartedXmlStanzasHandledIgnored(self):
     992         """
Note: See TracChangeset for help on using the changeset viewer.