Changeset 66:b713f442b222 in ralphm-patches


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.

Files:
6 edited

Legend:

Unmodified
Added
Removed
  • c2s_server_factory.patch

    r57 r66  
    11# HG changeset patch
    2 # Parent d76497171af8f3acf1efd2c8433fbdc3c4a55f92
     2# Parent 23dbe722e4482b8286153d29bf87902b464f919f
    33Add factory for accepting client connections.
    44
     
    2121 * Add tests.
    2222
    23 diff -r d76497171af8 wokkel/client.py
    24 --- a/wokkel/client.py  Wed Nov 30 09:31:07 2011 +0100
    25 +++ b/wokkel/client.py  Wed Nov 30 09:32:01 2011 +0100
    26 @@ -20,6 +20,7 @@
     23diff --git a/wokkel/client.py b/wokkel/client.py
     24--- a/wokkel/client.py
     25+++ b/wokkel/client.py
     26@@ -21,6 +21,7 @@
    2727 from twisted.words.xish import domish
    2828 
    2929 from wokkel import generic
    3030+from wokkel.compat import XmlStreamServerFactory
     31 from wokkel.iwokkel import IUserSession
    3132 from wokkel.subprotocols import StreamManager
    3233 
    33  NS_CLIENT = 'jabber:client'
    34 @@ -347,3 +348,98 @@
    35  
    36  
    37  
    38 +class RecipientUnavailable(Exception):
    39 +    """
    40 +    The addressed entity is not, or no longer, available.
    41 +    """
     34@@ -401,3 +402,61 @@
     35             self.portal = self.portals[self.xmlstream.thisEntity]
     36         except KeyError:
     37             raise error.StreamError('host-unknown')
    4238+
    4339+
     
    4541+class XMPPC2SServerFactory(XmlStreamServerFactory):
    4642+
    47 +    def __init__(self, service):
    48 +        self.service = service
     43+    def __init__(self, portal):
     44+        self.portal = portal
    4945+
    5046+        def authenticatorFactory():
    51 +            return XMPPClientListenAuthenticator(service)
     47+            return XMPPClientListenAuthenticator(portal)
    5248+
    5349+        XmlStreamServerFactory.__init__(self, authenticatorFactory)
     
    5854+
    5955+        self.serial = 0
    60 +        self.streams = {}
    6156+
    6257+
     
    8277+            xs.rawDataOutFn = logDataOut
    8378+
     79+        xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost,
     80+                                                   0, xs)
    8481+        xs.addObserver(xmlstream.STREAM_ERROR_EVENT, self.onError)
    8582+
     
    8885+        log.msg("Client connection %d authenticated" % xs.serial)
    8986+
    90 +        xs.addObserver(xmlstream.STREAM_END_EVENT, self.onConnectionLost,
    91 +                                                   0, xs)
    92 +        xs.addObserver('/*', self.onElement, 0, xs)
    93 +
    94 +        # Record this stream as bound to the authenticated JID
    95 +        self.streams[xs.otherEntity] = xs
     87+        xs.addObserver('/*', xs.avatar.send)
    9688+
    9789+
     
    9991+        log.msg("Client connection %d disconnected" % xs.serial)
    10092+
    101 +        entity = xs.otherEntity
    102 +        self.service.unbindResource(entity.user,
    103 +                                    entity.host,
    104 +                                    entity.resource,
    105 +                                    reason)
    106 +
    107 +        # If the lost connections had been bound, remove the reference
    108 +        if xs.otherEntity in self.streams:
    109 +            del self.streams[xs.otherEntity]
    110 +
    11193+
    11294+    def onError(self, reason):
    11395+        log.err(reason, "Stream Error")
    114 +
    115 +
    116 +    def onElement(self, xs, stanza):
    117 +        """
    118 +        Called when an element was received from one of the connected streams.
    119 +
    120 +        """
    121 +        if stanza.handled:
    122 +            return
    123 +        else:
    124 +            self.service.onElement(stanza, xs.otherEntity)
    125 +
    126 +
    127 +    def deliverStanza(self, element, recipient):
    128 +        if recipient in self.streams:
    129 +            self.streams[recipient].send(element)
    130 +        else:
    131 +            raise RecipientUnavailable(u"There is no connection for %s" %
    132 +                                       recipient.full())
  • c2s_stanza_handlers.patch

    r62 r66  
    11# HG changeset patch
    2 # Parent 8c6fa8ea95402e5c968f57e7fde2f8e249c11d12
     2# Parent 41c2620133080ea88612732ff211561c660cfdec
    33Add c2s protocol handlers for iq, message and presence stanzas.
    44
     
    1212--- /dev/null
    1313+++ b/doc/examples/client_service.tac
    14 @@ -0,0 +1,75 @@
     14@@ -0,0 +1,82 @@
    1515+from twisted.application import service, strports
     16+from twisted.cred.portal import Portal
     17+from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
    1618+from twisted.internet import defer
    1719+
     
    7577+sessionManager.setHandlerParent(component)
    7678+
     79+checker = InMemoryUsernamePasswordDatabaseDontUse(ralphm='secret',
     80+                                                  termie='secret')
     81+portal = Portal(sessionManager, (checker,))
     82+portals = {JID(domain): portal}
     83+
    7784+xmppim.AccountIQHandler(sessionManager).setHandlerParent(component)
    7885+xmppim.AccountMessageHandler(sessionManager).setHandlerParent(component)
     
    8289+PingHandler().setHandlerParent(component)
    8390+
    84 +c2sFactory = client.XMPPC2SServerFactory(sessionManager)
     91+c2sFactory = client.XMPPC2SServerFactory(portals)
    8592+c2sFactory.logTraffic = True
    8693+c2sService = strports.service('5224', c2sFactory)
  • 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         """
  • listening-authenticator-stream-features.patch

    r65 r66  
    11# HG changeset patch
    2 # Parent 8b17590769db3088336f1fa65710c48e5ad5dcc1
     2# Parent 9393ad83138bbe6ab1c5249a6a115b8e56144622
    33Add FeatureListeningAuthenticator.
    44
     
    66for stream negotiation, similar to inializers for clients.
    77
    8 TODO:
    9 
    10  * Add docstrings.
    11  * Add support for stream restarts.
    12 
    138diff --git a/wokkel/generic.py b/wokkel/generic.py
    149--- a/wokkel/generic.py
    1510+++ b/wokkel/generic.py
     11@@ -10,13 +10,13 @@
     12 from zope.interface import implements
     13 
     14 from twisted.internet import defer, protocol
     15-from twisted.python import reflect
     16+from twisted.python import log, reflect
     17 from twisted.words.protocols.jabber import error, jid, xmlstream
     18 from twisted.words.protocols.jabber.xmlstream import toResponse
     19 from twisted.words.xish import domish, utility
     20 from twisted.words.xish.xmlstream import BootstrapMixin
     21 
     22-from wokkel.iwokkel import IDisco
     23+from wokkel.iwokkel import IDisco, IReceivingInitializer
     24 from wokkel.subprotocols import XMPPHandler
     25 
     26 IQ_GET = '/iq[@type="get"]'
    1627@@ -25,6 +25,8 @@
    1728 NS_VERSION = 'jabber:iq:version'
     
    2334     """
    2435     Parse serialized XML into a DOM structure.
    25 @@ -327,3 +329,116 @@
     36@@ -327,3 +329,287 @@
    2637 
    2738     def clientConnectionFailed(self, connector, reason):
     
    3142+
    3243+class TestableXmlStream(xmlstream.XmlStream):
     44+    """
     45+    XML Stream that buffers outgoing data and catches special events.
     46+
     47+    This implementation overrides relevant methods to prevent any data
     48+    to be sent out on a transport. Instead it buffers all outgoing stanzas,
     49+    sets flags instead of sending the stream header and footer and logs stream
     50+    errors so it can be caught by a logging observer.
     51+
     52+    @ivar output: Sequence of objects sent out using L{send}. Usually these are
     53+        L{domish.Element} instances.
     54+    @type output: C{list}
     55+
     56+    @ivar headerSent: Flag set when a stream header would have been sent. When
     57+        a stream restart occurs through L{reset}, this flag is reset as well.
     58+    @type headerSent: C{bool}
     59+
     60+    @ivar footerSent: Flag set when a stream footer would have been sent
     61+        explicitly. Note that it is not set when a stream error is sent
     62+        using L{sendStreamError}.
     63+    @type footerSent: C{bool}
     64+    """
     65+
    3366+
    3467+    def __init__(self, authenticator):
     
    3669+        self.headerSent = False
    3770+        self.footerSent = False
    38 +        self.streamError = None
    3971+        self.output = []
    4072+
     
    5486+
    5587+    def sendStreamError(self, streamError):
    56 +        self.streamError = streamError
     88+        """
     89+        Log a stream error.
     90+
     91+        If this is called from a Twisted Trial test case, the stream error
     92+        will be observed by the Trial logging observer. If it is not explicitly
     93+        tested for (i.e. flushed), this will cause the test case to
     94+        automatically fail. See L{assertStreamError} for a convenience method
     95+        to test for stream errors.
     96+
     97+        @type streamError: L{error.StreamError}
     98+        """
     99+        log.err(streamError)
     100+
     101+
     102+    @staticmethod
     103+    def assertStreamError(testcase, condition=None, exc=None):
     104+        """
     105+        Check if a stream error was sent out.
     106+
     107+        To check for stream errors sent out by L{sendStreamError}, this method
     108+        will flush logged stream errors and inspect the last one. If
     109+        C{condition} was passed, the logged error is asserted to match that
     110+        condition. If C{exc} was passed, the logged error is asserted to be
     111+        identical to it.
     112+
     113+        Note that this is takes the calling test case as the first argument, to
     114+        be able to hook into its methods for flushing errors and making
     115+        assertions.
     116+
     117+        @param testcase: The test case instance that is calling this method.
     118+        @type testcase: {twisted.trial.unittest.TestCase}
     119+
     120+        @param condition: The optional stream error condition to match against.
     121+        @type condition: C{unicode}.
     122+
     123+        @param exc: The optional stream error to check identity against.
     124+        @type exc: L{error.StreamError}
     125+        """
     126+
     127+        loggedErrors = testcase.flushLoggedErrors(error.StreamError)
     128+        testcase.assertTrue(loggedErrors, "No stream error was sent")
     129+        streamError = loggedErrors[-1].value
     130+        if condition:
     131+            testcase.assertEqual(condition, streamError.condition)
     132+        elif exc:
     133+            testcase.assertIdentical(exc, streamError)
    57134+
    58135+
    59136+    def send(self, obj):
     137+        """
     138+        Buffer all outgoing stanzas.
     139+
     140+        @type obj: L{domish.Element}
     141+        """
    60142+        self.output.append(obj)
     143+
     144+
     145+
     146+class BaseReceivingInitializer(object):
     147+    """
     148+    Base stream initializer for receiving entities.
     149+    """
     150+    implements(IReceivingInitializer)
     151+
     152+    required = False
     153+
     154+    def __init__(self, name, xs):
     155+        self.name = name
     156+        self.xmlstream = xs
     157+        self.deferred = defer.Deferred()
     158+
     159+
     160+    def getFeatures(self):
     161+        raise NotImplementedError()
     162+
     163+
     164+    def initialize(self):
     165+        return self.deferred
    61166+
    62167+
     
    91196+
    92197+
     198+    def connectionMade(self):
     199+        """
     200+        Called when the connection has been made.
     201+
     202+        Adds an observer to reject XML Stanzas until stream feature negotiation
     203+        has completed.
     204+        """
     205+        xmlstream.ListenAuthenticator.connectionMade(self)
     206+        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
     207+
     208+
     209+    def _cbInit(self, result):
     210+        """
     211+        Mark the initializer as completed and continue to the next.
     212+        """
     213+        result, index = result
     214+        self.completedInitializers.append(self._initializers[index].name)
     215+        del self._initializers[index]
     216+
     217+        if result is xmlstream.Reset:
     218+            # The initializer initiated a stream restart, bail.
     219+            return
     220+        else:
     221+            self._initializeStream()
     222+
     223+
     224+    def _ebInit(self, failure):
     225+        """
     226+        Called when an initializer raises an exception.
     227+
     228+        If the exception is a L{error.StreamError} it is sent out, otherwise
     229+        the error is logged and a stream error with condition
     230+        C{'internal-server-error'} is sent out instead.
     231+        """
     232+        firstError = failure.value
     233+        subFailure = firstError.subFailure
     234+        if subFailure.check(error.StreamError):
     235+            exc = subFailure.value
     236+        else:
     237+            log.err(subFailure, index=firstError.index)
     238+            exc = error.StreamError('internal-server-error')
     239+
     240+        self.xmlstream.sendStreamError(exc)
     241+
     242+
    93243+    def _initializeStream(self):
    94 +        def cb(result):
    95 +            result, index = result
    96 +            self.completedInitializers.append(self._initializers[index])
    97 +            del self._initializers[index]
    98 +            self._initializeStream()
    99 +
     244+        """
     245+        Initialize the stream.
     246+
     247+        This walks all initializers to retrieve their features and determine
     248+        if there is at least one required initializer. If not, the stream is
     249+        ready for the exchange of stanzas. The features are sent out and each
     250+        initializer will have its C{initialize} method called.
     251+
     252+        If negation has completed, L{xmlstream.STREAM_AUTHD_EVENT} is
     253+        dispatched and the observer rejecting incoming stanzas is removed.
     254+        """
    100255+        features = domish.Element((xmlstream.NS_STREAMS, 'features'))
    101256+        ds = []
     
    107262+            d = initializer.initialize()
    108263+            ds.append(d)
     264+
    109265+        self.xmlstream.send(features)
     266+
    110267+        if not required:
     268+            # There are no required initializers anymore. This stream is
     269+            # now ready for the exchange of stanzas.
    111270+            self.xmlstream.removeObserver(XPATH_ALL, self._onElementFallback)
    112271+            self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT)
     272+
    113273+        if ds:
    114 +            d = defer.DeferredList(ds, fireOnOneCallback=True)
    115 +            d.addCallback(cb)
     274+            d = defer.DeferredList(ds, fireOnOneCallback=True,
     275+                                       fireOnOneErrback=True,
     276+                                       consumeErrors=True)
     277+            d.addCallbacks(self._cbInit, self._ebInit)
     278+
    116279+
    117280+    def getInitializers(self):
    118 +        return []
     281+        """
     282+        Get the initializers for the current stage of stream negotiation.
     283+
     284+        This will be called at the start of each stream start to retrieve
     285+        the initializers for which the features are advertised and initiated.
     286+
     287+        @rtype: C{list} of C{IReceivingInitializer} instances.
     288+        """
     289+        raise NotImplementedError()
    119290+
    120291+
    121292+    def checkStream(self):
    122 +        # check namespace
     293+        """
     294+        Check the stream before sending out a stream header and initialization.
     295+
     296+        This is the place to inspect the stream properties and raise a relevant
     297+        L{error.StreamError} if needed.
     298+        """
     299+        # Check stream namespace
    123300+        if self.xmlstream.namespace != self.namespace:
    124301+            self.xmlstream.namespace = self.namespace
     
    127304+
    128305+    def streamStarted(self, rootElement):
     306+        """
     307+        Called when the stream header has been received.
     308+
     309+        Check the stream properties through L{checkStream}, send out
     310+        a stream header, and retrieve and initialize the stream initializers.
     311+        """
    129312+        xmlstream.ListenAuthenticator.streamStarted(self, rootElement)
    130313+
     
    135318+            return
    136319+
    137 +        self.xmlstream.addObserver(XPATH_ALL, self._onElementFallback, -1)
    138320+        self.xmlstream.sendHeader()
    139321+
    140322+        self._initializers = self.getInitializers()
    141323+        self._initializeStream()
     324diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
     325--- a/wokkel/iwokkel.py
     326+++ b/wokkel/iwokkel.py
     327@@ -11,7 +11,7 @@
     328            'IPubSubClient', 'IPubSubService', 'IPubSubResource',
     329            'IMUCClient', 'IMUCStatuses']
     330 
     331-from zope.interface import Interface
     332+from zope.interface import Attribute, Interface
     333 from twisted.python.deprecate import deprecatedModuleAttribute
     334 from twisted.python.versions import Version
     335 from twisted.words.protocols.jabber.ijabber import IXMPPHandler
     336@@ -982,3 +982,50 @@
     337         """
     338         Return the number of status conditions.
     339         """
     340+
     341+
     342+
     343+class IReceivingInitializer(Interface):
     344+    """
     345+    Interface for XMPP stream initializers for receiving entities.
     346+    """
     347+
     348+    required = Attribute(
     349+        """
     350+        This initializer is required to complete feature negotiation.
     351+        """)
     352+    name = Attribute(
     353+        """
     354+        Identifier for this initializer.
     355+
     356+        This identifier is included in
     357+        L{wokkel.generic.FeatureListenAuthenticator} when an initializer has
     358+        completed.
     359+        """)
     360+    xmlstream = Attribute(
     361+        """
     362+        The XML Stream.
     363+        """)
     364+    deferred = Attribute(
     365+        """
     366+        The deferred returned from initialize.
     367+        """)
     368+
     369+
     370+    def getFeatures():
     371+        """
     372+        Get stream features for this initializer.
     373+
     374+        @rtype: C{list} of L{twisted.words.xish.domish.Element}
     375+        """
     376+
     377+
     378+    def initialize():
     379+        """
     380+        Initialize the initializer.
     381+
     382+        This is where observers for feature negotiation are set up. When
     383+        the returned deferred fires, it is assumed to have completed.
     384+
     385+        @rtype: L{twisted.internet.defer.Deferred}
     386+        """
    142387diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    143388--- a/wokkel/test/test_generic.py
    144389+++ b/wokkel/test/test_generic.py
    145 @@ -5,9 +5,12 @@
     390@@ -5,11 +5,16 @@
    146391 Tests for L{wokkel.generic}.
    147392 """
    148393 
     394+from zope.interface import verify
     395+
    149396+from twisted.internet import defer
    150397+from twisted.test import proto_helpers
     
    154401+from twisted.words.protocols.jabber import error, xmlstream
    155402 
    156  from wokkel import generic
     403-from wokkel import generic
     404+from wokkel import generic, iwokkel
    157405 from wokkel.test.helpers import XmlStreamStub
    158 @@ -268,3 +271,218 @@
     406 
     407 NS_VERSION = 'jabber:iq:version'
     408@@ -268,3 +273,331 @@
    159409         The default is no timeout.
    160410         """
     
    163413+
    164414+
    165 +class TestableReceivingInitializer(object):
     415+class BaseReceivingInitializerTest(unittest.TestCase):
     416+    """
     417+    Tests for L{generic.BaseReceivingInitializer}.
     418+    """
     419+
     420+    def setUp(self):
     421+        self.init = generic.BaseReceivingInitializer('init', None)
     422+
     423+
     424+    def test_interface(self):
     425+        verify.verifyObject(iwokkel.IReceivingInitializer, self.init)
     426+
     427+
     428+    def test_getFeatures(self):
     429+        self.assertRaises(NotImplementedError, self.init.getFeatures)
     430+
     431+
     432+    def test_initialize(self):
     433+        d = self.init.initialize()
     434+        self.init.deferred.callback(None)
     435+        return d
     436+
     437+
     438+
     439+class TestableReceivingInitializer(generic.BaseReceivingInitializer):
    166440+    """
    167441+    Testable initializer for receiving entities.
     
    172446+
    173447+    @ivar uri: Namespace of the stream feature.
    174 +    @ivar name: Element localname for the stream feature.
     448+    @ivar localname: Element localname for the stream feature.
    175449+    """
    176450+    required = True
    177451+
    178 +    def __init__(self, xs, uri, name):
    179 +        self.xmlstream = xs
     452+    def __init__(self, name, xs, uri, localname):
     453+        generic.BaseReceivingInitializer.__init__(self, name, xs)
    180454+        self.uri = uri
    181 +        self.name = name
     455+        self.localname = localname
    182456+        self.deferred = defer.Deferred()
    183457+
    184458+
    185459+    def getFeatures(self):
    186 +        return [domish.Element((self.uri, self.name))]
    187 +
    188 +
    189 +    def initialize(self):
    190 +        return self.deferred
     460+        return [domish.Element((self.uri, self.localname))]
     461+
     462+
    191463+
    192464+class FeatureListenAuthenticatorTest(unittest.TestCase):
     
    206478+                                   self.onInitFailed)
    207479+
    208 +        self.init = TestableReceivingInitializer(self.xmlstream, 'testns', 'test')
     480+        self.init = TestableReceivingInitializer('init', self.xmlstream,
     481+                                                 'testns', 'test')
    209482+
    210483+        def getInitializers():
     
    220493+    def onInitFailed(self, failure):
    221494+        self.initFailure = failure
     495+
     496+
     497+    def test_getInitializers(self):
     498+        """
     499+        Unoverridden getInitializers raises NotImplementedError.
     500+        """
     501+        authenticator = generic.FeatureListenAuthenticator()
     502+        self.assertRaises(
     503+            NotImplementedError,
     504+            authenticator.getInitializers)
    222505+
    223506+
     
    231514+        themselves up.
    232515+        """
    233 +
    234516+        xs = self.xmlstream
    235517+        xs.makeConnection(proto_helpers.StringTransport())
     
    255537+
    256538+
     539+    def test_streamStartedStreamError(self):
     540+        """
     541+        A stream error raised by the initializer is sent out.
     542+        """
     543+        xs = self.xmlstream
     544+        xs.makeConnection(proto_helpers.StringTransport())
     545+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     546+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     547+                         "from='example.com' to='example.org' id='12345' "
     548+                         "version='1.0'>")
     549+
     550+        self.assertTrue(xs.headerSent)
     551+
     552+        xs.output = []
     553+        exc = error.StreamError('policy-violation')
     554+        self.init.deferred.errback(exc)
     555+
     556+        self.xmlstream.assertStreamError(self, exc=exc)
     557+        self.assertFalse(xs.output)
     558+        self.assertFalse(self.gotAuthenticated)
     559+
     560+
     561+    def test_streamStartedOtherError(self):
     562+        """
     563+        Initializer exceptions are logged and yield a internal-server-error.
     564+        """
     565+        xs = self.xmlstream
     566+        xs.makeConnection(proto_helpers.StringTransport())
     567+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     568+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     569+                         "from='example.com' to='example.org' id='12345' "
     570+                         "version='1.0'>")
     571+
     572+        self.assertTrue(xs.headerSent)
     573+
     574+        xs.output = []
     575+        class Error(Exception):
     576+            pass
     577+        self.init.deferred.errback(Error())
     578+
     579+        self.xmlstream.assertStreamError(self, condition='internal-server-error')
     580+        self.assertFalse(xs.output)
     581+        self.assertFalse(self.gotAuthenticated)
     582+        self.assertEqual(1, len(self.flushLoggedErrors(Error)))
     583+
     584+
    257585+    def test_streamStartedInitializerCompleted(self):
    258586+        """
     
    266594+                         "version='1.0'>")
    267595+
     596+        xs.output = []
    268597+        self.init.deferred.callback(None)
    269 +        self.assertEqual([self.init], self.authenticator.completedInitializers)
     598+        self.assertEqual(['init'], self.authenticator.completedInitializers)
     599+
     600+
     601+    def test_streamStartedInitializerCompletedFeatures(self):
     602+        """
     603+        After completing an initializer, stream features are sent again.
     604+
     605+        In this case, with only one initializer, there are no more features.
     606+        """
     607+        xs = self.xmlstream
     608+        xs.makeConnection(proto_helpers.StringTransport())
     609+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     610+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     611+                         "from='example.com' to='example.org' id='12345' "
     612+                         "version='1.0'>")
     613+
     614+        xs.output = []
     615+        self.init.deferred.callback(None)
     616+
     617+        self.assertEqual(1, len(xs.output))
     618+        features = xs.output[-1]
     619+        self.assertEqual('features', features.name)
     620+        self.assertEqual(xmlstream.NS_STREAMS, features.uri)
     621+        self.assertFalse(features.children)
     622+
     623+
     624+    def test_streamStartedInitializerCompletedReset(self):
     625+        """
     626+        If an initializer completes with Reset, no features are sent.
     627+        """
     628+        xs = self.xmlstream
     629+        xs.makeConnection(proto_helpers.StringTransport())
     630+        xs.dataReceived("<stream:stream xmlns='jabber:server' "
     631+                         "xmlns:stream='http://etherx.jabber.org/streams' "
     632+                         "from='example.com' to='example.org' id='12345' "
     633+                         "version='1.0'>")
     634+
     635+        xs.output = []
     636+        self.init.deferred.callback(xmlstream.Reset)
     637+
     638+        self.assertEqual(0, len(xs.output))
    270639+
    271640+
     
    285654+                        "</iq>")
    286655+
    287 +        self.assertEqual('not-authorized', xs.streamError.condition)
     656+        self.xmlstream.assertStreamError(self, condition='not-authorized')
    288657+
    289658+
     
    304673+                        "  <query xmlns='jabber:iq:version'/>"
    305674+                        "</iq>")
    306 +
    307 +        self.assertIdentical(None, xs.streamError)
    308675+
    309676+
     
    325692+        xs.dispatch(iq)
    326693+
    327 +        self.assertIdentical(None, xs.streamError)
    328 +
    329694+
    330695+    def test_streamStartedNonXmlStanzasIgnored(self):
     
    340705+
    341706+        xs.dataReceived("<test xmlns='myns'/>")
    342 +
    343 +        self.assertIdentical(None, xs.streamError)
    344707+
    345708+
     
    359722+                         "version='1.0'>")
    360723+
    361 +        self.assertEqual('undefined-condition', xs.streamError.condition)
     724+        self.xmlstream.assertStreamError(self, condition='undefined-condition')
    362725+        self.assertFalse(xs.headerSent)
    363726+
     
    374737+                         "version='1.0'>")
    375738+
    376 +        self.assertEqual('invalid-namespace', xs.streamError.condition)
     739+        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
  • series

    r65 r66  
    11roster_server.patch #+c2s
    22router_unknown.patch #+c2s
     3listening-authenticator-stream-features.patch #+c2s
    34
    4 listening-authenticator-stream-features.patch #+c2s
    55client_listen_authenticator.patch #+c2s
    66
    7 c2s_server_factory.patch #+c2s-broken
    8 session_manager.patch #+c2s-broken
    9 c2s_stanza_handlers.patch #+c2s-broken
     7c2s_server_factory.patch #+c2s
     8session_manager.patch #+c2s
     9c2s_stanza_handlers.patch #+c2s
    1010
    1111version.patch
  • session_manager.patch

    r57 r66  
    11# HG changeset patch
    2 # Parent 4e25d6deb8beeb732cadb38349ee820f0dc98b3a
     2# Parent 4962500d52ed0bd4a71047c93818303accea55fd
    33
    4 diff -r 4e25d6deb8be wokkel/client.py
    5 --- a/wokkel/client.py  Wed Nov 30 09:32:01 2011 +0100
    6 +++ b/wokkel/client.py  Wed Nov 30 09:33:11 2011 +0100
    7 @@ -13,15 +13,16 @@
     4diff --git a/wokkel/client.py b/wokkel/client.py
     5--- a/wokkel/client.py
     6+++ b/wokkel/client.py
     7@@ -12,18 +12,21 @@
     8 
    89 import base64
    910 
     11+from zope.interface import implements
     12+
    1013 from twisted.application import service
    11 -from twisted.internet import reactor
     14-from twisted.cred import credentials, error as ecred
     15+from twisted.cred import credentials, error as ecred, portal
     16 from twisted.internet import defer, reactor
    1217-from twisted.python import log
    13 +from twisted.internet import defer, reactor
    1418+from twisted.python import log, randbytes
    1519 from twisted.names.srvconnect import SRVConnector
     
    2125 from wokkel import generic
    2226 from wokkel.compat import XmlStreamServerFactory
     27 from wokkel.iwokkel import IUserSession
    2328-from wokkel.subprotocols import StreamManager
    2429+from wokkel.subprotocols import StreamManager, XMPPHandler
     
    2631 NS_CLIENT = 'jabber:client'
    2732 
    28 @@ -443,3 +444,105 @@
    29          else:
    30              raise RecipientUnavailable(u"There is no connection for %s" %
    31                                         recipient.full())
    32 +
    33 +
    34 +class Session(object):
     33@@ -501,3 +504,161 @@
     34 
     35     def onError(self, reason):
     36         log.err(reason, "Stream Error")
     37+
     38+
     39+
     40+class UserSession(object):
     41+
     42+    implements(IUserSession)
     43+
     44+    realm = None
     45+    mind = None
     46+
     47+    connected = False
     48+    interested = False
     49+    presence = None
     50+
     51+    clientStream = None
     52+
    3553+    def __init__(self, entity):
    3654+        self.entity = entity
     55+
     56+
     57+    def loggedIn(self, realm, mind):
     58+        self.realm = realm
     59+        self.mind = mind
     60+
     61+
     62+    def bindResource(self, resource):
     63+        def cb(entity):
     64+            self.entity = entity
     65+            self.connected = True
     66+            return entity
     67+
     68+        d = self.realm.bindResource(self, resource)
     69+        d.addCallback(cb)
     70+        return d
     71+
     72+
     73+    def logout(self):
    3774+        self.connected = False
    38 +        self.interested = False
    39 +        self.presence = None
     75+        self.realm.unbindResource(self)
     76+
     77+
     78+    def send(self, element):
     79+        self.realm.onElement(element, self)
     80+
     81+
     82+    def receive(self, element):
     83+        self.mind.send(element)
    4084+
    4185+
    4286+
    4387+class SessionManager(XMPPHandler):
    44 +
    4588+    """
    4689+    Session Manager.
     
    5396+    """
    5497+
     98+    implements(portal.IRealm)
     99+
    55100+    def __init__(self, domain, accounts):
    56101+        XMPPHandler.__init__(self)
     
    58103+        self.accounts = accounts
    59104+
    60 +        self.connectionManager = None
    61105+        self.sessions = {}
    62106+        self.clientStream = utility.EventDispatcher()
     
    64108+
    65109+
    66 +    def bindResource(self, localpart, domain, resource):
    67 +        if domain != self.domain:
    68 +            raise Exception("I don't host this domain!")
     110+    def requestAvatar(self, avatarId, mind, *interfaces):
     111+        if IUserSession not in interfaces:
     112+            raise NotImplementedError(self, interfaces)
     113+
     114+        localpart = avatarId.decode('utf-8')
     115+        entity = JID(tuple=(localpart, self.domain, None))
     116+        session = UserSession(entity)
     117+        session.loggedIn(self, mind)
     118+        return IUserSession, session, session.logout
     119+
     120+
     121+    def bindResource(self, session, resource):
     122+        localpart = session.entity.user
    69123+
    70124+        try:
     
    78132+            resource = resource + ' ' + randbytes.secureRandom(8).encode('hex')
    79133+
    80 +        entity = JID(tuple=(localpart, domain, resource))
    81 +        session = Session(entity)
    82 +        session.connected = True
     134+        entity = JID(tuple=(session.entity.user, session.entity.host, resource))
    83135+        userSessions[resource] = session
    84136+
     
    86138+
    87139+
    88 +    def unbindResource(self, localpart, domain, resource, reason=None):
    89 +        try:
    90 +            session = self.sessions[localpart][resource]
    91 +        except KeyError:
    92 +            pass
    93 +        else:
    94 +            session.connected = False
    95 +            del self.sessions[localpart][resource]
    96 +            if not self.sessions[localpart]:
    97 +                del self.sessions[localpart]
     140+    def lookupSession(self, entity):
     141+        localpart = entity.user
     142+        resource = entity.resource
     143+
     144+        userSessions = self.sessions[localpart]
     145+        session = userSessions[resource]
     146+        return session
     147+
     148+
     149+    def unbindResource(self, session, reason=None):
     150+        session.connected = False
     151+
     152+        localpart = session.entity.user
     153+        resource = session.entity.resource
     154+
     155+        del self.sessions[localpart][resource]
     156+        if not self.sessions[localpart]:
     157+            del self.sessions[localpart]
    98158+
    99159+        return defer.succeed(None)
    100160+
    101161+
    102 +    def onElement(self, element, sender):
     162+    def onElement(self, element, session):
    103163+        # Make sure each stanza has a sender address
    104164+        if (element.name == 'presence' and
    105165+            element.getAttribute('type') in ('subscribe', 'subscribed',
    106166+                                             'unsubscribe', 'unsubscribed')):
    107 +            element['from'] = sender.userhost()
     167+            element['from'] = session.entity.userhost()
    108168+        else:
    109 +            element['from'] = sender.full()
     169+            element['from'] = session.entity.full()
    110170+
    111171+        self.clientStream.dispatch(element)
     
    113173+
    114174+    def routeOrDeliver(self, element):
     175+        """
     176+        Deliver a stanza locally or pass on for routing.
     177+        """
    115178+        if element.handled:
    116179+            return
     
    124187+            # This stanza is for remote routing
    125188+            log.msg("Routing remotely: %r" % element.toXml())
    126 +            self.xmlstream.send(element)
     189+            XMPPHandler.send(self, element)
    127190+
    128191+
    129192+    def deliverStanza(self, element, recipient):
    130 +        if self.connectionManager:
    131 +            self.connectionManager.deliverStanza(element, recipient)
    132 +        else:
    133 +            raise Exception("No connection manager set")
     193+        session = self.lookupSession(recipient)
     194+        session.receive(element)
     195diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
     196--- a/wokkel/test/test_client.py
     197+++ b/wokkel/test/test_client.py
     198@@ -7,7 +7,7 @@
     199 
     200 from base64 import b64encode
     201 
     202-from zope.interface import implements
     203+from zope.interface import implements, verify
     204 
     205 from twisted.cred.portal import IRealm, Portal
     206 from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
     207@@ -601,6 +601,7 @@
     208 
     209 
     210 
     211+
     212 class XMPPClientListenAuthenticatorTest(unittest.TestCase):
     213     """
     214     Tests for L{client.XMPPClientListenAuthenticator}.
     215@@ -670,3 +671,26 @@
     216                          "to='example.com' "
     217                          "version='1.0'>")
     218         self.xmlstream.assertStreamError(self, condition='host-unknown')
     219+
     220+
     221+
     222+class UserSessionTest(unittest.TestCase):
     223+
     224+    def setUp(self):
     225+        self.session = client.UserSession(JID('user@example.org'))
     226+
     227+
     228+    def test_interface(self):
     229+        verify.verifyObject(client.IUserSession, self.session)
     230+
     231+
     232+
     233+class SessionManagerTest(unittest.TestCase):
     234+
     235+    def setUp(self):
     236+        accounts = {'user': None}
     237+        self.sessionManager = client.SessionManager('example.org', accounts)
     238+
     239+
     240+    def test_interface(self):
     241+        verify.verifyObject(IRealm, self.sessionManager)
Note: See TracChangeset for help on using the changeset viewer.