source: ralphm-patches/client_listen_authenticator.patch @ 66:b713f442b222

Last change on this file since 66:b713f442b222 was 66:b713f442b222, checked in by Ralph Meijer <ralphm@…>, 8 years ago

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

File size: 33.9 KB
  • wokkel/client.py

    # HG changeset patch
    # Parent a1648553ea06b7f0b38fec775db71ac03782e3d6
    Add authenticator for accepting XMPP client connections.
    
    The new authenticator XMPPClientListenAuthenticator is to be used together
    with an `XmlStream` created for an incoming XMPP stream. It uses the
    new initializers for SASL (PLAIN only), resource binding and session
    establishement.
    
    This authenticator needs at least one Twisted Cred portal to hold the
    domain served. After authenticating, an avatar and a logout callback are
    returned. Upon binding a resource, the avatar's `bindResource` method is
    called with the desired resource name. Upon stream disconnect, the
    logout callback is called.
    
    diff --git a/wokkel/client.py b/wokkel/client.py
    a b  
    1010that should probably eventually move there.
    1111"""
    1212
     13import base64
     14
    1315from twisted.application import service
    14 from twisted.internet import reactor
     16from twisted.cred import credentials, error as ecred
     17from twisted.internet import defer, reactor
     18from twisted.python import log
    1519from twisted.names.srvconnect import SRVConnector
    16 from twisted.words.protocols.jabber import client, sasl, xmlstream
     20from twisted.words.protocols.jabber import client, error, sasl, xmlstream
     21from twisted.words.xish import domish
    1722
    1823from wokkel import generic
     24from wokkel.iwokkel import IUserSession
    1925from wokkel.subprotocols import StreamManager
    2026
     27NS_CLIENT = 'jabber:client'
     28
     29XPATH_AUTH = "/auth[@xmlns='%s']" % sasl.NS_XMPP_SASL
     30XPATH_BIND = "/iq[@type='set']/bind[@xmlns='%s']" % client.NS_XMPP_BIND
     31XPATH_SESSION = "/iq[@type='set']/session[@xmlns='%s']" % \
     32                client.NS_XMPP_SESSION
     33
    2134class CheckAuthInitializer(object):
    2235    """
    2336    Check what authentication methods are available.
     
    5164    autentication.
    5265    """
    5366
    54     namespace = 'jabber:client'
     67    namespace = NS_CLIENT
    5568
    5669    def __init__(self, jid, password):
    5770        xmlstream.ConnectAuthenticator.__init__(self, jid.host)
     
    186199    c = XMPPClientConnector(reactor, domain, factory)
    187200    c.connect()
    188201    return factory.deferred
     202
     203
     204
     205class InvalidMechanism(Exception):
     206    """
     207    The requested SASL mechanism is invalid.
     208    """
     209
     210
     211
     212class AuthorizationIdentifierNotSupported(Exception):
     213    """
     214    Authorization Identifiers are not supported.
     215    """
     216
     217
     218
     219class SASLReceivingInitializer(generic.BaseReceivingInitializer):
     220    """
     221    Stream initializer for SASL authentication, receiving side.
     222    """
     223
     224    required = True
     225
     226    def __init__(self, name, xs, portal):
     227        generic.BaseReceivingInitializer.__init__(self, name, xs)
     228        self.portal = portal
     229        self.failureGrace = 3
     230
     231
     232    def getFeatures(self):
     233        feature = domish.Element((sasl.NS_XMPP_SASL, 'mechanisms'))
     234        feature.addElement('mechanism', content='PLAIN')
     235        return [feature]
     236
     237
     238    def initialize(self):
     239        self.xmlstream.avatar = None
     240        self.xmlstream.addObserver(XPATH_AUTH, self._onAuth)
     241        return self.deferred
     242
     243
     244    def _onAuth(self, auth):
     245        """
     246        Called when the start of the SASL negotiation is received.
     247
     248        @type auth: L{domish.Element}.
     249        """
     250        auth.handled = True
     251
     252        def cb(_):
     253            response = domish.Element((sasl.NS_XMPP_SASL, 'success'))
     254            self.xmlstream.send(response)
     255            self.xmlstream.reset()
     256            self.deferred.callback(xmlstream.Reset)
     257
     258        def eb(failure):
     259            if failure.check(ecred.UnauthorizedLogin):
     260                condition = 'not-authorized'
     261            elif failure.check(InvalidMechanism):
     262                condition = 'invalid-mechanism'
     263            elif failure.check(AuthorizationIdentifierNotSupported):
     264                condition = 'invalid-authz'
     265            else:
     266                log.err(failure)
     267                condition = 'temporary-auth-failure'
     268
     269            response = domish.Element((sasl.NS_XMPP_SASL, 'failure'))
     270            response.addElement(condition)
     271            self.xmlstream.send(response)
     272
     273            # Close stream on too many failing authentication attempts
     274            self.failureGrace -= 1
     275            if self.failureGrace == 0:
     276                self.deferred.errback(error.StreamError('policy-violation'))
     277            else:
     278                return
     279
     280        d = defer.maybeDeferred(self._doAuth, auth)
     281        d.addCallbacks(cb, eb)
     282
     283
     284    def _credentialsFromPlain(self, auth):
     285        """
     286        Create credentials from the initial response for PLAIN.
     287        """
     288        initialResponse = base64.b64decode(unicode(auth))
     289        authzid, authcid, passwd = initialResponse.split('\x00')
     290
     291        if authzid:
     292            raise AuthorizationIdentifierNotSupported()
     293
     294        creds = credentials.UsernamePassword(username=authcid,
     295                                             password=passwd)
     296        return creds
     297
     298
     299    def _doAuth(self, auth):
     300        """
     301        Start authentication.
     302        """
     303        if auth.getAttribute('mechanism') != 'PLAIN':
     304            raise InvalidMechanism()
     305
     306        creds = self._credentialsFromPlain(auth)
     307
     308        def cb((iface, avatar, logout)):
     309            self.xmlstream.avatar = avatar
     310            self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT,
     311                                       lambda _: logout())
     312
     313        d = self.portal.login(creds, self.xmlstream, IUserSession)
     314        d.addCallback(cb)
     315        return d
     316
     317
     318
     319class BindReceivingInitializer(generic.BaseReceivingInitializer):
     320    """
     321    Stream initializer for resource binding, receiving side.
     322    """
     323
     324    required = True
     325
     326    def getFeatures(self):
     327        feature = domish.Element((client.NS_XMPP_BIND, 'bind'))
     328        return [feature]
     329
     330
     331    def initialize(self):
     332        self.xmlstream.addOnetimeObserver(XPATH_BIND, self.onBind)
     333        return self.deferred
     334
     335
     336    def onBind(self, iq):
     337        def cb(boundJID):
     338            self.xmlstream.otherEntity = boundJID
     339
     340            response = xmlstream.toResponse(iq, 'result')
     341            response.addElement((client.NS_XMPP_BIND, 'bind'))
     342            response.bind.addElement((client.NS_XMPP_BIND, 'jid'),
     343                                     content=boundJID.full())
     344
     345            return response
     346
     347        iq.handled = True
     348        resource = unicode(iq.bind) or None
     349        d = self.xmlstream.avatar.bindResource(resource)
     350        d.addCallback(cb)
     351        d.addCallback(self.xmlstream.send)
     352        d.chainDeferred(self.deferred)
     353
     354
     355
     356class SessionReceivingInitializer(generic.BaseReceivingInitializer):
     357    """
     358    Stream initializer for session establishment, receiving side.
     359
     360    This is mostly a no-op and just returns a result stanza. If resource
     361    binding hasn't yet completed, this will return a stanza error with the
     362    condition C{'forbidden'}.
     363
     364    Note that RFC 6120 deprecated the session establishment protocol. This
     365    is provided for backwards compatibility.
     366    """
     367
     368    required = False
     369
     370    def getFeatures(self):
     371        feature = domish.Element((client.NS_XMPP_SESSION, 'session'))
     372        return [feature]
     373
     374
     375    def initialize(self):
     376        self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1)
     377        return self.deferred
     378
     379
     380    def onSession(self, iq):
     381        iq.handled = True
     382
     383        reply = domish.Element((None, 'iq'))
     384
     385        if self.xmlstream.otherEntity:
     386            reply = xmlstream.toResponse(iq, 'result')
     387        else:
     388            reply = error.StanzaError('forbidden').toResponse(iq)
     389        self.xmlstream.send(reply)
     390        self.deferred.callback(None)
     391
     392
     393
     394class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator):
     395    """
     396    XML Stream authenticator for XMPP clients, server side.
     397
     398    @ivar portals: Mapping of server JIDs to Cred Portals.
     399    @type portals: C{dict} of L{twisted.words.protocols.jabber.jid.JID} to
     400        L{twisted.cred.portal.Portal}.
     401    """
     402
     403    namespace = NS_CLIENT
     404
     405    def __init__(self, portals):
     406        generic.FeatureListenAuthenticator.__init__(self)
     407        self.portals = portals
     408        self.portal = None
     409
     410
     411    def getInitializers(self):
     412        """
     413        Return initializers based on previously completed initializers.
     414
     415        This has three stages: 1. SASL, 2. Resource binding and session
     416        establishment. 3. Completed. Note that session establishment
     417        is optional.
     418        """
     419        if not self.completedInitializers:
     420            return [SASLReceivingInitializer('sasl', self.xmlstream, self.portal)]
     421        elif self.completedInitializers[-1] == 'sasl':
     422            return [BindReceivingInitializer('bind', self.xmlstream),
     423                    SessionReceivingInitializer('session', self.xmlstream)]
     424        else:
     425            return []
     426
     427
     428    def checkStream(self):
     429        """
     430        Check that the stream header has proper addressing.
     431
     432        The C{'to'} attribute must be present and there should have a matching
     433        portal in L{portals}.
     434        """
     435        generic.FeatureListenAuthenticator.checkStream(self)
     436
     437        if not self.xmlstream.thisEntity:
     438            raise error.StreamError('improper-addressing')
     439
     440        # Check if we serve the domain and use the associated portal.
     441        try:
     442            self.portal = self.portals[self.xmlstream.thisEntity]
     443        except KeyError:
     444            raise error.StreamError('host-unknown')
  • wokkel/generic.py

    diff --git a/wokkel/generic.py b/wokkel/generic.py
    a b  
    465465
    466466    def __init__(self):
    467467        self.completedInitializers = []
     468        self._initialized = False
    468469
    469470
    470471    def _onElementFallback(self, element):
     
    556557
    557558        self.xmlstream.send(features)
    558559
    559         if not required:
     560        if not required and not self._initialized:
    560561            # There are no required initializers anymore. This stream is
    561562            # now ready for the exchange of stanzas.
    562563            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
    563564            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
     565            self._initialized = True
    564566
    565567        if ds:
    566568            d = defer.DeferredList(ds, fireOnOneCallback=True,
  • wokkel/iwokkel.py

    diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
    a b  
    985985
    986986
    987987
     988class IUserSession(Interface):
     989    def loggedIn(realm, mind):
     990        """
     991        Called by the realm when login occurs.
     992
     993        @param realm: The realm though which login is occurring.
     994        @param mind: The mind object.
     995        """
     996
     997
     998    def bindResource(resource):
     999        """
     1000        Bind a resource to this session.
     1001
     1002        @type resource: C{unicode}.
     1003        """
     1004
     1005
     1006    def logout():
     1007        """
     1008        End this session.
     1009
     1010        This is called when the stream is disconnected.
     1011        """
     1012
     1013
     1014    def send(element):
     1015        """
     1016        Called when the client sends a stanza.
     1017        """
     1018
     1019
     1020    def receive(element):
     1021        """
     1022        Have the client receive a stanza.
     1023        """
     1024
     1025
     1026
    9881027class IReceivingInitializer(Interface):
    9891028    """
    9901029    Interface for XMPP stream initializers for receiving entities.
  • wokkel/test/test_client.py

    diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
    a b  
    55Tests for L{wokkel.client}.
    66"""
    77
     8from base64 import b64encode
     9
     10from zope.interface import implements
     11
     12from twisted.cred.portal import IRealm, Portal
     13from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
    814from twisted.internet import defer
     15from twisted.test import proto_helpers
    916from twisted.trial import unittest
    10 from twisted.words.protocols.jabber import xmlstream
     17from twisted.words.protocols.jabber import error, xmlstream
     18from twisted.words.protocols.jabber.client import NS_XMPP_BIND
     19from twisted.words.protocols.jabber.client import NS_XMPP_SESSION
    1120from twisted.words.protocols.jabber.client import XMPPAuthenticator
    1221from twisted.words.protocols.jabber.jid import JID
     22from twisted.words.protocols.jabber.sasl import NS_XMPP_SASL
     23from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
     24from twisted.words.protocols.jabber.xmlstream import NS_STREAMS
    1325from twisted.words.protocols.jabber.xmlstream import STREAM_AUTHD_EVENT
    14 from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
    1526from twisted.words.protocols.jabber.xmlstream import XMPPHandler
     27from twisted.words.xish import xpath
    1628
    17 from wokkel import client
     29from wokkel import client, iwokkel
     30from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator
    1831
    1932class XMPPClientTest(unittest.TestCase):
    2033    """
     
    155168        self.assertEqual(factory.deferred, d2)
    156169
    157170        return d1
     171
     172
     173
     174class TestSession(object):
     175    implements(iwokkel.IUserSession)
     176
     177    def __init__(self, domain, user):
     178        self.domain = domain
     179        self.user = user
     180
     181
     182    def bindResource(self, resource):
     183        return defer.succeed(JID(tuple=(self.user, self.domain, resource)))
     184
     185
     186
     187class TestRealm(object):
     188
     189    implements(IRealm)
     190
     191    logoutCalled = False
     192
     193    def __init__(self, domain):
     194        self.domain = domain
     195
     196
     197    def requestAvatar(self, avatarId, mind, *interfaces):
     198        return (iwokkel.IUserSession,
     199                TestSession(self.domain, avatarId.decode('utf-8')),
     200                self.logout)
     201
     202
     203    def logout(self):
     204        self.logoutCalled = True
     205
     206
     207
     208class TestableFeatureListenAuthenticator(FeatureListenAuthenticator):
     209    namespace = 'jabber:client'
     210
     211    initialized = None
     212
     213    def __init__(self, getInitializers):
     214        """
     215        Set up authenticator.
     216
     217        @param getInitializers: Function to override the getInitializers
     218            method. It will receive C{self} as the only argument.
     219        """
     220        FeatureListenAuthenticator.__init__(self)
     221
     222        import types
     223        self.getInitializers = types.MethodType(getInitializers, self)
     224
     225        xs = TestableXmlStream(self)
     226        xs.makeConnection(proto_helpers.StringTransport())
     227
     228
     229    def streamStarted(self, rootElement):
     230        """
     231        Set up observers for authentication events.
     232        """
     233        def authenticated(_):
     234            self.initialized = True
     235
     236        self.xmlstream.addObserver(STREAM_AUTHD_EVENT, authenticated)
     237        FeatureListenAuthenticator.streamStarted(self, rootElement)
     238
     239
     240
     241class SASLReceivingInitializerTest(unittest.TestCase):
     242    """
     243    Tests for L{client.SASLReceivingInitializer}.
     244    """
     245
     246    def setUp(self):
     247        realm = TestRealm(u'example.org')
     248        checker = InMemoryUsernamePasswordDatabaseDontUse(test='secret')
     249        self.portal = portal = Portal(realm, (checker,))
     250
     251        def getInitializers(self):
     252            self.initializer = client.SASLReceivingInitializer('sasl',
     253                                                               self.xmlstream,
     254                                                               portal)
     255            return [self.initializer]
     256
     257        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
     258        self.xmlstream = self.authenticator.xmlstream
     259
     260
     261    def test_getFeatures(self):
     262        """
     263        The stream features list SASL with the PLAIN mechanism.
     264        """
     265        xs = self.xmlstream
     266        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     267                         "xmlns:stream='http://etherx.jabber.org/streams' "
     268                         "to='example.org' "
     269                         "version='1.0'>")
     270
     271        self.assertTrue(xs.headerSent)
     272
     273        # Check SASL mechanisms
     274        features = xs.output[-1]
     275        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
     276                                          "/mechanisms[@xmlns='%s']"
     277                                          "/mechanism[@xmlns='%s' and "
     278                                                     "text()='PLAIN']" %
     279                                          (NS_STREAMS, NS_XMPP_SASL, NS_XMPP_SASL),
     280                                      features))
     281
     282
     283    def test_auth(self):
     284        """
     285        Authenticating causes an avatar to be set on the authenticator.
     286        """
     287        xs = self.xmlstream
     288        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     289                         "xmlns:stream='http://etherx.jabber.org/streams' "
     290                         "to='example.org' "
     291                         "version='1.0'>")
     292        xs.output = []
     293        response = b64encode('\x00'.join(['', 'test', 'secret']))
     294        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     295                         "mechanism='PLAIN'>%s</auth>" % response)
     296        self.assertTrue(iwokkel.IUserSession.providedBy(self.xmlstream.avatar))
     297        self.assertFalse(xs.headerSent)
     298        self.assertEqual(1, len(xs.output))
     299        self.assertFalse(self.authenticator.initialized)
     300
     301
     302    def test_authInvalidMechanism(self):
     303        """
     304        Authenticating with an invalid SASL mechanism causes a streamError.
     305        """
     306        xs = self.xmlstream
     307        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     308                         "xmlns:stream='http://etherx.jabber.org/streams' "
     309                         "to='example.org' "
     310                         "version='1.0'>")
     311        xs.output = []
     312        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     313                         "mechanism='unknown'/>")
     314        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     315                                       "/invalid-mechanism[@xmlns='%s']" %
     316                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
     317                                      xs.output[-1]))
     318
     319
     320    def test_authFail(self):
     321        """
     322        Authenticating causes an avatar to be set on the authenticator.
     323        """
     324        xs = self.xmlstream
     325        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     326                         "xmlns:stream='http://etherx.jabber.org/streams' "
     327                         "to='example.org' "
     328                         "version='1.0'>")
     329        xs.output = []
     330        response = b64encode('\x00'.join(['', 'test', 'bad']))
     331        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     332                         "mechanism='PLAIN'>%s</auth>" % response)
     333        self.assertIdentical(None, self.xmlstream.avatar)
     334        self.assertTrue(xs.headerSent)
     335
     336        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     337                                       "/not-authorized[@xmlns='%s']" %
     338                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
     339                                      xs.output[-1]))
     340
     341        self.assertFalse(self.authenticator.initialized)
     342
     343
     344    def test_authFailMultiple(self):
     345        """
     346        Authenticating causes an avatar to be set on the authenticator.
     347        """
     348        xs = self.xmlstream
     349        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     350                         "xmlns:stream='http://etherx.jabber.org/streams' "
     351                         "to='example.org' "
     352                         "version='1.0'>")
     353
     354        xs.output = []
     355        response = b64encode('\x00'.join(['', 'test', 'bad']))
     356
     357        attempts = self.authenticator.initializer.failureGrace
     358        for attempt in xrange(attempts):
     359            xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     360                             "mechanism='PLAIN'>%s</auth>" % response)
     361            self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     362                                           "/not-authorized[@xmlns='%s']" %
     363                                           (NS_XMPP_SASL, NS_XMPP_SASL)),
     364                                          xs.output[-1]))
     365        self.xmlstream.assertStreamError(self, condition='policy-violation')
     366        self.assertFalse(self.authenticator.initialized)
     367
     368
     369    def test_authException(self):
     370        """
     371        Other authentication exceptions yield temporary-auth-failure.
     372        """
     373        class Error(Exception):
     374            pass
     375
     376        def login(credentials, mind, *interfaces):
     377            raise Error()
     378
     379        self.portal.login = login
     380
     381        xs = self.xmlstream
     382        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     383                         "xmlns:stream='http://etherx.jabber.org/streams' "
     384                         "to='example.org' "
     385                         "version='1.0'>")
     386        xs.output = []
     387        response = b64encode('\x00'.join(['', 'test', 'bad']))
     388        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     389                         "mechanism='PLAIN'>%s</auth>" % response)
     390        self.assertIdentical(None, self.xmlstream.avatar)
     391        self.assertTrue(xs.headerSent)
     392
     393        self.assertTrue(xpath.matches(("/failure[@xmlns='%s']"
     394                                       "/temporary-auth-failure[@xmlns='%s']" %
     395                                       (NS_XMPP_SASL, NS_XMPP_SASL)),
     396                                      xs.output[-1]))
     397        self.assertFalse(self.authenticator.initialized)
     398        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
     399
     400
     401    def test_authNonAsciiUsername(self):
     402        """
     403        Authenticating causes an avatar to be set on the authenticator.
     404        """
     405        xs = self.xmlstream
     406        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     407                         "xmlns:stream='http://etherx.jabber.org/streams' "
     408                         "to='example.org' "
     409                         "version='1.0'>")
     410        xs.output = []
     411        response = b64encode('\x00'.join(['', 'test\xa1', 'secret']))
     412        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     413                         "mechanism='PLAIN'>%s</auth>" % response)
     414        self.assertIdentical(None, self.xmlstream.avatar)
     415        self.assertTrue(xs.headerSent)
     416
     417        self.assertEqual(1, len(xs.output))
     418        failure = xs.output[-1]
     419        condition = failure.elements().next()
     420        self.assertEqual('not-authorized', condition.name)
     421
     422
     423    def test_authAuthorizationIdentifier(self):
     424        """
     425        Authorization Identifiers are not supported.
     426        """
     427        xs = self.xmlstream
     428        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     429                         "xmlns:stream='http://etherx.jabber.org/streams' "
     430                         "to='example.org' "
     431                         "version='1.0'>")
     432        xs.output = []
     433        response = b64encode('\x00'.join(['other', 'test', 'secret']))
     434        xs.dataReceived("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' "
     435                         "mechanism='PLAIN'>%s</auth>" % response)
     436        self.assertIdentical(None, self.xmlstream.avatar)
     437        self.assertTrue(xs.headerSent)
     438
     439        self.assertEqual(1, len(xs.output))
     440        failure = xs.output[-1]
     441        condition = failure.elements().next()
     442        self.assertEqual('invalid-authz', condition.name)
     443
     444
     445
     446class BindReceivingInitializerTest(unittest.TestCase):
     447    """
     448    Tests for L{client.BindReceivingInitializer}.
     449    """
     450
     451    def setUp(self):
     452        def getInitializers(self):
     453            self.initializer = client.BindReceivingInitializer('bind',
     454                                                               self.xmlstream)
     455            return [self.initializer]
     456
     457        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
     458        self.xmlstream = self.authenticator.xmlstream
     459        self.xmlstream.avatar = TestSession('example.org', 'test')
     460
     461
     462    def test_getFeatures(self):
     463        """
     464        The stream features include resource binding.
     465        """
     466        xs = self.xmlstream
     467        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     468                         "xmlns:stream='http://etherx.jabber.org/streams' "
     469                         "to='example.org' "
     470                         "version='1.0'>")
     471
     472        features = xs.output[-1]
     473        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
     474                                          "/bind[@xmlns='%s']" %
     475                                          (NS_STREAMS, NS_XMPP_BIND),
     476                                      features))
     477
     478
     479    def test_bind(self):
     480        """
     481        To bind a resource, the avatar is requested one and a JID is returned.
     482        """
     483        xs = self.xmlstream
     484        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     485                         "xmlns:stream='http://etherx.jabber.org/streams' "
     486                         "to='example.org' "
     487                         "version='1.0'>")
     488
     489        # This initializer is required.
     490        self.assertFalse(self.authenticator.initialized)
     491
     492        xs.output = []
     493        xs.dataReceived("""<iq type='set'>
     494                             <bind xmlns='%s'>Home</bind>
     495                           </iq>""" % NS_XMPP_BIND)
     496
     497        self.assertTrue(xs.headerSent, "Unexpected stream restart")
     498
     499        # In response to the bind request, a result iq and the new stream
     500        # features are sent
     501        response = xs.output[-2]
     502        self.assertTrue(xpath.matches("/iq[@type='result']"
     503                                          "/bind[@xmlns='%s']"
     504                                          "/jid[@xmlns='%s' and "
     505                                               "text()='%s']" %
     506                                          (NS_XMPP_BIND,
     507                                           NS_XMPP_BIND,
     508                                           'test@example.org/Home'),
     509                                      response))
     510
     511        self.assertTrue(self.authenticator.initialized)
     512
     513
     514
     515class SessionReceivingInitializerTest(unittest.TestCase):
     516    """
     517    Tests for L{client.SessionReceivingInitializer}.
     518    """
     519
     520    def setUp(self):
     521        def getInitializers(self):
     522            self.initializer = client.SessionReceivingInitializer('session',
     523                                                                  self.xmlstream)
     524            return [self.initializer]
     525
     526        self.authenticator = TestableFeatureListenAuthenticator(getInitializers)
     527        self.xmlstream = self.authenticator.xmlstream
     528
     529
     530    def test_getFeatures(self):
     531        """
     532        The stream features include session establishment.
     533        """
     534        xs = self.xmlstream
     535        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     536                         "xmlns:stream='http://etherx.jabber.org/streams' "
     537                         "to='example.org' "
     538                         "version='1.0'>")
     539
     540        features = xs.output[-1]
     541        self.assertTrue(xpath.matches("/features[@xmlns='%s']"
     542                                          "/session[@xmlns='%s']" %
     543                                          (NS_STREAMS, NS_XMPP_SESSION),
     544                                      features))
     545
     546    def test_session(self):
     547        """
     548        Session establishment is a no-op iq exchange.
     549        """
     550        xs = self.xmlstream
     551        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     552                         "xmlns:stream='http://etherx.jabber.org/streams' "
     553                         "to='example.org' "
     554                         "version='1.0'>")
     555
     556        # This initializer is not required.
     557        self.assertTrue(self.authenticator.initialized)
     558
     559        # If resource binding has completed, xs.otherEntity has been set.
     560        xs.otherEntity = JID('test@example.org/Home')
     561
     562        xs.output = []
     563        xs.dataReceived("""<iq type='set'>
     564                             <session xmlns='%s'/>
     565                           </iq>""" % NS_XMPP_SESSION)
     566
     567        self.assertTrue(xs.headerSent, "Unexpected stream restart")
     568
     569        # In response to the session request, a result iq and the new stream
     570        # features are sent
     571        response = xs.output[-2]
     572        self.assertTrue(xpath.matches("/iq[@type='result']", response))
     573
     574
     575
     576    def test_sessionNoBind(self):
     577        """
     578        Session establishment requires resource binding being completed.
     579        """
     580        xs = self.xmlstream
     581        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     582                         "xmlns:stream='http://etherx.jabber.org/streams' "
     583                         "to='example.org' "
     584                         "version='1.0'>")
     585
     586        # This initializer is not required.
     587        self.assertTrue(self.authenticator.initialized)
     588
     589        xs.output = []
     590        xs.dataReceived("""<iq type='set'>
     591                             <session xmlns='%s'/>
     592                           </iq>""" % NS_XMPP_SESSION)
     593
     594        self.assertTrue(xs.headerSent, "Unexpected stream restart")
     595
     596        # In response to the session request, a result iq and the new stream
     597        # features are sent
     598        response = xs.output[-2]
     599        stanzaError = error.exceptionFromStanza(response)
     600        self.assertEqual('forbidden', stanzaError.condition)
     601
     602
     603
     604class XMPPClientListenAuthenticatorTest(unittest.TestCase):
     605    """
     606    Tests for L{client.XMPPClientListenAuthenticator}.
     607    """
     608
     609    def setUp(self):
     610        portals = {JID('example.org'): None}
     611        self.authenticator = client.XMPPClientListenAuthenticator(portals)
     612        self.xmlstream = TestableXmlStream(self.authenticator)
     613        self.xmlstream.makeConnection(self)
     614
     615
     616    def test_getInitializersStart(self):
     617        """
     618        Upon the start of negotation, only the SASL initializer is available.
     619        """
     620        inits = self.authenticator.getInitializers()
     621        (init,) = inits
     622        self.assertEqual('sasl', init.name)
     623        self.assertIsInstance(init, client.SASLReceivingInitializer)
     624
     625
     626    def test_getInitializersPostSASL(self):
     627        """
     628        After SASL, the resource binding and session establishment initializers
     629        are available.
     630        """
     631        self.authenticator.completedInitializers = ['sasl']
     632        inits = self.authenticator.getInitializers()
     633        (bind, session) = inits
     634        self.assertEqual('bind', bind.name)
     635        self.assertIsInstance(bind, client.BindReceivingInitializer)
     636        self.assertEqual('session', session.name)
     637        self.assertIsInstance(session, client.SessionReceivingInitializer)
     638
     639
     640    def test_streamStartedWrongNamespace(self):
     641        """
     642        An incorrect stream namespace causes a stream error.
     643        """
     644        xs = self.xmlstream
     645        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     646                         "xmlns:stream='http://etherx.jabber.org/streams' "
     647                         "to='example.org' "
     648                         "version='1.0'>")
     649        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
     650
     651
     652    def test_streamStartedNoTo(self):
     653        """
     654        A missing 'to' attribute on the stream header causes a stream error.
     655        """
     656        xs = self.xmlstream
     657        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     658                         "xmlns:stream='http://etherx.jabber.org/streams' "
     659                         "version='1.0'>")
     660        self.xmlstream.assertStreamError(self, condition='improper-addressing')
     661
     662
     663    def test_streamStartedUnknownHost(self):
     664        """
     665        An unknown 'to' on the stream header causes a stream error.
     666        """
     667        xs = self.xmlstream
     668        xs.dataReceived("<stream:stream xmlns='jabber:client' "
     669                         "xmlns:stream='http://etherx.jabber.org/streams' "
     670                         "to='example.com' "
     671                         "version='1.0'>")
     672        self.xmlstream.assertStreamError(self, condition='host-unknown')
  • wokkel/test/test_generic.py

    diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    a b  
    331331    """
    332332
    333333    def setUp(self):
    334         self.gotAuthenticated = False
    335         self.initFailure = None
     334        self.gotAuthenticated = 0
    336335        self.authenticator = generic.FeatureListenAuthenticator()
    337336        self.authenticator.namespace = 'jabber:server'
    338337        self.xmlstream = generic.TestableXmlStream(self.authenticator)
    339338        self.xmlstream.addObserver('//event/stream/authd',
    340339                                   self.onAuthenticated)
    341         self.xmlstream.addObserver('//event/xmpp/initfailed',
    342                                    self.onInitFailed)
    343340
    344341        self.init = TestableReceivingInitializer('init', self.xmlstream,
    345342                                                 'testns', 'test')
     
    351348
    352349
    353350    def onAuthenticated(self, obj):
    354         self.gotAuthenticated = True
    355 
    356 
    357     def onInitFailed(self, failure):
    358         self.initFailure = failure
     351        self.gotAuthenticated += 1
    359352
    360353
    361354    def test_getInitializers(self):
     
    537530                        "  <query xmlns='jabber:iq:version'/>"
    538531                        "</iq>")
    539532
     533    def test_streamStartedInitializerNotRequired(self):
     534        """
     535        If no initializers are required, initialization is done.
     536        """
     537        self.init.required = False
     538        xs = self.xmlstream
     539        xs.makeConnection(proto_helpers.StringTransport())
     540        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     541                         "xmlns:stream='http://etherx.jabber.org/streams' "
     542                         "from='example.com' to='example.org' id='12345' "
     543                         "version='1.0'>")
     544
     545        self.assertEqual(1, self.gotAuthenticated)
     546
     547
     548    def test_streamStartedInitializerNotRequiredDoneOnce(self):
     549        """
     550        If no initializers are required, the authd event is not sent again.
     551        """
     552        self.init.required = False
     553        xs = self.xmlstream
     554        xs.makeConnection(proto_helpers.StringTransport())
     555        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     556                         "xmlns:stream='http://etherx.jabber.org/streams' "
     557                         "from='example.com' to='example.org' id='12345' "
     558                         "version='1.0'>")
     559
     560        self.assertEqual(1, self.gotAuthenticated)
     561        xs.output = []
     562        self.init.deferred.callback(None)
     563        self.assertEqual(1, self.gotAuthenticated)
     564
    540565
    541566    def test_streamStartedXmlStanzasHandledIgnored(self):
    542567        """
Note: See TracBrowser for help on using the repository browser.