source: ralphm-patches/client_listen_authenticator.patch

Last change on this file was 72:727b4d29c48e, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Major reworking of avatars, session manager and stanza handlers.

File size: 35.4 KB
  • wokkel/client.py

    # HG changeset patch
    # Parent c22caa54600c4f85db2a400c7fbea5497f943aa1
    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    This authenticator uses L{Twisted Cred<twisted.cred>}, the pluggable
     224    authentication system. As such it takes a
     225    L{Portal<twisted.cred.portal.Portal>} to select authentication mechanisms,
     226    creates a credential object for the selected authentication mechanism and
     227    passes it to the portal to login and acquire an avatar.
     228
     229    The avatar will be set on the C{avatar} attribute of the
     230    L{xmlstream.XmlStream}.
     231
     232    Currently, only the C{PLAIN} SASL mechanism is supported.
     233    """
     234
     235    required = True
     236    _mechanisms = None
     237    __credentialsMap = {
     238        credentials.IAnonymous: 'ANONYMOUS',
     239        credentials.IUsernamePassword: 'PLAIN',
     240        }
     241
     242    def __init__(self, name, xs, portal):
     243        generic.BaseReceivingInitializer.__init__(self, name, xs)
     244        self.portal = portal
     245        self.failureGrace = 3
     246
     247
     248    def getFeatures(self):
     249        feature = domish.Element((sasl.NS_XMPP_SASL, 'mechanisms'))
     250
     251        # Advertise supported SASL mechanisms that have corresponding
     252        # checkers in the Portal.
     253        self._mechanisms = set()
     254        for interface in self.portal.listCredentialsInterfaces():
     255            try:
     256                mechanism = self.__credentialsMap[interface]
     257            except KeyError:
     258                pass
     259            else:
     260                self._mechanisms.add(mechanism)
     261                feature.addElement('mechanism', content=mechanism)
     262
     263        return [feature]
     264
     265
     266    def initialize(self):
     267        self.xmlstream.avatar = None
     268        self.xmlstream.addObserver(XPATH_AUTH, self._onAuth)
     269        return self.deferred
     270
     271
     272    def _onAuth(self, auth):
     273        """
     274        Called when the start of the SASL negotiation is received.
     275
     276        @type auth: L{domish.Element}.
     277        """
     278        auth.handled = True
     279
     280        def cb(_):
     281            response = domish.Element((sasl.NS_XMPP_SASL, 'success'))
     282            self.xmlstream.send(response)
     283            self.xmlstream.reset()
     284            self.deferred.callback(xmlstream.Reset)
     285
     286        def eb(failure):
     287            if failure.check(ecred.UnauthorizedLogin):
     288                condition = 'not-authorized'
     289            elif failure.check(InvalidMechanism):
     290                condition = 'invalid-mechanism'
     291            elif failure.check(AuthorizationIdentifierNotSupported):
     292                condition = 'invalid-authz'
     293            else:
     294                log.err(failure)
     295                condition = 'temporary-auth-failure'
     296
     297            response = domish.Element((sasl.NS_XMPP_SASL, 'failure'))
     298            response.addElement(condition)
     299            self.xmlstream.send(response)
     300
     301            # Close stream on too many failing authentication attempts
     302            self.failureGrace -= 1
     303            if self.failureGrace == 0:
     304                self.deferred.errback(error.StreamError('policy-violation'))
     305            else:
     306                return
     307
     308        d = defer.maybeDeferred(self._doAuth, auth)
     309        d.addCallbacks(cb, eb)
     310
     311
     312    def _credentialsFrom_PLAIN(self, auth):
     313        """
     314        Create credentials from the initial response for PLAIN.
     315        """
     316        initialResponse = base64.b64decode(unicode(auth))
     317        authzid, authcid, passwd = initialResponse.split('\x00')
     318
     319        if authzid:
     320            raise AuthorizationIdentifierNotSupported()
     321
     322        creds = credentials.UsernamePassword(username=authcid,
     323                                             password=passwd)
     324        return creds
     325
     326
     327    def _credentialsFrom_ANONYMOUS(self, auth):
     328        """
     329        Create credentials from the initial response for ANONYMOUS.
     330        """
     331        return credentials.Anonymous()
     332
     333
     334    def _doAuth(self, auth):
     335        """
     336        Start authentication.
     337        """
     338        mechanism = auth.getAttribute('mechanism')
     339
     340        if mechanism not in self._mechanisms:
     341            raise InvalidMechanism()
     342
     343        creds = getattr(self, '_credentialsFrom_' + mechanism)(auth)
     344
     345        def cb((iface, avatar, logout)):
     346            self.xmlstream.avatar = avatar
     347            self.xmlstream.addObserver(xmlstream.STREAM_END_EVENT,
     348                                       lambda _: logout())
     349
     350        d = self.portal.login(creds, self.xmlstream, IUserSession)
     351        d.addCallback(cb)
     352        return d
     353
     354
     355
     356class BindReceivingInitializer(generic.BaseReceivingInitializer):
     357    """
     358    Stream initializer for resource binding, receiving side.
     359
     360    Upon a request for resource binding, this will call C{bindResource} on
     361    the stream's avatar.
     362    """
     363
     364    required = True
     365
     366    def getFeatures(self):
     367        feature = domish.Element((client.NS_XMPP_BIND, 'bind'))
     368        return [feature]
     369
     370
     371    def initialize(self):
     372        self.xmlstream.addOnetimeObserver(XPATH_BIND, self.onBind)
     373        return self.deferred
     374
     375
     376    def onBind(self, iq):
     377        def cb(boundJID):
     378            self.xmlstream.otherEntity = boundJID
     379
     380            response = xmlstream.toResponse(iq, 'result')
     381            response.addElement((client.NS_XMPP_BIND, 'bind'))
     382            response.bind.addElement((client.NS_XMPP_BIND, 'jid'),
     383                                     content=boundJID.full())
     384
     385            return response
     386
     387        iq.handled = True
     388        resource = unicode(iq.bind) or None
     389        d = self.xmlstream.avatar.bindResource(resource)
     390        d.addCallback(cb)
     391        d.addCallback(self.xmlstream.send)
     392        d.chainDeferred(self.deferred)
     393
     394
     395
     396class SessionReceivingInitializer(generic.BaseReceivingInitializer):
     397    """
     398    Stream initializer for session establishment, receiving side.
     399
     400    This is mostly a no-op and just returns a result stanza. If resource
     401    binding hasn't yet completed, this will return a stanza error with the
     402    condition C{'forbidden'}.
     403
     404    Note that RFC 6120 deprecated the session establishment protocol. This
     405    is provided for backwards compatibility.
     406    """
     407
     408    required = False
     409
     410    def getFeatures(self):
     411        feature = domish.Element((client.NS_XMPP_SESSION, 'session'))
     412        return [feature]
     413
     414
     415    def initialize(self):
     416        self.xmlstream.addOnetimeObserver(XPATH_SESSION, self.onSession, 1)
     417        return self.deferred
     418
     419
     420    def onSession(self, iq):
     421        iq.handled = True
     422
     423        reply = domish.Element((None, 'iq'))
     424
     425        if self.xmlstream.otherEntity:
     426            reply = xmlstream.toResponse(iq, 'result')
     427        else:
     428            reply = error.StanzaError('forbidden').toResponse(iq)
     429        self.xmlstream.send(reply)
     430        self.deferred.callback(None)
     431
     432
     433
     434class XMPPClientListenAuthenticator(generic.FeatureListenAuthenticator):
     435    """
     436    XML Stream authenticator for XMPP clients, server side.
     437
     438    @ivar portals: Mapping of server JIDs to Cred Portals.
     439    @type portals: C{dict} of L{twisted.words.protocols.jabber.jid.JID} to
     440        L{twisted.cred.portal.Portal}.
     441    """
     442
     443    namespace = NS_CLIENT
     444
     445    def __init__(self, portals):
     446        generic.FeatureListenAuthenticator.__init__(self)
     447        self.portals = portals
     448        self.portal = None
     449
     450
     451    def getInitializers(self):
     452        """
     453        Return initializers based on previously completed initializers.
     454
     455        This has three stages: 1. SASL, 2. Resource binding and session
     456        establishment. 3. Completed. Note that session establishment
     457        is optional.
     458        """
     459        if not self.completedInitializers:
     460            return [SASLReceivingInitializer('sasl', self.xmlstream, self.portal)]
     461        elif self.completedInitializers[-1] == 'sasl':
     462            return [BindReceivingInitializer('bind', self.xmlstream),
     463                    SessionReceivingInitializer('session', self.xmlstream)]
     464
     465
     466    def checkStream(self):
     467        """
     468        Check that the stream header has proper addressing.
     469
     470        The C{'to'} attribute must be present and there should have a matching
     471        portal in L{portals}.
     472        """
     473        generic.FeatureListenAuthenticator.checkStream(self)
     474
     475        if not self.xmlstream.thisEntity:
     476            raise error.StreamError('improper-addressing')
     477
     478        # Check if we serve the domain and use the associated portal.
     479        try:
     480            self.portal = self.portals[self.xmlstream.thisEntity]
     481        except KeyError:
     482            raise error.StreamError('host-unknown')
  • wokkel/generic.py

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